Add index-of string primitive: spec, Python, JS, rebootstrap
(index-of s needle from?) returns first index of needle in s, or -1. Optional start offset. Specced in primitives.sx, implemented in both hand-written primitives.py and bootstrapper templates, rebootstrapped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,7 @@
|
||||
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
||||
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
||||
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
||||
PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
|
||||
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
|
||||
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) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
||||
@@ -3316,87 +3317,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (typeof aser === "function") PRIMITIVES["aser"] = aser;
|
||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
||||
|
||||
// =========================================================================
|
||||
// Extension: Delimited continuations (shift/reset)
|
||||
// =========================================================================
|
||||
|
||||
function Continuation(fn) { this.fn = fn; }
|
||||
Continuation.prototype._continuation = true;
|
||||
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
||||
|
||||
function ShiftSignal(kName, body, env) {
|
||||
this.kName = kName;
|
||||
this.body = body;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||
|
||||
var _resetResume = [];
|
||||
|
||||
function sfReset(args, env) {
|
||||
var body = args[0];
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} catch (e) {
|
||||
if (e instanceof ShiftSignal) {
|
||||
var sig = e;
|
||||
var cont = new Continuation(function(value) {
|
||||
if (value === undefined) value = NIL;
|
||||
_resetResume.push(value);
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} finally {
|
||||
_resetResume.pop();
|
||||
}
|
||||
});
|
||||
var sigEnv = merge(sig.env);
|
||||
sigEnv[sig.kName] = cont;
|
||||
return trampoline(evalExpr(sig.body, sigEnv));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sfShift(args, env) {
|
||||
if (_resetResume.length > 0) {
|
||||
return _resetResume[_resetResume.length - 1];
|
||||
}
|
||||
var kName = symbolName(args[0]);
|
||||
var body = args[1];
|
||||
throw new ShiftSignal(kName, body, env);
|
||||
}
|
||||
|
||||
// Wrap evalList to intercept reset/shift
|
||||
var _baseEvalList = evalList;
|
||||
evalList = function(expr, env) {
|
||||
var head = expr[0];
|
||||
if (isSym(head)) {
|
||||
var name = head.name;
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
}
|
||||
return _baseEvalList(expr, env);
|
||||
};
|
||||
|
||||
// Wrap aserSpecial to handle reset/shift in SX wire mode
|
||||
if (typeof aserSpecial === "function") {
|
||||
var _baseAserSpecial = aserSpecial;
|
||||
aserSpecial = function(name, expr, env) {
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
return _baseAserSpecial(name, expr, env);
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap typeOf to recognize continuations
|
||||
var _baseTypeOf = typeOf;
|
||||
typeOf = function(x) {
|
||||
if (x != null && x._continuation) return "continuation";
|
||||
return _baseTypeOf(x);
|
||||
};
|
||||
|
||||
|
||||
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
||||
var parse = sxParse;
|
||||
|
||||
@@ -3488,4 +3408,4 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
|
||||
else global.Sx = Sx;
|
||||
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
@@ -299,6 +299,10 @@ def prim_slice(coll: Any, start: int, end: Any = None) -> Any:
|
||||
return coll[start:]
|
||||
return coll[start:int(end)]
|
||||
|
||||
@register_primitive("index-of")
|
||||
def prim_index_of(s: str, needle: str, start: int = 0) -> int:
|
||||
return str(s).find(needle, int(start))
|
||||
|
||||
@register_primitive("starts-with?")
|
||||
def prim_starts_with(s, prefix: str) -> bool:
|
||||
if not isinstance(s, str):
|
||||
|
||||
@@ -1386,6 +1386,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
||||
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
||||
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
||||
PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
|
||||
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
|
||||
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) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
||||
|
||||
@@ -1705,6 +1705,7 @@ PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||
PRIMITIVES["index-of"] = lambda s, needle, start=0: str(s).find(needle, start)
|
||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||
|
||||
@@ -307,6 +307,11 @@
|
||||
:returns "any"
|
||||
:doc "Slice a string or list from start to end (exclusive). End is optional.")
|
||||
|
||||
(define-primitive "index-of"
|
||||
:params (s needle &rest from)
|
||||
:returns "number"
|
||||
:doc "Index of first occurrence of needle in s, or -1 if not found. Optional start index.")
|
||||
|
||||
(define-primitive "starts-with?"
|
||||
:params (s prefix)
|
||||
:returns "boolean"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
|
||||
"""
|
||||
sx_ref.py -- Generated from reference SX evaluator specification.
|
||||
|
||||
@@ -725,6 +726,7 @@ PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||
PRIMITIVES["index-of"] = lambda s, needle, start=0: str(s).find(needle, start)
|
||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||
@@ -1169,64 +1171,6 @@ def _wrap_aser_outputs():
|
||||
aser_fragment = _aser_fragment_wrapped
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Extension: delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
# Wrap eval_list to inject shift/reset dispatch
|
||||
_base_eval_list = eval_list
|
||||
def _eval_list_with_continuations(expr, env):
|
||||
head = first(expr)
|
||||
if type_of(head) == "symbol":
|
||||
name = symbol_name(head)
|
||||
args = rest(expr)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
return _base_eval_list(expr, env)
|
||||
eval_list = _eval_list_with_continuations
|
||||
|
||||
# Inject into aser_special
|
||||
_base_aser_special = aser_special
|
||||
def _aser_special_with_continuations(name, expr, env):
|
||||
if name == "reset":
|
||||
return sf_reset(expr[1:], env)
|
||||
if name == "shift":
|
||||
return sf_shift(expr[1:], env)
|
||||
return _base_aser_special(name, expr, env)
|
||||
aser_special = _aser_special_with_continuations
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Public API
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user