lua: string library (len/upper/lower/rep/sub/byte/char/find/match/gmatch/gsub/format) +19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 18:45:03 +00:00
parent a5947e1295
commit 8c25527205
3 changed files with 293 additions and 1 deletions

View File

@@ -651,3 +651,230 @@
(dict-set! coroutine "yield" lua-coroutine-yield)
(dict-set! coroutine "status" lua-coroutine-status)
(dict-set! coroutine "wrap" lua-coroutine-wrap)
;; ── string library ────────────────────────────────────────────
(define string {})
(define __ascii-32-126 " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")
(define
lua-string-len
(fn (s) (len s)))
(define
lua-string-upper
(fn (s) (upcase s)))
(define
lua-string-lower
(fn (s) (downcase s)))
(define
lua-string-rep
(fn (s n)
(cond
((<= n 0) "")
((= n 1) s)
(else (str s (lua-string-rep s (- n 1)))))))
(define
lua-string-sub
(fn (&rest args)
(let ((s (first args))
(slen (len (first args)))
(i (nth args 1))
(j (if (> (len args) 2) (nth args 2) -1)))
(let ((ni (cond
((< i 0) (+ slen i 1))
((= i 0) 1)
(else i)))
(nj (cond
((< j 0) (+ slen j 1))
((> j slen) slen)
(else j))))
(let ((ci (if (< ni 1) 1 ni))
(cj (if (> nj slen) slen nj)))
(cond
((> ci cj) "")
(else (substring s (- ci 1) cj))))))))
(define
lua-string-byte
(fn (&rest args)
(let ((s (first args))
(i (if (> (len args) 1) (nth args 1) 1)))
(cond
((or (< i 1) (> i (len s))) nil)
(else (char-code (char-at s (- i 1))))))))
(define
lua-char-one
(fn (n)
(cond
((= n 9) "\t")
((= n 10) "\n")
((= n 13) "\r")
((and (>= n 32) (<= n 126)) (char-at __ascii-32-126 (- n 32)))
(else (error (str "lua: string.char out of range: " n))))))
(define
lua-string-char
(fn (&rest args)
(cond
((= (len args) 0) "")
(else
(let ((out ""))
(begin
(define
loop
(fn (i)
(when (< i (len args))
(begin
(set! out (str out (lua-char-one (nth args i))))
(loop (+ i 1))))))
(loop 0)
out))))))
;; Literal-only string.find: returns (start, end) 1-indexed or nil.
(define
lua-string-find
(fn (&rest args)
(let ((s (first args))
(pat (nth args 1))
(init (if (> (len args) 2) (nth args 2) 1)))
(let ((start-i (cond
((< init 0) (+ (len s) init 1))
((= init 0) 1)
(else init))))
(let ((sub (if (<= start-i 1) s (substring s (- start-i 1) (len s)))))
(let ((idx (index-of sub pat)))
(cond
((< idx 0) nil)
(else
(list
(quote lua-multi)
(+ start-i idx)
(+ start-i idx (len pat) -1))))))))))
;; Literal-only string.match: returns matched substring or nil (no captures since no pattern).
(define
lua-string-match
(fn (&rest args)
(let ((s (first args)) (pat (nth args 1)))
(let ((idx (index-of s pat)))
(cond
((< idx 0) nil)
(else pat))))))
;; Literal-only string.gmatch: iterator producing each literal match of pat.
(define
lua-string-gmatch
(fn (s pat)
(let ((pos 0))
(fn (&rest __)
(cond
((> pos (len s)) nil)
(else
(let ((rest-str (if (= pos 0) s (substring s pos (len s)))))
(let ((idx (index-of rest-str pat)))
(cond
((< idx 0) (begin (set! pos (+ (len s) 1)) nil))
(else
(begin
(set! pos (+ pos idx (len pat)))
pat)))))))))))
;; Literal-only string.gsub: replace all occurrences of pat with repl (string only for now).
(define
lua-string-gsub
(fn (&rest args)
(let ((s (first args))
(pat (nth args 1))
(repl (nth args 2))
(max-n (if (> (len args) 3) (nth args 3) -1)))
(cond
((= (len pat) 0) (list (quote lua-multi) s 0))
(else
(let ((out "") (pos 0) (count 0) (done false))
(begin
(define
loop
(fn ()
(when (and (not done) (<= pos (len s)))
(let ((rest-str (if (= pos 0) s (substring s pos (len s)))))
(let ((idx (index-of rest-str pat)))
(cond
((< idx 0)
(begin
(set! out (str out rest-str))
(set! done true)))
((and (>= max-n 0) (>= count max-n))
(begin
(set! out (str out rest-str))
(set! done true)))
(else
(let ((before (substring rest-str 0 idx)))
(begin
(set! out (str out before (if (= (type-of repl) "string") repl (str repl))))
(set! pos (+ pos idx (len pat)))
(set! count (+ count 1))
(loop))))))))))
(loop)
(list (quote lua-multi) out count))))))))
;; Basic string.format: %s %d %f (%%.Nf ignored), %%.
(define
lua-format-int
(fn (n)
(cond
((= (type-of n) "number") (str (floor n)))
(else (str n)))))
(define
lua-string-format
(fn (&rest args)
(let ((fmt (first args)) (vals (rest args)))
(let ((out "") (i 0) (vi 0))
(begin
(define
loop
(fn ()
(when (< i (len fmt))
(let ((c (char-at fmt i)))
(cond
((and (= c "%") (< (+ i 1) (len fmt)))
(let ((spec (char-at fmt (+ i 1))))
(cond
((= spec "%")
(begin (set! out (str out "%")) (set! i (+ i 2)) (loop)))
((= spec "s")
(begin
(set! out (str out (lua-concat-coerce (nth vals vi))))
(set! vi (+ vi 1)) (set! i (+ i 2)) (loop)))
((= spec "d")
(begin
(set! out (str out (lua-format-int (nth vals vi))))
(set! vi (+ vi 1)) (set! i (+ i 2)) (loop)))
((= spec "f")
(begin
(set! out (str out (str (nth vals vi))))
(set! vi (+ vi 1)) (set! i (+ i 2)) (loop)))
(else
(begin (set! out (str out c)) (set! i (+ i 1)) (loop))))))
(else
(begin (set! out (str out c)) (set! i (+ i 1)) (loop))))))))
(loop)
out)))))
(dict-set! string "len" lua-string-len)
(dict-set! string "upper" lua-string-upper)
(dict-set! string "lower" lua-string-lower)
(dict-set! string "rep" lua-string-rep)
(dict-set! string "sub" lua-string-sub)
(dict-set! string "byte" lua-string-byte)
(dict-set! string "char" lua-string-char)
(dict-set! string "find" lua-string-find)
(dict-set! string "match" lua-string-match)
(dict-set! string "gmatch" lua-string-gmatch)
(dict-set! string "gsub" lua-string-gsub)
(dict-set! string "format" lua-string-format)

View File

@@ -728,6 +728,46 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 1040)
(eval "(lua-eval-ast \"local function iter() coroutine.yield(10) coroutine.yield(20) coroutine.yield(30) end local co = coroutine.create(iter) local sum = 0 for i = 1, 3 do local ok, v = coroutine.resume(co) sum = sum + v end return sum\")")
;; ── Phase 6: string library ───────────────────────────────────
(epoch 1100)
(eval "(lua-eval-ast \"return string.len(\\\"hello\\\")\")")
(epoch 1101)
(eval "(lua-eval-ast \"return string.upper(\\\"hi\\\")\")")
(epoch 1102)
(eval "(lua-eval-ast \"return string.lower(\\\"HI\\\")\")")
(epoch 1103)
(eval "(lua-eval-ast \"return string.rep(\\\"ab\\\", 3)\")")
(epoch 1110)
(eval "(lua-eval-ast \"return string.sub(\\\"hello\\\", 2, 4)\")")
(epoch 1111)
(eval "(lua-eval-ast \"return string.sub(\\\"hello\\\", -3)\")")
(epoch 1112)
(eval "(lua-eval-ast \"return string.sub(\\\"hello\\\", 1, -2)\")")
(epoch 1120)
(eval "(lua-eval-ast \"return string.byte(\\\"A\\\")\")")
(epoch 1121)
(eval "(lua-eval-ast \"return string.byte(\\\"ABC\\\", 2)\")")
(epoch 1130)
(eval "(lua-eval-ast \"return string.char(72, 105)\")")
(epoch 1131)
(eval "(lua-eval-ast \"return string.char(97, 98, 99)\")")
(epoch 1140)
(eval "(lua-eval-ast \"local s, e = string.find(\\\"hello world\\\", \\\"wor\\\") return s * 100 + e\")")
(epoch 1141)
(eval "(lua-eval-ast \"if string.find(\\\"abc\\\", \\\"z\\\") == nil then return 1 else return 0 end\")")
(epoch 1150)
(eval "(lua-eval-ast \"return string.match(\\\"hello\\\", \\\"ell\\\")\")")
(epoch 1160)
(eval "(lua-eval-ast \"local r, n = string.gsub(\\\"abcabc\\\", \\\"a\\\", \\\"X\\\") return r .. \\\":\\\" .. n\")")
(epoch 1161)
(eval "(lua-eval-ast \"local r, n = string.gsub(\\\"aaaa\\\", \\\"a\\\", \\\"b\\\", 2) return r .. \\\":\\\" .. n\")")
(epoch 1170)
(eval "(lua-eval-ast \"local c = 0 for w in string.gmatch(\\\"aa aa aa\\\", \\\"aa\\\") do c = c + 1 end return c\")")
(epoch 1180)
(eval "(lua-eval-ast \"return string.format(\\\"%s=%d\\\", \\\"x\\\", 42)\")")
(epoch 1181)
(eval "(lua-eval-ast \"return string.format(\\\"%d%%\\\", 50)\")")
EPOCHS
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
@@ -1090,6 +1130,27 @@ check 1020 "resume dead returns error" '"cannot resume dead coroutine"'
check 1030 "coroutine.wrap" '6'
check 1040 "iterator via coroutine" '60'
# ── Phase 6: string library ───────────────────────────────────
check 1100 "string.len" '5'
check 1101 "string.upper" '"HI"'
check 1102 "string.lower" '"hi"'
check 1103 "string.rep" '"ababab"'
check 1110 "string.sub(s,i,j)" '"ell"'
check 1111 "string.sub(s,-3)" '"llo"'
check 1112 "string.sub(s,1,-2)" '"hell"'
check 1120 "string.byte" '65'
check 1121 "string.byte(s,i)" '66'
check 1130 "string.char(72,105)" '"Hi"'
check 1131 "string.char(97,98,99)" '"abc"'
check 1140 "string.find literal hit" '709'
check 1141 "string.find literal miss" '1'
check 1150 "string.match literal" '"ell"'
check 1160 "string.gsub replace all" '"XbcXbc:2"'
check 1161 "string.gsub with limit" '"bbaa:2"'
check 1170 "string.gmatch iterator" '3'
check 1180 "string.format %s=%d" '"x=42"'
check 1181 "string.format %d%%" '"50%"'
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo "ok $PASS/$TOTAL Lua-on-SX tests passed"

View File

@@ -68,7 +68,7 @@ Each item: implement → tests → tick box → update progress log.
- [x] `coroutine.create`/`.resume`/`.yield`/`.status`/`.wrap` via `perform`/`cek-resume`
### Phase 6 — standard library
- [ ] `string``format`, `sub`, `find`, `match`, `gmatch`, `gsub`, `len`, `rep`, `upper`, `lower`, `byte`, `char`
- [x] `string``format`, `sub`, `find`, `match`, `gmatch`, `gsub`, `len`, `rep`, `upper`, `lower`, `byte`, `char`
- [ ] `math` — full surface
- [ ] `table``insert`, `remove`, `concat`, `sort`, `unpack`
- [ ] `io` — minimal stub (read/write to SX IO surface)
@@ -82,6 +82,7 @@ Each item: implement → tests → tick box → update progress log.
_Newest first. Agent appends on every commit._
- 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.
@@ -105,4 +106,7 @@ _Shared-file issues that need someone else to fix. Minimal repro only._
## Known limitations (own code, not shared)
- **`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 32126 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.