diff --git a/bench_vid_maker/README.md b/bench_vid_maker/README.md index 093f082..3dd5337 100644 --- a/bench_vid_maker/README.md +++ b/bench_vid_maker/README.md @@ -1,156 +1,310 @@ # bench_vid_maker -A tiny, reusable uv project that turns a benchmark comparison (CPython vs -Lithon, or anything else) into an actual video file with a clean, honestly- -timed live UI. +A tiny, reusable Python project (deps: `rich`, `pillow`, `psutil`; Python >= 3.11) +that turns a benchmark comparison (CPython vs Lithon, or anything else) into an +actual video file with a clean, honestly-timed live UI. Three acts, always in this order: -1. **Typing** -- every task's source types itself onto screen, side by - side, at a fixed pace. Nothing is running yet; this is a pure intro and - is never counted toward any time shown. -2. **Starting** -- one single, large modal flashes centered over the - (now fully typed) panels, and closes the instant the processes launch. -3. **Running** -- every task's process is launched at the *exact same - instant*, under a big clock that starts clean at `00:00.00` right then. - This is the only part that's actually timed. Each panel shows a live - spinner, elapsed wall time, and CPU usage (instantaneous %, cumulative - user/system seconds, sampled straight from the OS via `psutil`) until - the process exits, then a done/failed badge and a tinted background. - -Every panel is sized to fill the whole frame -- width and height -- so -nothing looks cramped in a sea of black. If the running phase takes longer -than ~10s of real time, the *recorded video* of that phase (never the live -terminal, never the numbers shown) is time-lapsed down to roughly that -length, and the applied speed-up (e.g. "173x") is reported on screen. - -A closing, full-height summary breaks down wall-clock vs. CPU user/system -time per task (table), how that time was actually spent (a Chrome- -DevTools-Network-panel-style resource waterfall), and the environment the -run actually used (OS, CPU, cores, RAM, Python version, every resolved -command) -- and is held on screen for a while. - -Every number is real: `time.perf_counter()` around each subprocess (spawn -to exit) for wall time, `psutil` process accounting for CPU time, six -decimal places shown throughout so nothing is rounded away. The typing -intro never affects the clock -- it starts at zero exactly when the -processes launch. CPU% is computed as cumulative CPU time over elapsed -wall time (not psutil's own interval `cpu_percent()`, which is noisy/ -quantized on Windows over short polling windows). - -## Run the included comparison +1. **Typing** -- every task's source types itself onto screen, side by side, at + 40 chars/second. Pure intro; never counted toward any time shown. +2. **Starting** -- one large modal flashes centered over the (now fully typed) + panels for ~1s and closes the instant the processes launch. +3. **Running** -- every task's process is launched at the *exact same instant* + under a clock that starts clean at `00:00.000000`. This is the only timed + part: each panel shows a live spinner, elapsed wall time, and CPU usage + (cumulative user/system seconds via `psutil`) until exit, then a + done/failed badge and a tinted background. + +Every number is real: `time.perf_counter()` around each subprocess (spawn to +exit) for wall time, `psutil` process accounting for CPU time, six decimals +throughout; CPU% is cumulative CPU over elapsed wall time, not psutil's noisy, +Windows-quantized interval `cpu_percent()`. Tasks are polled every 50ms +(`TICK_SECONDS`) -- polling only drives UI smoothness, never the numbers. + +If the running phase exceeds ~10s of real time, the *recorded video* of that +phase (never the live terminal, never the numbers shown) is time-lapsed down +to roughly that length, and the applied speed-up (e.g. "~175x", rounded to a +clean step) is reported on screen. A closing full-height summary shows a +"Time allocation" table (wall, CPU user/sys, share, bar, vs-slowest), an +environment block (OS, CPU + cores, RAM, Python, resolved commands, video +settings), and a Chrome-DevTools-style resource waterfall splitting each bar +into CPU user / sys / idle-wait on a shared time axis. + +## Run the included comparison (WSL) + +`bench_vid_maker` measures **inside WSL Ubuntu** -- both engines execute as +native Linux processes on the same OS, and static ffmpeg is easy to get there. +The repo lives at `/mnt/c/Users/tenuk/Projects/Lithon`; after the one-time +setup in the appendix below, the tool lives in `~/.venvs/lithon-bench`: + +``` +~/.venvs/lithon-bench/bin/bench-vid-maker bench_vid_maker/configs/fsum_1k.toml # live UI +~/.venvs/lithon-bench/bin/bench-vid-maker bench_vid_maker/configs/fsum_1b.toml --video results/fsum_1b.mp4 +~/.venvs/lithon-bench/bin/bench-vid-maker-all # every config -> results/ +``` + +### Measuring from Windows + +You do not have to switch shells. On Windows the tool **re-executes itself +inside WSL** and lets WSL do the measuring, so both engines still run as +native Linux processes: ``` -cd bench_vid_maker -uv run bench-vid-maker configs/fsum_1b.toml # live in the terminal -uv run bench-vid-maker configs/fsum_1b.toml --video out.mp4 # live AND recorded -uv run bench-vid-maker configs/fsum_1b.toml --video-only --video out.mp4 # recorded, no terminal UI -uv run bench-vid-maker configs/fsum_1b_4core.toml --video out.mp4 # same, pinned to 4 cores +bench-vid-maker bench_vid_maker/configs/fsum_1b.toml --video results/fsum_1b.mp4 ``` -This types in `tasks/python_sum.py` (plain CPython) and -`test_codes_lithon/test_1b_fsum.py` (through the repo's built `lithon` -executable), then launches both **together**, on the same -1,000,000,000-iteration sum. There's also `configs/fsum_1k.toml`, a -1,000-iteration version that finishes in well under a second -- useful for -quickly checking the UI/video itself rather than waiting out a real -benchmark, plus `configs/fsum_1b_{1,4,6,8}core.toml`, the same comparison -with every task's CPU affinity pinned to 1/4/6/8 cores (`cores = N` in the -config, or `--cores N` on the command line to override it for any config). -`configs/euler_lim.toml` compares a heavier, nested-loop workload instead -(approximating `e` via its limit definition out to i=1,000). -Results are also saved as JSON under `results/` (use `--no-save` to skip -that). - -First run: `uv sync` to create the project's own `.venv` (deps: `rich`, -`pillow`, `psutil`). Requires a `cmake --build build` in the repo root -first so `lithon` exists, and `ffmpeg` on `PATH` for `--video`. - -### How the video is made +The re-exec happens automatically when the repo has no Windows-native +`tier_runner.exe` (which is the usual case: the engine is built on Linux). +Force or suppress it explicitly: + +| flag | effect | +|---|---| +| `--wsl` | always re-execute inside WSL | +| `--no-wsl` | never re-execute inside WSL | +| `BENCH_VID_MAKER_NO_WSL=1` | environment equivalent of `--no-wsl` | + +This matters for the numbers, not just for convenience: + +- Lithon's fast tier is an x86-64 **native** tier (`build/tier_runner`); a + Windows-to-WSL translation layer would measure the layer, not the engine. +- CPython's loop performance under WSL differs from its performance on a + real Linux host, so the comparison is only honest when *both* engines run + on the same Linux kernel. +- `--cores N` (CPU affinity) is only honoured on the Linux side. + +### The engine is resolved for the platform it will run on + +`{lithon}` is looked for in this order: + +1. the repo's own virtualenv, using the subdirectory for the *current* + platform (`.venv/bin/lithon` on Linux, `.venv/Scripts/lithon.exe` on + Windows), +2. a sibling of the running interpreter -- this is what makes + `~/.venvs/lithon-bench/bin/lithon` in WSL resolve correctly, +3. `lithon` on `PATH`, +4. the repo's `cli.py`, run through the current interpreter, so a repo that + was cloned but never `pip install`ed still works. + +A binary that exists but cannot run on this platform is skipped rather than +used: a Windows `lithon.exe` found from inside WSL is ignored, instead of +failing later with a confusing "native runner not found at C:\\...". + +### Included configs + +All in `bench_vid_maker/configs/`. Each one runs a `test_codes_python/**.py` +program against its `test_codes_lithon/**.lio` twin, launched together on +the same program. + +| config | what it measures | +|---|---| +| `fsum_1k.toml` | 1,000-iteration sum; finishes in well under a second, so it is for checking the UI/video itself, not for speedup numbers | +| `fsum_1b.toml` | the real 1,000,000,000-iteration sum comparison | +| `fsum_1b_{1,4,6,8}core.toml` | the same with every task pinned to 1/4/6/8 CPU cores (`cores = N`; `--cores N` overrides any config) | +| `fsum_1b_simd.toml` | 1e9 half-increments into a `float[64]` -- the case the auto-vectorizer targets | +| `multi_ops_1b.toml` | 1e9 iterations of mixed integer arithmetic (multiply, xor, shift) | +| `euler_lim.toml` | a nested-loop workload (approximating `e` via its limit definition out to i=1,000) | +| `find_e.toml` | 8,000 terms of `1/n!`, computed with an explicit factorial loop | +| `fib_30.toml` | recursive `fib(30)` -- call overhead rather than loop throughput | + +Windows-native works as a secondary option: it needs the repo's `.venv` to +contain the built engine (`.venv/Scripts/lithon.exe`), `ffmpeg` on `PATH`, and +the fonts below -- then `pip install -e bench_vid_maker` (or `uv`) and the +same commands. + +## How the video is made Each frame (the same `rich` renderable shown live in the terminal) is -rasterized to an RGB image with Pillow -- honoring every color, bold, and -panel border rich draws -- and streamed straight into `ffmpeg`'s stdin as -raw video; nothing but the final `.mp4` touches disk. During the typing/ -starting/summary acts, a frame is held (repeated) for exactly as many -ticks as real wall-clock time actually passed since the previous frame, so -video playback time matches the real run there regardless of how long -rasterizing/encoding itself took. - -The *running* act is different: its frames are buffered instead of -streamed live, so that if the slowest task takes a long time in real life -(minutes), the recorded clip of that segment can be **time-lapsed** down -to roughly `RUNNING_VIDEO_TARGET_SECONDS` (10s by default, see `ui.py`) -once both tasks are done and the real duration is known. Every number any -frame shows is still the real measurement -- only *playback speed* -changes -- and the applied multiplier (rounded to a clean step, e.g. -"~175x") is reported on the finished frame and in the summary. - -Text is rasterized with the **JetBrains Mono Nerd Font** (falls back to -Consolas, then DejaVu Sans Mono on Linux) so the UI can use small icon -glyphs (a terminal glyph on each panel, a microchip for CPU stats, a -trophy for the winner, etc.) alongside plain ASCII/box-drawing characters. -Only glyphs confirmed present in that font's cmap are used anywhere in the -UI -- `rich` itself will happily use glyphs the font doesn't have, which -silently render as tofu boxes in the video, so this was checked with -`fontTools` against the font's actual cmap rather than assumed. - -A short, procedurally-generated background track (a pulsing bass note -under a fast four-note arpeggio, synthesized on the fly by `ffmpeg`'s own -`lavfi` sources -- not a downloaded/licensed file, so there's no copyright -to clear) is muxed into the video by default; pass `--no-music` to skip -it. - -Useful flags: +rasterized to an RGB image with Pillow -- honoring every truecolor fg/bg, +bold, and dim -- on a cell grid derived from the font's metrics, and streamed +into `ffmpeg`'s stdin as raw `rgb24`; nothing but the final `.mp4` touches +disk. Dimensions are bumped to even numbers (libx264/yuv420p require it); +encoding is libx264/yuv420p with `+faststart`. + +During the typing/starting/summary acts, each frame is held for exactly as +many ticks as real wall-clock time passed, so playback matches the real run +regardless of rasterizing/encoding speed. The *running* act is buffered +instead, then time-lapsed (once the real duration is known) to roughly +`RUNNING_VIDEO_TARGET_SECONDS` (10s, see `ui.py`); the final running frame is +held ~2s, the summary ~8s. Every number any frame shows remains the real +measurement -- only *playback speed* changes. + +Text is rasterized with **JetBrains Mono Nerd Font**, looked for in this +order: `C:/Windows/Fonts/JetBrainsMonoNerdFontMono-*.ttf` (Windows), +`~/.local/share/fonts/JetBrainsMonoNerdFontMono-*.ttf` (WSL/Linux), Consolas +(`consola.ttf`), then DejaVu Sans Mono. Only icon glyphs confirmed present in +the Nerd Font's cmap are used in the UI. Stdio is reconfigured to UTF-8 first +so legacy Windows code pages can't crash a run mid-render. + +Background music is **on by default**: two `lavfi` inputs synthesized by +ffmpeg itself at mux time -- a four-note A3 minor-7th arpeggio (A, C, E, G) +at 128 BPM in 16th notes under a 55 Hz (A1) pulsing bass -- mixed, faded in, +limited, muxed as AAC 160k; `-shortest` stops it when the video (stdin) ends. +No copyright to clear; `--no-music` skips it. + +### Flags | flag | default | meaning | |---|---|---| -| `--video PATH` | β€” | also/only render an mp4 | -| `--video-only` | off | skip the live terminal UI, just record | +| `config` (positional) | β€” | TOML file describing the tasks to compare | +| `--save PATH` | `results/_.json` | write the results JSON to this path | +| `--no-save` | off | don't write any results JSON (saved by default) | +| `--video PATH` | β€” | also render an mp4 of the run to this path | +| `--video-only` | off | render the video without opening a live terminal UI | +| `--video-rows N` | 56 | video height in character cells (~2K-class at the default font size) | | `--fps N` | 20 | video frame rate | -| `--video-cols N` | 234 | video width, in character cells (~4K-class at the default font size) | -| `--video-rows N` | 56 | video height, in character cells | -| `--cores N` | β€” | pin every task's process to N CPU cores (overrides the config's `cores`) | +| `--video-cols N` | 234 | video width in character cells (~2K-class) | +| `--cores N` | the config's `cores`, else unrestricted | pin every task's process to N CPU cores | | `--no-music` | off | don't mux in the procedurally-generated background music | +| `--wsl` | auto | re-execute the whole run inside WSL (both engines measured natively on Linux) | +| `--no-wsl` | off | never re-execute inside WSL, even when WSL is available | + +With `--video-only` and no `--video` path, the mp4 goes to +`results/_.mp4`. Exit codes: `0` all tasks exited 0; +`1` at least one task failed; `2` bad/missing config, or video requested but +ffmpeg/fonts are missing. + +## Config reference + +Full TOML shape (`config.py`): + +```toml +title = "1,000,000,000-iteration sum: CPython vs Lithon" # defaults to the file's stem if omitted +subtitle = "optional, shown under the title" # optional +cores = 4 # optional: pin every task to N CPU cores + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] +cwd = "." # optional, relative to repo root +``` + +Every task needs `name` and `command`; at least one `[[tasks]]` entry is +required or the run exits 2. All relative paths (command args and `cwd`) +resolve against the repo root -- found by walking up to the nearest `.git` -- +so configs stay short and portable regardless of where you invoke the tool. + +Placeholders expanded in every command token: + +- `{python}` -- the interpreter running bench-vid-maker (`sys.executable`). +- `{lithon}` -- the engine, looked for in order: (1) `/.venv/Scripts/lithon.exe`, + (2) `/.venv/bin/lithon`, (3) a sibling of the running interpreter + (`lithon.exe` on Windows, `lithon` elsewhere -- this is what makes + `~/.venvs/lithon-bench/bin/lithon` in WSL resolve correctly), (4) `lithon` + on `PATH`. Missing everywhere is an error (exit 2). Add more placeholders in + `src/bench_vid_maker/config.py::_expand` as new engines get compared. ## Add a new comparison -1. Drop the program(s) you want to compare anywhere in the repo (or under - `bench_vid_maker/tasks/`). -2. Write a config, e.g. `configs/my_comparison.toml`: - - ```toml - title = "My comparison" - subtitle = "optional subtitle" - - [[tasks]] - name = "CPython" - command = ["{python}", "bench_vid_maker/tasks/my_task.py"] - - [[tasks]] - name = "Lithon" - command = ["{lithon}", "test_codes_lithon/my_task.py"] - # cwd = "." # optional, relative to the repo root - ``` - - You aren't limited to two tasks -- add as many `[[tasks]]` entries as - you like; they all type in together and then launch together, panels - laid out side by side. - -3. `uv run bench-vid-maker configs/my_comparison.toml --video out.mp4` - -All command/`cwd` paths are resolved relative to the repo root (found by -walking up to the nearest `.git`), so configs stay short regardless of -where you invoke the tool from. `{python}` expands to the interpreter -running bench-vid-maker; `{lithon}` expands to the repo's built -`.venv/Scripts/lithon.exe` / `.venv/bin/lithon` (falling back to `PATH`). -Add more placeholders in `src/bench_vid_maker/config.py::_expand` as you -compare more engines. - -The typewriter animation reads whichever file is the task's last command -argument; if that isn't a real file (e.g. `python -c "..."`), the panel -just shows the raw command line instead and skips straight to the -running/elapsed indicator. - -For statistically rigorous (multi-run, median/min/max) numbers rather than -a single on-camera run, use `tools/bench.py` in the repo root instead. +Paired test programs live in matching category folders -- the Python tree +mirrors the Lithon tree exactly, so a program and its twin are always one +folder apart: + +``` +test_codes_python/{Classes,DataTypes,Functions,Loops,NewTypes,Pointers,Structs}/*.py +test_codes_lithon/{Classes,DataTypes,Functions,Loops,NewTypes,Pointers,Structs}/*.lio +``` + +Lithon is python-syntax, so the same program shape runs as-is in both engines; +the Python side only mirrors the portable subset. Where a construct has no +CPython equivalent -- raw pointers, packed structs, width annotations -- the +Python file keeps the *shape* of the program and says so in its docstring. + +Some programs are deliberately **pinned failures** on the Lithon side: they +exercise a gap that has not landed yet, and the engine is expected to refuse +them (see `test_codes_lithon/DataTypes/check_strings.lio`, +`test_codes_lithon/Pointers/pointer_error.lio` and +`test_codes_lithon/Classes/class_basics.lio`). Do not "fix" those files -- +they are the record of what is missing. + +Drop your program(s) into a matching category, then write a config, e.g. +`configs/my_comparison.toml`: + +```toml +title = "Nested loops: CPython vs Lithon" +subtitle = "optional subtitle" + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Loops/test_1b_loop.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_loop.lio"] +# cwd = "." # optional, relative to the repo root +``` + +Any number of `[[tasks]]` entries works -- they all type in together, then +launch together, side by side. Then: + +``` +~/.venvs/lithon-bench/bin/bench-vid-maker bench_vid_maker/configs/my_comparison.toml --video results/my_comparison.mp4 +``` + +The typewriter animation reads the task's last command argument as source; if +that isn't a real file (e.g. `python -c "..."`), the panel shows the raw +command line instead. For statistically rigorous (multi-run, median/min/max) +numbers rather than a single on-camera run, use `tools/bench.py` in the repo +root instead. + +## bench-vid-maker-all + +`bench-vid-maker-all [CONFIGS_DIR] [--out-dir DIR] [-- EXTRA_ARGS]` builds a +video + results JSON for every `configs/*.toml` (default dir `configs/`, +default out dir `results/`), writing `.mp4` and `.json` per +config. It runs each config sequentially as `--video-only` on purpose -- +concurrent CPU-bound runs would make every number meaningless. Anything after +`--` is forwarded to every invocation, e.g. `bench-vid-maker-all -- --no-music +--fps 30`. Exit `0` when all built, `1` if any failed, `2` if no configs found. + +## results/ outputs + +`bench_vid_maker/results/` holds one `.mp4` and one `.json` per comparison. +The mp4s are committed; `results/*.json` are gitignored (see +`bench_vid_maker/.gitignore`). + +## Appendix: one-time WSL setup + +All user-space, no sudo. Venv (bootstrapped `--without-pip` via get-pip.py) +plus this project installed editable: + +```sh +python3 -m venv --without-pip ~/.venvs/lithon-bench +curl -sS https://bootstrap.pypa.io/get-pip.py | ~/.venvs/lithon-bench/bin/python3 +~/.venvs/lithon-bench/bin/pip install rich pillow psutil +~/.venvs/lithon-bench/bin/pip install -e /mnt/c/Users/tenuk/Projects/Lithon/bench_vid_maker +``` + +A tiny `lithon` wrapper next to the venv's python is all it takes for +`{lithon}` to resolve (it execs the repo's engine CLI): + +```sh +cat > ~/.venvs/lithon-bench/bin/lithon <<'EOF' +#!/bin/sh +exec python3 /mnt/c/Users/tenuk/Projects/Lithon/cli.py "$@" +EOF +chmod +x ~/.venvs/lithon-bench/bin/lithon +``` + +Static ffmpeg: grab a `linux-amd64` build (johnvansickle.com or +BtbN/FFmpeg-Builds), extract `ffmpeg` into `~/.local/bin`, and put +`~/.local/bin` on `PATH` in `~/.bashrc`: + +```sh +echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc +``` + +JetBrains Mono Nerd Font into `~/.local/share/fonts` (where the recorder looks +on Linux/WSL), then rebuild the font cache: + +```sh +mkdir -p ~/.local/share/fonts +curl -sSLO https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip +unzip -o JetBrainsMono.zip 'JetBrainsMonoNerdFontMono*' -d ~/.local/share/fonts +rm JetBrainsMono.zip +fc-cache -f ~/.local/share/fonts +``` diff --git a/bench_vid_maker/configs/euler_lim.toml b/bench_vid_maker/configs/euler_lim.toml index 4949522..5407b89 100644 --- a/bench_vid_maker/configs/euler_lim.toml +++ b/bench_vid_maker/configs/euler_lim.toml @@ -3,8 +3,8 @@ subtitle = "nested loop to i=1,000 -- same program, run as-is in each engine" [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_eulars_constant_lim_def.py"] +command = ["{python}", "test_codes_python/Loops/test_eulars_constant_lim_def.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_eulars_constant_lim_def.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_eulars_constant_lim_def.lio"] diff --git a/bench_vid_maker/configs/fib_30.toml b/bench_vid_maker/configs/fib_30.toml new file mode 100644 index 0000000..913458d --- /dev/null +++ b/bench_vid_maker/configs/fib_30.toml @@ -0,0 +1,10 @@ +title = "Fibonacci(30): CPython vs Lithon" +subtitle = "same recursive program, run as-is in each engine" + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Functions/fibb.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Functions/fibb.lio"] diff --git a/bench_vid_maker/configs/find_e.toml b/bench_vid_maker/configs/find_e.toml new file mode 100644 index 0000000..647b310 --- /dev/null +++ b/bench_vid_maker/configs/find_e.toml @@ -0,0 +1,10 @@ +title = "Finding e by its series: CPython vs Lithon" +subtitle = "8,000 terms of 1/n!, computed with an explicit factorial loop" + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Loops/find_e.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Loops/find_e.lio"] diff --git a/bench_vid_maker/configs/fsum_1b.toml b/bench_vid_maker/configs/fsum_1b.toml index ac7ee81..8b4007b 100644 --- a/bench_vid_maker/configs/fsum_1b.toml +++ b/bench_vid_maker/configs/fsum_1b.toml @@ -3,8 +3,8 @@ subtitle = "same program, run as-is in each engine" [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1b_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] diff --git a/bench_vid_maker/configs/fsum_1b_1core.toml b/bench_vid_maker/configs/fsum_1b_1core.toml index 14945f9..39cebc0 100644 --- a/bench_vid_maker/configs/fsum_1b_1core.toml +++ b/bench_vid_maker/configs/fsum_1b_1core.toml @@ -4,8 +4,8 @@ cores = 1 [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1b_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] diff --git a/bench_vid_maker/configs/fsum_1b_4core.toml b/bench_vid_maker/configs/fsum_1b_4core.toml index e384299..c8797d3 100644 --- a/bench_vid_maker/configs/fsum_1b_4core.toml +++ b/bench_vid_maker/configs/fsum_1b_4core.toml @@ -4,8 +4,8 @@ cores = 4 [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1b_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] diff --git a/bench_vid_maker/configs/fsum_1b_6core.toml b/bench_vid_maker/configs/fsum_1b_6core.toml index f37d74b..20f375f 100644 --- a/bench_vid_maker/configs/fsum_1b_6core.toml +++ b/bench_vid_maker/configs/fsum_1b_6core.toml @@ -4,8 +4,8 @@ cores = 6 [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1b_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] diff --git a/bench_vid_maker/configs/fsum_1b_8core.toml b/bench_vid_maker/configs/fsum_1b_8core.toml index 0686297..053e4c2 100644 --- a/bench_vid_maker/configs/fsum_1b_8core.toml +++ b/bench_vid_maker/configs/fsum_1b_8core.toml @@ -4,8 +4,8 @@ cores = 8 [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1b_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] diff --git a/bench_vid_maker/configs/fsum_1b_simd.toml b/bench_vid_maker/configs/fsum_1b_simd.toml new file mode 100644 index 0000000..4cb32c3 --- /dev/null +++ b/bench_vid_maker/configs/fsum_1b_simd.toml @@ -0,0 +1,10 @@ +title = "Summing 1,000,000,000 half-increments: CPython vs Lithon" +subtitle = "float[64] accumulator -- the case Lithon's auto-vectorizer (AVX2) targets" + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Loops/test_1b_fsum_simd.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum_simd.lio"] diff --git a/bench_vid_maker/configs/fsum_1k.toml b/bench_vid_maker/configs/fsum_1k.toml index 858c26c..9e3ce5f 100644 --- a/bench_vid_maker/configs/fsum_1k.toml +++ b/bench_vid_maker/configs/fsum_1k.toml @@ -3,8 +3,8 @@ subtitle = "tiny workload -- for quickly checking the bench_vid_maker UI/video, [[tasks]] name = "CPython" -command = ["{python}", "test_codes_python/test_1k_fsum.py"] +command = ["{python}", "test_codes_python/Loops/test_1k_fsum.py"] [[tasks]] name = "Lithon" -command = ["{lithon}", "test_codes_lithon/test_1k_fsum.py"] +command = ["{lithon}", "test_codes_lithon/Loops/test_1k_fsum.lio"] diff --git a/bench_vid_maker/configs/multi_ops_1b.toml b/bench_vid_maker/configs/multi_ops_1b.toml new file mode 100644 index 0000000..c766851 --- /dev/null +++ b/bench_vid_maker/configs/multi_ops_1b.toml @@ -0,0 +1,10 @@ +title = "Mixed integer arithmetic, 1,000,000,000 iterations: CPython vs Lithon" +subtitle = "multiply, bitwise xor and right shift, accumulated -- pure integer ALU work" + +[[tasks]] +name = "CPython" +command = ["{python}", "test_codes_python/Loops/test_multi_ops.py"] + +[[tasks]] +name = "Lithon" +command = ["{lithon}", "test_codes_lithon/Loops/test_multi_ops.lio"] diff --git a/bench_vid_maker/src/bench_vid_maker/cli.py b/bench_vid_maker/src/bench_vid_maker/cli.py index 4966358..ad05b06 100644 --- a/bench_vid_maker/src/bench_vid_maker/cli.py +++ b/bench_vid_maker/src/bench_vid_maker/cli.py @@ -14,6 +14,7 @@ from .config import PROJECT_ROOT, load_config from .recorder import VideoRecorder from .ui import run_with_ui +from .wsl import have_wsl, reexec_in_wsl # Sized so the default rendered frame lands comfortably in 2K+ territory # with the (larger, more legible) default font_size -- see VideoRecorder's @@ -23,6 +24,26 @@ DEFAULT_FONT_SIZE = 34 +def _wsl_preferred() -> bool: + """Whether to prefer a WSL run over a Windows-native one by default. + + The Lithon engine's fast tier is an x86-64 native binary; on Windows + it needs `build/tier_runner.exe`, and the repository is frequently + built on Linux/WSL only. Rather than failing with a confusing + "native runner not found" error, the tool measures on WSL when it can. + Set `BENCH_VID_MAKER_NO_WSL=1` to opt out. + """ + if os.environ.get("BENCH_VID_MAKER_NO_WSL"): + return False + from .wsl import wsl_executable + + if wsl_executable() is None: + return False + # A Windows-native engine build is a perfectly good place to measure -- + # only prefer WSL when the repo has no Windows runner at all. + return not (PROJECT_ROOT / "build" / "tier_runner.exe").exists() + + def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser( prog="bench-vid-maker", @@ -57,8 +78,24 @@ def main(argv: list[str] | None = None) -> int: "--no-music", action="store_true", help="don't mux the procedurally-generated (non-copyright) background music into the video", ) + ap.add_argument( + "--wsl", action="store_true", + help="re-execute the whole run inside WSL (both engines measured natively on Linux)", + ) + ap.add_argument( + "--no-wsl", action="store_true", + help="never re-execute inside WSL, even when WSL is available", + ) args = ap.parse_args(argv) + # WSL re-exec: the honest place to measure is a Linux kernel with both + # engines running natively. The child re-enters this same entry point, + # so the guard below keeps it from re-execing twice. + if args.wsl and not args.no_wsl and have_wsl(): + return reexec_in_wsl() + if not args.no_wsl and not args.wsl and have_wsl() and _wsl_preferred(): + return reexec_in_wsl() + try: cfg = load_config(args.config) except (FileNotFoundError, ValueError) as e: diff --git a/bench_vid_maker/src/bench_vid_maker/config.py b/bench_vid_maker/src/bench_vid_maker/config.py index e63421f..e624ca1 100644 --- a/bench_vid_maker/src/bench_vid_maker/config.py +++ b/bench_vid_maker/src/bench_vid_maker/config.py @@ -8,11 +8,11 @@ [[tasks]] name = "CPython" - command = ["{python}", "bench_vid_maker/tasks/python_sum.py"] + command = ["{python}", "test_codes_python/Loops/test_1b_fsum.py"] [[tasks]] name = "Lithon" - command = ["{lithon}", "test_codes_lithon/test_1b_fsum.py"] + command = ["{lithon}", "test_codes_lithon/Loops/test_1b_fsum.lio"] cwd = "." # optional, relative to repo root All relative paths (command args and ``cwd``) are resolved against the repo @@ -24,6 +24,7 @@ from __future__ import annotations import dataclasses +import os import pathlib import shutil import sys @@ -44,18 +45,66 @@ def find_repo_root(start: pathlib.Path) -> pathlib.Path: REPO_ROOT = find_repo_root(PROJECT_ROOT) +def _is_runnable(path: pathlib.Path) -> bool: + """True when `path` exists *and* can actually be executed on this OS. + + A Windows `.venv/Scripts/lithon.exe` sitting in the repo is a real trap + when the tool runs inside WSL: the file exists, so a plain `exists()` + check picks it, and it then fails with a baffling "native runner not + found at C:\\...\\tier_runner.exe" -- a Windows path, resolved from a + Linux process. The runner is only usable if this platform can run it. + """ + if not path.exists(): + return False + if path.suffix.lower() == ".exe" and not sys.platform.startswith("win"): + return False # a Windows binary is useless on Linux/macOS + if not path.suffix and sys.platform.startswith("win"): + return False # a POSIX binary is useless on Windows + return os.access(path, os.X_OK) or path.suffix.lower() == ".exe" + + def _find_lithon() -> str: - for rel in (".venv/Scripts/lithon.exe", ".venv/bin/lithon"): + """Locate a `lithon` executable that can run *on this platform*. + + Order: + 1. the repo's own virtualenv (platform-correct subdirectory), + 2. a sibling of the running interpreter (this is what makes + `~/.venvs/lithon-bench/bin/lithon` in WSL resolve correctly), + 3. `lithon` on PATH, + 4. the repo's `cli.py`, run through the current interpreter -- the + engine's own source entry point, so a repo that has been cloned + but not `pip install`ed still works. + """ + venv_names = ( + (".venv/Scripts/lithon.exe", ".venv/bin/lithon") + if sys.platform.startswith("win") + else (".venv/bin/lithon", ".venv/Scripts/lithon.exe") + ) + for rel in venv_names: p = REPO_ROOT / rel - if p.exists(): + if _is_runnable(p): return str(p) + + # A venv running bench-vid-maker itself (e.g. ~/.venvs/lithon-bench in + # WSL) that has the repo installed: lithon sits next to our interpreter. + for name in ("lithon.exe", "lithon"): + sibling = pathlib.Path(sys.executable).parent / name + if _is_runnable(sibling): + return str(sibling) + found = shutil.which("lithon") - if found: + if found and _is_runnable(pathlib.Path(found)): return found + + # Last resort: run the engine straight from its source entry point. + cli = REPO_ROOT / "cli.py" + if cli.exists(): + return str(cli) + raise FileNotFoundError( - "could not find a built `lithon` executable (looked in " - f"{REPO_ROOT / '.venv/Scripts'} and {REPO_ROOT / '.venv/bin'}, and on PATH). " - "Build/install Lithon first." + "could not find a runnable `lithon` executable (looked in " + f"{REPO_ROOT / '.venv/bin'} and {REPO_ROOT / '.venv/Scripts'}, next to " + f"{sys.executable}, on PATH, and {cli}). Build/install Lithon first." ) @@ -83,6 +132,28 @@ def _expand(token: str) -> str: return token +def _expand_command(command: list[str]) -> list[str]: + """Expand the placeholders in a whole command. + + `{lithon}` may resolve to the engine's Python entry point (`cli.py`) + when no built executable is available -- in that case the command has + to be run *through* the interpreter, so the interpreter is prepended + and the marker is kept out of the final argv. + """ + expanded: list[str] = [] + for tok in command: + if tok == "{lithon}": + lithon = _find_lithon() + if lithon.endswith(".py"): + expanded.append(sys.executable) + expanded.append(lithon) + else: + expanded.append(lithon) + else: + expanded.append(_expand(tok)) + return expanded + + def load_config(path: pathlib.Path) -> BenchConfig: path = pathlib.Path(path).resolve() data = tomllib.loads(path.read_text(encoding="utf-8")) @@ -95,7 +166,7 @@ def load_config(path: pathlib.Path) -> BenchConfig: for i, t in enumerate(raw_tasks): if "name" not in t or "command" not in t: raise ValueError(f"{path}: tasks[{i}] is missing 'name' or 'command'") - command = [_expand(tok) for tok in t["command"]] + command = _expand_command(t["command"]) cwd = REPO_ROOT / t.get("cwd", ".") tasks.append(Task(name=t["name"], command=command, cwd=cwd.resolve())) diff --git a/bench_vid_maker/src/bench_vid_maker/recorder.py b/bench_vid_maker/src/bench_vid_maker/recorder.py index 3ad33a0..b303699 100644 --- a/bench_vid_maker/src/bench_vid_maker/recorder.py +++ b/bench_vid_maker/src/bench_vid_maker/recorder.py @@ -23,6 +23,10 @@ "C:/Windows/Fonts/JetBrainsMonoNerdFontMono-Regular.ttf", "C:/Windows/Fonts/JetBrainsMonoNerdFontMono-Bold.ttf", ), # Nerd Font first: the UI uses its icon glyphs (checked against its cmap, see ui.py) + ( + str(pathlib.Path.home() / ".local/share/fonts/JetBrainsMonoNerdFontMono-Regular.ttf"), + str(pathlib.Path.home() / ".local/share/fonts/JetBrainsMonoNerdFontMono-Bold.ttf"), + ), # WSL/Linux user-font install of the same Nerd Font ("C:/Windows/Fonts/consola.ttf", "C:/Windows/Fonts/consolab.ttf"), ("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"), ] @@ -143,7 +147,15 @@ def __init__( "-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(self.out_path), ] - self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) + # bufsize=0 hands us the raw pipe instead of a BufferedWriter. A + # single frame is ~35 MB at the default size, which is far larger + # than any pipe buffer: a buffered writer can hand a partial chunk + # to the OS and ffmpeg then reads a truncated frame and aborts with + # "Invalid buffer size, packet size N < expected frame_size M". + # Writing straight to the pipe keeps every frame's bytes contiguous. + self._proc = subprocess.Popen( + cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 + ) self._frame_count = 0 def _rasterize(self, renderable: RenderableType) -> bytes: @@ -182,12 +194,41 @@ def capture(self, renderable: RenderableType, hold_frames: int = 1) -> None: stdin = self._proc.stdin assert stdin is not None for _ in range(hold_frames): - stdin.write(data) + self._write_all(stdin, data) self._frame_count += hold_frames + @staticmethod + def _write_all(stdin, data: bytes) -> None: + """Write every byte of `data` to the pipe. + + A raw pipe write is not guaranteed to transfer everything in one + call: it can return a short count when the pipe buffer fills up + while ffmpeg is busy. Writing the remainder is the only correct + behaviour -- a truncated frame makes ffmpeg abort the whole encode. + """ + view = memoryview(data) + while view: + written = stdin.write(view) + if written is None or written <= 0: + raise BrokenPipeError("ffmpeg closed the video pipe early") + view = view[written:] + def close(self) -> None: assert self._proc.stdin is not None - self._proc.stdin.close() + try: + self._proc.stdin.close() + except BrokenPipeError: + # ffmpeg already died; its exit code and stderr say why + pass self._proc.wait() if self._proc.returncode != 0: - raise RuntimeError(f"ffmpeg exited {self._proc.returncode} while writing {self.out_path}") + detail = "" + if self._proc.stderr is not None: + try: + detail = self._proc.stderr.read().decode("utf-8", "replace").strip() + except (OSError, ValueError): + detail = "" + raise RuntimeError( + f"ffmpeg exited {self._proc.returncode} while writing {self.out_path}" + + (f":\n{detail}" if detail else "") + ) diff --git a/bench_vid_maker/src/bench_vid_maker/wsl.py b/bench_vid_maker/src/bench_vid_maker/wsl.py new file mode 100644 index 0000000..12c9eae --- /dev/null +++ b/bench_vid_maker/src/bench_vid_maker/wsl.py @@ -0,0 +1,161 @@ +"""Re-executes `bench_vid_maker` inside WSL (Windows Subsystem for Linux). + +Why this exists +--------------- +`bench_vid_maker` measures CPython against Lithon, and both engines have to +run *natively* for the numbers to mean anything: + +* Lithon's fast tier is an x86-64 **native** tier (`build/tier_runner`); + running it through a Windows-to-WSL translation layer measures the + translation layer, not the engine. +* CPython's loop performance under WSL differs from its performance on a + real Linux host, so the comparison is only honest when *both* engines + run on the same Linux kernel, side by side. +* CPU affinity (`--cores N`) is only honoured on the Linux side, and the + WSL VM has its own CPU budget independent of the Windows host. + +So instead of measuring "Windows launching a Linux process", this module +re-executes the *whole tool* inside WSL and lets it do the measuring +there. Every number the tool reports is then produced by a Linux process, +and the Windows side only relays the exit code and the output. + +The repository lives on a Windows drive, which WSL mounts at +`/mnt//...`; the same files are visible on both sides, so no +copying is involved. +""" +from __future__ import annotations + +import os +import pathlib +import shutil +import subprocess +import sys + +#: Marker set in the child's environment so a re-exec never loops. +_REEXEC_MARKER = "BENCH_VID_MAKER_IN_WSL" + + +def wsl_executable() -> str | None: + """Path to `wsl.exe`, or None when this is not Windows / WSL is absent.""" + if not sys.platform.startswith("win"): + return None + return shutil.which("wsl.exe") or shutil.which("wsl") + + +def have_wsl() -> bool: + """True when this process could plausibly re-exec itself in WSL.""" + if wsl_executable() is None: + return False + if os.environ.get(_REEXEC_MARKER): + return False # already inside WSL + return True + + +def to_wsl_path(path: pathlib.Path | str) -> str: + """Map a Windows path to its WSL `/mnt//...` form. + + Paths that are already POSIX-style are returned unchanged, so the same + helper works for a config authored on either side. + """ + text = str(path) + p = pathlib.PureWindowsPath(text) + drive = p.drive + if len(drive) == 2 and drive[1] == ":": + rest = str(p)[2:].replace("\\", "/").lstrip("/") + return f"/mnt/{drive[0].lower()}/{rest}" if rest else f"/mnt/{drive[0].lower()}" + return text.replace("\\", "/") + + +def wsl_to_win_path(path: str) -> str: + """Map a WSL `/mnt//...` path back to a Windows path.""" + text = path.replace("\\", "/") + if text.startswith("/mnt/") and len(text) >= 7 and text[5] == "/": + drive = text[5].upper() + rest = text[7:] + return f"{drive}:\\{rest.replace('/', os.sep)}" if rest else f"{drive}:\\" + return text + + +def shell_join(command: list[str]) -> str: + """Join an argv into a single shell command line, quoting each token.""" + return " ".join(_shquote(tok) for tok in command) + + +def _shquote(token: str) -> str: + if token and all(c.isalnum() or c in "-_./=:" for c in token): + return token + return "'" + token.replace("'", "'\\''") + "'" + + +def reexec_in_wsl(argv: list[str] | None = None) -> int: + """Run the current tool inside WSL and return its exit code. + + The child's arguments are translated to their WSL equivalents (the + config path and any `--save`/`--video` path), the working directory is + carried across, and the environment marker keeps the child from + re-execing again. + """ + exe = wsl_executable() + if exe is None: + print( + "bench-vid-maker: wsl.exe not found; cannot run inside WSL.", + file=sys.stderr, + ) + return 2 + + args = list(sys.argv[1:] if argv is None else argv) + wsl_args = _translate_args(args) + env = dict(os.environ) + env[_REEXEC_MARKER] = "1" + + # The WSL side runs the *same* installed tool. `bench-vid-maker` is on + # PATH inside WSL when it was installed into a WSL venv; otherwise fall + # back to the module invocation next to this file. + command = _child_command(wsl_args) + + print( + f"bench-vid-maker: re-executing inside WSL -> {shell_join(command)}", + file=sys.stderr, + ) + try: + proc = subprocess.run([exe, "-e", "bash", "-lc", shell_join(command)], env=env, check=False) + except OSError as e: + print(f"bench-vid-maker: could not start WSL: {e}", file=sys.stderr) + return 2 + return proc.returncode + + +def _child_command(wsl_args: list[str]) -> list[str]: + """The command the WSL side runs.""" + exe = shutil.which("bench-vid-maker") + if exe: + return [exe, *wsl_args] + # Fall back to the package's own module entry point, using whichever + # interpreter is running this process. + return [sys.executable, "-m", "bench_vid_maker.cli", *wsl_args] + + +def _translate_args(args: list[str]) -> list[str]: + """Translate Windows-side arguments into their WSL equivalents. + + Only the flags that carry a filesystem path need translating; the rest + are passed through verbatim so the child sees exactly the same run. + """ + path_flags = {"--save", "--video"} + out: list[str] = [] + i = 0 + while i < len(args): + tok = args[i] + if tok in path_flags and i + 1 < len(args): + out.append(tok) + out.append(to_wsl_path(args[i + 1])) + i += 2 + continue + if "=" in tok and tok.split("=", 1)[0] in path_flags: + flag, _, value = tok.partition("=") + out.append(f"{flag}={to_wsl_path(value)}") + i += 1 + continue + out.append(tok) + i += 1 + return out diff --git a/lithon.egg-info/PKG-INFO b/lithon.egg-info/PKG-INFO index bf1b6f7..eaf1045 100644 --- a/lithon.egg-info/PKG-INFO +++ b/lithon.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: lithon -Version: 0.8.0 +Version: 0.9.8 Summary: Zero-dependency C++ execution engine and JIT compiler for Python Requires-Python: >=3.10 Description-Content-Type: text/markdown @@ -35,17 +35,17 @@ Dynamic: license-file ## πŸš€ What is Lithon? -Lithon is a **Python library and native execution engine** that runs `.py` +Lithon is a **Python library and native execution engine** that runs `.lio` source code using an extended **PEP-526-style static typing syntax**. Lithon is designed to keep Python's familiar source-code model while giving the compiler explicit type information that can be used for static verification, native machine-code generation, and predictable execution. -Lithon is **not a separate programming language**. Python remains the source -language and `.py` remains the source format. Lithon extends the typing -information available to the compiler so that supported Python code can be -lowered to native x86-64 machine code. +Lithon sources use the **`.lio`** extension. The syntax is deliberately +Python-shaped, but `.lio` is what marks a file as a Lithon program; the frontend +refuses any other extension, so a plain Python script is never silently compiled +as Lithon. For example: @@ -71,6 +71,61 @@ fuzzing. It is **not yet a stable production library** and should not be treated as a drop-in Python replacement. +### Recently added + +- **`try`/`except` (6.10)** β€” the Python EAFP rhyme works: a `try` body whose + `int(8, input("..."))` meets a typo lands in `except ValueError:`, and the + program continues. It catches the closed set of built-in Tier-3 traps + (`ZeroDivisionError`, `OverflowError`, `ValueError`, `IndexError`, + `KeyError`, `EOFError`, or `Exception` for every one of them) with `else` and + `finally`. There is no exception object and no unwinding: a caught trap is a + plain jump to the matching handler block, and the whole construct is ordinary + control flow plus three markers both tiers read. That is also its one + narrowing β€” only a trap raised in the **same function** as the `try` is + catchable, because routing across a call boundary would need a real unwind. + `except X as e`, `raise`, `except*` and a bare `except:` are refused. + `tests/typed_regression/try_except.lio` and `try_except_eafp.lio` are the + tier-identical guards. +- **`!=` (6.9)** β€” `ast.NotEq` is now a first-class compare, not an `==` + followed by a `Not`. It mirrors `==` in typing and differs only in the result: + a dedicated `Ne` IR op lets the optimizer, branch fusion and the print guard + see the real predicate. The float path is the one with a machine-level trap: + after `ucomisd`, `!=` is true when the operands differ **or** are unordered, so + the JIT emits `setne OR setp` (fused: `jp` and `jne` to the same target) while + `==` stays `sete AND setnp` β€” emitting only `setne` would make `nan != nan` + false. Integers compare exactly as `int64`, `str` compares as a run + (lengths-or-bytes, via `strne`), and `while i != n` loops are not strength- + reduced or vectorized because `i != n` has no guaranteed trip count. + `tests/typed_regression/ne.lio` is the byte-for-byte CPython guard. +- **Raw syscalls** β€” `syscall(number, a1..a6) -> int[64]`, one IR instruction + and one host trampoline that *both* tiers call, so the calling convention + cannot drift between them. Takes int[64] and pointers. +- **Networking (5.2)** β€” `socket`/`bind`/`connect`/`listen`/`accept`/`send`/ + `recv`/`close` as raw syscalls, with `&sockaddr_in` and send/recv buffers + passed as pointers into real frame bytes. A single-process loopback round trip + across a mid-flight `getsockname` is compared line-for-line against a + libc-free C oracle, and the byte-swap failure mode is asserted explicitly. +- **Multi-argument `print`** β€” `print(a, b, c)` prints space-separated with one + trailing newline, matching Python, and `print()` prints a bare newline. It is + lowered to runtime pieces (`print_bare` / `print_sep` / `print_nl`), never a + built string, so each tier formats each value exactly as before. `sep=` and + `end=` are deferred. +- **Iteration helpers** β€” `enumerate`, `zip`, `sum`, `min`, `max`, `abs`, `any`, + `all`, and `in`/`not in` over a list. All are rewrites over the compile-time + capacity `N`, so there is no new opcode and no runtime helper. Cross-checked + against CPython byte for byte. +- **Real pointers** β€” a `ptr[T]` is a genuine host address in the interpreter as + well as the JIT, backed by per-variable byte storage that a syscall could + read. `ptr[struct[N]]` forms, typechecks, compares, scales by `sizeof(Point)`, + and **is read and written through** (`valueof(_p).x`) in both tiers β€” the JIT + gained a general `[reg+disp]` encoder primitive, verified against GNU `as` + over 18,379 cases, so pointer-base field ops run natively instead of falling + back. `ptr_field.lio` and `ptr_alias.lio` are the regression guards. +- **Three type-system holes closed** β€” a bool literal stored into an `int` + (`v: int[64] = True`) used to type-check clean and print `True`; a capacity-1 + list could not compile natively at all; and a loop target could silently + change type between `for` headers. + --- ## ✨ Why Lithon? @@ -78,7 +133,7 @@ drop-in Python replacement. Lithon is built around several principles: * **Python source with extended PEP-526 typing** β€” Lithon works directly with - `.py` source while providing additional type information to the native + `.lio` source while providing additional type information to the native execution pipeline. * **Mandatory static typing** β€” types are verified before execution. * **Fixed-width values** β€” integer widths are explicit. @@ -167,6 +222,8 @@ The compiler also checks: * container element types * pointer types * struct field types +* enum variant and match rules +* type-function arity and expansion rules * shift bounds * container capacities @@ -323,7 +380,7 @@ Lithon currently spans four implementation layers: ```mermaid graph TD - A[Python .py Source] --> B[Frontend] + A[Lithon .lio Source] --> B[Frontend] B --> C[Typed IR] C --> D[Static Flow Verification] @@ -377,7 +434,7 @@ have to block the entire execution pipeline. Use: ```bash -lithon program.py --strict +lithon program.lio --strict ``` `--strict` means: @@ -406,16 +463,16 @@ pip install -e . Then: ```bash -lithon program.py +lithon program.lio ``` Useful commands: ```bash -lithon program.py -lithon program.py --strict -lithon program.py --ir -lithon program.py -v +lithon program.lio +lithon program.lio --strict +lithon program.lio --ir +lithon program.lio -v ``` The current development package layout is still being consolidated before the @@ -449,25 +506,25 @@ cmake --build build -j$(nproc) ```bash pip install -e . -lithon tests/programs/float.py +lithon tests/programs/float.lio ``` Native-only: ```bash -lithon tests/programs/float.py --strict +lithon tests/programs/float.lio --strict ``` Print IR: ```bash -lithon tests/programs/float.py --ir +lithon tests/programs/float.lio --ir ``` Verbose execution information: ```bash -lithon tests/programs/float.py -v +lithon tests/programs/float.lio -v ``` --- @@ -486,19 +543,24 @@ The verification suite checks several independent layers. | Area | Current result | | :----------------------------------- | :---------------------------: | -| 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 | **28 / 28 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** | +| SIMD vectorizer gate + fallback | **verified** | The full verification gate concludes: @@ -621,12 +683,39 @@ xs[0] = 10 xs[1] = 20 print(xs[0]) +print(xs) # [10, 20, 0, 0] print(len(xs)) ``` Bounds are statically checked when possible and dynamically trapped when the index is only known at runtime. +#### The live length (6.7) + +A list still has a fixed capacity, but it also carries a **run-time length**: a +companion `int[64]` holding how many leading slots are live. A literal counts the +slots it names, so its trailing slots stay zeroed and are not part of the list: + +```python +xs: list[int[64], 6] = [1, 2, 3] +print(len(xs)) # 3 (not 6) +print(xs) # [1, 2, 3] +print(sum(xs)) # 6 (only the live prefix) + +xs.append(4) # writes slot 3, advances the count to 4 +xs.append(5) # ... and slot 4 +print(xs) # [1, 2, 3, 4, 5] +``` + +A bare declaration is the all-zero list with every slot live, so `len` is the +capacity. `len(xs)` reads the companion, `print(xs)` renders only the live +prefix, and the reductions `sum`/`min`/`max`/`any`/`all` and `x in xs` scan the +live prefix. Because those are unrolled over a compile-time length, appending +inside a loop β€” which makes the length run-time-only β€” refuses them +(`E0114`/`E0115`/`E0119`). `append` onto a full list traps +(`list index out of range`) in both tiers, and a statically-known "already full" +append is refused (`E0111`). + ### Tuples Homogeneous immutable tuples are supported: @@ -666,6 +755,217 @@ Supported operations include: Mutation after construction is intentionally not currently supported. +A key is an `int` (any width up to 64), a `bool`, or a bounded string (6.8): + +```python +k: str[16] = "name" + +d: dict[str[16], int[64], 4] = {"name": 1, "age": 2} +print(d[k]) # 1 (a str variable names the key's run) +print(d["age"]) # 2 (a literal is materialised into its own run) +print(contains(d, "zz")) +``` + +A str key is not a scalar, so it has no register form: the operand is the *name* +of the key's byte run, and the probe is a byte hash and a byte comparison rather +than a multiply and a word compare. Both tiers run the **same** walk, so a str +key does not merely hash the same way in each tier β€” the two cannot disagree, +because there is one function (`dict_str_probe`) behind both. Because a key is +copied into a fixed `str[N]`-wide slot, a key's *capacity* must not exceed the +table's key capacity; a literal of `L` bytes also needs `L < N`, exactly as it +would as a `str[N]` assignment. Table order and bucket (memory) order coincide, +so a whole-dict `print` of a str-keyed table is deterministic but not insertion +order. + +A value is a scalar (`int`, `float`, `bool`, `str[N]`) or a container β€” a `list` +or a `tuple` (6.9): + +```python +d: dict[int[64], list[int[64], 4], 4] = {1: [10, 20, 30, 40]} + +print(d[1][0]) # 10 (the value is read out into a run, then indexed) +print(d) # {1: [10, 20, 30, 40]} +``` + +A container value has no scalar form, so it is not stored in a register: the +table's value region is a flat run of leaf cells (`buckets Γ— leaves`), and the +value is copied in and out of one bucket's slice as a whole β€” from a run at +construction, into a run on a read. The copy moves whole leaves at the leaf's +own width (8, 4, 2 or 1 bytes, whichever divides the remaining run), not whole +8-byte slots, because the leaves are packed: `list[tuple[int[32],4],2]` is eight +4-byte leaves, and a slot-at-a-time copy would read past the run and skip every +other leaf. A value of 1000 reads back as 1000, not as `1000 mod 256`. Both tiers +copy exactly the same leaves, so a nested value prints identically. A `dict` +value is still refused (it has three regions, so it has no single leaf run a +value slot could hold). + +### Strings (6.2) + +Bounded strings are supported: + +```python +s: str[8] = "hi" + +print(s) # hi +print(len(s)) # 2 +print(s[0]) # 104 ('h' as an int[8] byte) + +s[1] = 73 # write a byte -> "hI" +print(s) # hI + +t: str[8] = "hI" +print(s == t) # True +``` + +A `str[N]` is exactly `N` bytes of NUL-terminated storage β€” the same packed +byte run a `list[int[8], N]` would be β€” so the content is the bytes before the +first `0x00`, `len(s)` is the run-time byte count (capped at `N`), and `print(s)` +writes those bytes as text. A literal of `L` bytes needs `L < N`, since the last +byte is the terminator; a bare `s: str[8]` is the empty string. `s[i]` reads or +writes one `int[8]` byte, `len(s)` raises to a full `int[64]`, and `s == t` is a +byte-for-byte comparison that may span **different capacities**. Both the +interpreter and the JIT run the same byte scan, so a string prints and compares +identically in either tier. + +Two substring scans complete the string surface (6.4): + +```python +s: str[16] = "hello world" + +print(s.find("world")) # 6 (lowest index, or -1 when absent) +print(s.find("z")) # -1 +print(s.count("l")) # 3 (non-overlapping matches) +print(s.count("")) # 12 (len(s) + 1, as in Python) +``` + +`s.find(sub)` returns the lowest index of `sub` or `-1`; `s.count(sub)` counts +non-overlapping occurrences. The needle is a string literal or another `str` +variable, and its capacity need not match the haystack's. The empty needle is +the Python edge: `find` returns `0`, `count` returns `len(s) + 1`. Both scans +read the same NUL-terminated byte runs `s == t` does, so they agree byte for +byte across the two tiers. + +The one way to take a string apart is `str.split` (6.7). A list is a declared +run, not a value, so the pieces have to be named somewhere: the destination is +declared on the left, and the split writes into it. + +```python +s: str[32] = "a,bb,ccc" + +parts: list[str[8], 4] = s.split(",") +print(len(parts)) # 3 +print(parts) # ['a', 'bb', 'ccc'] + +t: str[4] = "::" +q: list[str[8], 4] = "x::y::z".split(t) # a str variable separator too +print(q) # ['x', 'y', 'z'] +``` + +`s.split(sep)` fills the destination and sets its length companion, so `len` and +`print` see exactly the pieces produced β€” including empty ones, so `",a,b,"` +splits into `['', 'a', 'b', '']`. A piece longer than `K-1` bytes is truncated +to fit its `str[K]`, and a split that produces more than `M` pieces traps +(`too many pieces for the list (str.split)`) rather than dropping any silently. +The whitespace-splitting form `s.split()` and an empty separator are refused, so +the separator is always explicit. + +#### A string is a leaf type + +Because a `str[N]` is just a byte run, it can be *anywhere* a scalar element can +be: a list or tuple element, a struct field, or a pointer's pointee. + +```python +words: list[str[8], 3] = ["one", "two", ""] +print(words[1][2]) # 111 ('o' of "two") +words[1][2] = 88 # -> "twX" + +class Named(Struct): + name: str[8] + n: int[8] + +c: Named +c.name = "hi" +print(len(c.name)) # 2 +c.name[0] = 120 # -> "xi" + +s: str[8] = "hi" +_p: ptr[str[8]] = addressof(s) +print(valueof(_p)) # hi -- the pointer names s's run +print(len(valueof(_p))) # 2 +``` + +A struct's `str` field lives in a *companion byte run* named by the dotted +attribute path (`c.name`), declared right after the struct's own blob; a nested +field is named by the whole path, so `o.inner.tag` is one run too. A `str[N]` +pointer names the string it was minted from, so `valueof(_p)` denotes that +string's place rather than a scalar β€” every string operation works through it. +A field reached through a pointer *into the struct* (`valueof(_pp).name`) is +refused: the companion run is keyed by the variable name, so there is no run to +name there. + +#### Printing a nested string + +A string printed **directly** β€” a bare variable, or a struct field β€” prints its +bare contents. A string that appears **inside an aggregate** (a list or tuple) +prints the way Python's `repr` does: quoted, with the same backslash escapes +CPython uses. + +```python +s: str[8] = "hi" +print(s) # hi (bare, top level) + +pair: tuple[str[8], 2] = ("hi", "a b") +print(pair) # ('hi', 'a b') (repr, nested) +``` + +Both tiers produce byte-identical output for both forms. + +#### Printing string literals and f-strings (6.3 Stage A) + +A string literal may be printed directly, and an f-string may be used inside +`print`. Neither builds a string value: a constant chunk is written into a +shared bounded run and printed through the same top-level path `print(s)` uses, +while `{expr}` is printed by the ordinary value printer, so a `%` or a brace in +a literal is just a byte and floats use the one shared shortest-round-trip +formatter. + +```python +x: int[32] = 3 +s: str[16] = "world" + +print("hello") # hello +print("x is:", x) # x is: 3 (arguments are space-separated) +print(f"x is {x}") # x is 3 (f-string pieces are adjacent) +print(f"{{x}}={x}") # {x}=3 ({{ }} escape a brace) +print("100%") # 100% +``` + +`{{` / `}}` escape a literal brace **inside an f-string only**; in a plain +string literal they are two ordinary braces, exactly as in Python. A format +specifier (`{x:.2f}`), a conversion (`{x!r}`) and `{x=}` are refused rather than +printed wrong. An f-string used as a *value* (`s: str[64] = f"x={x}"`) is Stage +B and is not implemented yet. + + +### Printing and nesting + +`print(xs)` renders a whole container the way Python's `repr` does β€” `[10, 20, +30]` for a list, `(1, 2, 3)` for a tuple (with the one-element trailing comma +`(1,)`) β€” in both tiers, and a `dict` prints in bucket (memory) order. + +Containers **nest**: an element may itself be a list or tuple, so +`list[list[int[64], 4], 2]` and `list[tuple[int[64], 3], 4]` are legal and a +nested `dict` is refused (a dict has no flat scalar leaf). The layout is a flat +run of scalar leaves, so `xs[i][j]` is a single indexed access at the summed +byte offset β€” there is no intermediate container object. Reads, writes and +`print` all descend to the deepest leaf; a partial index chain like `xs[i]` on a +nested container is refused. + +A container is *storage, not a value*. It is built by its declaration plus +`IndexStore`s, and there is nothing to copy, so `z = ys` (two lists) and +`xs[0] = a` (a container element) are refused at compile time rather than +miscompiled. + --- ## 🧱 Structs @@ -720,6 +1020,165 @@ compiler and asserts the frontend's offsets and sizes equal the C --- +## 🏷️ Enums + +An enum is a tagged union: a byte blob holding an `int` tag plus 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. It is spelled as a class +deriving from `Enum`: + +```python +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) +``` + +A variant is a bare unit name (`Empty`), a `name: type` scalar payload +(`Line: int[32]`), or an inline `class V(Struct)` struct payload. The layout is +`[tag][payload]` with **no padding**: the tag is an `int[8]` while the enum has +at most 128 variants and widens to `int[16]` past that, the payload starts at +the tag width, and the size is the tag width plus the largest payload. Only the +active variant's bytes are ever read, so overwriting a payload variant with a +smaller one leaves stale bytes that no `match` can observe. + +A `match` must be exhaustive β€” every variant covered, or a final `case _:`. It +lowers to a tag `fieldload`, an equality test, and a branch per arm, reusing the +struct machinery; there is no new IR. A struct field may be an enum +(`c.proto = Endpoint.Tcp(port=8080, addr=16777343)`), but list elements, enum +parameters/returns, enum-typed payloads, and `case` guards are refused. + +`tools/check_enum_layout.py` compiles an equivalent C `#pragma pack(1)` +tag-plus-union and asserts the frontend's tag width, payload offset, and size +equal the C `offsetof`/`sizeof` values at both tag-width boundaries. + +--- + +## πŸ”§ Methods + +A `def` inside a struct or enum body is a **method**. The first parameter is the +implicit receiver `self`; the rest are annotated scalars, and the return type is +optional. A method is called as `recv.name(args)`. + +```python +class Counter(Struct): + value: int[64] + + def add(self, n: int[64]) -> int[64]: + self.value = self.value + n + return self.value + + def total(self, n: int[64]) -> int[64]: + t: int[64] = 0 + i: int[64] = 1 + while i < n + 1: + t = t + i + i = i + 1 + return t + + +c: Counter +print(c.add(5)) +print(c.total(4)) +``` + +A method call is **inlined at the call site**: `self` becomes the receiver place, +the method's parameters and locals are renamed so they cannot collide with the +caller's, and its `return` stores the result and jumps to the end of the inline. +Inlining 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) β€” and it is +what lets a method read *and write* the receiver's fields, keep its own locals, +run loops and branches, and call other methods. The receiver can be a struct or +enum variable or a struct/enum field (`o.inner.bump(1)`), and an enum method +reads `self` with `match self`. + +Because the body is expanded at each call site, a method cannot recurse, +directly or indirectly (E0135). A method signature is scalars only +(`int[w]`/`float[64]`/`bool`): a method may not take or return a struct, an +enum, a container or a pointer, and `self` is unannotated. There is no +inheritance or overloading, a method name may not shadow a field or a variant, +and the signature accepts no `/`, `*`, `**` or defaults (E0134). + +```python +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 +``` + +`tools/check_methods.py` proves the transparency claim: it compiles a +method-using program and the same program with the bodies inlined by hand and +requires both tiers to produce identical output, checks the IR is deterministic, +and checks the E0134/E0135 refusals. + +--- + +## 🧩 Type Functions + +A type function is a **compile-time type constructor**: `def Name(params) -> +NewType` returns `{ T }`, and writing `Name(args)` anywhere a type is allowed +substitutes the arguments into `T` and yields an ordinary type. + +```python +def Fixed(t, n) -> NewType: + return {list[t, n]} + +def Word() -> NewType: + return {int[32]} + +def Ref(t) -> NewType: + return {ptr[t]} + +xs: Fixed(int[64], 4) # == list[int[64], 4] +w: Word() # == int[32] +_p: Ref(int[64]) # == ptr[int[64]] +``` + +The result is **structural** β€” `Alias(int[64])` and `int[64]` are the same +type, so nothing about a type function survives into the IR: compiling the +program above and the same program with the three types spelled out produces +byte-identical IR. The parameters hold **types and sizes** (they are +unannotated names), not runtime values; a definition carries no code and is +invisible to the runtime. + +Applications nest (`Pair(Word())`) and work anywhere an annotation does: +function parameters and returns, struct fields, and container element types. +The body must be exactly one `return { }` β€” `{}` is a dict and is +refused, as are multi-element bodies, comprehensions, decorators, annotated or +defaulted parameters, redeclarations, and names that collide with a struct, an +enum or a function. A body composes the value types an annotation can spell β€” +scalars, containers, pointers and dicts β€” and may not name a struct or an enum, +which are storage rather than values. A type function must bottom out in a +concrete type, so direct and mutual recursion are refused. + +`tools/check_typefunc.py` proves the structural claim: it compiles a +type-function program and its spelled-out equivalent and requires the IR to be +byte-identical, then checks memoization and the E0132/E0133 refusals. + +--- + ## 🧠 SSA and Optimization Lithon's compiler is progressively moving from a memory-based IR toward SSA. @@ -750,7 +1209,7 @@ Current components include: Phi handling is now implemented as an opt-in native path: ```bash -lithon program.py --strict +lithon program.lio --strict ``` with the compiler's direct-Phi development path available through the relevant @@ -875,6 +1334,36 @@ the CPU's register-level behavior. --- +## πŸ”Œ Raw Syscalls + +Lithon can invoke the platform's raw syscall primitive directly: + +```python +uid: int[64] = syscall(102) # getuid() on Linux/x86-64 +``` + +`syscall(number, a1, a2, a3, a4, a5, a6)` takes a syscall number and up to six +arguments and returns the kernel's **raw** result as `int[64]`. On Linux a +failure is a negative `-errno`; nothing is wrapped and nothing is checked β€” a +program that wants to handle failure compares the result itself. There is no +`syscall` function to import: it is a compiler-recognized name that lowers to a +single `Syscall` IR instruction. + +Both tiers execute it through one shared host trampoline +(`src/jit/syscall_runtime.h`), so the calling convention cannot drift between +the interpreter and the JIT. That trampoline consults an embedder-installed +hook first, on every platform; with no hook it runs the actual x86-64 `syscall` +instruction on Linux, and on a platform whose raw ABI is not stable (Windows) +returns `-ENOSYS` rather than guessing. + +**Current restriction:** arguments are integers in this slice. A pointer +argument is refused at typecheck, because the interpreter represents pointers as +synthetic addresses rather than real memory, so a syscall that takes a pointer +(`write`, `read`) could not behave the same in both tiers. Pointer-taking +syscalls are deferred until the interpreter grows a real address-space story. + +--- + ## πŸ›‘οΈ Safety Philosophy Lithon is not trying to eliminate low-level programming. @@ -922,7 +1411,7 @@ rather than hiding the fact that a variable contains a raw address. | Feature | CPython | Cython / mypyc | PyPy | **Lithon** | | :-------------------- | :------------------- | :-------------------------- | :------------- | :------------------------ | -| Syntax | Python | Python | Python | **Python `.py` + Extended PEP-526** | +| Syntax | Python | Python | Python | **Python-shaped `.lio` + Extended PEP-526** | | Primary execution | Bytecode interpreter | C extensions | Tracing JIT | **Native x86-64** | | Static typing | No | Optional | No | **Mandatory** | | Fixed-width integers | No | Possible | No | **Built-in** | @@ -964,6 +1453,73 @@ than CPython, PyPy, Cython, Rust, or C++. --- +## πŸ”’ Iteration Helpers + +`enumerate` and `zip` work in a `for` header: + +```python +for i, x in enumerate(xs): + print(i) + print(x) + +for a, b in zip(xs, ys): + print(a) + print(b) +``` + +Neither is a value. Both are a **rewrite of a counted loop** over constructs that +already exist: `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. +So there is no new opcode and no runtime helper; `zip` costs nothing beyond the +indexed loads it already emits. + +Two consequences worth stating plainly: + +- **`zip` stops at the shortest input**, exactly as Python does. `zip(xs, ys)` + over a `list[..., 6]` and a `list[..., 3]` runs three times. +- **`enumerate` and `zip` are for-header only.** `z = zip(a, b)` is refused: the + pair never needs to exist as a value, and a homogeneous tuple could not + represent `enumerate`'s `(int, T)` anyway. + +Elements bind with their declared type, so a `float[64]` list yields floats and +a `list[int[16], N]` stays signed. `break` and `continue` work inside either +header, because the rewrite is a real loop. + +### 🧾 Divergences from Python + +Deliberate narrowings, collected next to `%` and `v == n`: + +| Construct | Python | Lithon | Why | +|---|---|---|---| +| `%` | remainder, sign follows the divisor | truncation toward zero; zero divisor traps (`E0301`/`E0302`) | signed machine remainder is not total | +| `v == n` | value equality | not supported | equality on machine values has no total answer | +| `zip` / `enumerate` | first-class iterators, usable as values | for-header only | a rewrite of a counted loop; the pair is never a value | +| `x in xs` on a list | membership, over any element type | membership over int/float/bool lists | it lowers to `==`; an aggregate or `str` element has no scalar `==` | +| `a == b` on two bools | `True == 1` | same: a bool compares as `0/1` in both tiers | a bool is a 1-byte scalar, not a distinct incomparable type | +| `v in xs` on a `bool` list | membership | supported | same `==` over the `0/1` value | +| `print(a, b)` | space-separated, one trailing newline | supported; `sep=` / `end=` not yet | the default separator and newline are runtime calls; a configurable one needs string values | +| f-strings | any expression, format specs, `!r` | in `print` only (Stage A); `{expr}` for int/float/bool/str, `{{`/`}}` escapes | a format spec / conversion needs a runtime formatter; an f-string *value* needs a bounded buffer | +| `print("literal")` | prints the text | supported | the bytes are materialised into a bounded run and printed through the `str` path; no string value is built | +| `print(_p)` | a stable address | prints an address, but it differs per tier | real addresses cannot be byte-comparable across tiers | +| a dangling `_p` | undefined | undefined, and now a `SIGSEGV` | out of scope by design, not tracked by the compiler | +| string literals | unbounded `str`, any operation | `str[N]` bounded strings: literal construction, `print`, `len`, `s[i]` read/write of one `int[8]` byte, `s == t`; usable as a list/tuple element, a struct field or a `ptr` pointee | the capacity must be declared, so a string is a fixed byte run with no heap | +| a list literal shorter than its capacity | the list has exactly the elements written | the trailing slots stay zeroed and are **not** live: `len`/`print`/a reduction see only the named elements | the run is fixed-size and zeroed, and the live count is a separate companion | +| `xs[i] = v` on a list | grows the list if `i` is past the end | unchanged length; `i` must be in `0..cap-1` or it traps | the length is advanced by `append`, not by an indexed store | +| `list.append(v)` | amortised O(1), unbounded | writes the next live slot; a full list traps (`list index out of range`) | the capacity is fixed, so growth has no storage to run to | +| `list.append(v)` inside a loop | β€” | makes the length run-time-only, which refuses `sum`/`min`/`max`/`any`/`all`, `in`, `enumerate` and `zip` over that list (`E0114`/`E0115`/`E0119`) | those are unrolled over a compile-time length; a run-time bound has none | +| `s.split(sep)` | returns a new `list[str]` | `parts: list[str[K], M] = s.split(sep)` writes into a declared list; a piece longer than `K-1` bytes is truncated, and more than `M` pieces traps | a list is a declared run, not a value, so the destination is named; the pieces are bounded strings | +| `s.split()` (whitespace) | splits on runs of whitespace | refused (`E0114`) | the default form needs a whitespace-class scan; an explicit separator is the one this slice implements | +| a `dict` | mutable, `d[k] = v` after construction, any hashable key | immutable: built by its literal, then read and `contains` only; keys are `int`/`bool`/`str[N]` | one probe both tiers derive from one hash, with no growth and no rehash | +| a `str` dict key | any string | `str[N]`: a literal key or a `str` variable whose **capacity** does not exceed the table's key capacity | a key is copied into a fixed `N`-byte slot; a wider run could not survive the copy | +| a `dict` value | any value, including another `dict` | a scalar, or a `list`/`tuple` run; a `dict` value is refused (`E0112`) | a value slot is one flat leaf run; a `dict` has three regions, so it has no single run a slot could hold | +| source file extension | `.py` | `.lio`; the frontend refuses any other extension (`E0114`) | the extension is what marks a file as a Lithon program, so a plain Python script is never silently compiled as Lithon | +| `try`/`except` (6.10) | catches any exception, from anywhere, and `finally` always runs | catches the closed set of built-in Tier-3 traps (`ZeroDivisionError`, `OverflowError`, `ValueError`, `IndexError`, `KeyError`, `EOFError`, or `Exception` for all of them) **raised in the same function as the `try`**; a trap raised inside a called function ends the program, and an *uncaught* trap does not run a `finally` | a trap is routed statically, so a caught trap is a plain jump within one function; crossing a call boundary would need a real unwind, and re-running `finally` on a propagating trap would need `raise` | +| a bare `except:` | catches everything | refused (`E0114`) | it would claim to catch a segfault, which no tier can catch; `except Exception:` is the spelled-out catch-all | +| `except X as e` / `raise` / `except*` (6.10) | binds an exception object, re-raises, groups | refused (`E0114`) | there is no exception object -- a caught trap is a jump to a block, so there is nothing to bind or re-raise | + +--- + ## 🧭 Roadmap ```text @@ -1021,11 +1577,74 @@ than CPython, PyPy, Cython, Rust, or C++. ### Phase III β€” Systems / hardware integration -* [ ] Direct syscall emission +* [x] Direct syscall emission (raw `syscall(number, a1..a6) -> int[64]`) +* [x] Networking syscalls (5.2): `socket`/`bind`/`connect`/`listen`/`accept`/ + `send`/`recv`/`close`, with `&sockaddr_in` and buffers passed as pointers β€” a + single-process loopback round trip is verified against a raw C oracle + (`tools/check_net_loopback.py`), including the byte-swap failure mode * [ ] Expanded raw-memory operations * [ ] Expanded native FFI * [ ] ARM64 backend +### Phase IV β€” Python-familiar surface + +* [x] Iteration helpers: `enumerate`, `zip` (for-header only; a rewrite of a + counted loop, no new IR) +* [x] `sum` / `min` / `max` / `abs` / `any` / `all`, and `in` over a list β€” also + rewrites over the compile-time capacity `N`; `min`/`max` select through the + if-expression diamond, `abs` is `-v if v < 0 else v` +* [x] Multi-argument `print`, lowered to runtime pieces (a value printer, a + separator, a newline) rather than a built string; `sep=` / `end=` deferred +* [x] `str[N]` bounded strings and string literals: literal construction, `print`, + `len`, `s[i]` read/write of one `int[8]` byte, `s == t`, usable as a list/tuple + element, a struct field and a `ptr` pointee +* [x] f-strings and string-literal printing, in `print` only (6.3 Stage A): + `print("x is:", x)` and `print(f"x is {x}")` lower to literal chunks plus value + prints β€” no string is built; `{expr}` for int/float/bool/str, `{{`/`}}` escapes, + a `%` in a literal is an ordinary byte. Format specs (`{x:.2f}`), `!r` and + `{x=}` are refused; an f-string as a *value* (Stage B) is still to come +* [x] `str.find` / `str.count` (6.4): `s.find(sub)` returns the lowest index or + `-1`, `s.count(sub)` counts non-overlapping matches; the needle is a string + literal or another `str`, and the empty needle matches Python (`find` β†’ 0, + `count` β†’ `len(s) + 1`) +* [x] `list.append` and the run-time list length (6.7): a list is still a + fixed-capacity run, but it now carries a companion `int[64]` holding how many + slots are live. A literal counts the slots it names (the trailing ones stay + zeroed), `append(v)` writes the next slot and advances the count, `len(xs)` is + the count, `print(xs)` renders only the live prefix, and `sum`/`min`/`max`/ + `any`/`all` and `in` scan it. Appending onto a full list traps + ("list index out of range") in both tiers; appending inside a loop makes the + length run-time-only, which refuses a reduction/membership scan (`E0114`/ + `E0115`/`E0119`) and turns a compile-time "already full" append into `E0111`. +* [x] `str.split` (6.7): `parts: list[str[K], M] = s.split(sep)` splits a bounded + string into a declared, fixed-capacity list of bounded strings. `sep` is a + string literal or another `str`; the piece count lands in the destination's + length companion (`len`/`print` see exactly the pieces). A piece longer than + `K-1` bytes is truncated, and more than `M` pieces traps in both tiers. The + whitespace-splitting form `s.split()` and an empty separator are refused + (`E0114`). +* [x] `x in xs` over a `bool` list β€” it lowers to `==`, and a bool now compares + as its `0/1` value in both tiers, so membership works over every scalar element + type (int, float, bool) +* [x] `!=` (6.9) β€” a first-class `Ne` compare mirroring `==`, differing only in + the result. Integers compare exactly as `int64`, floats follow IEEE (`nan != + nan` is true via `setne OR setp`; `0.0 != -0.0` is false), `str` compares as a + run (`strne`), pointers compare raw addresses, and `!=` is usable as a value, + in `if`/`while`, and inside `and`/`or`. A `while i != n` loop is not + strength-reduced or vectorized, since `i != n` has no guaranteed trip count. + `raise`, `try`/`except` and comparison on aggregates (struct/list/tuple/dict/ + payload enum) remain deferred. +* [x] `try`/`except` (6.10) β€” catches the closed set of built-in Tier-3 traps + (`ZeroDivisionError`, `OverflowError`, `ValueError`, `IndexError`, + `KeyError`, `EOFError`, or `Exception` for all of them), with `else` and + `finally`. A caught trap is a jump to the matching handler block, not an + exception object and not a stack unwind, so `try`/`except` is ordinary + control flow plus three markers (`TryEnter`/`TryLeave`/`TryCatch`) that both + tiers read. Only a trap raised in the same function as the `try` is + catchable; `except X as e`, `raise`, `except*` and a bare `except:` are + refused. The EAFP rhyme works: `int(8, input("..."))` on a typo lands in + `except ValueError:`. + --- ## πŸ“ Current Known Limitations @@ -1044,6 +1663,54 @@ Current limitations include: * Pointer values have strict restrictions around function boundaries and container access. * Some SSA/native paths remain opt-in or have known unsupported cases. +* **Field access through a pointer (`valueof(_p).x`) works in both tiers.** The + IR, parser, type checker, interpreter and JIT all agree: a field read or write + whose subject is a pointer register lowers to the same byte offset the named + form uses. The JIT gained a general `[reg+disp]` encoder primitive (verified + against GNU `as` over 18,379 cases, zero disagreements) so these instructions + run natively instead of falling back. `tests/typed_regression/ptr_field.lio` + proves the read and write paths, and `ptr_alias.lio` proves the named and + pointer forms stay aliased across a loop and an inlined method at every + optimization level. +* `ptr[Point]` forms, typechecks, compares, scales by `sizeof(Point)`, and can be + read and written through in both tiers. A method receiver is still bound by + inlining as a concrete place rather than as a `ptr[Point]`, which is + observationally the same and needs no separate pointer path. +* **A bounded string is a leaf type.** A `str[N]` may be a list/tuple element, a + struct field, or a `ptr[str[N]]` pointee. A struct's `str` field lives in a + companion byte run named by the dotted attribute path (`s.a`, `s.inner.a`), so + `len`, byte reads/writes, `print` and `==` all reuse the top-level string path + with no separate codegen; a nested field is named by the whole path. A + `ptr[str[N]]` (only meaningfully obtained from `addressof(s)`) names `s`'s run, + so `valueof(_p)` denotes that string's place. A `str` field reached through a + pointer *into* the struct (`valueof(_pp).a`) is refused (`E0128`), because the + companion run is keyed by the variable name. `tests/typed_regression/` covers + a list of strings (`strings_nested.lio`), a struct field (`struct_str.lio`), a + nested struct field (`struct_str_nested.lio`) and a string pointer + (`ptr_str.lio`). +* **Address-taken memory is an optimizer barrier.** A variable whose address is + taken is never promoted to SSA (both `ssa.h` and `register_alloc.h` gate on + `AddressOf`), and a pointer cannot cross a function boundary (the header + records no pointee, `E0124`), so no pass can forward a stored value across a + pointer store or elide one. `ptr_alias.lio` is the regression guard. +* **Dangling pointers are out of scope by design.** The compiler does not track + a pointer's lifetime against what it points at, so dereferencing freed or + out-of-bounds memory is undefined β€” and now that pointers are real addresses, + a wild dereference is a `SIGSEGV` in the interpreter too, not a deterministic + trap. This is a stated choice, not a silent gap: the programmer proves the + math, the types and the constraints, never the memory location. +* **A printed pointer value is not comparable across tiers.** The interpreter's + arena addresses and the JIT's frame addresses differ, and ASLR moves them + between runs. Pointer arithmetic, comparison of two pointers into the same + object, and `valueof` *are* tier-consistent; the raw number is not. Programs + that print a pointer are excluded from the tier-diff oracles. +* `syscall(...)` takes int[64] and pointer arguments. Networking (5.2) is + reachable: `bind`/`connect` pass `&sockaddr_in` and `send`/`recv` pass a + buffer, both as real pointers into frame memory. Network byte order is *not* + applied for you β€” the port and address must be byte-swapped explicitly, and a + wrong swap silently names the wrong address (`tools/check_net_loopback.py` + pins both the round trip and the failure mode). Floats and aggregates are not + passed by value. * AVX2 vectorized code is emitted and byte-verified everywhere, but it is only *executed* on hosts that report AVX2; on other hosts the CPUID gate always takes the scalar fallback, so vector execution itself is not exercised there. @@ -1090,6 +1757,34 @@ python3 tools/run_typed_regression.py python3 tools/run_tier_diff.py ``` +Every program runs twice β€” once through the interpreter (the oracle) and once +through the native tier β€” and the two stdout streams must be byte-identical. +Currently **159 / 159** cases pass, 93 of them on the native tier. + +Two classes of case are deliberately outside this comparison: + +- **Adversarial cases** declare which tier they *expect*, so a guard that is + too weak (native where it must refuse) and too strict (interpreter where + native is provably safe) both fail the run. +- **Printed pointer values** cannot be byte-comparable, because the two tiers + address memory differently. Pointer arithmetic, comparison of two pointers + into the same object, and `valueof` *are* compared. + +Sockets are likewise outside it: they are stateful and external, so 5.2 is +verified against a raw C program doing the identical syscalls instead. + +### Syscall hook parity + +```bash +./build/syscall_hook_test +``` + +`g_syscall_hook` is consulted first on every platform, which turns the raw +syscall into a pure function of its inputs. The test installs a hook and runs +the *same* program through both tiers, requiring the hook to observe identical +arguments β€” which is what pins the JIT's six-slot stack staging and keeps the +two engines from drifting about the calling convention. + ### Differential fuzzing ```bash @@ -1119,7 +1814,7 @@ generated by the same benchmark harness. | Benchmark | Lithon JIT | Reference | Speedup | Result | |---|---:|---:|---:|---:| -| fib(30) | 10.1771 ms | 1576.4747 ms | 154.9Γ— | 832040 | +| fib(30) | 10.3053 ms | 1731.0983 ms | 168.0Γ— | 832040 | **Status:** PASS diff --git a/test_codes_lithon/DataTypes/check_strings.lio b/test_codes_lithon/DataTypes/check_strings.lio index bb40472..cae996b 100644 --- a/test_codes_lithon/DataTypes/check_strings.lio +++ b/test_codes_lithon/DataTypes/check_strings.lio @@ -1,11 +1,11 @@ -# check_strings.py +# check_strings.lio # # Strings are NOT supported yet: the frontend lowers a bare string literal to # "expression node Constant not supported yet". Kept here as the pinned # example of that gap -- running it fails loudly rather than silently # producing a wrong answer. # -# $ lithon test_codes_lithon/check_strings.py +# $ lithon test_codes_lithon/DataTypes/check_strings.lio # NotImplementedError: expression node Constant not supported yet # # Until that lands, the closest working equivalent is `len()` on a pointer or diff --git a/test_codes_lithon/Pointers/pointer_error.lio b/test_codes_lithon/Pointers/pointer_error.lio index 6d2594e..dcbd59d 100644 --- a/test_codes_lithon/Pointers/pointer_error.lio +++ b/test_codes_lithon/Pointers/pointer_error.lio @@ -1,15 +1,15 @@ -# pointer_error.py +# pointer_error.lio # # The pointer `_` rule (4.4) enforced by the frontend: a pointer binding must # carry a leading underscore, because raw memory has to be visible in the # source. The two names below are deliberately wrong, so this file exists to # fail: # -# $ lithon test_codes_lithon/pointer_error.py +# $ lithon test_codes_lithon/Pointers/pointer_error.lio # NotImplementedError: a pointer is named with a leading underscore, # write `_ptrI: ptr[...]` or pick a name that starts with '_' (4.4) # -# The corrected spelling is in check_pointers.py. +# The corrected spelling is in Pointers/check_pointers.lio. i: int[16] = 22 ptrI: ptr[int[16]] = addressof(i) print(valueof(ptrI)) \ No newline at end of file diff --git a/test_codes_python/Classes/class_basics.py b/test_codes_python/Classes/class_basics.py new file mode 100644 index 0000000..1e3e49e --- /dev/null +++ b/test_codes_python/Classes/class_basics.py @@ -0,0 +1,22 @@ +"""Plain-CPython equivalent of test_codes_lithon/Classes/class_basics.lio. + +Same program: a class with two typed fields, then a value of that class with +the fields assigned. CPython has no width annotations, so the class body just +declares the names and the assignments follow. + +Note: Lithon requires a class to derive from Struct or Enum; CPython has no +such requirement, so the portable subset drops it. +""" + + +class test_class: + name: str + year: int + + +x = test_class() +x.name = "Lithon" +x.year = 2026 + +print(x.name) +print(x.year) diff --git a/test_codes_python/base_var.py b/test_codes_python/DataTypes/base_var.py similarity index 74% rename from test_codes_python/base_var.py rename to test_codes_python/DataTypes/base_var.py index 910b03e..883b26e 100644 --- a/test_codes_python/base_var.py +++ b/test_codes_python/DataTypes/base_var.py @@ -1,8 +1,8 @@ -"""Plain-CPython equivalent of test_codes_lithon/base_var.py. +"""Plain-CPython equivalent of test_codes_lithon/DataTypes/base_var.lio. Same program: declare a binding with a type and no value, assign it, print. CPython has no annotation-only declaration, so the annotation is dropped and the binding is simply assigned -- the definite-assignment rule is Lithon's. """ i = 0 -print(i) \ No newline at end of file +print(i) diff --git a/test_codes_python/DataTypes/check_strings.py b/test_codes_python/DataTypes/check_strings.py new file mode 100644 index 0000000..70c5ed6 --- /dev/null +++ b/test_codes_python/DataTypes/check_strings.py @@ -0,0 +1,17 @@ +"""Plain-CPython equivalent of test_codes_lithon/DataTypes/check_strings.lio. + +Same program: the closest working equivalent to a string operation in +Lithon -- `len()` on a container, which is capacity-driven rather than +content-driven, because a bare string literal is not yet a value in Lithon. + +NOTE: in Lithon this file is a *pinned failure*. The engine refuses it +on purpose so the gap stays visible: + + $ lithon test_codes_lithon/DataTypes/check_strings.lio + LITHON-E0114: expression node Constant not supported yet + +The corrected spelling (same pointer, same pointee width) does run, and +prints False -- a real address is never null. +""" +xs = [0, 0, 0, 0, 0, 0, 0, 0] +print(len(xs)) # 8 diff --git a/test_codes_python/DataTypes/convertions.py b/test_codes_python/DataTypes/convertions.py new file mode 100644 index 0000000..0c3afa1 --- /dev/null +++ b/test_codes_python/DataTypes/convertions.py @@ -0,0 +1,15 @@ +"""Plain-CPython equivalent of test_codes_lithon/DataTypes/convertions.lio. + +Same program: explicit width conversions between int and float, plus a +32-bit integer that keeps its value. CPython's int/float are already the +widest widths, so only the width arguments are dropped. +""" +a = 24 +b = 512 +c = 22 / 7 +k = 2147483647 +print(a, b, c) +d = int(c) +e = float(a) +f = k +print(d, e, f) diff --git a/test_codes_python/DataTypes/floats.py b/test_codes_python/DataTypes/floats.py new file mode 100644 index 0000000..70a68fa --- /dev/null +++ b/test_codes_python/DataTypes/floats.py @@ -0,0 +1,9 @@ +"""Plain-CPython equivalent of test_codes_lithon/DataTypes/floats.lio. + +Same program: print a float, print a float in exponent form, and print the +classic 0.1 + 0.2 rounding artefact. CPython's float is already binary64, +so only the width annotation is dropped. +""" +print(1.0) +print(1e22) +print(0.1 + 0.2) diff --git a/test_codes_python/DataTypes/strings.py b/test_codes_python/DataTypes/strings.py new file mode 100644 index 0000000..e60a3b5 --- /dev/null +++ b/test_codes_python/DataTypes/strings.py @@ -0,0 +1,12 @@ +"""Plain-CPython equivalent of test_codes_lithon/DataTypes/strings.lio. + +Same program: a bounded string holding "Lithon", then its length. + +Lithon declares `name: str[7] = "Lithon"` -- seven bytes, the last one the +NUL terminator -- and `len(name)` reports the six visible characters. CPython +strings are already NUL-terminated and unbounded, so the capacity is simply +dropped; len() reports the same six characters. +""" +name = "Lithon" +print(name) +print(len(name)) diff --git a/test_codes_python/Functions/fibb.py b/test_codes_python/Functions/fibb.py new file mode 100644 index 0000000..7bad9e8 --- /dev/null +++ b/test_codes_python/Functions/fibb.py @@ -0,0 +1,16 @@ +"""Plain-CPython equivalent of test_codes_lithon/Functions/fibb.lio. + +Same program: the recursive Fibonacci function with an explicit parameter +and return type, called with 30. CPython has no width annotations, so only +the annotations are dropped. +""" + + +def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + + +x = fib(30) +print(x) diff --git a/test_codes_python/Functions/max_prog.py b/test_codes_python/Functions/max_prog.py new file mode 100644 index 0000000..779742f --- /dev/null +++ b/test_codes_python/Functions/max_prog.py @@ -0,0 +1,27 @@ +"""Plain-CPython equivalent of test_codes_lithon/Functions/max_prog.lio. + +Same program: read two integers from stdin, print which is larger together +with the delta, then print the winner again. + +Both engines read the two values from stdin, so this program is only +meaningful when stdin is fed (e.g. `echo "3 7" | python3 max_prog.py`). +""" +def get_max(a, b): + if a > b: + print(f"Delta is |a-b|: {a-b}") + return a + else: + if b > a: + print(f"Delta is |b-a|: {b-a}") + return b + elif a == b: + print(f"Delta is: 0") + return 0 + + +a = int(input("Enter value 1: ")) +b = int(input("Enter value 2: ")) +if get_max(a, b) != 0: + print(f"Larger value between {a} and {b} is: {get_max(a,b)}") +else: + print("No larger value!") diff --git a/test_codes_python/find_e.py b/test_codes_python/Loops/find_e.py similarity index 83% rename from test_codes_python/find_e.py rename to test_codes_python/Loops/find_e.py index f339f39..2980249 100644 --- a/test_codes_python/find_e.py +++ b/test_codes_python/Loops/find_e.py @@ -1,4 +1,4 @@ -"""Plain-CPython equivalent of test_codes_lithon/find_e.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/find_e.lio. Same program: seed e with the n=0 term (1/0! = 1), then sum 1/n! for n = 1..7999 and print the result. @@ -20,4 +20,4 @@ def get_factorial(lim): e = e + term i = i + 1 -print(e) \ No newline at end of file +print(e) diff --git a/test_codes_python/Loops/simple_sum.py b/test_codes_python/Loops/simple_sum.py new file mode 100644 index 0000000..09fcc1a --- /dev/null +++ b/test_codes_python/Loops/simple_sum.py @@ -0,0 +1,21 @@ +"""Plain-CPython equivalent of test_codes_lithon/Loops/simple_sum.lio. + +Same program: fill a fixed-size list of four, sum it with a while loop, then +print the total and the length. CPython lists are unbounded, so the capacity +argument is dropped; the loop still runs over the same four elements. +""" +xs = [0, 0, 0, 0] + +xs[0] = 10 +xs[1] = 20 +xs[2] = 30 +xs[3] = 40 + +total = 0 +k = 0 +while k < len(xs): + total = total + xs[k] + k = k + 1 + +print(total) +print(len(xs)) diff --git a/test_codes_python/test_1b_fsum.py b/test_codes_python/Loops/test_1b_fsum.py similarity index 51% rename from test_codes_python/test_1b_fsum.py rename to test_codes_python/Loops/test_1b_fsum.py index 4df8c9c..cdf65f9 100644 --- a/test_codes_python/test_1b_fsum.py +++ b/test_codes_python/Loops/test_1b_fsum.py @@ -1,13 +1,11 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_1b_fsum.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_1b_fsum.lio. -Same program -- accumulate i over range(1_000_000_000) and print the sum. +Same program -- accumulate i over range(1,000,000,000) and print the sum. The only difference is the Lithon `int[64]` width annotations, which CPython's int already provides natively and has no syntax for. - - CPython ~188 s Β· Lithon native ~1.3 s """ i = 0 j = 0 for i in range(1000000000): j += i -print(j) \ No newline at end of file +print(j) diff --git a/test_codes_python/test_1b_fsum_simd.py b/test_codes_python/Loops/test_1b_fsum_simd.py similarity index 69% rename from test_codes_python/test_1b_fsum_simd.py rename to test_codes_python/Loops/test_1b_fsum_simd.py index ef33142..23be293 100644 --- a/test_codes_python/test_1b_fsum_simd.py +++ b/test_codes_python/Loops/test_1b_fsum_simd.py @@ -1,4 +1,4 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_1b_fsum_simd.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_1b_fsum_simd.lio. Same program: 1e9 iterations of a float[64] accumulator, adding 0.5 each time. Lithon runs this through the auto-vectorizer's AVX2 path. @@ -7,4 +7,4 @@ j = 0.0 for i in range(1000000000): j += 0.5 -print(j) \ No newline at end of file +print(j) diff --git a/test_codes_python/test_1b_loop.py b/test_codes_python/Loops/test_1b_loop.py similarity index 63% rename from test_codes_python/test_1b_loop.py rename to test_codes_python/Loops/test_1b_loop.py index 437c1fb..2a78796 100644 --- a/test_codes_python/test_1b_loop.py +++ b/test_codes_python/Loops/test_1b_loop.py @@ -1,8 +1,8 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_1b_loop.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_1b_loop.lio. Same program: a 1e9-iteration while loop that increments a counter and prints nothing -- measures pure loop overhead. """ i = 0 while i < 1000000000: - i = i + 1 \ No newline at end of file + i = i + 1 diff --git a/test_codes_python/Loops/test_1b_loop_print.py b/test_codes_python/Loops/test_1b_loop_print.py new file mode 100644 index 0000000..2c8f2dd --- /dev/null +++ b/test_codes_python/Loops/test_1b_loop_print.py @@ -0,0 +1,10 @@ +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_1b_loop_print.lio. + +Same program as Loops/test_1b_loop.py, but printing the counter each iteration. +This one is dominated by output, and is kept because the difference between +the two isolates loop cost from print cost. +""" +i = 0 +while i < 1000000000: + i = i + 1 + print(i) diff --git a/test_codes_python/test_1k_fsum.py b/test_codes_python/Loops/test_1k_fsum.py similarity index 70% rename from test_codes_python/test_1k_fsum.py rename to test_codes_python/Loops/test_1k_fsum.py index e8a5a0a..9222a2f 100644 --- a/test_codes_python/test_1k_fsum.py +++ b/test_codes_python/Loops/test_1k_fsum.py @@ -1,4 +1,4 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_1k_fsum.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_1k_fsum.lio. Same program over 1,000 iterations -- tiny workload, used for checking the benchmark UI and video recorder rather than for real speedup numbers. @@ -7,4 +7,4 @@ j = 0 for i in range(1000): j += i -print(j) \ No newline at end of file +print(j) diff --git a/test_codes_python/test_eulars_constant_lim_def.py b/test_codes_python/Loops/test_eulars_constant_lim_def.py similarity index 86% rename from test_codes_python/test_eulars_constant_lim_def.py rename to test_codes_python/Loops/test_eulars_constant_lim_def.py index aa258a5..ee2a331 100644 --- a/test_codes_python/test_eulars_constant_lim_def.py +++ b/test_codes_python/Loops/test_eulars_constant_lim_def.py @@ -1,5 +1,5 @@ """Plain-CPython equivalent of -test_codes_lithon/test_eulars_constant_lim_def.py. +test_codes_lithon/Loops/test_eulars_constant_lim_def.lio. Same program: approximate e via the limit definition (1 + 1/k)**k, built up as an explicit nested product since Lithon has no `**` operator, keeping the @@ -20,4 +20,4 @@ if e > le: le = e -print(le) \ No newline at end of file +print(le) diff --git a/test_codes_python/test_float.py b/test_codes_python/Loops/test_float.py similarity index 76% rename from test_codes_python/test_float.py rename to test_codes_python/Loops/test_float.py index c5dfe85..022c560 100644 --- a/test_codes_python/test_float.py +++ b/test_codes_python/Loops/test_float.py @@ -1,4 +1,4 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_float.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_float.lio. Same program: a zero-argument function returning float[64] that adds 1.0 to a local, called three times. CPython's float is already binary64, so only @@ -14,4 +14,4 @@ def f(): print(f()) print(f()) -print(f()) \ No newline at end of file +print(f()) diff --git a/test_codes_python/test_multi_ops.py b/test_codes_python/Loops/test_multi_ops.py similarity index 72% rename from test_codes_python/test_multi_ops.py rename to test_codes_python/Loops/test_multi_ops.py index 54f34cf..b889875 100644 --- a/test_codes_python/test_multi_ops.py +++ b/test_codes_python/Loops/test_multi_ops.py @@ -1,4 +1,4 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_multi_ops.py. +"""Plain-CPython equivalent of test_codes_lithon/Loops/test_multi_ops.lio. Same program: 1e9 iterations of mixed integer arithmetic -- a multiply, a bitwise xor, and a right shift -- accumulated into a total. @@ -11,4 +11,4 @@ total += (i * 31) ^ (i >> 3) i += 1 -print(total) \ No newline at end of file +print(total) diff --git a/test_codes_python/NewTypes/new_type.py b/test_codes_python/NewTypes/new_type.py new file mode 100644 index 0000000..1a800d1 --- /dev/null +++ b/test_codes_python/NewTypes/new_type.py @@ -0,0 +1,19 @@ +"""Plain-CPython equivalent of test_codes_lithon/NewTypes/new_type.lio. + +Same program: a type function that takes a list of widths and produces a +concrete container type, then a value of that type is declared and printed. + +Lithon's type functions are compile-time type constructors -- they expand +structurally into the underlying type. CPython has no such construct, so the +portable subset keeps the *shape* of the program: the same field names, the +same nested list/tuple structure, the same printed result. +""" +def custom(a, b, c, d, e): + # Lithon: dict[str[e], list[tuple[int[d], a], b], c] + # The widths are values here, not types, so the same nested shape is + # built explicitly. + return {"test": [(1, 2, 3, 4), (4, 3, 2, 1)]} + + +t = custom(4, 2, 1, 32, 16) +print(t) diff --git a/test_codes_python/Pointers/check_pointers.py b/test_codes_python/Pointers/check_pointers.py new file mode 100644 index 0000000..1fd73c2 --- /dev/null +++ b/test_codes_python/Pointers/check_pointers.py @@ -0,0 +1,26 @@ +"""Plain-CPython equivalent of test_codes_lithon/Pointers/check_pointers.lio. + +Same program: take the address of a variable, read the value back, walk the +pointer by a fixed offset, alias it through a second pointer, and watch the +original change through the alias. + +CPython has no raw addresses, so the portable subset models the *shape* of +the program: a one-element cell stands in for the pointed-to storage, and +reads/writes go through that cell. The two addresses the Lithon program +prints are host-dependent and have no CPython equivalent, so they are +dropped; every deterministic value matches. +""" +i = 16 + +# Lithon: _ptrI: ptr[int[16]] = addressof(i) +# A one-element cell stands in for the pointee's storage. +_ptrI = [i] + +print(_ptrI[0]) # 16 -- valueof(_ptrI) +print(0) # 0 -- valueof(_ptrI-100): memory before the cell +_ptrK = _ptrI # a second pointer to the same cell +t = _ptrI[0] # t: int[16] = i (a copy of the value) +i = 64 +_ptrI[0] = i # the alias sees the change: it is the same cell +print(t) # 16 -- the copy did not change +print(_ptrK[0]) # 64 -- valueof(_ptrK) diff --git a/test_codes_python/Pointers/pointer_error.py b/test_codes_python/Pointers/pointer_error.py new file mode 100644 index 0000000..828ceb7 --- /dev/null +++ b/test_codes_python/Pointers/pointer_error.py @@ -0,0 +1,16 @@ +"""Plain-CPython equivalent of test_codes_lithon/Pointers/pointer_error.lio. + +NOTE: in Lithon this file is a *pinned failure*. The engine refuses it on +purpose, so the naming rule stays visible: + + $ lithon test_codes_lithon/Pointers/pointer_error.lio + LITHON-E0122: a pointer is named with a leading underscore, + write `_ptrI: ptr[...]` or pick a name that starts with '_' (4.4) + +The corrected spelling is in Pointers/check_pointers.lio. CPython has no +pointer naming rule, so this file runs the corrected shape here to keep the +pair side by side. +""" +i = 22 +_ptrI = [i] +print(_ptrI[0]) diff --git a/test_codes_python/Structs/structs.py b/test_codes_python/Structs/structs.py new file mode 100644 index 0000000..b3eff37 --- /dev/null +++ b/test_codes_python/Structs/structs.py @@ -0,0 +1,22 @@ +"""Plain-CPython equivalent of test_codes_lithon/Structs/structs.lio. + +Same program: a struct with a bounded string field and an int field, then a +value of that struct with the fields assigned. + +CPython has no width annotations, so the field types are dropped and the +class body just declares the names. Lithon packs the fields with no +padding; the field values are the same either way. +""" + + +class LithonStruct: + name: str + year: int + + +Lithon = LithonStruct() +Lithon.name = "Lithon" +Lithon.year = 2026 + +print(Lithon.name) +print(Lithon.year) diff --git a/test_codes_python/__pycache__/base_var.cpython-312.pyc b/test_codes_python/__pycache__/base_var.cpython-312.pyc deleted file mode 100644 index c16a55c..0000000 Binary files a/test_codes_python/__pycache__/base_var.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/find_e.cpython-312.pyc b/test_codes_python/__pycache__/find_e.cpython-312.pyc deleted file mode 100644 index b08ba70..0000000 Binary files a/test_codes_python/__pycache__/find_e.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_1b_fsum.cpython-312.pyc b/test_codes_python/__pycache__/test_1b_fsum.cpython-312.pyc deleted file mode 100644 index 031d878..0000000 Binary files a/test_codes_python/__pycache__/test_1b_fsum.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_1b_fsum_simd.cpython-312.pyc b/test_codes_python/__pycache__/test_1b_fsum_simd.cpython-312.pyc deleted file mode 100644 index ccef8b6..0000000 Binary files a/test_codes_python/__pycache__/test_1b_fsum_simd.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_1b_loop.cpython-312.pyc b/test_codes_python/__pycache__/test_1b_loop.cpython-312.pyc deleted file mode 100644 index 9ee0cb7..0000000 Binary files a/test_codes_python/__pycache__/test_1b_loop.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_1b_loop_print.cpython-312.pyc b/test_codes_python/__pycache__/test_1b_loop_print.cpython-312.pyc deleted file mode 100644 index 774588d..0000000 Binary files a/test_codes_python/__pycache__/test_1b_loop_print.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_1k_fsum.cpython-312.pyc b/test_codes_python/__pycache__/test_1k_fsum.cpython-312.pyc deleted file mode 100644 index fffaf00..0000000 Binary files a/test_codes_python/__pycache__/test_1k_fsum.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_eulars_constant_lim_def.cpython-312.pyc b/test_codes_python/__pycache__/test_eulars_constant_lim_def.cpython-312.pyc deleted file mode 100644 index 489f9e6..0000000 Binary files a/test_codes_python/__pycache__/test_eulars_constant_lim_def.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_float.cpython-312.pyc b/test_codes_python/__pycache__/test_float.cpython-312.pyc deleted file mode 100644 index d9bd646..0000000 Binary files a/test_codes_python/__pycache__/test_float.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/__pycache__/test_multi_ops.cpython-312.pyc b/test_codes_python/__pycache__/test_multi_ops.cpython-312.pyc deleted file mode 100644 index 48d7f41..0000000 Binary files a/test_codes_python/__pycache__/test_multi_ops.cpython-312.pyc and /dev/null differ diff --git a/test_codes_python/test_1b_loop_print.py b/test_codes_python/test_1b_loop_print.py deleted file mode 100644 index 4033028..0000000 --- a/test_codes_python/test_1b_loop_print.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Plain-CPython equivalent of test_codes_lithon/test_1b_loop_print.py. - -Same program as test_1b_loop.py, but printing the counter each iteration. -This one is dominated by output, and is kept because the difference between -the two isolates loop cost from print cost. -""" -i = 0 -while i < 1000000000: - i = i + 1 - print(i) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 9418af2..626420c 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.10" [[package]] name = "lithon" -version = "0.8.0" +version = "0.9.8" source = { editable = "." } diff --git a/web/apps/web/public/benchmarks/manifest.json b/web/apps/web/public/benchmarks/manifest.json index 7ee0905..1560eb1 100644 --- a/web/apps/web/public/benchmarks/manifest.json +++ b/web/apps/web/public/benchmarks/manifest.json @@ -13,9 +13,9 @@ "stdout": "2.7169239322355985\n", "returncode": 0, "command": [ - "test_codes_python/test_eulars_constant_lim_def.py" + "test_codes_python/Loops/test_eulars_constant_lim_def.py" ], - "script": "test_codes_python/test_eulars_constant_lim_def.py" + "script": "test_codes_python/Loops/test_eulars_constant_lim_def.py" }, { "name": "Lithon", @@ -23,9 +23,9 @@ "stdout": "2.7169239322355985\n", "returncode": 0, "command": [ - "test_codes_lithon/test_eulars_constant_lim_def.py" + "test_codes_lithon/Loops/test_eulars_constant_lim_def.lio" ], - "script": "test_codes_lithon/test_eulars_constant_lim_def.py" + "script": "test_codes_lithon/Loops/test_eulars_constant_lim_def.lio" } ] }, @@ -43,9 +43,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_python/test_1b_fsum.py" + "test_codes_python/Loops/test_1b_fsum.py" ], - "script": "test_codes_python/test_1b_fsum.py" + "script": "test_codes_python/Loops/test_1b_fsum.py" }, { "name": "Lithon", @@ -53,9 +53,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1b_fsum.py" + "test_codes_lithon/Loops/test_1b_fsum.lio" ], - "script": "test_codes_lithon/test_1b_fsum.py" + "script": "test_codes_lithon/Loops/test_1b_fsum.lio" } ] }, @@ -73,9 +73,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_python/test_1b_fsum.py" + "test_codes_python/Loops/test_1b_fsum.py" ], - "script": "test_codes_python/test_1b_fsum.py" + "script": "test_codes_python/Loops/test_1b_fsum.py" }, { "name": "Lithon", @@ -83,9 +83,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1b_fsum.py" + "test_codes_lithon/Loops/test_1b_fsum.lio" ], - "script": "test_codes_lithon/test_1b_fsum.py" + "script": "test_codes_lithon/Loops/test_1b_fsum.lio" } ] }, @@ -103,9 +103,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_python/test_1b_fsum.py" + "test_codes_python/Loops/test_1b_fsum.py" ], - "script": "test_codes_python/test_1b_fsum.py" + "script": "test_codes_python/Loops/test_1b_fsum.py" }, { "name": "Lithon", @@ -113,9 +113,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1b_fsum.py" + "test_codes_lithon/Loops/test_1b_fsum.lio" ], - "script": "test_codes_lithon/test_1b_fsum.py" + "script": "test_codes_lithon/Loops/test_1b_fsum.lio" } ] }, @@ -133,9 +133,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_python/test_1b_fsum.py" + "test_codes_python/Loops/test_1b_fsum.py" ], - "script": "test_codes_python/test_1b_fsum.py" + "script": "test_codes_python/Loops/test_1b_fsum.py" }, { "name": "Lithon", @@ -143,9 +143,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1b_fsum.py" + "test_codes_lithon/Loops/test_1b_fsum.lio" ], - "script": "test_codes_lithon/test_1b_fsum.py" + "script": "test_codes_lithon/Loops/test_1b_fsum.lio" } ] }, @@ -163,9 +163,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_python/test_1b_fsum.py" + "test_codes_python/Loops/test_1b_fsum.py" ], - "script": "test_codes_python/test_1b_fsum.py" + "script": "test_codes_python/Loops/test_1b_fsum.py" }, { "name": "Lithon", @@ -173,9 +173,9 @@ "stdout": "499999999500000000\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1b_fsum.py" + "test_codes_lithon/Loops/test_1b_fsum.lio" ], - "script": "test_codes_lithon/test_1b_fsum.py" + "script": "test_codes_lithon/Loops/test_1b_fsum.lio" } ] }, @@ -193,9 +193,9 @@ "stdout": "499500\n", "returncode": 0, "command": [ - "test_codes_python/test_1k_fsum.py" + "test_codes_python/Loops/test_1k_fsum.py" ], - "script": "test_codes_python/test_1k_fsum.py" + "script": "test_codes_python/Loops/test_1k_fsum.py" }, { "name": "Lithon", @@ -203,9 +203,9 @@ "stdout": "499500\n", "returncode": 0, "command": [ - "test_codes_lithon/test_1k_fsum.py" + "test_codes_lithon/Loops/test_1k_fsum.lio" ], - "script": "test_codes_lithon/test_1k_fsum.py" + "script": "test_codes_lithon/Loops/test_1k_fsum.lio" } ] } diff --git a/web/apps/web/public/manifest.json b/web/apps/web/public/manifest.json index 078ef50..50971e3 100644 --- a/web/apps/web/public/manifest.json +++ b/web/apps/web/public/manifest.json @@ -1,25 +1,24 @@ { - "short_name": "TanStack App", - "name": "Create TanStack App Sample", + "name": "Lithon", + "short_name": "Lithon", + "description": "A zero-dependency, dual-tier execution engine that runs statically typed Python as native x86-64.", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" + "src": "favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" }, { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" + "src": "favicon.ico", + "sizes": "64x64 32x32 24x32 16x16", + "type": "image/x-icon" } ], "start_url": ".", + "scope": ".", "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" + "orientation": "any", + "theme_color": "#121212", + "background_color": "#121212" } diff --git a/web/apps/web/public/robots.txt b/web/apps/web/public/robots.txt index 2ce9f09..da07271 100644 --- a/web/apps/web/public/robots.txt +++ b/web/apps/web/public/robots.txt @@ -2,4 +2,4 @@ User-agent: * Allow: / Disallow: /api/ -Sitemap: https://lithon-lang.pages.dev/sitemap.xml +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 07fbe25..3e65939 100644 --- a/web/apps/web/public/sitemap.xml +++ b/web/apps/web/public/sitemap.xml @@ -1,182 +1,202 @@ - https://lithon-lang.pages.dev/ + https://project-lithon.github.io/Lithon/ weekly 1.0 - https://lithon-lang.pages.dev/docs + https://project-lithon.github.io/Lithon/docs weekly 0.9 - https://lithon-lang.pages.dev/playground + https://project-lithon.github.io/Lithon/playground weekly 0.7 - https://lithon-lang.pages.dev/benchmarks + https://project-lithon.github.io/Lithon/benchmarks weekly 0.7 - https://lithon-lang.pages.dev/roadmap + https://project-lithon.github.io/Lithon/roadmap weekly 0.6 - https://lithon-lang.pages.dev/use-cases + https://project-lithon.github.io/Lithon/use-cases weekly 0.6 - https://lithon-lang.pages.dev/contributors + https://project-lithon.github.io/Lithon/roadmap/language weekly 0.6 - https://lithon-lang.pages.dev/roadmap/language + https://project-lithon.github.io/Lithon/roadmap/phase-1-dual-tier weekly 0.6 - https://lithon-lang.pages.dev/roadmap/phase-1-dual-tier + https://project-lithon.github.io/Lithon/roadmap/phase-2-aot weekly 0.6 - https://lithon-lang.pages.dev/roadmap/phase-2-aot + https://project-lithon.github.io/Lithon/roadmap/phase-3-systems weekly 0.6 - https://lithon-lang.pages.dev/roadmap/phase-3-systems + https://project-lithon.github.io/Lithon/roadmap/verification weekly 0.6 - https://lithon-lang.pages.dev/roadmap/verification + https://project-lithon.github.io/Lithon/docs/welcome weekly - 0.6 + 0.8 + + + https://project-lithon.github.io/Lithon/docs/hello-world + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/installation + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/mental-model + weekly + 0.8 + + + https://project-lithon.github.io/Lithon/docs/types + weekly + 0.8 - https://lithon-lang.pages.dev/docs/welcome + https://project-lithon.github.io/Lithon/docs/declarations weekly 0.8 - https://lithon-lang.pages.dev/docs/hello-world + https://project-lithon.github.io/Lithon/docs/control-flow weekly 0.8 - https://lithon-lang.pages.dev/docs/installation + https://project-lithon.github.io/Lithon/docs/functions weekly 0.8 - https://lithon-lang.pages.dev/docs/mental-model + https://project-lithon.github.io/Lithon/docs/operators weekly 0.8 - https://lithon-lang.pages.dev/docs/types + https://project-lithon.github.io/Lithon/docs/containers weekly 0.8 - https://lithon-lang.pages.dev/docs/declarations + https://project-lithon.github.io/Lithon/docs/pointers weekly 0.8 - https://lithon-lang.pages.dev/docs/control-flow + https://project-lithon.github.io/Lithon/docs/structs weekly 0.8 - https://lithon-lang.pages.dev/docs/functions + https://project-lithon.github.io/Lithon/docs/enums weekly 0.8 - https://lithon-lang.pages.dev/docs/operators + https://project-lithon.github.io/Lithon/docs/methods weekly 0.8 - https://lithon-lang.pages.dev/docs/containers + https://project-lithon.github.io/Lithon/docs/type-functions weekly 0.8 - https://lithon-lang.pages.dev/docs/pointers + https://project-lithon.github.io/Lithon/docs/syscalls weekly 0.8 - https://lithon-lang.pages.dev/docs/architecture + https://project-lithon.github.io/Lithon/docs/architecture weekly 0.8 - https://lithon-lang.pages.dev/docs/dual-tier + https://project-lithon.github.io/Lithon/docs/dual-tier weekly 0.8 - https://lithon-lang.pages.dev/docs/typechecking + https://project-lithon.github.io/Lithon/docs/typechecking weekly 0.8 - https://lithon-lang.pages.dev/docs/pipeline + https://project-lithon.github.io/Lithon/docs/pipeline weekly 0.8 - https://lithon-lang.pages.dev/docs/simd + https://project-lithon.github.io/Lithon/docs/simd weekly 0.8 - https://lithon-lang.pages.dev/docs/errors + https://project-lithon.github.io/Lithon/docs/errors weekly 0.8 - https://lithon-lang.pages.dev/docs/builtins + https://project-lithon.github.io/Lithon/docs/builtins weekly 0.8 - https://lithon-lang.pages.dev/docs/semantics + https://project-lithon.github.io/Lithon/docs/semantics weekly 0.8 - https://lithon-lang.pages.dev/docs/verification + https://project-lithon.github.io/Lithon/docs/verification weekly 0.8 - https://lithon-lang.pages.dev/docs/limits + https://project-lithon.github.io/Lithon/docs/limits weekly 0.8 - https://lithon-lang.pages.dev/docs/source-to-machine + https://project-lithon.github.io/Lithon/docs/source-to-machine weekly 0.8 - https://lithon-lang.pages.dev/docs/registers-abi + https://project-lithon.github.io/Lithon/docs/registers-abi weekly 0.8 - https://lithon-lang.pages.dev/docs/instruction-encoding + https://project-lithon.github.io/Lithon/docs/instruction-encoding weekly 0.8 diff --git a/web/apps/web/src/docs/generated/ir.ts b/web/apps/web/src/docs/generated/ir.ts index 849161e..5e3052f 100644 --- a/web/apps/web/src/docs/generated/ir.ts +++ b/web/apps/web/src/docs/generated/ir.ts @@ -2,66 +2,52 @@ // Every entry is real output from src/frontend/frontend.py. export const EXAMPLE_IR: Record = { - "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, %5\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, %5\r\n %6 = load x\r\n %7 = load y\r\n %8 = sub %6, %7\r\n call print, %8\r\n %9 = load x\r\n %10 = load y\r\n %11 = mul %9, %10\r\n call print, %11\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, %1\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, %6\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, %2\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, %2\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, %1\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, %1\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, %4\r\n %5 = load f\r\n %6 = load i\r\n %7 = add %5, %6\r\n call print, %7\r\n %8 = load i\r\n %9 = const_i64 4\r\n %10 = div %8, %9\r\n call print, %10\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, %2\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, %3\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, %1\r\n return", - bba9661c: - "function __main__():\r\nblock0:\r\n %0 = load i\r\n call print, %0\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, %5\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, %12\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, %7\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, %18\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 return\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, %3\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, %4\r\n %5 = const_i64 7\r\n %6 = const_i64 3\r\n %7 = mod %5, %6\r\n call print, %7\r\n %8 = const_i64 -7\r\n %9 = const_i64 3\r\n %10 = mod %8, %9\r\n call print, %10\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, %5\r\n %6 = load b\r\n %7 = load a\r\n %8 = gt %6, %7\r\n call print, %8\r\n %9 = load a\r\n %10 = load a\r\n %11 = eq %9, %10\r\n call print, %11\r\n %12 = load c\r\n %13 = load b\r\n %14 = lt %12, %13\r\n call print, %14\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, %2\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, %2\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, %9\r\n %10 = load y\r\n call print, %10\r\n %11 = load z\r\n call print, %11\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, %5\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, %11\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, %17\r\n return", - fe363d56: - "function __main__():\r\nblock0:\r\n store xs : list[int[64], 6]\r\n %0 = const_i64 0\r\n %1 = const_i64 10\r\n IndexStore xs, %0, %1\r\n %2 = const_i64 1\r\n %3 = const_i64 20\r\n IndexStore xs, %2, %3\r\n %4 = const_i64 2\r\n %5 = const_i64 30\r\n IndexStore xs, %4, %5\r\n %6 = const_i64 0\r\n %7 = Index xs, %6\r\n call print, %7\r\n %8 = const_i64 1\r\n %9 = Index xs, %8\r\n call print, %9\r\n %10 = const_i64 2\r\n %11 = Index xs, %10\r\n call print, %11\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 %8 = const_i64 0\r\n %9 = Index t, %8\r\n call print, %9\r\n %10 = const_i64 3\r\n %11 = Index t, %10\r\n call print, %11\r\n return", - f0834cb4: - "function __main__():\r\nblock0:\r\n store d : dict[int[64], int[64], 4]\r\n %0 = const_i64 1\r\n %1 = const_i64 10\r\n DictStore d, %0, %1\r\n %2 = const_i64 2\r\n %3 = const_i64 20\r\n DictStore d, %2, %3\r\n %4 = const_i64 3\r\n %5 = const_i64 30\r\n DictStore d, %4, %5\r\n %6 = const_i64 4\r\n %7 = const_i64 40\r\n DictStore d, %6, %7\r\n %8 = const_i64 1\r\n %9 = DictIndex d, %8\r\n call print, %9\r\n %10 = const_i64 4\r\n %11 = DictIndex d, %10\r\n call print, %11\r\n %12 = const_i64 2\r\n store k, %12 : int[64]\r\n %13 = load k\r\n %14 = DictIndex d, %13\r\n call print, %14\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 %19 = load j\r\n %20 = DictIndex d, %19\r\n call print, %20\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, %3\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, %7\r\n %8 = load _p\r\n %9 = load _q\r\n %10 = eq %8, %9\r\n call print, %10\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, %3\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, %6\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, %5\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, %2\r\n %3 = const_i64 7\r\n %4 = const_i64 -3\r\n %5 = mod %3, %4\r\n call print, %5\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, %2\r\n return", + "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" } diff --git a/web/apps/web/src/docs/pages/engine.ts b/web/apps/web/src/docs/pages/engine.ts index b7bc71e..7e095f5 100644 --- a/web/apps/web/src/docs/pages/engine.ts +++ b/web/apps/web/src/docs/pages/engine.ts @@ -4,7 +4,7 @@ const architecture: DocPage = { slug: "architecture", title: "Engine architecture", description: - "Four layers from .py source to executable memory: frontend, IR, verifier, encoder.", + "Four layers from .lio source to executable memory: frontend, IR, verifier, encoder.", group: "engine", tags: [ "architecture", @@ -29,7 +29,7 @@ const architecture: DocPage = { items: [ { title: "1 Β· Frontend", - text: "`src/frontend/frontend.py`: parses `.py`, runs the extended PEP-526 checks, emits text IR. Pure Python, no engine needed.", + text: "`src/frontend/frontend.py`: parses `.lio`, runs the extended PEP-526 checks, emits text IR. Pure Python, no engine needed.", }, { title: "2 Β· IR text format", @@ -64,8 +64,8 @@ const architecture: DocPage = { { kind: "code", example: { - title: "tests/programs/arithmetic.py", - sourceRef: "tests/programs/arithmetic.py", + title: "tests/programs/arithmetic.lio", + sourceRef: "tests/programs/arithmetic.lio", expectedRef: "tests/programs/expected/arithmetic.out", source: `x: int[64] = 10 y: int[64] = 20 @@ -86,15 +86,18 @@ block0: %4 = add %2, %3 store z, %4 : int[64] %5 = load z - call print, %5 + call print_bare, %5 + call print_nl %6 = load x %7 = load y %8 = sub %6, %7 - call print, %8 + call print_bare, %8 + call print_nl %9 = load x %10 = load y %11 = mul %9, %10 - call print, %11 + call print_bare, %11 + call print_nl return`, asm: `main: push rbp @@ -159,6 +162,37 @@ block0: }, ], }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Source to machine", + href: "/docs/source-to-machine", + text: "The four artifacts one program produces, end to end.", + }, + { + title: "Registers & ABI", + href: "/docs/registers-abi", + text: "The register file and the calling discipline.", + }, + { + title: "Instruction encoding", + href: "/docs/instruction-encoding", + text: "Why the encoder is hand-rolled, and how it is proven.", + }, + { + title: "Verification", + href: "/docs/verification", + text: "How the claims on this page get checked.", + }, + ], + }, + ], + }, ], } @@ -192,7 +226,7 @@ const dualTier: DocPage = { example: { lang: "bash", title: "forcing the native lane", - source: `$ lithon tests/programs/float.py --strict + source: `$ lithon tests/programs/float.lio --strict [tier1] native 0.3333333333333333`, note: "`[tier1] native` goes to stderr; program output goes to stdout. Under `--strict`, exit 0 means the native tier really ran.", @@ -227,7 +261,7 @@ const dualTier: DocPage = { example: { lang: "bash", title: "terminal", - source: `$ lithon program.py --strict # native only, refuse to fall back + source: `$ lithon program.lio --strict # native only, refuse to fall back $ ./build/tier_runner prog.ir --strict # same thing, two-step form`, }, }, @@ -294,7 +328,7 @@ const typechecking: DocPage = { example: { lang: "bash", title: "terminal", - source: `$ python3 tools/typecheck.py program.py + source: `$ python3 tools/typecheck.py program.lio $ echo $? 0`, }, diff --git a/web/apps/web/src/docs/pages/language.ts b/web/apps/web/src/docs/pages/language.ts index a7f4c4b..36fd65a 100644 --- a/web/apps/web/src/docs/pages/language.ts +++ b/web/apps/web/src/docs/pages/language.ts @@ -175,11 +175,13 @@ block0: %2 = toint %1 : int[64] store n, %2 : int[64] %3 = load n - call print, %3 + call print_bare, %3 + call print_nl %4 = inputint 656e7465722061206e756d6265723a20 : int[32] store w, %4 : int[32] %5 = load w - call print, %5 + call print_bare, %5 + call print_nl return`, note: "`int()`/`float()` lower to `toint`/`tofloat`; `int(w, input(prompt))` lowers to one `inputint` op, the prompt hex-encoded so any character survives the text IR.", }, @@ -204,8 +206,8 @@ block0: { kind: "code", example: { - title: "tests/typed_regression/mixed_numeric.py", - sourceRef: "tests/typed_regression/mixed_numeric.py", + title: "tests/typed_regression/mixed_numeric.lio", + sourceRef: "tests/typed_regression/mixed_numeric.lio", expectedRef: "tests/typed_regression/expected/mixed_numeric.out", source: `i: int[64] = 10 f: float[64] = 2.5 @@ -297,8 +299,8 @@ print(narrow)`, { kind: "code", example: { - title: "tests/typed_regression/uninit_decl.py", - sourceRef: "tests/typed_regression/uninit_decl.py", + title: "tests/typed_regression/uninit_decl.lio", + sourceRef: "tests/typed_regression/uninit_decl.lio", expectedRef: "tests/typed_regression/expected/uninit_decl.out", source: `i: int[8] i = 5 @@ -361,7 +363,7 @@ const controlFlow: DocPage = { slug: "control-flow", title: "Control flow", description: - "if / elif / else, while, for over range, and the one construct that does not exist.", + "if / elif / else, while, for over range, try / except over the built-in traps, and the one construct that does not exist.", group: "language", tags: [ "control flow", @@ -372,6 +374,10 @@ const controlFlow: DocPage = { "range", "branch", "loop", + "try", + "except", + "exception", + "trap", ], sections: [ { @@ -385,8 +391,8 @@ const controlFlow: DocPage = { { kind: "code", example: { - title: "tests/typed_regression/if.py", - sourceRef: "tests/typed_regression/if.py", + title: "tests/typed_regression/if.lio", + sourceRef: "tests/typed_regression/if.lio", expectedRef: "tests/typed_regression/expected/if.out", source: `x: int[64] = 5 if x < 3: @@ -405,11 +411,13 @@ block0: branch %3, block1, block2 block1: %4 = const_i64 1 - call print, %4 + call print_bare, %4 + call print_nl jump block3 block2: %5 = const_i64 3 - call print, %5 + call print_bare, %5 + call print_nl jump block3 block3: return`, @@ -430,8 +438,8 @@ block3: { kind: "code", example: { - title: "tests/typed_regression/while.py", - sourceRef: "tests/typed_regression/while.py", + title: "tests/typed_regression/while.lio", + sourceRef: "tests/typed_regression/while.lio", expectedRef: "tests/typed_regression/expected/while.out", typecheck: false, source: `i: int[64] = 0 @@ -447,7 +455,7 @@ print(total)`, kind: "note", tone: "warn", title: "The checker does not cover `while` yet", - text: "`tools/typecheck.py` still answers `statement While not supported by the type-checker yet`: including for the repo's own `while.py`. 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.", + text: "`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.", }, ], }, @@ -462,8 +470,8 @@ print(total)`, { kind: "code", example: { - title: "tests/typed_regression/for_range.py", - sourceRef: "tests/typed_regression/for_range.py", + title: "tests/typed_regression/for_range.lio", + sourceRef: "tests/typed_regression/for_range.lio", expectedRef: "tests/typed_regression/expected/for_range.out", source: `total: int[64] = 0 i: int[64] = 0 @@ -501,7 +509,8 @@ block3: jump block4 block2: %13 = load total - call print, %13 + call print_bare, %13 + call print_nl return`, note: "The `for` lowers to an init / test / body / increment block structure: a pre-test loop with an explicit back edge, exactly what the encoder emits a `jcc` for.", }, @@ -520,9 +529,9 @@ block2: { kind: "code", example: { - title: "tests/programs/range_step.py", + title: "tests/programs/range_step.lio", typecheck: false, - sourceRef: "tests/programs/range_step.py", + sourceRef: "tests/programs/range_step.lio", expectedRef: "tests/programs/expected/range_step.out", source: `for i in range(1, 10, 3): print(i) @@ -560,9 +569,9 @@ for i in range(-1, -5, -2): { kind: "code", example: { - title: "tests/programs/break_continue.py", + title: "tests/programs/break_continue.lio", typecheck: false, - sourceRef: "tests/programs/break_continue.py", + sourceRef: "tests/programs/break_continue.lio", expectedRef: "tests/programs/expected/break_continue.out", source: `total: int[64] = 0 for i in range(100): @@ -592,9 +601,9 @@ print(total)`, { kind: "code", example: { - title: "tests/typed_regression/iter_helpers.py (excerpt)", + title: "tests/typed_regression/iter_helpers.lio (excerpt)", typecheck: false, - sourceRef: "tests/typed_regression/iter_helpers.py", + sourceRef: "tests/typed_regression/iter_helpers.lio", expectedRef: "tests/typed_regression/expected/iter_helpers.out", source: `xs: list[int[64], 5] xs[0] = 10 @@ -642,8 +651,8 @@ print(total)`, { kind: "code", example: { - title: "tests/typed_regression/if_expr.py (excerpt)", - sourceRef: "tests/typed_regression/if_expr.py", + title: "tests/typed_regression/if_expr.lio (excerpt)", + sourceRef: "tests/typed_regression/if_expr.lio", expectedRef: "tests/typed_regression/expected/if_expr.out", typecheck: false, source: `x: int[64] = 5 @@ -692,6 +701,125 @@ else: }, ], }, + { + id: "try-except", + title: "try / except, over the built-in traps", + blocks: [ + { + kind: "p", + text: "`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.", + }, + { + kind: "code", + example: { + title: "catching a division by zero", + // tools/typecheck.py is a mirror of the older scalar-only static + // checks and does not know `try` yet; the real checker (the C++ + // one, which runs inside tier_runner) does. + typecheck: false, + source: `n: int[64] = 0 +try: + q: int[64] = 100 % n + print(q) +except ZeroDivisionError: + print(1)`, + output: "1", + machine: { + ir: `function __main__(): +block0: + %0 = const_i64 0 + store n, %0 : int[64] + tryenter ZeroDivisionError, block1 + jump block2 +block2: + %1 = const_i64 100 + %2 = load n + %3 = mod %1, %2 + store q, %3 : int[64] + %4 = load q + call print_bare, %4 + call print_nl + tryleave + jump block3 +block1: + trycatch ZeroDivisionError + %5 = const_i64 1 + call print_bare, %5 + call print_nl + jump block3 +block3: + return`, + }, + }, + }, + { + kind: "p", + text: "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.", + }, + { + kind: "p", + text: "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.", + }, + { + kind: "code", + example: { + title: "rejected: not a catchable trap", + expect: "error", + tool: "frontend", + errorMatch: "is not a catchable trap", + source: `try: + print(1) +except RuntimeError: + print(2)`, + output: + "LITHON-E0114: `except RuntimeError:` is not a catchable trap -- the catchable set is EOFError, IndexError, KeyError, OverflowError, ValueError, ZeroDivisionError, plus `Exception` for all of them (6.10)", + }, + }, + { + kind: "table", + caption: "which trap lands in which handler", + rows: [ + [ + "`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`)"], + ], + }, + { + kind: "note", + tone: "warn", + title: "three things that are deliberately not supported", + text: "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.", + }, + { + kind: "code", + example: { + title: "rejected: a bare except", + expect: "error", + tool: "frontend", + errorMatch: "a bare `except:` is not supported", + source: `try: + print(1) +except: + print(2)`, + 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)", + }, + }, + { + kind: "p", + text: "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.", + }, + ], + }, ], } @@ -721,8 +849,8 @@ const functions: DocPage = { { kind: "code", example: { - title: "tests/typed_regression/function.py", - sourceRef: "tests/typed_regression/function.py", + title: "tests/typed_regression/function.lio", + sourceRef: "tests/typed_regression/function.lio", expectedRef: "tests/typed_regression/expected/function.out", source: `def add(a: int[64], b: int[64]) -> int[64]: return a + b @@ -745,7 +873,8 @@ block0: %2 = call add, %0, %1 store x, %2 : int[64] %3 = load x - call print, %3 + call print_bare, %3 + call print_nl return`, note: "Real frontend output. Parameters live in the frame like any other slot; the call passes constants as immediates and stores the return value into the caller's `x` slot.", }, @@ -779,8 +908,8 @@ print(x)`, { kind: "code", example: { - title: "tests/typed_regression/recursion.py", - sourceRef: "tests/typed_regression/recursion.py", + title: "tests/typed_regression/recursion.lio", + sourceRef: "tests/typed_regression/recursion.lio", expectedRef: "tests/typed_regression/expected/recursion.out", source: `def fib(n: int[64]) -> int[64]: if n < 2: @@ -841,7 +970,8 @@ block0: %1 = call fib, %0 store x, %1 : int[64] %2 = load x - call print, %2 + call print_bare, %2 + call print_nl return`, note: "Real frontend output. The base case and the recursive case are separate blocks; `call fib` appears twice, once per recursive branch.", }, @@ -929,13 +1059,13 @@ print(-7 % 3)`, blocks: [ { kind: "p", - text: "`==` `<` `>` and the other comparisons produce `bool`. `and` `or` `not` operate on `bool`. Note what is *not* here yet: `!=` (`NotEq`) is the one comparison the frontend has not lowered: write `not (a == b)` in the meantime.", + text: "`==` `!=` `<` `>` 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.", }, { kind: "code", example: { - title: "tests/typed_regression/comparison.py", - sourceRef: "tests/typed_regression/comparison.py", + title: "tests/typed_regression/comparison.lio", + sourceRef: "tests/typed_regression/comparison.lio", expectedRef: "tests/typed_regression/expected/comparison.out", source: `a: int[64] = 1 b: int[64] = 2 @@ -947,6 +1077,23 @@ print(c < b)`, output: "True\nTrue\nTrue\nTrue", }, }, + { + kind: "code", + example: { + title: "tests/typed_regression/ne.lio (excerpt)", + sourceRef: "tests/typed_regression/ne.lio", + expectedRef: "tests/typed_regression/expected/ne.out", + typecheck: false, + source: `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\nFalse\nTrue", + }, + }, { kind: "note", tone: "note", @@ -1015,8 +1162,8 @@ print(z)`, { kind: "code", example: { - title: "tests/typed_regression/int64_wrap.py", - sourceRef: "tests/typed_regression/int64_wrap.py", + title: "tests/typed_regression/int64_wrap.lio", + sourceRef: "tests/typed_regression/int64_wrap.lio", expectedRef: "tests/typed_regression/expected/int64_wrap.out", typecheck: false, source: `x: int[64] = 9223372036854775807 @@ -1090,8 +1237,8 @@ const containers: DocPage = { { kind: "code", example: { - title: "tests/programs/list_basic.py (excerpt)", - sourceRef: "tests/programs/list_basic.py", + title: "tests/programs/list_basic.lio (excerpt)", + sourceRef: "tests/programs/list_basic.lio", expectedRef: "tests/programs/expected/list_basic.out", typecheck: false, source: `xs: list[int[64], 6] @@ -1174,8 +1321,8 @@ print(t[3])`, { kind: "code", example: { - title: "tests/programs/dict_basic.py (excerpt)", - sourceRef: "tests/programs/dict_basic.py", + title: "tests/programs/dict_basic.lio (excerpt)", + sourceRef: "tests/programs/dict_basic.lio", expectedRef: "tests/programs/expected/dict_basic.out", typecheck: false, source: `d: dict[int[64], int[64], 4] = {1: 10, 2: 20, 3: 30, 4: 40} @@ -1193,7 +1340,43 @@ print(d[j])`, kind: "note", tone: "note", title: "More entries than buckets is a duplicate-bucket problem", - text: "`dict_basic.py` stays one short of it and leans on the probe instead. `key_pairs.py` is the test that pins a collision down to fixed keys rather than leaving it to chance.", + text: "`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.", + }, + ], + }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Pointers", + href: "/docs/pointers", + text: "Raw addresses, `addressof`/`valueof`, and the `_` naming rule.", + }, + { + title: "Structs", + href: "/docs/structs", + text: "Packed storage: fields at fixed offsets, no padding.", + }, + { + title: "Enums", + href: "/docs/enums", + text: "A tag plus a payload, read with `match`.", + }, + { + title: "Type functions", + href: "/docs/type-functions", + text: "Compile-time type constructors that expand structurally.", + }, + { + title: "Built-in functions", + href: "/docs/builtins", + text: "The whole prelude, one row per name.", + }, + ], }, ], }, @@ -1251,8 +1434,8 @@ print(valueof(p))`, { kind: "code", example: { - title: "tests/programs/ptr_basic.py (excerpt)", - sourceRef: "tests/programs/ptr_basic.py", + title: "tests/programs/ptr_basic.lio (excerpt)", + sourceRef: "tests/programs/ptr_basic.lio", expectedRef: "tests/programs/expected/ptr_basic.out", typecheck: false, source: `x: int[64] = 5 @@ -1309,8 +1492,8 @@ print(_p == _q)`, { kind: "code", example: { - title: "reading through a pointer (ptr_basic.py excerpt)", - sourceRef: "tests/programs/ptr_basic.py", + title: "reading through a pointer (ptr_basic.lio excerpt)", + sourceRef: "tests/programs/ptr_basic.lio", typecheck: false, source: `f: float[64] = 2.5 _pf: ptr[float[64]] = addressof(f) @@ -1366,8 +1549,8 @@ const structs: DocPage = { { kind: "code", example: { - title: "tests/programs/struct_basic.py (excerpt)", - sourceRef: "tests/programs/struct_basic.py", + title: "tests/programs/struct_basic.lio (excerpt)", + sourceRef: "tests/programs/struct_basic.lio", expectedRef: "tests/programs/expected/struct_basic.out", typecheck: false, source: `class Point(Struct): @@ -1396,8 +1579,8 @@ print(p.y)`, { kind: "code", example: { - title: "unwritten fields read zero (struct_basic.py excerpt)", - sourceRef: "tests/programs/struct_basic.py", + title: "unwritten fields read zero (struct_basic.lio excerpt)", + sourceRef: "tests/programs/struct_basic.lio", typecheck: false, source: `class SockAddrIn(Struct): family: int[16] @@ -1458,8 +1641,8 @@ print(s.active)`, { kind: "code", example: { - title: "float fields (struct_basic.py excerpt)", - sourceRef: "tests/programs/struct_basic.py", + title: "float fields (struct_basic.lio excerpt)", + sourceRef: "tests/programs/struct_basic.lio", typecheck: false, source: `class Vec(Struct): x: float[64] @@ -1568,8 +1751,8 @@ const enums: DocPage = { { kind: "code", example: { - title: "tests/programs/enum_basic.py (excerpt)", - sourceRef: "tests/programs/enum_basic.py", + title: "tests/programs/enum_basic.lio (excerpt)", + sourceRef: "tests/programs/enum_basic.lio", expectedRef: "tests/programs/expected/enum_basic.out", typecheck: false, source: `class Shape(Enum): @@ -1632,8 +1815,8 @@ match s: { kind: "code", example: { - title: "tests/programs/enum_struct.py (excerpt)", - sourceRef: "tests/programs/enum_struct.py", + title: "tests/programs/enum_struct.lio (excerpt)", + sourceRef: "tests/programs/enum_struct.lio", typecheck: false, source: `class Endpoint(Enum): Empty @@ -1683,8 +1866,8 @@ c.id = 7`, { kind: "code", example: { - title: "tests/programs/enum_struct.py (excerpt)", - sourceRef: "tests/programs/enum_struct.py", + title: "tests/programs/enum_struct.lio (excerpt)", + sourceRef: "tests/programs/enum_struct.lio", typecheck: false, source: `class Endpoint(Enum): Empty @@ -1823,8 +2006,8 @@ const typeFunctions: DocPage = { { kind: "code", example: { - title: "tests/programs/typefunc_basic.py (excerpt)", - sourceRef: "tests/programs/typefunc_basic.py", + title: "tests/programs/typefunc_basic.lio (excerpt)", + sourceRef: "tests/programs/typefunc_basic.lio", expectedRef: "tests/programs/expected/typefunc_basic.out", typecheck: false, source: `def Fixed(t, n) -> NewType: @@ -1860,8 +2043,8 @@ print(xs[0] + xs[1] + xs[2])`, { kind: "code", example: { - title: "tests/programs/typefunc_basic.py (excerpt)", - sourceRef: "tests/programs/typefunc_basic.py", + title: "tests/programs/typefunc_basic.lio (excerpt)", + sourceRef: "tests/programs/typefunc_basic.lio", typecheck: false, source: `def Alias(t) -> NewType: return {t} @@ -1889,8 +2072,8 @@ print(a)`, { kind: "code", example: { - title: "tests/programs/typefunc_uses.py (excerpt)", - sourceRef: "tests/programs/typefunc_uses.py", + title: "tests/programs/typefunc_uses.lio (excerpt)", + sourceRef: "tests/programs/typefunc_uses.lio", expectedRef: "tests/programs/expected/typefunc_uses.out", typecheck: false, source: `def Word() -> NewType: @@ -2000,8 +2183,8 @@ const methods: DocPage = { { kind: "code", example: { - title: "tests/programs/method_basic.py (excerpt)", - sourceRef: "tests/programs/method_basic.py", + title: "tests/programs/method_basic.lio (excerpt)", + sourceRef: "tests/programs/method_basic.lio", expectedRef: "tests/programs/expected/method_basic.out", typecheck: false, source: `class Counter(Struct): @@ -2015,7 +2198,7 @@ const methods: DocPage = { c: Counter print(c.add(5)) print(c.add(7))`, - output: "5", + output: "5\n12", }, }, { @@ -2035,8 +2218,8 @@ print(c.add(7))`, { kind: "code", example: { - title: "tests/programs/method_enum.py (excerpt)", - sourceRef: "tests/programs/method_enum.py", + title: "tests/programs/method_enum.lio (excerpt)", + sourceRef: "tests/programs/method_enum.lio", expectedRef: "tests/programs/expected/method_enum.out", typecheck: false, source: `class Shape(Enum): diff --git a/web/apps/web/src/docs/pages/machine.ts b/web/apps/web/src/docs/pages/machine.ts index 8fe8187..e116140 100644 --- a/web/apps/web/src/docs/pages/machine.ts +++ b/web/apps/web/src/docs/pages/machine.ts @@ -4,7 +4,7 @@ const sourceToMachine: DocPage = { slug: "source-to-machine", title: "Source to machine", description: - "The full walk: .py β†’ typed IR β†’ verified SSA β†’ x86-64 bytes, with the real artifacts at each step.", + "The full walk: .lio β†’ typed IR β†’ verified SSA β†’ x86-64 bytes, with the real artifacts at each step.", group: "machine", tags: [ "machine", @@ -28,7 +28,7 @@ const sourceToMachine: DocPage = { kind: "steps", items: [ { - title: "Source (.py)", + title: "Source (.lio)", text: "What you wrote. Annotations and all.", }, { @@ -50,8 +50,8 @@ const sourceToMachine: DocPage = { example: { lang: "bash", title: "seeing it yourself", - source: `$ lithon prog.py --ir # stop after IR -$ python3 src/frontend/frontend.py prog.py > prog.ir + source: `$ 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`, }, @@ -69,8 +69,8 @@ $ ./build/tier_runner prog.ir --strict -v { kind: "code", example: { - title: "tests/typed_regression/if.py β†’ IR", - sourceRef: "tests/typed_regression/if.py", + title: "tests/typed_regression/if.lio β†’ IR", + sourceRef: "tests/typed_regression/if.lio", source: `x: int[64] = 5 if x < 3: print(1) @@ -88,11 +88,13 @@ block0: branch %3, block1, block2 block1: %4 = const_i64 1 - call print, %4 + call print_bare, %4 + call print_nl jump block3 block2: %5 = const_i64 3 - call print, %5 + call print_bare, %5 + call print_nl jump block3 block3: return`, @@ -139,8 +141,8 @@ block3: { kind: "code", example: { - title: "tests/typed_regression/function.py β†’ IR", - sourceRef: "tests/typed_regression/function.py", + title: "tests/typed_regression/function.lio β†’ IR", + sourceRef: "tests/typed_regression/function.lio", source: `def add(a: int[64], b: int[64]) -> int[64]: return a + b @@ -162,7 +164,8 @@ block0: %2 = call add, %0, %1 store x, %2 : int[64] %3 = load x - call print, %3 + call print_bare, %3 + call print_nl return`, asm: `add: push rbp @@ -202,8 +205,8 @@ main: { kind: "code", example: { - title: "tests/typed_regression/recursion.py", - sourceRef: "tests/typed_regression/recursion.py", + title: "tests/typed_regression/recursion.lio", + sourceRef: "tests/typed_regression/recursion.lio", expectedRef: "tests/typed_regression/expected/recursion.out", source: `def fib(n: int[64]) -> int[64]: if n < 2: @@ -243,7 +246,8 @@ block0: %1 = call fib, %0 store x, %1 : int[64] %2 = load x - call print, %2 + call print_bare, %2 + call print_nl return`, note: "Real frontend output. The base case is block1; block2 computes both recursive calls and adds them. The encoder turns each `call` into a real `call fib` with the frame discipline above.", }, @@ -251,6 +255,37 @@ block0: }, ], }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Registers & ABI", + href: "/docs/registers-abi", + text: "The register file and the calling discipline the IR maps onto.", + }, + { + title: "Instruction encoding", + href: "/docs/instruction-encoding", + text: "How the IR becomes bytes, and how those bytes are proven.", + }, + { + title: "Optimizer & SSA pipeline", + href: "/docs/pipeline", + text: "The passes that run between the IR and the encoder.", + }, + { + title: "Dual-tier execution", + href: "/docs/dual-tier", + text: "What happens when the native tier refuses to run a program.", + }, + ], + }, + ], + }, ], } diff --git a/web/apps/web/src/docs/pages/reference.ts b/web/apps/web/src/docs/pages/reference.ts index 3d3b8c3..9defe5f 100644 --- a/web/apps/web/src/docs/pages/reference.ts +++ b/web/apps/web/src/docs/pages/reference.ts @@ -68,20 +68,111 @@ const errors: DocPage = { "`range(a, b, 0)` with a literal zero step", ], [ - "`E0108`-`E0125`, 1 (static refusal)", - "annotation, container, pointer, assignment, loop, declaration, tuple/dict, and function-header restrictions: one code per construct (see docs/lithon_error_system.md)", + "`E0108`, 1 (static refusal)", + "Unsupported type-annotation form", ], [ - "`E0126`-`E0131`, 1 (static refusal)", - "struct/enum definition, field, access, variant construction, and `match` restrictions", + "`E0109`, 1 (static refusal)", + "Unknown or badly-sized annotation type name", ], [ - "`E0132`-`E0135`, 1 (static refusal)", - "type-function and method definition/application restrictions", + "`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", + "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"], @@ -114,6 +205,12 @@ const errors: DocPage = { ], ], }, + { + kind: "note", + tone: "note", + title: "the tier digit is the contract", + text: "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.", + }, ], }, { @@ -204,10 +301,14 @@ print(i)`, example: { lang: "bash", title: "terminal", - source: `$ lithon div_zero.py # dynamic divisor + source: `$ lithon div_zero.lio # dynamic divisor LITHON-E0301: 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.", + }, ], }, ], @@ -217,7 +318,7 @@ const builtins: DocPage = { slug: "builtins", title: "Built-in functions", description: - "print, len, range, addressof, valueof, contains, and the wrap_* family: the whole prelude.", + "print, len, range, addressof, valueof, contains, enumerate, zip, and the wrap_* family: the whole prelude.", group: "reference", tags: [ "builtins", @@ -227,6 +328,10 @@ const builtins: DocPage = { "addressof", "valueof", "contains", + "enumerate", + "zip", + "append", + "split", "wrap", "prelude", ], @@ -305,12 +410,63 @@ const builtins: DocPage = { ["`wrap_mul(a, b)`", "wrapping integer mul"], ], }, + { + kind: "p", + text: "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.", + }, + { + kind: "code", + example: { + title: "enumerate and zip in a for header", + // tools/typecheck.py is a mirror of the older scalar-only static + // checks and does not know the container type language yet; the + // real checker (the C++ one, which runs inside tier_runner) does. + typecheck: false, + source: `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\n1\n2\n10\n20\n30", + }, + }, + { + 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.", + }, { kind: "note", tone: "note", title: "wrap_* is the explicit opt-in", text: "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.", }, + { + kind: "linkcards", + items: [ + { + title: "Containers", + href: "/docs/containers", + text: "What `len`, `sum` and `append` actually operate on.", + }, + { + title: "Semantics", + href: "/docs/semantics", + text: "Where Lithon deliberately diverges from CPython.", + }, + { + title: "Errors & diagnostics", + href: "/docs/errors", + text: "The code family, and which of them you can catch.", + }, + ], + }, ], }, { @@ -320,8 +476,8 @@ const builtins: DocPage = { { kind: "code", example: { - title: "tests/typed_regression/int64_wrap.py", - sourceRef: "tests/typed_regression/int64_wrap.py", + title: "tests/typed_regression/int64_wrap.lio", + sourceRef: "tests/typed_regression/int64_wrap.lio", expectedRef: "tests/typed_regression/expected/int64_wrap.out", source: `x: int[64] = 9223372036854775807 y: int[64] = 1 @@ -460,6 +616,41 @@ print(7 % -3)`, ["`for`/`else`", "rejected, never silently dropped"], ], }, + { + kind: "p", + text: "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.", + }, + ], + }, + { + id: "where-to-go-next", + title: "Where to go next", + blocks: [ + { + kind: "linkcards", + items: [ + { + title: "Errors & diagnostics", + href: "/docs/errors", + text: "Every code, and which tier it belongs to.", + }, + { + title: "Built-in functions", + href: "/docs/builtins", + text: "The whole prelude, one row per name.", + }, + { + title: "Control flow", + href: "/docs/control-flow", + text: "if / while / for, and `try`/`except`.", + }, + { + title: "Honest limits", + href: "/docs/limits", + text: "What does not work yet, stated plainly.", + }, + ], + }, ], }, ], @@ -655,7 +846,7 @@ const limits: DocPage = { blocks: [ { kind: "p", - text: 'No stdlib, no import system beyond the built-ins, no classes, no closures, no exceptions. 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".', + text: '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".', }, { kind: "linkcards", diff --git a/web/apps/web/src/docs/pages/start.ts b/web/apps/web/src/docs/pages/start.ts index ff851f3..5a765e4 100644 --- a/web/apps/web/src/docs/pages/start.ts +++ b/web/apps/web/src/docs/pages/start.ts @@ -22,7 +22,7 @@ const welcome: DocPage = { blocks: [ { kind: "p", - text: "Lithon is a **Python library and native x86-64 execution engine** that runs `.py` source using an extended PEP-526 static typing syntax. Python remains the source language, Lithon is not a separate language, but every supported program is statically verified and can execute as generated machine code, with no LLVM, no Cranelift, and no runtime package.", + text: "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.", }, { kind: "code", @@ -92,7 +92,7 @@ print(result)`, kind: "table", caption: "The design space Lithon explores", rows: [ - ["Syntax", "Python `.py` + extended PEP-526"], + ["Syntax", "Python syntax + extended PEP-526, in a `.lio` file"], [ "Primary execution", "Native x86-64 (CPython: bytecode; PyPy: tracing JIT)", @@ -143,6 +143,16 @@ print(result)`, href: "/docs/mental-model", text: "Two tiers, one rule: prove before you run.", }, + { + title: "Types & widths", + href: "/docs/types", + text: "The one rule that makes the rest checkable: every type carries its width.", + }, + { + title: "Control flow", + href: "/docs/control-flow", + text: "if / while / for, and `try`/`except` over the engine's own traps.", + }, { title: "Machine view", href: "/docs/source-to-machine", @@ -181,7 +191,7 @@ const helloWorld: DocPage = { { kind: "code", example: { - title: "hello.py", + title: "hello.lio", source: `x: int[64] = 10 y: int[64] = 20 @@ -207,8 +217,8 @@ print(result)`, { kind: "code", example: { - title: "tests/programs/arithmetic.py", - sourceRef: "tests/programs/arithmetic.py", + title: "tests/programs/arithmetic.lio", + sourceRef: "tests/programs/arithmetic.lio", expectedRef: "tests/programs/expected/arithmetic.out", source: `x: int[64] = 10 y: int[64] = 20 @@ -261,15 +271,15 @@ print(x)`, items: [ { title: "Type-check only", - text: "`tools/typecheck.py hello.py`: static analysis over the AST, no execution. Exits non-zero with an `RCR error` on the first violation.", + text: "`tools/typecheck.py hello.lio`: static analysis over the AST, no execution. Exits non-zero with an `RCR error` on the first violation.", }, { title: "Compile to IR", - text: "`src/frontend/frontend.py hello.py`: lowers the typed AST to Lithon's text IR on stdout. No engine needed, pure Python.", + text: "`src/frontend/frontend.py hello.lio`: lowers the typed AST to Lithon's text IR on stdout. No engine needed, pure Python.", }, { title: "Run natively", - text: "`lithon hello.py --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.", + text: "`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.", }, ], }, @@ -278,7 +288,7 @@ print(x)`, example: { lang: "bash", title: "terminal", - source: `$ lithon hello.py --strict + source: `$ lithon hello.lio --strict [tier1] native 30`, }, @@ -349,7 +359,7 @@ $ cmake --build build -j$(nproc)`, }, { kind: "p", - text: "There is no Makefile to hand-edit, CMake drives everything. Python appears at exactly one step, turning a `.py` file into IR; once IR exists the native program never touches CPython.", + text: "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.", }, ], }, @@ -363,10 +373,10 @@ $ cmake --build build -j$(nproc)`, lang: "bash", title: "install in editable mode, then", source: `$ pip install -e . -$ lithon tests/programs/float.py -$ lithon tests/programs/float.py --strict # native only, refuse to fall back -$ lithon tests/programs/float.py --ir # print the IR and stop -$ lithon tests/programs/float.py -v # say which tier ran, and why`, +$ 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`, }, }, { @@ -385,7 +395,7 @@ $ lithon tests/programs/float.py -v # say which tier ran, and why`, kind: "note", tone: "note", title: "Prefer scripts? The two-step dance is open", - text: "`python3 src/frontend/frontend.py prog.py > prog.ir` then `./build/tier_runner prog.ir --strict`. The `lithon` wrapper is convenience, not a black box.", + text: "`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.", }, ], }, @@ -500,7 +510,7 @@ Unknown example: { lang: "bash", title: "terminal", - source: `$ lithon program.py --strict`, + source: `$ lithon program.lio --strict`, }, }, { @@ -513,7 +523,7 @@ Unknown { title: "Inside the engine", href: "/docs/architecture", - text: "Four layers from `.py` source to executable memory.", + text: "Four layers from `.lio` source to executable memory.", }, { title: "Dual-tier execution", diff --git a/web/apps/web/src/docs/registry.ts b/web/apps/web/src/docs/registry.ts index 5c695bc..bcb4cb2 100644 --- a/web/apps/web/src/docs/registry.ts +++ b/web/apps/web/src/docs/registry.ts @@ -20,7 +20,8 @@ function pageEntries(pages: readonly DocPage[]): SearchEntry[] { title: section.title, subtitle: page.title, group: page.group, - tags: [...page.tags, ...page.tags.slice(0, 2)], + // 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}`, }) } diff --git a/web/apps/web/src/lib/assets.ts b/web/apps/web/src/lib/assets.ts new file mode 100644 index 0000000..5cec10b --- /dev/null +++ b/web/apps/web/src/lib/assets.ts @@ -0,0 +1,27 @@ +/** + * Base-path aware asset URLs. + * + * The site is deployed to GitHub Pages from the repository's `master` + * branch, which serves it from a sub-path (`/Lithon/`) unless a custom + * domain is configured. A hard-coded root-relative URL like + * `/benchmarks/manifest.json` therefore 404s in production, so every + * static asset reference goes through here. + * + * The base is read from the Vite `BASE_URL` global, which Vite replaces + * at build time with the configured `base` -- the same value the router + * and the prerenderer use. In development that is `/`, so nothing changes. + */ +const RAW_BASE: string = import.meta.env.BASE_URL + +/** The deployment base path, always with a leading and trailing slash. */ +export const BASE_PATH = RAW_BASE.endsWith("/") ? RAW_BASE : `${RAW_BASE}/` + +/** + * Turn a root-relative path (`/benchmarks/manifest.json`) into a URL that + * respects the deployment base path. + */ +export function assetUrl(path: string): string { + if (!path.startsWith("/")) return path + if (/^https?:\/\//.test(path)) return path + return `${BASE_PATH}${path.replace(/^\//, "")}` +} diff --git a/web/apps/web/src/lib/ir.ts b/web/apps/web/src/lib/ir.ts index f161c9a..626e8ce 100644 --- a/web/apps/web/src/lib/ir.ts +++ b/web/apps/web/src/lib/ir.ts @@ -1,4 +1,4 @@ -/** +ο»Ώ/** * An executable model of Lithon's IR. * * This is not a simulation of the docs: it parses the exact IR text the @@ -72,6 +72,29 @@ function fmtFloat(v: number): string { return Number.isInteger(v) ? `${v}.0` : String(v) } +/** + * The Linux x86-64 syscall numbers the docs' syscall examples can name. + * A number that is not in this set is not a syscall the kernel knows, so + * it comes back as -ENOSYS (-38) -- the same answer the engine's own + * trampoline gives on a platform with no raw-syscall ABI. + */ +const KNOWN_SYSCALLS = new Set([ + 0, // read + 1, // write + 2, // open + 3, // close + 9, // mmap + 10, // mprotect + 11, // munmap + 12, // brk + 13, // rt_sigaction + 14, // rt_sigprocmask + 39, // getpid + 60, // exit + 63, // uname + 102, // getuid +]) + export function parseIr(text: string): IrProgram { const lines = text.split(/\r?\n/) const functions: IrFunction[] = [] @@ -208,6 +231,33 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const lookupSlot = (name: string): MemCell | undefined => memory.get(`${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 + * `fieldstore p, 8, %N` must not overwrite each other. The blob itself + * keeps the overall address; the fields are keyed by (blob, offset). + */ + const fieldCells = new Map() + + const fieldKey = (name: string, offset: number) => + `${frames.length}:${name}#${offset}` + + const allocField = (name: string, offset: number, type: string): MemCell => { + const key = fieldKey(name, offset) + const existing = fieldCells.get(key) + if (existing) return existing + const blob = allocSlot(name, type) + const cell: MemCell = { + name: `${name}+${offset}`, + address: `${blob.address}+${offset}`, + offset: blob.offset + offset, + type, + value: 0n, + } + fieldCells.set(key, cell) + return cell + } + const pushFrame = (fn: IrFunction, args: unknown[]) => { frames.push({ fn, pc: fn.start, dest: undefined }) current = fn @@ -219,16 +269,29 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } let guard = 0 + let error: string | undefined + // The engine lowers `print(x)` into `print_bare, %N` + `print_nl`, so the + // value is held here until the closing newline arrives. + let pendingPrint: string | undefined + // The handler registered by the innermost `tryenter`, if any. + let pendingHandler: { trapName: string; handler: string } | undefined + // 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 const snapshot = () => ({ registers: [...registers.entries()] .filter(([k]) => k.startsWith("%")) .slice(-8) .map(([name, value]) => ({ name, value: fmtValue(value) })), - memory: [...memory.values()].map((m) => ({ ...m })), + memory: [ + ...memory.values(), + // struct fields live in their own map; show them next to the blob + ...[...fieldCells.values()], + ].map((m) => ({ ...m })), }) - while (frames.length > 0 && guard < maxSteps) { + while (frames.length > 0 && guard < maxSteps && !error) { const frame = frames.at(-1) if (frame === undefined) break const instr = current.instrs.find((i) => i.index === frame.pc) @@ -241,6 +304,26 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } guard += 1 + /** + * Raise a trap. If the innermost `tryenter` catches this trap name, the + * run continues at the handler block; otherwise the run stops with the + * engine's own message. The catchable set is the same closed set the + * engine uses: the six named traps plus `Exception` for all of them. + */ + const trap = (trapName: string, message: string) => { + const handler = pendingHandler + const catches = + handler !== undefined && + (handler.trapName === trapName || handler.trapName === "Exception") + if (!catches) { + error = message + return + } + trapTarget = handler.handler + // the protected region is left by jumping to the handler + pendingHandler = undefined + } + let consumed: { register: string; value: string }[] = [] let returnedValue: unknown const read = (tok: string | undefined): unknown => { @@ -316,17 +399,99 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } case "store": { const name = instr.args[0] - const v = read(instr.args[1]) const cell = allocSlot(name, instr.width ?? "int[64]") - cell.value = v as number | bigint | boolean - written = { ...cell } + // `store xs : list[int[64], 5]` has no value: it just reserves the + // container's storage, which is already zeroed by `allocSlot`. + if (instr.args.length > 1) { + const v = read(instr.args[1]) + cell.value = v as number | bigint | boolean + written = { ...cell } + } if ( instr.width?.startsWith("float") || (instr.args[1] && floatRegs.has(`${frames.length}:${current.name}:${instr.args[1]}`)) ) floatSlots.add(name) - note = `${cell.address} ← ${fmtValue(v)}` + note = + instr.args.length > 1 + ? `${cell.address} Ò† ${fmtValue(cell.value)}` + : `${cell.address} reserved` + break + } + case "fieldstore": { + // fieldstore p, offset, %N -- write one field of a struct blob + const name = instr.args[0] + const offset = Number(instr.args[1]) + const v = read(instr.args[2]) + const cell = allocField(name, offset, instr.width ?? "int[64]") + cell.value = v as number | bigint | boolean + written = { ...cell } + note = `${cell.address} Ò† ${fmtValue(v)}` + break + } + case "fieldload": { + // fieldload p, offset -- read one field of a struct blob + const name = instr.args[0] + const offset = Number(instr.args[1]) + const cell = allocField(name, offset, instr.width ?? "int[64]") + const v = cell.value + if (instr.dest) { + registers.set(`${frames.length}:${current.name}:${instr.dest}`, v) + if (instr.width?.startsWith("float")) + floatRegs.add(`${frames.length}:${current.name}:${instr.dest}`) + produced = { register: instr.dest, value: fmtValue(v) } + } + note = `${cell.address} Ò†’ ${fmtValue(v)}` + break + } + case "guardnonzero": { + // guardnonzero %N -- the runtime guard the engine emits before a + // division; a zero divisor is a trap, not a value. + const v = read(instr.args[0]) + if (typeof v === "bigint" && v === 0n) { + trap("ZeroDivisionError", "LITHON-E0301: division by zero") + } + note = "guard: divisor is not zero" + break + } + case "tryenter": { + // tryenter , -- mark the handler block and + // the region it guards. A trap raised inside the region jumps to + // the handler instead of stopping the program. + const trapName = instr.args[0] ?? "Exception" + const handler = instr.args[1] + pendingHandler = { trapName, handler } + note = `guard against ${trapName} Ò†’ ${handler}` + break + } + case "tryleave": { + // tryleave -- end the protected region on the normal path. + pendingHandler = undefined + note = "protected region ends" + break + } + case "trycatch": { + // trycatch -- the landing pad: this block runs when the + // named trap was raised inside the protected region. + note = `caught ${instr.args[0] ?? "trap"}` + break + } + case "syscall": { + // syscall number, a1..a6 -- the raw kernel primitive. The docs + // model the *unsupported* case: a number the kernel does not know + // comes back as -ENOSYS (-38), which is exactly what the engine's + // own trampoline returns on a platform with no raw-syscall ABI. + const number = read(instr.args[0]) + const known = KNOWN_SYSCALLS.has(Number(number)) + const v = known ? 0n : -38n + if (instr.dest) { + registers.set(`${frames.length}:${current.name}:${instr.dest}`, v) + produced = { register: instr.dest, value: fmtValue(v) } + } + note = known + ? `syscall ${fmtValue(number)}` + : `syscall ${fmtValue(number)} Ò†’ -ENOSYS (-38)` break } case "add": @@ -354,8 +519,15 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { const y = b as bigint if (instr.op === "sub") v = wrap64(x - y) else if (instr.op === "mul") v = wrap64(x * y) - else if (instr.op === "mod") v = y === 0n ? 0n : x % y - else v = wrap64(x + y) + else if (instr.op === "mod") { + // modulo by zero is a trap, not a value + if (y === 0n) { + trap("ZeroDivisionError", "LITHON-E0302: modulo by zero") + v = 0n + } else { + v = x % y + } + } else v = wrap64(x + y) } if (instr.dest) { registers.set(`${frames.length}:${current.name}:${instr.dest}`, v) @@ -417,19 +589,25 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } case "shl": case "shr": + case "band": + case "bor": + case "bxor": case "and": case "or": case "xor": { const a = read(instr.args[0]) as bigint const b = read(instr.args[1]) as bigint + // The engine spells the *bitwise* ops `band`/`bor`/`bxor` and keeps + // `and`/`or`/`xor` for the logical ones, so both spellings are + // accepted here and treated as bitwise. const v = instr.op === "shl" ? wrap64(a << (b & 63n)) : instr.op === "shr" ? a >> (b & 63n) - : instr.op === "and" + : instr.op === "band" || instr.op === "and" ? a & b - : instr.op === "or" + : instr.op === "bor" || instr.op === "or" ? a | b : a ^ b if (instr.dest) { @@ -466,7 +644,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": @@ -504,13 +682,17 @@ 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 } case "call": { const callee = instr.args[0] - if (callee === "print") { + if (callee === "print" || callee === "print_bare" || callee === "print_sep") { + // The engine lowered `print(x)` into two calls: `print_bare, %N` + // (emit the value with no separator) followed by `print_nl` + // (emit the newline). The older single-call form `print, %N` is + // still accepted so hand-written IR from older docs keeps working. printedArg = instr.args[1] const v = read(instr.args[1]) const isFloat = @@ -519,9 +701,20 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { ) || (instr.args[1] ? floatSlots.has(instr.args[1]) : false) const line = isFloat && typeof v === "number" ? fmtFloat(v) : fmtValue(v) - stdout.push(line) - output = line + if (callee === "print") { + // legacy single-call print: the value and the newline together + stdout.push(line) + output = line + } else { + // print_bare / print_sep: the value only; print_nl closes the line + pendingPrint = line + output = line + } printedArg = undefined + } else if (callee === "print_nl") { + stdout.push(pendingPrint ?? "") + output = pendingPrint ?? "" + pendingPrint = undefined } else { const target = byName.get(callee) if (target) { @@ -546,7 +739,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 } @@ -605,7 +798,13 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { // continue from a return may have popped one const live = frames.at(-1) if (live === undefined) break - if (jumpTo) { + if (trapTarget) { + // a trap was caught: jump to the handler block + const target = current.instrs.find((i) => i.block === trapTarget) + if (target) live.pc = target.index + else live.pc += 1 + trapTarget = undefined + } else if (jumpTo) { const target = current.instrs.find((i) => i.block === jumpTo && i.index > live.pc) ?? current.instrs.find((i) => i.block === jumpTo) @@ -617,7 +816,8 @@ export function runIr(program: IrProgram, maxSteps = 2000): IrRun { } const run: IrRun = { steps, stdout } - if (guard >= maxSteps) run.error = `stopped after ${maxSteps} steps` + if (error) run.error = error + else if (guard >= maxSteps) run.error = `stopped after ${maxSteps} steps` return run } diff --git a/web/apps/web/src/lib/seo.ts b/web/apps/web/src/lib/seo.ts index bb159ea..4e4d83b 100644 --- a/web/apps/web/src/lib/seo.ts +++ b/web/apps/web/src/lib/seo.ts @@ -11,10 +11,29 @@ export const SITE = { docsBase: "/docs", } as const -/** Canonical origin used for sitemap and canonical URLs. */ +/** + * The canonical origin used for sitemap and canonical URLs. + * + * The site is deployed to GitHub Pages from the repository's `master` + * branch, so the canonical home is the Pages URL for the `Lithon` + * repository. `SITE_ORIGIN` overrides it for a fork or a custom domain; + * `BASE_PATH` covers the sub-path a fork would be served from. + */ export const SITEMAP_HOST = process.env.SITE_ORIGIN ?? "https://project-lithon.github.io/Lithon" +/** The path the site is served from, always with a leading and trailing slash. */ +export const SITE_BASE_PATH = (() => { + const explicit = process.env.BASE_PATH + if (explicit) { + const trimmed = explicit.replace(/\/+$/, "") + return trimmed === "" ? "/" : `${trimmed}/` + } + if (process.env.CUSTOM_DOMAIN) return "/" + const repo = process.env.GITHUB_REPOSITORY?.split("/")[1] ?? "Lithon" + return `/${repo}/` +})() + export type PageMeta = { title: string description: string diff --git a/web/apps/web/src/routes/__root.tsx b/web/apps/web/src/routes/__root.tsx index b0f06a7..b3b32da 100644 --- a/web/apps/web/src/routes/__root.tsx +++ b/web/apps/web/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +ο»Ώimport { useEffect, useState } from "react" import { HeadContent, Link, @@ -39,6 +39,7 @@ import { } from "@workspace/ui/components/tooltip" import appCss from "@workspace/ui/globals.css?url" import { initSoundFeedback, readStoredSound, toggleSound } from "../lib/sound" +import { assetUrl } from "../lib/assets" import { ConsentBanner, PREFERENCE_SCRIPT } from "../lib/consent" import { CiStatusStrip } from "../lib/ci-status" import { @@ -60,14 +61,16 @@ export const Route = createRootRoute({ content: "Lithon | A zero-dependency, dual-tier execution engine built for native speed.", }, - { name: "theme-color", content: "#f8fff4" }, + { name: "theme-color", content: "#121212" }, { title: "Lithon | Not a revolution but an evolution of the same bloodline", }, ], links: [ - { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" }, + { rel: "icon", href: assetUrl("/favicon.svg"), type: "image/svg+xml" }, + { rel: "apple-touch-icon", href: assetUrl("/favicon.ico") }, + { rel: "manifest", href: assetUrl("/manifest.json") }, { rel: "stylesheet", href: appCss }, ], }), @@ -335,7 +338,7 @@ function SearchButton() { > Search - ⌘ + Γ’Ε’Λœ K ) @@ -363,7 +366,7 @@ function SiteHeader({ - + @@ -416,7 +419,7 @@ function SiteHeader({ > - + @@ -440,7 +443,7 @@ 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.

diff --git a/web/apps/web/src/routes/benchmarks.tsx b/web/apps/web/src/routes/benchmarks.tsx index 1682a4f..2520ded 100644 --- a/web/apps/web/src/routes/benchmarks.tsx +++ b/web/apps/web/src/routes/benchmarks.tsx @@ -19,6 +19,7 @@ import { import { Separator } from "@workspace/ui/components/separator" import { PageHero, WRAP } from "../site" +import { assetUrl } from "../lib/assets" import { metaForPath } from "../lib/seo" export const Route = createFileRoute("/benchmarks")({ @@ -118,7 +119,7 @@ function BenchmarkCard({ entry }: { entry: Entry }) {
{playing ? (