Summary
The observer — the language's defining feature — is emitted unconditionally on every assignment, with no compile-time liveness gate. Performance-sensitive consumers therefore disable it wholesale with unobserved: (EigenMiniSat ships tseitin_ladder_unobserved.eigs; measured ~88% of runtime / 8.6× on a CDCL solver, EigenScript#915). When every serious consumer switches off the moat to be usable, that avoidance is the loudest architectural signal in the tree.
This is the Rank-3 finding of the 2026-08-18 whole-language architecture review.
The gap (verified)
emit_assign_for_tos emits OBSERVE_ASSIGN(_LOCAL) after every assignment, unconditionally — src/compiler.c:1804-1808.
- The scalar path runs
entropy_of_num (two log2 + a divide) then observer_slot_record_value (a divide + two ring-buffer pushes) on every write — src/eigenscript.c:290-292,523-532,583-587. The comment states it is updated eagerly "no dirty/lazy step" (eigenscript.c:399-403).
- The precedent already exists one subsystem over. The temporal tape (
prev/at/state_at) is gated by g_trace_hist, armed only when the program contains a temporal query — src/trace.c:294-321, documented in .claude/rules/c-runtime-memory.md. The observer entropy/dH channel has no equivalent.
- Half the machinery is already there:
scan_for_interrogated (src/compiler.c:1261-1268) whole-program-walks the AST — but collects only AST_INTERROGATE names (not AST_PREDICATE/report/classify) and uses them merely to force the env-binding path (:1754), never to gate observation OFF.
Note: the per-assignment container-walk cost was already fixed to O(direct children) in #685 (4.93s→0.06s at N=8000); this issue is not about that. It's about paying for measurements nobody reads.
Proposed fix
Extend scan_for_interrogated (compiler.c:1261) to also collect AST_PREDICATE and report/classify targets, producing a per-unit "is any observer state ever read?" answer. When a binding's observer state is provably never read, don't emit OBSERVE_ASSIGN(_LOCAL) at all (compiler.c:1804) — no opcode, no dispatch, no entropy_of_num, no ring pushes. Same mechanism the temporal tape already uses safely.
Does it touch identity? No.
The crux answer from the review: "always-on, per-assignment, eager" is an early implementation default, not a founding requirement. The thesis requires the observer be available (a binding can locate itself by its trajectory), not that entropy be computed on every write regardless of whether anyone looks. A program that reads observer state gets byte-identical behavior; one that never reads it pays nothing. The moat stays semantically always-on and on-by-default — and consumers can delete unobserved: wholesale, stopping the observer-disabled variants that are the avoidance signal, without opt-in and without weakening "observer with no outside."
Leverage: high. Class: REALIGN.
Summary
The observer — the language's defining feature — is emitted unconditionally on every assignment, with no compile-time liveness gate. Performance-sensitive consumers therefore disable it wholesale with
unobserved:(EigenMiniSat shipstseitin_ladder_unobserved.eigs; measured ~88% of runtime / 8.6× on a CDCL solver, EigenScript#915). When every serious consumer switches off the moat to be usable, that avoidance is the loudest architectural signal in the tree.This is the Rank-3 finding of the 2026-08-18 whole-language architecture review.
The gap (verified)
emit_assign_for_tosemitsOBSERVE_ASSIGN(_LOCAL)after every assignment, unconditionally —src/compiler.c:1804-1808.entropy_of_num(twolog2+ a divide) thenobserver_slot_record_value(a divide + two ring-buffer pushes) on every write —src/eigenscript.c:290-292,523-532,583-587. The comment states it is updated eagerly "no dirty/lazy step" (eigenscript.c:399-403).prev/at/state_at) is gated byg_trace_hist, armed only when the program contains a temporal query —src/trace.c:294-321, documented in.claude/rules/c-runtime-memory.md. The observer entropy/dH channel has no equivalent.scan_for_interrogated(src/compiler.c:1261-1268) whole-program-walks the AST — but collects onlyAST_INTERROGATEnames (notAST_PREDICATE/report/classify) and uses them merely to force the env-binding path (:1754), never to gate observation OFF.Note: the per-assignment container-walk cost was already fixed to O(direct children) in #685 (4.93s→0.06s at N=8000); this issue is not about that. It's about paying for measurements nobody reads.
Proposed fix
Extend
scan_for_interrogated(compiler.c:1261) to also collectAST_PREDICATEand report/classify targets, producing a per-unit "is any observer state ever read?" answer. When a binding's observer state is provably never read, don't emitOBSERVE_ASSIGN(_LOCAL)at all (compiler.c:1804) — no opcode, no dispatch, noentropy_of_num, no ring pushes. Same mechanism the temporal tape already uses safely.Does it touch identity? No.
The crux answer from the review: "always-on, per-assignment, eager" is an early implementation default, not a founding requirement. The thesis requires the observer be available (a binding can locate itself by its trajectory), not that entropy be computed on every write regardless of whether anyone looks. A program that reads observer state gets byte-identical behavior; one that never reads it pays nothing. The moat stays semantically always-on and on-by-default — and consumers can delete
unobserved:wholesale, stopping the observer-disabled variants that are the avoidance signal, without opt-in and without weakening "observer with no outside."Leverage: high. Class: REALIGN.