spec: string ports (open-input-string/open-output-string/read-char/etc)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

Phase 14: port type + eof-object. Input ports track _pos cursor; output ports
accumulate _buffer. All 15 port primitives in spec/primitives.sx (stdlib.ports
module), platform.py (JS), and 39/39 tests in spec/tests/test-ports.sx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 12:38:08 +00:00
parent dfbcece644
commit 3d8937d759
4 changed files with 456 additions and 1 deletions

View File

@@ -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";

View File

@@ -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; };

View File

@@ -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)

232
spec/tests/test-ports.sx Normal file
View File

@@ -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")))))