Files
hustoj/web/include/llm_guidance.inc.php

658 lines
28 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Shared, side-effect-free policy helpers for submission guidance.
*
* Keep this file independent from the database so the policy can be tested by
* the PHP CLI without loading the HUSTOJ runtime.
*/
if (!function_exists('llm_guidance_is_terminal_error')) {
function llm_guidance_is_terminal_error($result_code) {
$result_code = intval($result_code);
return $result_code >= 5 && $result_code <= 11;
}
}
if (!function_exists('llm_guidance_source_metrics')) {
function llm_guidance_source_metrics($source) {
$source = str_replace(array("\r\n", "\r"), "\n", strval($source));
$clean = preg_replace('!/\*.*?\*/!s', ' ', $source);
$clean = preg_replace('/\/\/[^\n]*/', ' ', $clean);
if ($clean === null) $clean = $source;
$lines = explode("\n", $clean);
$meaningful_lines = 0;
$activity_lines = 0;
$metric_lines = array();
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') continue;
// Preprocessor directives and # comments are not student logic.
// This also prevents Python/Ruby/shell comment-only submissions
// from inflating completion metrics.
if (strpos($line, '#') === 0) continue;
if (preg_match('/^using\s+namespace\b.*;$/i', $line)) continue;
if (preg_match('/^(?:package|import)\s+[\w.*]+;?$/i', $line)) continue;
if (preg_match('/^[{};]+$/', $line)) continue;
$meaningful_lines++;
$metric_lines[] = $line;
$activity = preg_replace('/\breturn\s+(?:0|null|none)\s*;?$/i', '', $line);
$activity = preg_replace('/\b(?:int|void)\s+main\s*\([^)]*\)\s*\{?$/i', '', $activity);
$activity = trim($activity);
if ($activity !== '' && !preg_match('/^(?:public|private|protected)?\s*(?:class|struct)\s+\w+\s*\{?$/i', $activity)) {
$activity_lines++;
}
}
$metric_source = implode("\n", $metric_lines);
$non_space = preg_replace('/\s+/', '', $metric_source);
$non_space_chars = strlen($non_space === null ? '' : $non_space);
$analysis_source = preg_replace('/"(?:\\\\.|[^"\\\\])*"/s', '""', $metric_source);
$analysis_source = preg_replace("/'(?:\\\\.|[^'\\\\])*'/s", "''", $analysis_source);
if ($analysis_source === null) $analysis_source = $metric_source;
$signal_count = preg_match_all(
'/\b(?:if|else|for|while|do|switch|case|return|break|continue|cin|cout|scanf|printf|input|print|read|readln|readline|read_line|write|writeln|writeline|readFileSync)\b|(?<![=!<>])=(?!=)|:=|\+\+|--/i',
$analysis_source,
$unused
);
if ($signal_count === false) $signal_count = 0;
// Completion is based on observable program stages, not source length.
// This keeps legitimate one-line Python/C++ programs out of the
// "blank submission" bucket while still catching templates and
// constant-answer fishing.
$has_input = preg_match(
'/\b(?:cin|scanf|fscanf|getchar|gets|fgets|input|read(?:ln|_?line)?|readfilesync|prompt|next(?:int|long|double|float|line)?|scanner|stdin|fmt\s*\.\s*scan\w*)\b/i',
$analysis_source
) === 1;
$has_output = preg_match(
'/\b(?:cout|cerr|printf|fprintf|puts|putchar|print|println|write(?:ln|line)?|console\s*\.\s*log|system\s*\.\s*out|fmt\s*\.\s*print\w*|echo)\b/i',
$analysis_source
) === 1;
$has_processing = preg_match(
'/\b(?:if|else|for|while|do|switch|case|sort|sum|max|min|count|reverse|reduce|filter|map)\b|'
. '(?:\+\+|--|:=|==|!=|<=|>=|&&|\|\|)|'
. '(?:[\w\]\)][ \t]*(?:\+|-|\*|\/|%)[ \t]*[\w\[\(])|'
. '(?:(?<!<)<(?![<=])|(?<!>)>(?![>=])|(?<!\|)\|(?!\|)|[&^~]|\?[^:\r\n]+:)|'
. '(?:\[[^\]\r\n]*:[^\]\r\n]*\])/i',
$analysis_source
) === 1;
$stage_count = intval($has_input) + intval($has_processing) + intval($has_output);
$constant_output_only = $has_output && !$has_input && !$has_processing;
$template_only = !$has_input
&& !$has_output
&& !$has_processing
&& ($non_space_chars < 120 || $signal_count <= 1);
$trivial = $non_space_chars < 5 || $constant_output_only || $template_only;
$source_score = 0.12
+ 0.22 * intval($has_input)
+ 0.30 * intval($has_processing)
+ 0.22 * intval($has_output)
+ min(0.14, $activity_lines * 0.02);
if ($trivial) $source_score = min($source_score, 0.20);
return array(
'non_space_chars' => $non_space_chars,
'meaningful_lines' => $meaningful_lines,
'activity_lines' => $activity_lines,
'signal_count' => $signal_count,
'has_input' => $has_input,
'has_processing' => $has_processing,
'has_output' => $has_output,
'stage_count' => $stage_count,
'constant_output_only' => $constant_output_only,
'template_only' => $template_only,
'source_score' => max(0, min(1, $source_score)),
'trivial' => $trivial,
);
}
}
if (!function_exists('llm_guidance_assess')) {
/**
* Return the guidance mode without consulting a reference solution.
* pass_rate accepts either 0..1 (database form) or 0..100.
*/
function llm_guidance_assess($source, $result_code, $pass_rate) {
$result_code = intval($result_code);
$metrics = llm_guidance_source_metrics($source);
$pass_fraction = floatval($pass_rate);
if ($pass_fraction > 1) $pass_fraction /= 100;
$pass_fraction = max(0, min(1, $pass_fraction));
if (!llm_guidance_is_terminal_error($result_code)) {
return array_merge($metrics, array(
'score' => 0,
'level' => 'none',
'mode' => 'hidden',
'judge_pass_rate' => $pass_fraction,
'auto_fetch' => false,
'show_output_diff' => false,
));
}
// CE locations come only from the compiler and the student's own
// source. They are useful and non-spoiling even for very short code.
if ($result_code === 11) {
$level = $metrics['trivial'] ? 'low' : 'medium';
$combined = $metrics['trivial'] ? 0.20 : max(0.45, $metrics['source_score']);
$mode = 'compile_location';
} elseif ($result_code === 5 && $metrics['has_output']) {
// PE is judge-proven evidence that meaningful output exists. Even
// a compact program should go straight to the format comparison.
$level = 'high';
$combined = 0.72;
$mode = 'output_diff';
} elseif ($metrics['trivial']) {
$level = 'low';
$combined = min(0.20, $metrics['source_score']);
$mode = 'flowchart';
} else {
// The judge stops at the first failed testcase, so pass_rate=0 is
// not negative evidence. A complete input/process/output pipeline
// is enough to treat the submission as a real attempt.
$combined = max($metrics['source_score'], min(0.85, 0.45 + 0.40 * $pass_fraction));
$complete_pipeline = intval($metrics['stage_count']) === 3;
$positive_judge_evidence = $pass_fraction > 0
&& (intval($metrics['stage_count']) >= 2 || $pass_fraction >= 0.20);
if (($result_code === 5 || $result_code === 6)
&& ($complete_pipeline || $positive_judge_evidence)) {
$level = 'high';
$combined = max($combined, 0.72);
} else {
$level = 'medium';
}
if ($level === 'medium') $combined = min($combined, 0.69);
if ($level === 'high') $combined = max($combined, 0.70);
if ($level === 'low') {
$mode = 'flowchart';
} elseif ($level === 'high' && ($result_code === 5 || $result_code === 6)) {
$mode = 'output_diff';
} else {
$mode = 'focused_hint';
}
}
return array_merge($metrics, array(
'score' => intval(round(max(0, min(1, $combined)) * 100)),
'level' => $level,
'mode' => $mode,
'judge_pass_rate' => $pass_fraction,
'auto_fetch' => true,
'show_output_diff' => $mode === 'output_diff',
));
}
}
if (!function_exists('llm_guidance_mode_label')) {
function llm_guidance_mode_label($mode) {
$labels = array(
'flowchart' => '流程图引导',
'focused_hint' => '聚焦排错',
'output_diff' => '输出差异定位',
'judge_output' => '评测输出定位',
'compile_location' => '编译错误定位',
);
return isset($labels[$mode]) ? $labels[$mode] : 'AI 指导';
}
}
if (!function_exists('llm_guidance_parse_diff_line')) {
/** Parse one judge-generated Expected/Yours row. */
function llm_guidance_parse_diff_line($line) {
$line = rtrim(strval($line), "\r\n");
if ($line === '') return null;
if (isset($line[0]) && $line[0] === '|') {
$rest = substr($line, 1);
// judge_client normalizes pipes in the student's output but not in
// the expected output, so the final pipe is the field delimiter.
$separator = strrpos($rest, '|');
if ($separator !== false) {
return array(
substr($rest, 0, $separator),
substr($rest, $separator + 1),
);
}
}
// diff -y format: greedily select the last spaced pipe delimiter.
if (preg_match('/^(.*)\s+\|\s*(.*)$/', $line, $match)) {
return array(trim($match[1]), trim($match[2]));
}
// Legacy diff -y output can use a whitespace-surrounded backslash as
// the changed-line delimiter.
if (preg_match('/^(.*)\s+\\\\\s+(.*)$/', $line, $match)) {
return array(trim($match[1]), trim($match[2]));
}
if (preg_match('/^(.*?)\s+([<>])\s+(.*)$/', $line, $match)) {
return array($match[1], $match[3]);
}
return null;
}
}
if (!function_exists('llm_guidance_parse_diff_rows')) {
/** Parse the rows after a judge Expected/Yours header. */
function llm_guidance_parse_diff_rows($body) {
$body = str_replace(array("\r\n", "\r"), "\n", strval($body));
$lines = explode("\n", $body);
$expected = array();
$yours = array();
$found_header = false;
foreach ($lines as $line) {
if (!$found_header && preg_match('/^\|?\s*Expected\s*\|\s*Yours\s*\|?\s*$/i', trim($line))) {
$found_header = true;
continue;
}
// The Markdown separator is metadata even after the header flag
// has been set; never display it as a fake first diff row.
if (preg_match('/^\s*\|?\s*-+\s*\|\s*-+\s*\|?\s*$/', $line)) continue;
if (!$found_header || $line === '') continue;
$parsed = llm_guidance_parse_diff_line($line);
if ($parsed === null) continue;
$expected[] = $parsed[0];
$yours[] = $parsed[1];
}
return array('expected' => $expected, 'yours' => $yours);
}
}
if (!function_exists('llm_guidance_parse_diff_blocks')) {
/**
* Parse both current simple Markdown blocks and legacy =====[file]=====
* blocks. Full diff mode is rejected before any content is interpreted.
*/
function llm_guidance_parse_diff_blocks($text) {
$text = str_replace(array("\r\n", "\r"), "\n", strval($text));
$blocks = array();
$seen = array();
if (trim($text) === '' || llm_guidance_has_full_diff_sections($text)) return $blocks;
$append_block = function($name, $body) use (&$blocks, &$seen) {
$name = trim(strval($name));
if (llm_guidance_testcase_base($name) === null) return;
$rows = llm_guidance_parse_diff_rows($body);
if (empty($rows['expected'])) return;
$key = hash('sha256', $name . "\0" . serialize($rows));
if (isset($seen[$key])) return;
$seen[$key] = true;
$blocks[] = array(
'name' => $name,
'expected' => $rows['expected'],
'yours' => $rows['yours'],
'full_mode' => false,
);
};
// A blank line before the closing ===== line is valid legacy output,
// so do not split these blocks on blank lines first.
if (preg_match_all(
'/^=+\[([^\]\r\n]+)\]=+[ \t]*\n(.*?)^[ \t]*=+[ \t]*$/ms',
$text,
$legacy_blocks,
PREG_SET_ORDER
)) {
foreach ($legacy_blocks as $legacy_block) {
$append_block($legacy_block[1], trim($legacy_block[2], "\n\r"));
}
}
// Current simple mode separates testcase blocks with a blank line.
$chunks = preg_split('/\n\s*\n/', $text);
foreach ($chunks as $chunk) {
$chunk = trim($chunk, "\r\n");
if ($chunk === '') continue;
if (preg_match('/^([^\n=|]+)\n--\n([\s\S]+)$/', $chunk, $simple_block)) {
$append_block($simple_block[1], $simple_block[2]);
}
}
return $blocks;
}
}
if (!function_exists('llm_guidance_output_attempt')) {
/**
* Inspect judge-generated Expected/Yours rows without returning either
* value to callers. This catches padded/template submissions that produced
* no meaningful output even when their source text is long.
*/
function llm_guidance_output_attempt($runtime_info) {
$lines = preg_split('/\r\n|\r|\n/', strval($runtime_info));
$after_header = false;
$pairs = 0;
$expected_chars = 0;
$yours_chars = 0;
$expected_nonempty_lines = 0;
$yours_nonempty_lines = 0;
foreach ($lines as $line) {
$line = rtrim($line, "\r\n");
if (preg_match('/Expected.*\|.*Yours/i', $line)) {
$after_header = true;
continue;
}
if (!$after_header || trim($line) === '') continue;
if (preg_match('/^\s*=+\s*$/', $line)) {
$after_header = false;
continue;
}
if (preg_match('/^\s*\|?\s*-+\s*\|\s*-+\s*\|?\s*$/', $line)) continue;
$parsed = llm_guidance_parse_diff_line($line);
if ($parsed === null) continue;
$expected = $parsed[0];
$yours = $parsed[1];
$expected_compact = preg_replace('/\s+/', '', $expected);
$yours_compact = preg_replace('/\s+/', '', $yours);
$expected_length = strlen($expected_compact === null ? '' : $expected_compact);
$yours_length = strlen($yours_compact === null ? '' : $yours_compact);
$pairs++;
$expected_chars += $expected_length;
$yours_chars += $yours_length;
if ($expected_length > 0) $expected_nonempty_lines++;
if ($yours_length > 0) $yours_nonempty_lines++;
}
$ratio = $expected_chars > 0
? min(1, $yours_chars / $expected_chars)
: ($yours_chars > 0 ? 1 : 0);
return array(
'available' => $pairs > 0,
'pairs' => $pairs,
'expected_chars' => $expected_chars,
'yours_chars' => $yours_chars,
'expected_nonempty_lines' => $expected_nonempty_lines,
'yours_nonempty_lines' => $yours_nonempty_lines,
'ratio' => $ratio,
);
}
}
if (!function_exists('llm_guidance_testcase_base')) {
/**
* Normalize a judge testcase filename/request to its safe basename.
* Only conservative ASCII names are accepted because the value is later
* used to open files below OJ_DATA.
*/
function llm_guidance_testcase_base($candidate) {
$candidate = trim(strval($candidate));
if (preg_match('/\.out$/i', $candidate)) {
$candidate = substr($candidate, 0, -4);
}
if ($candidate === ''
|| strpos($candidate, '..') !== false
|| !preg_match('/^[A-Za-z0-9][A-Za-z0-9_.-]*$/', $candidate)) {
return null;
}
return $candidate;
}
}
if (!function_exists('llm_guidance_diff_testcase_names')) {
/**
* Return only testcase basenames that the judge recorded for this SID.
* Expected/user output values are intentionally not returned.
*/
function llm_guidance_diff_testcase_names($runtime_info) {
$text = str_replace(array("\r\n", "\r"), "\n", strval($runtime_info));
$lines = explode("\n", $text);
$names = array();
$seen = array();
$count = count($lines);
for ($i = 0; $i < $count; $i++) {
$line = rtrim($lines[$i]);
$candidate = null;
// OJ_FULL_DIFF header. The caller separately decides whether full
// mode is safe to expose; recognizing its name is still useful for
// diagnostics and tests.
if (preg_match('/^=+\[([^\]\r\n]+\.out)\]=+\s*$/i', $line, $match)) {
$candidate = $match[1];
} elseif ($i + 2 < $count
&& trim($lines[$i + 1]) === '--'
&& preg_match('/^\|?\s*Expected\s*\|\s*Yours\s*\|?\s*$/i', trim($lines[$i + 2]))) {
// Simple diff blocks begin at the start of the runtime text or
// after the blank separator written by judge_client.
$at_boundary = $i === 0 || trim($lines[$i - 1]) === '';
if ($at_boundary && preg_match('/\.out\s*$/i', $line)) {
$candidate = trim($line);
}
}
if ($candidate === null || !preg_match('/\.out$/i', $candidate)) continue;
$base = llm_guidance_testcase_base($candidate);
if ($base === null || isset($seen[$base])) continue;
$seen[$base] = true;
$names[] = $base;
}
return $names;
}
}
if (!function_exists('llm_guidance_has_full_diff_sections')) {
/** Full mode contains hidden test input and must not reach normal users. */
function llm_guidance_has_full_diff_sections($runtime_info) {
$text = strval($runtime_info);
// runtimeinfo is capped, so later sections may be truncated. One
// distinctive full-mode marker is enough to fail closed. Accept both
// a real newline and shells that preserve the literal "\n" prefix.
return preg_match(
'/(?:^|[\r\n]|\\\\n)\s*-{3,}\s*(?:(?:test in|test out|user out) top|diff out) \d+ lines\s*-{3,}/i',
$text
) === 1;
}
}
if (!function_exists('llm_guidance_redact_full_diff')) {
/**
* Fail closed for full mode. Its separator-looking lines can also occur
* inside testcase content, so parsing the same untrusted blob cannot safely
* recover only the student's section.
*/
function llm_guidance_redact_full_diff($runtime_info) {
return '评测器生成了完整差异记录,其中包含隐藏测试输入和期望输出,'
. '已按当前账号权限省略。管理员或源码浏览权限账号可查看原文。';
}
}
if (!function_exists('llm_guidance_refine_with_output_attempt')) {
function llm_guidance_refine_with_output_attempt($assessment, $result_code, $attempt) {
$result_code = intval($result_code);
if ($result_code !== 5 && $result_code !== 6) {
return $assessment;
}
if (empty($attempt['available'])) {
if ($assessment['mode'] === 'output_diff') {
$assessment['mode'] = 'judge_output';
$assessment['show_output_diff'] = false;
}
return $assessment;
}
// A single failing testcase can legitimately produce no output (for
// example, an early return or runtime fault). It is evidence about that
// testcase, not proof that the whole submission is a blank answer.
return $assessment;
}
}
if (!function_exists('llm_guidance_render_focus_payload')) {
/**
* Convert an allowlisted model classification into fixed server wording.
* No free-form model prose is ever returned to the browser.
*/
function llm_guidance_render_focus_payload($payload, $source) {
$data = json_decode(trim(strval($payload)), true);
if (!is_array($data)
|| !isset($data['category'])
|| !isset($data['confidence'])) {
return null;
}
$templates = array(
'input' => array('输入读取', '核对读取顺序、数量和数据类型,再用样例逐项对应。'),
'output' => array('输出生成', '检查是否多输出文字、少输出内容,或格式与题目要求不一致。'),
'condition' => array('条件判断', '用一组边界输入分别判断条件为真和为假时会走到哪里。'),
'loop' => array('循环控制', '只检查循环的起点、终点和每轮更新,手算前两轮与最后一轮。'),
'boundary' => array('边界处理', '分别代入最小值、最大值和临界值,观察下标与条件是否越界。'),
'initialization' => array('初值与重置', '检查变量第一次使用前的值,以及多轮处理之间是否需要重新初始化。'),
'type' => array('数据类型', '估算变量可能达到的范围,并检查运算中的类型转换。'),
'runtime' => array('运行时位置', '用最小输入复现,查看数组下标、除数和对象状态。'),
'complexity' => array('执行次数', '按最大数据范围估算最内层语句会执行多少次。'),
'memory' => array('内存使用', '核对数组或容器的规模、作用域和重复分配。'),
'state' => array('变量状态', '逐步记录关键变量,找到第一次与手算结果不同的位置。'),
'unknown' => array('最小复现', '选一组最小输入逐步手算,定位第一次偏离预期的位置。'),
);
$category = strval($data['category']);
if (!isset($templates[$category])) return null;
$confidence = strval($data['confidence']);
if (!in_array($confidence, array('low', 'medium', 'high'), true)) return null;
$line = isset($data['line']) ? intval($data['line']) : 0;
$source_lines = preg_split('/\r\n|\r|\n/', strval($source));
if ($line < 1 || $line > count($source_lines)) $line = 0;
if ($confidence !== 'high') $line = 0;
$where = $line > 0
? '第 ' . $line . ' 行附近:' . $templates[$category][0]
: $templates[$category][0];
return "### 建议检查\n" . $where . '' . $templates[$category][1];
}
}
if (!function_exists('llm_guidance_limit_text')) {
function llm_guidance_limit_text($text, $max_length) {
$text = trim(strval($text));
$max_length = max(1, intval($max_length));
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
return mb_strlen($text, 'UTF-8') > $max_length
? mb_substr($text, 0, $max_length, 'UTF-8') . '…'
: $text;
}
return strlen($text) > $max_length
? substr($text, 0, $max_length) . '...'
: $text;
}
}
if (!function_exists('llm_guidance_extract_compile_locations')) {
/**
* Parse common GCC/Clang/Javac/MSVC/Python locations and return excerpts
* from the student's own source only. No corrected/reference code exists
* in this data structure.
*/
function llm_guidance_extract_compile_locations($error_text, $source, $context_lines, $max_locations) {
$context_lines = max(0, intval($context_lines));
$max_locations = max(1, intval($max_locations));
$source = str_replace(array("\r\n", "\r"), "\n", strval($source));
$source_lines = explode("\n", $source);
$error_lines = preg_split('/\r\n|\r|\n/', strval($error_text));
$locations = array();
$seen = array();
$pending_message = '编译错误';
foreach ($error_lines as $error_line) {
$line_number = 0;
$column_number = 0;
$message = '';
if (preg_match('/^\s*(?:fatal\s+)?error(?:\[[^\]]+\])?:\s*(.+)$/i', $error_line, $headline)) {
$pending_message = $headline[1];
}
// GCC, Clang and javac: file.cpp:12:7: error: message
if (preg_match('/^.*?:(\d+):(?:(\d+):)?\s*(?:fatal\s+)?error:\s*(.+)$/i', $error_line, $match)) {
$line_number = intval($match[1]);
$column_number = isset($match[2]) ? intval($match[2]) : 0;
$message = $match[3];
}
// Free Pascal: main.pas(12,7) Error: message
elseif (preg_match('/^.*?\((\d+)(?:,(\d+))?\)\s*(?:Fatal|Error):\s*(.+)$/i', $error_line, $match)) {
$line_number = intval($match[1]);
$column_number = isset($match[2]) ? intval($match[2]) : 0;
$message = $match[3];
}
// MSVC: file.cpp(12,7): error C2143: message
elseif (preg_match('/^.*?\((\d+)(?:,(\d+))?\)\s*:\s*(?:fatal\s+)?error[^:]*:\s*(.+)$/i', $error_line, $match)) {
$line_number = intval($match[1]);
$column_number = isset($match[2]) ? intval($match[2]) : 0;
$message = $match[3];
}
// Rust: " --> src/main.rs:12:7" follows an error headline.
elseif (preg_match('/^\s*-->\s*.*?:(\d+):(\d+)\s*$/', $error_line, $match)) {
$line_number = intval($match[1]);
$column_number = intval($match[2]);
$message = $pending_message;
}
// Python-style parser location: File "main.py", line 12
elseif (preg_match('/File\s+"[^"]+",\s*line\s+(\d+)/i', $error_line, $match)) {
$line_number = intval($match[1]);
$message = '编译器在此行附近报告语法错误';
}
// PHP: "... in Main.php on line 12"
elseif (preg_match('/\bin\s+\S+\s+on\s+line\s+(\d+)\b/i', $error_line, $match)) {
$line_number = intval($match[1]);
$message = '解释器在此行附近报告语法错误';
}
// Bash: "Main.sh: line 12: syntax error"
elseif (preg_match('/:\s*line\s+(\d+):/i', $error_line, $match)) {
$line_number = intval($match[1]);
$message = '解释器在此行附近报告语法错误';
}
// Ruby and other single-column diagnostics: Main.rb:12: syntax error
elseif (preg_match('/^.*?:(\d+):\s*(?:(?:syntax|parse)\s+error.*|.+Error:.*)$/i', $error_line, $match)) {
$line_number = intval($match[1]);
$message = '解释器在此行附近报告语法错误';
}
// Go and similar compilers: main.go:12:7: undefined: name
elseif (preg_match('/^.*?:(\d+):(\d+):\s*(.+)$/', $error_line, $match)
&& !preg_match('/^\s*(?:warning|note):/i', $match[3])) {
$line_number = intval($match[1]);
$column_number = intval($match[2]);
$message = $match[3];
}
if ($line_number < 1 || $line_number > count($source_lines)) continue;
$key = $line_number . ':' . $column_number;
if (isset($seen[$key])) continue;
$seen[$key] = true;
$start = max(1, $line_number - $context_lines);
$end = min(count($source_lines), $line_number + $context_lines);
$excerpt = array();
for ($line = $start; $line <= $end; $line++) {
$excerpt[] = array(
'number' => $line,
'text' => $source_lines[$line - 1],
'is_error' => $line === $line_number,
);
}
$locations[] = array(
'line' => $line_number,
'column' => $column_number,
'message' => '编译错误',
'excerpt' => $excerpt,
);
if (count($locations) >= $max_locations) break;
}
return $locations;
}
}