diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 9cdee64f..5512ad49 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -1115,6 +1115,69 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { PRIMITIVES["list->string"] = function(chars) { return chars.map(function(c) { return String.fromCharCode(c.codepoint); }).join(''); }; + // Phase 14: string ports + eof-object + var _eof = {_eof: true}; + PRIMITIVES["eof-object"] = function() { return _eof; }; + PRIMITIVES["eof-object?"] = function(v) { return v != null && v._eof === true; }; + var isEofObject = PRIMITIVES["eof-object?"]; + PRIMITIVES["open-input-string"] = function(s) { + return {_port: true, _kind: "input", _source: String(s), _pos: 0, _closed: false}; + }; + PRIMITIVES["open-output-string"] = function() { + return {_port: true, _kind: "output", _buffer: "", _closed: false}; + }; + PRIMITIVES["get-output-string"] = function(p) { + if (!p || p._kind !== "output") throw new Error("get-output-string: expected output port"); + return p._buffer; + }; + PRIMITIVES["port?"] = function(v) { return v != null && v._port === true; }; + PRIMITIVES["input-port?"] = function(v) { return v != null && v._port === true && v._kind === "input"; }; + PRIMITIVES["output-port?"] = function(v) { return v != null && v._port === true && v._kind === "output"; }; + PRIMITIVES["close-port"] = function(p) { + if (p && p._port) p._closed = true; + return NIL; + }; + PRIMITIVES["read-char"] = function(p) { + if (p === undefined || p === NIL || p == null) { + return _eof; // no stdin in this env + } + if (!p._port || p._kind !== "input") throw new Error("read-char: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + var cp = p._source.charCodeAt(p._pos); + p._pos++; + return makeChar(cp); + }; + PRIMITIVES["peek-char"] = function(p) { + if (p === undefined || p === NIL || p == null) return _eof; + if (!p._port || p._kind !== "input") throw new Error("peek-char: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + return makeChar(p._source.charCodeAt(p._pos)); + }; + PRIMITIVES["read-line"] = function(p) { + if (p === undefined || p === NIL || p == null) return _eof; + if (!p._port || p._kind !== "input") throw new Error("read-line: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + var start = p._pos; + while (p._pos < p._source.length && p._source[p._pos] !== '\\n') p._pos++; + var line = p._source.slice(start, p._pos); + if (p._pos < p._source.length) p._pos++; // skip \n + return line; + }; + PRIMITIVES["write-char"] = function(c, p) { + if (!p || !p._port || p._kind !== "output") throw new Error("write-char: expected char and output port"); + if (!p._closed) p._buffer += String.fromCharCode(c.codepoint); + return NIL; + }; + PRIMITIVES["write-string"] = function(s, p) { + if (!p || !p._port || p._kind !== "output") throw new Error("write-string: expected string and output port"); + if (!p._closed) p._buffer += String(s); + return NIL; + }; + PRIMITIVES["char-ready?"] = function(p) { + if (p === undefined || p === NIL || p == null) return false; + if (!p._port || p._kind !== "input") return false; + return !p._closed && p._pos < p._source.length; + }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; var stringLength = PRIMITIVES["string-length"]; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; @@ -1433,6 +1496,8 @@ PLATFORM_JS_PRE = ''' if (x._raw) return "raw-html"; if (x._sx_expr) return "sx-expr"; if (x._char) return "char"; + if (x._eof) return "eof-object"; + if (x._port) return x._kind === "input" ? "input-port" : "output-port"; if (x._vector) return "vector"; if (x._string_buffer) return "string-buffer"; if (x._hash_table) return "hash-table"; diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 9e699954..855ec505 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -31,7 +31,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-05-01T11:46:28Z"; + var SX_VERSION = "2026-05-01T12:34:38Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -169,6 +169,8 @@ if (x._raw) return "raw-html"; if (x._sx_expr) return "sx-expr"; if (x._char) return "char"; + if (x._eof) return "eof-object"; + if (x._port) return x._kind === "input" ? "input-port" : "output-port"; if (x._vector) return "vector"; if (x._string_buffer) return "string-buffer"; if (x._hash_table) return "hash-table"; @@ -511,6 +513,70 @@ PRIMITIVES["list->string"] = function(chars) { return chars.map(function(c) { return String.fromCharCode(c.codepoint); }).join(''); }; + // Phase 14: string ports + eof-object + var _eof = {_eof: true}; + PRIMITIVES["eof-object"] = function() { return _eof; }; + PRIMITIVES["eof-object?"] = function(v) { return v != null && v._eof === true; }; + var isEofObject = PRIMITIVES["eof-object?"]; + PRIMITIVES["open-input-string"] = function(s) { + return {_port: true, _kind: "input", _source: String(s), _pos: 0, _closed: false}; + }; + PRIMITIVES["open-output-string"] = function() { + return {_port: true, _kind: "output", _buffer: "", _closed: false}; + }; + PRIMITIVES["get-output-string"] = function(p) { + if (!p || p._kind !== "output") throw new Error("get-output-string: expected output port"); + return p._buffer; + }; + PRIMITIVES["port?"] = function(v) { return v != null && v._port === true; }; + PRIMITIVES["input-port?"] = function(v) { return v != null && v._port === true && v._kind === "input"; }; + PRIMITIVES["output-port?"] = function(v) { return v != null && v._port === true && v._kind === "output"; }; + PRIMITIVES["close-port"] = function(p) { + if (p && p._port) p._closed = true; + return NIL; + }; + PRIMITIVES["read-char"] = function(p) { + if (p === undefined || p === NIL || p == null) { + return _eof; // no stdin in this env + } + if (!p._port || p._kind !== "input") throw new Error("read-char: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + var cp = p._source.charCodeAt(p._pos); + p._pos++; + return makeChar(cp); + }; + PRIMITIVES["peek-char"] = function(p) { + if (p === undefined || p === NIL || p == null) return _eof; + if (!p._port || p._kind !== "input") throw new Error("peek-char: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + return makeChar(p._source.charCodeAt(p._pos)); + }; + PRIMITIVES["read-line"] = function(p) { + if (p === undefined || p === NIL || p == null) return _eof; + if (!p._port || p._kind !== "input") throw new Error("read-line: expected input port"); + if (p._closed || p._pos >= p._source.length) return _eof; + var start = p._pos; + while (p._pos < p._source.length && p._source[p._pos] !== '\n') p._pos++; + var line = p._source.slice(start, p._pos); + if (p._pos < p._source.length) p._pos++; // skip + + return line; + }; + PRIMITIVES["write-char"] = function(c, p) { + if (!p || !p._port || p._kind !== "output") throw new Error("write-char: expected char and output port"); + if (!p._closed) p._buffer += String.fromCharCode(c.codepoint); + return NIL; + }; + PRIMITIVES["write-string"] = function(s, p) { + if (!p || !p._port || p._kind !== "output") throw new Error("write-string: expected string and output port"); + if (!p._closed) p._buffer += String(s); + return NIL; + }; + PRIMITIVES["char-ready?"] = function(p) { + if (p === undefined || p === NIL || p == null) return false; + if (!p._port || p._kind !== "input") return false; + return !p._closed && p._pos < p._source.length; + }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; var stringLength = PRIMITIVES["string-length"]; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; diff --git a/spec/primitives.sx b/spec/primitives.sx index e5d3de46..7aa39d4f 100644 --- a/spec/primitives.sx +++ b/spec/primitives.sx @@ -856,4 +856,96 @@ :returns "number" :doc "Number of bits needed to represent integer a (excluding sign).") +(define-module :stdlib.ports) + +(define-primitive + "eof-object" + :params () + :returns "eof-object" + :doc "The EOF sentinel value.") + +(define-primitive + "eof-object?" + :params (v) + :returns "boolean" + :doc "True if v is the EOF sentinel.") + +(define-primitive + "open-input-string" + :params ((s :as string)) + :returns "input-port" + :doc "Open a string as an input port.") + +(define-primitive + "open-output-string" + :params () + :returns "output-port" + :doc "Open a fresh output string port.") + +(define-primitive + "get-output-string" + :params ((p :as output-port)) + :returns "string" + :doc "Flush output port contents to a string.") + +(define-primitive + "port?" + :params (v) + :returns "boolean" + :doc "True if v is any port.") + +(define-primitive + "input-port?" + :params (v) + :returns "boolean" + :doc "True if v is an input port.") + +(define-primitive + "output-port?" + :params (v) + :returns "boolean" + :doc "True if v is an output port.") + +(define-primitive + "close-port" + :params ((p :as port)) + :returns "nil" + :doc "Close a port.") + +(define-primitive + "read-char" + :params (&rest (p :as input-port)) + :returns "any" + :doc "Read next char from port; returns eof-object at end.") + +(define-primitive + "peek-char" + :params (&rest (p :as input-port)) + :returns "any" + :doc "Peek next char without consuming; returns eof-object at end.") + +(define-primitive + "read-line" + :params (&rest (p :as input-port)) + :returns "any" + :doc "Read a line from port; returns eof-object at end.") + +(define-primitive + "write-char" + :params ((c :as char) &rest (p :as output-port)) + :returns "nil" + :doc "Write a char to output port.") + +(define-primitive + "write-string" + :params ((s :as string) &rest (p :as output-port)) + :returns "nil" + :doc "Write a string to output port.") + +(define-primitive + "char-ready?" + :params (&rest (p :as input-port)) + :returns "boolean" + :doc "True if a char is immediately available on the port.") + (define-module :stdlib.hash-table) diff --git a/spec/tests/test-ports.sx b/spec/tests/test-ports.sx new file mode 100644 index 00000000..a4426d45 --- /dev/null +++ b/spec/tests/test-ports.sx @@ -0,0 +1,232 @@ +;; Phase 14 — String ports + eof-object + +(deftest + "eof-object" + (deftest + "eof-object is eof" + (assert= + true + (eof-object? (eof-object)) + "eof-object? returns true for eof-object")) + (deftest + "non-eof values are not eof" + (assert= false (eof-object? nil) "nil is not eof") + (assert= false (eof-object? "") "string is not eof") + (assert= false (eof-object? 0) "zero is not eof") + (assert= false (eof-object? false) "false is not eof")) + (deftest + "type-of eof-object" + (assert= + "eof-object" + (type-of (eof-object)) + "type-of eof-object is eof-object"))) + +(deftest + "open-input-string" + (deftest + "creates input port" + (let + (p (open-input-string "hello")) + (assert= true (port? p) "is a port") + (assert= true (input-port? p) "is an input port") + (assert= false (output-port? p) "is not an output port"))) + (deftest + "type-of input port" + (let + (p (open-input-string "x")) + (assert= "input-port" (type-of p) "type-of is input-port")))) + +(deftest + "open-output-string" + (deftest + "creates output port" + (let + (p (open-output-string)) + (assert= true (port? p) "is a port") + (assert= true (output-port? p) "is an output port") + (assert= false (input-port? p) "is not an input port"))) + (deftest + "type-of output port" + (let + (p (open-output-string)) + (assert= "output-port" (type-of p) "type-of is output-port")))) + +(deftest + "read-char" + (deftest + "reads chars sequentially" + (let + (p (open-input-string "ab")) + (let + (c1 (read-char p)) + (assert= true (char? c1) "first result is char") + (assert= 97 (char->integer c1) "first char is a")))) + (deftest + "reads second char" + (let + (p (open-input-string "ab")) + (read-char p) + (let + (c2 (read-char p)) + (assert= true (char? c2) "second result is char") + (assert= 98 (char->integer c2) "second char is b")))) + (deftest + "returns eof at end" + (let + (p (open-input-string "x")) + (read-char p) + (assert= true (eof-object? (read-char p)) "eof after last char"))) + (deftest + "empty string yields eof immediately" + (let + (p (open-input-string "")) + (assert= true (eof-object? (read-char p)) "eof from empty string")))) + +(deftest + "peek-char" + (deftest + "peeks without consuming" + (let + (p (open-input-string "x")) + (let + (c1 (peek-char p)) + (let + (c2 (peek-char p)) + (assert= + (char->integer c1) + (char->integer c2) + "peek twice gives same char"))))) + (deftest + "peek then read" + (let + (p (open-input-string "z")) + (let + (peeked (peek-char p)) + (let + (read (read-char p)) + (assert= + (char->integer peeked) + (char->integer read) + "peek and read agree"))))) + (deftest + "peek at end returns eof" + (let + (p (open-input-string "")) + (assert= true (eof-object? (peek-char p)) "eof on empty peek")))) + +(deftest + "read-line" + (deftest + "reads a single line" + (let + (p (open-input-string "hello")) + (assert= "hello" (read-line p) "reads whole string as line"))) + (deftest + "reads line up to newline" + (let + (p (open-input-string "foo\nbar")) + (assert= "foo" (read-line p) "first line is foo"))) + (deftest + "reads second line" + (let + (p (open-input-string "foo\nbar")) + (read-line p) + (assert= "bar" (read-line p) "second line is bar"))) + (deftest + "returns eof on empty port" + (let + (p (open-input-string "")) + (assert= true (eof-object? (read-line p)) "eof on empty"))) + (deftest + "returns eof after last line" + (let + (p (open-input-string "hi")) + (read-line p) + (assert= true (eof-object? (read-line p)) "eof after reading")))) + +(deftest + "write-char and get-output-string" + (deftest + "write single char" + (let + (p (open-output-string)) + (write-char (make-char 65) p) + (assert= "A" (get-output-string p) "write char A"))) + (deftest + "write multiple chars" + (let + (p (open-output-string)) + (write-char (make-char 72) p) + (write-char (make-char 105) p) + (assert= "Hi" (get-output-string p) "write Hi")))) + +(deftest + "write-string" + (deftest + "write a string to port" + (let + (p (open-output-string)) + (write-string "hello" p) + (assert= "hello" (get-output-string p) "write-string result"))) + (deftest + "multiple writes concatenate" + (let + (p (open-output-string)) + (write-string "foo" p) + (write-string "bar" p) + (assert= "foobar" (get-output-string p) "concatenated writes")))) + +(deftest + "get-output-string idempotent" + (let + (p (open-output-string)) + (write-string "test" p) + (assert= "test" (get-output-string p) "first call") + (assert= "test" (get-output-string p) "second call same result"))) + +(deftest + "char-ready?" + (deftest + "ready when chars available" + (let + (p (open-input-string "x")) + (assert= true (char-ready? p) "ready with content"))) + (deftest + "not ready when empty" + (let + (p (open-input-string "")) + (assert= false (char-ready? p) "not ready when empty")))) + +(deftest + "close-port" + (deftest + "close input port" + (let + (p (open-input-string "hello")) + (close-port p) + (assert= true (eof-object? (read-char p)) "read after close gives eof"))) + (deftest + "close output port" + (let + (p (open-output-string)) + (write-string "ok" p) + (close-port p) + (assert= "ok" (get-output-string p) "output preserved after close")))) + +(deftest + "roundtrip string via ports" + (let + (in (open-input-string "abc")) + (let + (out (open-output-string)) + (do + (let + (c1 (read-char in)) + (when (not (eof-object? c1)) (write-char c1 out))) + (let + (c2 (read-char in)) + (when (not (eof-object? c2)) (write-char c2 out))) + (let + (c3 (read-char in)) + (when (not (eof-object? c3)) (write-char c3 out))) + (assert= "abc" (get-output-string out) "roundtrip via ports")))))