Reserve report/report_value as observer forms; add -e CLI flag (#1102, #1108) - #1110
Merged
Conversation
#1108) Reject every source binding and first-class use of report/report_value with parser diagnostic E005 before the unit executes. Require an identifier operand (parentheses allowed); preserve the existing observer opcodes and name/slot behavior. Carry the diagnostic code and token location through lint and LSP, with no binding-order analysis or per-entry-point policy. Add CLI -e source execution through the file compilation path: main did not previously implement this entry point. Adapt the meta-interpreter bridge, indirect observer-gate probe, and opaque-value fixture to the reserved syntax. Document the breaking rule and operand decision throughout the language docs. Regression: the issue's shadowing program printed moving then mine on main. The 209-check fixture covers 24 binding/read shapes, both names, file/-e/lint, operand refusal, eval/load_file/import/REPL, field keys, formatting and the meta-interpreter bridge. Pristine main: 27/209 passed, 182 failed. Removing only the two keyword registrations: 27/209 passed, 182 failed. Restored: 209/209 passed, 0 failed. Embedding smoke: OK; LSP checks: 107 passed. Validation (sequential in /home/jon/src/wt/es-1102): release: RESULTS: 4253/4253 passed, 0 failed ASAN_OPTIONS=detect_leaks=1: RESULTS: 4242/4242 passed, 0 failed LeakSanitizer tally: 0 jit_diff: OK (230 programs x {jit, osr} vs the interpreter; 4 arms adjudicated by replay; 0 ledgered) Measured brief corrections: main's report of 5 returned equilibrium (report was a registered builtin); report_value of 5 raised undefined variable. -e was not implemented. Match cases compare expressions, not bind names. Closes #1102. Closes #1108. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB
#1102) The reservation fixture ignored EIGS, so a command requesting the main binary silently tested the branch and passed. Honor EIGS using the existing sibling fixture convention, defaulting to this worktree's src/eigenscript. Keep the 209-check fixture and suite section [42a] intact. Remove report from the builtin reference table and exclude both reserved report forms from the generated LSP builtin index. Derive the excluded names from the lexer's existing keyword registrations and add the lexer dependency to Makefile. Regenerate src/lsp_builtin_index.h and make it trackable as requested. Its generator reads runtime registrations, and the header was previously ignored. Pin the actual editor completion response for both names, with print as a positive control against accidentally dropping all builtins. Keep builtin_report and its registration: the bytecode bridge still reaches it through GET_NAME("report") + CALL. Add two bytecode fixture assertions for its equilibrium/data and opaque/callable results, and document this compatibility entry in BUILTINS.md, CHANGELOG.md, and the function comment. The reservation logic, E005, -e, JIT, and observer runtime behavior are unchanged. Reachability repro (branch): src/eigenscript -e 'print of (vm_run_bytecode of [1, [25,0,0,0,1,0,39,1,0,40], ["report",5]])' equilibrium exit 0 src/eigenscript tests/test_vm_run_bytecode.eigs Tests: 63 | Pass: 63 | Fail: 0 All tests passed. Fixture oracle, using the existing main binary read-only (no canonical build): EIGS=/home/jon/src/InauguralSystems/EigenScriptEcosystem/EigenScript/src/eigenscript bash tests/test_report_reserved.sh Before F1: RESULTS: 209/209 passed, 0 failed (reserved observer forms), exit 0 After F1: RESULTS: 27/209 passed, 182 failed (reserved observer forms), exit 1 env -u EIGS bash tests/test_report_reserved.sh RESULTS: 209/209 passed, 0 failed (reserved observer forms), exit 0 Section [42a] also passes all 209 checks inside both full suites. LSP completion evidence: Before: {"label":"report","kind":3,"detail":"report — builtin; see docs/BUILTINS.md"} After: no report or report_value completion entries. Plant: temporarily remove `if (name in excluded) next` from the real generator, regenerate/rebuild with make lsp, then run python3 tests/test_lsp.py. FAIL: completion excludes reserved observer form 'report' Results: 109 passed, 1 failed, exit 1 Restore the generator and rebuild: Results: 110 passed, 0 failed, exit 0. ASan/UBSan LSP build, ASAN_OPTIONS=detect_leaks=1: Results: 110 passed, 0 failed, exit 0; no sanitizer reports. Gates (full release suite ONCE and full ASan suite ONCE in this round): make; cd tests && env -u EIGS bash run_all_tests.sh RESULTS: 4258/4258 passed, 0 failed, exit 0 make asan; cd tests && env -u EIGS ASAN_OPTIONS=detect_leaks=1 bash run_all_tests.sh RESULTS: 4247/4247 passed, 0 failed, exit 0 Leak tally: 0 in both runs; no leak-only exit notes. make jit-smoke JIT smoke: all cases passed. bash tools/gen_lsp_builtin_index.sh /tmp/es-1102-r2-index.h gen_lsp_builtin_index: 252 core + 87 extension builtins, 205 with signature comments cmp src/lsp_builtin_index.h /tmp/es-1102-r2-index.h LSP index sync: byte-identical, exit 0 (also checked after restoring the plant). bash tools/stdlib_index_check.sh stdlib index OK: 201 builtins + 77 modules all documented bash tools/stdlib_index_check.sh --selftest SELFTEST OK: gate flags an injected undocumented builtin Warning/header gate inside both suites: werror warning gate OK: all 456 compile invocations across 28 dry-run targets + 7 script(s) carry: -Werror=switch -Werror=comment -Werror=misleading-indentation Executable docs: all 84 doc examples match; all 19 error examples fail as documented. Evidence logs: /tmp/es-1102-r2-{fixture-main,fixture-branch,lsp-plant,lsp-green,lsp-asan,release-suite,asan-suite,jit-smoke}.log The ASan log also contains a sed Broken pipe line during the exit-ledger self-test; that self-test and the suite finish successfully, and the child ledger reports 0 nonzero exits. No extra changes were made for this output. Prepared on fix-1102 atop d24c077. Git metadata is read-only for this builder; include the generated src/lsp_builtin_index.h when committing. Release binary alias restored after the ASan gate. No known remaining F1/F2 gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB
… macOS) Round 2 passed the two reserved observer words to awk via `-v reserved="..."` extracted newline-separated from src/lexer.c. GNU awk (Linux CI) tolerates a newline inside a -v value; BSD awk (macOS CI) rejects it — `awk: newline in string report` — so the werror-switch gate [99i], which regenerates the LSP index, failed on both macos lanes (and cascaded to [99p]). Collapse the names to a single space-separated line with `tr '\n' ' '`; the awk split() already accepts space or newline. Regenerated header is byte-identical; local werror gate green. Only macOS CI can confirm the BSD-awk path. Also note in CHANGELOG that the two new TokType members shift test_corpus identifier vocab ids by +2 (iLambdaAi corpora rebuild). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB
InauguralPhysicist
marked this pull request as ready for review
September 6, 2026 12:29
InauguralPhysicist
pushed a commit
that referenced
this pull request
Sep 8, 2026
…n OP_GET_NAME, not string data (#1046, #915) The write-path gate (#915) armed on the PRESENCE of any `import` (OP_IMPORT sat in opcode_is_observer_reader) and on any string constant spelling an observer builtin's name (const_pool_names_observer matched the whole pool). Both were conservative stand-ins for scans nobody had written; both cost read-free programs their gate. Repro (issue rows, baseline v0.43.0 binary, EIGS_OBS_GATE_STATS=1): D: msg is "report" + 200k-frame write loop -> obs-gate: observed imp_b: import linalg (never used) + the same loop -> obs-gate: observed ctl: host with pre-import history; import probe -> observed / diverging After (this build), n=5 medians, wall seconds, under load (~1.4 avg), provisional: A loads wlib only 0.035 -> 0.035 unobserved -> unobserved B + load_file dead read 0.049 -> 0.051 observed -> observed (per-function liveness, out of scope) C + load_file control 0.036 -> 0.037 unobserved -> unobserved D msg is "report" 0.051 -> 0.030 observed -> unobserved (row A is 0.035) imp_a no import 0.023 -> 0.019 unobserved -> unobserved imp_b + import linalg 0.032 -> 0.021 observed -> unobserved (row imp_a is 0.019) ctl import reader diverging -> diverging, observed both sides (rc=0) What changed: * src/builtins_host.c: eigs_import_resolve() -- the ONE `import NAME` resolver (project-first, then stdlib, #821/#904/#1056), extracted from the inline logic in the OP_IMPORT handler. #915 deferred the import half because a second resolver would drift (#737); now there is one, and both the handler and the gate's eager pass call it. Its path scratch lives on the heap: it runs inside vm_execute, which recurses on nested imports, and -fstack-usage measured the stack version at 28,800 bytes per frame against 112 now (the C-stack rule; tools/embed_stack_soak.sh green). The freestanding profile gets a stub that resolves nothing. * src/vm.c OP_IMPORT: calls the shared resolver (the collision warning is unchanged, driven by the `shadowed` out-param), and gains the same outcome guard builtin_load_file has: a module that reads observer state while the gate was closed for this program's earlier assignments RAISES ("import: '<name>' reads observer state ...") instead of answering a rest value. This is what makes the compile-time scan sound against a module rewritten or shadowed between scan and import. * src/chunk.c: OP_IMPORT leaves the reader set (vm.h marks it obs:NONE; the sync gate's exemption is spent and removed). chunk_scan_static_loads hands OP_IMPORT targets to the visitor (is_import=1) next to literal load_file targets. The constant-pool string match is replaced by chunk_name_loads_observer_builtin: OP_GET_NAME operands only -- the one binding-load opcode. Verified with EIGS_DUMP_BC: a string literal, dict key or printed literal is CONST; `local r is observe` is GET_NAME; a field access `tbl.eval` is DOT_GET (a field name on a user value -- it armed the keyword-table fixture until the population was narrowed to GET_NAME); `define observe` is a binder. OBS_BUILTINS is unchanged and mirrored name-for-name by OBS_NAMES in compiler.c, where the AST scan needs `report`/`report_value` (the parser spells `report of x` as a relation headed by that IDENT). * src/compiler.c: the eager pass's load list carries a kind; AST_IMPORT is noted instead of arming; import entries resolve through eigs_import_resolve and are parsed and scanned transitively like literal loads (nested imports anchored at the module's own directory, mixed import/load_file chains included); a provider-served module (embed source provider) stays conservative. for_loop_reads_observer keeps its pre-change answer for an import in a loop body (#1062 tier decision). Circular and self imports terminate in the pass (memo + depth cap) and the runtime's existing circular-dependency error is unchanged. * tools/obs_reader_sync_check.sh: EXEMPT="OP_INTERROGATE", SWITCH_FLOOR 16, selftest rows updated (11/11). * docs/OBSERVER.md: "What arms the gate -- the rule, precisely" (five numbered conditions, each executed), import named in the refuse-instead-of-answer section, the stale "compiled twice" residual corrected (parse-only since #1031). Regression test: tests/test_obs_gate_import.sh, suite section [99u+], 19 checks pinned by count (12/19 on the baseline binary): unused stdlib import, string literal, keyword table, alias of observe/eval/classify (still arm), user-defined observe + field named eval (do not), --lint verdict, EIGS_OBS_FORCE, the check-40 invariant on the VALUE (host history visible to an imported reader: diverging), a reader two imports down, project-first shadowing and eigs.json project-root resolution through the shared resolver, an import inside an uncalled function, an unresolvable import (open), a module rewritten between scan and import (raises), a clean project import (runs, closed). Planted faults, each rebuilt and run: OP_IMPORT back in the reader set -> 3 red + sync gate red; CONST matched again -> 4 red; import guard removed -> "silent:equilibrium"; no eager import scan and no guard -> 8 red including `equilibrium` where the truth is `diverging` (the #861 inversion suite check 40 guards). Corpus oracle: tools/observer_gate_diff.sh capture base1/base12 (baseline binary, the determinism reference) and mine2 (this build); compare base1 mine2 -> 497 deterministic programs, 0 mismatches after normalising by hand the two #1115 artifacts of an out-of-tree baseline binary (the exe-dir substring in resolver error text, 5 programs; the project-vs- stdlib import-shadow warning that fires only because the baseline's stdlib is not the worktree's lib/, 2 programs). Raw compare: 7 mismatches, all of that shape; nothing else moved. Suites: release 4249/4249, 0 failed; ASan (detect_leaks=1) full suite, leak tally unchanged (0); freestanding-check OK; embed_stack_soak OK; obs_reader_sync_check + selftest, obs_marker_check + selftest green. Residuals: row B (a load_file'd module with a DEAD read) stays observed -- per-function liveness, out of scope. ouroboros src/frontend.eigs does not lint on v0.43.0 at all (E005 at line 2300, a bare `report`, reserved by #1110); its line-56 keyword table holds the six PREDICATE keywords, which are opcode forms and were never in the name list, so the table never armed anything on either binary -- the arming #1046's comment 5 measured was the two first-class builtin loads at 2300/2308 (`_env_set_local of [env, "observe", observe]`), genuine aliases that must arm. With both patched out the file lints `unobserved` on the baseline AND this build; with only 2300 patched it lints `observed` on both, correctly. Provider-served imports (embed) stay conservative. No chunk is reused between the eager pass and the import: since #1031 the pass parses and scans an AST it then frees, so a literal module is parsed twice and compiled once (#1031 stays open for chunk reuse). Closes #1046 Closes #915 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A8rPrmr564H7yB2tQ6DwV9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1102. Closes #1108.
#1102 —
report/report_valueare reserved observer formsBefore this,
report of <ident>compiled to the observer op whilereport of <expr>called a same-named user function — one name resolving two ways by operand shape, and a userdefine report(...)silently half-shadowed. Now any binding or first-class use ofreport/report_valueis a compile-time error (diagnosticE005) before the unit runs, exactly like the predicate keywords; the operand must be an identifier (parens allowed); observer opcodes and name/slot behavior are unchanged. The rule is enforced once (parser), carried through--lintand the LSP, and the two names are removed from the builtins doc table and the LSP completion index (an editor no longer offers a "builtin" the parser rejects).builtin_reportstays registered — it remains reachable through direct bytecode (vm_run_bytecode), verified.Fixture
tests/test_report_reserved.sh(suite[42a]): 24 binding/read shapes × both names × {file,-e, lint}, operand refusal, every entry road, field keys, formatting, the meta-interpreter bridge — 209 checks. Red on the released runtime (27/209), green here; planting out only the keyword registrations, or neutering the parser error, reds it.#1108 —
-e <source>CLI flageigenscripthad no way to run a source string (python -c/node -e/perl -eall do; its absence was hit twice while testing entry-point coverage).eigenscript -e <source> [args...]runs the string through the file compile/execute path; remaining argv reaches the program; with no file,load_file/importinside it resolve against the working directory (the no-file road from #1056).Two blind-critic rounds (Fable + GPT-6 Astra); both found the feature correct in round 1, round 2 closed a fixture that ignored
$EIGSand the LSP/docs still advertising the reserved names.The self-hosted compiler in ouroboros still mirrors the old behavior (its parity fixture points here) and flips at the next pin bump.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB