Status: implemented
A body
constis the body's, and binds as a module's does, a module being a function too (a module is a function): it names a value thereturnand the statements after it may use, it may not be written twice, and it is not in its own initializer's scope — except that a function that is the whole initializer has the name as its own, as a moduleconst's does (above). It is evaluated as a module's is, as its own statement, at every call and whether or not thereturnreaches it (shared values):() => { const x = null.x; return 1; }loads and throws when called, as in JavaScript. The parameter is a name of the body too, so aconstmay not take it. A bodyconstmay take a name a scope around it binds, shadowing it as in JavaScript (no-shadowing) — unless the body has already read that name from outside, before theconstor in its own initializer. That is not supported yet and is refused (capture shadowed) rather than compiled to another value: JavaScript resolves every reference in the body to the body'sconst, a read before its declaration throwing and a function written earlier reading it once called, where this compiler would read the capture. It is no restriction of the language — nothing leaks through it — but a forward reference inside a body, whichbody-const-forward-reference.mdtracks.export default (...args) => { const first = args[0]; const pair = [first, first]; return [pair, pair]; };pairis one array however many references reach it, as a moduleconstis one value — which is the whole reason a body has them.The body is an expression or a block, and
valueand{ return value; }denote the same function. As an expression the body is any value except a bare object literal: after=>JavaScript reads{as a block, never as an object, so the spelling is refused rather than read another way, and the object is written in parentheses instead (grouping) —(...args) => ({ a: 1 }), as in JavaScript. The block is any number of statements — declarations, assignments, call statements (call statements), andifstatements (below) — and then one terminating statement, areturnor athrow(below), or a switch with adefault(switch), or a terminal unconditional loop, each ended as every statement is — by its;, or by the newline before the next statement, a guard's by its}, and the last one's by the}(module structure) — and an object literal is an ordinary value again, since afterreturnJavaScript expects an expression.returnand the value share a line: a newline between them ends the statement in JavaScript, which would returnundefined, so it is refused here rather than read another way, exactly as a newline before=>is. Nothing follows the terminating statement: a statement after it is unreachable, and JavaScript's admitting the text does not make it anything but a mistake.A parameterless function called where it is written, with no arguments, denotes its body where the call stands:
a ? (() => { const x = f(); return [x, x]; })() : 4is the idiom for aconstinside an expression, an arm of a conditional say, where no statement can stand, and it lowers to the arm holding[x, x]withxone shared node, no function and no call. Nothing observes the function: it is called once and compared with nothing, so it mints no identity the program could see, and its body evaluates exactly once, where the call stands — a slot of its frame is the enclosing scope's own node, so what the body shares stays shared and nothing else is. A bodyconstthe returned value does not reach is evaluated where the call stands, as in JavaScript; in the graph it is anchored at the nearest position that opens a block, the scope's root or a lazy operand (operators), which is where JavaScript's own semantics can tell no difference under the failure contract.(() => [1, 2])()is[1, 2], and a module holding either hashes the same. A capture a body names only through an unused alias,const x = c;and nothing more, is no slot of its frame, in a body or in a call inlined into one: the alias is dropped, so nothing reads it, and the scope'sconstis anchored as one nothing reaches. A call with an argument,(() => 1)(null.x), has an argument to evaluate; a function with a parameter binds a name; and a body reading its own rest array names what the enclosing scope does not hold — each stays a call. The FunctionalScript writer uses the same idiom in reverse: a value shared under one lazy operand alone, which noconstof the scope could hold without evaluating it whatever the operator decides, is written in a block opened at the operand,a ? (() => { const $0 = [1]; return [$0, $0]; })() : 4, which reads back as the operand. Round-tripping through that writer keeps the graph, not the text: the function written in the source is gone from both.throw value;terminates a block asreturn value;does, and a function whose body ends in it fails at every call — the one failure outcome the language has (failure is one outcome), reached on purpose, as a panic on a broken invariant rather than an expected error, which travels as a value (fjs/AGENTS.md§1.5). Everything else about it is JavaScript's, read asreturnis: the value is any expression and is evaluated first, sothrow f(x)callsfbefore failing;throwand its value share a line, since JavaScript's restricted production forbids a line terminator between them, andthrow;with no value is not JavaScript; and nothing in the language catches the value, so the language promises the failure and not the payload, which an executor carries out of band for a human or a test runner to read.export const todo = () => { throw "not implemented"; }; export const checked = (n) => { const half = n / 2; throw ["not yet", half]; };The body lowers to the node
['throw', v], an operation of one operand that always fails, and it follows the positional laziness every operator has: in an eager position it is evaluated and fails, and in a lazy one — an arm of?:, the right operand of&&,||or??— it is not established unless JavaScript would establish it, soc ? 1 : (() => { throw 0; })()is1under a truec. The FunctionalScript writer spells the node in a terminating position, a block's or the module's, as the statement it came from, and anywhere else — wherever a graph puts one, and an arm of?:onceiflowers there — as the call of a function that throws,(() => { throw v; })(): JavaScript's one spelling of an expression that fails, and FunctionalScript itself — the call of a parameterless function written where it is called, which the front end inlines (above), so the text reads back as the node it was written from and fails at the same point. That is the writer's spelling, not a second source form:throwis a statement only, and an expression that must fail goes through a function that throws, as in JavaScript. The EDAG output carries the node, and the Rust output fails as the VM fails (output).A terminating
if (condition) blockis a guard: a statement of a block body, standing where aconstmay, any number of times before the body's terminating statement. Its block is a block as above — any number of statements and then areturn, athrow, a switch with adefault(switch), or a terminal unconditional loop — so the branch never completes normally, and the statements after the guard, up to and including the body's own terminator, are what runs when the condition is falsy. The condition is any expression, evaluated where the statement stands and tested as?:tests its condition. This terminating-guard form requires braces and has noelse; conditional joins and early returns are described below. An unbraced consequent,if (c) return v;, and anifat module level remain outside admission. No;follows the}, as in JavaScript: one there is the empty statement the language refuses (module structure), and the next statement may share the guard's line.export const unwrap = r => { if (r[0] === 'error') { throw r[1]; } return r[1]; }; export const sign = n => { if (n < 0) { return -1; } if (n > 0) { return 1; } return 0; };The guard is syntactic sugar over the conditional and adds no node: a body
s… if (c) B restdenotesc ? (() => B)() : (() => rest)(), the guard's block and the statements after it each the body of a parameterless function called where it stands (above), and the compiler reads it as exactly that, soif (a) { const x = [1]; return [x, x]; } return 0;anda ? (() => { const x = [1]; return [x, x]; })() : 0are one graph and one hash —['?:', a, ['[]', [x, x]], 0]withxone shared node — and a second guard nests as the alternate of the first. Where aconstlives is which edges reach it: one before the guard is the scope's, evaluated eagerly as JavaScript evaluates it before the condition, soconst a = [1]; if (m) { return a; } return 0;is[',', [a, ['?:', m, a, 0]]], the anchoring rule of operators — andm ? a : ais not folded toa, the condition being an evaluation of its own that may fail; one in the guard's block, or after the guard, is reached from its arm alone, and evaluated only when JavaScript would evaluate it, soif (a) { throw 1; } const y = [2]; return y;is['?:', a, ['throw', 1], ['[]', [2]]]and an unusedconst z = [2]in its place is anchored at the alternate's comma,[',', [z, 1]], never at the function's root. The statements after a guard are JavaScript's one block with the ones before it, so a name that block has bound — a parameter, aconstbefore the guard, or one after an earlier guard — may not be bound again there (duplicate id), and neither may a word that block has already read from an enclosing scope, in a statement before the guard, in the condition or in the guard's block (capture shadowed), since in JavaScript every such read would have named the laterconstbefore its declaration; the guard's block is a block of its own and may shadow either. For this terminating-guard profile, the FunctionalScript writer spells the conditional, and an arm that holds aconstor athrowas the block opened at the operand (above), so the guarded body reads back as the same graph; the EDAG output carries the?:, and the Rust output prints it as it prints any conditional.
An ordinary function also admits a braced if whose selected arm can finish normally, with an optional braced else. A path may instead return from the owning function. The function still has its own explicit terminator:
export default (condition, f) => {
let a = 0;
if (condition) { a += 2; }
return f(a);
};
Evaluate the condition once, then run only the selected arm. Both arms start from the binding values established after the test. The arms may assign at most one binding declared outside them; their normal results select its latest value for following statements. An arm that does not assign it, or an omitted else, supplies its incoming value. Following work such as f(a) runs once after that join. This uses the existing FAST branch results and adds no EDAG operation.
Each arm has its own lexical scope. Its declarations and snapshots stay local, while an assignment to an outer variable establishes the version used after the if. Assignments to branch-local variables do not count as outer writes. The usual declaration, const and capture checks still apply, including to unselected syntax. The one-outer-binding limit counts writes on returning paths too.
A partly returning arm bypasses the normal join and the following work. For example:
export default (a, m, f, g) => {
if (a) {
const x = f();
m = x.a;
if (x.b) { return x.c; }
}
return g(m);
};
When a is false, g receives the incoming m. When a is true and x.b is false, it receives x.a. When both tests are true, the function returns x.c and skips g. The read of x.a remains required even on that returning path. Only normal paths supply the joined binding version and reach the one following g call.
Nested conditionals and loops keep their lexical scopes. Loop-local break and continue retain their targets; an owning return bypasses those loops and finishes its original function. A nested ordinary function keeps its own return owner and does not return from the surrounding function.
This mixed-arm profile does not admit explicit throws within its arms, including nested guards and loops. Standalone terminating throw guards keep their existing admission. Selected expressions can still fail, and an unselected arm performs none of its work. Joins assigning several outer bindings remain a follow-up in reachable block ends. The syntax AST already retains those broader forms even when FJS admission refuses them.
The compiler demo offers the programs guards in module.f.js, each with its real output.
The demo on this page runs sample.f.js, this page's example as a function of one number — a body with a const and two terminating if guards — with every output fjs compile makes of it: the expression graph (EDAG), the Rust and the WebAssembly that Rust builds to. Its Run button calls the default export with -5, 0 and 7, as JavaScript and as WebAssembly, and shows what each call returned or threw.
export default (...a) => {
const n = a[0];
if (n < 0) { return "negative"; }
if (n > 0) { return "positive"; }
return "zero";
};