CruftScript statements and body language

The per-element reference for the code inside a CruftScript function or method body: declarations, if and switch, ternaries, operators, classes, and array and string calls. Each entry says whether the form is checked, runs, or is rejected, since loops, recursion, and try/catch are deliberately left out.

This is the per-element reference for the code inside a CruftScript function or method body: declarations, control flow, expressions, and operators. For the language overview see the CruftScript reference; for the type system and the boundary system see type system and boundaries.

What a body can do

TypeScript hands a function body the whole language and erases the types before it runs. CruftScript keeps the types and runs the body itself, so a body holds only the forms the checker can prove and the runtime can execute. That surface is now broad: ordinary expressions, most operators, if/else and switch, ternaries, local bindings and reassignment, classes with methods, and the array and string libraries all run and return ordinary values. The narrow parts are the ones a sound, non-recursive, loop-free language gives up on purpose, and they are collected under Limitations.

You run a body two ways. cruft app.fts checks the file and then runs it; a JavaScript or TypeScript entry can also import a compartment's exported functions and call them, and the call is validated as it crosses the boundary. A form the checker refuses fails cruft --check outright and never runs. A small set of forms (for, while, async, object spread) pass the check but refuse when the body is asked to run. Each element below says which it is.

Two invariants hold throughout: no any (an operand the checker cannot type is an error), and unknown is a bottom you must narrow before use.

A body is a sequence of statements, one per line. A newline separates two statements; a semicolon does not, so write each binding, assignment, and return on its own line rather than joining them with ;.

Declarations and bindings

const and let local bindings

const answer: number = 42
let count = 3

Both parse to one declaration distinguished only by a mutable flag (let mutable, const not). The checker infers the initializer's type, enforces a declared annotation (a mismatch is a LocalInitializerTypeMismatch), and otherwise types the binding unknown. Only a typed let may be a constructor assignment target. The initializer is mandatory: an initializer-less let x: T is rejected. One or more leading bindings followed by a return run as an ordinary body:

let a: number = n + 1
let b: number = a * 2
return b

return

return x * 2

The value-producing statement. The checker enforces that the returned expression is assignable to the declared return type, forbids a class-constructor value from escaping, and requires class methods to declare a return type and end in a return. Returns are not confined to the tail: a guarded early return and an if-return chain both run (see if/else). A bare valueless return is rejected.

Arrow functions and block bodies

xs.map((n: number) => n + n)
xs.map((s) => { const u = s.toUpperCase(); return u.concat("!") })

An arrow is CruftScript's only closure form, admitted only as an inline callback argument to an admitted higher-order builtin (map/filter/reduce/find/ findIndex/some/every/flatMap/replace). It is not first-class: binding an arrow to a local and calling it later, or returning it, is a checker error. An unannotated parameter is filled contextually from the expected callback signature, never silently any. A block body admits a single return, an if-return chain, and leading const/let bindings. Callbacks are same-turn only, with nothing retained across turns.

Function calls and declarations

The exported top-level function is the first-class declaration form. A call is an expression, recognized where an expression is expected (a return, an initializer, an argument); a bare call on its own line and a nested inner function are captured as opaque statements. Recursion is rejected: the checker refuses any self-recursive function and any cycle among same-compartment functions, reporting RecursiveCallGraph. Same-compartment calls are trusted; imported and cross-compartment calls are validated at the boundary.

Class declarations

compartment C {
  export class Point {
    x: number
    y: number
    constructor(x: number, y: number) { this.x = x; this.y = y }
    dist(): number { return this.x + this.y }
  }
}

A class is admitted only inside a compartment (a top-level class is a diagnostic), in a bounded data-class shape: type-annotated public data fields (with readonly/?/initializer), static fields, one field-assigning constructor, and plain methods. new Point(1, 2) builds a real instance, this reads its fields, and calling a method runs the method body. No extends/super, no #private members, no get/set accessors, no accessor, no decorators: each is a refusal. Field and static initializers must conform, and method bodies follow the same rules as function bodies.

Control flow

A control-flow condition is not a free boolean expression: it must parse as a recognized narrowing guard or a proven boolean, or the construct is rejected.

if / else

if (typeof x === "string") { return x } else { return "n" }

The condition must be a supported guard (typeof/instanceof/in/truthiness/ comparison); an unsupported one is an error. Both branches get their own cloned environment, so a narrowing does not leak across branches, and else if desugars to a nested if. Both terminal if/else and a guarded early return run:

if (n < 0) { return 0 }
return n * 2

switch / case

switch (kind) {
  case "get": return 1
  case "post": return 2
  default: return 0
}

A switch over a number, string, or boolean subject runs, with literal case labels that conform to the subject and a returning default. Arms terminate by return; there is no break, and every selected case must reach a return. It runs in an ordinary function body, not only as a class-method tail.

Conditional (ternary) expressions

tag === "admin" ? 1 : 0
flag ? 10 : 20

Both shapes run: a narrowing-guard conditional whose head is a typeof or equality guard refines each branch, and a general ternary with a plain boolean head selects a branch at runtime. Branches with no common type yield unknown, which the caller must narrow.

for and while

for (let i = 0; i < 3; i++) { acc = acc + i }
while (i < 5) { total = total + i; i = i + 1 }

Loops are the language's deliberate hole. Only the C-style three-clause for parses (for...of and for...in are rejected outright), and both for and while pass the checker but refuse when the body runs, reporting UnsupportedBodyShape. There is no break or continue. Iterate with the array methods (map/filter/reduce/…) instead; see Limitations.

Expressions and operators

Operators and the expression surface

Arithmetic
+ - * / %*
Comparison
< > <= >=
Equality (strict only)
=== !==
Logical
&& || !
Bitwise and shifts
& | ^<< >>
Unary
- !
Nullish and optional
???.

All of these run over runtime operands, not only over literals. Equality is strict: loose == and != are rejected. There is no operator overloading, and + is the only coercing operator (string concat). Operands must be concrete primitives.

Precedence does not match JavaScript's. A mixed arithmetic-and-comparison expression such as 1 + 5 > 4 is rejected because it does not group as (1 + 5) > 4. Parenthesize any expression that mixes arithmetic with comparison.

Template literals

return `hello ${name}`

${} interpolation desugars into a + concat chain, so a template is soundly string-typed and runs. Interpolants must be string or number (a boolean or object interpolant is an error, not a silent stringify). Escape sequences are interpreted, so "a\nb" is three characters with a real newline between them. Tagged templates are not recognized.

Optional chaining (?.)

obj?.prop      obj?.[i]      fn?.(args)

All three variants parse. The checker widens the result to T | undefined and forces narrowing before use (a non-optional in-bounds static index instead narrows to plain T). It runs, short-circuiting to undefined when the object is absent.

Regular-expression literals

let re = /foo/g
re.test("food")

A /pattern/flags literal desugars to new RegExp("pattern", "flags") and rides the RegExp surface (test, exec, source, flags, global). match and exec return Array<string> | null, so narrow before indexing. Bind the literal to a let before calling it: a regex literal used directly as a call receiver (/^h/.test(s)) mis-splits and is rejected.

Assignment and compound update

n = n + 4      i++      ++i      xs[1] = 99      total += 5

Assignment writes to a mutable let target, an object property, or an array element with a conforming value; element writes require a proven Array<T> and a numeric in-bounds index. ++ and -- require a proven number. Of the compound forms, += runs; the others (-=, *=, ??=, and the rest) are rejected, so write them out as x = x - 1. These run in ordinary function and method bodies.

Limitations

The following ordinary JS/TS forms have no body node and do not run. Most are rejected by cruft --check, so the file fails before anything executes; a few are accepted by the checker and refuse only when the body runs. The stance is to refuse rather than admit a form the checker cannot model: TypeScript would bind catch (e) as any or let await erase a Promise's type, and CruftScript gives those forms no node so the checker never vouches for what it cannot prove.

Rejected at the check (cruft --check fails):

  • Loose equality == and !=; strict === / !== only.
  • try / catch / finally and throw.
  • for...of / for...in, destructuring, and delete.
  • Compound assignment other than += (-=, *=, ??=, …).
  • Recursion, self or mutual (RecursiveCallGraph).
  • instanceof as a returned value (it is admitted only as an if guard).
  • Unnarrowed dynamic shape reads. JSON.parse(s).prop with no assertion is rejected; a wrong assertion type-checks but faults at the boundary (RuntimeTypeAssertionFailed). Assert the shape first: JSON.parse(s) as { v: number }.
  • Retained or returned callbacks, and spreading a non-array ([...s] over a string).

Accepted by the checker but refused when run:

  • for and while loops (UnsupportedBodyShape). Iterate with array methods.
  • General async / await in a body (UnsupportedFunctionShape).
  • Object spread { ...o } (UnsupportedBodyShape); array spread into a new literal, [0, ...xs], does run.