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:

Receiver types
stringArray<T>RegExpJSONMathNumberObject

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).

MethodStatusNotes
replace(string, string)OKreplaces first match
replace(RegExp, string)OK/l/g literal receiver-arg
replace(RegExp, callback)OKinline (m) => … replacer runs
replaceAll(string, string)OK
split(string or RegExp)OKreturns 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?)OKpadEnd likewise
repeat(number)OK
match(RegExp)OKreturns Array<string> or null
.lengthOKproperty 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.

MethodStatusNotes
map((x) => …)OKinline callback runs
filter((x) => boolean)OK
reduce((acc, x) => …, init)OK
some / every((x) => boolean)OKreturns boolean
find((x) => boolean)OKreturns T or undefined, see below
findIndex((x) => boolean)OKreturns number
flatMap((x) => Array<U>)OKinline callback runs
join(string)OK
push(x)OKreturns new length
slice(number, number?)OKreturns Array<T>
concat(Array<T>)OK
indexOf(x) / includes(x)OK
.lengthOK
forEach((x) => …)rejectedUnsupportedBodyExpression
sort() / sort(cmp)rejectedPropertyReadUnsupported (not in table)
flat()rejectedPropertyReadUnsupported (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.

FormStatusNotes
/pat/flags as replace/split/match argOK
let r = /pat/; r.test(s)OKassign, then call
new RegExp("pat").test(s)OK
/pat/.test(s) (literal as immediate receiver)rejectedReturnTypeMismatch; 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

MethodStatusNotes
JSON.stringify(v)OK
JSON.parse(s)OKreturns unknown (must narrow)
Math.floor / round / max / …OKnamespace methods
x.toFixed(number)OKon a number receiver
Object.keys(o)OKreturns Array<string>
parseInt(s, radix)rejectedglobal fn not resolvable
parseFloat(s)rejectedglobal fn not resolvable
Number(x)rejectedglobal 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

FeatureStatusNotes
Template literals with ${...}OK
Ternary c ? a : bOKplain boolean head
Optional chaining a.b?.cOKwith declared nested types
Nullish coalescing x ?? dOKfallback taken when the head is nullish
Nested typed member access a.b.cOK
Local reassignment n = n + 1OKat statement level
Compound update n += 1, n++OKat statement level
Spread after an element [0, ...a]OK
Spread in first position [...a], [...a, ...b]rejectedReturnTypeMismatch
Spreading a non-array [...n]rejectedArgumentTypeMismatch
Array destructuring let [x, y] = arejectedUnresolvedValueRef
Object destructuring let { c } = orejectedUnsupportedBodyExpression
Mutation inside a callback bodyrejectedwrite 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, and forEach. The first two are absent from the signature table (PropertyReadUnsupported); forEach is present in spirit but blocked by its void return and the callback-mutation gap. Reach for reduce or map.
  • Global coercion functions parseInt, parseFloat, Number. These do not resolve to a typed result; use Math.* and x.toFixed instead.
  • A regex literal as an immediate call receiver. /^h/.test(s) is rejected; bind the literal to a let first.
  • Leading-position spread and destructuring. [0, ...a] runs, but [...a], [...a, ...b], let [x, y] = a, and let { c } = o do 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 reduce instead.

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.