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["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
||||||
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
||||||
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
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["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["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); };
|
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 aser === "function") PRIMITIVES["aser"] = aser;
|
||||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
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)
|
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
||||||
var parse = sxParse;
|
var parse = sxParse;
|
||||||
|
|
||||||
@@ -3488,4 +3408,4 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
|
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
|
||||||
else global.Sx = 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:]
|
||||||
return coll[start:int(end)]
|
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?")
|
@register_primitive("starts-with?")
|
||||||
def prim_starts_with(s, prefix: str) -> bool:
|
def prim_starts_with(s, prefix: str) -> bool:
|
||||||
if not isinstance(s, str):
|
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["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
||||||
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
||||||
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
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["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["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); };
|
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["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
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["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(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:]
|
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||||
|
|||||||
@@ -307,6 +307,11 @@
|
|||||||
:returns "any"
|
:returns "any"
|
||||||
:doc "Slice a string or list from start to end (exclusive). End is optional.")
|
: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?"
|
(define-primitive "starts-with?"
|
||||||
:params (s prefix)
|
:params (s prefix)
|
||||||
:returns "boolean"
|
: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.
|
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["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
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["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(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:]
|
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
|
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
|
# Public API
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ PRIMITIVES = {
|
|||||||
"Arithmetic": ["+", "-", "*", "/", "mod", "sqrt", "pow", "abs", "floor", "ceil", "round", "min", "max"],
|
"Arithmetic": ["+", "-", "*", "/", "mod", "sqrt", "pow", "abs", "floor", "ceil", "round", "min", "max"],
|
||||||
"Comparison": ["=", "!=", "<", ">", "<=", ">="],
|
"Comparison": ["=", "!=", "<", ">", "<=", ">="],
|
||||||
"Logic": ["not", "and", "or"],
|
"Logic": ["not", "and", "or"],
|
||||||
"String": ["str", "upper", "lower", "trim", "split", "join", "starts-with?", "ends-with?", "replace", "substring"],
|
"String": ["str", "upper", "lower", "trim", "split", "join", "index-of", "starts-with?", "ends-with?", "replace", "substring"],
|
||||||
"Collections": ["list", "dict", "len", "first", "last", "rest", "nth", "cons", "append", "keys", "vals", "merge", "assoc", "range", "concat", "reverse", "sort", "flatten", "zip"],
|
"Collections": ["list", "dict", "len", "first", "last", "rest", "nth", "cons", "append", "keys", "vals", "merge", "assoc", "range", "concat", "reverse", "sort", "flatten", "zip"],
|
||||||
"Higher-Order": ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"],
|
"Higher-Order": ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"],
|
||||||
"Predicates": ["nil?", "number?", "string?", "list?", "dict?", "empty?", "contains?", "odd?", "even?", "zero?"],
|
"Predicates": ["nil?", "number?", "string?", "list?", "dict?", "empty?", "contains?", "odd?", "even?", "zero?"],
|
||||||
|
|||||||
Reference in New Issue
Block a user