From 5f05f46cfc25d6f349a864adb491a0201fec7f99 Mon Sep 17 00:00:00 2001 From: mario4tier Date: Tue, 8 Sep 2026 12:38:00 -0400 Subject: [PATCH] fix(java): let javac hold the step frame's invariant, and stop the gate measuring the wrong class Three defects from an adversarial review of the commits that never had one. The step switch's arms return (6c2d9e6c6) but its `default` still broke, so the end of the switch stayed reachable and javac accepted a statement after it -- dead on all twelve MATypes, reached only by the unreachable default. Before 6c2d9e6c6 such a tail ran on every arm and was correct, so that commit is what turned it into a trap. C's sibling has exactly that tail (ta_MA.c:900-902); Java only avoids it because the bookkeeping lives in `update`. `default` now returns too: measured 305 bytes either way, byte-identical class file, and javac rejects the tail as unreachable -- the enforcement is free. The gate searched the concatenated javap output, so a signature was bound to whatever class printed first. Demonstrated: with `Core` dumped first and a same-shaped `peek(double)` on it, the old script measured MaStream.peek as 2 bytes and reported green. It now parses per class and refuses an ambiguous match. And `check=True` was inert: javap exits 0 when only SOME named classes resolve, naming the rest on stderr, which the script captured and discarded. A missing Core$MaStream.class therefore failed with "the signature moved -- fix the pattern", sending the reader to edit a correct regex. stderr is now a rejection in its own right. The step emitter's comment claimed the frame was "4 bytes over" the budget -- true before that same commit, false after it at 20 under, and never right again as the enum grows. It now states the invariant instead: nothing may follow the switch, and the default must keep returning or the trap comes back. Claude-Session: https://claude.ai/code/session_01N7HZcFbUwe3tB9XpKxkFPK --- scripts/check_java_inline_budget.py | 57 ++++++++++++++----- .../generator/src/backends/java_stream.rs | 11 ++-- ta_codegen/output/java/fragments/Core_MA.java | 2 +- .../main/java/io/github/talib/BuildStamp.java | 2 +- .../src/main/java/io/github/talib/Core.java | 2 +- .../output/java/tools/TaCodegenServe.java | 4 +- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/scripts/check_java_inline_budget.py b/scripts/check_java_inline_budget.py index f4d0dfbb7d..f91800b132 100755 --- a/scripts/check_java_inline_budget.py +++ b/scripts/check_java_inline_budget.py @@ -43,19 +43,50 @@ def die(msg: str): sys.exit(1) -def disassemble(classes: str) -> str: - """One javap call for both frames -- two would be two JVM startups.""" +def disassemble(classes: str) -> dict: + """One javap call for both frames -- two would be two JVM startups. + + Returned per class, NOT as one blob: the two signatures are distinct today + only by luck, and a search over the concatenation would happily answer for + the wrong class. javap also exits 0 when only SOME of the named classes + resolve, reporting the rest on stderr, so its status says nothing and the + stderr has to be read. + """ names = ["io.github.talib." + c for c, _ in FRAMES] try: - return subprocess.run(["javap", "-p", "-c", "-cp", classes] + names, - capture_output=True, text=True, check=True).stdout - except (subprocess.CalledProcessError, FileNotFoundError) as e: - die("javap could not read %s from %s: %s" % (", ".join(names), classes, e)) - - -def code_length(out: str, cls: str, sig: re.Pattern) -> int: - lines = out.splitlines() - start = next((i for i, l in enumerate(lines) if sig.match(l)), None) + p = subprocess.run(["javap", "-p", "-c", "-cp", classes] + names, + capture_output=True, text=True) + except FileNotFoundError as e: + die("javap is not on PATH: %s" % e) + if p.returncode != 0: + die("javap failed on %s: %s" % (classes, p.stderr.strip() or p.stdout.strip())) + if p.stderr.strip(): + die("javap could not read every class from %s -- it exits 0 for this, so " + "the gate must reject it explicitly: %s" % (classes, p.stderr.strip())) + + # Split on the class header javap emits once per class. + sections, cur = {}, None + for line in p.stdout.splitlines(): + m = re.match(r"(?:public |final |abstract )*class io\.github\.talib\.(\S+) ", line) + if m: + cur = m.group(1).rstrip("{").strip() + sections[cur] = [] + elif cur is not None: + sections[cur].append(line) + for cls, _ in FRAMES: + if cls not in sections: + die("javap printed no section for io.github.talib.%s -- it was asked " + "for it and did not refuse, so the disassembly parse moved." % cls) + return sections + + +def code_length(sections: dict, cls: str, sig: re.Pattern) -> int: + lines = sections[cls] + matches = [i for i, l in enumerate(lines) if sig.match(l)] + if len(matches) > 1: + die("%s: %d methods match %s -- the gate cannot tell which frame it is " + "measuring." % (cls, len(matches), sig.pattern)) + start = matches[0] if matches else None if start is None: die("%s: no method matching %s -- the signature moved, so this gate " "measured NOTHING. Fix the pattern in this script rather than " @@ -82,10 +113,10 @@ def main(): die("usage: check_java_inline_budget.py ") classes = sys.argv[1] - out = disassemble(classes) + sections = disassemble(classes) over = [] for cls, sig in FRAMES: - n = code_length(out, cls, sig) + n = code_length(sections, cls, sig) name = "%s.%s" % (cls, "peek" if "peek" in sig.pattern else "maStepImpl") print("%-24s %3d bytes (budget %d, %+d)" % (name, n, BUDGET, n - BUDGET)) if n > BUDGET: diff --git a/ta_codegen/generator/src/backends/java_stream.rs b/ta_codegen/generator/src/backends/java_stream.rs index 35d599db4b..73aee262a1 100644 --- a/ta_codegen/generator/src/backends/java_stream.rs +++ b/ta_codegen/generator/src/backends/java_stream.rs @@ -3107,14 +3107,17 @@ fn emit_dispatch( } } } - // `return`, not `break`: the switch is the whole method body, so this - // costs a byte where the jump to the end cost three, and the step frame - // is 4 bytes over the same 325-byte budget the peek frame is kept under. + // Every arm returns, including `default` below, which is what makes + // javac reject anything emitted after this switch. Nothing may go + // there: the arms are the only writers of `sp.cur_*` and a tail would + // be dead on every real MAType, reached only by the unreachable + // default. Keep the two in step -- a `break` default silently restores + // the trap, at no saving. let _ = writeln!(o, " return;"); let _ = writeln!(o, " }}"); } let _ = writeln!(o, " default:"); - let _ = writeln!(o, " break; /* unreachable: open rejects arms without a sub-stream */"); + let _ = writeln!(o, " return; /* unreachable: open rejects arms without a sub-stream */"); let _ = writeln!(o, " }}"); let _ = writeln!(o, " }}"); diff --git a/ta_codegen/output/java/fragments/Core_MA.java b/ta_codegen/output/java/fragments/Core_MA.java index da8f0f2fd7..56610557b2 100644 --- a/ta_codegen/output/java/fragments/Core_MA.java +++ b/ta_codegen/output/java/fragments/Core_MA.java @@ -826,7 +826,7 @@ void maStepImpl( MaStream sp, double inReal ) return; } default: - break; /* unreachable: open rejects arms without a sub-stream */ + return; /* unreachable: open rejects arms without a sub-stream */ } } private RetCode maOpenImpl( MaStream sp, double inReal[], int startIdx, int optInTimePeriod, MAType optInMAType ) diff --git a/ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java b/ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java index c3b1ae4981..36e4bb59d2 100644 --- a/ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java +++ b/ta_codegen/output/java/library/src/main/java/io/github/talib/BuildStamp.java @@ -9,7 +9,7 @@ */ public final class BuildStamp { /** Digest of the generated {@code Core} method text this build carries. */ - public static final String GENCODE_DIGEST = "861471d115c85ebe"; + public static final String GENCODE_DIGEST = "30b672eff480d5e1"; private BuildStamp() { } diff --git a/ta_codegen/output/java/library/src/main/java/io/github/talib/Core.java b/ta_codegen/output/java/library/src/main/java/io/github/talib/Core.java index e8382af22b..a768f13706 100644 --- a/ta_codegen/output/java/library/src/main/java/io/github/talib/Core.java +++ b/ta_codegen/output/java/library/src/main/java/io/github/talib/Core.java @@ -113830,7 +113830,7 @@ void maStepImpl( MaStream sp, double inReal ) return; } default: - break; /* unreachable: open rejects arms without a sub-stream */ + return; /* unreachable: open rejects arms without a sub-stream */ } } private RetCode maOpenImpl( MaStream sp, double inReal[], int startIdx, int optInTimePeriod, MAType optInMAType ) diff --git a/ta_codegen/output/java/tools/TaCodegenServe.java b/ta_codegen/output/java/tools/TaCodegenServe.java index e1ae6b92a3..bd89e52453 100644 --- a/ta_codegen/output/java/tools/TaCodegenServe.java +++ b/ta_codegen/output/java/tools/TaCodegenServe.java @@ -113501,7 +113501,7 @@ void maStepImpl( MaStream sp, double inReal ) return; } default: - break; /* unreachable: open rejects arms without a sub-stream */ + return; /* unreachable: open rejects arms without a sub-stream */ } } private RetCode maOpenImpl( MaStream sp, double inReal[], int startIdx, int optInTimePeriod, MAType optInMAType ) @@ -182151,7 +182151,7 @@ public ZlemaStream zlemaOpenAndFill( double inReal[], int optInTimePeriod, doubl public class TaCodegenServe { static Core core = new Core(); - static final String SPLICED_GENCODE_DIGEST = "861471d115c85ebe"; + static final String SPLICED_GENCODE_DIGEST = "30b672eff480d5e1"; static final int MAX_ARRAY_SIZE = 200000; static double[] refOpen = new double[MAX_ARRAY_SIZE]; static double[] refHigh = new double[MAX_ARRAY_SIZE];