Skip to content

Tutorial 04: Compiling to Multiple Targets

Write once in ll-lang, emit F#, TypeScript, Python, Java, C#, or LLVM IR.

The source

module Shapes

Shape = Circle Float | Rect Float Float | Empty

area(s Shape) Float =
  | Circle r  -> 3.14159 * r * r
  | Rect w h  -> w * h
  | Empty     -> 0.0

describe(s Shape) Str =
  | Circle r  -> strConcat "circle r=" (floatToStr r)
  | Rect w h  -> strConcat "rect " (strConcat (floatToStr w) (strConcat "x" (floatToStr h)))
  | Empty     -> "empty"

Compile to each target

lllc build --target fs   shapes.lll   # → shapes.fs
lllc build --target ts   shapes.lll   # → shapes.ts
lllc build --target py   shapes.lll   # → shapes.py
lllc build --target java shapes.lll   # → shapes.java
lllc build --target cs   shapes.lll   # → shapes.cs
lllc build --target llvm shapes.lll   # → shapes.ll

Generated F# (default)

module Shapes

type Shape =
    | Circle of float
    | Rect of float * float
    | Empty

let area (s: Shape) : float =
    match s with
    | Circle r -> 3.14159 * r * r
    | Rect(w, h) -> w * h
    | Empty -> 0.0

let describe (s: Shape) : string =
    match s with
    | Circle r -> "circle r=" + string r
    | Rect(w, h) -> "rect " + string w + "x" + string h
    | Empty -> "empty"

F# discriminated unions. Idiomatic — you can drop this into any F# project.

Generated TypeScript

// Generated by lllc

type Shape =
    | { readonly tag: "Circle"; readonly r: number }
    | { readonly tag: "Rect"; readonly w: number; readonly h: number }
    | { readonly tag: "Empty" };

function area(s: Shape): number {
    switch (s.tag) {
        case "Circle": return 3.14159 * s.r * s.r;
        case "Rect":   return s.w * s.h;
        case "Empty":  return 0.0;
    }
}

function describe(s: Shape): string {
    switch (s.tag) {
        case "Circle": return "circle r=" + s.r.toString();
        case "Rect":   return "rect " + s.w.toString() + "x" + s.h.toString();
        case "Empty":  return "empty";
    }
}

Tagged union pattern. TypeScript's switch exhaustiveness is enforced by the never fallthrough — the compiler emits that too.

Generated Python

# Generated by lllc
from __future__ import annotations
from dataclasses import dataclass
from typing import Union

@dataclass(frozen=True)
class Circle:
    r: float

@dataclass(frozen=True)
class Rect:
    w: float
    h: float

@dataclass(frozen=True)
class Empty:
    pass

Shape = Union[Circle, Rect, Empty]

def area(s: Shape) -> float:
    match s:
        case Circle(r=r):    return 3.14159 * r * r
        case Rect(w=w, h=h): return w * h
        case Empty():        return 0.0

def describe(s: Shape) -> str:
    match s:
        case Circle(r=r):    return "circle r=" + str(r)
        case Rect(w=w, h=h): return "rect " + str(w) + "x" + str(h)
        case Empty():        return "empty"

Python 3.10+ structural pattern matching. Frozen dataclasses for immutability.

Generated Java 21

// Generated by lllc
public sealed interface Shape permits Shape.Circle, Shape.Rect, Shape.Empty {
    record Circle(double r) implements Shape {}
    record Rect(double w, double h) implements Shape {}
    record Empty() implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle(double r) -> 3.14159 * r * r;
            case Rect(double w, double h) -> w * h;
            case Empty() -> 0.0;
        };
    }

    static String describe(Shape s) {
        return switch (s) {
            case Circle(double r) -> "circle r=" + r;
            case Rect(double w, double h) -> "rect " + w + "x" + h;
            case Empty() -> "empty";
        };
    }
}

Java 21 sealed interfaces + records + pattern switch. Exhaustiveness is enforced by the JVM.

Multi-target projects via lll.toml

For a project that needs to emit to multiple targets simultaneously, configure lll.toml:

[project]
name = "shapes"

[platform]
use = ["fsharp", "typescript", "python"]

Then one command emits all three:

lllc build
# → bin/fsharp/shapes.fs
# → bin/typescript/shapes.ts
# → bin/python/shapes.py

No flags, no scripts. The manifest drives the build.

When to use which target

Target Use when
fs (default) F# / .NET ecosystem, self-hosting the compiler
ts Frontend, Node.js, Deno — share types across a TypeScript codebase
py Data pipelines, scripting, teams working in Python
java JVM services, Android, teams on Java 21+
cs .NET/C# integration and tooling pipelines
llvm IR-level toolchains, low-level compiler integration

Checking target compatibility

lllc build --target ts shapes.lll

The compiler reports target-specific compatibility errors during build. E007 PlatformMismatch remains reserved for Platform.* availability checks.

Next steps

  • ../why-ll-lang.md — the full value proposition with benchmark numbers
  • lllc mcp — wire an LLM agent to the compiler for instant multi-target feedback