Skip to content

feat: script mode, implicit main for top-level statements (ILO-439) - #785

Merged
danieljohnmorris merged 7 commits into
mainfrom
feature/script-mode-mixed
Aug 5, 2026
Merged

feat: script mode, implicit main for top-level statements (ILO-439)#785
danieljohnmorris merged 7 commits into
mainfrom
feature/script-mode-mixed

Conversation

@danieljohnmorris

Copy link
Copy Markdown
Collaborator

Summary

Bare top-level statements now wrap into a synthetic main>_; automatically. prnt +2 2 alone in a file prints 4, and - the case that actually matters - a file can declare a function and then just call it on the next line:

tri n:n>n;/(*n +n 1) 2
prnt tri 10

That is the shape a model writes when asked for a compact program, and until now it was a parse error. In the ILO-364 closed-loop benchmark the missing-wrapper diagnostics dominated every ilo failure (ILO-P102 fired 19 times in one N=3 run) and models could not recover even though the hint named the fix. This removes the tax instead of documenting it. Manifesto P1: the wrapper was 4-5 tokens of ceremony per program plus, empirically, several hundred tokens of failed retries.

Note this deliberately inverts ILO-439's original 'verifier rejects mixed' rule, based on that trace evidence. Pure-statement scripts (the ticket's original goal) work too.

Repro before/after

Before: ilo file.@ main on the two-line program above -> ILO-P011 "prnt is a builtin and cannot be used as a function name" (misleading), model flails to retry cap.
After: prints 55.

What's in the diff (per commit)

  • parser: script mode (163030e) - bare statements collected in source order into a synthetic main. Each must start its own top-level line; the existing decl_boundary array (unindented newlines were already the decl boundary) separates deliberate script lines from wreckage glued to a broken decl, so the tailored diagnostics for f>n;1e, main->n, stray }, foreign let/if/return, and glued name=expr (ILO-P102, registry text updated) all keep firing. Explicit main + bare statements -> new ILO-P104. Obsolete 'top-level statements are illegal' tests rewritten to pin the new contract.
  • repl guard (d54976f) - new parser::is_script_mode_input so the REPL keeps evaluating +1 2 to 3 instead of reporting defined: main() -> _.
  • verify: ILO-V500 (2b7a874) - a straight-line body that calls its own function can never terminate; previously spun silently for the runtime budget (tail calls trampoline, so no stack overflow). Now a verify-time error whose hint names both fixes. First code in the reserved V500-599 namespace. Also fixes a test fixture that was genuinely infinite recursion.
  • docs (9277fd4) - SPEC section (ai.txt regenerates from it), ilo-language skill module + SKILL.md quick ref, examples/script-mode-mixed.ilo with run/out assertions, CHANGELOG.

Test plan

  • Full suite green: 42 suites, 3500+ lib tests; only the two pre-existing doctest failures on main remain (ILO-532, unrelated)
  • 8 new V500 unit tests including negatives: guarded recursion, match-arm recursion, ternary-branch recursion, HOF fn-refs all untouched
  • Six diagnostics that script mode initially swallowed were caught by the suite and restored (arrow hint, stray brace, REPL, orphaned operand, glued negative literal, incomplete exponent) - each now pinned by an updated test
  • examples/script-mode-mixed.ilo exercises the decl+call shape across every engine via the examples harness
  • Benchmark impact measured (ILO-364, N=3 Sonnet): P102 19 -> 1, failures shifted from entry-point ceremony to genuine type errors, ilo success 1-2/5 -> 2-4/5

Follow-ups

  • ILO-529: top-level builtin-call misdiagnosis is mostly mooted by this PR but P011 still fires for a genuine prnt>...; declaration
  • Statements-then-declaration (42 x:n>n;x) still errors with a body-parse message rather than 'expected declaration'; decl-then-statements (the shape that matters) works
  • Site reference page updated locally in the site repo; will commit and push there once this merges so public docs do not describe an unreleased feature

…main (ILO-439)

A file no longer needs a main>_; wrapper: bare statements at top level
are gathered in source order and wrapped in a synthetic main. Decls and
statements mix freely as long as each statement starts its own top-level
line (unindented newlines were already the decl boundary, tracked in
decl_boundary, so no new line semantics).

Statements glued to a broken decl (f>n;1e, main->n, stray brace, foreign
let/if/return) still route to parse_decl so their targeted diagnostics
keep firing. A glued name=expr after a decl still raises ILO-P102, and
its registry text now describes the post-script-mode reality. A file
with both an explicit main and bare statements raises new ILO-P104
(P103 was taken by the AST-depth guard).

The k-means chain that motivated P102 now just runs. Old tests pinning
'top-level statements are illegal' rewritten to pin the new contract;
the P102/P011/P003 hint tests now target shapes that still reject.
Script mode wraps bare statements in a synthesised main, which is
indistinguishable from a hand-written definition in the AST, so the
REPL's def-detection started reporting 'defined: main() -> _' for
'+1 2' instead of printing 3. New parser::is_script_mode_input asks
what shape the input was before parsing erases the distinction.
A straight-line body (no guards, match, loops, or early returns) that
directly calls its own function can never terminate. Tail-call
trampolining made this spin silently at runtime instead of overflowing
the stack, most commonly via the script-mode near-miss where a trailing
call glued to the definition line joins the body: tri n:n>n;...;prnt tri 10.

Verify-time error, hint names both fixes (add a base case, or move the
call to its own line). Conservative: any branching construct disables
the check. Conditional contexts are excluded precisely - ternary/match/
nil-coalesce branches and closure bodies don't count, their
always-evaluated sides do.

First code allocated in the reserved V500-599 verifier namespace. Also
corrects a test fixture V500 caught red-handed: compat_text_to_sum_param's
'g y:S a b>n;g "hello"' was genuinely infinite.
SPEC gains a 'Script mode (implicit main)' section under Functions
(ai.txt regenerates from it via build.rs). ilo-language skill module
gets the own-line rule so agents load it via ilo skill get; SKILL.md
gets a quick-reference entry. examples/script-mode-mixed.ilo pins the
decl-plus-trailing-call shape across engines via run/out assertions.
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.57364% with 14 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/parser/mod.rs 91.55% 13 Missing ⚠️
src/verify.rs 99.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

rust 1.97 adds useless_borrows_in_formatting; CI lint runs -D warnings
so the pre-existing borrow in this untouched test now blocks every PR.
Newlines are filtered from the token stream before expression parsing,
so two bare script lines glued into one call: r=quad 7 then prnt r on
its own line parsed as r=quad(7, prnt, r) and surfaced as a baffling
arity error on visibly correct code (18 of 33 type errors in the
post-script-mode benchmark traced to this shape family).

New script_stmt_boundary ctx flag makes can_start_operand treat any
top-level decl boundary as the end of an operand chain while collecting
script statements - the same stop the ident= carve-out already applied,
generalised. Set only around script collection, so function bodies and
nested contexts parse exactly as before. Two regression tests pin the
multi-statement shapes.
CI budget check caught the first draft at 2011 tokens (cap 1950).
Trimmed to essentials; module now 1944.
@danieljohnmorris
danieljohnmorris merged commit 1192484 into main Aug 5, 2026
11 of 12 checks passed
@danieljohnmorris
danieljohnmorris deleted the feature/script-mode-mixed branch August 5, 2026 20:54
danieljohnmorris added a commit that referenced this pull request Aug 7, 2026
Brings in:
- Script mode: bare top-level statements auto-wrapped in main (#785)
- V500: unconditional recursion detection
- Persona-smoke fence stripping + baseline refresh (#787)
- ai.txt bootstrap index preserved (ILO-538)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant