Language docs

Optimized compiler

Current shared TypeScript compiler pipeline, optimizer passes, inlining rules, and peephole optimizations.

The current optimized compiler is the shared TypeScript compiler plus the shared TypeScript optimizer. Deno, Node, Bun, and the web playground all use the same core implementation from typescript/core/src/compiler.ts and typescript/core/src/optimizer.ts.

Optimization is optional in the command-line tools. Pass --opt or -O to run it:

deno task compile --opt program.ffp
node node/bin/ff-compile.ts --opt program.ffp
bun bun/bin/ff-compile.ts --opt program.ffp

The web playground exposes the same optimizer through its optimize toggle.

Pipeline

The optimized compile path is:

  1. Preprocess .ffp input with .load, .import, .m, and stdlib support.
  2. Tokenize the preprocessed source on whitespace.
  3. Compile tokens to IR.
  4. Validate the unoptimized IR unless validation is disabled.
  5. Run the optimizer when --opt / -O is enabled.
  6. Print IR, disassemble, or encode optimized IR to .ffb bytecode.

The optimizer does not change the language frontend. It receives ordinary IR instructions:

{ op: "push" | "call", value: bigint, meta?: {...} }

During bytecode encoding, a push becomes value << 1 and a call becomes (value << 1) | 1. Calls to opcode 0 (nop) are dropped by the encoder.

IR Metadata

The compiler attaches metadata that the optimizer uses:

MetadataSourceOptimizer meaning
nameword names, pointers, strings, special formsPreserves readable names in IR and direct-call lowering.
pointer[word], definition names, .pointerMarks a pushed integer as a word pointer. Referenced pointer definitions are retained.
inline.inlineForces inlining for the marked call or definition.
unsafe.unsafePrevents optimizer inlining for queue-sensitive calls or definitions.
uidoptimizer-created anonymous definitionsGives a stable synthetic word id to a quotation.

Compiler directives apply to the previous IR instruction. For example, .inline marks the previous instruction as safe to inline, and .unsafe marks the previous instruction as unsafe for optimizer inlining.

Optimizer Passes

Optimizer.optimize() resets state, records the pre-optimization IR size, and then runs repeated optimization loops.

Each optimization loop does three things:

  1. Run all peephole rules until no peephole rule matches.
  2. Scan definitions and quotations into an internal definition map.
  3. Inline eligible user-defined words.

The optimizer runs at least two loops and at most twenty loops. After the loop, it pulls user definitions out of the main IR, replaces quotations with pointer pushes, and then adds back only definitions still referenced by the optimized main program or by other retained definitions.

The retained definition pass matters because F-flat-minor definitions are runtime values: a word can be called directly or pushed as a pointer and later evaluated. The optimizer keeps definitions that remain reachable through either calls or pointer pushes.

Peephole Optimizations

Peephole rules match short adjacent IR windows. The peephole engine scans from left to right, applies the first matching rule, rewinds a little, and repeats until a full scan makes no changes.

The current rules are listed below using source-level notation. These are descriptions of the IR transformations, not a separate surface syntax.

No-Op Removal

PatternReplacementEffect
nopemptyRemoves explicit no-op calls.

Null Sequences

PatternReplacementEffect
swap swapemptySwapping twice restores the original stack.
dup dropemptyDuplicating and immediately discarding restores the original stack.
q< q>emptyMoving the top stack value to the queue and immediately back is a no-op.
clock dropemptyReads the clock and discards the result.
rand dropemptyReads a random value and discards the result.
depth dropemptyReads stack depth and discards the result.
~ ~emptyBitwise NOT twice restores the original integer.
n dropemptyPushes a constant or pointer and immediately discards it.
0 evalemptyEvaluating pointer 0 is treated as a no-op.

The clock drop and rand drop rules remove observable reads whose result is unused. That is consistent with the optimizer’s current treatment of those words as removable when their value is immediately discarded.

Direct-Call Lowering

PatternReplacementEffect
[word] evalwordConverts an indirect call through a pushed pointer into a direct IR call.

This also works for any pushed integer followed by eval; the replacement is a direct call to that integer value. If the pushed pointer had a readable name, the optimizer strips a leading & from that name for the resulting call metadata.

Constant Folding

PatternReplacementEffect
a b +a+bFolds addition of two pushed constants.
a b -a-bFolds subtraction of two pushed constants.
a b *a*bFolds multiplication of two pushed constants.
a b /a/bFolds integer division when b is nonzero.
a b %a%bFolds remainder when b is nonzero.
a b <<a<<bFolds left shifts.
a b >>a>>bFolds right shifts.
a b &a&bFolds bitwise AND.
a b |a|bFolds bitwise OR.
a ~~aFolds bitwise NOT.
a b <flagFolds less-than comparisons to 1 or 0.
a b =flagFolds equality comparisons to 1 or 0.
a b >flagFolds greater-than comparisons to 1 or 0.
a b ^a^bFolds exponentiation when b is nonnegative.
a dupa aPropagates a pushed constant through dup.

The / and % folds guard zero divisors. The ^ constant fold guards negative exponents because native bigint exponentiation rejects them. The 1 ^ identity is still safe for nonconstant bases because exponent 1 does not trigger that runtime error.

Algebraic Simplification

PatternReplacementEffect
0 +emptyRewrites a 0 + to a.
swap ++Addition is commutative, so the swap is unnecessary.
0 -emptyRewrites a 0 - to a.
1 *emptyRewrites a 1 * to a.
swap **Multiplication is commutative, so the swap is unnecessary.
1 /emptyRewrites a 1 / to a.
0 |emptyRewrites a 0 | to a.
0 <<emptyRewrites a 0 << to a.
0 >>emptyRewrites a 0 >> to a.
1 ^emptyRewrites a 1 ^ to a.
swap &&Bitwise AND is commutative, so the swap is unnecessary.
swap ||Bitwise OR is commutative, so the swap is unnecessary.
swap ==Equality is commutative, so the swap is unnecessary.

These are local stack-shape rewrites. For example, 0 + means the adjacent instructions push 0 then call +; the left operand remains earlier in the IR.

Control Flow

PatternReplacementEffect
0 [quote] ?emptyRemoves a statically false conditional branch.
n [quote] ?, n != 0quoteConverts a statically true conditional branch to a direct call.

The true-branch rule calls the pushed quotation pointer directly. It relies on the same truth convention as the runtime: 0 is false and any nonzero integer is true.

Quotation Compaction

PatternReplacementEffect
[ ][0]Replaces an empty quotation with pointer 0.
[ word ][word]Replaces a single-word quotation with that word pointer.

This works with the 0 eval rule: an empty quotation that is immediately evaluated can disappear after repeated peephole passes.

The single-word rule only compacts quotations whose body is one call instruction. Numeric quotations such as [ 1 ] remain anonymous definitions.

Inlining

After each peephole phase, the optimizer records user definitions and inlines eligible calls.

A user word is inlined when:

  • the call itself is not marked .unsafe;
  • the target definition exists in the optimizer’s definition map;
  • the optimizer is not already inlining that same word recursively;
  • the definition terminator is not marked .unsafe;
  • either the call or definition is marked .inline, or the definition is small.

Small definitions are currently inlined when the full stored definition length is at most 7 IR instructions. Stored definitions include the pointer, :, body, and ;, so this corresponds to a body of four IR instructions or fewer.

When .inline is present on the call or definition, the optimizer inlines the definition body with an effectively unlimited length limit. Recursive inlining is still blocked by the seen list.

The body inserted during inlining excludes the definition wrapper:

[word] : body... ;

becomes:

body...

Definition Pulling And Retention

The optimizer separates definitions from the main program after its repeated passes:

  • Named definitions are removed from the main IR and stored by word id.
  • Anonymous quotations are assigned synthetic ids above the system opcode range.
  • Anonymous quotations are converted into named-definition-shaped IR internally.
  • The main program receives a pointer push where the quotation originally was.
  • Only definitions referenced by calls or pointer pushes are added back to the front of the optimized IR.

This final layout keeps bytecode compact by dropping dead definitions while preserving words that are still callable or evaluable through pointers.

Statistics

When --stats is used with --opt, the CLI prints optimizer counters:

CounterMeaning
pre_optimization_ir_sizeInput IR instruction count.
post_optimization_ir_sizeFinal optimized IR instruction count.
user_defined_anon_defsAnonymous quotations found on the first pass.
user_defined_named_defsNamed definitions found on the first pass.
post_optimization_user_defsDefinitions retained after optimization.
inlined_callsCalls replaced with definition bodies.
peephole_optimizationsPeephole rule applications.
optimization_passesWhole optimizer loop count.

Current Limits

The optimizer is intentionally local and simple. It is not a dataflow optimizer, does not infer stack effects, and does not prove general equivalences.

Notable current gaps:

  • no stack-effect-aware rewrites beyond fixed adjacent instruction windows;
  • no general dead-code analysis inside retained definitions;
  • no replacement of empty named definitions with a direct no-op strategy.

See .agents/docs/plans/typescript-optimizer-peephole-candidates.md for planned peephole extensions that are not part of the current optimizer yet.