Files
lux/docs/COMPILER_OPTIMIZATIONS.md
Brandon Lucas 7e76acab18 feat: rebuild website with full learning funnel
Website rebuilt from scratch based on analysis of 11 beloved language
websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc,
Rust, Go).

New website structure:
- Homepage with hero, playground, three pillars, install guide
- Language Tour with interactive lessons (hello world, types, effects)
- Examples cookbook with categorized sidebar
- API documentation index
- Installation guide (Nix and source)
- Sleek/noble design (black/gold, serif typography)

Also includes:
- New stdlib/json.lux module for JSON serialization
- Enhanced stdlib/http.lux with middleware and routing
- New string functions (charAt, indexOf, lastIndexOf, repeat)
- LSP improvements (rename, signature help, formatting)
- Package manager transitive dependency resolution
- Updated documentation for effects and stdlib
- New showcase example (task_manager.lux)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:05:35 -05:00

11 KiB

Compiler Optimizations from Behavioral Types

This document describes optimization opportunities enabled by Lux's behavioral type system. When functions are annotated with properties like is pure, is total, is idempotent, is deterministic, or is commutative, the compiler gains knowledge that enables aggressive optimizations.

Overview

Property Key Optimizations
is pure Memoization, CSE, dead code elimination, auto-parallelization
is total No exception handling, aggressive inlining, loop unrolling
is deterministic Result caching, test reproducibility, parallel execution
is idempotent Duplicate call elimination, retry optimization
is commutative Argument reordering, parallel reduction, algebraic simplification

Pure Function Optimizations

When a function is marked is pure:

1. Memoization (Automatic Caching)

fn fib(n: Int): Int is pure =
    if n <= 1 then n else fib(n - 1) + fib(n - 2)

Optimization: The compiler can automatically memoize results. Since fib is pure, fib(10) will always return the same value, so we can cache it.

Implementation approach:

  • Maintain a hash map of argument → result mappings
  • Before computing, check if result exists
  • Store results after computation
  • Use LRU eviction for memory management

Impact: Reduces exponential recursive calls to linear time.

2. Common Subexpression Elimination (CSE)

fn compute(x: Int): Int is pure =
    expensive(x) + expensive(x)  // Same call twice

Optimization: The compiler recognizes both calls are identical and computes expensive(x) only once.

Transformed to:

fn compute(x: Int): Int is pure =
    let temp = expensive(x)
    temp + temp

Impact: Eliminates redundant computation.

3. Dead Code Elimination

fn example(): Int is pure = {
    let unused = expensiveComputation()  // Result not used
    42
}

Optimization: Since expensiveComputation is pure (no side effects), and its result is unused, the entire call can be eliminated.

Impact: Removes unnecessary work.

4. Auto-Parallelization

fn processAll(items: List<Item>): List<Result> is pure =
    List.map(items, processItem)  // processItem is pure

Optimization: Since processItem is pure, each invocation is independent. The compiler can automatically parallelize the map operation.

Implementation approach:

  • Detect pure functions in map/filter/fold operations
  • Split work across available cores
  • Merge results (order-preserving for map)

Impact: Linear speedup with core count for CPU-bound operations.

5. Speculative Execution

fn decide(cond: Bool, a: Int, b: Int): Int is pure =
    if cond then computeA(a) else computeB(b)

Optimization: Both branches can be computed in parallel before the condition is known, since neither has side effects.

Impact: Reduced latency when condition evaluation is slow.

Total Function Optimizations

When a function is marked is total:

1. Exception Handling Elimination

fn safeCompute(x: Int): Int is total =
    complexCalculation(x)

Optimization: No try/catch blocks needed around calls to safeCompute. The compiler knows it will never throw or fail.

Generated code difference:

// Without is total - needs error checking
Result result = safeCompute(x);
if (result.is_error) { handle_error(); }

// With is total - direct call
int result = safeCompute(x);

Impact: Reduced code size, better branch prediction.

2. Aggressive Inlining

fn square(x: Int): Int is total = x * x

fn sumOfSquares(a: Int, b: Int): Int is total =
    square(a) + square(b)

Optimization: Total functions are safe to inline aggressively because:

  • They won't change control flow unexpectedly
  • They won't introduce exception handling complexity
  • Their termination is guaranteed

Impact: Eliminates function call overhead, enables further optimizations.

3. Loop Unrolling

fn sumList(xs: List<Int>): Int is total =
    List.fold(xs, 0, fn(acc: Int, x: Int): Int is total => acc + x)

Optimization: When the list size is known at compile time and the fold function is total, the loop can be fully unrolled.

Impact: Eliminates loop overhead, enables vectorization.

4. Termination Assumptions

fn processRecursive(data: Tree): Result is total =
    match data {
        Leaf(v) => Result.single(v),
        Node(left, right) => {
            let l = processRecursive(left)
            let r = processRecursive(right)
            Result.merge(l, r)
        }
    }

Optimization: The compiler can assume this recursion terminates, allowing optimizations like:

  • Converting recursion to iteration
  • Allocating fixed stack space
  • Tail call optimization

Impact: Stack safety, predictable memory usage.

Deterministic Function Optimizations

When a function is marked is deterministic:

1. Compile-Time Evaluation

fn hashConstant(s: String): Int is deterministic = computeHash(s)

let key = hashConstant("api_key")  // Constant input

Optimization: Since the input is a compile-time constant and the function is deterministic, the result can be computed at compile time.

Transformed to:

let key = 7823491  // Pre-computed

Impact: Zero runtime cost for constant computations.

2. Result Caching Across Runs

fn parseConfig(path: String): Config is deterministic with {File} =
    Json.parse(File.read(path))

Optimization: Results can be cached persistently. If the file hasn't changed, the cached result is valid.

Implementation approach:

  • Hash inputs (including file contents)
  • Store results in persistent cache
  • Validate cache on next run

Impact: Faster startup times, reduced I/O.

3. Reproducible Parallel Execution

fn renderImages(images: List<Image>): List<Bitmap> is deterministic =
    List.map(images, render)

Optimization: Deterministic parallel execution guarantees same results regardless of scheduling order. This enables:

  • Work stealing without synchronization concerns
  • Speculative execution without rollback complexity
  • Distributed computation across machines

Impact: Easier parallelization, simpler distributed systems.

Idempotent Function Optimizations

When a function is marked is idempotent:

1. Duplicate Call Elimination

fn setFlag(config: Config, flag: Bool): Config is idempotent =
    { ...config, enabled: flag }

fn configure(c: Config): Config is idempotent =
    c |> setFlag(true) |> setFlag(true) |> setFlag(true)

Optimization: Multiple consecutive calls with the same arguments can be collapsed to one.

Transformed to:

fn configure(c: Config): Config is idempotent =
    setFlag(c, true)

Impact: Eliminates redundant operations.

2. Retry Optimization

fn sendRequest(data: Request): Response is idempotent with {Http} =
    Http.put("/api/resource", data)

fn reliableSend(data: Request): Response with {Http} =
    retry(3, fn(): Response => sendRequest(data))

Optimization: The retry mechanism knows the operation is safe to retry without side effects accumulating.

Implementation approach:

  • No need for transaction logs
  • No need for "already processed" checks
  • Simple retry loop

Impact: Simpler error recovery, reduced complexity.

3. Convergent Computation

fn normalize(value: Float): Float is idempotent =
    clamp(round(value, 2), 0.0, 1.0)

Optimization: In iterative algorithms, the compiler can detect when a value has converged (applying the function no longer changes it).

// Can terminate early when values stop changing
fn iterateUntilStable(values: List<Float>): List<Float> =
    let normalized = List.map(values, normalize)
    if normalized == values then values
    else iterateUntilStable(normalized)

Impact: Early termination of iterative algorithms.

Commutative Function Optimizations

When a function is marked is commutative:

1. Argument Reordering

fn multiply(a: Int, b: Int): Int is commutative = a * b

// In a computation
multiply(expensiveA(), cheapB())

Optimization: Evaluate the cheaper argument first to enable short-circuit optimizations or better register allocation.

Impact: Improved instruction scheduling.

2. Parallel Reduction

fn add(a: Int, b: Int): Int is commutative = a + b

fn sum(xs: List<Int>): Int =
    List.fold(xs, 0, add)

Optimization: Since add is commutative (and associative), the fold can be parallelized:

[1, 2, 3, 4, 5, 6, 7, 8]
    ↓ parallel reduce
[(1+2), (3+4), (5+6), (7+8)]
    ↓ parallel reduce
[(3+7), (11+15)]
    ↓ parallel reduce
[36]

Impact: O(log n) parallel reduction instead of O(n) sequential.

3. Algebraic Simplification

fn add(a: Int, b: Int): Int is commutative = a + b

// Expression: add(x, add(y, z))

Optimization: Commutative operations can be reordered for simplification:

  • add(x, 0)x
  • add(add(x, 1), add(y, 1))add(add(x, y), 2)

Impact: Constant folding, strength reduction.

Combined Property Optimizations

Properties can be combined for even more powerful optimizations:

Pure + Deterministic + Total

fn computeKey(data: String): Int
    is pure
    is deterministic
    is total = {
    // Hash computation
    List.fold(String.chars(data), 0, fn(acc: Int, c: Char): Int =>
        acc * 31 + Char.code(c))
}

Enabled optimizations:

  • Compile-time evaluation for constants
  • Automatic memoization at runtime
  • Parallel execution in batch operations
  • No exception handling needed
  • Safe to inline anywhere

Idempotent + Commutative

fn setUnionItem<T>(set: Set<T>, item: T): Set<T>
    is idempotent
    is commutative = {
    Set.add(set, item)
}

Enabled optimizations:

  • Parallel set building (order doesn't matter)
  • Duplicate insertions are free (idempotent)
  • Reorder insertions for cache locality

Implementation Status

Optimization Status
Pure: CSE Planned
Pure: Dead code elimination Partial (basic)
Pure: Auto-parallelization Planned
Total: Exception elimination Planned
Total: Aggressive inlining Partial
Deterministic: Compile-time eval Planned
Idempotent: Duplicate elimination Planned
Commutative: Parallel reduction Planned

Adding New Optimizations

When implementing new optimizations based on behavioral types:

  1. Verify the property is correct: The optimization is only valid if the property holds
  2. Consider combinations: Multiple properties together enable more optimizations
  3. Measure impact: Profile before and after to ensure benefit
  4. Handle assume: Functions using assume bypass verification but still enable optimizations (risk is on the programmer)

Future Work

  1. Inter-procedural analysis: Track properties across function boundaries
  2. Automatic property inference: Derive properties when not explicitly stated
  3. Profile-guided optimization: Use runtime data to decide when to apply optimizations
  4. LLVM integration: Pass behavioral hints to LLVM for backend optimizations