diff --git a/lib/erlang/runtime.sx b/lib/erlang/runtime.sx index b4043b1e..f6db3733 100644 --- a/lib/erlang/runtime.sx +++ b/lib/erlang/runtime.sx @@ -1561,7 +1561,66 @@ (er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash) (er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes) (er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string) + +;; ── binary_to_list / list_to_binary (Step 3b — term codec) ────── +;; Standard Erlang semantics: +;; binary_to_list(<>) -> [B1, B2, ...] (Erlang cons of ints) +;; list_to_binary(IoList) -> <<...>> (flattens nested +;; iolists; elements are byte ints 0-255 or binaries) +;; Bad arg / out-of-range byte / non-iolist element -> error:badarg. + +(define er-bif-binary-to-list + (fn (vs) + (let ((v (nth vs 0))) + (cond + (not (er-binary? v)) + (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else + (let ((bs (get v :bytes)) (out (er-mk-nil))) + (for-each + (fn (i) + (set! out (er-mk-cons (nth bs (- (- (len bs) 1) i)) out))) + (range 0 (len bs))) + out))))) + +;; Walk an Erlang iolist, appending bytes to `acc` (a mutable SX list). +;; Accepts: nil, cons-of-X, binary, integer in 0..255. Anything else +;; signals failure by setting (nth fail 0) to true. +(define er-iolist-walk! + (fn (v acc fail) + (cond + (nth fail 0) nil + (er-nil? v) nil + (er-cons? v) + (do (er-iolist-walk! (get v :head) acc fail) + (er-iolist-walk! (get v :tail) acc fail)) + (er-binary? v) + (for-each + (fn (i) (append! acc (nth (get v :bytes) i))) + (range 0 (len (get v :bytes)))) + (= (type-of v) "number") + (cond + (and (>= v 0) (<= v 255)) (append! acc v) + :else (set-nth! fail 0 true)) + :else (set-nth! fail 0 true)))) + +(define er-bif-list-to-binary + (fn (vs) + (let ((v (nth vs 0)) (acc (list)) (fail (list false))) + (cond + (not (or (er-nil? v) (er-cons? v) (er-binary? v))) + (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else + (do + (er-iolist-walk! v acc fail) + (cond + (nth fail 0) + (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else (er-mk-binary acc))))))) + (er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir) + (er-register-pure-bif! "erlang" "binary_to_list" 1 er-bif-binary-to-list) + (er-register-pure-bif! "erlang" "list_to_binary" 1 er-bif-list-to-binary) (er-mk-atom "ok"))) ;; Register everything at load time. diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index f5b6e981..a86b5fc6 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,18 +1,18 @@ { "language": "erlang", - "total_pass": 729, - "total": 729, + "total_pass": 761, + "total": 761, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, - {"name":"eval","pass":385,"total":385,"status":"ok"}, + {"name":"eval","pass":408,"total":408,"status":"ok"}, {"name":"runtime","pass":93,"total":93,"status":"ok"}, {"name":"ring","pass":4,"total":4,"status":"ok"}, {"name":"ping-pong","pass":4,"total":4,"status":"ok"}, {"name":"bank","pass":8,"total":8,"status":"ok"}, {"name":"echo","pass":7,"total":7,"status":"ok"}, {"name":"fib","pass":8,"total":8,"status":"ok"}, - {"name":"ffi","pass":28,"total":28,"status":"ok"}, + {"name":"ffi","pass":37,"total":37,"status":"ok"}, {"name":"vm","pass":78,"total":78,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 75f3fe39..bd4087cc 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,19 +1,19 @@ # Erlang-on-SX Scoreboard -**Total: 729 / 729 tests passing** +**Total: 761 / 761 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | tokenize | 62 | 62 | | ✅ | parse | 52 | 52 | -| ✅ | eval | 385 | 385 | +| ✅ | eval | 408 | 408 | | ✅ | runtime | 93 | 93 | | ✅ | ring | 4 | 4 | | ✅ | ping-pong | 4 | 4 | | ✅ | bank | 8 | 8 | | ✅ | echo | 7 | 7 | | ✅ | fib | 8 | 8 | -| ✅ | ffi | 28 | 28 | +| ✅ | ffi | 37 | 37 | | ✅ | vm | 78 | 78 | diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index 4bd322db..dca0765d 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -228,9 +228,10 @@ (er-eval-test "tuple_size 0" (ev "tuple_size({})") 0) ;; ── BIFs: atom / list conversions ─────────────────────────────── -(er-eval-test "atom_to_list" (ev "atom_to_list(hello)") "hello") +(er-eval-test "atom_to_list -> charlist length" (ev "length(atom_to_list(hello))") 5) +(er-eval-test "atom_to_list -> head $h" (ev "hd(atom_to_list(hello))") 104) (er-eval-test "list_to_atom roundtrip" - (nm (ev "list_to_atom(atom_to_list(foo))")) "foo") + (nm (ev "list_to_atom(atom_to_list(foo))")) "foo") ;; round-trip via charlist (er-eval-test "list_to_atom fresh" (nm (ev "list_to_atom(\"bar\")")) "bar") @@ -1060,11 +1061,13 @@ (er-eval-test "list_to_tuple roundtrip" (ev "tuple_size(list_to_tuple([10, 20, 30]))") 3) -(er-eval-test "integer_to_list" (ev "integer_to_list(42)") "42") -(er-eval-test "integer_to_list neg" (ev "integer_to_list(-99)") "-99") +(er-eval-test "integer_to_list -> charlist length" (ev "length(integer_to_list(42))") 2) +(er-eval-test "integer_to_list 42 head $4" (ev "hd(integer_to_list(42))") 52) +(er-eval-test "integer_to_list neg -> charlist length" (ev "length(integer_to_list(-99))") 3) +(er-eval-test "integer_to_list -99 head $-" (ev "hd(integer_to_list(-99))") 45) (er-eval-test "list_to_integer" (ev "list_to_integer(\"123\")") 123) (er-eval-test "list_to_integer roundtrip" - (ev "list_to_integer(integer_to_list(7))") 7) + (ev "list_to_integer(integer_to_list(7))") 7) ;; round-trip via charlist (er-eval-test "is_function fun" (nm (ev "F = fun (X) -> X end, is_function(F)")) "true") @@ -1341,6 +1344,42 @@ (get (nth (get er-rt-cap-result :elements) 4) :name) "true") + +;; ── $X char literals (Step 3b substrate fix 2026-06-04) ────────── +(er-eval-test "char $A" (ev "$A") 65) +(er-eval-test "char $a" (ev "$a") 97) +(er-eval-test "char $0 is digit, not escape-NUL" (ev "$0") 48) +(er-eval-test "char $\\n is newline (10)" (ev "$\\n") 10) +(er-eval-test "char $\\t is tab (9)" (ev "$\\t") 9) +(er-eval-test "char $\\r is CR (13)" (ev "$\\r") 13) +(er-eval-test "char $\\s is space (32)" (ev "$\\s") 32) +(er-eval-test "char $\\0 is NUL (0)" (ev "$\\0") 0) +(er-eval-test "char $\\\\ is backslash (92)" (ev "$\\\\") 92) +(er-eval-test "[$h,$i] head is 104" (ev "hd([$h, $i])") 104) +(er-eval-test "list_to_binary char-list -> bytes" + (ev "byte_size(list_to_binary([$f, $e, $d]))") 3) +(er-eval-test "list_to_binary char-list round-trip" + (nm (ev "list_to_binary([$h, $i]) =:= <<104, 105>>")) "true") + + +;; ── atom_to_list / integer_to_list charlist semantics (Step 3b substrate fix #3) ── +(er-eval-test "atom_to_list hd is char code" + (ev "hd(atom_to_list(hi))") 104) +(er-eval-test "atom_to_list maps to bytes via list_to_binary" + (ev "byte_size(list_to_binary(atom_to_list(hello)))") 5) +(er-eval-test "atom_to_list -> list_to_binary -> bytes content" + (nm (ev "list_to_binary(atom_to_list(ok)) =:= <<111, 107>>")) "true") +(er-eval-test "integer_to_list 12345 -> 5 chars" + (ev "length(integer_to_list(12345))") 5) +(er-eval-test "integer_to_list -> bytes -> back" + (ev "list_to_integer(integer_to_list(99999))") 99999) +(er-eval-test "list_to_atom from charlist" + (nm (ev "list_to_atom([$f, $o, $o])")) "foo") +(er-eval-test "list_to_atom from SX-string back-compat" + (nm (ev "list_to_atom(\"bar\")")) "bar") +(er-eval-test "list_to_integer from charlist" + (ev "list_to_integer([$1, $0, $0])") 100) + (define er-eval-test-summary (str "eval " er-eval-test-pass "/" er-eval-test-count)) diff --git a/lib/erlang/tests/ffi.sx b/lib/erlang/tests/ffi.sx index e08a31bf..29af1c9e 100644 --- a/lib/erlang/tests/ffi.sx +++ b/lib/erlang/tests/ffi.sx @@ -160,6 +160,51 @@ (ffi-nm (ffi-ev "element(2, file:list_dir(\"/no/such/dir/xyz\"))")) "enoent") +(er-ffi-test + "binary_to_list <<1,2,3>> length" + (ffi-ev "length(binary_to_list(<<1,2,3,4,5>>))") + 5) + +(er-ffi-test + "binary_to_list hd byte" + (ffi-ev "hd(binary_to_list(<<7,8,9>>))") + 7) + +(er-ffi-test + "binary_to_list empty -> []" + (ffi-nm (ffi-ev "case binary_to_list(<<>>) of [] -> empty end")) + "empty") + +(er-ffi-test + "list_to_binary flat list bytes" + (ffi-ev "byte_size(list_to_binary([1,2,3]))") + 3) + +(er-ffi-test + "list_to_binary nested iolist" + (ffi-ev "byte_size(list_to_binary([1, <<2,3>>, [4, [5]]]))") + 5) + +(er-ffi-test + "list_to_binary round-trip via binary_to_list" + (ffi-nm (ffi-ev "list_to_binary(binary_to_list(<<10,20,30>>)) =:= <<10,20,30>>")) + "true") + +(er-ffi-test + "binary_to_list non-binary -> error:badarg" + (ffi-nm (ffi-ev "try binary_to_list(42) catch error:badarg -> ok end")) + "ok") + +(er-ffi-test + "list_to_binary out-of-range byte -> error:badarg" + (ffi-nm (ffi-ev "try list_to_binary([300]) catch error:badarg -> ok end")) + "ok") + +(er-ffi-test + "list_to_binary non-iolist -> error:badarg" + (ffi-nm (ffi-ev "try list_to_binary(42) catch error:badarg -> ok end")) + "ok") + ;; ── Still deferred (no host primitive): httpc (HTTP client, v2), ;; sqlite-* (v2 indexes). Assert NOT registered so a future iteration ;; that wires them without updating this suite fails fast. diff --git a/lib/erlang/tokenizer.sx b/lib/erlang/tokenizer.sx index c46e7bc6..8a70bde4 100644 --- a/lib/erlang/tokenizer.sx +++ b/lib/erlang/tokenizer.sx @@ -229,13 +229,37 @@ (= ch "$") (do (er-advance! 1) - (if - (and (< pos src-len) (= (er-cur) "\\")) - (do - (er-advance! 1) - (when (< pos src-len) (er-advance! 1))) - (when (< pos src-len) (er-advance! 1))) - (er-emit! "integer" (slice src start pos) start) + ;; Emit the char's decimal code as the integer token value + ;; (was: raw "$X" text — parse-number then returned nil). + (let + ((code (cond + (>= pos src-len) 0 + (= (er-cur) "\\") + (do + (er-advance! 1) + (let ((esc (if (< pos src-len) (er-cur) ""))) + (when (< pos src-len) (er-advance! 1)) + (cond + (= esc "n") 10 + (= esc "t") 9 + (= esc "r") 13 + (= esc "s") 32 + (= esc "b") 8 + (= esc "e") 27 + (= esc "f") 12 + (= esc "v") 11 + (= esc "d") 127 + (= esc "0") 0 + (= esc "\\") 92 + (= esc "\"") 34 + (= esc "'") 39 + (= esc "") 0 + :else (char->integer (nth (string->list esc) 0))))) + :else + (let ((c (er-cur))) + (er-advance! 1) + (char->integer (nth (string->list c) 0)))))) + (er-emit! "integer" (str code) start)) (scan!)) (er-lower? ch) (do diff --git a/lib/erlang/transpile.sx b/lib/erlang/transpile.sx index 915d31b6..12e14b6f 100644 --- a/lib/erlang/transpile.sx +++ b/lib/erlang/transpile.sx @@ -107,7 +107,12 @@ (let ((ty (get node :type))) (cond - (= ty "integer") (parse-number (get node :value)) + (= ty "integer") + (let ((n (parse-number (get node :value)))) + (cond + (= n nil) (error (str "Erlang: invalid integer literal: " + (get node :value))) + :else (truncate n))) (= ty "float") (parse-number (get node :value)) (= ty "atom") (er-mk-atom (get node :value)) (= ty "string") (get node :value) @@ -821,16 +826,30 @@ (len (get v :elements)) (error "Erlang: tuple_size: not a tuple"))))) +(define er-string->charlist + (fn (s) + (let ((cs (string->list s)) (out (er-mk-nil))) + (for-each + (fn (i) + (set! out (er-mk-cons + (char->integer (nth cs (- (- (len cs) 1) i))) + out))) + (range 0 (len cs))) + out))) + (define er-bif-atom-to-list (fn (vs) (let ((v (er-bif-arg1 vs "atom_to_list"))) + ;; Standard Erlang: atom_to_list/1 returns an Erlang charlist + ;; (list of integer char codes). Was: SX string of :name — + ;; unusable from Erlang-land for [Char|T] / ++ / binary segments. (if (er-atom? v) - (get v :name) - (error "Erlang: atom_to_list: not an atom"))))) + (er-string->charlist (get v :name)) + (raise (er-mk-error-marker (er-mk-atom "badarg"))))))) (define er-bif-list-to-atom @@ -838,10 +857,11 @@ (vs) (let ((v (er-bif-arg1 vs "list_to_atom"))) - (if - (= (type-of v) "string") - (er-mk-atom v) - (error "Erlang: list_to_atom: not a string"))))) + ;; Accept Erlang charlist (cons of ints) or SX string. + (let ((s (er-source-to-string v))) + (cond + (= s nil) (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else (er-mk-atom s)))))) ;; ── lists module ───────────────────────────────────────────────── (define @@ -1597,10 +1617,12 @@ (vs) (let ((v (er-bif-arg1 vs "integer_to_list"))) + ;; Standard Erlang: integer_to_list/1 returns an Erlang charlist + ;; (e.g. integer_to_list(42) -> [$4, $2] -> [52, 50]). (cond (not (= (type-of v) "number")) (raise (er-mk-error-marker (er-mk-atom "badarg"))) - :else (str v))))) + :else (er-string->charlist (str v)))))) (define er-bif-list-to-integer @@ -1608,15 +1630,14 @@ (vs) (let ((v (er-bif-arg1 vs "list_to_integer"))) - (cond - (not (= (type-of v) "string")) - (raise (er-mk-error-marker (er-mk-atom "badarg"))) - :else (let - ((n (parse-number v))) - (cond - (= n nil) - (raise (er-mk-error-marker (er-mk-atom "badarg"))) - :else n)))))) + ;; Accept Erlang charlist (cons of ints) or SX string. + (let ((s (er-source-to-string v))) + (cond + (= s nil) (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else (let ((n (parse-number s))) + (cond + (= n nil) (raise (er-mk-error-marker (er-mk-atom "badarg"))) + :else n))))))) (define er-bif-is-function