From d24c0776b017e87179e928dcac19c9d58599e37c Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Sun, 6 Sep 2026 04:30:07 -0500 Subject: [PATCH 1/3] Reserve report/report_value as observer forms (#1102); add -e CLI flag (#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 Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB --- CHANGELOG.md | 12 +++ docs/BUILTINS.md | 15 ++- docs/COMPARISON.md | 5 + docs/DIAGNOSTICS.md | 5 +- docs/GRAMMAR.md | 12 +++ docs/OBSERVER.md | 10 ++ docs/SPEC.md | 23 +++++ docs/STDLIB.md | 6 ++ docs/llms.txt | 12 ++- lib/eigen.eigs | 10 +- src/compiler.c | 3 + src/eigenlsp.c | 5 +- src/eigenscript.c | 10 +- src/eigenscript.h | 5 + src/embed_smoke.c | 29 ++++++ src/lexer.c | 5 + src/lint.c | 7 +- src/lint_host.c | 8 +- src/main.c | 19 +++- src/parser.c | 60 ++++++++++-- tests/run_all_tests.sh | 26 ++++- tests/test_cli.sh | 30 ++++++ tests/test_lint.sh | 4 +- tests/test_lsp.py | 10 ++ tests/test_opaque_fn.eigs | 33 ++++--- tests/test_report_reserved.sh | 174 ++++++++++++++++++++++++++++++++++ 26 files changed, 484 insertions(+), 54 deletions(-) create mode 100644 tests/test_report_reserved.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index cba44969..f757c60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to EigenScript are documented here. ### Breaking changes +- **`report` and `report_value` are reserved observer forms (#1102): they + cannot be bound or used as values, and `report of name` / `report_value of name` + require an identifier operand (parentheses around the identifier are allowed).** + Every binding position now fails before the source unit runs, with parse + diagnostic `E005`, including parameters, loops, comprehensions, destructuring, + catches, and imports. Non-identifier operands also fail with `E005`; assign an + expression to a variable before reporting its trajectory. Name/slot observer + behavior is unchanged, and dict keys may still use these words. The shared + parser enforces the rule for files, REPL units, dynamic loads/eval, and hosts; + lint and LSP carry the same code. CLI `-e [args...]` now accepts a + source string through the file execution path. + - **File resolution is independent of the process working directory (#1056).** `load_file` and `import` search the containing file's directory, the existing `eigs_modules` walk, the nearest `eigs.json` project root, then the stdlib diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index 191f46b1..c27fe6a6 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -269,21 +269,26 @@ Query a binding's assignment history. Always on for top-level bindings; | Name | Signature | Description | |------|-----------|-------------| -| `report` | `report of value` | Classify change trajectory: "improving", "diverging", "stable", "equilibrium", "oscillating", "converged" — or "moving" when a full window matches none of them (#735) | +| `report` | `report of name` | Reserved observer form; requires an identifier (parentheses allowed). Classify change trajectory: "improving", "diverging", "stable", "equilibrium", "oscillating", "converged" — or "moving" when a full window matches none of them (#735) | | `observe` | `observe of value` | Return [status, entropy, dH, prev_dH] snapshot | | `classify` | `classify of t` or `classify of [t, "entropy"]` | Classify a trajectory snapshot (from `trajectory of x`, #421): value-channel label by default, entropy-channel with `"entropy"`. Raises `type_mismatch` on a non-snapshot — a bare value never silently classifies | +**`report` and `report_value` are reserved** (#1102). They cannot be bound or +used as first-class values. Non-identifier operands, including `report of 5` +and `report_value of (x + 0.0)`, are compile-time `E005` errors; assign the +expression to a variable first. Dict keys such as `d.report` remain legal. + **`report`, `report_value`, `observe`, and `trajectory` on a plain variable are observer special forms** (decided in #459): like the predicates and interrogatives, `report of x` / `report_value of x` / `observe of x` / `trajectory of x` are resolved by the compiler to the named *binding's* slot -trajectory — an operation on the name, not the value — so a user rebinding of -these names does not change them (`--lint` W013 warns on the shadowing -attempt). `trajectory of x` (#421) snapshots the slot's observer windows into +trajectory — an operation on the name, not the value. The report words are +reserved; a user rebinding of `observe` or `trajectory` does not change their +name-keyed forms (`--lint` W013 warns on those shadowing attempts). `trajectory of x` (#421) snapshots the slot's observer windows into a plain dict (`kind`/`rel`/`raw`/`dh`/`entropy`/…) that survives a call boundary, for `classify` to read on the other side — the binding slot itself is binding-identity and a passed value arrives with no history. The non-ident -forms (`report of (x + 0.0)`, `observe of expr`) are ordinary calls to the +forms of `observe` / `trajectory` (`observe of expr`) are ordinary calls to the value-path builtins. `dispatch` is deliberately NOT in this set — it is a plain builtin and a user rebinding wins (see Lists above). diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 7e144314..614864d7 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -439,6 +439,11 @@ stops moving, which would make a runaway look like the calmest possible trajectory — so the observer treats the ceiling as evidence of divergence, not of rest (#861). Keep the runaway above going and the verdict holds: +`report` and `report_value` are reserved observer forms: unlike ordinary +functions in Python or JavaScript, they cannot be rebound or passed as values. +Their `of` operand must be a variable name (parentheses allowed); other operands +and all binding attempts fail before the source unit runs with `E005`. + ```eigenscript r is 1.0 i is 0 diff --git a/docs/DIAGNOSTICS.md b/docs/DIAGNOSTICS.md index 7fc2d580..37aaedc4 100644 --- a/docs/DIAGNOSTICS.md +++ b/docs/DIAGNOSTICS.md @@ -248,6 +248,7 @@ a code's meaning never changes, and retired codes are not reused. | `E002` | error | Parse error (parser). `--lint --json` reports the first one. | | `E003` | error | Undefined name — **no binding on any path** (#404). A name is read somewhere but bound nowhere: not by any assignment in any scope, not a parameter/binder (function/lambda params, `for`/comprehension variables, `catch` names, list-pattern names, `import` module names), not a builtin, and not a top-level name of a file pulled in by a literal `load_file` (resolved with the runtime's own resolution chain, transitively). The runtime raises `undefined variable` the moment such a path executes; `E003` surfaces it statically, including on cold branches. See "Name resolution (`E003`)" below for the exact model and escapes. | | `E004` | error | Compile error — the file parses and still cannot be turned into bytecode (`break` outside a loop, an expression past the nesting limit, a constant pool past 65536; the full set is under **Compile errors** above). `--lint` compiles the unit and discards the chunk, so a green lint means *at minimum* that the file builds (#927); `--lint --json` reports the first compile error. Not suppressible, like `E002`: an allow-list applies to lint findings, not to a file the compiler refuses. | +| `E005` | error | Reserved observer form misuse: `report` / `report_value` used as a binding or first-class value, or given a non-identifier operand. Emitted by the parser at the offending word, before any statement in that source unit runs; `--lint --json` and LSP preserve this code and token location. Not suppressible. Use `report of x` / `report_value of x` (parentheses around `x` allowed); assign expressions to a variable first. | | `E100` | error | Uncaught runtime error (category code; see note below). | | `W001` | warning | Unused variable. | | `W002` | warning | Unused function parameter. | @@ -261,13 +262,13 @@ a code's meaning never changes, and retired codes are not reused. | `W010` | warning | Duplicate dict key. | | `W011` | warning | `name is ...` used in a condition (likely meant `==`). | | `W012` | warning | Assignment shadows a builtin. The builtin set is derived from `register_builtins()` itself plus the extension names (`ext_names.h`) — never a hand list (#459: the old hand-copied array was ~120 names behind the binary, so shadowing `dispatch`, `chr`, `eval`, … lint'd clean). | -| `W013` | warning | Function definition shadows a builtin (same registry-derived set as `W012`). For the observer special forms (`report`, `report_value`, `observe`) this warning is load-bearing: their name-keyed forms are compiler-resolved and a rebinding does NOT change them (#459) — the shadowing function is only reachable through non-ident argument forms. | +| `W013` | warning | Function definition shadows a builtin (same registry-derived set as `W012`). For the shadowable observer special forms (`observe`, `trajectory`) this warning is load-bearing: their name-keyed forms are compiler-resolved and a rebinding does NOT change them (#459) — the shadowing function is only reachable through non-ident argument forms. `report` and `report_value` are reserved and instead rejected with `E005`. | | `W014` | warning | Bare trajectory predicate in a loop condition reads the last-observed binding, but the body assigns two or more bindings — name it (` of `). | | `W015` | warning | A function assigns (without `local`) over a module-level **function** name, clobbering it via mutate-outward so later ` of ...` calls fail — add `local` or rename. (Scoped to function clobbering; benign module-variable reuse is not flagged — the general local-discipline fence is tracked by #870. `_`-prefixed names are skipped as intentional module state.) | | `W016` | warning | Bare trajectory predicate **outside a loop condition** (`if stable:`, `ok is converged`, `return diverging`) reads the last-observed binding — an invisible alias (#247/#262) — write ` of `. Loop conditions are exempt: the single-assign `loop while not converged` form is the documented idiom, and the ambiguous multi-assign case is `W014`. Any explicit subject counts as named, including `stable of (x + 0.0)`; deliberate bare reads carry `# lint: allow W016`. | | `W017` | warning | Bare 1-element literal arg list: `f of [x]` passes **one argument** — the element, not the list (#405; the pre-#405 rule meant the opposite, so the form reads ambiguously). Write `f of x` for one argument, or `f of ([x])` (#355) to pass a 1-element list. Doubles as the #405 migration audit: `--lint` over a consumer repo surfaces every behavior-changed call site. | | `W018` | warning | A `catch`-bound error's `.kind` is compared (`==`/`!=`) against a string that is a **near-miss** of a real kind — a case variant (`"IO"`) or a single-character typo (`"index_rage"`), or a kind renamed out from under the handler — so the branch is dead code that silently never fires (#469). Kinds are a closed set (below). Zero-false-positive by construction: only near-misses of a closed kind fire, and only off a catch-bound variable — an exactly-valid kind, and a genuinely custom `throw {kind: "..."}` value many edits from every builtin, both stay silent. | -| `W019` | warning | An interrogative used as a **bare statement** — `why is "..."`, `what is x`, `prev of y` at statement level — evaluates and **discards** its result: a silent no-op (#583). Question words (`what/who/when/where/why/how`) cannot be assigned with `is` — the "assignment" is the interrogative expression form — so when a same-named binding exists in scope the statement is almost certainly a mistaken assignment (the real hit: a catch handler "reassigning" a `local why` that silently kept its stale value). An interrogative inside an expression (`print of (why is x)`, `r is prev of y`) is never flagged. Extended by #736 to the observer **query** forms over an ident — `report of x`, `report_value of x`, `observe of x`, `trajectory of x` at statement level — which are the same silent no-op through the other door: they print nothing and raise nothing, so silence is indistinguishable from "the observer had nothing to say". Zero false positives by construction: over an ident these are compiler-resolved special forms that never reach a user function even when one shadows the name (#459, see `W013`), and a non-ident argument (`report of (x + 0.0)`) is an ordinary call the check never sees. | +| `W019` | warning | An interrogative used as a **bare statement** — `why is "..."`, `what is x`, `prev of y` at statement level — evaluates and **discards** its result: a silent no-op (#583). Question words (`what/who/when/where/why/how`) cannot be assigned with `is` — the "assignment" is the interrogative expression form — so when a same-named binding exists in scope the statement is almost certainly a mistaken assignment (the real hit: a catch handler "reassigning" a `local why` that silently kept its stale value). An interrogative inside an expression (`print of (why is x)`, `r is prev of y`) is never flagged. Extended by #736 to the observer **query** forms over an ident — `report of x`, `report_value of x`, `observe of x`, `trajectory of x` at statement level — which are the same silent no-op through the other door: they print nothing and raise nothing, so silence is indistinguishable from "the observer had nothing to say". Zero false positives by construction: over an ident these are compiler-resolved special forms that never reach a user function (`report` and `report_value` are reserved; the other names retain their special forms even when shadowed, #459). Non-ident operands of `report` / `report_value` are `E005` errors; non-ident operands of `observe` / `trajectory` are ordinary calls this check never sees. | | `W020` | warning | An `unobserved:` block in which **every** assignment targets a dict field or list element (`d.k is ...`, `xs[i] is ...`) — a provable no-op (#655). Observer bookkeeping is gated on the named env path, so a dict field is never observed and there is nothing to skip; the in-place numeric mutation the block used to enable became unconditional with NaN-boxing B-3a (`dict_set_cached_immediate`), so it costs nothing to drop the block. This shape was true when written and expired silently, which is why it needs a lint — our own README shipped the dead form as its headline example for two months. Conservative by construction: `g_unobserved_depth` is a global, so a **call** inside the block runs the callee unobserved too and suppresses the warning; any plain-variable assignment, or a name-binding form (`for` / listcomp / `catch` / `match`), also suppresses it. Only a provably inert block fires. | | `W021` | hint | Function definition shadows a **public stdlib function** from a module the file never imported (`define 'median' shadows lib/stats.eigs 'median' (import stats to use it)`) — a discoverability nudge toward `lib/*.eigs` (#591), sibling of `W013` (which covers compiled-in builtins; a name that is both stays `W013`-only). The name table is scraped from the public top-level defines of the same `lib/` directories the import resolver searches; the hint stays silent when the module is imported, and when the linted file *is* the module that ships the name. Name-only matching has false positives (a deliberately-different local `mean`), so this is hint-severity: advisory, **never fails `--lint`** under either `--lint-level`, and suppressible like any other code. | | `W022` | warning | A bare literal argument list with **more elements than the callee's parameters** — with `define two(a, b)`, `two of [1, 2, 99]` passes 3 arguments to a 2-parameter callee (#733). Since #974 the runtime raises a catchable `value`-kind error at that call site (`call passes 3 arguments but the callee takes 2`); this warning catches the same mistake earlier, statically, without running the program. Conservative by construction, and **same-file only**: it fires only when the callee name provably has one meaning in the file — exactly one `define` of it anywhere and no other binding (assignment, param, lambda param, loop/comprehension var, catch name, list-pattern name, import, or any identifier inside a `match` pattern poisons the name), so a call into an imported module's callee is never checked here — cross-module over-arity reaches the runtime raise instead. One-parameter callees are exempt by the #405 semantics themselves: a 2+-element bare list binds WHOLE to a single parameter — nothing is dropped, and that shape is the deliberate variadic idiom (`reverse of [1, 2, 3]`). Parenthesized lists (`f of ([...])`) are a single argument and never fire. | diff --git a/docs/GRAMMAR.md b/docs/GRAMMAR.md index d415bd03..9c9604d7 100644 --- a/docs/GRAMMAR.md +++ b/docs/GRAMMAR.md @@ -78,6 +78,18 @@ ordinary identifiers everywhere else. The six question words above are soft in the same way — `what` not followed by `is` parses as an identifier. +### Reserved Observer Forms + +``` +report report_value +``` + +These words cannot be identifiers or binding names (`E005`). Their expression +form is `('report' | 'report_value') 'of' identifier_operand`, where +`identifier_operand = IDENT | '(' identifier_operand ')'`. Other operand shapes +are `E005` errors; `of` retains normal precedence. Like every word keyword, +these words are allowed as dict keys after a dot. + ### Observer Predicates ``` diff --git a/docs/OBSERVER.md b/docs/OBSERVER.md index 27c8a5f0..7b7f9bd4 100644 --- a/docs/OBSERVER.md +++ b/docs/OBSERVER.md @@ -136,6 +136,16 @@ you put the thresholds — see [Resolution](#resolution). coverage, not the value's motion — the same visible-gap rule as `moving`. +### Reserved report forms + +`report` and `report_value` are reserved words, never binding names or +first-class functions. Use `report of x` / `report_value of x` with a named +variable; parentheses around that name are allowed. Any binding of either word, +or a non-identifier operand (literal, arithmetic, call, index, field, or argument +list), is rejected at compile time with `E005`. Assign an expression to a +variable first so there is a binding history to inspect. Dict fields such as +`d.report` remain legal. See [SPEC](SPEC.md#observer-semantics-and-predicates) for the language rule. + ### Two signals: entropy vs. value (`report` vs. `report_value`) **Since #861 the predicate words and `report` ROUTE: numeric bindings diff --git a/docs/SPEC.md b/docs/SPEC.md index 6a797754..6a521924 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1292,6 +1292,29 @@ iterations pass without certification (a runaway pinned at the saturation ceiling, sub-deadband drift); `__loop_exit__` records which one happened. +`report` and `report_value` are **reserved observer forms**, like the +predicate keywords: neither may be a binding name (including function names, +parameters, `local`, loop/comprehension variables, destructuring targets, +`catch` names, or an `import` module name). They are not first-class values. +Misuse is a compile-time parse error **`E005`**, before any statement in that +source unit executes. The rule also applies to REPL input, `eval`, `load_file`, +`import`, the embedding API, and `--lint`; the CLI accepts source strings with +`eigenscript -e ''`. + +Both forms require an **identifier operand**, optionally parenthesized: +`report of x`, `report of (x)`, and `report_value of ((x))` query the same +binding history as before. Literals, arithmetic expressions, calls, indexing, +field access, and literal argument lists (`[]`, `[x]`, `[x, y]`) are rejected +with `E005` and “requires a variable name operand”. Assign an expression to a +variable first; a temporary value has no named assignment history. This also +replaces `report`'s old non-identifier fallback (`equilibrium`, or `opaque` for +a function) and `report_value`'s undefined-name error. `of` precedence is +unchanged: `report of x + "!"` appends to the report of `x`. + +As with other keywords, quoted dict keys and dot fields remain legal: +`d.report` and `d.report_value` are data fields, not bindings of reserved names. +`match` cases compare expressions; they do not introduce binders. + **`report of x`** names the most specific band true of the same trajectory the predicates read (value channel for numerics, entropy otherwise — #861), resolving `oscillating` → `diverging` → `improving` → diff --git a/docs/STDLIB.md b/docs/STDLIB.md index 3aeb3612..8a82b829 100644 --- a/docs/STDLIB.md +++ b/docs/STDLIB.md @@ -964,6 +964,12 @@ print of msg # "World is running v0.5" ### lib/eigen.eigs — Meta-Circular Interpreter +The meta-interpreter's `report` bridge classifies values without the host's +binding trajectories: it returns `equilibrium` for ordinary values and `opaque` +for functions. Since #1102 a fresh-parameter wrapper uses the reserved host +syntax to preserve that existing fallback; the meta-interpreter remains a +separate, partial implementation of the language. + | Function | Signature | Description | |----------|-----------|-------------| | `eigen_tokenize` | `eigen_tokenize of source` | Tokenize source string into token list | diff --git a/docs/llms.txt b/docs/llms.txt index cc384205..d7bd9872 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -77,7 +77,11 @@ not any outer `n`. ## Reserved and soft keywords (cannot be plain variable names) - **Reserved** (never identifiers): `in`, and the observer predicates - `converged`, `stable`, `improving`, `diverging`, `oscillating`, `equilibrium`. + `converged`, `stable`, `improving`, `diverging`, `oscillating`, `equilibrium`, + plus `report` and `report_value`. Never bind either report name, even as a + parameter or loop variable (`E005`). Use `report of x` / `report_value of x` + with an identifier (parentheses allowed); expressions/arg lists are `E005` + errors, so bind the expression first. Dict fields `d.report` remain legal. - **Soft** (`prev`, `at`, and the six question words `what who when where why how`): usable as params/loop-vars/catch-names, but you can NEVER bind one with plain `is` (`what is e` is the interrogative form, a silent no-op as an @@ -104,9 +108,9 @@ print of ("sqrt(2) = " + (str of (newton_sqrt of 2))) # sqrt(2) = 1.4142135623 - `loop while not converged` uses the **bare** predicate reading the loop's last-assigned value. Put it **inside a function** so the loop gets a fresh binding to watch (a module-scope version can loop forever). -- `report of x` returns the entropy-channel state string; `report_value of x` - the value-channel state (use the value channel for convergence vs divergence — - the entropy channel calls a monotonically exploding value `converged`). +- `report of x` returns the routed state string (value channel for numeric + bindings, entropy channel otherwise); `report_value of x` uses the value + channel explicitly. Both read the named binding's trajectory. - `converged` needs LOW absolute entropy on top of a settled window: a value pinned at `5.0` reports `stable`, never `converged`; pinned at `0.0`/`1.0` it converges. Don't guess the predicate — see docs/PREDICATES.md. diff --git a/lib/eigen.eigs b/lib/eigen.eigs index 6d1bc60b..bd4ea970 100644 --- a/lib/eigen.eigs +++ b/lib/eigen.eigs @@ -1696,9 +1696,9 @@ define _eval_block(stmts, env) as: define _make_default_env as: env is _env_new of null - # Bridge ALL C builtins into the meta-interpreter's environment. + # Bridge the C builtins into the meta-interpreter's environment. # Since we're running inside the C runtime, these names resolve to - # VAL_BUILTIN values. The evaluator's "call" handler calls them + # VAL_BUILTIN values (report uses the wrapper below). The "call" handler calls them # with `fn of arg` which the C runtime handles natively. _env_set_local of [env, "print", print] _env_set_local of [env, "write", write] @@ -1708,7 +1708,11 @@ define _make_default_env as: _env_set_local of [env, "num", num] _env_set_local of [env, "append", append] _env_set_local of [env, "type", type] - _env_set_local of [env, "report", report] + # #1102: report is reserved syntax, so it cannot be stored as a value. + # This fresh-parameter wrapper preserves this interpreter's existing + # value-only bridge: no trajectory -> equilibrium, functions -> opaque. + # It does not expose the host's named binding history to the meta env. + _env_set_local of [env, "report", (v) => report of v] _env_set_local of [env, "assert", assert] _env_set_local of [env, "throw", throw] _env_set_local of [env, "keys", keys] diff --git a/src/compiler.c b/src/compiler.c index 270f8462..bfbec22a 100644 --- a/src/compiler.c +++ b/src/compiler.c @@ -2715,6 +2715,9 @@ static void compile_node_inner(Compiler *c, ASTNode *node) { emit_op_u16_u16(c, OP_PREDICATE_NAME, pkind, (uint16_t)pnidx, node->line); break; } + /* #1102: the parser reserves the report words and validates + * their identifier operand before any unit reaches compilation. + * Keep these name/slot emissions and runtime semantics unchanged. */ if (fn_node && fn_node->type == AST_IDENT && strcmp(fn_node->data.ident.name, "report") == 0 && arg_node && arg_node->type == AST_IDENT) { diff --git a/src/eigenlsp.c b/src/eigenlsp.c index 3148bc5b..266e84a9 100644 --- a/src/eigenlsp.c +++ b/src/eigenlsp.c @@ -738,8 +738,9 @@ static void send_diagnostics(Document *doc) { strbuf_append_fmt(&sb, "%d", err_col); strbuf_append(&sb, "},\"end\":{\"line\":"); strbuf_append_fmt(&sb, "%d", err_line); - strbuf_append_fmt(&sb, ",\"character\":%d}},\"severity\":1,\"code\":\"E002\"," - "\"source\":\"eigenscript\",\"message\":", err_end); + strbuf_append_fmt(&sb, ",\"character\":%d}},\"severity\":1,\"code\":\"%s\"," + "\"source\":\"eigenscript\",\"message\":", err_end, + g_first_error_code ? g_first_error_code : "E002"); json_escape_to(&sb, full); strbuf_append_char(&sb, '}'); } else if (doc->ast) { diff --git a/src/eigenscript.c b/src/eigenscript.c index 7284ac74..7171ff28 100644 --- a/src/eigenscript.c +++ b/src/eigenscript.c @@ -50,7 +50,8 @@ * four siblings (#955) — per-context, so two EigsStates on one thread * cannot cross-read known-ness. */ -void eigs_record_first_error_at(int line, int col, int len, const char *msg) { +void eigs_record_first_error_code_at(int line, int col, int len, + const char *code, const char *msg) { int candidate_col_known = col >= 0 && len > 0; if (g_first_error_line > 0) { if (line <= 0 || line > g_first_error_line) return; @@ -59,6 +60,7 @@ void eigs_record_first_error_at(int line, int col, int len, const char *msg) { col >= g_first_error_col) return; } } + g_first_error_code = code; g_first_error_line = line; g_first_error_col = candidate_col_known ? col : 0; g_first_error_len = len; @@ -66,6 +68,10 @@ void eigs_record_first_error_at(int line, int col, int len, const char *msg) { snprintf(g_first_error_msg, sizeof(g_first_error_msg), "%s", msg ? msg : "syntax error"); } +void eigs_record_first_error_at(int line, int col, int len, const char *msg) { + eigs_record_first_error_code_at(line, col, len, "E002", msg); +} + void eigs_record_first_error(int line, const char *msg) { eigs_record_first_error_at(line, -1, 0, msg); } @@ -191,6 +197,8 @@ const char* tok_type_name(TokType t) { case TOK_IN: return "'in'"; case TOK_NULL: return "'null'"; case TOK_UNOBSERVED: return "'unobserved'"; + case TOK_REPORT: return "'report'"; + case TOK_REPORT_VALUE: return "'report_value'"; case TOK_LOCAL: return "'local'"; case TOK_PLUS: return "'+'"; case TOK_MINUS: return "'-'"; diff --git a/src/eigenscript.h b/src/eigenscript.h index 6fd6b94a..f9336cb8 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -132,6 +132,7 @@ typedef enum { TOK_TRY, TOK_CATCH, TOK_BREAK, TOK_CONTINUE, TOK_IMPORT, TOK_MATCH, TOK_CASE, TOK_UNOBSERVED, + TOK_REPORT, TOK_REPORT_VALUE, TOK_LOCAL, TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH, TOK_PERCENT, TOK_LT, TOK_GT, TOK_LE, TOK_GE, TOK_EQ, TOK_NE, TOK_ASSIGN, @@ -727,6 +728,7 @@ struct EigsThread { * set this instead of printing when the VM is live. */ int error_print_pending; char error_msg[4096]; + const char *first_error_code; /* stable parse diagnostic, normally E002 */ char first_error_msg[256]; struct Value *error_value; /* thrown payload for structured catch */ /* #406: structured runtime errors. Kind (ErrKind), 1-based line, and @@ -947,6 +949,7 @@ extern __thread EigsThread *eigs_current; #define g_first_error_col_known (eigs_current->first_error_col_known) #define g_error_print_pending (eigs_current->error_print_pending) #define g_error_msg (eigs_current->error_msg) +#define g_first_error_code (eigs_current->first_error_code) #define g_first_error_msg (eigs_current->first_error_msg) #define g_error_value (eigs_current->error_value) #define g_error_kind (eigs_current->error_kind) @@ -1640,6 +1643,8 @@ void vm_print_stack_trace(FILE *out); /* uncaught-error call stack (vm.c); no-o int vm_current_line(void); /* live source line (vm.c); 0 without a VM */ void eigs_record_first_error(int line, const char *msg); void eigs_record_first_error_at(int line, int col, int len, const char *msg); +void eigs_record_first_error_code_at(int line, int col, int len, + const char *code, const char *msg); /* #407: one-line source excerpt + `^` caret under `col` (0-based), the * shared format for parse-time and runtime diagnostics. No-op when src is * NULL or the position is out of range. */ diff --git a/src/embed_smoke.c b/src/embed_smoke.c index c54c105f..31efa435 100644 --- a/src/embed_smoke.c +++ b/src/embed_smoke.c @@ -196,6 +196,35 @@ int main(void) { CHECK(r != NULL && eigs_value_as_num(r) == 100.0, "eval still works after error"); eigs_value_release(r); + /* #1102: source reservation is shared by the embedding parser. A + * rejected unit must not execute its earlier assignment, and another + * eval must recover with no stale diagnostic code. */ + { + const char *names[] = {"report", "report_value"}; + for (int i = 0; i < 2; i++) { + char source[192]; + snprintf(source, sizeof(source), + "embed_reserved_ran is 1\ndefine f(\n %s\n) as:\n return 0\n", + names[i]); + r = eigs_eval_string(source); + CHECK(r == NULL && g_parse_errors > 0, + "reserved observer parameter rejected by embed eval"); + CHECK(g_first_error_line == 3 && g_first_error_code && + strcmp(g_first_error_code, "E005") == 0 && + strstr(g_first_error_msg, "reserved observer form"), + "embed reserved diagnostic code and offending line"); + eigs_value_release(r); + EigsValue *ran = eigs_get_global("embed_reserved_ran"); + CHECK(!ran || eigs_value_type(ran) == EIGS_TYPE_NULL, + "embed rejected unit executes no earlier statement"); + eigs_value_release(ran); + r = eigs_eval_string("1 + 2"); + CHECK(r && eigs_value_as_num(r) == 3.0 && g_first_error_line == 0, + "embed recovers after reserved observer error"); + eigs_value_release(r); + } + } + /* --- FFI: register a C function, call from script. --------------- */ eigs_register_function("host_add", host_add); r = eigs_eval_string("host_add of [3, 4]"); diff --git a/src/lexer.c b/src/lexer.c index 17466787..6e28faab 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -84,6 +84,8 @@ static TokType keyword_type(const char *word) { if (strcmp(word, "prev") == 0) return TOK_PREV; break; case 'r': + if (strcmp(word, "report") == 0) return TOK_REPORT; + if (strcmp(word, "report_value") == 0) return TOK_REPORT_VALUE; if (strcmp(word, "return") == 0) return TOK_RETURN; break; case 's': @@ -158,6 +160,8 @@ const char* tok_base_string(TokType t) { case TOK_MATCH: return "match "; case TOK_CASE: return "case "; case TOK_UNOBSERVED: return "unobserved "; + case TOK_REPORT: return "report "; + case TOK_REPORT_VALUE:return "report_value "; case TOK_LOCAL: return "local "; case TOK_PLUS: return "+ "; case TOK_MINUS: return "- "; @@ -217,6 +221,7 @@ static TokenList tokenize_at_line(const char *source, int initial_line, int init * this document's diagnostic. Nested f-string tokenization bumps * g_tokenize_depth, so only reset at the outermost pass. */ if (g_tokenize_depth == 0) { + g_first_error_code = "E002"; g_first_error_line = 0; g_first_error_col = 0; g_first_error_len = 0; diff --git a/src/lint.c b/src/lint.c index 496b2046..502f6fac 100644 --- a/src/lint.c +++ b/src/lint.c @@ -756,9 +756,10 @@ static void check_builtin_shadow(ASTNode *node, LintContext *ctx) { * printing nothing and raising nothing. That is the worst affordance the * observer has: silence is indistinguishable from "nothing to say", and the * bare form is what issue bodies and READMEs reach for. Zero false positives by - * construction: over an ident these names never reach a user function even when - * one shadows them (#459, see W013), and a non-ident argument (`report of (x + - * 0.0)`) is an ordinary call this check never sees. */ + * construction: over an ident these names never reach a user function. The + * report words are reserved (#1102); observe/trajectory retain their special + * forms even when shadowed (#459, W013). Non-ident report operands are parse + * errors; non-ident observe/trajectory operands are calls this check never sees. */ static const char *disc_observer_query(ASTNode *node) { static const char *names[] = {"report", "report_value", "observe", "trajectory"}; diff --git a/src/lint_host.c b/src/lint_host.c index 48bacbdc..104ebf83 100644 --- a/src/lint_host.c +++ b/src/lint_host.c @@ -1262,12 +1262,14 @@ int eigenscript_lint(const char *path, int json_mode, int fail_on_warning) { lint_json_escape(g_first_error_msg[0] ? g_first_error_msg : "parse error", esc, sizeof(esc)); lint_json_escape(path, pesc, sizeof(pesc)); - printf("[{\"code\":\"E002\",\"severity\":\"error\",\"line\":%d," + printf("[{\"code\":\"%s\",\"severity\":\"error\",\"line\":%d," "\"column\":%d,\"file\":\"%s\",\"message\":\"%s\"}]\n", + g_first_error_code ? g_first_error_code : "E002", g_first_error_line, g_first_error_col + 1, pesc, esc); } else { - fprintf(stderr, "%s: %d parse error(s) [E002] — cannot lint\n", - path, g_parse_errors); + fprintf(stderr, "%s: %d parse error(s) [%s] — cannot lint\n", + path, g_parse_errors, + g_first_error_code ? g_first_error_code : "E002"); } free_ast(ast); free_tokenlist(&tl); diff --git a/src/main.c b/src/main.c index 996e9ccc..7309837c 100644 --- a/src/main.c +++ b/src/main.c @@ -86,6 +86,7 @@ int main(int argc, char **argv) { "\n" "Usage:\n" " eigenscript [args...] run a script (args readable via `args of null`)\n" + " eigenscript -e [args...] run a source string\n" " eigenscript start the REPL\n" " eigenscript --fmt [--write] format a source file (stdout, or rewrite with --write)\n" " eigenscript --lint [--json] [--lint-level error|warning] \n" @@ -292,6 +293,17 @@ int main(int argc, char **argv) { return repl_exit_code; } + /* Source strings use the same parse/compile/execute path as files. + * Keep argv[1] as the source identity and strip only the source argument + * from the script-visible args; relative loads start at the caller cwd. */ + int source_string = strcmp(argv[1], "-e") == 0; + if (source_string && argc < 3) { + fprintf(stderr, "Usage: eigenscript -e [args...]\n"); + eigs_thread_detach(); + eigs_state_destroy(eigs_st); + return 1; + } + /* Extract script directory for load_file resolution. g_script_dir * is an EigsState bridge macro — state is already attached above. */ { @@ -301,7 +313,8 @@ int main(int argc, char **argv) { } long src_size = 0; - char *source = read_file_util(argv[1], &src_size); + char *source = source_string ? xstrdup(argv[2]) + : read_file_util(argv[1], &src_size); if (!source) { fprintf(stderr, "Error: cannot read file '%s'\n", argv[1]); eigs_thread_detach(); @@ -309,6 +322,10 @@ int main(int argc, char **argv) { return 1; } + if (source_string) { + for (int i = 2; i + 1 < argc; i++) argv[i] = argv[i + 1]; + argv[--argc] = NULL; + } srand(time(NULL)); eigenscript_set_args(argc, argv); diff --git a/src/parser.c b/src/parser.c index 307883b5..91be54df 100644 --- a/src/parser.c +++ b/src/parser.c @@ -161,8 +161,34 @@ static void p_end_statement(Parser *p) { p_match(p, TOK_NEWLINE); } +/* #1102: one reservation rule for every parser entry point. These word + * tokens are never identifiers; only the observer-call and dot-key grammar + * consume them successfully. Keep the diagnostic on the name token, including + * multiline parameter lists and patterns. */ +static int tok_is_report(TokType type) { + return type == TOK_REPORT || type == TOK_REPORT_VALUE; +} + +static int p_report_error(Token *t, int operand) { + if (!tok_is_report(t->type)) return 0; + char msg[192]; + snprintf(msg, sizeof(msg), "'%s' is a reserved observer form; %s", + t->str_val, operand ? "requires a variable name operand" + : "use it with 'of variable', never as a binding"); + fprintf(stderr, "Parse error line %d:%d: %s [E005]\n", + t->line, t->col + 1, msg); + eigs_record_first_error_code_at(t->line, t->col, t->len, "E005", msg); + p_print_caret(t->line, t->col); + g_parse_errors++; + return 1; +} + static void p_expect(Parser *p, TokType type) { if (p_cur(p)->type != type) { + if (p_report_error(p_cur(p), 0)) { + p_advance(p); + return; + } fprintf(stderr, "Parse error line %d:%d: expected %s, got %s", p_cur(p)->line, p_cur(p)->col + 1, tok_type_name(type), tok_type_name(p_cur(p)->type)); if (p_cur(p)->str_val) fprintf(stderr, " ('%s')", p_cur(p)->str_val); @@ -912,6 +938,15 @@ static ASTNode* parse_primary(Parser *p) { return make_node(AST_NULL, p_cur(p)->line); } + if (tok_is_report(t->type)) { + p_advance(p); + if (p_cur(p)->type != TOK_OF) p_report_error(t, 0); + ASTNode *n = make_node_col(AST_IDENT, t->line, t->col); + n->data.ident.name = xstrdup(t->str_val); + set_name_hash(n, n->data.ident.name); + return n; + } + if (t->type == TOK_IDENT) { p_advance(p); ASTNode *n = make_node_col(AST_IDENT, t->line, t->col); @@ -926,10 +961,10 @@ static ASTNode* parse_primary(Parser *p) { int is_lambda = 0; p_advance(p); /* skip ( */ /* Scan forward: if we see IDENT [, IDENT]* ) => then it's a lambda */ - if (tok_is_ident_like(p_cur(p)->type) || p_cur(p)->type == TOK_RPAREN) { + if (tok_is_ident_like(p_cur(p)->type) || tok_is_report(p_cur(p)->type) || p_cur(p)->type == TOK_RPAREN) { int scan = p->pos; while (scan < p->tl->count && - (tok_is_ident_like(p->tl->tokens[scan].type) || p->tl->tokens[scan].type == TOK_COMMA)) + (tok_is_ident_like(p->tl->tokens[scan].type) || tok_is_report(p->tl->tokens[scan].type) || p->tl->tokens[scan].type == TOK_COMMA)) scan++; if (scan + 1 < p->tl->count && p->tl->tokens[scan].type == TOK_RPAREN && p->tl->tokens[scan+1].type == TOK_ARROW) @@ -942,7 +977,8 @@ static ASTNode* parse_primary(Parser *p) { char **params = xmalloc_array(MAX_PARAMS, sizeof(char*)); int param_count = 0; int lambda_cap_reported = 0; - while (tok_is_ident_like(p_cur(p)->type)) { + while (tok_is_ident_like(p_cur(p)->type) || tok_is_report(p_cur(p)->type)) { + p_report_error(p_cur(p), 0); if (param_count >= MAX_PARAMS) { /* #354: one loud diagnostic, then drain (see match). */ if (!lambda_cap_reported) { @@ -1158,6 +1194,7 @@ static int chain_too_deep(Parser *p) { } static ASTNode* parse_relation(Parser *p) { + Token *callee = p_cur(p); ASTNode *left = parse_primary(p); if (p_cur(p)->type == TOK_OF) { @@ -1168,6 +1205,8 @@ static ASTNode* parse_relation(Parser *p) { * absorbing trailing infix arithmetic: `len of xs - 1` now * parses as `(len of xs) - 1`, not `len of (xs - 1)`. */ ASTNode *right = parse_unary(p); + if (tok_is_report(callee->type) && right && right->type != AST_IDENT) + p_report_error(callee, 1); ASTNode *n = make_node_col(AST_RELATION, op_tok->line, op_tok->col); n->data.relation.left = left; n->data.relation.right = right; @@ -1448,7 +1487,8 @@ static ASTNode* parse_statement_inner(Parser *p) { params = xmalloc_array(MAX_PARAMS, sizeof(char*)); defaults = xcalloc(MAX_PARAMS, sizeof(ASTNode*)); int param_cap_reported = 0; - while (tok_is_ident_like(p_cur(p)->type)) { + while (tok_is_ident_like(p_cur(p)->type) || tok_is_report(p_cur(p)->type)) { + p_report_error(p_cur(p), 0); if (param_count >= MAX_PARAMS) { /* #354: one loud diagnostic, then drain (see match). */ if (!param_cap_reported) { @@ -1790,11 +1830,13 @@ static ASTNode* parse_statement_inner(Parser *p) { char *names_tmp[64]; for (;;) { if (p_cur(p)->type != TOK_IDENT) { - fprintf(stderr, - "Parse error line %d: destructuring pattern requires " - "identifiers (index/field targets like a[0] or a.x " - "are not supported)\n", p_cur(p)->line); - g_parse_errors++; + if (!p_report_error(p_cur(p), 0)) { + fprintf(stderr, + "Parse error line %d: destructuring pattern requires " + "identifiers (index/field targets like a[0] or a.x " + "are not supported)\n", p_cur(p)->line); + g_parse_errors++; + } for (int k = 0; k < n; k++) free(names_tmp[k]); while (p_cur(p)->type != TOK_NEWLINE && p_cur(p)->type != TOK_EOF) p_advance(p); diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index b45522a5..45574c93 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -2045,6 +2045,23 @@ else fi echo "" +# #1102: reserved observer forms. Require the complete fixture population as +# well as its exit status; a partial run cannot silently reduce this section. +echo "[42a] Reserved observer forms (#1102)" +REPORT_OUT=$(bash "$TESTS_DIR/test_report_reserved.sh" 2>&1); REPORT_RC=$? +REPORT_PASS=$(echo "$REPORT_OUT" | grep -c "^PASS:" || true) +REPORT_FAIL=$(echo "$REPORT_OUT" | grep -c "^FAIL:" || true) +TOTAL=$((TOTAL + 1)) +if [ "$REPORT_RC" -eq 0 ] && [ "$REPORT_PASS" -eq 209 ] && [ "$REPORT_FAIL" -eq 0 ]; then + PASS=$((PASS + 1)) + echo " PASS: all $REPORT_PASS reserved observer checks" +else + FAIL=$((FAIL + 1)) + echo " FAIL: reserved observer forms (rc=$REPORT_RC, $REPORT_PASS/209 checks passed)" + echo "$REPORT_OUT" | tail -20 +fi +echo "" + # #971: strict math mode (EIGS_STRICT) — domain ops raise instead of clamping. echo "Strict math mode (EIGS_STRICT domain-op raises)" SM_OUTPUT=$(bash "$TESTS_DIR/test_strict_math.sh" 2>&1) @@ -3822,13 +3839,14 @@ check "gate CLOSES on a program with no observer surface" "$OBS_G1" "1" # 3. And OPEN on a direct observer surface. OBS_G2=$(EIGS_OBS_GATE_STATS=1 $EIGS_BIN "$TESTS_DIR/test_observer_level_set.eigs" 2>&1 | grep -c 'obs-gate: observed') check "gate OPENS on a direct observer surface" "$OBS_G2" "1" -# 4. And OPEN on the INDIRECT form. `local r is report` emits NO reader opcode — +# 4. And OPEN on the INDIRECT form. `local r is observe` emits NO reader opcode — # it compiles to GET_NAME + CALL — so this passes only because the scan also # matches observer-read builtin names in the constant pool. An opcode-only -# scan reports "unobserved" here and silently breaks every aliased report. -printf 'x is 1.0\nlocal r is report\nx is 2.0\nprint of (r of x)\n' > "$OBS_GATE_TMP/alias.eigs" +# scan reports "unobserved" here and silently breaks aliased observer reads. +# #1102: report is now reserved; observe still exercises the same mechanism. +printf 'x is 1.0\nlocal r is observe\nx is 2.0\nprint of (r of x)\n' > "$OBS_GATE_TMP/alias.eigs" OBS_G3=$(EIGS_OBS_GATE_STATS=1 $EIGS_BIN "$OBS_GATE_TMP/alias.eigs" 2>&1 | grep -c 'obs-gate: observed') -check "gate OPENS on an aliased report (no reader opcode emitted)" "$OBS_G3" "1" +check "gate OPENS on an aliased observe (no reader opcode emitted)" "$OBS_G3" "1" # 5. The escape hatch, which is also the baseline arm for perf work: ONE # byte-identical binary serves both arms, so a measurement cannot be # confounded by a second build. diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 8f229a63..b17088e4 100755 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -180,6 +180,36 @@ else fail "CLI15 div-zero raises" "rc=$RC out='$OUT'" fi +# ---- #1102: source-string execution shares the file compilation path ---- +OUT=$("$EIGS" -e 'print of (1 + 2)' 2>&1) +RC=$? +if [ "$RC" -eq 0 ] && [ "$OUT" = "3" ]; then + ok "CLI16 -e executes a source string" +else + fail "CLI16 -e executes" "rc=$RC out='$OUT'" +fi +OUT=$("$EIGS" -e 'print of (len of (args of null))' first second 2>&1) +RC=$? +if [ "$RC" -eq 0 ] && [ "$OUT" = "2" ]; then + ok "CLI17 -e passes script args without source text" +else + fail "CLI17 -e args" "rc=$RC out='$OUT'" +fi +OUT=$("$EIGS" -e 'exit of 7' 2>&1) +RC=$? +if [ "$RC" -eq 7 ] && [ -z "$OUT" ]; then + ok "CLI18 -e preserves explicit exit status" +else + fail "CLI18 -e exit" "rc=$RC out='$OUT'" +fi +OUT=$("$EIGS" -e 2>&1) +RC=$? +if [ "$RC" -eq 1 ] && [ "$OUT" = "Usage: eigenscript -e [args...]" ]; then + ok "CLI19 -e missing source is a usage error" +else + fail "CLI19 -e missing source" "rc=$RC out='$OUT'" +fi + # ---- Summary ---- echo "" echo "CLI: $PASS passed, $FAIL failed" diff --git a/tests/test_lint.sh b/tests/test_lint.sh index 0263736d..97c622e3 100644 --- a/tests/test_lint.sh +++ b/tests/test_lint.sh @@ -468,14 +468,14 @@ TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) cat > "$TMPFILE" << 'EIGS' define dispatch(a, b, c) as: return 999 -define report(v) as: +define observe(v) as: return v chr is 7 print of "hi" EIGS OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) check_contains "W013 fires on define dispatch (#459)" "$OUTPUT" "W013.*'dispatch'" -check_contains "W013 fires on define report (observer special form)" "$OUTPUT" "W013.*'report'" +check_contains "W013 fires on define observe (observer special form)" "$OUTPUT" "W013.*'observe'" check_contains "W012 fires on a registry-only builtin (chr)" "$OUTPUT" "W012.*'chr'" rm -f "$TMPFILE" diff --git a/tests/test_lsp.py b/tests/test_lsp.py index 601f8c79..8eb5d303 100755 --- a/tests/test_lsp.py +++ b/tests/test_lsp.py @@ -174,6 +174,16 @@ def main(): check("diagnostic severity is error (1)", bool(d) and d[0]["severity"] == 1) check("diagnostic mentions expected colon", bool(d) and "expected ':'" in d[0]["message"]) + # #1102: parser code and exact offending token survive LSP transport. + for name in ("report", "report_value"): + source = "define f(\n " + name + "\n) as:\n return 0\n" + d = diagnostics(converse([INIT, did_open(source), SHUTDOWN, EXIT])) + check(name + " reserved observer error code/range (#1102)", + bool(d) and d[0].get("code") == "E005" and d[0]["severity"] == 1 + and "reserved observer form" in d[0]["message"] + and d[0]["range"]["start"] == {"line": 1, "character": 4} + and d[0]["range"]["end"] == {"line": 1, "character": 4 + len(name)}) + # --- unexpected character → diagnostic naming the char --- r = converse([INIT, did_open("x is @\n"), SHUTDOWN, EXIT]) d = diagnostics(r) diff --git a/tests/test_opaque_fn.eigs b/tests/test_opaque_fn.eigs index 4a57f9d3..6f55effc 100644 --- a/tests/test_opaque_fn.eigs +++ b/tests/test_opaque_fn.eigs @@ -5,6 +5,8 @@ # itself is unchanged: containers and numeric trajectories measure as # before; this file pins both halves. +load_file of "lib/test.eigs" + define a() as: return 1 define b(p, q, r) as: @@ -13,38 +15,39 @@ define b(p, q, r) as: # ---- report/report_value/predicates/observe on a rebound fn binding ---- f is a f is b -assert of [(report of f) == "opaque", "OP01 report of a fn binding is opaque"] -assert of [(report_value of f) == "opaque", "OP02 report_value is opaque too"] -assert of [(equilibrium of f) == 0, "OP03 equilibrium predicate is false"] -assert of [(converged of f) == 0, "OP04 converged predicate is false"] -assert of [(stable of f) == 0, "OP05 stable predicate is false"] +assert_true of [(report of f) == "opaque", "OP01 report of a fn binding is opaque"] +assert_true of [(report_value of f) == "opaque", "OP02 report_value is opaque too"] +assert_true of [(equilibrium of f) == 0, "OP03 equilibrium predicate is false"] +assert_true of [(converged of f) == 0, "OP04 converged predicate is false"] +assert_true of [(stable of f) == 0, "OP05 stable predicate is false"] obs is observe of f -assert of [obs[0] == "opaque", "OP06 observe band is opaque"] +assert_true of [obs[0] == "opaque", "OP06 observe band is opaque"] # ---- a function named directly, and a builtin binding ---- -assert of [(report of a) == "opaque", "OP07 report of a define'd name is opaque"] +assert_true of [(report of a) == "opaque", "OP07 report of a define'd name is opaque"] g is print -assert of [(report of g) == "opaque", "OP08 a builtin binding is opaque"] +assert_true of [(report of g) == "opaque", "OP08 a builtin binding is opaque"] -# ---- the value-operand fallback path (no binding) ---- +# ---- extract a value to a binding before reporting (#1102) ---- hs is [a] -assert of [(report of hs[0]) == "opaque", "OP09 fn value operand is opaque"] +held_fn is hs[0] +assert_true of [(report of held_fn) == "opaque", "OP09 extracted fn binding is opaque"] # ---- bare predicate reads the last-observed binding ---- f2 is a f2 is b -assert of [equilibrium == 0, "OP10 bare predicate on a fn binding is false"] +assert_true of [equilibrium == 0, "OP10 bare predicate on a fn binding is false"] # ---- the check is ask-time: rebinding to a number answers again ---- f is 42 -assert of [((report of f) == "opaque") == 0, "OP11 number rebind is not opaque"] +assert_true of [((report of f) == "opaque") == 0, "OP11 number rebind is not opaque"] # ---- numeric bindings and containers are untouched ---- n is 1 n is 2 n is 3 -assert of [((report of n) == "opaque") == 0, "OP12 numeric binding classifies"] +assert_true of [((report of n) == "opaque") == 0, "OP12 numeric binding classifies"] d is {"h": a} -assert of [((report of d) == "opaque") == 0, "OP13 container holding a fn is not opaque"] +assert_true of [((report of d) == "opaque") == 0, "OP13 container holding a fn is not opaque"] -print of "All tests passed" +test_summary of null diff --git a/tests/test_report_reserved.sh b/tests/test_report_reserved.sh new file mode 100644 index 00000000..fcb9643c --- /dev/null +++ b/tests/test_report_reserved.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# #1102: language-level reservation, through the real compile entry points. +# Every rejection checks status, diagnostic identity/location, and absence of +# executed statements. Controls ensure the source templates are valid programs. +set -eu +TESTS_DIR="$(cd "$(dirname "$0")" && pwd)" +python3 - "$TESTS_DIR/../src/eigenscript" <<'PY' +import json +import pathlib +import re +import subprocess +import sys +import tempfile + +binary = str(pathlib.Path(sys.argv[1]).resolve()) +passed = failed = 0 + +def check(label, ok, result=None): + global passed, failed + if ok: + passed += 1 + print('PASS: ' + label) + else: + failed += 1 + print('FAIL: ' + label) + if result is not None: + print(f' rc={result.returncode} stdout={result.stdout[:180]!r} stderr={result.stderr[:300]!r}') + +def run(args, stdin=None): + return subprocess.run([binary, *args], input=stdin, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=work, timeout=30) + +def clean(r): + return not re.search(r'AddressSanitizer|LeakSanitizer|runtime error:|UndefinedBehaviorSanitizer', r.stdout + r.stderr) + +# Tuple = construct, valid source with NAME substituted, offending source line. +# Anchored to parser binding families: FUNC, LAMBDA, ASSIGN/LOCAL, FOR, +# TRY, LISTCOMP, destructuring, IMPORT; match patterns are expression +# reads, not binders. Additional +# shapes exercise lookahead, multiline positions, and nonexecuted branches. +shapes = [ + ('define', 'define NAME(v) as:\n return "mine"\nx is 1\nx is 2\nprint of (NAME of x)\nprint of (NAME of 5)\n', 1), + ('implicit parameter define', 'define NAME as:\n return n\n', 1), + ('assignment', 'NAME is 5\n', 1), + ('local', 'local NAME is 5\n', 1), + ('compound assignment', 'NAME += 1\n', 1), + ('parameter', 'define f(NAME) as:\n return 0\n', 1), + ('second parameter', 'define f(a, NAME) as:\n return 0\n', 1), + ('default parameter', 'define f(NAME is 3) as:\n return 0\n', 1), + ('multiline parameter', 'define f(\n a,\n NAME\n) as:\n return 0\n', 3), + ('lambda parameter', 'f is (NAME) => 0\n', 1), + ('second lambda parameter', 'f is (a, NAME) => 0\n', 1), + ('multiline lambda parameter', 'f is (a,\n NAME) => 0\n', 2), + ('for binder', 'for NAME in [1, 2]:\n 0\n', 1), + ('catch binder', 'try:\n 0\ncatch NAME:\n 0\n', 3), + ('list match name', 'match [1, 2]:\n case [NAME, tail]:\n 0\n', 2), + ('second list match name', 'match [1, 2]:\n case [head, NAME]:\n 0\n', 2), + ('comprehension binder', 'xs is [0 for NAME in [1, 2]]\n', 1), + ('filtered comprehension binder', 'xs is [0 for NAME in [1, 2] if 1]\n', 1), + ('destructure first', '[NAME, tail] is [1, 2]\n', 1), + ('destructure second', '[head, NAME] is [1, 2]\n', 1), + ('import binder', 'import NAME\n', 1), + ('cold branch', 'if 0:\n NAME is 5\n', 2), + ('nested local', 'define f(x) as:\n local NAME is 5\n', 2), + ('unobserved', 'unobserved:\n NAME is 5\n', 2), +] + +with tempfile.TemporaryDirectory(prefix='eigs_report_reserved_') as tmp: + work = pathlib.Path(tmp) + (work / 'user_report.eigs').write_text('value is 1\n') + source_file = work / 'program.eigs' + for label, template, line in shapes: + control = template.replace('NAME', 'user_report') + if 'list match name' in label: + control = 'user_report is 1\nhead is 1\ntail is 2\n' + control + if label == 'compound assignment': + control = 'user_report is 0\n' + control + source_file.write_text(control) + r = run([str(source_file)]) + check('control ' + label, r.returncode == 0 and clean(r), r) + for name in ('report', 'report_value'): + src = 'print of "EXECUTED"\n' + template.replace('NAME', name) + source_file.write_text(src) + for mode, args in [('file', [str(source_file)]), ('-e', ['-e', src]), + ('lint', ['--lint', '--json', str(source_file)])]: + r = run(args) + ok = r.returncode == 1 and clean(r) + if mode == 'lint': + try: + ds = json.loads(r.stdout) + ok &= len(ds) == 1 and ds[0]['code'] == 'E005' and ds[0]['severity'] == 'error' + ok &= ds[0]['line'] == line + 1 and f"'{name}' is a reserved observer form" in ds[0]['message'] + except (ValueError, KeyError, TypeError, IndexError): + ok = False + else: + ok &= r.stdout == '' and re.search(rf'^Parse error line {line + 1}:\d+: \'{name}\' is a reserved observer form.*\[E005\]$', r.stderr, re.M) is not None + check(f'{name}: {label} / {mode}', ok, r) + + # All RHS shapes other than an optionally parenthesized identifier are + # errors, including bare call arg lists at every cardinality. + for name in ('report', 'report_value'): + for operand in ('5', '(x + 1)', 'x[0]', 'd.key', '[x]', '[]', '[x, x]', + '([x])', '(len of x)', '"literal"', 'null', '((x + 0))'): + src = f'print of "EXECUTED"\nx is [1, 2]\nd is {{"key": 1}}\nprint of ({name} of {operand})\n' + r = run(['-e', src]) + check(f'{name}: operand {operand}', r.returncode == 1 and r.stdout == '' and clean(r) + and f"'{name}' is a reserved observer form; requires a variable name operand" in r.stderr + and '[E005]' in r.stderr, r) + for expr in (name, f'({name})', f'5 |> {name}'): + r = run(['-e', f'print of "EXECUTED"\nf is {expr}\n']) + check(f'{name}: cannot take first-class form {expr}', r.returncode == 1 and r.stdout == '' and clean(r) + and 'reserved observer form' in r.stderr and '[E005]' in r.stderr, r) + + inner = f'print of "EXECUTED"\n{name} is 4\n' + (work / 'badmodule.eigs').write_text(inner) + for label, outer in [('eval', 'eval of ' + json.dumps(inner)), + ('load_file', 'load_file of "badmodule.eigs"'), + ('import', 'import badmodule')]: + source_file.write_text(outer + '\n') + r = run([str(source_file)]) + check(f'{name}: {label}', r.returncode == 1 and r.stdout == '' and clean(r) + and re.search(rf'^Parse error line 2:\d+: \'{name}\' is a reserved observer form.*\[E005\]$', r.stderr, re.M) is not None, r) + # REPL accepts another unit after a rejected unit (its exit status is + # 0 for other reserved-word syntax errors too). + r = run([], f'{name} is 4\nprint of "RECOVERED"\nexit\n') + check(f'{name}: REPL rejection and recovery', r.returncode == 0 and clean(r) + and 'reserved observer form' in r.stderr and '[E005]' in r.stderr + and 'RECOVERED' in r.stdout, r) + + # Pin the unchanged name/slot operands, precedence and opaque band. Fields + # named after keywords are still data keys, never lexical bindings. + valid = '''x is 1 +x is 2 +print of (report of x) +print of (report_value of x) +print of (report of (x)) +print of (report_value of ((x))) +print of (report of x + "!") +define f(v) as: + v is 1 + v is 2 + print of (report of v) + print of (report_value of v) +f of 0 +print of (report of f) +print of (report_value of f) +d is {"report": 7, "report_value": 8} +print of d.report +print of d.report_value +d.report is 9 +d.report_value += 2 +print of d.report +print of d.report_value +print of f"{report of x}:{report_value of x}" +load_file of "lib/eigen.eigs" +print of (eigen_run of "report of 5") +print of (eigen_run of "report of print") +''' + expected = 'moving\nmoving\nmoving\nmoving\nmoving!\nmoving\nmoving\nopaque\nopaque\n7\n8\n9\n10\nmoving:moving\nequilibrium\nopaque\n' + source_file.write_text(valid) + r = run([str(source_file)]) + check('identifier operands and field keys', r.returncode == 0 and r.stdout == expected and clean(r), r) + r = run(['--fmt', str(source_file)]) + ok = r.returncode == 0 and clean(r) + source_file.write_text(r.stdout) + rr = run([str(source_file)]) + check('formatted observer forms run unchanged', ok and rr.returncode == 0 and rr.stdout == expected and clean(rr), rr) + rr = run(['--fmt', str(source_file)]) + check('formatter is idempotent', ok and rr.returncode == 0 and rr.stdout == r.stdout and clean(rr), rr) + +print(f'RESULTS: {passed}/{passed + failed} passed, {failed} failed (reserved observer forms)') +sys.exit(bool(failed)) +PY From 695c3af85f3e1f5417d76b11fd006b6d2d9b4824 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Sun, 6 Sep 2026 06:27:33 -0500 Subject: [PATCH 2/3] Honor report fixture binary overrides and exclude reserved completions (#1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB --- .gitignore | 1 - CHANGELOG.md | 3 + Makefile | 6 +- docs/BUILTINS.md | 9 +- src/builtins.c | 12 +- src/lsp_builtin_index.h | 347 ++++++++++++++++++++++++++++++++ tests/test_lsp.py | 10 + tests/test_report_reserved.sh | 3 +- tests/test_vm_run_bytecode.eigs | 5 + tools/gen_lsp_builtin_index.sh | 21 +- 10 files changed, 400 insertions(+), 17 deletions(-) create mode 100644 src/lsp_builtin_index.h diff --git a/.gitignore b/.gitignore index 5a1a0110..e6ae5e62 100644 --- a/.gitignore +++ b/.gitignore @@ -48,7 +48,6 @@ tests/tmp_mod_373*.eigs build/ # Generated LSP stdlib index (make lsp / build.sh lsp, #590) src/lsp_stdlib_index.h -src/lsp_builtin_index.h src/eigsdap # Python bytecode caches. tests/test_doc_examples.py is IMPORTED (not just diff --git a/CHANGELOG.md b/CHANGELOG.md index f757c60c..9594e370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ All notable changes to EigenScript are documented here. parser enforces the rule for files, REPL units, dynamic loads/eval, and hosts; lint and LSP carry the same code. CLI `-e [args...]` now accepts a source string through the file execution path. + The builtin reference table and LSP Function completions omit both forms. + The VM retains the `report` builtin for existing bytecode that resolves + its name through `vm_run_bytecode`; its value-only behavior is unchanged. - **File resolution is independent of the process working directory (#1056).** `load_file` and `import` search the containing file's directory, the existing diff --git a/Makefile b/Makefile index 2114c2a5..642ef500 100644 --- a/Makefile +++ b/Makefile @@ -284,11 +284,11 @@ $(SRC_DIR)/lsp_stdlib_index.h: $(wildcard lib/*.eigs) tools/gen_lsp_stdlib_index bash tools/gen_lsp_stdlib_index.sh # Builtin half of the same idea (#742): names from the registration seams + -# ext_names.h, hover detail from the signature comments. Also a build -# artifact, never committed. +# ext_names.h, hover detail from the signature comments. Reserved report +# forms are excluded using lexer.c. Commit the generated header for review. $(SRC_DIR)/lsp_builtin_index.h: $(SRC_DIR)/builtins.c $(SRC_DIR)/builtins_host.c \ $(SRC_DIR)/hash.c $(SRC_DIR)/ext_store.c $(SRC_DIR)/ext_names.h \ - tools/gen_lsp_builtin_index.sh + $(SRC_DIR)/lexer.c tools/gen_lsp_builtin_index.sh bash tools/gen_lsp_builtin_index.sh # Real file targets (#825): rebuilt when their sources, any header, the diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index c27fe6a6..ea246e51 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -269,7 +269,6 @@ Query a binding's assignment history. Always on for top-level bindings; | Name | Signature | Description | |------|-----------|-------------| -| `report` | `report of name` | Reserved observer form; requires an identifier (parentheses allowed). Classify change trajectory: "improving", "diverging", "stable", "equilibrium", "oscillating", "converged" — or "moving" when a full window matches none of them (#735) | | `observe` | `observe of value` | Return [status, entropy, dH, prev_dH] snapshot | | `classify` | `classify of t` or `classify of [t, "entropy"]` | Classify a trajectory snapshot (from `trajectory of x`, #421): value-channel label by default, entropy-channel with `"entropy"`. Raises `type_mismatch` on a non-snapshot — a bare value never silently classifies | @@ -277,6 +276,14 @@ Query a binding's assignment history. Always on for top-level bindings; used as first-class values. Non-identifier operands, including `report of 5` and `report_value of (x + 0.0)`, are compile-time `E005` errors; assign the expression to a variable first. Dict keys such as `d.report` remain legal. +See [OBSERVER.md](OBSERVER.md) for their trajectory classifications. + +The VM retains the old `report` registry entry for bytecode compatibility: +`vm_run_bytecode` can still resolve the string `"report"` with `GET_NAME` +and `CALL` it, returning `"equilibrium"` for data or `"opaque"` for a callable. +This entry is absent from the source builtin table and LSP Function completions; +it does not make `report` a callable name in source. `report_value` has no +runtime builtin registration. **`report`, `report_value`, `observe`, and `trajectory` on a plain variable are observer special forms** (decided in #459): like the predicates and diff --git a/src/builtins.c b/src/builtins.c index 94d57c35..016ff18c 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -584,13 +584,11 @@ Value* builtin_append(Value *arg) { Value* builtin_report(Value *arg) { - /* #262 Step E: observer trajectories live on the Env slot, never on the - * Value. `report of ` is a slot-keyed special form (REPORT_SLOT/ - * REPORT_NAME); this builtin is reached only for a value-based operand with - * no binding (a computed expr, or an unobserved param), which has no - * trajectory → the no-observation band, "equilibrium". - * #708: a function/builtin operand answers "opaque" here too, matching - * the slot-keyed forms — a fn has no content the observer can sample. */ + /* #1102: source report forms are reserved and always slot-keyed. Retain + * this registry entry for vm_run_bytecode's GET_NAME("report") + CALL; + * tests/test_vm_run_bytecode.eigs pins that compatibility path. Values + * have no binding trajectory (#262), hence "equilibrium"; callables + * remain "opaque" (#708). The LSP excludes this entry from completions. */ if (arg && (arg->type == VAL_FN || arg->type == VAL_BUILTIN)) return make_str("opaque"); return make_str("equilibrium"); diff --git a/src/lsp_builtin_index.h b/src/lsp_builtin_index.h new file mode 100644 index 00000000..d2ea1aa3 --- /dev/null +++ b/src/lsp_builtin_index.h @@ -0,0 +1,347 @@ +/* Generated by tools/gen_lsp_builtin_index.sh — DO NOT EDIT (#742). + * Names come from the registration seams + ext_names.h; hover text + * from the `name of ...` signature comment above each definition. + * Reserved report forms are excluded using the lexer keyword table. + * Regenerated by the Makefile lsp target and build.sh lsp. */ +static const char *builtin_docs[][2] = { + {"abs", "abs — builtin; see docs/BUILTINS.md"}, + {"acos", "acos — builtin; see docs/BUILTINS.md"}, + {"add", "add — builtin; see docs/BUILTINS.md"}, + {"append", "append — builtin; see docs/BUILTINS.md"}, + {"arena_mark", "arena_mark of null — saves current arena position. All Values allocated"}, + {"arena_reset", "arena_reset of null — reclaims all Values allocated since the last arena_mark."}, + {"arena_stats", "arena_stats of null — returns total bytes allocated through the arena."}, + {"args", "args of null → list of command-line arguments (after the script name)."}, + {"asin", "asin — builtin; see docs/BUILTINS.md"}, + {"assert", "assert — builtin; see docs/BUILTINS.md"}, + {"atan", "atan — builtin; see docs/BUILTINS.md"}, + {"atan2", "atan2 — builtin; see docs/BUILTINS.md"}, + {"audio_capture_close", "audio_capture_close of null — stop and close the recording device."}, + {"audio_capture_open", "audio_capture_open of [freq, channels] — open the recording device"}, + {"audio_capture_read", "audio_capture_read of null — drain accumulated samples since the"}, + {"audio_clear", "audio_clear of null — stop all mixer channels and free their clips"}, + {"audio_close", "audio_close of null"}, + {"audio_envelope", "audio_envelope of [samples, attack, decay, sustain_level, release] — ADSR"}, + {"audio_gain", "audio_gain of [samples, volume] — scale and clamp"}, + {"audio_mix", "audio_mix of [samples_a, samples_b] — add and clamp"}, + {"audio_music_play", "audio_music_play of [path, loops] — stream a music file (mp3/ogg/wav) via"}, + {"audio_music_stop", "audio_music_stop of null — halt and free the current track."}, + {"audio_music_volume", "audio_music_volume of v — music volume 0..128"}, + {"audio_noise", "audio_noise of [duration, amplitude] — white noise"}, + {"audio_open", "audio_open of [freq, channels] — open the mixer device (callback mode)"}, + {"audio_pause", "audio_pause of flag — 1=pause, 0=unpause"}, + {"audio_play", "audio_play of samples — convert float list [-1,1] to int16, queue"}, + {"audio_play_loop", "audio_play_loop of [samples, loops] — play `samples` `loops` times on"}, + {"audio_queue_size", "audio_queue_size of null — bytes queued"}, + {"audio_saw", "audio_saw of [freq, duration, amplitude] — sawtooth wave"}, + {"audio_sine", "audio_sine of [freq, duration, amplitude] — generate sine wave samples"}, + {"audio_square", "audio_square of [freq, duration, amplitude] — square wave"}, + {"audio_stop", "audio_stop of channel — stop one mixer channel. Returns 1, or 0 on a"}, + {"audio_stream_clear", "audio_stream_clear of null — drop any buffered audio on the live"}, + {"audio_stream_close", "audio_stream_close of null — stop and close the live stream device."}, + {"audio_stream_open", "audio_stream_open of [freq, channels] — open the live streaming"}, + {"audio_stream_push", "audio_stream_push of samples — queue a block of float samples [-1, 1]"}, + {"audio_stream_queued", "audio_stream_queued of null — samples still buffered (not yet played)"}, + {"audio_sweep", "audio_sweep of [freq_start, freq_end, duration, amplitude, waveform]"}, + {"audio_volume", "audio_volume of [channel, vol] — live per-channel volume, 0.0..4.0"}, + {"bit_and", "bit_and — builtin; see docs/BUILTINS.md"}, + {"bit_not", "bit_not — builtin; see docs/BUILTINS.md"}, + {"bit_or", "bit_or — builtin; see docs/BUILTINS.md"}, + {"bit_shl", "bit_shl — builtin; see docs/BUILTINS.md"}, + {"bit_shr", "bit_shr — builtin; see docs/BUILTINS.md"}, + {"bit_xor", "bit_xor — builtin; see docs/BUILTINS.md"}, + {"buf_copy", "buf_copy of [src, src_off, dst, dst_off, count] — bulk copy between buffers."}, + {"buf_deinterleave", "buf_deinterleave of [src, channel, nch, count?] — every nch-th sample"}, + {"buf_dot", "buf_dot of [a, b, a_off, b_off, count] — windowed dot product:"}, + {"buf_fill", "buf_fill of [b, off, count, value] — bulk store over a window:"}, + {"buf_from_list", "buf_from_list of list — convert list of numbers to buffer"}, + {"buf_from_pcm16le", "buf_from_pcm16le of [bytes, byte_off, count] — decode `count`"}, + {"buf_get", "buf_get of [buf, index] — O(1) indexed read"}, + {"buf_len", "buf_len of buf — return buffer length"}, + {"buf_mix", "buf_mix of [dst, src, dst_off, src_off, count, gain] —"}, + {"buf_peak", "buf_peak of [b, off, count] — max |x| over a window (normalize and"}, + {"buf_resample_linear", "buf_resample_linear of [src, dst_len] — endpoint-inclusive linear"}, + {"buf_scale_range", "buf_scale_range of [b, off, count, gain] — in-place multiply over a"}, + {"buf_set", "buf_set of [buf, index, value] — O(1) indexed write"}, + {"buf_to_pcm16le", "buf_to_pcm16le of [floats, off, count] — encode `count` samples from"}, + {"buffer", "buffer of count — create a zero-filled numeric buffer"}, + {"build_corpus", "build_corpus of [file_list, top_n, stream_path, vocab_path]"}, + {"ceil", "ceil — builtin; see docs/BUILTINS.md"}, + {"channel", "channel — builtin; see docs/BUILTINS.md"}, + {"channel_closed", "channel_closed — builtin; see docs/BUILTINS.md"}, + {"char_at", "char_at of [string, index] → single character as string, or \"\" if out of range."}, + {"chdir", "chdir of \"path\" → 1 on success, 0 on failure"}, + {"chr", "chr of n → single-character string from ASCII code"}, + {"classify", "classify — builtin; see docs/BUILTINS.md"}, + {"clear_math_flags", "clear_math_flags of null"}, + {"clock_unix", "clock_unix of null — seconds since the Unix epoch from CLOCK_REALTIME"}, + {"close_channel", "close_channel — builtin; see docs/BUILTINS.md"}, + {"coalesce", "coalesce of [value, default] — returns value unless empty/null"}, + {"concat", "concat of [list_a, list_b] → new 1D list with a's elements then b's"}, + {"contains", "contains — builtin; see docs/BUILTINS.md"}, + {"copy_into", "copy_into of [dest, dest_offset, src]"}, + {"cos", "cos — builtin; see docs/BUILTINS.md"}, + {"db_connect", "db_connect — db extension builtin; see docs/BUILTINS.md"}, + {"db_execute", "db_execute — db extension builtin; see docs/BUILTINS.md"}, + {"db_query_json", "db_query_json — db extension builtin; see docs/BUILTINS.md"}, + {"db_query_value", "db_query_value — db extension builtin; see docs/BUILTINS.md"}, + {"deflate", "deflate — builtin; see docs/BUILTINS.md"}, + {"dict_remove", "dict_remove — builtin; see docs/BUILTINS.md"}, + {"dict_set", "dict_set — builtin; see docs/BUILTINS.md"}, + {"dispatch", "dispatch of [table, key, arg] — O(1) function dispatch."}, + {"divide", "divide — builtin; see docs/BUILTINS.md"}, + {"dot", "dot — builtin; see docs/BUILTINS.md"}, + {"eigen_checkpoint_info", "eigen_checkpoint_info — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_eval_loss", "eigen_eval_loss — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_generate", "eigen_generate — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_model_info", "eigen_model_info — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_model_load", "eigen_model_load — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_model_loaded", "eigen_model_loaded — model extension builtin; see docs/BUILTINS.md"}, + {"eigen_model_save_binary", "eigen_model_save_binary — model extension builtin; see docs/BUILTINS.md"}, + {"ends_with", "ends_with — builtin; see docs/BUILTINS.md"}, + {"env_get", "env_get — builtin; see docs/BUILTINS.md"}, + {"eval", "eval of code_string — execute EigenScript code and return result"}, + {"exe_path", "exe_path of null → absolute path of the running interpreter binary."}, + {"exec_capture", "exec_capture of [\"cmd\", \"arg1\", ...] → [exit_code, stdout_text]"}, + {"exit", "exit of N — request a clean process exit with code N (default 0). Sets the"}, + {"exp", "exp — builtin; see docs/BUILTINS.md"}, + {"f64_from_bytes", "f64_from_bytes of → the decoded double."}, + {"f64_to_bytes", "f64_to_bytes of x → list of 8 ints: the big-endian IEEE-754 double encoding"}, + {"file_exists", "file_exists — builtin; see docs/BUILTINS.md"}, + {"fill", "fill of [count, value] — create a list of `count` elements all set to `value`."}, + {"floor", "floor — builtin; see docs/BUILTINS.md"}, + {"flush", "flush of null — flush stdout"}, + {"free_val", "free_val of value → frees a heap-allocated Value tree. Returns null."}, + {"gather", "gather of [tensor, indices, dim] → select elements at indices along last dim"}, + {"get_at", "get_at of [list, index] or get_at of [list, row, col]"}, + {"get_observer_thresholds", "get_observer_thresholds — builtin; see docs/BUILTINS.md"}, + {"getcwd", "getcwd of null → current working directory as string"}, + {"gfx_circle", "gfx_circle of [cx, cy, radius, r, g, b] — filled circle via midpoint"}, + {"gfx_clear", "gfx_clear of [r, g, b]"}, + {"gfx_clip", "gfx_clip of [x, y, w, h] — set render clip rectangle."}, + {"gfx_close", "gfx_close of null"}, + {"gfx_delay", "gfx_delay of ms"}, + {"gfx_fb", "gfx_fb of [buffer, width, height, x, y, scale]"}, + {"gfx_line", "gfx_line of [x1, y1, x2, y2, r, g, b]"}, + {"gfx_open", "gfx_open of [width, height, title]"}, + {"gfx_point", "gfx_point of [x, y, r, g, b]"}, + {"gfx_poll", "gfx_poll of null — return next event as dict, or null if none."}, + {"gfx_present", "gfx_present of null — flip buffer to screen"}, + {"gfx_read", "gfx_read of [x, y] — read back one rendered pixel as [r, g, b]."}, + {"gfx_rect", "gfx_rect of [x, y, w, h, r, g, b] or [x, y, w, h, r, g, b, a]"}, + {"gfx_rrect", "gfx_rrect of [x, y, w, h, radius, r, g, b] or [..., a]"}, + {"gfx_text", "gfx_text of [x, y, text, r, g, b] or [x, y, text, r, g, b, scale]"}, + {"gfx_text_height", "gfx_text_height of scale? (number, [scale], or null → 1) — pixel line"}, + {"gfx_text_width", "gfx_text_width of [text, scale?] (or of \"text\") — pixel width of `text`"}, + {"gfx_ticks", "gfx_ticks of null — milliseconds since SDL_Init"}, + {"gfx_title", "gfx_title of \"new title\""}, + {"has_key", "has_key — builtin; see docs/BUILTINS.md"}, + {"heap_inuse", "heap_inuse of null — bytes currently in use by the C allocator"}, + {"hex", "hex of n → uppercase hex string, minimal digits (\"0\" for 0)."}, + {"hmac_sha256", "hmac_sha256 — builtin; see docs/BUILTINS.md"}, + {"http_cors", "http_cors of origin — configure CORS. Pass \"*\" for wildcard, null to disable."}, + {"http_early_bind", "http_early_bind — http extension builtin; see docs/BUILTINS.md"}, + {"http_post", "http_post of [url, headers_json, body_string] -> response body string"}, + {"http_request_body", "http_request_body — http_request extension builtin; see docs/BUILTINS.md"}, + {"http_request_headers", "http_request_headers of null → raw request headers as string."}, + {"http_route", "http_route — http extension builtin; see docs/BUILTINS.md"}, + {"http_route_authed", "http_route_authed — http extension builtin; see docs/BUILTINS.md"}, + {"http_serve", "http_serve — http extension builtin; see docs/BUILTINS.md"}, + {"http_session_id", "http_session_id — http_request extension builtin; see docs/BUILTINS.md"}, + {"http_static", "http_static — http extension builtin; see docs/BUILTINS.md"}, + {"index_of", "index_of of [haystack, needle] → first index, or -1. Non-string operands"}, + {"inflate", "inflate — builtin; see docs/BUILTINS.md"}, + {"is_dir", "is_dir of path — 1 if path names a directory, 0 for a plain file, a"}, + {"is_file", "is_file — builtin; see docs/BUILTINS.md"}, + {"join", "join of [list, separator] — concatenate list elements into a string."}, + {"json_build", "json_build of [key1, val1, key2, val2, ...] — properly escaped JSON object"}, + {"json_decode", "json_decode — builtin; see docs/BUILTINS.md"}, + {"json_encode", "json_encode — builtin; see docs/BUILTINS.md"}, + {"json_path", "json_path of [json_string, \"dot.path\"] -> value as string, or \"\""}, + {"json_raw", "json_raw — builtin; see docs/BUILTINS.md"}, + {"keys", "keys — builtin; see docs/BUILTINS.md"}, + {"leaky_relu", "leaky_relu of tensor → element-wise max(0.01*x, x). Works on 1D or 2D."}, + {"len", "len — builtin; see docs/BUILTINS.md"}, + {"list_contains", "list_contains of [list, value] — 1 if any element structurally equals"}, + {"list_index_of", "list_index_of of [list, value] — index of the first element structurally"}, + {"list_insert_at", "list_insert_at of [list, index, value] — insert value at index, shift tail"}, + {"list_remove_at", "list_remove_at of [list, index] — remove element at index, shift tail down."}, + {"list_slice", "list_slice of [list, start, end] → new list with the elements of [start, end)."}, + {"list_truncate", "list_truncate of [list, new_len] — shrink list in-place to new_len items."}, + {"load_file", "load_file — builtin; see docs/BUILTINS.md"}, + {"log", "log of 1e-15 answered ln(1e-10) with no flag, a silent plateau that a"}, + {"log_softmax", "log_softmax — builtin; see docs/BUILTINS.md"}, + {"ls", "ls of \"path\" → list of filenames in directory, or [] on failure."}, + {"math_flags", "math_flags — builtin; see docs/BUILTINS.md"}, + {"matmul", "matmul — builtin; see docs/BUILTINS.md"}, + {"max", "max — builtin; see docs/BUILTINS.md"}, + {"md5", "md5 — builtin; see docs/BUILTINS.md"}, + {"md5_file", "md5_file — builtin; see docs/BUILTINS.md"}, + {"mean", "mean — builtin; see docs/BUILTINS.md"}, + {"min", "min — builtin; see docs/BUILTINS.md"}, + {"mkdir", "mkdir of \"path\" → 1 on success, 0 on failure. Creates parents."}, + {"mktemp", "mktemp of null → path to a new temporary file"}, + {"model_load_weights", "model_load_weights — model extension builtin; see docs/BUILTINS.md"}, + {"model_save_weights", "model_save_weights — model extension builtin; see docs/BUILTINS.md"}, + {"monotonic_ms", "monotonic_ms of null — milliseconds from CLOCK_MONOTONIC"}, + {"monotonic_ns", "monotonic_ns of null — nanoseconds from CLOCK_MONOTONIC"}, + {"multiply", "multiply — builtin; see docs/BUILTINS.md"}, + {"must_not_yield", "must_not_yield of fn — run `fn of null` as an ATOMIC critical section and"}, + {"native_train_step_builtin", "native_train_step_builtin — model extension builtin; see docs/BUILTINS.md"}, + {"nearest_in_range", "nearest_in_range of [entities, x, y, range, world_w, world_h, px_key, py_key, active_key]"}, + {"nearest_in_range_all", "nearest_in_range_all of [entities, range, world_w, world_h, px_key?, py_key?, active_key?]"}, + {"negative", "negative — builtin; see docs/BUILTINS.md"}, + {"net_accept", "net_accept of listener_id"}, + {"net_close", "net_close of handle_id → null. Deterministic and untraced: closing is"}, + {"net_dial", "net_dial of [host, port]"}, + {"net_listen", "net_listen of port → listener handle id, or null when the bind/listen"}, + {"net_port", "net_port of listener_id → the locally bound port (the kernel's pick"}, + {"net_recv", "net_recv of [conn_id, max_bytes]"}, + {"net_send", "net_send of [conn_id, data] — data is a string or a buffer/list of"}, + {"norm", "norm of a → L2 (Euclidean) norm = sqrt(sum_i a[i]^2). Buffers and tensors."}, + {"num", "num — builtin; see docs/BUILTINS.md"}, + {"num_copy", "num_copy of val → fresh heap-allocated copy of a numeric Value."}, + {"numerical_grad", "numerical_grad of [loss_fn, param, eps]"}, + {"numerical_grad_cols", "numerical_grad_cols of [loss_fn, matrix, col_indices, eps]"}, + {"numerical_grad_rows", "numerical_grad_rows of [loss_fn, matrix, row_indices, eps]"}, + {"observe", "observe — builtin; see docs/BUILTINS.md"}, + {"ord", "ord of s → first byte of s as integer (0..255), or -1 on empty / non-string"}, + {"path_base", "path_base of \"a/b/c.txt\" → \"c.txt\""}, + {"path_dir", "path_dir of \"a/b/c.txt\" → \"a/b\""}, + {"path_ext", "path_ext of \"a/b/c.txt\" → \".txt\""}, + {"path_join", "path_join of [a, b] → \"a/b\""}, + {"pi", "pi — builtin; see docs/BUILTINS.md"}, + {"pow", "pow — builtin; see docs/BUILTINS.md"}, + {"ppu_render_frame", "ppu_render_frame of [mem_buf, fb_buf]"}, + {"print", "print — builtin; see docs/BUILTINS.md"}, + {"proc_close", "proc_close of fd → null (idempotent on EBADF)"}, + {"proc_read", "proc_read of [out_fd, max_bytes] → string (raw bytes; NUL-truncates) | null EOF"}, + {"proc_read_buf", "proc_read_buf of [out_fd, max_bytes] → VAL_BUFFER (binary-safe) | null EOF"}, + {"proc_read_line", "proc_read_line of out_fd → string (no trailing \\n) | null EOF"}, + {"proc_spawn", "proc_spawn of [\"cmd\", \"arg1\", ...] → [pid, in_fd, out_fd] | [-1,-1,-1]"}, + {"proc_wait", "proc_wait of pid → exit_code | -1 on error"}, + {"proc_write", "proc_write of [in_fd, \"text\"] → bytes_written | -1 on broken pipe"}, + {"random", "random of null → float in [0, 1)"}, + {"random_hex", "random_hex of n → string of n random hex characters from /dev/urandom."}, + {"random_int", "random_int of [lo, hi] → integer in [lo, hi] inclusive"}, + {"random_normal", "random_normal of [rows, cols, scale] → 2D, or random_normal of [len, scale] → 1D"}, + {"range", "range of n → [0, 1, ..., n-1]"}, + {"raw_key", "raw_key — builtin; see docs/BUILTINS.md"}, + {"read_bytes", "read_bytes of path — read binary file, return list of byte values (0-255)"}, + {"read_bytes_buf", "read_bytes_buf of path — read binary file, return VAL_BUFFER of byte values."}, + {"read_line", "read_line of null — blocking line read from stdin via getline(3):"}, + {"read_text", "read_text of \"path\" → file contents as string, or \"\" on failure."}, + {"record_history", "record_history of flag — enable (nonzero) or disable (0) per-assignment"}, + {"recv", "recv — builtin; see docs/BUILTINS.md"}, + {"recv_timeout", "recv_timeout of [channel, ms] — bounded wait. Returns the value if one"}, + {"regex_find", "regex_find — builtin; see docs/BUILTINS.md"}, + {"regex_match", "regex_match — builtin; see docs/BUILTINS.md"}, + {"regex_replace", "regex_replace of [string, pattern, replacement] -> string"}, + {"relu", "relu of tensor → element-wise max(0, x). Works on 1D or 2D."}, + {"remove_file", "remove_file of path — delete a file. Returns 1 on success, 0 on failure."}, + {"rename", "rename of [old_path, new_path] — rename/replace a file. On POSIX rename(2) is"}, + {"reshape", "reshape of [buf, rows, cols] -> a shaped copy of the flat buffer (rows*cols"}, + {"rm", "rm of \"path\" → 1 on success, 0 on failure"}, + {"round", "round — builtin; see docs/BUILTINS.md"}, + {"sandbox_run", "sandbox_run of [descriptor, max_iterations?] — run an EigenScript-assembled"}, + {"scan_int_tokens", "scan_int_tokens of text"}, + {"scan_ints", "scan_ints of text"}, + {"scan_tokens", "scan_tokens of text"}, + {"screen_clear", "screen_clear of null — clear terminal and hide cursor"}, + {"screen_end", "screen_end of null — show cursor and reset"}, + {"screen_put", "screen_put of [row, col, char, color_code] — write a character at terminal position"}, + {"screen_render", "screen_render of [entities_list, screen_w, screen_h, player_x, player_y, world_w, world_h]"}, + {"secure_equals", "secure_equals of [a, b] → 1 if the two strings are equal, else 0."}, + {"seed_random", "seed_random of n → seeds the RNG, returns 1"}, + {"send", "send — builtin; see docs/BUILTINS.md"}, + {"set_at", "set_at of [list, index, value] — sets list[index] = value, returns list"}, + {"set_observer_thresholds", "set_observer_thresholds — builtin; see docs/BUILTINS.md"}, + {"sgd_update", "sgd_update of [param, grad, lr] — in-place param = param - lr * grad"}, + {"sgd_update_cols", "sgd_update_cols of [matrix, grad, col_indices, lr]"}, + {"sgd_update_rows", "sgd_update_rows of [matrix, grad, row_indices, lr]"}, + {"sha256", "sha256 — builtin; see docs/BUILTINS.md"}, + {"sha256_file", "sha256_file — builtin; see docs/BUILTINS.md"}, + {"shape", "shape of tensor → [rows, cols] for 2D, [len] for 1D"}, + {"shared_clear", "shared_clear — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_delete", "shared_delete — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_get", "shared_get — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_has", "shared_has — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_incr", "shared_incr — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_keys", "shared_keys — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_set", "shared_set — http_request extension builtin; see docs/BUILTINS.md"}, + {"shared_size", "shared_size — http_request extension builtin; see docs/BUILTINS.md"}, + {"sign_extend", "sign_extend of [val, bits] — sign-extend val from given bit width."}, + {"sin", "sin — builtin; see docs/BUILTINS.md"}, + {"softmax", "softmax — builtin; see docs/BUILTINS.md"}, + {"sort", "sort of list — in-place qsort of an all-number or all-string list."}, + {"sort_by", "sort_by of [list, key_fn] — sort list by numeric keys from key_fn."}, + {"spawn", "spawn — builtin; see docs/BUILTINS.md"}, + {"split", "split — builtin; see docs/BUILTINS.md"}, + {"sqrt", "sqrt — builtin; see docs/BUILTINS.md"}, + {"starts_with", "starts_with — builtin; see docs/BUILTINS.md"}, + {"state_at", "state_at — builtin; see docs/BUILTINS.md"}, + {"store_close", "store_close — builtin; see docs/BUILTINS.md"}, + {"store_collections", "store_collections — builtin; see docs/BUILTINS.md"}, + {"store_count", "store_count — builtin; see docs/BUILTINS.md"}, + {"store_delete", "store_delete — builtin; see docs/BUILTINS.md"}, + {"store_drop", "store_drop — builtin; see docs/BUILTINS.md"}, + {"store_get", "store_get — builtin; see docs/BUILTINS.md"}, + {"store_open", "store_open — builtin; see docs/BUILTINS.md"}, + {"store_put", "store_put — builtin; see docs/BUILTINS.md"}, + {"store_query", "store_query — builtin; see docs/BUILTINS.md"}, + {"store_update", "store_update — builtin; see docs/BUILTINS.md"}, + {"str", "str — builtin; see docs/BUILTINS.md"}, + {"str_from_bytes", "str_from_bytes of → string of those raw bytes."}, + {"str_lower", "str_lower — builtin; see docs/BUILTINS.md"}, + {"str_replace", "str_replace — builtin; see docs/BUILTINS.md"}, + {"str_upper", "str_upper — builtin; see docs/BUILTINS.md"}, + {"stream_close", "stream_close of null → closes the stream file, returns 1"}, + {"stream_open", "stream_open of [\"path\", count] → opens file, writes header with count, returns 1"}, + {"stream_write", "stream_write of value → writes one float64, returns 1"}, + {"substr", "substr of [string, start, length] → substring"}, + {"subtract", "subtract — builtin; see docs/BUILTINS.md"}, + {"sum", "sum — builtin; see docs/BUILTINS.md"}, + {"tan", "tan — builtin; see docs/BUILTINS.md"}, + {"task_alive", "task_alive of id → 1 while the task is READY/RUNNING/SUSPENDED, else 0"}, + {"task_detach", "task_detach of id -> 1 (0 for main/unknown). Marks the task fire-and-forget"}, + {"task_join", "task_join of id — block until task `id` finishes; return its (deep-copied)"}, + {"task_kill", "task_kill of id — deterministically tear down task `id`: drop its mailbox"}, + {"task_now", "task_now of null → the current virtual-clock value (a number, 0 before any"}, + {"task_recv", "task_recv of null — return the next message from this task's mailbox, or"}, + {"task_sched_seed", "task_sched_seed of n — install a scheduling seed. By default tasks run FIFO"}, + {"task_self", "task_self of null → the running task's id (a number, in the same integer"}, + {"task_send", "task_send of [id, value] — append a deep-copied message to task `id`'s"}, + {"task_sleep", "task_sleep of ticks — suspend the current task until the virtual clock"}, + {"task_spawn", "task_spawn of fn / task_spawn of [fn, arg1, ...] → task id (a number)."}, + {"task_try_recv", "task_try_recv of null — non-blocking receive: the next message, or null if"}, + {"task_yield", "task_yield of null — cooperatively hand control to the next ready task."}, + {"tensor_load", "tensor_load of path — load 1D or 2D tensor from binary file."}, + {"tensor_save", "tensor_save of [tensor, path] — save 1D or 2D list to binary file."}, + {"text_builder_append", "text_builder_append — builtin; see docs/BUILTINS.md"}, + {"text_builder_append_line", "text_builder_append_line — builtin; see docs/BUILTINS.md"}, + {"text_builder_clear", "text_builder_clear — builtin; see docs/BUILTINS.md"}, + {"text_builder_extend", "text_builder_extend — builtin; see docs/BUILTINS.md"}, + {"text_builder_new", "text_builder_new — builtin; see docs/BUILTINS.md"}, + {"text_builder_part_count", "text_builder_part_count — builtin; see docs/BUILTINS.md"}, + {"text_builder_to_string", "text_builder_to_string — builtin; see docs/BUILTINS.md"}, + {"thread_join", "thread_join — builtin; see docs/BUILTINS.md"}, + {"throw", "throw — builtin; see docs/BUILTINS.md"}, + {"token_name", "token_name of id → string name of token type (for display)"}, + {"tokenize_ids", "tokenize_ids of string → list of token type IDs (integers)."}, + {"tokenize_with_names", "tokenize_with_names of string → list of [type_id, name_str] pairs."}, + {"trim", "trim — builtin; see docs/BUILTINS.md"}, + {"try_parse", "try_parse of string → 1 if valid EigenScript syntax, 0 if not."}, + {"try_recv", "try_recv of channel — non-blocking receive, returns null if empty"}, + {"type", "type — builtin; see docs/BUILTINS.md"}, + {"usleep", "usleep of microseconds — pause execution"}, + {"values", "values — builtin; see docs/BUILTINS.md"}, + {"vm_run_bytecode", "vm_run_bytecode of — assemble a chunk (and its nested"}, + {"write", "write of value — output without trailing newline"}, + {"write_bytes", "write_bytes of [path, data, append?] — write raw bytes to a file."}, + {"write_text", "write_text of [\"path\", text] → 1 on success, 0 on failure."}, + {"zeros", "zeros of n → 1D list of n zeros"}, + {"zeros_like", "zeros_like — builtin; see docs/BUILTINS.md"}, + {"zlib_deflate", "zlib_deflate — builtin; see docs/BUILTINS.md"}, + {"zlib_inflate", "zlib_inflate — builtin; see docs/BUILTINS.md"}, + {0, 0} +}; diff --git a/tests/test_lsp.py b/tests/test_lsp.py index 8eb5d303..a282d37f 100755 --- a/tests/test_lsp.py +++ b/tests/test_lsp.py @@ -247,6 +247,16 @@ def main(): # `undefined variable: input` at runtime. check("completion does not advertise a phantom 'input' builtin", isinstance(items, list) and not any(it.get("label") == "input" for it in items)) + # #1102: the runtime's bytecode compatibility registry is broader than + # the source builtin surface. Check the actual editor response, including + # a callable control so dropping all builtins cannot pass these checks. + check("completion still offers the callable print builtin", + isinstance(items, list) and any(it.get("label") == "print" and + it.get("kind") == 3 for it in items)) + for name in ("report", "report_value"): + check("completion excludes reserved observer form '" + name + "'", + isinstance(items, list) and not any(it.get("label") == name + for it in items)) # --- #590: stdlib (lib/) completion + hover from the generated index --- # Completion is import-aware: the document's own `import`s scope which diff --git a/tests/test_report_reserved.sh b/tests/test_report_reserved.sh index fcb9643c..3e9be9e8 100644 --- a/tests/test_report_reserved.sh +++ b/tests/test_report_reserved.sh @@ -4,7 +4,8 @@ # executed statements. Controls ensure the source templates are valid programs. set -eu TESTS_DIR="$(cd "$(dirname "$0")" && pwd)" -python3 - "$TESTS_DIR/../src/eigenscript" <<'PY' +EIGS="${EIGS:-$TESTS_DIR/../src/eigenscript}" +python3 - "$EIGS" <<'PY' import json import pathlib import re diff --git a/tests/test_vm_run_bytecode.eigs b/tests/test_vm_run_bytecode.eigs index e9f447df..008afab2 100644 --- a/tests/test_vm_run_bytecode.eigs +++ b/tests/test_vm_run_bytecode.eigs @@ -44,6 +44,11 @@ assert_eq of [vm_run_bytecode of [ABI, [CONST,0,0, CONST,1,0, GT, RETURN], [10, # builtin call: len of "hello" -> 5 (GET_NAME resolves "len" in the global env) assert_eq of [vm_run_bytecode of [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["len", "hello"]], 5, "builtin call via GET_NAME+CALL"] +# #1102: report is reserved in source, but existing bytecode can still call +# its value-only compatibility entry. Neither path reads a binding trajectory. +assert_eq of [vm_run_bytecode of [ABI, [GET_NAME,0,0, CONST,1,0, CALL,1,0, RETURN], ["report", 5]], "equilibrium", "report compatibility builtin remains reachable from bytecode"] +assert_eq of [vm_run_bytecode of [ABI, [GET_NAME,0,0, GET_NAME,1,0, CALL,1,0, RETURN], ["report", "print"]], "opaque", "report compatibility builtin still classifies callable values"] + # control flow: max(a, b) via JUMP_IF_FALSE (relative forward offset) # CONST a; CONST b; GT; JUMP_IF_FALSE +4; CONST a; RETURN; CONST b; RETURN maxcode is [CONST,0,0, CONST,1,0, GT, JUMP_IF_FALSE,4,0, CONST,0,0, RETURN, CONST,1,0, RETURN] diff --git a/tools/gen_lsp_builtin_index.sh b/tools/gen_lsp_builtin_index.sh index 356e5f1f..a4312801 100755 --- a/tools/gen_lsp_builtin_index.sh +++ b/tools/gen_lsp_builtin_index.sh @@ -23,9 +23,11 @@ # definition never wrote one fall back to a docs/BUILTINS.md pointer — # back-filling those comments is upstream work, not papered over here. # -# The generated header is a build artifact, NOT committed (the -# lsp_stdlib_index.h / amalgamation precedent, #397): the Makefile `lsp` -# target and `build.sh lsp` regenerate it before compiling eigenlsp. +# Reserved report words come from the lexer's keyword registrations (#1102). +# The runtime retains a report builtin for bytecode compatibility, but source +# programs cannot use it as a function, so it must not be a Function completion. +# The generated header is committed for review; the Makefile `lsp` target and +# `build.sh lsp` regenerate it before compiling eigenlsp. # # Usage: tools/gen_lsp_builtin_index.sh [output-header] # default output: src/lsp_builtin_index.h @@ -50,6 +52,13 @@ TMP_NAMES=$(mktemp) TMP_OUT=$(mktemp) trap 'rm -f "$TMP_NAMES" "$TMP_OUT"' EXIT +# Use the language's reservation site, not another hand-maintained word list. +RESERVED_REPORT_NAMES=$(sed -nE 's/.*strcmp\(word, "([^"]+)"\).*return TOK_REPORT(_VALUE)?;.*/\1/p' src/lexer.c) +if [ "$(printf '%s\n' "$RESERVED_REPORT_NAMES" | wc -w)" -ne 2 ]; then + echo "gen_lsp_builtin_index: expected two reserved observer words from lexer" >&2 + exit 1 +fi + # ---- 1. core names from the registration seams ------------------------- # Line shape: env_set_local_owned(env, "name", make_builtin(fn)); grep -h 'env_set_local_owned(env, "' $CORE_SRCS \ @@ -80,7 +89,7 @@ if [ "$total" -eq 0 ]; then fi # ---- 3. emit, resolving each name's signature comment ------------------ -sort -u -t"$(printf '\t')" -k1,1 "$TMP_NAMES" | awk -v OUT="$TMP_OUT" -v docs="$DOC_SRCS" ' +sort -u -t"$(printf '\t')" -k1,1 "$TMP_NAMES" | awk -v OUT="$TMP_OUT" -v docs="$DOC_SRCS" -v reserved="$RESERVED_REPORT_NAMES" ' function c_escape(s) { gsub(/\\/, "\\\\", s) gsub(/"/, "\\\"", s) @@ -88,6 +97,8 @@ function c_escape(s) { } BEGIN { FS = "\t" + n = split(reserved, words, /[ \t\n]+/) + for (i = 1; i <= n; i++) excluded[words[i]] = 1 # Preload every signature-comment line from the runtime TUs: # a comment line whose text starts `name of ` (after comment markers). n = split(docs, files, /[ \t]+/) @@ -106,12 +117,14 @@ BEGIN { print "/* Generated by tools/gen_lsp_builtin_index.sh — DO NOT EDIT (#742)." > OUT print " * Names come from the registration seams + ext_names.h; hover text" > OUT print " * from the `name of ...` signature comment above each definition." > OUT + print " * Reserved report forms are excluded using the lexer keyword table." > OUT print " * Regenerated by the Makefile lsp target and build.sh lsp. */" > OUT print "static const char *builtin_docs[][2] = {" > OUT core = 0; ext = 0; documented = 0 } { name = $1; group = $2 + if (name in excluded) next if (name in sig) { detail = sig[name] documented++ From 0c2a02534c88b95ab1f904665437166355cdfd7b Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Sun, 6 Sep 2026 06:55:22 -0500 Subject: [PATCH 3/3] gen_lsp_builtin_index: space-separate reserved names for BSD awk (#1102 macOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Kpzyjv1SaLaqBf45FSFDhB --- CHANGELOG.md | 3 +++ tools/gen_lsp_builtin_index.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9594e370..0c7d0721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ All notable changes to EigenScript are documented here. lint and LSP carry the same code. CLI `-e [args...]` now accepts a source string through the file execution path. The builtin reference table and LSP Function completions omit both forms. + Two new token kinds (`TOK_REPORT`, `TOK_REPORT_VALUE`) shift the tokenizer's + identifier vocabulary ids by +2, visible in `tests/test_corpus.eigs` output; + consumers that key corpora on raw token ids (iLambdaAi) must rebuild them. The VM retains the `report` builtin for existing bytecode that resolves its name through `vm_run_bytecode`; its value-only behavior is unchanged. diff --git a/tools/gen_lsp_builtin_index.sh b/tools/gen_lsp_builtin_index.sh index a4312801..7cf44d29 100755 --- a/tools/gen_lsp_builtin_index.sh +++ b/tools/gen_lsp_builtin_index.sh @@ -53,7 +53,7 @@ TMP_OUT=$(mktemp) trap 'rm -f "$TMP_NAMES" "$TMP_OUT"' EXIT # Use the language's reservation site, not another hand-maintained word list. -RESERVED_REPORT_NAMES=$(sed -nE 's/.*strcmp\(word, "([^"]+)"\).*return TOK_REPORT(_VALUE)?;.*/\1/p' src/lexer.c) +RESERVED_REPORT_NAMES=$(sed -nE 's/.*strcmp\(word, "([^"]+)"\).*return TOK_REPORT(_VALUE)?;.*/\1/p' src/lexer.c | tr '\n' ' ') if [ "$(printf '%s\n' "$RESERVED_REPORT_NAMES" | wc -w)" -ne 2 ]; then echo "gen_lsp_builtin_index: expected two reserved observer words from lexer" >&2 exit 1