Skip to content

Observer: add a compile-time liveness gate so bookkeeping isn't emitted when never read (consumers can drop unobserved:) #972

Description

@InauguralPhysicist

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions