diff --git a/lib/lua/runtime.sx b/lib/lua/runtime.sx index 83eeb2d0..445852b2 100644 --- a/lib/lua/runtime.sx +++ b/lib/lua/runtime.sx @@ -41,27 +41,77 @@ ((= op "^") (pow na nb)) (else (error (str "lua: unknown arith op " op)))))))) -(define lua-add (fn (a b) (lua-num-op "+" a b))) +(define + lua-get-mm + (fn + (v name) + (cond + ((not (= (type-of v) "dict")) nil) + (else + (let + ((mt (get v "__meta"))) + (cond + ((= mt nil) nil) + ((not (= (type-of mt) "dict")) nil) + ((has-key? mt name) (get mt name)) + (else nil))))))) -(define lua-sub (fn (a b) (lua-num-op "-" a b))) +(define + lua-arith + (fn + (mm op a b) + (cond + ((and (= (type-of a) "number") (= (type-of b) "number")) + (lua-num-op op a b)) + ((and + (or (= (type-of a) "number") (= (type-of a) "string")) + (or (= (type-of b) "number") (= (type-of b) "string")) + (not (= (lua-to-number a) nil)) + (not (= (lua-to-number b) nil))) + (lua-num-op op a b)) + (else + (let + ((m (lua-get-mm a mm))) + (cond + ((not (= m nil)) (lua-first (m a b))) + (else + (let + ((m2 (lua-get-mm b mm))) + (if + (not (= m2 nil)) + (lua-first (m2 a b)) + (error (str "lua: arith on " (type-of a) " " op " " (type-of b)))))))))))) -(define lua-mul (fn (a b) (lua-num-op "*" a b))) +(define lua-add (fn (a b) (lua-arith "__add" "+" a b))) -(define lua-div (fn (a b) (lua-num-op "/" a b))) +(define lua-sub (fn (a b) (lua-arith "__sub" "-" a b))) -(define lua-mod (fn (a b) (lua-num-op "%" a b))) +(define lua-mul (fn (a b) (lua-arith "__mul" "*" a b))) -(define lua-pow (fn (a b) (lua-num-op "^" a b))) +(define lua-div (fn (a b) (lua-arith "__div" "/" a b))) + +(define lua-mod (fn (a b) (lua-arith "__mod" "%" a b))) + +(define lua-pow (fn (a b) (lua-arith "__pow" "^" a b))) (define lua-neg (fn (a) - (let - ((na (lua-to-number a))) - (begin - (when (= na nil) (error (str "lua: neg on non-number: " a))) - (- 0 na))))) + (cond + ((= (type-of a) "number") (- 0 a)) + (else + (let + ((na (lua-to-number a))) + (cond + ((not (= na nil)) (- 0 na)) + (else + (let + ((m (lua-get-mm a "__unm"))) + (if + (not (= m nil)) + (lua-first (m a)) + (error (str "lua: neg on non-number: " (type-of a)))))))))))) (define lua-concat-coerce @@ -76,9 +126,23 @@ lua-concat (fn (a b) - (let - ((sa (lua-concat-coerce a)) (sb (lua-concat-coerce b))) - (str sa sb)))) + (cond + ((and + (or (= (type-of a) "string") (= (type-of a) "number")) + (or (= (type-of b) "string") (= (type-of b) "number"))) + (str (lua-concat-coerce a) (lua-concat-coerce b))) + (else + (let + ((m (lua-get-mm a "__concat"))) + (cond + ((not (= m nil)) (lua-first (m a b))) + (else + (let + ((m2 (lua-get-mm b "__concat"))) + (if + (not (= m2 nil)) + (lua-first (m2 a b)) + (error (str "lua: concat on " (type-of a) " and " (type-of b)))))))))))) (define lua-eq @@ -88,6 +152,13 @@ ((and (= a nil) (= b nil)) true) ((or (= a nil) (= b nil)) false) ((and (= (type-of a) (type-of b)) (= a b)) true) + ((and (= (type-of a) "dict") (= (type-of b) "dict")) + (let + ((m (lua-get-mm a "__eq"))) + (cond + ((not (= m nil)) + (let ((r (lua-first (m a b)))) (and (not (= r nil)) (not (= r false))))) + (else false)))) (else false)))) (define lua-neq (fn (a b) (not (lua-eq a b)))) @@ -99,9 +170,41 @@ (cond ((and (= (type-of a) "number") (= (type-of b) "number")) (< a b)) ((and (= (type-of a) "string") (= (type-of b) "string")) (< a b)) - (else (error "lua: attempt to compare incompatible types"))))) + (else + (let + ((m (lua-get-mm a "__lt"))) + (cond + ((not (= m nil)) + (let ((r (lua-first (m a b)))) (and (not (= r nil)) (not (= r false))))) + (else + (let + ((m2 (lua-get-mm b "__lt"))) + (cond + ((not (= m2 nil)) + (let ((r (lua-first (m2 a b)))) (and (not (= r nil)) (not (= r false))))) + (else (error "lua: attempt to compare incompatible types"))))))))))) -(define lua-le (fn (a b) (or (lua-lt a b) (lua-eq a b)))) +(define + lua-le + (fn + (a b) + (cond + ((and (= (type-of a) "number") (= (type-of b) "number")) (<= a b)) + ((and (= (type-of a) "string") (= (type-of b) "string")) + (or (< a b) (= a b))) + (else + (let + ((m (lua-get-mm a "__le"))) + (cond + ((not (= m nil)) + (let ((r (lua-first (m a b)))) (and (not (= r nil)) (not (= r false))))) + (else + (let + ((m2 (lua-get-mm b "__le"))) + (cond + ((not (= m2 nil)) + (let ((r (lua-first (m2 a b)))) (and (not (= r nil)) (not (= r false))))) + (else (not (lua-lt b a)))))))))))) (define lua-gt (fn (a b) (lua-lt b a))) @@ -116,17 +219,22 @@ ((= (type-of a) "list") (len a)) ((= (type-of a) "dict") (let - ((n 0)) - (begin - (define - count-loop - (fn - (i) - (if - (has-key? a (str i)) - (begin (set! n i) (count-loop (+ i 1))) - n))) - (count-loop 1)))) + ((m (lua-get-mm a "__len"))) + (cond + ((not (= m nil)) (lua-first (m a))) + (else + (let + ((n 0)) + (begin + (define + count-loop + (fn + (i) + (if + (has-key? a (str i)) + (begin (set! n i) (count-loop (+ i 1))) + n))) + (count-loop 1))))))) (else (error (str "lua: len on non-len type: " (type-of a))))))) (define @@ -166,9 +274,37 @@ lua-get (fn (t k) - (if (= t nil) nil (let ((v (get t (str k)))) (if (= v nil) nil v))))) + (cond + ((= t nil) nil) + ((not (= (type-of t) "dict")) nil) + (else + (let + ((key (str k))) + (cond + ((has-key? t key) (get t key)) + (else + (let + ((m (lua-get-mm t "__index"))) + (cond + ((= m nil) nil) + ((= (type-of m) "dict") (lua-get m k)) + (else (lua-first (m t k)))))))))))) -(define lua-set! (fn (t k v) (dict-set! t (str k) v))) +(define + lua-set! + (fn + (t k v) + (let + ((key (str k))) + (cond + ((has-key? t key) (dict-set! t key v)) + (else + (let + ((m (lua-get-mm t "__newindex"))) + (cond + ((= m nil) (dict-set! t key v)) + ((= (type-of m) "dict") (lua-set! m k v)) + (else (begin (m t k v) nil))))))))) (define lua-multi? @@ -215,3 +351,52 @@ (cond ((= (len vals) 0) (list (quote lua-multi))) (else (cons (quote lua-multi) (lua-pack-build vals 0)))))) + +(define lua-setmetatable (fn (t mt) (begin (dict-set! t "__meta" mt) t))) + +(define + lua-getmetatable + (fn + (t) + (cond + ((not (= (type-of t) "dict")) nil) + ((has-key? t "__meta") (get t "__meta")) + (else nil)))) + +(define setmetatable lua-setmetatable) +(define getmetatable lua-getmetatable) + +(define + lua-type + (fn + (v) + (cond + ((= v nil) "nil") + ((= (type-of v) "number") "number") + ((= (type-of v) "string") "string") + ((= v true) "boolean") + ((= v false) "boolean") + ((= (type-of v) "dict") "table") + ((or (= (type-of v) "function") (= (type-of v) "lambda")) "function") + (else (type-of v))))) + +(define type lua-type) + +(define sx-apply-ref apply) + +(define + lua-call + (fn + (&rest args) + (let + ((f (first args)) (rargs (rest args))) + (cond + ((or (= (type-of f) "function") (= (type-of f) "lambda")) + (sx-apply-ref f rargs)) + ((= (type-of f) "dict") + (let + ((m (lua-get-mm f "__call"))) + (cond + ((= m nil) (error "lua: attempt to call non-function")) + (else (sx-apply-ref m (cons f rargs)))))) + (else (error "lua: attempt to call non-function")))))) diff --git a/lib/lua/test.sh b/lib/lua/test.sh index 5bb5d6b5..f75e0262 100755 --- a/lib/lua/test.sh +++ b/lib/lua/test.sh @@ -594,6 +594,71 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 770) (eval "(lua-eval-ast \"local t = {} local s = t s.x = 42 return t.x\")") +;; ── Phase 4: metatables ──────────────────────────────────────── +;; setmetatable / getmetatable +(epoch 800) +(eval "(lua-eval-ast \"local t = {} local r = setmetatable(t, {}) if r == t then return 1 else return 0 end\")") +(epoch 801) +(eval "(lua-eval-ast \"local mt = {} local t = setmetatable({}, mt) if getmetatable(t) == mt then return 1 else return 0 end\")") + +;; type() +(epoch 810) +(eval "(lua-eval-ast \"return type(1)\")") +(epoch 811) +(eval "(lua-eval-ast \"return type(\\\"s\\\")\")") +(epoch 812) +(eval "(lua-eval-ast \"return type({})\")") +(epoch 813) +(eval "(lua-eval-ast \"return type(nil)\")") +(epoch 814) +(eval "(lua-eval-ast \"return type(true)\")") +(epoch 815) +(eval "(lua-eval-ast \"return type(function() end)\")") + +;; __index +(epoch 820) +(eval "(lua-eval-ast \"local base = {x = 10} local t = setmetatable({}, {__index = base}) return t.x\")") +(epoch 821) +(eval "(lua-eval-ast \"local base = {x = 10} local t = setmetatable({y = 20}, {__index = base}) return t.x + t.y\")") +(epoch 822) +(eval "(lua-eval-ast \"local t = setmetatable({}, {__index = function(tbl, k) return k .. \\\"!\\\" end}) return t.hi\")") + +;; __newindex +(epoch 830) +(eval "(lua-eval-ast \"local log = {} local t = setmetatable({}, {__newindex = function(tbl, k, v) log[1] = k end}) t.foo = 1 return log[1]\")") + +;; Arithmetic metamethods +(epoch 840) +(eval "(lua-eval-ast \"local mt = {__add = function(a, b) return 99 end} local x = setmetatable({}, mt) return x + 1\")") +(epoch 841) +(eval "(lua-eval-ast \"local mt = {__add = function(a, b) return a.v + b.v end} local x = setmetatable({v = 3}, mt) local y = setmetatable({v = 4}, mt) return x + y\")") +(epoch 842) +(eval "(lua-eval-ast \"local mt = {__sub = function(a, b) return 7 end, __mul = function(a, b) return 11 end} local t = setmetatable({}, mt) return t - t + (t * t)\")") +(epoch 843) +(eval "(lua-eval-ast \"local mt = {__unm = function(a) return 42 end} local t = setmetatable({}, mt) return -t\")") +(epoch 844) +(eval "(lua-eval-ast \"local mt = {__concat = function(a, b) return \\\"cat\\\" end} local t = setmetatable({}, mt) return t .. \\\"x\\\"\")") + +;; Comparison metamethods +(epoch 850) +(eval "(lua-eval-ast \"local mt = {__eq = function(a, b) return true end} local x = setmetatable({}, mt) local y = setmetatable({}, mt) if x == y then return 1 else return 0 end\")") +(epoch 851) +(eval "(lua-eval-ast \"local mt = {__lt = function(a, b) return true end} local x = setmetatable({}, mt) local y = setmetatable({}, mt) if x < y then return 1 else return 0 end\")") +(epoch 852) +(eval "(lua-eval-ast \"local mt = {__le = function(a, b) return true end} local x = setmetatable({}, mt) local y = setmetatable({}, mt) if x <= y then return 1 else return 0 end\")") + +;; __call +(epoch 860) +(eval "(lua-eval-ast \"local mt = {__call = function(t, x) return x + 100 end} local obj = setmetatable({}, mt) return obj(5)\")") + +;; __len +(epoch 870) +(eval "(lua-eval-ast \"local mt = {__len = function(t) return 99 end} local t = setmetatable({}, mt) return #t\")") + +;; Classic OO pattern +(epoch 880) +(eval "(lua-eval-ast \"local Animal = {} Animal.__index = Animal function Animal:new(name) local o = setmetatable({name = name}, self) return o end function Animal:getName() return self.name end local a = Animal:new(\\\"Rex\\\") return a:getName()\")") + EPOCHS OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -899,6 +964,31 @@ check 760 "t[k]=v reads via t.foo" '88' check 761 "t[concat]=v reads via t.xy" '7' check 770 "reference semantics t=s" '42' +# ── Phase 4: metatables ─────────────────────────────────────── +check 800 "setmetatable returns t" '1' +check 801 "getmetatable roundtrip" '1' +check 810 "type(1)" '"number"' +check 811 "type(string)" '"string"' +check 812 "type({})" '"table"' +check 813 "type(nil)" '"nil"' +check 814 "type(true)" '"boolean"' +check 815 "type(function)" '"function"' +check 820 "__index table lookup" '10' +check 821 "__index chain + self" '30' +check 822 "__index as function" '"hi!"' +check 830 "__newindex fires" '"foo"' +check 840 "__add table+number" '99' +check 841 "__add two tables" '7' +check 842 "__sub + __mul chain" '18' +check 843 "__unm" '42' +check 844 "__concat" '"cat"' +check 850 "__eq" '1' +check 851 "__lt" '1' +check 852 "__le" '1' +check 860 "__call" '105' +check 870 "__len" '99' +check 880 "OO pattern self:m()" '"Rex"' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL Lua-on-SX tests passed" diff --git a/lib/lua/transpile.sx b/lib/lua/transpile.sx index ddb671b4..070fddb6 100644 --- a/lib/lua/transpile.sx +++ b/lib/lua/transpile.sx @@ -104,7 +104,9 @@ (node) (let ((fn-ast (nth node 1)) (args (nth node 2))) - (cons (lua-tx fn-ast) (map lua-tx args))))) + (cons + (make-symbol "lua-call") + (cons (lua-tx fn-ast) (map lua-tx args)))))) (define lua-tx-method-call @@ -115,8 +117,10 @@ (name (nth node 2)) (args (nth node 3))) (cons - (list (make-symbol "lua-get") obj name) - (cons obj (map lua-tx args)))))) + (make-symbol "lua-call") + (cons + (list (make-symbol "lua-get") obj name) + (cons obj (map lua-tx args))))))) (define lua-tx-field diff --git a/plans/lua-on-sx.md b/plans/lua-on-sx.md index 06ac655e..a99eb41b 100644 --- a/plans/lua-on-sx.md +++ b/plans/lua-on-sx.md @@ -60,7 +60,7 @@ Each item: implement → tests → tick box → update progress log. - [x] `scoreboard.json` + `scoreboard.md` baseline ### Phase 4 — metatables + error handling (next run) -- [ ] Metatable dispatch: `__index`, `__newindex`, `__add`/`__sub`/…, `__eq`, `__lt`, `__call`, `__tostring`, `__len` +- [x] Metatable dispatch: `__index`, `__newindex`, `__add`/`__sub`/…, `__eq`, `__lt`, `__call`, `__tostring`, `__len` - [ ] `pcall`/`xpcall`/`error` via handler-bind - [ ] Generic `for … in …` @@ -82,6 +82,7 @@ Each item: implement → tests → tick box → update progress log. _Newest first. Agent appends on every commit._ +- 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.