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.
Found by pointing the AOT at the module/
load_filesplice 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
forbody binding survives on two roads out of threeIdentical three-line file,
mp.eigs:eigenscript mp.eigs)5, exit 0import mpError line 3: undefined variable "inner", exit 1load_file of "mp.eigs"5, exit 0The module road also drops it from the module dict (
keys of Momitsinner,M.inneris null) — self-consistent with the error, but not with the other two roads.2. Top-level
returnhas three meanings, one per roadload_filetargetload_filecall, caller continuesEach 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:
load_filetarget 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.Suggested direction (not a demand — the shape matters more than my guess)
A file`s top level should mean one thing regardless of arrival:
forshould scope its body consistently on all three roads (either it leaks everywhere or nowhere; ouroboros measured thatif/loop while/try/match caseall leak and onlyfordoes not, which is itself worth a second look).returnshould 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 AOT
s 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 thereturn` 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.