vextoly/cryn
Cryn 0.1.0: bootstrap compiler, tests, examples, documentation
Commit 90a2d5c · Vextoly · 2026-10-04T15:35:54+02:00
Cryn 0.1.0: bootstrap compiler, tests, examples, documentation - 34-command English-worded compiled systems language - lexer/parser/AST/semantic analysis/IR/optimizer/C backend in C11 - static type system via abstract machine interpretation - runtime embedded into generated C (alloc, threads, streams, output) - 35-test suite incl. recursion, memory, files, threads, wordcount, sort, allocator; 7 example programs - README.md, LANGUAGE.md, TUTORIAL.md
Comments
No comments yet.
Diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f265a3c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# build artifacts
+cryn
+tools/embed
+src/rt_src.h
+*.o
+*.a
+*.so
+
+# test/run artifacts
+tests/cryn_test_*.txt
+*.exe
+
+# generated compiler output
+out.c
+*.c.gen.c
+
+# editors and OS
+.vscode/
+.idea/
+*.swp
+*~
+.DS_Store
diff --git a/LANGUAGE.md b/LANGUAGE.md
new file mode 100644
index 0000000..26e9950
--- /dev/null
+++ b/LANGUAGE.md
@@ -0,0 +1,439 @@
+# The Cryn Programming Language Reference
+
+This document defines Cryn precisely enough for an independent
+implementation. It describes the language implemented by the `cryn`
+compiler (bootstrap version).
+
+Cryn is a compiled, statically typed systems language. Its execution
+model is a small abstract machine consisting of **cells**, named and
+indexed **connections**, a **focus**, a **working value**, a **value
+stack**, and a **comparison state** inside each **execution context**,
+plus a shared **world** of cells.
+
+---
+
+## 1. Lexical rules
+
+### 1.1 Source encoding
+
+Source files are byte streams. Tokens are formed from ASCII words,
+digits, and the delimiters below. Non-ASCII bytes may appear only inside
+string and character literals and comments.
+
+### 1.2 Whitespace
+
+Space, tab, carriage return, and newline separate tokens and have **no
+semantic meaning**. Any two programs with the same token sequence are the
+same program, regardless of layout.
+
+### 1.3 Comments
+
+A comment starts with the exact sequence `[#` and ends with the exact
+sequence `#]`, possibly spanning lines. Comments nest nothing, produce no
+tokens, and are the only comment syntax. An unterminated comment is a
+compile error. `#` or `]` alone are not comment delimiters; outside
+strings they are invalid characters.
+
+### 1.4 Identifiers and keywords
+
+An identifier is `[A-Za-z_][A-Za-z0-9_]*`. Identifiers containing hyphens
+do not exist; hyphens appear only in the two condition keywords
+`less-or-equal` and `greater-or-equal`.
+
+All of the following are reserved words and cannot be identifiers:
+
+* commands: `move back home address make free read write set add subtract
+ multiply divide remainder negate push pop swap compare convert jump call
+ return stop input output open close system spawn wait define end mark`
+* type words: `nothing bool char int8 int16 int32 int64 uint8 uint16
+ uint32 uint64 float32 float64 address array string transform`
+* value words: `true false`
+* argument words: `of when result index at text both less equal greater
+ different less-or-equal greater-or-equal`
+
+### 1.5 Literals
+
+* **Integer:** decimal digits or `0x` hex digits, optionally preceded by
+ `-`. Range: magnitude must fit in `uint64`; the value range of a use is
+ checked contextually (see §7).
+* **Float:** `digits [. digits] [e[+|-]digits]`, optionally negated;
+ IEEE 754 double precision.
+* **Character:** `'a'`, `'\n'`, `'\t'`, `'\r'`, `'\0'`, `'\\'`, `'\''`.
+ A character literal denotes its byte value (0–255).
+* **String:** `"..."` with the same escapes; may contain any other byte.
+ Strings are NUL-terminated byte arrays (§5.4); therefore string
+ literals cannot contain `\0`.
+* **Boolean:** `true`, `false`. **Void:** `nothing`.
+
+### 1.6 Locations
+
+Diagnostics carry 1-based line and column positions.
+
+## 2. Program structure
+
+```text
+program := { define | worldmake }
+worldmake := "make" ( basetype name
+ | "array" int "of" type name
+ | "string" stringlit name )
+define := "define" name params [ "result" type ] { command } "end"
+params := { paramtype name }
+```
+
+A program is a set of transformations and an optional world model (shared
+cells created by top-level `make`). Exactly one transformation named
+`main` with zero parameters must exist; execution starts there.
+
+Parameter lists are `{ type name }` pairs. They end at the first token
+that cannot begin a parameter; `address` only opens a parameter when a
+pointee type follows it. This makes bodies unambiguous without
+punctuation.
+
+## 3. Types
+
+| Type | Meaning | Size |
+|-----------------|-------------------------------------------|------|
+| `nothing` | absence of a value | – |
+| `bool` | `false` / `true`, stored as 0/1 | 1 |
+| `char` | byte value 0–255 (codepoint in v1) | 4 |
+| `int8..int64` | two's-complement signed integers | 1/2/4/8 |
+| `uint8..uint64` | unsigned integers | 1/2/4/8 |
+| `float32/64` | IEEE 754 binary32/64 | 4/8 |
+| `address T` | address of a cell/memory of type `T`; `address` alone means `address nothing` | 8 |
+| `array N of T` | fixed array of N T (N ≥ 1) | N·sizeof(T) |
+| `array of T` | unsized array; only as the pointee of `address`, produced by dynamic `make` | element-sized |
+| `transform` | reference to a transformation | 16 (ref) |
+
+Arrays cannot be parameters or results; pass an address. Unsized arrays
+cannot be parameters, results, or cells; they exist only behind
+addresses.
+
+The static type system also has an internal *integer literal* type that
+carries a literal's value; it is not writable in source.
+
+### 3.1 Type equality
+
+Two types are equal iff constructed identically. Array sizes are part of
+the type.
+
+## 4. The machine
+
+### 4.1 World and cells
+
+A **cell** has identity, a type, and an optional value. Cells are:
+
+* **Frame cells**: a transformation's parameters and every named cell
+ made with `make ... name` in its body. They belong to the frame
+ (automatic storage): they are created zeroed each time the `make`
+ executes and live until the frame returns. Taking their address is
+ legal but the address is only valid during the frame's lifetime.
+* **World cells**: created by top-level `make`; zero-initialized at
+ program start; shared by all execution contexts.
+* **Heap blocks**: created by dynamic `make` (§6.3); exist until `free`.
+
+Two cells with equal values remain distinct cells.
+
+### 4.2 Connections
+
+Cells connect to other cells by **key**: a name (frame and world cells;
+each frame connects to all world cells) or an integer index (arrays and
+blocks, indices `0..N-1`). `move` follows connections. Linked structures
+are built by storing address values in cells.
+
+### 4.3 Execution context
+
+Each running transformation invocation is an execution context holding:
+
+* a program counter,
+* the **focus**: a stack of cell references, bottom entry being the
+ frame root,
+* the **working value** (one typed value; initially `nothing`),
+* the **value stack** (unbounded stack of typed values),
+* the **comparison state** (invalid until a `compare`),
+* the current input/output streams (stdin/stdout until `open`),
+* spawn state for `wait`.
+
+Contexts share world cells and heap blocks and nothing else. Focus,
+working value, value stack, and streams are per context.
+
+### 4.4 Focus
+
+The focus identifies the currently selected cell. Its abstract state is a
+stack:
+
+* `move name` **anchors**: the history is reset to `[frame, cell]`.
+* `move <index>`, `move index`, `move at` **push** the previous focus
+ onto the history and focus the target.
+* `back` pops one relative entry (error if only `[frame]` remains — at
+ runtime it restores the frame root focus when the history is empty at
+ the current frame level is impossible; the checker guarantees it).
+* `home` resets to `[frame]`.
+
+`read` and `write` operate on the focused cell; `address` (no argument)
+yields its address.
+
+### 4.5 Working value
+
+One typed value. Set by `set`, `read`, `pop`, arithmetic, `input`,
+`open`, `close`, `system`, `wait`, calls (see §8), conversions. It is
+also the **return value** of a transformation: at `return` (or implicit
+return at `end`) the working value's type must be identical at every
+return point of the transformation; that type is the transformation's
+result type. A call to a transformation whose result is `nothing`
+preserves the caller's working value; any other result replaces it.
+
+Integer literals adapt: a literal working value may be written into,
+compared with, or passed to any integer/char/bool/float context it fits
+(or any float context) without `convert`; otherwise `convert` is required.
+
+### 4.6 Value stack
+
+Holds typed values for preservation across navigation and for argument
+passing. The checker tracks the stack shape at every program point; all
+paths joining at a label must have identical stack depth and slot types,
+the working value type must join (identical, or literal widening), and
+the focus stack must join. The stack must be empty at `return`.
+
+## 5. Values and their semantics
+
+### 5.1 Integers
+
+Two's complement; signed overflow wraps (defined); unsigned overflow
+wraps modulo 2^N.
+
+### 5.2 Division
+
+* Division or remainder by zero: the program aborts with a runtime
+ diagnostic and exit status 1.
+* Signed `INT_MIN / -1` yields `INT_MIN`; `INT_MIN % -1` yields `0`
+ (defined wrap).
+* Truncation is toward zero (C semantics).
+
+### 5.3 Floats
+
+IEEE 754 binary32/binary64 with round-to-nearest-even. Operations on
+`float32` operands compute in single precision. Division by zero yields
+infinities/NaN per IEEE. **NaN comparison:** for `compare`, NaN yields
+`equal` (and not `different`, `less`, or `greater`).
+
+### 5.4 Strings
+
+A string literal used with `make string` creates a byte array of
+length+1 with a terminating NUL. Strings have no other magic; see §7
+`output text` and §6.3 for address-based access.
+
+### 5.5 Addresses
+
+Addresses are 8-byte values. `address nothing` is the type of `null`
+(produce with `set 0; convert address`). `add`/`subtract` on
+`address T` with an integer scales by `sizeof(T)` (for `address array of
+T`, by `sizeof(T)`). `subtract` between two `address T` (same or any
+pointee in v1) yields the element distance as `int64`. Addresses compare
+as unsigned 64-bit integers. Converting between `int64`/`uint64` and any
+address type reinterprets bits.
+
+### 5.6 Conversions (`convert`)
+
+Between any scalar numeric/bool/char/address types, with these rules:
+
+* int → int / char: wrap modulo 2^N.
+* int/uint → float: round to nearest (IEEE).
+* float → int: truncate toward zero; if out of target range, **saturate
+ at the target width** (`float32 1e30 → int32` gives `2147483647`).
+* float32 ↔ float64: round.
+* anything numeric → bool: 0 → `false`, else `true`.
+* char ↔ int: codepoint value.
+* address ↔ int64/uint64 / other address types: bit reinterpretation.
+
+Conversions to or from `nothing`, `transform`, or arrays are errors.
+
+## 6. Commands
+
+34 commands total.
+
+### 6.1 Navigation
+
+| Command | Effect |
+|---|---|
+| `move name` | Focus the frame's cell `name` (params, local cells, world cells). Anchors the focus history to `[frame, cell]`. |
+| `move i` | From an array/block focus, focus element `i` (literal). Compile-time bounds check. Pushes focus. |
+| `move index` | As above, using the working value as index (any integer type; interpreted as unsigned). Runtime bounds check. Working value unchanged. |
+| `move at` | Focus the cell at the address in the working value (`address T` → cell of `T`; `address array of T` → the block). Pushes focus. |
+| `back` | Undo the last relative move. |
+| `home` | Reset focus to `[frame]`. |
+
+### 6.2 Cells and addresses
+
+| Command | Effect |
+|---|---|
+| `make T name` | Create/zero frame cell `name` of type `T`. |
+| `make T` | Create an anonymous heap cell of `T`; working value := its address (`address T`). |
+| `make array N of T name` / `make array N of T` | Frame array / anonymous heap array. |
+| `make string "s" name` | Frame byte array with the bytes and NUL. |
+| `make string "s"` | Working value := address of a static NUL-terminated byte array. |
+| `make n of T` / `make 8 of T` / `make n of T p` | Heap block of n elements (n from an integer cell or literal); working value := `address array of T`; optional `p` additionally binds a frame cell of that type. |
+| `free` | Free the heap block whose address is in the working value. Aborts if the address is not a heap block. |
+| `read` | Working value := value of focused cell. |
+| `write` | Focused cell := working value (literal adaptation applies; else types must match exactly or `convert` first). |
+| `address` | Working value := `address T` of the focused cell. |
+| `address name` | Working value := address of frame/world cell `name`, or (for a transformation name) a `transform` reference to it. |
+
+Frame cells store their values directly; address cells store addresses.
+Writing through an address requires `move at` first.
+
+### 6.3 Working value operations
+
+| Command | Effect |
+|---|---|
+| `set lit` | Working value := literal. |
+| `add`/`subtract`/`multiply`/`divide`/`remainder` [`lit`] | With operand: combine working value with literal (adapted). Without: pop one value and combine. Operand types must match after adaptation (mixed width/sign pairs widen per §7). Address ± integer scales. |
+| `negate` | Two's-complement / float negation. |
+| `convert T` | See §5.6. |
+
+### 6.4 Value stack
+
+| Command | Effect |
+|---|---|
+| `push` | Copy the working value onto the stack. Working value unchanged. |
+| `pop` | Working value := popped value. |
+| `swap` | Exchange working value with the stack top. |
+
+### 6.5 Comparison and control flow
+
+| Command | Effect |
+|---|---|
+| `compare` [`lit`] | Compare working value with popped value (or literal). Sets comparison state to `less`, `equal`, or `greater`. |
+| `jump label` | Unconditional. |
+| `jump label when cond` | cond ∈ `less equal greater different less-or-equal greater-or-equal`. Requires a prior `compare`. |
+| `mark label` | Define label (transformation scoped). |
+| `call name` / `call` | See §8. |
+| `return` | End transformation; result = working value type. |
+| `stop` | End program; exit status = low 8 bits of the working value if numeric, else 0. |
+
+There is no `if`/`while`/`for`; conditional jumps over marks are the
+control flow. A conditional jump **may** jump backward or forward.
+Unreachable commands and labels with no live incoming edge are compile
+errors.
+
+### 6.6 External world
+
+| Command | Effect |
+|---|---|
+| `input` | Read one byte from the current input stream; working value := `int64` 0–255, or `-1` at end of input. |
+| `output` | Write the working value to the current output stream, formatted by its static type: signed ints decimal; unsigned decimal; `uint8` and `char` as one raw byte; `bool` as `true`/`false`; floats shortest-round-trip decimal; `address` as `0x`-hex. |
+| `output text` | Write the NUL-terminated byte string at the working value's address (pointee `uint8` or array of `uint8`). |
+| `open read`/`open write`/`open both` | Open the path (working value address, NUL-terminated) for reading / writing (create/truncate) / both. On success the handle (`int64`) becomes the working value and the context's input and/or output stream is routed through the file; negative means failure and streams are unchanged. |
+| `close` | Close the handle in the working value; restores default streams if they pointed at it. Working value := 0 or -1. |
+| `system` | Run the shell command (working value address, NUL-terminated); working value := exit status as `int64`. |
+
+### 6.7 Concurrency
+
+| Command | Effect |
+|---|---|
+| `spawn name` | Start a new execution context (OS thread) running transformation `name`, which must take zero parameters. The new context starts with working value `nothing`; communication happens through world cells and heap blocks. The spawner's working value is unchanged. Up to 64 outstanding spawns per context. |
+| `wait` | Join all contexts spawned by this one. Working value := `int64 0`. This is the only synchronization guarantee: all memory writes performed by spawned contexts are visible after `wait`. |
+
+Racy access without `wait` yields unspecified values of the cell's type
+(no tearing for aligned ≤ 8-byte cells).
+
+## 7. Arithmetic and comparison typing
+
+* integer literal + any numeric → the numeric type (literal must fit, or
+ `convert` first; float targets always accept).
+* two integer literals → `int64`.
+* same type → that type.
+* integer types of different width/sign → the wider type; mixed
+ signedness becomes signed of the wider width. `uint64` vs signed uses
+ the signed type of width 64; explicit `convert` is recommended.
+* any integer + float → float (float32 only when both are float32).
+* `char` participates as an unsigned 32-bit value.
+* `bool` compares only with `bool` (and literals 0/1).
+* addresses compare with addresses (unsigned) or integer literals.
+
+## 8. Transformations, calls, references
+
+* `define name { type param } result T? ... end`
+* Arguments: push values in parameter order (first parameter deepest),
+ then `call name` — the call pops the parameters. Callee working value
+ starts as `nothing`.
+* Result: the callee's working value at return becomes the caller's
+ working value unless the result type is `nothing` (then the caller's
+ working value is preserved). Result types are inferred from the first
+ syntactic `return`; all return points must agree; recursion requires an
+ explicit `result <type>` declaration.
+* The value stack must be empty at every return.
+* Recursion, mutual recursion, and nested calls are ordinary C-stack
+ recursion — one frame per call.
+* `address name-of-transformation` yields a `transform` reference value.
+ `call` without a name calls the referenced transformation; the
+ reference must denote a zero-parameter transformation (checked
+ statically when known, at runtime otherwise). The result of an indirect
+ call is `nothing` in v1; return data through world cells or memory.
+ Transformations can be passed as arguments (type `transform`) —
+ callbacks.
+
+## 9. Numeric summary
+
+| Situation | Behavior |
+|---|---|
+| signed overflow | wrap (two's complement) |
+| unsigned overflow | wrap modulo 2^N |
+| `/` or `%` by zero | runtime abort, exit 1 |
+| `INT_MIN / -1` | `INT_MIN` (wrap) |
+| float → int out of range | saturate at target width |
+| int → float | round to nearest |
+| index out of bounds | runtime abort, exit 1 |
+| freeing a non-heap address | runtime abort, exit 1 |
+| negative dynamic allocation size | runtime abort, exit 1 |
+
+## 10. Grammar (informal)
+
+```text
+command :=
+ "move" ( name | int | "index" | "at" )
+ | "back" | "home"
+ | "address" [ name ]
+ | "make" ( type [name]
+ | "array" int "of" type [name]
+ | "string" str [name]
+ | (name|int) "of" type [name] )
+ | "free" | "read" | "write"
+ | "set" (int|float|char|true|false|nothing)
+ | ("add"|"subtract"|"multiply"|"divide"|"remainder") [int|float]
+ | "negate"
+ | "push" | "pop" | "swap"
+ | "compare" [int|float|char|true|false]
+ | "convert" type
+ | "jump" name [ "when" cond ]
+ | "call" [name]
+ | "return" | "stop"
+ | "input" | "output" ["text"]
+ | "open" ("read"|"write"|"both") | "close" | "system"
+ | "spawn" name | "wait"
+ | "mark" name
+
+type := basetype | "address" [type] | "array" [int] "of" type
+cond := "less"|"equal"|"greater"|"different"|"less-or-equal"|"greater-or-equal"
+```
+
+## 11. Errors
+
+Compile errors report file, line, column, and message; the first error
+stops compilation of a transformation. Runtime errors print
+`cryn: runtime error: <message>` to stderr and exit with status 1.
+Defined runtime errors: division by zero, remainder by zero, index out
+of bounds, free of a non-heap address, negative or oversized allocation,
+focus history overflow, spawn limit exceeded, indirect call of a
+parameterized transformation, thread creation failure, out of memory.
+
+## 12. Limits
+
+| Limit | Value |
+|---|---|
+| focus history | 256 per frame |
+| value stack depth (static) | 1024 |
+| cells per transformation | 256 |
+| labels per transformation | 256 |
+| outstanding spawns per context | 64 |
+| open files per context | 64 |
+| dynamic allocation | ≤ 2^40 bytes |
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6999493
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+# The Cryn Programming Language - bootstrap compiler build
+#
+# make build the cryn compiler
+# make test run the test suite
+# make clean remove build artifacts
+
+CC ?= cc
+CFLAGS ?= -O2 -std=c11 -Wall -Wextra -Wno-unused-parameter
+BIN = cryn
+
+SRCS = src/main.c src/util.c src/diag.c src/lex.c src/parse.c src/types.c \
+ src/check.c src/opt.c src/be_c.c
+HDRS = src/util.h src/diag.h src/lex.h src/ast.h src/parse.h src/types.h \
+ src/ir.h src/check.h src/opt.h src/be_c.h src/rt_src.h
+
+all: $(BIN)
+
+tools/embed: tools/embed.c
+ $(CC) $(CFLAGS) -o $@ $<
+
+src/rt_src.h: tools/embed src/rt/cryn_rt.h src/rt/cryn_rt.c
+ ./tools/embed src/rt/cryn_rt.h cr_rt_h_src src/rt/cryn_rt.c cr_rt_c_src > $@
+
+$(BIN): $(SRCS) $(HDRS)
+ $(CC) $(CFLAGS) -o $(BIN) $(SRCS)
+
+test: $(BIN)
+ sh tests/run.sh
+
+clean:
+ rm -f $(BIN) tools/embed src/rt_src.h
+
+.PHONY: all test clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..138b779
--- /dev/null
+++ b/README.md
@@ -0,0 +1,232 @@
+# The Cryn Programming Language
+
+Cryn is a compiled systems programming language built on a very small and
+unusual core: **cells, connections, focus, a working value, and a value
+stack**. There are no symbolic operators, no braces, no semicolons, and no
+indentation rules. Programs read like small English sentences describing
+operations on a machine:
+
+```text
+define main
+ set 10
+ push
+ set 20
+ add
+ output
+ return
+end
+```
+
+This compiles to a native executable through portable generated C:
+
+```text
+Cryn source -> lexer -> parser -> AST -> semantic analysis -> Cryn IR
+ -> optimizer -> C backend -> host C compiler -> executable
+```
+
+The bootstrap compiler (`cryn`) is written in portable ISO C11 and has no
+dependencies beyond a C compiler and pthreads.
+
+---
+
+## Design philosophy
+
+> C-level power through a radically small set of composable primitives.
+
+* **34 commands total.** No `if`, `while`, `for`, `switch`, `array`,
+ `struct`, `class`, or `pointer` keywords. Control flow is explicit
+ jumps; arrays are indexed connections; structures are blocks of memory;
+ pointers are address values.
+* **Whitespace has no meaning.** Every program can be written on one line.
+ Blocks end with `end`.
+* **One comment syntax:** `[# ... #]`, possibly spanning lines.
+* **English words instead of symbols:** `add`, `compare`, `jump ... when
+ less`, `move`, `write`.
+* **The graph model is the semantics; the compiler lowers it.** Cells and
+ connections are what the language means; the compiler compiles them into
+ ordinary locals, arrays, and pointer arithmetic, so Cryn code runs at
+ native speed with no graph overhead.
+* **Static typing with defined behavior.** Fixed-width integer and float
+ types, defined overflow (wrap), defined division by zero (diagnostic
+ abort), saturating float-to-int conversion, bounds-checked indexing.
+* **No hidden runtime.** The runtime support layer is a few hundred lines:
+ allocation headers, context structs, thread spawn/join, and output
+ formatting. Every dependency is documented in this README.
+
+See `LANGUAGE.md` for the formal language reference and `TUTORIAL.md` for
+a course from first principles.
+
+## Repository layout
+
+```text
+src/
+ main.c CLI entry point (build / run / check / emit-c / emit-ir)
+ util.[ch] buffers, lists, file I/O
+ diag.[ch] diagnostics (file:line:col reporting)
+ lex.[ch] lexer: keywords, literals, comments, locations
+ ast.h AST node definitions
+ parse.[ch] recursive-descent parser (formatting independent)
+ types.[ch] interned type objects, sizes, classifications
+ check.[ch] semantic analysis: abstract machine interpretation,
+ produces fully typed IR (src/check.c is the semantic heart)
+ ir.h Cryn IR instruction definitions
+ opt.[ch] IR cleanup: unreachable code and dead label removal
+ be_c.[ch] C backend: emits a self-contained C11 translation unit
+ rt/
+ cryn_rt.h minimal runtime interface (embedded into output)
+ cryn_rt.c runtime implementation
+ rt_src.h generated: runtime sources as strings (tools/embed)
+tools/
+ embed.c turns runtime sources into string headers
+tests/
+ *.cryn test programs
+ expected/ expected outputs
+ input/ stdin fixtures
+ errors/ programs that must fail to compile
+ run.sh test runner
+examples/ hello, fib, fizzbuzz, sorting, wordcount, linkedlist,
+ allocator
+```
+
+The frontend (lexer/parser/AST), the semantic layer (types/check), the IR,
+and the backend (be_c) are cleanly separated. A native code backend can be
+added by translating `src/ir.h` instructions directly; nothing in the
+semantic layer is tied to C generation.
+
+## Building
+
+```sh
+make # builds ./cryn
+make test # runs the test suite
+make clean
+```
+
+Requirements: any ISO C11 compiler and POSIX `make`. pthreads is required
+for `spawn` / `wait` support.
+
+## Using the compiler
+
+```sh
+cryn hello.cryn # compile to executable "hello"
+cryn build hello.cryn -o out # compile to executable "out"
+cryn run hello.cryn # compile and execute
+cryn check hello.cryn # type check only
+cryn emit-c hello.cryn -o out.c # write the generated C
+cryn emit-ir hello.cryn # print the Cryn IR
+cryn help
+cryn version
+```
+
+`cryn` invokes the host C compiler (`$CC`, default `cc`) with
+`-O2 -std=c11 -pthread` to turn generated C into an executable. Generated
+programs are normal executables and need no Cryn runtime at runtime; the
+runtime is compiled into every generated translation unit.
+
+## How the compiler works
+
+1. **Lexing** (`lex.c`): comments are stripped as `[# ... #]`; whitespace
+ (including newlines) is skipped everywhere; line/column tracked for
+ diagnostics.
+2. **Parsing** (`parse.c`): recursive descent over the token stream.
+ Transformation parameters are `{ type name }` pairs terminated by the
+ first non-type token, which keeps parsing unambiguous without any
+ punctuation.
+3. **Semantic analysis** (`check.c`): an abstract interpreter over the
+ Cryn machine. It tracks the static type of the working value, the
+ abstract focus stack, the value stack shape, and comparison state, and
+ merges these states at every label. All merges must agree exactly
+ (integer literals widen to the type they fit). It also infers result
+ types, resolves all names, checks argument counts/types, and emits the
+ fully typed IR in one pass. Recursive transformations with unclear
+ results must declare `result <type>`.
+4. **IR** (`ir.h`): ~50 typed instructions (navigation, value ops, memory
+ access, comparisons, calls, external operations).
+5. **Optimizer** (`opt.c`): removes unreachable code and unreferenced
+ labels. (Deliberately small; the C compiler performs the heavy lifting
+ on the readable C we emit.)
+6. **C backend** (`be_c.c`): each transformation becomes a C function;
+ frame cells become C locals; `move` becomes pointer assignment; value
+ stack and focus history live in a per-thread context struct. The
+ runtime sources are embedded and emitted into every translation unit,
+ making the output a single self-contained `.c` file.
+
+## Runtime dependencies (complete list)
+
+The runtime (`src/rt/cryn_rt.*`) provides only:
+
+| Facility | Used by | Notes |
+|-----------------|----------------------------------|--------------------------------|
+| `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for validation and bounds checks |
+| value stack | `push`, `pop`, `swap`, `call` | growable array, per thread |
+| focus history | `move` (relative), `back`, `home`| fixed 256 entries per frame |
+| FILE* streams | `input`, `output`, `open`, `close`| stdin/stdout by default |
+| `system()` | `system` | shell escape |
+| pthreads | `spawn`, `wait` | join provides the only ordering guarantee |
+| formatting | `output` | shortest round-trip float printing |
+
+There is no garbage collector, no interpreter, and no mandatory runtime
+library beyond the above.
+
+## Testing
+
+```sh
+make test
+```
+
+35 checks: arithmetic (including overflow wrap and division edge cases),
+unsigned arithmetic, floats, all six comparison conditions, branches,
+loops, nested loops, functions, recursion, mutual recursion, the value
+stack, cells and focus navigation, arrays (sieve of Eratosthenes),
+strings, manual memory, pointer arithmetic, linked lists, callbacks
+through transform references, file I/O, threads (`spawn`/`wait`), a word
+counter, bubble sort, a free-list allocator, and ten programs that must
+fail to compile.
+
+## Current limitations
+
+These are honest boundaries of the first implementation, not pretenses:
+
+* **Backend is C.** A native x86-64 backend is planned; the IR is ready
+ for it.
+* **Concurrency is minimal.** `spawn` starts a thread running a
+ zero-parameter transformation; `wait` joins all spawned contexts of the
+ current one. Shared world cells may be accessed concurrently; v1 gives
+ no atomicity guarantees beyond `wait` acting as a full join/sync point.
+ Communication of results happens through shared world cells.
+* **Indirect calls** (through `transform` references) are only allowed to
+ zero-parameter transformations; checked at runtime when the reference is
+ not statically known.
+* **`open` sets the context's current input/output stream** rather than
+ exposing a first-class handle type; `close` uses the last handle value.
+ One stream pair per context at a time.
+* **Bounds checks abort** with a diagnostic. A release mode that removes
+ them is future work.
+* **Fixed limits:** 256 focus history entries, 256 labels, 256 cells per
+ transformation, 64 concurrent spawns per context, 64 open files per
+ context.
+* **No separate compilation** yet: one `.cryn` file per program.
+* **The compiler itself is not yet written in Cryn.** Self-hosting is the
+ long-term goal; the architecture (small passes, plain data structures,
+ no frameworks) was chosen for it.
+
+## Contributing
+
+* Keep the command count at or below 40. Before adding anything, prove it
+ cannot be expressed with the existing primitives.
+* Keep the compiler portable ISO C11; isolate platform specifics in
+ `src/rt/`.
+* Every language change needs: a test in `tests/`, updated `LANGUAGE.md`
+ and `TUTORIAL.md`, and a note in this README if it affects the runtime
+ surface.
+* Run `make test` before committing.
+
+## Future work
+
+1. Native x86-64 backend (direct IR lowering; the C backend remains the
+ portable reference).
+2. Self-hosting: rewrite the compiler in Cryn.
+3. Release mode without bounds checks; compile-time elimination of
+ statically provable checks.
+4. First-class file handles; more `open` modes.
+5. Atomics or another minimal, honest concurrency primitive if a use case
+ cannot be met with `spawn`/`wait`.
diff --git a/TUTORIAL.md b/TUTORIAL.md
new file mode 100644
index 0000000..64fe07f
--- /dev/null
+++ b/TUTORIAL.md
@@ -0,0 +1,769 @@
+# Learning Cryn: A Tutorial
+
+This tutorial teaches Cryn from zero. It explains the machine, not just
+the commands. Every example compiles with the current `cryn` compiler.
+
+---
+
+## 1. Hello, machine
+
+```text
+define main
+ set 10
+ push
+
+ set 20
+ add
+
+ output
+
+ return
+end
+```
+
+Run it:
+
+```sh
+cryn run hello.cryn
+```
+
+Output:
+
+```text
+30
+```
+
+What happened?
+
+* `define main ... end` defines a **transformation** — Cryn's unit of
+ computation. Execution starts at `main`.
+* `set 10` puts 10 into the **working value**: the one value your
+ context is currently holding.
+* `push` copies the working value onto the **value stack** so it
+ survives while we use the working value for something else.
+* `set 20` replaces the working value with 20.
+* `add` pops the stack and adds it to the working value: 20 + 10 = 30.
+* `output` writes the working value to stdout (integers print in
+ decimal).
+* `return` ends the transformation.
+
+Formatting never matters. This is the same program:
+
+```text
+define main set 10 push set 20 add output return end
+```
+
+So is this:
+
+```text
+define
+main
+set
+10
+push
+set
+20
+add
+output
+return
+end
+```
+
+Comments are `[# ... #]` and may span lines. They are the only comment
+syntax:
+
+```text
+set 10 [# working value is ten #]
+```
+
+## 2. The machine you are driving
+
+Every running transformation is an **execution context** containing:
+
+1. A **working value** — one typed value, the thing commands operate on.
+2. A **value stack** — for saving values temporarily.
+3. A **focus** — a stack of currently selected **cells**; the bottom of
+ the stack is the **frame root** (the transformation's own namespace).
+4. A **comparison state** — set by `compare`, tested by conditional
+ jumps.
+
+The **world** is the collection of cells. Some cells belong to your
+frame (locals and parameters). Some belong to everyone (world cells,
+made at the top level). Blocks of raw memory are cells too.
+
+Data flows through three registers of attention:
+
+```text
+world cells <--move/read/write--> focus
+working value <--set/read/push/pop/arith--> value stack / literals
+```
+
+## 3. Cells: make, move, read, write
+
+A cell is a named place that holds one value:
+
+```text
+define main
+ make int64 x [# a cell named x, holding an int64, starting at 0 #]
+
+ set 41
+ move x [# focus the cell x #]
+ write [# x := working value (41) #]
+
+ move x
+ read [# working value := x #]
+ add 1
+ move x
+ write [# x := 42 #]
+
+ move x
+ read
+ output
+ return
+end
+```
+
+This prints `42`.
+
+* `make int64 x` creates cell `x` in the current frame. Executing the
+ `make` again re-zeroes it.
+* `move x` follows the **named connection** `x` from the frame root and
+ focuses the cell. `move` with a name always resolves from the frame
+ root, and it **anchors** the focus: the previous focus is replaced.
+* `read` copies the focused cell into the working value.
+* `write` copies the working value into the focused cell.
+
+Cell types are checked at compile time. Try writing a string address
+into an `int64` cell and the compiler refuses.
+
+### All the integer types
+
+```text
+int8 int16 int32 int64 signed
+uint8 uint16 uint32 uint64 unsigned
+float32 float64 floats
+bool char nothing
+```
+
+`make uint8 c`, `make float32 f`, `make bool b` — all work the same.
+`set 'A'` is a character literal; `set true` a boolean.
+
+## 4. Arithmetic
+
+Arithmetic works on the working value, either with a literal operand or
+by popping the stack:
+
+```text
+set 2
+add 3 [# 5 ]
+multiply 4 [# 20 ]
+subtract 5 [# 15 ]
+
+set 100
+push
+set 7
+divide [# pops 100, working value 7 -> 0.07? No: integer 7/100 is 0 ]
+```
+
+Careful: `divide` combines *working value* with *popped value*: the
+popped value is the right-hand operand. `100 pushed, working value 7`
+computes `7 / 100`.
+
+The exact rules (LANGUAGE.md §5, §7):
+
+* Integer overflow **wraps** in both directions. `set 9223372036854775807
+ add 1` gives `-9223372036854775808`.
+* Division by zero aborts the program with a diagnostic. `-7 / 2` is
+ `-3` (truncates toward zero).
+* Mixing widths: the wider type wins; mixed signedness becomes signed.
+ Different types otherwise require `convert`:
+ `set 1 convert float32 divide 3`.
+* `negate` flips the sign.
+
+```text
+set 1.5
+add 2.25 [# 3.75 ]
+multiply 2 [# 7.5 ]
+output [# prints 7.5 ]
+```
+
+Floats print as the shortest decimal that reads back exactly.
+
+## 5. Comparison and branching
+
+There is no `if`. There is `compare` and conditional `jump`:
+
+```text
+set 10
+compare 20
+jump smaller when less
+set 'N'
+jump done
+
+mark smaller
+set 'Y'
+
+mark done
+output
+```
+
+`compare 20` compares the working value (10) with 20 and sets the
+comparison state. `jump smaller when less` jumps when the state is
+`less`. Conditions: `less equal greater different less-or-equal
+greater-or-equal`.
+
+`compare` without an operand pops the stack:
+
+```text
+move a
+read
+push
+move b
+read
+compare [# b (working) vs a (popped) #]
+```
+
+Labels (`mark`) are transformation-scoped names. Any jump may go forward
+or backward.
+
+### The pattern for if/else
+
+```text
+compare 0
+jump negative when less
+set 1 [# then-branch #]
+jump end
+mark negative
+set 0 [# else-branch #]
+mark end
+```
+
+## 6. Loops
+
+A loop is a backward jump. Both of these print the numbers 0 through 4:
+
+```text
+make int64 i
+make int64 n
+
+set 0
+move i
+write
+set 5
+move n
+write
+set 0
+
+mark loop
+move i
+read
+compare 5
+jump done when greater-or-equal
+move i
+read
+push
+call newline
+pop
+output
+
+move i
+read
+add 1
+move i
+write
+set 0
+
+jump loop
+mark done
+```
+
+### The one rule you must know about loops
+
+The checker tracks the working value type, the focus, and the value
+stack at every label. **Every path that reaches a label must agree.**
+The back edge of a loop and the loop's entry must carry:
+
+* the same working value **type** (integer literals widen to the type
+ they fit),
+* the same focus,
+* the same value stack shape.
+
+The reliable idiom: before entering a loop and before jumping back,
+normalize with `set 0` and `home`:
+
+```text
+set 0
+home
+mark loop
+ ...
+ set 0
+ home
+ jump loop
+```
+
+If you forget, the compiler tells you exactly where the two paths
+disagree — for example *"working value type differs between paths (0 vs
+int64); normalize it with 'set'"*.
+
+(This strictness is deliberate: it keeps every program point typed and
+lets the compiler emit plain C locals.)
+
+## 7. The value stack: push, pop, swap
+
+The stack exists to preserve values while the working value does
+something else:
+
+```text
+set 1
+push [# stack: 1 ]
+set 2
+add [# pops 1: working value 3 ]
+```
+
+* `push` copies the working value to the stack (the working value is
+ unchanged).
+* `pop` brings the top back into the working value.
+* `swap` exchanges working value and stack top.
+
+The stack must be empty at `return`. The compiler enforces this; there
+are no leaks.
+
+## 8. Transformations: parameters and results
+
+```text
+define gcd int64 a int64 b result int64
+ mark loop
+ move b
+ read
+ compare 0
+ jump done when equal
+
+ move b
+ read
+ push
+ move b
+ read
+ push
+ move a
+ read
+ remainder
+ push
+ move b
+ write
+ pop
+ pop
+ move a
+ write
+ set nothing
+ home
+ jump loop
+
+ mark done
+ move a
+ read
+ return
+end
+
+define main
+ set 252
+ push
+ set 105
+ push
+ call gcd
+ output
+ return
+end
+```
+
+This prints `21`.
+
+* Parameters are `{ type name }` pairs after the transformation name.
+ They are ordinary frame cells.
+* **Arguments are pushed in parameter order** (first parameter deepest);
+ `call` pops them into the parameter cells.
+* The **result is the working value at return**. `result int64` declares
+ it; without a declaration it is inferred from the first `return`.
+ Recursive transformations must declare `result`.
+* After a call, the caller's working value is the callee's result —
+ unless the result is `nothing`, in which case the caller's working
+ value is preserved.
+* A helper that returns nothing should end with `set nothing` before
+ `return` (a stray value would become its result type):
+
+```text
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+```
+
+Calls may recurse (one real frame per call); mutual recursion just
+works.
+
+### Transform references and callbacks
+
+`address transformation-name` produces a `transform` reference value.
+`call` without a name calls it. References can be passed as arguments:
+
+```text
+define ping result nothing
+ make string "ping
+" msg
+ address msg
+ output text
+ set nothing
+ return
+end
+
+define twice transform f result nothing
+ move f
+ read
+ push
+ call
+ pop
+ call
+ set nothing
+ return
+end
+
+define main
+ address ping
+ push
+ call twice
+ return
+end
+```
+
+(Indirect calls target zero-parameter transformations in this version.)
+
+## 9. Arrays: indexed connections
+
+An array is a cell with indexed connections. Create with
+`make array N of T name`:
+
+```text
+make array 5 of int64 a
+```
+
+Two ways to reach an element:
+
+```text
+[# static index #]
+set 7
+move a
+move 0 [# element 0 #]
+write
+
+[# dynamic index: put the index in the working value FIRST #]
+move i
+read [# working value = i #]
+move a [# focus array; working value survives named moves #]
+move index [# element i; uses the working value as index #]
+read
+```
+
+Out-of-bounds indexing aborts the program with a diagnostic (the checker
+catches constant indices at compile time).
+
+The full array idiom, summed up: **index into the working value, then
+focus, then `move index`**. If you need the index afterwards, push it
+first and pop after.
+
+## 10. Strings
+
+A string is a byte array with a terminating NUL — nothing more:
+
+```text
+make string "Hello, World!" msg
+address msg
+output text
+```
+
+* `make string "..." msg` creates a byte-array cell.
+* `make string "..."` (no name) points the working value at static
+ NUL-terminated bytes.
+* `output text` writes bytes until the NUL.
+
+To process text, loop over bytes:
+
+```text
+make string "interrupt" word
+make int64 n
+set 0
+move n
+write
+set 0
+
+mark count
+move n
+read
+move word
+move index
+read
+compare 0
+jump counted when equal
+move n
+read
+add 1
+move n
+write
+set 0
+jump count
+mark counted
+```
+
+`input` reads one byte (returns -1 at end of input); `output` of a
+`uint8` or `char` writes one raw byte. That is the whole I/O vocabulary;
+word counters and parsers are built from it (see `examples/`).
+
+## 11. Memory: blocks and addresses
+
+Manual memory, no garbage collector:
+
+```text
+make 4 of int64 block [# heap block of 4 int64s, zeroed #]
+```
+
+After a dynamic `make`, the working value holds the block's address
+(type `address array of int64`). Two idioms:
+
+```text
+[# write element 0 immediately: the address is still in the working value #]
+move at [# focus the block #]
+set 40
+move 0
+write
+back [# back to the block #]
+
+[# write element i for a computed i #]
+move i
+read
+push [# save the index #]
+move block
+read [# working value := block address #]
+move at [# focus the block #]
+pop [# restore the index #]
+move index [# focus element i #]
+set 41
+write
+```
+
+`free` releases a heap block:
+
+```text
+move block
+read
+free
+```
+
+### Addresses are values
+
+* `address x` — working value := the address of cell `x` (or of a
+ transformation: a `transform` reference).
+* `address` (no name) — the address of the focused cell.
+* `move at` — focus the cell an address points to.
+* `add 1` / `subtract 1` on an address moves by **one element**
+ (scaled by the element size), like pointer arithmetic in C.
+* Converting between `int64`/`uint64` and addresses reinterprets bits —
+ that is how you store links inside homogeneous blocks.
+
+Addresses compare (`compare`) as unsigned numbers; `compare 0` tests
+for the null address.
+
+## 12. Structures and linked lists
+
+There is no `struct` command. A structure is a block of memory; fields
+are elements; links are addresses stored as integers. A linked list node
+`[ value next ]` is a block of two `int64`s:
+
+```text
+make 2 of int64 n1 [# value=10, next -> n2 #]
+make 2 of int64 n2 [# value=20, next = 0 #]
+make 2 of int64 cur
+
+move n2
+read
+move at
+set 20
+move 0
+write
+back
+set 0
+move 1
+write
+back
+
+move n2
+read
+convert int64 [# the link, as a number #]
+push
+move n1
+read
+move at
+set 10
+move 0
+write
+back
+move 1
+pop
+write
+back
+```
+
+Walking means converting the stored link back to an address:
+
+```text
+move cur
+read
+move at
+move 1
+read [# the next link #]
+convert address array of int64
+move cur
+write
+```
+
+The complete working program is `examples/linkedlist.cryn`.
+
+## 13. Files and the outside world
+
+Each context has a current input and output stream (stdin/stdout by
+default). `open` reroutes them through a file:
+
+```text
+make string "out.txt" path
+address path
+open write [# create/truncate; negative result = failure #]
+compare 0
+jump failed when less
+
+set 'H'
+output [# goes to the file #]
+
+close
+stop
+
+mark failed
+stop
+```
+
+`open read` reroutes input; `input` then reads the file byte by byte
+until it returns `-1`. `open both` opens for reading and writing.
+`system` runs a shell command:
+
+```text
+make string "touch cryn.txt" cmd
+address cmd
+system
+```
+
+There are no per-file read/write commands and no sockets in v1 — one
+context tracks one stream pair at a time (documented limitation). Files,
+networks, and devices are all expected to grow from this small surface.
+
+## 14. Concurrency
+
+```text
+make int64 counter
+
+define worker
+ make int64 k
+ set 0
+ move k
+ write
+ mark w
+ move k
+ read
+ compare 100
+ jump done when greater-or-equal
+ move counter
+ read
+ add 1
+ move counter
+ write
+ set 0
+ move k
+ read
+ add 1
+ move k
+ write
+ set 0
+ jump w
+ mark done
+ return
+end
+
+define main
+ spawn worker
+ spawn worker
+ wait
+ move counter
+ read
+ output
+ return
+end
+```
+
+`spawn name` starts a new context (a real thread) running a
+zero-parameter transformation. Contexts share world cells and heap
+blocks — that is how they communicate. `wait` joins everything this
+context spawned and makes all their writes visible. Without `wait` there
+are no ordering guarantees. This is a minimal, honest model: v1 has no
+locks or atomics.
+
+## 15. A real program
+
+`examples/wordcount.cryn` counts lines, words, and characters from
+stdin using: world cells, a bump helper that increments a cell through
+its address, byte classification with `compare`, and normalization at
+loop labels. Run it:
+
+```sh
+cryn run examples/wordcount.cryn < examples/wordcount.cryn
+```
+
+Other complete programs in `examples/`: `sorting.cryn` (bubble sort over
+a heap block with element helpers), `allocator.cryn` (a free-list
+allocator), `linkedlist.cryn`, `fib.cryn`, `fizzbuzz.cryn`.
+
+## 16. When the compiler argues with you
+
+The checker is strict so that the machine stays simple. The errors you
+will meet and what they mean:
+
+* *"working value type differs between paths"* — normalize with `set`
+ at loop entries and back edges.
+* *"focus depth/type differs between paths"* — add `home` (or `back`)
+ before jumps into shared labels.
+* *"the value stack must be empty at return"* — you pushed something
+ and never popped it.
+* *"cannot write X into a cell of type Y"* — types match exactly or you
+ `convert` first (literals adapt automatically when they fit).
+* *"unreachable command"* — code after an unconditional jump with no
+ label bringing it back to life. Delete it or label it.
+* *"'move index' requires an integer working value"* — get the index
+ into the working value *before* focusing the array (§9).
+
+## 17. The whole language
+
+You have now seen every mechanism. Thirty-four commands:
+
+```text
+move back home address make free read write
+set add subtract multiply divide remainder negate
+push pop swap compare convert
+jump call return stop
+input output open close system
+spawn wait define end mark
+```
+
+Everything else — control flow, arrays, structures, strings, pointers,
+callbacks — is patterns built from these. When you wonder "how do I do X
+in Cryn?", ask instead: *"what does the machine have to do to make X
+happen?"* — and write that.
+
+Welcome to the machine.
diff --git a/examples/allocator.cryn b/examples/allocator.cryn
new file mode 100644
index 0000000..8a999bb
--- /dev/null
+++ b/examples/allocator.cryn
@@ -0,0 +1,182 @@
+[# a small fixed-slot free-list allocator built on a raw block #]
+make string "\n" nl
+make int64 head
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define getslot address array of int64 blk int64 idx result int64
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ read
+ return
+end
+
+define setslot address array of int64 blk int64 idx int64 val result nothing
+ move val
+ read
+ push
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ pop
+ write
+ set nothing
+ return
+end
+
+define alloc address array of int64 blk result int64
+ move head
+ read
+ push
+ move blk
+ read
+ push
+ move head
+ read
+ push
+ call getslot
+ move head
+ write
+ pop
+ return
+end
+
+define dealloc address array of int64 blk int64 slot result nothing
+ move blk
+ read
+ push
+ move slot
+ read
+ push
+ move head
+ read
+ push
+ call setslot
+ move slot
+ read
+ move head
+ write
+ set nothing
+ return
+end
+
+define putval int64 v result nothing
+ move v
+ read
+ output
+ call newline
+ set nothing
+ return
+end
+
+define main
+ make 17 of int64 pool
+ make int64 i
+
+ set 1
+ move i
+ write
+ set 0
+ home
+
+ mark build
+ move i
+ read
+ compare 16
+ jump capped when greater-or-equal
+ set 0
+ home
+
+ move pool
+ read
+ push
+ move i
+ read
+ push
+ move i
+ read
+ add 1
+ push
+ call setslot
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ home
+ jump build
+
+ mark capped
+ move pool
+ read
+ push
+ set 16
+ push
+ set 0
+ push
+ call setslot
+
+ set 1
+ move head
+ write
+ set 0
+ home
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ set 2
+ push
+ call dealloc
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ free
+ return
+end
diff --git a/examples/fib.cryn b/examples/fib.cryn
new file mode 100644
index 0000000..1796bb3
--- /dev/null
+++ b/examples/fib.cryn
@@ -0,0 +1,70 @@
+[# iterative fibonacci: prints fib(1) .. fib(12) #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ make int64 a
+ make int64 b
+ make int64 t
+ make int64 n
+
+ set 1
+ move a
+ write
+ set 1
+ move b
+ write
+ set 12
+ move n
+ write
+ set 0
+ home
+
+ mark loop
+ move n
+ read
+ compare 0
+ jump done when less-or-equal
+ set 0
+ home
+
+ move a
+ read
+ output
+ call newline
+
+ move b
+ read
+ push
+ move a
+ read
+ push
+ move b
+ read
+ add
+ push
+ move b
+ write
+ pop
+ pop
+ move a
+ write
+
+ move n
+ read
+ subtract 1
+ move n
+ write
+ set 0
+ home
+ jump loop
+
+ mark done
+ return
+end
diff --git a/examples/fizzbuzz.cryn b/examples/fizzbuzz.cryn
new file mode 100644
index 0000000..b855057
--- /dev/null
+++ b/examples/fizzbuzz.cryn
@@ -0,0 +1,101 @@
+make string "\n" nleol
+
+define nl result nothing
+ address nleol
+ output text
+ set nothing
+ return
+end
+
+[# fizzbuzz 1..30: no strings needed, just markers #]
+define main
+ make int64 i
+ make int64 fizz
+ make int64 buzz
+
+ set 1
+ move i
+ write
+
+ mark loop
+ move i
+ read
+ compare 30
+ jump done when greater
+ set 0
+ home
+
+ move i
+ read
+ remainder 3
+ move fizz
+ write
+
+ move i
+ read
+ remainder 5
+ move buzz
+ write
+ set 0
+ home
+
+ move fizz
+ read
+ home
+ compare 0
+ jump checkBuzz when equal
+ move buzz
+ read
+ home
+ compare 0
+ jump printB when equal
+ move i
+ read
+ output
+ home
+ call nl
+ set 0
+ jump next
+
+ mark checkBuzz
+ move buzz
+ read
+ home
+ compare 0
+ jump printFB when equal
+ set 'F'
+ output
+ home
+ call nl
+ set 0
+ jump next
+
+ mark printB
+ set 'B'
+ output
+ home
+ call nl
+ set 0
+ jump next
+
+ mark printFB
+ set 'F'
+ output
+ set 'B'
+ output
+ home
+ call nl
+ set 0
+
+ mark next
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ jump loop
+
+ mark done
+ return
+end
diff --git a/examples/hello.cryn b/examples/hello.cryn
new file mode 100644
index 0000000..1a99309
--- /dev/null
+++ b/examples/hello.cryn
@@ -0,0 +1,14 @@
+[# the canonical first program from the language definition #]
+define main
+
+set 10
+push
+
+set 20
+add
+
+output
+
+return
+
+end
diff --git a/examples/linkedlist.cryn b/examples/linkedlist.cryn
new file mode 100644
index 0000000..57d319e
--- /dev/null
+++ b/examples/linkedlist.cryn
@@ -0,0 +1,123 @@
+[# a linked list built from raw blocks: node = [value next] #]
+make string "\n" nl
+make int64 total
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ make 2 of int64 n1
+ make 2 of int64 n2
+ make 2 of int64 n3
+ make 2 of int64 cur
+
+ move n3
+ read
+ move at
+ set 30
+ move 0
+ write
+ back
+ set 0
+ move 1
+ write
+ back
+
+ move n3
+ read
+ convert int64
+ push
+ move n2
+ read
+ move at
+ set 20
+ move 0
+ write
+ back
+ move 1
+ pop
+ write
+ back
+
+ move n2
+ read
+ convert int64
+ push
+ move n1
+ read
+ move at
+ set 10
+ move 0
+ write
+ back
+ move 1
+ pop
+ write
+ back
+
+ set 0
+ move total
+ write
+
+ move n1
+ read
+ push
+ move cur
+ write
+ set 0
+ move total
+ write
+ pop
+ home
+
+ mark walk
+ move cur
+ read
+ compare 0
+ jump done when equal
+
+ move cur
+ read
+ move at
+ move 0
+ read
+ output
+ call newline
+ push
+ move total
+ read
+ add
+ move total
+ write
+
+ move cur
+ read
+ move at
+ move 1
+ read
+ convert address array of int64
+ move cur
+ write
+ home
+ jump walk
+
+ mark done
+ move total
+ read
+ output
+
+ move n1
+ read
+ free
+ move n2
+ read
+ free
+ move n3
+ read
+ free
+ return
+end
diff --git a/examples/sorting.cryn b/examples/sorting.cryn
new file mode 100644
index 0000000..12ff43f
--- /dev/null
+++ b/examples/sorting.cryn
@@ -0,0 +1,287 @@
+[# bubble sort over a heap block of int64, using element helpers #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define getelem address array of int64 blk int64 idx result int64
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ read
+ return
+end
+
+define setelem address array of int64 blk int64 idx int64 val result nothing
+ move val
+ read
+ push
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ pop
+ write
+ set nothing
+ return
+end
+
+define load address array of int64 blk int64 idx int64 val result nothing
+ move blk
+ read
+ push
+ move idx
+ read
+ push
+ move val
+ read
+ push
+ call setelem
+ set nothing
+ return
+end
+
+define put int64 v result nothing
+ move v
+ read
+ output
+ call newline
+ set nothing
+ return
+end
+
+define main
+ make 8 of int64 a
+ make int64 i
+ make int64 j
+ make int64 tmp
+
+ move a
+ read
+ push
+ set 0
+ push
+ set 42
+ push
+ call load
+
+ move a
+ read
+ push
+ set 1
+ push
+ set 17
+ push
+ call load
+
+ move a
+ read
+ push
+ set 2
+ push
+ set 99
+ push
+ call load
+
+ move a
+ read
+ push
+ set 3
+ push
+ set 3
+ push
+ call load
+
+ move a
+ read
+ push
+ set 4
+ push
+ set 58
+ push
+ call load
+
+ move a
+ read
+ push
+ set 5
+ push
+ set 21
+ push
+ call load
+
+ move a
+ read
+ push
+ set 6
+ push
+ set 7
+ push
+ call load
+
+ move a
+ read
+ push
+ set 7
+ push
+ set 65
+ push
+ call load
+
+ set 7
+ move i
+ write
+ set 0
+ home
+
+ mark outer
+ move i
+ read
+ compare 0
+ jump sorted when less
+ set 0
+ home
+
+ set 0
+ move j
+ write
+ set 0
+ home
+
+ mark inner
+ move i
+ read
+ push
+ move j
+ read
+ compare
+ jump nextPass when greater-or-equal
+ set 0
+ home
+
+ move a
+ read
+ push
+ move j
+ read
+ push
+ call getelem
+ move tmp
+ write
+
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ call getelem
+ push
+ move tmp
+ read
+ compare
+ jump noSwap when less-or-equal
+
+ move a
+ read
+ push
+ move j
+ read
+ push
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ call getelem
+ push
+ call setelem
+
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ move tmp
+ read
+ push
+ call setelem
+
+ mark noSwap
+ move j
+ read
+ add 1
+ move j
+ write
+ set 0
+ home
+ jump inner
+
+ mark nextPass
+ move i
+ read
+ subtract 1
+ move i
+ write
+ set 0
+ home
+ jump outer
+
+ mark sorted
+ set 0
+ move i
+ write
+ set 0
+ home
+
+ mark print
+ move i
+ read
+ compare 8
+ jump done when greater-or-equal
+ set 0
+ home
+
+ move a
+ read
+ push
+ move i
+ read
+ push
+ call getelem
+ push
+ call put
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ home
+ jump print
+
+ mark done
+ move a
+ read
+ free
+ return
+end
diff --git a/examples/wordcount.cryn b/examples/wordcount.cryn
new file mode 100644
index 0000000..232aa0e
--- /dev/null
+++ b/examples/wordcount.cryn
@@ -0,0 +1,128 @@
+[# word counter: reads stdin, prints "lines words chars" #]
+make int64 lines
+make int64 words
+make int64 chars
+make int64 inword
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define bump address int64 cell result nothing
+ move cell
+ read
+ move at
+ read
+ add 1
+ write
+ set nothing
+ return
+end
+
+define doline result nothing
+ address lines
+ push
+ call bump
+ set nothing
+ return
+end
+
+define startword result nothing
+ set 1
+ move inword
+ write
+ address words
+ push
+ call bump
+ set nothing
+ return
+end
+
+define put int64 v result nothing
+ move v
+ read
+ output
+ set ' '
+ output
+ set nothing
+ return
+end
+
+define main
+ set 0
+ move lines
+ write
+ set 0
+ move words
+ write
+ set 0
+ move chars
+ write
+ set 0
+ move inword
+ write
+ set 0
+ home
+
+ mark next
+ input
+ compare -1
+ jump done when equal
+ push
+
+ address chars
+ push
+ call bump
+
+ pop
+ compare 10
+ jump newline when equal
+ compare 32
+ jump space when equal
+ compare 9
+ jump space when equal
+ jump word
+
+ mark newline
+ call doline
+
+ mark space
+ set 0
+ move inword
+ write
+ set 0
+ home
+ jump next
+
+ mark word
+ move inword
+ read
+ compare 0
+ jump cont when different
+ call startword
+
+ mark cont
+ set 0
+ home
+ jump next
+
+ mark done
+ move lines
+ read
+ push
+ call put
+ move words
+ read
+ push
+ call put
+ move chars
+ read
+ push
+ call put
+ call newline
+ return
+end
diff --git a/src/ast.h b/src/ast.h
new file mode 100644
index 0000000..274dea1
--- /dev/null
+++ b/src/ast.h
@@ -0,0 +1,116 @@
+#ifndef CR_AST_H
+#define CR_AST_H
+
+#include "diag.h"
+#include "lex.h"
+
+/* ---- literal values ---- */
+typedef enum { IM_NONE, IM_INT, IM_FLOAT, IM_CHAR, IM_BOOL } ImmKind;
+
+typedef struct Imm {
+ ImmKind kind;
+ uint64_t u; /* integer magnitude or char code */
+ int neg; /* integer sign */
+ int b; /* boolean */
+ double d; /* float */
+} Imm;
+
+/* ---- type syntax tree ---- */
+typedef enum {
+ TB_BASE, /* base type word (TokKind stored in base) */
+ TB_ADDR, /* address <inner> */
+ TB_ARR /* array [count] of <inner>; count==0 -> unsized */
+} TypeAstKind;
+
+typedef struct TypeAst {
+ TypeAstKind kind;
+ TokKind base; /* TB_BASE */
+ struct TypeAst* inner; /* TB_ADDR / TB_ARR */
+ uint64_t count; /* TB_ARR */
+ Loc loc;
+} TypeAst;
+
+/* ---- commands ---- */
+typedef enum {
+ A_MOVE_NAME, /* a.name */
+ A_MOVE_LIT, /* a.idx (integer) */
+ A_MOVE_INDEX, /* working value is the index */
+ A_MOVE_AT, /* working value is an address */
+ A_BACK, A_HOME,
+ A_ADDRESS_NAME,/* a.name (cell or transformation) */
+ A_ADDRESS_HERE,
+ A_MAKE_VALUE, /* t + optional name */
+ A_MAKE_ARRAY, /* t(elem via count) + optional name */
+ A_MAKE_STRING, /* str + optional name */
+ A_MAKE_DYN, /* size: name or literal; elem type; optional name */
+ A_FREE, A_READ, A_WRITE,
+ A_SET, /* imm */
+ A_ARITH, /* arith op + optional imm */
+ A_NEG,
+ A_PUSH, A_POP, A_SWAP,
+ A_CMP, /* optional imm */
+ A_CONV, /* t */
+ A_JMP, /* label + optional cond */
+ A_CALL_NAME, A_CALL_IND,
+ A_RET, A_STOP,
+ A_INPUT, A_OUTPUT, A_OUTPUT_TEXT,
+ A_OPEN, A_CLOSE, A_SYSTEM,
+ A_SPAWN, A_WAIT,
+ A_MARK
+} AstKind;
+
+typedef enum { AR_ADD, AR_SUB, AR_MUL, AR_DIV, AR_MOD } ArithOp;
+
+typedef struct Node {
+ AstKind kind;
+ Loc loc;
+ char* name; /* move/address/jump/call/spawn/mark names, dyn size cell */
+ char* name2; /* make dyn: optional bind name; make string: optional name */
+ char* str; /* make string content (NOT NUL-terminated) */
+ size_t strlen;
+ Imm imm; /* set/arith/cmp/make dyn size */
+ int has_imm;
+ ArithOp arith;
+ TypeAst* t; /* make value / convert */
+ TokKind cond; /* jump condition token */
+ int has_cond;
+ TokKind mode; /* open mode: T_READ/T_WRITE/T_BOTH */
+} Node;
+
+/* ---- top level ---- */
+typedef struct ParamAst {
+ char* name;
+ TypeAst* t;
+ Loc loc;
+} ParamAst;
+
+typedef struct FnAst {
+ char* name;
+ Loc loc;
+ ParamAst* params;
+ size_t nparams;
+ TypeAst* result; /* NULL = infer */
+ Node* body; /* command list */
+ size_t nbody;
+} FnAst;
+
+typedef enum { G_VALUE, G_ARRAY, G_STRING } GlobKind;
+
+typedef struct GlobAst {
+ GlobKind kind;
+ char* name;
+ Loc loc;
+ TypeAst* t; /* G_VALUE / G_ARRAY */
+ uint64_t count; /* G_ARRAY */
+ char* str; /* G_STRING */
+ size_t strlen;
+} GlobAst;
+
+typedef struct ProgramAst {
+ FnAst* fns;
+ size_t nfns;
+ GlobAst* globs;
+ size_t nglobs;
+} ProgramAst;
+
+#endif
diff --git a/src/be_c.c b/src/be_c.c
new file mode 100644
index 0000000..51e4ba9
--- /dev/null
+++ b/src/be_c.c
@@ -0,0 +1,850 @@
+#include "be_c.h"
+#include "rt_src.h"
+#include "util.h"
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ------------------------------------------------------------------ */
+/* helpers */
+/* ------------------------------------------------------------------ */
+
+static const char* ctype(const Type* t) {
+ switch (t->kind) {
+ case TY_NOTHING: return "uint8_t";
+ case TY_BOOL: return "uint8_t";
+ case TY_CHAR: return "uint32_t";
+ case TY_INT8: return "int8_t";
+ case TY_INT16: return "int16_t";
+ case TY_INT32: return "int32_t";
+ case TY_INT64: return "int64_t";
+ case TY_UINT8: return "uint8_t";
+ case TY_UINT16: return "uint16_t";
+ case TY_UINT32: return "uint32_t";
+ case TY_UINT64: return "uint64_t";
+ case TY_FLOAT32: return "float";
+ case TY_FLOAT64: return "double";
+ case TY_ADDR: return "uint64_t";
+ case TY_FUNC: return "CrFnRef*";
+ case TY_ARR: return ctype(t->inner);
+ case TY_INTLIT: return "int64_t";
+ }
+ return "int64_t";
+}
+
+/* element size used for pointer scaling on addresses */
+static uint64_t addr_elem_size(const Type* addr) {
+ const Type* pt = addr->inner;
+ if (pt->kind == TY_ARR) return type_size(pt->inner);
+ return type_size(pt);
+}
+
+static void emit_c_str(Buf* o, const char* s, size_t len) {
+ buf_putc(o, '"');
+ for (size_t i = 0; i < len; i++) {
+ unsigned char c = (unsigned char)s[i];
+ switch (c) {
+ case '"': buf_puts(o, "\\\""); break;
+ case '\\': buf_puts(o, "\\\\"); break;
+ case '\n': buf_puts(o, "\\n"); break;
+ case '\t': buf_puts(o, "\\t"); break;
+ case '\r': buf_puts(o, "\\r"); break;
+ default:
+ if (c < 32 || c > 126) buf_printf(o, "\\%03o", c);
+ else buf_putc(o, (char)c);
+ }
+ }
+ buf_putc(o, '"');
+}
+
+static void emit_imm_int(Buf* o, ImmV imm) {
+ if (imm.kind == IV_INT)
+ buf_printf(o, "(int64_t)%" PRId64, (int64_t)imm.u);
+ else if (imm.kind == IV_UINT)
+ buf_printf(o, "(uint64_t)%" PRIu64 "ULL", imm.u);
+ else
+ buf_printf(o, "(int64_t)0");
+}
+
+/* ------------------------------------------------------------------ */
+/* fn signature */
+/* ------------------------------------------------------------------ */
+
+static void emit_fn_sig(Buf* o, ProgramIR* p, int fi) {
+ FnIR* fn = &p->fns[fi];
+ buf_printf(o, "static Val cr_fn_%s(", fn->name);
+ if (fn->nparams == 0) {
+ buf_puts(o, "void");
+ } else {
+ for (int i = 0; i < fn->nparams; i++)
+ buf_printf(o, "%sVal p%d", i ? ", " : "", i);
+ }
+ buf_puts(o, ")");
+}
+
+/* ------------------------------------------------------------------ */
+/* locals */
+/* ------------------------------------------------------------------ */
+
+static void emit_local_decl(Buf* o, LocalIR* l) {
+ if (l->type->kind == TY_ARR) {
+ buf_printf(o, " %s l_%s[%llu] = {0}", ctype(l->type->inner), l->name,
+ (unsigned long long)(l->type->count ? l->type->count : 1));
+ } else {
+ buf_printf(o, " %s l_%s = 0", ctype(l->type), l->name);
+ }
+ buf_puts(o, ";\n");
+}
+
+/* ------------------------------------------------------------------ */
+/* statement emission */
+/* ------------------------------------------------------------------ */
+
+static void emit_store(Buf* o, Insn* in) {
+ const Type* t = in->t;
+ const char* ct = ctype(t);
+ switch (in->b) {
+ case SC_EXACT:
+ if (ty_is_float(t)) {
+ buf_printf(o, " *(%s*)F = (%s)wv.d;\n", ct, ct);
+ } else if (t->kind == TY_FUNC) {
+ buf_printf(o, " *(CrFnRef**)F = wv.p;\n");
+ } else if (ty_is_signed_int(t)) {
+ buf_printf(o, " *(%s*)F = (%s)wv.i;\n", ct, ct);
+ } else {
+ buf_printf(o, " *(%s*)F = (%s)wv.u;\n", ct, ct);
+ }
+ break;
+ case SC_INTLIT:
+ if (ty_is_float(t)) {
+ buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
+ } else {
+ buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
+ }
+ break;
+ case SC_INTLIT_BOOL:
+ buf_printf(o, " *(uint8_t*)F = (uint8_t)(int64_t)wv.u;\n");
+ break;
+ case SC_INTLIT_FLOAT:
+ buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
+ break;
+ case SC_F32:
+ buf_printf(o, " *(float*)F = (float)wv.d;\n");
+ break;
+ default:
+ buf_printf(o, " /* store coercion %d */\n", in->b);
+ break;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* function emission */
+/* ------------------------------------------------------------------ */
+
+static void emit_fn(Buf* o, ProgramIR* p, int fi) {
+ FnIR* fn = &p->fns[fi];
+ emit_fn_sig(o, p, fi);
+ buf_puts(o, " {\n");
+ buf_puts(o, " uint64_t cr_fh0 = cr_ctx.fh_sp;\n");
+ buf_puts(o, " int cr_cmp = 0;\n");
+ buf_puts(o, " Val wv; wv.u = 0;\n");
+ buf_puts(o, " void* F = 0;\n");
+
+ for (int i = 0; i < fn->nlocals; i++) {
+ LocalIR* l = &fn->locals[i];
+ if (l->is_param) continue;
+ emit_local_decl(o, l);
+ }
+ /* param cells */
+ for (int i = 0; i < fn->nparams; i++) {
+ LocalIR* l = &fn->locals[i];
+ Type* t = l->type;
+ if (t->kind == TY_ARR) continue; /* rejected by checker */
+ if (ty_is_float(t)) {
+ if (t->kind == TY_FLOAT32)
+ buf_printf(o, " float l_%s = (float)p%d.d;\n", l->name, i);
+ else
+ buf_printf(o, " double l_%s = p%d.d;\n", l->name, i);
+ } else if (t->kind == TY_FUNC) {
+ buf_printf(o, " CrFnRef* l_%s = p%d.p;\n", l->name, i);
+ } else if (ty_is_signed_int(t)) {
+ buf_printf(o, " %s l_%s = (%s)p%d.i;\n", ctype(t), l->name, ctype(t), i);
+ } else {
+ buf_printf(o, " %s l_%s = (%s)p%d.u;\n", ctype(t), l->name, ctype(t), i);
+ }
+ }
+
+ Type* cur = ty_nothing(); /* static type of wv for the backend's own use */
+
+ for (int i = 0; i < fn->nins; i++) {
+ Insn* in = &fn->ins[i];
+ switch (in->op) {
+ case I_LBL:
+ buf_printf(o, "L%d_%d: ;\n", fi, in->a);
+ break;
+ case I_JMP:
+ buf_printf(o, " goto L%d_%d;\n", fi, in->a);
+ break;
+ case I_JMPCOND: {
+ const char* cond;
+ switch (in->b) {
+ case CMP_LESS: cond = "cr_cmp < 0"; break;
+ case CMP_EQUAL: cond = "cr_cmp == 0"; break;
+ case CMP_GREATER: cond = "cr_cmp > 0"; break;
+ case CMP_DIFFERENT: cond = "cr_cmp != 0"; break;
+ case CMP_LESS_OR_EQUAL: cond = "cr_cmp <= 0"; break;
+ default: cond = "cr_cmp >= 0"; break;
+ }
+ buf_printf(o, " if (%s) goto L%d_%d;\n", cond, fi, in->a);
+ break;
+ }
+ case I_MOVE_LOCAL:
+ buf_printf(o, " cr_ctx.fh_sp = cr_fh0; F = &l_%s;\n", fn->locals[in->a].name);
+ break;
+ case I_MOVE_GLOBAL:
+ buf_printf(o, " cr_ctx.fh_sp = cr_fh0; F = &g_%s;\n", p->globs[in->a].name);
+ break;
+ case I_MOVE_LIT:
+ case I_MOVE_INDEX: {
+ const char* ct = ctype(in->t);
+ uint64_t esz = type_size(in->t);
+ if (in->op == I_MOVE_LIT) {
+ if (in->b > 0)
+ buf_printf(o, " crn_check_range((uint64_t)%" PRIu64 ", %d);\n",
+ in->imm.u, in->b);
+ else if (in->b < 0)
+ buf_printf(o, " crn_check_block(F, (uint64_t)%" PRIu64 ", %" PRIu64 ");\n",
+ in->imm.u, esz);
+ buf_printf(o, " crn_focus_push(F); F = (%s*)F + %" PRIu64 ";\n", ct, in->imm.u);
+ } else {
+ if (in->b > 0)
+ buf_printf(o, " crn_check_range(wv.u, %d);\n", in->b);
+ else if (in->b < 0)
+ buf_printf(o, " crn_check_block(F, wv.u, %" PRIu64 ");\n", esz);
+ buf_printf(o, " crn_focus_push(F); F = (%s*)F + wv.u;\n", ct);
+ }
+ break;
+ }
+ case I_MOVE_AT:
+ buf_puts(o, " crn_focus_push(F); F = wv.p;\n");
+ break;
+ case I_BACK:
+ buf_puts(o, " F = (cr_ctx.fh_sp > cr_fh0) ? crn_focus_pop() : 0;\n");
+ break;
+ case I_HOME:
+ buf_puts(o, " cr_ctx.fh_sp = cr_fh0; F = 0;\n");
+ break;
+ case I_ADDR_HERE:
+ buf_puts(o, " wv.u = (uint64_t)(uintptr_t)F;\n");
+ cur = in->t;
+ break;
+ case I_ADDR_LOCAL:
+ buf_printf(o, " wv.u = (uint64_t)(uintptr_t)&l_%s;\n", fn->locals[in->a].name);
+ cur = in->t;
+ break;
+ case I_ADDR_GLOBAL:
+ buf_printf(o, " wv.u = (uint64_t)(uintptr_t)&g_%s;\n", p->globs[in->a].name);
+ cur = in->t;
+ break;
+ case I_ADDR_FN:
+ buf_printf(o, " wv.p = (void*)&cr_ref_%s;\n", p->fns[in->a].name);
+ cur = ty_func();
+ break;
+ case I_MAKE_LOCAL: {
+ LocalIR* l = &fn->locals[in->a];
+ if (l->type->kind == TY_ARR)
+ buf_printf(o, " memset(l_%s, 0, sizeof l_%s);\n", l->name, l->name);
+ else if (l->type->kind == TY_FUNC)
+ buf_printf(o, " l_%s = 0;\n", l->name);
+ else
+ buf_printf(o, " l_%s = 0;\n", l->name);
+ break;
+ }
+ case I_MAKE_ANON:
+ buf_printf(o, " wv.u = (uint64_t)(uintptr_t)crn_alloc(%" PRIu64 ");\n",
+ type_size(in->t));
+ cur = ty_addr_to(in->t);
+ break;
+ case I_MAKE_DYN: {
+ uint64_t esz = type_size(in->t);
+ if (in->b == 1)
+ buf_printf(o, " { uint64_t n = (uint64_t)%" PRIu64 ";"
+ " if (n > 1099511627776ULL) crn_die(\"allocation too large\");"
+ " wv.u = (uint64_t)(uintptr_t)crn_alloc(n * %" PRIu64 "); }\n",
+ in->imm.u, esz);
+ else if (in->b == 0)
+ buf_printf(o, " { uint64_t n = (uint64_t)(int64_t)l_%s;"
+ " if (n > 1099511627776ULL) crn_die(\"allocation too large\");"
+ " wv.u = (uint64_t)(uintptr_t)crn_alloc(n * %" PRIu64 "); }\n",
+ fn->locals[in->a].name, esz);
+ else
+ buf_printf(o, " { uint64_t n = (uint64_t)(int64_t)g_%s;"
+ " if (n > 1099511627776ULL) crn_die(\"allocation too large\");"
+ " wv.u = (uint64_t)(uintptr_t)crn_alloc(n * %" PRIu64 "); }\n",
+ p->globs[in->a].name, esz);
+ cur = ty_addr_to(ty_array(in->t, 0));
+ break;
+ }
+ case I_MAKE_STRLOCAL: {
+ LocalIR* l = &fn->locals[in->a];
+ buf_printf(o, " memcpy(l_%s, ", l->name);
+ emit_c_str(o, l->str_init ? l->str_init : "", l->str_initlen);
+ buf_printf(o, ", %llu);\n", (unsigned long long)(l->str_initlen ? l->str_initlen : 1));
+ break;
+ }
+ case I_MAKE_STRANON:
+ buf_printf(o, " wv.u = (uint64_t)(uintptr_t)cr_str_%d;\n", in->a);
+ cur = in->t;
+ break;
+ case I_BIND_LOCAL:
+ buf_printf(o, " l_%s = wv.u;\n", fn->locals[in->a].name);
+ break;
+ case I_FREE:
+ buf_puts(o, " crn_free(wv.p);\n");
+ break;
+ case I_LOAD: {
+ const char* ct = ctype(in->t);
+ if (ty_is_float(in->t))
+ buf_printf(o, " wv.d = (double)*(%s*)F;\n", ct);
+ else if (in->t->kind == TY_FUNC)
+ buf_puts(o, " wv.p = *(CrFnRef**)F;\n");
+ else if (ty_is_signed_int(in->t))
+ buf_printf(o, " wv.i = (int64_t)*(%s*)F;\n", ct);
+ else
+ buf_printf(o, " wv.u = (uint64_t)*(%s*)F;\n", ct);
+ cur = in->t;
+ break;
+ }
+ case I_STORE:
+ emit_store(o, in);
+ break;
+ case I_SET:
+ if (ty_is_float(in->t) && in->imm.kind == IV_FLOAT)
+ buf_printf(o, " wv.d = (double)%.17g;\n", in->imm.d);
+ else if (in->imm.kind == IV_UINT)
+ buf_printf(o, " wv.u = (uint64_t)%" PRIu64 "ULL;\n", in->imm.u);
+ else
+ buf_printf(o, " wv.u = (uint64_t)%" PRIu64 "ULL;\n", in->imm.u);
+ cur = in->t;
+ break;
+ case I_ADD: case I_SUB: case I_MUL: case I_DIV: case I_MOD: {
+ /* operand first */
+ char opstr = in->op == I_ADD ? '+' : in->op == I_SUB ? '-' :
+ in->op == I_MUL ? '*' : ' ';
+ const Type* t = in->t;
+ if (ty_is_float(t)) {
+ if (in->b == 1) {
+ if (in->imm.kind == IV_FLOAT)
+ buf_printf(o, " { double b = (double)%.17g; ", in->imm.d);
+ else if (in->imm.kind == IV_INT)
+ buf_printf(o, " { double b = (double)(int64_t)%" PRIu64 "ULL; ", in->imm.u);
+ else
+ buf_printf(o, " { double b = (double)%" PRIu64 "ULL; ", in->imm.u);
+ } else {
+ buf_puts(o, " { double b = crn_pop().d; ");
+ }
+ if (in->op == I_DIV)
+ buf_puts(o, "wv.d = wv.d / b; }\n");
+ else if (in->op == I_MOD)
+ buf_puts(o, "wv.d = wv.d - b * (wv.d / b); }\n");
+ else if (t->kind == TY_FLOAT32)
+ buf_printf(o, "wv.d = (double)((float)wv.d %c (float)b); }\n", opstr);
+ else
+ buf_printf(o, "wv.d = wv.d %c b; }\n", opstr);
+ } else if (in->op == I_DIV || in->op == I_MOD) {
+ const char* dv = in->op == I_DIV ? "/" : "%";
+ if (ty_is_signed_int(t)) {
+ const char* ct = ctype(t);
+ if (in->b == 1) {
+ buf_printf(o, " { %s b = (%s)", ct, ct);
+ emit_imm_int(o, in->imm);
+ buf_printf(o, "; %s a = (%s)wv.i; "
+ "if (b == 0) crn_die(\"division by zero\"); "
+ "wv.i = (int64_t)(b == -1 ? (%s)(0 - (uint64_t)a) : (a %s b)); }\n",
+ ct, ct, ct, dv);
+ } else {
+ buf_printf(o, " { %s b = (%s)crn_pop().i; %s a = (%s)wv.i; "
+ "if (b == 0) crn_die(\"division by zero\"); "
+ "wv.i = (int64_t)(b == -1 ? (%s)(0 - (uint64_t)a) : (a %s b)); }\n",
+ ct, ct, ct, ct, ct, dv);
+ }
+ } else {
+ const char* ct = ctype(t);
+ if (in->b == 1) {
+ buf_printf(o, " { %s b = (%s)", ct, ct);
+ emit_imm_int(o, in->imm);
+ buf_printf(o, "; if (b == 0) crn_die(\"division by zero\"); "
+ "wv.u = (uint64_t)((%s)wv.u %s b); }\n", ct, dv);
+ } else {
+ buf_printf(o, " { %s b = (%s)crn_pop().u; "
+ "if (b == 0) crn_die(\"division by zero\"); "
+ "wv.u = (uint64_t)((%s)wv.u %s b); }\n", ct, ct, ct, dv);
+ }
+ }
+ } else if (t->kind == TY_ADDR) {
+ uint64_t esz = addr_elem_size(t);
+ if (in->b == 1) {
+ buf_puts(o, " { int64_t n = ");
+ emit_imm_int(o, in->imm);
+ buf_printf(o, "; wv.u = (uint64_t)((int64_t)wv.u %c n * (int64_t)%" PRIu64 "); }\n",
+ opstr, esz);
+ } else {
+ buf_printf(o, " { int64_t n = (int64_t)crn_pop().u; "
+ "wv.u = (uint64_t)((int64_t)wv.u %c n * (int64_t)%" PRIu64 "); }\n",
+ opstr, esz);
+ }
+ } else if (in->a == 1 && in->op == I_SUB) {
+ /* pointer difference */
+ uint64_t esz = addr_elem_size(cur);
+ buf_printf(o, " wv.i = (int64_t)(((%s*)wv.p) - ((%s*)crn_pop().p));\n",
+ ctype(cur->inner->kind == TY_ARR ? cur->inner->inner : cur->inner),
+ ctype(cur->inner->kind == TY_ARR ? cur->inner->inner : cur->inner));
+ (void)esz;
+ } else if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
+ const char* ct = ctype(t);
+ if (in->b == 1) {
+ buf_printf(o, " { int64_t b = ");
+ emit_imm_int(o, in->imm);
+ buf_printf(o, "; wv.i = (int64_t)(%s)((uint64_t)(int64_t)wv.i %c (uint64_t)b); }\n",
+ ct, opstr);
+ } else {
+ buf_printf(o, " wv.i = (int64_t)(%s)((uint64_t)(int64_t)wv.i %c "
+ "(uint64_t)(int64_t)crn_pop().i);\n", ct, opstr);
+ }
+ } else {
+ const char* ct = ctype(t);
+ if (in->b == 1) {
+ buf_printf(o, " { uint64_t b = ");
+ if (in->imm.kind == IV_UINT)
+ buf_printf(o, "(uint64_t)%" PRIu64 "ULL", in->imm.u);
+ else
+ emit_imm_int(o, in->imm);
+ buf_printf(o, "; wv.u = (uint64_t)(%s)(wv.u %c b); }\n", ct, opstr);
+ } else {
+ buf_printf(o, " wv.u = (uint64_t)(%s)(wv.u %c crn_pop().u);\n", ct, opstr);
+ }
+ }
+ if (!(in->op == I_SUB && in->a == 1)) cur = in->t;
+ else cur = ty_int(64);
+ break;
+ }
+ case I_NEG:
+ if (ty_is_float(in->t)) {
+ if (in->t->kind == TY_FLOAT32)
+ buf_puts(o, " wv.d = (double)(-(float)wv.d);\n");
+ else
+ buf_puts(o, " wv.d = -wv.d;\n");
+ } else {
+ buf_printf(o, " wv.i = (int64_t)(%s)(0 - (uint64_t)(int64_t)wv.i);\n",
+ ctype(in->t));
+ }
+ break;
+ case I_PUSH:
+ buf_puts(o, " crn_push(wv);\n");
+ break;
+ case I_POP:
+ buf_puts(o, " wv = crn_pop();\n");
+ cur = in->t;
+ break;
+ case I_SWAP:
+ buf_puts(o, " { Val t = crn_pop(); crn_push(wv); wv = t; }\n");
+ break;
+ case I_CMP: {
+ const Type* t = in->t;
+ if (in->b == 1) {
+ if (ty_is_float(t)) {
+ if (in->imm.kind == IV_FLOAT)
+ buf_printf(o, " { double b = (double)%.17g; ", in->imm.d);
+ else if (in->imm.kind == IV_INT)
+ buf_printf(o, " { double b = (double)(int64_t)%" PRIu64 "ULL; ", in->imm.u);
+ else
+ buf_printf(o, " { double b = (double)%" PRIu64 "ULL; ", in->imm.u);
+ buf_puts(o, "double a = wv.d; cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ } else if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
+ buf_puts(o, " { int64_t b = ");
+ emit_imm_int(o, in->imm);
+ buf_puts(o, "; int64_t a = wv.i; cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ } else {
+ buf_puts(o, " { uint64_t b = ");
+ if (in->imm.kind == IV_UINT)
+ buf_printf(o, "(uint64_t)%" PRIu64 "ULL", in->imm.u);
+ else
+ emit_imm_int(o, in->imm);
+ buf_puts(o, "; uint64_t a = wv.u; cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ }
+ } else {
+ if (ty_is_float(t)) {
+ buf_puts(o, " { double b = crn_pop().d; double a = wv.d; "
+ "cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ } else if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
+ buf_puts(o, " { int64_t b = crn_pop().i; int64_t a = wv.i; "
+ "cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ } else {
+ buf_puts(o, " { uint64_t b = crn_pop().u; uint64_t a = wv.u; "
+ "cr_cmp = a < b ? -1 : a > b ? 1 : 0; }\n");
+ }
+ }
+ break;
+ }
+ case I_CONV: {
+ const Type* from = cur;
+ Type* to = in->t;
+ const char* ct = ctype(to);
+ if (ty_is_float(from)) {
+ if (to->kind == TY_FLOAT64) {
+ /* no change needed */
+ } else if (to->kind == TY_FLOAT32) {
+ buf_puts(o, " wv.d = (double)(float)wv.d;\n");
+ } else if (to->kind == TY_BOOL) {
+ buf_puts(o, " wv.u = (uint64_t)(wv.d != 0.0);\n");
+ } else if (ty_is_signed_int(to)) {
+ /* saturate at the target width */
+ if (to->kind == TY_INT8)
+ buf_puts(o, " wv.i = (int64_t)crn_d2i8(wv.d);\n");
+ else if (to->kind == TY_INT16)
+ buf_puts(o, " wv.i = (int64_t)crn_d2i16(wv.d);\n");
+ else if (to->kind == TY_INT32)
+ buf_puts(o, " wv.i = (int64_t)crn_d2i32(wv.d);\n");
+ else
+ buf_puts(o, " wv.i = crn_d2i64(wv.d);\n");
+ } else {
+ if (to->kind == TY_UINT8)
+ buf_puts(o, " wv.u = (uint64_t)crn_d2u8(wv.d);\n");
+ else if (to->kind == TY_UINT16)
+ buf_puts(o, " wv.u = (uint64_t)crn_d2u16(wv.d);\n");
+ else if (to->kind == TY_UINT32)
+ buf_puts(o, " wv.u = (uint64_t)crn_d2u32(wv.d);\n");
+ else
+ buf_puts(o, " wv.u = crn_d2u64(wv.d);\n");
+ }
+ } else if (to->kind == TY_FLOAT64) {
+ if (from->kind == TY_INTLIT || ty_is_signed_int(from))
+ buf_puts(o, " wv.d = (double)(int64_t)wv.u;\n");
+ else
+ buf_puts(o, " wv.d = (double)wv.u;\n");
+ } else if (to->kind == TY_FLOAT32) {
+ if (from->kind == TY_INTLIT || ty_is_signed_int(from))
+ buf_puts(o, " wv.d = (double)(float)(int64_t)wv.u;\n");
+ else
+ buf_puts(o, " wv.d = (double)(float)wv.u;\n");
+ } else if (to->kind == TY_BOOL) {
+ if (from->kind == TY_INTLIT || ty_is_signed_int(from))
+ buf_puts(o, " wv.u = (uint64_t)((int64_t)wv.u != 0);\n");
+ else
+ buf_puts(o, " wv.u = (uint64_t)(wv.u != 0);\n");
+ } else if (to->kind == TY_FUNC || from->kind == TY_FUNC) {
+ /* impossible */
+ } else if (ty_is_signed_int(to)) {
+ buf_printf(o, " wv.i = (int64_t)(%s)wv.u;\n", ct);
+ } else {
+ buf_printf(o, " wv.u = (uint64_t)(%s)wv.u;\n", ct);
+ }
+ cur = to;
+ break;
+ }
+ case I_CALL: {
+ FnIR* callee = &p->fns[in->a];
+ int np = callee->nparams;
+ if (np == 0) {
+ if (callee->result->kind != TY_NOTHING)
+ buf_printf(o, " wv = cr_fn_%s();\n", callee->name);
+ else
+ buf_printf(o, " cr_fn_%s();\n", callee->name);
+ } else {
+ buf_printf(o, " { uint64_t s = cr_ctx.sp; cr_ctx.sp = s - %d;\n", np);
+ if (callee->result->kind != TY_NOTHING)
+ buf_puts(o, " wv = ");
+ else
+ buf_puts(o, " ");
+ buf_printf(o, "cr_fn_%s(", callee->name);
+ for (int k = 0; k < np; k++) {
+ Type* want = callee->locals[k].type;
+ unsigned kind = ((unsigned)in->b >> (2 * k)) & 3u;
+ if (k) buf_puts(o, ", ");
+ if (kind == 0) {
+ buf_printf(o, "cr_ctx.stk[s - %d]", np - k);
+ } else if (kind == 1) {
+ if (want->kind == TY_FLOAT32)
+ buf_printf(o, "(Val){.d = (double)(float)(int64_t)cr_ctx.stk[s - %d].u}",
+ np - k);
+ else if (want->kind == TY_FLOAT64)
+ buf_printf(o, "(Val){.d = (double)(int64_t)cr_ctx.stk[s - %d].u}",
+ np - k);
+ else if (ty_is_signed_int(want))
+ buf_printf(o, "(Val){.u = (uint64_t)(%s)cr_ctx.stk[s - %d].u}",
+ ctype(want), np - k);
+ else
+ buf_printf(o, "(Val){.u = (uint64_t)(%s)cr_ctx.stk[s - %d].u}",
+ ctype(want), np - k);
+ } else {
+ buf_printf(o, "(Val){.d = (double)(float)cr_ctx.stk[s - %d].d}", np - k);
+ }
+ }
+ buf_puts(o, "); }\n");
+ }
+ if (callee->result->kind != TY_NOTHING) cur = callee->result;
+ break;
+ }
+ case I_CALLIND:
+ buf_puts(o, " { CrFnRef* r = (CrFnRef*)wv.p; "
+ "if (r->np != 0) crn_die(\"indirect call of a transformation "
+ "that takes parameters\"); r->fn(); wv.u = 0; }\n");
+ cur = ty_nothing();
+ break;
+ case I_RET:
+ buf_puts(o, " cr_ctx.fh_sp = cr_fh0; return wv;\n");
+ break;
+ case I_STOP:
+ if (ty_is_float(cur))
+ buf_puts(o, " crn_stop(crn_d2i64(wv.d));\n");
+ else if (cur->kind == TY_NOTHING || cur->kind == TY_FUNC)
+ buf_puts(o, " crn_stop(0);\n");
+ else
+ buf_puts(o, " crn_stop((int64_t)wv.u);\n");
+ break;
+ case I_INPUT:
+ buf_puts(o, " wv.i = crn_input();\n");
+ cur = ty_int(64);
+ break;
+ case I_OUTPUT:
+ if (ty_is_float(cur)) {
+ if (cur->kind == TY_FLOAT32)
+ buf_puts(o, " crn_out_f32((float)wv.d);\n");
+ else
+ buf_puts(o, " crn_out_f64(wv.d);\n");
+ } else if (cur->kind == TY_CHAR || cur->kind == TY_UINT8) {
+ buf_puts(o, " crn_out_char((unsigned)wv.u);\n");
+ } else if (cur->kind == TY_BOOL) {
+ buf_puts(o, " crn_out_bool((int)wv.u);\n");
+ } else if (cur->kind == TY_ADDR) {
+ buf_puts(o, " crn_out_addr(wv.u);\n");
+ } else if (cur->kind == TY_INTLIT || ty_is_signed_int(cur)) {
+ buf_puts(o, " crn_out_i64((int64_t)wv.u);\n");
+ } else {
+ buf_puts(o, " crn_out_u64(wv.u);\n");
+ }
+ break;
+ case I_OUTTEXT:
+ buf_puts(o, " crn_out_text((uint8_t*)(uintptr_t)wv.u);\n");
+ break;
+ case I_OPEN:
+ buf_printf(o, " wv.i = crn_open((uint8_t*)(uintptr_t)wv.u, %d);\n", in->b);
+ cur = ty_int(64);
+ break;
+ case I_CLOSE:
+ buf_puts(o, " wv.i = crn_close((int64_t)wv.u);\n");
+ cur = ty_int(64);
+ break;
+ case I_SYSTEM:
+ buf_puts(o, " wv.i = crn_system((uint8_t*)(uintptr_t)wv.u);\n");
+ cur = ty_int(64);
+ break;
+ case I_SPAWN:
+ buf_printf(o, " crn_spawn(&cr_ref_%s);\n", p->fns[in->a].name);
+ break;
+ case I_WAIT:
+ buf_puts(o, " crn_wait(); wv.i = 0;\n");
+ cur = ty_int(64);
+ break;
+ }
+ }
+ /* implicit return handled by checker (I_RET always present or unreachable) */
+ buf_puts(o, " cr_ctx.fh_sp = cr_fh0; return wv;\n");
+ buf_puts(o, "}\n\n");
+}
+
+/* ------------------------------------------------------------------ */
+/* program emission */
+/* ------------------------------------------------------------------ */
+
+void be_c_emit(ProgramIR* p, Buf* out) {
+ buf_puts(out, "/* generated by the Cryn compiler - do not edit */\n");
+ buf_puts(out, "/* Cryn source compiled to portable C11 */\n\n");
+ buf_puts(out, cr_rt_h_src);
+ buf_putc(out, '\n');
+ buf_puts(out, "#include <stdlib.h>\n#include <string.h>\n\n");
+ buf_puts(out, cr_rt_c_src);
+ buf_putc(out, '\n');
+
+ /* string table */
+ for (int i = 0; i < p->nstrs; i++) {
+ StrIR* s = &p->strs[i];
+ buf_printf(out, "static const uint8_t cr_str_%d[] = ", i);
+ emit_c_str(out, s->bytes, s->len + 1);
+ buf_puts(out, ";\n");
+ }
+ buf_putc(out, '\n');
+
+ /* world cells */
+ for (int i = 0; i < p->nglobs; i++) {
+ GlobIR* g = &p->globs[i];
+ if (g->type->kind == TY_ARR) {
+ buf_printf(out, "static %s g_%s[%llu] = {0};\n", ctype(g->type->inner), g->name,
+ (unsigned long long)g->type->count);
+ } else {
+ buf_printf(out, "static %s g_%s = 0;\n", ctype(g->type), g->name);
+ }
+ }
+ /* string globals need init */
+ for (int i = 0; i < p->nglobs; i++) {
+ GlobIR* g = &p->globs[i];
+ if (g->str_init) {
+ buf_printf(out, "static const uint8_t g_%s_init[] = ", g->name);
+ emit_c_str(out, g->str_init, g->str_initlen);
+ buf_puts(out, ";\n");
+ }
+ }
+ buf_putc(out, '\n');
+
+ /* forward declarations */
+ for (int i = 0; i < p->nfns; i++) {
+ emit_fn_sig(out, p, i);
+ buf_puts(out, ";\n");
+ }
+ buf_putc(out, '\n');
+
+ /* transformation references */
+ for (int i = 0; i < p->nfns; i++) {
+ FnIR* fn = &p->fns[i];
+ buf_printf(out, "static CrFnRef cr_ref_%s = {(Val(*)(void))cr_fn_%s, %d};\n",
+ fn->name, fn->name, fn->nparams);
+ }
+ buf_putc(out, '\n');
+
+ /* string global initializers run before main */
+ buf_puts(out, "static void crn_init_world(void) {\n");
+ for (int i = 0; i < p->nglobs; i++) {
+ GlobIR* g = &p->globs[i];
+ if (g->str_init)
+ buf_printf(out, " memcpy(g_%s, g_%s_init, %llu);\n", g->name, g->name,
+ (unsigned long long)g->str_initlen);
+ }
+ buf_puts(out, "}\n\n");
+
+ /* functions */
+ for (int i = 0; i < p->nfns; i++)
+ emit_fn(out, p, i);
+
+ /* entry point */
+ buf_puts(out, "int main(void) {\n crn_init();\n crn_init_world();\n");
+ buf_puts(out, " cr_fn_main();\n fflush(NULL);\n return 0;\n}\n");
+}
+
+/* ------------------------------------------------------------------ */
+/* IR dump */
+/* ------------------------------------------------------------------ */
+
+static const char* irop_name(IROp op) {
+ switch (op) {
+ case I_LBL: return "label";
+ case I_JMP: return "jump";
+ case I_JMPCOND: return "jump-if";
+ case I_MOVE_LOCAL: return "move-local";
+ case I_MOVE_GLOBAL: return "move-global";
+ case I_MOVE_LIT: return "move-idx";
+ case I_MOVE_INDEX: return "move-index";
+ case I_MOVE_AT: return "move-at";
+ case I_BACK: return "back";
+ case I_HOME: return "home";
+ case I_ADDR_HERE: return "address-here";
+ case I_ADDR_LOCAL: return "address-local";
+ case I_ADDR_GLOBAL: return "address-global";
+ case I_ADDR_FN: return "address-fn";
+ case I_MAKE_LOCAL: return "make-local";
+ case I_MAKE_ANON: return "make-anon";
+ case I_MAKE_DYN: return "make-dyn";
+ case I_MAKE_STRLOCAL: return "make-str-local";
+ case I_MAKE_STRANON: return "make-str-anon";
+ case I_BIND_LOCAL: return "bind-local";
+ case I_FREE: return "free";
+ case I_LOAD: return "load";
+ case I_STORE: return "store";
+ case I_SET: return "set";
+ case I_ADD: return "add";
+ case I_SUB: return "subtract";
+ case I_MUL: return "multiply";
+ case I_DIV: return "divide";
+ case I_MOD: return "remainder";
+ case I_NEG: return "negate";
+ case I_PUSH: return "push";
+ case I_POP: return "pop";
+ case I_SWAP: return "swap";
+ case I_CMP: return "compare";
+ case I_CONV: return "convert";
+ case I_CALL: return "call";
+ case I_CALLIND: return "call-indirect";
+ case I_RET: return "return";
+ case I_STOP: return "stop";
+ case I_INPUT: return "input";
+ case I_OUTPUT: return "output";
+ case I_OUTTEXT: return "output-text";
+ case I_OPEN: return "open";
+ case I_CLOSE: return "close";
+ case I_SYSTEM: return "system";
+ case I_SPAWN: return "spawn";
+ case I_WAIT: return "wait";
+ }
+ return "?";
+}
+
+void ir_dump(ProgramIR* p, Buf* out) {
+ for (int i = 0; i < p->nglobs; i++) {
+ GlobIR* g = &p->globs[i];
+ char b[128];
+ buf_printf(out, "world %s : %s\n", g->name, type_name(g->type, b, sizeof b));
+ }
+ for (int i = 0; i < p->nfns; i++) {
+ FnIR* fn = &p->fns[i];
+ char b[128];
+ buf_printf(out, "\ntransformation %s(", fn->name);
+ for (int j = 0; j < fn->nparams; j++) {
+ if (j) buf_puts(out, " ");
+ buf_printf(out, "%s:%s", fn->locals[j].name,
+ type_name(fn->locals[j].type, b, sizeof b));
+ }
+ buf_printf(out, ") result %s\n",
+ type_name(fn->result ? fn->result : ty_nothing(), b, sizeof b));
+ for (int j = 0; j < fn->nins; j++) {
+ Insn* in = &fn->ins[j];
+ buf_printf(out, " %4d %s", j, irop_name(in->op));
+ switch (in->op) {
+ case I_LBL: case I_JMP:
+ buf_printf(out, " %s", fn->label_names && in->a < fn->nlabels
+ ? fn->label_names[in->a] : "?");
+ break;
+ case I_JMPCOND:
+ buf_printf(out, " %s when %d", fn->label_names[in->a], in->b);
+ break;
+ case I_MOVE_LOCAL: case I_ADDR_LOCAL: case I_MAKE_LOCAL:
+ case I_MAKE_STRLOCAL: case I_BIND_LOCAL:
+ if (in->a >= 0 && in->a < fn->nlocals)
+ buf_printf(out, " %s", fn->locals[in->a].name);
+ break;
+ case I_MOVE_GLOBAL: case I_ADDR_GLOBAL:
+ buf_printf(out, " %s", p->globs[in->a].name);
+ break;
+ case I_ADDR_FN: case I_CALL: case I_SPAWN:
+ buf_printf(out, " %s", p->fns[in->a].name);
+ break;
+ case I_MOVE_LIT:
+ buf_printf(out, " %llu", (unsigned long long)in->imm.u);
+ break;
+ default:
+ if (in->imm.kind == IV_INT)
+ buf_printf(out, " %lld", (long long)(int64_t)in->imm.u);
+ else if (in->imm.kind == IV_UINT)
+ buf_printf(out, " %llu", (unsigned long long)in->imm.u);
+ else if (in->imm.kind == IV_FLOAT)
+ buf_printf(out, " %.17g", in->imm.d);
+ break;
+ }
+ if (in->t) {
+ char b2[128];
+ buf_printf(out, " [%s]", type_name(in->t, b2, sizeof b2));
+ }
+ buf_putc(out, '\n');
+ }
+ }
+}
diff --git a/src/be_c.h b/src/be_c.h
new file mode 100644
index 0000000..4506f56
--- /dev/null
+++ b/src/be_c.h
@@ -0,0 +1,9 @@
+#ifndef CR_BE_C_H
+#define CR_BE_C_H
+
+#include "ir.h"
+
+/* Emit a complete, self-contained C translation unit for the program. */
+void be_c_emit(ProgramIR* p, Buf* out);
+
+#endif
diff --git a/src/check.c b/src/check.c
new file mode 100644
index 0000000..51f34bd
--- /dev/null
+++ b/src/check.c
@@ -0,0 +1,1607 @@
+#include "check.h"
+#include "util.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * The checker is a small abstract interpreter over the abstract machine:
+ *
+ * - working value: one static type (TY_INTLIT keeps the literal value)
+ * - focus: a stack of abstract cells; entry 0 is always the frame root
+ * - value stack: a list of static types
+ * - comparison state: valid/invalid
+ *
+ * The body is walked in syntactic order. Every `jump` merges the current
+ * abstract state into the target label's state; all merges must agree
+ * exactly. Because a label may be targeted from code that appears before
+ * the label while the fallthrough into it is dead, the walk is repeated
+ * until no label gains a new state, then one final emitting walk runs.
+ *
+ * A transformation's result type is the working value type at its first
+ * syntactic `return` (or at the implicit return at `end`) unless declared
+ * with `result <type>`.
+ */
+
+#define MAX_FOC 256
+#define MAX_STK 1024
+#define MAX_LOCALS 256
+#define MAX_LABELS 256
+
+typedef struct FocusEnt {
+ int is_frame;
+ Type* cell; /* cell type when !is_frame */
+} FocusEnt;
+
+typedef struct CState {
+ Type* wv;
+ FocusEnt foc[MAX_FOC];
+ int nfoc;
+ Type* stk[MAX_STK];
+ int nstk;
+ int cmp;
+ int dead; /* 0 = live; 1 = unreachable (see unk) */
+ int unk; /* dead only because label state is not yet known */
+} CState;
+
+typedef struct Chk {
+ ProgramIR* prog;
+ FnIR* fn;
+ FnAst* fnast;
+ CState st;
+
+ struct { char* name; CState* state; Loc loc; } labels[MAX_LABELS];
+ int nlabels;
+
+ int emitting; /* final round: emit IR */
+ int erroring; /* final round: report unreachable errors */
+ int changed; /* a label gained/updated state this round */
+ int blocked; /* a callee result is not yet inferred */
+
+ Type* result_first;
+ int result_seen;
+} Chk;
+
+/* ------------------------------------------------------------------ */
+/* diagnostics */
+/* ------------------------------------------------------------------ */
+
+static void err(Loc loc, const char* fmt, ...) {
+ va_list ap;
+ char msg[512];
+ va_start(ap, fmt);
+ vsnprintf(msg, sizeof msg, fmt, ap);
+ va_end(ap);
+ crn_error(loc, "%s", msg);
+}
+
+/* ------------------------------------------------------------------ */
+/* types */
+/* ------------------------------------------------------------------ */
+
+static Type* ast_to_type(TypeAst* t, Loc loc) {
+ if (!t) return ty_nothing();
+ switch (t->kind) {
+ case TB_BASE:
+ switch (t->base) {
+ case T_NOTHING: return ty_nothing();
+ case T_BOOL: return ty_bool();
+ case T_CHAR_T: return ty_char();
+ case T_INT8: return ty_int(8);
+ case T_INT16: return ty_int(16);
+ case T_INT32: return ty_int(32);
+ case T_INT64: return ty_int(64);
+ case T_UINT8: return ty_uint(8);
+ case T_UINT16: return ty_uint(16);
+ case T_UINT32: return ty_uint(32);
+ case T_UINT64: return ty_uint(64);
+ case T_FLOAT32: return ty_float32();
+ case T_FLOAT64: return ty_float64();
+ case T_TRANSFORM: return ty_func();
+ default:
+ err(loc, "not a base type");
+ return ty_nothing();
+ }
+ case TB_ADDR:
+ return ty_addr_to(t->inner ? ast_to_type(t->inner, loc) : ty_nothing());
+ case TB_ARR: {
+ if (!t->inner) { err(loc, "array needs an element type"); return ty_nothing(); }
+ Type* elem = ast_to_type(t->inner, t->inner->loc);
+ if (elem->kind == TY_FUNC) err(loc, "arrays of transform references are not supported");
+ if (elem->kind == TY_ARR && elem->count == 0)
+ err(loc, "array element type cannot be an unsized array");
+ return ty_array(elem, t->count);
+ }
+ }
+ return ty_nothing();
+}
+
+static int lit_value_int(Type* t, int64_t* out) {
+ if (t->kind != TY_INTLIT) return 0;
+ uint64_t m = t->litval;
+ if (t->litneg) {
+ if (m == (1ULL << 63)) *out = INT64_MIN;
+ else *out = -(int64_t)m;
+ } else {
+ if (m > (uint64_t)INT64_MAX) return 0;
+ *out = (int64_t)m;
+ }
+ return 1;
+}
+
+static Type* intlit_from_int64(int64_t v) {
+ if (v < 0) {
+ if (v == INT64_MIN) return ty_intlit(1ULL << 63, 1);
+ return ty_intlit((uint64_t)(-v), 1);
+ }
+ return ty_intlit((uint64_t)v, 0);
+}
+
+/* ------------------------------------------------------------------ */
+/* IR emission */
+/* ------------------------------------------------------------------ */
+
+static Insn* emit(Chk* c, IROp op, Loc loc) {
+ if (c->fn->nins == c->fn->cap) {
+ c->fn->cap = c->fn->cap ? c->fn->cap * 2 : 64;
+ c->fn->ins = xrealloc(c->fn->ins, (size_t)c->fn->cap * sizeof(Insn));
+ }
+ Insn* in = &c->fn->ins[c->fn->nins++];
+ memset(in, 0, sizeof *in);
+ in->op = op;
+ in->loc = loc;
+ return in;
+}
+
+static void set_imm(Insn* in, Imm* m) {
+ switch (m->kind) {
+ case IM_INT:
+ if (m->neg) {
+ in->imm.kind = IV_INT;
+ in->imm.u = m->u == (1ULL << 63) ? m->u : (uint64_t)(0 - m->u);
+ } else if (m->u <= (uint64_t)INT64_MAX) {
+ in->imm.kind = IV_INT;
+ in->imm.u = m->u;
+ } else {
+ in->imm.kind = IV_UINT;
+ in->imm.u = m->u;
+ }
+ break;
+ case IM_FLOAT:
+ in->imm.kind = IV_FLOAT;
+ in->imm.d = m->d;
+ break;
+ case IM_CHAR:
+ in->imm.kind = IV_UINT;
+ in->imm.u = m->u;
+ break;
+ case IM_BOOL:
+ in->imm.kind = IV_UINT;
+ in->imm.u = (uint64_t)m->b;
+ break;
+ default:
+ break;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* symbols */
+/* ------------------------------------------------------------------ */
+
+static int find_local(Chk* c, const char* name) {
+ for (int i = 0; i < c->fn->nlocals; i++)
+ if (strcmp(c->fn->locals[i].name, name) == 0) return i;
+ return -1;
+}
+
+static int find_label(Chk* c, const char* name) {
+ for (int i = 0; i < c->nlabels; i++)
+ if (strcmp(c->labels[i].name, name) == 0) return i;
+ return -1;
+}
+
+static int find_fn(Chk* c, const char* name) {
+ return prog_find_fn(c->prog, name);
+}
+
+static int find_glob(Chk* c, const char* name) {
+ return prog_find_glob(c->prog, name);
+}
+
+/* ------------------------------------------------------------------ */
+/* abstract state */
+/* ------------------------------------------------------------------ */
+
+static void st_init(Chk* c) {
+ memset(&c->st, 0, sizeof c->st);
+ c->st.wv = ty_nothing();
+ c->st.foc[0].is_frame = 1;
+ c->st.nfoc = 1;
+ c->st.dead = 0;
+ c->st.unk = 0;
+}
+
+static void st_clone(CState* d, CState* s) { *d = *s; }
+
+/* join two static working-value/stack types for control flow merges.
+ Integer literals widen to the type they fit; two literals join to int64. */
+static Type* join_types(Type* a, Type* b) {
+ if (type_eq(a, b)) return a;
+ if (a->kind == TY_INTLIT && b->kind == TY_INTLIT) return ty_int(64);
+ if (a->kind == TY_INTLIT && (ty_is_numeric(b) || b->kind == TY_CHAR || b->kind == TY_BOOL))
+ return intlit_fits(a->litval, a->litneg, b) ? b : NULL;
+ if (b->kind == TY_INTLIT && (ty_is_numeric(a) || a->kind == TY_CHAR || a->kind == TY_BOOL))
+ return intlit_fits(b->litval, b->litneg, a) ? a : NULL;
+ return NULL;
+}
+
+static int st_merge(CState* into, CState* from, Loc loc, const char* what) {
+ if (into->dead || from->dead) return 1;
+ int changed = 0;
+ Type* jwv = join_types(into->wv, from->wv);
+ if (!jwv) {
+ char b1[128], b2[128];
+ err(loc, "%s: working value type differs between paths (%s vs %s); "
+ "normalize it with 'set' before entering the loop or branch",
+ what, type_name(into->wv, b1, sizeof b1), type_name(from->wv, b2, sizeof b2));
+ return 0;
+ }
+ if (!type_eq(jwv, into->wv)) { into->wv = jwv; changed = 1; }
+ if (into->nfoc != from->nfoc) {
+ err(loc, "%s: focus depth differs between paths (%d vs %d)", what, into->nfoc, from->nfoc);
+ return 0;
+ }
+ for (int i = 0; i < into->nfoc; i++) {
+ if (into->foc[i].is_frame != from->foc[i].is_frame ||
+ (!into->foc[i].is_frame && !type_eq(into->foc[i].cell, from->foc[i].cell))) {
+ err(loc, "%s: focus differs between paths at depth %d", what, i);
+ return 0;
+ }
+ }
+ if (into->nstk != from->nstk) {
+ err(loc, "%s: value stack depth differs between paths (%d vs %d)", what, into->nstk, from->nstk);
+ return 0;
+ }
+ for (int i = 0; i < into->nstk; i++) {
+ Type* js = join_types(into->stk[i], from->stk[i]);
+ if (!js) {
+ char b1[128], b2[128];
+ err(loc, "%s: value stack slot %d differs between paths (%s vs %s)",
+ what, i, type_name(into->stk[i], b1, sizeof b1), type_name(from->stk[i], b2, sizeof b2));
+ return 0;
+ }
+ if (!type_eq(js, into->stk[i])) { into->stk[i] = js; changed = 1; }
+ }
+ if (from->cmp && !into->cmp) {
+ into->cmp = 1;
+ changed = 1;
+ }
+ return changed ? 2 : 1;
+}
+
+/* merge the current state into a label; creates it if needed.
+ returns: 0 = error, 1 = ok (no change), 2 = ok (label state created/strengthened) */
+static int label_merge(Chk* c, int li, Loc loc) {
+ if (c->st.dead) return 1;
+ if (!c->labels[li].state) {
+ c->labels[li].state = xmalloc(sizeof(CState));
+ st_clone(c->labels[li].state, &c->st);
+ c->changed = 1;
+ return 2;
+ }
+ return st_merge(c->labels[li].state, &c->st, loc, "control flow joins at this label");
+}
+
+/* ------------------------------------------------------------------ */
+/* numeric type pairing */
+/* ------------------------------------------------------------------ */
+
+static Type* numeric_pair_type(Type* a, Type* b) {
+ if (a->kind == TY_INTLIT && b->kind == TY_INTLIT) return ty_int(64);
+ if (a->kind == TY_INTLIT) {
+ if (ty_is_int(b) || ty_is_float(b) || b->kind == TY_CHAR) return b;
+ if (b->kind == TY_BOOL) return ty_bool();
+ return NULL;
+ }
+ if (b->kind == TY_INTLIT) {
+ if (ty_is_int(a) || ty_is_float(a) || a->kind == TY_CHAR) return a;
+ if (a->kind == TY_BOOL) return ty_bool();
+ return NULL;
+ }
+ if (ty_is_float(a) && ty_is_float(b)) return a->kind == TY_FLOAT64 ? a : b;
+ if (ty_is_float(a) || ty_is_float(b)) return ty_float64();
+ if ((ty_is_int(a) || a->kind == TY_CHAR) && (ty_is_int(b) || b->kind == TY_CHAR)) {
+ if (type_eq(a, b)) return a;
+ int aw = a->kind == TY_CHAR ? 32 : ty_int_bits(a);
+ int bw = b->kind == TY_CHAR ? 32 : ty_int_bits(b);
+ int as = a->kind == TY_CHAR ? 0 : ty_is_signed_int(a);
+ int bs = b->kind == TY_CHAR ? 0 : ty_is_signed_int(b);
+ int w = aw > bw ? aw : bw;
+ int sg = as || bs;
+ return sg ? ty_int(w) : ty_uint(w);
+ }
+ if (a->kind == TY_BOOL && b->kind == TY_BOOL) return ty_bool();
+ return NULL;
+}
+
+/* intlit operand must fit the resolved integer type */
+static int lit_fits_rt(Type* lit, Type* rt, Loc loc, const char* what) {
+ if (lit->kind != TY_INTLIT) return 1;
+ if (ty_is_int(rt) || rt->kind == TY_CHAR || rt->kind == TY_BOOL) {
+ if (!intlit_fits(lit->litval, lit->litneg, rt)) {
+ char b[128];
+ err(loc, "%s literal does not fit %s; use 'convert'",
+ what, type_name(rt, b, sizeof b));
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/* ------------------------------------------------------------------ */
+/* focus */
+/* ------------------------------------------------------------------ */
+
+static FocusEnt* focus_top(Chk* c) { return &c->st.foc[c->st.nfoc - 1]; }
+
+static void focus_push_cell(Chk* c, Type* t, Loc loc) {
+ if (c->st.nfoc >= MAX_FOC) {
+ err(loc, "focus nesting too deep (limit %d)", MAX_FOC);
+ c->st.dead = 1;
+ return;
+ }
+ c->st.foc[c->st.nfoc].is_frame = 0;
+ c->st.foc[c->st.nfoc].cell = t;
+ c->st.nfoc++;
+}
+
+/* ------------------------------------------------------------------ */
+/* the per-command checker (one abstract step + IR emission) */
+/* ------------------------------------------------------------------ */
+
+static void check_mark(Chk* c, Node* n);
+
+static void check_insn(Chk* c, Node* n) {
+ if (n->kind == A_MARK) { check_mark(c, n); return; }
+ if (c->st.dead) {
+ if (c->st.unk) return; /* not yet known; re-walk later */
+ if (c->erroring) err(n->loc, "unreachable command");
+ return;
+ }
+ FnIR* fn = c->fn;
+ CState* st = &c->st;
+
+ switch (n->kind) {
+ /* ---------------- navigation ---------------- */
+ case A_MOVE_NAME: {
+ /* Named connections resolve against the frame root and anchor the
+ focus: the history is reset to [frame, cell]. Only indexed and
+ address moves are relative (they push onto the history). */
+ int li = find_local(c, n->name);
+ if (li >= 0) {
+ Type* t = fn->locals[li].type;
+ Insn* in = emit(c, I_MOVE_LOCAL, n->loc);
+ in->a = li;
+ in->t = t;
+ st->nfoc = 1;
+ focus_push_cell(c, t, n->loc);
+ return;
+ }
+ int gi = find_glob(c, n->name);
+ if (gi >= 0) {
+ Type* t = c->prog->globs[gi].type;
+ Insn* in = emit(c, I_MOVE_GLOBAL, n->loc);
+ in->a = gi;
+ in->t = t;
+ st->nfoc = 1;
+ focus_push_cell(c, t, n->loc);
+ return;
+ }
+ err(n->loc, "no cell named '%s' in the current frame", n->name);
+ return;
+ }
+ case A_MOVE_LIT: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame || f->cell->kind != TY_ARR) {
+ err(n->loc, "focus must be an array or block to move by index");
+ return;
+ }
+ if (n->imm.neg || n->imm.u > 0x7FFFFFFFULL) {
+ err(n->loc, "array index literal out of range");
+ return;
+ }
+ if (f->cell->count && n->imm.u >= f->cell->count) {
+ char b[128];
+ err(n->loc, "index %llu out of bounds for %s",
+ (unsigned long long)n->imm.u, type_name(f->cell, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_MOVE_LIT, n->loc);
+ in->imm.kind = IV_UINT;
+ in->imm.u = n->imm.u;
+ in->t = f->cell->inner;
+ in->b = (int)(f->cell->count ? f->cell->count : 0);
+ if (f->cell->count == 0) in->b = -1; /* unsized: runtime header check */
+ focus_push_cell(c, f->cell->inner, n->loc);
+ return;
+ }
+ case A_MOVE_INDEX: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame || f->cell->kind != TY_ARR) {
+ err(n->loc, "focus must be an array or block to move by index");
+ return;
+ }
+ if (!ty_is_any_int(st->wv)) {
+ char b[128];
+ err(n->loc, "'move index' requires an integer working value (found %s)",
+ type_name(st->wv, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_MOVE_INDEX, n->loc);
+ in->t = f->cell->inner;
+ in->b = f->cell->count ? (int)f->cell->count : -1;
+ focus_push_cell(c, f->cell->inner, n->loc);
+ return;
+ }
+ case A_MOVE_AT: {
+ if (st->wv->kind != TY_ADDR) {
+ char b[128];
+ err(n->loc, "'move at' requires an address working value (found %s)",
+ type_name(st->wv, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_MOVE_AT, n->loc);
+ in->t = st->wv->inner;
+ focus_push_cell(c, st->wv->inner, n->loc);
+ return;
+ }
+ case A_BACK: {
+ if (st->nfoc <= 1) {
+ err(n->loc, "focus is already at the frame root");
+ return;
+ }
+ emit(c, I_BACK, n->loc);
+ st->nfoc--;
+ return;
+ }
+ case A_HOME: {
+ emit(c, I_HOME, n->loc);
+ st->nfoc = 1;
+ return;
+ }
+
+ /* ---------------- addresses ---------------- */
+ case A_ADDRESS_HERE: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame) {
+ err(n->loc, "cannot take the address of the frame root");
+ return;
+ }
+ Insn* in = emit(c, I_ADDR_HERE, n->loc);
+ in->t = ty_addr_to(f->cell);
+ st->wv = in->t;
+ return;
+ }
+ case A_ADDRESS_NAME: {
+ int li = find_local(c, n->name);
+ if (li >= 0) {
+ Insn* in = emit(c, I_ADDR_LOCAL, n->loc);
+ in->a = li;
+ in->t = ty_addr_to(fn->locals[li].type);
+ st->wv = in->t;
+ return;
+ }
+ int gi = find_glob(c, n->name);
+ if (gi >= 0) {
+ Insn* in = emit(c, I_ADDR_GLOBAL, n->loc);
+ in->a = gi;
+ in->t = ty_addr_to(c->prog->globs[gi].type);
+ st->wv = in->t;
+ return;
+ }
+ int fi = find_fn(c, n->name);
+ if (fi >= 0) {
+ Insn* in = emit(c, I_ADDR_FN, n->loc);
+ in->a = fi;
+ st->wv = ty_func();
+ return;
+ }
+ err(n->loc, "no cell or transformation named '%s'", n->name);
+ return;
+ }
+
+ /* ---------------- cells ---------------- */
+ case A_MAKE_VALUE: {
+ Type* t = ast_to_type(n->t, n->loc);
+ if (t->kind == TY_ARR && t->count == 0) {
+ err(n->loc, "unsized array cells must be created with 'make <size> of <type>'");
+ return;
+ }
+ if (n->name) {
+ int li = find_local(c, n->name);
+ Insn* in = emit(c, I_MAKE_LOCAL, n->loc);
+ in->a = li;
+ in->t = t;
+ } else {
+ Insn* in = emit(c, I_MAKE_ANON, n->loc);
+ in->t = t;
+ st->wv = ty_addr_to(t);
+ }
+ return;
+ }
+ case A_MAKE_ARRAY: {
+ Type* t = ast_to_type(n->t, n->loc);
+ if (t->kind != TY_ARR || t->count == 0) {
+ err(n->loc, "'make array' requires a size and an element type");
+ return;
+ }
+ if (n->name) {
+ int li = find_local(c, n->name);
+ Insn* in = emit(c, I_MAKE_LOCAL, n->loc);
+ in->a = li;
+ in->t = t;
+ } else {
+ Insn* in = emit(c, I_MAKE_ANON, n->loc);
+ in->t = t;
+ st->wv = ty_addr_to(t);
+ }
+ return;
+ }
+ case A_MAKE_STRING: {
+ if (n->name) {
+ int li = find_local(c, n->name);
+ if (li < 0) return;
+ LocalIR* loc = &fn->locals[li];
+ if (!loc->str_init) {
+ loc->str_init = xmalloc(n->strlen + 1);
+ memcpy(loc->str_init, n->str, n->strlen);
+ loc->str_init[n->strlen] = 0;
+ loc->str_initlen = n->strlen + 1;
+ }
+ Insn* in = emit(c, I_MAKE_STRLOCAL, n->loc);
+ in->a = li;
+ in->t = ty_array(ty_uint(8), n->strlen + 1);
+ } else {
+ int si = c->prog->nstrs++;
+ c->prog->strs = xrealloc(c->prog->strs, (size_t)c->prog->nstrs * sizeof(StrIR));
+ c->prog->strs[si].bytes = xmalloc(n->strlen + 1);
+ memcpy(c->prog->strs[si].bytes, n->str, n->strlen);
+ c->prog->strs[si].bytes[n->strlen] = 0;
+ c->prog->strs[si].len = n->strlen;
+ Insn* in = emit(c, I_MAKE_STRANON, n->loc);
+ in->a = si;
+ in->t = ty_addr_to(ty_array(ty_uint(8), n->strlen + 1));
+ st->wv = in->t;
+ }
+ return;
+ }
+ case A_MAKE_DYN: {
+ Type* elem = ast_to_type(n->t, n->loc);
+ if (elem->kind == TY_ARR) {
+ err(n->loc, "block elements must be values, not arrays");
+ return;
+ }
+ if (elem->kind == TY_FUNC) {
+ err(n->loc, "blocks of transform references are not supported");
+ return;
+ }
+ Insn* in = emit(c, I_MAKE_DYN, n->loc);
+ in->t = elem;
+ if (n->name) {
+ int li = find_local(c, n->name);
+ if (li >= 0) {
+ Type* st_t = fn->locals[li].type;
+ if (!ty_is_int(st_t) && st_t->kind != TY_INTLIT) {
+ err(n->loc, "size cell '%s' must be an integer", n->name);
+ return;
+ }
+ in->b = 0;
+ in->a = li;
+ } else {
+ int gi = find_glob(c, n->name);
+ if (gi < 0) {
+ err(n->loc, "no cell named '%s' to use as a size", n->name);
+ return;
+ }
+ if (!ty_is_int(c->prog->globs[gi].type)) {
+ err(n->loc, "size cell '%s' must be an integer", n->name);
+ return;
+ }
+ in->b = 2;
+ in->a = gi;
+ }
+ } else {
+ if (n->imm.neg) {
+ err(n->loc, "allocation size cannot be negative");
+ return;
+ }
+ in->b = 1;
+ set_imm(in, &n->imm);
+ }
+ st->wv = ty_addr_to(ty_array(elem, 0));
+ if (n->name2) {
+ int li = find_local(c, n->name2);
+ if (li >= 0) {
+ Insn* bind = emit(c, I_BIND_LOCAL, n->loc);
+ bind->a = li;
+ bind->t = st->wv;
+ }
+ }
+ return;
+ }
+ case A_FREE: {
+ if (st->wv->kind != TY_ADDR) {
+ char b[128];
+ err(n->loc, "'free' requires an address working value (found %s)",
+ type_name(st->wv, b, sizeof b));
+ return;
+ }
+ emit(c, I_FREE, n->loc);
+ return;
+ }
+ case A_READ: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame) {
+ err(n->loc, "the frame root holds no value; move to a cell first");
+ return;
+ }
+ if (f->cell->kind == TY_ARR) {
+ err(n->loc, "cannot read a whole array; move to an element first");
+ return;
+ }
+ Insn* in = emit(c, I_LOAD, n->loc);
+ in->t = f->cell;
+ st->wv = f->cell;
+ return;
+ }
+ case A_WRITE: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame) {
+ err(n->loc, "the frame root cannot be written");
+ return;
+ }
+ if (f->cell->kind == TY_ARR) {
+ err(n->loc, "cannot write a whole array; move to an element first");
+ return;
+ }
+ Type* cell = f->cell;
+ Type* wv = st->wv;
+ Insn* in = emit(c, I_STORE, n->loc);
+ in->t = cell;
+ if (type_eq(cell, wv)) {
+ in->b = SC_EXACT;
+ } else if (wv->kind == TY_INTLIT && intlit_fits(wv->litval, wv->litneg, cell)) {
+ if (cell->kind == TY_BOOL) in->b = SC_INTLIT_BOOL;
+ else if (ty_is_float(cell)) in->b = SC_INTLIT_FLOAT;
+ else in->b = SC_INTLIT;
+ } else if (wv->kind == TY_FLOAT64 && cell->kind == TY_FLOAT32) {
+ in->b = SC_F32;
+ } else {
+ char b1[128], b2[128];
+ err(n->loc, "cannot write %s into a cell of type %s; use 'convert'",
+ type_name(wv, b1, sizeof b1), type_name(cell, b2, sizeof b2));
+ return;
+ }
+ return;
+ }
+
+ /* ---------------- working value ---------------- */
+ case A_SET: {
+ Insn* in = emit(c, I_SET, n->loc);
+ switch (n->imm.kind) {
+ case IM_NONE:
+ in->imm.kind = IV_NONE;
+ in->t = ty_nothing();
+ break;
+ case IM_INT:
+ in->t = ty_intlit(n->imm.u, n->imm.neg);
+ set_imm(in, &n->imm);
+ break;
+ case IM_FLOAT:
+ in->imm.kind = IV_FLOAT;
+ in->imm.d = n->imm.d;
+ in->t = ty_float64();
+ break;
+ case IM_CHAR:
+ in->imm.kind = IV_UINT;
+ in->imm.u = n->imm.u;
+ in->t = ty_char();
+ break;
+ case IM_BOOL:
+ in->imm.kind = IV_UINT;
+ in->imm.u = (uint64_t)n->imm.b;
+ in->t = ty_bool();
+ break;
+ }
+ st->wv = in->t;
+ return;
+ }
+ case A_ARITH: {
+ Type* wv = st->wv;
+ IROp op = n->arith == AR_ADD ? I_ADD : n->arith == AR_SUB ? I_SUB :
+ n->arith == AR_MUL ? I_MUL : n->arith == AR_DIV ? I_DIV : I_MOD;
+
+ if (n->has_imm) {
+ if (n->imm.kind != IM_INT && n->imm.kind != IM_FLOAT) {
+ err(n->loc, "arithmetic operands must be numeric");
+ return;
+ }
+ Type* lit = n->imm.kind == IM_INT ? ty_intlit(n->imm.u, n->imm.neg) : ty_float64();
+
+ if (wv->kind == TY_ADDR) {
+ if (n->arith != AR_ADD && n->arith != AR_SUB) {
+ err(n->loc, "only add and subtract apply to addresses");
+ return;
+ }
+ if (n->imm.kind != IM_INT) {
+ err(n->loc, "address arithmetic requires an integer");
+ return;
+ }
+ if (!lit_fits_rt(lit, ty_int(64), n->loc, "address offset")) return;
+ Insn* in = emit(c, op, n->loc);
+ in->b = 1;
+ in->t = wv;
+ set_imm(in, &n->imm);
+ return;
+ }
+
+ Type* rt = numeric_pair_type(wv, lit);
+ if (!rt || !ty_is_numeric(rt)) {
+ char b[128];
+ err(n->loc, "arithmetic requires numeric operands (working value is %s)",
+ type_name(wv, b, sizeof b));
+ return;
+ }
+ if (!lit_fits_rt(lit, rt, n->loc, "arithmetic")) return;
+
+ /* constant folding: intlit op intlit */
+ if (wv->kind == TY_INTLIT && n->imm.kind == IM_INT) {
+ int64_t a, b;
+ if (lit_value_int(wv, &a) && lit_value_int(lit, &b)) {
+ int64_t r = 0;
+ switch (n->arith) {
+ case AR_ADD: r = (int64_t)((uint64_t)a + (uint64_t)b); break;
+ case AR_SUB: r = (int64_t)((uint64_t)a - (uint64_t)b); break;
+ case AR_MUL: r = (int64_t)((uint64_t)a * (uint64_t)b); break;
+ case AR_DIV:
+ if (b == 0) { err(n->loc, "division by zero in constant expression"); return; }
+ r = b == -1 ? (int64_t)(0 - (uint64_t)a) : a / b;
+ break;
+ case AR_MOD:
+ if (b == 0) { err(n->loc, "remainder by zero in constant expression"); return; }
+ r = b == -1 ? 0 : a % b;
+ break;
+ }
+ Insn* s = emit(c, I_SET, n->loc);
+ s->imm.kind = IV_INT;
+ s->imm.u = (uint64_t)r;
+ s->t = intlit_from_int64(r);
+ st->wv = s->t;
+ return;
+ }
+ }
+
+ /* coerce an intlit working value into float arithmetic */
+ if (wv->kind == TY_INTLIT && ty_is_float(rt)) {
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = ty_float64();
+ st->wv = ty_float64();
+ wv = ty_float64();
+ rt = ty_float64();
+ if (n->imm.kind == IM_INT) lit = ty_float64();
+ }
+
+ Insn* in = emit(c, op, n->loc);
+ in->b = 1;
+ in->t = rt;
+ set_imm(in, &n->imm);
+ st->wv = rt;
+ return;
+ }
+
+ /* operand popped from the value stack */
+ if (st->nstk < 1) {
+ err(n->loc, "value stack is empty");
+ return;
+ }
+ Type* popped = st->stk[st->nstk - 1];
+
+ if (wv->kind == TY_ADDR || popped->kind == TY_ADDR) {
+ if (wv->kind == TY_ADDR && ty_is_any_int(popped) &&
+ (n->arith == AR_ADD || n->arith == AR_SUB)) {
+ Insn* in = emit(c, op, n->loc);
+ in->b = 0;
+ in->t = wv;
+ st->nstk--;
+ return;
+ }
+ if (n->arith == AR_SUB && wv->kind == TY_ADDR && popped->kind == TY_ADDR) {
+ Insn* in = emit(c, I_SUB, n->loc);
+ in->b = 0;
+ in->a = 1; /* pointer difference */
+ in->t = ty_int(64);
+ st->nstk--;
+ st->wv = ty_int(64);
+ return;
+ }
+ err(n->loc, "invalid address arithmetic (address +/- integer, or address - address)");
+ return;
+ }
+
+ Type* rt = numeric_pair_type(wv, popped);
+ if (!rt || !ty_is_numeric(rt)) {
+ char b1[128], b2[128];
+ err(n->loc, "arithmetic requires matching numeric types (%s and %s); use 'convert'",
+ type_name(wv, b1, sizeof b1), type_name(popped, b2, sizeof b2));
+ return;
+ }
+ if (!lit_fits_rt(wv, rt, n->loc, "arithmetic")) return;
+ if (!lit_fits_rt(popped, rt, n->loc, "arithmetic")) return;
+ Insn* in = emit(c, op, n->loc);
+ in->b = 0;
+ in->t = rt;
+ st->nstk--;
+ st->wv = rt;
+ return;
+ }
+ case A_NEG: {
+ Type* wv = st->wv;
+ if (wv->kind == TY_INTLIT) {
+ int64_t v;
+ if (lit_value_int(wv, &v)) {
+ st->wv = intlit_from_int64(v == INT64_MIN ? INT64_MIN : -v);
+ Insn* in = emit(c, I_SET, n->loc);
+ in->t = st->wv;
+ in->imm.kind = IV_INT;
+ in->imm.u = (uint64_t)(v == INT64_MIN ? INT64_MIN : -v);
+ return;
+ }
+ }
+ if (!ty_is_signed_int(wv) && !ty_is_float(wv)) {
+ char b[128];
+ err(n->loc, "negate requires a signed integer or a float (found %s)",
+ type_name(wv, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_NEG, n->loc);
+ in->t = wv;
+ return;
+ }
+
+ /* ---------------- value stack ---------------- */
+ case A_PUSH: {
+ if (st->nstk >= MAX_STK) {
+ err(n->loc, "value stack too deep (limit %d)", MAX_STK);
+ return;
+ }
+ Insn* in = emit(c, I_PUSH, n->loc);
+ in->t = st->wv;
+ st->stk[st->nstk++] = st->wv;
+ return;
+ }
+ case A_POP: {
+ if (st->nstk < 1) {
+ err(n->loc, "value stack is empty");
+ return;
+ }
+ Insn* in = emit(c, I_POP, n->loc);
+ in->t = st->stk[st->nstk - 1];
+ st->nstk--;
+ st->wv = in->t;
+ return;
+ }
+ case A_SWAP: {
+ if (st->nstk < 1) {
+ err(n->loc, "value stack is empty");
+ return;
+ }
+ emit(c, I_SWAP, n->loc);
+ Type* tmp = st->stk[st->nstk - 1];
+ st->stk[st->nstk - 1] = st->wv;
+ st->wv = tmp;
+ return;
+ }
+
+ /* ---------------- comparison ---------------- */
+ case A_CMP: {
+ Type* wv = st->wv;
+ Type* rt;
+ if (n->has_imm) {
+ Type* lit = NULL;
+ if (n->imm.kind == IM_INT) lit = ty_intlit(n->imm.u, n->imm.neg);
+ else if (n->imm.kind == IM_FLOAT) lit = ty_float64();
+ else if (n->imm.kind == IM_CHAR) lit = ty_char();
+ else if (n->imm.kind == IM_BOOL) lit = ty_bool();
+ else { err(n->loc, "invalid comparison operand"); return; }
+
+ if (wv->kind == TY_ADDR) {
+ if (lit->kind == TY_INTLIT && intlit_fits(lit->litval, lit->litneg, ty_uint(64))) {
+ rt = ty_uint(64);
+ } else {
+ err(n->loc, "addresses compare with addresses or nonnegative integer literals");
+ return;
+ }
+ } else if (wv->kind == TY_FUNC || wv->kind == TY_ARR) {
+ err(n->loc, "transform references and arrays cannot be compared");
+ return;
+ } else if (wv->kind == TY_BOOL) {
+ if (lit->kind != TY_BOOL && !(lit->kind == TY_INTLIT && intlit_fits(lit->litval, lit->litneg, ty_bool()))) {
+ err(n->loc, "booleans compare only with booleans");
+ return;
+ }
+ rt = ty_bool();
+ } else {
+ rt = numeric_pair_type(wv, lit);
+ if (!rt || (!ty_is_numeric(rt) && rt->kind != TY_CHAR)) {
+ char b[128];
+ err(n->loc, "cannot compare %s with that operand",
+ type_name(wv, b, sizeof b));
+ return;
+ }
+ if (!lit_fits_rt(lit, rt, n->loc, "comparison")) return;
+ }
+ Insn* in = emit(c, I_CMP, n->loc);
+ in->b = 1;
+ in->t = rt;
+ set_imm(in, &n->imm);
+ } else {
+ if (st->nstk < 1) {
+ err(n->loc, "value stack is empty");
+ return;
+ }
+ Type* popped = st->stk[st->nstk - 1];
+ if (wv->kind == TY_ADDR && popped->kind == TY_ADDR) {
+ rt = ty_uint(64);
+ } else if (wv->kind == TY_ADDR || popped->kind == TY_ADDR) {
+ err(n->loc, "addresses compare only with addresses");
+ return;
+ } else if (wv->kind == TY_FUNC || popped->kind == TY_FUNC) {
+ err(n->loc, "transform references cannot be compared");
+ return;
+ } else if (wv->kind == TY_ARR || popped->kind == TY_ARR) {
+ err(n->loc, "arrays cannot be compared");
+ return;
+ } else if (wv->kind == TY_BOOL || popped->kind == TY_BOOL) {
+ if (!(wv->kind == TY_BOOL && popped->kind == TY_BOOL)) {
+ err(n->loc, "booleans compare only with booleans");
+ return;
+ }
+ rt = ty_bool();
+ } else {
+ rt = numeric_pair_type(wv, popped);
+ if (!rt || (!ty_is_numeric(rt) && rt->kind != TY_CHAR)) {
+ char b1[128], b2[128];
+ err(n->loc, "cannot compare %s with %s; use 'convert'",
+ type_name(wv, b1, sizeof b1), type_name(popped, b2, sizeof b2));
+ return;
+ }
+ if (!lit_fits_rt(wv, rt, n->loc, "comparison")) return;
+ if (!lit_fits_rt(popped, rt, n->loc, "comparison")) return;
+ }
+ Insn* in = emit(c, I_CMP, n->loc);
+ in->b = 0;
+ in->t = rt;
+ st->nstk--;
+ }
+ st->cmp = 1;
+ return;
+ }
+
+ /* ---------------- conversion ---------------- */
+ case A_CONV: {
+ Type* to = ast_to_type(n->t, n->loc);
+ Type* from = st->wv;
+ if (to->kind == TY_ARR) { err(n->loc, "cannot convert to an array type"); return; }
+ if (to->kind == TY_FUNC) { err(n->loc, "cannot convert to a transform reference"); return; }
+ if (from->kind == TY_FUNC) { err(n->loc, "cannot convert a transform reference"); return; }
+ if (from->kind == TY_ARR) { err(n->loc, "cannot convert an array"); return; }
+ if (to->kind == TY_NOTHING || from->kind == TY_NOTHING) {
+ err(n->loc, "cannot convert to or from nothing");
+ return;
+ }
+ int ok;
+ if (from->kind == TY_INTLIT) {
+ /* integer literals convert to any scalar numeric type; the value
+ wraps modulo the target width like any integer conversion */
+ ok = ty_is_int(to) || ty_is_float(to) || to->kind == TY_CHAR ||
+ to->kind == TY_BOOL || to->kind == TY_ADDR;
+ } else {
+ ok = (ty_is_int(from) || ty_is_float(from) || ty_is_bool(from) ||
+ ty_is_char(from) || ty_is_addr(from)) &&
+ (ty_is_int(to) || ty_is_float(to) || ty_is_bool(to) ||
+ ty_is_char(to) || ty_is_addr(to));
+ }
+ if (!ok) {
+ char b[128];
+ err(n->loc, "cannot convert from %s", type_name(from, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_CONV, n->loc);
+ in->t = to;
+ st->wv = to;
+ return;
+ }
+
+ /* ---------------- control flow ---------------- */
+ case A_JMP: {
+ int li = find_label(c, n->name);
+ if (li < 0) {
+ err(n->loc, "no label named '%s' in transformation '%s'", n->name, fn->name);
+ return;
+ }
+ if (n->has_cond && !st->cmp) {
+ err(n->loc, "conditional jump before any comparison");
+ return;
+ }
+ if (!label_merge(c, li, n->loc)) return;
+ Insn* in = emit(c, n->has_cond ? I_JMPCOND : I_JMP, n->loc);
+ in->a = li;
+ if (n->has_cond) {
+ switch (n->cond) {
+ case T_LESS: in->b = CMP_LESS; break;
+ case T_EQUAL: in->b = CMP_EQUAL; break;
+ case T_GREATER: in->b = CMP_GREATER; break;
+ case T_DIFFERENT: in->b = CMP_DIFFERENT; break;
+ case T_LESS_OR_EQUAL: in->b = CMP_LESS_OR_EQUAL; break;
+ case T_GREATER_OR_EQUAL: in->b = CMP_GREATER_OR_EQUAL; break;
+ default: in->b = CMP_EQUAL; break;
+ }
+ }
+ if (!n->has_cond) st->dead = 1;
+ return;
+ }
+ case A_CALL_NAME: {
+ int fi = find_fn(c, n->name);
+ if (fi < 0) {
+ err(n->loc, "no transformation named '%s'", n->name);
+ return;
+ }
+ FnIR* callee = &c->prog->fns[fi];
+ int np = callee->nparams;
+ if (st->nstk < np) {
+ err(n->loc, "transformation '%s' expects %d argument%s on the value stack (found %d)",
+ n->name, np, np == 1 ? "" : "s", st->nstk);
+ return;
+ }
+ if (callee->result == NULL) {
+ c->blocked = 1;
+ return;
+ }
+ unsigned mask = 0;
+ for (int i = 0; i < np; i++) {
+ Type* want = callee->locals[i].type;
+ Type* got = st->stk[st->nstk - np + i];
+ unsigned k = 0;
+ if (type_eq(want, got)) k = 0;
+ else if (got->kind == TY_INTLIT && intlit_fits(got->litval, got->litneg, want)) k = 1;
+ else if (got->kind == TY_FLOAT64 && want->kind == TY_FLOAT32) k = 2;
+ else {
+ char b1[128], b2[128];
+ err(n->loc, "argument %d of '%s' needs %s but the stack holds %s",
+ i + 1, n->name, type_name(want, b1, sizeof b1), type_name(got, b2, sizeof b2));
+ return;
+ }
+ mask |= k << (2 * i);
+ }
+ st->nstk -= np;
+ Insn* in = emit(c, I_CALL, n->loc);
+ in->a = fi;
+ in->b = (int)mask;
+ in->t = callee->result;
+ if (callee->result->kind != TY_NOTHING) st->wv = callee->result;
+ return;
+ }
+ case A_CALL_IND: {
+ if (st->wv->kind != TY_FUNC) {
+ char b[128];
+ err(n->loc, "'call' without a name requires a transform reference working value (found %s)",
+ type_name(st->wv, b, sizeof b));
+ return;
+ }
+ emit(c, I_CALLIND, n->loc);
+ st->wv = ty_nothing();
+ return;
+ }
+ case A_RET: {
+ if (st->nstk != 0) {
+ err(n->loc, "the value stack must be empty at return (holds %d value%s)",
+ st->nstk, st->nstk == 1 ? "" : "s");
+ return;
+ }
+ /* coerce an integer literal working value to the result type */
+ if (st->wv->kind == TY_INTLIT) {
+ Type* want = fn->result;
+ if (want && intlit_fits(st->wv->litval, st->wv->litneg, want) &&
+ (ty_is_int(want) || want->kind == TY_CHAR || want->kind == TY_BOOL ||
+ ty_is_float(want))) {
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = want;
+ st->wv = want;
+ } else {
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = intlit_fits(st->wv->litval, st->wv->litneg, ty_int(64))
+ ? ty_int(64) : ty_uint(64);
+ st->wv = cv->t;
+ }
+ }
+ if (!c->result_seen) {
+ c->result_first = st->wv;
+ c->result_seen = 1;
+ } else if (!type_eq(c->result_first, st->wv)) {
+ char b1[128], b2[128];
+ err(n->loc, "return type mismatch: an earlier return produces %s but this one produces %s",
+ type_name(c->result_first, b1, sizeof b1), type_name(st->wv, b2, sizeof b2));
+ return;
+ }
+ if (fn->declared_result && fn->result && !type_eq(fn->result, st->wv)) {
+ char b1[128], b2[128];
+ err(n->loc, "return type mismatch: '%s' declares result %s but returns %s",
+ fn->name, type_name(fn->result, b1, sizeof b1), type_name(st->wv, b2, sizeof b2));
+ return;
+ }
+ emit(c, I_RET, n->loc);
+ st->dead = 1;
+ return;
+ }
+ case A_STOP: {
+ Insn* in = emit(c, I_STOP, n->loc);
+ in->t = st->wv;
+ st->dead = 1;
+ return;
+ }
+ case A_MARK:
+ /* handled before the dead-state check in check_insn */
+ return;
+
+ /* ---------------- external world ---------------- */
+ case A_INPUT: {
+ emit(c, I_INPUT, n->loc);
+ st->wv = ty_int(64);
+ return;
+ }
+ case A_OUTPUT: {
+ Type* wv = st->wv;
+ if (!(ty_is_numeric(wv) || wv->kind == TY_BOOL || wv->kind == TY_CHAR ||
+ wv->kind == TY_ADDR)) {
+ char b[128];
+ err(n->loc, "cannot output %s", type_name(wv, b, sizeof b));
+ return;
+ }
+ Insn* in = emit(c, I_OUTPUT, n->loc);
+ in->t = wv;
+ return;
+ }
+ case A_OUTPUT_TEXT: {
+ Type* wv = st->wv;
+ if (!(wv->kind == TY_ADDR &&
+ (wv->inner->kind == TY_UINT8 ||
+ (wv->inner->kind == TY_ARR && wv->inner->inner->kind == TY_UINT8)))) {
+ char b[128];
+ err(n->loc, "'output text' requires an address to bytes or to a byte array (found %s)",
+ type_name(wv, b, sizeof b));
+ return;
+ }
+ emit(c, I_OUTTEXT, n->loc);
+ return;
+ }
+ case A_OPEN: {
+ Type* wv = st->wv;
+ if (!(wv->kind == TY_ADDR &&
+ (wv->inner->kind == TY_UINT8 ||
+ (wv->inner->kind == TY_ARR && wv->inner->inner->kind == TY_UINT8)))) {
+ err(n->loc, "'open' requires an address to a NUL-terminated byte string");
+ return;
+ }
+ Insn* in = emit(c, I_OPEN, n->loc);
+ in->b = n->mode == T_READ ? 0 : n->mode == T_WRITE ? 1 : 2;
+ st->wv = ty_int(64);
+ return;
+ }
+ case A_CLOSE: {
+ if (!ty_is_any_int(st->wv)) {
+ err(n->loc, "'close' requires an integer handle working value");
+ return;
+ }
+ emit(c, I_CLOSE, n->loc);
+ st->wv = ty_int(64);
+ return;
+ }
+ case A_SYSTEM: {
+ Type* wv = st->wv;
+ if (!(wv->kind == TY_ADDR &&
+ (wv->inner->kind == TY_UINT8 ||
+ (wv->inner->kind == TY_ARR && wv->inner->inner->kind == TY_UINT8)))) {
+ err(n->loc, "'system' requires an address to a NUL-terminated byte string");
+ return;
+ }
+ emit(c, I_SYSTEM, n->loc);
+ st->wv = ty_int(64);
+ return;
+ }
+
+ /* ---------------- concurrency ---------------- */
+ case A_SPAWN: {
+ int fi = find_fn(c, n->name);
+ if (fi < 0) {
+ err(n->loc, "no transformation named '%s'", n->name);
+ return;
+ }
+ if (c->prog->fns[fi].nparams != 0) {
+ err(n->loc, "spawned transformations must take no parameters ('%s' takes %d)",
+ n->name, c->prog->fns[fi].nparams);
+ return;
+ }
+ Insn* in = emit(c, I_SPAWN, n->loc);
+ in->a = fi;
+ return;
+ }
+ case A_WAIT: {
+ emit(c, I_WAIT, n->loc);
+ st->wv = ty_int(64);
+ return;
+ }
+ }
+}
+
+/* Marks are processed even in dead state so that labels targeted by
+ forward jumps can revive execution. */
+static void check_mark(Chk* c, Node* n) {
+ int li = find_label(c, n->name);
+ if (li < 0) return;
+ Insn* in = emit(c, I_LBL, n->loc);
+ in->a = li;
+ if (!c->st.dead) {
+ /* merge the fallthrough edge */
+ if (!label_merge(c, li, n->loc)) return;
+ }
+ CState* ls = c->labels[li].state;
+ if (!ls) {
+ /* no live edge reaches this label */
+ if (!c->st.dead) { c->st.dead = 1; c->st.unk = 1; }
+ } else {
+ st_clone(&c->st, ls);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* locals & labels */
+/* ------------------------------------------------------------------ */
+
+static void collect_labels(Chk* c, FnAst* fa) {
+ c->nlabels = 0;
+ for (size_t i = 0; i < fa->nbody; i++) {
+ Node* n = &fa->body[i];
+ if (n->kind != A_MARK) continue;
+ if (find_label(c, n->name) >= 0) {
+ err(n->loc, "duplicate label '%s' in transformation '%s'", n->name, fa->name);
+ continue;
+ }
+ c->labels[c->nlabels].name = n->name;
+ c->labels[c->nlabels].state = NULL;
+ c->labels[c->nlabels].loc = n->loc;
+ c->nlabels++;
+ if (c->nlabels >= MAX_LABELS) {
+ err(n->loc, "too many labels in one transformation (limit %d)", MAX_LABELS);
+ return;
+ }
+ }
+}
+
+static void free_label_states(Chk* c) {
+ for (int i = 0; i < MAX_LABELS; i++) {
+ free(c->labels[i].state);
+ c->labels[i].state = NULL;
+ }
+}
+
+static Type* make_local_type(Chk* c, Node* n) {
+ (void)c;
+ switch (n->kind) {
+ case A_MAKE_STRING: return ty_array(ty_uint(8), n->strlen + 1);
+ case A_MAKE_DYN: return ty_addr_to(ty_array(ast_to_type(n->t, n->loc), 0));
+ default: return ast_to_type(n->t, n->loc);
+ }
+}
+
+static void collect_locals(Chk* c, FnAst* fa) {
+ FnIR* fn = c->fn;
+ fn->nlocals = 0;
+ for (size_t i = 0; i < fa->nparams; i++) {
+ if (find_local(c, fa->params[i].name) >= 0) {
+ err(fa->params[i].loc, "duplicate parameter '%s'", fa->params[i].name);
+ continue;
+ }
+ Type* t = ast_to_type(fa->params[i].t, fa->params[i].loc);
+ if (t->kind == TY_ARR) {
+ err(fa->params[i].loc, "arrays cannot be parameters; pass an address instead");
+ t = ty_nothing();
+ }
+ LocalIR* loc = &fn->locals[fn->nlocals++];
+ memset(loc, 0, sizeof *loc);
+ loc->name = fa->params[i].name;
+ loc->type = t;
+ loc->is_param = 1;
+ }
+ for (size_t i = 0; i < fa->nbody && fn->nlocals < MAX_LOCALS; i++) {
+ Node* n = &fa->body[i];
+ const char* name = NULL;
+ if (n->kind == A_MAKE_VALUE && n->name) name = n->name;
+ else if (n->kind == A_MAKE_ARRAY && n->name) name = n->name;
+ else if (n->kind == A_MAKE_STRING && n->name) name = n->name;
+ else if (n->kind == A_MAKE_DYN && n->name2) name = n->name2;
+ if (!name) continue;
+ if (find_local(c, name) >= 0) {
+ err(n->loc, "duplicate cell '%s' in transformation '%s'", name, fa->name);
+ continue;
+ }
+ Type* ty = make_local_type(c, n);
+ if (ty->kind == TY_ARR && ty->count == 0) {
+ err(n->loc, "cell '%s' would be an unsized array", name);
+ ty = ty_nothing();
+ }
+ if (fn->nlocals >= MAX_LOCALS) {
+ err(n->loc, "too many cells in one transformation (limit %d)", MAX_LOCALS);
+ break;
+ }
+ LocalIR* loc = &fn->locals[fn->nlocals++];
+ memset(loc, 0, sizeof *loc);
+ loc->name = (char*)name;
+ loc->type = ty;
+ loc->is_param = 0;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* body checking */
+/* ------------------------------------------------------------------ */
+
+static int fn_declared_conflict(Chk* c);
+
+static int check_fn_body(Chk* c, FnAst* fa) {
+ c->fn->nins = 0;
+ c->blocked = 0;
+ c->result_first = NULL;
+ c->result_seen = 0;
+
+ collect_labels(c, fa);
+ collect_locals(c, fa);
+ free_label_states(c);
+
+ /* settling rounds: walk without emitting until no label gains a state */
+ for (int round = 0; round <= c->nlabels + 1; round++) {
+ c->changed = 0;
+ c->emitting = 0;
+ c->erroring = 0;
+ c->fn->nins = 0;
+ st_init(c);
+ for (size_t i = 0; i < fa->nbody; i++) {
+ check_insn(c, &fa->body[i]);
+ if (c->blocked) return 0;
+ if (crn_error_count()) return 0;
+ }
+ if (!c->changed) break;
+ }
+
+ /* final emitting walk */
+ c->emitting = 1;
+ c->erroring = 1;
+ c->fn->nins = 0;
+ st_init(c);
+ for (size_t i = 0; i < fa->nbody; i++) {
+ check_insn(c, &fa->body[i]);
+ if (c->blocked) return 0;
+ if (crn_error_count()) return 0;
+ }
+
+ /* unreachable label detection (labels with no live edge) */
+ for (size_t i = 0; i < fa->nbody; i++) {
+ Node* n = &fa->body[i];
+ if (n->kind != A_MARK) continue;
+ int li = find_label(c, n->name);
+ if (li >= 0 && !c->labels[li].state)
+ err(n->loc, "label '%s' can never be reached", n->name);
+ }
+ if (crn_error_count()) return 0;
+
+ /* implicit return at the end of the body */
+ if (!c->st.dead) {
+ if (c->st.nstk != 0) {
+ err(fa->loc, "the value stack must be empty when transformation '%s' ends (holds %d)",
+ fa->name, c->st.nstk);
+ return 0;
+ }
+ if (!c->result_seen) {
+ c->result_first = c->st.wv;
+ c->result_seen = 1;
+ } else if (!type_eq(c->result_first, c->st.wv)) {
+ char b1[128], b2[128];
+ err(fa->loc, "return type mismatch in '%s': %s vs %s", fa->name,
+ type_name(c->result_first, b1, sizeof b1), type_name(c->st.wv, b2, sizeof b2));
+ return 0;
+ }
+ emit(c, I_RET, fa->loc);
+ }
+
+ if (fn_declared_conflict(c)) return 0;
+
+ c->fn->nlabels = c->nlabels;
+ free(c->fn->label_names);
+ c->fn->label_names = xmalloc(sizeof(char*) * (size_t)(c->nlabels ? c->nlabels : 1));
+ for (int i = 0; i < c->nlabels; i++) c->fn->label_names[i] = c->labels[i].name;
+
+ return 1;
+}
+
+static int fn_declared_conflict(Chk* c) {
+ FnIR* fn = c->fn;
+ if (!fn->declared_result || !fn->result) return 0;
+ if (!c->result_seen) {
+ /* no returns at all: declared result must be nothing-compatible */
+ if (fn->result->kind != TY_NOTHING) {
+ char b[128];
+ err(fn->loc, "'%s' declares result %s but never returns a value",
+ fn->name, type_name(fn->result, b, sizeof b));
+ return 1;
+ }
+ return 0;
+ }
+ if (!type_eq(fn->result, c->result_first)) {
+ char b1[128], b2[128];
+ err(fn->loc, "'%s' declares result %s but returns %s", fn->name,
+ type_name(fn->result, b1, sizeof b1), type_name(c->result_first, b2, sizeof b2));
+ return 1;
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* program level */
+/* ------------------------------------------------------------------ */
+
+ProgramIR* check_program(ProgramAst* ast) {
+ ProgramIR* p = xmalloc(sizeof(ProgramIR));
+ memset(p, 0, sizeof *p);
+
+ /* world cells */
+ for (size_t i = 0; i < ast->nglobs; i++) {
+ GlobAst* g = &ast->globs[i];
+ int dup = 0;
+ for (size_t j = 0; j < (size_t)p->nglobs; j++)
+ if (strcmp(p->globs[j].name, g->name) == 0) {
+ crn_error(g->loc, "duplicate world cell '%s'", g->name);
+ dup = 1;
+ }
+ for (size_t j = 0; j < ast->nfns; j++)
+ if (strcmp(ast->fns[j].name, g->name) == 0) {
+ crn_error(g->loc, "world cell '%s' collides with a transformation name", g->name);
+ dup = 1;
+ }
+ if (dup) continue;
+ p->globs = xrealloc(p->globs, ((size_t)p->nglobs + 1) * sizeof(GlobIR));
+ GlobIR* gi = &p->globs[p->nglobs++];
+ memset(gi, 0, sizeof *gi);
+ gi->name = g->name;
+ if (g->kind == G_STRING) {
+ gi->type = ty_array(ty_uint(8), g->strlen + 1);
+ gi->str_init = xmalloc(g->strlen + 1);
+ memcpy(gi->str_init, g->str, g->strlen);
+ gi->str_init[g->strlen] = 0;
+ gi->str_initlen = g->strlen + 1;
+ } else {
+ gi->type = ast_to_type(g->t, g->loc);
+ if (gi->type->kind == TY_ARR && gi->type->count == 0)
+ crn_error(g->loc, "world arrays need a size");
+ }
+ }
+
+ /* transformation shells */
+ for (size_t i = 0; i < ast->nfns; i++) {
+ FnAst* fa = &ast->fns[i];
+ int dup = 0;
+ for (size_t j = 0; j < (size_t)p->nfns; j++)
+ if (strcmp(p->fns[j].name, fa->name) == 0) {
+ crn_error(fa->loc, "duplicate transformation '%s'", fa->name);
+ dup = 1;
+ }
+ for (size_t j = 0; j < (size_t)p->nglobs; j++)
+ if (strcmp(p->globs[j].name, fa->name) == 0) {
+ crn_error(fa->loc, "transformation '%s' collides with a world cell name", fa->name);
+ dup = 1;
+ }
+ if (dup) continue;
+ p->fns = xrealloc(p->fns, ((size_t)p->nfns + 1) * sizeof(FnIR));
+ FnIR* fi = &p->fns[p->nfns++];
+ memset(fi, 0, sizeof *fi);
+ fi->name = fa->name;
+ fi->loc = fa->loc;
+ fi->locals = xmalloc(sizeof(LocalIR) * (MAX_LOCALS + 1));
+ memset(fi->locals, 0, sizeof(LocalIR) * (MAX_LOCALS + 1));
+ fi->nparams = (int)fa->nparams;
+ /* fill parameter cells now so callers can check arguments before
+ the callee body has been checked */
+ for (size_t pi = 0; pi < fa->nparams && (int)pi < MAX_LOCALS; pi++) {
+ Type* pt = ast_to_type(fa->params[pi].t, fa->params[pi].loc);
+ if (pt->kind == TY_ARR) pt = ty_nothing();
+ fi->locals[pi].name = fa->params[pi].name;
+ fi->locals[pi].type = pt;
+ fi->locals[pi].is_param = 1;
+ }
+ fi->nlocals = (int)fa->nparams;
+ fi->result = NULL;
+ fi->declared_result = fa->result != NULL;
+ if (fa->result) {
+ Type* t = ast_to_type(fa->result, fa->loc);
+ if (t->kind == TY_ARR) {
+ crn_error(fa->loc, "arrays cannot be results; return an address instead");
+ t = ty_nothing();
+ }
+ fi->result = t;
+ }
+ }
+ if (crn_error_count()) return NULL;
+
+ if (prog_find_fn(p, "main") < 0) {
+ crn_error((Loc){1, 1}, "no 'main' transformation defined");
+ return NULL;
+ }
+ {
+ int mi = prog_find_fn(p, "main");
+ if (p->fns[mi].nparams != 0) {
+ crn_error(p->fns[mi].loc, "'main' must take no parameters");
+ return NULL;
+ }
+ }
+
+ /* body checking with iterative result inference */
+ Chk chk;
+ memset(&chk, 0, sizeof chk);
+ chk.prog = p;
+
+ for (int pass = 0; pass < p->nfns + 2; pass++) {
+ int remaining = 0;
+ for (int i = 0; i < p->nfns; i++) {
+ FnIR* fi = &p->fns[i];
+ if (fi->checked_done) continue;
+ FnAst* fa = NULL;
+ for (size_t j = 0; j < ast->nfns; j++)
+ if (strcmp(ast->fns[j].name, fi->name) == 0) { fa = &ast->fns[j]; break; }
+ if (!fa) continue;
+ chk.fn = fi;
+ chk.fnast = fa;
+ if (check_fn_body(&chk, fa)) {
+ if (!fi->result)
+ fi->result = chk.result_seen ? chk.result_first : ty_nothing();
+ fi->checked_done = 1;
+ } else {
+ if (crn_error_count()) return NULL;
+ remaining++;
+ }
+ }
+ if (remaining == 0) break;
+ if (pass == p->nfns + 1) {
+ for (int i = 0; i < p->nfns; i++) {
+ if (!p->fns[i].checked_done)
+ crn_error(p->fns[i].loc,
+ "cannot infer the result of recursive transformation '%s'; "
+ "declare it with 'result <type>'",
+ p->fns[i].name);
+ }
+ return NULL;
+ }
+ }
+ return p;
+}
+
+int prog_find_fn(ProgramIR* p, const char* name) {
+ for (int i = 0; i < p->nfns; i++)
+ if (strcmp(p->fns[i].name, name) == 0) return i;
+ return -1;
+}
+
+int prog_find_glob(ProgramIR* p, const char* name) {
+ for (int i = 0; i < p->nglobs; i++)
+ if (strcmp(p->globs[i].name, name) == 0) return i;
+ return -1;
+}
diff --git a/src/check.h b/src/check.h
new file mode 100644
index 0000000..6019ce2
--- /dev/null
+++ b/src/check.h
@@ -0,0 +1,11 @@
+#ifndef CR_CHECK_H
+#define CR_CHECK_H
+
+#include "ast.h"
+#include "ir.h"
+
+/* Semantic analysis: AST -> fully checked IR.
+ Reports errors via crn_error. Returns NULL if any error occurred. */
+ProgramIR* check_program(ProgramAst* ast);
+
+#endif
diff --git a/src/diag.c b/src/diag.c
new file mode 100644
index 0000000..7d37186
--- /dev/null
+++ b/src/diag.c
@@ -0,0 +1,23 @@
+#include "diag.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+
+static const char* g_file = "input";
+static int g_errors = 0;
+
+void crn_diag_set_file(const char* file) { g_file = file; }
+
+void crn_error(Loc loc, const char* fmt, ...) {
+ va_list ap;
+ fprintf(stderr, "%s:%d:%d: error: ", g_file, loc.line, loc.col);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ g_errors++;
+}
+
+int crn_error_count(void) { return g_errors; }
+
+void crn_reset_errors(void) { g_errors = 0; }
diff --git a/src/diag.h b/src/diag.h
new file mode 100644
index 0000000..df0d33d
--- /dev/null
+++ b/src/diag.h
@@ -0,0 +1,18 @@
+#ifndef CR_DIAG_H
+#define CR_DIAG_H
+
+typedef struct Loc {
+ int line; /* 1-based */
+ int col; /* 1-based */
+} Loc;
+
+/* Set the file name shown in diagnostics. */
+void crn_diag_set_file(const char* file);
+/* Record a compile error. Does not abort. */
+void crn_error(Loc loc, const char* fmt, ...);
+/* Number of errors recorded so far. */
+int crn_error_count(void);
+/* Reset error count (used by tests). */
+void crn_reset_errors(void);
+
+#endif
diff --git a/src/ir.h b/src/ir.h
new file mode 100644
index 0000000..6893e81
--- /dev/null
+++ b/src/ir.h
@@ -0,0 +1,143 @@
+#ifndef CR_IR_H
+#define CR_IR_H
+
+#include "diag.h"
+#include "types.h"
+#include "util.h"
+
+/*
+ * Cryn IR: a typed, flat instruction stream per transformation.
+ * Produced by semantic analysis (check.c) from the AST.
+ * Each instruction has already been fully resolved and type-checked.
+ */
+
+typedef enum {
+ I_LBL, /* a = label index */
+ I_JMP, /* a = label index */
+ I_JMPCOND, /* a = label index, b = CondKind */
+ I_MOVE_LOCAL, /* a = local index; focus becomes local cell (t) */
+ I_MOVE_GLOBAL, /* a = global index */
+ I_MOVE_LIT, /* imm = index (nonneg), t = element type, b = static bound (0 = unsized) */
+ I_MOVE_INDEX, /* t = element type, b = static bound (0 = unsized) */
+ I_MOVE_AT, /* t = pointee type of address in working value */
+ I_BACK,
+ I_HOME,
+ I_ADDR_HERE, /* t = resulting address type */
+ I_ADDR_LOCAL, /* a = local index, t = address type */
+ I_ADDR_GLOBAL, /* a = global index, t = address type */
+ I_ADDR_FN, /* a = fn index */
+ I_MAKE_LOCAL, /* a = local index (re-zero the cell) */
+ I_MAKE_ANON, /* t = cell type; working value = address of new heap cell */
+ I_MAKE_DYN, /* b = 0: a = local index holding size; b = 1: imm = size;
+ b = 2: a = global index holding size;
+ t = element type; working value = address of block */
+ I_MAKE_STRLOCAL,/* a = local index (array with initial bytes) */
+ I_MAKE_STRANON, /* a = string index; working value = address of static block */
+ I_BIND_LOCAL, /* a = local index; store working value into local cell */
+ I_FREE,
+ I_LOAD, /* t = cell type; working value = value at focus */
+ I_STORE, /* t = cell type, b = store coercion (see SC_*) */
+ I_SET, /* imm, t = working value type */
+ I_ADD, I_SUB, I_MUL, I_DIV, I_MOD, /* t = operand type; b = 0 pop, 1 imm; imm (floats/intlit) */
+ I_NEG, /* t = type */
+ I_PUSH, /* t = type of value pushed */
+ I_POP, /* t = type of value popped into working value */
+ I_SWAP,
+ I_CMP, /* t = comparison type; b = 0 pop, 1 imm */
+ I_CONV, /* t = target type (from = current static type) */
+ I_CALL, /* a = fn index, b = arg coercion mask */
+ I_CALLIND, /* indirect call through working value */
+ I_RET,
+ I_STOP, /* t = working value type (numeric -> exit code) */
+ I_INPUT,
+ I_OUTPUT, /* t = working value type */
+ I_OUTTEXT,
+ I_OPEN, /* b = mode 0 read, 1 write, 2 both */
+ I_CLOSE,
+ I_SYSTEM,
+ I_SPAWN, /* a = fn index */
+ I_WAIT
+} IROp;
+
+typedef enum {
+ CMP_LESS = 0, CMP_EQUAL, CMP_GREATER, CMP_DIFFERENT,
+ CMP_LESS_OR_EQUAL, CMP_GREATER_OR_EQUAL
+} CondKind;
+
+/* store coercions */
+enum {
+ SC_EXACT = 0, /* working value type == cell type */
+ SC_INTLIT, /* integer literal -> int/uint/char cell (fits, checked) */
+ SC_INTLIT_BOOL,/* integer literal 0/1 -> bool */
+ SC_INTLIT_FLOAT,/* integer literal -> float cell */
+ SC_F32 /* float64 -> float32 cell */
+};
+
+/* immediate literal */
+typedef struct ImmV {
+ enum { IV_NONE, IV_INT, IV_UINT, IV_FLOAT } kind;
+ uint64_t u;
+ double d;
+} ImmV;
+
+typedef struct Insn {
+ IROp op;
+ int a, b;
+ ImmV imm;
+ Type* t;
+ Loc loc;
+} Insn;
+
+typedef struct LocalIR {
+ char* name;
+ Type* type;
+ int is_param;
+ char* str_init; /* for make string locals: initial bytes (len+1 incl NUL) */
+ size_t str_initlen;
+} LocalIR;
+
+typedef struct FnIR {
+ char* name;
+ Loc loc;
+ LocalIR* locals;
+ int nlocals;
+ int nparams;
+ Type* result; /* NULL until resolved; nothing = void */
+ Insn* ins;
+ int nins, cap;
+ /* label names for IR dumps */
+ char** label_names;
+ int nlabels;
+ int declared_result; /* 1 when `result` was written explicitly */
+ int checked_done; /* body fully checked and emitted */
+} FnIR;
+
+typedef struct GlobIR {
+ char* name;
+ Type* type;
+ /* string globals: initial bytes */
+ char* str_init;
+ size_t str_initlen;
+} GlobIR;
+
+typedef struct StrIR {
+ char* bytes; /* includes terminating NUL */
+ size_t len; /* length excluding NUL */
+} StrIR;
+
+typedef struct ProgramIR {
+ FnIR* fns;
+ int nfns;
+ GlobIR* globs;
+ int nglobs;
+ StrIR* strs;
+ int nstrs;
+} ProgramIR;
+
+int prog_find_fn(ProgramIR* p, const char* name);
+int prog_find_glob(ProgramIR* p, const char* name);
+
+/* IR dump for `cryn emit-ir` */
+void ir_dump(ProgramIR* p, Buf* out);
+
+#endif
diff --git a/src/lex.c b/src/lex.c
new file mode 100644
index 0000000..eca7544
--- /dev/null
+++ b/src/lex.c
@@ -0,0 +1,362 @@
+#include "lex.h"
+#include "util.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct Lexer {
+ const char* src;
+ size_t pos, len;
+ int line, col;
+ List toks; /* Token* */
+} Lexer;
+
+static Token* tok_new(Lexer* lx, TokKind kind) {
+ Token* t = xmalloc(sizeof(Token));
+ t->kind = kind;
+ t->loc.line = lx->line;
+ t->loc.col = lx->col;
+ t->text = NULL;
+ t->ival = 0;
+ t->neg = 0;
+ t->fval = 0;
+ t->str = NULL;
+ t->strlen = 0;
+ list_add(&lx->toks, t);
+ return t;
+}
+
+static void adv(Lexer* lx, size_t n) {
+ for (size_t i = 0; i < n && lx->pos < lx->len; i++) {
+ char c = lx->src[lx->pos++];
+ if (c == '\n') {
+ lx->line++;
+ lx->col = 1;
+ } else {
+ lx->col++;
+ }
+ }
+}
+
+static int at_end(Lexer* lx) { return lx->pos >= lx->len; }
+static char cur(Lexer* lx) { return at_end(lx) ? 0 : lx->src[lx->pos]; }
+static char peek(Lexer* lx, size_t n) {
+ return lx->pos + n < lx->len ? lx->src[lx->pos + n] : 0;
+}
+
+static int is_word_start(char c) { return isalpha((unsigned char)c) || c == '_'; }
+static int is_word_char(char c) { return isalnum((unsigned char)c) || c == '_'; }
+
+/* keyword table */
+typedef struct { const char* word; TokKind kind; } Kw;
+static const Kw k_keywords[] = {
+ {"move", T_MOVE}, {"back", T_BACK}, {"home", T_HOME}, {"address", T_ADDRESS},
+ {"make", T_MAKE}, {"free", T_FREE}, {"read", T_READ}, {"write", T_WRITE},
+ {"set", T_SET}, {"add", T_ADD}, {"subtract", T_SUBTRACT}, {"multiply", T_MULTIPLY},
+ {"divide", T_DIVIDE}, {"remainder", T_REMAINDER}, {"negate", T_NEGATE},
+ {"push", T_PUSH}, {"pop", T_POP}, {"swap", T_SWAP}, {"compare", T_COMPARE},
+ {"convert", T_CONVERT}, {"jump", T_JUMP}, {"call", T_CALL}, {"return", T_RETURN},
+ {"stop", T_STOP}, {"input", T_INPUT}, {"output", T_OUTPUT}, {"open", T_OPEN},
+ {"close", T_CLOSE}, {"system", T_SYSTEM}, {"spawn", T_SPAWN}, {"wait", T_WAIT},
+ {"define", T_DEFINE}, {"end", T_END}, {"mark", T_MARK},
+ {"nothing", T_NOTHING}, {"bool", T_BOOL}, {"char", T_CHAR_T},
+ {"int8", T_INT8}, {"int16", T_INT16}, {"int32", T_INT32}, {"int64", T_INT64},
+ {"uint8", T_UINT8}, {"uint16", T_UINT16}, {"uint32", T_UINT32}, {"uint64", T_UINT64},
+ {"float32", T_FLOAT32}, {"float64", T_FLOAT64},
+ {"array", T_ARRAY}, {"string", T_STRING_T},
+ {"transform", T_TRANSFORM},
+ {"true", T_TRUE}, {"false", T_FALSE},
+ {"of", T_OF}, {"when", T_WHEN}, {"result", T_RESULT},
+ {"index", T_INDEX}, {"at", T_AT}, {"text", T_TEXT}, {"both", T_BOTH},
+ {"less", T_LESS}, {"equal", T_EQUAL}, {"greater", T_GREATER}, {"different", T_DIFFERENT},
+ {"less-or-equal", T_LESS_OR_EQUAL}, {"greater-or-equal", T_GREATER_OR_EQUAL},
+ {NULL, T_EOF}
+};
+
+static TokKind lookup_word(const char* w, size_t n) {
+ for (const Kw* k = k_keywords; k->word; k++) {
+ if (strlen(k->word) == n && memcmp(k->word, w, n) == 0) return k->kind;
+ }
+ return T_EOF;
+}
+
+static void lex_comment(Lexer* lx) {
+ /* caller verified '[#' at pos; consume until '#]' or EOF */
+ adv(lx, 2);
+ for (;;) {
+ if (at_end(lx)) {
+ crn_error((Loc){lx->line, lx->col}, "unterminated comment (missing '#]')");
+ return;
+ }
+ if (cur(lx) == '#' && peek(lx, 1) == ']') {
+ adv(lx, 2);
+ return;
+ }
+ adv(lx, 1);
+ }
+}
+
+static int lex_escape(Lexer* lx, unsigned* out) {
+ /* cur(lx) == '\\' */
+ adv(lx, 1);
+ if (at_end(lx)) return -1;
+ char c = cur(lx);
+ unsigned v;
+ switch (c) {
+ case 'n': v = '\n'; break;
+ case 't': v = '\t'; break;
+ case 'r': v = '\r'; break;
+ case '0': v = 0; break;
+ case '\\': v = '\\'; break;
+ case '\'': v = '\''; break;
+ case '"': v = '"'; break;
+ default:
+ crn_error((Loc){lx->line, lx->col}, "unknown escape sequence '\\%c'", c);
+ v = (unsigned char)c;
+ break;
+ }
+ adv(lx, 1);
+ *out = v;
+ return 0;
+}
+
+static void lex_word(Lexer* lx) {
+ size_t start = lx->pos;
+ while (!at_end(lx) && is_word_char(cur(lx))) adv(lx, 1);
+ size_t n = lx->pos - start;
+
+ /* hyphenated keywords: less-or-equal / greater-or-equal */
+ if ((n == 4 && memcmp(lx->src + start, "less", 4) == 0) ||
+ (n == 7 && memcmp(lx->src + start, "greater", 7) == 0)) {
+ const char* tail = "-or-equal";
+ size_t tl = strlen(tail);
+ if (lx->pos + tl <= lx->len && memcmp(lx->src + lx->pos, tail, tl) == 0) {
+ adv(lx, tl);
+ tok_new(lx, n == 4 ? T_LESS_OR_EQUAL : T_GREATER_OR_EQUAL);
+ return;
+ }
+ }
+
+ TokKind k = lookup_word(lx->src + start, n);
+ if (k != T_EOF) {
+ tok_new(lx, k);
+ return;
+ }
+ Token* t = tok_new(lx, T_IDENT);
+ t->text = xmalloc(n + 1);
+ memcpy(t->text, lx->src + start, n);
+ t->text[n] = 0;
+}
+
+static void lex_number(Lexer* lx, int neg) {
+ Loc loc = {lx->line, lx->col};
+
+ /* hex */
+ if (cur(lx) == '0' && (peek(lx, 1) == 'x' || peek(lx, 1) == 'X')) {
+ adv(lx, 2);
+ uint64_t v = 0;
+ int any = 0;
+ for (;;) {
+ char c = cur(lx);
+ int d;
+ if (c >= '0' && c <= '9') d = c - '0';
+ else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
+ else break;
+ if (v > (0xFFFFFFFFFFFFFFFFULL - (uint64_t)d) / 16) {
+ crn_error(loc, "integer literal out of range");
+ v = 0;
+ }
+ v = v * 16 + (uint64_t)d;
+ any = 1;
+ adv(lx, 1);
+ }
+ if (!any) crn_error(loc, "malformed hex literal");
+ if (is_word_start(cur(lx)))
+ crn_error(loc, "junk after integer literal");
+ Token* t = tok_new(lx, T_INT);
+ t->ival = v;
+ t->neg = neg;
+ return;
+ }
+
+ Buf digits = {0};
+ while (isdigit((unsigned char)cur(lx))) { buf_putc(&digits, cur(lx)); adv(lx, 1); }
+ if (digits.len == 0) { crn_error(loc, "malformed number"); buf_free(&digits); return; }
+
+ int is_float = 0;
+ Buf full = {0};
+ buf_put(&full, digits.data, digits.len);
+ if (cur(lx) == '.' && isdigit((unsigned char)peek(lx, 1))) {
+ is_float = 1;
+ buf_putc(&full, '.');
+ adv(lx, 1);
+ while (isdigit((unsigned char)cur(lx))) { buf_putc(&full, cur(lx)); adv(lx, 1); }
+ }
+ if (cur(lx) == 'e' || cur(lx) == 'E') {
+ size_t save = lx->pos;
+ Buf exp = {0};
+ buf_putc(&exp, 'e');
+ adv(lx, 1);
+ if (cur(lx) == '+' || cur(lx) == '-') { buf_putc(&exp, cur(lx)); adv(lx, 1); }
+ if (!isdigit((unsigned char)cur(lx))) {
+ lx->pos = save;
+ /* note: column tracking for rewind is imperfect; acceptable for error path */
+ } else {
+ while (isdigit((unsigned char)cur(lx))) { buf_putc(&exp, cur(lx)); adv(lx, 1); }
+ buf_put(&full, exp.data, exp.len);
+ is_float = 1;
+ }
+ buf_free(&exp);
+ }
+ buf_putc(&full, 0);
+
+ if (is_word_start(cur(lx)))
+ crn_error(loc, "junk after numeric literal");
+
+ if (is_float) {
+ Token* t = tok_new(lx, T_FLOAT);
+ t->fval = strtod(full.data, NULL);
+ if (neg) t->fval = -t->fval;
+ } else {
+ uint64_t v = strtoull(full.data, NULL, 10);
+ if (v == 0xFFFFFFFFFFFFFFFFULL && strchr(full.data, '9')) {
+ /* strtoull clamps; crude overflow detect: compare length > 20 */
+ }
+ if (full.len - 1 > 20 || (full.len - 1 == 20 && strcmp(full.data, "18446744073709551615") > 0)) {
+ crn_error(loc, "integer literal out of range");
+ v = 0;
+ }
+ Token* t = tok_new(lx, T_INT);
+ t->ival = v;
+ t->neg = neg;
+ }
+ buf_free(&digits);
+ buf_free(&full);
+}
+
+static void lex_string(Lexer* lx) {
+ Loc loc = {lx->line, lx->col};
+ adv(lx, 1); /* opening quote */
+ Buf b = {0};
+ for (;;) {
+ if (at_end(lx) || cur(lx) == '\n') {
+ crn_error(loc, "unterminated string literal");
+ break;
+ }
+ if (cur(lx) == '"') { adv(lx, 1); break; }
+ if (cur(lx) == '\\') {
+ unsigned v;
+ if (lex_escape(lx, &v) == 0) buf_putc(&b, (char)v);
+ else { crn_error(loc, "unterminated string literal"); break; }
+ } else {
+ buf_putc(&b, cur(lx));
+ adv(lx, 1);
+ }
+ }
+ Token* t = tok_new(lx, T_STRING);
+ t->str = b.data ? b.data : xstrdup("");
+ t->strlen = b.len;
+}
+
+static void lex_char(Lexer* lx) {
+ Loc loc = {lx->line, lx->col};
+ adv(lx, 1); /* opening quote */
+ unsigned v = 0;
+ if (at_end(lx)) {
+ crn_error(loc, "unterminated character literal");
+ } else if (cur(lx) == '\'') {
+ crn_error(loc, "empty character literal");
+ adv(lx, 1);
+ } else if (cur(lx) == '\\') {
+ if (lex_escape(lx, &v) != 0) crn_error(loc, "unterminated character literal");
+ } else {
+ v = (unsigned char)cur(lx);
+ adv(lx, 1);
+ }
+ if (cur(lx) == '\'') {
+ adv(lx, 1);
+ } else if (!at_end(lx) && cur(lx) != '\n') {
+ crn_error(loc, "character literal must contain exactly one character");
+ while (!at_end(lx) && cur(lx) != '\'' && cur(lx) != '\n') adv(lx, 1);
+ if (cur(lx) == '\'') adv(lx, 1);
+ } else {
+ crn_error(loc, "unterminated character literal");
+ }
+ Token* t = tok_new(lx, T_CHAR);
+ t->ival = v;
+}
+
+Token* lex_source(const char* src, size_t* out_count) {
+ Lexer lx;
+ lx.src = src;
+ lx.pos = 0;
+ lx.len = strlen(src);
+ lx.line = 1;
+ lx.col = 1;
+ lx.toks.items = NULL;
+ lx.toks.len = lx.toks.cap = 0;
+
+ for (;;) {
+ /* skip whitespace */
+ while (!at_end(&lx) && (cur(&lx) == ' ' || cur(&lx) == '\t' ||
+ cur(&lx) == '\n' || cur(&lx) == '\r'))
+ adv(&lx, 1);
+ if (at_end(&lx)) break;
+
+ if (cur(&lx) == '[' && peek(&lx, 1) == '#') {
+ lex_comment(&lx);
+ continue;
+ }
+
+ char c = cur(&lx);
+ if (is_word_start(c)) { lex_word(&lx); continue; }
+ if (isdigit((unsigned char)c)) { lex_number(&lx, 0); continue; }
+ if (c == '-' && isdigit((unsigned char)peek(&lx, 1))) {
+ adv(&lx, 1);
+ lex_number(&lx, 1);
+ continue;
+ }
+ if (c == '"') { lex_string(&lx); continue; }
+ if (c == '\'') { lex_char(&lx); continue; }
+
+ crn_error((Loc){lx.line, lx.col}, "unexpected character '%c' (0x%02X)",
+ isprint((unsigned char)c) ? c : '?', (unsigned char)c);
+ adv(&lx, 1);
+ }
+
+ tok_new(&lx, T_EOF);
+ Token* arr = xmalloc(lx.toks.len * sizeof(Token));
+ for (size_t i = 0; i < lx.toks.len; i++) arr[i] = *(Token*)lx.toks.items[i];
+ if (out_count) *out_count = lx.toks.len;
+ return arr;
+}
+
+const char* tok_kind_name(TokKind k) {
+ switch (k) {
+ case T_EOF: return "end of input";
+ case T_IDENT: return "identifier";
+ case T_INT: return "integer literal";
+ case T_FLOAT: return "float literal";
+ case T_CHAR: return "character literal";
+ case T_STRING: return "string literal";
+ default: break;
+ }
+ for (const Kw* kw = k_keywords; kw->word; kw++)
+ if (kw->kind == k) return kw->word;
+ return "token";
+}
+
+int tok_is_type_start(TokKind k) {
+ switch (k) {
+ case T_NOTHING: case T_BOOL: case T_CHAR_T:
+ case T_INT8: case T_INT16: case T_INT32: case T_INT64:
+ case T_UINT8: case T_UINT16: case T_UINT32: case T_UINT64:
+ case T_FLOAT32: case T_FLOAT64:
+ case T_ADDRESS: case T_ADDRESS_T: case T_ARRAY: case T_TRANSFORM:
+ return 1;
+ default:
+ return 0;
+ }
+}
diff --git a/src/lex.h b/src/lex.h
new file mode 100644
index 0000000..09ab725
--- /dev/null
+++ b/src/lex.h
@@ -0,0 +1,62 @@
+#ifndef CR_LEX_H
+#define CR_LEX_H
+
+#include "diag.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef enum {
+ T_EOF = 0,
+ T_IDENT,
+
+ /* literals */
+ T_INT, /* u64 magnitude + neg flag */
+ T_FLOAT, /* double */
+ T_CHAR, /* unsigned byte/codepoint value in ival */
+ T_STRING, /* text/len in a token-local buffer */
+
+ /* commands */
+ T_MOVE, T_BACK, T_HOME, T_ADDRESS, T_MAKE, T_FREE, T_READ, T_WRITE,
+ T_SET, T_ADD, T_SUBTRACT, T_MULTIPLY, T_DIVIDE, T_REMAINDER, T_NEGATE,
+ T_PUSH, T_POP, T_SWAP, T_COMPARE, T_CONVERT,
+ T_JUMP, T_CALL, T_RETURN, T_STOP,
+ T_INPUT, T_OUTPUT, T_OPEN, T_CLOSE, T_SYSTEM,
+ T_SPAWN, T_WAIT,
+ T_DEFINE, T_END, T_MARK,
+
+ /* type words */
+ T_NOTHING, T_BOOL, T_CHAR_T, T_INT8, T_INT16, T_INT32, T_INT64,
+ T_UINT8, T_UINT16, T_UINT32, T_UINT64,
+ T_FLOAT32, T_FLOAT64, T_ADDRESS_T, T_ARRAY, T_STRING_T, T_TRANSFORM,
+
+ /* value words */
+ T_TRUE, T_FALSE,
+
+ /* structural / argument words */
+ T_OF, T_WHEN, T_RESULT, T_INDEX, T_AT, T_TEXT, T_BOTH,
+ T_LESS, T_EQUAL, T_GREATER, T_DIFFERENT, T_LESS_OR_EQUAL, T_GREATER_OR_EQUAL
+} TokKind;
+
+typedef struct Token {
+ TokKind kind;
+ Loc loc;
+ char* text; /* identifier text (owned) */
+ uint64_t ival; /* integer magnitude or char code */
+ int neg; /* integer literal was negative */
+ double fval; /* float value */
+ char* str; /* string literal bytes (owned, not NUL-terminated) */
+ size_t strlen; /* string literal length */
+} Token;
+
+/* Lex the whole source into a NUL(kind=T_EOF)-terminated token array.
+ Returns a malloc'd array. On lexical errors, reports via crn_error
+ and returns the tokens lexed so far (always terminated). */
+Token* lex_source(const char* src, size_t* out_count);
+
+const char* tok_kind_name(TokKind k);
+
+/* Is this token a type-starting word? */
+int tok_is_type_start(TokKind k);
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..e9560f2
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,186 @@
+/* cryn - The Cryn Programming Language compiler (bootstrap) */
+#include "be_c.h"
+#include "check.h"
+#include "diag.h"
+#include "lex.h"
+#include "opt.h"
+#include "parse.h"
+#include "util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define CRYN_VERSION "0.1.0"
+
+static void usage(void) {
+ printf(
+ "cryn - The Cryn Programming Language compiler (v%s)\n"
+ "\n"
+ "usage:\n"
+ " cryn <file.cryn> compile to an executable\n"
+ " cryn build <file.cryn> [-o out]\n"
+ " cryn run <file.cryn> compile and execute\n"
+ " cryn check <file.cryn> type check only\n"
+ " cryn emit-c <file.cryn> [-o out.c]\n"
+ " write generated C\n"
+ " cryn emit-ir <file.cryn> print the Cryn IR\n"
+ " cryn help\n"
+ " cryn version\n"
+ "\n"
+ "environment:\n"
+ " CC host C compiler used for code generation (default: cc)\n",
+ CRYN_VERSION);
+}
+
+static char* find_host_cc(void) {
+ const char* cc = getenv("CC");
+ if (cc && cc[0]) return xstrdup(cc);
+ return xstrdup("cc");
+}
+
+static int run_host_cc(const char* cc, const char* cfile, const char* out) {
+ char* cmd;
+ size_t n = snprintf(NULL, 0, "%s -O2 -std=c11 -pthread -o %s %s", cc, out, cfile);
+ cmd = xmalloc(n + 1);
+ snprintf(cmd, n + 1, "%s -O2 -std=c11 -pthread -o %s %s", cc, out, cfile);
+ int rc = system(cmd);
+ free(cmd);
+ if (rc == -1) return -1;
+ if (WIFEXITED(rc)) return WEXITSTATUS(rc);
+ return -1;
+}
+
+static const char* tmp_dir(void) {
+ const char* t = getenv("TMPDIR");
+ if (t && t[0]) return t;
+ return "/tmp";
+}
+
+typedef enum { CMD_COMPILE, CMD_RUN, CMD_CHECK, CMD_EMITC, CMD_EMITIR } CmdKind;
+
+static int compile_file(const char* path, CmdKind kind, const char* out) {
+ char* src = NULL;
+ size_t len = 0;
+ if (read_file(path, &src, &len) != 0) {
+ fprintf(stderr, "cryn: cannot read '%s'\n", path);
+ return 1;
+ }
+ crn_diag_set_file(path);
+
+ Token* toks = lex_source(src, NULL);
+ ProgramAst* ast = parse_program(toks);
+ if (crn_error_count()) return 1;
+
+ ProgramIR* ir = check_program(ast);
+ if (!ir) return 1;
+
+ opt_program(ir);
+
+ switch (kind) {
+ case CMD_CHECK:
+ return 0;
+ case CMD_EMITIR: {
+ Buf b = {0};
+ ir_dump(ir, &b);
+ fwrite(b.data, 1, b.len, stdout);
+ buf_free(&b);
+ return 0;
+ }
+ case CMD_EMITC: {
+ Buf b = {0};
+ be_c_emit(ir, &b);
+ const char* dst = out ? out : "out.c";
+ if (write_file(dst, b.data, b.len) != 0) {
+ fprintf(stderr, "cryn: cannot write '%s'\n", dst);
+ return 1;
+ }
+ return 0;
+ }
+ case CMD_COMPILE:
+ case CMD_RUN: {
+ char cfile[4096], exefile[4096];
+ if (kind == CMD_RUN) {
+ long id = (long)getpid() * 1000 + (long)(time(NULL) % 1000);
+ snprintf(cfile, sizeof cfile, "%s/cryn%ld.c", tmp_dir(), id);
+ snprintf(exefile, sizeof exefile, "%s/cryn%ld", tmp_dir(), id);
+ } else {
+ char base[512];
+ snprintf(base, sizeof base - 8, "%s", out ? out : path);
+ if (!out) path_strip_ext(base);
+ snprintf(cfile, sizeof cfile, "%s.c", base);
+ snprintf(exefile, sizeof exefile, "%s", base);
+ }
+ Buf b = {0};
+ be_c_emit(ir, &b);
+ if (write_file(cfile, b.data, b.len) != 0) {
+ fprintf(stderr, "cryn: cannot write '%s'\n", cfile);
+ return 1;
+ }
+ char* cc = find_host_cc();
+ int rc = run_host_cc(cc, cfile, exefile);
+ free(cc);
+ if (rc != 0) {
+ fprintf(stderr, "cryn: host C compiler failed (exit %d)\n", rc);
+ if (kind == CMD_RUN) remove(cfile);
+ return 1;
+ }
+ if (kind == CMD_COMPILE) {
+ remove(cfile);
+ return 0;
+ }
+ fflush(NULL);
+ int prc = system(exefile);
+ remove(cfile);
+ remove(exefile);
+ if (prc == -1) return 1;
+ if (WIFEXITED(prc)) return WEXITSTATUS(prc);
+ return 1;
+ }
+ }
+ return 1;
+}
+
+int main(int argc, char** argv) {
+ if (argc < 2) {
+ usage();
+ return 1;
+ }
+ const char* cmd = argv[1];
+
+ if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
+ usage();
+ return 0;
+ }
+ if (strcmp(cmd, "version") == 0 || strcmp(cmd, "--version") == 0) {
+ printf("cryn %s\n", CRYN_VERSION);
+ return 0;
+ }
+
+ CmdKind kind = CMD_COMPILE;
+ const char* out = NULL;
+ const char* file = NULL;
+ int has_sub = 1;
+ if (strcmp(cmd, "build") == 0) kind = CMD_COMPILE;
+ else if (strcmp(cmd, "run") == 0) kind = CMD_RUN;
+ else if (strcmp(cmd, "check") == 0) kind = CMD_CHECK;
+ else if (strcmp(cmd, "emit-c") == 0) kind = CMD_EMITC;
+ else if (strcmp(cmd, "emit-ir") == 0) kind = CMD_EMITIR;
+ else { file = cmd; has_sub = 0; }
+
+ if (has_sub) {
+ if (argc < 3) {
+ fprintf(stderr, "cryn: '%s' needs a source file\n", cmd);
+ return 1;
+ }
+ file = argv[2];
+ }
+ int start = has_sub ? 3 : 2;
+ for (int i = start; i < argc; i++) {
+ if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) out = argv[++i];
+ }
+ return compile_file(file, kind, out);
+}
diff --git a/src/opt.c b/src/opt.c
new file mode 100644
index 0000000..7ef8ff9
--- /dev/null
+++ b/src/opt.c
@@ -0,0 +1,44 @@
+#include "opt.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+/* Remove code that is unreachable (after unconditional jumps/returns/stop)
+ up to the next referenced label, and drop labels nobody targets. */
+static void opt_fn(FnIR* fn) {
+ if (fn->nins == 0) return;
+
+ /* which labels are referenced? */
+ char* used = xmalloc((size_t)(fn->nlabels ? fn->nlabels : 1));
+ memset(used, 0, (size_t)(fn->nlabels ? fn->nlabels : 1));
+ for (int i = 0; i < fn->nins; i++) {
+ Insn* in = &fn->ins[i];
+ if (in->op == I_JMP || in->op == I_JMPCOND) used[in->a] = 1;
+ }
+
+ Insn* out = xmalloc((size_t)fn->nins * sizeof(Insn));
+ int nout = 0;
+ int dead = 0;
+ for (int i = 0; i < fn->nins; i++) {
+ Insn* in = &fn->ins[i];
+ if (in->op == I_LBL) {
+ dead = 0;
+ if (!used[in->a]) continue; /* drop unreferenced label */
+ out[nout++] = *in;
+ continue;
+ }
+ if (dead) continue;
+ out[nout++] = *in;
+ if (in->op == I_JMP || in->op == I_RET || in->op == I_STOP) dead = 1;
+ }
+ free(used);
+ if (nout < fn->nins) {
+ memcpy(fn->ins, out, (size_t)nout * sizeof(Insn));
+ fn->nins = nout;
+ }
+ free(out);
+}
+
+void opt_program(ProgramIR* p) {
+ for (int i = 0; i < p->nfns; i++) opt_fn(&p->fns[i]);
+}
diff --git a/src/opt.h b/src/opt.h
new file mode 100644
index 0000000..ed2c160
--- /dev/null
+++ b/src/opt.h
@@ -0,0 +1,9 @@
+#ifndef CR_OPT_H
+#define CR_OPT_H
+
+#include "ir.h"
+
+/* IR-to-IR optimizations: unreachable code removal, dead label removal. */
+void opt_program(ProgramIR* p);
+
+#endif
diff --git a/src/parse.c b/src/parse.c
new file mode 100644
index 0000000..1f6d628
--- /dev/null
+++ b/src/parse.c
@@ -0,0 +1,417 @@
+#include "parse.h"
+#include "util.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct Parser {
+ Token* t;
+ size_t i;
+ FnAst* cur_fn; /* for error context */
+} Parser;
+
+static Token* peek(Parser* p) { return &p->t[p->i]; }
+static Token* next(Parser* p) { return &p->t[p->i++]; }
+
+static int at(Parser* p, TokKind k) { return peek(p)->kind == k; }
+static int accept(Parser* p, TokKind k) {
+ if (at(p, k)) { p->i++; return 1; }
+ return 0;
+}
+
+static Token* expect(Parser* p, TokKind k, const char* what) {
+ Token* t = peek(p);
+ if (t->kind != k) {
+ crn_error(t->loc, "expected %s%s%s but found '%s'",
+ what, what[0] ? " " : "", what[0] ? "" : "", tok_kind_name(t->kind));
+ return t;
+ }
+ return next(p);
+}
+
+static Node* node_new(Parser* p, AstKind kind) {
+ Node* n = xmalloc(sizeof(Node));
+ memset(n, 0, sizeof *n);
+ n->kind = kind;
+ n->loc = peek(p)->loc;
+ (void)p;
+ return n;
+}
+
+static TypeAst* parse_type(Parser* p);
+
+static TypeAst* type_new(Parser* p, TypeAstKind k) {
+ TypeAst* t = xmalloc(sizeof(TypeAst));
+ memset(t, 0, sizeof *t);
+ t->kind = k;
+ t->loc = peek(p)->loc;
+ (void)p;
+ return t;
+}
+
+static TypeAst* parse_type(Parser* p) {
+ Token* t = peek(p);
+ if (t->kind == T_ADDRESS_T || t->kind == T_ADDRESS) {
+ next(p);
+ TypeAst* a = type_new(p, TB_ADDR);
+ if (tok_is_type_start(peek(p)->kind))
+ a->inner = parse_type(p);
+ else
+ a->inner = NULL; /* address of nothing */
+ return a;
+ }
+ if (t->kind == T_ARRAY) {
+ next(p);
+ TypeAst* a = type_new(p, TB_ARR);
+ if (at(p, T_INT)) {
+ Token* n = next(p);
+ if (n->neg) crn_error(n->loc, "array size cannot be negative");
+ a->count = n->ival;
+ if (a->count == 0) crn_error(n->loc, "array size must be at least 1");
+ } else {
+ a->count = 0; /* unsized array (only valid as address pointee) */
+ }
+ expect(p, T_OF, "'of'");
+ a->inner = parse_type(p);
+ if (a->inner && a->inner->kind == TB_ARR && a->inner->count == 0)
+ crn_error(t->loc, "array element type cannot be an unsized array");
+ return a;
+ }
+ if (t->kind == T_STRING_T) {
+ crn_error(t->loc, "'string' is not a type; it is used with 'make'");
+ next(p);
+ return type_new(p, TB_BASE);
+ }
+ if (tok_is_type_start(t->kind)) {
+ next(p);
+ TypeAst* a = type_new(p, TB_BASE);
+ a->base = t->kind;
+ return a;
+ }
+ crn_error(t->loc, "expected a type but found '%s'", tok_kind_name(t->kind));
+ next(p);
+ return NULL;
+}
+
+static char* parse_name(Parser* p, const char* what) {
+ Token* t = peek(p);
+ if (t->kind != T_IDENT) {
+ crn_error(t->loc, "expected %s but found '%s'", what, tok_kind_name(t->kind));
+ if (t->kind != T_EOF) next(p);
+ return xstrdup("_bad");
+ }
+ next(p);
+ return xstrdup(t->text);
+}
+
+static Imm imm_from_token(Token* t) {
+ Imm m;
+ memset(&m, 0, sizeof m);
+ switch (t->kind) {
+ case T_INT: m.kind = IM_INT; m.u = t->ival; m.neg = t->neg; break;
+ case T_FLOAT: m.kind = IM_FLOAT; m.d = t->fval; break;
+ case T_CHAR: m.kind = IM_CHAR; m.u = t->ival; break;
+ case T_TRUE: m.kind = IM_BOOL; m.b = 1; break;
+ case T_FALSE: m.kind = IM_BOOL; m.b = 0; break;
+ default: m.kind = IM_NONE; break;
+ }
+ return m;
+}
+
+static Node* parse_command(Parser* p) {
+ Token* t = peek(p);
+ Node* n;
+ switch (t->kind) {
+ case T_MOVE: {
+ next(p);
+ Token* a = peek(p);
+ if (a->kind == T_IDENT) { next(p); n = node_new(p, A_MOVE_NAME); n->name = xstrdup(a->text); }
+ else if (a->kind == T_INT) { next(p); n = node_new(p, A_MOVE_LIT); n->imm = imm_from_token(a); n->has_imm = 1; }
+ else if (a->kind == T_INDEX) { next(p); n = node_new(p, A_MOVE_INDEX); }
+ else if (a->kind == T_AT) { next(p); n = node_new(p, A_MOVE_AT); }
+ else { crn_error(a->loc, "expected connection name, index, 'index', or 'at' after 'move'"); n = node_new(p, A_HOME); }
+ return n;
+ }
+ case T_BACK: next(p); return node_new(p, A_BACK);
+ case T_HOME: next(p); return node_new(p, A_HOME);
+ case T_ADDRESS: {
+ next(p);
+ if (at(p, T_IDENT)) { n = node_new(p, A_ADDRESS_NAME); n->name = parse_name(p, "a cell or transformation name"); }
+ else n = node_new(p, A_ADDRESS_HERE);
+ return n;
+ }
+ case T_MAKE: {
+ next(p);
+ Token* k = peek(p);
+ if (k->kind == T_STRING_T) {
+ next(p);
+ Token* s = expect(p, T_STRING, "a string literal");
+ n = node_new(p, A_MAKE_STRING);
+ if (s->kind == T_STRING) {
+ n->str = xmalloc(s->strlen ? s->strlen : 1);
+ memcpy(n->str, s->str ? s->str : "", s->strlen);
+ n->strlen = s->strlen;
+ }
+ if (at(p, T_IDENT)) n->name = parse_name(p, "a cell name");
+ return n;
+ }
+ if (k->kind == T_ARRAY) {
+ TypeAst* a = parse_type(p);
+ n = node_new(p, A_MAKE_ARRAY);
+ n->t = a;
+ if (at(p, T_IDENT)) n->name = parse_name(p, "a cell name");
+ return n;
+ }
+ if (tok_is_type_start(k->kind)) {
+ TypeAst* a = parse_type(p);
+ n = node_new(p, A_MAKE_VALUE);
+ n->t = a;
+ if (at(p, T_IDENT)) n->name = parse_name(p, "a cell name");
+ return n;
+ }
+ if (k->kind == T_IDENT || k->kind == T_INT) {
+ /* dynamic block: make <size> of <type> [name] */
+ n = node_new(p, A_MAKE_DYN);
+ if (k->kind == T_IDENT) {
+ next(p);
+ n->name = xstrdup(k->text); /* size cell name */
+ n->has_imm = 0;
+ } else {
+ next(p);
+ n->imm = imm_from_token(k);
+ n->has_imm = 1;
+ n->name = NULL;
+ }
+ expect(p, T_OF, "'of'");
+ n->t = parse_type(p);
+ if (at(p, T_IDENT)) n->name2 = parse_name(p, "a cell name");
+ return n;
+ }
+ crn_error(k->loc, "expected a type, 'array', 'string', a size name, or a size literal after 'make'");
+ next(p);
+ return node_new(p, A_HOME);
+ }
+ case T_FREE: next(p); return node_new(p, A_FREE);
+ case T_READ: next(p); return node_new(p, A_READ);
+ case T_WRITE: next(p); return node_new(p, A_WRITE);
+ case T_SET: {
+ next(p);
+ Token* v = peek(p);
+ if (v->kind != T_INT && v->kind != T_FLOAT && v->kind != T_CHAR &&
+ v->kind != T_TRUE && v->kind != T_FALSE && v->kind != T_NOTHING) {
+ crn_error(v->loc, "expected a literal value after 'set' but found '%s'", tok_kind_name(v->kind));
+ if (v->kind != T_EOF) next(p);
+ return node_new(p, A_SET);
+ }
+ next(p);
+ n = node_new(p, A_SET);
+ n->imm = imm_from_token(v);
+ if (v->kind == T_NOTHING) n->imm.kind = IM_NONE;
+ n->has_imm = 1;
+ return n;
+ }
+ case T_ADD: case T_SUBTRACT: case T_MULTIPLY: case T_DIVIDE: case T_REMAINDER: {
+ TokKind op = next(p)->kind;
+ n = node_new(p, A_ARITH);
+ n->arith = op == T_ADD ? AR_ADD : op == T_SUBTRACT ? AR_SUB :
+ op == T_MULTIPLY ? AR_MUL : op == T_DIVIDE ? AR_DIV : AR_MOD;
+ Token* v = peek(p);
+ if (v->kind == T_INT || v->kind == T_FLOAT) {
+ next(p);
+ n->imm = imm_from_token(v);
+ n->has_imm = 1;
+ }
+ return n;
+ }
+ case T_NEGATE: next(p); return node_new(p, A_NEG);
+ case T_PUSH: next(p); return node_new(p, A_PUSH);
+ case T_POP: next(p); return node_new(p, A_POP);
+ case T_SWAP: next(p); return node_new(p, A_SWAP);
+ case T_COMPARE: {
+ next(p);
+ n = node_new(p, A_CMP);
+ Token* v = peek(p);
+ if (v->kind == T_INT || v->kind == T_FLOAT || v->kind == T_CHAR ||
+ v->kind == T_TRUE || v->kind == T_FALSE) {
+ next(p);
+ n->imm = imm_from_token(v);
+ n->has_imm = 1;
+ }
+ return n;
+ }
+ case T_CONVERT: {
+ next(p);
+ n = node_new(p, A_CONV);
+ n->t = parse_type(p);
+ return n;
+ }
+ case T_JUMP: {
+ next(p);
+ n = node_new(p, A_JMP);
+ n->name = parse_name(p, "a label name");
+ if (accept(p, T_WHEN)) {
+ Token* c = peek(p);
+ if (c->kind == T_LESS || c->kind == T_EQUAL || c->kind == T_GREATER ||
+ c->kind == T_DIFFERENT || c->kind == T_LESS_OR_EQUAL ||
+ c->kind == T_GREATER_OR_EQUAL) {
+ next(p);
+ n->cond = c->kind;
+ n->has_cond = 1;
+ } else {
+ crn_error(c->loc, "expected a condition (less, equal, greater, different, less-or-equal, greater-or-equal) after 'when'");
+ }
+ }
+ return n;
+ }
+ case T_CALL: {
+ next(p);
+ if (at(p, T_IDENT)) { n = node_new(p, A_CALL_NAME); n->name = parse_name(p, "a transformation name"); }
+ else n = node_new(p, A_CALL_IND);
+ return n;
+ }
+ case T_RETURN: next(p); return node_new(p, A_RET);
+ case T_STOP: next(p); return node_new(p, A_STOP);
+ case T_INPUT: next(p); return node_new(p, A_INPUT);
+ case T_OUTPUT: {
+ next(p);
+ if (at(p, T_TEXT)) { next(p); return node_new(p, A_OUTPUT_TEXT); }
+ return node_new(p, A_OUTPUT);
+ }
+ case T_OPEN: {
+ next(p);
+ n = node_new(p, A_OPEN);
+ Token* m = peek(p);
+ if (m->kind == T_READ || m->kind == T_WRITE || m->kind == T_BOTH) {
+ next(p);
+ n->mode = m->kind;
+ } else {
+ crn_error(m->loc, "expected 'read', 'write', or 'both' after 'open'");
+ n->mode = T_READ;
+ }
+ return n;
+ }
+ case T_CLOSE: next(p); return node_new(p, A_CLOSE);
+ case T_SYSTEM: next(p); return node_new(p, A_SYSTEM);
+ case T_SPAWN: {
+ next(p);
+ n = node_new(p, A_SPAWN);
+ n->name = parse_name(p, "a transformation name");
+ return n;
+ }
+ case T_WAIT: next(p); return node_new(p, A_WAIT);
+ case T_MARK: {
+ next(p);
+ n = node_new(p, A_MARK);
+ n->name = parse_name(p, "a label name");
+ return n;
+ }
+ default:
+ crn_error(t->loc, "expected a command but found '%s'", tok_kind_name(t->kind));
+ next(p);
+ return NULL;
+ }
+}
+
+static void parse_define(Parser* p, ProgramAst* prog) {
+ Token* d = expect(p, T_DEFINE, "'define'");
+ FnAst fn;
+ memset(&fn, 0, sizeof fn);
+ fn.loc = d->loc;
+ fn.name = parse_name(p, "a transformation name");
+
+ /* parameters: { type name } until a non-type token.
+ 'address' only opens a parameter when a pointee type follows,
+ so 'address mycell' as the first command is not swallowed. */
+ List params = {0};
+ for (;;) {
+ Token* t = peek(p);
+ int is_type = tok_is_type_start(t->kind) && t->kind != T_ADDRESS;
+ if (t->kind == T_ADDRESS && tok_is_type_start(p->t[p->i + 1].kind))
+ is_type = 1;
+ if (!is_type) break;
+ ParamAst pa;
+ pa.loc = peek(p)->loc;
+ pa.t = parse_type(p);
+ pa.name = parse_name(p, "a parameter name");
+ ParamAst* pp = xmalloc(sizeof(ParamAst));
+ *pp = pa;
+ list_add(¶ms, pp);
+ }
+
+ if (accept(p, T_RESULT)) fn.result = parse_type(p);
+
+ List body = {0};
+ while (!at(p, T_END) && !at(p, T_EOF)) {
+ Node* n = parse_command(p);
+ if (n) list_add(&body, n);
+ }
+ expect(p, T_END, "'end'");
+
+ /* copy param and body pointer lists into contiguous arrays */
+ fn.params = xmalloc((params.len ? params.len : 1) * sizeof(ParamAst));
+ for (size_t i = 0; i < params.len; i++) fn.params[i] = *(ParamAst*)params.items[i];
+ fn.body = xmalloc((body.len ? body.len : 1) * sizeof(Node));
+ for (size_t i = 0; i < body.len; i++) fn.body[i] = *(Node*)body.items[i];
+ fn.nbody = body.len;
+ fn.nparams = params.len;
+
+ /* append */
+ prog->nfns++;
+ prog->fns = xrealloc(prog->fns, prog->nfns * sizeof(FnAst));
+ prog->fns[prog->nfns - 1] = fn;
+}
+
+static void parse_global_make(Parser* p, ProgramAst* prog) {
+ Token* m = expect(p, T_MAKE, "'make'");
+ GlobAst g;
+ memset(&g, 0, sizeof g);
+ g.loc = m->loc;
+ Token* k = peek(p);
+ if (k->kind == T_STRING_T) {
+ next(p);
+ Token* s = expect(p, T_STRING, "a string literal");
+ if (s->kind == T_STRING) { g.str = xmalloc(s->strlen + 1); memcpy(g.str, s->str, s->strlen); g.str[s->strlen] = 0; g.strlen = s->strlen; }
+ g.kind = G_STRING;
+ g.name = parse_name(p, "a world cell name");
+ } else if (k->kind == T_ARRAY) {
+ g.kind = G_ARRAY;
+ g.t = parse_type(p);
+ if (g.t->kind == TB_ARR) g.count = g.t->count;
+ g.name = parse_name(p, "a world cell name");
+ } else if (tok_is_type_start(k->kind)) {
+ g.kind = G_VALUE;
+ g.t = parse_type(p);
+ g.name = parse_name(p, "a world cell name");
+ } else {
+ crn_error(k->loc, "a world cell made with 'make' at the top level must be a value, array, or string");
+ next(p);
+ return;
+ }
+ prog->nglobs++;
+ prog->globs = xrealloc(prog->globs, prog->nglobs * sizeof(GlobAst));
+ prog->globs[prog->nglobs - 1] = g;
+}
+
+ProgramAst* parse_program(Token* toks) {
+ Parser p;
+ p.t = toks;
+ p.i = 0;
+ p.cur_fn = NULL;
+
+ ProgramAst* prog = xmalloc(sizeof(ProgramAst));
+ prog->fns = NULL;
+ prog->nfns = 0;
+ prog->globs = NULL;
+ prog->nglobs = 0;
+
+ while (!at(&p, T_EOF)) {
+ if (at(&p, T_DEFINE)) parse_define(&p, prog);
+ else if (at(&p, T_MAKE)) parse_global_make(&p, prog);
+ else {
+ Token* t = peek(&p);
+ crn_error(t->loc, "expected 'define' or 'make' at top level but found '%s'", tok_kind_name(t->kind));
+ next(&p);
+ }
+ }
+ return prog;
+}
diff --git a/src/parse.h b/src/parse.h
new file mode 100644
index 0000000..696b99e
--- /dev/null
+++ b/src/parse.h
@@ -0,0 +1,10 @@
+#ifndef CR_PARSE_H
+#define CR_PARSE_H
+
+#include "ast.h"
+
+/* Parse a token array into a program. Reports errors via crn_error.
+ Always returns a ProgramAst (possibly empty on hard error). */
+ProgramAst* parse_program(Token* toks);
+
+#endif
diff --git a/src/rt/cryn_rt.c b/src/rt/cryn_rt.c
new file mode 100644
index 0000000..ff9f743
--- /dev/null
+++ b/src/rt/cryn_rt.c
@@ -0,0 +1,264 @@
+#include "rt/cryn_rt.h"
+#include "util.h"
+
+#include <errno.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+_Thread_local CrCtx cr_ctx = {0};
+
+void crn_init(void) {
+ memset(&cr_ctx, 0, sizeof cr_ctx);
+ cr_ctx.in = stdin;
+ cr_ctx.out = stdout;
+}
+
+void crn_die(const char* msg) {
+ fprintf(stderr, "cryn: runtime error: %s\n", msg);
+ exit(1);
+}
+
+void crn_die_range(uint64_t i, uint64_t n) {
+ fprintf(stderr, "cryn: runtime error: index %llu out of bounds (size %llu)\n",
+ (unsigned long long)i, (unsigned long long)n);
+ exit(1);
+}
+
+/* ---- value stack ---- */
+
+void crn_push(Val v) {
+ if (cr_ctx.sp == cr_ctx.cap) {
+ cr_ctx.cap = cr_ctx.cap ? cr_ctx.cap * 2 : 64;
+ cr_ctx.stk = realloc(cr_ctx.stk, cr_ctx.cap * sizeof(Val));
+ if (!cr_ctx.stk) crn_die("out of memory");
+ }
+ cr_ctx.stk[cr_ctx.sp++] = v;
+}
+
+Val crn_pop(void) { return cr_ctx.stk[--cr_ctx.sp]; }
+
+/* ---- focus ---- */
+
+void crn_focus_push(void* f) {
+ if (cr_ctx.fh_sp >= CR_FH_MAX) crn_die("focus history overflow");
+ cr_ctx.fh[cr_ctx.fh_sp++] = f;
+}
+
+void* crn_focus_pop(void) {
+ if (cr_ctx.fh_sp == 0) crn_die("focus history underflow");
+ return cr_ctx.fh[--cr_ctx.fh_sp];
+}
+
+/* ---- heap ---- */
+
+typedef struct CrHdr {
+ uint64_t magic;
+ uint64_t size; /* data bytes */
+} CrHdr;
+
+#define CR_MAGIC 0x4352594E424C4BULL /* "CRYNBLK" */
+
+void* crn_alloc(uint64_t nbytes) {
+ CrHdr* h = malloc(sizeof(CrHdr) + (size_t)nbytes + 16);
+ if (!h) crn_die("out of memory");
+ h->magic = CR_MAGIC;
+ h->size = nbytes;
+ unsigned char* p = (unsigned char*)h;
+ p += sizeof(CrHdr);
+ /* keep data 16-aligned: sizeof(CrHdr)==16 on 64-bit */
+ memset(p, 0, (size_t)nbytes);
+ return p;
+}
+
+void crn_free(void* p) {
+ if (!p) crn_die("free of the null address");
+ CrHdr* h = (CrHdr*)((unsigned char*)p - sizeof(CrHdr));
+ if (h->magic != CR_MAGIC) {
+ crn_die("free of an address that is not a heap block "
+ "(only blocks made with 'make <size> of <type>' or anonymous 'make' can be freed)");
+ }
+ h->magic = 0;
+ free(h);
+}
+
+uint64_t crn_block_n(void* data, uint64_t elemsz) {
+ if (elemsz == 0) elemsz = 1;
+ CrHdr* h = (CrHdr*)((unsigned char*)data - sizeof(CrHdr));
+ if (h->magic != CR_MAGIC) {
+ crn_die("index into memory that is not a heap block "
+ "(only blocks made with 'make <size> of <type>' support bounds checks)");
+ }
+ return h->size / elemsz;
+}
+
+void crn_check_block(void* data, uint64_t i, uint64_t elemsz) {
+ uint64_t n = crn_block_n(data, elemsz);
+ if (i >= n) crn_die_range(i, n);
+}
+
+void crn_check_range(uint64_t i, uint64_t n) {
+ if (i >= n) crn_die_range(i, n);
+}
+
+/* ---- conversions ---- */
+
+int64_t crn_d2i64(double d) {
+ if (d >= 9223372036854775808.0) return INT64_MAX;
+ if (d <= -9223372036854775808.0) return INT64_MIN;
+ return (int64_t)d;
+}
+
+uint64_t crn_d2u64(double d) {
+ if (d >= 18446744073709551616.0) return UINT64_MAX;
+ if (d <= 0.0) return 0;
+ return (uint64_t)d;
+}
+
+int64_t crn_d2i8(double d) {
+ if (d >= 127.0) return 127;
+ if (d <= -128.0) return -128;
+ return (int64_t)(int8_t)d;
+}
+
+int64_t crn_d2i16(double d) {
+ if (d >= 32767.0) return 32767;
+ if (d <= -32768.0) return -32768;
+ return (int64_t)(int16_t)d;
+}
+
+int64_t crn_d2i32(double d) {
+ if (d >= 2147483648.0) return 2147483647;
+ if (d <= -2147483649.0) return -2147483648;
+ return (int64_t)(int32_t)d;
+}
+
+uint64_t crn_d2u8(double d) {
+ if (d >= 255.0) return 255;
+ if (d <= 0.0) return 0;
+ return (uint64_t)(uint8_t)d;
+}
+
+uint64_t crn_d2u16(double d) {
+ if (d >= 65535.0) return 65535;
+ if (d <= 0.0) return 0;
+ return (uint64_t)(uint16_t)d;
+}
+
+uint64_t crn_d2u32(double d) {
+ if (d >= 4294967296.0) return 4294967295ULL;
+ if (d <= 0.0) return 0;
+ return (uint64_t)(uint32_t)d;
+}
+
+/* ---- output ---- */
+
+static void crn_out_str(const char* s) { fputs(s, cr_ctx.out); }
+
+void crn_out_i64(int64_t v) { fprintf(cr_ctx.out, "%lld", (long long)v); }
+void crn_out_u64(uint64_t v) { fprintf(cr_ctx.out, "%llu", (unsigned long long)v); }
+
+static void crn_out_f(double v, int maxprec) {
+ char buf[64];
+ for (int prec = 1; prec <= maxprec; prec++) {
+ snprintf(buf, sizeof buf, "%.*g", prec, v);
+ if (strtod(buf, NULL) == v) break;
+ }
+ crn_out_str(buf);
+}
+
+void crn_out_f64(double v) { crn_out_f(v, 17); }
+void crn_out_f32(float v) {
+ char buf[64];
+ for (int prec = 1; prec <= 9; prec++) {
+ snprintf(buf, sizeof buf, "%.*g", prec, v);
+ if ((float)strtod(buf, NULL) == v) break;
+ }
+ crn_out_str(buf);
+}
+void crn_out_bool(int b) { crn_out_str(b ? "true" : "false"); }
+void crn_out_char(unsigned c) { fputc((int)(c & 0xFF), cr_ctx.out); }
+void crn_out_addr(uint64_t a) { fprintf(cr_ctx.out, "0x%llx", (unsigned long long)a); }
+
+void crn_out_text(uint8_t* p) {
+ FILE* f = cr_ctx.out;
+ while (*p) fputc(*p++, f);
+}
+
+/* ---- input / files / system ---- */
+
+int64_t crn_input(void) {
+ int c = fgetc(cr_ctx.in);
+ if (c == EOF) return -1;
+ return c;
+}
+
+int64_t crn_open(uint8_t* path, int mode) {
+ const char* m;
+ int flags_ok = 1;
+ (void)flags_ok;
+ switch (mode) {
+ case 0: m = "rb"; break;
+ case 1: m = "wb"; break;
+ default: m = "w+b"; break;
+ }
+ FILE* f = fopen((const char*)path, m);
+ if (!f) return -1;
+ /* success: route this context's streams through the file */
+ if (mode == 0) cr_ctx.in = f;
+ else if (mode == 1) cr_ctx.out = f;
+ else { cr_ctx.in = f; cr_ctx.out = f; }
+ cr_ctx.files[cr_ctx.nfiles < CR_FILES_MAX ? cr_ctx.nfiles++ : 0] = f;
+ return (int64_t)(uintptr_t)f;
+}
+
+int64_t crn_close(int64_t h) {
+ FILE* f = (FILE*)(uintptr_t)h;
+ if (h <= 0) return -1;
+ if (cr_ctx.in == f) cr_ctx.in = stdin;
+ if (cr_ctx.out == f) cr_ctx.out = stdout;
+ for (int i = 0; i < cr_ctx.nfiles; i++) {
+ if (cr_ctx.files[i] == f) {
+ cr_ctx.files[i] = cr_ctx.files[--cr_ctx.nfiles];
+ break;
+ }
+ }
+ return fclose(f) == 0 ? 0 : -1;
+}
+
+int64_t crn_system(uint8_t* cmd) { return (int64_t)system((const char*)cmd); }
+
+void crn_stop(int64_t code) { exit((int)(code & 0xFF)); }
+
+/* ---- concurrency ---- */
+
+typedef struct CrSpawnArg {
+ CrFnRef* fn;
+} CrSpawnArg;
+
+static void* crn_thread_main(void* argp) {
+ CrSpawnArg* a = argp;
+ CrFnRef* fn = a->fn;
+ free(a);
+ crn_init();
+ fn->fn();
+ return NULL;
+}
+
+void crn_spawn(CrFnRef* f) {
+ if (f->np != 0)
+ crn_die("spawn of a transformation that takes parameters");
+ if (cr_ctx.nspawned >= CR_SPAWN_MAX)
+ crn_die("too many outstanding spawned contexts (limit 64)");
+ CrSpawnArg* a = malloc(sizeof(CrSpawnArg));
+ if (!a) crn_die("out of memory");
+ a->fn = f;
+ if (pthread_create(&cr_ctx.spawned[cr_ctx.nspawned++], NULL, crn_thread_main, a) != 0)
+ crn_die("thread creation failed");
+}
+
+void crn_wait(void) {
+ for (int i = 0; i < cr_ctx.nspawned; i++)
+ pthread_join(cr_ctx.spawned[i], NULL);
+ cr_ctx.nspawned = 0;
+}
diff --git a/src/rt/cryn_rt.h b/src/rt/cryn_rt.h
new file mode 100644
index 0000000..907cd96
--- /dev/null
+++ b/src/rt/cryn_rt.h
@@ -0,0 +1,97 @@
+#ifndef CRYN_RT_H
+#define CRYN_RT_H
+
+/*
+ * Minimal runtime support for compiled Cryn programs.
+ * Everything here exists only because it cannot be expressed as inline C
+ * in generated code (context management, heap headers, threads, output
+ * formatting). The language's semantics live in the compiler.
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <pthread.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+#define CRN_NORETURN __attribute__((noreturn))
+#else
+#define CRN_NORETURN
+#endif
+
+typedef union Val {
+ uint64_t u; /* unsigned, address (as integer), bool, char */
+ int64_t i; /* signed integers, integer literals */
+ double d; /* float32 (widened) and float64 */
+ void* p; /* focus target / data pointer */
+} Val;
+
+/* A transformation reference: function pointer + parameter count.
+ Indirect calls and spawns are rejected at runtime unless np == 0. */
+typedef struct CrFnRef {
+ Val (*fn)(void);
+ int np;
+} CrFnRef;
+
+#define CR_FH_MAX 256
+#define CR_SPAWN_MAX 64
+#define CR_FILES_MAX 64
+
+typedef struct CrCtx {
+ Val* stk; /* value stack */
+ uint64_t sp, cap;
+ void* fh[CR_FH_MAX]; /* focus history */
+ uint64_t fh_sp;
+ FILE* in; /* current input stream (default stdin) */
+ FILE* out; /* current output stream (default stdout) */
+ FILE* files[CR_FILES_MAX];
+ int nfiles;
+ pthread_t spawned[CR_SPAWN_MAX];
+ int nspawned;
+} CrCtx;
+
+extern _Thread_local CrCtx cr_ctx;
+
+void crn_init(void);
+void crn_die(const char* msg) CRN_NORETURN;
+void crn_die_range(uint64_t i, uint64_t n) CRN_NORETURN;
+
+void crn_push(Val v);
+Val crn_pop(void);
+
+void crn_focus_push(void* f);
+void* crn_focus_pop(void);
+
+void* crn_alloc(uint64_t nbytes);
+void crn_free(void* p);
+uint64_t crn_block_n(void* data, uint64_t elemsz);
+void crn_check_block(void* data, uint64_t i, uint64_t elemsz);
+void crn_check_range(uint64_t i, uint64_t n);
+
+int64_t crn_d2i64(double d);
+uint64_t crn_d2u64(double d);
+int64_t crn_d2i8(double d);
+int64_t crn_d2i16(double d);
+int64_t crn_d2i32(double d);
+uint64_t crn_d2u8(double d);
+uint64_t crn_d2u16(double d);
+uint64_t crn_d2u32(double d);
+
+void crn_out_i64(int64_t v);
+void crn_out_u64(uint64_t v);
+void crn_out_f64(double v);
+void crn_out_f32(float v);
+void crn_out_bool(int b);
+void crn_out_char(unsigned c);
+void crn_out_addr(uint64_t a);
+void crn_out_text(uint8_t* p);
+
+int64_t crn_input(void);
+int64_t crn_open(uint8_t* path, int mode);
+int64_t crn_close(int64_t h);
+int64_t crn_system(uint8_t* cmd);
+void crn_stop(int64_t code) CRN_NORETURN;
+
+void crn_spawn(CrFnRef* f);
+void crn_wait(void);
+
+#endif
diff --git a/src/types.c b/src/types.c
new file mode 100644
index 0000000..27a47db
--- /dev/null
+++ b/src/types.c
@@ -0,0 +1,194 @@
+#include "types.h"
+#include "util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_TYPES 65536
+static Type g_types[MAX_TYPES];
+static int g_ntypes = 0;
+
+static Type* intern(Type t) {
+ for (int i = 0; i < g_ntypes; i++) {
+ Type* a = &g_types[i];
+ if (a->kind == t.kind && a->inner == t.inner && a->count == t.count &&
+ a->litval == t.litval && a->litneg == t.litneg)
+ return a;
+ }
+ if (g_ntypes >= MAX_TYPES) {
+ fprintf(stderr, "cryn: too many distinct types\n");
+ exit(1);
+ }
+ g_types[g_ntypes] = t;
+ return &g_types[g_ntypes++];
+}
+
+static Type base(TypeKind k) {
+ Type t;
+ memset(&t, 0, sizeof t);
+ t.kind = k;
+ return t;
+}
+
+Type* ty_nothing(void) { return intern(base(TY_NOTHING)); }
+Type* ty_bool(void) { return intern(base(TY_BOOL)); }
+Type* ty_char(void) { return intern(base(TY_CHAR)); }
+Type* ty_int(int bits) {
+ switch (bits) {
+ case 8: return intern(base(TY_INT8));
+ case 16: return intern(base(TY_INT16));
+ case 32: return intern(base(TY_INT32));
+ default: return intern(base(TY_INT64));
+ }
+}
+Type* ty_uint(int bits) {
+ switch (bits) {
+ case 8: return intern(base(TY_UINT8));
+ case 16: return intern(base(TY_UINT16));
+ case 32: return intern(base(TY_UINT32));
+ default: return intern(base(TY_UINT64));
+ }
+}
+Type* ty_float32(void) { return intern(base(TY_FLOAT32)); }
+Type* ty_float64(void) { return intern(base(TY_FLOAT64)); }
+Type* ty_addr_to(Type* pointee) {
+ Type t;
+ memset(&t, 0, sizeof t);
+ t.kind = TY_ADDR;
+ t.inner = pointee;
+ return intern(t);
+}
+Type* ty_array(Type* elem, uint64_t count) {
+ Type t;
+ memset(&t, 0, sizeof t);
+ t.kind = TY_ARR;
+ t.inner = elem;
+ t.count = count;
+ return intern(t);
+}
+Type* ty_func(void) { return intern(base(TY_FUNC)); }
+Type* ty_intlit(uint64_t mag, int neg) {
+ Type t;
+ memset(&t, 0, sizeof t);
+ t.kind = TY_INTLIT;
+ t.litval = mag;
+ t.litneg = neg;
+ return intern(t);
+}
+
+int type_eq(const Type* a, const Type* b) { return a == b; }
+
+uint64_t type_size(const Type* t) {
+ switch (t->kind) {
+ case TY_NOTHING: return 1;
+ case TY_BOOL: return 1;
+ case TY_CHAR: return 4;
+ case TY_INT8: case TY_UINT8: return 1;
+ case TY_INT16: case TY_UINT16: return 2;
+ case TY_INT32: case TY_UINT32: case TY_FLOAT32: return 4;
+ case TY_INT64: case TY_UINT64: case TY_FLOAT64: case TY_ADDR: case TY_FUNC: return 8;
+ case TY_ARR: return t->count ? t->count * type_size(t->inner) : type_size(t->inner);
+ case TY_INTLIT: return 8;
+ }
+ return 8;
+}
+
+int ty_is_int(const Type* t) {
+ switch (t->kind) {
+ case TY_INT8: case TY_INT16: case TY_INT32: case TY_INT64:
+ case TY_UINT8: case TY_UINT16: case TY_UINT32: case TY_UINT64:
+ return 1;
+ default: return 0;
+ }
+}
+int ty_is_uint(const Type* t) {
+ switch (t->kind) {
+ case TY_UINT8: case TY_UINT16: case TY_UINT32: case TY_UINT64: return 1;
+ default: return 0;
+ }
+}
+int ty_is_signed_int(const Type* t) {
+ switch (t->kind) {
+ case TY_INT8: case TY_INT16: case TY_INT32: case TY_INT64: return 1;
+ default: return 0;
+ }
+}
+int ty_is_any_int(const Type* t) { return ty_is_int(t) || t->kind == TY_INTLIT; }
+int ty_is_float(const Type* t) { return t->kind == TY_FLOAT32 || t->kind == TY_FLOAT64; }
+int ty_is_numeric(const Type* t) { return ty_is_any_int(t) || ty_is_float(t); }
+int ty_is_addr(const Type* t) { return t->kind == TY_ADDR; }
+int ty_is_array(const Type* t) { return t->kind == TY_ARR; }
+int ty_is_intlit(const Type* t) { return t->kind == TY_INTLIT; }
+int ty_is_bool(const Type* t) { return t->kind == TY_BOOL; }
+int ty_is_char(const Type* t) { return t->kind == TY_CHAR; }
+int ty_is_func(const Type* t) { return t->kind == TY_FUNC; }
+int ty_is_nothing(const Type* t) { return t->kind == TY_NOTHING; }
+
+int ty_int_bits(const Type* t) {
+ switch (t->kind) {
+ case TY_INT8: case TY_UINT8: return 8;
+ case TY_INT16: case TY_UINT16: return 16;
+ case TY_INT32: case TY_UINT32: return 32;
+ case TY_INT64: case TY_UINT64: return 64;
+ default: return 0;
+ }
+}
+
+static uint64_t umax_for(int bits) {
+ uint64_t v = 0xFFFFFFFFFFFFFFFFULL;
+ return v >> (64 - bits);
+}
+
+int intlit_fits(uint64_t mag, int neg, const Type* t) {
+ if (t->kind == TY_BOOL) return mag <= 1;
+ if (t->kind == TY_CHAR) return !neg && mag <= 255;
+ if (ty_is_signed_int(t)) {
+ int bits = ty_int_bits(t);
+ uint64_t max = (1ULL << (bits - 1)) - 1;
+ if (neg) return mag <= max + 1;
+ return mag <= max;
+ }
+ if (ty_is_uint(t)) return !neg && mag <= umax_for(ty_int_bits(t));
+ if (t->kind == TY_ADDR) return 1; /* reinterpret */
+ if (t->kind == TY_FLOAT32 || t->kind == TY_FLOAT64) return 1;
+ return 0;
+}
+
+const char* type_name(const Type* t, char* buf, size_t bufsz) {
+ char tmp[128];
+ switch (t->kind) {
+ case TY_NOTHING: return "nothing";
+ case TY_BOOL: return "bool";
+ case TY_CHAR: return "char";
+ case TY_INT8: return "int8";
+ case TY_INT16: return "int16";
+ case TY_INT32: return "int32";
+ case TY_INT64: return "int64";
+ case TY_UINT8: return "uint8";
+ case TY_UINT16: return "uint16";
+ case TY_UINT32: return "uint32";
+ case TY_UINT64: return "uint64";
+ case TY_FLOAT32: return "float32";
+ case TY_FLOAT64: return "float64";
+ case TY_FUNC: return "transform";
+ case TY_ADDR:
+ if (t->inner->kind == TY_NOTHING) return "address";
+ snprintf(buf, bufsz, "address %s", type_name(t->inner, tmp, sizeof tmp));
+ return buf;
+ case TY_ARR:
+ if (t->count)
+ snprintf(buf, bufsz, "array %llu of %s",
+ (unsigned long long)t->count,
+ type_name(t->inner, tmp, sizeof tmp));
+ else
+ snprintf(buf, bufsz, "array of %s",
+ type_name(t->inner, tmp, sizeof tmp));
+ return buf;
+ case TY_INTLIT:
+ snprintf(buf, bufsz, "%s%llu", t->litneg ? "-" : "",
+ (unsigned long long)t->litval);
+ return buf;
+ }
+ return "?";
+}
diff --git a/src/types.h b/src/types.h
new file mode 100644
index 0000000..3b5a8bd
--- /dev/null
+++ b/src/types.h
@@ -0,0 +1,70 @@
+#ifndef CR_TYPES_H
+#define CR_TYPES_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef enum {
+ TY_NOTHING,
+ TY_BOOL,
+ TY_CHAR, /* 32-bit codepoint */
+ TY_INT8, TY_INT16, TY_INT32, TY_INT64,
+ TY_UINT8, TY_UINT16, TY_UINT32, TY_UINT64,
+ TY_FLOAT32, TY_FLOAT64,
+ TY_ADDR, /* pointer; inner = pointee (TY_NOTHING for bare address) */
+ TY_ARR, /* array; inner = element; count = 0 -> unsized block */
+ TY_FUNC, /* transform reference */
+ TY_INTLIT /* integer literal with known value and sign */
+} TypeKind;
+
+typedef struct Type {
+ TypeKind kind;
+ struct Type* inner; /* TY_ADDR pointee, TY_ARR element */
+ uint64_t count; /* TY_ARR size; 0 = unsized */
+ uint64_t litval; /* TY_INTLIT magnitude */
+ int litneg; /* TY_INTLIT sign */
+} Type;
+
+/* Interned singletons and constructors (never free the results). */
+Type* ty_nothing(void);
+Type* ty_bool(void);
+Type* ty_char(void);
+Type* ty_int(int bits); /* 8/16/32/64 -> signed */
+Type* ty_uint(int bits);
+Type* ty_float32(void);
+Type* ty_float64(void);
+Type* ty_addr_to(Type* pointee);
+Type* ty_array(Type* elem, uint64_t count); /* count 0 = unsized */
+Type* ty_func(void);
+Type* ty_intlit(uint64_t mag, int neg);
+
+int type_eq(const Type* a, const Type* b); /* pointer equality works, but keep explicit */
+
+/* Size in bytes of a value of this type (element size for arrays). */
+uint64_t type_size(const Type* t);
+
+/* Classification */
+int ty_is_int(const Type* t); /* signed or unsigned integer kinds (not intlit) */
+int ty_is_uint(const Type* t);
+int ty_is_any_int(const Type* t); /* int/uint/intlit */
+int ty_is_float(const Type* t);
+int ty_is_numeric(const Type* t); /* int/uint/intlit/float */
+int ty_is_signed_int(const Type* t);
+int ty_is_addr(const Type* t);
+int ty_is_array(const Type* t);
+int ty_is_intlit(const Type* t);
+int ty_is_bool(const Type* t);
+int ty_is_char(const Type* t);
+int ty_is_func(const Type* t);
+int ty_is_nothing(const Type* t);
+
+/* Does int literal (mag,neg) fit the integer-ish type t? (t = int/uint/char/bool) */
+int intlit_fits(uint64_t mag, int neg, const Type* t);
+
+/* Bit width for integer kinds; 0 otherwise. */
+int ty_int_bits(const Type* t);
+
+/* Human-readable name into caller buffer. */
+const char* type_name(const Type* t, char* buf, size_t bufsz);
+
+#endif
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..a292200
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,121 @@
+#include "util.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void* xmalloc(size_t n) {
+ void* p = malloc(n ? n : 1);
+ if (!p) {
+ fprintf(stderr, "cryn: out of memory\n");
+ exit(1);
+ }
+ return p;
+}
+
+void* xrealloc(void* p, size_t n) {
+ void* q = realloc(p, n ? n : 1);
+ if (!q) {
+ fprintf(stderr, "cryn: out of memory\n");
+ exit(1);
+ }
+ return q;
+}
+
+char* xstrdup(const char* s) {
+ size_t n = strlen(s) + 1;
+ char* p = xmalloc(n);
+ memcpy(p, s, n);
+ return p;
+}
+
+void buf_put(Buf* b, const void* p, size_t n) {
+ if (b->len + n > b->cap) {
+ size_t nc = b->cap ? b->cap * 2 : 128;
+ while (nc < b->len + n) nc *= 2;
+ b->data = xrealloc(b->data, nc);
+ b->cap = nc;
+ }
+ memcpy(b->data + b->len, p, n);
+ b->len += n;
+}
+
+void buf_putc(Buf* b, char c) { buf_put(b, &c, 1); }
+
+void buf_puts(Buf* b, const char* s) { buf_put(b, s, strlen(s)); }
+
+void buf_printf(Buf* b, const char* fmt, ...) {
+ char tmp[1024];
+ va_list ap;
+ va_start(ap, fmt);
+ int n = vsnprintf(tmp, sizeof tmp, fmt, ap);
+ va_end(ap);
+ if (n < 0) return;
+ if ((size_t)n < sizeof tmp) {
+ buf_put(b, tmp, (size_t)n);
+ } else {
+ char* big = xmalloc((size_t)n + 1);
+ va_start(ap, fmt);
+ vsnprintf(big, (size_t)n + 1, fmt, ap);
+ va_end(ap);
+ buf_put(b, big, (size_t)n);
+ free(big);
+ }
+}
+
+void buf_free(Buf* b) {
+ free(b->data);
+ b->data = NULL;
+ b->len = b->cap = 0;
+}
+
+void list_add(List* l, void* p) {
+ if (l->len == l->cap) {
+ l->cap = l->cap ? l->cap * 2 : 16;
+ l->items = xrealloc(l->items, l->cap * sizeof(void*));
+ }
+ l->items[l->len++] = p;
+}
+
+void* list_get(List* l, size_t i) { return l->items[i]; }
+
+void list_free(List* l) {
+ free(l->items);
+ l->items = NULL;
+ l->len = l->cap = 0;
+}
+
+int read_file(const char* path, char** out, size_t* outlen) {
+ FILE* f = fopen(path, "rb");
+ if (!f) return -1;
+ if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
+ long sz = ftell(f);
+ if (sz < 0) { fclose(f); return -1; }
+ rewind(f);
+ char* buf = xmalloc((size_t)sz + 1);
+ if (sz > 0 && fread(buf, 1, (size_t)sz, f) != (size_t)sz) {
+ free(buf);
+ fclose(f);
+ return -1;
+ }
+ fclose(f);
+ buf[sz] = 0;
+ *out = buf;
+ if (outlen) *outlen = (size_t)sz;
+ return 0;
+}
+
+int write_file(const char* path, const char* data, size_t len) {
+ FILE* f = fopen(path, "wb");
+ if (!f) return -1;
+ if (len > 0 && fwrite(data, 1, len, f) != len) { fclose(f); return -1; }
+ fclose(f);
+ return 0;
+}
+
+void path_strip_ext(char* path) {
+ char* slash = strrchr(path, '/');
+ char* dot = strrchr(path, '.');
+ if (dot && (!slash || dot > slash)) *dot = 0;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..660f2df
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,42 @@
+#ifndef CR_UTIL_H
+#define CR_UTIL_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+void* xmalloc(size_t n);
+void* xrealloc(void* p, size_t n);
+char* xstrdup(const char* s);
+
+/* Growable byte buffer */
+typedef struct Buf {
+ char* data;
+ size_t len, cap;
+} Buf;
+
+void buf_put(Buf* b, const void* p, size_t n);
+void buf_putc(Buf* b, char c);
+void buf_puts(Buf* b, const char* s);
+void buf_printf(Buf* b, const char* fmt, ...);
+void buf_free(Buf* b);
+
+/* Growable pointer array (void*) */
+typedef struct List {
+ void** items;
+ size_t len, cap;
+} List;
+
+void list_add(List* l, void* p);
+void* list_get(List* l, size_t i);
+void list_free(List* l);
+
+/* Read an entire file. Returns 0 on success. NUL-terminates. */
+int read_file(const char* path, char** out, size_t* outlen);
+
+/* Write a buffer to a file. Returns 0 on success. */
+int write_file(const char* path, const char* data, size_t len);
+
+/* Remove the extension of a path (in place) */
+void path_strip_ext(char* path);
+
+#endif
diff --git a/tests/01_hello.cryn b/tests/01_hello.cryn
new file mode 100644
index 0000000..1a99309
--- /dev/null
+++ b/tests/01_hello.cryn
@@ -0,0 +1,14 @@
+[# the canonical first program from the language definition #]
+define main
+
+set 10
+push
+
+set 20
+add
+
+output
+
+return
+
+end
diff --git a/tests/02_format.cryn b/tests/02_format.cryn
new file mode 100644
index 0000000..25dd03f
--- /dev/null
+++ b/tests/02_format.cryn
@@ -0,0 +1,12 @@
+[# whitespace has no meaning: this program is formatted badly on purpose #]
+define
+main
+set
+10
+push
+set
+20
+add
+output
+return
+end
diff --git a/tests/03_arith.cryn b/tests/03_arith.cryn
new file mode 100644
index 0000000..adceaa6
--- /dev/null
+++ b/tests/03_arith.cryn
@@ -0,0 +1,34 @@
+[# integer arithmetic with precedence done by ordering #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ set 2
+ add 3
+ multiply 4
+ subtract 5
+ output
+ call newline
+
+ set 100
+ divide 7
+ output
+ call newline
+
+ set 100
+ remainder 7
+ output
+ call newline
+
+ set 42
+ negate
+ output
+ call newline
+ return
+end
diff --git a/tests/04_wrap.cryn b/tests/04_wrap.cryn
new file mode 100644
index 0000000..4fb1970
--- /dev/null
+++ b/tests/04_wrap.cryn
@@ -0,0 +1,30 @@
+[# overflow wraps in both directions #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ set 9223372036854775807
+ add 1
+ output
+ call newline
+
+ set 0
+ subtract 1
+ convert uint8
+ convert int64
+ output
+ call newline
+
+ set 250
+ convert uint8
+ add 10
+ convert int64
+ output
+ return
+end
diff --git a/tests/05_float.cryn b/tests/05_float.cryn
new file mode 100644
index 0000000..3486a65
--- /dev/null
+++ b/tests/05_float.cryn
@@ -0,0 +1,28 @@
+[# floating point #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ set 1.5
+ add 2.25
+ multiply 2
+ output
+ call newline
+
+ set 1
+ convert float32
+ divide 3
+ output
+ call newline
+
+ set 0.1
+ add 0.2
+ output
+ return
+end
diff --git a/tests/06_compare.cryn b/tests/06_compare.cryn
new file mode 100644
index 0000000..bfd62bb
--- /dev/null
+++ b/tests/06_compare.cryn
@@ -0,0 +1,64 @@
+[# every comparison condition, one character per outcome #]
+define main
+ set 10
+ compare 20
+ jump n1 when less
+ set 'N'
+ jump p1
+ mark n1
+ set 'Y'
+ mark p1
+ output
+
+ set 10
+ compare 10
+ jump n2 when equal
+ set 'N'
+ jump p2
+ mark n2
+ set 'Y'
+ mark p2
+ output
+
+ set 30
+ compare 20
+ jump n3 when greater
+ set 'N'
+ jump p3
+ mark n3
+ set 'Y'
+ mark p3
+ output
+
+ set 5
+ compare 5
+ jump y4 when different
+ set 'N'
+ jump p4
+ mark y4
+ set 'Y'
+ mark p4
+ output
+
+ set 5
+ compare 5
+ jump n5 when less-or-equal
+ set 'N'
+ jump p5
+ mark n5
+ set 'Y'
+ mark p5
+ output
+
+ set 5
+ compare 4
+ jump n6 when greater-or-equal
+ set 'N'
+ jump p6
+ mark n6
+ set 'Y'
+ mark p6
+ output
+
+ return
+end
diff --git a/tests/07_branch.cryn b/tests/07_branch.cryn
new file mode 100644
index 0000000..a3a9f87
--- /dev/null
+++ b/tests/07_branch.cryn
@@ -0,0 +1,40 @@
+[# conditional selection: max of two stored cells #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ make int64 a
+ make int64 b
+
+ set 17
+ move a
+ write
+ set 42
+ move b
+ write
+
+ move a
+ read
+ push
+ move b
+ read
+ compare
+ jump takeB when greater
+
+ move a
+ read
+ output
+ return
+
+ mark takeB
+ move b
+ read
+ output
+ return
+end
diff --git a/tests/08_loop.cryn b/tests/08_loop.cryn
new file mode 100644
index 0000000..1b4892b
--- /dev/null
+++ b/tests/08_loop.cryn
@@ -0,0 +1,44 @@
+[# loop: sum 1..100 #]
+define main
+ make int64 i
+ make int64 sum
+
+ set 0
+ move i
+ write
+ set 0
+ move sum
+ write
+ set 0
+
+ mark loop
+ move i
+ read
+ compare 101
+ jump done when greater-or-equal
+
+ move i
+ read
+ push
+ move sum
+ read
+ add
+ move sum
+ write
+ set 0
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+
+ jump loop
+ mark done
+
+ move sum
+ read
+ output
+ return
+end
diff --git a/tests/09_nested.cryn b/tests/09_nested.cryn
new file mode 100644
index 0000000..7e860ac
--- /dev/null
+++ b/tests/09_nested.cryn
@@ -0,0 +1,63 @@
+[# nested loops: count iterations of a 3x4 grid #]
+define main
+ make int64 i
+ make int64 j
+ make int64 count
+
+ set 0
+ move i
+ write
+ set 0
+ move j
+ write
+ set 0
+ move count
+ write
+ set 0
+
+ mark outer
+ move i
+ read
+ compare 3
+ jump endOuter when greater-or-equal
+ set 0
+ move j
+ write
+ set 0
+
+ mark inner
+ move j
+ read
+ compare 4
+ jump endInner when greater-or-equal
+
+ move count
+ read
+ add 1
+ move count
+ write
+ set 0
+
+ move j
+ read
+ add 1
+ move j
+ write
+ set 0
+ jump inner
+ mark endInner
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ jump outer
+ mark endOuter
+
+ move count
+ read
+ output
+ return
+end
diff --git a/tests/10_func.cryn b/tests/10_func.cryn
new file mode 100644
index 0000000..9719230
--- /dev/null
+++ b/tests/10_func.cryn
@@ -0,0 +1,41 @@
+[# functions with parameters: gcd #]
+define gcd int64 a int64 b result int64
+ mark loop
+ move b
+ read
+ compare 0
+ jump done when equal
+ move b
+ read
+ push
+ move b
+ read
+ push
+ move a
+ read
+ remainder
+ push
+ move b
+ write
+ pop
+ pop
+ move a
+ write
+ set nothing
+ home
+ jump loop
+ mark done
+ move a
+ read
+ return
+end
+
+define main
+ set 252
+ push
+ set 105
+ push
+ call gcd
+ output
+ return
+end
diff --git a/tests/11_recursion.cryn b/tests/11_recursion.cryn
new file mode 100644
index 0000000..db58f65
--- /dev/null
+++ b/tests/11_recursion.cryn
@@ -0,0 +1,56 @@
+[# recursion: factorial(10) and fib(20) #]
+define fact int64 n result int64
+ move n
+ read
+ compare 1
+ jump base when less-or-equal
+ move n
+ read
+ push
+ move n
+ read
+ subtract 1
+ push
+ call fact
+ multiply
+ return
+ mark base
+ set 1
+ return
+end
+
+define fib int64 n result int64
+ move n
+ read
+ compare 2
+ jump base when less
+ move n
+ read
+ subtract 1
+ push
+ call fib
+ push
+ move n
+ read
+ subtract 2
+ push
+ call fib
+ add
+ return
+ mark base
+ set 1
+ return
+end
+
+define main
+ set 10
+ push
+ call fact
+ output
+
+ set 20
+ push
+ call fib
+ output
+ return
+end
diff --git a/tests/12_mutual.cryn b/tests/12_mutual.cryn
new file mode 100644
index 0000000..74fffa2
--- /dev/null
+++ b/tests/12_mutual.cryn
@@ -0,0 +1,48 @@
+[# mutual recursion #]
+define is_even int64 n result bool
+ move n
+ read
+ compare 0
+ jump yes when equal
+ move n
+ read
+ subtract 1
+ push
+ call is_odd
+ return
+ mark yes
+ set true
+ return
+end
+
+define is_odd int64 n result bool
+ move n
+ read
+ compare 0
+ jump yes when equal
+ move n
+ read
+ subtract 1
+ push
+ call is_even
+ return
+ mark yes
+ set false
+ return
+end
+
+define main
+ set 10
+ push
+ call is_even
+ output
+ set 7
+ push
+ call is_odd
+ output
+ set 7
+ push
+ call is_even
+ output
+ return
+end
diff --git a/tests/13_stack.cryn b/tests/13_stack.cryn
new file mode 100644
index 0000000..d1bc35d
--- /dev/null
+++ b/tests/13_stack.cryn
@@ -0,0 +1,21 @@
+[# value stack: push, pop, swap #]
+define main
+ set 2
+ push
+ set 3
+ push
+
+ pop
+ output
+ pop
+ output
+
+ set 9
+ push
+ set 4
+ swap
+ output
+ pop
+ output
+ return
+end
diff --git a/tests/14_cells.cryn b/tests/14_cells.cryn
new file mode 100644
index 0000000..ca1af89
--- /dev/null
+++ b/tests/14_cells.cryn
@@ -0,0 +1,41 @@
+[# cells, connections, focus navigation: back and home #]
+define main
+ make int64 x
+ make int64 y
+
+ set 11
+ move x
+ write
+ set 22
+ move y
+ write
+
+ [# focus is on y; move to x needs no back because named moves anchor #]
+ move x
+ read
+ output
+
+ [# indexed navigation uses the focus stack: back undoes it #]
+ make array 2 of int64 pair
+ set 7
+ move pair
+ move 0
+ write
+ back
+ set 8
+ move 1
+ write
+ back
+
+ move pair
+ move 1
+ read
+ output
+ back
+ home
+
+ move y
+ read
+ output
+ return
+end
diff --git a/tests/15_arrays.cryn b/tests/15_arrays.cryn
new file mode 100644
index 0000000..0c30599
--- /dev/null
+++ b/tests/15_arrays.cryn
@@ -0,0 +1,97 @@
+[# arrays as indexed connections: sieve of Eratosthenes under 50 #]
+define main
+ make array 50 of int64 sieve
+ make int64 i
+ make int64 j
+ make int64 count
+
+ set 2
+ move i
+ write
+ set 0
+ move count
+ write
+ set 0
+ home
+
+ mark outer
+ move i
+ read
+ compare 50
+ jump endOuter when greater-or-equal
+ set 0
+ home
+
+ move i
+ read
+ move sieve
+ move index
+ read
+ home
+ compare 0
+ jump skip when different
+ set 0
+ home
+
+ move count
+ read
+ add 1
+ move count
+ write
+ set 0
+ home
+
+ move i
+ read
+ multiply 2
+ move j
+ write
+ set 0
+ home
+
+ mark inner
+ move j
+ read
+ home
+ compare 50
+ jump outerInc when greater-or-equal
+ set 0
+ home
+
+ move j
+ read
+ move sieve
+ move index
+ set 1
+ write
+ home
+
+ move j
+ read
+ push
+ move i
+ read
+ add
+ move j
+ write
+ set 0
+ home
+ jump inner
+
+ mark skip
+ mark outerInc
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ home
+ jump outer
+
+ mark endOuter
+ move count
+ read
+ output
+ return
+end
diff --git a/tests/16_strings.cryn b/tests/16_strings.cryn
new file mode 100644
index 0000000..10d6669
--- /dev/null
+++ b/tests/16_strings.cryn
@@ -0,0 +1,73 @@
+[# strings are byte arrays: print text, measure length, reverse #]
+make string "interrupt" word
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ address word
+ output text
+ call newline
+
+ make int64 n
+ set 0
+ move n
+ write
+
+ mark count
+ move n
+ read
+ move word
+ move index
+ read
+ compare 0
+ jump counted when equal
+ move n
+ read
+ add 1
+ move n
+ write
+ set 0
+ jump count
+ mark counted
+
+ move n
+ read
+ output
+ call newline
+
+ make int64 k
+ move n
+ read
+ subtract 1
+ move k
+ write
+ set 0
+
+ mark reverse
+ move k
+ read
+ compare 0
+ jump done when less
+ move k
+ read
+ move word
+ move index
+ read
+ output
+ move k
+ read
+ subtract 1
+ move k
+ write
+ set 0
+ jump reverse
+ mark done
+ call newline
+ return
+end
diff --git a/tests/17_memory.cryn b/tests/17_memory.cryn
new file mode 100644
index 0000000..b400d52
--- /dev/null
+++ b/tests/17_memory.cryn
@@ -0,0 +1,72 @@
+[# manual memory: make a block, fill it, sum it, free it #]
+define main
+ make 4 of int64 block
+
+ move at
+ set 40
+ move 0
+ write
+ back
+ set 41
+ move 1
+ write
+ back
+ set 42
+ move 2
+ write
+ back
+ set 43
+ move 3
+ write
+ back
+
+ make int64 i
+ make int64 sum
+ set 3
+ move i
+ write
+ set 0
+ home
+
+ mark sum
+ move i
+ read
+ compare 0
+ jump done when less
+ set 0
+ home
+
+ move i
+ read
+ push
+ move block
+ read
+ move at
+ pop
+ move index
+ read
+ push
+ move sum
+ read
+ add
+ move sum
+ write
+ move i
+ read
+ subtract 1
+ move i
+ write
+ set 0
+ home
+ jump sum
+
+ mark done
+ move sum
+ read
+ output
+
+ move block
+ read
+ free
+ return
+end
diff --git a/tests/18_pointers.cryn b/tests/18_pointers.cryn
new file mode 100644
index 0000000..d848a7a
--- /dev/null
+++ b/tests/18_pointers.cryn
@@ -0,0 +1,69 @@
+[# addresses: walking a heap block backwards with the push-index idiom #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ make 3 of int64 block
+
+ move at
+ set 5
+ move 0
+ write
+ back
+ set 6
+ move 1
+ write
+ back
+ set 7
+ move 2
+ write
+ back
+
+ make int64 i
+ set 2
+ move i
+ write
+ set 0
+ home
+
+ mark walk
+ move i
+ read
+ compare 0
+ jump done when less
+ set 0
+ home
+
+ move i
+ read
+ push
+ move block
+ read
+ move at
+ pop
+ move index
+ read
+ output
+ call newline
+
+ move i
+ read
+ subtract 1
+ move i
+ write
+ set 0
+ home
+ jump walk
+
+ mark done
+ move block
+ read
+ free
+ return
+end
diff --git a/tests/19_linkedlist.cryn b/tests/19_linkedlist.cryn
new file mode 100644
index 0000000..57d319e
--- /dev/null
+++ b/tests/19_linkedlist.cryn
@@ -0,0 +1,123 @@
+[# a linked list built from raw blocks: node = [value next] #]
+make string "\n" nl
+make int64 total
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define main
+ make 2 of int64 n1
+ make 2 of int64 n2
+ make 2 of int64 n3
+ make 2 of int64 cur
+
+ move n3
+ read
+ move at
+ set 30
+ move 0
+ write
+ back
+ set 0
+ move 1
+ write
+ back
+
+ move n3
+ read
+ convert int64
+ push
+ move n2
+ read
+ move at
+ set 20
+ move 0
+ write
+ back
+ move 1
+ pop
+ write
+ back
+
+ move n2
+ read
+ convert int64
+ push
+ move n1
+ read
+ move at
+ set 10
+ move 0
+ write
+ back
+ move 1
+ pop
+ write
+ back
+
+ set 0
+ move total
+ write
+
+ move n1
+ read
+ push
+ move cur
+ write
+ set 0
+ move total
+ write
+ pop
+ home
+
+ mark walk
+ move cur
+ read
+ compare 0
+ jump done when equal
+
+ move cur
+ read
+ move at
+ move 0
+ read
+ output
+ call newline
+ push
+ move total
+ read
+ add
+ move total
+ write
+
+ move cur
+ read
+ move at
+ move 1
+ read
+ convert address array of int64
+ move cur
+ write
+ home
+ jump walk
+
+ mark done
+ move total
+ read
+ output
+
+ move n1
+ read
+ free
+ move n2
+ read
+ free
+ move n3
+ read
+ free
+ return
+end
diff --git a/tests/20_files.cryn b/tests/20_files.cryn
new file mode 100644
index 0000000..ab18000
--- /dev/null
+++ b/tests/20_files.cryn
@@ -0,0 +1,68 @@
+[# file output and input round trip #]
+define main
+ make string "cryn_test_20.txt" path
+
+ address path
+ open write
+ compare 0
+ jump fail when less
+ push
+
+ make string "cryn was here\n" data
+ make int64 i
+ set 0
+ move i
+ write
+ set 0
+
+ mark wr
+ move i
+ read
+ compare 15
+ jump written when greater-or-equal
+ move i
+ read
+ move data
+ move index
+ read
+ output
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ jump wr
+
+ mark written
+ pop
+ close
+ home
+ compare 0
+ jump fail when less
+
+ address path
+ open read
+ home
+ compare 0
+ jump fail when less
+ push
+
+ set 0
+ mark rd
+ input
+ compare -1
+ jump readDone when equal
+ convert uint8
+ output
+ jump rd
+ mark readDone
+
+ pop
+ close
+ stop
+
+ mark fail
+ set 1
+ stop
+end
diff --git a/tests/21_threads.cryn b/tests/21_threads.cryn
new file mode 100644
index 0000000..5f7757f
--- /dev/null
+++ b/tests/21_threads.cryn
@@ -0,0 +1,39 @@
+[# spawn and wait: two contexts incrementing a shared world cell #]
+make int64 counter
+
+define worker
+ make int64 k
+ set 0
+ move k
+ write
+ mark w
+ move k
+ read
+ compare 100
+ jump done when greater-or-equal
+ move counter
+ read
+ add 1
+ move counter
+ write
+ set 0
+ move k
+ read
+ add 1
+ move k
+ write
+ set 0
+ jump w
+ mark done
+ return
+end
+
+define main
+ spawn worker
+ spawn worker
+ wait
+ move counter
+ read
+ output
+ return
+end
diff --git a/tests/22_callbacks.cryn b/tests/22_callbacks.cryn
new file mode 100644
index 0000000..fc1cc48
--- /dev/null
+++ b/tests/22_callbacks.cryn
@@ -0,0 +1,52 @@
+[# transform references: apply two different strategies through one caller #]
+make int64 log
+
+define ping result nothing
+ move log
+ read
+ add 1
+ move log
+ write
+ set nothing
+ return
+end
+
+define pong result nothing
+ move log
+ read
+ add 10
+ move log
+ write
+ set nothing
+ return
+end
+
+define twice transform f result nothing
+ move f
+ read
+ push
+ call
+ pop
+ call
+ set nothing
+ return
+end
+
+define main
+ set 0
+ move log
+ write
+
+ address ping
+ push
+ call twice
+
+ address pong
+ push
+ call twice
+
+ move log
+ read
+ output
+ return
+end
diff --git a/tests/23_sort.cryn b/tests/23_sort.cryn
new file mode 100644
index 0000000..12ff43f
--- /dev/null
+++ b/tests/23_sort.cryn
@@ -0,0 +1,287 @@
+[# bubble sort over a heap block of int64, using element helpers #]
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define getelem address array of int64 blk int64 idx result int64
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ read
+ return
+end
+
+define setelem address array of int64 blk int64 idx int64 val result nothing
+ move val
+ read
+ push
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ pop
+ write
+ set nothing
+ return
+end
+
+define load address array of int64 blk int64 idx int64 val result nothing
+ move blk
+ read
+ push
+ move idx
+ read
+ push
+ move val
+ read
+ push
+ call setelem
+ set nothing
+ return
+end
+
+define put int64 v result nothing
+ move v
+ read
+ output
+ call newline
+ set nothing
+ return
+end
+
+define main
+ make 8 of int64 a
+ make int64 i
+ make int64 j
+ make int64 tmp
+
+ move a
+ read
+ push
+ set 0
+ push
+ set 42
+ push
+ call load
+
+ move a
+ read
+ push
+ set 1
+ push
+ set 17
+ push
+ call load
+
+ move a
+ read
+ push
+ set 2
+ push
+ set 99
+ push
+ call load
+
+ move a
+ read
+ push
+ set 3
+ push
+ set 3
+ push
+ call load
+
+ move a
+ read
+ push
+ set 4
+ push
+ set 58
+ push
+ call load
+
+ move a
+ read
+ push
+ set 5
+ push
+ set 21
+ push
+ call load
+
+ move a
+ read
+ push
+ set 6
+ push
+ set 7
+ push
+ call load
+
+ move a
+ read
+ push
+ set 7
+ push
+ set 65
+ push
+ call load
+
+ set 7
+ move i
+ write
+ set 0
+ home
+
+ mark outer
+ move i
+ read
+ compare 0
+ jump sorted when less
+ set 0
+ home
+
+ set 0
+ move j
+ write
+ set 0
+ home
+
+ mark inner
+ move i
+ read
+ push
+ move j
+ read
+ compare
+ jump nextPass when greater-or-equal
+ set 0
+ home
+
+ move a
+ read
+ push
+ move j
+ read
+ push
+ call getelem
+ move tmp
+ write
+
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ call getelem
+ push
+ move tmp
+ read
+ compare
+ jump noSwap when less-or-equal
+
+ move a
+ read
+ push
+ move j
+ read
+ push
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ call getelem
+ push
+ call setelem
+
+ move a
+ read
+ push
+ move j
+ read
+ add 1
+ push
+ move tmp
+ read
+ push
+ call setelem
+
+ mark noSwap
+ move j
+ read
+ add 1
+ move j
+ write
+ set 0
+ home
+ jump inner
+
+ mark nextPass
+ move i
+ read
+ subtract 1
+ move i
+ write
+ set 0
+ home
+ jump outer
+
+ mark sorted
+ set 0
+ move i
+ write
+ set 0
+ home
+
+ mark print
+ move i
+ read
+ compare 8
+ jump done when greater-or-equal
+ set 0
+ home
+
+ move a
+ read
+ push
+ move i
+ read
+ push
+ call getelem
+ push
+ call put
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ home
+ jump print
+
+ mark done
+ move a
+ read
+ free
+ return
+end
diff --git a/tests/24_wordcount.cryn b/tests/24_wordcount.cryn
new file mode 100644
index 0000000..232aa0e
--- /dev/null
+++ b/tests/24_wordcount.cryn
@@ -0,0 +1,128 @@
+[# word counter: reads stdin, prints "lines words chars" #]
+make int64 lines
+make int64 words
+make int64 chars
+make int64 inword
+make string "\n" nl
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define bump address int64 cell result nothing
+ move cell
+ read
+ move at
+ read
+ add 1
+ write
+ set nothing
+ return
+end
+
+define doline result nothing
+ address lines
+ push
+ call bump
+ set nothing
+ return
+end
+
+define startword result nothing
+ set 1
+ move inword
+ write
+ address words
+ push
+ call bump
+ set nothing
+ return
+end
+
+define put int64 v result nothing
+ move v
+ read
+ output
+ set ' '
+ output
+ set nothing
+ return
+end
+
+define main
+ set 0
+ move lines
+ write
+ set 0
+ move words
+ write
+ set 0
+ move chars
+ write
+ set 0
+ move inword
+ write
+ set 0
+ home
+
+ mark next
+ input
+ compare -1
+ jump done when equal
+ push
+
+ address chars
+ push
+ call bump
+
+ pop
+ compare 10
+ jump newline when equal
+ compare 32
+ jump space when equal
+ compare 9
+ jump space when equal
+ jump word
+
+ mark newline
+ call doline
+
+ mark space
+ set 0
+ move inword
+ write
+ set 0
+ home
+ jump next
+
+ mark word
+ move inword
+ read
+ compare 0
+ jump cont when different
+ call startword
+
+ mark cont
+ set 0
+ home
+ jump next
+
+ mark done
+ move lines
+ read
+ push
+ call put
+ move words
+ read
+ push
+ call put
+ move chars
+ read
+ push
+ call put
+ call newline
+ return
+end
diff --git a/tests/25_allocator.cryn b/tests/25_allocator.cryn
new file mode 100644
index 0000000..8a999bb
--- /dev/null
+++ b/tests/25_allocator.cryn
@@ -0,0 +1,182 @@
+[# a small fixed-slot free-list allocator built on a raw block #]
+make string "\n" nl
+make int64 head
+
+define newline result nothing
+ address nl
+ output text
+ set nothing
+ return
+end
+
+define getslot address array of int64 blk int64 idx result int64
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ read
+ return
+end
+
+define setslot address array of int64 blk int64 idx int64 val result nothing
+ move val
+ read
+ push
+ move idx
+ read
+ push
+ move blk
+ read
+ move at
+ pop
+ move index
+ pop
+ write
+ set nothing
+ return
+end
+
+define alloc address array of int64 blk result int64
+ move head
+ read
+ push
+ move blk
+ read
+ push
+ move head
+ read
+ push
+ call getslot
+ move head
+ write
+ pop
+ return
+end
+
+define dealloc address array of int64 blk int64 slot result nothing
+ move blk
+ read
+ push
+ move slot
+ read
+ push
+ move head
+ read
+ push
+ call setslot
+ move slot
+ read
+ move head
+ write
+ set nothing
+ return
+end
+
+define putval int64 v result nothing
+ move v
+ read
+ output
+ call newline
+ set nothing
+ return
+end
+
+define main
+ make 17 of int64 pool
+ make int64 i
+
+ set 1
+ move i
+ write
+ set 0
+ home
+
+ mark build
+ move i
+ read
+ compare 16
+ jump capped when greater-or-equal
+ set 0
+ home
+
+ move pool
+ read
+ push
+ move i
+ read
+ push
+ move i
+ read
+ add 1
+ push
+ call setslot
+
+ move i
+ read
+ add 1
+ move i
+ write
+ set 0
+ home
+ jump build
+
+ mark capped
+ move pool
+ read
+ push
+ set 16
+ push
+ set 0
+ push
+ call setslot
+
+ set 1
+ move head
+ write
+ set 0
+ home
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ push
+ set 2
+ push
+ call dealloc
+
+ move pool
+ read
+ push
+ call alloc
+ push
+ call putval
+
+ move pool
+ read
+ free
+ return
+end
diff --git a/tests/errors/bad_jump.cryn b/tests/errors/bad_jump.cryn
new file mode 100644
index 0000000..8459221
--- /dev/null
+++ b/tests/errors/bad_jump.cryn
@@ -0,0 +1,4 @@
+define main
+ jump nowhere
+ return
+end
diff --git a/tests/errors/cond_jump_without_compare.cryn b/tests/errors/cond_jump_without_compare.cryn
new file mode 100644
index 0000000..9e110fc
--- /dev/null
+++ b/tests/errors/cond_jump_without_compare.cryn
@@ -0,0 +1,5 @@
+define main
+ jump end when equal
+ mark end
+ return
+end
diff --git a/tests/errors/div_by_zero_const.cryn b/tests/errors/div_by_zero_const.cryn
new file mode 100644
index 0000000..a0e03cf
--- /dev/null
+++ b/tests/errors/div_by_zero_const.cryn
@@ -0,0 +1,5 @@
+define main
+ set 1
+ divide 0
+ return
+end
diff --git a/tests/errors/free_non_address.cryn b/tests/errors/free_non_address.cryn
new file mode 100644
index 0000000..f5853c6
--- /dev/null
+++ b/tests/errors/free_non_address.cryn
@@ -0,0 +1,5 @@
+define main
+ set 5
+ free
+ return
+end
diff --git a/tests/errors/no_main.cryn b/tests/errors/no_main.cryn
new file mode 100644
index 0000000..3ad531e
--- /dev/null
+++ b/tests/errors/no_main.cryn
@@ -0,0 +1,3 @@
+define notmain
+ return
+end
diff --git a/tests/errors/stack_underflow.cryn b/tests/errors/stack_underflow.cryn
new file mode 100644
index 0000000..a8a4a3d
--- /dev/null
+++ b/tests/errors/stack_underflow.cryn
@@ -0,0 +1,4 @@
+define main
+ pop
+ return
+end
diff --git a/tests/errors/type_mismatch.cryn b/tests/errors/type_mismatch.cryn
new file mode 100644
index 0000000..f51def3
--- /dev/null
+++ b/tests/errors/type_mismatch.cryn
@@ -0,0 +1,8 @@
+define main
+ make int64 x
+ make string "t" s
+ address s
+ move x
+ write
+ return
+end
diff --git a/tests/errors/undefined_cell.cryn b/tests/errors/undefined_cell.cryn
new file mode 100644
index 0000000..f41b675
--- /dev/null
+++ b/tests/errors/undefined_cell.cryn
@@ -0,0 +1,5 @@
+define main
+ move nowhere
+ read
+ return
+end
diff --git a/tests/errors/unterminated_comment.cryn b/tests/errors/unterminated_comment.cryn
new file mode 100644
index 0000000..38ed1b3
--- /dev/null
+++ b/tests/errors/unterminated_comment.cryn
@@ -0,0 +1,4 @@
+[# this comment never ends
+define main
+ return
+end
diff --git a/tests/errors/write_nothing.cryn b/tests/errors/write_nothing.cryn
new file mode 100644
index 0000000..898bc85
--- /dev/null
+++ b/tests/errors/write_nothing.cryn
@@ -0,0 +1,6 @@
+define main
+ make int64 x
+ move x
+ write
+ return
+end
diff --git a/tests/expected/01_hello.txt b/tests/expected/01_hello.txt
new file mode 100644
index 0000000..8580e7b
--- /dev/null
+++ b/tests/expected/01_hello.txt
@@ -0,0 +1 @@
+30
\ No newline at end of file
diff --git a/tests/expected/02_format.txt b/tests/expected/02_format.txt
new file mode 100644
index 0000000..8580e7b
--- /dev/null
+++ b/tests/expected/02_format.txt
@@ -0,0 +1 @@
+30
\ No newline at end of file
diff --git a/tests/expected/03_arith.txt b/tests/expected/03_arith.txt
new file mode 100644
index 0000000..53d6967
--- /dev/null
+++ b/tests/expected/03_arith.txt
@@ -0,0 +1,4 @@
+15
+14
+2
+-42
\ No newline at end of file
diff --git a/tests/expected/04_wrap.txt b/tests/expected/04_wrap.txt
new file mode 100644
index 0000000..7460a8c
--- /dev/null
+++ b/tests/expected/04_wrap.txt
@@ -0,0 +1,3 @@
+-9223372036854775808
+255
+4
\ No newline at end of file
diff --git a/tests/expected/05_float.txt b/tests/expected/05_float.txt
new file mode 100644
index 0000000..368bbbb
--- /dev/null
+++ b/tests/expected/05_float.txt
@@ -0,0 +1,3 @@
+7.5
+0.33333334
+0.30000000000000004
\ No newline at end of file
diff --git a/tests/expected/06_compare.txt b/tests/expected/06_compare.txt
new file mode 100644
index 0000000..5d1866e
--- /dev/null
+++ b/tests/expected/06_compare.txt
@@ -0,0 +1 @@
+YYYNYY
\ No newline at end of file
diff --git a/tests/expected/07_branch.txt b/tests/expected/07_branch.txt
new file mode 100644
index 0000000..f70d7bb
--- /dev/null
+++ b/tests/expected/07_branch.txt
@@ -0,0 +1 @@
+42
\ No newline at end of file
diff --git a/tests/expected/08_loop.txt b/tests/expected/08_loop.txt
new file mode 100644
index 0000000..c9515ef
--- /dev/null
+++ b/tests/expected/08_loop.txt
@@ -0,0 +1 @@
+5050
\ No newline at end of file
diff --git a/tests/expected/09_nested.txt b/tests/expected/09_nested.txt
new file mode 100644
index 0000000..3cacc0b
--- /dev/null
+++ b/tests/expected/09_nested.txt
@@ -0,0 +1 @@
+12
\ No newline at end of file
diff --git a/tests/expected/10_func.txt b/tests/expected/10_func.txt
new file mode 100644
index 0000000..b5045cc
--- /dev/null
+++ b/tests/expected/10_func.txt
@@ -0,0 +1 @@
+21
\ No newline at end of file
diff --git a/tests/expected/11_recursion.txt b/tests/expected/11_recursion.txt
new file mode 100644
index 0000000..1f8ad37
--- /dev/null
+++ b/tests/expected/11_recursion.txt
@@ -0,0 +1 @@
+362880010946
\ No newline at end of file
diff --git a/tests/expected/12_mutual.txt b/tests/expected/12_mutual.txt
new file mode 100644
index 0000000..962d20a
--- /dev/null
+++ b/tests/expected/12_mutual.txt
@@ -0,0 +1 @@
+truetruefalse
\ No newline at end of file
diff --git a/tests/expected/13_stack.txt b/tests/expected/13_stack.txt
new file mode 100644
index 0000000..bc2fb86
--- /dev/null
+++ b/tests/expected/13_stack.txt
@@ -0,0 +1 @@
+3294
\ No newline at end of file
diff --git a/tests/expected/14_cells.txt b/tests/expected/14_cells.txt
new file mode 100644
index 0000000..c27e76c
--- /dev/null
+++ b/tests/expected/14_cells.txt
@@ -0,0 +1 @@
+11822
\ No newline at end of file
diff --git a/tests/expected/15_arrays.txt b/tests/expected/15_arrays.txt
new file mode 100644
index 0000000..3f10ffe
--- /dev/null
+++ b/tests/expected/15_arrays.txt
@@ -0,0 +1 @@
+15
\ No newline at end of file
diff --git a/tests/expected/16_strings.txt b/tests/expected/16_strings.txt
new file mode 100644
index 0000000..a58cac7
--- /dev/null
+++ b/tests/expected/16_strings.txt
@@ -0,0 +1,3 @@
+interrupt
+9
+tpurretni
\ No newline at end of file
diff --git a/tests/expected/17_memory.txt b/tests/expected/17_memory.txt
new file mode 100644
index 0000000..09df927
--- /dev/null
+++ b/tests/expected/17_memory.txt
@@ -0,0 +1 @@
+166
\ No newline at end of file
diff --git a/tests/expected/18_pointers.txt b/tests/expected/18_pointers.txt
new file mode 100644
index 0000000..128939d
--- /dev/null
+++ b/tests/expected/18_pointers.txt
@@ -0,0 +1,3 @@
+7
+6
+5
\ No newline at end of file
diff --git a/tests/expected/19_linkedlist.txt b/tests/expected/19_linkedlist.txt
new file mode 100644
index 0000000..1c6b766
--- /dev/null
+++ b/tests/expected/19_linkedlist.txt
@@ -0,0 +1,4 @@
+10
+20
+30
+60
\ No newline at end of file
diff --git a/tests/expected/20_files.txt b/tests/expected/20_files.txt
new file mode 100644
index 0000000..c864352
--- /dev/null
+++ b/tests/expected/20_files.txt
@@ -0,0 +1 @@
+cryn was here
\ No newline at end of file
diff --git a/tests/expected/21_threads.txt b/tests/expected/21_threads.txt
new file mode 100644
index 0000000..ae4ee13
--- /dev/null
+++ b/tests/expected/21_threads.txt
@@ -0,0 +1 @@
+200
\ No newline at end of file
diff --git a/tests/expected/22_callbacks.txt b/tests/expected/22_callbacks.txt
new file mode 100644
index 0000000..8fdd954
--- /dev/null
+++ b/tests/expected/22_callbacks.txt
@@ -0,0 +1 @@
+22
\ No newline at end of file
diff --git a/tests/expected/23_sort.txt b/tests/expected/23_sort.txt
new file mode 100644
index 0000000..847232d
--- /dev/null
+++ b/tests/expected/23_sort.txt
@@ -0,0 +1,8 @@
+3
+7
+17
+21
+42
+58
+65
+99
\ No newline at end of file
diff --git a/tests/expected/24_wordcount.txt b/tests/expected/24_wordcount.txt
new file mode 100644
index 0000000..4e2f097
--- /dev/null
+++ b/tests/expected/24_wordcount.txt
@@ -0,0 +1 @@
+4 9 45
\ No newline at end of file
diff --git a/tests/expected/25_allocator.txt b/tests/expected/25_allocator.txt
new file mode 100644
index 0000000..fa2fa9f
--- /dev/null
+++ b/tests/expected/25_allocator.txt
@@ -0,0 +1,4 @@
+1
+2
+3
+2
\ No newline at end of file
diff --git a/tests/input/24_wordcount.txt b/tests/input/24_wordcount.txt
new file mode 100644
index 0000000..87b60a0
--- /dev/null
+++ b/tests/input/24_wordcount.txt
@@ -0,0 +1,4 @@
+the quick brown fox
+jumps over
+
+the lazy dog
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100644
index 0000000..d51c3e4
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+# Cryn test suite: compile and run each tests/*.cryn and compare with
+# tests/expected/<name>.txt. Error tests in tests/errors must fail to compile.
+set -u
+cd "$(dirname "$0")"
+CRYN=../cryn
+pass=0
+fail=0
+
+for src in *.cryn; do
+ name=$(basename "$src" .cryn)
+ exp="expected/$name.txt"
+ [ -f "$exp" ] || continue
+ if [ -f "input/$name.txt" ]; then
+ out=$($CRYN run "$src" < "input/$name.txt" 2>/dev/null)
+ else
+ out=$($CRYN run "$src" 2>/dev/null)
+ fi
+ if [ "$out" = "$(cat "$exp")" ]; then
+ pass=$((pass + 1))
+ else
+ fail=$((fail + 1))
+ echo "FAIL: $name"
+ echo " expected: $(tr '\n' '|' < "$exp")"
+ echo " got: $(printf '%s' "$out" | tr '\n' '|')"
+ fi
+done
+
+for src in errors/*.cryn; do
+ name=$(basename "$src" .cryn)
+ if $CRYN check "$src" >/dev/null 2>&1; then
+ fail=$((fail + 1))
+ echo "FAIL (should not compile): $name"
+ else
+ pass=$((pass + 1))
+ fi
+done
+
+echo "$pass passed, $fail failed"
+[ "$fail" -eq 0 ]
diff --git a/tools/embed.c b/tools/embed.c
new file mode 100644
index 0000000..088e1c1
--- /dev/null
+++ b/tools/embed.c
@@ -0,0 +1,58 @@
+/* tools/embed.c - turn runtime sources into C string headers for the compiler.
+ Lines of the form '#include "..."' are dropped (their content is inlined
+ around the string); system includes with <...> are kept. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void emit_escaped(const char* s, size_t n) {
+ for (size_t i = 0; i < n; i++) {
+ unsigned char c = (unsigned char)s[i];
+ switch (c) {
+ case '\\': printf("\\\\"); break;
+ case '"': printf("\\\""); break;
+ case '\t': printf("\\t"); break;
+ case '\r': break;
+ default:
+ if (c < 32 || c > 126) printf("\\%03o", c);
+ else putchar(c);
+ }
+ }
+}
+
+static void emit_string(const char* name, const char* path) {
+ FILE* f = fopen(path, "rb");
+ if (!f) {
+ fprintf(stderr, "embed: cannot open %s\n", path);
+ exit(1);
+ }
+ printf("static const char %s[] =\n", name);
+ char line[1024];
+ int first = 1;
+ while (fgets(line, sizeof line, f)) {
+ if (strncmp(line, "#include \"", 10) == 0) continue; /* local include: dropped */
+ size_t n = strlen(line);
+ /* strip trailing newline */
+ while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) n--;
+ if (!first) printf("\n");
+ printf("\"");
+ emit_escaped(line, n);
+ printf("\\n\"");
+ first = 0;
+ }
+ printf(";\n\n");
+ fclose(f);
+}
+
+int main(int argc, char** argv) {
+ if (argc < 3 || (argc - 1) % 2 != 0) {
+ fprintf(stderr, "usage: embed <fil
[Diff truncated. Use the raw view or local clone for the full content.]