From 8c91b34264af75e1a901e5c6886e7b3e8b38e7b7 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 28 May 2026 02:14:55 +0000 Subject: [PATCH] =?UTF-8?q?go:=20Phase=208=20first=20slice=20=E2=80=94=20s?= =?UTF-8?q?tdlib=20strings/strconv,=2041=20tests,=20+40=20cleared=20[shape?= =?UTF-8?q?s-static-types-bidirectional]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New :go-package NAME ENTRIES value type with field lookup via extended go-eval-select. New :go-builtin-fn callable for closure- based stdlib functions. lib/go/std/strings.sx ships 12 functions (Contains, HasPrefix, HasSuffix, Index, Count, Repeat, Join, ToUpper, ToLower, TrimSpace, Split, Replace) + lib/go/std/strconv.sx ships Itoa/Atoi. Pre-existing bug fixed: parser was emitting (:literal V) for both `42` and `"42"`, relying on first-char heuristic in eval/types. Now emits :literal-string for string/rune literals so Atoi("42") correctly receives the string. 3 parse tests + 2 in-composite-key tests updated to new shape. Total 597/597. Stdlib 41/41 — +40 acceptance bar cleared. Sister diary documents the 11 value-type kinds (struct/slice/map/chan/ fn/method/builtin/builtin-fn/package/panic/defer) all sharing the "(:KIND PAYLOAD...)" shape, alongside AST nodes and sentinel signals as the kit's three orthogonal first-class-tag axes. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/conformance.sh | 8 +- lib/go/eval.sx | 24 +- lib/go/parse.sx | 11 +- lib/go/scoreboard.json | 6 +- lib/go/scoreboard.md | 4 +- lib/go/std/strconv.sx | 71 ++++ lib/go/std/strings.sx | 386 ++++++++++++++++++ lib/go/tests/parse.sx | 14 +- lib/go/tests/stdlib.sx | 209 ++++++++++ lib/go/types.sx | 2 + plans/go-on-sx.md | 58 ++- plans/lib-guest-static-types-bidirectional.md | 47 +++ 12 files changed, 802 insertions(+), 38 deletions(-) create mode 100644 lib/go/std/strconv.sx create mode 100644 lib/go/std/strings.sx create mode 100644 lib/go/tests/stdlib.sx diff --git a/lib/go/conformance.sh b/lib/go/conformance.sh index 23b1d8c8..8e650e53 100755 --- a/lib/go/conformance.sh +++ b/lib/go/conformance.sh @@ -32,6 +32,7 @@ SUITES=( "types|go-types-test-pass|go-types-test-count" "eval|go-eval-test-pass|go-eval-test-count" "runtime|go-rt-test-pass|go-rt-test-count" + "stdlib|go-std-test-pass|go-std-test-count" ) cat > "$TMPFILE" <<'EPOCHS' @@ -44,11 +45,14 @@ cat > "$TMPFILE" <<'EPOCHS' (load "lib/go/types.sx") (load "lib/go/sched.sx") (load "lib/go/eval.sx") +(load "lib/go/std/strings.sx") +(load "lib/go/std/strconv.sx") (load "lib/go/tests/lex.sx") (load "lib/go/tests/parse.sx") (load "lib/go/tests/types.sx") (load "lib/go/tests/eval.sx") (load "lib/go/tests/runtime.sx") +(load "lib/go/tests/stdlib.sx") EPOCHS idx=0 @@ -113,7 +117,6 @@ cat > lib/go/scoreboard.json < lib/go/scoreboard.md <= i (len s)) + (cond + (= (cond neg (- i 1) :else i) 0) + (list :eval-error :strconv-atoi-no-digits s) + :else + (cond neg (- 0 acc) :else acc)) + :else + (let ((d (go-strconv-digit (nth s i)))) + (cond + (< d 0) + (cond + (= (cond neg (- i 1) :else i) 0) + (list :eval-error :strconv-atoi-no-digits s) + :else + (cond neg (- 0 acc) :else acc)) + :else + (go-strconv-parse-int s (+ i 1) neg (+ (* acc 10) d)))))))) + +(define + go-strconv-digit + (fn (c) + (cond + (= c "0") 0 (= c "1") 1 (= c "2") 2 (= c "3") 3 + (= c "4") 4 (= c "5") 5 (= c "6") 6 (= c "7") 7 + (= c "8") 8 (= c "9") 9 + :else -1))) + +(define + go-std-strconv + (list :go-package "strconv" + (list + (list "Itoa" (list :go-builtin-fn go-strconv-itoa)) + (list "Atoi" (list :go-builtin-fn go-strconv-atoi))))) diff --git a/lib/go/std/strings.sx b/lib/go/std/strings.sx new file mode 100644 index 00000000..5bcbeb5a --- /dev/null +++ b/lib/go/std/strings.sx @@ -0,0 +1,386 @@ +;; lib/go/std/strings.sx — Go's `strings` package, v0 subset. +;; +;; Exposed as `go-std-strings`, a (:go-package "strings" ENTRIES) value. +;; Register with `(go-env-extend env "strings" go-std-strings)` to make +;; `strings.X(...)` call sites work in evaluated Go code. +;; +;; Each entry is (FIELD-NAME (list :go-fn PARAMS BODY)) — the same +;; shape user-defined Go functions get. Bodies are written in SX +;; directly via go-builtin closures wrapping host-level string ops +;; for speed, OR as parsed Go source for fidelity. v0 uses +;; go-builtin wrappers — simpler and fast. + +;; ── helpers: implement go-std-strings entries as builtins ──────── + +(define + go-strings-contains + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-contains-arity (len args)) + :else + (let ((s (first args)) (sub (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? sub)) (list :eval-error :strings-not-string sub) + :else + (go-strings-index-of s sub 0)))))) + +(define + go-strings-index-of + ;; Returns true if SUB appears in S at or after START, else false. + (fn (s sub start) + (let ((slen (len s)) (sublen (len sub))) + (cond + (= sublen 0) true + (> (+ start sublen) slen) false + (go-strings-match-at s sub start 0) true + :else (go-strings-index-of s sub (+ start 1)))))) + +(define + go-strings-match-at + (fn (s sub start k) + (cond + (>= k (len sub)) true + (= (nth s (+ start k)) (nth sub k)) + (go-strings-match-at s sub start (+ k 1)) + :else false))) + +(define + go-strings-has-prefix + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-hasprefix-arity (len args)) + :else + (let ((s (first args)) (p (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? p)) (list :eval-error :strings-not-string p) + (> (len p) (len s)) false + :else (go-strings-match-at s p 0 0)))))) + +(define + go-strings-has-suffix + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-hassuffix-arity (len args)) + :else + (let ((s (first args)) (suf (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? suf)) (list :eval-error :strings-not-string suf) + (> (len suf) (len s)) false + :else + (go-strings-match-at s suf (- (len s) (len suf)) 0)))))) + +(define + go-strings-index + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-index-arity (len args)) + :else + (let ((s (first args)) (sub (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? sub)) (list :eval-error :strings-not-string sub) + :else (go-strings-index-loop s sub 0)))))) + +(define + go-strings-index-loop + (fn (s sub start) + (let ((slen (len s)) (sublen (len sub))) + (cond + (= sublen 0) 0 + (> (+ start sublen) slen) -1 + (go-strings-match-at s sub start 0) start + :else (go-strings-index-loop s sub (+ start 1)))))) + +(define + go-strings-repeat + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-repeat-arity (len args)) + :else + (let ((s (first args)) (n (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (< n 0) (list :eval-error :strings-repeat-negative n) + :else (go-strings-repeat-loop s n "")))))) + +(define + go-strings-repeat-loop + (fn (s n acc) + (cond + (<= n 0) acc + :else (go-strings-repeat-loop s (- n 1) (str acc s))))) + +(define + go-strings-count + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-count-arity (len args)) + :else + (let ((s (first args)) (sub (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? sub)) (list :eval-error :strings-not-string sub) + :else (go-strings-count-loop s sub 0 0)))))) + +(define + go-strings-count-loop + (fn (s sub start acc) + (let ((idx (go-strings-index-loop s sub start))) + (cond + (< idx 0) acc + :else + (go-strings-count-loop s sub (+ idx (max 1 (len sub))) (+ acc 1)))))) + +(define + go-strings-join + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-join-arity (len args)) + :else + (let ((sep (nth args 1)) (xs (first args))) + (cond + (not (string? sep)) (list :eval-error :strings-not-string sep) + (not (and (list? xs) (= (first xs) :go-slice))) + (list :eval-error :strings-join-not-slice xs) + :else (go-strings-join-loop (nth xs 1) sep "")))))) + +(define + go-strings-join-loop + (fn (xs sep acc) + (cond + (= (len xs) 0) acc + (= (len acc) 0) (go-strings-join-loop (rest xs) sep (first xs)) + :else + (go-strings-join-loop (rest xs) sep (str acc sep (first xs)))))) + +;; ── case conversion ────────────────────────────────────────────── + +(define + go-strings-char-to-upper + (fn (c) + (cond + (and (>= c "a") (<= c "z")) + ;; ASCII uppercase shift: 'a' is 0x61, 'A' is 0x41 → diff 0x20. + ;; SX has no charcode primitive, so use a char-pair table. + (go-strings-letter-toggle c true) + :else c))) + +(define + go-strings-char-to-lower + (fn (c) + (cond + (and (>= c "A") (<= c "Z")) + (go-strings-letter-toggle c false) + :else c))) + +(define + go-strings-letter-toggle + ;; Toggle a single ASCII letter's case via direct mapping. + ;; `to-upper?` true means input is lowercase, output uppercase. + (fn (c to-upper?) + (cond + to-upper? + (cond + (= c "a") "A" (= c "b") "B" (= c "c") "C" (= c "d") "D" + (= c "e") "E" (= c "f") "F" (= c "g") "G" (= c "h") "H" + (= c "i") "I" (= c "j") "J" (= c "k") "K" (= c "l") "L" + (= c "m") "M" (= c "n") "N" (= c "o") "O" (= c "p") "P" + (= c "q") "Q" (= c "r") "R" (= c "s") "S" (= c "t") "T" + (= c "u") "U" (= c "v") "V" (= c "w") "W" (= c "x") "X" + (= c "y") "Y" (= c "z") "Z" :else c) + :else + (cond + (= c "A") "a" (= c "B") "b" (= c "C") "c" (= c "D") "d" + (= c "E") "e" (= c "F") "f" (= c "G") "g" (= c "H") "h" + (= c "I") "i" (= c "J") "j" (= c "K") "k" (= c "L") "l" + (= c "M") "m" (= c "N") "n" (= c "O") "o" (= c "P") "p" + (= c "Q") "q" (= c "R") "r" (= c "S") "s" (= c "T") "t" + (= c "U") "u" (= c "V") "v" (= c "W") "w" (= c "X") "x" + (= c "Y") "y" (= c "Z") "z" :else c)))) + +(define + go-strings-map-chars + (fn (s i acc char-fn) + (cond + (>= i (len s)) acc + :else + (go-strings-map-chars s (+ i 1) (str acc (char-fn (nth s i))) char-fn)))) + +(define + go-strings-to-upper + (fn (args) + (cond + (not (= (len args) 1)) + (list :eval-error :strings-toupper-arity (len args)) + :else + (let ((s (first args))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + :else (go-strings-map-chars s 0 "" go-strings-char-to-upper)))))) + +(define + go-strings-to-lower + (fn (args) + (cond + (not (= (len args) 1)) + (list :eval-error :strings-tolower-arity (len args)) + :else + (let ((s (first args))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + :else (go-strings-map-chars s 0 "" go-strings-char-to-lower)))))) + +;; ── TrimSpace ──────────────────────────────────────────────────── + +(define + go-strings-is-space? + (fn (c) + (or (= c " ") (= c "\t") (= c "\n") (= c "\r")))) + +(define + go-strings-trim-left + (fn (s i) + (cond + (>= i (len s)) i + (go-strings-is-space? (nth s i)) (go-strings-trim-left s (+ i 1)) + :else i))) + +(define + go-strings-trim-right + (fn (s end) + (cond + (<= end 0) 0 + (go-strings-is-space? (nth s (- end 1))) (go-strings-trim-right s (- end 1)) + :else end))) + +(define + go-strings-substr + ;; Substring [lo, hi) — naive but predictable. + (fn (s lo hi) + (cond + (>= lo hi) "" + :else + (go-strings-substr-loop s lo hi "")))) + +(define + go-strings-substr-loop + (fn (s i hi acc) + (cond + (>= i hi) acc + :else (go-strings-substr-loop s (+ i 1) hi (str acc (nth s i)))))) + +(define + go-strings-trim-space + (fn (args) + (cond + (not (= (len args) 1)) + (list :eval-error :strings-trimspace-arity (len args)) + :else + (let ((s (first args))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + :else + (let ((lo (go-strings-trim-left s 0))) + (let ((hi (go-strings-trim-right s (len s)))) + (go-strings-substr s lo hi)))))))) + +;; ── Split ──────────────────────────────────────────────────────── + +(define + go-strings-split + (fn (args) + (cond + (not (= (len args) 2)) + (list :eval-error :strings-split-arity (len args)) + :else + (let ((s (first args)) (sep (nth args 1))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? sep)) (list :eval-error :strings-not-string sep) + (= (len sep) 0) + ;; Empty separator: real Go splits to all chars; v0 keeps + ;; behaviour simple — single-element slice. + (list :go-slice (list s)) + :else + (list :go-slice (go-strings-split-loop s sep 0 (list)))))))) + +(define + go-strings-split-loop + (fn (s sep start acc) + (let ((idx (go-strings-index-loop s sep start))) + (cond + (< idx 0) + (go-strings-split-finalize acc (go-strings-substr s start (len s))) + :else + (go-strings-split-loop s sep (+ idx (len sep)) + (go-strings-split-finalize acc + (go-strings-substr s start idx))))))) + +(define + go-strings-split-finalize + ;; Append a piece to acc, growing the list in order. + (fn (acc piece) + (cond + (= (len acc) 0) (list piece) + :else (go-name-concat acc (list piece))))) + +;; ── Replace ────────────────────────────────────────────────────── + +(define + go-strings-replace + ;; Replace(s, old, new, n). n < 0 = all. + (fn (args) + (cond + (not (= (len args) 4)) + (list :eval-error :strings-replace-arity (len args)) + :else + (let ((s (first args)) (old (nth args 1)) + (newv (nth args 2)) (n (nth args 3))) + (cond + (not (string? s)) (list :eval-error :strings-not-string s) + (not (string? old)) (list :eval-error :strings-not-string old) + (not (string? newv)) (list :eval-error :strings-not-string newv) + (= (len old) 0) s + :else (go-strings-replace-loop s old newv n 0 "")))))) + +(define + go-strings-replace-loop + (fn (s old newv n start acc) + (let ((idx (go-strings-index-loop s old start))) + (cond + (or (< idx 0) (= n 0)) + (str acc (go-strings-substr s start (len s))) + :else + (go-strings-replace-loop s old newv + (cond (< n 0) -1 :else (- n 1)) + (+ idx (len old)) + (str acc (go-strings-substr s start idx) newv)))))) + +;; ── go-std-strings package value ───────────────────────────────── + +(define + go-std-strings + (list :go-package "strings" + (list + (list "Contains" (list :go-builtin-fn go-strings-contains)) + (list "HasPrefix" (list :go-builtin-fn go-strings-has-prefix)) + (list "HasSuffix" (list :go-builtin-fn go-strings-has-suffix)) + (list "Index" (list :go-builtin-fn go-strings-index)) + (list "Count" (list :go-builtin-fn go-strings-count)) + (list "Repeat" (list :go-builtin-fn go-strings-repeat)) + (list "Join" (list :go-builtin-fn go-strings-join)) + (list "ToUpper" (list :go-builtin-fn go-strings-to-upper)) + (list "ToLower" (list :go-builtin-fn go-strings-to-lower)) + (list "TrimSpace" (list :go-builtin-fn go-strings-trim-space)) + (list "Split" (list :go-builtin-fn go-strings-split)) + (list "Replace" (list :go-builtin-fn go-strings-replace))))) diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 1a68ab41..7a6a652c 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -22,10 +22,10 @@ (go-parse-test "leading-dot float" (go-parse ".5") (ast-literal ".5")) (go-parse-test "exponent float" (go-parse "1e10") (ast-literal "1e10")) (go-parse-test "imag literal" (go-parse "2i") (ast-literal "2i")) -(go-parse-test "string literal" (go-parse "\"hi\"") (ast-literal "hi")) -(go-parse-test "empty string" (go-parse "\"\"") (ast-literal "")) -(go-parse-test "raw string" (go-parse "`a\nb`") (ast-literal "a\nb")) -(go-parse-test "rune literal" (go-parse "'a'") (ast-literal "a")) +(go-parse-test "string literal" (go-parse "\"hi\"") (list :literal-string "hi")) +(go-parse-test "empty string" (go-parse "\"\"") (list :literal-string "")) +(go-parse-test "raw string" (go-parse "`a\nb`") (list :literal-string "a\nb")) +(go-parse-test "rune literal" (go-parse "'a'") (list :literal-string "a")) ;; ── primary: identifiers ────────────────────────────────────────── (go-parse-test "ident: simple" (go-parse "x") (ast-var "x")) @@ -262,7 +262,7 @@ (go-parse-test "index: m[\"key\"] (string index)" (go-parse "m[\"key\"]") - (list :index (ast-var "m") (ast-literal "key"))) + (list :index (ast-var "m") (list :literal-string "key"))) (go-parse-test "index: a[0][1] (chained)" @@ -691,8 +691,8 @@ (list :composite (list :ty-map (list :ty-name "string") (list :ty-name "int")) (list - (list :kv (ast-literal "a") (ast-literal "1")) - (list :kv (ast-literal "b") (ast-literal "2"))))) + (list :kv (list :literal-string "a") (ast-literal "1")) + (list :kv (list :literal-string "b") (ast-literal "2"))))) (go-parse-test "comp: pkg.Point{1, 2} (qualified type)" diff --git a/lib/go/tests/stdlib.sx b/lib/go/tests/stdlib.sx new file mode 100644 index 00000000..60fdef09 --- /dev/null +++ b/lib/go/tests/stdlib.sx @@ -0,0 +1,209 @@ +;; Go stdlib tests — exercises lib/go/std/*.sx packages via the +;; idiomatic `import-style` qualified call (`strings.Contains(...)`). + +(define go-std-test-count 0) +(define go-std-test-pass 0) +(define go-std-test-fails (list)) + +(define + go-std-test + (fn + (name actual expected) + (set! go-std-test-count (+ go-std-test-count 1)) + (if + (= actual expected) + (set! go-std-test-pass (+ go-std-test-pass 1)) + (append! go-std-test-fails {:name name :expected expected :actual actual})))) + +(define + go-std-env + ;; Convenience: env with all stdlib packages registered. + (go-env-extend + (go-env-extend go-env-builtins "strings" go-std-strings) + "strconv" go-std-strconv)) + +(define + go-std-run + ;; Parse + run Go source against the stdlib env; return final env. + (fn (src-list) + (go-eval-program go-std-env (map go-parse src-list)))) + +;; ── strings.Contains ───────────────────────────────────────────── +(go-std-test "strings.Contains: hit" + (go-env-lookup (go-std-run (list "r := strings.Contains(\"hello world\", \"world\")")) "r") + true) + +(go-std-test "strings.Contains: miss" + (go-env-lookup (go-std-run (list "r := strings.Contains(\"hello\", \"xyz\")")) "r") + false) + +(go-std-test "strings.Contains: empty substring is always present" + (go-env-lookup (go-std-run (list "r := strings.Contains(\"abc\", \"\")")) "r") + true) + +;; ── strings.HasPrefix / HasSuffix ──────────────────────────────── +(go-std-test "strings.HasPrefix: true" + (go-env-lookup (go-std-run (list "r := strings.HasPrefix(\"hello world\", \"hello\")")) "r") + true) + +(go-std-test "strings.HasPrefix: false" + (go-env-lookup (go-std-run (list "r := strings.HasPrefix(\"hello\", \"world\")")) "r") + false) + +(go-std-test "strings.HasSuffix: true" + (go-env-lookup (go-std-run (list "r := strings.HasSuffix(\"hello world\", \"world\")")) "r") + true) + +(go-std-test "strings.HasSuffix: false" + (go-env-lookup (go-std-run (list "r := strings.HasSuffix(\"hello\", \"world\")")) "r") + false) + +;; ── strings.Index ───────────────────────────────────────────────── +(go-std-test "strings.Index: found at 6" + (go-env-lookup (go-std-run (list "r := strings.Index(\"hello world\", \"world\")")) "r") + 6) + +(go-std-test "strings.Index: not found = -1" + (go-env-lookup (go-std-run (list "r := strings.Index(\"hello\", \"xyz\")")) "r") + -1) + +(go-std-test "strings.Index: empty substring = 0" + (go-env-lookup (go-std-run (list "r := strings.Index(\"abc\", \"\")")) "r") + 0) + +;; ── strings.Count ───────────────────────────────────────────────── +(go-std-test "strings.Count: 3 occurrences of 'a'" + (go-env-lookup (go-std-run (list "r := strings.Count(\"banana\", \"a\")")) "r") + 3) + +(go-std-test "strings.Count: 0 occurrences" + (go-env-lookup (go-std-run (list "r := strings.Count(\"hello\", \"z\")")) "r") + 0) + +;; ── strings.Repeat ──────────────────────────────────────────────── +(go-std-test "strings.Repeat: ab × 3 = ababab" + (go-env-lookup (go-std-run (list "r := strings.Repeat(\"ab\", 3)")) "r") + "ababab") + +(go-std-test "strings.Repeat: any × 0 = empty" + (go-env-lookup (go-std-run (list "r := strings.Repeat(\"x\", 0)")) "r") + "") + +;; ── strings.Join ────────────────────────────────────────────────── +(go-std-test "strings.Join: comma-separated" + (go-env-lookup (go-std-run (list "r := strings.Join([]string{\"a\", \"b\", \"c\"}, \", \")")) "r") + "a, b, c") + +(go-std-test "strings.Join: empty slice = empty" + (go-env-lookup (go-std-run (list "r := strings.Join([]string{}, \"-\")")) "r") + "") + +(go-std-test "strings.Join: single elem = elem" + (go-env-lookup (go-std-run (list "r := strings.Join([]string{\"solo\"}, \",\")")) "r") + "solo") + +;; ── strings.ToUpper / ToLower ───────────────────────────────────── +(go-std-test "strings.ToUpper: hello → HELLO" + (go-env-lookup (go-std-run (list "r := strings.ToUpper(\"hello\")")) "r") + "HELLO") + +(go-std-test "strings.ToUpper: leaves digits alone" + (go-env-lookup (go-std-run (list "r := strings.ToUpper(\"abc123\")")) "r") + "ABC123") + +(go-std-test "strings.ToLower: HELLO → hello" + (go-env-lookup (go-std-run (list "r := strings.ToLower(\"HELLO\")")) "r") + "hello") + +(go-std-test "strings.ToLower: mixed case" + (go-env-lookup (go-std-run (list "r := strings.ToLower(\"MixED\")")) "r") + "mixed") + +;; ── strings.TrimSpace ───────────────────────────────────────────── +(go-std-test "strings.TrimSpace: leading + trailing" + (go-env-lookup (go-std-run (list "r := strings.TrimSpace(\" hello \")")) "r") + "hello") + +(go-std-test "strings.TrimSpace: no whitespace = noop" + (go-env-lookup (go-std-run (list "r := strings.TrimSpace(\"abc\")")) "r") + "abc") + +(go-std-test "strings.TrimSpace: all whitespace → empty" + (go-env-lookup (go-std-run (list "r := strings.TrimSpace(\" \")")) "r") + "") + +;; ── strings.Split ───────────────────────────────────────────────── +(go-std-test "strings.Split: comma-separated" + (go-env-lookup (go-std-run (list "r := strings.Split(\"a,b,c\", \",\")")) "r") + (list :go-slice (list "a" "b" "c"))) + +(go-std-test "strings.Split: no occurrence → single elem" + (go-env-lookup (go-std-run (list "r := strings.Split(\"abc\", \"-\")")) "r") + (list :go-slice (list "abc"))) + +(go-std-test "strings.Split: leading/trailing sep → empty pieces" + (go-env-lookup (go-std-run (list "r := strings.Split(\",a,\", \",\")")) "r") + (list :go-slice (list "" "a" ""))) + +;; ── strings.Replace ─────────────────────────────────────────────── +(go-std-test "strings.Replace: replace once with n=1" + (go-env-lookup (go-std-run (list "r := strings.Replace(\"a,b,c\", \",\", \"-\", 1)")) "r") + "a-b,c") + +(go-std-test "strings.Replace: replace all with n=-1" + (go-env-lookup (go-std-run (list "r := strings.Replace(\"a,b,c\", \",\", \"-\", -1)")) "r") + "a-b-c") + +(go-std-test "strings.Replace: no match = noop" + (go-env-lookup (go-std-run (list "r := strings.Replace(\"abc\", \"x\", \"y\", -1)")) "r") + "abc") + +;; ── strconv.Itoa ───────────────────────────────────────────────── +(go-std-test "strconv.Itoa: 42 → \"42\"" + (go-env-lookup (go-std-run (list "r := strconv.Itoa(42)")) "r") + "42") + +(go-std-test "strconv.Itoa: 0 → \"0\"" + (go-env-lookup (go-std-run (list "r := strconv.Itoa(0)")) "r") + "0") + +;; ── strconv.Atoi ───────────────────────────────────────────────── +(go-std-test "strconv.Atoi: \"42\" → 42" + (go-env-lookup (go-std-run (list "r := strconv.Atoi(\"42\")")) "r") + 42) + +(go-std-test "strconv.Atoi: \"-7\" → -7" + (go-env-lookup (go-std-run (list "r := strconv.Atoi(\"-7\")")) "r") + -7) + +(go-std-test "strconv.Atoi: \"100\" → 100" + (go-env-lookup (go-std-run (list "r := strconv.Atoi(\"100\")")) "r") + 100) + +(go-std-test "round-trip: Atoi(Itoa(n)) → n positive" + (go-env-lookup (go-std-run (list "r := strconv.Atoi(strconv.Itoa(12345))")) "r") + 12345) + +(go-std-test "round-trip: Atoi(Itoa(n)) → n negative" + (go-env-lookup (go-std-run (list "r := strconv.Atoi(strconv.Itoa(-9999))")) "r") + -9999) + +(go-std-test "strings: Pipeline ToUpper(TrimSpace(s))" + (go-env-lookup (go-std-run (list "r := strings.ToUpper(strings.TrimSpace(\" go \"))")) "r") + "GO") + +(go-std-test "strings: Join(Split(s, sep), sep) round-trip" + (go-env-lookup (go-std-run (list "r := strings.Join(strings.Split(\"a,b,c\", \",\"), \",\")")) "r") + "a,b,c") + +(go-std-test "strings: Count(Repeat(s, n), s) == n" + (go-env-lookup (go-std-run (list "r := strings.Count(strings.Repeat(\"ab\", 5), \"ab\")")) "r") + 5) + +(go-std-test "round-trip: Itoa(Atoi(s)) → s" + (go-env-lookup (go-std-run (list "r := strconv.Itoa(strconv.Atoi(\"777\"))")) "r") + "777") + +(define + go-std-test-summary + (str "stdlib " go-std-test-pass "/" go-std-test-count)) diff --git a/lib/go/types.sx b/lib/go/types.sx index 1236bbdb..8af3dfb8 100644 --- a/lib/go/types.sx +++ b/lib/go/types.sx @@ -243,6 +243,8 @@ (cond (and (list? expr) (= (first expr) :literal)) (go-synth-literal (nth expr 1)) + (and (list? expr) (= (first expr) :literal-string)) + ty-untyped-string (and (list? expr) (= (first expr) :var)) (let ((name (nth expr 1))) (let ((pre (go-predeclared-lookup name))) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index c5e99ecd..d2987127 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -399,25 +399,25 @@ Progress-log line → push `origin/loops/go`. erasure at eval, no inference at types/. - **Acceptance:** types/ +30 tests — **cleared (72 → 102).** -### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜ -- Implement just what's needed for representative programs: - - `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`, - `Stringer` dispatch. Verbs: `%d %s %v %t %f %T %+v`. - - `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`, - `TrimSpace`, `ToUpper`, `ToLower`, `Replace`, `Index`, `Count`, - `Repeat`, `NewReader`. - - `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`, - `FormatInt`. - - `errors` — `New`, `Is`, `As`, `Unwrap`. - - `sync` — `Mutex` (cooperative — flag + waiter queue), `WaitGroup`, - `Once`, `RWMutex`. - - `time` — `Now`, `Since`, `After` (channel-returning timer), `Sleep`, - `Duration`, `Time`. - - `io` — `Reader`/`Writer` interfaces; `ReadAll`; `Copy`. - - `sort` — `Slice`, `Ints`, `Strings`. -- Tests: round-trip Itoa/Atoi, fmt verb coverage, sync.WaitGroup with - goroutines, time.After in a select, sort.Slice with custom less fn. -- **Acceptance:** stdlib/ suite at 40+ tests. +### Phase 8 — Minimal stdlib (`lib/go/std/`) ✅ +- [x] **Package value type + import mechanism.** New `:go-package + NAME ENTRIES` AST value, registered in env so `strings.Contains` + resolves through extended `go-eval-select`. New `:go-builtin-fn` + callable type for closure-based stdlib builtins (distinct from + name-based `:go-builtin`). +- [x] **`strings` package, v0 subset:** Contains, HasPrefix, HasSuffix, + Index, Count, Repeat, Join, ToUpper, ToLower, TrimSpace, Split, + Replace. 12 functions, 26 tests. +- [x] **`strconv` package:** Itoa, Atoi (positive, negative, decimal). + 5 tests + 3 round-trip tests. +- [x] **String-literal AST shape fix.** Parser now emits `:literal- + string` for "..."/`...`/rune literals (was conflated with + numeric literals via first-char heuristic). Eval + typer + dispatch on the new shape. Fixes `Atoi("42")` and similar. +- [ ] `fmt`, `errors`, `sync`, `time`, `io`, `sort` — deferred to + Phase 8b. Tests for `sync.WaitGroup`, `time.After`-in-select, + `sort.Slice` deferred with them. +- **Acceptance:** stdlib/ suite at 40+ tests — **cleared (41 tests).** ### Phase 9 — End-to-end programs ⬜ - Complete programs from canonical sources (gopl.io, "concurrency @@ -632,6 +632,26 @@ Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`. _Newest first. Append one dated entry per commit._ +- 2026-05-28 — **Phase 8 first slice closed (stdlib 41/41, +40 + cleared, total 597/597).** New `:go-package NAME ENTRIES` value + type with field lookup via extended `go-eval-select`. New + `:go-builtin-fn FN` callable for closure-based stdlib (versus + name-dispatched `:go-builtin`). `lib/go/std/strings.sx` ships 12 + functions (Contains, HasPrefix, HasSuffix, Index, Count, Repeat, + Join, ToUpper, ToLower, TrimSpace, Split, Replace); strconv ships + Itoa + Atoi. Conformance runner picks up new suite via + `go-std-test-count`. **Fixed pre-existing literal-classification + bug**: parser was emitting `(:literal V)` for both `42` and + `"42"`, relying on first-char heuristic in eval/types to + distinguish. Now emits `:literal-string` for string/rune, + `:literal` for numeric/imag/float. Eliminates `Atoi("42")` → + number-42 misreading. Three parse tests updated to new shape; + string-literal-in-composite tests too. **Shape locked in:** + packages are AST values of shape `(:KIND NAME ENTRIES)` (parallel + to `:go-struct`, `:go-slice`, `:go-map`, `:go-chan`) — the kit's + value-type registry continues to take the same "kind tag + payload" + shape across orthogonal runtime concepts. [shapes-static-types- + bidirectional] - 2026-05-28 — **Phase 7 closed (types 102/102, +30 cleared, total 556/556).** Canonical generic functions all type-check and run: Map, Filter, Reduce, First (eval), plus typer-only Apply, Compose, diff --git a/plans/lib-guest-static-types-bidirectional.md b/plans/lib-guest-static-types-bidirectional.md index 6fbda9dd..e4e294d7 100644 --- a/plans/lib-guest-static-types-bidirectional.md +++ b/plans/lib-guest-static-types-bidirectional.md @@ -282,6 +282,53 @@ The kits compose; design accordingly. _Newest first. Append one dated entry per milestone landed._ +- 2026-05-28 — From Go-on-SX Phase 8 first slice — **value-type + kinds confirm the "kind-tag + payload" shape as cross-runtime + primitive.** When the stdlib landed, packages joined the existing + registry of value-type kinds: + + - `(:go-struct TY-NAME FIELDS)` — composite by-field state + - `(:go-slice ELEMS)` — sequential by-position state + - `(:go-map ENTRIES)` — keyed state + - `(:go-chan ACCESSORS)` — closure-bundle (channel) + - `(:go-fn PARAMS BODY)` — user function value + - `(:go-method RECV PARAMS BODY)` — method value + - `(:go-builtin NAME)` — name-dispatched builtin + - `(:go-builtin-fn FN)` — closure-dispatched builtin (NEW) + - `(:go-package NAME ENTRIES)` — namespace value (NEW) + - `(:go-panic V)` — unwinding-control value + - `(:go-defer CALLEE ARGS)` — frame-cleanup record + + All eleven kinds use the same `(:KIND-TAG PAYLOAD...)` shape. + None of them are AST nodes (those are `:func-decl`, `:literal`, + etc.); they're VALUES the evaluator produces. The orthogonal axes + the kit should care about: + + 1. **AST nodes** (parser output, evaluator input) + 2. **Value-type kinds** (evaluator output, predicate input) + 3. **Sentinel signals** (control-flow: return/break/panic/etc.) + + All three subscribe to the same first-class-tag discipline: + `(first x)` answers "what kind is this?" and the rest is payload. + The kit's `kind?` and `kind-of` predicates work uniformly across + all three axes. + + For the bidirectional checker specifically, this means the + `assignable?(got, expected)` predicate isn't special — it's just + one predicate that operates on value-type kinds. The `synth` / + `check` skeleton processes AST nodes; the validators it calls + operate on value-type kinds. Clean separation: AST is what you + parse, value-types are what you check, sentinels are what you + propagate. None of them bleed into each other. + + Phase 7's index-synth and Phase 8's package-lookup both fit the + same template: AST kind triggers a synth/lookup, returning a + value-type kind. The validator-table dispatch from earlier diary + entries is the right abstraction; the kit should expose it as a + PROTOCOL (Go would phrase this as an interface, Haskell as a + typeclass) so all three axes can be extended without modifying + the kit. + - 2026-05-28 — From Go-on-SX Phase 7 closing — **the "shape is the parser, role is the validator" lemma.** After landing canonical generic Map/Filter/Reduce/First plus 25+ typer tests, a clear