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
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 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.
|
||||
|
||||
Reference in New Issue
Block a user