Merge branch 'worktree-api-urls' into macros

This commit is contained in:
2026-03-13 12:04:30 +00:00
15 changed files with 1323 additions and 38 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T05:11:12Z";
var SX_VERSION = "2026-03-13T11:02:33Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -410,6 +410,7 @@
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
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["string-length"] = function(s) { return String(s).length; };
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
PRIMITIVES["concat"] = function() {
@@ -774,6 +775,7 @@
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
var charFromCode = PRIMITIVES["char-from-code"];
// === Transpiled from eval ===
@@ -1382,6 +1384,7 @@ if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\r")))) { p
continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1);
skipComment();
continue; } else { return NIL; } } } else { return NIL; } } };
var hexDigitValue = function(ch) { return indexOf_("0123456789abcdef", lower(ch)); };
var readString = function() { pos = (pos + 1);
return (function() {
var buf = "";
@@ -1389,9 +1392,19 @@ return (function() {
if (isSxTruthy((ch == "\""))) { pos = (pos + 1);
return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1);
{ var esc = nth(source, pos);
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc)))));
if (isSxTruthy((esc == "u"))) { pos = (pos + 1);
{ var d0 = hexDigitValue(nth(source, pos));
var _ = (pos = (pos + 1));
var d1 = hexDigitValue(nth(source, pos));
var _ = (pos = (pos + 1));
var d2 = hexDigitValue(nth(source, pos));
var _ = (pos = (pos + 1));
var d3 = hexDigitValue(nth(source, pos));
var _ = (pos = (pos + 1));
buf = (String(buf) + String(charFromCode(((((d0 * 4096) + (d1 * 256)) + (d2 * 16)) + d3))));
continue; } } else { buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc)))));
pos = (pos + 1);
continue; } } else { buf = (String(buf) + String(ch));
continue; } } } else { buf = (String(buf) + String(ch));
pos = (pos + 1);
continue; } } } } };
readStrLoop();

View File

@@ -106,6 +106,14 @@ def _unescape_string(s: str) -> str:
while i < len(s):
if s[i] == "\\" and i + 1 < len(s):
nxt = s[i + 1]
if nxt == "u" and i + 5 < len(s):
hex_str = s[i + 2:i + 6]
try:
out.append(chr(int(hex_str, 16)))
i += 6
continue
except ValueError:
pass # fall through to default handling
out.append(_ESCAPE_MAP.get(nxt, nxt))
i += 2
else:

View File

@@ -1259,11 +1259,21 @@
(define js-emit-infix
(fn ((op :as string) (args :as list))
(let ((js-op (js-op-symbol op)))
(if (and (= (len args) 1) (= op "-"))
(str "(-" (js-expr (first args)) ")")
(str "(" (js-expr (first args))
" " js-op " " (js-expr (nth args 1)) ")")))))
(let ((js-op (js-op-symbol op))
(n (len args)))
(cond
(and (= n 1) (= op "-"))
(str "(-" (js-expr (first args)) ")")
(= n 2)
(str "(" (js-expr (first args))
" " js-op " " (js-expr (nth args 1)) ")")
;; Variadic: left-fold (a op b op c op d ...)
:else
(let ((result (js-expr (first args))))
(for-each (fn (arg)
(set! result (str "(" result " " js-op " " (js-expr arg) ")")))
(rest args))
result)))))
;; --------------------------------------------------------------------------

View File

@@ -80,6 +80,9 @@
;; -- Atom readers --
(define hex-digit-value :effects []
(fn (ch) (index-of "0123456789abcdef" (lower ch))))
(define read-string :effects []
(fn ()
(set! pos (inc pos)) ;; skip opening "
@@ -95,14 +98,29 @@
(= ch "\\")
(do (set! pos (inc pos))
(let ((esc (nth source pos)))
(set! buf (str buf
(cond
(= esc "n") "\n"
(= esc "t") "\t"
(= esc "r") "\r"
:else esc)))
(set! pos (inc pos))
(read-str-loop)))
(if (= esc "u")
;; Unicode escape: \uXXXX → char
(do (set! pos (inc pos))
(let ((d0 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d1 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d2 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d3 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos))))
(set! buf (str buf (char-from-code
(+ (* d0 4096) (* d1 256) (* d2 16) d3))))
(read-str-loop)))
;; Standard escapes: \n \t \r or literal
(do (set! buf (str buf
(cond
(= esc "n") "\n"
(= esc "t") "\t"
(= esc "r") "\r"
:else esc)))
(set! pos (inc pos))
(read-str-loop)))))
:else
(do (set! buf (str buf ch))
(set! pos (inc pos))

View File

@@ -983,6 +983,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
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["string-length"] = function(s) { return String(s).length; };
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
PRIMITIVES["concat"] = function() {
@@ -1600,6 +1601,7 @@ PLATFORM_PARSER_JS = r"""
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
var charFromCode = PRIMITIVES["char-from-code"];
"""

View File

@@ -881,6 +881,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0
"core.strings": '''
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["char-from-code"] = lambda n: chr(_b_int(n))
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()

View File

@@ -323,6 +323,11 @@
:returns "number"
:doc "Length of string in characters.")
(define-primitive "char-from-code"
:params ((n :as number))
:returns "string"
:doc "Convert Unicode code point to single-character string.")
(define-primitive "substring"
:params ((s :as string) (start :as number) (end :as number))
:returns "string"

View File

@@ -830,11 +830,21 @@
(define py-emit-infix
(fn ((op :as string) (args :as list) (cell-vars :as list))
(let ((py-op (py-op-symbol op)))
(if (and (= (len args) 1) (= op "-"))
(str "(-" (py-expr-with-cells (first args) cell-vars) ")")
(str "(" (py-expr-with-cells (first args) cell-vars)
" " py-op " " (py-expr-with-cells (nth args 1) cell-vars) ")")))))
(let ((py-op (py-op-symbol op))
(n (len args)))
(cond
(and (= n 1) (= op "-"))
(str "(-" (py-expr-with-cells (first args) cell-vars) ")")
(= n 2)
(str "(" (py-expr-with-cells (first args) cell-vars)
" " py-op " " (py-expr-with-cells (nth args 1) cell-vars) ")")
;; Variadic: left-fold (a op b op c op d ...)
:else
(let ((result (py-expr-with-cells (first args) cell-vars)))
(for-each (fn (arg)
(set! result (str "(" result " " py-op " " (py-expr-with-cells arg cell-vars) ")")))
(rest args))
result)))))
;; --------------------------------------------------------------------------

View File

@@ -851,6 +851,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["char-from-code"] = lambda n: chr(_b_int(n))
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()

View File

@@ -940,6 +940,18 @@ def _load_types(env):
# types.sx uses component-has-children (no ?), test runner has component-has-children?
if "component-has-children" not in env:
env["component-has-children"] = lambda c: getattr(c, 'has_children', False)
# types.sx uses map-dict for record type resolution
if "map-dict" not in env:
from shared.sx.types import Lambda as _Lambda
def _map_dict(fn, d):
result = {}
for k, v in d.items():
if isinstance(fn, _Lambda):
result[k] = _trampoline(_eval([fn, k, v], env))
else:
result[k] = fn(k, v)
return result
env["map-dict"] = _map_dict
# Try bootstrapped types first, fall back to eval
try: