Parse foreign Lua files with the compiler's own Lua 5.1 parser - #197
Merged
Conversation
…173) Replace the lexical splitter that extracted FFI export values as opaque text blobs with a full Lua 5.1 parser (megaparsec) producing the backend's own AST. Foreign values become visible to Lua-level optimizations, comments ride in annotation slots, and a syntax error in an FFI file is a compile-time error. The parser matches the reference implementation's rejections (ambiguous line-broken calls, '...' outside a vararg function) and caps syntactic nesting at 500 levels. Verification is anchored outside the parser/printer pair: luac must accept everything the printer emits, and a semantic differential compares the printer's precedence tables against the lua interpreter itself.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR replaces the previous lexical splitting approach for foreign (FFI) .lua files with a full Lua 5.1 parser that produces the backend’s Lua AST, enabling Lua-level optimizations across foreign boundaries and turning foreign-file syntax errors into compile-time errors. It also expands the Lua AST and printer to cover missing Lua 5.1 constructs, preserves comments via AST annotations, and strengthens correctness with round-trip and differential tests.
Changes:
- Add a megaparsec-based Lua 5.1 parser and wire it into foreign-module ingestion and runtime fixtures.
- Extend the Lua AST/printer/traversal/optimizer to support additional statement/expression forms, comment annotations, and correct
elseif/separator/precedence printing. - Add extensive unit/property/differential test coverage and update golden Lua outputs to the new canonical formatting.
Reviewed changes
Copilot reviewed 60 out of 60 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Test/Lua.hs | Adds shared helpers for invoking luac/lua in tests via temp files. |
| test/Main.hs | Registers new Lua parser/differential/traversal specs in the test suite. |
| test/Language/PureScript/Backend/Lua/Traversal/Spec.hs | Adds traversal identity properties to pin comment-threading invariants. |
| test/Language/PureScript/Backend/Lua/Run/Spec.hs | Switches runtime fixture injection from verbatim text to parsed statements. |
| test/Language/PureScript/Backend/Lua/Printer/Spec.hs | Adds printer regression tests (statement separators, negative literal precedence, new syntax forms, comment printing) and reparse checks. |
| test/Language/PureScript/Backend/Lua/Parser/Spec.hs | Adds detailed conformance, edge-case, and print/reparse properties for the new Lua 5.1 parser. |
| test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs | Extends optimizer tests for containsReturn behavior across new statement forms. |
| test/Language/PureScript/Backend/Lua/Linker/Foreign/Spec.hs | Updates foreign-module parsing tests to the new AST-based parser and stricter module-shape checks. |
| test/Language/PureScript/Backend/Lua/Golden/Spec.hs | Adds parser readback of printed goldens to ensure generated Lua remains parseable by the compiler parser. |
| test/Language/PureScript/Backend/Lua/Gen.hs | Introduces Lua AST generators for parser/printer round-trip and differential tests. |
| test/Language/PureScript/Backend/Lua/Differential/Spec.hs | Adds luac acceptance and semantic differential checks against the reference Lua implementation. |
| pslua.cabal | Exposes the new parser module and includes new test modules in the spec suite. |
| lib/Language/PureScript/Backend/Lua/Types.hs | Extends the Lua AST (comments annotations, varargs, method calls, paren node, loops, multi-assign, multi-return, etc.). |
| lib/Language/PureScript/Backend/Lua/Traversal.hs | Reworks traversal/rewrite utilities to preserve comment annotations across new AST forms. |
| lib/Language/PureScript/Backend/Lua/Printer.hs | Updates printer for new AST, comment emission, elseif resugaring, statement separators, and precedence correctness. |
| lib/Language/PureScript/Backend/Lua/Optimizer.hs | Updates optimizer to handle new AST shapes and more accurate “contains return” analysis. |
| lib/Language/PureScript/Backend/Lua/NestingCheck.hs | Updates nesting-depth computation for expanded statement/expression grammar. |
| lib/Language/PureScript/Backend/Lua/Linker/Foreign.hs | Replaces lexical FFI splitter with parse+interpret into header statements + AST exports; improves error reporting. |
| lib/Language/PureScript/Backend/Lua/Key.hs | Removes old key parser and documents the new foreign-key interpretation flow. |
| lib/Language/PureScript/Backend/Lua/Fixture.hs | Parses embedded runtime fixtures into AST statements instead of emitting verbatim source. |
| lib/Language/PureScript/Backend/Lua.hs | Integrates parsed foreign modules into codegen as AST and threads comments into exported rows. |
| docs/QUIRKS.md | Updates FFI authoring documentation to the new parsed foreign-module contract. |
| CLAUDE.md | Updates repository guidance to include the new Lua parser and comment-annotated AST. |
| changelog.d/20260707_130000_unisay_foreign_lua_parser.md | Adds changelog entry describing the parser migration, AST/printer fixes, and behavior changes. |
| test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua | Golden output updated to new printer formatting (elseif, separators, spacing). |
| test/ps/output/Golden.StringCodePoints.Test/golden.lua | Golden output updated to new printer formatting and canonicalized constructs. |
| test/ps/output/Golden.RecursiveBindings.Test/golden.lua | Golden output updated to new printer formatting (elseif chains). |
| test/ps/output/Golden.RecordsUpdate.Test/golden.lua | Golden output updated to new printer formatting (single-line if/else). |
| test/ps/output/Golden.RecGroupOrder.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.ProfunctorDictLens.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.PatternMatching.Test2/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.PatternMatching.Test1/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.Nested.Test/golden.lua | Golden output updated to new printer formatting (elseif resugaring). |
| test/ps/output/Golden.NameShadowing.Test/golden.lua | Golden output updated to new printer formatting (elseif). |
| test/ps/output/Golden.MaybeChainModule.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.MaybeChain.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.LongWriterBind.Test/golden.lua | Golden output updated to new printer formatting (canonical #(x) printing, indentation). |
| test/ps/output/Golden.LongStackBind.Test/golden.lua | Golden output updated to new printer formatting and elseif flattening. |
| test/ps/output/Golden.LongMaybeBindModule.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.LongMaybeBind.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.LongExceptBind.Test/golden.lua | Golden output updated to new printer formatting and elseif flattening. |
| test/ps/output/Golden.LongEitherBind.Test/golden.lua | Golden output updated to new printer formatting and elseif flattening. |
| test/ps/output/Golden.LongDoBlock.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.LongBindFlipped.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.LongApplyChain.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.Issue37.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.HelloPrelude.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.GenericEqTwoTypes.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.ForeignSharing.Test/golden.lua | Golden output updated to new printer formatting (collapsed lambdas). |
| test/ps/output/Golden.Foreign.Lib/golden.lua | Golden output updated to reflect fixed negative-literal precedence printing. |
| test/ps/output/Golden.FloatIn.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.Fibonacci.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.DerivedFunctor.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.CharLiterals.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.BugListGenericEq.Test/golden.lua | Golden output updated to new printer formatting (elseif flattening). |
| test/ps/output/Golden.ArrayPatternMatch.Test/golden.lua | Golden output updated to new printer formatting. |
| test/ps/output/Golden.ArrayOfUnits.Test/golden.lua | Golden output updated to new printer formatting (notably for step printing as -(1)). |
| test/ps/output/Golden.Annotations.M2/golden.lua | Golden output updated to new printer formatting (collapsed functions). |
| test/ps/output/Golden.Annotations.M1/golden.lua | Golden output updated to new printer formatting (collapsed functions). |
Top-level '...' is legal Lua (a main chunk is vararg) and the parser accepts it, but an FFI file is embedded into a function scope in the generated output (a header becomes an IIFE body, export values land in expression positions), where chunk varargs are a syntax error or silently rebound. Rejecting at compile time beats emitting broken Lua; forwarding varargs through a vararg wrapper would only work while the wrapper stays at the output's top level, which inlining does not guarantee. Found by Copilot review on PR #197: #197 (comment)
Same counters, same abort reasons; only artifact line numbers moved with the new printer's comment placement and elseif resugaring. Regenerated with ./bench/ci --accept.
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.
Closes #173.
What changed
Lua.Linker.Foreignno longer splits an FFI file lexically into text blobs. The whole file goes through a new complete Lua 5.1 parser (Language.PureScript.Backend.Lua.Parser, megaparsec) straight into the backend's Lua AST: header statements and export values become real AST nodes, visible to every Lua-level optimization, and a syntax error anywhere in a foreign file is now a compile-time error instead of a luacheck/runtime one. The balanced-paren scanner and the "wrap every export value in parens" contract are gone; comments are preserved through the existing annotation slots and reattached in the printed output.To support this, the Lua AST grew the missing Lua 5.1 statement and expression forms (loops,
local function, multiple assignment, method calls, varargs, array-part table rows, multi-valuereturn), the printer learned to render nestedif/else-of-ifchains aselseif, and the runtime fixtures are parsed into the AST instead of being emitted as verbatim text.Parser design
Operator parsing is a transcription of
subexprfrom Lua 5.1's own lparser.c, priority table included, so associativity and unary binding levels are pinned to the reference. The parser also matches the reference's rejections, each verified againstluac5.1.5 before the corresponding test was written:(on a different line than its callee is "ambiguous syntax (function call x new statement)", with a hint about;; string and table arguments stay legal across a line break, as in the reference;...outside a vararg function is an error (the main chunk counts as vararg);NestingChecklimit of 180: the cap exists only to turn adversarially nested input into a clean parse error instead of a compiler stack overflow.The one deliberate divergence is rejecting
gotoas an identifier, which Lua 5.1 itself allows. Note [Parsing foreign Lua sources] records the reason: parsed FFI names and generated names share one output chunk, and that chunk has to stay loadable on LuaJIT and Lua 5.2+, wheregotois a keyword.Verification
A
parse . print ≡ idround trip alone cannot see a matched pair of defects, where the parser misreads a construct exactly the way the printer misprints it. The suite therefore anchors to arbiters that cannot share a bug with the printer:luacmust accept everything the printer emits: a property over generated chunks (with comments), plus a deterministic anchor that feeds every snippet of the hand-written round-trip corpus toluacin both source and printed form (newDifferential.Spec, with aTest.Luahelper shared with the golden harness).luaitself (NaN accounted for). This is the only check of the precedence and associativity tables against the interpreter rather than against themselves.;insertion and hardline comments.everywhereStat/everywhereExpwithidentitymust beid, comments included) pins the annotation-threading contract the rewrite rules rely on.;separator, thecontainsReturnbranches offoldFieldProjectionThroughScopeCall, and the parser's number, string, and comment edge cases (exponent padding,\r\nescapes, false long-bracket opens, a comment beforeelseifblocking resugaring in both directions).Two printer correctness gaps surfaced by all this and are fixed here: a negative numeric literal now carries unary precedence (
(-2) ^ 2used to print as-2 ^ 2, which Lua reads as-(2 ^ 2)), and a statement whose printed form starts with(is now separated from the previous statement with;.Every golden module's rendered output is additionally re-read by the compiler's own parser inside the golden harness, so the whole corpus doubles as a round-trip fixture.
Coverage
hpcover the full suite:Lua.Parser100% of top-level definitions, 94% of expressions, 85% of alternatives. The remainder is six unreachable-by-designbinOpPriorityrows (5.3 operators absent from the grammar) and error-message thunks that tests trigger as branches but never force as strings.Goldens
35
golden.luafiles move structurally (mostlyelseifresugaring and comment placement);golden.irand everyeval/golden.txtoracle are untouched, so generated-code semantics are unchanged.