CruftScript standard library
Lists which JavaScript built-in methods a CruftScript .fts body may call: the string, array, RegExp, JSON, Math, Number, and Object methods the checker gives a sound signature, which ones are rejected, and the build-time diagnostic you get when a method is not in the table.
Node hands a script the whole JavaScript standard library and its own modules on top; CruftScript admits a curated subset, the built-in methods the checker can give a sound signature. This page enumerates that surface inside a CruftScript compartment: which built-in methods a .fts body may call, which are rejected, and where the boundary of the sound stdlib sits today. It is the companion enumeration to CruftScript, which covers the language model, the boundary machinery, and the T0–T3 temporal stack but deliberately stops short of listing methods. Read that page first.
The surface is broad enough to port real string- and array-shaped code unmodified: replace chains, regex literals, split/map/join/filter with inline callbacks, and template literals all run.
Each entry below names a built-in method and whether a .fts body may call it. A method is OK when the compartment type-checks, lowers, and returns the right value. It is rejected when the checker or lowerer refuses it, and the entry records the diagnostic code you get at build time.
How the stdlib is built
There is no any, no ambient lib.d.ts, and no runtime reflection into JavaScript's prototypes. Instead the type checker carries a small signature table that registers a handful of built-in receiver types as sound Object-typed aliases whose members are readonly methods:
A call like s.replace(a, b) is admitted by the same receiver-function path that already admits a class-method call g.greet(x); a matching lowering node then dispatches to the real engine built-in at run time.
Two consequences follow, and both show up in the tables below. First, the signatures are sound approximations, so a return that JavaScript widens (Array.prototype.find returns T | undefined) is admitted at the narrower sound type and your declared return must match it. Second, a method that is simply not in the table is refused with PropertyReadUnsupported rather than approximated unsoundly. Absence is the default; presence is curated.
String
Every String method real templating code reaches for is present, including all three replace overloads. The callback form of replace runs, and string escape sequences are real ("\n" is a newline).
| Method | Status | Notes |
|---|---|---|
replace(string, string) | OK | replaces first match |
replace(RegExp, string) | OK | /l/g literal receiver-arg |
replace(RegExp, callback) | OK | inline (m) => … replacer runs |
replaceAll(string, string) | OK | |
split(string or RegExp) | OK | returns Array<string> |
slice(number, number?) | OK | |
substring(number, number?) | OK | |
trim() | OK | |
toUpperCase() / toLowerCase() | OK | |
charAt(number) | OK | |
concat(string) | OK | |
indexOf(string) | OK | |
includes(string) | OK | |
startsWith / endsWith(string) | OK | |
padStart(number, string?) | OK | padEnd likewise |
repeat(number) | OK | |
match(RegExp) | OK | returns Array<string> or null |
.length | OK | property read, number |
// str.fts, every method here type-checks and runs
boundary default = secure
compartment Core boundary(secure) {
export function shout(s: string): string boundary(secure) {
return s.trim().replace(/l/g, (m) => m.toUpperCase()).toUpperCase()
}
}
// edge.mjs
import { shout } from "./str.fts";
shout(" hello "); // "HELLO"
Array
Non-callback methods are present, and the callback-taking methods run today with inline arrow callbacks: map, filter, reduce, find, findIndex, some, every, and flatMap. The arrow's parameters are typed from the element type, and the callback is invoked same-turn by the engine's own iteration.
| Method | Status | Notes |
|---|---|---|
map((x) => …) | OK | inline callback runs |
filter((x) => boolean) | OK | |
reduce((acc, x) => …, init) | OK | |
some / every((x) => boolean) | OK | returns boolean |
find((x) => boolean) | OK | returns T or undefined, see below |
findIndex((x) => boolean) | OK | returns number |
flatMap((x) => Array<U>) | OK | inline callback runs |
join(string) | OK | |
push(x) | OK | returns new length |
slice(number, number?) | OK | returns Array<T> |
concat(Array<T>) | OK | |
indexOf(x) / includes(x) | OK | |
.length | OK | |
forEach((x) => …) | rejected | UnsupportedBodyExpression |
sort() / sort(cmp) | rejected | PropertyReadUnsupported (not in table) |
flat() | rejected | PropertyReadUnsupported (not in table) |
find returns the sound T | undefined, so declaring a bare number return fails with ReturnTypeMismatch; declare number | undefined and it passes. This is the sound-approximation rule working as intended.
forEach is effectively unavailable. Its return type is void, so calling it as a statement expression is refused (UnsupportedBodyExpression), and the usual forEach idiom mutates an outer variable from inside the callback, which is also refused (see the mutation note below). Use reduce or map instead.
// array callbacks: filter / map / flatMap all run
export function evens(a: Array<number>): Array<number> boundary(secure) {
return a.filter((x) => x > 0).map((x) => x * 2)
}
RegExp
Regex literals lex and type as RegExp, and the common uses, as a replace or split argument, String.match, and .test on a named RegExp, all work. A constructed new RegExp("…") works too, and escape classes such as \d inside a literal behave.
| Form | Status | Notes |
|---|---|---|
/pat/flags as replace/split/match arg | OK | |
let r = /pat/; r.test(s) | OK | assign, then call |
new RegExp("pat").test(s) | OK | |
/pat/.test(s) (literal as immediate receiver) | rejected | ReturnTypeMismatch; bind to a let first |
The one rough edge: a regex literal used directly as a call receiver (/^h/.test(s)) is rejected. Bind it to a local first (let r = /^h/; return r.test(s)) and it type-checks and runs.
JSON, Number, Math, Object
| Method | Status | Notes |
|---|---|---|
JSON.stringify(v) | OK | |
JSON.parse(s) | OK | returns unknown (must narrow) |
Math.floor / round / max / … | OK | namespace methods |
x.toFixed(number) | OK | on a number receiver |
Object.keys(o) | OK | returns Array<string> |
parseInt(s, radix) | rejected | global fn not resolvable |
parseFloat(s) | rejected | global fn not resolvable |
Number(x) | rejected | global fn not resolvable |
JSON.parse soundly returns unknown, keeping with the language's bottom-type discipline: parsed data is untyped until you narrow it, either with a type assertion or an asserted member read. The three global coercion functions parseInt, parseFloat, and Number do not produce a usable typed result: a .fts body that returns parseInt(s, 10) as a declared number is rejected at T0 with ReturnTypeMismatch, so you cannot reach a number through them. Namespaced numeric work (Math.*, x.toFixed) is the supported path.
Language features in the body
| Feature | Status | Notes |
|---|---|---|
Template literals with ${...} | OK | |
Ternary c ? a : b | OK | plain boolean head |
Optional chaining a.b?.c | OK | with declared nested types |
Nullish coalescing x ?? d | OK | fallback taken when the head is nullish |
Nested typed member access a.b.c | OK | |
Local reassignment n = n + 1 | OK | at statement level |
Compound update n += 1, n++ | OK | at statement level |
Spread after an element [0, ...a] | OK | |
Spread in first position [...a], [...a, ...b] | rejected | ReturnTypeMismatch |
Spreading a non-array [...n] | rejected | ArgumentTypeMismatch |
Array destructuring let [x, y] = a | rejected | UnresolvedValueRef |
Object destructuring let { c } = o | rejected | UnsupportedBodyExpression |
| Mutation inside a callback body | rejected | write forms not lowered across the callback |
Optional chaining and nullish coalescing compose: o.a?.b ?? 99 returns the fallback when the chain is undefined. Statement-level reassignment of a local works, but assignment reaching into a callback body does not, which is the second reason forEach is unusable. Array spread runs when at least one plain element precedes it ([0, ...a]); a spread in the leading position, and both destructuring forms, are parser-or-lowering gaps rather than soundness refusals.
Limitations
What a developer coming from JavaScript will find missing is a short, specific list. Each of these fails loudly at build time (T0) with the diagnostic named below, rather than misbehaving at run time.
Array.prototype.sort,flat, andforEach. The first two are absent from the signature table (PropertyReadUnsupported);forEachis present in spirit but blocked by itsvoidreturn and the callback-mutation gap. Reach forreduceormap.- Global coercion functions
parseInt,parseFloat,Number. These do not resolve to a typed result; useMath.*andx.toFixedinstead. - A regex literal as an immediate call receiver.
/^h/.test(s)is rejected; bind the literal to aletfirst. - Leading-position spread and destructuring.
[0, ...a]runs, but[...a],[...a, ...b],let [x, y] = a, andlet { c } = odo not. - Mutation across a callback boundary. A callback cannot write back to an outer local, so the mutate-an-accumulator idiom is unavailable; fold with
reduceinstead.
None of these are soundness compromises; they are curation and parser-coverage edges. The stdlib grows by adding entries to the signature table, so the boundary moves outward one method family at a time, and the array-callback surface is now inside it.