Skip to content

Reserve report/report_value as observer forms; add -e CLI flag (#1102, #1108) - #1110

Merged
InauguralPhysicist merged 3 commits into
mainfrom
fix-1102
Sep 6, 2026
Merged

Reserve report/report_value as observer forms; add -e CLI flag (#1102, #1108)#1110
InauguralPhysicist merged 3 commits into
mainfrom
fix-1102

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #1102. Closes #1108.

#1102report / report_value are reserved observer forms

Before this, report of <ident> compiled to the observer op while report of <expr> called a same-named user function — one name resolving two ways by operand shape, and a user define report(...) silently half-shadowed. Now any binding or first-class use of report/report_value is a compile-time error (diagnostic E005) 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 --lint and 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_report stays 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 flag

eigenscript had no way to run a source string (python -c / node -e / perl -e all 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/import inside 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 $EIGS and the LSP/docs still advertising the reserved names.

Release: 4258/4258   ASan (detect_leaks=1): 4247/4247, leak tally 0   LSP: 110   jit-smoke: pass   stdlib index: OK

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

InauguralPhysicist and others added 2 commits September 6, 2026 04:30
#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
Comment thread src/main.c Dismissed
… 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
InauguralPhysicist marked this pull request as ready for review September 6, 2026 12:29
@InauguralPhysicist
InauguralPhysicist merged commit cd99388 into main Sep 6, 2026
20 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix-1102 branch 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants