Skip to content

A file's semantics depend on HOW IT WAS REACHED (main / import / load_file) — for-body scoping and top-level return both differ #1056

Description

@InauguralPhysicist

Found by pointing the AOT at the module/load_file splice roads (ouroboros#141/#146). The AOT compiles ahead of time, so it has to know what a construct MEANS before it knows which road the file arrived by — and that turns out to be unanswerable today, because the same file means different things on different roads.

1. A for body binding survives on two roads out of three

Identical three-line file, mp.eigs:

for q in range of 2:
    inner is 5
print of inner
reached as result
main program (eigenscript mp.eigs) prints 5, exit 0
import mp Error line 3: undefined variable "inner", exit 1
load_file of "mp.eigs" prints 5, exit 0

The module road also drops it from the module dict (keys of M omits inner, M.inner is null) — self-consistent with the error, but not with the other two roads.

2. Top-level return has three meanings, one per road

reached as meaning
main program end the program, exit 0, returned value discarded
imported module end the module; bindings below it never come into existence
load_file target end THAT FILE only, yield its value to the load_file call, caller continues

Each is defensible alone. Together they mean a file cannot be understood without knowing its caller.

Why this is worth changing rather than documenting

EigenScript is pre-v1 with no external consumers, so this is a design decision that can still be made once rather than lived with. Concretely, road-dependence costs:

  • The AOT cannot compile two of the three roads. Both are now refused loudly (ouroboros#146). A module or load_file target using either construct is outside the AOT envelope entirely — not because the construct is hard, but because its meaning is not a property of the file.
  • It is invisible to testing. Every one of these behaviours is individually sensible and individually tested; only the cross-road comparison shows the disagreement, and nothing was comparing roads.
  • It blocks static tooling generally, not just the AOT — a linter, an LSP, or a docs generator asking "what does this module export?" has the same problem the AOT has.

Suggested direction (not a demand — the shape matters more than my guess)

A file`s top level should mean one thing regardless of arrival:

  • for should scope its body consistently on all three roads (either it leaks everywhere or nowhere; ouroboros measured that if/loop while/try/match case all leak and only for does not, which is itself worth a second look).
  • Top-level return should uniformly end THE CURRENT FILE and yield its value; the main-program case is then just the outermost file, and "exit 0, value discarded" falls out of nobody consuming the outermost value.

That rule is compilable, which the current one is not.

Provenance

Fourteen blind-critic rounds against the AOTs import/splice implementation. Rounds 9-12 were all the same underlying question — the module dict is "a snapshot of what RAN", so a static walk cannot reproduce it — and rounds 13-14 found the return` roads. The consumer-forces-the-language loop applies to the AOT as much as to any other consumer: the AOT shows what has to be TRUE in the VM for compilation to be possible at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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