spec: character type (char? char->integer #\a literals + predicates)

- Add SxChar tagged object {_char, codepoint} to JS platform
- char? char->integer integer->char char-upcase char-downcase
- char=? char<? char>? char<=? char>=? comparators
- char-ci=? char-ci<? char-ci>? char-ci<=? char-ci>=? case-insensitive
- char-alphabetic? char-numeric? char-whitespace? char-upper-case? char-lower-case?
- string->list (returns chars) and list->string (accepts chars)
- #\a #\space #\newline reader syntax in spec/parser.sx
- integer->char alias in spec/evaluator.sx
- js-char-renames dict in transpiler.sx for ->-containing names
- 43 tests in spec/tests/test-chars.sx, all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 11:50:04 +00:00
parent 46da676c29
commit 4b600f17e8
7 changed files with 788 additions and 297 deletions

View File

@@ -1080,6 +1080,41 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); };
PRIMITIVES["char-code"] = function(s) { return String(s).charCodeAt(0); };
var charCode = PRIMITIVES["char-code"];
function makeChar(n) { return {_char: true, codepoint: n}; }
PRIMITIVES["make-char"] = makeChar;
var isChar = function(v) { return v != null && typeof v === "object" && v._char === true; };
PRIMITIVES["char?"] = isChar;
var charToInteger = function(c) { return c.codepoint; };
PRIMITIVES["char->integer"] = charToInteger;
var charUpcase = function(c) { return makeChar(String.fromCharCode(c.codepoint).toUpperCase().charCodeAt(0)); };
PRIMITIVES["char-upcase"] = charUpcase;
var charDowncase = function(c) { return makeChar(String.fromCharCode(c.codepoint).toLowerCase().charCodeAt(0)); };
PRIMITIVES["char-downcase"] = charDowncase;
PRIMITIVES["char=?"] = function(a, b) { return a.codepoint === b.codepoint; };
PRIMITIVES["char<?"] = function(a, b) { return a.codepoint < b.codepoint; };
PRIMITIVES["char>?"] = function(a, b) { return a.codepoint > b.codepoint; };
PRIMITIVES["char<=?"] = function(a, b) { return a.codepoint <= b.codepoint; };
PRIMITIVES["char>=?"] = function(a, b) { return a.codepoint >= b.codepoint; };
PRIMITIVES["char-ci=?"] = function(a, b) { return charDowncase(a).codepoint === charDowncase(b).codepoint; };
PRIMITIVES["char-ci<?"] = function(a, b) { return charDowncase(a).codepoint < charDowncase(b).codepoint; };
PRIMITIVES["char-ci>?"] = function(a, b) { return charDowncase(a).codepoint > charDowncase(b).codepoint; };
PRIMITIVES["char-ci<=?"] = function(a, b) { return charDowncase(a).codepoint <= charDowncase(b).codepoint; };
PRIMITIVES["char-ci>=?"] = function(a, b) { return charDowncase(a).codepoint >= charDowncase(b).codepoint; };
PRIMITIVES["char-alphabetic?"] = function(c) { var n = c.codepoint; return (n >= 65 && n <= 90) || (n >= 97 && n <= 122); };
PRIMITIVES["char-numeric?"] = function(c) { var n = c.codepoint; return n >= 48 && n <= 57; };
PRIMITIVES["char-whitespace?"] = function(c) { var n = c.codepoint; return n === 32 || n === 9 || n === 10 || n === 13; };
PRIMITIVES["char-upper-case?"] = function(c) { var n = c.codepoint; return n >= 65 && n <= 90; };
PRIMITIVES["char-lower-case?"] = function(c) { var n = c.codepoint; return n >= 97 && n <= 122; };
PRIMITIVES["string->list"] = function(s) {
var chars = []; var str = String(s);
for (var i = 0; i < str.length; i++) chars.push(makeChar(str.charCodeAt(i)));
return chars;
};
PRIMITIVES["list->string"] = function(chars) {
return chars.map(function(c) { return String.fromCharCode(c.codepoint); }).join('');
};
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; };
@@ -1397,6 +1432,7 @@ PLATFORM_JS_PRE = '''
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (x._sx_expr) return "sx-expr";
if (x._char) return "char";
if (x._vector) return "vector";
if (x._string_buffer) return "string-buffer";
if (x._hash_table) return "hash-table";
@@ -2045,6 +2081,9 @@ PLATFORM_PARSER_JS = r"""
}
function sxExprSource(e) { return typeof e === "string" ? e : (e && e.source ? e.source : String(e)); }
var charFromCode = PRIMITIVES["char-from-code"];
var makeChar = PRIMITIVES["make-char"];
var charToInteger = PRIMITIVES["char->integer"];
var isChar = PRIMITIVES["char?"];
"""