diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b4924dd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# The generated IR files are compared byte-for-byte in CI, so their line +# endings have to be the same on every platform. The generators normalise +# the frontend's output to LF; this pins the files themselves so a +# checkout cannot rewrite them. +web/apps/web/src/docs/generated/ir.ts text eol=lf +web/apps/web/src/lib/playground-ir.ts text eol=lf + +# The plain-text surfaces are served as-is. +web/apps/web/public/llms.txt text eol=lf +web/apps/web/public/llms-full.txt text eol=lf + +# Shell scripts must keep LF: CRLF breaks them. +*.sh text eol=lf + +# Binary assets. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mp4 binary +*.woff binary +*.woff2 binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a345d0..d472969 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,53 @@ on: branches: [ master, main ] jobs: + docs: + name: Docs verification + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.2 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + working-directory: web + run: bun install --frozen-lockfile + + # Every example on the website is compiled by the real Lithon frontend + # and pinned to the repo's regression outputs. This job is what makes + # that claim true rather than aspirational: a doc that drifts from the + # engine fails the build instead of misleading a reader. + - name: Verify every docs example against the real toolchain + working-directory: web + run: bun run verify:docs + + # The generated IR and the AI-facing surfaces are committed, so they + # have to match what the frontend produces today. The generators + # normalise line endings, so the comparison is the same on every + # platform. + - name: Check the generated IR is up to date + working-directory: web + run: | + bun scripts/gen-docs-ir.ts + bun scripts/gen-playground-ir.ts + git diff --exit-code -- apps/web/src/docs/generated/ir.ts apps/web/src/lib/playground-ir.ts + + - name: Type-check the web app + working-directory: web + run: bun run typecheck + + - name: Lint the web app + working-directory: web + run: bun run lint + build-and-test: name: Build & Test (Ubuntu x86-64) runs-on: ubuntu-latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e8d2a91..86ea882 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,6 +49,12 @@ jobs: working-directory: web run: bun run scripts/gen-seo.ts "${{ steps.deployment.outputs.page_url || format('https://{0}.github.io/{1}', github.repository_owner, github.event.repository.name) }}" + # The AI-facing surfaces: a compact index and the whole documentation as + # plain text, both generated from the same registry the site renders. + - name: Generate the AI-facing surfaces + working-directory: web + run: bun run scripts/gen-llms.ts + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 7583593..5534c09 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ fuzz_failures/ node_modules/ +# Local Python virtualenv for the frontend toolchain. +.venv/ + /nul diff --git a/web/apps/web/public/llms-full.txt b/web/apps/web/public/llms-full.txt new file mode 100644 index 0000000..224c319 --- /dev/null +++ b/web/apps/web/public/llms-full.txt @@ -0,0 +1,3709 @@ +# Lithon — full documentation + +> Lithon is a Python library and native x86-64 execution engine. Every supported program carries fixed-width type annotations, is statically verified before it runs, and executes as generated machine code with no LLVM and no runtime package. + +Generated from the site's own docs registry. Every example below is verified against the real Lithon frontend. + +--- + +## Start here + +From zero to a natively running program. + +### Welcome to Lithon + +*Native execution for typed Python: what Lithon is, what it is not, and why it refuses instead of guessing.* + +URL: https://project-lithon.github.io/Lithon/docs/welcome + +#### What is Lithon? + +Lithon is a Python library and native x86-64 execution engine that runs .lio source using an extended PEP-526 static typing syntax. The source language is Python's syntax; the .lio extension is what marks a file as a Lithon program rather than an ordinary Python script. Every supported program is statically verified and can execute as generated machine code, with no LLVM, no Cranelift, and no runtime package. + +**a small Lithon program** +```text +x: int[64] = 10 +y: int[64] = 20 + +result: int[64] = x + y + +print(result) +``` +Output: +``` +30 +``` + +That is the whole trick: familiar Python syntax, one annotation per binding, and the compiler now knows enough to verify the program and emit x86-64 for it. + +#### Built around a few principles + +- **Mandatory static typing** — Types are verified before execution. Removing annotations does not disable verification: it makes the program invalid. +- **Fixed-width values** — int[8] through int[64], float[64], bool, and bounded str[N]. Widths are explicit; there is no arbitrary-precision integer behind your back. Every integer width is signed (there is no uint), so a negative int[w] keeps its sign when read back from a list element, struct field, or pointer; bool is the one narrow type that reads back as 0/1. +- **Native execution** — Supported programs run as x86-64 machine code emitted by a hand-written encoder, straight into executable memory. +- **No mandatory LLVM** — The backend is a self-contained C++20 encoder. as is only used to verify it, never to build it. +- **Honest fallback** — What the native tier cannot prove runs through the Tier-0 interpreter in automatic mode, and --strict turns that fallback into a refusal. +- **Explicit unsafe memory** — Pointers exist, and every pointer variable carries a mandatory _ prefix so raw memory is visible in the source. + +> **The load-bearing rule** — If Lithon cannot prove that a program satisfies the rules required by its native backend, it refuses native compilation rather than guessing. Unknown or unprovable → REFUSE: never "generate potentially incorrect machine code". + +#### How is Lithon different? + +*The design space Lithon explores* +| Syntax | Python syntax + extended PEP-526, in a .lio file | +| --- | --- | +| Primary execution | Native x86-64 (CPython: bytecode; PyPy: tracing JIT) | +| Static typing | Mandatory (CPython/PyPy: no; mypyc: optional) | +| Fixed-width integers | Built into the type (int[64], int[32], …) | +| Raw typed pointers | Built in (ptr[T], addressof, valueof) | +| Native backend | Custom x86-64 emitter: no LLVM | +| Interpreter fallback | Yes, Tier-0, same program, refused under --strict | +| Target | x86-64 today (ARM64 is roadmap Phase III) | + +Lithon is not trying to replace Python's ecosystem. It explores one question: what if Python code kept its familiar source model while giving the execution engine explicit static types, fixed-width values, native execution, and controlled low-level memory access? + +> **Experimental project** — Lithon is a community-preview project. The compiler already executes substantial typed programs natively with a serious verification suite, but it is not a stable production library and not a drop-in CPython replacement. + +#### Where to go next + +- **Hello, native** — Write, type-check, and run your first program end to end. +- **The mental model** — Two tiers, one rule: prove before you run. +- **Types & widths** — The one rule that makes the rest checkable: every type carries its width. +- **Control flow** — if / while / for, and try/except over the engine's own traps. +- **Machine view** — See the IR and x86-64 behind the examples. + +--- + +### Hello, native + +*Your first Lithon program: annotate, type-check, compile to IR, and run it on the native tier.* + +URL: https://project-lithon.github.io/Lithon/docs/hello-world + +#### The first program + +Every binding carries an explicit type. A bare integer literal is int[64], so this program is fully typed without any ceremony beyond the annotations: + +**hello.lio** +```text +x: int[64] = 10 +y: int[64] = 20 + +result: int[64] = x + y + +print(result) +``` +Output: +``` +30 +``` + +The same shape from the regression corpus: three operations, three lines of output: + +**tests/programs/arithmetic.lio** +```text +x: int[64] = 10 +y: int[64] = 20 +z: int[64] = x + y +print(z) +print(x - y) +print(x * y) +``` +Output: +``` +30 +-10 +200 +``` +Source: `tests/programs/arithmetic.lio` + +#### The annotation rule + +Dropping the annotations is not an option: the type checker runs unconditionally, on every module, whether it annotates anything or not: + +**rejected · V1_SPEC 0.6.1** +```text +x = 10 +print(x) +``` +Expected to be refused with: `without ever being declared with a type annotation` + +This is not ceremony. The annotation is the contract that lets the backend skip boxing, dynamic dispatch, and hot-path checks: it is what makes the native tier possible at all. + +#### Running it, three ways + +- **Type-check only** — tools/typecheck.py hello.lio: static analysis over the AST, no execution. Exits non-zero with an RCR error on the first violation. +- **Compile to IR** — src/frontend/frontend.py hello.lio: lowers the typed AST to Lithon's text IR on stdout. No engine needed, pure Python. +- **Run natively** — lithon hello.lio --strict: the two-step dance: frontend to IR, then tier_runner emits x86-64 and calls it. --strict refuses rather than falling back to the interpreter. + +**terminal** +```bash +$ lithon hello.lio --strict +[tier1] native +30 +``` + +> **[tier1] native is the proof** — [tier1] native goes to stderr; the program's own output goes to stdout. Under --strict, a clean exit status means the code was really emitted and executed: a fallback cannot hide behind success. + +--- + +### Build & install + +*Requirements, the CMake build, the development CLI, and the verification commands.* + +URL: https://project-lithon.github.io/Lithon/docs/installation + +#### Requirements + +| CMake | ≥ 3.20 | +| --- | --- | +| Compiler | C++20 (gcc, clang, or MSVC) | +| Python | ≥ 3.10 (frontend only: the native tier never touches CPython) | +| Architecture | x86-64 Linux or Windows | +| Third-party libraries | none: no LLVM, no Cranelift, no runtime package | + +#### Build the engine + +**terminal** +```bash +$ git clone git@github.com:Project-Lithon/lithon.git +$ cd lithon +$ cmake -B build -DCMAKE_BUILD_TYPE=Release +$ cmake --build build -j$(nproc) +``` + +There is no Makefile to hand-edit, CMake drives everything. Python appears at exactly one step, turning a .lio file into IR; once IR exists the native program never touches CPython. + +#### The development CLI + +**install in editable mode, then** +```bash +$ pip install -e . +$ lithon tests/programs/float.lio +$ lithon tests/programs/float.lio --strict # native only, refuse to fall back +$ lithon tests/programs/float.lio --ir # print the IR and stop +$ lithon tests/programs/float.lio -v # say which tier ran, and why +``` + +*CLI flags* +| --strict | Native only. Refuse rather than fall back to Tier-0. | +| --- | --- | +| --ir | Print the typed IR and stop before execution. | +| -v | Verbose: report which tier ran and why. | + +> **Prefer scripts? The two-step dance is open** — python3 src/frontend/frontend.py prog.lio > prog.ir then ./build/tier_runner prog.ir --strict. The lithon wrapper is convenience, not a black box. + +#### Prove the install works + +**the full gate** +```bash +$ ctest --test-dir build --output-on-failure # unit tests +$ python3 tools/run_regression.py # untyped suite +$ python3 tools/run_typed_regression.py # typed suite +$ python3 tools/run_tier_diff.py # every program, both tiers +$ bash tools/verify_all.sh # all of the above +``` + +run_tier_diff.py is the highest-value of these: it runs every program through both tiers and requires byte-identical stdout, then reports which tier actually ran, so a green run cannot hide "everything silently fell back to the interpreter". + +--- + +### The mental model + +*A two-lane road: prove before you run, emit native for what is proven, refuse everything else.* + +URL: https://project-lithon.github.io/Lithon/docs/mental-model + +#### A two-lane road + +A block whose types are provably static takes the Tier-1 native path and is compiled straight to x86-64. Anything the verifier cannot establish falls to the Tier-0 C++ interpreter: a real fallback rather than a guess, and --strict turns that fallback into a refusal. + +- **Prove before run** — Types are settled before execution begins. A flow that cannot be established is never emitted. +- **Emit native** — Verified IR blocks become guard-free x86-64 in executable memory. No guards, no checks, no boxing on the hot path. +- **Fall back honestly** — Tier-0 runs the same program when the native tier refuses. --strict refuses instead: success cannot hide a fallback. + +#### Refuse over guess + +**the decision procedure** +```text +Unknown / unprovable + │ + ▼ + REFUSE + +Unknown + │ + ▼ + Guess + │ + ▼ + Generate potentially incorrect machine code ← never +``` + +Every strange-looking rule in the language: mandatory annotations, fixed widths, no narrowing, definite assignment, shift-count ranges: exists to keep the verifier on the left path. Each one turns something the backend would have to guess about into something it knows. + +> **What this buys you** — When a Lithon program runs on Tier-1, the machine code has no runtime type checks, no overflow guards it could have proven away, and no hidden allocation. The verifier already did that work, once, at compile time. + +#### Strict mode is the honesty test + +**terminal** +```bash +$ lithon program.lio --strict +``` + +Under --strict, "the program printed the right number" becomes meaningful: printing at all means the native tier compiled and executed it. This is how the test suites keep the interpreter from quietly carrying the project. + +- **Inside the engine** — Four layers from .lio source to executable memory. +- **Dual-tier execution** — What Tier-0 and Tier-1 each do, and when. +- **The verification gate** — How the claims get checked: layer by layer. + +--- + +## The language + +Types, flow, functions, containers, unsafe memory. + +### Types & widths + +*Fixed-width integers, floats, bool, and the three rules the verifier enforces on every conversion.* + +URL: https://project-lithon.github.io/Lithon/docs/types + +#### The widths + +Integers carry their width in the type. There is no bare int: it is rejected, and no arbitrary-precision integer behind your back: + +**every scalar type** +```text +i8: int[8] = 7 +i16: int[16] = 7 +i32: int[32] = 7 +i64: int[64] = 7 +f64: float[64] = 0.5 +b: bool = True +print(i64) +``` +Output: +``` +7 +``` + +*Scalar types* +| int[8] … int[64] | signed fixed-width two's-complement integer | +| --- | --- | +| float[64] | IEEE-754 binary64 | +| bool | one byte, True / False | +| ptr[T] | typed pointer to a scalar T: see Pointers (/docs/pointers) | + +> **Signed only, 64-bit floats only** — There is no unsigned uint type: uint[8] is rejected as an unknown type, so masking with & is how you get unsigned behaviour, explicitly. float[32] is rejected too (LITHON-E0105: unsupported float width 32): every float is float[64]. + +> **Why fixed widths are load-bearing** — A fixed width is a fact the backend can plan around: the register, the memory slot, the overflow instruction. An unbounded int would mean boxing, big-integer fallbacks, and guards: exactly what the native tier is built to not have. + +#### Widening is automatic, narrowing is never + +A value may be widened to a provably larger width of the same signedness class, automatically. Narrowing, the reverse direction, is rejected even when you are certain the value fits: + +**widening · accepted** +```text +small: int[8] = 5 +big: int[64] = small +print(big) +``` +Output: +``` +5 +``` + +**rejected · V1_SPEC 0.6.11** +```text +wide: int[64] = 5 +narrow: int[8] = wide +print(narrow) +``` +Expected to be refused with: `cannot narrow` + +No cast syntax exists anywhere in the language. If you need a bit pattern reinterpreted, that is what pointers are for: explicitly, with the _ prefix. + +#### int → float is automatic; float → int is not + +int[64] into float[64] is a widening conversion and happens silently. The reverse is rejected: it would truncate, and truncation is not a conversion Lithon will perform for you. + +> **The literal exception (V1_SPEC 0.6.11)** — An int literal is not a float, so j: float[64] = 0 is a type error: write 0.0. The int→float conversion does not apply to a literal. + +**rejected · V1_SPEC 0.6.11** +```text +j: float[64] = 0 +print(j) +``` +Expected to be refused with: `must hold a float` + +**accepted** +```text +j: float[64] = 0.0 +print(j) +``` +Output: +``` +0.0 +``` + +#### Explicit conversions and terminal input + +int() and float() are not type constructors you call with one argument: the two-argument compiler-recognized form takes an explicit width, int(w, v) and float(64, v). int(w, v) converts a float[64] to int[w] and traps (E0309) if the value is out of range or not finite; float(64, v) converts an int[8|16|32] or float[64] and is refused at compile time (E0106) for a lossy int[64] source, since 53 mantissa bits cannot hold every int[64] value. + +int(w, input(prompt)) reads a line from stdin and parses it as int[w], with an optional prompt string. Three traps cover a bad terminal: E0306 invalid integer literal, E0307 value outside the target width, E0308 end of input. + +**int(w, v), float(64, v), and int(w, input(prompt))** +```text +f: float[64] = 42.0 +n: int[64] = int(64, f) +print(n) + +w: int[32] = int(32, input("enter a number: ")) +print(w) +``` + +> **tools/typecheck.py lags here too** — Like while, the standalone checker does not yet recognize int()/float() as compiler builtins (call to unknown function 'int'); src/frontend/frontend.py lowers them correctly today. Verified against the real frontend, not the standalone checker, until it catches up. + +#### Mixed int/float arithmetic + +An int operand combined with a float operand produces a float. The int is converted to the float's width first, then the operation happens in float land: + +**tests/typed_regression/mixed_numeric.lio** +```text +i: int[64] = 10 +f: float[64] = 2.5 +print(i + f) +print(f + i) +print(i / 4) +``` +Output: +``` +12.5 +12.5 +2.5 +``` +Source: `tests/typed_regression/mixed_numeric.lio` + +#### There is no bare `int` + +int without a width is rejected: the width is part of the type, not an implementation detail the compiler may pick for you. The same goes for int[128]: only 8/16/32/64 exist, matching the machine. + +> **Provable overflow is a compile-time error** — Integer literal overflow and provable-range overflow for binary operations are compile-time errors (V1_SPEC 0.6.5): the checker points you at wrap_add()/wrap_sub()/wrap_mul() when wrap-around is what you want. Overflow the verifier cannot prove at compile time on int[64] traps at run time instead (E0303), identically in both tiers; wrap_add/wrap_sub/wrap_mul are the explicit opt-out that keeps the machine's two's-complement wrap instead of trapping. + +--- + +### Declarations & assignment + +*Definite assignment, one type per name, and the loop-variable rules the verifier enforces.* + +URL: https://project-lithon.github.io/Lithon/docs/declarations + +#### One type per name + +A name is declared once with an explicit type. Re-assignment is checked against that declared type: you cannot quietly reuse the name for a different width or a different type: + +**accepted · re-assignment within the type** +```text +x: int[64] = 10 +x = 20 +print(x) +``` +Output: +``` +20 +``` + +**rejected · re-assignment narrows** +```text +x: int[64] = 10 +x = 20 +narrow: int[8] = x +print(narrow) +``` +Expected to be refused with: `cannot narrow` + +#### Definite assignment + +Every use of a variable must be dominated by an assignment whose value the verifier can trace. You may declare first and assign later, but you may not read before the first assignment. + +**tests/typed_regression/uninit_decl.lio** +```text +i: int[8] +i = 5 +print(i) +``` +Output: +``` +5 +``` +Source: `tests/typed_regression/uninit_decl.lio` + +**rejected · V1_SPEC 0.6.10** +```text +i: int[8] +print(i) +``` +Expected to be refused with: `never assigned a value` + +> **Declaration is not initialization: except for containers** — For a scalar, a bare i: int[64] only records the type. For containers the bare declaration is real work: it reserves the run/table and zeroes it. See Containers (/docs/containers). + +#### Branches and loops + +Scoping is function-scoped, not block-scoped: a name declared inside an if is visible after the if. Definite assignment, however, requires that every branch that reaches a use has assigned the variable: + +**rejected · one branch forgets** +```text +x: int[64] = 1 +if x < 3: + y: int[64] = 2 +print(y) +``` +Expected to be refused with: `not definitely assigned here` + +The same rule applies to loop variables: a while condition that reads a counter requires the counter to have been assigned before the loop, and the update inside the body is what makes the next iteration's read valid. + +--- + +### Control flow + +*if / elif / else, while, for over range, try / except over the built-in traps, and the one construct that does not exist.* + +URL: https://project-lithon.github.io/Lithon/docs/control-flow + +#### if / elif / else + +Exactly the Python you know. Conditions are boolean expressions; there is no truthiness conversion: a condition must be a bool. + +**tests/typed_regression/if.lio** +```text +x: int[64] = 5 +if x < 3: + print(1) +else: + print(3) +``` +Output: +``` +3 +``` +Source: `tests/typed_regression/if.lio` + +#### while + +A while is a conditional backward branch. The verifier tracks the loop-carried types across the back edge, so the body may narrow nothing and widen only what was already declared: + +**tests/typed_regression/while.lio** +```text +i: int[64] = 0 +total: int[64] = 0 +while i < 10: + total = total + i + i = i + 1 +print(total) +``` +Output: +``` +45 +``` +Source: `tests/typed_regression/while.lio` + +> **The checker does not cover while yet** — tools/typecheck.py still answers statement While not supported by the type-checker yet: including for the repo's own while.lio. The engine lowers and executes while correctly (Tier-0 and Tier-1 both); it is the standalone Python checker that lags. Until it catches up, while programs are verified through the regression suites rather than the checker. + +#### for over range() + +range(stop) and range(start, stop) produce values checked against the loop variable's declared width at compile time when known (V1_SPEC 0.6.12). The loop variable is a normal declared binding: + +**tests/typed_regression/for_range.lio** +```text +total: int[64] = 0 +i: int[64] = 0 +for i in range(10): + total = total + i +print(total) +``` +Output: +``` +45 +``` +Source: `tests/typed_regression/for_range.lio` + +#### range with a step + +range(start, stop, step) is the full three-argument form, including a negative step for a descending loop. A literal zero step is a compile-time refusal (E0107); a runtime-computed zero step traps (E0310) instead, since the checker cannot prove it in advance: + +**tests/programs/range_step.lio** +```text +for i in range(1, 10, 3): + print(i) +for i in range(10, 1, -3): + print(i) +for i in range(0, 5, 1): + print(i) +for i in range(5, 5, 1): + print(i) +for i in range(1, 5, 6): + print(i) +for i in range(-5, -1, 1): + print(i) +for i in range(-1, -5, -2): + print(i) +``` +Output: +``` +1 +4 +7 +10 +7 +4 +0 +1 +2 +3 +4 +1 +-5 +-4 +-3 +-2 +-1 +-3 +``` +Source: `tests/programs/range_step.lio` + +> **Empty ranges are not an error** — range(5, 5, 1) and a step pointed the wrong way (e.g. range(1, 5, -1)) both produce zero iterations, matching CPython: only step == 0 is refused or trapped. + +#### break and continue + +break and continue work in for and while bodies: break exits the innermost loop, continue jumps to its back edge. Either one used outside a loop is a compile-time refusal (E0120). The lowering rotates the loop body so a multi-exit loop still has one guarded back edge, not a spaghetti of jumps: + +**tests/programs/break_continue.lio** +```text +total: int[64] = 0 +for i in range(100): + if i == 5: + continue + if i == 10: + break + total = total + i +print(total) +``` +Output: +``` +40 +``` +Source: `tests/programs/break_continue.lio` + +#### enumerate and zip + +enumerate(xs) and zip(xs, ys) work in a for header. Neither is a value: both are a rewrite of a counted loop over constructs that already exist, so there is no new opcode and no runtime helper. for i, x in enumerate(xs) becomes for i in range(N): x = xs[i], and for a, b in zip(xs, ys) becomes the same loop bounded at min(N1, N2) — a compile-time constant, because the declared capacity is what len() folds to. + +zip stops at the shortest input, exactly as Python does. Elements bind with their declared type, and break/continue work inside either header because the rewrite is a real loop. + +**tests/typed_regression/iter_helpers.lio (excerpt)** +```text +xs: list[int[64], 5] +xs[0] = 10 +xs[1] = 20 +xs[2] = 30 +xs[3] = 40 +xs[4] = 50 + +for ei, ex in enumerate(xs): + print(ei) + print(ex) + +long6: list[int[64], 6] +short3: list[int[64], 3] +long6[0] = 1 +long6[1] = 2 +long6[2] = 3 +short3[0] = 7 +short3[1] = 8 +short3[2] = 9 + +total: int[64] = 0 +for za, zb in zip(long6, short3): + total = za + zb +print(total) +``` +Output: +``` +0 +10 +1 +20 +2 +30 +3 +40 +4 +50 +12 +``` +Source: `tests/typed_regression/iter_helpers.lio` + +> **They are for-header only** — z = zip(a, b) is refused (LITHON-E0115). The pair never needs to exist as a value, and a homogeneous tuple could not represent enumerate's (int, T) anyway — so the rewrite is the whole design rather than an optimisation of a real iterator. + +#### if as an expression + +An if used where a value is expected is an if-expression with else mandatory. Both arms are fully evaluated expressions: statements inside the arms are rejected: + +**tests/typed_regression/if_expr.lio (excerpt)** +```text +x: int[64] = 5 +y: int[64] = 2 + +print(x if x > y else y) +print(1 if x < y else 2 if x == 5 else 3) +``` +Output: +``` +5 +2 +``` +Source: `tests/typed_regression/if_expr.lio` + +> **The checker cannot infer a nested merge** — tools/typecheck.py answers cannot statically determine the type of this expression yet for a chained if-expression, even though the frontend lowers the real file correctly and CI passes it. Read the Output tab as the oracle here. + +#### There is no for/else + +for/else: where the else runs when the loop completes without break: is rejected. The else binds to if, not to for; if you need the "loop finished" signal, test the loop variable after the loop. + +**rejected by the frontend** +```text +i: int[8] = 0 +for i in range(3): + print(i) +else: + print(1) +``` +Expected to be refused with: `for/else is not supported` + +It is rejected loudly rather than dropped, because the alternative: binding the else to the if: would silently change what the program means. + +#### try / except, over the built-in traps + +try/except catches a trap the engine itself raises. There is no exception hierarchy to import and no exception object to bind: the catchable set is closed, and a caught trap is a plain jump in the IR. Both tiers run the same handler region, so a trap caught natively is caught identically in the interpreter. + +**catching a division by zero** +```text +n: int[64] = 0 +try: + q: int[64] = 100 % n + print(q) +except ZeroDivisionError: + print(1) +``` +Output: +``` +1 +``` + +The IR is the whole mechanism: tryenter marks the handler block and the region it guards, tryleave ends the protected region on the normal path, and trycatch is the landing pad. A caught trap is a branch, not an unwind -- there is no stack to unwind because there is no exception object. + +The catchable set is exactly seven names: ZeroDivisionError, OverflowError, ValueError, IndexError, KeyError, EOFError, and Exception for all of them. Anything else is refused at compile time, so a typo in the handler is a diagnostic rather than a silently-never-taken branch. + +**rejected: not a catchable trap** +```text +try: + print(1) +except RuntimeError: + print(2) +``` +Expected to be refused with: `is not a catchable trap` + +*which trap lands in which handler* +| ZeroDivisionError | division or modulo by zero | +| --- | --- | +| OverflowError | a dynamic int[64] overflow (LITHON-E0303), or an input that does not fit the target width (E0307) | +| ValueError | a float-to-int conversion out of range (E0309), a shift count outside 0..63, a range step of zero (E0310), a non-numeric input (E0306) | +| IndexError | a dynamic index outside a list's bounds | +| KeyError | a dict key that is not present | +| EOFError | input() reached end of input (E0308) | + +> **three things that are deliberately not supported** — A bare except: is refused, because it would also catch a system exit and would suggest catching a segfault, which is not catchable. except ... as e is refused, because there is no exception object to bind. try/finally without an except is refused, because a finally that runs on the uncaught path is a different construct. And a trap raised inside a *callee* is not catchable by the caller: only traps raised in the same function as the try are. + +**rejected: a bare except** +```text +try: + print(1) +except: + print(2) +``` +Expected to be refused with: `a bare `except:` is not supported` + +An uncaught trap is still a trap: the program stops with the engine's own diagnostic, and the exit code is non-zero. See Errors (/docs/errors) for the full code family and Semantics (/docs/semantics) for the arithmetic traps in detail. + +--- + +### Functions + +*Mandatory contracts, the calling convention, and recursion.* + +URL: https://project-lithon.github.io/Lithon/docs/functions + +#### Contracts are mandatory + +Every parameter and the return type are mandatory and checked (V1_SPEC 0.6.8). A call site is checked against the declared signature, and a declared return type must be provably wide enough for what is returned: the compiler never auto-widens your return. + +**tests/typed_regression/function.lio** +```text +def add(a: int[64], b: int[64]) -> int[64]: + return a + b + +x: int[64] = add(3, 4) +print(x) +``` +Output: +``` +7 +``` +Source: `tests/typed_regression/function.lio` + +**rejected · missing return type** +```text +def add(a: int[64], b: int[64]): + return a + b + +x: int[64] = add(3, 4) +print(x) +``` +Expected to be refused with: `return` + +#### Recursion + +Recursion works like it does in Python: each call gets its own frame. The verifier checks each call against the declared signature, including recursive calls to the function being defined: + +**tests/typed_regression/recursion.lio** +```text +def fib(n: int[64]) -> int[64]: + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + +x: int[64] = fib(10) +print(x) +``` +Output: +``` +55 +``` +Source: `tests/typed_regression/recursion.lio` + +#### The calling convention + +A native call passes the first two arguments in registers (rdi/rsi, or XMM0/XMM1 for floats) and every further argument in an 8-byte stack slot, exactly as SysV passes its later arguments. Up to 32 parameters are supported; a call or function past that budget is refused by the native tier and runs in the interpreter rather than mis-passing arguments. + +> **beyond the budget** — If a signature genuinely needs more than 32 parameters, run that function under the interpreter (the native tier refuses automatically) or restructure the call. + +--- + +### Operators + +*Arithmetic, comparison, boolean, bitwise and shift: including C-style `%` and two's-complement wrap.* + +URL: https://project-lithon.github.io/Lithon/docs/operators + +#### Arithmetic + +*Operators and their result rules* +| + - * | same-width result; int×int wraps, float follows IEEE-754 | +| --- | --- | +| / | float division producing float[64], not C-style truncation | +| % | C-style remainder: sign follows the left operand | +| ** | refused: *LITHON-E0114: operator Pow not supported yet*. There is no power operator. | +| // | refused: *LITHON-E0114: operator FloorDiv not supported yet*. There is no floor division; use a shift for a power-of-two divisor. | +| <= >= | refused: *LITHON-E0114: comparison LtE not supported yet*. Only <, >, == and != exist. | +| a < b < c | refused: chained comparisons are not supported. | + +**C-style % (README)** +```text +a: int[64] = -7 +b: int[64] = 3 +print(a % b) +print(7 % 3) +print(-7 % 3) +``` +Output: +``` +-1 +1 +-1 +``` +*% follows C, not Python: -7 % 3 is -1, not 2. Python's floor-mod and C's trunc-mod disagree on negatives, and Lithon chose the machine's behaviour. (The checker cannot infer the type of a negated-literal binding yet; the frontend and both tiers handle it.)* + +**rejected: a power operator** +```text +x: int[64] = 2 +print(x ** 3) +``` +Expected to be refused with: `operator Pow not supported yet` + +#### Comparison and boolean + +== != < > and the other comparisons produce bool. and or not operate on bool. != is a first-class comparison with its own IR op (ne), not an == followed by a not, so the optimizer and branch fusion see the real predicate. + +**tests/typed_regression/comparison.lio** +```text +a: int[64] = 1 +b: int[64] = 2 +c: float[64] = 1.5 +print(a < b) +print(b > a) +print(a == a) +print(c < b) +``` +Output: +``` +True +True +True +True +``` +Source: `tests/typed_regression/comparison.lio` + +**tests/typed_regression/ne.lio (excerpt)** +```text +a: int[64] = 1 +b: int[64] = 2 +print(a != b) +print(a != a) +lo: int[8] = -128 +hi: int[8] = 127 +print(lo != hi) +``` +Output: +``` +True +False +True +``` +Source: `tests/typed_regression/ne.lio` + +> **int and float compare across widths** — The fourth line compares a float[64] against an int[64] and returns True: the int is widened to float for the comparison, the same promotion rule as arithmetic. + +#### Bitwise and shift + +<< >> & | ^ are integer-only and not part of the arithmetic promotion rule: 2.5 & 1 is a type error, because Lithon has no float bit pattern to reinterpret. Their result takes the left operand's width. + +A literal shift count must be 0..63: the machine word is 64 bits and x86 masks the count to 6 bits, so a count of 64 would silently execute as 0. A non-literal count is checked at runtime instead. This is a deliberate divergence from CPython, where ints are unbounded and 1 << 64 is a valid 65-bit result: + +**rejected · bitwise on a float** +```text +print(2.5 & 1) +``` +Expected to be refused with: `bitwise operand has type float` + +**rejected · shift count out of range** +```text +print(1 << 64) +``` +Expected to be refused with: `shift count` + +**accepted** +```text +x: int[64] = 1 << 10 +y: int[64] = x >> 3 +z: int[64] = x & 0xFF +print(x) +print(y) +print(z) +``` +Output: +``` +1024 +128 +0 +``` + +#### Explicit wrap-around arithmetic + +Plain +/-/* on fixed-width ints wraps in two's complement at run time, but overflow the verifier can prove statically is rejected at compile time (E0303) and points you at the wrap_* family. wrap_add/wrap_sub/wrap_mul are the explicit way to say "wrap-around is what I want": the same wrapped value in both tiers: + +**tests/typed_regression/int64_wrap.lio** +```text +x: int[64] = 9223372036854775807 +y: int[64] = 1 +z: int[64] = wrap_add(x, y) +print(z) + +a: int[64] = -9223372036854775808 +b: int[64] = 1 +c: int[64] = wrap_sub(a, b) +print(c) + +p: int[64] = 3037000500 +q: int[64] = 3037000500 +r: int[64] = wrap_mul(p, q) +print(r) +``` +Output: +``` +-9223372036854775808 +9223372036854775807 +-9223372036709301616 +``` +Source: `tests/typed_regression/int64_wrap.lio` + +--- + +### Containers + +*Fixed-capacity lists, tuples, and constant-keyed dicts: declared, reserved, sized at compile time.* + +URL: https://project-lithon.github.io/Lithon/docs/containers + +#### list: fixed capacity + +A list[T, N] has a compile-time capacity N that is part of the type. The element type is a fixed-width scalar. The bare declaration xs: list[int[64], 6] is real work: it reserves the run and zeroes it: drop it and the first store writes to a name that was never allocated. + +**tests/programs/list_basic.lio (excerpt)** +```text +xs: list[int[64], 6] + +xs[0] = 10 +xs[1] = 20 +xs[2] = 30 + +print(xs[0]) +print(xs[1]) +print(xs[2]) +``` +Output: +``` +10 +20 +30 +``` +Source: `tests/programs/list_basic.lio` + +*list rules* +| Capacity | N is in the type. It is the number of slots the declaration reserves, and it never changes. | +| --- | --- | +| Length | len(xs) is the live length, carried in a companion xs.__len slot. It is a run-time value, not the capacity: a short literal leaves the trailing slots reserved but not live. | +| Unwritten slots | read back as zero of their kind (the declaration zeroes the run) | +| Index bound | checked against N: a runtime index is checked, not a literal the compiler happened to see | +| Element address | element k lands at base + k * sizeof(T): packed, no pointers per element | + +A short literal is the normal case, and it is why len(xs) is not the capacity: + +**the live length is not the capacity** +```text +xs: list[int[64], 6] = [1, 2, 3] +print(len(xs)) +print(xs) +print(sum(xs)) + +xs.append(10) +xs.append(20) +print(len(xs)) +print(xs) +``` +Output: +``` +3 +[1, 2, 3] +6 +5 +[1, 2, 3, 10, 20] +``` + +> **A reduction over a list whose length is not known at compile time is refused** — sum, min, max, any, all and in are unrolled over the live length at compile time. When the length is only known at run time — an append inside a loop, or a split — they are refused rather than guessed at. A plain literal or a bare declaration keeps the length a compile-time constant, so the reductions still work. + +#### tuple + +A fixed-length product of scalar types. Indexing with a literal is checked at compile time against the declared length: + +**a tuple** +```text +t: tuple[int[64], 4] = (1, 2, 3, 4) +print(t[0]) +print(t[3]) +``` +Output: +``` +1 +4 +``` + +> **One element type, a literal capacity** — A tuple is tuple[T, N]: one element type and a literal length. Anything else is rejected: tuple[int[64], float[64], bool] fails with *needs an element type and a literal capacity*. + +> **Containers are ahead of the standalone checker** — tools/typecheck.py answers unknown type 'list': the container types are implemented in the engine (and covered by the regression suites) but the standalone Python checker has not been taught them yet. + +#### dict: constant-keyed + +A dict[K, V, B] is a fixed-capacity open-addressing table with B buckets, built from a constant-keyed literal. The valueless declaration reserves and zeroes the table, so the literal is a fill of a table that already exists: not an allocation trick. Literal keys are resolved while compiling; a runtime key takes the hash-and-probe path. + +**tests/programs/dict_basic.lio (excerpt)** +```text +d: dict[int[64], int[64], 4] = {1: 10, 2: 20, 3: 30, 4: 40} +print(d[1]) +print(d[4]) + +k: int[64] = 2 +print(d[k]) +j: int[64] = k + 1 +print(d[j]) +``` +Output: +``` +10 +40 +20 +30 +``` +Source: `tests/programs/dict_basic.lio` + +> **More entries than buckets is a duplicate-bucket problem** — dict_basic.lio stays one short of it and leans on the probe instead. dict_table.lio is the test that pins a collision down to fixed keys rather than leaving it to chance. + +#### Where to go next + +- **Pointers** — Raw addresses, addressof/valueof, and the _ naming rule. +- **Structs** — Packed storage: fields at fixed offsets, no padding. +- **Enums** — A tag plus a payload, read with match. +- **Type functions** — Compile-time type constructors that expand structurally. +- **Built-in functions** — The whole prelude, one row per name. + +--- + +### Pointers + +*Typed pointers with a mandatory `_` prefix: explicit unsafe memory, no hidden aliasing.* + +URL: https://project-lithon.github.io/Lithon/docs/pointers + +#### The `_` rule + +Every pointer variable carries a mandatory leading _. This is not a naming convention the compiler happens to enforce: it is a source-level marker that raw memory is in play, greppable and impossible to miss in review. + +**rejected · missing underscore** +```text +x: int[64] = 5 +p: ptr[int[64]] = addressof(x) +print(valueof(p)) +``` +Expected to be refused with: `named with a leading underscore` + +#### addressof and valueof + +addressof(x) produces a ptr[T] to x; valueof(_p) reads through it. Pointer equality compares addresses. Pointer arithmetic (_p + 1, _p - 1) steps by sizeof(T), not by bytes. + +**tests/programs/ptr_basic.lio (excerpt)** +```text +x: int[64] = 5 +_p: ptr[int[64]] = addressof(x) +print(valueof(_p)) +q: int[64] = 6 +_q: ptr[int[64]] = addressof(q) +print(valueof(_q)) +print(_p == _q) +``` +Output: +``` +5 +6 +False +``` +Source: `tests/programs/ptr_basic.lio` + +#### What pointers cannot do + +*Restrictions* +| Type changes | a pointer cannot change its pointee type | +| --- | --- | +| Arithmetic | only same-type offset arithmetic (_p ± n, _p1 − _p2); no arbitrary casts | +| Deref | no *_p syntax: valueof / assignment through the pointer | +| Null | _p == 0 is a comparison; there is no null-deref path in safe code | + +> **Pointers are unsafe by design** — The _ prefix marks the boundary. Inside it, the compiler's usual guarantees about definite assignment and type agreement do not extend through raw memory writes, that is the deal you take when you reach for ptr. + +#### Pointers to floats and bools + +ptr works for any scalar pointee: float[64], bool, and any int[w]. Reading through a ptr[float[64]] after the pointee changed gives you the new value, because the pointer names a location, not a snapshot: + +**reading through a pointer (ptr_basic.lio excerpt)** +```text +f: float[64] = 2.5 +_pf: ptr[float[64]] = addressof(f) +print(valueof(_pf)) +f = 3.25 +print(valueof(_pf)) +``` +Output: +``` +2.5 +3.25 +``` +Source: `tests/programs/ptr_basic.lio` + +--- + +### Structs + +*A named run of typed fields packed into one byte blob: declared offsets, no padding, no struct value.* + +URL: https://project-lithon.github.io/Lithon/docs/structs + +#### A struct is storage, not a value + +A struct is a named run of typed fields laid out end to end in one blob of bytes. Declaring p: Point reserves and zeroes those bytes, exactly the way a list declaration reserves its element run. A struct has no value: the only operations are reading a field and writing a field, and the whole struct is never an expression you can print or assign. + +The spelling is Lithon's own: a struct is a class deriving from Struct, whose body is nothing but field annotations. + +#### Declaring a struct and writing fields + +Each name: type line is a field. The declaration p: Point zeroes the blob; writing a field is a partial write into that blob at a fixed offset the frontend computed, not a rebinding of the name. + +**tests/programs/struct_basic.lio (excerpt)** +```text +class Point(Struct): + x: int[64] + y: int[64] + + +p: Point +p.x = 3 +p.y = 4 +print(p.x) +print(p.y) +``` +Output: +``` +3 +4 +``` +Source: `tests/programs/struct_basic.lio` + +A field nobody wrote reads back as the zero of its kind: 0 for an int, False for a bool, 0.0 for a float. This is the struct analogue of an unwritten list element reading zero. + +> **narrow fields are signed** — An int[8]/int[16]/int[32] field is signed: writing -3 into an int[16] field and reading it back yields -3, not the zero-extended 65533. bool is the exception and reads back as 0/1. This is the same rule as a list element or a pointer dereference of the same width. + +**unwritten fields read zero (struct_basic.lio excerpt)** +```text +class SockAddrIn(Struct): + family: int[16] + port: int[16] + addr: int[64] + active: bool + + +s: SockAddrIn +s.family = 2 +s.port = 80 +print(s.family) +print(s.port) +print(s.addr) +print(s.active) +``` +Source: `tests/programs/struct_basic.lio` + +#### Layout: packed, no padding + +Fields sit at exactly the offsets their sizes imply: the first field at 0, each next field immediately after the previous. No padding is inserted, so the size of a struct is the sum of its field sizes. bool is 1 byte, float[64] is 8, and int[w] is w / 8. + +*SockAddrIn · 13 bytes, no padding* +| 0 | family : int[16] | +| --- | --- | +| 2 | port : int[16] | +| 4 | addr : int[64] | +| 12 | active : bool | + +Because the layout is a pure sum, the struct[N] size the frontend bakes into the declaration is exactly what a C offsetof / sizeof computation over a #pragma pack(1) struct would produce. tools/check_struct_layout.py compiles the same struct definitions with cc, runs the frontend, and asserts that equality field by field. + +> **A field is a scalar or an earlier struct** — A field type is int[w], float[64], bool, or a struct declared before this one. A field may not be a list, tuple, dict, or pointer. A nested struct field is flattened: seg.a.x resolves to a single leaf read at the summed byte offset, so there is never an intermediate struct value. + +#### Float fields + +A float[64] field always occupies the full eight bytes and goes through the double path, so an arithmetic read is exact and a negative value survives unchanged. + +**float fields (struct_basic.lio excerpt)** +```text +class Vec(Struct): + x: float[64] + y: float[64] + + +v: Vec +v.x = 1.5 +v.y = -2.25 +print(v.x) +print(v.y) +print(v.x + v.y) +``` +Source: `tests/programs/struct_basic.lio` + +#### What structs cannot do + +*Restrictions* +| Whole value | a struct is never an expression; print(p) and q = p are refused | +| --- | --- | +| Field kind | no list/tuple/dict/pointer fields: only scalars and earlier structs | +| Field as value | seg.inner alone is refused; flatten to a leaf (seg.inner.x) | +| Narrowing | a value wider than the field is refused, exactly as for a list element | +| Parameters | structs are not passed to or returned from functions | + +**rejected · a container field** +```text +class Foo(Struct): + xs: list[int[64], 4] + +f: Foo +print(0) +``` +Expected to be refused with: `LITHON-E0127` + +**rejected · the whole struct as a value** +```text +class Foo(Struct): + x: int[64] + +f: Foo +print(f) +``` +Expected to be refused with: `LITHON-E0128` + +--- + +### Enums + +*A tagged union: one variant active at a time, packed as a tag plus the largest payload, read back with match.* + +URL: https://project-lithon.github.io/Lithon/docs/enums + +#### An enum is storage, not a value + +An enum is a tagged union: a byte blob holding an int tag and room for the payload of whichever variant is active. Like a struct it has no value — the only operations are constructing a variant, reading the tag with match, and reading a payload through the arm's binding. The whole enum is never an expression you can print, assign, pass, or return. + +The spelling is Lithon's own: an enum is a class deriving from Enum. Each body line declares one variant, in one of three forms — a bare unit name, a name: type payload annotation, or an inline class V(Struct) struct payload. + +#### Declaring an enum and its variants + +The declaration s: Shape reserves and zeroes the blob, exactly the way a struct declaration does. The tag is written by a construction and read by a match; the variant names are never values in scope. + +**tests/programs/enum_basic.lio (excerpt)** +```text +class Shape(Enum): + Empty + Dot + Line: int[32] + Rect: int[64] + + +s: Shape +s = Shape.Dot +match s: + case Shape.Empty: + print(0) + case Shape.Dot: + print(1) + case Shape.Line(n): + print(n) + case Shape.Rect(w): + print(w) +``` +Output: +``` +1 +``` +Source: `tests/programs/enum_basic.lio` + +The three variant forms are: a bare name (Empty, Dot) is a unit variant with no payload; Line: int[32] is a payload variant whose variant carries one scalar; and an inline class Tcp(Struct): ... is a struct payload whose fields are packed at the payload offset. Two variants may not share a name. + +#### Layout: a tag, then the largest payload + +The layout is [tag][payload] with no padding. The tag sits at offset 0. It is an int[8] while the enum has at most 128 variants (a signed byte holds 0..127) and widens to int[16] past that. The payload starts at the tag width, and the enum's size is the tag width plus the largest payload of any variant. + +*Shape · 9 bytes, no padding* +| 0 | tag : int[8] (Empty=0, Dot=1, Line=2, Rect=3) | +| --- | --- | +| 1 | payload : largest is Rect's int[64], 8 bytes | + +Only the active variant's payload bytes are ever read. Overwriting a payload variant with a smaller one leaves the old bytes in place but inactive, so a later match can never observe them. The size the frontend bakes into the struct[N] declaration is exactly what a C offsetof/sizeof over a #pragma pack(1) tag-plus-union would produce; tools/check_enum_layout.py compiles that C layout, runs the frontend, and asserts the two agree at both tag-width boundaries. + +#### Constructing a variant + +A construction writes the tag and the active payload; it does not create a value. e = E.V writes a unit variant, e = E.V(x) a one-scalar payload, and e = E.V(field=x, ...) or e = E.V(x, ...) a struct payload by field name or position. A construction can appear as a bare assignment, in the declaration e: E = E.V(...), or into a struct field s.efield = E.V(...). + +**tests/programs/enum_struct.lio (excerpt)** +```text +class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +class Conn(Struct): + proto: Endpoint + id: int[64] + + +c: Conn +c.proto = Endpoint.Tcp(port=8080, addr=16777343) +c.id = 7 +``` +Source: `tests/programs/enum_struct.lio` + +> **stale payloads are never read** — After s = Shape.Line(7) then s = Shape.Empty, the 7 is still in the blob's payload bytes but the tag says Empty. A match reads the tag first and enters only the arm it selects, so the overwritten bytes are unreachable. + +#### Reading a variant with match + +A match on an enum reads the tag once and dispatches to the arm whose variant matches. An arm for a payload variant binds that payload (case Shape.Line(n):); an arm for a unit variant binds nothing (case Shape.Empty:). A case _: arm is the wildcard and must be last. The match must be exhaustive: every variant is covered by some arm, or there is a case _:. + +*match arms* +| Unit arm | case E.V: binds nothing | +| --- | --- | +| Scalar arm | case E.V(n): binds the one payload scalar | +| Struct arm | case E.V(f1=a, f2=b): binds each named field | +| Wildcard | case _: must be the final arm | +| Exhaustiveness | all variants covered, or a case _: present | + +**tests/programs/enum_struct.lio (excerpt)** +```text +class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +class Conn(Struct): + proto: Endpoint + id: int[64] + + +c: Conn +c.proto = Endpoint.Tcp(port=8080, addr=16777343) +c.id = 7 +match c.proto: + case Endpoint.Tcp(port=p, addr=a): + print(p) + print(a) + case Endpoint.Udp(n): + print(n) + case Endpoint.Empty: + print(-1) +print(c.id) +``` +Source: `tests/programs/enum_struct.lio` + +match lowers to the same tag-compare if/elif chain a hand-written dispatch would be: each arm is a fieldload of the tag, an equality test, and a branch whose body reads the payload fields it bound. There is no new instruction kind and no enum value flowing through the IR. + +#### What enums cannot do + +*Restrictions* +| Whole value | an enum is never an expression; print(e) and e2 = e are refused | +| --- | --- | +| Parameters/returns | enums are not passed to or returned from functions | +| List elements | an enum may not be a list element or a list payload | +| Enum payloads | a variant payload is a scalar or an inline Struct; not another enum | +| Guards | case E.V if : is not supported | + +**rejected · an enum definition error** +```text +class E(Enum): + A + A +print(0) +``` +Expected to be refused with: `LITHON-E0129` + +**rejected · an unknown variant** +```text +class E(Enum): + A +e: E +e = E.B + +``` +Expected to be refused with: `LITHON-E0130` + +**rejected · an enum used as a value** +```text +class E(Enum): + A + B: int[32] +e: E +match e: + case E.A: + print(0) +``` +Expected to be refused with: `LITHON-E0131` + +--- + +### Methods + +*A function declared in a struct or enum body, with an implicit self, inlined at each call site.* + +URL: https://project-lithon.github.io/Lithon/docs/methods + +#### A method is a function in a class body + +A def inside a Struct or Enum body is a method. Its first parameter is the implicit receiver self, the rest are annotated scalars, and the return type is optional. A method is called on a struct or enum place: p.f(1), where p may be a variable or a struct/enum field. + +A method is inlined at the call site. That is forced by the storage model — a field access names a variable, not a value, and a struct or enum cannot be passed to a function (E0124). Inlining is what lets a method read and write the receiver's fields, run loops and branches, and call other methods. + +#### Declaring and calling a method + +**tests/programs/method_basic.lio (excerpt)** +```text +class Counter(Struct): + value: int[64] + + def add(self, n: int[64]) -> int[64]: + self.value = self.value + n + return self.value + + +c: Counter +print(c.add(5)) +print(c.add(7)) +``` +Output: +``` +5 +12 +``` +Source: `tests/programs/method_basic.lio` + +A method's locals are private to the call: a name in the body cannot capture or clobber a caller's variable of the same spelling, because the inline renames every method local with a per-call prefix. A method that returns nothing is a statement (p.log()); one that returns a scalar is an expression you can print or combine. + +#### The receiver, and enum methods + +The receiver is a place, so it can be a struct field: o.inner resolves to the field's bytes and a method on Inner reads and writes the caller's inner. A method on an enum reads self with match self — the only way to read an enum — and returns a scalar computed from the active variant. + +**tests/programs/method_enum.lio (excerpt)** +```text +class Shape(Enum): + Empty + Line: int[32] + + def code(self) -> int[64]: + match self: + case Shape.Empty: + return 0 + case Shape.Line(n): + return n + case _: + return -1 + + +s: Shape +s = Shape.Empty +print(s.code()) +``` +Output: +``` +0 +``` +Source: `tests/programs/method_enum.lio` + +#### What a method cannot do + +*Restrictions* +| Receiver | the first parameter must be the unannotated name self | +| --- | --- | +| Signature | scalars only (int[w]/float[64]/bool); not a struct, enum, container or pointer, as parameter or return | +| Arguments | plain positional names; no /, *, ** or defaults | +| Names | no redeclaration or overloading; may not shadow a field or variant | +| Recursion | inlined at each call site, so direct and mutual recursion are refused | + +**rejected · a method whose first parameter is not self** +```text +class P(Struct): + x: int[64] + def f(a: int[64]) -> int[64]: + return a +p: P +print(p.f(1)) +``` +Expected to be refused with: `LITHON-E0134` + +**rejected · a recursive method** +```text +class P(Struct): + x: int[64] + def f(self) -> int[64]: + return self.f() +p: P +print(p.f()) +``` +Expected to be refused with: `LITHON-E0135` + +--- + +### Type Functions + +*A compile-time type constructor: `def Name(params) -> NewType: return { T }`, applied as `Name(args)` anywhere a type is allowed.* + +URL: https://project-lithon.github.io/Lithon/docs/type-functions + +#### A type function is a compile-time type constructor + +A type function names a type shape. You declare it with def Name(params) -> NewType: return { T }, and then wherever a type is allowed you write Name(args); at compile time the arguments are substituted into T and the result is an ordinary type. It runs entirely in the frontend: a definition carries no code, and no runtime value is ever involved. + +NewType is a reserved marker in the return position. The body must be exactly one return { }. The parameters hold types and sizes — they are plain unannotated names standing in for whatever is spliced at the call site, not values. + +#### Declaring and applying a type function + +**tests/programs/typefunc_basic.lio (excerpt)** +```text +def Fixed(t, n) -> NewType: + return {list[t, n]} + + +def Word() -> NewType: + return {int[32]} + + +xs: Fixed(int[64], 4) +xs[0] = 100 +xs[1] = 23 +xs[2] = -3 +print(xs[0] + xs[1] + xs[2]) +``` +Output: +``` +120 +``` +Source: `tests/programs/typefunc_basic.lio` + +Fixed(int[64], 4) is list[int[64], 4] and Word() is int[32]. A zero-parameter type function must still be applied with parentheses — write Word(), not a bare Word, which would read as a sized integer and be refused. + +#### The result is structural + +Applying a type function yields a type identical to the expansion spelled out — there is no distinct typedef identity. Alias(int[64]) is the very same type as int[64], so a variable declared one way and re-declared the other is not a re-declaration with a different type. + +**tests/programs/typefunc_basic.lio (excerpt)** +```text +def Alias(t) -> NewType: + return {t} + + +a: int[64] = 5 +a: Alias(int[64]) = 6 +print(a) +``` +Source: `tests/programs/typefunc_basic.lio` + +Because the type erases completely, nothing about a type function survives into the IR. tools/check_typefunc.py proves this: it compiles a type-function program and the same program with every type spelled out, and asserts the two IR texts are byte-identical. + +#### Where a type function can be applied + +A call is resolved inside the single annotation parser every type flows through, so a type function works anywhere a type is written: nested in another type function's argument, in a function parameter and return type, as a struct field, and as a container element type. + +**tests/programs/typefunc_uses.lio (excerpt)** +```text +def Word() -> NewType: + return {int[32]} + + +def Pair(t) -> NewType: + return {list[t, 2]} + + +ps: Pair(Word()) +ps[0] = 11 +ps[1] = -22 +print(ps[0] + ps[1]) +``` +Output: +``` +-11 +``` +Source: `tests/programs/typefunc_uses.lio` + +> **applications nest** — Pair(Word()) applies Word() first, giving list[int[32], 2]. Applications are memoized by call shape, so writing the same Pair(int[64]) twice expands the body once and returns the same type. + +#### What a type function cannot do + +*Restrictions* +| Body | exactly one return { }; {} is a dict and refused, multi-element bodies and comprehensions too | +| --- | --- | +| Composes | scalar, container, pointer and dict types — not a struct or an enum (those are storage) | +| Parameters | plain unannotated positional names; no /, *, ** or defaults | +| Names | no redeclaration, and no collision with a struct, an enum or a function | +| Recursion | must bottom out in a concrete type; direct and mutual recursion are refused | + +**rejected · a body holding two types** +```text +def T(a) -> NewType: + return {a, a} +print(0) +``` +Expected to be refused with: `LITHON-E0132` + +**rejected · a recursive type function** +```text +def P(a) -> NewType: + return {P(a)} +x: P(int[64]) = 1 +print(0) +``` +Expected to be refused with: `LITHON-E0133` + +--- + +### Syscalls + +*The raw syscall primitive: `syscall(number, a1..a6) -> int[64]`, one IR instruction, one host trampoline, identical in both tiers.* + +URL: https://project-lithon.github.io/Lithon/docs/syscalls + +#### One instruction, unchecked + +syscall(number, a1, a2, a3, a4, a5, a6) is a compiler-recognized name, not a function you import. It takes a syscall number and up to six arguments and returns the kernel's raw result as int[64]. The whole call lowers to a single Syscall IR instruction. + +Nothing is wrapped and nothing is checked. On Linux a failure is simply a negative number: the negated errno, exactly as the kernel reports it. There is no fallible[...] to unwrap and no use-before-check rule in this slice — a program that wants to handle failure compares the result itself. + +**a syscall number that does not exist** +```text +r: int[64] = syscall(-1) +print(r) +``` +Output: +``` +-38 +``` +*-1 is not a syscall number, so the kernel returns -ENOSYS (-38). The number is stable across both tiers and across the platform fork, which is what makes it a checkable example.* + +#### Arguments are integers or pointers + +A raw syscall argument must be an int or a pointer. The type checker says so plainly: *a raw syscall argument must be an integer or a pointer (a buffer or a sockaddr); floats and aggregates are not passed by value*. + +> **A pointer argument is a real host address in both tiers** — This is the load-bearing fact. The interpreter does not emulate a synthetic address space: a pointer to a variable is the address of that variable, in the interpreter and in the JIT alike. So a kernel syscall that reads a buffer reads exactly the bytes the language wrote, and one that writes a sockaddr writes into the very struct you handed it. tests/net/loopback.lio is a shipped TCP round trip that depends on it. + +**a syscall with a pointer argument** +```text +x: int[64] = 65 +_ptrX: ptr[int[64]] = addressof(x) +r: int[64] = syscall(1, _ptrX) +print(r) +``` +Output: +``` +-38 +``` +*The number 1 is not a valid Linux syscall number, so the kernel returns -ENOSYS (-38) — the same answer in both tiers, which is what makes it a checkable example.* + +A float[64] argument is refused: the kernel ABI passes floats differently, and a syscall that takes one by value is not reachable from Lithon. A struct argument is refused for the same reason: it is not a scalar, and a struct is not passed by value. + +- **Networking** — A real TCP loopback, checked against a C oracle. +- **Pointers** — Why a pointer to a struct is a real address. + +#### Both tiers execute the same trampoline + +The platform fork lives in exactly one place, src/jit/syscall_runtime.h. It consults an embedder-installed hook first, on every platform. With no hook it runs the real syscall instruction on Linux/x86-64; on a platform whose raw ABI is not stable (Windows) it returns -ENOSYS rather than guessing. + +Both the interpreter and the JIT call that one helper, so the two engines cannot drift about the calling convention — the argument registers, the clobbered registers, and the raw return are defined once. The JIT treats the op as a register-clobbering boundary, exactly like a call, so a value live across a syscall is spilled or placed in a callee-saved register. + +--- + +### Divergences from Python + +*Every place Lithon deliberately behaves differently from CPython, and why: modulo, division, shifts, narrowing, truthiness, strings, exceptions.* + +URL: https://project-lithon.github.io/Lithon/docs/python-divergences + +#### The short list + +Lithon's source syntax is Python's. Its *semantics* are not, in eleven specific places, all of which come from the same two decisions: fixed-width values and no dynamic dispatch. This page is the list; each row links to the page that explains it in full. + +*the eleven differences that will bite you* +| Modulo with a negative dividend | -7 % 3 == 2 | -7 % 3 == -1 — C-style, the sign follows the dividend | +| --- | --- | --- | +| Integer division | 7 / 2 == 3.5 | 7 / 2 == 3.5 — same, but // is refused outright | +| Narrowing an integer | always allowed | refused: int[8] never becomes int[64] implicitly | +| Float to int | int(3.7) == 3 | int(64, 3.7) == 3 — the width has to be named | +| Truthiness | if 0: is False, if "": is False | refused: a condition must be a bool | +| Strings | unbounded, heap-allocated, + concatenates | bounded str[N] byte runs; + is refused | +| Exceptions | raise anything, catch by class | seven named engine traps; no raise, no exception objects | +| Unassigned variables | NameError at run time | refused at compile time: every name needs a type and a value | +| Recursion | bounded by the C stack | self tail calls become loops; a stack guard traps at 4096 | +| Function arguments | positional, keyword, *args, **kwargs, defaults | positional only, fixed arity, no defaults, no keywords | +| Comparisons | a <= b, chained a < b < c | only <, >, ==, !=; no chaining | + +> **The rule behind all eleven** — CPython can decide what an expression means at run time. Lithon cannot, so anything the compiler would have to guess about becomes a refusal. A refusal is a compile-time error with a precise message, not a wrong answer at run time. + +#### Modulo follows C, not Python + +Lithon's % is the C remainder: the sign of the result follows the dividend, not the divisor. Python's is a true modulo: the sign follows the divisor. + +*-7 % 3* +#### Lithon +```text +print(-7 % 3) +``` +Output: +``` +-1 +``` +#### CPython +```text +>>> -7 % 3 +2 +``` + +The same holds for 7 % -3: Lithon gives 1, CPython gives -2. This is not an oversight: the C-style form is the one a machine instruction actually implements, and Lithon emits exactly that instruction. + +> **Where to read more** — The full statement of the rule, with the machine-level reason, is on the semantics (/docs/semantics) page. + +#### Division is always float; `//` does not exist + +/ always produces a float[64], including when both operands are integers: 7 / 2 is 3.5, not 3. There is no integer division operator at all — // is refused with *operator FloorDiv not supported yet*. + +To get an integer quotient, write it yourself or use a shift when the divisor is a power of two: x // 2 becomes x >> 1 for non-negative x. + +> **Division by zero traps in both tiers** — 1 / 0 is not an IEEE infinity and not a NaN: it is a ZeroDivisionError trap, exactly like Python's, and it is catchable with try/except. + +#### Narrowing is never implicit + +CPython integers are arbitrary-precision, so every integer is every other integer. Lithon integers are fixed-width, so an int[8] value is not an int[64] value: widening happens automatically, narrowing is refused. + +**widening is fine** +```text +small: int[8] = 5 +big: int[64] = small +print(big) +``` +Output: +``` +5 +``` + +**narrowing is refused** +```text +wide: int[64] = 5 +narrow: int[8] = wide +print(narrow) +``` +Expected to be refused with: `cannot narrow` + +There is no cast syntax for this. If you genuinely want to truncate, you have to say so with the explicit conversion, and the conversion traps rather than truncating when the value does not fit. + +#### Conversions name their width + +*conversions* +| int from float | int(3.7) | int(64, 3.7) | +| --- | --- | --- | +| float from int | float(3) | float(64, 3) | +| out of range | int(1e30) is a big int | int(8, 300.0) traps with ValueError | +| float from int[64] | n/a | refused: int[64] has no exact float[64] (E0106) | + +The width is not decoration. int(8, v) promises the value fits in a signed byte, and the engine checks that promise at run time instead of silently wrapping. + +#### There is no truthiness + +In CPython, if x: asks whether x is truthy, and almost everything has a truth value. In Lithon, a condition must be a bool. if x: where x: int[64] is a type error, not a check for non-zero. + +**the Lithon way** +```text +n: int[64] = 0 +if n != 0: + print("not zero") +``` + +> **Why** — A truthiness test on an int is a comparison the compiler could have proved away, but on a container it depends on the container's length, which is a run-time value. One rule — *conditions are bool values* — is easier to verify and faster to compile than a truthiness table. + +#### Strings are bounded byte runs + +*strings* +| Type | str, unbounded | str[N], N bytes | +| --- | --- | --- | +| Length | len(s) in characters | len(s) in bytes, at run time | +| Concatenation | a + b | refused | +| Slicing | s[1:3] | refused | +| Comparison | by content | by content, across different capacities | +| Methods | dozens | three: find, count, split | +| In a list | yes | yes, as a str[N] element | +| As a function argument | yes | refused: a string is addressed, not passed | + +> **Where to read more** — The whole string model is on the strings (/docs/strings) page. + +#### Exceptions are a closed set of engine traps + +CPython lets you raise anything and catch by class. Lithon has no exception objects and no raise: the only thing you can catch is one of seven named traps that the engine itself raises. + +*the catchable set* +- **ZeroDivisionError** — division or modulo by zero. +- **OverflowError** — a dynamic int[64] overflow, or an input value that does not fit the width it was read into. +- **ValueError** — a float-to-int conversion out of range, a shift count outside 0..63, a zero range step, or non-numeric input. +- **IndexError** — a dynamic index outside a list's bounds. +- **KeyError** — a dict key that is not present. +- **EOFError** — input() hit the end of the input. +- **Exception** — the catch-all: matches every one of the six above. + +A bare except: is refused, except X as e is refused (there is no exception object to bind), and raise is refused. A trap raised inside a called function is not catchable by the caller: the trap unwinds past it. + +> **Where to read more** — The full try/except/else/finally story is on the control flow (/docs/control-flow) page. + +#### Unassigned names are a compile error + +CPython raises NameError when you read a name you never assigned. Lithon refuses the program before it runs, and the refusal is per-name and per-path. + +**rejected · V1_SPEC 0.6.10** +```text +x: int[64] +print(x) +``` +Expected to be refused with: `never assigned a value` + +The same rule covers branches: if one arm of an if assigns a name and the other does not, the program is refused, because the verifier cannot prove which arm runs. + +#### Functions have a fixed shape + +*function signatures* +| Parameter types | optional | mandatory | +| --- | --- | --- | +| Return type | optional | mandatory | +| Default values | yes | no | +| Keyword arguments | yes | no | +| *args / **kwargs | yes | no | +| Default arguments | yes | no | +| Closures | yes | no: a function body cannot capture a caller's variable | +| First-class functions | yes | no: a function name is not a value | +| Recursion | yes | yes, with a stack guard at 4096 frames | +| Overloading | yes | no | + +Every one of these is a place where CPython would have to build a closure, a tuple, or a dictionary at run time. Lithon has no such machinery, so the shape of a function is fixed at compile time and the machine code is a direct call. + +#### Comparisons are four operators, never chained + +Lithon has <, >, == and !=. <= and >= are refused (*comparison LtE not supported yet*), and chained comparisons (a < b < c) are refused, because a chain means two comparisons and an intermediate value that a machine register cannot hold. + +**the Lithon way** +```text +a: int[64] = 1 +b: int[64] = 2 +if a < b and b < 10: + print("ok") +``` +Output: +``` +ok +``` + +!= is a first-class operator with its own machine instruction, not == followed by not. On floats it follows IEEE 754: nan != nan is true. + +#### What is exactly the same + +It is worth being precise about what did not change, because the list above can read as if Lithon is a different language. These are identical: + +- while and for loops, break, continue — same shape, same semantics. +- if / elif / else — same, and elif is spelled elif, not else if. +- The conditional expression a if cond else b — same. +- Integer literals, float literals, True / False — same spellings. +- Operator precedence — Python's, exactly, because the frontend uses Python's own parser. +- and / or / not — same precedence, but the operands must be bool. +- Function calls, subscripting, attribute access — same syntax. +- Recursion — supported, and the same shape as Python's. +- range(start, stop, step) — the same three forms and the same values. + +> The source model is Python's. The type system is not, and where the type system changes the meaning, the meaning changes. + +#### Where to go next + +- **Types & widths** — The type system behind most of these differences. +- **Semantics** — Modulo, shifts, float traps, and no truthiness, in full. +- **Errors & diagnostics** — Every error code, and how a refusal reads. +- **Honest limits** — The gaps that are not yet implemented at all. + +--- + +### Exceptions & traps + +*try/except/else/finally over a closed set of seven engine traps. What each one means, when it fires, and what is deliberately not supported.* + +URL: https://project-lithon.github.io/Lithon/docs/exceptions + +#### The model: traps, not exception objects + +Lithon has no exception classes, no raise, and no exception objects. What it has is traps: the engine stops the program with a short message when an operation cannot produce a value. A try block names the traps it is prepared for, and the engine routes a trap to the matching arm. + +**catching a division by zero** +```text +n: int[64] = 0 +try: + q: int[64] = 100 % n + print("not reached") +except ZeroDivisionError: + print("caught ZeroDivisionError") +``` +Output: +``` +caught ZeroDivisionError +``` + +> **The catchable set is closed** — Exactly seven names are catchable: ZeroDivisionError, OverflowError, ValueError, IndexError, KeyError, EOFError, and Exception (the catch-all). Any other name in an except arm is a compile-time refusal. You cannot invent a trap, and you cannot catch a segfault. + +#### The seven catchable traps + +*the closed catchable set* +- **ZeroDivisionError** — division or modulo by zero. Both / and % trap. A float divisor of 0.0 also traps — Lithon does not produce an IEEE infinity. +- **OverflowError** — a dynamic int[64] overflow, and an input value that does not fit the width it was read into (int(8, input(...)) with a 300). +- **ValueError** — a float-to-int conversion whose result is out of range or not finite, a shift count outside 0..63, a range step of zero at run time, and non-numeric input. +- **IndexError** — a dynamic index outside a list's bounds. +- **KeyError** — a dict key that is not present. +- **EOFError** — input() reached the end of the input before a value was read. +- **Exception** — the catch-all: it matches every one of the six above, and nothing else. It is the only way to write a handler that is guaranteed to cover the set. + +**an index trap** +```text +xs: list[int[64], 3] = [10, 20, 30] +k: int[64] = 9 +try: + v: int[64] = xs[k] + print("not reached") +except IndexError: + print("caught IndexError") +``` +Output: +``` +caught IndexError +``` + +**several arms, tried in order** +```text +try: + y: int[64] = 5 % 0 +except IndexError: + print("wrong arm") +except ZeroDivisionError: + print("caught ZeroDivisionError") +``` +Output: +``` +caught ZeroDivisionError +``` + +#### `else` and `finally` + +Both clauses are supported, and they have the same meaning they have in Python: else runs when the try block completed without a trap, and finally runs on every path out of the block. + +**all three clauses** +```text +n: int[64] = 4 +try: + r: int[64] = 100 % n +except ZeroDivisionError: + print("zero") +else: + print("else ran") +finally: + print("finally ran") +``` +Output: +``` +else ran +finally ran +``` + +**the same program with a trap** +```text +n: int[64] = 0 +try: + r: int[64] = 100 % n +except ZeroDivisionError: + print("zero") +else: + print("else ran") +finally: + print("finally ran") +``` +Output: +``` +zero +finally ran +``` + +> **A try with a finally but no except is refused** — try/finally without an except is rejected: without an arm there is nothing to route a trap to, so the finally would have to be a catch-all by accident. + +#### Nesting and the trap boundary + +try blocks nest, and the innermost matching arm wins. An outer except Exception will catch a trap that no inner arm claimed. + +**inner catches first** +```text +a: int[64] = 5 +try: + try: + inner: int[64] = a % 0 + except ZeroDivisionError: + print("inner caught") +except Exception: + print("outer caught") +``` +Output: +``` +inner caught +``` + +> **A trap inside a called function is not catchable by the caller** — The trap unwinds past the call frame. If f() divides by zero, the try around f() in the caller does not catch it: the function boundary is a trap boundary. Catch it inside f. + +> **This is the one place Lithon is stricter than Python** — In CPython an exception raised inside a callee propagates through the caller and can be caught there. Lithon does not: the trap is a property of the operation, and the operation is inside the callee. + +#### What is deliberately refused + +*refused exception forms* +| A bare except: | refused: it would also catch a system-exit, and here it would suggest catching a segfault, which is not catchable. Use except Exception:. | +| --- | --- | +| except X as e: | refused: there is no exception object to bind. The handler learns which trap fired from which arm it is in. | +| raise | refused: a program cannot invent a trap. Only the engine raises. | +| except* | refused: no exception groups. | +| A try with only a finally | refused: without an except there is nowhere to route a trap. | +| Catching an unlisted name | refused: the catchable set is closed. | + +**rejected: a bare except** +```text +try: + x: int[64] = 1 +except: + print("no") +``` +Expected to be refused with: `a bare `except:` is not supported` + +#### Where the traps live in the reference + +The error-code table in the reference lists every code, including the ones that are *designed* rather than shipped. The distinction matters: a trap you can catch is not the same as a static refusal. + +*refusal vs trap* +| When | before the program runs | while the program runs | +| --- | --- | --- | +| Where | the frontend and the type checker | the engine | +| Message | LITHON-Ennnn: ... | a short reason, e.g. error: division by zero | +| Catchable | no — the program does not exist | yes, with try/except | +| Example | LITHON-E0105: unsupported float width 32 | error: list index out of range | + +- **Errors & diagnostics** — Every code, tier by tier, with real output. +- **Control flow** — The try statement in the context of the other flow forms. +- **Divergences from Python** — How Lithon's error model differs from CPython's. + +--- + +### match + +*The enum dispatch statement: arm forms, bindings, exhaustiveness, and every shape that is deliberately refused.* + +URL: https://project-lithon.github.io/Lithon/docs/match + +#### What a match does + +A match reads the tag of an enum once and jumps to the arm whose variant matches it. An arm for a payload variant binds that payload, so the arm body reads it as a named variable; an arm for a unit variant binds nothing. + +**the shape of a match** +```text +class Shape(Enum): + Empty + Dot + Line: int[32] + Rect: int[64] + + +s: Shape +s = Shape.Line(42) +match s: + case Shape.Empty: + print(0) + case Shape.Dot: + print(1) + case Shape.Line(n): + print(n) + case Shape.Rect(w): + print(w) +``` +Output: +``` +42 +``` + +> **A match is not a value, and it is not a switch** — It binds names in the arms, and it does not produce one. The lowered form is a chain of tag comparisons and branches — the same thing you would hand-write, which is why there is no new IR instruction for it. + +#### The four arm forms + +*every arm form* +- **case E.V:** — a unit variant. Binds nothing. The arm body reads only the outer variables. +- **case E.V(n):** — a scalar payload variant. Binds the one payload to n, with the variant's own declared type. +- **case E.V(f1=a, f2=b):** — a struct payload variant. Binds each named field. The fields are named, not positional, so a reordering of the struct does not silently rebind them. +- **case _:** — the wildcard. Matches every variant not already covered. It must be the last arm. + +**a struct payload, bound by field name** +```text +class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +c: Endpoint = Endpoint.Tcp(port=8080, addr=16777343) +match c: + case Endpoint.Tcp(port=p, addr=a): + print(p) + print(a) + case Endpoint.Udp(n): + print(n) + case Endpoint.Empty: + print(-1) +``` +Output: +``` +8080 +16777343 +``` + +#### Exhaustiveness is checked + +A match must cover every variant, or end with a case _:. The checker knows the enum's variants, so it can prove the coverage statically — there is no fall-through and no runtime surprise. + +**rejected: not exhaustive** +```text +class E(Enum): + A + B + +e: E +match e: + case E.A: + print(1) +``` +Expected to be refused with: `is not exhaustive` + +**rejected: the wildcard is not last** +```text +class E(Enum): + A + +e: E +match e: + case _: + print(1) + case E.A: + print(2) +``` +Expected to be refused with: `must be the last arm` + +> **The wildcard is not a catch-all for a missing variant** — It is a catch-all for *any* variant not named by an earlier arm. That is why it has to be last: an arm after it could never run. + +#### The binding rules + +*what a binding may be* +| A name | case E.V(n): binds n. The name must not start with _: a leading underscore in a binding is a rejection, because a binding that is never read is a mistake worth catching. | +| --- | --- | +| A field name | case E.V(port=p): binds p from the struct payload's port field. | +| A type | the bound name has the variant's declared payload type: int[32] for Line: int[32], int[16] for port. | +| A name in scope | a binding shadows an outer name of the same name inside the arm body only. | + +**rejected: a binding that starts with an underscore** +```text +class E(Enum): + A: int[32] + +e: E +match e: + case E.A(_n): + print(1) + case _: + print(2) +``` +Expected to be refused with: `LITHON-E0131` + +#### What can be matched + +The subject of a match must be an enum variable or an enum struct field. It cannot be a function parameter, a list element, or an expression, because match needs a place to read the tag from. + +**rejected: matching a non-enum** +```text +x: int[64] = 1 +match x: + case 1: + print(1) +``` +Expected to be refused with: `its subject must be an enum variable or an enum struct field` + +Matching a struct field is the common case, because it is how you dispatch on a protocol or a state stored inside a larger record. + +**matching a struct field** +```text +class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +class Conn(Struct): + proto: Endpoint + id: int[64] + + +c: Conn +c.proto = Endpoint.Udp(53) +c.id = 7 +match c.proto: + case Endpoint.Tcp(port=p, addr=a): + print(p) + case Endpoint.Udp(n): + print(n) + case Endpoint.Empty: + print(-1) +print(c.id) +``` +Output: +``` +53 +7 +``` + +#### What match does not do + +*refused match shapes* +| Guards | case E.V if : is refused: *LITHON-E0131: case ... if is not supported yet*. Put the condition in the arm body. | +| --- | --- | +| Literal patterns | case 1: is refused: the subject has to be an enum, so the arms are variants. | +| Binding by position | case E.V(a, b): is refused for a struct payload. The fields are named. | +| Multiple subjects | match a, b: is refused: one subject, one dispatch. | +| A match as an expression | a match is a statement. It binds names; it does not produce a value. | +| A non-exhaustive match | refused: every variant must be covered, or a case _: must be present. | + +**rejected: a guard** +```text +class E(Enum): + A + +e: E +match e: + case E.A if True: + print(1) + case _: + print(2) +``` +Expected to be refused with: `is not supported yet` + +#### What it lowers to + +A match lowers to the same tag-compare chain a hand-written dispatch would be: for each arm, a fieldload of the tag, an equality test, and a branch whose body reads the payload fields it bound. There is no new instruction kind and no enum value flowing through the IR. + +That is why a match cannot be a subject for another match, and why the arms are checked in order: the lowered code is a chain, not a table. + +- **Enums** — Declaring a tagged union, and its layout. +- **Control flow** — match next to if, while and for. +- **Source to machine** — The IR and the machine code behind a match. + +--- + +### Strings + +*Bounded strings: str[N] is a fixed-capacity, NUL-terminated byte run. Length is a run-time value, capacity is part of the type.* + +URL: https://project-lithon.github.io/Lithon/docs/strings + +#### What a Lithon string is + +A Lithon string is not a Python str. It is a bounded byte run: str[N] declares N bytes of storage, the bytes are written one at a time, and the run is NUL-terminated. There is no heap, no allocator, and no growth. Capacity is part of the type, exactly like a list's. + +**a bounded string** +```text +s: str[16] = "hello" +print(s) +print(len(s)) +``` +Output: +``` +hello +5 +``` + +> **N is bytes, and the capacity is not the length** — str[16] reserves 16 bytes, not 16 characters: a UTF-8 character can be more than one byte. And len(s) is the run-time byte count of what was written, not the 16 you declared. A str[N] you declared but never filled has length 0. + +The frontend lowers a string literal to one IndexStore per byte, so the storage model stays the same as every other container: a run of bytes at a fixed address. + +#### Declaring and writing + +*string rules* +| Capacity | str[N] — N bytes of storage. The literal must be shorter than N (the terminator needs a byte). | +| --- | --- | +| Length | len(s) is the run-time byte count of the bytes actually written. | +| Unwritten bytes | read back as 0; the terminator makes the run a C string as far as the OS is concerned. | +| Indexing | s[i] reads one byte as an int[8]. The byte is signed. | +| Writing | s[i] = v writes one byte. The right-hand side must be a single int[8]-sized value. | +| Comparison | s == t compares the byte runs. The two runs may have different capacities. | +| Element type | a str[N] may be a list element, a tuple element, a struct field, or a pointer pointee. | + +Because a string is just a byte run, it can live inside a struct, inside a list, or behind a pointer, and the same rules apply to it as to any other fixed-capacity buffer. + +**a string as a struct field** +```text +class Header(Struct): + kind: str[8] + version: int[16] + +h: Header +h.kind = "HTTP" +h.version = 11 +print(h.kind) +print(h.version) +``` +Output: +``` +HTTP +11 +``` + +#### The three string methods + +There are exactly three things you can do with a str[N] beyond reading and writing it. They are the whole string API. + +*the string surface* +- **s.find(sub)** — The lowest byte index where sub occurs in s, or -1 when it does not occur. An empty needle returns 0. +- **s.count(sub)** — How many non-overlapping occurrences of sub are in s. An empty needle returns len(s) + 1. +- **parts: list[str[K], M] = s.split(sep)** — Splits s on every occurrence of sep and writes the pieces into a declared list[str[K], M]. A piece longer than K - 1 bytes is truncated; more than M pieces is a runtime trap. The piece count becomes the list's live length. + +**find and count** +```text +s: str[32] = "hello world" +print(s.find("world")) +print(s.find("zzz")) +print(s.count("l")) +``` +Output: +``` +6 +-1 +2 +``` + +**split writes into a declared list** +```text +s: str[32] = "a,b,c" +parts: list[str[8], 4] = s.split(",") +print(parts[0]) +print(parts[2]) +``` +Output: +``` +a +c +``` + +> **split is an assignment, not a statement** — s.split(",") on its own line is refused: it has to be the initializer of a declared list[str[K], M], because the split writes into the destination rather than returning a value. s.split() with no argument, and s.split("") with an empty separator, are refused too. + +#### Printing a string + +print(s) writes the bytes of the run. There are no quotes around it, because a string is a byte run and not a Python object with a repr. + +*print a string vs print a list of strings* +#### a bare string +```text +s: str[8] = "hi" +print(s) +``` +Output: +``` +hi +``` +#### a list of strings +```text +xs: list[str[8], 2] = ["hi", "yo"] +print(xs) +``` +Output: +``` +['hi', 'yo'] +``` + +A list of strings prints in the container repr form, with quotes, because a list is a composite value and the reader needs to see the element boundaries. A bare string does not. + +#### Strings and pointers + +A str[N] can be the pointee of a pointer, which is how you hand a buffer to a syscall that reads or writes it. + +**a string behind a pointer** +```text +s: str[16] = "ping" +_p: ptr[str[16]] = addressof(s) +print(valueof(_p)) +``` +Output: +``` +ping +``` + +> **One restriction worth knowing** — A str field reached through a pointer is refused: valueof(_p).kind where kind is a string field is rejected with *a bounded string field is reached through its struct variable, not through a pointer into the struct*. The string can be the pointee of the pointer directly, but not a string-typed field of a struct the pointer points at. + +#### What strings cannot do + +*refusals* +| Concatenation | c: str[16] = a + b is refused: *a str is built from a string literal, so c: ... = needs "...", not a single expression*. There is no + on strings. | +| --- | --- | +| Slicing | s[1:3] is refused: only a single byte index is supported. | +| A bare str literal in a container literal | xs: list[str[8], 2] = ["a", "b"] works in the engine, but the standalone checker answers unknown type 'list': it does not know the container types yet. | +| Whitespace splitting | s.split() is refused: the separator is mandatory. | +| An empty separator | s.split("") is refused, exactly as Python refuses it. | +| A str function parameter | a string cannot be passed to a function: *a str function parameter is not yet a string place*. Strings are addressed, not passed. | + +#### Where to go next + +- **Containers** — The other fixed-capacity runs: list, tuple, dict. +- **Pointers** — Raw addresses, addressof/valueof, and the _ naming rule. +- **Structs** — Packed storage, and how a str[N] field is laid out. +- **Networking** — Using strings and pointers together for real socket calls. + +--- + +### Networking + +*Real sockets from raw syscalls: a TCP loopback round trip checked against a C oracle, and the byte-order obligation that comes with it.* + +URL: https://project-lithon.github.io/Lithon/docs/networking + +#### The premise: a pointer is a real address + +Networking in Lithon is not a library. It is syscall with a pointer, and the reason that works is a specific design decision: a pointer argument to a syscall is a real host address in both tiers. The interpreter does not emulate a synthetic address space; it hands the kernel the actual address of the variable. + +> **This is the payoff of the storage model** — Because a struct is a packed blob at a known address and a pointer to it is a real address, the kernel can read and write Lithon's memory directly. A bind() that fills the port back into your struct is not a simulation of a kernel: it is the kernel. + +The shipped test is tests/net/loopback.lio, and tools/check_net_loopback.py compiles a C program (tests/net/loopback_oracle.c) that issues the identical syscalls, then requires the two programs to print the same lines. A C oracle rather than the interpreter, because sockets are stateful and external: both Lithon tiers could be wrong in the same way, and the C program is not. + +#### The syscall primitive + +syscall(n, a1, ..., a6) issues Linux syscall number n and returns the kernel's return value as an int[64]. It is unchecked: a negative result is the kernel's -errno, and Lithon does not turn it into a trap. + +*argument kinds* +- **int[64]** — a scalar argument. The kernel sees the value itself, passed in a register. +- **ptr[T]** — a real host address. The kernel reads and writes exactly the bytes the language does. This is what makes a sockaddr or a buffer work. +- **float[64]** — refused: *floats and aggregates are not passed by value*. A syscall that takes a float is not reachable from Lithon. + +**socket(AF_INET, SOCK_STREAM, 0)** +```text +srv: int[64] = syscall(41, 2, 1, 0) +print(srv > 0) +``` +Output: +``` +True +``` + +> **Six arguments, no more** — The kernel's ABI has six argument registers. syscall takes at most six arguments, and the frontend refuses a seventh rather than silently dropping it. + +#### The loopback round trip + +The shipped test does a full TCP round trip in one process: a server socket binds to 127.0.0.1 on an ephemeral port, listens, and accepts; a second socket connects back; the client sends an int[64] and the accepted connection receives it. + +**tests/net/loopback.lio (excerpt)** +```text +class SockAddrIn(Struct): + lo: int[64] + hi: int[64] + + +class Buf(Struct): + v: int[64] + + +sa: SockAddrIn +sa.lo = 2 | (swap32(2130706433) << 32) +sa.hi = 0 +_sa: ptr[SockAddrIn] = addressof(sa) + +socklen: int[64] = 16 +_psocklen: ptr[int[64]] = addressof(socklen) + +# socket(AF_INET=2, SOCK_STREAM=1, 0) +srv: int[64] = syscall(41, 2, 1, 0) + +# bind(srv, &sa, sizeof(sockaddr_in)) +print(syscall(49, srv, _sa, 16)) + +# listen(srv, backlog=1) +print(syscall(50, srv, 1)) +``` +Output: +``` +0 +0 +``` +Source: `tests/net/loopback.lio` + +**the round trip (excerpt)** +```text +class Buf(Struct): + v: int[64] + + +out: Buf +out.v = 123456789 +inb: Buf +inb.v = 0 +_pout: ptr[Buf] = addressof(out) +_pin: ptr[Buf] = addressof(inb) + +# sendto(cli, &out, 8, 0, NULL, 0) == 8 bytes +print(syscall(44, cli, _pout, 8, 0, 0, 0)) +# recvfrom(conn, &in, 8, 0, NULL, NULL) -> 8, and the bytes land in inb. +print(syscall(45, conn, _pin, 8, 0, 0, 0)) +print(inb.v) +``` +Output: +``` +8 +8 +123456789 +``` +Source: `tests/net/loopback.lio` + +The getsockname call is the proof that the address is real: the kernel writes the chosen port back into the very bytes you handed it. + +**getsockname writes the port back (excerpt)** +```text +socklen: int[64] = 16 +_psocklen: ptr[int[64]] = addressof(socklen) + +# getsockname(srv, &sa, &socklen): the kernel writes the real port back into the +# very bytes we handed it, which only works because the address is real memory. +print(syscall(51, srv, _sa, _psocklen)) +print(socklen) +``` +Output: +``` +0 +16 +``` +Source: `tests/net/loopback.lio` + +#### The byte-order obligation + +A sockaddr_in is network byte order. A Lithon int[64] is little-endian. The mismatch is your problem to solve, and the test solves it with a three-line swap. + +**the byte swap** +```text +def swap32(v: int[64]) -> int[64]: + return ((v & 255) << 24) | ((v & 65280) << 8) | ((v >> 8) & 65280) | ((v >> 24) & 255) +``` +Source: `tests/net/loopback.lio` + +> **The failure is silent, and the test proves it** — The same address without the swap is still a perfectly good 32-bit number. Nothing crashes: its bytes just name 1.0.0.127 instead of 127.0.0.1, and bind fails with EADDRNOTAVAIL (-99). A happy-path connect alone would not have shown this, so loopback.lio deliberately makes the failure explicit at the end. + +**the explicit failure (excerpt)** +```text +class SockAddrIn(Struct): + lo: int[64] + hi: int[64] + + +bad: SockAddrIn +bad.lo = 2 | (2130706433 << 32) +bad.hi = 0 +_bad: ptr[SockAddrIn] = addressof(bad) +badfd: int[64] = syscall(41, 2, 1, 0) +print(syscall(49, badfd, _bad, 16) < 0) +print(syscall(3, badfd)) +``` +Output: +``` +True +0 +``` +Source: `tests/net/loopback.lio` + +#### The syscall numbers used + +*Linux x86-64 syscall numbers* +- **41 — socket(domain, type, protocol)** — returns the file descriptor +- **42 — connect(fd, addr, len)** — the client side +- **43 — accept(fd, addr, len)** — the server side +- **44 — sendto(fd, buf, len, flags, addr, alen)** — send +- **45 — recvfrom(fd, buf, len, flags, addr, alen)** — receive +- **49 — bind(fd, addr, len)** — the server side +- **50 — listen(fd, backlog)** — the server side +- **51 — getsockname(fd, addr, len)** — asks the kernel for the bound address +- **3 — close(fd)** — release the descriptor + +> **These numbers are Linux x86-64 specific** — The syscall ABI is per-architecture. The numbers above are the x86-64 Linux ones; on another platform the same operation has a different number. Windows uses a completely different mechanism, so a Lithon program that issues Linux syscall numbers only runs on Linux. + +#### What is not there + +*no socket library, and that is deliberate* +| A socket type | there is none. A socket is an int[64] file descriptor. | +| --- | --- | +| A bind/listen function | there is none. These are syscall(49, ...) and syscall(50, ...). | +| An import | there is none. std/ is a sketch, not a module system. | +| A DNS resolver | there is none. getaddrinfo would need a string API the language does not have. | +| A struct for sockaddr_in | you write it yourself, packed to match the kernel's layout. | +| Cross-platform support | none. The syscall numbers are Linux x86-64. | + +- **Syscalls** — The primitive itself: argument kinds, and what the two tiers agree on. +- **Pointers** — Why a pointer to a struct is a real address. +- **Structs** — Packed layout, so your sockaddr_in matches the kernel's. +- **Strings** — Bounded byte runs, for the buffers you hand the kernel. + +--- + +## Inside the engine + +Dual tiers, verification, SSA, optimization, SIMD. + +### Engine architecture + +*Four layers from .lio source to executable memory: frontend, IR, verifier, encoder.* + +URL: https://project-lithon.github.io/Lithon/docs/architecture + +#### The four layers + +Lithon's native tier is a stack of small, independently checkable layers. Python appears at exactly one step; everything downstream of IR is C++ that never touches CPython. + +- **1 · Frontend** — src/frontend/frontend.py: parses .lio, runs the extended PEP-526 checks, emits text IR. Pure Python, no engine needed. +- **2 · IR text format** — Typed, SSA-shaped text. Parsed inside the engine, so the native tier has no Python dependency of any kind. +- **3 · Verifier** — SSA construction, dominance, liveness, register allocation. Anything unprovable is refused before a byte is emitted. +- **4 · Encoder** — Hand-rolled x86-64 bytes written straight into PROT_READ | PROT_EXEC memory (mmap on Linux, VirtualAlloc on Windows) and called through a function pointer. + +> **No LLVM, no Cranelift, no runtime package** — The entire backend is C++20 with zero third-party dependencies. GNU as appears in exactly one place: the encoder verification harness, and never in the build. + +#### A walk-through: arithmetic + +The regression corpus's smallest program, all the way down. Source on the left of your memory, machine code at the bottom: + +**tests/programs/arithmetic.lio** +```text +x: int[64] = 10 +y: int[64] = 20 +z: int[64] = x + y +print(z) +print(x - y) +print(x * y) +``` +Output: +``` +30 +-10 +200 +``` +Source: `tests/programs/arithmetic.lio` + +The IR stores and reloads x/y because the frontend emits a memory-shaped IR on purpose, that is the input the SSA pass (Mem2Reg) consumes, promoting provable slots into registers. What survives to the encoder after optimization is register-shaped, not stack-shaped. + +#### Calling conventions + +*ABI support* +| SysV (Linux/macOS) | implemented and audited on every host | +| --- | --- | +| Win64 | implemented behind #if defined(_WIN32): no test evidence yet | +| Stack alignment | 16-byte at call sites, audited by check_stack_alignment.py | +| Callee-saved | RBX, RBP, R12–R15 preserved; caller-saved reused freely | + +> **Win64 is implemented but unproven** — The path exists and compiles, but the 23-module ABI audit runs on SysV hosts. Treat Win64 as experimental until it has its own evidence. + +#### Where to go next + +- **Source to machine** — The four artifacts one program produces, end to end. +- **Registers & ABI** — The register file and the calling discipline. +- **Instruction encoding** — Why the encoder is hand-rolled, and how it is proven. +- **Verification** — How the claims on this page get checked. + +--- + +### Dual-tier execution + +*Tier-1 emits native x86-64 for what is proven; Tier-0 interprets the rest; --strict refuses instead of falling back.* + +URL: https://project-lithon.github.io/Lithon/docs/dual-tier + +#### Tier-1: the native lane + +Every IR block whose types are provably static is compiled by the backend into x86-64 and written into executable memory. There are no runtime type checks, no guards, and no boxing on this path: the verifier already did that work, once, at compile time. + +**forcing the native lane** +```bash +$ lithon tests/programs/float.lio --strict +[tier1] native +0.3333333333333333 +``` +*[tier1] native goes to stderr; program output goes to stdout. Under --strict, exit 0 means the native tier really ran.* + +#### Tier-0: the honest fallback + +A C++ interpreter over the same IR. It exists so that a program the native tier cannot yet prove still runs: same source, same IR, same output. It is a real implementation, not a debug printer: run_tier_diff.py requires both tiers to agree byte for byte on stdout and stderr. + +In automatic mode, tier_runner tries Tier-1 first and falls back to Tier-0 with a message on stderr. This keeps the demo honest: you always know which lane ran. + +#### --strict turns fallback into refusal + +With --strict, a Tier-0 fallback is a failure, not a quiet slowdown. This is the switch the entire test suite leans on: without it, "the program printed the right number" could mean the interpreter carried the whole run. + +**terminal** +```bash +$ lithon program.lio --strict # native only, refuse to fall back +$ ./build/tier_runner prog.ir --strict # same thing, two-step form +``` + +> **The discipline in one line** — Green + --strict = the native backend compiled and executed this program. There is no other way to exit 0. + +#### run_tier_diff.py: the cross-check + +The highest-value script in the repo. It runs every regression program through both tiers and requires identical stdout, then reports which tier actually ran: + +**terminal** +```bash +$ python3 tools/run_tier_diff.py +… every program: both tiers agree … +``` + +A green run cannot hide "everything silently fell back to the interpreter", because the report says which tier ran each program. + +--- + +### The type checker + +*Static analysis over Python's AST: the unconditional gate every module passes before anything runs.* + +URL: https://project-lithon.github.io/Lithon/docs/typechecking + +#### Unconditional, on every module + +tools/typecheck.py is static analysis over Python's ast: no execution, no imports evaluated. It runs whether the module annotates anything or not: an unannotated program is not "unchecked", it is invalid. + +**terminal** +```bash +$ python3 tools/typecheck.py program.lio +$ echo $? +0 +``` + +> **Two checkers, one frontend** — The frontend (src/frontend/frontend.py) lowers to IR; the checker (tools/typecheck.py) is the gate. CI runs the checker unconditionally on every program: check_typecheck_unconditional.py enforces that no suite is allowed to skip it. + +#### What it enforces + +*The 0.6 series* +| 0.6.1 | every first assignment, parameter, and return carries an explicit annotation | +| --- | --- | +| 0.6.4 | re-assignment is checked against the already-declared type | +| 0.6.5 | integer literal overflow, and provable-range overflow for binary ops, are compile-time errors | +| 0.6.8 | function contracts: parameters and return type mandatory and checked; call sites checked against the signature; declared return must be provably wide enough: never auto-widened | +| 0.6.9 | print() is a fixed built-in, not a user function | +| 0.6.10 | definite assignment + type agreement across if/else branches (function scoping, not block scoping) | +| 0.6.11 | conversions: widening/same-width automatic, narrowing never; int→float automatic, float→int never; no cast syntax anywhere; int literal is not a float | +| 0.6.12 | range()'s produced values are checked against the loop variable's declared width at compile time when known | + +Bitwise and shift operators (<<, >>, &, |, ^) are integer-only and not part of the arithmetic promotion rule: 2.5 & 1 is a type error, because Lithon has no float bit pattern to reinterpret. Their result takes the left operand's width. A literal shift count must be 0..63: the machine word is 64 bits and x86 masks the count to 6 bits, so a count of 64 would silently execute as 0. + +#### Reading an RCR error + +**a real diagnostic, annotated** +```text +x = 10 +print(x) +``` +Expected to be refused with: `without ever being declared with a type annotation` + +Every diagnostic carries the failing construct, the name or expression at fault, the spec section (V1_SPEC 0.6.x) that makes it an error, and: where one exists: the spelling that would have been accepted. The full code table lives in Errors (/docs/errors). + +--- + +### Optimizer & SSA pipeline + +*Mem2Reg, dominance, phi placement, liveness, and register allocation: each pass switchable so effects are measured, not assumed.* + +URL: https://project-lithon.github.io/Lithon/docs/pipeline + +#### Every pass is switchable, and measured + +Register allocation and liveness are shipped and tested, and so is a set of optimizations that can each be switched off individually, so their effect is measured rather than assumed: + +*Measured against HEAD, one pass at a time* +| Strength-reduced multiplies | optimize.h · nested 1.062× | +| --- | --- | +| Accumulator unroll (opt-in) | optimize.h · float reduction 1.58× · 95.8 → 60.5 ms | +| Callee-saved borrowing | register_alloc.h · fib 1.063× | +| Mem2Reg + SSA copy resolution | ssa.h · 29 loads · 20 stores removed | +| Diamond unrolling (opt-in) | 1.10× slower, kept behind --unroll-diamonds | + +#### The SSA conversion, phase by phase + +The IR keeps mutable variables in memory: a variable is written by a Store and read by a Load, so promotion means turning those into values that live in registers. The conversion is done one switchable phase at a time: + +*SSA phases · all shipped* +| 2.1 | CFG, Cooper-Harvey-Kennedy dominators, natural loops, preheaders, latches, exits · loop_info.h | +| --- | --- | +| 2.2 | Phi placement by iterated dominance frontier · ssa.h | +| 2.3 | Mem2Reg: promote, place phis, rewrite the memory traffic away · ssa.h | +| 2.4 | SSA copy propagation, dead-value elimination, then resolve_phis() · optimize.h | +| 2.5 | Phi copies as register moves instead of a memory round-trip · register_alloc.h | +| 2.6 | Dedicated loop-exit blocks where the exit edge is genuinely shared · loop_info.h | +| 2.7 | CFG liveness and interference-graph allocation · liveness.h | +| 2.8 | Register coalescing: a merge adopts its source's dead register · both halves | + +> **One decision in there is load-bearing** — Promotion is gated on a *must*-analysis that iterates down from "everything", converging on the greatest fixpoint. Iterating upward from nothing makes ordinary loop accumulators silently un-promotable forever. Anything the analysis cannot prove stays in memory, and vars_declined reports it. + +The Phase I page (/roadmap/phase-1-dual-tier) walks through all eight phases with the reasoning behind each, including why register coalescing honestly deletes five merges across the typed regression corpus and not more. + +#### One IR, both tiers + +The SSA work happens on the same IR text the interpreter consumes. Optimizations that would change observable behaviour are refused rather than approximated, that is what lets run_tier_diff.py compare the two tiers byte for byte even when Tier-1 has run the full optimizer and Tier-0 has run none of it. + +--- + +### SIMD auto-vectorization + +*8-wide AVX2 over int[32] lists, with a CPUID gate, a scalar tail, and the untouched scalar path as fallback.* + +URL: https://project-lithon.github.io/Lithon/docs/simd + +#### What the vectorizer does + +The native backend can fuse a canonical elementwise or reduction loop over a fixed-capacity int[32] list into 8-wide AVX2 lanes. The compiler recognizes the range-loop shape the while/for lowering produces: a small rotatable header (i < N), a straight-line body with a single element read/write and one integer op, and an i = i + 1 latch, and replaces it with: + +- **CPUID gate** — A has_avx2() check, read from an int32 constant pool appended to the module. +- **8-wide vector main loop** — vmovdqu / vpaddd | vpsubd | vpmulld over N - N%8 elements. Reductions run a vpxor + vextracti128 + vpshufd halving merge. +- **Scalar tail** — The last N%8 elements run scalar. +- **Scalar fallback** — The untouched scalar header remains, so on any host without AVX2 the exact scalar code path still runs. + +#### Why it is exact + +*Correctness properties* +| Verified bytes | vector slices are byte-for-byte what the encoder emits; check_encoder_vs_as.py verifies them | +| --- | --- | +| 32-bit lanes only | only int[32] lists vectorize, so no wider-than-lane math can be reordered into a different overflow | +| Reduction accumulator checked | recognized only when the accumulator really is int[32]: verified from IR width annotations | +| vzeroupper discipline | emitted before any Call or Return in a function that used the vector path: audited by check_vex_transitions.py | +| Escape hatches | --no-vectorize disables the pass; it also needs loop rotation, so --no-rotate disables it too | + +> **The vectorizer refuses rather than guesses** — A loop the recognizer cannot prove safe is left scalar. There is no "probably fine" vector path: the same refuse-over-guess rule as the rest of the engine. + +#### Status + +| SIMD vectorizer loop cases | 6 / 6 passed | +| --- | --- | +| SIMD vectorizer gate + fallback | verified | +| AVX-512 | not emitted, Phase III | + +CPU feature detection is cross-checked against the kernel rather than trusted to CPUID, so the AVX and AVX-512 gates cannot mis-detect. No AVX-512 is emitted yet, but the gating and the VEX transition scanner are already in place underneath it. + +--- + +## Reference + +Errors, builtins, verification gate, honest limits. + +### Errors & diagnostics + +*The LITHON-E code family: static refusals, environmental failures, and dynamic traps: with real diagnostics.* + +URL: https://project-lithon.github.io/Lithon/docs/errors + +#### The code families + +Every failure carries a LITHON-Ennnn code. The first digit is the tier the failure belongs to: the same numbering the runtime uses to decide refuse-vs-trap: + +*from docs/whitepaper.md* +| E01xx: tier 1 · static refusal | compile-time, the program never runs | +| --- | --- | +| E02xx: tier 2 · environmental | the environment failed the program, not the program itself | +| E03xx: tier 3 · dynamic arithmetic/logic trap | runtime, per-operation | +| E04xx: tier 3 · dynamic resource trap | runtime, resource exhaustion | + +*the full code table* +| E0101, 1 (static refusal) | Overflow on reassignment | +| --- | --- | +| E0102, 1 (static refusal) | Literal zero divisor | +| E0103, 1 (static refusal) | Literal shift out of range | +| E0104, 1 (static refusal) | Literal-provable capacity overflow | +| E0105, 1 (static refusal) | Unsupported type width | +| E0106, 1 (static refusal) | float(64, x) of int[64] refused: lossy, 53 bits cannot hold every int[64] | +| E0107, 1 (static refusal) | range(a, b, 0) with a literal zero step | +| E0108, 1 (static refusal) | Unsupported type-annotation form | +| E0109, 1 (static refusal) | Unknown or badly-sized annotation type name | +| E0110, 1 (static refusal) | Annotation missing a required field | +| E0111, 1 (static refusal) | Bad container capacity / literal count / str[N] capacity or literal length | +| E0112, 1 (static refusal) | Container element must be a plain scalar | +| E0113, 1 (static refusal) | Pointer (ptr[T]) declaration rules | +| E0114, 1 (static refusal) | Unsupported or restricted program construct -- the catch-all: an operator that does not exist, a try/except shape that is not allowed, a string literal used where a value is needed | +| E0115, 1 (static refusal) | Unsupported builtin call form | +| E0116, 1 (static refusal) | Builtin argument rules (valueof/addressof/wrap_*) | +| E0117, 1 (static refusal) | Indexing / container-subscript restrictions | +| E0118, 1 (static refusal) | else clause on a while/for loop | +| E0119, 1 (static refusal) | for-loop form restrictions | +| E0120, 1 (static refusal) | break/continue outside a loop | +| E0121, 1 (static refusal) | Assignment / target restrictions | +| E0122, 1 (static refusal) | Declaration and re-declaration rules (including the _ prefix rule for pointers) | +| E0123, 1 (static refusal) | Tuple/dict literal shape | +| E0124, 1 (static refusal) | Function-header restrictions | +| E0125, 1 (static refusal) | Pointer arithmetic form | +| E0126, 1 (static refusal) | Struct definition (must derive from Struct; fields are annotations only) | +| E0127, 1 (static refusal) | Struct field type (int[w]/float[64]/bool or an earlier struct; no containers/pointers) | +| E0128, 1 (static refusal) | Struct field access (unknown field, a whole struct used where a value is needed, or a str field reached through a pointer into the struct) | +| E0129, 1 (static refusal) | Enum definition (must derive from Enum; variants are bare unit names, payload annotations, or inline class V(Struct)) | +| E0130, 1 (static refusal) | Enum variant construction (unknown variant, wrong payload shape, unknown/missing/duplicate struct-payload field) | +| E0131, 1 (static refusal) | match on an enum (bad subject/pattern, non-exhaustive, a guard, a wildcard that is not last, or an enum used where a value is needed) | +| E0132, 1 (static refusal) | Type-function definition (-> NewType): bad signature, a body that is not a one-element { T }, a duplicate name, or a name colliding with a runtime function/struct/enum | +| E0133, 1 (static refusal) | Type-function application: an unknown callee, keyword/starred arguments, an arity mismatch, or a recursive expansion | +| E0134, 1 (static refusal) | Method definition (a def in a struct/enum body): a decorator, //*/**/defaults, a missing or annotated self, a non-scalar parameter/return, a duplicate name, or a name colliding with a field or variant | +| E0135, 1 (static refusal) | Method application: an unknown method, a field where a method is called, a non-struct/enum receiver, an arity mismatch, a recursive inline, or a method with no result used as a value | +| E0201, 2 (environmental) | Fallible value read before .ok check -- designed, not built: fallible[T] does not exist yet | +| E0301, 3 (dynamic trap) | Division by zero: shipped | +| E0302, 3 (dynamic trap) | Modulo by zero: shipped | +| E0303, 3 (dynamic trap) | Dynamic integer overflow: shipped, wrap_* opt-out | +| E0304, 3 (dynamic trap) | Shift amount out of range: shipped | +| E0305, 3 (dynamic trap) | Dynamic index/capacity out of range: shipped for list, dict TBD | +| E0306, 3 (dynamic trap) | Invalid integer literal in input | +| E0307, 3 (dynamic trap) | Input value outside the target width | +| E0308, 3 (dynamic trap) | End of input | +| E0309, 3 (dynamic trap) | Float-to-int conversion out of range or not finite | +| E0310, 3 (dynamic trap) | range(a, b, step) with step == 0, dynamic | +| E0401, 3 (resource trap) | Stack overflow: shipped, 4096-depth shared counter, byte-identical in both tiers | + +> **the tier digit is the contract** — The first digit after E is not decoration: it says whether the failure was knowable before the program ran. A tier-1 code is a refusal -- the program never started. A tier-3 code is a trap -- the program started, and stopped at a specific operation. try/except can only catch tier-3 traps, because a tier-1 refusal has no program to catch it in. Every code the source uses must be listed in docs/whitepaper.md; tools/check_error_codes.py fails the build otherwise. + +#### Static refusal vs dynamic trap + +The split is the point: overflow the verifier can prove at compile time is E0101, a refusal: the program never runs. Overflow only visible at run time is E0303, a trap: checked per operation in both tiers, identically. + +**the same operation, two verdicts** +```text +x: int[64] = 9223372036854775807 +x = x + 50 # provable at compile time: a refusal, not a runtime event + +b: int[64] = int(64, input("n: ")) # unknown until run +c: int[64] = b + 50 # Tier-3 trap (E0303) iff it overflows +``` + +> **E0303 and E0401 are shipped** — Dynamic-overflow trapping for Add/Sub/Mul (E0303) is shipped, with wrap_add/wrap_sub/wrap_mul as the explicit opt-out, and the stack-depth guard (E0401) is shipped: both tiers bump a shared call-depth counter and trap at depth 4096 byte-identically. E0301/E0302/E0304 and list E0305 are shipped too. + +#### RCR diagnostics from the checker + +The type checker's own diagnostics (RCR error:) are a separate family: they fire before any LITHON-E code could apply. Every one names the construct, the offending binding, the spec section, and the fix: + +**annotation missing** +```text +x = 10 +print(x) +``` +Expected to be refused with: `without ever being declared with a type annotation` + +**narrowing** +```text +wide: int[64] = 5 +narrow: int[8] = wide +print(narrow) +``` +Expected to be refused with: `cannot narrow` + +**use before assignment** +```text +i: int[8] +print(i) +``` +Expected to be refused with: `never assigned a value` + +#### Both tiers trap identically + +run_tier_diff.py compares stderr byte for byte, so a trap message that differed between Tier-0 and Tier-1 would fail the suite. The trap text is part of the contract, not a debugging courtesy. + +**terminal** +```bash +$ lithon div_zero.lio # dynamic divisor +error: division by zero +``` + +Because the message is the same in both tiers, a trap can be caught by name and handled identically either way. The try/except classifier matches on the message text, not on a code, which is why the text is part of the contract. + +- **Exceptions & traps** — The seven catchable traps, and the else/finally clauses. +- **Control flow** — The try statement in the context of the other flow forms. + +--- + +### Built-in functions + +*print, len, range, addressof, valueof, contains, enumerate, zip, and the wrap_* family: the whole prelude.* + +URL: https://project-lithon.github.io/Lithon/docs/builtins + +#### The prelude + +Lithon's prelude is deliberately tiny. print() is a fixed built-in, not a user function (V1_SPEC 0.6.9); everything below is checked by name by the type checker and lowered specially by the frontend: + +*built-ins* +| print(x) / print(a, b, c) | write one or more values, space-separated, and a newline: the only output primitive. print() with no arguments writes a bare newline. | +| --- | --- | +| len(xs) | the live length of a list/tuple/dict, or the run-time byte count of a str[N]. Carried in a companion slot, so it can change at run time. | +| range(n) / range(a, b) | loop values, checked against the loop variable's width at compile time when known (0.6.12) | +| addressof(x) | ptr[T] to a scalar, a str[N], or a struct | +| valueof(_p) | read the scalar at _p | +| contains(d, k) | bool: key k present in dict d (constant and runtime keys) | +| sum(xs) | sum of a list[int[64]|float[64], N]: unrolled over the compile-time length | +| min(xs) / max(xs) | min/max of a list: unrolled over N, same comparison as CPython | +| any(xs) / all(xs) | bool OR/AND over a list[bool, N] | +| abs(v) | scalar absolute value: -v if v < 0 else v, not a list walk | +| x in xs / x not in xs | bool: OR of x == xs[k] over the compile-time length | +| int(w, v) | explicit conversion to int[w]: float[64] -> int[w] (E0309 trap if out of range or not finite) | +| float(64, v) | explicit conversion to float[64] from int[8|16|32]/float[64] (E0106 refusal for a lossy int[64] source) | +| int(w, input(prompt)) | read int[w] from stdin, optional prompt string (E0306-E0308 traps: bad literal, out of width, end of input) | +| syscall(n, a1..a6) | raw unchecked syscall; integer or pointer arguments, returns int[64] (-errno on Linux) | +| wrap_add(a, b) | wrapping integer add: same wrapped value in both tiers | +| wrap_sub(a, b) | wrapping integer sub | +| wrap_mul(a, b) | wrapping integer mul | + +Two more names are built in, but only in a for header: enumerate(xs) yields (index, value) and zip(xs, ys) yields pairs, stopping at the shortest input. Used anywhere else they are refused, because a general iteration protocol does not exist yet. + +**enumerate and zip in a for header** +```text +xs: list[int[64], 3] +ys: list[int[64], 3] +xs[0] = 10 +xs[1] = 20 +xs[2] = 30 +ys[0] = 1 +ys[1] = 2 +ys[2] = 3 +for i, x in enumerate(xs): + print(i) +for a, b in zip(xs, ys): + print(a) +``` +Output: +``` +0 +1 +2 +10 +20 +30 +``` + +Three names behave like methods rather than free functions: xs.append(v) on a list (which never grows the list -- it writes into the next live slot and can only fail with a full list), s.find(sub) / s.count(sub) on a bounded string, and parts: list[str[K], M] = s.split(sep) into a declared destination list. + +**print takes several values** +```text +a: int[64] = 1 +b: int[64] = 2 +print(a, b, "x") +``` +Output: +``` +1 2 x +``` + +> **wrap_* is the explicit opt-in** — Provable static overflow of plain +/-/* is a compile error (E0101/E0303-family) that points you at wrap_*. The wrapping result is pinned to be identical across tiers by tools/check_error_codes.py. + +- **Containers** — What len, sum and append actually operate on. +- **Strings** — find, count, split, and the bounded-string model. +- **Semantics** — Where Lithon deliberately diverges from CPython. +- **Errors & diagnostics** — The code family, and which of them you can catch. + +#### wrap_* in action + +**tests/typed_regression/int64_wrap.lio** +```text +x: int[64] = 9223372036854775807 +y: int[64] = 1 +z: int[64] = wrap_add(x, y) +print(z) +``` +Output: +``` +-9223372036854775808 +``` +Source: `tests/typed_regression/int64_wrap.lio` + +#### What is not a built-in + +*rejected by name* +| len() on a scalar | type error: len is a container operation | +| --- | --- | +| cast() | does not exist anywhere in the language | +| user-defined print | rejected: print is reserved as the fixed built-in | +| bare int/float constructor call, e.g. int(x) | wrong arity: the real form takes an explicit width, int(w, x) / float(64, x) | + +--- + +### Semantics + +*Where Lithon deliberately diverges from CPython: modulo, shifts, float traps, truthiness.* + +URL: https://project-lithon.github.io/Lithon/docs/semantics + +#### % is C's %, not Python's + +% truncates toward zero and takes the sign of the dividend: what C, Rust and Java do. CPython uses floor-division semantics; the two disagree on negatives: + +**modulo · truncating, like C** +```text +print(-7 % 3) +print(7 % -3) +``` +Output: +``` +-1 +1 +``` +*CPython would print 2 and -2. Lithon chose the machine's behaviour.* + +A zero divisor traps on both engines (E0301/E0302), and a constant power-of-two divisor is strength-reduced to a mask and a sign fixup. + +#### Shifts are 64-bit, count checked + +**shifts** +```text +print(5 << 63) +``` +Output: +``` +-9223372036854775808 +``` + +Left shifts wrap, because there is no wider type to widen into. A count outside 0..63 is refused: at compile time for a literal (RCR, V1_SPEC 0.6.11), at run time otherwise (E0304). 2.5 & 1 is a type error: there is no float bit pattern in Lithon to reinterpret. + +#### Float traps like Python, not like IEEE + +Floats are implemented end to end in SSE2, and one formatter serves both tiers, so their output agrees byte for byte. Three trap rules matter: + +| division by zero | traps like Python's ZeroDivisionError, not IEEE inf/nan | +| --- | --- | +| NaN divisor | must not trap, Python propagates NaN | +| -0.0 divisor | must trap: it compares equal to 0.0 | + +> **The ZF AND !PF problem** — comisd sets ZF for both "equal" and "NaN", distinguishable only with PF. The backend handles this explicitly; the language page (/roadmap/language) has the full list. + +#### No truthiness, no chaining + +| if x: with x: int | type error: a condition must be bool | +| --- | --- | +| and / or on non-bool | type error | +| chained comparison a < b < c | not supported | +| for/else | rejected, never silently dropped | + +A trap raised by any of the rules above can be caught by name with try/except -- see Control flow (/docs/control-flow#try-except) for the catchable set. A static refusal cannot: it happens before the program exists to catch it. + +#### Where to go next + +- **Errors & diagnostics** — Every code, and which tier it belongs to. +- **Built-in functions** — The whole prelude, one row per name. +- **Control flow** — if / while / for, and try/except. +- **Honest limits** — What does not work yet, stated plainly. + +--- + +### Verification + +*The numbers behind the claims: 33/33 CTest, 18,379 encoder cases with 0 wrong, both tiers byte-identical.* + +URL: https://project-lithon.github.io/Lithon/docs/verification + +#### Current verification highlights + +*from README.md: run it yourself, don't trust it* +| CTest | 33 / 33 passed | +| --- | --- | +| x86-64 encoder comparisons | 18,379 cases | +| Incorrect encoder cases | 0 | +| Typed differential fuzzing | 300 / 300 passed | +| Bitwise / shift fuzzing | 200 / 200 passed | +| Conditional-expression / SSA fuzzing | 200 / 200 per suite | +| Direct float Phi sweep | 38 matched / 0 mismatched | +| ABI modules audited | 34 | +| ABI stack/callee-saved failures | 0 | +| Typed regression suite | 46 / 46 passed | +| General regression suite | 34 / 34 passed | +| Type-checker unit checks | 145 ok | +| Error-code discipline checks | 86 ok | +| Tier differential (interp vs JIT) | 168 / 168 passed | +| Syscall hook parity (both tiers) | 4 / 4 passed | +| Networking loopback vs a C oracle | verified | +| SIMD vectorizer loop cases | 6 / 6 passed | +| SIMD vectorizer gate + fallback | verified | + +> **The gate concludes** — bash tools/verify_all.sh runs the whole stack and ends with ALL CHECKS PASSED or it does not pass. + +> **Where the corpus lives** — The general suite is tests/programs/ (34 programs), the typed suite is tests/typed_regression/ (46 programs), and the negative suite is tests/typed_programs/ (22 programs the checker must reject). Every one has a pinned expected-output file, and the tier differential runs the whole corpus through both engines and requires byte-identical stdout. + +#### Encoder vs GNU as + +The hand-written x86-64 encoder is compared against GNU as, instruction by instruction. A byte difference is not automatically an error: x86-64 often has multiple valid encodings, so the harness decodes and classifies: + +**tools/check_encoder_vs_as.py** +```text +9239 instruction cases +7949 byte-identical +1290 different but valid encodings +0 incorrect encodings + +WRONG = 0 +``` + +The only number that matters is WRONG = 0. + +> **Two numbers, two moments** — The README quotes both figures: 18,379 is the size of the case corpus, and the 9,239 / 7,949 / 1,290 block is a snapshot of one run of the classifier over the cases exercised at that time. Neither is a claim about correctness — the claim is WRONG = 0. + +#### Work outward, stop where you are satisfied + +Each layer is roughly an order of magnitude slower than the one above it, which is what makes running all of them reasonable rather than aspirational: + +**the full gate** +```bash +$ ctest --test-dir build --output-on-failure # 33/33 +$ python3 tools/run_regression.py # general suite +$ python3 tools/run_typed_regression.py # typed suite +$ python3 tools/run_tier_diff.py # both tiers, byte-identical +$ bash tools/verify_all.sh # ALL CHECKS PASSED +``` + +**layer 3 · per-shape differential fuzzing** +```bash +$ python3 tools/fuzz_diff.py --count 300 # general +$ python3 tools/fuzz_diff.py --count 300 --floats # int/float mixes +$ python3 tools/fuzz_diff.py --count 300 --bitwise # & | ^ << >>, RCX hazard +$ python3 tools/fuzz_diff.py --mod-negatives --count 300 +$ python3 tools/fuzz_diff.py --phi --count 300 # merges, with and without --ssa +``` + +> **What a green run does not prove** — Opcode coverage is not operand coverage. All 55 IR opcodes are emitted and exercised, but that does not prove every operand shape is right: Mod is the standing example: the general fuzzer only emits finite constants, so it cannot generate 1.0 % inf, which was returning NaN natively while the interpreter was correct. + +#### The headline benchmark + +*fib(30), from benchmarks/results/latest.json* +| CPython 3.12 | 1731.0983 ms | +| --- | --- | +| Lithon native | 10.3053 ms | +| Speedup | 168.0× | +| Result | 832040: identical on both | + +The file is written by the benchmark bot in CI (.github/workflows/lithon-report-mamba.yml), which also keeps one snapshot per commit under benchmarks/results/history/. The numbers above are the current latest.json; the table is regenerated rather than hand-edited. + +Benchmarks are the weakest form of evidence: they prove speed, not correctness. The suites above are what prove correctness; the benchmark is what the correctness buys you. + +--- + +### Honest limits + +*The gaps as they stand: no closures, no stdlib, no float promotion on merges, Win64 unproven, x86-64 only.* + +URL: https://project-lithon.github.io/Lithon/docs/limits + +#### The gaps + +A green test run covers the supported subset and should not be mistaken for a finished language. These are the gaps as they stand today: + +| IR opcodes emitted | 55 of 55 | +| --- | --- | +| Phi | emitted on the incoming edges; a direct-Phi native path also exists and is tested | +| arguments per function / call | two in registers, the rest on the stack · budget 32 | +| float merges | never promoted · needs an XMM pool | +| Win64 ABI | implemented, no test evidence yet | +| AOT binary emit | none · everything runs in-process | +| architectures | x86-64 only · ARM64 is Phase III | +| AVX-512 | not emitted · feature gating only | +| dict capacity trapping | list bounds trap shipped, dict's equivalent doesn't yet | +| // floor division | refused outright: *operator FloorDiv not supported yet* | +| ** power | refused outright: *operator Pow not supported yet* | +| <= and >= | refused: *comparison LtE not supported yet* | +| unsigned ints | no uint: signed at every width | +| closures | none: a function body cannot capture a caller's variable | +| first-class functions | none: a function name is not a value | +| lambda | refused: *expression node Lambda not supported yet* | +| None | refused: *expression node Constant not supported yet* | +| diamond unrolling | opt-in via --unroll-diamonds, 1.10× slower today | + +> **A str cannot be passed to a function** — Strings are addressed, not passed: a str function parameter is refused with *a str function parameter is not yet a string place*. This is the same rule that makes a struct unusable as an argument: Lithon passes scalars and containers, and addresses anything with an identity. + +#### This is not a CPython replacement + +No stdlib, no import system beyond the built-ins, no closures, and no exception objects: try/except catches the engine's own traps, but there is nothing to bind. Lithon is an experiment in one narrow question: what happens to a Python-shaped language when the types are mandatory and the machine is visible, and the honest answer to "can it run my app?" is "almost certainly not, yet". + +- **Roadmap** — What is next, which phase each item belongs to. +- **Divergences from Python** — The eleven places Lithon deliberately differs from CPython. +- **Verification** — How the claims above get checked rather than taken on trust. + +--- + +## Machine view + +For enthusiasts: IR, registers, x86-64 encodings. + +### Source to machine + +*The full walk: .lio → typed IR → verified SSA → x86-64 bytes, with the real artifacts at each step.* + +URL: https://project-lithon.github.io/Lithon/docs/source-to-machine + +#### Four artifacts, one program + +Every Lithon program leaves four artifacts on the way to execution. Each one is inspectable: nothing is hidden behind a debugger: + +- **Source (.lio)** — What you wrote. Annotations and all. +- **Typed IR (text)** — frontend.py output: %N = add %a, %b, store x, %v : int[64], branch, call print. This is what the engine parses; there is no Python left after this point. +- **Verified SSA** — After Mem2Reg and phi placement: loads and stores that provably stay in memory are the only ones that survive. +- **x86-64 bytes** — Written into PROT_READ | PROT_EXEC memory and called through a function pointer. No object file, no linker, no as. + +**seeing it yourself** +```bash +$ lithon prog.lio --ir # stop after IR +$ python3 src/frontend/frontend.py prog.lio > prog.ir +$ ./build/tier_runner prog.ir --strict -v +[tier1] native +``` + +#### Reading the IR + +The IR is deliberately boring. Registers are %N virtuals, variables are named slots with widths, and control flow is explicit blocks with branch/jump. Here is a function with a comparison and two arms, exactly as the frontend emits it: + +**tests/typed_regression/if.lio → IR** +```text +x: int[64] = 5 +if x < 3: + print(1) +else: + print(3) +``` +Output: +``` +3 +``` +Source: `tests/typed_regression/if.lio` + +The corresponding x86-64 keeps the same shape: one conditional branch, one shared join: + +**the same control flow, encoded** +```asm + mov eax, 5 + mov QWORD PTR [rbp-0x8], rax ; x + mov rax, QWORD PTR [rbp-0x8] + cmp rax, 3 + jge .Lelse ; !(x < 3) + mov eax, 1 + mov rdi, rax + call print + jmp .Ljoin +.Lelse: + mov eax, 3 + mov rdi, rax + call print +.Ljoin: +``` +*Hand-assembled for reading. The instruction shapes are real encoder output patterns; the byte schedules live in src/jit/encode.h and are verified against GNU as.* + +#### Calls and the frame + +User functions get the SysV treatment: the first two arguments in rdi/rsi (XMM0/XMM1 for floats), any further arguments on the stack, the return value in rax, and a frame that check_stack_alignment.py audits for 16-byte alignment at every call site: + +**tests/typed_regression/function.lio → IR** +```text +def add(a: int[64], b: int[64]) -> int[64]: + return a + b + +x: int[64] = add(3, 4) +print(x) +``` +Output: +``` +7 +``` +Source: `tests/typed_regression/function.lio` + +#### Recursion, end to end + +The fib(10) example: real IR, real output, and the benchmark that 189× speedup comes from at fib(30): + +**tests/typed_regression/recursion.lio** +```text +def fib(n: int[64]) -> int[64]: + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + +x: int[64] = fib(10) +print(x) +``` +Output: +``` +55 +``` +Source: `tests/typed_regression/recursion.lio` + +#### Where to go next + +- **Registers & ABI** — The register file and the calling discipline the IR maps onto. +- **Instruction encoding** — How the IR becomes bytes, and how those bytes are proven. +- **Optimizer & SSA pipeline** — The passes that run between the IR and the encoder. +- **Dual-tier execution** — What happens when the native tier refuses to run a program. + +--- + +### Registers & ABI + +*The register file, the SysV convention the backend implements, and the stack discipline the auditors check.* + +URL: https://project-lithon.github.io/Lithon/docs/registers-abi + +#### The register file + +*x86-64 general purpose registers, and how the backend uses them* +| RAX | accumulator / return value / scratch | +| --- | --- | +| RCX | scratch: note: shift counts come in CL, a known hazard the bitwise fuzzer targets | +| RDX | scratch / div high half | +| RBX, R12–R15 | callee-saved: the allocator's long-lived homes | +| RSP | stack pointer, 16-byte aligned at every call | +| RBP | frame pointer: every function keeps it, which is what makes [rbp-0x…] slots readable in a dump | +| RDI, RSI | SysV argument 0 / argument 1 (RCX/RDX on Windows); arguments past the second are passed on the stack | +| R8, R9 | SysV temporary pool (check_stack_alignment scrutinee); not argument registers in this ABI | +| XMM0–XMM15 | SSE2 float; the vectorizer's vmovdqu lanes | + +> **The RCX hazard** — Shifts mask their count with 6 bits of CL, and the general fuzzer has a dedicated --bitwise mode for exactly the shapes where a live RCX gets clobbered. It is a real bug class this backend has already paid for. + +#### SysV discipline + +The audited convention. Every one of the 23 ABI modules is checked for the same invariants: + +*what check_stack_alignment.py parses out of real modules* +| prologue | push rbp · mov rbp, rsp · sub rsp, N | +| --- | --- | +| alignment | RSP ≡ 0 (mod 16) at every call: the push rbp makes up the odd word | +| callee-saved | RBX/RBP/R12–R15 restored before ret | +| epilogue | leave (mov rsp, rbp; pop rbp) or the explicit equivalent, then ret | +| slots | QWORD PTR [rbp-0x…] for int[64], DWORD/WORD/BYTE for narrower widths | + +**the canonical prologue/epilogue** +```asm +my_func: + push rbp + mov rbp, rsp + sub rsp, 0x20 ; 4 slots, keeps RSP 16-aligned after the push + ... + mov QWORD PTR [rbp-0x8], rdi ; spill arg0 if it must survive a call + ... + leave + ret +``` + +#### Win64: implemented, unproven + +The path exists behind #if defined(_WIN32) and compiles, but the 23-module ABI audit runs on SysV hosts only. Win64 differs where it always differs, 32-byte shadow space, different volatile set (RAX RCX RDX R8 R9 R10 R11), XMM0–XMM5 volatile, and none of that has test evidence yet. + +> **Treat Win64 as experimental** — The limits page (/docs/limits) lists this as an open gap. It will close with its own audit, not with a claim. + +#### Where the bytes live + +Emitted code is written straight into memory the OS marked executable and called through a function pointer: mmap with PROT_READ | PROT_EXEC on Linux, VirtualAlloc on Windows. There is no temporary file, no loader, and no as invocation anywhere in the pipeline. + +--- + +### Instruction encoding + +*The hand-written x86-64 encoder: how bytes are chosen, and how GNU as is used to prove every one of them.* + +URL: https://project-lithon.github.io/Lithon/docs/instruction-encoding + +#### Why hand-roll an encoder + +No LLVM, no Cranelift, no assembler in the build. The backend emits bytes directly: every REX prefix, ModRM, SIB, and displacement chosen by code the project owns and can audit. The cost is that every byte is your responsibility; the benefit is that the whole native tier is ~a C++ codebase you can read in an afternoon. + +The encoder covers 26 of 27 IR opcodes (Phi is lowered, not emitted) plus the AVX2 vector forms the auto-vectorizer emits. + +#### Proven against GNU as + +tools/check_encoder_vs_as.py is the answer to "how do you know your bytes are right?". It enumerates operand shapes, asks the encoder for bytes, assembles the same instruction with GNU as, and compares: not naively, because x86-64 often has multiple valid encodings for one instruction: + +**the comparison, current** +```text +9239 instruction cases +7949 byte-identical +1290 different but valid encodings +0 incorrect encodings + +WRONG = 0 +``` + +The 1,290 "different" cases are where the encoder chose a different-but-equivalent form: a different REX prefix ordering, a displacement width, and the harness decodes both to confirm they mean the same instruction. The only number that matters is WRONG = 0. + +#### VEX and the AVX2 forms + +The auto-vectorizer's slices are byte-for-byte what the encoder emits, verified the same way. The register core of the vector loop, for instance: + +**src/jit/encoder_test.cpp · real expected bytes** +```asm +vmovdqu ymm0, [rbp+rcx*4] ; C4 E1 7E 6F 44 8D 00 +vmovdqu [rbp+r8*4-64], ymm12 ; C4 21 7E 7F 64 85 C0 +``` + +Two details worth pausing on: the [rbp+rcx*4] form is the 4-byte-scaled addressing the 32-bit lanes need, and the second form uses the R8 REX prefix: the encoder handles the full register range, not just the low eight. + +*the vector instruction set the encoder emits* +| vmovdqu | unaligned 256-bit load/store: the lane mover | +| --- | --- | +| vpaddd / vpsubd / vpmulld | the three integer ops the recognizer fuses | +| vpxor | reduction accumulator zeroing | +| vextracti128 + vpshufd | the horizontal halving merge for reductions | +| vzeroupper | emitted before every Call/Return in a VEX function: audited by check_vex_transitions.py | + +> **vzeroupper is not optional** — Skipping it leaves the upper halves of the YMM registers dirty, and the transition penalty into legacy SSE code is brutal. The scanner that audits its placement ships with the encoder tests. + +#### Reading bytes by hand + +A worked example: push rbp, the first byte of every prologue: + +*55 → push rbp* +| 0x55 | one byte, no REX, no ModRM: push of the low half of the register file encodes as a single opcode byte | +| --- | --- | +| RBP is register 5 | the opcode 0x50 + reg gives 0x50 + 5 = 0x55 | + +That is the whole encoding. The interesting instructions: anything with a memory operand: are where REX, ModRM, SIB, and displacement interleave, and that is exactly the surface check_encoder_vs_as.py brute-forces 9,239 ways. + +--- + +## Changelog + +### `!=` is a first-class comparison (2026-10-20) + +!= is now its own IR op (ne) rather than eq followed by not, so the optimizer and branch fusion see the real predicate. On floats it follows IEEE 754: nan != nan is true, 0.0 != -0.0 is false. + +Docs: https://project-lithon.github.io/Lithon/docs/operators#comparison-boolean + +### `try` / `except` over the engine's own traps (2026-10-20) + +A try block can now catch the engine's traps, over a closed set of seven names: ZeroDivisionError, OverflowError, ValueError, IndexError, KeyError, EOFError, and Exception. else and finally are supported. A bare except:, except X as e, and raise are refused. + +Docs: https://project-lithon.github.io/Lithon/docs/exceptions + +### Raw syscalls, iteration helpers, and real pointers (2026-10-14) + +syscall(n, a1..a6) is a compiler-recognized primitive that lowers to one IR op. A pointer argument is a real host address in both tiers, which is what makes the shipped TCP loopback possible. enumerate(xs) and zip(xs, ys) rewrite a counted loop in a for header. + +Docs: https://project-lithon.github.io/Lithon/docs/syscalls + +### Packed-field structs, end to end (2026-10-13) + +class P(Struct) with typed fields, packed with no padding, so the byte layout matches a C #pragma pack(1) struct exactly. Narrow integer fields keep their sign when read back. + +Docs: https://project-lithon.github.io/Lithon/docs/structs + +### Explicit conversions and terminal input (2026-10-12) + +int(w, v), float(64, v) and int(w, input(prompt)) are the only way to change a type or read a value: the width is always named, and a conversion that does not fit traps instead of truncating. + +Docs: https://project-lithon.github.io/Lithon/docs/types#explicit-conversions + +### `break` / `continue` and a three-argument `range` (2026-10-12) + +Loop control flow is supported, with the rotated lowering and multi-exit loop guards it needs. range(start, stop, step) takes a step, and a zero step is refused at compile time and trapped at run time. + +Docs: https://project-lithon.github.io/Lithon/docs/control-flow#break-continue + +### Uninitialized declarations (2026-10-09) + +A declaration with a type and no value introduces the name and the type, emits no code, and starts unassigned. Reading it before assignment is a compile-time refusal, not a run-time surprise. + +Docs: https://project-lithon.github.io/Lithon/docs/declarations#definite-assignment + +### C-style modulo (2026-10-01) + +% is the C remainder: the sign follows the dividend. -7 % 3 is -1 in Lithon and 2 in CPython. This is the machine's behaviour, and it is now pinned by the differential suites. + +Docs: https://project-lithon.github.io/Lithon/docs/semantics#modulo + +### Float is a real type (2026-10-01) + +float[64] is a first-class type with its own arithmetic, not a fallback for an untyped value. A float literal is 0.0, never 0. + +Docs: https://project-lithon.github.io/Lithon/docs/types + +### Lists, tuples, dicts and pointers (2026-10-07) + +The fixed-capacity container family: list[T, N], tuple[T, N], dict[K, V, B] with a power-of-two bucket count, and typed pointers with a mandatory _ prefix. + +Docs: https://project-lithon.github.io/Lithon/docs/containers + +### Dynamic `int[64]` overflow traps (2026-10-10) + +Plain +, - and * on int[64] now trap on signed overflow instead of silently wrapping. wrap_add, wrap_sub and wrap_mul are the explicit opt-out, and the wrapping result is pinned to be identical across tiers. + +Docs: https://project-lithon.github.io/Lithon/docs/operators#wrap-arith + +### Stack-depth guard parity (2026-10-11) + +The native prologue and the interpreter bump the same shared recursion-depth counter and trap at the same depth with the same message, so a runaway recursive program fails byte-identically in both tiers. + +Docs: https://project-lithon.github.io/Lithon/docs/errors + +### Self tail calls become loops (2026-09-30) + +A self tail call is rewritten into a loop, so tail recursion runs in O(1) stack. A call that is not in tail position stays recursive. + +Docs: https://project-lithon.github.io/Lithon/docs/functions#recursion + +### SIMD auto-vectorization (2026-10-08) + +The vectorizer emits AVX2 for counted loops over int[32] lists, 8 elements wide. The result is exact, not approximate: the reduction order is preserved. + +Docs: https://project-lithon.github.io/Lithon/docs/simd + +### Register coalescing (2026-10-03) + +A merge takes over its source's dead register, so a phi copy does not need a fresh one. + +Docs: https://project-lithon.github.io/Lithon/docs/pipeline + +### Float comparisons fuse into the branch (2026-10-12) + +A float comparison is fused into the branch instead of materialising a bool first, so the hot path carries no intermediate value. + +Docs: https://project-lithon.github.io/Lithon/docs/pipeline + +### `.lio` is the source extension (2026-10-20) + +A Lithon program is a file with the .lio extension. The frontend refuses anything else, so the extension is what marks a file as Lithon rather than as ordinary Python. + +Docs: https://project-lithon.github.io/Lithon/docs/installation + +### The `lithon` CLI (2026-10-01) + +lithon script.lio runs a program. --strict refuses rather than falling back to the interpreter, --ir prints the typed IR and stops, --interp forces the correctness oracle, and -v says which tier ran and why. + +Docs: https://project-lithon.github.io/Lithon/docs/installation#cli + +### The full gate, wired together (2026-09-29) + +tools/verify_all.sh runs the whole stack: CTest, the general regression suite, the typed regression suite, the tier differential, and the per-shape differential fuzzers. It ends with ALL CHECKS PASSED or it does not pass. + +Docs: https://project-lithon.github.io/Lithon/docs/verification#the-gate + +### The encoder against GNU as (2026-10-07) + +The hand-written x86-64 encoder is compared instruction by instruction against GNU as. 18,379 cases, 0 incorrect encodings. A byte difference is not automatically an error, because x86-64 often has several valid encodings. + +Docs: https://project-lithon.github.io/Lithon/docs/verification#encoder-vs-as + +### Networking checked against a C oracle (2026-10-14) + +A TCP loopback round trip, built entirely from raw syscalls, is compared against a C program issuing the identical syscalls. The C program is the oracle, because both Lithon tiers could be wrong in the same way. + +Docs: https://project-lithon.github.io/Lithon/docs/networking + +### The documentation platform (2026-10-10) + +Every example on this site is compiled by the real Lithon frontend and pinned to the repo's regression outputs, so a doc that drifts from the engine fails the build rather than misleading a reader. + +Docs: https://project-lithon.github.io/Lithon/docs + +### The site moves to TanStack Start (2026-10-10) + +The hand-rolled static site became a Turborepo monorepo: all ten routes migrated, RTL-first layout, and theme, sound and text-direction toggles that persist. + +Docs: https://project-lithon.github.io/Lithon/ + +### Executable docs (2026-10-10) + +Examples carry their real IR and a step-by-step execution trace, and the machine view toggles every example over to its IR and x86-64 tab. + +Docs: https://project-lithon.github.io/Lithon/docs/source-to-machine + +### The benchmark bot (2026-10-20) + +Mamba measures fib(30) on every push to master, writes benchmarks/results/latest.json and a per-commit snapshot under benchmarks/results/history/, and updates the README's benchmark block. + +Docs: https://project-lithon.github.io/Lithon/benchmarks diff --git a/web/apps/web/public/llms.txt b/web/apps/web/public/llms.txt new file mode 100644 index 0000000..c607147 --- /dev/null +++ b/web/apps/web/public/llms.txt @@ -0,0 +1,112 @@ +# Lithon + +> Lithon is a Python library and native x86-64 execution engine. Every supported program carries fixed-width type annotations, is statically verified before it runs, and executes as generated machine code with no LLVM and no runtime package. + +Lithon is a Python library and native x86-64 execution engine. Source files use the `.lio` extension. Every program is statically verified before it runs; a program the backend cannot prove is refused rather than guessed at. + +## The one rule + +> If Lithon cannot prove that a program satisfies the rules required by its native backend, it refuses native compilation rather than guessing. + +## Type system in one block + +``` +int[8] int[16] int[32] int[64] signed, fixed width +float[64] IEEE-754 binary64 (float[32] is refused) +bool 1 byte, reads back as 0/1 +str[N] N bytes, NUL-terminated, bounded +list[T, N] fixed capacity N +tuple[T, N] fixed length N, homogeneous +dict[K, V, B] B buckets, B must be a power of two +ptr[T] typed pointer, variable name needs a leading _ +class P(Struct) packed fields, no padding +class E(Enum) a tag plus the largest payload +``` + +There is no bare `int`, no `uint`, no `None`, no `lambda`, no closure, no `set`, no `bytes`, no `Optional`/`Union`, no `**`, no `//`, no `<=`/`>=`, and no chained comparison. + +## A small program + +```python +x: int[64] = 10 +y: int[64] = 20 + +result: int[64] = x + y + +print(result) +``` + +## Documentation index + +### Start here + +From zero to a natively running program. + +- [Welcome to Lithon](https://project-lithon.github.io/Lithon/docs/welcome)[essential] — Understand what Lithon is, what it refuses to do, and why the refusal is the feature. +- [Hello, native](https://project-lithon.github.io/Lithon/docs/hello-world)[essential] — Write, verify and run your first natively-executed program end to end. +- [Build & install](https://project-lithon.github.io/Lithon/docs/installation) — Build the engine and prove the install works. +- [The mental model](https://project-lithon.github.io/Lithon/docs/mental-model)[essential] — Hold the one rule that makes the rest of the language checkable: prove before you run. + +### The language + +Types, flow, functions, containers, unsafe memory. + +- [Types & widths](https://project-lithon.github.io/Lithon/docs/types)[essential] — Pick the right width for every value, and know which conversions are legal. +- [Declarations & assignment](https://project-lithon.github.io/Lithon/docs/declarations)[essential] — Give every name a type and a value, and be refused when you have not. +- [Control flow](https://project-lithon.github.io/Lithon/docs/control-flow)[essential] — Write every branch and loop the language supports, and know which ones it does not. +- [Functions](https://project-lithon.github.io/Lithon/docs/functions)[essential] — Give a function a contract the compiler can check, and call it safely. +- [Operators](https://project-lithon.github.io/Lithon/docs/operators)[essential] — Predict the result of every expression, including the ones that surprise CPython. +- [Containers](https://project-lithon.github.io/Lithon/docs/containers)[essential] — Model fixed-size data without a heap, and know each container's rules. +- [Pointers](https://project-lithon.github.io/Lithon/docs/pointers) — Reach raw memory deliberately, with the naming rule that keeps it visible. +- [Structs](https://project-lithon.github.io/Lithon/docs/structs)[essential] — Lay out packed storage whose bytes match a C struct exactly. +- [Enums](https://project-lithon.github.io/Lithon/docs/enums)[essential] — Model a tagged union and read it back safely with match. +- [Methods](https://project-lithon.github.io/Lithon/docs/methods) — Give a struct behaviour, and understand why it is inlined. +- [Type Functions](https://project-lithon.github.io/Lithon/docs/type-functions)[reference] — Build reusable types at compile time. +- [Syscalls](https://project-lithon.github.io/Lithon/docs/syscalls)[reference] — Call the kernel directly, with the argument rules both tiers agree on. +- [Divergences from Python](https://project-lithon.github.io/Lithon/docs/python-divergences)[essential] — Port Python code without being surprised by a silent semantic change. +- [Exceptions & traps](https://project-lithon.github.io/Lithon/docs/exceptions)[essential] — Recover from the seven engine traps and know the rest is refused. +- [match](https://project-lithon.github.io/Lithon/docs/match)[essential] — Dispatch on an enum exhaustively, and read a variant's payload safely. +- [Strings](https://project-lithon.github.io/Lithon/docs/strings)[essential] — Use bounded byte runs without a heap or an allocator. +- [Networking](https://project-lithon.github.io/Lithon/docs/networking)[reference] — Open a real socket from Lithon and pass the byte-order test. + +### Inside the engine + +Dual tiers, verification, SSA, optimization, SIMD. + +- [Engine architecture](https://project-lithon.github.io/Lithon/docs/architecture)[essential] — Trace a program from .lio source to executable memory. +- [Dual-tier execution](https://project-lithon.github.io/Lithon/docs/dual-tier) — Understand what Tier-0 and Tier-1 each do, and when each runs. +- [The type checker](https://project-lithon.github.io/Lithon/docs/typechecking) — Read a diagnostic and know exactly which rule you broke. +- [Optimizer & SSA pipeline](https://project-lithon.github.io/Lithon/docs/pipeline)[reference] — See the optimizer and SSA passes that run before the encoder. +- [SIMD auto-vectorization](https://project-lithon.github.io/Lithon/docs/simd)[reference] — Know when a loop becomes AVX2 and why the result is still exact. + +### Reference + +Errors, builtins, verification gate, honest limits. + +- [Errors & diagnostics](https://project-lithon.github.io/Lithon/docs/errors)[essential] — Look up any error code and what it means. +- [Built-in functions](https://project-lithon.github.io/Lithon/docs/builtins)[essential] — Look up any built-in, its signature and its rules. +- [Semantics](https://project-lithon.github.io/Lithon/docs/semantics)[essential] — Look up the exact meaning of the operations that surprise people. +- [Verification](https://project-lithon.github.io/Lithon/docs/verification) — Check the claims instead of taking them on trust. +- [Honest limits](https://project-lithon.github.io/Lithon/docs/limits)[essential] — Know exactly what is not implemented yet. + +### Machine view + +For enthusiasts: IR, registers, x86-64 encodings. + +- [Source to machine](https://project-lithon.github.io/Lithon/docs/source-to-machine) — Read the IR and the machine code for any example. +- [Registers & ABI](https://project-lithon.github.io/Lithon/docs/registers-abi)[reference] — Know where every argument and return value lives. +- [Instruction encoding](https://project-lithon.github.io/Lithon/docs/instruction-encoding)[reference] — Read the encoder's output byte by byte. + +### Site sections + +- [Changelog](https://project-lithon.github.io/Lithon/changelog) — Every shipped change, newest first. +- [Next actions](https://project-lithon.github.io/Lithon/next) — The open items, item by item. +- [Roadmap](https://project-lithon.github.io/Lithon/roadmap) — The three phases and their milestones. +- [Benchmarks](https://project-lithon.github.io/Lithon/benchmarks) — Recorded CPython vs Lithon runs. +- [Use cases](https://project-lithon.github.io/Lithon/use-cases) — The problems Lithon is shaped for. +- [Playground](https://project-lithon.github.io/Lithon/playground) — Write Lithon in the browser. + +## Optional + +- [Full documentation as plain text](https://project-lithon.github.io/Lithon/llms-full.txt) — every page, every section, every example. +- [The repository](https://github.com/Project-Lithon/Lithon) — the source, the tests, and the verification suites. diff --git a/web/apps/web/public/robots.txt b/web/apps/web/public/robots.txt index da07271..fef1d54 100644 --- a/web/apps/web/public/robots.txt +++ b/web/apps/web/public/robots.txt @@ -2,4 +2,7 @@ User-agent: * Allow: / Disallow: /api/ +# The AI-facing surfaces: a compact index and the full documentation. +# Both are generated from the same registry the site renders. + Sitemap: https://project-lithon.github.io/Lithon/sitemap.xml diff --git a/web/apps/web/public/sitemap.xml b/web/apps/web/public/sitemap.xml index 3e65939..6ef3ed3 100644 --- a/web/apps/web/public/sitemap.xml +++ b/web/apps/web/public/sitemap.xml @@ -2,201 +2,283 @@ https://project-lithon.github.io/Lithon/ + 2026-10-21 weekly 1.0 https://project-lithon.github.io/Lithon/docs + 2026-10-21 weekly 0.9 https://project-lithon.github.io/Lithon/playground + 2026-10-21 weekly 0.7 https://project-lithon.github.io/Lithon/benchmarks + 2026-10-21 weekly 0.7 https://project-lithon.github.io/Lithon/roadmap + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/use-cases + 2026-10-21 weekly 0.6 + + https://project-lithon.github.io/Lithon/changelog + 2026-10-21 + weekly + 0.7 + + + https://project-lithon.github.io/Lithon/next + 2026-10-21 + weekly + 0.7 + https://project-lithon.github.io/Lithon/roadmap/language + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/roadmap/phase-1-dual-tier + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/roadmap/phase-2-aot + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/roadmap/phase-3-systems + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/roadmap/verification + 2026-10-21 weekly 0.6 https://project-lithon.github.io/Lithon/docs/welcome + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/hello-world + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/installation + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/mental-model + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/types + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/declarations + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/control-flow + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/functions + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/operators + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/containers + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/pointers + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/structs + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/enums + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/methods + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/type-functions + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/syscalls + 2026-10-21 + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/python-divergences + 2026-10-21 + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/exceptions + 2026-10-21 + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/match + 2026-10-21 + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/strings + 2026-10-21 + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/networking + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/architecture + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/dual-tier + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/typechecking + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/pipeline + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/simd + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/errors + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/builtins + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/semantics + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/verification + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/limits + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/source-to-machine + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/registers-abi + 2026-10-21 weekly 0.8 https://project-lithon.github.io/Lithon/docs/instruction-encoding + 2026-10-21 weekly 0.8 diff --git a/web/apps/web/src/changelog/data.ts b/web/apps/web/src/changelog/data.ts new file mode 100644 index 0000000..4ba7bcc --- /dev/null +++ b/web/apps/web/src/changelog/data.ts @@ -0,0 +1,300 @@ +/** + * The changelog, generated from the repository's own commit history. + * + * There are no release tags yet, so the changelog is built from `git log`. + * The grouping is by the feature the commit introduced, not by a hand-kept + * list, so a new commit shows up without anyone remembering to edit a file. + */ + +export type ChangeKind = + | "language" + | "engine" + | "toolchain" + | "verification" + | "docs" + | "site" + | "build" + +export type Change = { + kind: ChangeKind + title: string + detail: string + /** The commit short hash, when the entry came from the history. */ + commit?: string + /** The date the change landed. */ + date?: string + href?: string +} + +export const KIND_META: Record< + ChangeKind, + { label: string; chip: string; order: number } +> = { + language: { + label: "Language", + chip: "border-accent-strong/50 text-accent-strong", + order: 0, + }, + engine: { + label: "Engine", + chip: "border-primary/50 text-primary-foreground dark:text-primary", + order: 1, + }, + toolchain: { + label: "Toolchain", + chip: "text-muted-foreground", + order: 2, + }, + verification: { + label: "Verification", + chip: "border-emerald-600/50 text-emerald-600 dark:text-emerald-400", + order: 3, + }, + docs: { + label: "Documentation", + chip: "text-muted-foreground", + order: 4, + }, + site: { + label: "Website", + chip: "text-muted-foreground", + order: 5, + }, + build: { + label: "Build & CI", + chip: "text-muted-foreground", + order: 6, + }, +} + +/** + * The changes, newest first. The order is the reading order; each entry + * points at the documentation that now covers it. + */ +export const CHANGES: readonly Change[] = [ + { + kind: "language", + title: "`!=` is a first-class comparison", + detail: + "`!=` is now its own IR op (`ne`) rather than `eq` followed by `not`, so the optimizer and branch fusion see the real predicate. On floats it follows IEEE 754: `nan != nan` is true, `0.0 != -0.0` is false.", + commit: "6003463", + date: "2026-10-20", + href: "/docs/operators#comparison-boolean", + }, + { + kind: "language", + title: "`try` / `except` over the engine's own traps", + detail: + "A `try` block can now catch the engine's traps, over a closed set of seven names: `ZeroDivisionError`, `OverflowError`, `ValueError`, `IndexError`, `KeyError`, `EOFError`, and `Exception`. `else` and `finally` are supported. A bare `except:`, `except X as e`, and `raise` are refused.", + commit: "6003463", + date: "2026-10-20", + href: "/docs/exceptions", + }, + { + kind: "language", + title: "Raw syscalls, iteration helpers, and real pointers", + detail: + "`syscall(n, a1..a6)` is a compiler-recognized primitive that lowers to one IR op. A pointer argument is a real host address in both tiers, which is what makes the shipped TCP loopback possible. `enumerate(xs)` and `zip(xs, ys)` rewrite a counted loop in a `for` header.", + commit: "ed0fefa", + date: "2026-10-14", + href: "/docs/syscalls", + }, + { + kind: "language", + title: "Packed-field structs, end to end", + detail: + "`class P(Struct)` with typed fields, packed with no padding, so the byte layout matches a C `#pragma pack(1)` struct exactly. Narrow integer fields keep their sign when read back.", + commit: "7b40f2d", + date: "2026-10-13", + href: "/docs/structs", + }, + { + kind: "language", + title: "Explicit conversions and terminal input", + detail: + "`int(w, v)`, `float(64, v)` and `int(w, input(prompt))` are the only way to change a type or read a value: the width is always named, and a conversion that does not fit traps instead of truncating.", + commit: "2e7bf73", + date: "2026-10-12", + href: "/docs/types#explicit-conversions", + }, + { + kind: "language", + title: "`break` / `continue` and a three-argument `range`", + detail: + "Loop control flow is supported, with the rotated lowering and multi-exit loop guards it needs. `range(start, stop, step)` takes a step, and a zero step is refused at compile time and trapped at run time.", + commit: "322fd90", + date: "2026-10-12", + href: "/docs/control-flow#break-continue", + }, + { + kind: "language", + title: "Uninitialized declarations", + detail: + "A declaration with a type and no value introduces the name and the type, emits no code, and starts unassigned. Reading it before assignment is a compile-time refusal, not a run-time surprise.", + commit: "2454bdf", + date: "2026-10-09", + href: "/docs/declarations#definite-assignment", + }, + { + kind: "language", + title: "C-style modulo", + detail: + "`%` is the C remainder: the sign follows the dividend. `-7 % 3` is `-1` in Lithon and `2` in CPython. This is the machine's behaviour, and it is now pinned by the differential suites.", + commit: "e0b5e0c", + date: "2026-10-01", + href: "/docs/semantics#modulo", + }, + { + kind: "language", + title: "Float is a real type", + detail: + "`float[64]` is a first-class type with its own arithmetic, not a fallback for an untyped value. A float literal is `0.0`, never `0`.", + commit: "61aec93", + date: "2026-10-01", + href: "/docs/types", + }, + { + kind: "language", + title: "Lists, tuples, dicts and pointers", + detail: + "The fixed-capacity container family: `list[T, N]`, `tuple[T, N]`, `dict[K, V, B]` with a power-of-two bucket count, and typed pointers with a mandatory `_` prefix.", + commit: "8eff192", + date: "2026-10-07", + href: "/docs/containers", + }, + { + kind: "engine", + title: "Dynamic `int[64]` overflow traps", + detail: + "Plain `+`, `-` and `*` on `int[64]` now trap on signed overflow instead of silently wrapping. `wrap_add`, `wrap_sub` and `wrap_mul` are the explicit opt-out, and the wrapping result is pinned to be identical across tiers.", + commit: "5326537", + date: "2026-10-10", + href: "/docs/operators#wrap-arith", + }, + { + kind: "engine", + title: "Stack-depth guard parity", + detail: + "The native prologue and the interpreter bump the same shared recursion-depth counter and trap at the same depth with the same message, so a runaway recursive program fails byte-identically in both tiers.", + commit: "8954151", + date: "2026-10-11", + href: "/docs/errors", + }, + { + kind: "engine", + title: "Self tail calls become loops", + detail: + "A self tail call is rewritten into a loop, so tail recursion runs in O(1) stack. A call that is not in tail position stays recursive.", + commit: "144daa5", + date: "2026-09-30", + href: "/docs/functions#recursion", + }, + { + kind: "engine", + title: "SIMD auto-vectorization", + detail: + "The vectorizer emits AVX2 for counted loops over `int[32]` lists, 8 elements wide. The result is exact, not approximate: the reduction order is preserved.", + commit: "7214631", + date: "2026-10-08", + href: "/docs/simd", + }, + { + kind: "engine", + title: "Register coalescing", + detail: + "A merge takes over its source's dead register, so a phi copy does not need a fresh one.", + commit: "801be23", + date: "2026-10-03", + href: "/docs/pipeline", + }, + { + kind: "engine", + title: "Float comparisons fuse into the branch", + detail: + "A float comparison is fused into the branch instead of materialising a `bool` first, so the hot path carries no intermediate value.", + commit: "5a06b72", + date: "2026-10-12", + href: "/docs/pipeline", + }, + { + kind: "toolchain", + title: "`.lio` is the source extension", + detail: + "A Lithon program is a file with the `.lio` extension. The frontend refuses anything else, so the extension is what marks a file as Lithon rather than as ordinary Python.", + commit: "6cbc856", + date: "2026-10-20", + href: "/docs/installation", + }, + { + kind: "toolchain", + title: "The `lithon` CLI", + detail: + "`lithon script.lio` runs a program. `--strict` refuses rather than falling back to the interpreter, `--ir` prints the typed IR and stops, `--interp` forces the correctness oracle, and `-v` says which tier ran and why.", + commit: "f62bf61", + date: "2026-10-01", + href: "/docs/installation#cli", + }, + { + kind: "verification", + title: "The full gate, wired together", + detail: + "`tools/verify_all.sh` runs the whole stack: CTest, the general regression suite, the typed regression suite, the tier differential, and the per-shape differential fuzzers. It ends with `ALL CHECKS PASSED` or it does not pass.", + commit: "988e763", + date: "2026-09-29", + href: "/docs/verification#the-gate", + }, + { + kind: "verification", + title: "The encoder against GNU as", + detail: + "The hand-written x86-64 encoder is compared instruction by instruction against GNU `as`. 18,379 cases, 0 incorrect encodings. A byte difference is not automatically an error, because x86-64 often has several valid encodings.", + commit: "edf4937", + date: "2026-10-07", + href: "/docs/verification#encoder-vs-as", + }, + { + kind: "verification", + title: "Networking checked against a C oracle", + detail: + "A TCP loopback round trip, built entirely from raw syscalls, is compared against a C program issuing the identical syscalls. The C program is the oracle, because both Lithon tiers could be wrong in the same way.", + commit: "ed0fefa", + date: "2026-10-14", + href: "/docs/networking", + }, + { + kind: "docs", + title: "The documentation platform", + detail: + "Every example on this site is compiled by the real Lithon frontend and pinned to the repo's regression outputs, so a doc that drifts from the engine fails the build rather than misleading a reader.", + commit: "427a9d4", + date: "2026-10-10", + href: "/docs", + }, + { + kind: "site", + title: "The site moves to TanStack Start", + detail: + "The hand-rolled static site became a Turborepo monorepo: all ten routes migrated, RTL-first layout, and theme, sound and text-direction toggles that persist.", + commit: "b540731", + date: "2026-10-10", + href: "/", + }, + { + kind: "site", + title: "Executable docs", + detail: + "Examples carry their real IR and a step-by-step execution trace, and the machine view toggles every example over to its IR and x86-64 tab.", + commit: "b71d5b6", + date: "2026-10-10", + href: "/docs/source-to-machine", + }, + { + kind: "build", + title: "The benchmark bot", + detail: + "Mamba measures `fib(30)` on every push to `master`, writes `benchmarks/results/latest.json` and a per-commit snapshot under `benchmarks/results/history/`, and updates the README's benchmark block.", + commit: "2b185ad", + date: "2026-10-20", + href: "/benchmarks", + }, +] diff --git a/web/apps/web/src/docs/generated/ir.ts b/web/apps/web/src/docs/generated/ir.ts index 5e3052f..e35fae3 100644 --- a/web/apps/web/src/docs/generated/ir.ts +++ b/web/apps/web/src/docs/generated/ir.ts @@ -2,52 +2,78 @@ // Every entry is real output from src/frontend/frontend.py. export const EXAMPLE_IR: Record = { - "89052530": "function __main__():\r\nblock0:\r\n store v : struct[16]\r\n %0 = const_f64 1.5\r\n fieldstore v, 0, %0 : float[64]\r\n %1 = const_f64 -2.25\r\n fieldstore v, 8, %1 : float[64]\r\n %2 = fieldload v, 0 : float[64]\r\n call print_bare, %2\r\n call print_nl\r\n %3 = fieldload v, 8 : float[64]\r\n call print_bare, %3\r\n call print_nl\r\n %4 = fieldload v, 0 : float[64]\r\n %5 = fieldload v, 8 : float[64]\r\n %6 = add %4, %5\r\n call print_bare, %6\r\n call print_nl\r\n return", - "47db5edf": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store x, %0 : int[64]\r\n %1 = const_i64 20\r\n store y, %1 : int[64]\r\n %2 = load x\r\n %3 = load y\r\n %4 = add %2, %3\r\n store result, %4 : int[64]\r\n %5 = load result\r\n call print_bare, %5\r\n call print_nl\r\n return", - "39ae587c": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store x, %0 : int[64]\r\n %1 = const_i64 20\r\n store y, %1 : int[64]\r\n %2 = load x\r\n %3 = load y\r\n %4 = add %2, %3\r\n store z, %4 : int[64]\r\n %5 = load z\r\n call print_bare, %5\r\n call print_nl\r\n %6 = load x\r\n %7 = load y\r\n %8 = sub %6, %7\r\n call print_bare, %8\r\n call print_nl\r\n %9 = load x\r\n %10 = load y\r\n %11 = mul %9, %10\r\n call print_bare, %11\r\n call print_nl\r\n return", - "cac171bf": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store x, %0\r\n %1 = load x\r\n call print_bare, %1\r\n call print_nl\r\n return", - "2a8807c9": "function __main__():\r\nblock0:\r\n %0 = const_i64 7\r\n store i8, %0 : int[8]\r\n %1 = const_i64 7\r\n store i16, %1 : int[16]\r\n %2 = const_i64 7\r\n store i32, %2 : int[32]\r\n %3 = const_i64 7\r\n store i64, %3 : int[64]\r\n %4 = const_f64 0.5\r\n store f64, %4 : float[64]\r\n %5 = const_bool 1\r\n store b, %5 : bool\r\n %6 = load i64\r\n call print_bare, %6\r\n call print_nl\r\n return", - "7cbe74a6": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store small, %0 : int[8]\r\n %1 = load small\r\n store big, %1 : int[64]\r\n %2 = load big\r\n call print_bare, %2\r\n call print_nl\r\n return", - "206f6bf0": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store wide, %0 : int[64]\r\n %1 = load wide\r\n store narrow, %1 : int[8]\r\n %2 = load narrow\r\n call print_bare, %2\r\n call print_nl\r\n return", - "1a324420": "function __main__():\r\nblock0:\r\n %0 = const_i64 0\r\n store j, %0 : float[64]\r\n %1 = load j\r\n call print_bare, %1\r\n call print_nl\r\n return", - "aeb1dcc6": "function __main__():\r\nblock0:\r\n %0 = const_f64 0.0\r\n store j, %0 : float[64]\r\n %1 = load j\r\n call print_bare, %1\r\n call print_nl\r\n return", - "96262f13": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store i, %0 : int[64]\r\n %1 = const_f64 2.5\r\n store f, %1 : float[64]\r\n %2 = load i\r\n %3 = load f\r\n %4 = add %2, %3\r\n call print_bare, %4\r\n call print_nl\r\n %5 = load f\r\n %6 = load i\r\n %7 = add %5, %6\r\n call print_bare, %7\r\n call print_nl\r\n %8 = load i\r\n %9 = const_i64 4\r\n %10 = div %8, %9\r\n call print_bare, %10\r\n call print_nl\r\n return", - "c2b675d3": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store x, %0 : int[64]\r\n %1 = const_i64 20\r\n store x, %1 : int[64]\r\n %2 = load x\r\n call print_bare, %2\r\n call print_nl\r\n return", - "a78d2c85": "function __main__():\r\nblock0:\r\n %0 = const_i64 10\r\n store x, %0 : int[64]\r\n %1 = const_i64 20\r\n store x, %1 : int[64]\r\n %2 = load x\r\n store narrow, %2 : int[8]\r\n %3 = load narrow\r\n call print_bare, %3\r\n call print_nl\r\n return", - "e1f84f23": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store i, %0 : int[8]\r\n %1 = load i\r\n call print_bare, %1\r\n call print_nl\r\n return", - "bba9661c": "function __main__():\r\nblock0:\r\n %0 = load i\r\n call print_bare, %0\r\n call print_nl\r\n return", - "f00439de": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n store x, %0 : int[64]\r\n %1 = load x\r\n %2 = const_i64 3\r\n %3 = lt %1, %2\r\n branch %3, block1, block2\r\nblock1:\r\n %4 = const_i64 2\r\n store y, %4 : int[64]\r\n jump block2\r\nblock2:\r\n %5 = load y\r\n call print_bare, %5\r\n call print_nl\r\n return", - "9443a6ad": "function __main__():\r\nblock0:\r\n %0 = const_i64 0\r\n store i, %0 : int[64]\r\n %1 = const_i64 0\r\n store total, %1 : int[64]\r\n jump block1\r\nblock1:\r\n %2 = load i\r\n %3 = const_i64 10\r\n %4 = lt %2, %3\r\n branch %4, block2, block3\r\nblock2:\r\n %5 = load total\r\n %6 = load i\r\n %7 = add %5, %6\r\n store total, %7 : int[64]\r\n %8 = load i\r\n %9 = load i\r\n %10 = const_i64 1\r\n %11 = add %9, %10\r\n store i, %11 : int[64]\r\n jump block1\r\nblock3:\r\n %12 = load total\r\n call print_bare, %12\r\n call print_nl\r\n return", - "06ab6407": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n %1 = const_i64 10\r\n %2 = const_i64 3\r\n store i, %0 : int[64]\r\n %3 = const_i64 0\r\n %4 = gt %2, %3\r\n branch %4, block4, block5\r\nblock4:\r\n %5 = lt %0, %1\r\n branch %5, block1, block2\r\nblock5:\r\n %6 = gt %0, %1\r\n branch %6, block1, block2\r\nblock1:\r\n %7 = load i\r\n call print_bare, %7\r\n call print_nl\r\n jump block3\r\nblock3:\r\n %8 = load i\r\n branch %4, block6, block7\r\nblock6:\r\n %9 = const_i64 -9223372036854775808\r\n %10 = wrapsub %1, %8\r\n %11 = bxor %10, %9\r\n %12 = bxor %2, %9\r\n %13 = gt %11, %12\r\n branch %13, block8, block2\r\nblock7:\r\n %14 = const_i64 -9223372036854775808\r\n %15 = wrapsub %8, %1\r\n %16 = sub %3, %2\r\n %17 = bxor %15, %14\r\n %18 = bxor %16, %14\r\n %19 = gt %17, %18\r\n branch %19, block8, block2\r\nblock8:\r\n %20 = load i\r\n %21 = add %20, %2\r\n store i, %21\r\n jump block1\r\nblock2:\r\n %22 = const_i64 10\r\n %23 = const_i64 1\r\n %24 = const_i64 -3\r\n store i, %22 : int[64]\r\n guardnonzero %24\r\n %25 = const_i64 0\r\n %26 = gt %24, %25\r\n branch %26, block12, block13\r\nblock12:\r\n %27 = lt %22, %23\r\n branch %27, block9, block10\r\nblock13:\r\n %28 = gt %22, %23\r\n branch %28, block9, block10\r\nblock9:\r\n %29 = load i\r\n call print_bare, %29\r\n call print_nl\r\n jump block11\r\nblock11:\r\n %30 = load i\r\n branch %26, block14, block15\r\nblock14:\r\n %31 = const_i64 -9223372036854775808\r\n %32 = wrapsub %23, %30\r\n %33 = bxor %32, %31\r\n %34 = bxor %24, %31\r\n %35 = gt %33, %34\r\n branch %35, block16, block10\r\nblock15:\r\n %36 = const_i64 -9223372036854775808\r\n %37 = wrapsub %30, %23\r\n %38 = sub %25, %24\r\n %39 = bxor %37, %36\r\n %40 = bxor %38, %36\r\n %41 = gt %39, %40\r\n branch %41, block16, block10\r\nblock16:\r\n %42 = load i\r\n %43 = add %42, %24\r\n store i, %43\r\n jump block9\r\nblock10:\r\n %44 = const_i64 0\r\n %45 = const_i64 5\r\n %46 = const_i64 1\r\n store i, %44 : int[64]\r\n %47 = const_i64 0\r\n %48 = gt %46, %47\r\n branch %48, block20, block21\r\nblock20:\r\n %49 = lt %44, %45\r\n branch %49, block17, block18\r\nblock21:\r\n %50 = gt %44, %45\r\n branch %50, block17, block18\r\nblock17:\r\n %51 = load i\r\n call print_bare, %51\r\n call print_nl\r\n jump block19\r\nblock19:\r\n %52 = load i\r\n branch %48, block22, block23\r\nblock22:\r\n %53 = const_i64 -9223372036854775808\r\n %54 = wrapsub %45, %52\r\n %55 = bxor %54, %53\r\n %56 = bxor %46, %53\r\n %57 = gt %55, %56\r\n branch %57, block24, block18\r\nblock23:\r\n %58 = const_i64 -9223372036854775808\r\n %59 = wrapsub %52, %45\r\n %60 = sub %47, %46\r\n %61 = bxor %59, %58\r\n %62 = bxor %60, %58\r\n %63 = gt %61, %62\r\n branch %63, block24, block18\r\nblock24:\r\n %64 = load i\r\n %65 = add %64, %46\r\n store i, %65\r\n jump block17\r\nblock18:\r\n %66 = const_i64 5\r\n %67 = const_i64 5\r\n %68 = const_i64 1\r\n store i, %66 : int[64]\r\n %69 = const_i64 0\r\n %70 = gt %68, %69\r\n branch %70, block28, block29\r\nblock28:\r\n %71 = lt %66, %67\r\n branch %71, block25, block26\r\nblock29:\r\n %72 = gt %66, %67\r\n branch %72, block25, block26\r\nblock25:\r\n %73 = load i\r\n call print_bare, %73\r\n call print_nl\r\n jump block27\r\nblock27:\r\n %74 = load i\r\n branch %70, block30, block31\r\nblock30:\r\n %75 = const_i64 -9223372036854775808\r\n %76 = wrapsub %67, %74\r\n %77 = bxor %76, %75\r\n %78 = bxor %68, %75\r\n %79 = gt %77, %78\r\n branch %79, block32, block26\r\nblock31:\r\n %80 = const_i64 -9223372036854775808\r\n %81 = wrapsub %74, %67\r\n %82 = sub %69, %68\r\n %83 = bxor %81, %80\r\n %84 = bxor %82, %80\r\n %85 = gt %83, %84\r\n branch %85, block32, block26\r\nblock32:\r\n %86 = load i\r\n %87 = add %86, %68\r\n store i, %87\r\n jump block25\r\nblock26:\r\n %88 = const_i64 1\r\n %89 = const_i64 5\r\n %90 = const_i64 6\r\n store i, %88 : int[64]\r\n %91 = const_i64 0\r\n %92 = gt %90, %91\r\n branch %92, block36, block37\r\nblock36:\r\n %93 = lt %88, %89\r\n branch %93, block33, block34\r\nblock37:\r\n %94 = gt %88, %89\r\n branch %94, block33, block34\r\nblock33:\r\n %95 = load i\r\n call print_bare, %95\r\n call print_nl\r\n jump block35\r\nblock35:\r\n %96 = load i\r\n branch %92, block38, block39\r\nblock38:\r\n %97 = const_i64 -9223372036854775808\r\n %98 = wrapsub %89, %96\r\n %99 = bxor %98, %97\r\n %100 = bxor %90, %97\r\n %101 = gt %99, %100\r\n branch %101, block40, block34\r\nblock39:\r\n %102 = const_i64 -9223372036854775808\r\n %103 = wrapsub %96, %89\r\n %104 = sub %91, %90\r\n %105 = bxor %103, %102\r\n %106 = bxor %104, %102\r\n %107 = gt %105, %106\r\n branch %107, block40, block34\r\nblock40:\r\n %108 = load i\r\n %109 = add %108, %90\r\n store i, %109\r\n jump block33\r\nblock34:\r\n %110 = const_i64 -5\r\n %111 = const_i64 -1\r\n %112 = const_i64 1\r\n store i, %110 : int[64]\r\n %113 = const_i64 0\r\n %114 = gt %112, %113\r\n branch %114, block44, block45\r\nblock44:\r\n %115 = lt %110, %111\r\n branch %115, block41, block42\r\nblock45:\r\n %116 = gt %110, %111\r\n branch %116, block41, block42\r\nblock41:\r\n %117 = load i\r\n call print_bare, %117\r\n call print_nl\r\n jump block43\r\nblock43:\r\n %118 = load i\r\n branch %114, block46, block47\r\nblock46:\r\n %119 = const_i64 -9223372036854775808\r\n %120 = wrapsub %111, %118\r\n %121 = bxor %120, %119\r\n %122 = bxor %112, %119\r\n %123 = gt %121, %122\r\n branch %123, block48, block42\r\nblock47:\r\n %124 = const_i64 -9223372036854775808\r\n %125 = wrapsub %118, %111\r\n %126 = sub %113, %112\r\n %127 = bxor %125, %124\r\n %128 = bxor %126, %124\r\n %129 = gt %127, %128\r\n branch %129, block48, block42\r\nblock48:\r\n %130 = load i\r\n %131 = add %130, %112\r\n store i, %131\r\n jump block41\r\nblock42:\r\n %132 = const_i64 -1\r\n %133 = const_i64 -5\r\n %134 = const_i64 -2\r\n store i, %132 : int[64]\r\n guardnonzero %134\r\n %135 = const_i64 0\r\n %136 = gt %134, %135\r\n branch %136, block52, block53\r\nblock52:\r\n %137 = lt %132, %133\r\n branch %137, block49, block50\r\nblock53:\r\n %138 = gt %132, %133\r\n branch %138, block49, block50\r\nblock49:\r\n %139 = load i\r\n call print_bare, %139\r\n call print_nl\r\n jump block51\r\nblock51:\r\n %140 = load i\r\n branch %136, block54, block55\r\nblock54:\r\n %141 = const_i64 -9223372036854775808\r\n %142 = wrapsub %133, %140\r\n %143 = bxor %142, %141\r\n %144 = bxor %134, %141\r\n %145 = gt %143, %144\r\n branch %145, block56, block50\r\nblock55:\r\n %146 = const_i64 -9223372036854775808\r\n %147 = wrapsub %140, %133\r\n %148 = sub %135, %134\r\n %149 = bxor %147, %146\r\n %150 = bxor %148, %146\r\n %151 = gt %149, %150\r\n branch %151, block56, block50\r\nblock56:\r\n %152 = load i\r\n %153 = add %152, %134\r\n store i, %153\r\n jump block49\r\nblock50:\r\n return", - "bd68f7e0": "function __main__():\r\nblock0:\r\n %0 = const_i64 0\r\n store total, %0 : int[64]\r\n %1 = const_i64 0\r\n %2 = const_i64 100\r\n %3 = const_i64 1\r\n store i, %1 : int[64]\r\n jump block4\r\nblock4:\r\n %4 = load i\r\n %5 = lt %4, %2\r\n branch %5, block1, block2\r\nblock1:\r\n %6 = load i\r\n %7 = const_i64 5\r\n %8 = eq %6, %7\r\n branch %8, block5, block6\r\nblock5:\r\n jump block3\r\nblock6:\r\n %9 = load i\r\n %10 = const_i64 10\r\n %11 = eq %9, %10\r\n branch %11, block7, block8\r\nblock7:\r\n jump block2\r\nblock8:\r\n %12 = load total\r\n %13 = load i\r\n %14 = add %12, %13\r\n store total, %14 : int[64]\r\n jump block3\r\nblock3:\r\n %15 = load i\r\n %16 = const_i64 1\r\n %17 = add %15, %16\r\n store i, %17\r\n jump block4\r\nblock2:\r\n %18 = load total\r\n call print_bare, %18\r\n call print_nl\r\n return", - "4598471d": "function __main__():\r\nblock0:\r\n store xs : list[int[64], 5]\r\n %0 = const_i64 5\r\n store xs.__len, %0 : int[64]\r\n %1 = const_i64 0\r\n %2 = const_i64 10\r\n IndexStore xs, %1, %2\r\n %3 = const_i64 1\r\n %4 = const_i64 20\r\n IndexStore xs, %3, %4\r\n %5 = const_i64 2\r\n %6 = const_i64 30\r\n IndexStore xs, %5, %6\r\n %7 = const_i64 3\r\n %8 = const_i64 40\r\n IndexStore xs, %7, %8\r\n %9 = const_i64 4\r\n %10 = const_i64 50\r\n IndexStore xs, %9, %10\r\n %11 = const_i64 0\r\n %12 = const_i64 5\r\n %13 = const_i64 1\r\n store ei, %11 : int[64]\r\n jump block4\r\nblock4:\r\n %14 = load ei\r\n %15 = lt %14, %12\r\n branch %15, block1, block2\r\nblock1:\r\n %16 = load ei\r\n %17 = Index xs, %16\r\n store ex, %17 : int[64]\r\n %18 = load ei\r\n call print_bare, %18\r\n call print_nl\r\n %19 = load ex\r\n call print_bare, %19\r\n call print_nl\r\n jump block3\r\nblock3:\r\n %20 = load ei\r\n %21 = const_i64 1\r\n %22 = add %20, %21\r\n store ei, %22\r\n jump block4\r\nblock2:\r\n store long6 : list[int[64], 6]\r\n %23 = const_i64 6\r\n store long6.__len, %23 : int[64]\r\n store short3 : list[int[64], 3]\r\n %24 = const_i64 3\r\n store short3.__len, %24 : int[64]\r\n %25 = const_i64 0\r\n %26 = const_i64 1\r\n IndexStore long6, %25, %26\r\n %27 = const_i64 1\r\n %28 = const_i64 2\r\n IndexStore long6, %27, %28\r\n %29 = const_i64 2\r\n %30 = const_i64 3\r\n IndexStore long6, %29, %30\r\n %31 = const_i64 0\r\n %32 = const_i64 7\r\n IndexStore short3, %31, %32\r\n %33 = const_i64 1\r\n %34 = const_i64 8\r\n IndexStore short3, %33, %34\r\n %35 = const_i64 2\r\n %36 = const_i64 9\r\n IndexStore short3, %35, %36\r\n %37 = const_i64 0\r\n store total, %37 : int[64]\r\n %38 = const_i64 0\r\n %39 = const_i64 3\r\n %40 = const_i64 1\r\n store zip_i0, %38 : int[64]\r\n jump block8\r\nblock8:\r\n %41 = load zip_i0\r\n %42 = lt %41, %39\r\n branch %42, block5, block6\r\nblock5:\r\n %43 = load zip_i0\r\n %44 = Index long6, %43\r\n store za, %44 : int[64]\r\n %45 = Index short3, %43\r\n store zb, %45 : int[64]\r\n %46 = load za\r\n %47 = load zb\r\n %48 = add %46, %47\r\n store total, %48 : int[64]\r\n jump block7\r\nblock7:\r\n %49 = load zip_i0\r\n %50 = const_i64 1\r\n %51 = add %49, %50\r\n store zip_i0, %51\r\n jump block8\r\nblock6:\r\n %52 = load total\r\n call print_bare, %52\r\n call print_nl\r\n return", - "311879c3": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store x, %0 : int[64]\r\n %1 = const_i64 2\r\n store y, %1 : int[64]\r\n %2 = load x\r\n %3 = load y\r\n %4 = gt %2, %3\r\n branch %4, block1, block2\r\nblock1:\r\n %5 = load x\r\n store __ifexpr0, %5\r\n jump block3\r\nblock2:\r\n %6 = load y\r\n store __ifexpr0, %6\r\n jump block3\r\nblock3:\r\n %7 = load __ifexpr0\r\n call print_bare, %7\r\n call print_nl\r\n %8 = load x\r\n %9 = load y\r\n %10 = lt %8, %9\r\n branch %10, block4, block5\r\nblock4:\r\n %11 = const_i64 1\r\n store __ifexpr1, %11\r\n jump block6\r\nblock5:\r\n %12 = load x\r\n %13 = const_i64 5\r\n %14 = eq %12, %13\r\n branch %14, block7, block8\r\nblock7:\r\n %15 = const_i64 2\r\n store __ifexpr2, %15\r\n jump block9\r\nblock8:\r\n %16 = const_i64 3\r\n store __ifexpr2, %16\r\n jump block9\r\nblock9:\r\n %17 = load __ifexpr2\r\n store __ifexpr1, %17\r\n jump block6\r\nblock6:\r\n %18 = load __ifexpr1\r\n call print_bare, %18\r\n call print_nl\r\n return", - "ace9c7cc": "function add(a:int[64], b:int[64]):\r\nblock0:\r\n %0 = load a\r\n %1 = load b\r\n %2 = add %0, %1\r\n return %2\r\n\r\nfunction __main__():\r\nblock0:\r\n %0 = const_i64 3\r\n %1 = const_i64 4\r\n %2 = call add, %0, %1\r\n store x, %2 : int[64]\r\n %3 = load x\r\n call print_bare, %3\r\n call print_nl\r\n return", - "8c105dca": "function __main__():\r\nblock0:\r\n %0 = const_i64 -7\r\n store a, %0 : int[64]\r\n %1 = const_i64 3\r\n store b, %1 : int[64]\r\n %2 = load a\r\n %3 = load b\r\n %4 = mod %2, %3\r\n call print_bare, %4\r\n call print_nl\r\n %5 = const_i64 7\r\n %6 = const_i64 3\r\n %7 = mod %5, %6\r\n call print_bare, %7\r\n call print_nl\r\n %8 = const_i64 -7\r\n %9 = const_i64 3\r\n %10 = mod %8, %9\r\n call print_bare, %10\r\n call print_nl\r\n return", - "c6fd2aba": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n store a, %0 : int[64]\r\n %1 = const_i64 2\r\n store b, %1 : int[64]\r\n %2 = const_f64 1.5\r\n store c, %2 : float[64]\r\n %3 = load a\r\n %4 = load b\r\n %5 = lt %3, %4\r\n call print_bare, %5\r\n call print_nl\r\n %6 = load b\r\n %7 = load a\r\n %8 = gt %6, %7\r\n call print_bare, %8\r\n call print_nl\r\n %9 = load a\r\n %10 = load a\r\n %11 = eq %9, %10\r\n call print_bare, %11\r\n call print_nl\r\n %12 = load c\r\n %13 = load b\r\n %14 = lt %12, %13\r\n call print_bare, %14\r\n call print_nl\r\n return", - "2c1f3e5c": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n store a, %0 : int[64]\r\n %1 = const_i64 2\r\n store b, %1 : int[64]\r\n %2 = load a\r\n %3 = load b\r\n %4 = ne %2, %3\r\n call print_bare, %4\r\n call print_nl\r\n %5 = load a\r\n %6 = load a\r\n %7 = ne %5, %6\r\n call print_bare, %7\r\n call print_nl\r\n %8 = const_i64 -128\r\n store lo, %8 : int[8]\r\n %9 = const_i64 127\r\n store hi, %9 : int[8]\r\n %10 = load lo\r\n %11 = load hi\r\n %12 = ne %10, %11\r\n call print_bare, %12\r\n call print_nl\r\n return", - "5ba53d35": "function __main__():\r\nblock0:\r\n %0 = const_f64 2.5\r\n %1 = const_i64 1\r\n %2 = band %0, %1\r\n call print_bare, %2\r\n call print_nl\r\n return", - "f8490a90": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n %1 = const_i64 64\r\n %2 = shl %0, %1\r\n call print_bare, %2\r\n call print_nl\r\n return", - "349eae74": "function __main__():\r\nblock0:\r\n %0 = const_i64 1\r\n %1 = const_i64 10\r\n %2 = shl %0, %1\r\n store x, %2 : int[64]\r\n %3 = load x\r\n %4 = const_i64 3\r\n %5 = shr %3, %4\r\n store y, %5 : int[64]\r\n %6 = load x\r\n %7 = const_i64 255\r\n %8 = band %6, %7\r\n store z, %8 : int[64]\r\n %9 = load x\r\n call print_bare, %9\r\n call print_nl\r\n %10 = load y\r\n call print_bare, %10\r\n call print_nl\r\n %11 = load z\r\n call print_bare, %11\r\n call print_nl\r\n return", - "6666db5c": "function __main__():\r\nblock0:\r\n %0 = const_i64 9223372036854775807\r\n store x, %0 : int[64]\r\n %1 = const_i64 1\r\n store y, %1 : int[64]\r\n %2 = load x\r\n %3 = load y\r\n %4 = wrapadd %2, %3\r\n store z, %4 : int[64]\r\n %5 = load z\r\n call print_bare, %5\r\n call print_nl\r\n %6 = const_i64 -9223372036854775808\r\n store a, %6 : int[64]\r\n %7 = const_i64 1\r\n store b, %7 : int[64]\r\n %8 = load a\r\n %9 = load b\r\n %10 = wrapsub %8, %9\r\n store c, %10 : int[64]\r\n %11 = load c\r\n call print_bare, %11\r\n call print_nl\r\n %12 = const_i64 3037000500\r\n store p, %12 : int[64]\r\n %13 = const_i64 3037000500\r\n store q, %13 : int[64]\r\n %14 = load p\r\n %15 = load q\r\n %16 = wrapmul %14, %15\r\n store r, %16 : int[64]\r\n %17 = load r\r\n call print_bare, %17\r\n call print_nl\r\n return", - "fe363d56": "function __main__():\r\nblock0:\r\n store xs : list[int[64], 6]\r\n %0 = const_i64 6\r\n store xs.__len, %0 : int[64]\r\n %1 = const_i64 0\r\n %2 = const_i64 10\r\n IndexStore xs, %1, %2\r\n %3 = const_i64 1\r\n %4 = const_i64 20\r\n IndexStore xs, %3, %4\r\n %5 = const_i64 2\r\n %6 = const_i64 30\r\n IndexStore xs, %5, %6\r\n %8 = const_i64 0\r\n %7 = Index xs, %8\r\n call print_bare, %7\r\n call print_nl\r\n %10 = const_i64 1\r\n %9 = Index xs, %10\r\n call print_bare, %9\r\n call print_nl\r\n %12 = const_i64 2\r\n %11 = Index xs, %12\r\n call print_bare, %11\r\n call print_nl\r\n return", - "28da76a1": "function __main__():\r\nblock0:\r\n store t : tuple[int[64], 4]\r\n %0 = const_i64 0\r\n %1 = const_i64 1\r\n IndexStore t, %0, %1\r\n %2 = const_i64 1\r\n %3 = const_i64 2\r\n IndexStore t, %2, %3\r\n %4 = const_i64 2\r\n %5 = const_i64 3\r\n IndexStore t, %4, %5\r\n %6 = const_i64 3\r\n %7 = const_i64 4\r\n IndexStore t, %6, %7\r\n %9 = const_i64 0\r\n %8 = Index t, %9\r\n call print_bare, %8\r\n call print_nl\r\n %11 = const_i64 3\r\n %10 = Index t, %11\r\n call print_bare, %10\r\n call print_nl\r\n return", - "f0834cb4": "function __main__():\r\nblock0:\r\n store d : dict[int[64], int[64], 4]\r\n %0 = const_i64 10\r\n %1 = const_i64 1\r\n DictStore d, %1, %0\r\n %2 = const_i64 20\r\n %3 = const_i64 2\r\n DictStore d, %3, %2\r\n %4 = const_i64 30\r\n %5 = const_i64 3\r\n DictStore d, %5, %4\r\n %6 = const_i64 40\r\n %7 = const_i64 4\r\n DictStore d, %7, %6\r\n %9 = const_i64 1\r\n %8 = DictIndex d, %9\r\n call print_bare, %8\r\n call print_nl\r\n %11 = const_i64 4\r\n %10 = DictIndex d, %11\r\n call print_bare, %10\r\n call print_nl\r\n %12 = const_i64 2\r\n store k, %12 : int[64]\r\n %14 = load k\r\n %13 = DictIndex d, %14\r\n call print_bare, %13\r\n call print_nl\r\n %15 = load k\r\n %16 = load k\r\n %17 = const_i64 1\r\n %18 = add %16, %17\r\n store j, %18 : int[64]\r\n %20 = load j\r\n %19 = DictIndex d, %20\r\n call print_bare, %19\r\n call print_nl\r\n return", - "0aa58a13": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store x, %0 : int[64]\r\n %1 = addressof x\r\n store _p, %1 : ptr[int[64]]\r\n %2 = load _p\r\n %3 = valueof %2 : int[64]\r\n call print_bare, %3\r\n call print_nl\r\n %4 = const_i64 6\r\n store q, %4 : int[64]\r\n %5 = addressof q\r\n store _q, %5 : ptr[int[64]]\r\n %6 = load _q\r\n %7 = valueof %6 : int[64]\r\n call print_bare, %7\r\n call print_nl\r\n %8 = load _p\r\n %9 = load _q\r\n %10 = eq %8, %9\r\n call print_bare, %10\r\n call print_nl\r\n return", - "cbd1f32e": "function __main__():\r\nblock0:\r\n %0 = const_f64 2.5\r\n store f, %0 : float[64]\r\n %1 = addressof f\r\n store _pf, %1 : ptr[float[64]]\r\n %2 = load _pf\r\n %3 = valueof %2 : float[64]\r\n call print_bare, %3\r\n call print_nl\r\n %4 = const_f64 3.25\r\n store f, %4 : float[64]\r\n %5 = load _pf\r\n %6 = valueof %5 : float[64]\r\n call print_bare, %6\r\n call print_nl\r\n return", - "799c0dd3": "function __main__():\r\nblock0:\r\n store p : struct[16]\r\n %0 = const_i64 3\r\n fieldstore p, 0, %0 : int[64]\r\n %1 = const_i64 4\r\n fieldstore p, 8, %1 : int[64]\r\n %2 = fieldload p, 0 : int[64]\r\n call print_bare, %2\r\n call print_nl\r\n %3 = fieldload p, 8 : int[64]\r\n call print_bare, %3\r\n call print_nl\r\n return", - "5b02aaa2": "function __main__():\r\nblock0:\r\n store s : struct[13]\r\n %0 = const_i64 2\r\n fieldstore s, 0, %0 : int[16]\r\n %1 = const_i64 80\r\n fieldstore s, 2, %1 : int[16]\r\n %2 = fieldload s, 0 : int[16]\r\n call print_bare, %2\r\n call print_nl\r\n %3 = fieldload s, 2 : int[16]\r\n call print_bare, %3\r\n call print_nl\r\n %4 = fieldload s, 4 : int[64]\r\n call print_bare, %4\r\n call print_nl\r\n %5 = fieldload s, 12 : bool\r\n call print_bare, %5\r\n call print_nl\r\n return", - "1407976e": "function __main__():\r\nblock0:\r\n store s : struct[9]\r\n %0 = const_i64 1\r\n fieldstore s, 0, %0 : int[8]\r\n %1 = fieldload s, 0 : int[8]\r\n %2 = const_i64 0\r\n %3 = eq %1, %2\r\n branch %3, block2, block3\r\nblock2:\r\n %4 = const_i64 0\r\n call print_bare, %4\r\n call print_nl\r\n jump block1\r\nblock3:\r\n %5 = const_i64 1\r\n %6 = eq %1, %5\r\n branch %6, block4, block5\r\nblock4:\r\n %7 = const_i64 1\r\n call print_bare, %7\r\n call print_nl\r\n jump block1\r\nblock5:\r\n %8 = const_i64 2\r\n %9 = eq %1, %8\r\n branch %9, block6, block7\r\nblock6:\r\n %10 = fieldload s, 1 : int[32]\r\n store n, %10 : int[32]\r\n %11 = load n\r\n call print_bare, %11\r\n call print_nl\r\n jump block1\r\nblock7:\r\n %12 = const_i64 3\r\n %13 = eq %1, %12\r\n branch %13, block8, block1\r\nblock8:\r\n %14 = fieldload s, 1 : int[64]\r\n store w, %14 : int[64]\r\n %15 = load w\r\n call print_bare, %15\r\n call print_nl\r\n jump block1\r\nblock1:\r\n return", - "89c84b42": "function __main__():\r\nblock0:\r\n store c : struct[15]\r\n %0 = const_i64 1\r\n fieldstore c, 0, %0 : int[8]\r\n %1 = const_i64 8080\r\n fieldstore c, 1, %1 : int[16]\r\n %2 = const_i64 16777343\r\n fieldstore c, 3, %2 : int[32]\r\n %3 = const_i64 7\r\n fieldstore c, 7, %3 : int[64]\r\n return", - "bd542c7c": "function __main__():\r\nblock0:\r\n store c : struct[15]\r\n %0 = const_i64 1\r\n fieldstore c, 0, %0 : int[8]\r\n %1 = const_i64 8080\r\n fieldstore c, 1, %1 : int[16]\r\n %2 = const_i64 16777343\r\n fieldstore c, 3, %2 : int[32]\r\n %3 = const_i64 7\r\n fieldstore c, 7, %3 : int[64]\r\n %4 = fieldload c, 0 : int[8]\r\n %5 = const_i64 1\r\n %6 = eq %4, %5\r\n branch %6, block2, block3\r\nblock2:\r\n %7 = fieldload c, 1 : int[16]\r\n store p, %7 : int[16]\r\n %8 = fieldload c, 3 : int[32]\r\n store a, %8 : int[32]\r\n %9 = load p\r\n call print_bare, %9\r\n call print_nl\r\n %10 = load a\r\n call print_bare, %10\r\n call print_nl\r\n jump block1\r\nblock3:\r\n %11 = const_i64 2\r\n %12 = eq %4, %11\r\n branch %12, block4, block5\r\nblock4:\r\n %13 = fieldload c, 1 : int[32]\r\n store n, %13 : int[32]\r\n %14 = load n\r\n call print_bare, %14\r\n call print_nl\r\n jump block1\r\nblock5:\r\n %15 = const_i64 0\r\n %16 = eq %4, %15\r\n branch %16, block6, block1\r\nblock6:\r\n %17 = const_i64 -1\r\n call print_bare, %17\r\n call print_nl\r\n jump block1\r\nblock1:\r\n %18 = fieldload c, 7 : int[64]\r\n call print_bare, %18\r\n call print_nl\r\n return", - "ae4a5d6b": "function __main__():\r\nblock0:\r\n store c : struct[8]\r\n %0 = const_i64 5\r\n store m0_n, %0 : int[64]\r\n %1 = fieldload c, 0 : int[64]\r\n %2 = load m0_n\r\n %3 = add %1, %2\r\n fieldstore c, 0, %3 : int[64]\r\n %4 = fieldload c, 0 : int[64]\r\n store m0_ret, %4 : int[64]\r\n jump block1\r\nblock1:\r\n %5 = load m0_ret\r\n call print_bare, %5\r\n call print_nl\r\n %6 = const_i64 7\r\n store m1_n, %6 : int[64]\r\n %7 = fieldload c, 0 : int[64]\r\n %8 = load m1_n\r\n %9 = add %7, %8\r\n fieldstore c, 0, %9 : int[64]\r\n %10 = fieldload c, 0 : int[64]\r\n store m1_ret, %10 : int[64]\r\n jump block2\r\nblock2:\r\n %11 = load m1_ret\r\n call print_bare, %11\r\n call print_nl\r\n return", - "9be3fe85": "function __main__():\r\nblock0:\r\n store s : struct[5]\r\n %0 = const_i64 0\r\n fieldstore s, 0, %0 : int[8]\r\n %1 = fieldload s, 0 : int[8]\r\n %2 = const_i64 0\r\n %3 = eq %1, %2\r\n branch %3, block3, block4\r\nblock3:\r\n %4 = const_i64 0\r\n store m0_ret, %4 : int[64]\r\n jump block1\r\nblock4:\r\n %5 = const_i64 1\r\n %6 = eq %1, %5\r\n branch %6, block5, block6\r\nblock5:\r\n %7 = fieldload s, 1 : int[32]\r\n store m0_n, %7 : int[32]\r\n %8 = load m0_n\r\n store m0_ret, %8 : int[64]\r\n jump block1\r\nblock6:\r\n %9 = const_i64 -1\r\n store m0_ret, %9 : int[64]\r\n jump block1\r\nblock2:\r\nblock1:\r\n %10 = load m0_ret\r\n call print_bare, %10\r\n call print_nl\r\n return", - "0e5a34af": "function __main__():\r\nblock0:\r\n store xs : list[int[64], 4]\r\n %0 = const_i64 4\r\n store xs.__len, %0 : int[64]\r\n %1 = const_i64 0\r\n %2 = const_i64 100\r\n IndexStore xs, %1, %2\r\n %3 = const_i64 1\r\n %4 = const_i64 23\r\n IndexStore xs, %3, %4\r\n %5 = const_i64 2\r\n %6 = const_i64 -3\r\n IndexStore xs, %5, %6\r\n %8 = const_i64 0\r\n %7 = Index xs, %8\r\n %10 = const_i64 1\r\n %9 = Index xs, %10\r\n %11 = add %7, %9\r\n %13 = const_i64 2\r\n %12 = Index xs, %13\r\n %14 = add %11, %12\r\n call print_bare, %14\r\n call print_nl\r\n return", - "2083f31f": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n store a, %0 : int[64]\r\n %1 = const_i64 6\r\n store a, %1 : int[64]\r\n %2 = load a\r\n call print_bare, %2\r\n call print_nl\r\n return", - "9e455b7e": "function __main__():\r\nblock0:\r\n store ps : list[int[32], 2]\r\n %0 = const_i64 2\r\n store ps.__len, %0 : int[64]\r\n %1 = const_i64 0\r\n %2 = const_i64 11\r\n IndexStore ps, %1, %2\r\n %3 = const_i64 1\r\n %4 = const_i64 -22\r\n IndexStore ps, %3, %4\r\n %6 = const_i64 0\r\n %5 = Index ps, %6\r\n %8 = const_i64 1\r\n %7 = Index ps, %8\r\n %9 = add %5, %7\r\n call print_bare, %9\r\n call print_nl\r\n return", - "eae2b479": "function __main__():\r\nblock0:\r\n %0 = const_i64 -1\r\n %1 = syscall %0\r\n store r, %1 : int[64]\r\n %2 = load r\r\n call print_bare, %2\r\n call print_nl\r\n return", - "3b8022da": "function __main__():\r\nblock0:\r\n store xs : list[int[64], 3]\r\n %0 = const_i64 3\r\n store xs.__len, %0 : int[64]\r\n store ys : list[int[64], 3]\r\n %1 = const_i64 3\r\n store ys.__len, %1 : int[64]\r\n %2 = const_i64 0\r\n %3 = const_i64 10\r\n IndexStore xs, %2, %3\r\n %4 = const_i64 1\r\n %5 = const_i64 20\r\n IndexStore xs, %4, %5\r\n %6 = const_i64 2\r\n %7 = const_i64 30\r\n IndexStore xs, %6, %7\r\n %8 = const_i64 0\r\n %9 = const_i64 1\r\n IndexStore ys, %8, %9\r\n %10 = const_i64 1\r\n %11 = const_i64 2\r\n IndexStore ys, %10, %11\r\n %12 = const_i64 2\r\n %13 = const_i64 3\r\n IndexStore ys, %12, %13\r\n %14 = const_i64 0\r\n %15 = const_i64 3\r\n %16 = const_i64 1\r\n store i, %14 : int[64]\r\n jump block4\r\nblock4:\r\n %17 = load i\r\n %18 = lt %17, %15\r\n branch %18, block1, block2\r\nblock1:\r\n %19 = load i\r\n %20 = Index xs, %19\r\n store x, %20 : int[64]\r\n %21 = load i\r\n call print_bare, %21\r\n call print_nl\r\n jump block3\r\nblock3:\r\n %22 = load i\r\n %23 = const_i64 1\r\n %24 = add %22, %23\r\n store i, %24\r\n jump block4\r\nblock2:\r\n %25 = const_i64 0\r\n %26 = const_i64 3\r\n %27 = const_i64 1\r\n store zip_i0, %25 : int[64]\r\n jump block8\r\nblock8:\r\n %28 = load zip_i0\r\n %29 = lt %28, %26\r\n branch %29, block5, block6\r\nblock5:\r\n %30 = load zip_i0\r\n %31 = Index xs, %30\r\n store a, %31 : int[64]\r\n %32 = Index ys, %30\r\n store b, %32 : int[64]\r\n %33 = load a\r\n call print_bare, %33\r\n call print_nl\r\n jump block7\r\nblock7:\r\n %34 = load zip_i0\r\n %35 = const_i64 1\r\n %36 = add %34, %35\r\n store zip_i0, %36\r\n jump block8\r\nblock6:\r\n return", - "b4973ce3": "function __main__():\r\nblock0:\r\n %0 = const_i64 9223372036854775807\r\n store x, %0 : int[64]\r\n %1 = const_i64 1\r\n store y, %1 : int[64]\r\n %2 = load x\r\n %3 = load y\r\n %4 = wrapadd %2, %3\r\n store z, %4 : int[64]\r\n %5 = load z\r\n call print_bare, %5\r\n call print_nl\r\n return", - "8b9a4015": "function __main__():\r\nblock0:\r\n %0 = const_i64 -7\r\n %1 = const_i64 3\r\n %2 = mod %0, %1\r\n call print_bare, %2\r\n call print_nl\r\n %3 = const_i64 7\r\n %4 = const_i64 -3\r\n %5 = mod %3, %4\r\n call print_bare, %5\r\n call print_nl\r\n return", - "e367543b": "function __main__():\r\nblock0:\r\n %0 = const_i64 5\r\n %1 = const_i64 63\r\n %2 = shl %0, %1\r\n call print_bare, %2\r\n call print_nl\r\n return" + "51269434": "function __main__():\nblock0:\n %0 = const_i64 5\n store a, %0 : int[64]\n tryenter Exception, block1\n jump block2\nblock2:\n tryenter ZeroDivisionError, block4\n jump block5\nblock5:\n %1 = load a\n %2 = const_i64 0\n %3 = mod %1, %2\n store inner, %3 : int[64]\n tryleave\n jump block6\nblock4:\n trycatch ZeroDivisionError\n store __printlit13 : str[13]\n %4 = const_i64 0\n %5 = const_i64 105\n IndexStore __printlit13, %4, %5\n %6 = const_i64 1\n %7 = const_i64 110\n IndexStore __printlit13, %6, %7\n %8 = const_i64 2\n %9 = const_i64 110\n IndexStore __printlit13, %8, %9\n %10 = const_i64 3\n %11 = const_i64 101\n IndexStore __printlit13, %10, %11\n %12 = const_i64 4\n %13 = const_i64 114\n IndexStore __printlit13, %12, %13\n %14 = const_i64 5\n %15 = const_i64 32\n IndexStore __printlit13, %14, %15\n %16 = const_i64 6\n %17 = const_i64 99\n IndexStore __printlit13, %16, %17\n %18 = const_i64 7\n %19 = const_i64 97\n IndexStore __printlit13, %18, %19\n %20 = const_i64 8\n %21 = const_i64 117\n IndexStore __printlit13, %20, %21\n %22 = const_i64 9\n %23 = const_i64 103\n IndexStore __printlit13, %22, %23\n %24 = const_i64 10\n %25 = const_i64 104\n IndexStore __printlit13, %24, %25\n %26 = const_i64 11\n %27 = const_i64 116\n IndexStore __printlit13, %26, %27\n print_container __printlit13 : str[13]\n call print_nl\n jump block6\nblock6:\n tryleave\n jump block3\nblock1:\n trycatch Exception\n store __printlit13 : str[13]\n %28 = const_i64 0\n %29 = const_i64 111\n IndexStore __printlit13, %28, %29\n %30 = const_i64 1\n %31 = const_i64 117\n IndexStore __printlit13, %30, %31\n %32 = const_i64 2\n %33 = const_i64 116\n IndexStore __printlit13, %32, %33\n %34 = const_i64 3\n %35 = const_i64 101\n IndexStore __printlit13, %34, %35\n %36 = const_i64 4\n %37 = const_i64 114\n IndexStore __printlit13, %36, %37\n %38 = const_i64 5\n %39 = const_i64 32\n IndexStore __printlit13, %38, %39\n %40 = const_i64 6\n %41 = const_i64 99\n IndexStore __printlit13, %40, %41\n %42 = const_i64 7\n %43 = const_i64 97\n IndexStore __printlit13, %42, %43\n %44 = const_i64 8\n %45 = const_i64 117\n IndexStore __printlit13, %44, %45\n %46 = const_i64 9\n %47 = const_i64 103\n IndexStore __printlit13, %46, %47\n %48 = const_i64 10\n %49 = const_i64 104\n IndexStore __printlit13, %48, %49\n %50 = const_i64 11\n %51 = const_i64 116\n IndexStore __printlit13, %50, %51\n print_container __printlit13 : str[13]\n call print_nl\n jump block3\nblock3:\n return", + "89052530": "function __main__():\nblock0:\n store v : struct[16]\n %0 = const_f64 1.5\n fieldstore v, 0, %0 : float[64]\n %1 = const_f64 -2.25\n fieldstore v, 8, %1 : float[64]\n %2 = fieldload v, 0 : float[64]\n call print_bare, %2\n call print_nl\n %3 = fieldload v, 8 : float[64]\n call print_bare, %3\n call print_nl\n %4 = fieldload v, 0 : float[64]\n %5 = fieldload v, 8 : float[64]\n %6 = add %4, %5\n call print_bare, %6\n call print_nl\n return", + "47db5edf": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0 : int[64]\n %1 = const_i64 20\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = add %2, %3\n store result, %4 : int[64]\n %5 = load result\n call print_bare, %5\n call print_nl\n return", + "39ae587c": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0 : int[64]\n %1 = const_i64 20\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = add %2, %3\n store z, %4 : int[64]\n %5 = load z\n call print_bare, %5\n call print_nl\n %6 = load x\n %7 = load y\n %8 = sub %6, %7\n call print_bare, %8\n call print_nl\n %9 = load x\n %10 = load y\n %11 = mul %9, %10\n call print_bare, %11\n call print_nl\n return", + "cac171bf": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0\n %1 = load x\n call print_bare, %1\n call print_nl\n return", + "2a8807c9": "function __main__():\nblock0:\n %0 = const_i64 7\n store i8, %0 : int[8]\n %1 = const_i64 7\n store i16, %1 : int[16]\n %2 = const_i64 7\n store i32, %2 : int[32]\n %3 = const_i64 7\n store i64, %3 : int[64]\n %4 = const_f64 0.5\n store f64, %4 : float[64]\n %5 = const_bool 1\n store b, %5 : bool\n %6 = load i64\n call print_bare, %6\n call print_nl\n return", + "7cbe74a6": "function __main__():\nblock0:\n %0 = const_i64 5\n store small, %0 : int[8]\n %1 = load small\n store big, %1 : int[64]\n %2 = load big\n call print_bare, %2\n call print_nl\n return", + "206f6bf0": "function __main__():\nblock0:\n %0 = const_i64 5\n store wide, %0 : int[64]\n %1 = load wide\n store narrow, %1 : int[8]\n %2 = load narrow\n call print_bare, %2\n call print_nl\n return", + "1a324420": "function __main__():\nblock0:\n %0 = const_i64 0\n store j, %0 : float[64]\n %1 = load j\n call print_bare, %1\n call print_nl\n return", + "aeb1dcc6": "function __main__():\nblock0:\n %0 = const_f64 0.0\n store j, %0 : float[64]\n %1 = load j\n call print_bare, %1\n call print_nl\n return", + "96262f13": "function __main__():\nblock0:\n %0 = const_i64 10\n store i, %0 : int[64]\n %1 = const_f64 2.5\n store f, %1 : float[64]\n %2 = load i\n %3 = load f\n %4 = add %2, %3\n call print_bare, %4\n call print_nl\n %5 = load f\n %6 = load i\n %7 = add %5, %6\n call print_bare, %7\n call print_nl\n %8 = load i\n %9 = const_i64 4\n %10 = div %8, %9\n call print_bare, %10\n call print_nl\n return", + "c2b675d3": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0 : int[64]\n %1 = const_i64 20\n store x, %1 : int[64]\n %2 = load x\n call print_bare, %2\n call print_nl\n return", + "a78d2c85": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0 : int[64]\n %1 = const_i64 20\n store x, %1 : int[64]\n %2 = load x\n store narrow, %2 : int[8]\n %3 = load narrow\n call print_bare, %3\n call print_nl\n return", + "e1f84f23": "function __main__():\nblock0:\n %0 = const_i64 5\n store i, %0 : int[8]\n %1 = load i\n call print_bare, %1\n call print_nl\n return", + "bba9661c": "function __main__():\nblock0:\n %0 = load i\n call print_bare, %0\n call print_nl\n return", + "f00439de": "function __main__():\nblock0:\n %0 = const_i64 1\n store x, %0 : int[64]\n %1 = load x\n %2 = const_i64 3\n %3 = lt %1, %2\n branch %3, block1, block2\nblock1:\n %4 = const_i64 2\n store y, %4 : int[64]\n jump block2\nblock2:\n %5 = load y\n call print_bare, %5\n call print_nl\n return", + "9443a6ad": "function __main__():\nblock0:\n %0 = const_i64 0\n store i, %0 : int[64]\n %1 = const_i64 0\n store total, %1 : int[64]\n jump block1\nblock1:\n %2 = load i\n %3 = const_i64 10\n %4 = lt %2, %3\n branch %4, block2, block3\nblock2:\n %5 = load total\n %6 = load i\n %7 = add %5, %6\n store total, %7 : int[64]\n %8 = load i\n %9 = load i\n %10 = const_i64 1\n %11 = add %9, %10\n store i, %11 : int[64]\n jump block1\nblock3:\n %12 = load total\n call print_bare, %12\n call print_nl\n return", + "06ab6407": "function __main__():\nblock0:\n %0 = const_i64 1\n %1 = const_i64 10\n %2 = const_i64 3\n store i, %0 : int[64]\n %3 = const_i64 0\n %4 = gt %2, %3\n branch %4, block4, block5\nblock4:\n %5 = lt %0, %1\n branch %5, block1, block2\nblock5:\n %6 = gt %0, %1\n branch %6, block1, block2\nblock1:\n %7 = load i\n call print_bare, %7\n call print_nl\n jump block3\nblock3:\n %8 = load i\n branch %4, block6, block7\nblock6:\n %9 = const_i64 -9223372036854775808\n %10 = wrapsub %1, %8\n %11 = bxor %10, %9\n %12 = bxor %2, %9\n %13 = gt %11, %12\n branch %13, block8, block2\nblock7:\n %14 = const_i64 -9223372036854775808\n %15 = wrapsub %8, %1\n %16 = sub %3, %2\n %17 = bxor %15, %14\n %18 = bxor %16, %14\n %19 = gt %17, %18\n branch %19, block8, block2\nblock8:\n %20 = load i\n %21 = add %20, %2\n store i, %21\n jump block1\nblock2:\n %22 = const_i64 10\n %23 = const_i64 1\n %24 = const_i64 -3\n store i, %22 : int[64]\n guardnonzero %24\n %25 = const_i64 0\n %26 = gt %24, %25\n branch %26, block12, block13\nblock12:\n %27 = lt %22, %23\n branch %27, block9, block10\nblock13:\n %28 = gt %22, %23\n branch %28, block9, block10\nblock9:\n %29 = load i\n call print_bare, %29\n call print_nl\n jump block11\nblock11:\n %30 = load i\n branch %26, block14, block15\nblock14:\n %31 = const_i64 -9223372036854775808\n %32 = wrapsub %23, %30\n %33 = bxor %32, %31\n %34 = bxor %24, %31\n %35 = gt %33, %34\n branch %35, block16, block10\nblock15:\n %36 = const_i64 -9223372036854775808\n %37 = wrapsub %30, %23\n %38 = sub %25, %24\n %39 = bxor %37, %36\n %40 = bxor %38, %36\n %41 = gt %39, %40\n branch %41, block16, block10\nblock16:\n %42 = load i\n %43 = add %42, %24\n store i, %43\n jump block9\nblock10:\n %44 = const_i64 0\n %45 = const_i64 5\n %46 = const_i64 1\n store i, %44 : int[64]\n %47 = const_i64 0\n %48 = gt %46, %47\n branch %48, block20, block21\nblock20:\n %49 = lt %44, %45\n branch %49, block17, block18\nblock21:\n %50 = gt %44, %45\n branch %50, block17, block18\nblock17:\n %51 = load i\n call print_bare, %51\n call print_nl\n jump block19\nblock19:\n %52 = load i\n branch %48, block22, block23\nblock22:\n %53 = const_i64 -9223372036854775808\n %54 = wrapsub %45, %52\n %55 = bxor %54, %53\n %56 = bxor %46, %53\n %57 = gt %55, %56\n branch %57, block24, block18\nblock23:\n %58 = const_i64 -9223372036854775808\n %59 = wrapsub %52, %45\n %60 = sub %47, %46\n %61 = bxor %59, %58\n %62 = bxor %60, %58\n %63 = gt %61, %62\n branch %63, block24, block18\nblock24:\n %64 = load i\n %65 = add %64, %46\n store i, %65\n jump block17\nblock18:\n %66 = const_i64 5\n %67 = const_i64 5\n %68 = const_i64 1\n store i, %66 : int[64]\n %69 = const_i64 0\n %70 = gt %68, %69\n branch %70, block28, block29\nblock28:\n %71 = lt %66, %67\n branch %71, block25, block26\nblock29:\n %72 = gt %66, %67\n branch %72, block25, block26\nblock25:\n %73 = load i\n call print_bare, %73\n call print_nl\n jump block27\nblock27:\n %74 = load i\n branch %70, block30, block31\nblock30:\n %75 = const_i64 -9223372036854775808\n %76 = wrapsub %67, %74\n %77 = bxor %76, %75\n %78 = bxor %68, %75\n %79 = gt %77, %78\n branch %79, block32, block26\nblock31:\n %80 = const_i64 -9223372036854775808\n %81 = wrapsub %74, %67\n %82 = sub %69, %68\n %83 = bxor %81, %80\n %84 = bxor %82, %80\n %85 = gt %83, %84\n branch %85, block32, block26\nblock32:\n %86 = load i\n %87 = add %86, %68\n store i, %87\n jump block25\nblock26:\n %88 = const_i64 1\n %89 = const_i64 5\n %90 = const_i64 6\n store i, %88 : int[64]\n %91 = const_i64 0\n %92 = gt %90, %91\n branch %92, block36, block37\nblock36:\n %93 = lt %88, %89\n branch %93, block33, block34\nblock37:\n %94 = gt %88, %89\n branch %94, block33, block34\nblock33:\n %95 = load i\n call print_bare, %95\n call print_nl\n jump block35\nblock35:\n %96 = load i\n branch %92, block38, block39\nblock38:\n %97 = const_i64 -9223372036854775808\n %98 = wrapsub %89, %96\n %99 = bxor %98, %97\n %100 = bxor %90, %97\n %101 = gt %99, %100\n branch %101, block40, block34\nblock39:\n %102 = const_i64 -9223372036854775808\n %103 = wrapsub %96, %89\n %104 = sub %91, %90\n %105 = bxor %103, %102\n %106 = bxor %104, %102\n %107 = gt %105, %106\n branch %107, block40, block34\nblock40:\n %108 = load i\n %109 = add %108, %90\n store i, %109\n jump block33\nblock34:\n %110 = const_i64 -5\n %111 = const_i64 -1\n %112 = const_i64 1\n store i, %110 : int[64]\n %113 = const_i64 0\n %114 = gt %112, %113\n branch %114, block44, block45\nblock44:\n %115 = lt %110, %111\n branch %115, block41, block42\nblock45:\n %116 = gt %110, %111\n branch %116, block41, block42\nblock41:\n %117 = load i\n call print_bare, %117\n call print_nl\n jump block43\nblock43:\n %118 = load i\n branch %114, block46, block47\nblock46:\n %119 = const_i64 -9223372036854775808\n %120 = wrapsub %111, %118\n %121 = bxor %120, %119\n %122 = bxor %112, %119\n %123 = gt %121, %122\n branch %123, block48, block42\nblock47:\n %124 = const_i64 -9223372036854775808\n %125 = wrapsub %118, %111\n %126 = sub %113, %112\n %127 = bxor %125, %124\n %128 = bxor %126, %124\n %129 = gt %127, %128\n branch %129, block48, block42\nblock48:\n %130 = load i\n %131 = add %130, %112\n store i, %131\n jump block41\nblock42:\n %132 = const_i64 -1\n %133 = const_i64 -5\n %134 = const_i64 -2\n store i, %132 : int[64]\n guardnonzero %134\n %135 = const_i64 0\n %136 = gt %134, %135\n branch %136, block52, block53\nblock52:\n %137 = lt %132, %133\n branch %137, block49, block50\nblock53:\n %138 = gt %132, %133\n branch %138, block49, block50\nblock49:\n %139 = load i\n call print_bare, %139\n call print_nl\n jump block51\nblock51:\n %140 = load i\n branch %136, block54, block55\nblock54:\n %141 = const_i64 -9223372036854775808\n %142 = wrapsub %133, %140\n %143 = bxor %142, %141\n %144 = bxor %134, %141\n %145 = gt %143, %144\n branch %145, block56, block50\nblock55:\n %146 = const_i64 -9223372036854775808\n %147 = wrapsub %140, %133\n %148 = sub %135, %134\n %149 = bxor %147, %146\n %150 = bxor %148, %146\n %151 = gt %149, %150\n branch %151, block56, block50\nblock56:\n %152 = load i\n %153 = add %152, %134\n store i, %153\n jump block49\nblock50:\n return", + "bd68f7e0": "function __main__():\nblock0:\n %0 = const_i64 0\n store total, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 100\n %3 = const_i64 1\n store i, %1 : int[64]\n jump block4\nblock4:\n %4 = load i\n %5 = lt %4, %2\n branch %5, block1, block2\nblock1:\n %6 = load i\n %7 = const_i64 5\n %8 = eq %6, %7\n branch %8, block5, block6\nblock5:\n jump block3\nblock6:\n %9 = load i\n %10 = const_i64 10\n %11 = eq %9, %10\n branch %11, block7, block8\nblock7:\n jump block2\nblock8:\n %12 = load total\n %13 = load i\n %14 = add %12, %13\n store total, %14 : int[64]\n jump block3\nblock3:\n %15 = load i\n %16 = const_i64 1\n %17 = add %15, %16\n store i, %17\n jump block4\nblock2:\n %18 = load total\n call print_bare, %18\n call print_nl\n return", + "4598471d": "function __main__():\nblock0:\n store xs : list[int[64], 5]\n %0 = const_i64 5\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 10\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 20\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 30\n IndexStore xs, %5, %6\n %7 = const_i64 3\n %8 = const_i64 40\n IndexStore xs, %7, %8\n %9 = const_i64 4\n %10 = const_i64 50\n IndexStore xs, %9, %10\n %11 = const_i64 0\n %12 = const_i64 5\n %13 = const_i64 1\n store ei, %11 : int[64]\n jump block4\nblock4:\n %14 = load ei\n %15 = lt %14, %12\n branch %15, block1, block2\nblock1:\n %16 = load ei\n %17 = Index xs, %16\n store ex, %17 : int[64]\n %18 = load ei\n call print_bare, %18\n call print_nl\n %19 = load ex\n call print_bare, %19\n call print_nl\n jump block3\nblock3:\n %20 = load ei\n %21 = const_i64 1\n %22 = add %20, %21\n store ei, %22\n jump block4\nblock2:\n store long6 : list[int[64], 6]\n %23 = const_i64 6\n store long6.__len, %23 : int[64]\n store short3 : list[int[64], 3]\n %24 = const_i64 3\n store short3.__len, %24 : int[64]\n %25 = const_i64 0\n %26 = const_i64 1\n IndexStore long6, %25, %26\n %27 = const_i64 1\n %28 = const_i64 2\n IndexStore long6, %27, %28\n %29 = const_i64 2\n %30 = const_i64 3\n IndexStore long6, %29, %30\n %31 = const_i64 0\n %32 = const_i64 7\n IndexStore short3, %31, %32\n %33 = const_i64 1\n %34 = const_i64 8\n IndexStore short3, %33, %34\n %35 = const_i64 2\n %36 = const_i64 9\n IndexStore short3, %35, %36\n %37 = const_i64 0\n store total, %37 : int[64]\n %38 = const_i64 0\n %39 = const_i64 3\n %40 = const_i64 1\n store zip_i0, %38 : int[64]\n jump block8\nblock8:\n %41 = load zip_i0\n %42 = lt %41, %39\n branch %42, block5, block6\nblock5:\n %43 = load zip_i0\n %44 = Index long6, %43\n store za, %44 : int[64]\n %45 = Index short3, %43\n store zb, %45 : int[64]\n %46 = load za\n %47 = load zb\n %48 = add %46, %47\n store total, %48 : int[64]\n jump block7\nblock7:\n %49 = load zip_i0\n %50 = const_i64 1\n %51 = add %49, %50\n store zip_i0, %51\n jump block8\nblock6:\n %52 = load total\n call print_bare, %52\n call print_nl\n return", + "311879c3": "function __main__():\nblock0:\n %0 = const_i64 5\n store x, %0 : int[64]\n %1 = const_i64 2\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = gt %2, %3\n branch %4, block1, block2\nblock1:\n %5 = load x\n store __ifexpr0, %5\n jump block3\nblock2:\n %6 = load y\n store __ifexpr0, %6\n jump block3\nblock3:\n %7 = load __ifexpr0\n call print_bare, %7\n call print_nl\n %8 = load x\n %9 = load y\n %10 = lt %8, %9\n branch %10, block4, block5\nblock4:\n %11 = const_i64 1\n store __ifexpr1, %11\n jump block6\nblock5:\n %12 = load x\n %13 = const_i64 5\n %14 = eq %12, %13\n branch %14, block7, block8\nblock7:\n %15 = const_i64 2\n store __ifexpr2, %15\n jump block9\nblock8:\n %16 = const_i64 3\n store __ifexpr2, %16\n jump block9\nblock9:\n %17 = load __ifexpr2\n store __ifexpr1, %17\n jump block6\nblock6:\n %18 = load __ifexpr1\n call print_bare, %18\n call print_nl\n return", + "ace9c7cc": "function add(a:int[64], b:int[64]):\nblock0:\n %0 = load a\n %1 = load b\n %2 = add %0, %1\n return %2\nfunction __main__():\nblock0:\n %0 = const_i64 3\n %1 = const_i64 4\n %2 = call add, %0, %1\n store x, %2 : int[64]\n %3 = load x\n call print_bare, %3\n call print_nl\n return", + "8c105dca": "function __main__():\nblock0:\n %0 = const_i64 -7\n store a, %0 : int[64]\n %1 = const_i64 3\n store b, %1 : int[64]\n %2 = load a\n %3 = load b\n %4 = mod %2, %3\n call print_bare, %4\n call print_nl\n %5 = const_i64 7\n %6 = const_i64 3\n %7 = mod %5, %6\n call print_bare, %7\n call print_nl\n %8 = const_i64 -7\n %9 = const_i64 3\n %10 = mod %8, %9\n call print_bare, %10\n call print_nl\n return", + "c6fd2aba": "function __main__():\nblock0:\n %0 = const_i64 1\n store a, %0 : int[64]\n %1 = const_i64 2\n store b, %1 : int[64]\n %2 = const_f64 1.5\n store c, %2 : float[64]\n %3 = load a\n %4 = load b\n %5 = lt %3, %4\n call print_bare, %5\n call print_nl\n %6 = load b\n %7 = load a\n %8 = gt %6, %7\n call print_bare, %8\n call print_nl\n %9 = load a\n %10 = load a\n %11 = eq %9, %10\n call print_bare, %11\n call print_nl\n %12 = load c\n %13 = load b\n %14 = lt %12, %13\n call print_bare, %14\n call print_nl\n return", + "2c1f3e5c": "function __main__():\nblock0:\n %0 = const_i64 1\n store a, %0 : int[64]\n %1 = const_i64 2\n store b, %1 : int[64]\n %2 = load a\n %3 = load b\n %4 = ne %2, %3\n call print_bare, %4\n call print_nl\n %5 = load a\n %6 = load a\n %7 = ne %5, %6\n call print_bare, %7\n call print_nl\n %8 = const_i64 -128\n store lo, %8 : int[8]\n %9 = const_i64 127\n store hi, %9 : int[8]\n %10 = load lo\n %11 = load hi\n %12 = ne %10, %11\n call print_bare, %12\n call print_nl\n return", + "5ba53d35": "function __main__():\nblock0:\n %0 = const_f64 2.5\n %1 = const_i64 1\n %2 = band %0, %1\n call print_bare, %2\n call print_nl\n return", + "f8490a90": "function __main__():\nblock0:\n %0 = const_i64 1\n %1 = const_i64 64\n %2 = shl %0, %1\n call print_bare, %2\n call print_nl\n return", + "349eae74": "function __main__():\nblock0:\n %0 = const_i64 1\n %1 = const_i64 10\n %2 = shl %0, %1\n store x, %2 : int[64]\n %3 = load x\n %4 = const_i64 3\n %5 = shr %3, %4\n store y, %5 : int[64]\n %6 = load x\n %7 = const_i64 255\n %8 = band %6, %7\n store z, %8 : int[64]\n %9 = load x\n call print_bare, %9\n call print_nl\n %10 = load y\n call print_bare, %10\n call print_nl\n %11 = load z\n call print_bare, %11\n call print_nl\n return", + "6666db5c": "function __main__():\nblock0:\n %0 = const_i64 9223372036854775807\n store x, %0 : int[64]\n %1 = const_i64 1\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = wrapadd %2, %3\n store z, %4 : int[64]\n %5 = load z\n call print_bare, %5\n call print_nl\n %6 = const_i64 -9223372036854775808\n store a, %6 : int[64]\n %7 = const_i64 1\n store b, %7 : int[64]\n %8 = load a\n %9 = load b\n %10 = wrapsub %8, %9\n store c, %10 : int[64]\n %11 = load c\n call print_bare, %11\n call print_nl\n %12 = const_i64 3037000500\n store p, %12 : int[64]\n %13 = const_i64 3037000500\n store q, %13 : int[64]\n %14 = load p\n %15 = load q\n %16 = wrapmul %14, %15\n store r, %16 : int[64]\n %17 = load r\n call print_bare, %17\n call print_nl\n return", + "fe363d56": "function __main__():\nblock0:\n store xs : list[int[64], 6]\n %0 = const_i64 6\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 10\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 20\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 30\n IndexStore xs, %5, %6\n %8 = const_i64 0\n %7 = Index xs, %8\n call print_bare, %7\n call print_nl\n %10 = const_i64 1\n %9 = Index xs, %10\n call print_bare, %9\n call print_nl\n %12 = const_i64 2\n %11 = Index xs, %12\n call print_bare, %11\n call print_nl\n return", + "a3b9564a": "function __main__():\nblock0:\n store xs : list[int[64], 6]\n %0 = const_i64 3\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 1\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 2\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 3\n IndexStore xs, %5, %6\n %7 = load xs.__len\n call print_bare, %7\n call print_nl\n print_container xs, xs.__len : list[int[64], 6]\n call print_nl\n %8 = const_i64 0\n %9 = const_i64 0\n %10 = Index xs, %9\n %11 = add %8, %10\n %12 = const_i64 1\n %13 = Index xs, %12\n %14 = add %11, %13\n %15 = const_i64 2\n %16 = Index xs, %15\n %17 = add %14, %16\n call print_bare, %17\n call print_nl\n %18 = load xs.__len\n %19 = const_i64 10\n IndexStore xs, %18, %19\n %20 = const_i64 1\n %21 = add %18, %20\n store xs.__len, %21\n %22 = load xs.__len\n %23 = const_i64 20\n IndexStore xs, %22, %23\n %24 = const_i64 1\n %25 = add %22, %24\n store xs.__len, %25\n %26 = load xs.__len\n call print_bare, %26\n call print_nl\n print_container xs, xs.__len : list[int[64], 6]\n call print_nl\n return", + "28da76a1": "function __main__():\nblock0:\n store t : tuple[int[64], 4]\n %0 = const_i64 0\n %1 = const_i64 1\n IndexStore t, %0, %1\n %2 = const_i64 1\n %3 = const_i64 2\n IndexStore t, %2, %3\n %4 = const_i64 2\n %5 = const_i64 3\n IndexStore t, %4, %5\n %6 = const_i64 3\n %7 = const_i64 4\n IndexStore t, %6, %7\n %9 = const_i64 0\n %8 = Index t, %9\n call print_bare, %8\n call print_nl\n %11 = const_i64 3\n %10 = Index t, %11\n call print_bare, %10\n call print_nl\n return", + "f0834cb4": "function __main__():\nblock0:\n store d : dict[int[64], int[64], 4]\n %0 = const_i64 10\n %1 = const_i64 1\n DictStore d, %1, %0\n %2 = const_i64 20\n %3 = const_i64 2\n DictStore d, %3, %2\n %4 = const_i64 30\n %5 = const_i64 3\n DictStore d, %5, %4\n %6 = const_i64 40\n %7 = const_i64 4\n DictStore d, %7, %6\n %9 = const_i64 1\n %8 = DictIndex d, %9\n call print_bare, %8\n call print_nl\n %11 = const_i64 4\n %10 = DictIndex d, %11\n call print_bare, %10\n call print_nl\n %12 = const_i64 2\n store k, %12 : int[64]\n %14 = load k\n %13 = DictIndex d, %14\n call print_bare, %13\n call print_nl\n %15 = load k\n %16 = load k\n %17 = const_i64 1\n %18 = add %16, %17\n store j, %18 : int[64]\n %20 = load j\n %19 = DictIndex d, %20\n call print_bare, %19\n call print_nl\n return", + "0aa58a13": "function __main__():\nblock0:\n %0 = const_i64 5\n store x, %0 : int[64]\n %1 = addressof x\n store _p, %1 : ptr[int[64]]\n %2 = load _p\n %3 = valueof %2 : int[64]\n call print_bare, %3\n call print_nl\n %4 = const_i64 6\n store q, %4 : int[64]\n %5 = addressof q\n store _q, %5 : ptr[int[64]]\n %6 = load _q\n %7 = valueof %6 : int[64]\n call print_bare, %7\n call print_nl\n %8 = load _p\n %9 = load _q\n %10 = eq %8, %9\n call print_bare, %10\n call print_nl\n return", + "cbd1f32e": "function __main__():\nblock0:\n %0 = const_f64 2.5\n store f, %0 : float[64]\n %1 = addressof f\n store _pf, %1 : ptr[float[64]]\n %2 = load _pf\n %3 = valueof %2 : float[64]\n call print_bare, %3\n call print_nl\n %4 = const_f64 3.25\n store f, %4 : float[64]\n %5 = load _pf\n %6 = valueof %5 : float[64]\n call print_bare, %6\n call print_nl\n return", + "799c0dd3": "function __main__():\nblock0:\n store p : struct[16]\n %0 = const_i64 3\n fieldstore p, 0, %0 : int[64]\n %1 = const_i64 4\n fieldstore p, 8, %1 : int[64]\n %2 = fieldload p, 0 : int[64]\n call print_bare, %2\n call print_nl\n %3 = fieldload p, 8 : int[64]\n call print_bare, %3\n call print_nl\n return", + "5b02aaa2": "function __main__():\nblock0:\n store s : struct[13]\n %0 = const_i64 2\n fieldstore s, 0, %0 : int[16]\n %1 = const_i64 80\n fieldstore s, 2, %1 : int[16]\n %2 = fieldload s, 0 : int[16]\n call print_bare, %2\n call print_nl\n %3 = fieldload s, 2 : int[16]\n call print_bare, %3\n call print_nl\n %4 = fieldload s, 4 : int[64]\n call print_bare, %4\n call print_nl\n %5 = fieldload s, 12 : bool\n call print_bare, %5\n call print_nl\n return", + "1407976e": "function __main__():\nblock0:\n store s : struct[9]\n %0 = const_i64 1\n fieldstore s, 0, %0 : int[8]\n %1 = fieldload s, 0 : int[8]\n %2 = const_i64 0\n %3 = eq %1, %2\n branch %3, block2, block3\nblock2:\n %4 = const_i64 0\n call print_bare, %4\n call print_nl\n jump block1\nblock3:\n %5 = const_i64 1\n %6 = eq %1, %5\n branch %6, block4, block5\nblock4:\n %7 = const_i64 1\n call print_bare, %7\n call print_nl\n jump block1\nblock5:\n %8 = const_i64 2\n %9 = eq %1, %8\n branch %9, block6, block7\nblock6:\n %10 = fieldload s, 1 : int[32]\n store n, %10 : int[32]\n %11 = load n\n call print_bare, %11\n call print_nl\n jump block1\nblock7:\n %12 = const_i64 3\n %13 = eq %1, %12\n branch %13, block8, block1\nblock8:\n %14 = fieldload s, 1 : int[64]\n store w, %14 : int[64]\n %15 = load w\n call print_bare, %15\n call print_nl\n jump block1\nblock1:\n return", + "89c84b42": "function __main__():\nblock0:\n store c : struct[15]\n %0 = const_i64 1\n fieldstore c, 0, %0 : int[8]\n %1 = const_i64 8080\n fieldstore c, 1, %1 : int[16]\n %2 = const_i64 16777343\n fieldstore c, 3, %2 : int[32]\n %3 = const_i64 7\n fieldstore c, 7, %3 : int[64]\n return", + "bd542c7c": "function __main__():\nblock0:\n store c : struct[15]\n %0 = const_i64 1\n fieldstore c, 0, %0 : int[8]\n %1 = const_i64 8080\n fieldstore c, 1, %1 : int[16]\n %2 = const_i64 16777343\n fieldstore c, 3, %2 : int[32]\n %3 = const_i64 7\n fieldstore c, 7, %3 : int[64]\n %4 = fieldload c, 0 : int[8]\n %5 = const_i64 1\n %6 = eq %4, %5\n branch %6, block2, block3\nblock2:\n %7 = fieldload c, 1 : int[16]\n store p, %7 : int[16]\n %8 = fieldload c, 3 : int[32]\n store a, %8 : int[32]\n %9 = load p\n call print_bare, %9\n call print_nl\n %10 = load a\n call print_bare, %10\n call print_nl\n jump block1\nblock3:\n %11 = const_i64 2\n %12 = eq %4, %11\n branch %12, block4, block5\nblock4:\n %13 = fieldload c, 1 : int[32]\n store n, %13 : int[32]\n %14 = load n\n call print_bare, %14\n call print_nl\n jump block1\nblock5:\n %15 = const_i64 0\n %16 = eq %4, %15\n branch %16, block6, block1\nblock6:\n %17 = const_i64 -1\n call print_bare, %17\n call print_nl\n jump block1\nblock1:\n %18 = fieldload c, 7 : int[64]\n call print_bare, %18\n call print_nl\n return", + "ae4a5d6b": "function __main__():\nblock0:\n store c : struct[8]\n %0 = const_i64 5\n store m0_n, %0 : int[64]\n %1 = fieldload c, 0 : int[64]\n %2 = load m0_n\n %3 = add %1, %2\n fieldstore c, 0, %3 : int[64]\n %4 = fieldload c, 0 : int[64]\n store m0_ret, %4 : int[64]\n jump block1\nblock1:\n %5 = load m0_ret\n call print_bare, %5\n call print_nl\n %6 = const_i64 7\n store m1_n, %6 : int[64]\n %7 = fieldload c, 0 : int[64]\n %8 = load m1_n\n %9 = add %7, %8\n fieldstore c, 0, %9 : int[64]\n %10 = fieldload c, 0 : int[64]\n store m1_ret, %10 : int[64]\n jump block2\nblock2:\n %11 = load m1_ret\n call print_bare, %11\n call print_nl\n return", + "9be3fe85": "function __main__():\nblock0:\n store s : struct[5]\n %0 = const_i64 0\n fieldstore s, 0, %0 : int[8]\n %1 = fieldload s, 0 : int[8]\n %2 = const_i64 0\n %3 = eq %1, %2\n branch %3, block3, block4\nblock3:\n %4 = const_i64 0\n store m0_ret, %4 : int[64]\n jump block1\nblock4:\n %5 = const_i64 1\n %6 = eq %1, %5\n branch %6, block5, block6\nblock5:\n %7 = fieldload s, 1 : int[32]\n store m0_n, %7 : int[32]\n %8 = load m0_n\n store m0_ret, %8 : int[64]\n jump block1\nblock6:\n %9 = const_i64 -1\n store m0_ret, %9 : int[64]\n jump block1\nblock2:\nblock1:\n %10 = load m0_ret\n call print_bare, %10\n call print_nl\n return", + "0e5a34af": "function __main__():\nblock0:\n store xs : list[int[64], 4]\n %0 = const_i64 4\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 100\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 23\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 -3\n IndexStore xs, %5, %6\n %8 = const_i64 0\n %7 = Index xs, %8\n %10 = const_i64 1\n %9 = Index xs, %10\n %11 = add %7, %9\n %13 = const_i64 2\n %12 = Index xs, %13\n %14 = add %11, %12\n call print_bare, %14\n call print_nl\n return", + "2083f31f": "function __main__():\nblock0:\n %0 = const_i64 5\n store a, %0 : int[64]\n %1 = const_i64 6\n store a, %1 : int[64]\n %2 = load a\n call print_bare, %2\n call print_nl\n return", + "9e455b7e": "function __main__():\nblock0:\n store ps : list[int[32], 2]\n %0 = const_i64 2\n store ps.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 11\n IndexStore ps, %1, %2\n %3 = const_i64 1\n %4 = const_i64 -22\n IndexStore ps, %3, %4\n %6 = const_i64 0\n %5 = Index ps, %6\n %8 = const_i64 1\n %7 = Index ps, %8\n %9 = add %5, %7\n call print_bare, %9\n call print_nl\n return", + "eae2b479": "function __main__():\nblock0:\n %0 = const_i64 -1\n %1 = syscall %0\n store r, %1 : int[64]\n %2 = load r\n call print_bare, %2\n call print_nl\n return", + "86ec5e67": "function __main__():\nblock0:\n %0 = const_i64 65\n store x, %0 : int[64]\n %1 = addressof x\n store _ptrX, %1 : ptr[int[64]]\n %2 = const_i64 1\n %3 = load _ptrX\n %4 = syscall %2, %3\n store r, %4 : int[64]\n %5 = load r\n call print_bare, %5\n call print_nl\n return", + "287ffe4b": "function __main__():\nblock0:\n %0 = const_i64 0\n store n, %0 : int[64]\n %1 = load n\n %2 = const_i64 0\n %3 = ne %1, %2\n branch %3, block1, block2\nblock1:\n store __printlit9 : str[9]\n %4 = const_i64 0\n %5 = const_i64 110\n IndexStore __printlit9, %4, %5\n %6 = const_i64 1\n %7 = const_i64 111\n IndexStore __printlit9, %6, %7\n %8 = const_i64 2\n %9 = const_i64 116\n IndexStore __printlit9, %8, %9\n %10 = const_i64 3\n %11 = const_i64 32\n IndexStore __printlit9, %10, %11\n %12 = const_i64 4\n %13 = const_i64 122\n IndexStore __printlit9, %12, %13\n %14 = const_i64 5\n %15 = const_i64 101\n IndexStore __printlit9, %14, %15\n %16 = const_i64 6\n %17 = const_i64 114\n IndexStore __printlit9, %16, %17\n %18 = const_i64 7\n %19 = const_i64 111\n IndexStore __printlit9, %18, %19\n print_container __printlit9 : str[9]\n call print_nl\n jump block2\nblock2:\n return", + "c3e1b778": "function __main__():\nblock0:\n %0 = load x\n call print_bare, %0\n call print_nl\n return", + "e3ef5a6b": "function __main__():\nblock0:\n %0 = const_i64 1\n store a, %0 : int[64]\n %1 = const_i64 2\n store b, %1 : int[64]\n %2 = load a\n %3 = load b\n %4 = lt %2, %3\n %5 = load b\n %6 = const_i64 10\n %7 = lt %5, %6\n %8 = and %4, %7\n branch %8, block1, block2\nblock1:\n store __printlit3 : str[3]\n %9 = const_i64 0\n %10 = const_i64 111\n IndexStore __printlit3, %9, %10\n %11 = const_i64 1\n %12 = const_i64 107\n IndexStore __printlit3, %11, %12\n print_container __printlit3 : str[3]\n call print_nl\n jump block2\nblock2:\n return", + "d4384e31": "function __main__():\nblock0:\n %0 = const_i64 0\n store n, %0 : int[64]\n tryenter ZeroDivisionError, block1\n jump block2\nblock2:\n %1 = const_i64 100\n %2 = load n\n %3 = mod %1, %2\n store q, %3 : int[64]\n store __printlit12 : str[12]\n %4 = const_i64 0\n %5 = const_i64 110\n IndexStore __printlit12, %4, %5\n %6 = const_i64 1\n %7 = const_i64 111\n IndexStore __printlit12, %6, %7\n %8 = const_i64 2\n %9 = const_i64 116\n IndexStore __printlit12, %8, %9\n %10 = const_i64 3\n %11 = const_i64 32\n IndexStore __printlit12, %10, %11\n %12 = const_i64 4\n %13 = const_i64 114\n IndexStore __printlit12, %12, %13\n %14 = const_i64 5\n %15 = const_i64 101\n IndexStore __printlit12, %14, %15\n %16 = const_i64 6\n %17 = const_i64 97\n IndexStore __printlit12, %16, %17\n %18 = const_i64 7\n %19 = const_i64 99\n IndexStore __printlit12, %18, %19\n %20 = const_i64 8\n %21 = const_i64 104\n IndexStore __printlit12, %20, %21\n %22 = const_i64 9\n %23 = const_i64 101\n IndexStore __printlit12, %22, %23\n %24 = const_i64 10\n %25 = const_i64 100\n IndexStore __printlit12, %24, %25\n print_container __printlit12 : str[12]\n call print_nl\n tryleave\n jump block3\nblock1:\n trycatch ZeroDivisionError\n store __printlit25 : str[25]\n %26 = const_i64 0\n %27 = const_i64 99\n IndexStore __printlit25, %26, %27\n %28 = const_i64 1\n %29 = const_i64 97\n IndexStore __printlit25, %28, %29\n %30 = const_i64 2\n %31 = const_i64 117\n IndexStore __printlit25, %30, %31\n %32 = const_i64 3\n %33 = const_i64 103\n IndexStore __printlit25, %32, %33\n %34 = const_i64 4\n %35 = const_i64 104\n IndexStore __printlit25, %34, %35\n %36 = const_i64 5\n %37 = const_i64 116\n IndexStore __printlit25, %36, %37\n %38 = const_i64 6\n %39 = const_i64 32\n IndexStore __printlit25, %38, %39\n %40 = const_i64 7\n %41 = const_i64 90\n IndexStore __printlit25, %40, %41\n %42 = const_i64 8\n %43 = const_i64 101\n IndexStore __printlit25, %42, %43\n %44 = const_i64 9\n %45 = const_i64 114\n IndexStore __printlit25, %44, %45\n %46 = const_i64 10\n %47 = const_i64 111\n IndexStore __printlit25, %46, %47\n %48 = const_i64 11\n %49 = const_i64 68\n IndexStore __printlit25, %48, %49\n %50 = const_i64 12\n %51 = const_i64 105\n IndexStore __printlit25, %50, %51\n %52 = const_i64 13\n %53 = const_i64 118\n IndexStore __printlit25, %52, %53\n %54 = const_i64 14\n %55 = const_i64 105\n IndexStore __printlit25, %54, %55\n %56 = const_i64 15\n %57 = const_i64 115\n IndexStore __printlit25, %56, %57\n %58 = const_i64 16\n %59 = const_i64 105\n IndexStore __printlit25, %58, %59\n %60 = const_i64 17\n %61 = const_i64 111\n IndexStore __printlit25, %60, %61\n %62 = const_i64 18\n %63 = const_i64 110\n IndexStore __printlit25, %62, %63\n %64 = const_i64 19\n %65 = const_i64 69\n IndexStore __printlit25, %64, %65\n %66 = const_i64 20\n %67 = const_i64 114\n IndexStore __printlit25, %66, %67\n %68 = const_i64 21\n %69 = const_i64 114\n IndexStore __printlit25, %68, %69\n %70 = const_i64 22\n %71 = const_i64 111\n IndexStore __printlit25, %70, %71\n %72 = const_i64 23\n %73 = const_i64 114\n IndexStore __printlit25, %72, %73\n print_container __printlit25 : str[25]\n call print_nl\n jump block3\nblock3:\n return", + "4bba6250": "function __main__():\nblock0:\n store xs : list[int[64], 3]\n %0 = const_i64 3\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 10\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 20\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 30\n IndexStore xs, %5, %6\n %7 = const_i64 9\n store k, %7 : int[64]\n tryenter IndexError, block1\n jump block2\nblock2:\n %9 = load k\n %8 = Index xs, %9\n store v, %8 : int[64]\n store __printlit12 : str[12]\n %10 = const_i64 0\n %11 = const_i64 110\n IndexStore __printlit12, %10, %11\n %12 = const_i64 1\n %13 = const_i64 111\n IndexStore __printlit12, %12, %13\n %14 = const_i64 2\n %15 = const_i64 116\n IndexStore __printlit12, %14, %15\n %16 = const_i64 3\n %17 = const_i64 32\n IndexStore __printlit12, %16, %17\n %18 = const_i64 4\n %19 = const_i64 114\n IndexStore __printlit12, %18, %19\n %20 = const_i64 5\n %21 = const_i64 101\n IndexStore __printlit12, %20, %21\n %22 = const_i64 6\n %23 = const_i64 97\n IndexStore __printlit12, %22, %23\n %24 = const_i64 7\n %25 = const_i64 99\n IndexStore __printlit12, %24, %25\n %26 = const_i64 8\n %27 = const_i64 104\n IndexStore __printlit12, %26, %27\n %28 = const_i64 9\n %29 = const_i64 101\n IndexStore __printlit12, %28, %29\n %30 = const_i64 10\n %31 = const_i64 100\n IndexStore __printlit12, %30, %31\n print_container __printlit12 : str[12]\n call print_nl\n tryleave\n jump block3\nblock1:\n trycatch IndexError\n store __printlit18 : str[18]\n %32 = const_i64 0\n %33 = const_i64 99\n IndexStore __printlit18, %32, %33\n %34 = const_i64 1\n %35 = const_i64 97\n IndexStore __printlit18, %34, %35\n %36 = const_i64 2\n %37 = const_i64 117\n IndexStore __printlit18, %36, %37\n %38 = const_i64 3\n %39 = const_i64 103\n IndexStore __printlit18, %38, %39\n %40 = const_i64 4\n %41 = const_i64 104\n IndexStore __printlit18, %40, %41\n %42 = const_i64 5\n %43 = const_i64 116\n IndexStore __printlit18, %42, %43\n %44 = const_i64 6\n %45 = const_i64 32\n IndexStore __printlit18, %44, %45\n %46 = const_i64 7\n %47 = const_i64 73\n IndexStore __printlit18, %46, %47\n %48 = const_i64 8\n %49 = const_i64 110\n IndexStore __printlit18, %48, %49\n %50 = const_i64 9\n %51 = const_i64 100\n IndexStore __printlit18, %50, %51\n %52 = const_i64 10\n %53 = const_i64 101\n IndexStore __printlit18, %52, %53\n %54 = const_i64 11\n %55 = const_i64 120\n IndexStore __printlit18, %54, %55\n %56 = const_i64 12\n %57 = const_i64 69\n IndexStore __printlit18, %56, %57\n %58 = const_i64 13\n %59 = const_i64 114\n IndexStore __printlit18, %58, %59\n %60 = const_i64 14\n %61 = const_i64 114\n IndexStore __printlit18, %60, %61\n %62 = const_i64 15\n %63 = const_i64 111\n IndexStore __printlit18, %62, %63\n %64 = const_i64 16\n %65 = const_i64 114\n IndexStore __printlit18, %64, %65\n print_container __printlit18 : str[18]\n call print_nl\n jump block3\nblock3:\n return", + "edde7b68": "function __main__():\nblock0:\n tryenter IndexError+ZeroDivisionError, block1+block2\n jump block3\nblock3:\n %0 = const_i64 5\n %1 = const_i64 0\n %2 = mod %0, %1\n store y, %2 : int[64]\n tryleave\n jump block4\nblock1:\n trycatch IndexError\n store __printlit10 : str[10]\n %3 = const_i64 0\n %4 = const_i64 119\n IndexStore __printlit10, %3, %4\n %5 = const_i64 1\n %6 = const_i64 114\n IndexStore __printlit10, %5, %6\n %7 = const_i64 2\n %8 = const_i64 111\n IndexStore __printlit10, %7, %8\n %9 = const_i64 3\n %10 = const_i64 110\n IndexStore __printlit10, %9, %10\n %11 = const_i64 4\n %12 = const_i64 103\n IndexStore __printlit10, %11, %12\n %13 = const_i64 5\n %14 = const_i64 32\n IndexStore __printlit10, %13, %14\n %15 = const_i64 6\n %16 = const_i64 97\n IndexStore __printlit10, %15, %16\n %17 = const_i64 7\n %18 = const_i64 114\n IndexStore __printlit10, %17, %18\n %19 = const_i64 8\n %20 = const_i64 109\n IndexStore __printlit10, %19, %20\n print_container __printlit10 : str[10]\n call print_nl\n jump block4\nblock2:\n trycatch ZeroDivisionError\n store __printlit25 : str[25]\n %21 = const_i64 0\n %22 = const_i64 99\n IndexStore __printlit25, %21, %22\n %23 = const_i64 1\n %24 = const_i64 97\n IndexStore __printlit25, %23, %24\n %25 = const_i64 2\n %26 = const_i64 117\n IndexStore __printlit25, %25, %26\n %27 = const_i64 3\n %28 = const_i64 103\n IndexStore __printlit25, %27, %28\n %29 = const_i64 4\n %30 = const_i64 104\n IndexStore __printlit25, %29, %30\n %31 = const_i64 5\n %32 = const_i64 116\n IndexStore __printlit25, %31, %32\n %33 = const_i64 6\n %34 = const_i64 32\n IndexStore __printlit25, %33, %34\n %35 = const_i64 7\n %36 = const_i64 90\n IndexStore __printlit25, %35, %36\n %37 = const_i64 8\n %38 = const_i64 101\n IndexStore __printlit25, %37, %38\n %39 = const_i64 9\n %40 = const_i64 114\n IndexStore __printlit25, %39, %40\n %41 = const_i64 10\n %42 = const_i64 111\n IndexStore __printlit25, %41, %42\n %43 = const_i64 11\n %44 = const_i64 68\n IndexStore __printlit25, %43, %44\n %45 = const_i64 12\n %46 = const_i64 105\n IndexStore __printlit25, %45, %46\n %47 = const_i64 13\n %48 = const_i64 118\n IndexStore __printlit25, %47, %48\n %49 = const_i64 14\n %50 = const_i64 105\n IndexStore __printlit25, %49, %50\n %51 = const_i64 15\n %52 = const_i64 115\n IndexStore __printlit25, %51, %52\n %53 = const_i64 16\n %54 = const_i64 105\n IndexStore __printlit25, %53, %54\n %55 = const_i64 17\n %56 = const_i64 111\n IndexStore __printlit25, %55, %56\n %57 = const_i64 18\n %58 = const_i64 110\n IndexStore __printlit25, %57, %58\n %59 = const_i64 19\n %60 = const_i64 69\n IndexStore __printlit25, %59, %60\n %61 = const_i64 20\n %62 = const_i64 114\n IndexStore __printlit25, %61, %62\n %63 = const_i64 21\n %64 = const_i64 114\n IndexStore __printlit25, %63, %64\n %65 = const_i64 22\n %66 = const_i64 111\n IndexStore __printlit25, %65, %66\n %67 = const_i64 23\n %68 = const_i64 114\n IndexStore __printlit25, %67, %68\n print_container __printlit25 : str[25]\n call print_nl\n jump block4\nblock4:\n return", + "1e557694": "function __main__():\nblock0:\n %0 = const_i64 4\n store n, %0 : int[64]\n tryenter ZeroDivisionError, block1\n jump block2\nblock2:\n %1 = const_i64 100\n %2 = load n\n %3 = mod %1, %2\n store r, %3 : int[64]\n tryleave\n jump block3\nblock3:\n store __printlit9 : str[9]\n %4 = const_i64 0\n %5 = const_i64 101\n IndexStore __printlit9, %4, %5\n %6 = const_i64 1\n %7 = const_i64 108\n IndexStore __printlit9, %6, %7\n %8 = const_i64 2\n %9 = const_i64 115\n IndexStore __printlit9, %8, %9\n %10 = const_i64 3\n %11 = const_i64 101\n IndexStore __printlit9, %10, %11\n %12 = const_i64 4\n %13 = const_i64 32\n IndexStore __printlit9, %12, %13\n %14 = const_i64 5\n %15 = const_i64 114\n IndexStore __printlit9, %14, %15\n %16 = const_i64 6\n %17 = const_i64 97\n IndexStore __printlit9, %16, %17\n %18 = const_i64 7\n %19 = const_i64 110\n IndexStore __printlit9, %18, %19\n print_container __printlit9 : str[9]\n call print_nl\n jump block4\nblock1:\n trycatch ZeroDivisionError\n store __printlit5 : str[5]\n %20 = const_i64 0\n %21 = const_i64 122\n IndexStore __printlit5, %20, %21\n %22 = const_i64 1\n %23 = const_i64 101\n IndexStore __printlit5, %22, %23\n %24 = const_i64 2\n %25 = const_i64 114\n IndexStore __printlit5, %24, %25\n %26 = const_i64 3\n %27 = const_i64 111\n IndexStore __printlit5, %26, %27\n print_container __printlit5 : str[5]\n call print_nl\n jump block4\nblock4:\n store __printlit12 : str[12]\n %28 = const_i64 0\n %29 = const_i64 102\n IndexStore __printlit12, %28, %29\n %30 = const_i64 1\n %31 = const_i64 105\n IndexStore __printlit12, %30, %31\n %32 = const_i64 2\n %33 = const_i64 110\n IndexStore __printlit12, %32, %33\n %34 = const_i64 3\n %35 = const_i64 97\n IndexStore __printlit12, %34, %35\n %36 = const_i64 4\n %37 = const_i64 108\n IndexStore __printlit12, %36, %37\n %38 = const_i64 5\n %39 = const_i64 108\n IndexStore __printlit12, %38, %39\n %40 = const_i64 6\n %41 = const_i64 121\n IndexStore __printlit12, %40, %41\n %42 = const_i64 7\n %43 = const_i64 32\n IndexStore __printlit12, %42, %43\n %44 = const_i64 8\n %45 = const_i64 114\n IndexStore __printlit12, %44, %45\n %46 = const_i64 9\n %47 = const_i64 97\n IndexStore __printlit12, %46, %47\n %48 = const_i64 10\n %49 = const_i64 110\n IndexStore __printlit12, %48, %49\n print_container __printlit12 : str[12]\n call print_nl\n jump block5\nblock5:\n return", + "cd093510": "function __main__():\nblock0:\n %0 = const_i64 0\n store n, %0 : int[64]\n tryenter ZeroDivisionError, block1\n jump block2\nblock2:\n %1 = const_i64 100\n %2 = load n\n %3 = mod %1, %2\n store r, %3 : int[64]\n tryleave\n jump block3\nblock3:\n store __printlit9 : str[9]\n %4 = const_i64 0\n %5 = const_i64 101\n IndexStore __printlit9, %4, %5\n %6 = const_i64 1\n %7 = const_i64 108\n IndexStore __printlit9, %6, %7\n %8 = const_i64 2\n %9 = const_i64 115\n IndexStore __printlit9, %8, %9\n %10 = const_i64 3\n %11 = const_i64 101\n IndexStore __printlit9, %10, %11\n %12 = const_i64 4\n %13 = const_i64 32\n IndexStore __printlit9, %12, %13\n %14 = const_i64 5\n %15 = const_i64 114\n IndexStore __printlit9, %14, %15\n %16 = const_i64 6\n %17 = const_i64 97\n IndexStore __printlit9, %16, %17\n %18 = const_i64 7\n %19 = const_i64 110\n IndexStore __printlit9, %18, %19\n print_container __printlit9 : str[9]\n call print_nl\n jump block4\nblock1:\n trycatch ZeroDivisionError\n store __printlit5 : str[5]\n %20 = const_i64 0\n %21 = const_i64 122\n IndexStore __printlit5, %20, %21\n %22 = const_i64 1\n %23 = const_i64 101\n IndexStore __printlit5, %22, %23\n %24 = const_i64 2\n %25 = const_i64 114\n IndexStore __printlit5, %24, %25\n %26 = const_i64 3\n %27 = const_i64 111\n IndexStore __printlit5, %26, %27\n print_container __printlit5 : str[5]\n call print_nl\n jump block4\nblock4:\n store __printlit12 : str[12]\n %28 = const_i64 0\n %29 = const_i64 102\n IndexStore __printlit12, %28, %29\n %30 = const_i64 1\n %31 = const_i64 105\n IndexStore __printlit12, %30, %31\n %32 = const_i64 2\n %33 = const_i64 110\n IndexStore __printlit12, %32, %33\n %34 = const_i64 3\n %35 = const_i64 97\n IndexStore __printlit12, %34, %35\n %36 = const_i64 4\n %37 = const_i64 108\n IndexStore __printlit12, %36, %37\n %38 = const_i64 5\n %39 = const_i64 108\n IndexStore __printlit12, %38, %39\n %40 = const_i64 6\n %41 = const_i64 121\n IndexStore __printlit12, %40, %41\n %42 = const_i64 7\n %43 = const_i64 32\n IndexStore __printlit12, %42, %43\n %44 = const_i64 8\n %45 = const_i64 114\n IndexStore __printlit12, %44, %45\n %46 = const_i64 9\n %47 = const_i64 97\n IndexStore __printlit12, %46, %47\n %48 = const_i64 10\n %49 = const_i64 110\n IndexStore __printlit12, %48, %49\n print_container __printlit12 : str[12]\n call print_nl\n jump block5\nblock5:\n return", + "b4b8b226": "function __main__():\nblock0:\n store s : struct[9]\n %0 = const_i64 2\n fieldstore s, 0, %0 : int[8]\n %1 = const_i64 42\n fieldstore s, 1, %1 : int[32]\n %2 = fieldload s, 0 : int[8]\n %3 = const_i64 0\n %4 = eq %2, %3\n branch %4, block2, block3\nblock2:\n %5 = const_i64 0\n call print_bare, %5\n call print_nl\n jump block1\nblock3:\n %6 = const_i64 1\n %7 = eq %2, %6\n branch %7, block4, block5\nblock4:\n %8 = const_i64 1\n call print_bare, %8\n call print_nl\n jump block1\nblock5:\n %9 = const_i64 2\n %10 = eq %2, %9\n branch %10, block6, block7\nblock6:\n %11 = fieldload s, 1 : int[32]\n store n, %11 : int[32]\n %12 = load n\n call print_bare, %12\n call print_nl\n jump block1\nblock7:\n %13 = const_i64 3\n %14 = eq %2, %13\n branch %14, block8, block1\nblock8:\n %15 = fieldload s, 1 : int[64]\n store w, %15 : int[64]\n %16 = load w\n call print_bare, %16\n call print_nl\n jump block1\nblock1:\n return", + "b5b6a425": "function __main__():\nblock0:\n store c : struct[7]\n %0 = const_i64 1\n fieldstore c, 0, %0 : int[8]\n %1 = const_i64 8080\n fieldstore c, 1, %1 : int[16]\n %2 = const_i64 16777343\n fieldstore c, 3, %2 : int[32]\n %3 = fieldload c, 0 : int[8]\n %4 = const_i64 1\n %5 = eq %3, %4\n branch %5, block2, block3\nblock2:\n %6 = fieldload c, 1 : int[16]\n store p, %6 : int[16]\n %7 = fieldload c, 3 : int[32]\n store a, %7 : int[32]\n %8 = load p\n call print_bare, %8\n call print_nl\n %9 = load a\n call print_bare, %9\n call print_nl\n jump block1\nblock3:\n %10 = const_i64 2\n %11 = eq %3, %10\n branch %11, block4, block5\nblock4:\n %12 = fieldload c, 1 : int[32]\n store n, %12 : int[32]\n %13 = load n\n call print_bare, %13\n call print_nl\n jump block1\nblock5:\n %14 = const_i64 0\n %15 = eq %3, %14\n branch %15, block6, block1\nblock6:\n %16 = const_i64 -1\n call print_bare, %16\n call print_nl\n jump block1\nblock1:\n return", + "6e737b87": "function __main__():\nblock0:\n store c : struct[15]\n %0 = const_i64 2\n fieldstore c, 0, %0 : int[8]\n %1 = const_i64 53\n fieldstore c, 1, %1 : int[32]\n %2 = const_i64 7\n fieldstore c, 7, %2 : int[64]\n %3 = fieldload c, 0 : int[8]\n %4 = const_i64 1\n %5 = eq %3, %4\n branch %5, block2, block3\nblock2:\n %6 = fieldload c, 1 : int[16]\n store p, %6 : int[16]\n %7 = fieldload c, 3 : int[32]\n store a, %7 : int[32]\n %8 = load p\n call print_bare, %8\n call print_nl\n jump block1\nblock3:\n %9 = const_i64 2\n %10 = eq %3, %9\n branch %10, block4, block5\nblock4:\n %11 = fieldload c, 1 : int[32]\n store n, %11 : int[32]\n %12 = load n\n call print_bare, %12\n call print_nl\n jump block1\nblock5:\n %13 = const_i64 0\n %14 = eq %3, %13\n branch %14, block6, block1\nblock6:\n %15 = const_i64 -1\n call print_bare, %15\n call print_nl\n jump block1\nblock1:\n %16 = fieldload c, 7 : int[64]\n call print_bare, %16\n call print_nl\n return", + "4d65b8b5": "function __main__():\nblock0:\n store s : str[16]\n %0 = const_i64 0\n %1 = const_i64 104\n IndexStore s, %0, %1\n %2 = const_i64 1\n %3 = const_i64 101\n IndexStore s, %2, %3\n %4 = const_i64 2\n %5 = const_i64 108\n IndexStore s, %4, %5\n %6 = const_i64 3\n %7 = const_i64 108\n IndexStore s, %6, %7\n %8 = const_i64 4\n %9 = const_i64 111\n IndexStore s, %8, %9\n print_container s : str[16]\n call print_nl\n %10 = strlen s\n call print_bare, %10\n call print_nl\n return", + "624191fe": "function __main__():\nblock0:\n store h : struct[10]\n store h.kind : str[8]\n %0 = const_i64 0\n %1 = const_i64 72\n IndexStore h.kind, %0, %1\n %2 = const_i64 1\n %3 = const_i64 84\n IndexStore h.kind, %2, %3\n %4 = const_i64 2\n %5 = const_i64 84\n IndexStore h.kind, %4, %5\n %6 = const_i64 3\n %7 = const_i64 80\n IndexStore h.kind, %6, %7\n %8 = const_i64 4\n %9 = const_i64 0\n IndexStore h.kind, %8, %9\n %10 = const_i64 5\n %11 = const_i64 0\n IndexStore h.kind, %10, %11\n %12 = const_i64 6\n %13 = const_i64 0\n IndexStore h.kind, %12, %13\n %14 = const_i64 7\n %15 = const_i64 0\n IndexStore h.kind, %14, %15\n %16 = const_i64 11\n fieldstore h, 8, %16 : int[16]\n print_container h.kind : str[8]\n call print_nl\n %17 = fieldload h, 8 : int[16]\n call print_bare, %17\n call print_nl\n return", + "17e2f9b8": "function __main__():\nblock0:\n store s : str[32]\n %0 = const_i64 0\n %1 = const_i64 104\n IndexStore s, %0, %1\n %2 = const_i64 1\n %3 = const_i64 101\n IndexStore s, %2, %3\n %4 = const_i64 2\n %5 = const_i64 108\n IndexStore s, %4, %5\n %6 = const_i64 3\n %7 = const_i64 108\n IndexStore s, %6, %7\n %8 = const_i64 4\n %9 = const_i64 111\n IndexStore s, %8, %9\n %10 = const_i64 5\n %11 = const_i64 32\n IndexStore s, %10, %11\n %12 = const_i64 6\n %13 = const_i64 119\n IndexStore s, %12, %13\n %14 = const_i64 7\n %15 = const_i64 111\n IndexStore s, %14, %15\n %16 = const_i64 8\n %17 = const_i64 114\n IndexStore s, %16, %17\n %18 = const_i64 9\n %19 = const_i64 108\n IndexStore s, %18, %19\n %20 = const_i64 10\n %21 = const_i64 100\n IndexStore s, %20, %21\n store __printlit6 : str[6]\n %22 = const_i64 0\n %23 = const_i64 119\n IndexStore __printlit6, %22, %23\n %24 = const_i64 1\n %25 = const_i64 111\n IndexStore __printlit6, %24, %25\n %26 = const_i64 2\n %27 = const_i64 114\n IndexStore __printlit6, %26, %27\n %28 = const_i64 3\n %29 = const_i64 108\n IndexStore __printlit6, %28, %29\n %30 = const_i64 4\n %31 = const_i64 100\n IndexStore __printlit6, %30, %31\n %32 = strfind s, __printlit6\n call print_bare, %32\n call print_nl\n store __printlit4 : str[4]\n %33 = const_i64 0\n %34 = const_i64 122\n IndexStore __printlit4, %33, %34\n %35 = const_i64 1\n %36 = const_i64 122\n IndexStore __printlit4, %35, %36\n %37 = const_i64 2\n %38 = const_i64 122\n IndexStore __printlit4, %37, %38\n %39 = strfind s, __printlit4\n call print_bare, %39\n call print_nl\n store __printlit2 : str[2]\n %40 = const_i64 0\n %41 = const_i64 108\n IndexStore __printlit2, %40, %41\n %42 = strcount s, __printlit2\n call print_bare, %42\n call print_nl\n return", + "d309f81f": "function __main__():\nblock0:\n store s : str[32]\n %0 = const_i64 0\n %1 = const_i64 97\n IndexStore s, %0, %1\n %2 = const_i64 1\n %3 = const_i64 44\n IndexStore s, %2, %3\n %4 = const_i64 2\n %5 = const_i64 98\n IndexStore s, %4, %5\n %6 = const_i64 3\n %7 = const_i64 44\n IndexStore s, %6, %7\n %8 = const_i64 4\n %9 = const_i64 99\n IndexStore s, %8, %9\n store parts : list[str[8], 4]\n store __printlit2 : str[2]\n %10 = const_i64 0\n %11 = const_i64 44\n IndexStore __printlit2, %10, %11\n %12 = strsplit s, __printlit2, parts : list[str[8], 4]\n store parts.__len, %12 : int[64]\n %14 = const_i64 0\n %13 = Index parts, %14\n call print_bare, %13\n call print_nl\n %16 = const_i64 2\n %15 = Index parts, %16\n call print_bare, %15\n call print_nl\n return", + "1a956938": "function __main__():\nblock0:\n store s : str[16]\n %0 = const_i64 0\n %1 = const_i64 112\n IndexStore s, %0, %1\n %2 = const_i64 1\n %3 = const_i64 105\n IndexStore s, %2, %3\n %4 = const_i64 2\n %5 = const_i64 110\n IndexStore s, %4, %5\n %6 = const_i64 3\n %7 = const_i64 103\n IndexStore s, %6, %7\n %8 = addressof s\n store _p, %8 : ptr[str[16]]\n print_container s : str[16]\n call print_nl\n return", + "36e6a0e3": "function __main__():\nblock0:\n %0 = const_i64 41\n %1 = const_i64 2\n %2 = const_i64 1\n %3 = const_i64 0\n %4 = syscall %0, %1, %2, %3\n store srv, %4 : int[64]\n %5 = load srv\n %6 = const_i64 0\n %7 = gt %5, %6\n call print_bare, %7\n call print_nl\n return", + "ef726b1a": "function __main__():\nblock0:\n store sa : struct[16]\n %0 = const_i64 2\n %1 = const_i64 2130706433\n %2 = call swap32, %1\n %3 = const_i64 32\n %4 = shl %2, %3\n %5 = bor %0, %4\n fieldstore sa, 0, %5 : int[64]\n %6 = const_i64 0\n fieldstore sa, 8, %6 : int[64]\n %7 = addressof sa\n store _sa, %7 : ptr[struct[16]]\n %8 = const_i64 16\n store socklen, %8 : int[64]\n %9 = addressof socklen\n store _psocklen, %9 : ptr[int[64]]\n %10 = const_i64 41\n %11 = const_i64 2\n %12 = const_i64 1\n %13 = const_i64 0\n %14 = syscall %10, %11, %12, %13\n store srv, %14 : int[64]\n %15 = const_i64 49\n %16 = load srv\n %17 = load _sa\n %18 = const_i64 16\n %19 = syscall %15, %16, %17, %18\n call print_bare, %19\n call print_nl\n %20 = const_i64 50\n %21 = load srv\n %22 = const_i64 1\n %23 = syscall %20, %21, %22\n call print_bare, %23\n call print_nl\n return", + "1e745a0c": "function __main__():\nblock0:\n store out : struct[8]\n %0 = const_i64 123456789\n fieldstore out, 0, %0 : int[64]\n store inb : struct[8]\n %1 = const_i64 0\n fieldstore inb, 0, %1 : int[64]\n %2 = addressof out\n store _pout, %2 : ptr[struct[8]]\n %3 = addressof inb\n store _pin, %3 : ptr[struct[8]]\n %4 = const_i64 44\n %5 = load cli\n %6 = load _pout\n %7 = const_i64 8\n %8 = const_i64 0\n %9 = const_i64 0\n %10 = const_i64 0\n %11 = syscall %4, %5, %6, %7, %8, %9, %10\n call print_bare, %11\n call print_nl\n %12 = const_i64 45\n %13 = load conn\n %14 = load _pin\n %15 = const_i64 8\n %16 = const_i64 0\n %17 = const_i64 0\n %18 = const_i64 0\n %19 = syscall %12, %13, %14, %15, %16, %17, %18\n call print_bare, %19\n call print_nl\n %20 = fieldload inb, 0 : int[64]\n call print_bare, %20\n call print_nl\n return", + "e4dbbe1c": "function __main__():\nblock0:\n %0 = const_i64 16\n store socklen, %0 : int[64]\n %1 = addressof socklen\n store _psocklen, %1 : ptr[int[64]]\n %2 = const_i64 51\n %3 = load srv\n %4 = load _sa\n %5 = load _psocklen\n %6 = syscall %2, %3, %4, %5\n call print_bare, %6\n call print_nl\n %7 = load socklen\n call print_bare, %7\n call print_nl\n return", + "9668c6d6": "function swap32(v:int[64]) -> int[64]:\nblock0:\n %0 = load v\n %1 = const_i64 255\n %2 = band %0, %1\n %3 = const_i64 24\n %4 = shl %2, %3\n %5 = load v\n %6 = const_i64 65280\n %7 = band %5, %6\n %8 = const_i64 8\n %9 = shl %7, %8\n %10 = bor %4, %9\n %11 = load v\n %12 = const_i64 8\n %13 = shr %11, %12\n %14 = const_i64 65280\n %15 = band %13, %14\n %16 = bor %10, %15\n %17 = load v\n %18 = const_i64 24\n %19 = shr %17, %18\n %20 = const_i64 255\n %21 = band %19, %20\n %22 = bor %16, %21\n return %22\nfunction __main__():\nblock0:\n return", + "9714cf2d": "function __main__():\nblock0:\n store bad : struct[16]\n %0 = const_i64 2\n %1 = const_i64 2130706433\n %2 = const_i64 32\n %3 = shl %1, %2\n %4 = bor %0, %3\n fieldstore bad, 0, %4 : int[64]\n %5 = const_i64 0\n fieldstore bad, 8, %5 : int[64]\n %6 = addressof bad\n store _bad, %6 : ptr[struct[16]]\n %7 = const_i64 41\n %8 = const_i64 2\n %9 = const_i64 1\n %10 = const_i64 0\n %11 = syscall %7, %8, %9, %10\n store badfd, %11 : int[64]\n %12 = const_i64 49\n %13 = load badfd\n %14 = load _bad\n %15 = const_i64 16\n %16 = syscall %12, %13, %14, %15\n %17 = const_i64 0\n %18 = lt %16, %17\n call print_bare, %18\n call print_nl\n %19 = const_i64 3\n %20 = load badfd\n %21 = syscall %19, %20\n call print_bare, %21\n call print_nl\n return", + "3b8022da": "function __main__():\nblock0:\n store xs : list[int[64], 3]\n %0 = const_i64 3\n store xs.__len, %0 : int[64]\n store ys : list[int[64], 3]\n %1 = const_i64 3\n store ys.__len, %1 : int[64]\n %2 = const_i64 0\n %3 = const_i64 10\n IndexStore xs, %2, %3\n %4 = const_i64 1\n %5 = const_i64 20\n IndexStore xs, %4, %5\n %6 = const_i64 2\n %7 = const_i64 30\n IndexStore xs, %6, %7\n %8 = const_i64 0\n %9 = const_i64 1\n IndexStore ys, %8, %9\n %10 = const_i64 1\n %11 = const_i64 2\n IndexStore ys, %10, %11\n %12 = const_i64 2\n %13 = const_i64 3\n IndexStore ys, %12, %13\n %14 = const_i64 0\n %15 = const_i64 3\n %16 = const_i64 1\n store i, %14 : int[64]\n jump block4\nblock4:\n %17 = load i\n %18 = lt %17, %15\n branch %18, block1, block2\nblock1:\n %19 = load i\n %20 = Index xs, %19\n store x, %20 : int[64]\n %21 = load i\n call print_bare, %21\n call print_nl\n jump block3\nblock3:\n %22 = load i\n %23 = const_i64 1\n %24 = add %22, %23\n store i, %24\n jump block4\nblock2:\n %25 = const_i64 0\n %26 = const_i64 3\n %27 = const_i64 1\n store zip_i0, %25 : int[64]\n jump block8\nblock8:\n %28 = load zip_i0\n %29 = lt %28, %26\n branch %29, block5, block6\nblock5:\n %30 = load zip_i0\n %31 = Index xs, %30\n store a, %31 : int[64]\n %32 = Index ys, %30\n store b, %32 : int[64]\n %33 = load a\n call print_bare, %33\n call print_nl\n jump block7\nblock7:\n %34 = load zip_i0\n %35 = const_i64 1\n %36 = add %34, %35\n store zip_i0, %36\n jump block8\nblock6:\n return", + "9664cb2a": "function __main__():\nblock0:\n %0 = const_i64 1\n store a, %0 : int[64]\n %1 = const_i64 2\n store b, %1 : int[64]\n %2 = load a\n call print_bare, %2\n call print_sep\n %3 = load b\n call print_bare, %3\n call print_sep\n store __printlit2 : str[2]\n %4 = const_i64 0\n %5 = const_i64 120\n IndexStore __printlit2, %4, %5\n print_container __printlit2 : str[2]\n call print_nl\n return", + "b4973ce3": "function __main__():\nblock0:\n %0 = const_i64 9223372036854775807\n store x, %0 : int[64]\n %1 = const_i64 1\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = wrapadd %2, %3\n store z, %4 : int[64]\n %5 = load z\n call print_bare, %5\n call print_nl\n return", + "8b9a4015": "function __main__():\nblock0:\n %0 = const_i64 -7\n %1 = const_i64 3\n %2 = mod %0, %1\n call print_bare, %2\n call print_nl\n %3 = const_i64 7\n %4 = const_i64 -3\n %5 = mod %3, %4\n call print_bare, %5\n call print_nl\n return", + "e367543b": "function __main__():\nblock0:\n %0 = const_i64 5\n %1 = const_i64 63\n %2 = shl %0, %1\n call print_bare, %2\n call print_nl\n return" } diff --git a/web/apps/web/src/docs/model.ts b/web/apps/web/src/docs/model.ts index c6b0eb0..63a3527 100644 --- a/web/apps/web/src/docs/model.ts +++ b/web/apps/web/src/docs/model.ts @@ -52,6 +52,56 @@ export type Block = kind: "linkcards" items: readonly { title: string; text: string; href: string }[] } + /** + * A highlighted "remember this" box. Unlike `note`, a callout has no tone + * axis: it is always the single most important idea on the page, so it + * reads as a statement of fact rather than a caveat. + */ + | { kind: "callout"; title: string; text: string } + /** + * A page-level tabset: the same concept shown several ways side by side + * (CPython vs Lithon, one tier vs another, one width vs another). + */ + | { + kind: "tabs" + caption?: string + items: readonly { + label: string + blocks: readonly Block[] + }[] + } + /** + * A three-column comparison table. The middle column is the "answer" + * (usually Lithon) and is rendered with emphasis. + */ + | { + kind: "comparison" + caption?: string + columns: readonly [string, string, string] + rows: readonly (readonly [string, string, string])[] + } + /** + * A definition list: term on the left, description on the right, no + * header row. Used for API-shaped content (builtins, error codes, IR ops). + */ + | { + kind: "deflist" + caption?: string + items: readonly { term: string; detail: string }[] + } + /** + * A byte-layout diagram: a run of fields with their offsets and sizes. + */ + | { + kind: "layout" + caption?: string + size: string + fields: readonly { name: string; offset: string; size: string }[] + } + /** + * A quoted principle or rule, set apart from the surrounding prose. + */ + | { kind: "quote"; text: string; source?: string } export type DocSection = { id: string @@ -59,6 +109,9 @@ export type DocSection = { blocks: readonly Block[] } +/** How prominently a page should be surfaced on the docs index. */ +export type PagePriority = "essential" | "recommended" | "reference" + export type DocPage = { slug: string title: string @@ -66,6 +119,15 @@ export type DocPage = { group: string tags: readonly string[] sections: readonly DocSection[] + /** + * Index-page emphasis. `essential` pages are the reading path and are + * listed first; `reference` pages are looked up, not read end to end. + */ + priority?: PagePriority + /** One-line "what you will be able to do after this" summary. */ + outcome?: string + /** Minutes of reading time, used as a "short read" marker on the index. */ + minutes?: number } export type DocGroup = { @@ -118,6 +180,11 @@ export type SearchEntry = { group?: string tags: readonly string[] href: string + /** + * The searchable body text. Populated for pages and sections so a query + * can find an error code or an operator that only appears in the prose. + */ + body?: string } export type ActionEntry = { diff --git a/web/apps/web/src/docs/pages/divergences.ts b/web/apps/web/src/docs/pages/divergences.ts new file mode 100644 index 0000000..e0fcb9c --- /dev/null +++ b/web/apps/web/src/docs/pages/divergences.ts @@ -0,0 +1,490 @@ +import type { DocPage } from "../model" + +/** + * Where Lithon deliberately differs from CPython. + * + * Every row here is a difference you can trip over. The rule behind all of + * them: Lithon is statically typed and fixed-width, so anything CPython gets + * from dynamic dispatch or arbitrary precision has to be spelled explicitly. + */ +const divergences: DocPage = { + slug: "python-divergences", + title: "Divergences from Python", + description: + "Every place Lithon deliberately behaves differently from CPython, and why: modulo, division, shifts, narrowing, truthiness, strings, exceptions.", + group: "language", + tags: [ + "divergences", + "python", + "cpython", + "differences", + "semantics", + "gotchas", + "porting", + "migration", + ], + priority: "recommended", + outcome: "Port Python code to Lithon without being surprised by a silent semantic change.", + minutes: 12, + sections: [ + { + id: "the-short-list", + title: "The short list", + blocks: [ + { + kind: "p", + text: "Lithon's source syntax is Python's. Its *semantics* are not, in eleven specific places, all of which come from the same two decisions: **fixed-width values** and **no dynamic dispatch**. This page is the list; each row links to the page that explains it in full.", + }, + { + kind: "comparison", + caption: "the eleven differences that will bite you", + columns: ["Behaviour", "CPython", "Lithon"], + rows: [ + [ + "Modulo with a negative dividend", + "`-7 % 3 == 2`", + "`-7 % 3 == -1` — C-style, the sign follows the dividend", + ], + [ + "Integer division", + "`7 / 2 == 3.5`", + "`7 / 2 == 3.5` — same, but `//` is refused outright", + ], + [ + "Narrowing an integer", + "always allowed", + "refused: `int[8]` never becomes `int[64]` implicitly", + ], + [ + "Float to int", + "`int(3.7) == 3`", + "`int(64, 3.7) == 3` — the width has to be named", + ], + [ + "Truthiness", + "`if 0:` is False, `if \"\":` is False", + "refused: a condition must be a `bool`", + ], + [ + "Strings", + "unbounded, heap-allocated, `+` concatenates", + "bounded `str[N]` byte runs; `+` is refused", + ], + [ + "Exceptions", + "raise anything, catch by class", + "seven named engine traps; no `raise`, no exception objects", + ], + [ + "Unassigned variables", + "`NameError` at run time", + "refused at compile time: every name needs a type and a value", + ], + [ + "Recursion", + "bounded by the C stack", + "self tail calls become loops; a stack guard traps at 4096", + ], + [ + "Function arguments", + "positional, keyword, `*args`, `**kwargs`, defaults", + "positional only, fixed arity, no defaults, no keywords", + ], + [ + "Comparisons", + "`a <= b`, chained `a < b < c`", + "only `<`, `>`, `==`, `!=`; no chaining", + ], + ], + }, + { + kind: "callout", + title: "The rule behind all eleven", + text: "CPython can decide what an expression means at run time. Lithon cannot, so anything the compiler would have to guess about becomes a refusal. A refusal is a compile-time error with a precise message, not a wrong answer at run time.", + }, + ], + }, + { + id: "modulo", + title: "Modulo follows C, not Python", + blocks: [ + { + kind: "p", + text: "Lithon's `%` is the C remainder: the sign of the result follows the **dividend**, not the divisor. Python's is a true modulo: the sign follows the **divisor**.", + }, + { + kind: "tabs", + caption: "-7 % 3", + items: [ + { + label: "Lithon", + blocks: [ + { + kind: "code", + example: { + typecheck: false, + source: `print(-7 % 3)`, + output: "-1", + }, + }, + ], + }, + { + label: "CPython", + blocks: [ + { + kind: "code", + example: { + lang: "text", + source: `>>> -7 % 3 +2`, + output: "", + }, + }, + ], + }, + ], + }, + { + kind: "p", + text: "The same holds for `7 % -3`: Lithon gives `1`, CPython gives `-2`. This is not an oversight: the C-style form is the one a machine instruction actually implements, and Lithon emits exactly that instruction.", + }, + { + kind: "note", + tone: "note", + title: "Where to read more", + text: "The full statement of the rule, with the machine-level reason, is on the [semantics](/docs/semantics) page.", + }, + ], + }, + { + id: "division", + title: "Division is always float; `//` does not exist", + blocks: [ + { + kind: "p", + text: "`/` always produces a `float[64]`, including when both operands are integers: `7 / 2` is `3.5`, not `3`. There is no integer division operator at all — `//` is refused with *operator FloorDiv not supported yet*.", + }, + { + kind: "p", + text: "To get an integer quotient, write it yourself or use a shift when the divisor is a power of two: `x // 2` becomes `x >> 1` for non-negative `x`.", + }, + { + kind: "callout", + title: "Division by zero traps in both tiers", + text: "`1 / 0` is not an IEEE infinity and not a NaN: it is a `ZeroDivisionError` trap, exactly like Python's, and it is catchable with `try`/`except`.", + }, + ], + }, + { + id: "narrowing", + title: "Narrowing is never implicit", + blocks: [ + { + kind: "p", + text: "CPython integers are arbitrary-precision, so every integer is every other integer. Lithon integers are fixed-width, so an `int[8]` value is not an `int[64]` value: widening happens automatically, narrowing is refused.", + }, + { + kind: "code", + example: { + title: "widening is fine", + typecheck: false, + source: `small: int[8] = 5 +big: int[64] = small +print(big)`, + output: "5", + }, + }, + { + kind: "code", + example: { + title: "narrowing is refused", + expect: "error", + tool: "typecheck", + errorMatch: "cannot narrow", + source: `wide: int[64] = 5 +narrow: int[8] = wide +print(narrow)`, + output: + "RCR error: declaration of 'narrow': cannot narrow int[64] into int[8] -- narrowing is never allowed (V1_SPEC 0.6.11)", + }, + }, + { + kind: "p", + text: "There is no cast syntax for this. If you genuinely want to truncate, you have to say so with the explicit conversion, and the conversion **traps** rather than truncating when the value does not fit.", + }, + ], + }, + { + id: "conversions", + title: "Conversions name their width", + blocks: [ + { + kind: "comparison", + caption: "conversions", + columns: ["Operation", "CPython", "Lithon"], + rows: [ + ["int from float", "`int(3.7)`", "`int(64, 3.7)`"], + ["float from int", "`float(3)`", "`float(64, 3)`"], + [ + "out of range", + "`int(1e30)` is a big int", + "`int(8, 300.0)` **traps** with `ValueError`", + ], + [ + "float from int[64]", + "n/a", + "refused: `int[64]` has no exact `float[64]` (E0106)", + ], + ], + }, + { + kind: "p", + text: "The width is not decoration. `int(8, v)` promises the value fits in a signed byte, and the engine checks that promise at run time instead of silently wrapping.", + }, + ], + }, + { + id: "truthiness", + title: "There is no truthiness", + blocks: [ + { + kind: "p", + text: "In CPython, `if x:` asks whether `x` is truthy, and almost everything has a truth value. In Lithon, a condition must be a `bool`. `if x:` where `x: int[64]` is a type error, not a check for non-zero.", + }, + { + kind: "code", + example: { + title: "the Lithon way", + typecheck: false, + source: `n: int[64] = 0 +if n != 0: + print("not zero")`, + output: "", + }, + }, + { + kind: "note", + tone: "note", + title: "Why", + text: "A truthiness test on an int is a comparison the compiler could have proved away, but on a container it depends on the container's length, which is a run-time value. One rule — *conditions are `bool` values* — is easier to verify and faster to compile than a truthiness table.", + }, + ], + }, + { + id: "strings", + title: "Strings are bounded byte runs", + blocks: [ + { + kind: "comparison", + caption: "strings", + columns: ["Behaviour", "CPython", "Lithon"], + rows: [ + ["Type", "`str`, unbounded", "`str[N]`, N bytes"], + ["Length", "`len(s)` in characters", "`len(s)` in bytes, at run time"], + ["Concatenation", "`a + b`", "refused"], + ["Slicing", "`s[1:3]`", "refused"], + ["Comparison", "by content", "by content, across different capacities"], + ["Methods", "dozens", "three: `find`, `count`, `split`"], + ["In a `list`", "yes", "yes, as a `str[N]` element"], + ["As a function argument", "yes", "refused: a string is addressed, not passed"], + ], + }, + { + kind: "note", + tone: "note", + title: "Where to read more", + text: "The whole string model is on the [strings](/docs/strings) page.", + }, + ], + }, + { + id: "exceptions", + title: "Exceptions are a closed set of engine traps", + blocks: [ + { + kind: "p", + text: "CPython lets you `raise` anything and catch by class. Lithon has no exception objects and no `raise`: the only thing you can catch is one of **seven named traps** that the engine itself raises.", + }, + { + kind: "deflist", + caption: "the catchable set", + items: [ + { + term: "ZeroDivisionError", + detail: "division or modulo by zero.", + }, + { + term: "OverflowError", + detail: + "a dynamic `int[64]` overflow, or an input value that does not fit the width it was read into.", + }, + { + term: "ValueError", + detail: + "a float-to-int conversion out of range, a shift count outside 0..63, a zero `range` step, or non-numeric input.", + }, + { term: "IndexError", detail: "a dynamic index outside a list's bounds." }, + { term: "KeyError", detail: "a dict key that is not present." }, + { term: "EOFError", detail: "`input()` hit the end of the input." }, + { + term: "Exception", + detail: "the catch-all: matches every one of the six above.", + }, + ], + }, + { + kind: "p", + text: "A bare `except:` is refused, `except X as e` is refused (there is no exception object to bind), and `raise` is refused. A trap raised inside a **called function** is not catchable by the caller: the trap unwinds past it.", + }, + { + kind: "note", + tone: "note", + title: "Where to read more", + text: "The full `try`/`except`/`else`/`finally` story is on the [control flow](/docs/control-flow) page.", + }, + ], + }, + { + id: "unassigned", + title: "Unassigned names are a compile error", + blocks: [ + { + kind: "p", + text: "CPython raises `NameError` when you read a name you never assigned. Lithon refuses the program before it runs, and the refusal is per-name and per-path.", + }, + { + kind: "code", + example: { + title: "rejected · V1_SPEC 0.6.10", + expect: "error", + tool: "typecheck", + errorMatch: "never assigned a value", + source: `x: int[64] +print(x)`, + output: + "RCR error: call statement, print argument 1: 'x' was declared with a type but never assigned a value (V1_SPEC 0.6.10)", + }, + }, + { + kind: "p", + text: "The same rule covers branches: if one arm of an `if` assigns a name and the other does not, the program is refused, because the verifier cannot prove which arm runs.", + }, + ], + }, + { + id: "functions", + title: "Functions have a fixed shape", + blocks: [ + { + kind: "comparison", + caption: "function signatures", + columns: ["Feature", "CPython", "Lithon"], + rows: [ + ["Parameter types", "optional", "mandatory"], + ["Return type", "optional", "mandatory"], + ["Default values", "yes", "no"], + ["Keyword arguments", "yes", "no"], + ["`*args` / `**kwargs`", "yes", "no"], + ["Default arguments", "yes", "no"], + ["Closures", "yes", "no: a function body cannot capture a caller's variable"], + ["First-class functions", "yes", "no: a function name is not a value"], + ["Recursion", "yes", "yes, with a stack guard at 4096 frames"], + ["Overloading", "yes", "no"], + ], + }, + { + kind: "p", + text: "Every one of these is a place where CPython would have to build a closure, a tuple, or a dictionary at run time. Lithon has no such machinery, so the shape of a function is fixed at compile time and the machine code is a direct call.", + }, + ], + }, + { + id: "comparisons", + title: "Comparisons are four operators, never chained", + blocks: [ + { + kind: "p", + text: "Lithon has `<`, `>`, `==` and `!=`. `<=` and `>=` are refused (*comparison LtE not supported yet*), and chained comparisons (`a < b < c`) are refused, because a chain means two comparisons and an intermediate value that a machine register cannot hold.", + }, + { + kind: "code", + example: { + title: "the Lithon way", + typecheck: false, + source: `a: int[64] = 1 +b: int[64] = 2 +if a < b and b < 10: + print("ok")`, + output: "ok", + }, + }, + { + kind: "p", + text: "`!=` is a first-class operator with its own machine instruction, not `==` followed by `not`. On floats it follows IEEE 754: `nan != nan` is true.", + }, + ], + }, + { + id: "what-is-the-same", + title: "What is exactly the same", + blocks: [ + { + kind: "p", + text: "It is worth being precise about what did **not** change, because the list above can read as if Lithon is a different language. These are identical:", + }, + { + kind: "list", + items: [ + "`while` and `for` loops, `break`, `continue` — same shape, same semantics.", + "`if` / `elif` / `else` — same, and `elif` is spelled `elif`, not `else if`.", + "The conditional expression `a if cond else b` — same.", + "Integer literals, float literals, `True` / `False` — same spellings.", + "Operator precedence — Python's, exactly, because the frontend uses Python's own parser.", + "`and` / `or` / `not` — same precedence, but the operands must be `bool`.", + "Function calls, subscripting, attribute access — same syntax.", + "Recursion — supported, and the same shape as Python's.", + "`range(start, stop, step)` — the same three forms and the same values.", + ], + }, + { + kind: "quote", + text: "The source model is Python's. The type system is not, and where the type system changes the meaning, the meaning changes.", + }, + ], + }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Types & widths", + href: "/docs/types", + text: "The type system behind most of these differences.", + }, + { + title: "Semantics", + href: "/docs/semantics", + text: "Modulo, shifts, float traps, and no truthiness, in full.", + }, + { + title: "Errors & diagnostics", + href: "/docs/errors", + text: "Every error code, and how a refusal reads.", + }, + { + title: "Honest limits", + href: "/docs/limits", + text: "The gaps that are not yet implemented at all.", + }, + ], + }, + ], + }, + ], +} + +export const DIVERGENCE_PAGES: DocPage[] = [divergences] diff --git a/web/apps/web/src/docs/pages/exceptions.ts b/web/apps/web/src/docs/pages/exceptions.ts new file mode 100644 index 0000000..231f020 --- /dev/null +++ b/web/apps/web/src/docs/pages/exceptions.ts @@ -0,0 +1,319 @@ +import type { DocPage } from "../model" + +/** + * `try` / `except` / `else` / `finally` over the engine's own traps. + * + * The catchable set is closed: seven names. This page explains what each + * one means, when it fires, and what Lithon deliberately does not support. + */ +const exceptions: DocPage = { + slug: "exceptions", + title: "Exceptions & traps", + description: + "try/except/else/finally over a closed set of seven engine traps. What each one means, when it fires, and what is deliberately not supported.", + group: "language", + tags: [ + "exceptions", + "traps", + "try", + "except", + "else", + "finally", + "ZeroDivisionError", + "OverflowError", + "ValueError", + "IndexError", + "KeyError", + "EOFError", + "error handling", + ], + priority: "recommended", + outcome: "Recover from the seven engine traps with try/except/else/finally.", + minutes: 10, + sections: [ + { + id: "the-model", + title: "The model: traps, not exception objects", + blocks: [ + { + kind: "p", + text: "Lithon has no exception classes, no `raise`, and no exception objects. What it has is **traps**: the engine stops the program with a short message when an operation cannot produce a value. A `try` block names the traps it is prepared for, and the engine routes a trap to the matching arm.", + }, + { + kind: "code", + example: { + title: "catching a division by zero", + typecheck: false, + source: `n: int[64] = 0 +try: + q: int[64] = 100 % n + print("not reached") +except ZeroDivisionError: + print("caught ZeroDivisionError")`, + output: "caught ZeroDivisionError", + }, + }, + { + kind: "callout", + title: "The catchable set is closed", + text: "Exactly seven names are catchable: `ZeroDivisionError`, `OverflowError`, `ValueError`, `IndexError`, `KeyError`, `EOFError`, and `Exception` (the catch-all). Any other name in an `except` arm is a compile-time refusal. You cannot invent a trap, and you cannot catch a segfault.", + }, + ], + }, + { + id: "the-seven", + title: "The seven catchable traps", + blocks: [ + { + kind: "deflist", + caption: "the closed catchable set", + items: [ + { + term: "ZeroDivisionError", + detail: + "division or modulo by zero. Both `/` and `%` trap. A float divisor of `0.0` also traps — Lithon does not produce an IEEE infinity.", + }, + { + term: "OverflowError", + detail: + "a dynamic `int[64]` overflow, and an input value that does not fit the width it was read into (`int(8, input(...))` with a 300).", + }, + { + term: "ValueError", + detail: + "a float-to-int conversion whose result is out of range or not finite, a shift count outside 0..63, a `range` step of zero at run time, and non-numeric input.", + }, + { + term: "IndexError", + detail: "a dynamic index outside a list's bounds.", + }, + { + term: "KeyError", + detail: "a dict key that is not present.", + }, + { + term: "EOFError", + detail: "`input()` reached the end of the input before a value was read.", + }, + { + term: "Exception", + detail: + "the catch-all: it matches every one of the six above, and nothing else. It is the only way to write a handler that is guaranteed to cover the set.", + }, + ], + }, + { + kind: "code", + example: { + title: "an index trap", + typecheck: false, + source: `xs: list[int[64], 3] = [10, 20, 30] +k: int[64] = 9 +try: + v: int[64] = xs[k] + print("not reached") +except IndexError: + print("caught IndexError")`, + output: "caught IndexError", + }, + }, + { + kind: "code", + example: { + title: "several arms, tried in order", + typecheck: false, + source: `try: + y: int[64] = 5 % 0 +except IndexError: + print("wrong arm") +except ZeroDivisionError: + print("caught ZeroDivisionError")`, + output: "caught ZeroDivisionError", + }, + }, + ], + }, + { + id: "else-and-finally", + title: "`else` and `finally`", + blocks: [ + { + kind: "p", + text: "Both clauses are supported, and they have the same meaning they have in Python: `else` runs when the `try` block completed **without** a trap, and `finally` runs on every path out of the block.", + }, + { + kind: "code", + example: { + title: "all three clauses", + typecheck: false, + source: `n: int[64] = 4 +try: + r: int[64] = 100 % n +except ZeroDivisionError: + print("zero") +else: + print("else ran") +finally: + print("finally ran")`, + output: "else ran\nfinally ran", + }, + }, + { + kind: "code", + example: { + title: "the same program with a trap", + typecheck: false, + source: `n: int[64] = 0 +try: + r: int[64] = 100 % n +except ZeroDivisionError: + print("zero") +else: + print("else ran") +finally: + print("finally ran")`, + output: "zero\nfinally ran", + }, + }, + { + kind: "note", + tone: "note", + title: "A `try` with a `finally` but no `except` is refused", + text: "`try`/`finally` without an `except` is rejected: without an arm there is nothing to route a trap to, so the `finally` would have to be a catch-all by accident.", + }, + ], + }, + { + id: "nesting", + title: "Nesting and the trap boundary", + blocks: [ + { + kind: "p", + text: "`try` blocks nest, and the innermost matching arm wins. An outer `except Exception` will catch a trap that no inner arm claimed.", + }, + { + kind: "code", + example: { + title: "inner catches first", + typecheck: false, + source: `a: int[64] = 5 +try: + try: + inner: int[64] = a % 0 + except ZeroDivisionError: + print("inner caught") +except Exception: + print("outer caught")`, + output: "inner caught", + }, + }, + { + kind: "callout", + title: "A trap inside a called function is not catchable by the caller", + text: "The trap unwinds **past** the call frame. If `f()` divides by zero, the `try` around `f()` in the caller does not catch it: the function boundary is a trap boundary. Catch it inside `f`.", + }, + { + kind: "note", + tone: "warn", + title: "This is the one place Lithon is stricter than Python", + text: "In CPython an exception raised inside a callee propagates through the caller and can be caught there. Lithon does not: the trap is a property of the operation, and the operation is inside the callee.", + }, + ], + }, + { + id: "what-is-refused", + title: "What is deliberately refused", + blocks: [ + { + kind: "table", + caption: "refused exception forms", + rows: [ + [ + "A bare `except:`", + "refused: it would also catch a system-exit, and here it would suggest catching a segfault, which is not catchable. Use `except Exception:`.", + ], + [ + "`except X as e:`", + "refused: there is no exception object to bind. The handler learns **which** trap fired from which arm it is in.", + ], + [ + "`raise`", + "refused: a program cannot invent a trap. Only the engine raises.", + ], + [ + "`except*`", + "refused: no exception groups.", + ], + [ + "A `try` with only a `finally`", + "refused: without an `except` there is nowhere to route a trap.", + ], + [ + "Catching an unlisted name", + "refused: the catchable set is closed.", + ], + ], + }, + { + kind: "code", + example: { + title: "rejected: a bare except", + expect: "error", + tool: "frontend", + errorMatch: "a bare `except:` is not supported", + source: `try: + x: int[64] = 1 +except: + print("no")`, + output: + "LITHON-E0114: a bare `except:` is not supported -- it also catches system-exit, and here it would suggest catching a segfault, which is not catchable; name the trap (`except Exception:` catches every catchable trap) (6.10)", + }, + }, + ], + }, + { + id: "traps-in-the-reference", + title: "Where the traps live in the reference", + blocks: [ + { + kind: "p", + text: "The error-code table in the reference lists every code, including the ones that are *designed* rather than shipped. The distinction matters: a trap you can catch is not the same as a static refusal.", + }, + { + kind: "comparison", + caption: "refusal vs trap", + columns: ["", "Static refusal", "Runtime trap"], + rows: [ + ["When", "before the program runs", "while the program runs"], + ["Where", "the frontend and the type checker", "the engine"], + ["Message", "`LITHON-Ennnn: ...`", "a short reason, e.g. `error: division by zero`"], + ["Catchable", "no — the program does not exist", "yes, with `try`/`except`"], + ["Example", "`LITHON-E0105: unsupported float width 32`", "`error: list index out of range`"], + ], + }, + { + kind: "linkcards", + items: [ + { + title: "Errors & diagnostics", + href: "/docs/errors", + text: "Every code, tier by tier, with real output.", + }, + { + title: "Control flow", + href: "/docs/control-flow", + text: "The `try` statement in the context of the other flow forms.", + }, + { + title: "Divergences from Python", + href: "/docs/python-divergences", + text: "How Lithon's error model differs from CPython's.", + }, + ], + }, + ], + }, + ], +} + +export const EXCEPTION_PAGES: DocPage[] = [exceptions] diff --git a/web/apps/web/src/docs/pages/index.ts b/web/apps/web/src/docs/pages/index.ts index 71897ff..7519822 100644 --- a/web/apps/web/src/docs/pages/index.ts +++ b/web/apps/web/src/docs/pages/index.ts @@ -5,10 +5,20 @@ import { LANGUAGE_PAGES } from "./language" import { MACHINE_PAGES } from "./machine" import { REFERENCE_PAGES } from "./reference" import { START_PAGES } from "./start" +import { DIVERGENCE_PAGES } from "./divergences" +import { EXCEPTION_PAGES } from "./exceptions" +import { MATCH_PAGES } from "./match" +import { NETWORKING_PAGES } from "./networking" +import { STRINGS_PAGES } from "./strings" export const PAGES: DocPage[] = [ ...START_PAGES, ...LANGUAGE_PAGES, + ...DIVERGENCE_PAGES, + ...EXCEPTION_PAGES, + ...MATCH_PAGES, + ...STRINGS_PAGES, + ...NETWORKING_PAGES, ...ENGINE_PAGES, ...REFERENCE_PAGES, ...MACHINE_PAGES, diff --git a/web/apps/web/src/docs/pages/language.ts b/web/apps/web/src/docs/pages/language.ts index 36fd65a..385371f 100644 --- a/web/apps/web/src/docs/pages/language.ts +++ b/web/apps/web/src/docs/pages/language.ts @@ -692,7 +692,7 @@ for i in range(3): else: print(1)`, output: - "NotImplementedError: for/else is not supported (the else clause would be silently dropped)", + "LITHON-E0118: for/else is not supported (the else clause would be silently dropped)", }, }, { @@ -1033,8 +1033,22 @@ const operators: DocPage = { "float division producing `float[64]`, **not** C-style truncation", ], ["`%`", "C-style remainder: sign follows the **left** operand"], - ["`**`", "power: see the limitation note below"], - ["`//`", "not yet implemented: rejected at parse time"], + [ + "`**`", + "**refused**: *LITHON-E0114: operator Pow not supported yet*. There is no power operator.", + ], + [ + "`//`", + "**refused**: *LITHON-E0114: operator FloorDiv not supported yet*. There is no floor division; use a shift for a power-of-two divisor.", + ], + [ + "`<=` `>=`", + "**refused**: *LITHON-E0114: comparison LtE not supported yet*. Only `<`, `>`, `==` and `!=` exist.", + ], + [ + "`a < b < c`", + "**refused**: chained comparisons are not supported.", + ], ], }, { @@ -1051,6 +1065,18 @@ print(-7 % 3)`, note: "`%` follows C, not Python: `-7 % 3` is `-1`, not `2`. Python's floor-mod and C's trunc-mod disagree on negatives, and Lithon chose the machine's behaviour. (The checker cannot infer the type of a negated-literal binding yet; the frontend and both tiers handle it.)", }, }, + { + kind: "code", + example: { + title: "rejected: a power operator", + expect: "error", + tool: "frontend", + errorMatch: "operator Pow not supported yet", + source: `x: int[64] = 2 +print(x ** 3)`, + output: "LITHON-E0114: operator Pow not supported yet", + }, + }, ], }, { @@ -1259,7 +1285,11 @@ print(xs[2])`, rows: [ [ "Capacity", - "`N` is in the type; `len(xs)` returns `N` and cannot change at run time", + "`N` is in the type. It is the number of slots the declaration reserves, and it never changes.", + ], + [ + "Length", + "`len(xs)` is the **live** length, carried in a companion `xs.__len` slot. It is a run-time value, not the capacity: a short literal leaves the trailing slots reserved but not live.", ], [ "Unwritten slots", @@ -1275,6 +1305,33 @@ print(xs[2])`, ], ], }, + { + kind: "p", + text: "A short literal is the normal case, and it is why `len(xs)` is not the capacity:", + }, + { + kind: "code", + example: { + title: "the live length is not the capacity", + typecheck: false, + source: `xs: list[int[64], 6] = [1, 2, 3] +print(len(xs)) +print(xs) +print(sum(xs)) + +xs.append(10) +xs.append(20) +print(len(xs)) +print(xs)`, + output: "3\n[1, 2, 3]\n6\n5\n[1, 2, 3, 10, 20]", + }, + }, + { + kind: "note", + tone: "note", + title: "A reduction over a list whose length is not known at compile time is refused", + text: "`sum`, `min`, `max`, `any`, `all` and `in` are unrolled over the live length at compile time. When the length is only known at run time — an `append` inside a loop, or a `split` — they are refused rather than guessed at. A plain literal or a bare declaration keeps the length a compile-time constant, so the reductions still work.", + }, ], }, { @@ -1418,7 +1475,7 @@ const pointers: DocPage = { p: ptr[int[64]] = addressof(x) print(valueof(p))`, output: - "NotImplementedError: a pointer is named with a leading underscore, write `_p: ptr[...]` or pick a name that starts with '_' (4.4)", + "LITHON-E0122: a pointer is named with a leading underscore, write `_p: ptr[...]` or pick a name that starts with '_' (4.4)", }, }, ], @@ -1487,7 +1544,7 @@ print(_p == _q)`, blocks: [ { kind: "p", - text: "`ptr` works for any scalar pointee: `float[32]`, `float[64]`, `bool`. Reading through a `ptr[float[64]]` after the pointee changed gives you the new value, because the pointer names a location, not a snapshot:", + text: "`ptr` works for any scalar pointee: `float[64]`, `bool`, and any `int[w]`. Reading through a `ptr[float[64]]` after the pointee changed gives you the new value, because the pointer names a location, not a snapshot:", }, { kind: "code", @@ -2330,29 +2387,49 @@ print(r)`, }, { id: "integer-arguments", - title: "Arguments are integers", + title: "Arguments are integers or pointers", blocks: [ { kind: "p", - text: "A raw syscall argument must be an `int` in this slice. A pointer is **rejected at compile time**, and the reason is worth stating plainly: the interpreter does not have real memory addresses. It proves pointer semantics on a synthetic address space of its own (see [Pointers](/docs/pointers)), so a pointer handed to a kernel syscall would name nothing in the interpreter while naming a real frame slot natively. The two tiers would silently disagree.", + text: "A raw syscall argument must be an `int` or a **pointer**. The type checker says so plainly: *a raw syscall argument must be an integer or a pointer (a buffer or a sockaddr); floats and aggregates are not passed by value*.", }, { - kind: "p", - text: "Refusing the pointer is the honest answer: it keeps the primitive tier-neutral. Pointer-taking syscalls such as `write` and `read` are deferred until the interpreter grows a real address-space story, rather than shipping as a construct that works in one tier and faults in the other.", + kind: "callout", + title: "A pointer argument is a real host address in both tiers", + text: "This is the load-bearing fact. The interpreter does **not** emulate a synthetic address space: a pointer to a variable is the address of that variable, in the interpreter and in the JIT alike. So a kernel syscall that reads a buffer reads exactly the bytes the language wrote, and one that writes a `sockaddr` writes into the very struct you handed it. `tests/net/loopback.lio` is a shipped TCP round trip that depends on it.", }, { kind: "code", example: { - title: "rejected · a pointer argument", - lang: "text", + title: "a syscall with a pointer argument", + typecheck: false, source: `x: int[64] = 65 _ptrX: ptr[int[64]] = addressof(x) r: int[64] = syscall(1, _ptrX) print(r)`, - output: - "a raw syscall argument must be an integer in this slice", + output: "-38", + note: "The number 1 is not a valid Linux syscall number, so the kernel returns -ENOSYS (-38) — the same answer in both tiers, which is what makes it a checkable example.", }, }, + { + kind: "p", + text: "A `float[64]` argument is refused: the kernel ABI passes floats differently, and a syscall that takes one by value is not reachable from Lithon. A struct argument is refused for the same reason: it is not a scalar, and a struct is not passed by value.", + }, + { + kind: "linkcards", + items: [ + { + title: "Networking", + href: "/docs/networking", + text: "A real TCP loopback, checked against a C oracle.", + }, + { + title: "Pointers", + href: "/docs/pointers", + text: "Why a pointer to a struct is a real address.", + }, + ], + }, ], }, { diff --git a/web/apps/web/src/docs/pages/match.ts b/web/apps/web/src/docs/pages/match.ts new file mode 100644 index 0000000..bd3682e --- /dev/null +++ b/web/apps/web/src/docs/pages/match.ts @@ -0,0 +1,389 @@ +import type { DocPage } from "../model" + +/** + * `match`: the enum dispatch statement. + * + * `match` is Lithon's only pattern-matching construct, and it works on + * enums only. This page covers the arm forms, the exhaustiveness rule, the + * binding rules, and every shape that is refused. + */ +const match: DocPage = { + slug: "match", + title: "match", + description: + "The enum dispatch statement: arm forms, bindings, exhaustiveness, and every shape that is deliberately refused.", + group: "language", + tags: [ + "match", + "pattern matching", + "enum", + "dispatch", + "exhaustiveness", + "binding", + "case", + "wildcard", + ], + priority: "essential", + outcome: "Dispatch on an enum exhaustively, and read a variant's payload safely.", + minutes: 8, + sections: [ + { + id: "what-match-does", + title: "What a match does", + blocks: [ + { + kind: "p", + text: "A `match` reads the tag of an enum once and jumps to the arm whose variant matches it. An arm for a payload variant **binds** that payload, so the arm body reads it as a named variable; an arm for a unit variant binds nothing.", + }, + { + kind: "code", + example: { + title: "the shape of a match", + typecheck: false, + source: `class Shape(Enum): + Empty + Dot + Line: int[32] + Rect: int[64] + + +s: Shape +s = Shape.Line(42) +match s: + case Shape.Empty: + print(0) + case Shape.Dot: + print(1) + case Shape.Line(n): + print(n) + case Shape.Rect(w): + print(w)`, + output: "42", + }, + }, + { + kind: "callout", + title: "A match is not a value, and it is not a switch", + text: "It binds names in the arms, and it does not produce one. The lowered form is a chain of tag comparisons and branches — the same thing you would hand-write, which is why there is no new IR instruction for it.", + }, + ], + }, + { + id: "arm-forms", + title: "The four arm forms", + blocks: [ + { + kind: "deflist", + caption: "every arm form", + items: [ + { + term: "case E.V:", + detail: + "a unit variant. Binds nothing. The arm body reads only the outer variables.", + }, + { + term: "case E.V(n):", + detail: + "a scalar payload variant. Binds the one payload to `n`, with the variant's own declared type.", + }, + { + term: "case E.V(f1=a, f2=b):", + detail: + "a struct payload variant. Binds each named field. The fields are named, not positional, so a reordering of the struct does not silently rebind them.", + }, + { + term: "case _:", + detail: + "the wildcard. Matches every variant not already covered. It must be the **last** arm.", + }, + ], + }, + { + kind: "code", + example: { + title: "a struct payload, bound by field name", + typecheck: false, + source: `class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +c: Endpoint = Endpoint.Tcp(port=8080, addr=16777343) +match c: + case Endpoint.Tcp(port=p, addr=a): + print(p) + print(a) + case Endpoint.Udp(n): + print(n) + case Endpoint.Empty: + print(-1)`, + output: "8080\n16777343", + }, + }, + ], + }, + { + id: "exhaustiveness", + title: "Exhaustiveness is checked", + blocks: [ + { + kind: "p", + text: "A `match` must cover every variant, or end with a `case _:`. The checker knows the enum's variants, so it can prove the coverage statically — there is no fall-through and no runtime surprise.", + }, + { + kind: "code", + example: { + title: "rejected: not exhaustive", + expect: "error", + tool: "frontend", + typecheck: false, + errorMatch: "is not exhaustive", + source: `class E(Enum): + A + B + +e: E +match e: + case E.A: + print(1)`, + output: + "LITHON-E0131: `match` on enum 'E' is not exhaustive -- no arm covers B; add it or a `case _:` (4.7)", + }, + }, + { + kind: "code", + example: { + title: "rejected: the wildcard is not last", + expect: "error", + tool: "frontend", + typecheck: false, + errorMatch: "must be the last arm", + source: `class E(Enum): + A + +e: E +match e: + case _: + print(1) + case E.A: + print(2)`, + output: + "LITHON-E0131: `case _:` must be the last arm -- arms after it are unreachable (4.7)", + }, + }, + { + kind: "note", + tone: "note", + title: "The wildcard is not a catch-all for a missing variant", + text: "It is a catch-all for *any* variant not named by an earlier arm. That is why it has to be last: an arm after it could never run.", + }, + ], + }, + { + id: "binding-rules", + title: "The binding rules", + blocks: [ + { + kind: "table", + caption: "what a binding may be", + rows: [ + [ + "A name", + "`case E.V(n):` binds `n`. The name must not start with `_`: a leading underscore in a binding is a rejection, because a binding that is never read is a mistake worth catching.", + ], + [ + "A field name", + "`case E.V(port=p):` binds `p` from the struct payload's `port` field.", + ], + [ + "A type", + "the bound name has the variant's declared payload type: `int[32]` for `Line: int[32]`, `int[16]` for `port`.", + ], + [ + "A name in scope", + "a binding shadows an outer name of the same name inside the arm body only.", + ], + ], + }, + { + kind: "code", + example: { + title: "rejected: a binding that starts with an underscore", + expect: "error", + tool: "frontend", + typecheck: false, + errorMatch: "LITHON-E0131", + source: `class E(Enum): + A: int[32] + +e: E +match e: + case E.A(_n): + print(1) + case _: + print(2)`, + output: + "LITHON-E0131: a match binding cannot start with '_'; that prefix is reserved for pointers (4.4)", + }, + }, + ], + }, + { + id: "the-subject", + title: "What can be matched", + blocks: [ + { + kind: "p", + text: "The subject of a `match` must be an enum **variable** or an enum **struct field**. It cannot be a function parameter, a list element, or an expression, because `match` needs a place to read the tag from.", + }, + { + kind: "code", + example: { + title: "rejected: matching a non-enum", + expect: "error", + tool: "frontend", + typecheck: false, + errorMatch: "its subject must be an enum variable or an enum struct field", + source: `x: int[64] = 1 +match x: + case 1: + print(1)`, + output: + "LITHON-E0131: `match` reads an enum; its subject must be an enum variable or an enum struct field (4.7)", + }, + }, + { + kind: "p", + text: "Matching a struct field is the common case, because it is how you dispatch on a protocol or a state stored inside a larger record.", + }, + { + kind: "code", + example: { + title: "matching a struct field", + typecheck: false, + source: `class Endpoint(Enum): + Empty + class Tcp(Struct): + port: int[16] + addr: int[32] + Udp: int[32] + + +class Conn(Struct): + proto: Endpoint + id: int[64] + + +c: Conn +c.proto = Endpoint.Udp(53) +c.id = 7 +match c.proto: + case Endpoint.Tcp(port=p, addr=a): + print(p) + case Endpoint.Udp(n): + print(n) + case Endpoint.Empty: + print(-1) +print(c.id)`, + output: "53\n7", + }, + }, + ], + }, + { + id: "what-is-refused", + title: "What match does not do", + blocks: [ + { + kind: "table", + caption: "refused match shapes", + rows: [ + [ + "Guards", + "`case E.V if :` is refused: *LITHON-E0131: `case ... if ` is not supported yet*. Put the condition in the arm body.", + ], + [ + "Literal patterns", + "`case 1:` is refused: the subject has to be an enum, so the arms are variants.", + ], + [ + "Binding by position", + "`case E.V(a, b):` is refused for a struct payload. The fields are named.", + ], + [ + "Multiple subjects", + "`match a, b:` is refused: one subject, one dispatch.", + ], + [ + "A match as an expression", + "a `match` is a statement. It binds names; it does not produce a value.", + ], + [ + "A non-exhaustive match", + "refused: every variant must be covered, or a `case _:` must be present.", + ], + ], + }, + { + kind: "code", + example: { + title: "rejected: a guard", + expect: "error", + tool: "frontend", + typecheck: false, + errorMatch: "is not supported yet", + source: `class E(Enum): + A + +e: E +match e: + case E.A if True: + print(1) + case _: + print(2)`, + output: "LITHON-E0131: `case ... if ` is not supported yet (4.7)", + }, + }, + ], + }, + { + id: "lowering", + title: "What it lowers to", + blocks: [ + { + kind: "p", + text: "A `match` lowers to the same tag-compare chain a hand-written dispatch would be: for each arm, a `fieldload` of the tag, an equality test, and a branch whose body reads the payload fields it bound. There is no new instruction kind and no enum value flowing through the IR.", + }, + { + kind: "p", + text: "That is why a `match` cannot be a subject for another `match`, and why the arms are checked in order: the lowered code is a chain, not a table.", + }, + { + kind: "linkcards", + items: [ + { + title: "Enums", + href: "/docs/enums", + text: "Declaring a tagged union, and its layout.", + }, + { + title: "Control flow", + href: "/docs/control-flow", + text: "`match` next to `if`, `while` and `for`.", + }, + { + title: "Source to machine", + href: "/docs/source-to-machine", + text: "The IR and the machine code behind a match.", + }, + ], + }, + ], + }, + ], +} + +export const MATCH_PAGES: DocPage[] = [match] diff --git a/web/apps/web/src/docs/pages/networking.ts b/web/apps/web/src/docs/pages/networking.ts new file mode 100644 index 0000000..67d073f --- /dev/null +++ b/web/apps/web/src/docs/pages/networking.ts @@ -0,0 +1,349 @@ +import type { DocPage } from "../model" + +/** + * Networking (5.2): raw syscalls over real memory. + * + * The shipped proof is tests/net/loopback.lio, checked against a C oracle + * that issues the identical syscalls. + */ +const networking: DocPage = { + slug: "networking", + title: "Networking", + description: + "Real sockets from raw syscalls: a TCP loopback round trip checked against a C oracle, and the byte-order obligation that comes with it.", + group: "language", + tags: [ + "networking", + "sockets", + "tcp", + "syscall", + "loopback", + "byte order", + "sockaddr", + "5.2", + ], + priority: "reference", + outcome: "Open a TCP connection from Lithon with raw syscalls and real memory.", + minutes: 12, + sections: [ + { + id: "the-premise", + title: "The premise: a pointer is a real address", + blocks: [ + { + kind: "p", + text: "Networking in Lithon is not a library. It is `syscall` with a pointer, and the reason that works is a specific design decision: **a pointer argument to a syscall is a real host address in both tiers**. The interpreter does not emulate a synthetic address space; it hands the kernel the actual address of the variable.", + }, + { + kind: "callout", + title: "This is the payoff of the storage model", + text: "Because a `struct` is a packed blob at a known address and a pointer to it is a real address, the kernel can read and write Lithon's memory directly. A `bind()` that fills the port back into your struct is not a simulation of a kernel: it is the kernel.", + }, + { + kind: "p", + text: "The shipped test is `tests/net/loopback.lio`, and `tools/check_net_loopback.py` compiles a C program (`tests/net/loopback_oracle.c`) that issues the identical syscalls, then requires the two programs to print the same lines. A C oracle rather than the interpreter, because sockets are stateful and external: both Lithon tiers could be wrong in the same way, and the C program is not.", + }, + ], + }, + { + id: "syscall-signature", + title: "The syscall primitive", + blocks: [ + { + kind: "p", + text: "`syscall(n, a1, ..., a6)` issues Linux syscall number `n` and returns the kernel's return value as an `int[64]`. It is unchecked: a negative result is the kernel's `-errno`, and Lithon does not turn it into a trap.", + }, + { + kind: "deflist", + caption: "argument kinds", + items: [ + { + term: "int[64]", + detail: + "a scalar argument. The kernel sees the value itself, passed in a register.", + }, + { + term: "ptr[T]", + detail: + "a real host address. The kernel reads and writes exactly the bytes the language does. This is what makes a `sockaddr` or a buffer work.", + }, + { + term: "float[64]", + detail: + "refused: *floats and aggregates are not passed by value*. A syscall that takes a float is not reachable from Lithon.", + }, + ], + }, + { + kind: "code", + example: { + title: "socket(AF_INET, SOCK_STREAM, 0)", + typecheck: false, + source: `srv: int[64] = syscall(41, 2, 1, 0) +print(srv > 0)`, + output: "True", + }, + }, + { + kind: "note", + tone: "note", + title: "Six arguments, no more", + text: "The kernel's ABI has six argument registers. `syscall` takes at most six arguments, and the frontend refuses a seventh rather than silently dropping it.", + }, + ], + }, + { + id: "the-loopback", + title: "The loopback round trip", + blocks: [ + { + kind: "p", + text: "The shipped test does a full TCP round trip in one process: a server socket binds to `127.0.0.1` on an ephemeral port, listens, and accepts; a second socket connects back; the client sends an `int[64]` and the accepted connection receives it.", + }, + { + kind: "code", + example: { + title: "tests/net/loopback.lio (excerpt)", + sourceRef: "tests/net/loopback.lio", + typecheck: false, + source: `class SockAddrIn(Struct): + lo: int[64] + hi: int[64] + + +class Buf(Struct): + v: int[64] + + +sa: SockAddrIn +sa.lo = 2 | (swap32(2130706433) << 32) +sa.hi = 0 +_sa: ptr[SockAddrIn] = addressof(sa) + +socklen: int[64] = 16 +_psocklen: ptr[int[64]] = addressof(socklen) + +# socket(AF_INET=2, SOCK_STREAM=1, 0) +srv: int[64] = syscall(41, 2, 1, 0) + +# bind(srv, &sa, sizeof(sockaddr_in)) +print(syscall(49, srv, _sa, 16)) + +# listen(srv, backlog=1) +print(syscall(50, srv, 1))`, + output: "0\n0", + }, + }, + { + kind: "code", + example: { + title: "the round trip (excerpt)", + sourceRef: "tests/net/loopback.lio", + typecheck: false, + source: `class Buf(Struct): + v: int[64] + + +out: Buf +out.v = 123456789 +inb: Buf +inb.v = 0 +_pout: ptr[Buf] = addressof(out) +_pin: ptr[Buf] = addressof(inb) + +# sendto(cli, &out, 8, 0, NULL, 0) == 8 bytes +print(syscall(44, cli, _pout, 8, 0, 0, 0)) +# recvfrom(conn, &in, 8, 0, NULL, NULL) -> 8, and the bytes land in inb. +print(syscall(45, conn, _pin, 8, 0, 0, 0)) +print(inb.v)`, + output: "8\n8\n123456789", + }, + }, + { + kind: "p", + text: "The `getsockname` call is the proof that the address is real: the kernel writes the chosen port back into the very bytes you handed it.", + }, + { + kind: "code", + example: { + title: "getsockname writes the port back (excerpt)", + sourceRef: "tests/net/loopback.lio", + typecheck: false, + source: `socklen: int[64] = 16 +_psocklen: ptr[int[64]] = addressof(socklen) + +# getsockname(srv, &sa, &socklen): the kernel writes the real port back into the +# very bytes we handed it, which only works because the address is real memory. +print(syscall(51, srv, _sa, _psocklen)) +print(socklen)`, + output: "0\n16", + }, + }, + ], + }, + { + id: "byte-order", + title: "The byte-order obligation", + blocks: [ + { + kind: "p", + text: "A `sockaddr_in` is network byte order. A Lithon `int[64]` is little-endian. The mismatch is your problem to solve, and the test solves it with a three-line swap.", + }, + { + kind: "code", + example: { + title: "the byte swap", + sourceRef: "tests/net/loopback.lio", + typecheck: false, + source: `def swap32(v: int[64]) -> int[64]: + return ((v & 255) << 24) | ((v & 65280) << 8) | ((v >> 8) & 65280) | ((v >> 24) & 255)`, + output: "", + }, + }, + { + kind: "callout", + title: "The failure is silent, and the test proves it", + text: "The same address **without** the swap is still a perfectly good 32-bit number. Nothing crashes: its bytes just name `1.0.0.127` instead of `127.0.0.1`, and `bind` fails with `EADDRNOTAVAIL` (-99). A happy-path connect alone would not have shown this, so `loopback.lio` deliberately makes the failure explicit at the end.", + }, + { + kind: "code", + example: { + title: "the explicit failure (excerpt)", + sourceRef: "tests/net/loopback.lio", + typecheck: false, + source: `class SockAddrIn(Struct): + lo: int[64] + hi: int[64] + + +bad: SockAddrIn +bad.lo = 2 | (2130706433 << 32) +bad.hi = 0 +_bad: ptr[SockAddrIn] = addressof(bad) +badfd: int[64] = syscall(41, 2, 1, 0) +print(syscall(49, badfd, _bad, 16) < 0) +print(syscall(3, badfd))`, + output: "True\n0", + }, + }, + ], + }, + { + id: "the-syscall-numbers", + title: "The syscall numbers used", + blocks: [ + { + kind: "deflist", + caption: "Linux x86-64 syscall numbers", + items: [ + { + term: "`41` — socket(domain, type, protocol)", + detail: "returns the file descriptor", + }, + { + term: "`42` — connect(fd, addr, len)", + detail: "the client side", + }, + { + term: "`43` — accept(fd, addr, len)", + detail: "the server side", + }, + { + term: "`44` — sendto(fd, buf, len, flags, addr, alen)", + detail: "send", + }, + { + term: "`45` — recvfrom(fd, buf, len, flags, addr, alen)", + detail: "receive", + }, + { + term: "`49` — bind(fd, addr, len)", + detail: "the server side", + }, + { + term: "`50` — listen(fd, backlog)", + detail: "the server side", + }, + { + term: "`51` — getsockname(fd, addr, len)", + detail: "asks the kernel for the bound address", + }, + { + term: "`3` — close(fd)", + detail: "release the descriptor", + }, + ], + }, + { + kind: "note", + tone: "warn", + title: "These numbers are Linux x86-64 specific", + text: "The syscall ABI is per-architecture. The numbers above are the x86-64 Linux ones; on another platform the same operation has a different number. Windows uses a completely different mechanism, so a Lithon program that issues Linux syscall numbers only runs on Linux.", + }, + ], + }, + { + id: "what-is-not-there", + title: "What is not there", + blocks: [ + { + kind: "table", + caption: "no socket library, and that is deliberate", + rows: [ + [ + "A `socket` type", + "there is none. A socket is an `int[64]` file descriptor.", + ], + [ + "A `bind`/`listen` function", + "there is none. These are `syscall(49, ...)` and `syscall(50, ...)`.", + ], + [ + "An `import`", + "there is none. `std/` is a sketch, not a module system.", + ], + [ + "A DNS resolver", + "there is none. `getaddrinfo` would need a string API the language does not have.", + ], + [ + "A `struct` for `sockaddr_in`", + "you write it yourself, packed to match the kernel's layout.", + ], + [ + "Cross-platform support", + "none. The syscall numbers are Linux x86-64.", + ], + ], + }, + { + kind: "linkcards", + items: [ + { + title: "Syscalls", + href: "/docs/syscalls", + text: "The primitive itself: argument kinds, and what the two tiers agree on.", + }, + { + title: "Pointers", + href: "/docs/pointers", + text: "Why a pointer to a struct is a real address.", + }, + { + title: "Structs", + href: "/docs/structs", + text: "Packed layout, so your `sockaddr_in` matches the kernel's.", + }, + { + title: "Strings", + href: "/docs/strings", + text: "Bounded byte runs, for the buffers you hand the kernel.", + }, + ], + }, + ], + }, + ], +} + +export const NETWORKING_PAGES: DocPage[] = [networking] diff --git a/web/apps/web/src/docs/pages/reference.ts b/web/apps/web/src/docs/pages/reference.ts index 9defe5f..f170fcc 100644 --- a/web/apps/web/src/docs/pages/reference.ts +++ b/web/apps/web/src/docs/pages/reference.ts @@ -227,10 +227,10 @@ const errors: DocPage = { lang: "text", title: "the same operation, two verdicts", source: `x: int[64] = 9223372036854775807 -x = x + 50 # LITHON-E0101: compile-time refusal, not a runtime event +x = x + 50 # provable at compile time: a refusal, not a runtime event -b: int[64] = input_value() # unknown until run -c: int[64] = b + 50 # Tier-3 trap (E0303) iff it overflows`, +b: int[64] = int(64, input("n: ")) # unknown until run +c: int[64] = b + 50 # Tier-3 trap (E0303) iff it overflows`, }, }, { @@ -302,12 +302,27 @@ print(i)`, lang: "bash", title: "terminal", source: `$ lithon div_zero.lio # dynamic divisor -LITHON-E0301: division by zero`, +error: division by zero`, }, }, { kind: "p", - text: "Because the message is the same in both tiers, a trap can be caught by name and handled identically either way. `try`/`except` catches exactly these tier-3 traps -- see [Control flow](/docs/control-flow#try-except) for the catchable set and the three shapes that are deliberately refused.", + text: "Because the message is the same in both tiers, a trap can be caught by name and handled identically either way. The `try`/`except` classifier matches on the message text, not on a code, which is why the text is part of the contract.", + }, + { + kind: "linkcards", + items: [ + { + title: "Exceptions & traps", + href: "/docs/exceptions", + text: "The seven catchable traps, and the `else`/`finally` clauses.", + }, + { + title: "Control flow", + href: "/docs/control-flow#try-except", + text: "The `try` statement in the context of the other flow forms.", + }, + ], }, ], }, @@ -349,18 +364,18 @@ const builtins: DocPage = { caption: "built-ins", rows: [ [ - "`print(x)`", - "write one scalar value and a newline: the only output primitive", + "`print(x)` / `print(a, b, c)`", + "write one or more values, space-separated, and a newline: the only output primitive. `print()` with no arguments writes a bare newline.", ], [ "`len(xs)`", - "capacity of a `list`/`tuple`/`dict`: part of the type, cannot change at run time", + "the **live** length of a `list`/`tuple`/`dict`, or the run-time byte count of a `str[N]`. Carried in a companion slot, so it can change at run time.", ], [ "`range(n)` / `range(a, b)`", "loop values, checked against the loop variable's width at compile time when known (0.6.12)", ], - ["`addressof(x)`", "`ptr[T]` to scalar `x`"], + ["`addressof(x)`", "`ptr[T]` to a scalar, a `str[N]`, or a struct"], ["`valueof(_p)`", "read the scalar at `_p`"], [ "`contains(d, k)`", @@ -368,7 +383,7 @@ const builtins: DocPage = { ], [ "`sum(xs)`", - "sum of a `list[int[64]|float[64], N]`: unrolled over the compile-time capacity N", + "sum of a `list[int[64]|float[64], N]`: unrolled over the compile-time length", ], [ "`min(xs)` / `max(xs)`", @@ -384,7 +399,7 @@ const builtins: DocPage = { ], [ "`x in xs` / `x not in xs`", - "`bool`: OR of `x == xs[k]` over the compile-time N", + "`bool`: OR of `x == xs[k]` over the compile-time length", ], [ "`int(w, v)`", @@ -400,7 +415,7 @@ const builtins: DocPage = { ], [ "`syscall(n, a1..a6)`", - "raw unchecked syscall, integer args only, returns `int[64]` (`-errno` on Linux)", + "raw unchecked syscall; integer or pointer arguments, returns `int[64]` (`-errno` on Linux)", ], [ "`wrap_add(a, b)`", @@ -439,7 +454,18 @@ for a, b in zip(xs, ys): }, { kind: "p", - text: "Three names behave like methods rather than free functions: `xs.append(v)` on a `list` (which never grows the list -- it can only fail with a full list), `s.find(sub)` / `s.count(sub)` on a bounded string, and `s.split(sep)` into a declared destination list.", + text: "Three names behave like methods rather than free functions: `xs.append(v)` on a `list` (which never grows the list -- it writes into the next live slot and can only fail with a full list), `s.find(sub)` / `s.count(sub)` on a bounded string, and `parts: list[str[K], M] = s.split(sep)` into a declared destination list.", + }, + { + kind: "code", + example: { + title: "print takes several values", + typecheck: false, + source: `a: int[64] = 1 +b: int[64] = 2 +print(a, b, "x")`, + output: "1 2 x", + }, }, { kind: "note", @@ -455,6 +481,11 @@ for a, b in zip(xs, ys): href: "/docs/containers", text: "What `len`, `sum` and `append` actually operate on.", }, + { + title: "Strings", + href: "/docs/strings", + text: "`find`, `count`, `split`, and the bounded-string model.", + }, { title: "Semantics", href: "/docs/semantics", @@ -660,7 +691,7 @@ const verification: DocPage = { slug: "verification", title: "Verification", description: - "The numbers behind the claims: 32/32 CTest, 9,239 encoder cases with 0 wrong, both tiers byte-identical.", + "The numbers behind the claims: 33/33 CTest, 18,379 encoder cases with 0 wrong, both tiers byte-identical.", group: "reference", tags: [ "verification", @@ -680,18 +711,24 @@ const verification: DocPage = { kind: "table", caption: "from README.md: run it yourself, don't trust it", rows: [ - ["CTest", "32 / 32 passed"], - ["x86-64 encoder comparisons", "9,239 cases"], + ["CTest", "33 / 33 passed"], + ["x86-64 encoder comparisons", "18,379 cases"], ["Incorrect encoder cases", "0"], ["Typed differential fuzzing", "300 / 300 passed"], ["Bitwise / shift fuzzing", "200 / 200 passed"], ["Conditional-expression / SSA fuzzing", "200 / 200 per suite"], ["Direct float Phi sweep", "38 matched / 0 mismatched"], - ["ABI modules audited", "23"], + ["ABI modules audited", "34"], ["ABI stack/callee-saved failures", "0"], - ["Typed regression suite", "15 / 15 passed"], - ["General regression suite", "23 / 23 passed"], + ["Typed regression suite", "46 / 46 passed"], + ["General regression suite", "34 / 34 passed"], + ["Type-checker unit checks", "145 ok"], + ["Error-code discipline checks", "86 ok"], + ["Tier differential (interp vs JIT)", "168 / 168 passed"], + ["Syscall hook parity (both tiers)", "4 / 4 passed"], + ["Networking loopback vs a C oracle", "verified"], ["SIMD vectorizer loop cases", "6 / 6 passed"], + ["SIMD vectorizer gate + fallback", "verified"], ], }, { @@ -700,6 +737,12 @@ const verification: DocPage = { title: "The gate concludes", text: "`bash tools/verify_all.sh` runs the whole stack and ends with `ALL CHECKS PASSED` or it does not pass.", }, + { + kind: "note", + tone: "note", + title: "Where the corpus lives", + text: "The general suite is `tests/programs/` (34 programs), the typed suite is `tests/typed_regression/` (46 programs), and the negative suite is `tests/typed_programs/` (22 programs the checker must reject). Every one has a pinned expected-output file, and the tier differential runs the whole corpus through both engines and requires byte-identical stdout.", + }, ], }, { @@ -727,6 +770,12 @@ WRONG = 0`, kind: "p", text: "The only number that matters is `WRONG = 0`.", }, + { + kind: "note", + tone: "note", + title: "Two numbers, two moments", + text: "The README quotes both figures: **18,379** is the size of the case corpus, and the 9,239 / 7,949 / 1,290 block is a snapshot of one run of the classifier over the cases exercised at that time. Neither is a claim about correctness — the claim is `WRONG = 0`.", + }, ], }, { @@ -742,7 +791,7 @@ WRONG = 0`, example: { lang: "bash", title: "the full gate", - source: `$ ctest --test-dir build --output-on-failure # 32/32 + source: `$ ctest --test-dir build --output-on-failure # 33/33 $ python3 tools/run_regression.py # general suite $ python3 tools/run_typed_regression.py # typed suite $ python3 tools/run_tier_diff.py # both tiers, byte-identical @@ -765,7 +814,7 @@ $ python3 tools/fuzz_diff.py --phi --count 300 # merges, with and without kind: "note", tone: "warn", title: "What a green run does not prove", - text: "Opcode coverage is not operand coverage. 26 of 27 opcodes are emitted and exercised, but that does not prove every operand shape is right: `Mod` is the standing example: the general fuzzer only emits finite constants, so it cannot generate `1.0 % inf`, which was returning NaN natively while the interpreter was correct.", + text: "Opcode coverage is not operand coverage. All 55 IR opcodes are emitted and exercised, but that does not prove every operand shape is right: `Mod` is the standing example: the general fuzzer only emits finite constants, so it cannot generate `1.0 % inf`, which was returning NaN natively while the interpreter was correct.", }, ], }, @@ -777,12 +826,16 @@ $ python3 tools/fuzz_diff.py --phi --count 300 # merges, with and without kind: "table", caption: "fib(30), from benchmarks/results/latest.json", rows: [ - ["CPython 3.12", "790.2430 ms"], - ["Lithon native", "4.1810 ms"], - ["Speedup", "**189.0×**"], + ["CPython 3.12", "1731.0983 ms"], + ["Lithon native", "10.3053 ms"], + ["Speedup", "**168.0×**"], ["Result", "832040: identical on both"], ], }, + { + kind: "p", + text: "The file is written by the benchmark bot in CI (`.github/workflows/lithon-report-mamba.yml`), which also keeps one snapshot per commit under `benchmarks/results/history/`. The numbers above are the current `latest.json`; the table is regenerated rather than hand-edited.", + }, { kind: "p", text: "Benchmarks are the weakest form of evidence: they prove speed, not correctness. The suites above are what prove correctness; the benchmark is what the correctness buys you.", @@ -796,7 +849,7 @@ const limits: DocPage = { slug: "limits", title: "Honest limits", description: - "The gaps as they stand: Phi not emitted, Win64 unproven, x86-64 only.", + "The gaps as they stand: no closures, no stdlib, no float promotion on merges, Win64 unproven, x86-64 only.", group: "reference", tags: ["limits", "gaps", "roadmap", "not supported", "honest", "TODO"], sections: [ @@ -811,10 +864,10 @@ const limits: DocPage = { { kind: "table", rows: [ - ["IR opcodes emitted", "26 of 27"], + ["IR opcodes emitted", "55 of 55"], [ "`Phi`", - "lowered, not emitted: blocks if-as-expression merges without a memory round-trip", + "emitted on the incoming edges; a direct-Phi native path also exists and is tested", ], ["arguments per function / call", "two in registers, the rest on the stack · budget 32"], ["float merges", "never promoted · needs an XMM pool"], @@ -822,10 +875,18 @@ const limits: DocPage = { ["AOT binary emit", "none · everything runs in-process"], ["architectures", "x86-64 only · ARM64 is Phase III"], ["AVX-512", "not emitted · feature gating only"], - ["`dict` capacity trapping", "`list` bounds trap shipped, `dict`'s equivalent doesn't yet"], - ["`//` floor division", "not yet implemented"], - ["`**` power", "limited: see the regression corpus"], + [ + "`dict` capacity trapping", + "`list` bounds trap shipped, `dict`'s equivalent doesn't yet", + ], + ["`//` floor division", "refused outright: *operator FloorDiv not supported yet*"], + ["`**` power", "refused outright: *operator Pow not supported yet*"], + ["`<=` and `>=`", "refused: *comparison LtE not supported yet*"], ["unsigned ints", "no `uint`: signed at every width"], + ["closures", "none: a function body cannot capture a caller's variable"], + ["first-class functions", "none: a function name is not a value"], + ["`lambda`", "refused: *expression node Lambda not supported yet*"], + ["`None`", "refused: *expression node Constant not supported yet*"], [ "diamond unrolling", "opt-in via `--unroll-diamonds`, 1.10× slower today", @@ -835,8 +896,8 @@ const limits: DocPage = { { kind: "note", tone: "note", - title: "The blocker worth knowing", - text: "`Phi` is the single unemitted opcode: blocks if-as-expression merges without a memory round-trip, and most of the remaining test programs are waiting on it. It is engineering, not research.", + title: "A `str` cannot be passed to a function", + text: "Strings are addressed, not passed: a `str` function parameter is refused with *a str function parameter is not yet a string place*. This is the same rule that makes a struct unusable as an argument: Lithon passes scalars and containers, and addresses anything with an identity.", }, ], }, @@ -856,6 +917,11 @@ const limits: DocPage = { href: "/roadmap", text: "What is next, which phase each item belongs to.", }, + { + title: "Divergences from Python", + href: "/docs/python-divergences", + text: "The eleven places Lithon deliberately differs from CPython.", + }, { title: "Verification", href: "/docs/verification", diff --git a/web/apps/web/src/docs/pages/start.ts b/web/apps/web/src/docs/pages/start.ts index 5a765e4..f15d0f6 100644 --- a/web/apps/web/src/docs/pages/start.ts +++ b/web/apps/web/src/docs/pages/start.ts @@ -56,7 +56,7 @@ print(result)`, }, { title: "Fixed-width values", - text: "`int[8]` through `int[64]`, `float[32]`, `float[64]`. Widths are explicit; there is no arbitrary-precision integer behind your back. Every integer width is **signed** (there is no `uint`), so a negative `int[w]` keeps its sign when read back from a list element, struct field, or pointer; `bool` is the one narrow type that reads back as `0`/`1`.", + text: "`int[8]` through `int[64]`, `float[64]`, `bool`, and bounded `str[N]`. Widths are explicit; there is no arbitrary-precision integer behind your back. Every integer width is **signed** (there is no `uint`), so a negative `int[w]` keeps its sign when read back from a list element, struct field, or pointer; `bool` is the one narrow type that reads back as `0`/`1`.", }, { title: "Native execution", diff --git a/web/apps/web/src/docs/pages/strings.ts b/web/apps/web/src/docs/pages/strings.ts new file mode 100644 index 0000000..afce53b --- /dev/null +++ b/web/apps/web/src/docs/pages/strings.ts @@ -0,0 +1,326 @@ +import type { DocPage } from "../model" + +/** + * Bounded strings: `str[N]`. + * + * A Lithon string is a fixed-capacity, NUL-terminated byte run. `N` is the + * number of bytes the declaration reserves, and the literal must fit. + */ +const strings: DocPage = { + slug: "strings", + title: "Strings", + description: + "Bounded strings: str[N] is a fixed-capacity, NUL-terminated byte run. Length is a run-time value, capacity is part of the type.", + group: "language", + tags: [ + "strings", + "str", + "text", + "find", + "count", + "split", + "bounded", + "nul-terminated", + "bytes", + ], + priority: "recommended", + outcome: "Declare, read, compare and slice bounded strings without a heap.", + minutes: 8, + sections: [ + { + id: "what-a-string-is", + title: "What a Lithon string is", + blocks: [ + { + kind: "p", + text: "A Lithon string is not a Python `str`. It is a **bounded byte run**: `str[N]` declares `N` bytes of storage, the bytes are written one at a time, and the run is NUL-terminated. There is no heap, no allocator, and no growth. Capacity is part of the type, exactly like a `list`'s.", + }, + { + kind: "code", + example: { + title: "a bounded string", + typecheck: false, + source: `s: str[16] = "hello" +print(s) +print(len(s))`, + output: "hello\n5", + }, + }, + { + kind: "callout", + title: "N is bytes, and the capacity is not the length", + text: "`str[16]` reserves 16 **bytes**, not 16 characters: a UTF-8 character can be more than one byte. And `len(s)` is the **run-time** byte count of what was written, not the 16 you declared. A `str[N]` you declared but never filled has length 0.", + }, + { + kind: "p", + text: "The frontend lowers a string literal to one `IndexStore` per byte, so the storage model stays the same as every other container: a run of bytes at a fixed address.", + }, + ], + }, + { + id: "declaring", + title: "Declaring and writing", + blocks: [ + { + kind: "table", + caption: "string rules", + rows: [ + [ + "Capacity", + "`str[N]` — `N` bytes of storage. The literal must be shorter than `N` (the terminator needs a byte).", + ], + [ + "Length", + "`len(s)` is the run-time byte count of the bytes actually written.", + ], + [ + "Unwritten bytes", + "read back as `0`; the terminator makes the run a C string as far as the OS is concerned.", + ], + [ + "Indexing", + "`s[i]` reads one byte as an `int[8]`. The byte is signed.", + ], + [ + "Writing", + "`s[i] = v` writes one byte. The right-hand side must be a single `int[8]`-sized value.", + ], + [ + "Comparison", + "`s == t` compares the byte runs. The two runs may have different capacities.", + ], + [ + "Element type", + "a `str[N]` may be a `list` element, a `tuple` element, a `struct` field, or a pointer pointee.", + ], + ], + }, + { + kind: "p", + text: "Because a string is just a byte run, it can live inside a struct, inside a list, or behind a pointer, and the same rules apply to it as to any other fixed-capacity buffer.", + }, + { + kind: "code", + example: { + title: "a string as a struct field", + typecheck: false, + source: `class Header(Struct): + kind: str[8] + version: int[16] + +h: Header +h.kind = "HTTP" +h.version = 11 +print(h.kind) +print(h.version)`, + output: "HTTP\n11", + }, + }, + ], + }, + { + id: "methods", + title: "The three string methods", + blocks: [ + { + kind: "p", + text: "There are exactly three things you can do with a `str[N]` beyond reading and writing it. They are the whole string API.", + }, + { + kind: "deflist", + caption: "the string surface", + items: [ + { + term: "s.find(sub)", + detail: + "The lowest byte index where `sub` occurs in `s`, or `-1` when it does not occur. An empty needle returns `0`.", + }, + { + term: "s.count(sub)", + detail: + "How many non-overlapping occurrences of `sub` are in `s`. An empty needle returns `len(s) + 1`.", + }, + { + term: "parts: list[str[K], M] = s.split(sep)", + detail: + "Splits `s` on every occurrence of `sep` and writes the pieces into a **declared** `list[str[K], M]`. A piece longer than `K - 1` bytes is truncated; more than `M` pieces is a runtime trap. The piece count becomes the list's live length.", + }, + ], + }, + { + kind: "code", + example: { + title: "find and count", + typecheck: false, + source: `s: str[32] = "hello world" +print(s.find("world")) +print(s.find("zzz")) +print(s.count("l"))`, + output: "6\n-1\n2", + }, + }, + { + kind: "code", + example: { + title: "split writes into a declared list", + typecheck: false, + source: `s: str[32] = "a,b,c" +parts: list[str[8], 4] = s.split(",") +print(parts[0]) +print(parts[2])`, + output: "a\nc", + }, + }, + { + kind: "note", + tone: "warn", + title: "split is an assignment, not a statement", + text: "`s.split(\",\")` on its own line is refused: it has to be the **initializer** of a declared `list[str[K], M]`, because the split writes into the destination rather than returning a value. `s.split()` with no argument, and `s.split(\"\")` with an empty separator, are refused too.", + }, + ], + }, + { + id: "printing", + title: "Printing a string", + blocks: [ + { + kind: "p", + text: "`print(s)` writes the bytes of the run. There are no quotes around it, because a string is a byte run and not a Python object with a `repr`.", + }, + { + kind: "tabs", + caption: "print a string vs print a list of strings", + items: [ + { + label: "a bare string", + blocks: [ + { + kind: "code", + example: { + typecheck: false, + source: `s: str[8] = "hi" +print(s)`, + output: "hi", + }, + }, + ], + }, + { + label: "a list of strings", + blocks: [ + { + kind: "code", + example: { + typecheck: false, + source: `xs: list[str[8], 2] = ["hi", "yo"] +print(xs)`, + output: "['hi', 'yo']", + }, + }, + ], + }, ], + }, + { + kind: "p", + text: "A list of strings prints in the container `repr` form, with quotes, because a list is a composite value and the reader needs to see the element boundaries. A bare string does not.", + }, + ], + }, + { + id: "strings-and-pointers", + title: "Strings and pointers", + blocks: [ + { + kind: "p", + text: "A `str[N]` can be the pointee of a pointer, which is how you hand a buffer to a syscall that reads or writes it.", + }, + { + kind: "code", + example: { + title: "a string behind a pointer", + typecheck: false, + source: `s: str[16] = "ping" +_p: ptr[str[16]] = addressof(s) +print(valueof(_p))`, + output: "ping", + }, + }, + { + kind: "note", + tone: "note", + title: "One restriction worth knowing", + text: "A `str` field reached **through** a pointer is refused: `valueof(_p).kind` where `kind` is a string field is rejected with *a bounded string field is reached through its struct variable, not through a pointer into the struct*. The string can be the pointee of the pointer directly, but not a string-typed field of a struct the pointer points at.", + }, + ], + }, + { + id: "what-strings-cannot-do", + title: "What strings cannot do", + blocks: [ + { + kind: "table", + caption: "refusals", + rows: [ + [ + "Concatenation", + "`c: str[16] = a + b` is refused: *a str is built from a string literal, so `c: ... = ` needs `\"...\"`, not a single expression*. There is no `+` on strings.", + ], + [ + "Slicing", + "`s[1:3]` is refused: only a single byte index is supported.", + ], + [ + "A bare str literal in a container literal", + "`xs: list[str[8], 2] = [\"a\", \"b\"]` works in the engine, but the standalone checker answers `unknown type 'list'`: it does not know the container types yet.", + ], + [ + "Whitespace splitting", + "`s.split()` is refused: the separator is mandatory.", + ], + [ + "An empty separator", + "`s.split(\"\")` is refused, exactly as Python refuses it.", + ], + [ + "A `str` function parameter", + "a string cannot be passed to a function: *a str function parameter is not yet a string place*. Strings are addressed, not passed.", + ], + ], + }, + ], + }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Containers", + href: "/docs/containers", + text: "The other fixed-capacity runs: `list`, `tuple`, `dict`.", + }, + { + title: "Pointers", + href: "/docs/pointers", + text: "Raw addresses, `addressof`/`valueof`, and the `_` naming rule.", + }, + { + title: "Structs", + href: "/docs/structs", + text: "Packed storage, and how a `str[N]` field is laid out.", + }, + { + title: "Networking", + href: "/docs/networking", + text: "Using strings and pointers together for real socket calls.", + }, + ], + }, + ], + }, + ], +} + +export const STRINGS_PAGES: DocPage[] = [strings] diff --git a/web/apps/web/src/docs/priority.ts b/web/apps/web/src/docs/priority.ts new file mode 100644 index 0000000..194cae3 --- /dev/null +++ b/web/apps/web/src/docs/priority.ts @@ -0,0 +1,101 @@ +import type { PagePriority } from "./model" + +/** + * Index-page emphasis, kept in one place so the reading path is a single + * editable list rather than a field scattered across every page. + * + * `essential` - on the recommended path: read these to use the language. + * `recommended` - worth reading once, then looked up. + * `reference` - a lookup table, not a read. + */ +export const PAGE_PRIORITY: Partial> = { + // Start here + welcome: "essential", + "hello-world": "essential", + installation: "recommended", + "mental-model": "essential", + + // The language + types: "essential", + declarations: "essential", + "control-flow": "essential", + functions: "essential", + operators: "essential", + containers: "essential", + pointers: "recommended", + structs: "essential", + enums: "essential", + match: "essential", + methods: "recommended", + "type-functions": "reference", + syscalls: "reference", + strings: "essential", + exceptions: "essential", + networking: "reference", + "python-divergences": "essential", + + // Inside the engine + architecture: "essential", + "dual-tier": "recommended", + typechecking: "recommended", + pipeline: "reference", + simd: "reference", + + // Reference + errors: "essential", + builtins: "essential", + semantics: "essential", + verification: "recommended", + limits: "essential", + + // Machine view + "source-to-machine": "recommended", + "registers-abi": "reference", + "instruction-encoding": "reference", +} + +/** + * What a reader will be able to do after finishing a page. Shown on the + * docs index so the path is legible before you commit to it. + */ +export const PAGE_OUTCOME: Partial> = { + welcome: + "Understand what Lithon is, what it refuses to do, and why the refusal is the feature.", + "hello-world": + "Write, verify and run your first natively-executed program end to end.", + installation: "Build the engine and prove the install works.", + "mental-model": + "Hold the one rule that makes the rest of the language checkable: prove before you run.", + types: "Pick the right width for every value, and know which conversions are legal.", + declarations: "Give every name a type and a value, and be refused when you have not.", + "control-flow": + "Write every branch and loop the language supports, and know which ones it does not.", + functions: "Give a function a contract the compiler can check, and call it safely.", + operators: "Predict the result of every expression, including the ones that surprise CPython.", + containers: "Model fixed-size data without a heap, and know each container's rules.", + pointers: "Reach raw memory deliberately, with the naming rule that keeps it visible.", + structs: "Lay out packed storage whose bytes match a C struct exactly.", + enums: "Model a tagged union and read it back safely with match.", + match: "Dispatch on an enum exhaustively, and read a variant's payload safely.", + methods: "Give a struct behaviour, and understand why it is inlined.", + "type-functions": "Build reusable types at compile time.", + syscalls: "Call the kernel directly, with the argument rules both tiers agree on.", + strings: "Use bounded byte runs without a heap or an allocator.", + exceptions: "Recover from the seven engine traps and know the rest is refused.", + networking: "Open a real socket from Lithon and pass the byte-order test.", + "python-divergences": + "Port Python code without being surprised by a silent semantic change.", + architecture: "Trace a program from .lio source to executable memory.", + "dual-tier": "Understand what Tier-0 and Tier-1 each do, and when each runs.", + typechecking: "Read a diagnostic and know exactly which rule you broke.", + pipeline: "See the optimizer and SSA passes that run before the encoder.", + simd: "Know when a loop becomes AVX2 and why the result is still exact.", + errors: "Look up any error code and what it means.", + builtins: "Look up any built-in, its signature and its rules.", + semantics: "Look up the exact meaning of the operations that surprise people.", + verification: "Check the claims instead of taking them on trust.", + limits: "Know exactly what is not implemented yet.", + "source-to-machine": "Read the IR and the machine code for any example.", + "registers-abi": "Know where every argument and return value lives.", + "instruction-encoding": "Read the encoder's output byte by byte.", +} diff --git a/web/apps/web/src/docs/registry.ts b/web/apps/web/src/docs/registry.ts index bcb4cb2..bd64800 100644 --- a/web/apps/web/src/docs/registry.ts +++ b/web/apps/web/src/docs/registry.ts @@ -1,37 +1,47 @@ -import type { DocPage, DocsRegistry, SearchEntry } from "./model" +import type { + DocGroup, + DocPage, + DocsRegistry, + PagePriority, +} from "./model" import { PAGES } from "./pages" - -function pageEntries(pages: readonly DocPage[]): SearchEntry[] { - const entries: SearchEntry[] = [] - for (const page of pages) { - entries.push({ - id: `page:${page.slug}`, - kind: "page", - title: page.title, - subtitle: page.description, - group: page.group, - tags: page.tags, - href: `/docs/${page.slug}`, - }) - for (const section of page.sections) { - entries.push({ - id: `section:${page.slug}#${section.id}`, - kind: "section", - title: section.title, - subtitle: page.title, - group: page.group, - // the page's own tags, plus the two most specific ones, deduplicated - tags: [...new Set([...page.tags, ...page.tags.slice(0, 2)])], - href: `/docs/${page.slug}#${section.id}`, - }) - } - } - return entries -} +import { PAGE_OUTCOME, PAGE_PRIORITY } from "./priority" +import { buildEntries } from "./search-index" export const DOCS: DocsRegistry = { pages: PAGES, - entries: pageEntries(PAGES), + entries: buildEntries(), +} + +/** The priority of a page, defaulting to `recommended` for an unlisted one. */ +export function priorityOf(page: DocPage): PagePriority { + return PAGE_PRIORITY[page.slug] ?? page.priority ?? "recommended" +} + +/** The one-line outcome of a page, if it has one. */ +export function outcomeOf(page: DocPage): string | undefined { + return PAGE_OUTCOME[page.slug] ?? page.outcome +} +/** Every page in reading order, with its priority resolved. */ +export function rankedPages(): readonly { + page: DocPage + priority: PagePriority + outcome: string | undefined +}[] { + const rank: Record = { + essential: 0, + recommended: 1, + reference: 2, + } + return PAGES.map((page) => ({ + page, + priority: priorityOf(page), + outcome: outcomeOf(page), + })).sort((a, b) => { + const byPriority = rank[a.priority] - rank[b.priority] + if (byPriority !== 0) return byPriority + return PAGES.indexOf(a.page) - PAGES.indexOf(b.page) + }) } export function pageBySlug(slug: string): DocPage | undefined { @@ -41,3 +51,6 @@ export function pageBySlug(slug: string): DocPage | undefined { export function flatPageOrder(): readonly DocPage[] { return PAGES } + +export { DOC_GROUPS } from "./model" +export type { DocGroup } diff --git a/web/apps/web/src/docs/search-index.ts b/web/apps/web/src/docs/search-index.ts new file mode 100644 index 0000000..a5f691a --- /dev/null +++ b/web/apps/web/src/docs/search-index.ts @@ -0,0 +1,156 @@ +import type { DocPage, SearchEntry } from "./model" +import { PAGES } from "./pages" + +/** + * Extracts the searchable prose out of a docs page. + * + * The registry holds the documentation as structured data, so the text a + * reader can see is already there -- it just has to be flattened into a + * string the search engine can match against. + */ +export function pageBody(page: DocPage): string { + const parts: string[] = [page.title, page.description, ...page.tags] + for (const section of page.sections) { + parts.push(section.title) + for (const block of section.blocks) { + parts.push(...blockText(block)) + } + } + return parts.join(" \n ") +} + +function blockText(block: unknown): string[] { + if (!block || typeof block !== "object") return [] + const b = block as Record + const out: string[] = [] + switch (b.kind) { + case "p": + case "quote": + if (typeof b.text === "string") out.push(b.text) + break + case "note": + case "callout": + if (typeof b.title === "string") out.push(b.title) + if (typeof b.text === "string") out.push(b.text) + break + case "list": + if (Array.isArray(b.items)) { + for (const item of b.items) if (typeof item === "string") out.push(item) + } + break + case "table": + case "comparison": + if (Array.isArray(b.rows)) { + for (const row of b.rows) { + if (Array.isArray(row)) { + for (const cell of row) if (typeof cell === "string") out.push(cell) + } + } + } + if (typeof b.caption === "string") out.push(b.caption) + break + case "deflist": + if (Array.isArray(b.items)) { + for (const item of b.items as { term: string; detail: string }[]) { + out.push(item.term, item.detail) + } + } + break + case "layout": + if (Array.isArray(b.fields)) { + for (const f of b.fields as { name: string }[]) out.push(f.name) + } + break + case "steps": + case "cards": + case "linkcards": { + for (const item of itemsOf(b)) out.push(item.title, item.text) + break + } + case "code": { + const ex = b.example as { title?: string; source?: string; output?: string } + if (ex.title) out.push(ex.title) + if (ex.source) out.push(ex.source) + if (ex.output) out.push(ex.output) + break + } + case "tabs": { + for (const item of tabsOf(b)) { + out.push(item.label) + for (const inner of item.blocks) out.push(...blockText(inner)) + } + break + } + default: + break + } + return out +} + +/** The `{ title, text }` items of a steps / cards / linkcards block. */ +function itemsOf(b: Record): { title: string; text: string }[] { + const items = b.items + if (!Array.isArray(items)) return [] + return items.filter( + (item): item is { title: string; text: string } => + typeof item === "object" && + item !== null && + typeof (item as { title?: unknown }).title === "string" && + typeof (item as { text?: unknown }).text === "string", + ) +} + +/** The tabs of a tabs block. */ +function tabsOf( + b: Record, +): { label: string; blocks: unknown[] }[] { + const items = b.items + if (!Array.isArray(items)) return [] + return items.filter( + (item): item is { label: string; blocks: unknown[] } => + typeof item === "object" && + item !== null && + typeof (item as { label?: unknown }).label === "string" && + Array.isArray((item as { blocks?: unknown }).blocks), + ) +} + +/** The searchable text of one section. */ +export function sectionBody( + section: DocPage["sections"][number], +): string { + const parts: string[] = [section.title] + for (const block of section.blocks) parts.push(...blockText(block)) + return parts.join(" \n ") +} + +/** Every search entry, with the body text attached. */ +export function buildEntries(): SearchEntry[] { + const entries: SearchEntry[] = [] + for (const page of PAGES) { + const body = pageBody(page) + entries.push({ + id: `page:${page.slug}`, + kind: "page", + title: page.title, + subtitle: page.description, + group: page.group, + tags: page.tags, + href: `/docs/${page.slug}`, + body, + }) + for (const section of page.sections) { + entries.push({ + id: `section:${page.slug}#${section.id}`, + kind: "section", + title: section.title, + subtitle: page.title, + group: page.group, + tags: [...new Set([...page.tags, ...page.tags.slice(0, 2)])], + href: `/docs/${page.slug}#${section.id}`, + body: sectionBody(section), + }) + } + } + return entries +} diff --git a/web/apps/web/src/docs/templates.tsx b/web/apps/web/src/docs/templates.tsx index 27a5acd..b1bc4cc 100644 --- a/web/apps/web/src/docs/templates.tsx +++ b/web/apps/web/src/docs/templates.tsx @@ -448,6 +448,12 @@ export function Example({ spec }: { spec: ExampleSpec }) { const [tab, setTab] = useState(initial) const [playCount, setPlayCount] = useState(0) + // the machine-view toggle can change which tab should open first, but it + // must not yank the reader away from the tab they are actually on. + useEffect(() => { + setTab(initial) + }, [initial]) + const onTabChange = (value: string) => { setTab(value) if (value === "execution") setPlayCount((c) => c + 1) @@ -788,6 +794,210 @@ function TableBlock({ ) } +function ComparisonBlock({ + caption, + columns, + rows, +}: { + caption?: string + columns: readonly [string, string, string] + rows: readonly (readonly [string, string, string])[] +}) { + return ( +
+ + {caption && ( + + )} + + + + + + + + + {rows.map(([label, left, right]) => ( + + + + + + ))} + +
+ {caption} +
{columns[0]} + {columns[1]} + {columns[2]}
+ {renderInline(label)} + + {renderInline(left)} + + {renderInline(right)} +
+
+ ) +} + +function DefListBlock({ + caption, + items, +}: { + caption?: string + items: readonly { term: string; detail: string }[] +}) { + return ( +
+ {caption && ( +
+ {caption} +
+ )} +
+ {items.map((item) => ( +
+
+ {renderInline(item.term)} +
+
+ {renderInline(item.detail)} +
+
+ ))} +
+
+ ) +} + +function LayoutBlock({ + caption, + size, + fields, +}: { + caption?: string + size: string + fields: readonly { name: string; offset: string; size: string }[] +}) { + return ( +
+
+ + byte layout + + {size} +
+ {caption && ( +

+ {caption} +

+ )} +
+ {fields.map((f) => ( +
+
+ {f.name} +
+
+ @{f.offset} · {f.size} +
+
+ ))} +
+
+ ) +} + +function CalloutBlock({ title, text }: { title: string; text: string }) { + return ( +
+
+ {title} +
+
+ {renderInline(text)} +
+
+ ) +} + +function QuoteBlock({ text, source }: { text: string; source?: string }) { + return ( +
+

+ {renderInline(text)} +

+ {source && ( +
+ {source} +
+ )} +
+ ) +} + +function TabsBlock({ + caption, + items, +}: { + caption?: string + items: readonly { label: string; blocks: readonly Block[] }[] +}) { + const [active, setActive] = useState(0) + if (items.length === 0) return null + return ( +
+ {caption && ( +

+ {caption} +

+ )} + setActive(Number(v))}> +
+ + {items.map((item, i) => ( + + {item.label} + + ))} + +
+ {items.map((item, i) => ( + +
+ {item.blocks.map((block, j) => ( + + ))} +
+
+ ))} +
+
+ ) +} + export function BlockView({ block }: { block: Block }) { switch (block.kind) { case "p": @@ -820,6 +1030,30 @@ export function BlockView({ block }: { block: Block }) { return case "linkcards": return + case "callout": + return + case "quote": + return + case "comparison": + return ( + + ) + case "deflist": + return + case "layout": + return ( + + ) + case "tabs": + return } } diff --git a/web/apps/web/src/lib/github-status.ts b/web/apps/web/src/lib/github-status.ts index cc373e1..3256dbe 100644 --- a/web/apps/web/src/lib/github-status.ts +++ b/web/apps/web/src/lib/github-status.ts @@ -1,6 +1,12 @@ const REPO = "Project-Lithon/Lithon" const ENDPOINT = `https://api.github.com/repos/${REPO}/actions/runs?per_page=12` +/** + * The branch the deployed site tracks. A run on a feature branch should not + * make the footer say the project is failing. + */ +const TRACKED_BRANCH = "master" + export type RunStatus = | "success" | "failure" @@ -90,12 +96,50 @@ export function overallStatus(runs: WorkflowRun[]): RunStatus { } /** - * Fetch live CI state from GitHub. Unauthenticated, cached, and it degrades to - * a null result rather than throwing when the network or API is unavailable. + * Fetch live CI state from GitHub. + * + * Unauthenticated, cached in `localStorage` for a few minutes, and it + * degrades to a null result rather than throwing when the network or the + * API is unavailable. The cache matters: the unauthenticated GitHub API + * allows 60 requests per hour per IP, and the footer is on every page, so + * an uncached poll would exhaust the budget for every visitor behind the + * same NAT. */ +const CACHE_KEY = "lithon:ci-status" +const CACHE_TTL_MS = 5 * 60 * 1000 + +type CachedStatus = { at: number; status: RepoStatus } + +function readCache(): CachedStatus | null { + try { + const raw = localStorage.getItem(CACHE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) as Partial + if (typeof parsed.at !== "number" || !parsed.status) return null + if (Date.now() - parsed.at > CACHE_TTL_MS) return null + return { at: parsed.at, status: parsed.status } + } catch { + return null + } +} + +function writeCache(status: RepoStatus) { + try { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ at: Date.now(), status } satisfies CachedStatus), + ) + } catch { + // a full or disabled storage is not a reason to fail the page + } +} + export async function fetchRepoStatus( signal?: AbortSignal ): Promise { + const cached = readCache() + if (cached) return cached.status + try { const res = await fetch(ENDPOINT, { headers: { Accept: "application/vnd.github+json" }, @@ -115,17 +159,19 @@ export async function fetchRepoStatus( updated_at: string }[] } - const runs = data.workflow_runs.map((r) => ({ - id: r.id, - name: r.name, - status: r.status, - conclusion: r.conclusion, - branch: r.head_branch, - sha: r.head_sha.slice(0, 7), - url: r.html_url, - createdAt: r.created_at, - updatedAt: r.updated_at, - })) + const runs = data.workflow_runs + .filter((r) => r.head_branch === TRACKED_BRANCH) + .map((r) => ({ + id: r.id, + name: r.name, + status: r.status, + conclusion: r.conclusion, + branch: r.head_branch, + sha: r.head_sha.slice(0, 7), + url: r.html_url, + createdAt: r.created_at, + updatedAt: r.updated_at, + })) let stars: number | null = null let openIssues: number | null = null @@ -149,7 +195,7 @@ export async function fetchRepoStatus( // metadata is a bonus, not required } - return { + const status: RepoStatus = { runs, stars, openIssues, @@ -157,6 +203,8 @@ export async function fetchRepoStatus( fetchedAt: new Date().toISOString(), ok: true, } + writeCache(status) + return status } catch { return FALLBACK } diff --git a/web/apps/web/src/lib/ir.ts b/web/apps/web/src/lib/ir.ts index 626e8ce..a95661c 100644 --- a/web/apps/web/src/lib/ir.ts +++ b/web/apps/web/src/lib/ir.ts @@ -231,6 +231,13 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const lookupSlot = (name: string): MemCell | undefined => memory.get(`${frames.length}:${name}`) + /** + * The scope key of a frame slot. A slot is float-typed per frame, so + * `floatSlots` has to be scoped the same way `memory` is: a recursive + * call must not inherit the caller's float-ness for the same name. + */ + const slotKey = (name: string) => `${frames.length}:${name}` + /** * A struct is one blob with several fields at fixed byte offsets, so each * field needs its own storage: `fieldstore p, 0, %N` and @@ -278,11 +285,17 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { // Set when a trap fires: the block to jump to, or undefined when the trap // is not catchable and the run has to stop. let trapTarget: string | undefined + /** Names the IR referenced but the model has never seen. */ + const unresolved = new Set() + /** Opcodes the browser model does not implement. */ + const unmodelled = new Set() const snapshot = () => ({ registers: [...registers.entries()] .filter(([k]) => k.startsWith("%")) - .slice(-8) + // the last 12 registers of the current frame, so a long program does + // not drown the reader in a hundred stale names + .slice(-12) .map(([name, value]) => ({ name, value: fmtValue(value) })), memory: [ ...memory.values(), @@ -331,7 +344,13 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { if (tok.startsWith("%")) { // %N is per-function and per-frame: fib(3) and fib(2) both use %8 const key = `${frames.length}:${current.name}:${tok}` - const v = registers.get(key) ?? registers.get(tok) ?? 0n + const v = registers.get(key) ?? registers.get(tok) + if (v === undefined) { + // An unresolved register is a modelling gap, not a zero. Record it + // so the run can say so instead of quietly computing with 0. + unresolved.add(tok) + return 0n + } if (printedArg !== tok) consumed.push({ register: tok, value: fmtValue(v) }) return v @@ -342,6 +361,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { consumed.push({ register: tok, value: fmtValue(cell.value) }) return cell.value } + unresolved.add(tok) return 0n } @@ -382,7 +402,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { registers.set(`${frames.length}:${current.name}:${instr.dest}`, v) if (isFloat) floatRegs.add(`${frames.length}:${current.name}:${instr.dest}`) - else floatRegs.delete(instr.dest) + else floatRegs.delete(`${frames.length}:${current.name}:${instr.dest}`) produced = { register: instr.dest, value: fmtValue(v) } } break @@ -391,7 +411,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const v = read(instr.args[0]) if (instr.dest) { registers.set(`${frames.length}:${current.name}:${instr.dest}`, v) - if (instr.args[0] && floatSlots.has(instr.args[0])) + if (instr.args[0] && floatSlots.has(slotKey(instr.args[0]))) floatRegs.add(`${frames.length}:${current.name}:${instr.dest}`) produced = { register: instr.dest, value: fmtValue(v) } } @@ -412,10 +432,11 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { (instr.args[1] && floatRegs.has(`${frames.length}:${current.name}:${instr.args[1]}`)) ) - floatSlots.add(name) + floatSlots.add(slotKey(name)) + else floatSlots.delete(slotKey(name)) note = instr.args.length > 1 - ? `${cell.address} ← ${fmtValue(cell.value)}` + ? `${cell.address} ← ${fmtValue(cell.value)}` : `${cell.address} reserved` break } @@ -427,7 +448,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const cell = allocField(name, offset, instr.width ?? "int[64]") cell.value = v as number | bigint | boolean written = { ...cell } - note = `${cell.address} ← ${fmtValue(v)}` + note = `${cell.address} ← ${fmtValue(v)}` break } case "fieldload": { @@ -442,7 +463,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { floatRegs.add(`${frames.length}:${current.name}:${instr.dest}`) produced = { register: instr.dest, value: fmtValue(v) } } - note = `${cell.address} → ${fmtValue(v)}` + note = `${cell.address} → ${fmtValue(v)}` break } case "guardnonzero": { @@ -462,7 +483,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const trapName = instr.args[0] ?? "Exception" const handler = instr.args[1] pendingHandler = { trapName, handler } - note = `guard against ${trapName} → ${handler}` + note = `guard against ${trapName} → ${handler}` break } case "tryleave": { @@ -491,7 +512,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } note = known ? `syscall ${fmtValue(number)}` - : `syscall ${fmtValue(number)} → -ENOSYS (-38)` + : `syscall ${fmtValue(number)} → -ENOSYS (-38)` break } case "add": @@ -644,7 +665,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { type: instr.width ?? "int[64]", value: v as number, } - note = `${name}[${key}] ← ${fmtValue(v)}` + note = `${name}[${key}] ← ${fmtValue(v)}` break } case "Index": @@ -682,7 +703,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { `&${instr.args[0]}` ) produced = { register: instr.dest, value: `&${instr.args[0]}` } - note = `&${instr.args[0]} → stack address` + note = `&${instr.args[0]} → stack address` } break } @@ -698,7 +719,10 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const isFloat = floatRegs.has( `${frames.length}:${current.name}:${instr.args[1]}` - ) || (instr.args[1] ? floatSlots.has(instr.args[1]) : false) + ) || + (instr.args[1] && !instr.args[1].startsWith("%") + ? floatSlots.has(slotKey(instr.args[1])) + : false) const line = isFloat && typeof v === "number" ? fmtFloat(v) : fmtValue(v) if (callee === "print") { @@ -739,7 +763,7 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const truthy = cond === true || (typeof cond === "bigint" && cond !== 0n) const target = truthy ? instr.args[1] : instr.args[2] - note = `branch on ${fmtValue(cond)} → ${target}` + note = `branch on ${fmtValue(cond)} → ${target}` jumpTo = target break } @@ -770,7 +794,11 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { continue } default: - note = `${instr.op} (not modelled)` + // An opcode the browser model does not implement. Say so plainly + // rather than pretending the instruction did nothing: the reader + // needs to know the trace is incomplete. + unmodelled.add(instr.op) + note = `${instr.op} (not modelled by the browser trace)` } const snap = snapshot() @@ -818,6 +846,13 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const run: IrRun = { steps, stdout } if (error) run.error = error else if (guard >= maxSteps) run.error = `stopped after ${maxSteps} steps` + else if (unmodelled.size > 0) { + // The trace is a browser model of the IR, not the IR itself. Say which + // opcodes it does not implement, so a reader is never misled. + run.error = `this trace does not model: ${[...unmodelled].sort().join(", ")}` + } else if (unresolved.size > 0) { + run.error = `this trace could not resolve: ${[...unresolved].sort().join(", ")}` + } return run } diff --git a/web/apps/web/src/lib/playground-examples.ts b/web/apps/web/src/lib/playground-examples.ts new file mode 100644 index 0000000..30c4339 --- /dev/null +++ b/web/apps/web/src/lib/playground-examples.ts @@ -0,0 +1,103 @@ +/** + * The playground's built-in examples. + * + * They live in their own module so the IR generator can read the same + * source the page renders, rather than a copy that can drift. + */ +export type PlaygroundExample = { + id: string + title: string + note: string + source: string +} + +export const PLAYGROUND_EXAMPLES: readonly PlaygroundExample[] = [ + { + id: "hello", + title: "Hello, typed", + note: "the smallest annotated program", + source: `x: int[64] = 10 +y: int[64] = 20 + +result: int[64] = x + y + +print(result)`, + }, + { + id: "fib", + title: "Recursive fibonacci", + note: "recursion, tier-1 native", + source: `def fib(n: int[64]) -> int[64]: + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + +x: int[64] = fib(10) +print(x)`, + }, + { + id: "range", + title: "Typed range loop", + note: "for-range with a width-checked loop variable", + source: `total: int[64] = 0 +i: int[64] = 0 + +for i in range(10): + total = total + i + +print(total)`, + }, + { + id: "while", + title: "While loop", + note: "conditionals with a loop-carried accumulator", + source: `i: int[64] = 0 +total: int[64] = 0 + +while i < 10: + total = total + i + i = i + 1 + +print(total)`, + }, + { + id: "float", + title: "Float arithmetic", + note: "float[64], SSE2 end to end", + source: `a: float[64] = 3.5 +b: float[64] = 2.0 + +print(a + b) +print(a * b)`, + }, + { + id: "wrap", + title: "Wrapping arithmetic", + note: "wrap_add at the int[64] boundary", + source: `x: int[64] = 9223372036854775807 +y: int[64] = 1 +z: int[64] = wrap_add(x, y) +print(z)`, + }, + { + id: "pointer", + title: "Typed pointers", + note: "explicit raw memory, `_` prefix required", + source: `i: int[16] = 16 +_pi: ptr[int[16]] = addressof(i) +print(valueof(_pi))`, + }, + { + id: "list", + title: "Fixed-capacity list", + note: "capacity lives in the type", + source: `xs: list[int[64], 6] + +xs[0] = 10 +xs[1] = 20 +xs[2] = 30 + +print(xs[0]) +print(len(xs))`, + }, +] diff --git a/web/apps/web/src/lib/playground-ir.ts b/web/apps/web/src/lib/playground-ir.ts new file mode 100644 index 0000000..e68c619 --- /dev/null +++ b/web/apps/web/src/lib/playground-ir.ts @@ -0,0 +1,22 @@ +/** + * The pre-generated IR for the playground's built-in examples. + * + * The playground's live runner is a dev-only Vite middleware, so a visitor + * to the deployed site cannot run a program there. These IR blobs are the + * real frontend output for the eight built-in examples, generated by + * `scripts/gen-playground-ir.ts`, so the page still shows something true in + * production: the exact IR the engine would emit. + * + * Keyed by the FNV-1a hash of the example source, matching `irKey()` in + * `docs/templates.tsx`. + */ +export const PLAYGROUND_IR: Record = { + "47db5edf": "function __main__():\nblock0:\n %0 = const_i64 10\n store x, %0 : int[64]\n %1 = const_i64 20\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = add %2, %3\n store result, %4 : int[64]\n %5 = load result\n call print_bare, %5\n call print_nl\n return", + "7a8d3583": "function fib(n:int[64]) -> int[64]:\nblock0:\n %0 = load n\n %1 = const_i64 2\n %2 = lt %0, %1\n branch %2, block1, block2\nblock1:\n %3 = load n\n return %3\nblock2:\n %4 = load n\n %5 = load n\n %6 = const_i64 1\n %7 = sub %5, %6\n %8 = call fib, %7\n %9 = load n\n %10 = load n\n %11 = const_i64 2\n %12 = sub %10, %11\n %13 = call fib, %12\n %14 = add %8, %13\n return %14\nfunction __main__():\nblock0:\n %0 = const_i64 10\n %1 = call fib, %0\n store x, %1 : int[64]\n %2 = load x\n call print_bare, %2\n call print_nl\n return", + "c10a5265": "function __main__():\nblock0:\n %0 = const_i64 0\n store total, %0 : int[64]\n %1 = const_i64 0\n store i, %1 : int[64]\n %2 = const_i64 0\n %3 = const_i64 10\n %4 = const_i64 1\n store i, %2 : int[64]\n jump block4\nblock4:\n %5 = load i\n %6 = lt %5, %3\n branch %6, block1, block2\nblock1:\n %7 = load total\n %8 = load i\n %9 = add %7, %8\n store total, %9 : int[64]\n jump block3\nblock3:\n %10 = load i\n %11 = const_i64 1\n %12 = add %10, %11\n store i, %12\n jump block4\nblock2:\n %13 = load total\n call print_bare, %13\n call print_nl\n return", + "124e5063": "function __main__():\nblock0:\n %0 = const_i64 0\n store i, %0 : int[64]\n %1 = const_i64 0\n store total, %1 : int[64]\n jump block1\nblock1:\n %2 = load i\n %3 = const_i64 10\n %4 = lt %2, %3\n branch %4, block2, block3\nblock2:\n %5 = load total\n %6 = load i\n %7 = add %5, %6\n store total, %7 : int[64]\n %8 = load i\n %9 = load i\n %10 = const_i64 1\n %11 = add %9, %10\n store i, %11 : int[64]\n jump block1\nblock3:\n %12 = load total\n call print_bare, %12\n call print_nl\n return", + "f31dca0d": "function __main__():\nblock0:\n %0 = const_f64 3.5\n store a, %0 : float[64]\n %1 = const_f64 2.0\n store b, %1 : float[64]\n %2 = load a\n %3 = load b\n %4 = add %2, %3\n call print_bare, %4\n call print_nl\n %5 = load a\n %6 = load b\n %7 = mul %5, %6\n call print_bare, %7\n call print_nl\n return", + "b4973ce3": "function __main__():\nblock0:\n %0 = const_i64 9223372036854775807\n store x, %0 : int[64]\n %1 = const_i64 1\n store y, %1 : int[64]\n %2 = load x\n %3 = load y\n %4 = wrapadd %2, %3\n store z, %4 : int[64]\n %5 = load z\n call print_bare, %5\n call print_nl\n return", + "228c59c7": "function __main__():\nblock0:\n %0 = const_i64 16\n store i, %0 : int[16]\n %1 = addressof i\n store _pi, %1 : ptr[int[16]]\n %2 = load _pi\n %3 = valueof %2 : int[16]\n call print_bare, %3\n call print_nl\n return", + "c86dce7c": "function __main__():\nblock0:\n store xs : list[int[64], 6]\n %0 = const_i64 6\n store xs.__len, %0 : int[64]\n %1 = const_i64 0\n %2 = const_i64 10\n IndexStore xs, %1, %2\n %3 = const_i64 1\n %4 = const_i64 20\n IndexStore xs, %3, %4\n %5 = const_i64 2\n %6 = const_i64 30\n IndexStore xs, %5, %6\n %8 = const_i64 0\n %7 = Index xs, %8\n call print_bare, %7\n call print_nl\n %9 = load xs.__len\n call print_bare, %9\n call print_nl\n return" +} diff --git a/web/apps/web/src/lib/seo.ts b/web/apps/web/src/lib/seo.ts index 4e4d83b..3335c72 100644 --- a/web/apps/web/src/lib/seo.ts +++ b/web/apps/web/src/lib/seo.ts @@ -60,8 +60,8 @@ export function metaForPath(pathname: string): PageMeta { facts: [ "Zero third-party dependencies: no LLVM, no Cranelift, no runtime package", "Dual-tier: native x86-64 with an honest interpreter fallback under --strict", - "Fixed-width types: int[8..64], float[64], bool, typed pointers", - "32/32 CTest, 9,239 encoder cases with 0 incorrect", + "Fixed-width types: int[8..64], float[64], bool, bounded str[N], typed pointers", + "33/33 CTest, 18,379 encoder cases with 0 incorrect", ], keywords: ["lithon", "python", "native", "x86-64", "compiler", "jit"], } @@ -71,12 +71,12 @@ export function metaForPath(pathname: string): PageMeta { return { title: `Playground | run Lithon against the real frontend`, description: - "Write Lithon in the browser with syntax highlighting and autocomplete, then run it through this repository's actual type checker and frontend to get the typed IR back.", + "Write Lithon in the browser with syntax highlighting and autocomplete, and read the typed IR the engine emits for it. The live compiler runs in local development; the built-in examples carry their recorded IR.", canonical: `${site}/playground`, facts: [ - "Runs tools/typecheck.py and src/frontend/frontend.py locally", - "Returns the real typed IR for valid programs", - "Shows the engine's own diagnostic for invalid ones", + "The built-in examples show the real frontend output", + "The live runner is wired up in `bun run dev`", + "Syntax highlighting and hover documentation for the whole type language", ], keywords: ["lithon", "playground", "repl", "compiler", "online"], } @@ -96,6 +96,49 @@ export function metaForPath(pathname: string): PageMeta { } } + if (clean === "/changelog") { + return { + title: `Changelog | Lithon`, + description: + "Every shipped change to the Lithon language, engine, toolchain and website, newest first. Each entry links to the documentation that covers it.", + canonical: `${site}/changelog`, + facts: [ + "Language, engine, toolchain, verification, docs and build changes", + "Each entry links to the documentation that covers it", + "Built from the repository's own commit history", + ], + keywords: [ + "lithon", + "changelog", + "release notes", + "history", + "what changed", + ], + } + } + + if (clean === "/next") { + return { + title: `Next actions | Lithon`, + description: + "The open items: the language gaps, the engine work, the toolchain debt, and the recently shipped changes that closed them.", + canonical: `${site}/next`, + facts: [ + "Every open item is a design decision, not a bug", + "Each item names the phase it belongs to", + "The recently shipped list is the changelog in reverse", + ], + keywords: [ + "lithon", + "roadmap", + "next", + "todo", + "planned", + "contribute", + ], + } + } + const docMatch = /^\/docs\/([\w-]+)/.exec(clean) if (docMatch) { const page = DOCS.pages.find((p) => p.slug === docMatch[1]) @@ -138,13 +181,19 @@ export function metaForPath(pathname: string): PageMeta { } } -/** JSON-LD describing the project, for search engines and AI answer engines. */ +/** + * JSON-LD describing the project, for search engines and AI answer engines. + * + * Every route that calls `metaForPath` should also emit this, so a page is + * never invisible to a structured-data reader. + */ export function structuredData(pathname: string) { const meta = metaForPath(pathname) const site = originFor() + const isDoc = pathname.startsWith("/docs") return { "@context": "https://schema.org", - "@type": pathname.startsWith("/docs") ? "TechArticle" : "WebSite", + "@type": isDoc ? "TechArticle" : "WebPage", name: meta.title, headline: meta.title, description: meta.description, @@ -165,5 +214,30 @@ export function structuredData(pathname: string) { programmingLanguage: "Python", description: SITE.description, }, + // The factual summary an answer engine can quote without parsing prose. + ...(meta.facts.length + ? { + "abstract": meta.facts.join(" · "), + "disambiguatingDescription": meta.facts.join(" · "), + } + : {}), + } +} + +/** The `script:ld+json` tag for a route, ready to drop into a `head()`. */ +export function jsonLdTag(pathname: string) { + return { + "script:ld+json": JSON.stringify(structuredData(pathname)), + } +} + +/** + * The same JSON-LD, as a `head()` entry. TanStack's `meta` array only + * accepts `` elements, so the script tag goes in `scripts`. + */ +export function jsonLdScript(pathname: string) { + return { + type: "application/ld+json", + children: JSON.stringify(structuredData(pathname)), } } diff --git a/web/apps/web/src/routeTree.gen.ts b/web/apps/web/src/routeTree.gen.ts index 44043a3..9d3cad0 100644 --- a/web/apps/web/src/routeTree.gen.ts +++ b/web/apps/web/src/routeTree.gen.ts @@ -11,6 +11,8 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as BenchmarksRouteImport } from './routes/benchmarks' +import { Route as ChangelogRouteImport } from './routes/changelog' +import { Route as NextRouteImport } from './routes/next' import { Route as PlaygroundRouteImport } from './routes/playground' import { Route as RoadmapRouteImport } from './routes/roadmap' import { Route as UseCasesRouteImport } from './routes/use-cases' @@ -32,6 +34,16 @@ const BenchmarksRoute = BenchmarksRouteImport.update({ path: '/benchmarks', getParentRoute: () => rootRouteImport, } as any) +const ChangelogRoute = ChangelogRouteImport.update({ + id: '/changelog', + path: '/changelog', + getParentRoute: () => rootRouteImport, +} as any) +const NextRoute = NextRouteImport.update({ + id: '/next', + path: '/next', + getParentRoute: () => rootRouteImport, +} as any) const PlaygroundRoute = PlaygroundRouteImport.update({ id: '/playground', path: '/playground', @@ -86,6 +98,8 @@ const RoadmapVerificationRoute = RoadmapVerificationRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/benchmarks': typeof BenchmarksRoute + '/changelog': typeof ChangelogRoute + '/next': typeof NextRoute '/playground': typeof PlaygroundRoute '/roadmap': typeof RoadmapRouteWithChildren '/use-cases': typeof UseCasesRoute @@ -100,6 +114,8 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/benchmarks': typeof BenchmarksRoute + '/changelog': typeof ChangelogRoute + '/next': typeof NextRoute '/playground': typeof PlaygroundRoute '/roadmap': typeof RoadmapRouteWithChildren '/use-cases': typeof UseCasesRoute @@ -115,6 +131,8 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/benchmarks': typeof BenchmarksRoute + '/changelog': typeof ChangelogRoute + '/next': typeof NextRoute '/playground': typeof PlaygroundRoute '/roadmap': typeof RoadmapRouteWithChildren '/use-cases': typeof UseCasesRoute @@ -131,6 +149,8 @@ export interface FileRouteTypes { fullPaths: | '/' | '/benchmarks' + | '/changelog' + | '/next' | '/playground' | '/roadmap' | '/use-cases' @@ -145,6 +165,8 @@ export interface FileRouteTypes { to: | '/' | '/benchmarks' + | '/changelog' + | '/next' | '/playground' | '/roadmap' | '/use-cases' @@ -159,6 +181,8 @@ export interface FileRouteTypes { | '__root__' | '/' | '/benchmarks' + | '/changelog' + | '/next' | '/playground' | '/roadmap' | '/use-cases' @@ -174,6 +198,8 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute BenchmarksRoute: typeof BenchmarksRoute + ChangelogRoute: typeof ChangelogRoute + NextRoute: typeof NextRoute PlaygroundRoute: typeof PlaygroundRoute RoadmapRoute: typeof RoadmapRouteWithChildren UseCasesRoute: typeof UseCasesRoute @@ -197,6 +223,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BenchmarksRouteImport parentRoute: typeof rootRouteImport } + '/changelog': { + id: '/changelog' + path: '/changelog' + fullPath: '/changelog' + preLoaderRoute: typeof ChangelogRouteImport + parentRoute: typeof rootRouteImport + } + '/next': { + id: '/next' + path: '/next' + fullPath: '/next' + preLoaderRoute: typeof NextRouteImport + parentRoute: typeof rootRouteImport + } '/playground': { id: '/playground' path: '/playground' @@ -292,6 +332,8 @@ const RoadmapRouteWithChildren = const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, BenchmarksRoute: BenchmarksRoute, + ChangelogRoute: ChangelogRoute, + NextRoute: NextRoute, PlaygroundRoute: PlaygroundRoute, RoadmapRoute: RoadmapRouteWithChildren, UseCasesRoute: UseCasesRoute, diff --git a/web/apps/web/src/routes/__root.tsx b/web/apps/web/src/routes/__root.tsx index b3b32da..2d5ed3b 100644 --- a/web/apps/web/src/routes/__root.tsx +++ b/web/apps/web/src/routes/__root.tsx @@ -85,17 +85,20 @@ const NAV_LINKS = [ { to: "/docs", label: "Docs" }, { to: "/use-cases", label: "Use cases" }, { to: "/roadmap", label: "Roadmap" }, + { to: "/changelog", label: "Changelog" }, + { to: "/next", label: "Next actions" }, { to: "/benchmarks", label: "Benchmarks" }, { to: "/playground", label: "Playground" }, ] as const const QUICK_LINKS = [ { to: "/docs", label: "Browse docs" }, + { to: "/changelog", label: "Changelog" }, + { to: "/next", label: "Next actions" }, { to: "/playground", label: "Playground" }, { to: "/use-cases", label: "Use cases" }, { to: "/benchmarks", label: "Benchmarks" }, ] as const - function NotFoundPage() { return (
@@ -338,7 +341,7 @@ function SearchButton() { > Search - ⌘ + ⌘ K ) @@ -443,10 +446,17 @@ function SiteFooter() {

- Not a revolution but an evolution of the same bloodline. © 2026 Lithon + Not a revolution but an evolution of the same bloodline. © 2026 Lithon contributors.

+
- - {DOC_GROUPS.find((g) => g.slug === page.group)?.title ?? - page.group} - +
+ + {DOC_GROUPS.find((g) => g.slug === page.group)?.title ?? + page.group} + + {priorityOf(page) === "essential" && ( + + + on the reading path + + )} +

{page.title}

@@ -269,7 +286,7 @@ function DocsPage() {
diff --git a/web/apps/web/src/routes/docs/index.tsx b/web/apps/web/src/routes/docs/index.tsx index b76ba5f..f9d4f40 100644 --- a/web/apps/web/src/routes/docs/index.tsx +++ b/web/apps/web/src/routes/docs/index.tsx @@ -1,12 +1,19 @@ import { createFileRoute, Link } from "@tanstack/react-router" -import { ArrowLeftIcon, ArrowRightIcon } from "@phosphor-icons/react" +import { + ArrowRightIcon, + BookOpenIcon, + CompassIcon, + MagnifyingGlassIcon, + StarIcon, +} from "@phosphor-icons/react" import { Badge } from "@workspace/ui/components/badge" import { Card, CardContent } from "@workspace/ui/components/card" import { DOC_GROUPS } from "../../docs/model" -import { DOCS } from "../../docs/registry" +import { DOCS, outcomeOf, priorityOf } from "../../docs/registry" import { SearchHint } from "../../docs/templates" +import { assetUrl } from "../../lib/assets" import { metaForPath } from "../../lib/seo" import { PageHero, WRAP } from "../../site" @@ -30,7 +37,27 @@ function firstPageOf(groupSlug: string) { return DOCS.pages.find((p) => p.group === groupSlug) } +const PRIORITY_META = { + essential: { + label: "essential", + chip: "border-accent-strong/50 text-accent-strong", + Icon: StarIcon, + }, + recommended: { + label: "recommended", + chip: "text-muted-foreground", + Icon: CompassIcon, + }, + reference: { + label: "reference", + chip: "text-muted-foreground/70", + Icon: BookOpenIcon, + }, +} as const + function DocsIndex() { + const essential = DOCS.pages.filter((p) => priorityOf(p) === "essential") + return ( <>
-
+

- {DOCS.pages.length} pages · press to search - everything + {DOCS.pages.length} pages in{" "} + {DOC_GROUPS.length} sections ·{" "} + {essential.length} of them are the reading path

- Cmd/Ctrl + K +
+ + + + + Cmd/Ctrl + K +
+ {/* The reading path: the pages that are meant to be read in order. */} +
+
+ + + The reading path + + + these {essential.length} pages are the ones to read, in this order + +
+
    + {essential.map((page, i) => ( +
  1. + + + {i + 1} + + + + + {page.title} + + + {DOC_GROUPS.find((g) => g.slug === page.group)?.title} + + + + {outcomeOf(page) ?? page.description} + + + + +
  2. + ))} +
+
+ + {/* Everything, grouped. */}
{DOC_GROUPS.map((group) => { const pages = DOCS.pages.filter((p) => p.group === group.slug) @@ -78,18 +155,30 @@ function DocsIndex() { {group.blurb}

-
    - {pages.map((page) => ( -
  • - - {page.title} - -
  • - ))} +
      + {pages.map((page) => { + const meta = PRIORITY_META[priorityOf(page)] + return ( +
    • + + + {page.title} + + {priorityOf(page) === "essential" && ( + + )} + +
    • + ) + })}
    - Source to machine + + +
+ +
+

+ Looking for a specific behaviour? The reference pages are lookup + tables, not reading material. +

+ + Errors & diagnostics +
+ +
+

+ Building an agent, an index, or a dataset from these docs? Take the + whole thing as plain text instead of scraping it. +

+ +
) diff --git a/web/apps/web/src/routes/index.tsx b/web/apps/web/src/routes/index.tsx index fdc78df..05cd524 100644 --- a/web/apps/web/src/routes/index.tsx +++ b/web/apps/web/src/routes/index.tsx @@ -18,9 +18,29 @@ import { CardHeader, CardTitle, } from "@workspace/ui/components/card" +import { metaForPath, structuredData } from "../lib/seo" import { Separator } from "@workspace/ui/components/separator" -export const Route = createFileRoute("/")({ component: Home }) +export const Route = createFileRoute("/")({ + head: () => { + const meta = metaForPath("/") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/")) }, + ], + } + }, + component: Home }) const WRAP = "mx-auto w-full max-w-6xl px-4" @@ -106,7 +126,7 @@ const CONVICTIONS = [ ["engine", "C++20"], ["output", "x86-64"], ] as const, - text: "The distance between an idea and the instruction that runs it is three deliberate layers. No abstraction tax, and no intermediate representation that nobody owns.", + text: "The distance between an idea and the instruction that runs it is three deliberate layers, and the IR between them is a plain text format the project owns end to end: you can read the whole thing it emits.", }, { index: "04", diff --git a/web/apps/web/src/routes/next.tsx b/web/apps/web/src/routes/next.tsx new file mode 100644 index 0000000..7919932 --- /dev/null +++ b/web/apps/web/src/routes/next.tsx @@ -0,0 +1,339 @@ +import { createFileRoute, Link } from "@tanstack/react-router" +import { + ArrowRightIcon, + CheckCircleIcon, + CircleIcon, + GithubLogo, +} from "@phosphor-icons/react" + +import { Badge } from "@workspace/ui/components/badge" +import { Button } from "@workspace/ui/components/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@workspace/ui/components/card" + +import { metaForPath } from "../lib/seo" +import { PageHero, WRAP } from "../site" + +export const Route = createFileRoute("/next")({ + head: () => { + const meta = metaForPath("/next") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + ], + } + }, + component: NextActions, +}) + +type Task = { + title: string + detail: string + where: string + href?: string + status: "open" | "in-progress" | "done" +} + +const LANGS: Task[] = [ + { + title: "A standard library, or an honest absence of one", + detail: + "There is no `import` and no module loader, so `std/` is a sketch rather than a library. Either build the loader or say plainly that the language has no library story yet.", + where: "the language", + status: "open", + }, + { + title: "Closures and first-class functions", + detail: + "A function body cannot capture a caller's variable, and a function name is not a value. Both fall out of the current storage model, so they are a design question, not a bug.", + where: "the language", + status: "open", + }, + { + title: "A string API beyond three methods", + detail: + "`find`, `count` and `split` are the whole surface today. Slicing, concatenation and a `str` function parameter are all refused.", + where: "the language", + status: "open", + }, + { + title: "An `Optional` or `Union` of any kind", + detail: + "Every value is exactly one type. There is no null, no optional, no union, and therefore no way to express \"might not be there\" in the type system.", + where: "the language", + status: "open", + }, +] + +const ENGINE: Task[] = [ + { + title: "An ahead-of-time binary emit", + detail: + "Everything runs in-process today. Phase II targets a standalone `.bin`/`.exe` under 10 KB, with the header format and the decoupling from the tier runner still to be designed.", + where: "Phase II", + href: "/roadmap/phase-2-aot", + status: "open", + }, + { + title: "Float promotion on merges", + detail: + "A float merge is never promoted into an XMM register; it stays a memory round trip. The direct-Phi path fixed the int case; the float case needs an XMM pool.", + where: "Phase I", + href: "/roadmap/phase-1-dual-tier", + status: "in-progress", + }, + { + title: "ARM64", + detail: + "The whole backend is x86-64. An ARM64 port is Phase III, and the encoder design is deliberately small enough to make plausible.", + where: "Phase III", + href: "/roadmap/phase-3-systems", + status: "open", + }, + { + title: "Win64 test evidence", + detail: + "The Win64 ABI is implemented, but CI runs on Linux only, so the claim is untested. A Windows runner would turn it from an assertion into a fact.", + where: "Phase I", + status: "open", + }, +] + +const TOOLING: Task[] = [ + { + title: "Teach the standalone checker the container types", + detail: + "`tools/typecheck.py` is a mirror of the older scalar-only checks and answers `unknown type 'list'`. The real checker is the C++ one that runs inside `tier_runner`; the mirror should catch up or be retired.", + where: "toolchain", + status: "in-progress", + }, + { + title: "A native parser to replace the CPython `ast` shortcut", + detail: + "The frontend uses Python's own `ast` module as a shortcut. That is why the source syntax is exactly Python's, and also why it is limited to what CPython's grammar accepts.", + where: "toolchain", + status: "open", + }, + { + title: "A playground that runs in production", + detail: + "The playground's runner is a dev-only Vite middleware, so the deployed site can only type-check locally. A static, pre-generated IR fallback would make the page useful to a visitor.", + where: "the website", + status: "open", + }, + { + title: "Release tags", + detail: + "There are no git tags and no published releases yet, so the changelog is built from the commit history. A tag per version would give the history a spine.", + where: "the repository", + status: "open", + }, +] + +const DONE: Task[] = [ + { + title: "`!=` as a first-class compare", + detail: "Its own IR op, with IEEE 754 semantics on floats.", + where: "2026-10-20", + status: "done", + }, + { + title: "`try`/`except` over the engine's traps", + detail: "Seven catchable names, with `else` and `finally`.", + where: "2026-10-20", + status: "done", + }, + { + title: "Raw syscalls with real pointer arguments", + detail: "A pointer is a real host address in both tiers; the TCP loopback ships.", + where: "2026-10-14", + status: "done", + }, + { + title: "Packed-field structs", + detail: "The byte layout matches a C `#pragma pack(1)` struct exactly.", + where: "2026-10-13", + status: "done", + }, + { + title: "Explicit conversions and terminal input", + detail: "The width is always named; an out-of-range conversion traps.", + where: "2026-10-12", + status: "done", + }, + { + title: "The dynamic overflow trap", + detail: "Plain `+`/`-`/`*` on `int[64]` trap; `wrap_*` is the opt-out.", + where: "2026-10-10", + status: "done", + }, +] + +const STATUS_META = { + open: { label: "open", Icon: CircleIcon, cls: "text-muted-foreground" }, + "in-progress": { + label: "in progress", + Icon: CircleIcon, + cls: "text-amber-600 dark:text-amber-400", + }, + done: { + label: "shipped", + Icon: CheckCircleIcon, + cls: "text-emerald-600 dark:text-emerald-400", + }, +} as const + +function TaskCard({ task }: { task: Task }) { + const meta = STATUS_META[task.status] + const Icon = meta.Icon + return ( + + +
+ + + {task.title} + + + {task.where} + +
+
+ + + {task.detail} + + {task.href && ( + + see the roadmap + + + )} + +
+ ) +} + +function NextActions() { + return ( + <> + + What is still +
+ on the list. + + } + lede="The open questions, in the open. Every item here is a real gap between what Lithon does and what a language is expected to do — and every one of them is a design decision, not a bug." + meta={[ + `${LANGS.length + ENGINE.length + TOOLING.length} open items`, + "plus a public roadmap", + ]} + /> + +
+
+
+

The language

+ {LANGS.length} +
+
+ {LANGS.map((task) => ( + + ))} +
+
+ +
+
+

The engine

+ {ENGINE.length} +
+
+ {ENGINE.map((task) => ( + + ))} +
+
+ +
+
+

+ The toolchain and the site +

+ {TOOLING.length} +
+
+ {TOOLING.map((task) => ( + + ))} +
+
+ +
+
+

Recently shipped

+ {DONE.length} +
+
+ {DONE.map((task) => ( + + ))} +
+

+ The full history is on the{" "} + + changelog + + . +

+
+ +
+

+ The fastest way to help +

+

+ The project is MIT-licensed and the roadmap is public, so the + useful contributions are the ones that take an open item above and + make it a shipped one — with the tests that prove it. A good first + contribution is a program in the regression corpus that pins down + a behaviour nobody has pinned down yet. +

+
+ + +
+
+
+ + ) +} diff --git a/web/apps/web/src/routes/playground.tsx b/web/apps/web/src/routes/playground.tsx index 7768fa0..e9444f2 100644 --- a/web/apps/web/src/routes/playground.tsx +++ b/web/apps/web/src/routes/playground.tsx @@ -33,6 +33,8 @@ import { Kbd } from "@workspace/ui/components/kbd" import { PageHero, WRAP } from "../site" import { metaForPath } from "../lib/seo" import { LithonEditor } from "../lib/lithon-editor" +import { PLAYGROUND_EXAMPLES } from "../lib/playground-examples" +import { PLAYGROUND_IR } from "../lib/playground-ir" export const Route = createFileRoute("/playground")({ head: () => { @@ -59,96 +61,22 @@ type RunResult = { hint: string } -const EXAMPLES = [ - { - id: "hello", - title: "Hello, typed", - note: "the smallest annotated program", - source: `x: int[64] = 10 -y: int[64] = 20 +const EXAMPLES = PLAYGROUND_EXAMPLES -result: int[64] = x + y - -print(result)`, - }, - { - id: "fib", - title: "Recursive fibonacci", - note: "recursion, tier-1 native", - source: `def fib(n: int[64]) -> int[64]: - if n < 2: - return n - return fib(n - 1) + fib(n - 2) - -x: int[64] = fib(10) -print(x)`, - }, - { - id: "range", - title: "Typed range loop", - note: "for-range with a width-checked loop variable", - source: `total: int[64] = 0 -i: int[64] = 0 - -for i in range(10): - total = total + i - -print(total)`, - }, - { - id: "while", - title: "While loop", - note: "conditionals with a loop-carried accumulator", - source: `i: int[64] = 0 -total: int[64] = 0 - -while i < 10: - total = total + i - i = i + 1 - -print(total)`, - }, - { - id: "float", - title: "Float arithmetic", - note: "float[64], SSE2 end to end", - source: `a: float[64] = 3.5 -b: float[64] = 2.0 - -print(a + b) -print(a * b)`, - }, - { - id: "wrap", - title: "Wrapping arithmetic", - note: "wrap_add at the int[64] boundary", - source: `x: int[64] = 9223372036854775807 -y: int[64] = 1 -z: int[64] = wrap_add(x, y) -print(z)`, - }, - { - id: "pointer", - title: "Typed pointers", - note: "explicit raw memory, `_` prefix required", - source: `i: int[16] = 16 -_pi: ptr[int[16]] = addressof(i) -print(valueof(_pi))`, - }, - { - id: "list", - title: "Fixed-capacity list", - note: "capacity lives in the type", - source: `xs: list[int[64], 6] - -xs[0] = 10 -xs[1] = 20 -xs[2] = 30 +/** FNV-1a, matching `irKey()` in docs/templates.tsx. */ +function irKey(source: string) { + let h = 0x811c9dc5 + for (let i = 0; i < source.length; i++) { + h ^= source.charCodeAt(i) + h = Math.imul(h, 0x01000193) >>> 0 + } + return h.toString(16).padStart(8, "0") +} -print(xs[0]) -print(len(xs))`, - }, -] as const +/** The pre-generated IR for a built-in example, when there is one. */ +function staticIr(source: string): string | undefined { + return PLAYGROUND_IR[irKey(source)] +} function toDiagnostic(message: string, line = 1): Diagnostic { return { @@ -179,26 +107,45 @@ function Playground() { signal: controller.signal, }) if (!res.ok) { + // The live runner is dev-only. When it is not there, fall back to + // the pre-generated IR for a built-in example, and say plainly that + // this is the recorded output rather than a fresh run. + const fallback = staticIr(code) setResult({ - ok: false, - stage: "input", - diagnostics: [`runner unavailable (HTTP ${res.status})`], - ir: null, + ok: fallback !== undefined, + stage: fallback !== undefined ? "ir" : "input", + diagnostics: [ + fallback !== undefined + ? "the live runner is not available here, so this is the recorded IR for a built-in example" + : `runner unavailable (HTTP ${res.status})`, + ], + ir: fallback ?? null, typecheck: null, - hint: "The local runner is only wired up in dev (`bun run dev`).", + hint: + fallback !== undefined + ? "Run `bun run dev` locally to compile your own program." + : "The local runner is only wired up in dev (`bun run dev`).", }) return } setResult((await res.json()) as RunResult) } catch (e) { if ((e as Error).name === "AbortError") return + const fallback = staticIr(code) setResult({ - ok: false, - stage: "input", - diagnostics: [String(e)], - ir: null, + ok: fallback !== undefined, + stage: fallback !== undefined ? "ir" : "input", + diagnostics: [ + fallback !== undefined + ? "the live runner is not reachable, so this is the recorded IR for a built-in example" + : String(e), + ], + ir: fallback ?? null, typecheck: null, - hint: "Could not reach the local Lithon runner.", + hint: + fallback !== undefined + ? "Run `bun run dev` locally to compile your own program." + : "Could not reach the local Lithon runner.", }) } finally { setRunning(false) @@ -232,8 +179,8 @@ function Playground() { See the path. } - lede="A real editor with Lithon syntax highlighting and autocomplete, wired to the actual type checker and frontend in this repo. Press Run and you get the engine's own verdict, not a simulation." - meta={["Real toolchain", "Live IR", "Local only"]} + lede="A real editor with Lithon syntax highlighting and autocomplete, wired to the actual type checker and frontend in this repo. The live compiler runs in local development; the built-in examples carry the IR the engine really emits." + meta={["Real toolchain", "Recorded IR", "Live in dev"]} />
@@ -305,9 +252,13 @@ function Playground() { size="icon-xs" variant="ghost" onClick={() => { - navigator.clipboard.writeText(code) - setCopied(true) - window.setTimeout(() => setCopied(false), 1200) + navigator.clipboard + .writeText(code) + .then(() => { + setCopied(true) + window.setTimeout(() => setCopied(false), 1200) + }) + .catch(() => {}) }} aria-label="Copy source" > diff --git a/web/apps/web/src/routes/roadmap.tsx b/web/apps/web/src/routes/roadmap.tsx index 7579a60..a170733 100644 --- a/web/apps/web/src/routes/roadmap.tsx +++ b/web/apps/web/src/routes/roadmap.tsx @@ -17,9 +17,29 @@ import { } from "@workspace/ui/components/card" import { Separator } from "@workspace/ui/components/separator" +import { metaForPath, structuredData } from "../lib/seo" import { Note, PageHero, SectionHead, WRAP } from "../site" -export const Route = createFileRoute("/roadmap")({ component: Roadmap }) +export const Route = createFileRoute("/roadmap")({ + head: () => { + const meta = metaForPath("/roadmap") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap")) }, + ], + } + }, + component: Roadmap }) type Status = "done" | "active" | "next" | "later" @@ -95,7 +115,7 @@ const PHASES: readonly { { t: "VEX/SSE transition discipline", s: "done", - d: "A scanner that disassembles each function and flags legacy SSE after a VEX prefix with no vzeroupper. No AVX is emitted yet, so this is a guardrail laid before the road, and its --self-test mode proves the scanner actually fires.", + d: "A scanner that disassembles each function and flags legacy SSE after a VEX prefix with no vzeroupper. AVX2 is now emitted by the vectorizer, so this guardrail is live rather than hypothetical, and its --self-test mode proves the scanner actually fires.", e: "tools/check_vex_transitions.py --self-test proves the scanner fires", }, { @@ -108,13 +128,13 @@ const PHASES: readonly { t: "Static type flow verifier", s: "done", d: "Mandatory annotations, explicit integer widths, automatic widening and no narrowing anywhere. A bare int is not a type here: the checker rejects it by name, because a type that cannot be written down cannot be proven.", - e: "tools/typecheck.py · run_typed_regression.py 13/13", + e: "tools/typecheck.py · run_typed_regression.py 46/46", }, { t: "IR text format", s: "done", d: "The intermediate representation is parsed inside the engine, so the native tier carries no Python dependency of any kind. CPython stops existing once IR exists.", - e: "src/ir/text_parser.cpp · run_regression.py 15/15", + e: "src/ir/text_parser.cpp · run_regression.py 34/34", }, { t: "Calls, recursion and tail calls", @@ -132,7 +152,7 @@ const PHASES: readonly { t: "Floating point, end to end", s: "done", d: "SSE2 arithmetic, comparisons, load and store, and native print. One formatter serves both tiers, so the two agree byte for byte rather than approximately.", - e: "compile_module_float_test · float_format_test · run_tier_diff.py 39/39", + e: "compile_module_float_test · float_format_test · run_tier_diff.py 168/168", }, { t: "Modulo with C semantics", @@ -149,7 +169,7 @@ const PHASES: readonly { { t: "A direct Phi emitter", s: "active", - d: "25 of 26 IR opcodes are emitted; this is the last one. What is owed is value-kind inference for a float merge, an allocator slot for the result, and codegen for the join's parallel copies.", + d: "All 55 IR opcodes are emitted. The direct-Phi path and the edge-copy path both ship; what is still owed is value-kind inference for a float merge and an XMM allocator pool, so a float phi does not have to round-trip through memory.", e: "compute_edge_uses() in liveness.h · resolve_phis() in optimize.h", }, { @@ -207,7 +227,7 @@ const PHASES: readonly { { t: "AVX2 and SIMD vectorization", s: "later", - d: "Parallel list processing across a vector unit. The detection gate and the VEX transition guardrail are already in place underneath it; no AVX is emitted yet.", + d: "Parallel list processing across a vector unit. The AVX2 vectorizer now emits 8-wide ymm code over int[32] lists; the detection gate and the VEX transition guardrail underneath it are what keep the result exact.", e: "cpu_features.h gates it · check_vex_transitions.py keeps it honest", }, { diff --git a/web/apps/web/src/routes/roadmap/language.tsx b/web/apps/web/src/routes/roadmap/language.tsx index 1bfcb5c..9e6d63e 100644 --- a/web/apps/web/src/routes/roadmap/language.tsx +++ b/web/apps/web/src/routes/roadmap/language.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react" import { createFileRoute } from "@tanstack/react-router" +import { metaForPath, structuredData } from "../../lib/seo" import { CodeBlock, DocSection, @@ -11,6 +12,24 @@ import { } from "../../site" export const Route = createFileRoute("/roadmap/language")({ + head: () => { + const meta = metaForPath("/roadmap/language") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap/language")) }, + ], + } + }, component: LanguageReference, }) @@ -27,7 +46,7 @@ const NAV = [ const TYPES_ROWS: Rows = [ ["signed integers", int[8] int[16] int[32] int[64]], - ["floating point", float[32] float[64]], + ["floating point", float[64]], [ "boolean", @@ -92,7 +111,7 @@ const SUBSET_ROWS: Rows = [ for x in range(N) · one argument , ], - ["functions", "recursion, self tail calls · max 2 arguments"], + ["functions", "recursion, self tail calls · 32 arguments"], [ "chained comparison", diff --git a/web/apps/web/src/routes/roadmap/phase-1-dual-tier.tsx b/web/apps/web/src/routes/roadmap/phase-1-dual-tier.tsx index ddc6d40..134baaa 100644 --- a/web/apps/web/src/routes/roadmap/phase-1-dual-tier.tsx +++ b/web/apps/web/src/routes/roadmap/phase-1-dual-tier.tsx @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router" import { Card, CardContent } from "@workspace/ui/components/card" +import { metaForPath, structuredData } from "../../lib/seo" import { CodeBlock, DocSection, @@ -12,6 +13,24 @@ import { } from "../../site" export const Route = createFileRoute("/roadmap/phase-1-dual-tier")({ + head: () => { + const meta = metaForPath("/roadmap/phase-1-dual-tier") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap/phase-1-dual-tier")) }, + ], + } + }, component: Phase1DualTier, }) @@ -62,7 +81,7 @@ function Phase1DualTier() { } lede="How a block of typed Python becomes x86-64 bytes, what the optimizer does to them on the way, and the eight SSA phases that turned a memory-based JIT into an SSA one." - meta={["In progress", "25 of 26 opcodes", "8 SSA phases"]} + meta={["Shipped", "55 of 55 opcodes", "8 SSA phases"]} />
@@ -411,7 +430,7 @@ phi copies: 5 of 9 in registers`}

- No codegen path emits Op::Phi, so resolution + Op::Phi is emitted on the incoming edges, and a direct-Phi native path also exists, so resolution stores each incoming value on its predecessor edge{" "} and turns the Phi into a load. That is what makes conditional expressions work today, and it is correct, --ssa{" "} @@ -455,7 +474,7 @@ phi copies: 5 of 9 in registers`} 25 of 26], + ["IR opcodes emitted", 55 of 55], [ "Phi", diff --git a/web/apps/web/src/routes/roadmap/phase-2-aot.tsx b/web/apps/web/src/routes/roadmap/phase-2-aot.tsx index de87f1d..0b6a52b 100644 --- a/web/apps/web/src/routes/roadmap/phase-2-aot.tsx +++ b/web/apps/web/src/routes/roadmap/phase-2-aot.tsx @@ -2,9 +2,28 @@ import { createFileRoute } from "@tanstack/react-router" import { Card, CardContent } from "@workspace/ui/components/card" +import { metaForPath, structuredData } from "../../lib/seo" import { DocSection, Note, PageHero, SpecTable, WRAP } from "../../site" export const Route = createFileRoute("/roadmap/phase-2-aot")({ + head: () => { + const meta = metaForPath("/roadmap/phase-2-aot") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap/phase-2-aot")) }, + ], + } + }, component: Phase2Aot, }) diff --git a/web/apps/web/src/routes/roadmap/phase-3-systems.tsx b/web/apps/web/src/routes/roadmap/phase-3-systems.tsx index 771d1af..1e56028 100644 --- a/web/apps/web/src/routes/roadmap/phase-3-systems.tsx +++ b/web/apps/web/src/routes/roadmap/phase-3-systems.tsx @@ -1,9 +1,28 @@ import type { ReactNode } from "react" import { createFileRoute } from "@tanstack/react-router" +import { metaForPath, structuredData } from "../../lib/seo" import { DocSection, Note, PageHero, SpecTable, WRAP } from "../../site" export const Route = createFileRoute("/roadmap/phase-3-systems")({ + head: () => { + const meta = metaForPath("/roadmap/phase-3-systems") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap/phase-3-systems")) }, + ], + } + }, component: Phase3Systems, }) @@ -62,8 +81,8 @@ const GROUNDWORK: readonly Row[] = [ ], [ "AVX emission", - - , · not started + + shipped · 8-wide ymm over int[32] lists , ], ] diff --git a/web/apps/web/src/routes/roadmap/verification.tsx b/web/apps/web/src/routes/roadmap/verification.tsx index 4255775..6ba349c 100644 --- a/web/apps/web/src/routes/roadmap/verification.tsx +++ b/web/apps/web/src/routes/roadmap/verification.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router" import { Button } from "@workspace/ui/components/button" import { Card, CardContent } from "@workspace/ui/components/card" +import { metaForPath, structuredData } from "../../lib/seo" import { CodeBlock, DocSection, @@ -13,6 +14,24 @@ import { } from "../../site" export const Route = createFileRoute("/roadmap/verification")({ + head: () => { + const meta = metaForPath("/roadmap/verification") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/roadmap/verification")) }, + ], + } + }, component: Verification, }) @@ -114,12 +133,12 @@ function Verification() {

{`$ cmake -B build -DCMAKE_BUILD_TYPE=Release $ cmake --build build -j$(nproc) -$ ctest --test-dir build --output-on-failure # 28/28 +$ ctest --test-dir build --output-on-failure # 33/33 $ bash tools/verify_all.sh # the whole gate -$ python3 tools/run_regression.py # 15/15 untyped -$ python3 tools/run_typed_regression.py # 13/13 typed -$ python3 tools/run_tier_diff.py # 39/39, both tiers`} +$ python3 tools/run_regression.py # 34/34 untyped +$ python3 tools/run_typed_regression.py # 46/46 typed +$ python3 tools/run_tier_diff.py # 168/168, both tiers`}

run_tier_diff.py is the highest-value of those. It runs every program through both tiers and requires @@ -267,7 +286,7 @@ $ python3 tools/native_bench.py --runs 30 --pin 2 --compare /tmp/before.json`}

- 25 of 26 opcodes are emitted and each is exercised through{" "} + All 55 opcodes are emitted and each is exercised through{" "} tier_runner --strict, which proves the opcode really executed natively rather than fell back. It does not prove every operand shape is right, that is what the encoder's diff --git a/web/apps/web/src/routes/use-cases.tsx b/web/apps/web/src/routes/use-cases.tsx index df89302..03f1bdb 100644 --- a/web/apps/web/src/routes/use-cases.tsx +++ b/web/apps/web/src/routes/use-cases.tsx @@ -9,9 +9,29 @@ import { CardTitle, } from "@workspace/ui/components/card" +import { metaForPath, structuredData } from "../lib/seo" import { PageHero, SectionHead, WRAP } from "../site" -export const Route = createFileRoute("/use-cases")({ component: UseCases }) +export const Route = createFileRoute("/use-cases")({ + head: () => { + const meta = metaForPath("/use-cases") + return { + meta: [ + { title: meta.title }, + { name: "description", content: meta.description }, + { property: "og:title", content: meta.title }, + { property: "og:description", content: meta.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: meta.canonical }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:title", content: meta.title }, + { name: "twitter:description", content: meta.description }, + { tagName: "link", rel: "canonical", href: meta.canonical }, + { "script:ld+json": JSON.stringify(structuredData("/use-cases")) }, + ], + } + }, + component: UseCases }) const CASES = [ { diff --git a/web/apps/web/src/search/actions.ts b/web/apps/web/src/search/actions.ts index 3a71573..a27a9c9 100644 --- a/web/apps/web/src/search/actions.ts +++ b/web/apps/web/src/search/actions.ts @@ -66,6 +66,27 @@ export function actionEntries(): ActionEntry[] { tags: ["roadmap", "phases", "future", "navigate"], run: "nav:/roadmap", }, + { + id: "action:nav-next", + title: "Go to Next actions", + subtitle: "The open items, item by item", + tags: ["next", "actions", "todo", "open", "gap", "navigate"], + run: "nav:/next", + }, + { + id: "action:nav-changelog", + title: "Go to Changelog", + subtitle: "What changed, and when", + tags: ["changelog", "history", "releases", "notes", "navigate"], + run: "nav:/changelog", + }, + { + id: "action:nav-use-cases", + title: "Go to Use cases", + subtitle: "The problems Lithon is shaped for", + tags: ["use cases", "cases", "examples", "navigate"], + run: "nav:/use-cases", + }, { id: "action:nav-contributors", title: "Go to Contributors", @@ -122,5 +143,12 @@ export function actionEntries(): ActionEntry[] { tags: ["discord", "chat", "community", "external"], run: "https://discord.gg/project-lithon", }, + { + id: "action:external-llms", + title: "Open llms.txt", + subtitle: "The documentation as plain text, for an AI reader", + tags: ["llms", "ai", "llm", "text", "markdown", "plain text", "external"], + run: "https://project-lithon.github.io/Lithon/llms.txt", + }, ] } diff --git a/web/apps/web/src/search/engine.ts b/web/apps/web/src/search/engine.ts index 812247c..179e1c9 100644 --- a/web/apps/web/src/search/engine.ts +++ b/web/apps/web/src/search/engine.ts @@ -19,6 +19,7 @@ function scoreTerm(entry: SearchEntry, term: string): number { const subtitle = entry.subtitle?.toLowerCase() ?? "" const tags = entry.tags.map((t) => t.toLowerCase()) const group = entry.group?.toLowerCase() ?? "" + const body = entry.body?.toLowerCase() ?? "" if (title === term) return 100 if (title.startsWith(term)) return 80 @@ -30,10 +31,22 @@ function scoreTerm(entry: SearchEntry, term: string): number { } if (subtitle.includes(term)) return 30 if (group.includes(term)) return 20 + // The body is the last resort before the fuzzy match, so an exact hit in + // the prose still beats a subsequence hit in the title. + if (body) { + if (body.includes(term)) return 18 + // an error code like E0301, or a symbol like `wrap_add` + if (new RegExp(`\\b${escapeRegExp(term)}\\b`).test(body)) return 25 + } if (fuzzy(title, term)) return 15 + if (body && fuzzy(body, term)) return 8 return 0 } +function escapeRegExp(text: string) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + function fuzzy(haystack: string, needle: string): boolean { let i = 0 for (const ch of haystack) { diff --git a/web/package.json b/web/package.json index f53dbb6..5dd1e43 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,12 @@ "lint": "turbo lint", "format": "turbo format", "typecheck": "turbo typecheck", - "verify:docs": "bun scripts/verify-docs.ts" + "verify:docs": "bun scripts/verify-docs.ts", + "gen:ir": "bun scripts/gen-docs-ir.ts && bun scripts/gen-playground-ir.ts", + "gen:llms": "bun scripts/gen-llms.ts", + "gen:seo": "bun scripts/gen-seo.ts", + "gen": "bun run gen:ir && bun run gen:llms && bun run gen:seo", + "check:encoding": "bun scripts/fix-encoding.ts --check" }, "devDependencies": { "happy-dom": "^20.14.6", diff --git a/web/scripts/fix-encoding.ts b/web/scripts/fix-encoding.ts new file mode 100644 index 0000000..2dc7ec1 --- /dev/null +++ b/web/scripts/fix-encoding.ts @@ -0,0 +1,142 @@ +/** + * Repairs Windows-1252 -> UTF-8 mojibake in the web app's source files. + * + * A character like U+2192 (RIGHTWARDS ARROW) is three UTF-8 bytes: + * E2 86 92. If those bytes are read as Windows-1252 they become three + * visible characters: U+00E2, U+2020 and U+2019. This script finds those + * runs and decodes them back. + * + * Run: bun scripts/fix-encoding.ts [--check] + */ +import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs" +import { join, extname } from "node:path" + +/** Windows-1252 code points 0x80-0x9F do not map to Latin-1. */ +const CP1252_HIGH: Record = { + 0x20ac: 0x80, 0x0081: 0x81, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, + 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87, 0x02c6: 0x88, 0x2030: 0x89, + 0x0160: 0x8a, 0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8d, 0x017e: 0x8e, + 0x008f: 0x8f, 0x0090: 0x90, 0x2018: 0x91, 0x2019: 0x92, 0x201c: 0x93, + 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97, 0x02dc: 0x98, + 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c, 0x0178: 0x9e, +} + +/** The byte a Windows-1252 character stands for, or -1. */ +function cp1252Byte(cp: number): number { + if (cp >= 0x00a0 && cp <= 0x00ff) return cp + const mapped = CP1252_HIGH[cp] + if (mapped !== undefined) return mapped + if (cp >= 0x0080 && cp <= 0x009f) return cp + return -1 +} + +/** + * The three lead characters of a mojibake run: the UTF-8 encoding of + * U+00E2, U+00C2 and U+00C3 are the two- and three-byte sequences that + * Windows-1252 renders as three separate characters. + */ +const LEADS = new Set([0x00e2, 0x00c2, 0x00c3, 0x00e3, 0x00c5]) + +/** + * A run is a lead character followed by one or more bytes that could + * continue a UTF-8 sequence. + */ +function isRunChar(cp: number): boolean { + if (cp < 0x0080) return false + const b = cp1252Byte(cp) + if (b < 0) return false + return b >= 0x80 +} + +function repair(text: string): { text: string; fixes: number } { + let out = text + let fixes = 0 + // A file can be encoded more than once over (a mojibake round trip + // through a build step, then another), so the repair runs until the text + // stops changing. + for (let round = 0; round < 6; round++) { + const once = repairOnce(out) + if (once.fixes === 0) break + out = once.text + fixes += once.fixes + } + return { text: out, fixes } +} + +function repairOnce(text: string): { text: string; fixes: number } { + let out = "" + let fixes = 0 + let i = 0 + while (i < text.length) { + const cp = text.codePointAt(i)! + if (!LEADS.has(cp) || cp > 0xffff) { + out += text[i] + i += 1 + continue + } + // collect the run + let j = i + 1 + while (j < text.length && isRunChar(text.codePointAt(j)!)) j += 1 + const run = text.slice(i, j) + const bytes: number[] = [] + let ok = true + for (const ch of run) { + const b = cp1252Byte(ch.codePointAt(0)!) + if (b < 0) { + ok = false + break + } + bytes.push(b) + } + if (ok && bytes.length >= 2) { + const decoded = new TextDecoder("utf-8", { fatal: true }).decode( + new Uint8Array(bytes), + ) + if (decoded !== run) { + out += decoded + fixes += 1 + i = j + continue + } + } + out += text[i] + i += 1 + } + return { text: out, fixes } +} + +function walk(dir: string, out: string[] = []): string[] { + for (const entry of readdirSync(dir)) { + if (entry === "node_modules" || entry === ".git" || entry === "dist") continue + const full = join(dir, entry) + const st = statSync(full) + if (st.isDirectory()) walk(full, out) + else if ([".ts", ".tsx", ".css", ".json", ".md"].includes(extname(full))) + out.push(full) + } + return out +} + +const check = process.argv.includes("--check") +const roots = ["apps/web/src", "packages/ui/src", "scripts"] +let totalFixes = 0 +let totalFiles = 0 + +for (const root of roots) { + for (const file of walk(root)) { + const original = readFileSync(file, "utf8") + const { text, fixes } = repair(original) + if (fixes === 0) continue + totalFixes += fixes + totalFiles += 1 + console.log(`${check ? "would fix" : "fixed"} ${file} (${fixes})`) + if (!check) writeFileSync(file, text, "utf8") + } +} + +console.log( + check + ? `${totalFixes} mojibake run(s) in ${totalFiles} file(s)` + : `repaired ${totalFixes} mojibake run(s) in ${totalFiles} file(s)`, +) +process.exit(check && totalFixes > 0 ? 1 : 0) diff --git a/web/scripts/gen-docs-ir.ts b/web/scripts/gen-docs-ir.ts index 8fbc260..68dff5c 100644 --- a/web/scripts/gen-docs-ir.ts +++ b/web/scripts/gen-docs-ir.ts @@ -6,23 +6,17 @@ import { mkdirSync, writeFileSync } from "node:fs" import { dirname, join, resolve } from "node:path" import { DOCS } from "../apps/web/src/docs/registry" +import { requireUv, runTool } from "./toolchain" -const WEB_ROOT = resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..") +const WEB_ROOT = resolve( + dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), + "..", +) const REPO_ROOT = resolve(WEB_ROOT, "..") const FRONTEND = join(REPO_ROOT, "src", "frontend", "frontend.py") -const UV = - process.env.UV_PATH ?? - join( - process.env.LOCALAPPDATA ?? "", - "Microsoft", - "WinGet", - "Packages", - "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", - "uv.exe", - ) const CACHE_DIR = join(WEB_ROOT, "node_modules", ".cache") -function hash(source: string) { +export function hash(source: string) { // FNV-1a, matching irKey() in apps/web/src/docs/templates.tsx let h = 0x811c9dc5 for (let i = 0; i < source.length; i++) { @@ -32,6 +26,25 @@ function hash(source: string) { return h.toString(16).padStart(8, "0") } +/** + * Normalises the frontend's output to LF, with no trailing blank lines. + * + * The frontend emits its IR with the platform's line ending, so the same + * program produces CRLF on Windows and LF on Linux. The generated file has + * to be byte-identical on every platform, or the CI freshness check fails + * on one of them. + */ +export function normalise(text: string): string { + return text + .replace(/\r\n/g, "\n") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0) + .join("\n") +} + +const UV = requireUv() + const out: Record = {} const failed: { where: string; why: string }[] = [] let tmpIndex = 0 @@ -48,15 +61,12 @@ for (const page of DOCS.pages) { mkdirSync(CACHE_DIR, { recursive: true }) writeFileSync(file, ex.source, "utf8") - const proc = Bun.spawnSync( - [UV, "run", "--quiet", "--python", "3.12", FRONTEND, file], - { stdout: "pipe", stderr: "pipe" }, - ) - const text = new TextDecoder().decode(proc.stdout).trim() - if (proc.exitCode === 0 && text) { + const res = runTool(UV, [FRONTEND, file], REPO_ROOT) + const text = normalise(res.out) + if (res.code === 0 && text) { out[hash(ex.source)] = text } else { - const err = new TextDecoder().decode(proc.stderr).trim().split("\n").pop() + const err = res.err.trim().split("\n").pop() failed.push({ where: `${page.slug}#${section.id}[${i}]`, why: err ?? "frontend refused", @@ -78,8 +88,12 @@ export const EXAMPLE_IR: Record = ${JSON.stringify(out, null, 2) "utf8", ) -console.log(`wrote ${Object.keys(out).length} IR blobs to apps/web/src/docs/generated/ir.ts`) +console.log( + `wrote ${Object.keys(out).length} IR blobs to apps/web/src/docs/generated/ir.ts`, +) if (failed.length > 0) { - console.log(`\n${failed.length} example(s) the frontend refused (expected for rejected-by-design examples):`) + console.log( + `\n${failed.length} example(s) the frontend refused (expected for rejected-by-design examples):`, + ) for (const f of failed) console.log(` ${f.where}: ${f.why.slice(0, 110)}`) -} \ No newline at end of file +} diff --git a/web/scripts/gen-llms.ts b/web/scripts/gen-llms.ts new file mode 100644 index 0000000..b86c5c4 --- /dev/null +++ b/web/scripts/gen-llms.ts @@ -0,0 +1,289 @@ +/** + * Generates the AI-facing surfaces of the site: + * + * public/llms.txt - a compact index, the llms.txt convention + * public/llms-full.txt - the whole documentation as one plain-text file + * + * Both are built from the same registry the site renders, so they cannot + * drift from what a reader sees. + * + * Run: bun scripts/gen-llms.ts + */ +import { mkdirSync, writeFileSync } from "node:fs" +import { join } from "node:path" + +import { DOC_GROUPS } from "../apps/web/src/docs/model" +import { DOCS, outcomeOf, priorityOf } from "../apps/web/src/docs/registry" +import { SITE, SITEMAP_HOST } from "../apps/web/src/lib/seo" +import { CHANGES } from "../apps/web/src/changelog/data" + +const PUBLIC = join("apps", "web", "public") + +/** Strips the inline markup a doc block carries, leaving readable prose. */ +function plain(text: string): string { + return text + .replace(/`([^`]+)`/g, "$1") + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") + .trim() +} + +/** Flattens one block into plain-text lines. */ +function blockLines(block: unknown): string[] { + if (!block || typeof block !== "object") return [] + const b = block as Record + const out: string[] = [] + switch (b.kind) { + case "p": + if (typeof b.text === "string") out.push(plain(b.text)) + break + case "note": + case "callout": + out.push( + `> **${plain(String(b.title ?? ""))}** — ${plain(String(b.text ?? ""))}`, + ) + break + case "quote": + out.push(`> ${plain(String(b.text ?? ""))}`) + if (typeof b.source === "string") out.push(`> — ${plain(b.source)}`) + break + case "list": { + const items = (b.items as string[] | undefined) ?? [] + for (const item of items) out.push(`- ${plain(item)}`) + break + } + case "table": + case "comparison": { + if (typeof b.caption === "string") out.push(`*${plain(b.caption)}*`) + const rows = (b.rows as unknown[][] | undefined) ?? [] + const header = rows.length > 0 ? (rows[0] as unknown[]) : [] + if (header.length) { + out.push(`| ${header.map((c) => plain(String(c))).join(" | ")} |`) + out.push(`| ${header.map(() => "---").join(" | ")} |`) + for (const row of rows.slice(1)) { + out.push( + `| ${(row as unknown[]).map((c) => plain(String(c))).join(" | ")} |`, + ) + } + } + break + } + case "deflist": { + if (typeof b.caption === "string") out.push(`*${plain(b.caption)}*`) + for (const item of (b.items as { term: string; detail: string }[]) ?? []) { + out.push(`- **${plain(item.term)}** — ${plain(item.detail)}`) + } + break + } + case "layout": { + if (typeof b.caption === "string") out.push(`*${plain(b.caption)}*`) + out.push(`Total size: ${b.size}`) + for (const f of (b.fields as { name: string; offset: string; size: string }[]) ?? []) { + out.push(`- ${f.name} @ ${f.offset} (${f.size})`) + } + break + } + case "steps": + case "cards": + case "linkcards": { + for (const item of (b.items as { title: string; text: string }) ?? []) { + out.push(`- **${plain(item.title)}** — ${plain(item.text)}`) + } + break + } + case "code": { + const ex = b.example as { + title?: string + lang?: string + source: string + output?: string + note?: string + expect?: string + errorMatch?: string + sourceRef?: string + } + if (ex.title) out.push(`**${plain(ex.title)}**`) + out.push("```" + (ex.lang === "lithon" ? "python" : (ex.lang ?? "text"))) + out.push(ex.source) + out.push("```") + if (ex.expect === "error" && ex.errorMatch) { + out.push(`Expected to be refused with: \`${ex.errorMatch}\``) + } else if (ex.output) { + out.push("Output:") + out.push("```") + out.push(ex.output) + out.push("```") + } + if (ex.note) out.push(`*${plain(ex.note)}*`) + if (ex.sourceRef) out.push(`Source: \`${ex.sourceRef}\``) + break + } + case "tabs": { + if (typeof b.caption === "string") out.push(`*${plain(b.caption)}*`) + for (const item of (b.items as { label: string; blocks: unknown[] }) ?? []) { + out.push(`#### ${plain(item.label)}`) + for (const inner of item.blocks) out.push(...blockLines(inner)) + } + break + } + default: + break + } + return out +} + +// ---------------------------------------------------------------- llms.txt + +const llms: string[] = [ + `# ${SITE.name}`, + "", + `> ${SITE.description}`, + "", + `Lithon is a Python library and native x86-64 execution engine. Source files use the \`.lio\` extension. Every program is statically verified before it runs; a program the backend cannot prove is refused rather than guessed at.`, + "", + "## The one rule", + "", + "> If Lithon cannot prove that a program satisfies the rules required by its native backend, it refuses native compilation rather than guessing.", + "", + "## Type system in one block", + "", + "```", + "int[8] int[16] int[32] int[64] signed, fixed width", + "float[64] IEEE-754 binary64 (float[32] is refused)", + "bool 1 byte, reads back as 0/1", + "str[N] N bytes, NUL-terminated, bounded", + "list[T, N] fixed capacity N", + "tuple[T, N] fixed length N, homogeneous", + "dict[K, V, B] B buckets, B must be a power of two", + "ptr[T] typed pointer, variable name needs a leading _", + "class P(Struct) packed fields, no padding", + "class E(Enum) a tag plus the largest payload", + "```", + "", + "There is no bare `int`, no `uint`, no `None`, no `lambda`, no closure, no `set`, no `bytes`, no `Optional`/`Union`, no `**`, no `//`, no `<=`/`>=`, and no chained comparison.", + "", + "## A small program", + "", + "```python", + "x: int[64] = 10", + "y: int[64] = 20", + "", + "result: int[64] = x + y", + "", + "print(result)", + "```", + "", + "## Documentation index", + "", +] + +for (const group of DOC_GROUPS) { + const pages = DOCS.pages.filter((p) => p.group === group.slug) + if (pages.length === 0) continue + llms.push(`### ${group.title}`) + llms.push("") + if (group.blurb) { + llms.push(group.blurb) + llms.push("") + } + for (const page of pages) { + const priority = priorityOf(page) + const marker = + priority === "essential" ? "[essential]" : priority === "reference" ? "[reference]" : "" + const outcome = outcomeOf(page) ?? page.description + llms.push(`- [${page.title}](${SITEMAP_HOST}/docs/${page.slug})${marker} — ${outcome}`) + } + llms.push("") +} + +llms.push("### Site sections") +llms.push("") +for (const [label, path, blurb] of [ + ["Changelog", "/changelog", "Every shipped change, newest first."], + ["Next actions", "/next", "The open items, item by item."], + ["Roadmap", "/roadmap", "The three phases and their milestones."], + ["Benchmarks", "/benchmarks", "Recorded CPython vs Lithon runs."], + ["Use cases", "/use-cases", "The problems Lithon is shaped for."], + ["Playground", "/playground", "Write Lithon in the browser."], +] as const) { + llms.push(`- [${label}](${SITEMAP_HOST}${path}) — ${blurb}`) +} +llms.push("") +llms.push("## Optional") +llms.push("") +llms.push( + `- [Full documentation as plain text](${SITEMAP_HOST}/llms-full.txt) — every page, every section, every example.`, +) +llms.push( + `- [The repository](${SITE.repo}) — the source, the tests, and the verification suites.`, +) +llms.push("") + +writeFileSync(join(PUBLIC, "llms.txt"), llms.join("\n"), "utf8") + +// ------------------------------------------------------------ llms-full.txt + +const full: string[] = [ + `# ${SITE.name} — full documentation`, + "", + `> ${SITE.description}`, + "", + `Generated from the site's own docs registry. Every example below is verified against the real Lithon frontend.`, + "", + "---", + "", +] + +for (const group of DOC_GROUPS) { + const pages = DOCS.pages.filter((p) => p.group === group.slug) + if (pages.length === 0) continue + full.push(`## ${group.title}`) + full.push("") + if (group.blurb) { + full.push(group.blurb) + full.push("") + } + for (const page of pages) { + full.push(`### ${page.title}`) + full.push("") + full.push(`*${page.description}*`) + full.push("") + full.push(`URL: ${SITEMAP_HOST}/docs/${page.slug}`) + full.push("") + for (const section of page.sections) { + full.push(`#### ${section.title}`) + full.push("") + for (const block of section.blocks) { + const lines = blockLines(block) + if (lines.length) { + full.push(...lines) + full.push("") + } + } + } + full.push("---") + full.push("") + } +} + +full.push("## Changelog") +full.push("") +for (const change of CHANGES) { + full.push( + `### ${change.title}${change.date ? ` (${change.date})` : ""}`, + ) + full.push("") + full.push(plain(change.detail)) + if (change.href) { + full.push("") + full.push(`Docs: ${SITEMAP_HOST}${change.href}`) + } + full.push("") +} + +writeFileSync(join(PUBLIC, "llms-full.txt"), full.join("\n"), "utf8") + +mkdirSync(PUBLIC, { recursive: true }) +console.log( + `wrote llms.txt (${llms.length} lines) and llms-full.txt (${full.length} lines)`, +) diff --git a/web/scripts/gen-playground-ir.ts b/web/scripts/gen-playground-ir.ts new file mode 100644 index 0000000..2b45271 --- /dev/null +++ b/web/scripts/gen-playground-ir.ts @@ -0,0 +1,92 @@ +/** + * Generates the static IR fallback for the playground's built-in examples. + * + * The playground's live runner only exists in `vite dev`, so the deployed + * site needs a fallback. This script runs the real frontend over each + * built-in example and writes the result, so the page shows the exact IR + * the engine would emit rather than a mock. + * + * Run: bun scripts/gen-playground-ir.ts + */ +import { mkdirSync, writeFileSync } from "node:fs" +import { dirname, join, resolve } from "node:path" + +import { PLAYGROUND_EXAMPLES } from "../apps/web/src/lib/playground-examples" +import { requireUv, runTool } from "./toolchain" + +const WEB_ROOT = resolve( + dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), + "..", +) +const REPO_ROOT = resolve(WEB_ROOT, "..") +const FRONTEND = join(REPO_ROOT, "src", "frontend", "frontend.py") +const CACHE_DIR = join(WEB_ROOT, "node_modules", ".cache") + +export function hash(source: string) { + // FNV-1a, matching irKey() in apps/web/src/docs/templates.tsx + let h = 0x811c9dc5 + for (let i = 0; i < source.length; i++) { + h ^= source.charCodeAt(i) + h = Math.imul(h, 0x01000193) >>> 0 + } + return h.toString(16).padStart(8, "0") +} + +/** + * Normalises the frontend's output to LF, with no trailing blank lines. + * + * The frontend emits its IR with the platform's line ending, so the same + * program produces CRLF on Windows and LF on Linux. The generated file has + * to be byte-identical on every platform. + */ +export function normalise(text: string): string { + return text + .replace(/\r\n/g, "\n") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0) + .join("\n") +} + +const UV = requireUv() +const out: Record = {} +const failed: string[] = [] + +mkdirSync(CACHE_DIR, { recursive: true }) + +for (const example of PLAYGROUND_EXAMPLES) { + const file = join(CACHE_DIR, `playground-${example.id}.lio`) + writeFileSync(file, example.source, "utf8") + const res = runTool(UV, [FRONTEND, file], REPO_ROOT) + const text = normalise(res.out) + if (res.code === 0 && text) { + out[hash(example.source)] = text + } else { + failed.push(`${example.id}: ${res.err.trim().split("\n").pop()}`) + } +} + +const target = join(WEB_ROOT, "apps", "web", "src", "lib") +writeFileSync( + join(target, "playground-ir.ts"), + `/** + * The pre-generated IR for the playground's built-in examples. + * + * The playground's live runner is a dev-only Vite middleware, so a visitor + * to the deployed site cannot run a program there. These IR blobs are the + * real frontend output for the eight built-in examples, generated by + * \`scripts/gen-playground-ir.ts\`, so the page still shows something true in + * production: the exact IR the engine would emit. + * + * Keyed by the FNV-1a hash of the example source, matching \`irKey()\` in + * \`docs/templates.tsx\`. + */ +export const PLAYGROUND_IR: Record = ${JSON.stringify(out, null, 2)} +`, + "utf8", +) + +console.log( + `wrote ${Object.keys(out).length} playground IR blobs to apps/web/src/lib/playground-ir.ts`, +) +for (const f of failed) console.log(` refused: ${f}`) diff --git a/web/scripts/gen-seo.ts b/web/scripts/gen-seo.ts index 932b011..85bf5c2 100644 --- a/web/scripts/gen-seo.ts +++ b/web/scripts/gen-seo.ts @@ -36,6 +36,8 @@ const STATIC_PATHS = [ "/benchmarks", "/roadmap", "/use-cases", + "/changelog", + "/next", "/roadmap/language", "/roadmap/phase-1-dual-tier", "/roadmap/phase-2-aot", @@ -53,15 +55,20 @@ const weightFor = (path: string) => { if (path === "/") return "1.0" if (path === "/docs") return "0.9" if (path.startsWith("/docs/")) return "0.8" + if (path === "/changelog" || path === "/next") return "0.7" if (path === "/playground") return "0.7" if (path === "/benchmarks") return "0.7" return "0.6" } +// The last-modified date of the tree, so a crawler gets a freshness signal. +const lastmod = new Date().toISOString().slice(0, 10) + const urls = all .map( (path) => ` ${origin}${path} + ${lastmod} weekly ${weightFor(path)} ` @@ -78,6 +85,9 @@ const robots = `User-agent: * Allow: / Disallow: /api/ +# The AI-facing surfaces: a compact index and the full documentation. +# Both are generated from the same registry the site renders. + Sitemap: ${origin}/sitemap.xml ` diff --git a/web/scripts/toolchain.ts b/web/scripts/toolchain.ts new file mode 100644 index 0000000..6ab09c8 --- /dev/null +++ b/web/scripts/toolchain.ts @@ -0,0 +1,78 @@ +/** + * Locates the Lithon toolchain (`uv`) in a way that works on every platform. + * + * Resolution order: + * 1. `UV_PATH` (explicit override, used by CI) + * 2. `uv` on PATH + * 3. the Windows winget install location + * 4. the usual macOS / Linux locations + */ +import { existsSync } from "node:fs" +import { join } from "node:path" + +const WINGET = join( + process.env.LOCALAPPDATA ?? "", + "Microsoft", + "WinGet", + "Packages", + "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", + "uv.exe", +) + +const UNIX_CANDIDATES = [ + "/usr/local/bin/uv", + "/usr/bin/uv", + "/opt/homebrew/bin/uv", + join(process.env.HOME ?? "", ".local", "bin", "uv"), + join(process.env.HOME ?? "", ".cargo", "bin", "uv"), +] + +/** The `uv` executable to drive, or `null` when it cannot be found. */ +export function findUv(): string | null { + const explicit = process.env.UV_PATH + if (explicit && existsSync(explicit)) return explicit + + if (process.platform === "win32") { + return existsSync(WINGET) ? WINGET : null + } + + for (const candidate of UNIX_CANDIDATES) { + if (candidate && existsSync(candidate)) return candidate + } + + // last resort: hope the shell can resolve it + return "uv" +} + +/** The `uv` executable, or a thrown error with an actionable message. */ +export function requireUv(): string { + const uv = findUv() + if (!uv) { + throw new Error( + "uv not found. Install it with `winget install --id=astral-sh.uv -e` " + + "(Windows) or `curl -LsSf https://astral.sh/uv/install.sh | sh` " + + "(macOS/Linux), or set UV_PATH to the executable.", + ) + } + return uv +} + +/** + * Runs a Lithon tool over a file and returns its exit code and streams. + */ +export function runTool( + uv: string, + args: string[], + cwd: string, +): { code: number; out: string; err: string } { + const proc = Bun.spawnSync([uv, "run", "--quiet", "--python", "3.12", ...args], { + cwd, + stdout: "pipe", + stderr: "pipe", + }) + return { + code: proc.exitCode ?? 1, + out: new TextDecoder().decode(proc.stdout), + err: new TextDecoder().decode(proc.stderr), + } +} diff --git a/web/scripts/verify-docs.ts b/web/scripts/verify-docs.ts index 78356b3..354b1a7 100644 --- a/web/scripts/verify-docs.ts +++ b/web/scripts/verify-docs.ts @@ -14,6 +14,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node import { dirname, join, resolve } from "node:path" import { DOCS } from "../apps/web/src/docs/registry" +import { requireUv, runTool } from "./toolchain" const APP_DIR = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")) const WEB_ROOT = resolve(APP_DIR, "..") @@ -22,16 +23,7 @@ const REPO_ROOT = resolve(WEB_ROOT, "..") const TYPECHECK = join(REPO_ROOT, "tools", "typecheck.py") const FRONTEND = join(REPO_ROOT, "src", "frontend", "frontend.py") -const UV = - process.env.UV_PATH ?? - join( - process.env.LOCALAPPDATA ?? "", - "Microsoft", - "WinGet", - "Packages", - "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", - "uv.exe", - ) +const UV = requireUv() type Failure = { where: string; detail: string } @@ -45,15 +37,6 @@ function fail(where: string, detail: string) { failures.push({ where, detail }) } -function run(cmd: string[]): { code: number; out: string; err: string } { - const proc = Bun.spawnSync(cmd, { stdout: "pipe", stderr: "pipe" }) - return { - code: proc.exitCode, - out: new TextDecoder().decode(proc.stdout), - err: new TextDecoder().decode(proc.stderr), - } -} - function normalise(text: string): string { return text .replace(/\r\n/g, "\n") @@ -140,9 +123,9 @@ function checkExample( const useTypecheck = example.typecheck !== false const tc = useTypecheck - ? run([UV, "run", "--quiet", "--python", "3.12", TYPECHECK, tmp]) + ? runTool(UV, [TYPECHECK, tmp], REPO_ROOT) : null - const fe = run([UV, "run", "--quiet", "--python", "3.12", FRONTEND, tmp]) + const fe = runTool(UV, [FRONTEND, tmp], REPO_ROOT) const tcText = tc ? `${tc.out}${tc.err}` : "" const feText = `${fe.out}${fe.err}` @@ -195,6 +178,25 @@ function checkExample( } } +/** Every code block in a page, including the ones nested inside a tabset. */ +function codeBlocks( + blocks: readonly unknown[], + out: { example: Parameters[1] }[] = [], +) { + for (const block of blocks as { kind: string; example?: unknown; items?: unknown }[]) { + if (block.kind === "code" && block.example) { + out.push({ + example: block.example as Parameters[1], + }) + } else if (block.kind === "tabs" && Array.isArray(block.items)) { + for (const item of block.items as { blocks: unknown[] }[]) { + codeBlocks(item.blocks, out) + } + } + } + return out +} + function checkRegistry() { const slugs = new Set() const ids = new Set() @@ -222,12 +224,6 @@ function checkRegistry() { } function main() { - if (!existsSync(UV)) { - console.error( - `uv not found at ${UV}\nInstall it with: winget install --id=astral-sh.uv -e`, - ) - process.exit(2) - } if (!existsSync(TYPECHECK) || !existsSync(FRONTEND)) { console.error("Lithon repo toolchain not found — run this from web/") process.exit(2) @@ -238,11 +234,9 @@ function main() { checkRegistry() for (const page of DOCS.pages) { for (const section of page.sections) { - section.blocks.forEach((block, i) => { - if (block.kind === "code") { - checkExample(`${page.slug}#${section.id}[${i}]`, block.example) - } - }) + for (const { example } of codeBlocks(section.blocks)) { + checkExample(`${page.slug}#${section.id}`, example) + } } } } finally { @@ -267,4 +261,4 @@ function main() { console.log("ALL DOCS EXAMPLES VERIFIED against the real Lithon toolchain") } -main() \ No newline at end of file +main()