Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ 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 <source> [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.

- **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
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 17 additions & 5 deletions docs/BUILTINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,21 +269,33 @@ 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) |
| `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.
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
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).

Expand Down
5 changes: 5 additions & 0 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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 (`<predicate> of <var>`). |
| `W015` | warning | A function assigns (without `local`) over a module-level **function** name, clobbering it via mutate-outward so later `<fn> 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 `<predicate> of <var>`. 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. |
Expand Down
12 changes: 12 additions & 0 deletions docs/GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
10 changes: 10 additions & 0 deletions docs/OBSERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<source>'`.

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` →
Expand Down
Loading
Loading