feat: script mode, implicit main for top-level statements (ILO-439) - #785
Merged
Conversation
…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 Report❌ Patch coverage is
📢 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bare top-level statements now wrap into a synthetic
main>_;automatically.prnt +2 2alone 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: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.@ mainon 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)
f>n;1e,main->n, stray}, foreignlet/if/return, and gluedname=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.parser::is_script_mode_inputso the REPL keeps evaluating+1 2to 3 instead of reportingdefined: main() -> _.Test plan
Follow-ups
prnt>...;declaration42 x:n>n;x) still errors with a body-parse message rather than 'expected declaration'; decl-then-statements (the shape that matters) works