From 4d5923edec6c9caf103659b0fac9880f91007ff8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 10:40:36 +0100 Subject: [PATCH 1/7] chore: redundant code removal --- main.cpp | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/main.cpp b/main.cpp index 6bc2779..43ef72d 100644 --- a/main.cpp +++ b/main.cpp @@ -310,6 +310,24 @@ static AnalysisResult filterResult(const AnalysisResult& result, const AnalysisC return filtered; } +static AnalysisResult filterWarningsOnly(const AnalysisResult& result, const AnalysisConfig& cfg) +{ + if (!cfg.warningsOnly) + return result; + + AnalysisResult filtered; + filtered.config = result.config; + filtered.functions = result.functions; + for (const auto& d : result.diagnostics) + { + if (d.severity != DiagnosticSeverity::Info) + { + filtered.diagnostics.push_back(d); + } + } + return filtered; +} + void toto(void) { char test[974] = "Hello"; @@ -529,8 +547,9 @@ int main(int argc, char** argv) cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); if (results.size() == 1) { - const AnalysisResult filtered = + AnalysisResult filtered = applyFilter ? filterResult(results[0].second, cfg) : results[0].second; + filtered = filterWarningsOnly(filtered, cfg); llvm::outs() << ctrace::stack::toJson(filtered, results[0].first); } else @@ -545,7 +564,8 @@ int main(int argc, char** argv) merged.diagnostics.insert(merged.diagnostics.end(), res.diagnostics.begin(), res.diagnostics.end()); } - const AnalysisResult filtered = applyFilter ? filterResult(merged, cfg) : merged; + AnalysisResult filtered = applyFilter ? filterResult(merged, cfg) : merged; + filtered = filterWarningsOnly(filtered, cfg); llvm::outs() << ctrace::stack::toJson(filtered, inputFilenames); } return 0; @@ -557,8 +577,9 @@ int main(int argc, char** argv) cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); if (results.size() == 1) { - const AnalysisResult filtered = + AnalysisResult filtered = applyFilter ? filterResult(results[0].second, cfg) : results[0].second; + filtered = filterWarningsOnly(filtered, cfg); llvm::outs() << ctrace::stack::toSarif(filtered, results[0].first, "coretrace-stack-analyzer", "0.1.0"); } @@ -574,7 +595,8 @@ int main(int argc, char** argv) merged.diagnostics.insert(merged.diagnostics.end(), res.diagnostics.begin(), res.diagnostics.end()); } - const AnalysisResult filtered = applyFilter ? filterResult(merged, cfg) : merged; + AnalysisResult filtered = applyFilter ? filterResult(merged, cfg) : merged; + filtered = filterWarningsOnly(filtered, cfg); llvm::outs() << ctrace::stack::toSarif(filtered, inputFilenames.front(), "coretrace-stack-analyzer", "0.1.0"); } @@ -640,23 +662,6 @@ int main(int argc, char** argv) llvm::outs() << " max stack (including callees): " << f.maxStack << " bytes\n"; } - if (f.isRecursive) - { - llvm::outs() << " [!] recursive or mutually recursive function detected\n"; - } - - if (f.hasInfiniteSelfRecursion) - { - llvm::outs() << " [!!!] unconditional self recursion detected (no base case)\n"; - llvm::outs() << " this will eventually overflow the stack at runtime\n"; - } - - if (f.exceedsLimit) - { - llvm::outs() << " [!] potential stack overflow: exceeds limit of " - << result.config.stackLimit << " bytes\n"; - } - if (!result.config.quiet) { for (const auto& d : result.diagnostics) @@ -664,11 +669,8 @@ int main(int argc, char** argv) if (d.funcName != f.name) continue; - // Si warningsOnly est actif, on ignore les diagnostics Info if (result.config.warningsOnly && d.severity == DiagnosticSeverity::Info) - { continue; - } if (d.line != 0) { From aa875ab1149d8f38ed993e3795e2c48b3a5d7860 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 10:41:40 +0100 Subject: [PATCH 2/7] test: addition of a test that verifies parity between --format mode and HumanReadable output --- run_test.py | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 311 insertions(+), 6 deletions(-) diff --git a/run_test.py b/run_test.py index 6ad98e2..c1c0ded 100755 --- a/run_test.py +++ b/run_test.py @@ -2,6 +2,7 @@ import sys import subprocess import json +import re from pathlib import Path # Chemin vers ton binaire d'analyse @@ -92,6 +93,300 @@ def run_analyzer_on_file(c_path: Path) -> str: return output +def run_analyzer(args) -> subprocess.CompletedProcess: + """ + Run analyzer with custom args and return the CompletedProcess. + """ + return subprocess.run([str(ANALYZER)] + args, capture_output=True, text=True) + + +def parse_stack_line(line: str, label: str): + """ + Parse stack lines like: + "local stack: 123 bytes" + "local stack: unknown (>= 256 bytes)" + Returns dict with unknown/value/lower_bound or None if not matched. + """ + label_re = re.escape(label) + m_unknown = re.search( + rf"{label_re}:\s*unknown(?:\s*\(>=\s*(\d+)\s*bytes\))?", line + ) + if m_unknown: + lower_bound = int(m_unknown.group(1)) if m_unknown.group(1) else None + return {"unknown": True, "value": None, "lower_bound": lower_bound} + m_value = re.search(rf"{label_re}:\s*(\d+)\s*bytes", line) + if m_value: + return {"unknown": False, "value": int(m_value.group(1)), "lower_bound": None} + return None + + +def parse_human_functions(output: str): + """ + Parse human-readable output to extract per-function metadata. + """ + functions = {} + current = None + for line in output.splitlines(): + if line.startswith("Function: "): + # Skip diagnostic header lines like: "Function: foo (line 12, column 3)" + if "(line " in line: + continue + rest = line[len("Function: "):].strip() + if not rest: + current = None + continue + name = rest.split()[0] + functions[name] = { + "localStackUnknown": None, + "localStack": None, + "localStackLowerBound": None, + "maxStackUnknown": None, + "maxStack": None, + "maxStackLowerBound": None, + "isRecursive": False, + "hasInfiniteSelfRecursion": False, + "exceedsLimit": False, + } + current = name + continue + + if current is None: + continue + + stripped = line.strip() + if stripped.startswith("local stack:"): + info = parse_stack_line(stripped, "local stack") + if info: + functions[current]["localStackUnknown"] = info["unknown"] + functions[current]["localStack"] = info["value"] + functions[current]["localStackLowerBound"] = info["lower_bound"] + continue + if stripped.startswith("max stack (including callees):"): + info = parse_stack_line(stripped, "max stack (including callees)") + if info: + functions[current]["maxStackUnknown"] = info["unknown"] + functions[current]["maxStack"] = info["value"] + functions[current]["maxStackLowerBound"] = info["lower_bound"] + continue + if "recursive or mutually recursive function detected" in stripped: + functions[current]["isRecursive"] = True + continue + if "unconditional self recursion detected" in stripped: + functions[current]["hasInfiniteSelfRecursion"] = True + continue + if "potential stack overflow: exceeds limit of" in stripped: + functions[current]["exceedsLimit"] = True + continue + return functions + + +def parse_human_diagnostic_messages(output: str): + """ + Extract diagnostic message blocks from human-readable output. + """ + blocks = [] + lines = output.splitlines() + i = 0 + while i < len(lines): + stripped = lines[i].strip() + if stripped.startswith("Function:") and "(line " in stripped: + # Diagnostic blocks that start with a Function: header line. + block_lines = [lines[i]] + i += 1 + while i < len(lines): + next_line = lines[i] + next_stripped = next_line.strip() + if next_stripped == "": + break + if next_stripped.startswith(("Function:", "Mode:", "File:")): + break + if next_stripped.startswith(("local stack:", "max stack (including callees):")): + break + if next_stripped.startswith("[") and not next_line[:1].isspace(): + break + block_lines.append(next_line) + i += 1 + + blocks.append(normalize("\n".join(block_lines))) + + if i < len(lines) and lines[i].strip() == "": + i += 1 + continue + + if stripped.startswith("at line ") and ", column " in stripped: + # Diagnostic blocks that follow a source location line. + block_lines = [] + i += 1 + while i < len(lines): + next_line = lines[i] + next_stripped = next_line.strip() + if next_stripped == "": + break + if next_stripped.startswith(("Function:", "Mode:", "File:")): + break + if next_stripped.startswith(("local stack:", "max stack (including callees):")): + break + if next_stripped.startswith("[") and not next_line[:1].isspace(): + break + block_lines.append(next_line) + i += 1 + + if block_lines: + blocks.append(normalize("\n".join(block_lines))) + + if i < len(lines) and lines[i].strip() == "": + i += 1 + continue + + i += 1 + + return blocks + + +def check_human_vs_json_parity() -> bool: + """ + Compare human-readable output vs JSON output for the same input. + Fails if information present in one view is missing in the other. + """ + print("=== Testing human vs JSON parity ===") + samples = [] + for ext in ("*.c", "*.cpp"): + samples.extend(TEST_DIR.glob(f"**/{ext}")) + samples = sorted(samples) + if not samples: + print(" (no .c/.cpp files found, skipping)\n") + return True + + ok = True + for sample in samples: + sample_ok = True + human = run_analyzer([str(sample)]) + if human.returncode != 0: + print(f" ❌ human run failed for {sample} (code {human.returncode})") + print(human.stdout) + print(human.stderr) + sample_ok = False + ok = False + continue + + structured = run_analyzer([str(sample), "--format=json"]) + if structured.returncode != 0: + print(f" ❌ json run failed for {sample} (code {structured.returncode})") + print(structured.stdout) + print(structured.stderr) + sample_ok = False + ok = False + continue + + try: + payload = json.loads(structured.stdout) + except json.JSONDecodeError as exc: + print(f" ❌ invalid JSON output for {sample}: {exc}") + print(structured.stdout) + sample_ok = False + ok = False + continue + + human_output = (human.stdout or "") + (human.stderr or "") + norm_human = normalize(human_output) + human_functions = parse_human_functions(human_output) + human_diag_blocks = parse_human_diagnostic_messages(human_output) + + mode = payload.get("meta", {}).get("mode") + if mode and f"Mode: {mode}" not in human_output: + print(f" ❌ mode mismatch for {sample} (json={mode})") + sample_ok = False + + for f in payload.get("functions", []): + name = f.get("name", "") + if not name: + continue + if name not in human_functions: + print(f" ❌ function missing in human output: {name}") + sample_ok = False + continue + hf = human_functions[name] + + if hf["localStackUnknown"] is None: + print(f" ❌ local stack missing in human output for: {name}") + sample_ok = False + elif f.get("localStackUnknown") != hf["localStackUnknown"]: + print(f" ❌ local stack unknown flag mismatch for: {name}") + sample_ok = False + elif not f.get("localStackUnknown"): + if f.get("localStack") != hf["localStack"]: + print(f" ❌ local stack value mismatch for: {name}") + sample_ok = False + elif hf["localStackLowerBound"] is not None: + json_lb = f.get("localStackLowerBound") + if json_lb != hf["localStackLowerBound"]: + print(f" ❌ local stack lower bound mismatch for: {name}") + sample_ok = False + + if hf["maxStackUnknown"] is None: + print(f" ❌ max stack missing in human output for: {name}") + sample_ok = False + elif f.get("maxStackUnknown") != hf["maxStackUnknown"]: + print(f" ❌ max stack unknown flag mismatch for: {name}") + sample_ok = False + elif not f.get("maxStackUnknown"): + if f.get("maxStack") != hf["maxStack"]: + print(f" ❌ max stack value mismatch for: {name}") + sample_ok = False + elif hf["maxStackLowerBound"] is not None: + json_lb = f.get("maxStackLowerBound") + if json_lb != hf["maxStackLowerBound"]: + print(f" ❌ max stack lower bound mismatch for: {name}") + sample_ok = False + + if f.get("isRecursive") != hf["isRecursive"]: + print(f" ❌ recursion flag mismatch for: {name}") + sample_ok = False + if f.get("hasInfiniteSelfRecursion") != hf["hasInfiniteSelfRecursion"]: + print(f" ❌ infinite recursion flag mismatch for: {name}") + sample_ok = False + if f.get("exceedsLimit") != hf["exceedsLimit"]: + print(f" ❌ stack limit flag mismatch for: {name}") + sample_ok = False + + for d in payload.get("diagnostics", []): + details = d.get("details", {}) + msg = details.get("message", "") + if msg and normalize(msg) not in norm_human: + print(" ❌ diagnostic message missing in human output") + print(f" message: {msg}") + sample_ok = False + loc = d.get("location", {}) + line = loc.get("startLine", 0) + column = loc.get("startColumn", 0) + if line and column: + needle = normalize(f"at line {line}, column {column}") + if needle not in norm_human: + print(" ❌ diagnostic location missing in human output") + print(f" location: line {line}, column {column}") + sample_ok = False + + json_messages = { + normalize(d.get("details", {}).get("message", "")) + for d in payload.get("diagnostics", []) + if d.get("details", {}).get("message") + } + for block in human_diag_blocks: + if block and block not in json_messages: + print(" ❌ diagnostic message missing in JSON output") + print(f" message: {block}") + sample_ok = False + + if sample_ok: + print(f" ✅ parity OK for {sample}") + else: + print(f" ❌ parity FAIL for {sample}") + ok = ok and sample_ok + + print() + return ok + + def check_help_flags() -> bool: """ Vérifie que -h et --help affichent l'aide sur stdout et retournent 0. @@ -371,15 +666,25 @@ def check_file(c_path: Path): def main() -> int: - global_ok = check_help_flags() - if not check_multi_file_json(): + total_tests = 0 + passed_tests = 0 + + def record_ok(ok: bool): + nonlocal total_tests, passed_tests + total_tests += 1 + if ok: + passed_tests += 1 + return ok + + global_ok = record_ok(check_help_flags()) + if not record_ok(check_multi_file_json()): global_ok = False - if not check_multi_file_failure(): + if not record_ok(check_multi_file_failure()): global_ok = False - if not check_cli_parsing_and_filters(): + if not record_ok(check_cli_parsing_and_filters()): + global_ok = False + if not record_ok(check_human_vs_json_parity()): global_ok = False - total_tests = 0 - passed_tests = 0 c_files = sorted(list(TEST_DIR.glob("**/*.c")) + list(TEST_DIR.glob("**/*.cpp"))) if not c_files: From 43fad7529b02f7b71310698aa1ba4fda47831c54 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 10:43:11 +0100 Subject: [PATCH 3/7] fix: format some messages bypass format mode output even though they appear in hulan readable-mode --- src/StackUsageAnalyzer.cpp | 68 ++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/src/StackUsageAnalyzer.cpp b/src/StackUsageAnalyzer.cpp index 7be342d..1e91b56 100644 --- a/src/StackUsageAnalyzer.cpp +++ b/src/StackUsageAnalyzer.cpp @@ -4054,6 +4054,45 @@ namespace ctrace::stack result.functions.push_back(std::move(fr)); } + // 5b) Emit summary diagnostics for recursion/overflow flags (for JSON parity) + for (const auto& fr : result.functions) + { + if (fr.isRecursive) + { + Diagnostic diag; + diag.funcName = fr.name; + diag.filePath = fr.filePath; + diag.severity = DiagnosticSeverity::Warning; + diag.errCode = DescriptiveErrorCode::None; + diag.message = " [!] recursive or mutually recursive function detected\n"; + result.diagnostics.push_back(std::move(diag)); + } + + if (fr.hasInfiniteSelfRecursion) + { + Diagnostic diag; + diag.funcName = fr.name; + diag.filePath = fr.filePath; + diag.severity = DiagnosticSeverity::Warning; + diag.errCode = DescriptiveErrorCode::None; + diag.message = " [!!!] unconditional self recursion detected (no base case)\n" + " this will eventually overflow the stack at runtime\n"; + result.diagnostics.push_back(std::move(diag)); + } + + if (fr.exceedsLimit) + { + Diagnostic diag; + diag.funcName = fr.name; + diag.filePath = fr.filePath; + diag.severity = DiagnosticSeverity::Warning; + diag.errCode = DescriptiveErrorCode::None; + diag.message = " [!] potential stack overflow: exceeds limit of " + + std::to_string(config.stackLimit) + " bytes\n"; + result.diagnostics.push_back(std::move(diag)); + } + } + // 6) Détection des dépassements de buffer sur la stack (analyse intra-fonction) std::vector bufferIssues; for (llvm::Function& F : mod) @@ -5042,41 +5081,36 @@ namespace ctrace::stack os << f.localStack; } os << ",\n"; - os << " \"localStackUnknown\": " << (f.localStackUnknown ? "true" : "false") - << ",\n"; - os << " \"maxStack\": "; - if (f.maxStackUnknown) + os << " \"localStackLowerBound\": "; + if (f.localStackUnknown && f.localStack > 0) { - os << "null"; + os << f.localStack; } else { - os << f.maxStack; + os << "null"; } os << ",\n"; - os << " \"maxStackUnknown\": " << (f.maxStackUnknown ? "true" : "false") << ",\n"; - os << " \"hasDynamicAlloca\": " << (f.hasDynamicAlloca ? "true" : "false") + os << " \"localStackUnknown\": " << (f.localStackUnknown ? "true" : "false") << ",\n"; - os << " \"localStack\": "; - if (f.localStackUnknown) + os << " \"maxStack\": "; + if (f.maxStackUnknown) { os << "null"; } else { - os << f.localStack; + os << f.maxStack; } os << ",\n"; - os << " \"localStackUnknown\": " << (f.localStackUnknown ? "true" : "false") - << ",\n"; - os << " \"maxStack\": "; - if (f.maxStackUnknown) + os << " \"maxStackLowerBound\": "; + if (f.maxStackUnknown && f.maxStack > 0) { - os << "null"; + os << f.maxStack; } else { - os << f.maxStack; + os << "null"; } os << ",\n"; os << " \"maxStackUnknown\": " << (f.maxStackUnknown ? "true" : "false") << ",\n"; From c513b6e9527dacd156e092baffc066d8a37e67e4 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 10:43:39 +0100 Subject: [PATCH 4/7] test(alloca): adding some tests --- test/alloca/recursive-controlled-alloca.c | 7 ++++++- test/alloca/recursive-infinite-alloca.c | 11 ++++++++++- test/alloca/user-controlled.c | 7 ++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/alloca/recursive-controlled-alloca.c b/test/alloca/recursive-controlled-alloca.c index 0688ad7..a5c10b2 100644 --- a/test/alloca/recursive-controlled-alloca.c +++ b/test/alloca/recursive-controlled-alloca.c @@ -3,7 +3,12 @@ int rec(size_t n) { - // at line 12, column 22 + // at line 17, column 22 + // [!] dynamic stack allocation detected for variable 'p' + // allocated type: i8 + // size of this allocation is not compile-time constant (VLA / variable alloca) and may lead to unbounded stack usage + + // at line 17, column 22 // [!!] user-controlled alloca size for variable 'p' // allocation performed via alloca/VLA; stack usage grows with runtime value // size is unbounded at compile time diff --git a/test/alloca/recursive-infinite-alloca.c b/test/alloca/recursive-infinite-alloca.c index 997afbb..0a289d6 100644 --- a/test/alloca/recursive-infinite-alloca.c +++ b/test/alloca/recursive-infinite-alloca.c @@ -1,9 +1,18 @@ #include #include +// [!] recursive or mutually recursive function detected + +// [!!!] unconditional self recursion detected (no base case) +// this will eventually overflow the stack at runtime void boom(size_t n) { - // at line 12, column 22 + // at line 21, column 22 + // [!] dynamic stack allocation detected for variable 'p' + // allocated type: i8 + // size of this allocation is not compile-time constant (VLA / variable alloca) and may lead to unbounded stack usage + + // at line 21, column 22 // [!!] user-controlled alloca size for variable 'p' // allocation performed via alloca/VLA; stack usage grows with runtime value // size is unbounded at compile time diff --git a/test/alloca/user-controlled.c b/test/alloca/user-controlled.c index 9e657ad..7a7c045 100644 --- a/test/alloca/user-controlled.c +++ b/test/alloca/user-controlled.c @@ -3,7 +3,12 @@ void foo(size_t n) { - // at line 11, column 24 + // at line 16, column 24 + // [!] dynamic stack allocation detected for variable 'buf' + // allocated type: i8 + // size of this allocation is not compile-time constant (VLA / variable alloca) and may lead to unbounded stack usage + + // at line 16, column 24 // [!!] user-controlled alloca size for variable 'buf' // allocation performed via alloca/VLA; stack usage grows with runtime value // size is unbounded at compile time From 1bd5464ea8eb2ecfc787275ac840d74dbf427133 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 11:24:07 +0100 Subject: [PATCH 5/7] test(CI-Ubuntu): try to fix CI --- run_test.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/run_test.py b/run_test.py index c1c0ded..6af6ea3 100755 --- a/run_test.py +++ b/run_test.py @@ -126,14 +126,29 @@ def parse_human_functions(output: str): """ functions = {} current = None - for line in output.splitlines(): + lines = output.splitlines() + i = 0 + while i < len(lines): + line = lines[i] if line.startswith("Function: "): # Skip diagnostic header lines like: "Function: foo (line 12, column 3)" if "(line " in line: + i += 1 continue + # If the next non-empty line is not a stack summary, this is likely + # a diagnostic header embedded in the output. + j = i + 1 + while j < len(lines) and lines[j].strip() == "": + j += 1 + if j < len(lines): + next_stripped = lines[j].strip() + if not next_stripped.startswith("local stack:"): + i += 1 + continue rest = line[len("Function: "):].strip() if not rest: current = None + i += 1 continue name = rest.split()[0] functions[name] = { @@ -148,9 +163,11 @@ def parse_human_functions(output: str): "exceedsLimit": False, } current = name + i += 1 continue if current is None: + i += 1 continue stripped = line.strip() @@ -160,6 +177,7 @@ def parse_human_functions(output: str): functions[current]["localStackUnknown"] = info["unknown"] functions[current]["localStack"] = info["value"] functions[current]["localStackLowerBound"] = info["lower_bound"] + i += 1 continue if stripped.startswith("max stack (including callees):"): info = parse_stack_line(stripped, "max stack (including callees)") @@ -167,16 +185,21 @@ def parse_human_functions(output: str): functions[current]["maxStackUnknown"] = info["unknown"] functions[current]["maxStack"] = info["value"] functions[current]["maxStackLowerBound"] = info["lower_bound"] + i += 1 continue if "recursive or mutually recursive function detected" in stripped: functions[current]["isRecursive"] = True + i += 1 continue if "unconditional self recursion detected" in stripped: functions[current]["hasInfiniteSelfRecursion"] = True + i += 1 continue if "potential stack overflow: exceeds limit of" in stripped: functions[current]["exceedsLimit"] = True + i += 1 continue + i += 1 return functions From ce448007976c60b7d24d827c4faf1e63a74082e2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 11:31:04 +0100 Subject: [PATCH 6/7] test(CI-Ubuntu): try to fix CI --- run_test.py | 116 ++++++++++++++++++++++------------------------------ 1 file changed, 49 insertions(+), 67 deletions(-) diff --git a/run_test.py b/run_test.py index 6af6ea3..72f4c67 100755 --- a/run_test.py +++ b/run_test.py @@ -125,81 +125,63 @@ def parse_human_functions(output: str): Parse human-readable output to extract per-function metadata. """ functions = {} - current = None lines = output.splitlines() i = 0 while i < len(lines): line = lines[i] - if line.startswith("Function: "): - # Skip diagnostic header lines like: "Function: foo (line 12, column 3)" - if "(line " in line: - i += 1 - continue - # If the next non-empty line is not a stack summary, this is likely - # a diagnostic header embedded in the output. - j = i + 1 - while j < len(lines) and lines[j].strip() == "": - j += 1 - if j < len(lines): - next_stripped = lines[j].strip() - if not next_stripped.startswith("local stack:"): - i += 1 - continue - rest = line[len("Function: "):].strip() - if not rest: - current = None - i += 1 - continue - name = rest.split()[0] - functions[name] = { - "localStackUnknown": None, - "localStack": None, - "localStackLowerBound": None, - "maxStackUnknown": None, - "maxStack": None, - "maxStackLowerBound": None, - "isRecursive": False, - "hasInfiniteSelfRecursion": False, - "exceedsLimit": False, - } - current = name + if not line.startswith("Function: "): i += 1 continue - if current is None: - i += 1 - continue + # Detect the end of this function block. + j = i + 1 + while j < len(lines) and not lines[j].startswith("Function: "): + if lines[j].startswith("Mode: ") or lines[j].startswith("File: "): + break + j += 1 - stripped = line.strip() - if stripped.startswith("local stack:"): - info = parse_stack_line(stripped, "local stack") - if info: - functions[current]["localStackUnknown"] = info["unknown"] - functions[current]["localStack"] = info["value"] - functions[current]["localStackLowerBound"] = info["lower_bound"] - i += 1 - continue - if stripped.startswith("max stack (including callees):"): - info = parse_stack_line(stripped, "max stack (including callees)") - if info: - functions[current]["maxStackUnknown"] = info["unknown"] - functions[current]["maxStack"] = info["value"] - functions[current]["maxStackLowerBound"] = info["lower_bound"] - i += 1 - continue - if "recursive or mutually recursive function detected" in stripped: - functions[current]["isRecursive"] = True - i += 1 - continue - if "unconditional self recursion detected" in stripped: - functions[current]["hasInfiniteSelfRecursion"] = True - i += 1 - continue - if "potential stack overflow: exceeds limit of" in stripped: - functions[current]["exceedsLimit"] = True - i += 1 - continue - i += 1 + block = lines[i:j] + if any(l.strip().startswith("local stack:") for l in block): + if "(line " in line: + i = j + continue + rest = line[len("Function: "):].strip() + if rest: + name = rest.split()[0] + functions[name] = { + "localStackUnknown": None, + "localStack": None, + "localStackLowerBound": None, + "maxStackUnknown": None, + "maxStack": None, + "maxStackLowerBound": None, + "isRecursive": False, + "hasInfiniteSelfRecursion": False, + "exceedsLimit": False, + } + + for block_line in block: + stripped = block_line.strip() + if stripped.startswith("local stack:"): + info = parse_stack_line(stripped, "local stack") + if info: + functions[name]["localStackUnknown"] = info["unknown"] + functions[name]["localStack"] = info["value"] + functions[name]["localStackLowerBound"] = info["lower_bound"] + elif stripped.startswith("max stack (including callees):"): + info = parse_stack_line(stripped, "max stack (including callees)") + if info: + functions[name]["maxStackUnknown"] = info["unknown"] + functions[name]["maxStack"] = info["value"] + functions[name]["maxStackLowerBound"] = info["lower_bound"] + elif "recursive or mutually recursive function detected" in stripped: + functions[name]["isRecursive"] = True + elif "unconditional self recursion detected" in stripped: + functions[name]["hasInfiniteSelfRecursion"] = True + elif "potential stack overflow: exceeds limit of" in stripped: + functions[name]["exceedsLimit"] = True + + i = j return functions From f5537fab8fee7c7e7d86dd05b578f46cc0b54949 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 1 Feb 2026 12:45:01 +0100 Subject: [PATCH 7/7] test(CI-Ubuntu): try to fix CI --- run_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run_test.py b/run_test.py index 72f4c67..9a3aff3 100755 --- a/run_test.py +++ b/run_test.py @@ -346,12 +346,15 @@ def check_human_vs_json_parity() -> bool: if f.get("isRecursive") != hf["isRecursive"]: print(f" ❌ recursion flag mismatch for: {name}") + print(f" human: {hf['isRecursive']} json: {f.get('isRecursive')}") sample_ok = False if f.get("hasInfiniteSelfRecursion") != hf["hasInfiniteSelfRecursion"]: print(f" ❌ infinite recursion flag mismatch for: {name}") + print(f" human: {hf['hasInfiniteSelfRecursion']} json: {f.get('hasInfiniteSelfRecursion')}") sample_ok = False if f.get("exceedsLimit") != hf["exceedsLimit"]: print(f" ❌ stack limit flag mismatch for: {name}") + print(f" human: {hf['exceedsLimit']} json: {f.get('exceedsLimit')}") sample_ok = False for d in payload.get("diagnostics", []):