Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
130 lines
13 KiB
Markdown
130 lines
13 KiB
Markdown
# Lua-on-SX: Lua 5.1 on the CEK/VM
|
||
|
||
Compile Lua 5.1 AST to SX AST; the existing CEK evaluator runs it. Same architecture as `plans/js-on-sx.md` — reuse SX semantics wherever they fit, only shim the Lua-specific parts (tables/metatables, `nil`/`false`-only-falsy, multi-return, coroutines via `perform`/resume).
|
||
|
||
End-state goal: **100% of PUC-Rio Lua 5.1.5 test suite.** Running as a long-lived background agent that drives the scoreboard up one failure-mode at a time, like `lib/js/`.
|
||
|
||
## Ground rules
|
||
|
||
- **Scope:** only touch `lib/lua/**` and `plans/lua-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/js/**`, `lib/hyperscript/**`, `lib/prolog/**`, `lib/stdlib.sx`, or anything in `lib/` root. Lua-specific primitives go in `lib/lua/runtime.sx`.
|
||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix from this loop.
|
||
- **SX files:** use `sx-tree` MCP tools only (never `Edit`/`Read`/`Write` on `.sx` files). `sx_write_file` for new files, path/pattern edits for changes.
|
||
- **Architecture:** Lua source → Lua AST → SX AST → CEK. No standalone Lua evaluator.
|
||
- **Commits:** one feature per commit. Keep `## Progress log` updated (dated entries, newest first) and tick boxes in the roadmap.
|
||
|
||
## Architecture sketch
|
||
|
||
```
|
||
Lua source text
|
||
│
|
||
▼
|
||
lib/lua/tokenizer.sx — numbers, strings (short + long [[…]]), idents, ops, comments
|
||
│
|
||
▼
|
||
lib/lua/parser.sx — Lua AST as SX trees, e.g. (lua-for-num i a b c body)
|
||
│
|
||
▼
|
||
lib/lua/transpile.sx — Lua AST → SX AST (entry: lua-eval-ast)
|
||
│
|
||
▼
|
||
existing CEK / VM
|
||
```
|
||
|
||
Runtime shims in `lib/lua/runtime.sx`: `lua-truthy?`, string coercion for `..`/arithmetic, table ops (array + hash part), metatable dispatch, `pcall`/`error` bridge, `string`/`math`/`table` libs.
|
||
|
||
## Roadmap
|
||
|
||
Each item: implement → tests → tick box → update progress log.
|
||
|
||
### Phase 1 — tokenizer + parser
|
||
- [x] Tokenizer: numbers (int, float, hex), strings (short + long `[[…]]`), idents, keywords, operators, comments (`--`, `--[[…]]`)
|
||
- [x] Parser: blocks, `local`, `if/elseif/else/end`, `while`, numeric `for`, `function`, `return`, expressions, table constructors, indexing (`.`, `[]`), calls (`f(…)`, `f:m(…)`)
|
||
- [x] Skip for phase 1: generic `for … in …`, goto/labels, nested varargs `...`
|
||
- [x] Unit tests in `lib/lua/tests/parse.sx`: source → expected AST
|
||
|
||
### Phase 2 — transpile: control flow + arithmetic
|
||
- [x] `lua-eval-ast` entry
|
||
- [x] Arithmetic (Lua 5.1 semantics — `/` is float)
|
||
- [x] Comparison + logical (short-circuit, Lua truthy)
|
||
- [x] `..` concat with string/number coercion
|
||
- [x] `if`, `while`, numeric `for`, `local`, assignment, blocks
|
||
- [x] 30+ eval tests in `lib/lua/tests/eval.sx`
|
||
|
||
### Phase 3 — tables + functions + first PUC-Rio slice
|
||
- [x] `function` (anon, local, top-level), closures
|
||
- [x] Multi-return: return as list, unpack at call sites
|
||
- [x] Table constructors (array + hash + computed keys)
|
||
- [x] Raw table access `t.k` / `t[k]` (no metatables yet)
|
||
- [x] Vendor PUC-Rio 5.1.5 suite to `lib/lua/lua-tests/` (just `.lua` files)
|
||
- [x] `lib/lua/conformance.sh` + Python runner (model on `lib/js/test262-runner.py`)
|
||
- [x] `scoreboard.json` + `scoreboard.md` baseline
|
||
|
||
### Phase 4 — metatables + error handling (next run)
|
||
- [x] Metatable dispatch: `__index`, `__newindex`, `__add`/`__sub`/…, `__eq`, `__lt`, `__call`, `__tostring`, `__len`
|
||
- [x] `pcall`/`xpcall`/`error` via handler-bind
|
||
- [x] Generic `for … in …`
|
||
|
||
### Phase 5 — coroutines (the showcase)
|
||
- [x] `coroutine.create`/`.resume`/`.yield`/`.status`/`.wrap` via `perform`/`cek-resume`
|
||
|
||
### Phase 6 — standard library
|
||
- [x] `string` — `format`, `sub`, `find`, `match`, `gmatch`, `gsub`, `len`, `rep`, `upper`, `lower`, `byte`, `char`
|
||
- [x] `math` — full surface
|
||
- [x] `table` — `insert`, `remove`, `concat`, `sort`, `unpack`
|
||
- [x] `io` — minimal stub (read/write to SX IO surface)
|
||
- [x] `os` — time/date subset
|
||
|
||
### Phase 7 — modules + full conformance
|
||
- [x] `require` / `package` via SX `define-library`/`import`
|
||
- [ ] Drive PUC-Rio scoreboard to 100%
|
||
|
||
## Progress log
|
||
|
||
_Newest first. Agent appends on every commit._
|
||
|
||
- 2026-04-24: lua: scoreboard iteration — **`loadstring` scoping**. Temporarily instrumented `lua-assert` with a counter, found `locals.lua` fails at assertion #5: `loadstring('local a = {}')() → assert(type(a) ~= 'table')`. The loadstring'd code's `local a` was leaking to outer scope because `lua-eval-ast` ran at top-level. Fixed by transpiling once and wrapping the AST in `(let () …)` before `eval-expr`.
|
||
- 2026-04-24: lua: scoreboard iteration — **`if`/`else`/`elseif` body scoping** (latent bug). `else local x = 99` was leaking to enclosing scope. Wrap all three branches in `(let () …)` via `lua-tx-if-body`. 358 tests.
|
||
- 2026-04-24: lua: scoreboard iteration — **`do`-block proper scoping**. Was transpiling `do ... end` to a raw `lua-tx` pass-through, so `define`s inside leaked to the enclosing scope (`do local i = 100 end` overwrote outer `i`). Now wraps in `(let () body)` for proper lexical isolation. 355 tests, +2 scoping tests.
|
||
- 2026-04-24: lua: scoreboard iteration — `lua-to-number` trims whitespace before `parse-number` (Lua coerces `" 3e0 "` in arithmetic). math.lua moved past the arith-type error to deeper assertion-land. 12× asserts / 3× timeouts / 1× call-non-fn.
|
||
- 2026-04-24: lua: scoreboard iteration — `table.getn`/`setn`/`foreach`/`foreachi` (Lua 5.0-era), `string.reverse`. `sort.lua` unblocked past `getn`-undef; now times out on the 30k-element sort body (insertion sort too slow). 13 fail / 3 timeout / 0 pass.
|
||
- 2026-04-24: lua: scoreboard iteration — parser consumes trailing `;` after `return`; added `collectgarbage`/`setfenv`/`getfenv`/`T` stubs. All parse errors and undefined-symbol failures eliminated — every runnable test now executes deep into the script. Failure mix: **11× assertion failed**, 2× timeout, 2× call-non-fn, 1× arith. Still 0/16 pass but the remaining work is substantive (stdlib fidelity vs the exact PUC-Rio assertions).
|
||
- 2026-04-24: lua: scoreboard iteration — trailing-dot number literals (`5.`), preload stdlibs in `package.loaded` (`string`/`math`/`table`/`io`/`os`/`coroutine`/`package`/`_G`), `arg` stub, `debug` module stub. Assertion-failure count 4→**8**, parse errors 3→**1**, call-non-fn stable, module-not-found gone.
|
||
- 2026-04-24: lua: scoreboard iteration — **vararg `...` transpile**. Parser already emitted `(lua-vararg)`; transpile now: (a) binds `__varargs` in function body when `is-vararg`, (b) emits `__varargs` for `...` uses; `lua-varargs`/`lua-spread-last-multi` runtime helpers spread multi in last call-arg and last table-pos positions. Eliminated all 6× "transpile: unsupported" failures; top-5 now all real asserts. 353 unit tests.
|
||
- 2026-04-24: lua: scoreboard iteration — added `rawget`/`rawset`/`rawequal`/`rawlen`, `loadstring`/`load`, `select`, `assert`, `_G`, `_VERSION`. Failure mix now 6×vararg-transpile / 4×real-assertion / 3×parse / 2×call-non-fn / 1×timeout (was 14 parse + 1 print undef at baseline); tests now reach deep into real assertions. Still 0/16 runnable — next targets: vararg transpile, goto, loadstring-compile depth. 347 unit tests.
|
||
- 2026-04-24: lua: `require`/`package` via preload-only (no filesystem search). `package.loaded` caching, nil-returning modules cache as `true`, unknown modules error. 347 tests.
|
||
- 2026-04-24: lua: `os` stub — time/clock monotonic counter, difftime, date (default string / `*t` dict), getenv/remove/rename/tmpname/execute/exit stubs. Phase 6 complete. 342 tests.
|
||
- 2026-04-24: lua: `io` stub + `print`/`tostring`/`tonumber` globals. io buffers to internal `__io-buffer` (tests drain it via `io.__buffer()`). print: tab-sep + NL. tostring respects `__tostring` metamethod. 334 tests.
|
||
- 2026-04-24: lua: `table` lib — insert (append / at pos, shifts up), remove (last / at pos, shifts down), concat (sep, i, j), sort (insertion sort, optional cmp), unpack + table.unpack, maxn. Caught trap: local helper named `shift` collides with SX's `shift` special form → renamed to `tbl-shift-up`/`tbl-shift-down`. 322 tests.
|
||
- 2026-04-24: lua: `math` lib — pi/huge + abs/ceil/floor/sqrt/exp/log/log10/pow/trig (sin/cos/tan/asin/acos/atan/atan2)/deg/rad/min/max (&rest)/fmod/modf/random (0/1/2 arg)/randomseed. Most ops delegate to SX primitives; log w/ base via change-of-base. 309 tests.
|
||
- 2026-04-24: lua: `string` lib — len/upper/lower/rep/sub (1-idx + neg)/byte/char/find/match/gmatch/gsub/format. Patterns are literal-only (no `%d`/etc.); format is `%s`/`%d`/`%f`/`%%` only. `string.char` uses printable-ASCII lookup + tab/nl/cr. 292 tests.
|
||
- 2026-04-24: lua: phase 5 — coroutines (create/resume/yield/status/wrap) via `call/cc` (perform/cek-resume not exposed to SX userland). Handles multi-yield + final return + arg passthrough. Fix: body's final return must jump via `caller-k` to the **current** resume's caller, not unwind through the stale first-call continuation. 273 tests.
|
||
- 2026-04-24: lua: generic `for … in …` — parser split (`=` → num, else `in`), new `lua-for-in` node, transpile to `let`-bound `f,s,var` + recursive `__for_loop`. Added `ipairs`/`pairs`/`next`/`lua-arg` globals. Lua fns now arity-tolerant (`&rest __args` + indexed bind) — needed because generic for always calls iter with 2 args. Noted early-return-in-nested-block as pre-existing limitation. 265 tests.
|
||
- 2026-04-24: lua: `pcall`/`xpcall`/`error` via SX `guard` + `raise`. Added `lua-apply` (arity-dispatch 0-8, apply fallback) because SX `apply` re-wraps raises as "Unhandled exception". Table payloads preserved (`error({code = 42})`). 256 total tests.
|
||
- 2026-04-24: lua: phase 4 — metatable dispatch (`__index`/`__newindex`/arith/compare/`__call`/`__len`), `setmetatable`/`getmetatable`/`type` globals, OO `self:method` pattern. Transpile routes all calls through `lua-call` (stashed `sx-apply-ref` to dodge user-shadowing of SX `apply`). Skipped `__tostring` (needs `tostring()` builtin). 247 total tests.
|
||
- 2026-04-24: lua: PUC-Rio scoreboard baseline — 0/16 runnable pass (0.0%). Top modes: 14× parse error, 1× `print` undef, 1× vararg transpile. Phase 3 complete.
|
||
- 2026-04-24: lua: conformance runner — `conformance.sh` shim + `conformance.py` (long-lived sx_server, epoch protocol, classify_error, writes scoreboard.{json,md}). 24 files classified in full run: 8 skip / 16 fail / 0 timeout.
|
||
- 2026-04-24: lua: vendored PUC-Rio 5.1 test suite (lua5.1-tests.tar.gz from lua.org) to `lib/lua/lua-tests/` — 22 .lua files, 6304 lines; README kept for context.
|
||
- 2026-04-24: lua: raw table access — fix `lua-set!` to use `dict-set!` (mutating), fix `lua-len` `has?`→`has-key?`, `#t` works, mutation/chained/computed-key writes + reference semantics. 224 total tests.
|
||
- 2026-04-24: lua: phase 3 — table constructors verified (array, hash, computed keys, mixed, nested, dynamic values, fn values, sep variants). 205 total tests.
|
||
- 2026-04-24: lua: multi-return — `lua-multi` tagged value, `lua-first`/`lua-nth-ret`/`lua-pack-return` runtime, tail-position spread in return/local/assign. 185 total tests.
|
||
- 2026-04-24: lua: phase 3 — functions (anon/local/top-level) + closures verified (lexical capture, mutation-through-closure, recursion, HOFs). 175 total tests.
|
||
- 2026-04-24: lua: phase 2 transpile — arithmetic, comparison, short-circuit logical, `..` concat, if/while/repeat/for-num/local/assign. 157 total tests green.
|
||
- 2026-04-24: lua: parser (exprs with precedence, all phase-1 statements, funcbody, table ctors, method/chained calls) — 112 total tokenizer+parser tests
|
||
- 2026-04-24: lua: tokenizer (numbers/strings/long-brackets/keywords/ops/comments) + 56 tests
|
||
|
||
## Blockers
|
||
|
||
_Shared-file issues that need someone else to fix. Minimal repro only._
|
||
|
||
- _(none yet)_
|
||
|
||
## Known limitations (own code, not shared)
|
||
|
||
- **`require` supports `package.preload` only** — no filesystem search (we don't have Lua-file resolution inside sx_server). Users register a loader in `package.preload.name` and `require("name")` calls it with name as arg. Results cached in `package.loaded`; nil return caches as `true` per Lua convention.
|
||
- **`os` library is a stub** — `os.time()` returns a monotonic counter (not Unix epoch), `os.clock()` = counter/1000, `os.date()` returns hardcoded "1970-01-01 00:00:00" or a `*t` table with fixed fields; `os.getenv` returns nil; `os.remove`/`rename` return nil+error. No real clock/filesystem access.
|
||
- **`io` library is a stub** — `io.write`/`print` append to an internal `__io-buffer` (accessible via `io.__buffer()` which returns + clears it) instead of real stdout. `io.read`/`open`/`lines` return nil. Suitable for tests that inspect output; no actual stdio.
|
||
- **`string.find`/`match`/`gmatch`/`gsub` patterns are LITERAL only** — no `%d`/`%a`/`.`/`*`/`+`/etc. Implementing Lua patterns is a separate work item; literal search covers the common case.
|
||
- **`string.format`** supports only `%s`, `%d`, `%f`, `%%`. No width/precision flags (`%.2f`, `%5d`).
|
||
- **`string.char`** supports printable ASCII 32–126 plus `\t`/`\n`/`\r`; other codes error.
|
||
- **Early `return` inside nested block** — `if cond then return nil end ...rest` doesn't exit the enclosing function; `rest` runs anyway. Use `if cond then return X else return Y end` instead. Likely needs guard+raise sentinel for proper fix.
|