diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2ed228..6e56570 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,24 +57,25 @@ jobs: fi done - - name: Check self-hosted lexer (compiler) + - name: Native compiler package (yard) run: | - echo "Checking compiler package (yard)..." + scripts/embed_runtime.sh --check (cd compiler && ../target/release/yard check) - echo "Building compiler..." (cd compiler && ../target/release/yard build) - echo "Testing compiler (native lexer)..." (cd compiler && ../target/release/yard test) - echo "Checking compiler sources via track..." for f in compiler/src/*.trk; do echo "Checking $f..." ./target/release/track check "$f" done + - name: Bootstrap fixed point (stage0 -> stage3) + run: YARD=./target/release/yard scripts/bootstrap.sh + - name: Verify versions run: | - ./target/release/track --version | grep -q "0.7.0" - ./target/release/yard --version | grep -q "0.7.0" + ./target/release/track --version | grep -q "1.0.0" + ./target/release/yard --version | grep -q "1.0.0" + ./build/bootstrap/stage3/trackc --version | grep -q "1.0.0" echo "✓ versions ok" diff --git a/.gitignore b/.gitignore index 839b47f..37d0a74 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ scratch/benchmarks/branch scratch/benchmarks/tuple scratch/benchmarks/loop scratch/benchmarks/rec +/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b48d7..7de8121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,78 @@ All notable changes to the Track programming language and toolchain will be docu The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0] — 2026-10-30 -### Fixed -- Yard preserves source-relative paths under `target/objects/`, preventing nested modules with the same basename from overwriting each other or the runtime object. -- Parallel build failures, check diagnostics, dependency resolution, and build-cache serialization now use stable ordering. Cache-save errors are propagated. +Track is self-hosting. `trackc`, the compiler in `compiler/`, is written +entirely in Track and reaches a bootstrap fixed point: the Rust-built and +Track-built compilers emit byte-identical C, and the stage 2 and stage 3 +executables are byte-identical (`scripts/bootstrap.sh`, gated in CI). ### Added -- Build-determinism regression coverage for duplicate basenames, runtime-name collisions, incremental reuse, clean-build metadata, and error ordering. +- **Self-hosted compiler** (`compiler/src/`): lexer, full-grammar parser, + whole-program module loader, ownership checker, and a portable C99 backend. + CLI: `trackc build | run | check | emit-c | tokens | --version`. Lexer, + parser, and checker errors print `error[Exxxx]` diagnostics with a source + frame. See [docs/bootstrap.md](docs/bootstrap.md). +- **Shared runtime** `runtime/track_runtime.c`: one word-ABI implementation + linked by both compilers (embedded into `trackc` by + `scripts/embed_runtime.sh`). New: string builder, whole-file read/write, + unformatted output, collection pop/truncate, `track_str_bytes`. +- `scripts/bootstrap.sh` (stage 0 → 3 with the fixed-point gate) and + `tests/selfhost_tests.rs`: fixed point, behavioural parity on every + example, identical accept/reject verdicts from both checkers, and the + native suite run under `trackc`. +- `tests/codegen_semantics_tests.rs`: run-and-compare regressions for the + backend fixes below. + +- **Structs and arrays at run time** (both compilers): struct literals, + `value.field` reads, `p.x = v` / `t.0 = v` / `a[i] = v` updates (root must + be `let mut` or a lens alias), struct patterns that match by field, + bounds-checked array indexing, `a[s..e]` / `a[s..]` slices, and + `array_len`. Previously these parsed but evaluated to `0`. Unknown struct + fields are compile errors. `examples/structs_arrays.trk` demonstrates them. + +### Fixed (Rust bootstrap compiler) +- **Word ABI in the Cranelift backend.** Values mixed I8/I32/I64 and calls to + functions in other modules were declared with guessed signatures, so a + `bool` returned across modules could carry garbage in its upper bits and + C `int` results lost their sign (`file_remove` returning `-1` read as + `4294967295`). Every value is now a 64-bit word. +- `os_args_count()`/`os_arg()` never saw the program arguments. +- `&&`/`||` were bitwise (`1 && 2` was false) and did not short-circuit. +- `return` inside a `match` arm, directly in a `while` body, or in nested + `if`s panicked the backend ("block already filled"). +- `if`/`match` used as expressions read values from non-dominating blocks. +- Returning a local bound to a tuple literal returned a dangling stack slot. +- String literal patterns compared pointers instead of contents. +- Union payloads were lost (`Value::Int(42)` never matched `Value::Int(x)`); + payload unions are now `[tag, payload]` cells. Explicit enum + discriminants (`A = 5`) were ignored, and variant lookup used bare names, + so two enums sharing a variant name collided. +- Constants compiled to `0` when used from another module, and a constant + declared after the function using it was reported as undeclared. +- Variables declared inside `if`/`while`/`for`/`match`/`with` blocks leaked + into the enclosing scope, so shadowing changed the outer variable. +- `let mut x` made every later `x` mutable, in any function. Mutability is now + lexically scoped; parameters and loop variables are immutable. +- Assigning to an undeclared variable was silently accepted. +- `Vec` (a 16-byte struct return truncated to its data pointer) and `math_*` + (declared with `double` parameters, called with integers) returned garbage. +- `str_substr`/`str_char_at` scanned the whole string on every call. +- Open-ended slices (`a[2..]`) failed to parse. +- Generic calls inside index expressions and slice bounds were not + monomorphized. +- Yard reused cached objects after the compiler changed or after an imported + module's constants changed; the cache key now covers both. +- Yard preserves source-relative paths under `target/objects/`, preventing + nested modules with the same basename from overwriting each other or the + runtime object; build failures, check diagnostics, dependency resolution, + and build-cache serialization use stable ordering. + +### Changed +- Version `1.0.0` for `track`, `yard`, `trackc`, and `compiler/Track.toml`. +- `compiler/src/token.trk` and the v0.8.0a subset parser/AST were replaced by + the full implementation; their tests were rewritten. ## [0.7.0] — 2026-08-23 diff --git a/Cargo.lock b/Cargo.lock index da1f043..12d8a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "track" -version = "0.7.0" +version = "1.0.0" dependencies = [ "cranelift-codegen", "cranelift-frontend", diff --git a/Cargo.toml b/Cargo.toml index 669a0ea..538d2c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "track" -version = "0.7.0" +version = "1.0.0" edition = "2024" [[bin]] diff --git a/README.md b/README.md index de2242c..132993f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Track** is a low-level systems programming language designed for deterministic memory management, zero-cost abstractions, and real-time execution. -- **No Garbage Collector, No Runtime.** Resource lifecycles are verified and freed at compile time. +- **No Garbage Collector.** Resource lifecycles are verified at compile time; there is no collector and only a small C runtime. - **Linear Ownership.** Eliminates use-after-free, double-free, and memory leaks. - **Lexical Lenses.** Scoped mutable access without manual pointer arithmetic or ownership transfer. - **Cranelift Standalone Backend.** Fast native code generation without system LLVM dynamic library dependencies. @@ -22,7 +22,7 @@ fn main() -> void { ```track import "std/io"; -// Linear ownership — freed automatically at spend point +// Linear ownership — the checker rejects use after a move let mut v: Vec = vec_init(16); vec_push(&mut v, 42); @@ -96,24 +96,32 @@ yard run yard test ``` -### Self-Hosted Compiler (`compiler/`) +### Self-Hosted Compiler (`trackc`) -`compiler/` is a `yard` package that hosts the native Track lexer (`v0.7.0`): +Track is self-hosting: `compiler/` is the Track compiler written in Track. +It compiles itself to a verified fixed point. ```bash -# Check / build the self-hosted lexer -yard check # in compiler/ -yard build # -> compiler/target/trackc -yard test # 10 lexer tests +# Build trackc with the Rust bootstrap compiler +(cd compiler && yard build) # -> compiler/target/trackc -# Dump tokens for a string (oracle vs src/lexer/mod.rs) -compiler/target/trackc "let x = 42;" +compiler/target/trackc build examples/hello.trk -o hello +compiler/target/trackc run examples/generics.trk +compiler/target/trackc emit-c examples/hello.trk # generated C on stdout + +# Stage 0 -> 1 -> 2 -> 3, then check the fixed point +scripts/bootstrap.sh ``` +See [docs/bootstrap.md](docs/bootstrap.md) for the pipeline, the shared +word-ABI runtime, and the C backend contract. + ## Testing ```bash -cargo test +cargo test # Rust suites, incl. the self-hosting parity tests +(cd compiler && yard test) # native compiler tests +scripts/bootstrap.sh # bootstrap fixed point ``` ## License diff --git a/SPEC.md b/SPEC.md index 715d9fe..05bb7ca 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Track Core Language Specification (v0.6) +# Track Core Language Specification (v1.0) Track is a low-level systems programming language designed for deterministic memory safety without a garbage collector or complex lifetime annotations. @@ -46,7 +46,7 @@ An **Owned Linear** value has exactly one owner at any point during execution. A linear resource is consumed (spent) by: 1. **Move Assignment**: `let y = x;` transfers ownership from `x` to `y`. `x` transitions to state `Spent`. 2. **Function Call Transfer**: Passing an owned value into a function parameter transfers ownership to the callee. -3. **Implicit Scope Cleanup**: If a linear resource remains `Active` at scope exit, the compiler automatically emits cleanup deallocation code (`vec_free`, `str_free`). +3. **Scope Exit**: Manual frees (`free`, `str_free`, `vec_free`, `file_close`) are rejected, so a resource can never be freed twice or used after a free. In v1.0 neither compiler inserts deallocation code yet: heap memory owned by linear values is reclaimed when the process exits. Emitting frees at scope exit from the ownership analysis is planned. ### Struct Ownership & Field Move Policy Structs in Track are moved as **atomic units**. Moving any field or the struct itself consumes the entire struct instance, preventing use-after-free or partial double-destruction. @@ -99,7 +99,17 @@ A variable is usable after the loop only if the merged state is consistent acros --- -## 6. Anonymous Tuples & Destructuring +## 6. Structs, Arrays, Anonymous Tuples & Destructuring + +- **Structs**: `struct Point { x: i64, y: i64 }` declares a linear record. + `Point { x: 1, y: 2 }` constructs one (unknown fields are rejected), + `p.x` reads a field, and `p.x = v` updates it when `p` is `let mut` or a + lens alias. Struct patterns `Point { x: 0, y }` match by field. +- **Arrays**: `[a, b, c]` creates a linear, fixed-length array. `a[i]` and + `a[i] = v` are bounds checked at run time (out-of-range access aborts), + `a[s..e]` and `a[s..]` copy a slice, and `array_len(&a)` gives the length. +- Tuple elements can be updated in place with `t.0 = v`. + Tuples are heterogeneous value containers specified with `(T1, T2, ...)`. @@ -140,11 +150,32 @@ Generic functions are **templates** monomorphized at call sites. - **Syntax**: `fn name(x: T, y: U) -> T { ... }`. - **Inference**: Type parameters are inferred from argument types (variable bindings, literals). Annotate `let x: i64 = ...` when inference is ambiguous. - **Monomorphization**: Each distinct instantiation (`identity`, `identity`) produces a mangled concrete function (`identity__Ti32`). Call sites are rewritten; only instances are checked and emitted. No boxing, no runtime cost. +- **Self-hosted backend**: because every value is a machine word (§11), `trackc` emits one C function per generic template; it behaves exactly like the per-instance functions of the Rust compiler. - **Limitations (v0.6.0)**: Generic functions are fully supported. Generic structs (`struct Stack`) header-parsing is reserved for a follow-on; `Stack` as a type argument is not yet monomorphized. --- -## 10. Diagnostics & Error Reporting +## 10. Mutability + +Bindings are immutable unless declared `let mut` (or bound by a `let mut` +destructuring pattern). Mutability belongs to the binding: a later +`let x = ...` shadows a mutable `x` with an immutable one, and a binding goes +out of scope at the end of its block. Parameters, `for` loop variables, and +lens aliases are immutable. Assigning to an undeclared name is an error. + +## 11. Runtime Value Model + +Every value is one 64-bit word: integers and booleans directly (comparisons +and `&&`/`||` yield `0` or `1`; `&&`/`||` short-circuit), strings as +NUL-terminated byte pointers, references as the referenced value, tuples as +pointers to word arrays, enums as their discriminant, values of unions +with payload variants as heap cells `[tag, payload...]`, structs as +`[name hash, fields...]`, and arrays as `[length, elements...]`. Arithmetic wraps, +shift amounts are taken modulo 64, `>>` is logical, and integer division by +zero traps. Both compilers share this model and the runtime in +`runtime/track_runtime.c`; see [docs/bootstrap.md](docs/bootstrap.md). + +## 12. Diagnostics & Error Reporting Track compiler diagnostics report span location, root cause, and ownership state transitions: diff --git a/compiler/Track.toml b/compiler/Track.toml index f9bd475..5702d68 100644 --- a/compiler/Track.toml +++ b/compiler/Track.toml @@ -1,6 +1,6 @@ [package] name = "trackc" -version = "0.7.0" +version = "1.0.0" authors = [] [dependencies] diff --git a/compiler/src/ast.trk b/compiler/src/ast.trk index 44cd289..b30ef5c 100644 --- a/compiler/src/ast.trk +++ b/compiler/src/ast.trk @@ -1,128 +1,153 @@ -// ast.trk — compact native AST representation for the Phase 3 parser subset. +// ast.trk — node pool for the native AST. // -// `Expr` names the semantic node variants. `AstNode` is the bootstrap storage -// form used until Track supports generic, field-addressable native structs. -// Layout: (kind, value, auxiliary, start byte, end byte, extra). - -import "collections"; - -union Expr { - IntLiteral, StringLiteral, BoolLiteral, Identifier, - Unary, Binary, Let, Return, ExpressionStatement, Error, -} - -type Ast = ptr; -type AstNode = (i64, i64, i64, i64, i64, i64); - -fn ast_kind_int() -> i64 { return 1; } -fn ast_kind_string() -> i64 { return 2; } -fn ast_kind_bool() -> i64 { return 3; } -fn ast_kind_identifier() -> i64 { return 4; } -fn ast_kind_unary() -> i64 { return 5; } -fn ast_kind_binary() -> i64 { return 6; } -fn ast_kind_let() -> i64 { return 7; } -fn ast_kind_return() -> i64 { return 8; } -fn ast_kind_expression_statement() -> i64 { return 9; } -fn ast_kind_error() -> i64 { return 10; } - -fn ast_new(initial_nodes: i64) -> ptr { - return collection_new(initial_nodes * 6); -} - -fn ast_node_count(ast: &ptr) -> i64 { - return collection_len(ast) / 6; -} - -fn ast_push_node(ast: &ptr, node: &AstNode) -> i64 { - let node_index = ast_node_count(ast); - let kind: i64 = node.0; - let value: i64 = node.1; - let auxiliary: i64 = node.2; - let start: i64 = node.3; - let end: i64 = node.4; - let flags: i64 = node.5; - let _kind_slot = collection_push(ast, kind); - let _value_slot = collection_push(ast, value); - let _auxiliary_slot = collection_push(ast, auxiliary); - let _start_slot = collection_push(ast, start); - let _end_slot = collection_push(ast, end); - let _flags_slot = collection_push(ast, flags); - return node_index; -} - -fn ast_node_field(ast: &ptr, node_index: i64, field: i64) -> i64 { - if node_index < 0 { - return 0; +// Every node is N_WIDTH words: (kind, a, b, c, d, e, pos). Operand meaning +// depends on the kind (documented beside each constant). Child sequences +// are list ids into the list pool (`list_at`), names are interned ids, and +// `pos` is the global source position used for diagnostics. -1 marks an +// absent optional operand. + +import "base"; + +const N_WIDTH = 7; + +// Expressions and statements +const K_INT = 1; // a = value +const K_STR = 2; // a = string literal text +const K_BOOL = 3; // a = 0 / 1 +const K_VAR = 4; // a = name (may be `A::B` qualified) +const K_BIN = 5; // a = op, b = left, c = right +const K_UN = 6; // a = op, b = operand +const K_ARRAY = 7; // a = elements +const K_TUPLE = 8; // a = elements +const K_TINDEX = 9; // a = target, b = index +const K_INDEX = 10; // a = target, b = index expression +const K_SLICE = 11; // a = target, b = start (-1), c = end (-1) +const K_RANGE = 12; // a = start, b = end +const K_ADDR = 13; // a = target +const K_SINIT = 14; // a = type name, b = [field name, value]* +const K_LENS = 15; // a = target name, b = lens name, c = body +const K_CALL = 16; // a = callee name, b = arguments +const K_IF = 17; // a = condition, b = then body, c = else body +const K_WHILE = 18; // a = condition, b = body +const K_FOR = 19; // a = variable name, b = iterable, c = body +const K_RETURN = 20; // a = value (-1) +const K_ASSIGN = 21; // a = target, b = value +const K_FN = 22; // a = name, b = generics, c = [name, type]*, d = return type (-1), e = body +const K_USE = 23; // a = path text, b = import names (-1), c = alias name (-1) +const K_CONST = 24; // a = name, b = value +const K_TYPEDEF = 25; // a = name, b = target type +const K_MACRODEF = 26; // same layout as K_FN (no generics) +const K_MACROCALL = 27; // a = name, b = arguments, c = trailing body (-1) +const K_LET = 28; // a = name, b = mutable, c = type (-1), d = value +const K_LETD = 29; // a = pattern, b = mutable, c = value +const K_ENUM = 30; // a = name, b = underlying type (-1), c = [name, value (-1)]* +const K_UNION = 31; // a = name, b = [name, payload type (-1)]* +const K_MATCH = 32; // a = target, b = [pattern, guard (-1), body]* +const K_STRUCT = 33; // a = name, b = [field name, type]* +const K_FIELD = 34; // a = target, b = field name + +// Patterns +const P_IDENT = 40; // a = name +const P_WILD = 41; +const P_LIT = 42; // a = literal expression +const P_TUPLE = 43; // a = sub-patterns +const P_VARIANT = 44; // a = enum/union name, b = variant name, c = bindings +const P_STRUCT = 45; // a = type name, b = [field name, pattern]* + +// Types +const T_PRIM = 50; // a = primitive code (PRIM_*) +const T_PTR = 51; // a = inner +const T_REF = 52; // a = inner +const T_SLICE = 53; // a = inner +const T_ARRAY = 54; // a = inner, b = length +const T_TUPLE = 55; // a = element types +const T_CUSTOM = 56; // a = name + +const PRIM_U8 = 1; +const PRIM_I8 = 2; +const PRIM_I32 = 3; +const PRIM_U32 = 4; +const PRIM_I64 = 5; +const PRIM_U64 = 6; +const PRIM_BOOL = 7; +const PRIM_VOID = 8; + +// Binary operators +const OP_ADD = 1; +const OP_SUB = 2; +const OP_MUL = 3; +const OP_DIV = 4; +const OP_MOD = 5; +const OP_EQ = 6; +const OP_NEQ = 7; +const OP_LT = 8; +const OP_GT = 9; +const OP_LTE = 10; +const OP_GTE = 11; +const OP_AND = 12; +const OP_OR = 13; +const OP_BAND = 14; +const OP_BOR = 15; +const OP_SHL = 16; +const OP_SHR = 17; + +// Unary operators +const OP_NEG = 1; +const OP_NOT = 2; +const OP_DEREF = 3; + +fn node_new(c: i64, kind: i64, a: i64, b: i64, d3: i64, d4: i64, d5: i64, pos: i64) -> i64 { + let nodes = cx(c, CX_NODES); + let id = vlen(nodes) / N_WIDTH; + vpush(nodes, kind); + vpush(nodes, a); + vpush(nodes, b); + vpush(nodes, d3); + vpush(nodes, d4); + vpush(nodes, d5); + vpush(nodes, pos); + return id; +} + +fn node0(c: i64, kind: i64, pos: i64) -> i64 { + return node_new(c, kind, -1, -1, -1, -1, -1, pos); +} + +fn node1(c: i64, kind: i64, a: i64, pos: i64) -> i64 { + return node_new(c, kind, a, -1, -1, -1, -1, pos); +} + +fn node2(c: i64, kind: i64, a: i64, b: i64, pos: i64) -> i64 { + return node_new(c, kind, a, b, -1, -1, -1, pos); +} + +fn node3(c: i64, kind: i64, a: i64, b: i64, d3: i64, pos: i64) -> i64 { + return node_new(c, kind, a, b, d3, -1, -1, pos); +} + +fn node_count(c: i64) -> i64 { return vlen(cx(c, CX_NODES)) / N_WIDTH; } +fn nk(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH); } +fn na(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 1); } +fn nb(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 2); } +fn nc(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 3); } +fn nd(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 4); } +fn ne(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 5); } +fn npos(c: i64, n: i64) -> i64 { return vget(cx(c, CX_NODES), n * N_WIDTH + 6); } + +fn prim_type(c: i64, code: i64) -> i64 { + return node1(c, T_PRIM, code, -1); +} + +fn is_prim(c: i64, ty: i64, code: i64) -> bool { + if ty < 0 { + return false; } - if node_index >= ast_node_count(ast) { - return 0; - } - if field < 0 || field >= 6 { - return 0; - } - return collection_get(ast, node_index * 6 + field); -} - -fn ast_node_kind(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 0); -} - -fn ast_node_value(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 1); -} - -fn ast_node_auxiliary(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 2); -} - -fn ast_node_start(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 3); -} - -fn ast_node_end(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 4); -} - -fn ast_node_flags(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 5); -} - -fn ast_node_extra(ast: &ptr, node_index: i64) -> i64 { - return ast_node_field(ast, node_index, 5); -} - -fn ast_literal_node(kind: i64, value: i64, start: i64, end: i64) -> AstNode { - return (kind, value, -1, start, end, 0); -} - -fn ast_identifier_node(start: i64, end: i64) -> AstNode { - return (ast_kind_identifier(), 0, -1, start, end, 0); -} - -fn ast_unary_node(operator: i64, operand: i64, start: i64, end: i64) -> AstNode { - return (ast_kind_unary(), operator, operand, start, end, 0); -} - -fn ast_binary_node(operator: i64, left: i64, right: i64, start: i64, end: i64) -> AstNode { - return (ast_kind_binary(), operator, right, start, end, left); -} - -fn ast_let_node(name: i64, initializer: i64, mutable: bool, start: i64, end: i64) -> AstNode { - let mut flags: i64 = 0; - if mutable { - flags = 1; + if nk(c, ty) != T_PRIM { + return false; } - return (ast_kind_let(), name, initializer, start, end, flags); -} - -fn ast_return_node(value: i64, start: i64, end: i64) -> AstNode { - return (ast_kind_return(), value, -1, start, end, 0); -} - -fn ast_expression_statement_node(value: i64, start: i64, end: i64) -> AstNode { - return (ast_kind_expression_statement(), value, -1, start, end, 0); + return na(c, ty) == code; } -fn ast_error_node(start: i64, end: i64, error_code: i64) -> AstNode { - return (ast_kind_error(), error_code, -1, start, end, 0); +fn is_comparison_op(op: i64) -> bool { + return op >= OP_EQ && op <= OP_GTE; } diff --git a/compiler/src/ast_test.trk b/compiler/src/ast_test.trk deleted file mode 100644 index 60341a0..0000000 --- a/compiler/src/ast_test.trk +++ /dev/null @@ -1,45 +0,0 @@ -import "ast"; - -fn assert_i64(actual: i64, expected: i64) -> void { - if actual != expected { - abort("AST integer assertion failed"); - } -} - -fn test_ast_node_storage() -> void { - let ast = ast_new(2); - let literal = ast_literal_node(ast_kind_int(), 42, 4, 6); - let literal_index = ast_push_node(&ast, &literal); - let identifier = ast_identifier_node(10, 16); - let identifier_index = ast_push_node(&ast, &identifier); - - assert_i64(literal_index, 0); - assert_i64(identifier_index, 1); - assert_i64(ast_node_count(&ast), 2); - assert_i64(ast_node_kind(&ast, literal_index), ast_kind_int()); - assert_i64(ast_node_value(&ast, literal_index), 42); - assert_i64(ast_node_start(&ast, literal_index), 4); - assert_i64(ast_node_end(&ast, literal_index), 6); - assert_i64(ast_node_kind(&ast, identifier_index), ast_kind_identifier()); -} - -fn test_ast_relationships() -> void { - let ast = ast_new(4); - let left_node = ast_literal_node(ast_kind_int(), 1, 0, 1); - let left = ast_push_node(&ast, &left_node); - let right_node = ast_literal_node(ast_kind_int(), 2, 4, 5); - let right = ast_push_node(&ast, &right_node); - let binary_node = ast_binary_node(100, left, right, 0, 5); - let binary = ast_push_node(&ast, &binary_node); - - assert_i64(ast_node_kind(&ast, binary), ast_kind_binary()); - assert_i64(ast_node_value(&ast, binary), 100); - assert_i64(ast_node_flags(&ast, binary), left); - assert_i64(ast_node_auxiliary(&ast, binary), right); -} - -fn main() -> void { - test_ast_node_storage(); - test_ast_relationships(); - print("ok"); -} diff --git a/compiler/src/base.trk b/compiler/src/base.trk new file mode 100644 index 0000000..8cf1626 --- /dev/null +++ b/compiler/src/base.trk @@ -0,0 +1,313 @@ +// base.trk — shared constants, runtime wrappers, and the compiler context. +// +// Every compiler value is a 64-bit word: integers, booleans, string +// pointers, and handles to runtime-owned word vectors. Handles are plain +// `i64`s so they are copy values for the ownership checker; the compiler +// owns all of its tables for the life of the process. + +import "source_span"; +import "diagnostic"; + +// ----------------------------------------------------------------- vectors + +fn vnew() -> i64 { return track_i64_collection_new(16); } +fn vnew_cap(cap: i64) -> i64 { return track_i64_collection_new(cap); } +fn vlen(v: i64) -> i64 { return track_i64_collection_len(v); } +fn vpush(v: i64, x: i64) -> i64 { return track_i64_collection_push(v, x); } +fn vget(v: i64, i: i64) -> i64 { return track_i64_collection_get(v, i); } +fn vset(v: i64, i: i64, x: i64) -> i64 { return track_i64_collection_set(v, i, x); } +fn vpop(v: i64) -> i64 { return track_i64_collection_pop(v); } +fn vtrunc(v: i64, n: i64) -> i64 { return track_i64_collection_truncate(v, n); } +fn vlast(v: i64) -> i64 { return vget(v, vlen(v) - 1); } + +fn vfill(v: i64, n: i64, x: i64) -> i64 { + let mut i: i64 = 0; + while i < n { + vpush(v, x); + i = i + 1; + } + return v; +} + +// --------------------------------------------------------- string builders + +fn sb_new() -> i64 { return track_sb_new(); } +fn sb_add(b: i64, s: i64) -> i64 { return track_sb_push(b, s); } +fn sb_byte(b: i64, x: i64) -> i64 { return track_sb_push_byte(b, x); } +fn sb_int(b: i64, n: i64) -> i64 { return track_sb_push_int(b, n); } +fn sb_size(b: i64) -> i64 { return track_sb_len(b); } +fn sb_text(b: i64) -> i64 { return track_sb_str(b); } +fn sb_reset(b: i64) -> i64 { return track_sb_clear(b); } + +fn sb_line(b: i64, s: i64) -> i64 { + sb_add(b, s); + return sb_byte(b, 10); +} + +// ------------------------------------------------------------- text & I/O + +fn text_eq(a: i64, b: i64) -> bool { return str_eq(a, b); } +fn text_len(s: i64) -> i64 { return str_len(s); } +fn text_cat(a: i64, b: i64) -> i64 { return str_concat(a, b); } +fn text_cat3(a: i64, b: i64, c: i64) -> i64 { return text_cat(text_cat(a, b), c); } +fn text_of_int(n: i64) -> i64 { return int_to_str(n); } +fn text_bytes(s: i64) -> i64 { return track_str_bytes(s); } +fn text_ends(s: i64, suffix: i64) -> bool { return str_ends_with(s, suffix); } +fn text_starts(s: i64, prefix: i64) -> bool { return str_starts_with(s, prefix); } + +fn out(s: i64) -> i64 { return track_out(s); } +fn err(s: i64) -> i64 { return track_err(s); } +fn read_text(path: i64) -> i64 { return track_file_read(path); } +fn write_text(path: i64, s: i64) -> i64 { return track_file_write_all(path, s); } +fn path_exists(path: i64) -> bool { return file_exists(path) != 0; } +fn shell(cmd: i64) -> i64 { return sys_exec(cmd); } +fn arg_count() -> i64 { return os_args_count(); } +fn arg_text(i: i64) -> i64 { return os_arg(i); } +fn quit(code: i64) -> i64 { return sys_exit(code); } + +// Byte offset of the last '/' in `s`, or -1. +fn last_slash(s: i64) -> i64 { + let bytes = text_bytes(s); + let mut i: i64 = vlen(bytes) - 1; + while i >= 0 { + if vget(bytes, i) == 47 { + return i; + } + i = i - 1; + } + return -1; +} + +// Directory part of a path ("" when there is none, "/" for root files). +fn path_dir(s: i64) -> i64 { + let slash = last_slash(s); + if slash < 0 { + return ""; + } + if slash == 0 { + return "/"; + } + return str_substr(s, 0, slash); +} + +// ------------------------------------------------------- compiler context + +// Context slots: one word vector holds every table the compiler uses. +const CX_SRCS = 0; // file id -> source text +const CX_BYTES = 1; // file id -> byte vector of the source +const CX_PATHS = 2; // file id -> path +const CX_BASES = 3; // file id -> base offset in the global position space +const CX_TOKS = 4; // token records (TK_WIDTH words each) +const CX_NODES = 5; // AST nodes (N_WIDTH words each) +const CX_LISTS = 6; // list pool: [len, items...] +const CX_NAMES = 7; // name id -> interned string +const CX_HASH = 8; // open-addressed intern table (name id + 1, 0 = empty) +const CX_INFO = 9; // per-name info (NI_WIDTH words per name) +const CX_STACK = 10; // scratch stack used while building lists +const CX_SB = 11; // scratch string builder +const CX_ITEMS = 12; // top-level items of every loaded module, in load order +const CX_POS = 13; // parser: current token index +const CX_ALLOW = 14; // parser: struct literals allowed +const CX_MODS = 15; // loaded module paths +const CX_ITEMFILE = 16; // file id of each top-level item +const CX_MAINFILE = 17; // file id of the root module +const CX_TEND = 18; // parser: token index of the current file's EOF +const CX_CHK = 19; // checker state vector +const CX_GEN = 20; // C emitter state vector +const CX_STRCACHE = 21; // string literal pool (unescaped text per literal) +const CX_STRUCTS = 22; // struct names in declaration order +const CX_SLOTS = 23; + +fn cx_new() -> i64 { + let c = vnew_cap(CX_SLOTS); + vfill(c, CX_SLOTS, 0); + vset(c, CX_SRCS, vnew()); + vset(c, CX_BYTES, vnew()); + vset(c, CX_PATHS, vnew()); + vset(c, CX_BASES, vnew()); + vset(c, CX_TOKS, vnew_cap(65536)); + vset(c, CX_NODES, vnew_cap(65536)); + let lists = vnew_cap(65536); + vpush(lists, 0); // list id 0 is the shared empty list + vset(c, CX_LISTS, lists); + vset(c, CX_NAMES, vnew_cap(4096)); + vset(c, CX_HASH, vfill(vnew_cap(16384), 16384, 0)); + vset(c, CX_INFO, vnew_cap(4096 * NI_WIDTH)); + vset(c, CX_STACK, vnew_cap(1024)); + vset(c, CX_SB, sb_new()); + vset(c, CX_ITEMS, vnew()); + vset(c, CX_MODS, vnew()); + vset(c, CX_ITEMFILE, vnew()); + vset(c, CX_STRCACHE, vnew()); + vset(c, CX_STRUCTS, vnew()); + vset(c, CX_ALLOW, 1); + vset(c, CX_MAINFILE, 0); + return c; +} + +fn cx(c: i64, slot: i64) -> i64 { return vget(c, slot); } + +// ------------------------------------------------------- name interning + +// Per-name information slots (indexed by interned name id). +const NI_FN = 0; // FN / MACRODEF node, -1 if none +const NI_VARIANT = 1; // discriminant when the name is `Type::Variant` +const NI_OWNER = 2; // owning enum/union name id of a variant +const NI_TYPEKIND = 3; // 0 none, 1 enum, 2 union, 3 struct, 4 alias +const NI_TYPENODE = 4; // defining node (alias target type for aliases) +const NI_PAYLOAD = 5; // 1 when a union has payload variants +const NI_CONST = 6; // CONST node +const NI_VARSLOT = 7; // checker: newest variable slot with this name +const NI_FNKNOWN = 8; // checker: 1 when present in the function table +const NI_FNRET = 9; // checker: return type node (-1 = no return type) +const NI_GLOBAL = 10; // checker: global declaration (variant/const) state +const NI_CVAR = 11; // emitter: C local index bound to this name +const NI_MARK = 12; // scratch mark used by individual passes +const NI_CONSTOK = 13; // 1 when the name is an evaluated integer constant +const NI_CONSTVAL = 14; // value of an evaluated integer constant +const NI_EXTERN = 15; // emitter: arity of an extern/runtime function, -1 if unknown +const NI_WIDTH = 16; + +fn name_count(c: i64) -> i64 { return vlen(cx(c, CX_NAMES)); } +fn name_text(c: i64, id: i64) -> i64 { return vget(cx(c, CX_NAMES), id); } + +fn ni(c: i64, id: i64, slot: i64) -> i64 { + return vget(cx(c, CX_INFO), id * NI_WIDTH + slot); +} + +fn ni_set(c: i64, id: i64, slot: i64, value: i64) -> i64 { + return vset(cx(c, CX_INFO), id * NI_WIDTH + slot, value); +} + +// FNV-1a over the bytes of a string. +fn text_hash(s: i64) -> i64 { + let bytes = text_bytes(s); + let n = vlen(bytes); + let mut h: i64 = 2166136261; + let mut i: i64 = 0; + while i < n { + let b = vget(bytes, i); + h = (h | b) - (h & b); // xor + h = h * 16777619; + i = i + 1; + } + return h; +} + +fn grow_intern_table(c: i64) -> i64 { + let old = cx(c, CX_HASH); + let cap = vlen(old) * 2; + let table = vfill(vnew_cap(cap), cap, 0); + vset(c, CX_HASH, table); + let names = cx(c, CX_NAMES); + let mut id: i64 = 0; + while id < vlen(names) { + let mut slot = text_hash(vget(names, id)) & (cap - 1); + while vget(table, slot) != 0 { + slot = (slot + 1) & (cap - 1); + } + vset(table, slot, id + 1); + id = id + 1; + } + return 0; +} + +// Return the id for `s`, adding it to the table when new. +fn intern(c: i64, s: i64) -> i64 { + let names = cx(c, CX_NAMES); + if vlen(names) * 2 >= vlen(cx(c, CX_HASH)) { + grow_intern_table(c); + } + let table = cx(c, CX_HASH); + let mask = vlen(table) - 1; + let mut slot = text_hash(s) & mask; + while vget(table, slot) != 0 { + let id = vget(table, slot) - 1; + if text_eq(vget(names, id), s) { + return id; + } + slot = (slot + 1) & mask; + } + let id = vpush(names, s); + vset(table, slot, id + 1); + let info = cx(c, CX_INFO); + vfill(info, NI_WIDTH, -1); + ni_set(c, id, NI_TYPEKIND, 0); + ni_set(c, id, NI_PAYLOAD, 0); + ni_set(c, id, NI_FNKNOWN, 0); + ni_set(c, id, NI_MARK, 0); + ni_set(c, id, NI_CONSTOK, 0); + return id; +} + +// ---------------------------------------------------------------- lists + +// Lists are built on the scratch stack: remember `list_begin`, push items, +// then `list_end` copies them into the pool and returns the list id. +fn list_begin(c: i64) -> i64 { return vlen(cx(c, CX_STACK)); } +fn list_item(c: i64, x: i64) -> i64 { return vpush(cx(c, CX_STACK), x); } + +fn list_end(c: i64, mark: i64) -> i64 { + let stack = cx(c, CX_STACK); + let n = vlen(stack) - mark; + if n == 0 { + return 0; + } + let lists = cx(c, CX_LISTS); + let id = vpush(lists, n); + let mut i: i64 = 0; + while i < n { + vpush(lists, vget(stack, mark + i)); + i = i + 1; + } + vtrunc(stack, mark); + return id; +} + +fn list_len(c: i64, l: i64) -> i64 { + if l <= 0 { + return 0; + } + return vget(cx(c, CX_LISTS), l); +} + +fn list_at(c: i64, l: i64, i: i64) -> i64 { + return vget(cx(c, CX_LISTS), l + 1 + i); +} + +// ------------------------------------------------------------- diagnostics + +fn file_of_pos(c: i64, pos: i64) -> i64 { + let bases = cx(c, CX_BASES); + let mut f: i64 = vlen(bases) - 1; + while f > 0 && vget(bases, f) > pos { + f = f - 1; + } + return f; +} + +// Print `error[code]: message` with a source frame at global position `pos` +// and exit with status 1. Compilation stops at the first error. +fn fail_at(c: i64, pos: i64, code: i64, message: i64) -> i64 { + if vlen(cx(c, CX_BASES)) == 0 || pos < 0 { + err(text_cat3("error[", code, "]: ")); + err(message); + err("\n"); + quit(1); + return 0; + } + let f = file_of_pos(c, pos); + let src = vget(cx(c, CX_SRCS), f); + let path = vget(cx(c, CX_PATHS), f); + let offset = pos - vget(cx(c, CX_BASES), f); + let span = source_span(&src, offset, offset + 1); + let text = format_error(&src, &path, &code, &message, "here", span); + err(text); + err("\n"); + quit(1); + return 0; +} + +fn fail(c: i64, code: i64, message: i64) -> i64 { + return fail_at(c, -1, code, message); +} diff --git a/compiler/src/cgen.trk b/compiler/src/cgen.trk new file mode 100644 index 0000000..b7d52aa --- /dev/null +++ b/compiler/src/cgen.trk @@ -0,0 +1,992 @@ +// cgen.trk — portable C backend. +// +// Every Track value is a 64-bit word (`tk_w`), exactly as in the bootstrap +// Cranelift backend: integers and booleans are plain words, strings are +// `char*`, tuples and payload-union cells are pointers to word arrays. +// Expressions compile to C expression text; constructs that need +// statements (calls, `if`/`match` values, short-circuit logic) are lowered +// into temporaries declared at the top of the function, so the output is +// plain C99 with no compiler extensions. +// +// Output is deterministic: items are emitted in load order and every +// counter restarts per function. + +import "base"; +import "lexer"; +import "ast"; +import "symbols"; +import "runtime_c"; + +const G_DECLS = 0; // current function: local declarations +const G_BODY = 1; // current function: statements +const G_NLOCAL = 2; +const G_NTEMP = 3; +const G_NSLOT = 4; +const G_INDENT = 5; +const G_BOUND = 6; // scope stack: (name, previous binding) pairs +const G_TUPLES = 7; // local id -> tuple arity of a stack tuple (or -1) +const G_RETVAL = 8; // current function returns a value +const G_EXTERNS = 9; // extern (non-runtime) functions in first-use order +const G_FUNCS = 10; // emitted function definitions +const G_PROTOS = 11; // user function prototypes +const G_SLOTS = 12; + +fn g(c: i64, slot: i64) -> i64 { return vget(cx(c, CX_GEN), slot); } +fn g_set(c: i64, slot: i64, v: i64) -> i64 { return vset(cx(c, CX_GEN), slot, v); } + +fn g_counter(c: i64, slot: i64) -> i64 { + let n = g(c, slot); + g_set(c, slot, n + 1); + return n; +} + +// Append one statement line to the current function body. +fn emit(c: i64, line: i64) -> i64 { + let b = g(c, G_BODY); + let mut i: i64 = 0; + while i < g(c, G_INDENT) { + sb_add(b, " "); + i = i + 1; + } + return sb_line(b, line); +} + +fn indent(c: i64, delta: i64) -> i64 { + return g_set(c, G_INDENT, g(c, G_INDENT) + delta); +} + +fn temp(c: i64) -> i64 { + let name = text_cat("t", text_of_int(g_counter(c, G_NTEMP))); + sb_line(g(c, G_DECLS), text_cat3(" tk_w ", name, " = 0;")); + return name; +} + +fn local_name(c: i64, id: i64) -> i64 { + return text_cat("l", text_of_int(id)); +} + +// Declare a fresh C local for Track variable `name` and bind it. +fn new_local(c: i64, name: i64) -> i64 { + let id = g_counter(c, G_NLOCAL); + let text = local_name(c, id); + sb_line(g(c, G_DECLS), text_cat3(" tk_w ", text, text_cat3(" = 0; /* ", name_text(c, name), " */"))); + vpush(g(c, G_TUPLES), -1); + bind(c, name, id); + return id; +} + +fn bind(c: i64, name: i64, id: i64) -> i64 { + let bound = g(c, G_BOUND); + vpush(bound, name); + vpush(bound, ni(c, name, NI_CVAR)); + return ni_set(c, name, NI_CVAR, id); +} + +fn scope_begin(c: i64) -> i64 { return vlen(g(c, G_BOUND)); } + +fn scope_end(c: i64, mark: i64) -> i64 { + let bound = g(c, G_BOUND); + while vlen(bound) > mark { + let prev = vpop(bound); + let name = vpop(bound); + ni_set(c, name, NI_CVAR, prev); + } + return 0; +} + +fn set_tuple_arity(c: i64, id: i64, arity: i64) -> i64 { + return vset(g(c, G_TUPLES), id, arity); +} + +// ------------------------------------------------------------ literals + +fn c_string(s: i64) -> i64 { + let bytes = text_bytes(s); + let b = sb_new(); + sb_add(b, "TK_S(\""); + let mut i: i64 = 0; + while i < vlen(bytes) { + let x = vget(bytes, i); + if x == 34 { + sb_add(b, "\\\""); + } else if x == 92 { + sb_add(b, "\\\\"); + } else if x == 63 { + sb_add(b, "\\?"); + } else if x == 10 { + sb_add(b, "\\n"); + } else if x == 9 { + sb_add(b, "\\t"); + } else if x >= 32 && x < 127 { + sb_byte(b, x); + } else { + sb_byte(b, 92); + sb_byte(b, 48 + (x / 64) % 8); + sb_byte(b, 48 + (x / 8) % 8); + sb_byte(b, 48 + x % 8); + } + i = i + 1; + } + sb_add(b, "\")"); + return sb_text(b); +} + +// Integer literal text valid for every i64 (INT64_MIN has no literal). +fn c_int(v: i64) -> i64 { + if v == -9223372036854775807 - 1 { + return "INT64_MIN"; + } + return text_of_int(v); +} + +fn paren2(a: i64, op: i64, b: i64) -> i64 { + return text_cat3("(", text_cat3(a, op, b), ")"); +} + +// ---------------------------------------------------------- expressions + +fn gen_list_values(c: i64, l: i64) -> i64 { + let values = vnew(); + let mut i: i64 = 0; + while i < list_len(c, l) { + vpush(values, gen(c, list_at(c, l, i))); + i = i + 1; + } + return values; +} + +// Heap cell holding the given words (escaping tuples, union payloads). +fn gen_cell(c: i64, words: i64) -> i64 { + let t = temp(c); + let mut size = vlen(words); + if size < 1 { + size = 1; + } + emit(c, text_cat3(t, " = alloc(", text_cat(text_of_int(size * 8), ");"))); + let mut i: i64 = 0; + while i < vlen(words) { + emit(c, text_cat3("TK_AT(", t, text_cat3(", ", text_of_int(i), text_cat3(") = ", vget(words, i), ";")))); + i = i + 1; + } + return t; +} + +fn binary_text(op: i64, l: i64, r: i64) -> i64 { + if op == OP_ADD { return paren2(l, " + ", r); } + if op == OP_SUB { return paren2(l, " - ", r); } + if op == OP_MUL { return paren2(l, " * ", r); } + if op == OP_DIV { return text_cat3("tk_div(", l, text_cat3(", ", r, ")")); } + if op == OP_MOD { return text_cat3("tk_mod(", l, text_cat3(", ", r, ")")); } + if op == OP_EQ { return paren2(l, " == ", r); } + if op == OP_NEQ { return paren2(l, " != ", r); } + if op == OP_LT { return paren2(l, " < ", r); } + if op == OP_GT { return paren2(l, " > ", r); } + if op == OP_LTE { return paren2(l, " <= ", r); } + if op == OP_GTE { return paren2(l, " >= ", r); } + if op == OP_BAND { return paren2(l, " & ", r); } + if op == OP_BOR { return paren2(l, " | ", r); } + if op == OP_SHL { return text_cat3("tk_shl(", l, text_cat3(", ", r, ")")); } + return text_cat3("tk_shr(", l, text_cat3(", ", r, ")")); +} + +fn gen_logic(c: i64, n: i64) -> i64 { + let r = temp(c); + let l = gen(c, nb(c, n)); + emit(c, text_cat3(r, " = (", text_cat(l, " != 0);"))); + if na(c, n) == OP_AND { + emit(c, text_cat3("if (", r, ") {")); + } else { + emit(c, text_cat3("if (!", r, ") {")); + } + indent(c, 1); + let rhs = gen(c, nc(c, n)); + emit(c, text_cat3(r, " = (", text_cat(rhs, " != 0);"))); + indent(c, -1); + emit(c, "}"); + return r; +} + +// Struct literal: heap cell [tag, fields in declaration order]. +fn gen_struct_init(c: i64, n: i64) -> i64 { + let fields = nb(c, n); + let names = vnew(); + let values = vnew(); + let mut i: i64 = 0; + while i < list_len(c, fields) { + vpush(names, list_at(c, fields, i)); + vpush(values, gen(c, list_at(c, fields, i + 1))); + i = i + 2; + } + let words = vnew(); + vpush(words, c_int(struct_tag(c, na(c, n)))); + let decl = struct_def(c, na(c, n)); + if decl < 0 { + let mut j: i64 = 0; + while j < vlen(values) { + vpush(words, vget(values, j)); + j = j + 1; + } + return gen_cell(c, words); + } + let order = nb(c, decl); + let mut f: i64 = 0; + while f < list_len(c, order) { + let mut v: i64 = "0"; + let mut k: i64 = 0; + while k < vlen(names) { + if vget(names, k) == list_at(c, order, f) { + v = vget(values, k); + } + k = k + 1; + } + vpush(words, v); + f = f + 2; + } + return gen_cell(c, words); +} + +// Read (store == 0) or write a struct field of the cell in temp `base`. +// A field name declared by several structs dispatches on the tag. +fn gen_field_access(c: i64, base: i64, field: i64, store: i64) -> i64 { + let cands = field_candidates(c, field); + if vlen(cands) == 2 { + let slot = load_text(base, vget(cands, 1) + 1); + if store != 0 { + emit(c, text_cat3(slot, " = ", text_cat(store, ";"))); + return store; + } + return slot; + } + let tag = load_text(base, 0); + if store != 0 { + let mut i: i64 = 0; + while i < vlen(cands) { + let mut head: i64 = "if ("; + if i > 0 { + head = "} else if ("; + } + emit(c, text_cat3(head, paren2(tag, " == ", c_int(vget(cands, i))), ") {")); + emit(c, text_cat3(" ", load_text(base, vget(cands, i + 1) + 1), text_cat3(" = ", store, ";"))); + i = i + 2; + } + if vlen(cands) > 0 { + emit(c, "} else {"); + emit(c, " track_bad_field();"); + emit(c, "}"); + } else { + emit(c, "track_bad_field();"); + } + return store; + } + let b = sb_new(); + sb_add(b, "("); + let mut j: i64 = 0; + while j < vlen(cands) { + sb_add(b, paren2(tag, " == ", c_int(vget(cands, j)))); + sb_add(b, " ? "); + sb_add(b, load_text(base, vget(cands, j + 1) + 1)); + sb_add(b, " : "); + j = j + 2; + } + sb_add(b, "track_bad_field())"); + return sb_text(b); +} + +fn gen_into_temp(c: i64, value: i64) -> i64 { + let t = temp(c); + emit(c, text_cat3(t, " = ", text_cat(value, ";"))); + return t; +} + +fn gen_assign_place(c: i64, target: i64, v: i64) -> i64 { + let k = nk(c, target); + if k == K_FIELD { + let base = gen_into_temp(c, gen(c, na(c, target))); + return gen_field_access(c, base, nb(c, target), v); + } + if k == K_TINDEX { + let base = gen_into_temp(c, gen(c, na(c, target))); + emit(c, text_cat3(load_text(base, nb(c, target)), " = ", text_cat(v, ";"))); + return v; + } + if k == K_INDEX { + let arr = gen(c, na(c, target)); + let idx = gen(c, nb(c, target)); + emit(c, text_cat3("track_array_set(", arr, text_cat3(", ", idx, text_cat3(", ", v, ");")))); + } + return v; +} + +fn gen_var(c: i64, n: i64) -> i64 { + let name = na(c, n); + let local = ni(c, name, NI_CVAR); + if local >= 0 { + return local_name(c, local); + } + let disc = variant_disc(c, name); + if is_qualified(c, name) { + if disc < 0 { + return "0"; + } + if is_payload_variant(c, name) { + let words = vnew(); + vpush(words, text_of_int(disc)); + vpush(words, "0"); + return gen_cell(c, words); + } + return c_int(disc); + } + if ni(c, name, NI_CONSTOK) == 1 { + return c_int(ni(c, name, NI_CONSTVAL)); + } + return "0"; +} + +fn arg_at(args: i64, i: i64) -> i64 { + if i < vlen(args) { + return vget(args, i); + } + return "0"; +} + +fn call_text(sym: i64, args: i64, arity: i64) -> i64 { + let b = sb_new(); + sb_add(b, sym); + sb_add(b, "("); + let mut i: i64 = 0; + while i < arity { + if i > 0 { + sb_add(b, ", "); + } + sb_add(b, arg_at(args, i)); + i = i + 1; + } + sb_add(b, ")"); + return sb_text(b); +} + +fn gen_call(c: i64, n: i64, args_list: i64) -> i64 { + let name = na(c, n); + let args = gen_list_values(c, args_list); + if is_qualified(c, name) { + let disc = variant_disc(c, name); + if disc >= 0 { + if is_payload_variant(c, name) { + let words = vnew(); + vpush(words, text_of_int(disc)); + let mut i: i64 = 0; + while i < vlen(args) { + vpush(words, vget(args, i)); + i = i + 1; + } + return gen_cell(c, words); + } + return text_of_int(disc); + } + } + let mut target = name; + if ni(c, name, NI_FN) < 0 { + target = last_segment(c, name); + } + let t = temp(c); + let f = ni(c, target, NI_FN); + if f >= 0 { + let arity = list_len(c, nc(c, f)) / 2; + emit(c, text_cat3(t, " = ", text_cat(call_text(text_cat("tk_", name_text(c, target)), args, arity), ";"))); + return t; + } + let mut sym = target; + if text_eq(name_text(c, target), "abort") { + sym = intern(c, "track_abort"); + } + let mut arity = ni(c, sym, NI_EXTERN); + if arity < 0 { + arity = vlen(args); + ni_set(c, sym, NI_EXTERN, arity); + vpush(g(c, G_EXTERNS), sym); + } + emit(c, text_cat3(t, " = ", text_cat(call_text(name_text(c, sym), args, arity), ";"))); + return t; +} + +// Generate a block; returns the value text of its last statement. +fn gen_block(c: i64, body: i64) -> i64 { + let mark = scope_begin(c); + let mut last: i64 = "0"; + let mut i: i64 = 0; + while i < list_len(c, body) { + last = gen(c, list_at(c, body, i)); + i = i + 1; + } + scope_end(c, mark); + return last; +} + +fn gen_if(c: i64, n: i64) -> i64 { + let r = temp(c); + let cond = gen(c, na(c, n)); + emit(c, text_cat3("if (", cond, ") {")); + indent(c, 1); + let then_value = gen_block(c, nb(c, n)); + emit(c, text_cat3(r, " = ", text_cat(then_value, ";"))); + indent(c, -1); + emit(c, "} else {"); + indent(c, 1); + let else_value = gen_block(c, nc(c, n)); + emit(c, text_cat3(r, " = ", text_cat(else_value, ";"))); + indent(c, -1); + emit(c, "}"); + return r; +} + +fn gen_while(c: i64, n: i64) -> i64 { + emit(c, "while (1) {"); + indent(c, 1); + let cond = gen(c, na(c, n)); + emit(c, text_cat3("if (!(", cond, ")) break;")); + gen_block(c, nb(c, n)); + indent(c, -1); + emit(c, "}"); + return "0"; +} + +fn gen_for(c: i64, n: i64) -> i64 { + let iter = nb(c, n); + let start = temp(c); + let end = temp(c); + if nk(c, iter) == K_RANGE { + emit(c, text_cat3(start, " = ", text_cat(gen(c, na(c, iter)), ";"))); + emit(c, text_cat3(end, " = ", text_cat(gen(c, nb(c, iter)), ";"))); + } else { + emit(c, text_cat(start, " = 0;")); + emit(c, text_cat3(end, " = ", text_cat(gen(c, iter), ";"))); + } + emit(c, text_cat3("while (", start, text_cat3(" < ", end, ") {"))); + indent(c, 1); + let mark = scope_begin(c); + let var = new_local(c, na(c, n)); + emit(c, text_cat3(local_name(c, var), " = ", text_cat(start, ";"))); + gen_block(c, nc(c, n)); + scope_end(c, mark); + emit(c, text_cat3(start, " = ", text_cat(start, " + 1;"))); + indent(c, -1); + emit(c, "}"); + return "0"; +} + +fn gen_return(c: i64, n: i64) -> i64 { + let value = na(c, n); + let mut v: i64 = "0"; + if value >= 0 { + if nk(c, value) == K_TUPLE { + v = gen_cell(c, gen_list_values(c, na(c, value))); + } else if nk(c, value) == K_VAR && ni(c, na(c, value), NI_CVAR) >= 0 && vget(g(c, G_TUPLES), ni(c, na(c, value), NI_CVAR)) >= 0 { + // A tuple living in a stack slot must outlive the frame. + let local = ni(c, na(c, value), NI_CVAR); + let t = temp(c); + emit(c, text_cat3(t, " = track_words_copy(", text_cat3(local_name(c, local), ", ", text_cat(text_of_int(vget(g(c, G_TUPLES), local)), ");")))); + v = t; + } else { + v = gen(c, value); + } + } + if g(c, G_RETVAL) == 1 { + emit(c, text_cat3("return ", v, ";")); + } else { + emit(c, "return 0;"); + } + return "0"; +} + +fn gen_tuple(c: i64, n: i64) -> i64 { + let values = gen_list_values(c, na(c, n)); + let slot = text_cat("s", text_of_int(g_counter(c, G_NSLOT))); + let mut size = vlen(values); + if size < 1 { + size = 1; + } + sb_line(g(c, G_DECLS), text_cat3(" tk_w ", slot, text_cat3("[", text_of_int(size), "];"))); + let mut i: i64 = 0; + while i < vlen(values) { + emit(c, text_cat3(slot, text_cat3("[", text_of_int(i), "] = "), text_cat(vget(values, i), ";"))); + i = i + 1; + } + return text_cat3("TK_W(", slot, ")"); +} + +fn load_text(base: i64, i: i64) -> i64 { + return text_cat3("TK_AT(", base, text_cat3(", ", text_of_int(i), ")")); +} + +fn bind_pattern_value(c: i64, p: i64, value: i64) -> i64 { + let k = nk(c, p); + if k == P_IDENT { + let id = new_local(c, na(c, p)); + emit(c, text_cat3(local_name(c, id), " = ", text_cat(value, ";"))); + return 0; + } + if k == P_TUPLE { + let pats = na(c, p); + let mut i: i64 = 0; + while i < list_len(c, pats) { + bind_pattern_value(c, list_at(c, pats, i), load_text(value, i)); + i = i + 1; + } + return 0; + } + if k == P_VARIANT { + let bindings = nc(c, p); + let payload = is_payload_union(c, na(c, p)); + let mut i: i64 = 0; + while i < list_len(c, bindings) { + if payload { + bind_pattern_value(c, list_at(c, bindings, i), load_text(value, i + 1)); + } else { + bind_pattern_value(c, list_at(c, bindings, i), value); + } + i = i + 1; + } + return 0; + } + if k == P_STRUCT { + let decl = struct_def(c, na(c, p)); + if decl < 0 { + return 0; + } + let fields = nb(c, p); + let mut i: i64 = 0; + while i < list_len(c, fields) { + let idx = struct_field_index(c, decl, list_at(c, fields, i)); + if idx >= 0 { + bind_pattern_value(c, list_at(c, fields, i + 1), load_text(value, idx + 1)); + } + i = i + 2; + } + } + return 0; +} + +// C condition text that is non-zero when pattern `p` matches `value`. +fn pattern_cond(c: i64, p: i64, value: i64) -> i64 { + let k = nk(c, p); + if k == P_LIT { + let lit = na(c, p); + if nk(c, lit) == K_STR { + return text_cat3("(str_eq(", value, text_cat3(", ", c_string(na(c, lit)), ") != 0)")); + } + return paren2(value, " == ", gen(c, lit)); + } + if k == P_VARIANT { + let vname = variant_name(c, na(c, p), nb(c, p)); + let mut disc = variant_disc(c, vname); + if disc < 0 { + disc = 0; + } + if is_payload_union(c, na(c, p)) { + return paren2(load_text(value, 0), " == ", text_of_int(disc)); + } + return paren2(value, " == ", text_of_int(disc)); + } + if k == P_TUPLE { + let pats = na(c, p); + let mut cond: i64 = "(1"; + let mut i: i64 = 0; + while i < list_len(c, pats) { + cond = text_cat3(cond, " && ", pattern_cond(c, list_at(c, pats, i), load_text(value, i))); + i = i + 1; + } + return text_cat(cond, ")"); + } + if k == P_STRUCT { + let decl = struct_def(c, na(c, p)); + if decl < 0 { + return "1"; + } + let mut cond = text_cat3("(", paren2(load_text(value, 0), " == ", c_int(struct_tag(c, na(c, p)))), ""); + let fields = nb(c, p); + let mut i: i64 = 0; + while i < list_len(c, fields) { + let idx = struct_field_index(c, decl, list_at(c, fields, i)); + if idx >= 0 { + cond = text_cat3(cond, " && ", pattern_cond(c, list_at(c, fields, i + 1), load_text(value, idx + 1))); + } + i = i + 2; + } + return text_cat(cond, ")"); + } + return "1"; +} + +fn gen_match(c: i64, n: i64) -> i64 { + let m = temp(c); + emit(c, text_cat3(m, " = ", text_cat(gen(c, na(c, n)), ";"))); + let r = temp(c); + let done = temp(c); + let arms = nb(c, n); + let mut i: i64 = 0; + while i < list_len(c, arms) { + let pattern = list_at(c, arms, i); + let guard = list_at(c, arms, i + 1); + let body = list_at(c, arms, i + 2); + emit(c, text_cat3("if (!", done, ") {")); + indent(c, 1); + let cond = pattern_cond(c, pattern, m); + emit(c, text_cat3("if (", cond, ") {")); + indent(c, 1); + let mark = scope_begin(c); + bind_pattern_value(c, pattern, m); + if guard >= 0 { + let gv = gen(c, guard); + emit(c, text_cat3("if (", gv, ") {")); + indent(c, 1); + } + emit(c, text_cat(done, " = 1;")); + let value = gen(c, body); + emit(c, text_cat3(r, " = ", text_cat(value, ";"))); + if guard >= 0 { + indent(c, -1); + emit(c, "}"); + } + scope_end(c, mark); + indent(c, -1); + emit(c, "}"); + indent(c, -1); + emit(c, "}"); + i = i + 3; + } + return r; +} + +fn gen_letd(c: i64, n: i64) -> i64 { + let pattern = na(c, n); + let value = nc(c, n); + if nk(c, value) == K_TUPLE && nk(c, pattern) == P_TUPLE && list_len(c, na(c, value)) == list_len(c, na(c, pattern)) { + let values = gen_list_values(c, na(c, value)); + let tmps = vnew(); + let mut i: i64 = 0; + while i < vlen(values) { + let t = temp(c); + emit(c, text_cat3(t, " = ", text_cat(vget(values, i), ";"))); + vpush(tmps, t); + i = i + 1; + } + let pats = na(c, pattern); + let mut j: i64 = 0; + while j < vlen(tmps) { + bind_pattern_value(c, list_at(c, pats, j), vget(tmps, j)); + j = j + 1; + } + return "0"; + } + let t = temp(c); + emit(c, text_cat3(t, " = ", text_cat(gen(c, value), ";"))); + bind_pattern_value(c, pattern, t); + return "0"; +} + +fn gen_lens(c: i64, n: i64) -> i64 { + let mark = scope_begin(c); + let target = ni(c, na(c, n), NI_CVAR); + if target >= 0 { + bind(c, nb(c, n), target); + } + let value = gen_block(c, nc(c, n)); + let t = temp(c); + emit(c, text_cat3(t, " = ", text_cat(value, ";"))); + scope_end(c, mark); + return t; +} + +fn gen(c: i64, n: i64) -> i64 { + let k = nk(c, n); + if k == K_INT { return text_of_int(na(c, n)); } + if k == K_BOOL { return text_of_int(na(c, n)); } + if k == K_STR { return c_string(na(c, n)); } + if k == K_VAR { return gen_var(c, n); } + if k == K_BIN { + let op = na(c, n); + if op == OP_AND || op == OP_OR { + return gen_logic(c, n); + } + let l = gen(c, nb(c, n)); + let r = gen(c, nc(c, n)); + return binary_text(op, l, r); + } + if k == K_UN { + let v = gen(c, nb(c, n)); + if na(c, n) == OP_NEG { return text_cat3("(-", v, ")"); } + if na(c, n) == OP_NOT { return text_cat3("(", v, " == 0)"); } + return v; + } + if k == K_ADDR { return gen(c, na(c, n)); } + if k == K_CALL { return gen_call(c, n, nb(c, n)); } + if k == K_MACROCALL { return gen_call(c, n, nb(c, n)); } + if k == K_LET { + let v = gen(c, nd(c, n)); + let id = new_local(c, na(c, n)); + emit(c, text_cat3(local_name(c, id), " = ", text_cat(v, ";"))); + if nk(c, nd(c, n)) == K_TUPLE { + set_tuple_arity(c, id, list_len(c, na(c, nd(c, n)))); + } + return local_name(c, id); + } + if k == K_ASSIGN { + let v = gen(c, nb(c, n)); + let target = na(c, n); + if nk(c, target) == K_VAR && ni(c, na(c, target), NI_CVAR) >= 0 { + let id = ni(c, na(c, target), NI_CVAR); + emit(c, text_cat3(local_name(c, id), " = ", text_cat(v, ";"))); + if nk(c, nb(c, n)) == K_TUPLE { + set_tuple_arity(c, id, list_len(c, na(c, nb(c, n)))); + } else { + set_tuple_arity(c, id, -1); + } + return local_name(c, id); + } + return gen_assign_place(c, target, gen_into_temp(c, v)); + } + if k == K_IF { return gen_if(c, n); } + if k == K_WHILE { return gen_while(c, n); } + if k == K_FOR { return gen_for(c, n); } + if k == K_RETURN { return gen_return(c, n); } + if k == K_LENS { return gen_lens(c, n); } + if k == K_TUPLE { return gen_tuple(c, n); } + if k == K_TINDEX { return load_text(gen(c, na(c, n)), nb(c, n)); } + if k == K_LETD { return gen_letd(c, n); } + if k == K_MATCH { return gen_match(c, n); } + if k == K_SLICE { + let base = gen(c, na(c, n)); + let mut from: i64 = "0"; + if nb(c, n) >= 0 { + from = gen(c, nb(c, n)); + } + let mut to: i64 = "-1"; + if nc(c, n) >= 0 { + to = gen(c, nc(c, n)); + } + return gen_into_temp(c, text_cat3("track_array_slice(", base, text_cat3(", ", from, text_cat3(", ", to, ")")))); + } + if k == K_ARRAY { + let words = gen_list_values(c, na(c, n)); + let cell = vnew(); + vpush(cell, text_of_int(vlen(words))); + let mut i: i64 = 0; + while i < vlen(words) { + vpush(cell, vget(words, i)); + i = i + 1; + } + return gen_cell(c, cell); + } + if k == K_INDEX { + let arr = gen(c, na(c, n)); + let idx = gen(c, nb(c, n)); + return gen_into_temp(c, text_cat3("track_array_get(", arr, text_cat3(", ", idx, ")"))); + } + if k == K_FIELD { + let base = gen_into_temp(c, gen(c, na(c, n))); + return gen_into_temp(c, gen_field_access(c, base, nb(c, n), 0)); + } + if k == K_SINIT { return gen_struct_init(c, n); } + if k == K_RANGE { + let s = gen(c, na(c, n)); + let e = gen(c, nb(c, n)); + return paren2(e, " - ", s); + } + // Declarations nested in a body produce no code. + return "0"; +} + +// ------------------------------------------------------------ functions + +fn fn_symbol(c: i64, name: i64) -> i64 { + return text_cat("tk_", name_text(c, name)); +} + +fn fn_signature(c: i64, f: i64) -> i64 { + let params = nc(c, f); + let b = sb_new(); + sb_add(b, "tk_w "); + sb_add(b, fn_symbol(c, na(c, f))); + sb_add(b, "("); + let count = list_len(c, params) / 2; + if count == 0 { + sb_add(b, "void"); + } + let mut i: i64 = 0; + while i < count { + if i > 0 { + sb_add(b, ", "); + } + sb_add(b, "tk_w a"); + sb_int(b, i); + i = i + 1; + } + sb_add(b, ")"); + return sb_text(b); +} + +// Emit one function. `prelude` is a list of top-level statements run at +// entry (the root module's globals, for `main`), or -1. +fn gen_function(c: i64, f: i64, prelude: i64) -> i64 { + g_set(c, G_DECLS, sb_new()); + g_set(c, G_BODY, sb_new()); + g_set(c, G_NLOCAL, 0); + g_set(c, G_NTEMP, 0); + g_set(c, G_NSLOT, 0); + g_set(c, G_INDENT, 1); + g_set(c, G_TUPLES, vnew()); + let is_main = text_eq(name_text(c, na(c, f)), "main"); + let ret = nd(c, f); + let mut returns: i64 = 0; + if is_main || (ret >= 0 && !is_prim(c, ret, PRIM_VOID)) { + returns = 1; + } + g_set(c, G_RETVAL, returns); + let mark = scope_begin(c); + let params = nc(c, f); + let mut i: i64 = 0; + while i < list_len(c, params) { + let id = new_local(c, list_at(c, params, i)); + emit(c, text_cat3(local_name(c, id), " = a", text_cat(text_of_int(i / 2), ";"))); + set_tuple_arity(c, id, tuple_arity(c, list_at(c, params, i + 1))); + i = i + 2; + } + if prelude >= 0 { + let mut p: i64 = 0; + while p < vlen(prelude) { + gen(c, vget(prelude, p)); + p = p + 1; + } + } + let last = gen_block(c, ne(c, f)); + if is_main { + emit(c, "return 0;"); + } else if returns == 1 { + emit(c, text_cat3("return ", last, ";")); + } else { + emit(c, "return 0;"); + } + scope_end(c, mark); + let out = g(c, G_FUNCS); + sb_line(out, ""); + sb_add(out, fn_signature(c, f)); + sb_line(out, " {"); + sb_add(out, sb_text(g(c, G_DECLS))); + sb_add(out, sb_text(g(c, G_BODY))); + sb_line(out, "}"); + sb_line(g(c, G_PROTOS), text_cat(fn_signature(c, f), ";")); + return 0; +} + +fn is_declaration(c: i64, n: i64) -> bool { + let k = nk(c, n); + return k == K_FN || k == K_MACRODEF; +} + +fn c_prelude(b: i64) -> i64 { + sb_line(b, "/* Generated by trackc. Do not edit. */"); + sb_line(b, "#include "); + sb_line(b, "typedef int64_t tk_w;"); + sb_line(b, "#define TK_W(p) ((tk_w)(uintptr_t)(p))"); + sb_line(b, "#define TK_S(s) TK_W(s)"); + sb_line(b, "#define TK_AT(p, i) (((tk_w *)(uintptr_t)(p))[i])"); + sb_line(b, ""); + sb_line(b, "/* runtime (runtime/track_runtime.c) */"); + runtime_prototypes(b); + sb_line(b, ""); + sb_line(b, "static tk_w tk_div(tk_w a, tk_w b) {"); + sb_line(b, " if (b == 0 || (b == -1 && a == INT64_MIN)) return track_trap_div();"); + sb_line(b, " return a / b;"); + sb_line(b, "}"); + sb_line(b, "static tk_w tk_mod(tk_w a, tk_w b) {"); + sb_line(b, " if (b == 0 || (b == -1 && a == INT64_MIN)) return track_trap_div();"); + sb_line(b, " return a % b;"); + sb_line(b, "}"); + sb_line(b, "static tk_w tk_shl(tk_w a, tk_w b) { return (tk_w)((uint64_t)a << (b & 63)); }"); + sb_line(b, "static tk_w tk_shr(tk_w a, tk_w b) { return (tk_w)((uint64_t)a >> (b & 63)); }"); + return 0; +} + +// Generate the C translation unit for the loaded, checked program. +fn generate_c(c: i64) -> i64 { + let state = vfill(vnew_cap(G_SLOTS), G_SLOTS, 0); + vset(c, CX_GEN, state); + g_set(c, G_BOUND, vnew()); + g_set(c, G_EXTERNS, vnew()); + g_set(c, G_FUNCS, sb_new()); + g_set(c, G_PROTOS, sb_new()); + register_runtime(c); + let items = cx(c, CX_ITEMS); + let files = cx(c, CX_ITEMFILE); + let root = cx(c, CX_MAINFILE); + // Top-level statements of the root module run at the start of main. + let prelude = vnew(); + let mut main_fn: i64 = -1; + let mut i: i64 = 0; + while i < vlen(items) { + let n = vget(items, i); + if vget(files, i) == root && !is_declaration(c, n) { + vpush(prelude, n); + } + if nk(c, n) == K_FN && text_eq(name_text(c, na(c, n)), "main") { + main_fn = n; + } + i = i + 1; + } + let mut j: i64 = 0; + while j < vlen(items) { + let n = vget(items, j); + if is_declaration(c, n) { + if n == main_fn { + gen_function(c, n, prelude); + } else { + gen_function(c, n, -1); + } + } + j = j + 1; + } + if main_fn < 0 { + let synthetic = node_new(c, K_FN, intern(c, "main"), 0, 0, -1, 0, -1); + gen_function(c, synthetic, prelude); + } + let out = sb_new(); + c_prelude(out); + let externs = g(c, G_EXTERNS); + if vlen(externs) > 0 { + sb_line(out, ""); + sb_line(out, "/* external functions */"); + let mut e: i64 = 0; + while e < vlen(externs) { + let name = vget(externs, e); + let arity = ni(c, name, NI_EXTERN); + let b = sb_new(); + sb_add(b, "tk_w "); + sb_add(b, name_text(c, name)); + sb_add(b, "("); + if arity == 0 { + sb_add(b, "void"); + } + let mut a: i64 = 0; + while a < arity { + if a > 0 { + sb_add(b, ", "); + } + sb_add(b, "tk_w"); + a = a + 1; + } + sb_add(b, ");"); + sb_line(out, sb_text(b)); + e = e + 1; + } + } + sb_line(out, ""); + sb_line(out, "/* functions */"); + sb_add(out, sb_text(g(c, G_PROTOS))); + sb_add(out, sb_text(g(c, G_FUNCS))); + sb_line(out, ""); + sb_line(out, "int main(int argc, char **argv) {"); + sb_line(out, " track_runtime_init((tk_w)argc, TK_W(argv));"); + sb_line(out, " return (int)tk_main();"); + sb_line(out, "}"); + return sb_text(out); +} diff --git a/compiler/src/checker.trk b/compiler/src/checker.trk new file mode 100644 index 0000000..3a2a7db --- /dev/null +++ b/compiler/src/checker.trk @@ -0,0 +1,1733 @@ +// checker.trk — linear ownership checker. +// +// Implements the rules of the bootstrap `LinearChecker` (src/checker/): +// non-copy values move on use, `&x` borrows without moving, a reference +// stored in a variable freezes its target while the reference is live, +// `with x -> l { }` locks `x` for the block, both arms of an `if` must leave +// every resource in the same state, loops may not consume outer resources, +// references may not escape their function, and lenses may not escape +// their block. +// +// Variables live in a slot table (VW words per slot) instead of cloned +// maps: each slot records its name, ownership state, type, mutability, +// lens-alias flag, the previous slot with the same name (shadowing chain), +// its borrow provenance, and a dead flag. Branches snapshot and restore +// slot states. + +import "base"; +import "lexer"; +import "ast"; +import "symbols"; + +const ST_ACTIVE = 0; +const ST_LOCKED = 1; +const ST_SPENT = 2; + +const VW = 8; +const V_NAME = 0; +const V_STATE = 1; +const V_TYPE = 2; +const V_MUT = 3; +const V_LENS = 4; +const V_PREV = 5; +const V_BORROWS = 6; +const V_DEAD = 7; + +// Checker state slots (vector stored in CX_CHK) +const CK_VARS = 0; +const CK_LENSLOCK = 1; // names currently locked by a lens +const CK_LENSCOUNT = 2; // live lens aliases +const CK_FLOOR = 3; // slots below this index are invisible (macro bodies) +const CK_RET = 4; // current return type (-1 = none) +const CK_PARAMS = 5; // vector of current parameter names +const CK_GENERICS = 6; // list of current generic parameter names +const CK_FNNAMES = 7; // names present in the function table, in order +const CK_STAMP = 8; +const CK_TY_I32 = 9; +const CK_TY_BOOL = 10; +const CK_TY_VOID = 11; +const CK_TY_STRLIT = 12; +const CK_TY_I64 = 13; +const CK_NBORROW = 14; // slots that ever stored provenance +const CK_SLOTS = 15; + +fn ck(c: i64, slot: i64) -> i64 { return vget(cx(c, CX_CHK), slot); } +fn ck_set(c: i64, slot: i64, v: i64) -> i64 { return vset(cx(c, CX_CHK), slot, v); } + +fn cfail(c: i64, n: i64, message: i64) -> i64 { + let mut pos: i64 = -1; + if n >= 0 { + pos = npos(c, n); + } + return fail_at(c, pos, "E0200", message); +} + +fn quoted(c: i64, name: i64) -> i64 { + return text_cat3("'", name_text(c, name), "'"); +} + +// ------------------------------------------------------------------ types + +fn ty_custom(c: i64, text: i64) -> i64 { + return node1(c, T_CUSTOM, intern(c, text), -1); +} + +fn ty_ref(c: i64, inner: i64) -> i64 { + return node1(c, T_REF, inner, -1); +} + +fn is_generic_param(c: i64, name: i64) -> bool { + let g = ck(c, CK_GENERICS); + let mut i: i64 = 0; + while i < list_len(c, g) { + if list_at(c, g, i) == name { + return true; + } + i = i + 1; + } + return false; +} + +fn ty_is_copy(c: i64, t: i64) -> bool { + if t < 0 { + return false; + } + let k = nk(c, t); + if k == T_PRIM { + return na(c, t) != PRIM_VOID; + } + if k == T_REF { + return true; + } + if k == T_CUSTOM { + return is_generic_param(c, na(c, t)); + } + return false; +} + +fn ty_is_ref(c: i64, t: i64) -> bool { + return t >= 0 && nk(c, t) == T_REF; +} + +// ----------------------------------------------------- function table + +fn fn_known(c: i64, name: i64) -> bool { return ni(c, name, NI_FNKNOWN) == 1; } + +fn fn_register(c: i64, name: i64, ret: i64) -> i64 { + if !fn_known(c, name) { + ni_set(c, name, NI_FNKNOWN, 1); + vpush(ck(c, CK_FNNAMES), name); + } + ni_set(c, name, NI_FNRET, ret); + return 0; +} + +fn bi(c: i64, name: i64, ret: i64) -> i64 { + return fn_register(c, intern(c, name), ret); +} + +// The prelude of the bootstrap checker (`LinearChecker::check_program`). +fn register_builtins(c: i64) -> i64 { + let t_void = ck(c, CK_TY_VOID); + let t_i32 = ck(c, CK_TY_I32); + let t_i64 = ck(c, CK_TY_I64); + let t_bool = ck(c, CK_TY_BOOL); + let t_u8 = prim_type(c, PRIM_U8); + let t_u32 = prim_type(c, PRIM_U32); + let t_u64 = prim_type(c, PRIM_U64); + let t_str = ty_custom(c, "Str"); + let t_bytes = node1(c, T_PTR, ty_custom(c, "u8"), -1); + bi(c, "print", t_void); + bi(c, "alloc", t_bytes); + bi(c, "memset", t_void); + bi(c, "memcpy", t_void); + bi(c, "memcmp", t_i32); + bi(c, "str_len", t_u32); + bi(c, "str_eq", t_bool); + bi(c, "str_from_literal", t_str); + bi(c, "str_concat", t_str); + bi(c, "vec_init", ty_custom(c, "Vec")); + bi(c, "vec_push", t_void); + bi(c, "vec_get", t_i32); + bi(c, "vec_set", t_void); + bi(c, "vec_pop", t_i32); + bi(c, "print_str", t_void); + bi(c, "print_int", t_void); + bi(c, "print_hex", t_void); + bi(c, "read_line", t_str); + bi(c, "file_open", node1(c, T_PTR, ty_custom(c, "File"), -1)); + bi(c, "file_read_all", t_str); + bi(c, "file_write", t_void); + bi(c, "ring_init", ty_custom(c, "Ring")); + bi(c, "ring_push", t_bool); + bi(c, "ring_pop", t_i32); + bi(c, "ring_peek", t_i32); + bi(c, "ring_full", t_bool); + bi(c, "ring_empty", t_bool); + bi(c, "ring_count", t_u32); + bi(c, "math_clamp", t_i64); + bi(c, "math_random", t_u64); + bi(c, "str_find", t_i64); + bi(c, "str_to_int", t_i64); + bi(c, "int_to_str", t_str); + bi(c, "file_remove", t_i32); + bi(c, "file_size", t_i64); + bi(c, "sys_exec", t_i32); + bi(c, "sys_set_memory_limit", t_void); + bi(c, "sys_get_memory_used", t_i64); + bi(c, "env_get", t_str); + bi(c, "math_abs", t_i64); + bi(c, "math_min", t_i64); + bi(c, "math_max", t_i64); + bi(c, "math_pow", t_i64); + bi(c, "math_sqrt", t_i64); + bi(c, "math_floor", t_i64); + bi(c, "math_ceil", t_i64); + bi(c, "math_round", t_i64); + bi(c, "print_err", t_void); + bi(c, "eprint", t_void); + bi(c, "str_contains", t_i64); + bi(c, "str_is_int", t_i32); + bi(c, "abort", t_void); + bi(c, "env_exists", t_i32); + bi(c, "net_socket_tcp_listen", t_i32); + bi(c, "net_socket_accept", t_i32); + bi(c, "net_socket_connect", t_i32); + bi(c, "net_socket_send", t_i64); + bi(c, "net_socket_recv", t_i64); + bi(c, "net_socket_close", t_void); + bi(c, "os_args_count", t_i32); + bi(c, "os_arg", t_str); + bi(c, "dir_exists", t_bool); + bi(c, "file_copy", t_i32); + bi(c, "process_spawn", t_i32); + bi(c, "char_is_digit", t_bool); + bi(c, "char_is_alpha", t_bool); + bi(c, "char_is_alphanumeric", t_bool); + bi(c, "char_is_space", t_bool); + bi(c, "char_to_upper", t_u8); + bi(c, "char_to_lower", t_u8); + bi(c, "str_starts_with", t_bool); + bi(c, "str_ends_with", t_bool); + bi(c, "str_substr", t_str); + bi(c, "str_trim", t_str); + bi(c, "str_char_at", t_u8); + bi(c, "mem_realloc", t_bytes); + bi(c, "vec_reserve", t_void); + bi(c, "vec_clear", t_void); + bi(c, "vec_len", t_i32); + bi(c, "path_basename", t_str); + bi(c, "path_ext", t_str); + bi(c, "path_join", t_str); + // Runtime support used by the self-hosted compiler (word ABI). + bi(c, "track_i64_collection_new", t_i64); + bi(c, "track_i64_collection_len", t_i64); + bi(c, "track_i64_collection_push", t_i64); + bi(c, "track_i64_collection_get", t_i64); + bi(c, "track_i64_collection_set", t_i64); + bi(c, "track_i64_collection_pop", t_i64); + bi(c, "track_i64_collection_truncate", t_i64); + bi(c, "track_sb_new", t_i64); + bi(c, "track_sb_push", t_i64); + bi(c, "track_sb_push_byte", t_i64); + bi(c, "track_sb_push_int", t_i64); + bi(c, "track_sb_len", t_i64); + bi(c, "track_sb_str", t_i64); + bi(c, "track_sb_clear", t_i64); + bi(c, "track_str_bytes", t_i64); + bi(c, "track_file_read", t_i64); + bi(c, "track_file_write_all", t_i64); + bi(c, "track_out", t_i64); + bi(c, "track_err", t_i64); + bi(c, "track_words_copy", t_i64); + bi(c, "sys_exit", t_i64); + bi(c, "clock_ms", t_i64); + bi(c, "array_len", t_i64); + return 0; +} + +// Exports of the built-in namespaces, as (name, return type) pairs. +fn std_exports(c: i64, path: i64) -> i64 { + let t_void = ck(c, CK_TY_VOID); + let t_i32 = ck(c, CK_TY_I32); + let t_i64 = ck(c, CK_TY_I64); + let t_bool = ck(c, CK_TY_BOOL); + let t_vptr = node1(c, T_PTR, t_void, -1); + let e = vnew(); + if text_eq(path, "std") { + vpush(e, "print"); vpush(e, t_void); + vpush(e, "println"); vpush(e, t_void); + vpush(e, "eprint"); vpush(e, t_void); + vpush(e, "read"); vpush(e, t_i64); + vpush(e, "alloc"); vpush(e, t_vptr); + vpush(e, "dealloc"); vpush(e, t_void); + vpush(e, "exit"); vpush(e, t_void); + vpush(e, "abort"); vpush(e, t_void); + vpush(e, "clock_ms"); vpush(e, t_i64); + vpush(e, "io::print"); vpush(e, t_void); + vpush(e, "io::println"); vpush(e, t_void); + vpush(e, "io::read"); vpush(e, t_i64); + vpush(e, "io::eprint"); vpush(e, t_void); + vpush(e, "fs::file_open"); vpush(e, t_vptr); + vpush(e, "fs::file_close"); vpush(e, t_void); + vpush(e, "fs::file_exists"); vpush(e, t_i32); + vpush(e, "fs::file_copy"); vpush(e, t_i32); + vpush(e, "fs::file_size"); vpush(e, t_i64); + vpush(e, "fs::dir_exists"); vpush(e, t_bool); + vpush(e, "mem::alloc"); vpush(e, t_vptr); + vpush(e, "mem::dealloc"); vpush(e, t_void); + vpush(e, "sys::exit"); vpush(e, t_void); + vpush(e, "sys::abort"); vpush(e, t_void); + vpush(e, "sys::clock_ms"); vpush(e, t_i64); + vpush(e, "char::char_is_digit"); vpush(e, t_bool); + vpush(e, "str::str_find"); vpush(e, t_i64); + vpush(e, "process::process_spawn"); vpush(e, t_i32); + vpush(e, "net::net_socket_tcp_listen"); vpush(e, t_i32); + } else if text_eq(path, "std/io") { + vpush(e, "print"); vpush(e, t_void); + vpush(e, "println"); vpush(e, t_void); + vpush(e, "read"); vpush(e, t_i64); + vpush(e, "eprint"); vpush(e, t_void); + } else if text_eq(path, "std/fs") { + vpush(e, "file_open"); vpush(e, t_vptr); + vpush(e, "file_close"); vpush(e, t_void); + vpush(e, "file_exists"); vpush(e, t_i32); + } else if text_eq(path, "std/sys") { + vpush(e, "exit"); vpush(e, t_void); + vpush(e, "abort"); vpush(e, t_void); + vpush(e, "clock_ms"); vpush(e, t_i64); + } else if text_eq(path, "std/mem") { + vpush(e, "alloc"); vpush(e, t_vptr); + vpush(e, "dealloc"); vpush(e, t_void); + } else if text_eq(path, "math/vec") { + vpush(e, "add"); vpush(e, t_i64); + vpush(e, "sub"); vpush(e, t_i64); + vpush(e, "dot"); vpush(e, t_i64); + vpush(e, "cross"); vpush(e, t_i64); + } + return e; +} + +// Index of `name` (text) in an exports vector, or -1. +fn export_index(exports: i64, name: i64) -> i64 { + let mut i: i64 = 0; + while i < vlen(exports) { + if text_eq(vget(exports, i), name) { + return i; + } + i = i + 2; + } + return -1; +} + +fn check_use(c: i64, n: i64) -> i64 { + let path = na(c, n); + let norm = normalize_path_text(path); + if !builtin_namespace(norm) { + return 0; // local modules are part of the loaded program + } + let exports = std_exports(c, norm); + let imports = nb(c, n); + let alias = nc(c, n); + let mut default_ns = path; + let slash = last_slash(path); + if slash >= 0 { + default_ns = str_substr(path, slash + 1, text_len(path) - slash - 1); + } + if imports >= 0 { + let mut i: i64 = 0; + while i < list_len(c, imports) { + let item = list_at(c, imports, i); + let idx = export_index(exports, name_text(c, item)); + if idx < 0 { + cfail(c, n, text_cat3("Module '", path, text_cat3("' does not export '", name_text(c, item), "'"))); + } + if alias >= 0 && list_len(c, imports) == 1 { + fn_register(c, alias, vget(exports, idx + 1)); + } else if alias >= 0 { + fn_register(c, intern(c, text_cat3(name_text(c, alias), "::", name_text(c, item))), vget(exports, idx + 1)); + } else { + fn_register(c, item, vget(exports, idx + 1)); + } + i = i + 1; + } + return 0; + } + let mut prefix = default_ns; + if alias >= 0 { + prefix = name_text(c, alias); + } + let mut j: i64 = 0; + while j < vlen(exports) { + fn_register(c, intern(c, text_cat3(prefix, "::", vget(exports, j))), vget(exports, j + 1)); + j = j + 2; + } + return 0; +} + +fn normalize_path_text(path: i64) -> i64 { + let bytes = text_bytes(path); + let sb = sb_new(); + let mut i: i64 = 0; + while i < vlen(bytes) { + let b = vget(bytes, i); + if b == 58 && vget(bytes, i + 1) == 58 { + sb_byte(sb, 47); + i = i + 2; + } else { + sb_byte(sb, b); + i = i + 1; + } + } + return sb_text(sb); +} + +fn builtin_namespace(path: i64) -> bool { + if text_eq(path, "std") { return true; } + if text_eq(path, "std/io") { return true; } + if text_eq(path, "std/fs") { return true; } + if text_eq(path, "std/sys") { return true; } + if text_eq(path, "std/mem") { return true; } + return text_eq(path, "math/vec"); +} + +// ------------------------------------------------------------ variables + +fn vars(c: i64) -> i64 { return ck(c, CK_VARS); } +fn var_count(c: i64) -> i64 { return vlen(vars(c)) / VW; } +fn vf(c: i64, slot: i64, field: i64) -> i64 { return vget(vars(c), slot * VW + field); } +fn vf_set(c: i64, slot: i64, field: i64, x: i64) -> i64 { return vset(vars(c), slot * VW + field, x); } + +// Newest visible slot named `name`, or -1. +fn lookup(c: i64, name: i64) -> i64 { + let slot = ni(c, name, NI_VARSLOT); + if slot < ck(c, CK_FLOOR) { + return -1; + } + return slot; +} + +fn declare(c: i64, name: i64, ty: i64, mutable: i64) -> i64 { + let v = vars(c); + let slot = var_count(c); + vpush(v, name); + vpush(v, ST_ACTIVE); + vpush(v, ty); + vpush(v, mutable); + vpush(v, 0); + vpush(v, ni(c, name, NI_VARSLOT)); + vpush(v, -1); + vpush(v, 0); + ni_set(c, name, NI_VARSLOT, slot); + return slot; +} + +// Remove a slot from name lookup (lens names leave scope at block end). +fn kill_slot(c: i64, slot: i64) -> i64 { + let name = vf(c, slot, V_NAME); + if ni(c, name, NI_VARSLOT) == slot { + ni_set(c, name, NI_VARSLOT, vf(c, slot, V_PREV)); + } + vf_set(c, slot, V_DEAD, 1); + if vf(c, slot, V_LENS) == 1 { + ck_set(c, CK_LENSCOUNT, ck(c, CK_LENSCOUNT) - 1); + } + return 0; +} + +fn truncate_vars(c: i64, count: i64) -> i64 { + let mut slot = var_count(c) - 1; + while slot >= count { + if vf(c, slot, V_DEAD) == 0 { + kill_slot(c, slot); + } + slot = slot - 1; + } + vtrunc(vars(c), count * VW); + return 0; +} + +// Live slots that would be present in the bootstrap checker's registry. +fn slot_visible(c: i64, slot: i64) -> bool { + if slot < ck(c, CK_FLOOR) { + return false; + } + if vf(c, slot, V_DEAD) == 1 { + return false; + } + return ni(c, vf(c, slot, V_NAME), NI_VARSLOT) == slot; +} + +// Snapshot: [count, (state, type, borrows)*count, lens-lock length, lens count] +fn snapshot(c: i64) -> i64 { + let n = var_count(c); + let s = vnew_cap(n * 3 + 4); + vpush(s, n); + let mut i: i64 = 0; + while i < n { + vpush(s, vf(c, i, V_STATE)); + vpush(s, vf(c, i, V_TYPE)); + vpush(s, vf(c, i, V_BORROWS)); + i = i + 1; + } + vpush(s, vlen(ck(c, CK_LENSLOCK))); + vpush(s, ck(c, CK_LENSCOUNT)); + return s; +} + +fn restore(c: i64, s: i64) -> i64 { + let n = vget(s, 0); + truncate_vars(c, n); + let mut i: i64 = 0; + while i < n { + vf_set(c, i, V_STATE, vget(s, 1 + i * 3)); + vf_set(c, i, V_TYPE, vget(s, 2 + i * 3)); + vf_set(c, i, V_BORROWS, vget(s, 3 + i * 3)); + i = i + 1; + } + vtrunc(ck(c, CK_LENSLOCK), vget(s, 1 + n * 3)); + ck_set(c, CK_LENSCOUNT, vget(s, 2 + n * 3)); + return 0; +} + +fn state_name(st: i64) -> i64 { + if st == ST_ACTIVE { return "Active"; } + if st == ST_LOCKED { return "Locked"; } + return "Spent"; +} + +fn is_copy_name(c: i64, name: i64, slot: i64) -> bool { + if is_qualified(c, name) { + return true; + } + return ty_is_copy(c, vf(c, slot, V_TYPE)); +} + +fn read_or_move(c: i64, n: i64, name: i64) -> i64 { + let slot = lookup(c, name); + if slot < 0 { + return cfail(c, n, text_cat3("Undeclared variable ", quoted(c, name), ".")); + } + let st = vf(c, slot, V_STATE); + if st == ST_SPENT { + return cfail(c, n, text_cat3("Use-after-free! Resource ", quoted(c, name), " is already spent.")); + } + if st == ST_LOCKED { + return cfail(c, n, text_cat3("Resource ", quoted(c, name), " is frozen (either locked in a lens or borrowed).")); + } + if !is_copy_name(c, name, slot) && vf(c, slot, V_LENS) == 0 { + vf_set(c, slot, V_STATE, ST_SPENT); + } + return 0; +} + +fn lens_locked_name(c: i64, name: i64) -> bool { + let locks = ck(c, CK_LENSLOCK); + let mut i: i64 = 0; + while i < vlen(locks) { + if vget(locks, i) == name { + return true; + } + i = i + 1; + } + return false; +} + +fn update_borrow_states(c: i64) -> i64 { + let n = var_count(c); + let floor = ck(c, CK_FLOOR); + if ck(c, CK_NBORROW) == 0 && vlen(ck(c, CK_LENSLOCK)) == 0 { + let mut i = floor; + while i < n { + if vf(c, i, V_STATE) == ST_LOCKED && slot_visible(c, i) { + vf_set(c, i, V_STATE, ST_ACTIVE); + } + i = i + 1; + } + return 0; + } + let stamp = ck(c, CK_STAMP) + 1; + ck_set(c, CK_STAMP, stamp); + let locks = ck(c, CK_LENSLOCK); + let mut l: i64 = 0; + while l < vlen(locks) { + ni_set(c, vget(locks, l), NI_MARK, stamp); + l = l + 1; + } + let mut i = floor; + while i < n { + let b = vf(c, i, V_BORROWS); + if b >= 0 && vf(c, i, V_STATE) == ST_ACTIVE && slot_visible(c, i) { + let mut j: i64 = 0; + while j < vlen(b) { + ni_set(c, vget(b, j), NI_MARK, stamp); + j = j + 1; + } + } + i = i + 1; + } + let mut s = floor; + while s < n { + if slot_visible(c, s) { + let locked = ni(c, vf(c, s, V_NAME), NI_MARK) == stamp; + let st = vf(c, s, V_STATE); + if locked && st == ST_ACTIVE { + vf_set(c, s, V_STATE, ST_LOCKED); + } else if !locked && st == ST_LOCKED { + vf_set(c, s, V_STATE, ST_ACTIVE); + } + } + s = s + 1; + } + return 0; +} + +fn set_borrows(c: i64, slot: i64, prov: i64) -> i64 { + vf_set(c, slot, V_BORROWS, prov); + if prov >= 0 { + ck_set(c, CK_NBORROW, ck(c, CK_NBORROW) + 1); + } + return 0; +} + +// --------------------------------------------------------- type inference + +fn var_type(c: i64, name: i64) -> i64 { + let slot = lookup(c, name); + if slot < 0 { + return -1; + } + return vf(c, slot, V_TYPE); +} + +fn last_of(c: i64, body: i64) -> i64 { + let n = list_len(c, body); + if n == 0 { + return -1; + } + return list_at(c, body, n - 1); +} + +fn infer_last(c: i64, body: i64) -> i64 { + let last = last_of(c, body); + if last < 0 { + return -1; + } + return infer_type(c, last); +} + +// Replace generic parameter names in `ty` using argument types. +fn subst_generic(c: i64, ty: i64, f: i64, args: i64) -> i64 { + if ty < 0 { + return -1; + } + if nk(c, ty) == T_CUSTOM { + let generics = nb(c, f); + let name = na(c, ty); + let mut g: i64 = 0; + while g < list_len(c, generics) { + if list_at(c, generics, g) == name { + let params = nc(c, f); + let mut p: i64 = 0; + while p < list_len(c, params) && p / 2 < list_len(c, args) { + let pty = list_at(c, params, p + 1); + if nk(c, pty) == T_CUSTOM && na(c, pty) == name { + return infer_type(c, list_at(c, args, p / 2)); + } + p = p + 2; + } + return ck(c, CK_TY_I32); + } + g = g + 1; + } + return ty; + } + if nk(c, ty) == T_TUPLE { + let elems = na(c, ty); + let mark = list_begin(c); + let mut i: i64 = 0; + while i < list_len(c, elems) { + let e = subst_generic(c, list_at(c, elems, i), f, args); + list_item(c, e); + i = i + 1; + } + return node1(c, T_TUPLE, list_end(c, mark), -1); + } + return ty; +} + +fn call_type(c: i64, name: i64, args: i64) -> i64 { + if !fn_known(c, name) { + return -1; + } + let ret = ni(c, name, NI_FNRET); + let f = ni(c, name, NI_FN); + if f >= 0 && nk(c, f) == K_FN && list_len(c, nb(c, f)) > 0 { + return subst_generic(c, ret, f, args); + } + return ret; +} + +fn tuple_elems_of(c: i64, t: i64) -> i64 { + if t < 0 { + return -1; + } + if nk(c, t) == T_TUPLE { + return na(c, t); + } + if nk(c, t) == T_REF { + let inner = na(c, t); + if inner >= 0 && nk(c, inner) == T_TUPLE { + return na(c, inner); + } + } + return -1; +} + +fn infer_type(c: i64, n: i64) -> i64 { + let k = nk(c, n); + if k == K_INT { return ck(c, CK_TY_I32); } + if k == K_BOOL { return ck(c, CK_TY_BOOL); } + if k == K_STR { return ck(c, CK_TY_STRLIT); } + if k == K_VAR { return var_type(c, na(c, n)); } + if k == K_BIN { + if is_comparison_op(na(c, n)) { + return ck(c, CK_TY_BOOL); + } + return infer_type(c, nb(c, n)); + } + if k == K_UN { + let op = na(c, n); + if op == OP_NOT { return ck(c, CK_TY_BOOL); } + if op == OP_NEG { return infer_type(c, nb(c, n)); } + let inner = infer_type(c, nb(c, n)); + if inner >= 0 && (nk(c, inner) == T_PTR || nk(c, inner) == T_REF) { + return na(c, inner); + } + return -1; + } + if k == K_ARRAY { + let elems = na(c, n); + let mut elem = ck(c, CK_TY_I32); + if list_len(c, elems) > 0 { + let first = infer_type(c, list_at(c, elems, 0)); + if first >= 0 { + elem = first; + } + } + return node2(c, T_ARRAY, elem, list_len(c, elems), -1); + } + if k == K_TUPLE { + let elems = na(c, n); + let mark = list_begin(c); + let mut i: i64 = 0; + while i < list_len(c, elems) { + let mut t = infer_type(c, list_at(c, elems, i)); + if t < 0 { + t = ck(c, CK_TY_I32); + } + list_item(c, t); + i = i + 1; + } + return node1(c, T_TUPLE, list_end(c, mark), -1); + } + if k == K_TINDEX { + let elems = tuple_elems_of(c, infer_type(c, na(c, n))); + if elems < 0 || nb(c, n) >= list_len(c, elems) { + return -1; + } + return list_at(c, elems, nb(c, n)); + } + if k == K_INDEX { + let t = infer_type(c, na(c, n)); + if t < 0 { + return -1; + } + let tk = nk(c, t); + if tk == T_ARRAY || tk == T_PTR || tk == T_SLICE { + return na(c, t); + } + if tk == T_REF { + let inner = na(c, t); + let ik = nk(c, inner); + if ik == T_ARRAY || ik == T_PTR || ik == T_SLICE { + return na(c, inner); + } + return inner; + } + return -1; + } + if k == K_SLICE { + let t = infer_type(c, na(c, n)); + if t < 0 { + return -1; + } + let tk = nk(c, t); + if tk == T_ARRAY || tk == T_PTR || tk == T_SLICE { + return node1(c, T_SLICE, na(c, t), -1); + } + if tk == T_REF { + let inner = na(c, t); + let ik = nk(c, inner); + if ik == T_ARRAY || ik == T_PTR || ik == T_SLICE { + return node1(c, T_SLICE, na(c, inner), -1); + } + } + return -1; + } + if k == K_ADDR { + let t = infer_type(c, na(c, n)); + if t < 0 { + return -1; + } + return ty_ref(c, t); + } + if k == K_SINIT { return node1(c, T_CUSTOM, na(c, n), -1); } + if k == K_FIELD { + let mut t = infer_type(c, na(c, n)); + if t >= 0 && nk(c, t) == T_REF { + t = na(c, t); + } + if t < 0 || nk(c, t) != T_CUSTOM { + return -1; + } + let s = struct_def(c, na(c, t)); + if s < 0 { + return -1; + } + let idx = struct_field_index(c, s, nb(c, n)); + if idx < 0 { + return -1; + } + return list_at(c, nb(c, s), idx * 2 + 1); + } + if k == K_CALL { return call_type(c, na(c, n), nb(c, n)); } + if k == K_LENS { return infer_last(c, nc(c, n)); } + if k == K_IF { return infer_last(c, nb(c, n)); } + if k == K_MACROCALL { + if text_eq(name_text(c, na(c, n)), "now") { + return ck(c, CK_TY_I64); + } + return call_type(c, na(c, n), nb(c, n)); + } + if k == K_MATCH { + let arms = nb(c, n); + if list_len(c, arms) == 0 { + return -1; + } + return infer_type(c, list_at(c, arms, 2)); + } + return ck(c, CK_TY_VOID); +} + +// ------------------------------------------------------------ provenance + +fn prov_add(v: i64, name: i64) -> i64 { + let mut i: i64 = 0; + while i < vlen(v) { + if vget(v, i) == name { + return 0; + } + i = i + 1; + } + vpush(v, name); + return 0; +} + +fn prov_merge(dst: i64, src: i64) -> i64 { + let mut i: i64 = 0; + while i < vlen(src) { + prov_add(dst, vget(src, i)); + i = i + 1; + } + return 0; +} + +// Names whose storage an expression may reference. +fn provenance(c: i64, n: i64) -> i64 { + let out = vnew(); + let k = nk(c, n); + if k == K_ADDR { + let target = na(c, n); + if nk(c, target) == K_VAR { + vpush(out, na(c, target)); + } else if nk(c, target) == K_INDEX { + prov_merge(out, provenance(c, na(c, target))); + } else if nk(c, target) == K_FIELD { + let root = place_root(c, target); + if root >= 0 { + vpush(out, root); + } + } else { + prov_merge(out, provenance(c, target)); + } + } else if k == K_VAR { + let slot = lookup(c, na(c, n)); + if slot >= 0 && vf(c, slot, V_BORROWS) >= 0 { + prov_merge(out, vf(c, slot, V_BORROWS)); + } else if slot >= 0 && ty_is_ref(c, vf(c, slot, V_TYPE)) { + vpush(out, na(c, n)); + } + } else if k == K_CALL { + let args = nb(c, n); + let mut i: i64 = 0; + while i < list_len(c, args) { + let a = list_at(c, args, i); + if ty_is_ref(c, infer_type(c, a)) { + prov_merge(out, provenance(c, a)); + } + i = i + 1; + } + } else if k == K_IF { + let t = last_of(c, nb(c, n)); + if t >= 0 { + prov_merge(out, provenance(c, t)); + } + let e = last_of(c, nc(c, n)); + if e >= 0 { + prov_merge(out, provenance(c, e)); + } + } else if k == K_LENS { + let l = last_of(c, nc(c, n)); + if l >= 0 { + prov_merge(out, provenance(c, l)); + } + } + return out; +} + +// Variable at the base of `a.b.0[i]`, or -1. +fn place_root(c: i64, n: i64) -> i64 { + let k = nk(c, n); + if k == K_VAR { + return na(c, n); + } + if k == K_FIELD || k == K_TINDEX || k == K_INDEX { + return place_root(c, na(c, n)); + } + return -1; +} + +fn check_escape(c: i64, n: i64, value: i64) -> i64 { + let prov = provenance(c, value); + let params = ck(c, CK_PARAMS); + let mut i: i64 = 0; + while i < vlen(prov) { + let name = vget(prov, i); + let mut is_param = false; + let mut j: i64 = 0; + while j < vlen(params) { + if vget(params, j) == name { + is_param = true; + } + j = j + 1; + } + if !is_param { + cfail(c, n, text_cat3("Cannot return reference to local variable ", quoted(c, name), " (escapes function scope).")); + } + i = i + 1; + } + return 0; +} + +// ------------------------------------------------------- lens escapes + +fn lens_in_list(c: i64, l: i64) -> i64 { + let mut i: i64 = 0; + while i < list_len(c, l) { + let found = find_lens(c, list_at(c, l, i)); + if found >= 0 { + return found; + } + i = i + 1; + } + return -1; +} + +fn lens_in_either(c: i64, a: i64, b: i64) -> i64 { + if a >= 0 { + let found = find_lens(c, a); + if found >= 0 { + return found; + } + } + if b >= 0 { + return find_lens(c, b); + } + return -1; +} + +// Name of a lens alias the expression would move or store, or -1. +fn find_lens(c: i64, n: i64) -> i64 { + if ck(c, CK_LENSCOUNT) == 0 { + return -1; + } + let k = nk(c, n); + if k == K_VAR { + let slot = lookup(c, na(c, n)); + if slot >= 0 && vf(c, slot, V_LENS) == 1 { + return na(c, n); + } + return -1; + } + if k == K_BIN { return lens_in_either(c, nb(c, n), nc(c, n)); } + if k == K_UN { return find_lens(c, nb(c, n)); } + if k == K_ADDR { return find_lens(c, na(c, n)); } + if k == K_ARRAY || k == K_TUPLE { return lens_in_list(c, na(c, n)); } + if k == K_INDEX { return lens_in_either(c, na(c, n), nb(c, n)); } + if k == K_SLICE { + let found = lens_in_either(c, na(c, n), nb(c, n)); + if found >= 0 { + return found; + } + return lens_in_either(c, nc(c, n), -1); + } + if k == K_RANGE { return lens_in_either(c, na(c, n), nb(c, n)); } + if k == K_SINIT { + let fields = nb(c, n); + let mut i: i64 = 1; + while i < list_len(c, fields) { + let found = find_lens(c, list_at(c, fields, i)); + if found >= 0 { + return found; + } + i = i + 2; + } + return -1; + } + if k == K_IF { + let found = find_lens(c, na(c, n)); + if found >= 0 { + return found; + } + let t = lens_in_list(c, nb(c, n)); + if t >= 0 { + return t; + } + return lens_in_list(c, nc(c, n)); + } + if k == K_WHILE { + let found = find_lens(c, na(c, n)); + if found >= 0 { + return found; + } + return lens_in_list(c, nb(c, n)); + } + if k == K_RETURN { return lens_in_either(c, na(c, n), -1); } + if k == K_ASSIGN { return lens_in_either(c, na(c, n), nb(c, n)); } + if k == K_LET { return find_lens(c, nd(c, n)); } + if k == K_CONST { return find_lens(c, nb(c, n)); } + if k == K_LETD { return find_lens(c, nc(c, n)); } + if k == K_TINDEX { return find_lens(c, na(c, n)); } + if k == K_MACROCALL { + let found = lens_in_list(c, nb(c, n)); + if found >= 0 || nc(c, n) < 0 { + return found; + } + return lens_in_list(c, nc(c, n)); + } + if k == K_LENS { return lens_in_list(c, nc(c, n)); } + if k == K_MATCH { + let found = find_lens(c, na(c, n)); + if found >= 0 { + return found; + } + let arms = nb(c, n); + let mut i: i64 = 0; + while i < list_len(c, arms) { + let arm = lens_in_either(c, list_at(c, arms, i + 1), list_at(c, arms, i + 2)); + if arm >= 0 { + return arm; + } + i = i + 3; + } + return -1; + } + return -1; +} + +fn reject_lens_escape(c: i64, n: i64) -> i64 { + let found = find_lens(c, n); + if found >= 0 { + cfail(c, n, text_cat3("Lens ", quoted(c, found), " cannot be moved, stored, returned, or escape its with block.")); + } + return 0; +} + +// ---------------------------------------------------- suggestion distance + +fn min3(a: i64, b: i64, d: i64) -> i64 { + let mut m = a; + if b < m { m = b; } + if d < m { m = d; } + return m; +} + +fn levenshtein(a: i64, b: i64) -> i64 { + let x = text_bytes(a); + let y = text_bytes(b); + let n = vlen(x); + let m = vlen(y); + let mut prev = vnew_cap(m + 1); + let mut j: i64 = 0; + while j <= m { + vpush(prev, j); + j = j + 1; + } + let mut i: i64 = 1; + while i <= n { + let cur = vnew_cap(m + 1); + vpush(cur, i); + let mut k: i64 = 1; + while k <= m { + let mut cost: i64 = 1; + if vget(x, i - 1) == vget(y, k - 1) { + cost = 0; + } + vpush(cur, min3(vget(prev, k) + 1, vget(cur, k - 1) + 1, vget(prev, k - 1) + cost)); + k = k + 1; + } + prev = cur; + i = i + 1; + } + return vget(prev, m); +} + +fn check_call_target(c: i64, n: i64, name: i64) -> i64 { + let text = name_text(c, name); + if text_eq(text, "free") || text_eq(text, "str_free") || text_eq(text, "vec_free") || text_eq(text, "file_close") { + return cfail(c, n, text_cat3(quoted(c, name), " is managed automatically by the compiler.", " Linear types are freed at their spend points — do not call this directly.")); + } + let target = last_segment(c, name); + if fn_known(c, name) || fn_known(c, target) { + return 0; + } + let target_text = name_text(c, target); + let target_len = text_len(target_text); + let names = ck(c, CK_FNNAMES); + let mut best: i64 = -1; + let mut best_dist: i64 = 3; + let mut i: i64 = 0; + while i < vlen(names) { + let cand = name_text(c, vget(names, i)); + let diff = text_len(cand) - target_len; + if diff <= 2 && diff >= -2 { + let dist = levenshtein(target_text, cand); + if dist < best_dist { + best_dist = dist; + best = vget(names, i); + } + } + i = i + 1; + } + if best >= 0 { + cfail(c, n, text_cat3("Undefined function ", quoted(c, name), text_cat3(". Did you mean ", quoted(c, best), "?"))); + } + return 0; +} + +// --------------------------------------------------------------- checking + +fn check_borrow(c: i64, n: i64) -> i64 { + if nk(c, n) != K_VAR { + return check_expr(c, n); + } + let name = na(c, n); + let slot = lookup(c, name); + if slot < 0 { + return cfail(c, n, text_cat3("Undeclared variable ", quoted(c, name), ".")); + } + let st = vf(c, slot, V_STATE); + if st == ST_SPENT { + return cfail(c, n, text_cat3("Cannot borrow spent resource ", quoted(c, name), ".")); + } + if st == ST_LOCKED { + return cfail(c, n, text_cat3("Cannot borrow locked resource ", quoted(c, name), ".")); + } + return 0; +} + +fn check_list(c: i64, l: i64) -> i64 { + let mut i: i64 = 0; + while i < list_len(c, l) { + check_expr(c, list_at(c, l, i)); + i = i + 1; + } + return 0; +} + +// Statements of a block, updating borrow states after each. +fn check_body(c: i64, body: i64) -> i64 { + let mut i: i64 = 0; + while i < list_len(c, body) { + check_expr(c, list_at(c, body, i)); + update_borrow_states(c); + i = i + 1; + } + return 0; +} + +fn variant_binding_type(c: i64, owner: i64, variant: i64) -> i64 { + let o = name_text(c, owner); + let v = name_text(c, variant); + if text_eq(o, "Value") && text_eq(v, "Float") { + return ck(c, CK_TY_I64); + } + if text_eq(o, "Value") && text_eq(v, "Bool") { + return ck(c, CK_TY_BOOL); + } + if text_eq(o, "Result") && (text_eq(v, "Ok") || text_eq(v, "Err")) { + return ck(c, CK_TY_I64); + } + return ck(c, CK_TY_I32); +} + +fn bind_pattern(c: i64, p: i64, ty: i64, mutable: i64) -> i64 { + let k = nk(c, p); + if k == P_IDENT { + declare(c, na(c, p), ty, mutable); + return 0; + } + if k == P_TUPLE { + let pats = na(c, p); + let mut elems: i64 = -1; + if ty >= 0 && nk(c, ty) == T_TUPLE { + elems = na(c, ty); + } + let mut i: i64 = 0; + while i < list_len(c, pats) { + let mut et = ck(c, CK_TY_I32); + if elems >= 0 && i < list_len(c, elems) { + et = list_at(c, elems, i); + } + bind_pattern(c, list_at(c, pats, i), et, mutable); + i = i + 1; + } + return 0; + } + if k == P_VARIANT { + let bindings = nc(c, p); + let bt = variant_binding_type(c, na(c, p), nb(c, p)); + let mut i: i64 = 0; + while i < list_len(c, bindings) { + bind_pattern(c, list_at(c, bindings, i), bt, mutable); + i = i + 1; + } + return 0; + } + if k == P_STRUCT { + let fields = nb(c, p); + let mut i: i64 = 1; + while i < list_len(c, fields) { + bind_pattern(c, list_at(c, fields, i), ck(c, CK_TY_I32), mutable); + i = i + 2; + } + } + return 0; +} + +fn check_if(c: i64, n: i64) -> i64 { + check_expr(c, na(c, n)); + let pre = snapshot(c); + let count = vget(pre, 0); + check_body(c, nb(c, n)); + let then_end = snapshot(c); + restore(c, pre); + check_body(c, nc(c, n)); + let mut i: i64 = 0; + while i < count { + if slot_visible(c, i) { + let then_state = vget(then_end, 1 + i * 3); + let else_state = vf(c, i, V_STATE); + if then_state != else_state { + let name = vf(c, i, V_NAME); + cfail(c, n, text_cat3("Resource ", quoted(c, name), text_cat3(" has inconsistent state after if/else. Then branch: ", state_name(then_state), text_cat(", Else branch: ", state_name(else_state))))); + } + } + i = i + 1; + } + // Merge: state agrees, types revert to the pre-branch types, and + // provenance comes from the then branch when it has any. + let mut merged = vnew_cap(count * 2); + let mut j: i64 = 0; + while j < count { + let mut b = vget(then_end, 3 + j * 3); + if b < 0 { + b = vf(c, j, V_BORROWS); + } + vpush(merged, vf(c, j, V_STATE)); + vpush(merged, b); + j = j + 1; + } + restore(c, pre); + let mut s: i64 = 0; + while s < count { + vf_set(c, s, V_STATE, vget(merged, s * 2)); + vf_set(c, s, V_BORROWS, vget(merged, s * 2 + 1)); + s = s + 1; + } + update_borrow_states(c); + return 0; +} + +fn check_loop_body(c: i64, n: i64, body: i64, pre: i64, long_message: bool) -> i64 { + check_body(c, body); + let count = vget(pre, 0); + let mut i: i64 = 0; + while i < count { + let name = vf(c, i, V_NAME); + let pre_state = vget(pre, 1 + i * 3); + if slot_visible(c, i) && pre_state == ST_ACTIVE && !is_copy_name(c, name, i) && vf(c, i, V_STATE) == ST_SPENT { + if long_message { + cfail(c, n, text_cat3("Cannot consume linear resource ", quoted(c, name), " inside a loop. It was declared outside the loop and would be double-freed/moved on subsequent iterations.")); + } + cfail(c, n, text_cat3("Cannot consume linear resource ", quoted(c, name), " inside a loop.")); + } + i = i + 1; + } + restore(c, pre); + update_borrow_states(c); + return 0; +} + +fn for_elem_type(c: i64, t: i64) -> i64 { + if t >= 0 && (nk(c, t) == T_SLICE || nk(c, t) == T_ARRAY) { + return na(c, t); + } + return ck(c, CK_TY_I32); +} + +fn check_function(c: i64, n: i64, is_macro: bool) -> i64 { + let pre = snapshot(c); + let saved_floor = ck(c, CK_FLOOR); + let saved_params = ck(c, CK_PARAMS); + let saved_ret = ck(c, CK_RET); + let saved_generics = ck(c, CK_GENERICS); + let saved_locks = ck(c, CK_LENSLOCK); + let saved_lens = ck(c, CK_LENSCOUNT); + if is_macro { + // Macro bodies start from an empty scope (no globals). + ck_set(c, CK_FLOOR, var_count(c)); + } + ck_set(c, CK_LENSLOCK, vnew()); + ck_set(c, CK_LENSCOUNT, 0); + ck_set(c, CK_GENERICS, nb(c, n)); + ck_set(c, CK_RET, nd(c, n)); + let params = nc(c, n); + let names = vnew(); + let mut i: i64 = 0; + while i < list_len(c, params) { + vpush(names, list_at(c, params, i)); + declare(c, list_at(c, params, i), list_at(c, params, i + 1), 0); + i = i + 2; + } + ck_set(c, CK_PARAMS, names); + update_borrow_states(c); + let body = ne(c, n); + check_body(c, body); + if !is_macro && ty_is_ref(c, nd(c, n)) { + let mut has_return = false; + let mut j: i64 = 0; + while j < list_len(c, body) { + if nk(c, list_at(c, body, j)) == K_RETURN { + has_return = true; + } + j = j + 1; + } + let last = last_of(c, body); + if !has_return && last >= 0 { + check_escape(c, last, last); + } + } + ck_set(c, CK_FLOOR, saved_floor); + ck_set(c, CK_PARAMS, saved_params); + ck_set(c, CK_RET, saved_ret); + ck_set(c, CK_GENERICS, saved_generics); + ck_set(c, CK_LENSLOCK, saved_locks); + restore(c, pre); + ck_set(c, CK_LENSCOUNT, saved_lens); + return 0; +} + +fn check_lens(c: i64, n: i64) -> i64 { + let target = na(c, n); + let slot = lookup(c, target); + if slot < 0 || vf(c, slot, V_STATE) != ST_ACTIVE { + return cfail(c, n, text_cat3("Cannot create lens. ", quoted(c, target), " is not Active.")); + } + vpush(ck(c, CK_LENSLOCK), target); + vf_set(c, slot, V_STATE, ST_LOCKED); + let lens = declare(c, nb(c, n), vf(c, slot, V_TYPE), 0); + vf_set(c, lens, V_LENS, 1); + ck_set(c, CK_LENSCOUNT, ck(c, CK_LENSCOUNT) + 1); + let body = nc(c, n); + let mut i: i64 = 0; + while i < list_len(c, body) { + check_expr(c, list_at(c, body, i)); + i = i + 1; + } + // exit_lens: unlock the target and drop the alias + let locks = ck(c, CK_LENSLOCK); + let kept = vnew(); + let mut j: i64 = 0; + while j < vlen(locks) { + if vget(locks, j) != target { + vpush(kept, vget(locks, j)); + } + j = j + 1; + } + vtrunc(locks, 0); + prov_merge(locks, kept); + let tslot = lookup(c, target); + if tslot >= 0 && vf(c, tslot, V_STATE) == ST_LOCKED { + vf_set(c, tslot, V_STATE, ST_ACTIVE); + } + update_borrow_states(c); + kill_slot(c, lens); + return 0; +} + +fn check_match(c: i64, n: i64) -> i64 { + let target = na(c, n); + check_expr(c, target); + let mut tt = infer_type(c, target); + if tt < 0 { + tt = ck(c, CK_TY_I32); + } + let arms = nb(c, n); + let mut i: i64 = 0; + while i < list_len(c, arms) { + let pre = snapshot(c); + bind_pattern(c, list_at(c, arms, i), tt, 0); + let guard = list_at(c, arms, i + 1); + if guard >= 0 { + check_expr(c, guard); + } + check_expr(c, list_at(c, arms, i + 2)); + restore(c, pre); + i = i + 3; + } + return 0; +} + +// Assignment target `p.x`, `t.0`, or `a[i]`: the root variable must be +// usable and either `let mut` or a lens alias. +fn check_place(c: i64, place: i64) -> i64 { + let k = nk(c, place); + if k == K_FIELD || k == K_TINDEX { + return check_place(c, na(c, place)); + } + if k == K_INDEX { + check_borrow(c, nb(c, place)); + return check_place(c, na(c, place)); + } + if k == K_VAR { + check_borrow(c, place); + let name = na(c, place); + let slot = lookup(c, name); + if vf(c, slot, V_MUT) == 0 && vf(c, slot, V_LENS) == 0 { + return cfail(c, place, text_cat3("Cannot mutate immutable variable ", quoted(c, name), text_cat3(". Use 'let mut ", name_text(c, name), "' to declare a mutable variable."))); + } + return 0; + } + return check_expr(c, place); +} + +fn check_assign(c: i64, n: i64) -> i64 { + let target = na(c, n); + let value = nb(c, n); + reject_lens_escape(c, value); + check_expr(c, value); + if nk(c, target) == K_VAR { + let name = na(c, target); + let slot = lookup(c, name); + if slot < 0 { + return cfail(c, n, text_cat3("Undeclared variable ", quoted(c, name), ".")); + } + if vf(c, slot, V_MUT) == 0 { + return cfail(c, n, text_cat3("Cannot mutate immutable variable ", quoted(c, name), text_cat3(". Use 'let mut ", name_text(c, name), "' to declare a mutable variable."))); + } + vf_set(c, slot, V_STATE, ST_ACTIVE); + let ty = infer_type(c, value); + if ty >= 0 { + vf_set(c, slot, V_TYPE, ty); + if ty_is_ref(c, ty) { + set_borrows(c, slot, provenance(c, value)); + } else { + vf_set(c, slot, V_BORROWS, -1); + } + } + } else { + check_place(c, target); + } + update_borrow_states(c); + return 0; +} + +fn check_expr(c: i64, n: i64) -> i64 { + let k = nk(c, n); + if k == K_INT || k == K_STR || k == K_BOOL || k == K_TYPEDEF || k == K_STRUCT { + return 0; + } + if k == K_VAR { + return read_or_move(c, n, na(c, n)); + } + if k == K_BIN { + check_borrow(c, nb(c, n)); + return check_borrow(c, nc(c, n)); + } + if k == K_UN { + return check_expr(c, nb(c, n)); + } + if k == K_ARRAY || k == K_TUPLE { + return check_list(c, na(c, n)); + } + if k == K_TINDEX { + check_borrow(c, na(c, n)); + let elems = tuple_elems_of(c, infer_type(c, na(c, n))); + if elems >= 0 && nb(c, n) >= list_len(c, elems) { + cfail(c, n, text_cat3("Tuple index ", text_of_int(nb(c, n)), text_cat(" out of bounds for tuple of length ", text_of_int(list_len(c, elems))))); + } + return 0; + } + if k == K_INDEX || k == K_RANGE { + check_borrow(c, na(c, n)); + return check_borrow(c, nb(c, n)); + } + if k == K_SLICE { + check_borrow(c, na(c, n)); + if nb(c, n) >= 0 { + check_borrow(c, nb(c, n)); + } + if nc(c, n) >= 0 { + check_borrow(c, nc(c, n)); + } + return 0; + } + if k == K_ADDR { + return check_borrow(c, na(c, n)); + } + if k == K_FIELD { + check_borrow(c, na(c, n)); + if vlen(field_candidates(c, nb(c, n))) == 0 { + cfail(c, n, text_cat3("Unknown field ", quoted(c, nb(c, n)), " (no struct declares it).")); + } + return 0; + } + if k == K_SINIT { + let fields = nb(c, n); + let decl = struct_def(c, na(c, n)); + if decl >= 0 { + let mut f: i64 = 0; + while f < list_len(c, fields) { + if struct_field_index(c, decl, list_at(c, fields, f)) < 0 { + cfail(c, n, text_cat3("Struct ", quoted(c, na(c, n)), text_cat3(" has no field ", quoted(c, list_at(c, fields, f)), "."))); + } + f = f + 2; + } + } + let mut i: i64 = 1; + while i < list_len(c, fields) { + reject_lens_escape(c, list_at(c, fields, i)); + check_expr(c, list_at(c, fields, i)); + i = i + 2; + } + return 0; + } + if k == K_CALL { + check_call_target(c, n, na(c, n)); + return check_list(c, nb(c, n)); + } + if k == K_LET { + let value = nd(c, n); + reject_lens_escape(c, value); + check_expr(c, value); + let mut ty = nc(c, n); + if ty < 0 { + ty = infer_type(c, value); + } + if ty < 0 { + ty = ck(c, CK_TY_VOID); + } + let slot = declare(c, na(c, n), ty, nb(c, n)); + if ty_is_ref(c, ty) { + set_borrows(c, slot, provenance(c, value)); + } + return update_borrow_states(c); + } + if k == K_LENS { + return check_lens(c, n); + } + if k == K_IF { + return check_if(c, n); + } + if k == K_WHILE { + check_expr(c, na(c, n)); + let pre = snapshot(c); + return check_loop_body(c, n, nb(c, n), pre, true); + } + if k == K_FOR { + let iter = nb(c, n); + check_expr(c, iter); + let elem = for_elem_type(c, infer_type(c, iter)); + let pre = snapshot(c); + declare(c, na(c, n), elem, 0); + return check_loop_body(c, n, nc(c, n), pre, false); + } + if k == K_RETURN { + let value = na(c, n); + if value >= 0 { + reject_lens_escape(c, value); + check_expr(c, value); + if ty_is_ref(c, ck(c, CK_RET)) { + check_escape(c, n, value); + } + } + return 0; + } + if k == K_ASSIGN { + return check_assign(c, n); + } + if k == K_FN { + return check_function(c, n, false); + } + if k == K_MACRODEF { + return check_function(c, n, true); + } + if k == K_USE { + return check_use(c, n); + } + if k == K_CONST { + check_expr(c, nb(c, n)); + let mut ty = infer_type(c, nb(c, n)); + if ty < 0 { + ty = ck(c, CK_TY_VOID); + } + declare(c, na(c, n), ty, 0); + return update_borrow_states(c); + } + if k == K_MACROCALL { + if text_eq(name_text(c, na(c, n)), "compile_error") { + let args = nb(c, n); + if list_len(c, args) > 0 && nk(c, list_at(c, args, 0)) == K_STR { + return cfail(c, n, na(c, list_at(c, args, 0))); + } + return cfail(c, n, "@compile_error requires a string message"); + } + check_list(c, nb(c, n)); + if nc(c, n) >= 0 { + check_body(c, nc(c, n)); + } + return 0; + } + if k == K_ENUM { + declare_enum(c, n); + let variants = nc(c, n); + let mut i: i64 = 1; + while i < list_len(c, variants) { + if list_at(c, variants, i) >= 0 { + check_expr(c, list_at(c, variants, i)); + } + i = i + 2; + } + return 0; + } + if k == K_UNION { + return declare_union(c, n); + } + if k == K_LETD { + let value = nc(c, n); + reject_lens_escape(c, value); + check_expr(c, value); + let mut ty = infer_type(c, value); + if ty < 0 { + ty = ck(c, CK_TY_I32); + } + bind_pattern(c, na(c, n), ty, nb(c, n)); + return update_borrow_states(c); + } + if k == K_MATCH { + return check_match(c, n); + } + return 0; +} + +fn declare_enum(c: i64, n: i64) -> i64 { + let name = na(c, n); + let ty = node1(c, T_CUSTOM, name, -1); + let variants = nc(c, n); + let mut i: i64 = 0; + while i < list_len(c, variants) { + declare(c, variant_name(c, name, list_at(c, variants, i)), ty, 0); + i = i + 2; + } + return 0; +} + +fn declare_union(c: i64, n: i64) -> i64 { + let name = na(c, n); + let ty = node1(c, T_CUSTOM, name, -1); + let variants = nb(c, n); + let mut i: i64 = 0; + while i < list_len(c, variants) { + let vname = variant_name(c, name, list_at(c, variants, i)); + if list_at(c, variants, i + 1) >= 0 { + fn_register(c, vname, ty); + } else { + declare(c, vname, ty, 0); + } + i = i + 2; + } + return 0; +} + +fn checker_init(c: i64) -> i64 { + let s = vfill(vnew_cap(CK_SLOTS), CK_SLOTS, 0); + vset(c, CX_CHK, s); + ck_set(c, CK_VARS, vnew_cap(4096)); + ck_set(c, CK_LENSLOCK, vnew()); + ck_set(c, CK_PARAMS, vnew()); + ck_set(c, CK_FNNAMES, vnew_cap(1024)); + ck_set(c, CK_RET, -1); + ck_set(c, CK_GENERICS, 0); + ck_set(c, CK_TY_I32, prim_type(c, PRIM_I32)); + ck_set(c, CK_TY_BOOL, prim_type(c, PRIM_BOOL)); + ck_set(c, CK_TY_VOID, prim_type(c, PRIM_VOID)); + ck_set(c, CK_TY_I64, prim_type(c, PRIM_I64)); + ck_set(c, CK_TY_STRLIT, node1(c, T_PTR, prim_type(c, PRIM_I32), -1)); + register_builtins(c); + return 0; +} + +// Check the whole loaded program. Exits with a diagnostic on failure. +fn check_program(c: i64) -> i64 { + checker_init(c); + let items = cx(c, CX_ITEMS); + // Pass 1: declarations visible to every function. + let mut i: i64 = 0; + while i < vlen(items) { + let n = vget(items, i); + let k = nk(c, n); + if k == K_ENUM { + declare_enum(c, n); + } else if k == K_UNION { + declare_union(c, n); + } else if k == K_FN || k == K_MACRODEF { + fn_register(c, na(c, n), nd(c, n)); + } else if k == K_CONST { + let mut ty = infer_type(c, nb(c, n)); + if ty < 0 { + ty = ck(c, CK_TY_VOID); + } + declare(c, na(c, n), ty, 0); + } + i = i + 1; + } + // Pass 2: check every item in load order. + let mut j: i64 = 0; + while j < vlen(items) { + check_expr(c, vget(items, j)); + j = j + 1; + } + return 0; +} diff --git a/compiler/src/lexer.trk b/compiler/src/lexer.trk index ac5656e..d04f7b9 100644 --- a/compiler/src/lexer.trk +++ b/compiler/src/lexer.trk @@ -1,659 +1,433 @@ -// lexer.trk — Native tokenizer core (v0.7) -// Self-contained for `yard build` — duplicates `Token` from `token.trk` -// so codegen sees the union in this compilation unit. `token.trk` remains -// the canonical definition; this copy must stay in sync. +// lexer.trk — tokenizer for Track source files. +// +// Mirrors the bootstrap lexer (`src/lexer/mod.rs`): whitespace is +// [ \t\r\n], `//` comments run to the end of the line, identifiers are +// ASCII [A-Za-z_][A-Za-z0-9_]*, integers are decimal i64, and strings are +// unescaped at lex time. Each token is a TK_WIDTH-word record +// (kind, value, start, end) with positions in the global position space. -union Token { - Import, Use, Let, Mut, With, Fn, Return, If, Else, While, For, In, True, False, - Struct, Enum, Union, Match, As, AtMacro, Const, TypeDef, At, - TyU8, TyI8, TyI32, TyU32, TyI64, TyU64, TyBool, TyVoid, TyPtr, - Int(i64), StrLit(Str), Ident(Str), - Eq, EqEq, Neq, Bang, LBrace, RBrace, LParen, RParen, LBracket, RBracket, - Comma, Colon, ColonColon, Arrow, FatArrow, Semicolon, DotDot, Dot, - Underscore, Amp, Pipe, Star, Plus, Minus, Slash, Percent, Lt, Gt, Lte, Gte, - AmpAmp, PipePipe, Shl, Shr, Eof, Unknown, -} +import "base"; + +const TK_WIDTH = 4; + +const TK_EOF = 0; +const TK_INT = 1; +const TK_STR = 2; +const TK_IDENT = 3; + +// Keywords (their spellings are interned first, in this order, so the +// name id of a keyword spelling is its index in `keyword_spellings`). +const TK_IMPORT = 10; +const TK_USE = 11; +const TK_LET = 12; +const TK_MUT = 13; +const TK_WITH = 14; +const TK_FN = 15; +const TK_RETURN = 16; +const TK_IF = 17; +const TK_ELSE = 18; +const TK_WHILE = 19; +const TK_FOR = 20; +const TK_IN = 21; +const TK_TRUE = 22; +const TK_FALSE = 23; +const TK_STRUCT = 24; +const TK_ENUM = 25; +const TK_UNION = 26; +const TK_MATCH = 27; +const TK_AS = 28; +const TK_CONST = 29; +const TK_TYPE = 30; +const TK_TY_U8 = 31; +const TK_TY_I8 = 32; +const TK_TY_I32 = 33; +const TK_TY_U32 = 34; +const TK_TY_I64 = 35; +const TK_TY_U64 = 36; +const TK_TY_BOOL = 37; +const TK_TY_VOID = 38; +const TK_TY_PTR = 39; +const KW_COUNT = 30; -fn keyword_or_ident_extended(lex: Str) -> Token { - let eq_use = str_eq(&lex, "use"); - if eq_use { let _used = lex; return Token::Use; } else { - let eq_with = str_eq(&lex, "with"); - if eq_with { let _used = lex; return Token::With; } else { - let eq_for = str_eq(&lex, "for"); - if eq_for { let _used = lex; return Token::For; } else { - let eq_in = str_eq(&lex, "in"); - if eq_in { let _used = lex; return Token::In; } else { - let eq_as = str_eq(&lex, "as"); - if eq_as { let _used = lex; return Token::As; } else { - let eq_const = str_eq(&lex, "const"); - if eq_const { let _used = lex; return Token::Const; } else { - let eq_type = str_eq(&lex, "type"); - if eq_type { let _used = lex; return Token::TypeDef; } else { - let eq_u8 = str_eq(&lex, "u8"); - if eq_u8 { let _used = lex; return Token::TyU8; } else { - let eq_i8 = str_eq(&lex, "i8"); - if eq_i8 { let _used = lex; return Token::TyI8; } else { - let eq_i32 = str_eq(&lex, "i32"); - if eq_i32 { let _used = lex; return Token::TyI32; } else { - let eq_u32 = str_eq(&lex, "u32"); - if eq_u32 { let _used = lex; return Token::TyU32; } else { - let eq_i64 = str_eq(&lex, "i64"); - if eq_i64 { let _used = lex; return Token::TyI64; } else { - let eq_u64 = str_eq(&lex, "u64"); - if eq_u64 { let _used = lex; return Token::TyU64; } else { - let eq_bool = str_eq(&lex, "bool"); - if eq_bool { let _used = lex; return Token::TyBool; } else { - let eq_void = str_eq(&lex, "void"); - if eq_void { let _used = lex; return Token::TyVoid; } else { - let eq_ptr = str_eq(&lex, "ptr"); - if eq_ptr { let _used = lex; return Token::TyPtr; } else { - return keyword_or_ident(lex); - } } } } } } } } } } } } } } } } +const TK_ATMACRO = 40; +const TK_AT = 41; +const TK_EQ = 60; +const TK_EQEQ = 61; +const TK_NEQ = 62; +const TK_BANG = 63; +const TK_LBRACE = 64; +const TK_RBRACE = 65; +const TK_LPAREN = 66; +const TK_RPAREN = 67; +const TK_LBRACKET = 68; +const TK_RBRACKET = 69; +const TK_COMMA = 70; +const TK_COLON = 71; +const TK_COLONCOLON = 72; +const TK_ARROW = 73; +const TK_FATARROW = 74; +const TK_SEMI = 75; +const TK_DOTDOT = 76; +const TK_DOT = 77; +const TK_UNDERSCORE = 78; +const TK_AMP = 79; +const TK_PIPE = 80; +const TK_STAR = 81; +const TK_PLUS = 82; +const TK_MINUS = 83; +const TK_SLASH = 84; +const TK_PERCENT = 85; +const TK_LT = 86; +const TK_GT = 87; +const TK_LTE = 88; +const TK_GTE = 89; +const TK_AMPAMP = 90; +const TK_PIPEPIPE = 91; +const TK_SHL = 92; +const TK_SHR = 93; + +fn keyword_spelling(i: i64) -> i64 { + if i == 0 { return "import"; } + if i == 1 { return "use"; } + if i == 2 { return "let"; } + if i == 3 { return "mut"; } + if i == 4 { return "with"; } + if i == 5 { return "fn"; } + if i == 6 { return "return"; } + if i == 7 { return "if"; } + if i == 8 { return "else"; } + if i == 9 { return "while"; } + if i == 10 { return "for"; } + if i == 11 { return "in"; } + if i == 12 { return "true"; } + if i == 13 { return "false"; } + if i == 14 { return "struct"; } + if i == 15 { return "enum"; } + if i == 16 { return "union"; } + if i == 17 { return "match"; } + if i == 18 { return "as"; } + if i == 19 { return "const"; } + if i == 20 { return "type"; } + if i == 21 { return "u8"; } + if i == 22 { return "i8"; } + if i == 23 { return "i32"; } + if i == 24 { return "u32"; } + if i == 25 { return "i64"; } + if i == 26 { return "u64"; } + if i == 27 { return "bool"; } + if i == 28 { return "void"; } + return "ptr"; } -fn keyword_or_ident(lex: Str) -> Token { - // Keep this table in lock-step with src/lexer/mod.rs. Parser work depends - // on every reserved word being classified before it reaches the AST. - let eq_import = str_eq(&lex, "import"); - if eq_import { - let _c = lex; - return Token::Import; - } else { - let eq_let = str_eq(&lex, "let"); - if eq_let { - let _c2 = lex; - return Token::Let; - } else { - let eq_mut = str_eq(&lex, "mut"); - if eq_mut { - let _c3 = lex; - return Token::Mut; - } else { - let eq_fn = str_eq(&lex, "fn"); - if eq_fn { - let _c4 = lex; - return Token::Fn; - } else { - let eq_return = str_eq(&lex, "return"); - if eq_return { - let _c5 = lex; - return Token::Return; - } else { - let eq_if = str_eq(&lex, "if"); - if eq_if { - let _c6 = lex; - return Token::If; - } else { - let eq_else = str_eq(&lex, "else"); - if eq_else { - let _c7 = lex; - return Token::Else; - } else { - let eq_while = str_eq(&lex, "while"); - if eq_while { - let _c8 = lex; - return Token::While; - } else { - let eq_true = str_eq(&lex, "true"); - if eq_true { - let _c9 = lex; - return Token::True; - } else { - let eq_false = str_eq(&lex, "false"); - if eq_false { - let _c10 = lex; - return Token::False; - } else { - let eq_struct = str_eq(&lex, "struct"); - if eq_struct { - let _c11 = lex; - return Token::Struct; - } else { - let eq_enum = str_eq(&lex, "enum"); - if eq_enum { - let _c12 = lex; - return Token::Enum; - } else { - let eq_union = str_eq(&lex, "union"); - if eq_union { - let _c13 = lex; - return Token::Union; - } else { - let eq_match = str_eq(&lex, "match"); - if eq_match { - let _c14 = lex; - return Token::Match; - } else { - return Token::Ident(lex); - } - } - } - } - } - } - } - } - } - } - } - } - } - } +// Intern keyword spellings so that name id == keyword index. Must run on a +// fresh context before anything else is interned. +fn lex_init(c: i64) -> i64 { + let mut i: i64 = 0; + while i < KW_COUNT { + intern(c, keyword_spelling(i)); + i = i + 1; + } + return 0; } -fn token_kind_name(tok: Token) -> Str { - let mut out = "Other"; - match tok { - Token::Import => out = "Import", - Token::Use => out = "Use", - Token::Let => out = "Let", - Token::Mut => out = "Mut", - Token::With => out = "With", - Token::Fn => out = "Fn", - Token::Return => out = "Return", - Token::If => out = "If", - Token::Else => out = "Else", - Token::While => out = "While", - Token::For => out = "For", - Token::In => out = "In", - Token::True => out = "True", - Token::False => out = "False", - Token::Struct => out = "Struct", - Token::Enum => out = "Enum", - Token::Union => out = "Union", - Token::Match => out = "Match", - Token::As => out = "As", - Token::Const => out = "Const", - Token::TypeDef => out = "TypeDef", - Token::TyU8 => out = "TyU8", - Token::TyI8 => out = "TyI8", - Token::TyI32 => out = "TyI32", - Token::TyU32 => out = "TyU32", - Token::TyI64 => out = "TyI64", - Token::TyU64 => out = "TyU64", - Token::TyBool => out = "TyBool", - Token::TyVoid => out = "TyVoid", - Token::TyPtr => out = "TyPtr", - Token::Eof => out = "Eof", - Token::Unknown => out = "Unknown", - Token::Int(_) => out = "Int", - Token::StrLit(_) => out = "StrLit", - Token::Ident(_) => out = "Ident", - _ => out = "Other", - } - return out; +fn tok_count(c: i64) -> i64 { return vlen(cx(c, CX_TOKS)) / TK_WIDTH; } +fn tok_kind(c: i64, t: i64) -> i64 { return vget(cx(c, CX_TOKS), t * TK_WIDTH); } +fn tok_value(c: i64, t: i64) -> i64 { return vget(cx(c, CX_TOKS), t * TK_WIDTH + 1); } +fn tok_start(c: i64, t: i64) -> i64 { return vget(cx(c, CX_TOKS), t * TK_WIDTH + 2); } +fn tok_end(c: i64, t: i64) -> i64 { return vget(cx(c, CX_TOKS), t * TK_WIDTH + 3); } + +fn push_token(c: i64, kind: i64, value: i64, start: i64, end: i64) -> i64 { + let toks = cx(c, CX_TOKS); + vpush(toks, kind); + vpush(toks, value); + vpush(toks, start); + vpush(toks, end); + return 0; } -fn token_kind_code(tok: Token) -> i64 { - let mut out: i64 = 0; - match tok { - Token::Use => out = 1, - Token::With => out = 2, - Token::For => out = 3, - Token::In => out = 4, - Token::As => out = 5, - Token::Const => out = 6, - Token::TypeDef => out = 7, - Token::TyI32 => out = 8, - Token::TyU64 => out = 9, - Token::TyBool => out = 10, - Token::TyVoid => out = 11, - Token::TyPtr => out = 12, - _ => out = 0, - } - return out; +fn is_ident_start_byte(b: i64) -> bool { + if b >= 97 && b <= 122 { return true; } + if b >= 65 && b <= 90 { return true; } + return b == 95; } -fn is_alpha_char(c: u8) -> bool { - return char_is_alpha(c); +fn is_digit_byte(b: i64) -> bool { + return b >= 48 && b <= 57; } -fn is_digit_char(c: u8) -> bool { - return char_is_digit(c); +fn is_ident_byte(b: i64) -> bool { + if is_ident_start_byte(b) { return true; } + return is_digit_byte(b); } -fn is_ident_start(c: u8) -> bool { - if char_is_alpha(c) { - return true; - } - if c == 95 { - return true; - } - return false; +fn is_space_byte(b: i64) -> bool { + if b == 32 || b == 10 { return true; } + return b == 9 || b == 13; } -fn is_ident_continue(c: u8) -> bool { - if char_is_alphanumeric(c) { - return true; +// Byte at `i`, or -1 past the end. +fn byte_at(bytes: i64, i: i64) -> i64 { + if i >= vlen(bytes) { + return -1; } - if c == 95 { - return true; - } - return false; + return vget(bytes, i); } -fn skip_ws_and_comments(src: &Str, pos: i64) -> i64 { - let len = str_len(src); - let mut p = pos; - let mut skipping = true; - while p < len && skipping { - let ch = str_char_at(src, p); - if char_is_space(ch) { - p = p + 1; - } else { - if ch == 47 { - let nxt = str_char_at(src, p + 1); - if nxt == 47 { - p = p + 2; - let mut in_comment = true; - while p < len && in_comment { - let c2 = str_char_at(src, p); - if c2 == 10 { - in_comment = false; - p = p + 1; - } else { - p = p + 1; - } - } - } else { - skipping = false; - } - } else { - skipping = false; - } - } - } - return p; +// Punctuation token kind starting at `i`, and its length (0 if none). +fn punct(b: i64, b2: i64) -> (i64, i64) { + if b == 61 { + if b2 == 61 { return (TK_EQEQ, 2); } + if b2 == 62 { return (TK_FATARROW, 2); } + return (TK_EQ, 1); + } + if b == 33 { + if b2 == 61 { return (TK_NEQ, 2); } + return (TK_BANG, 1); + } + if b == 58 { + if b2 == 58 { return (TK_COLONCOLON, 2); } + return (TK_COLON, 1); + } + if b == 45 { + if b2 == 62 { return (TK_ARROW, 2); } + return (TK_MINUS, 1); + } + if b == 46 { + if b2 == 46 { return (TK_DOTDOT, 2); } + return (TK_DOT, 1); + } + if b == 60 { + if b2 == 61 { return (TK_LTE, 2); } + if b2 == 60 { return (TK_SHL, 2); } + return (TK_LT, 1); + } + if b == 62 { + if b2 == 61 { return (TK_GTE, 2); } + if b2 == 62 { return (TK_SHR, 2); } + return (TK_GT, 1); + } + if b == 38 { + if b2 == 38 { return (TK_AMPAMP, 2); } + return (TK_AMP, 1); + } + if b == 124 { + if b2 == 124 { return (TK_PIPEPIPE, 2); } + return (TK_PIPE, 1); + } + if b == 123 { return (TK_LBRACE, 1); } + if b == 125 { return (TK_RBRACE, 1); } + if b == 40 { return (TK_LPAREN, 1); } + if b == 41 { return (TK_RPAREN, 1); } + if b == 91 { return (TK_LBRACKET, 1); } + if b == 93 { return (TK_RBRACKET, 1); } + if b == 44 { return (TK_COMMA, 1); } + if b == 59 { return (TK_SEMI, 1); } + if b == 42 { return (TK_STAR, 1); } + if b == 43 { return (TK_PLUS, 1); } + if b == 47 { return (TK_SLASH, 1); } + if b == 37 { return (TK_PERCENT, 1); } + return (0, 0); } -fn lex_number(src: &Str, start: i64) -> (Token, i64) { - let len = str_len(src); - let mut p = start; - let mut val: i64 = 0; - let mut cont = true; - while p < len && cont { - let ch = str_char_at(src, p); - if char_is_digit(ch) { - let digit: i64 = ch - 48; - val = val * 10 + digit; - p = p + 1; - } else { - cont = false; - } +fn lex_ident(c: i64, bytes: i64, start: i64, base: i64) -> i64 { + let mut i = start; + while is_ident_byte(byte_at(bytes, i)) { + i = i + 1; + } + if i - start == 1 && vget(bytes, start) == 95 { + push_token(c, TK_UNDERSCORE, 0, base + start, base + i); + return i; + } + let sb = cx(c, CX_SB); + sb_reset(sb); + let mut j = start; + while j < i { + sb_byte(sb, vget(bytes, j)); + j = j + 1; + } + let id = intern(c, sb_text(sb)); + if id < KW_COUNT { + push_token(c, TK_IMPORT + id, id, base + start, base + i); + } else { + push_token(c, TK_IDENT, id, base + start, base + i); } - let tok = Token::Int(val); - return (tok, p); + return i; } -fn lex_ident(src: &Str, start: i64) -> (Token, i64) { - let len = str_len(src); - let mut p = start; - let mut cont = true; - while p < len && cont { - let ch = str_char_at(src, p); - if is_ident_continue(ch) { - p = p + 1; - } else { - cont = false; +fn lex_int(c: i64, bytes: i64, start: i64, base: i64) -> i64 { + let mut i = start; + let mut value: i64 = 0; + while is_digit_byte(byte_at(bytes, i)) { + let digit = vget(bytes, i) - 48; + if value > (9223372036854775807 - digit) / 10 { + fail_at(c, base + start, "E0002", "integer literal is out of range for i64"); } + value = value * 10 + digit; + i = i + 1; } - let slice_len = p - start; - let lexeme = str_substr(src, start, slice_len); - let tok = keyword_or_ident_extended(lexeme); - return (tok, p); + push_token(c, TK_INT, value, base + start, base + i); + return i; } -fn lex_string(src: &Str, start: i64) -> (Token, i64) { - let len = str_len(src); - let mut p = start + 1; +fn lex_string(c: i64, bytes: i64, start: i64, base: i64) -> i64 { + let sb = cx(c, CX_SB); + sb_reset(sb); + let mut i = start + 1; let mut open = true; - while p < len && open { - let ch = str_char_at(src, p); - if ch == 92 { - p = p + 2; - } else { - if ch == 34 { - p = p + 1; - open = false; + while open { + let b = byte_at(bytes, i); + if b < 0 { + fail_at(c, base + start, "E0003", "unterminated string literal"); + } + if b == 34 { + open = false; + i = i + 1; + } else if b == 92 { + let e = byte_at(bytes, i + 1); + if e < 0 { + fail_at(c, base + start, "E0003", "unterminated string literal"); + } + if e == 110 { + sb_byte(sb, 10); + } else if e == 114 { + sb_byte(sb, 13); + } else if e == 116 { + sb_byte(sb, 9); + } else if e == 48 { + sb_byte(sb, 0); + } else if e == 34 || e == 39 || e == 92 { + sb_byte(sb, e); } else { - p = p + 1; + sb_byte(sb, 92); + sb_byte(sb, e); } - } - } - let inner_len = p - start - 2; - let inner = str_substr(src, start + 1, inner_len); - let tok = Token::StrLit(inner); - return (tok, p); -} - -fn lex_next(src: &Str, pos: i64) -> (Token, i64) { - let p0 = skip_ws_and_comments(src, pos); - let len = str_len(src); - if p0 >= len { - return (Token::Eof, p0); - } - let ch = str_char_at(src, p0); - let ch2 = str_char_at(src, p0 + 1); - - if ch == 34 { - return lex_string(src, p0); - } - if char_is_digit(ch) { - return lex_number(src, p0); - } - if ch == 95 && !is_ident_continue(ch2) { - return (Token::Underscore, p0 + 1); - } - if is_ident_start(ch) { - return lex_ident(src, p0); - } - - if ch == 58 { - if ch2 == 58 { - return (Token::ColonColon, p0 + 2); - } - return (Token::Colon, p0 + 1); - } - if ch == 61 { - if ch2 == 61 { - return (Token::EqEq, p0 + 2); - } - if ch2 == 62 { - return (Token::FatArrow, p0 + 2); - } - return (Token::Eq, p0 + 1); - } - if ch == 33 { - if ch2 == 61 { - return (Token::Neq, p0 + 2); - } - return (Token::Bang, p0 + 1); - } - if ch == 60 { - if ch2 == 61 { - return (Token::Lte, p0 + 2); - } - if ch2 == 60 { - return (Token::Shl, p0 + 2); - } - return (Token::Lt, p0 + 1); - } - if ch == 62 { - if ch2 == 61 { - return (Token::Gte, p0 + 2); - } - if ch2 == 62 { - return (Token::Shr, p0 + 2); - } - return (Token::Gt, p0 + 1); - } - if ch == 45 { - if ch2 == 62 { - return (Token::Arrow, p0 + 2); - } - return (Token::Minus, p0 + 1); - } - if ch == 46 { - if ch2 == 46 { - return (Token::DotDot, p0 + 2); - } - return (Token::Dot, p0 + 1); - } - if ch == 38 { - if ch2 == 38 { - return (Token::AmpAmp, p0 + 2); - } - return (Token::Amp, p0 + 1); - } - if ch == 124 { - if ch2 == 124 { - return (Token::PipePipe, p0 + 2); - } - return (Token::Pipe, p0 + 1); - } - - if ch == 123 { - return (Token::LBrace, p0 + 1); - } - if ch == 125 { - return (Token::RBrace, p0 + 1); - } - if ch == 40 { - return (Token::LParen, p0 + 1); - } - if ch == 41 { - return (Token::RParen, p0 + 1); - } - if ch == 91 { - return (Token::LBracket, p0 + 1); - } - if ch == 93 { - return (Token::RBracket, p0 + 1); - } - if ch == 44 { - return (Token::Comma, p0 + 1); - } - if ch == 59 { - return (Token::Semicolon, p0 + 1); - } - if ch == 42 { - return (Token::Star, p0 + 1); - } - if ch == 43 { - return (Token::Plus, p0 + 1); - } - if ch == 47 { - return (Token::Slash, p0 + 1); - } - if ch == 37 { - return (Token::Percent, p0 + 1); - } - if ch == 95 { - return (Token::Underscore, p0 + 1); - } - if ch == 64 { - let maybe = str_substr(src, p0, 6); - let is_macro = str_eq(&maybe, "@macro"); - if is_macro { - let _c = maybe; - return (Token::AtMacro, p0 + 6); + i = i + 2; } else { - let _c2 = maybe; - return (Token::At, p0 + 1); + sb_byte(sb, b); + i = i + 1; } } - - return (Token::Unknown, p0 + 1); -} - -// Parser-facing API: preserves the token's end-exclusive byte range while -// retaining the existing two-value lexer API for v0.7 callers. -fn lex_next_spanned(src: &Str, pos: i64) -> (Token, i64, i64) { - let start = skip_ws_and_comments(src, pos); - let (token, next) = lex_next(src, pos); - return (token, start, next); -} - -// Stable parser token record: (tag, start byte, end byte, next byte). -// Payloads are recovered from the source span, avoiding ABI coupling between -// tagged-union payloads and the bootstrap parser's object files. -type TokenRecord = (i64, i64, i64, i64); - -fn token_tag_eof() -> i64 { return 0; } -fn token_tag_let() -> i64 { return 3; } -fn token_tag_mut() -> i64 { return 4; } -fn token_tag_return() -> i64 { return 7; } -fn token_tag_true() -> i64 { return 13; } -fn token_tag_false() -> i64 { return 14; } -fn token_tag_int() -> i64 { return 32; } -fn token_tag_string() -> i64 { return 33; } -fn token_tag_identifier() -> i64 { return 34; } -fn token_tag_eq() -> i64 { return 35; } -fn token_tag_bang() -> i64 { return 38; } -fn token_tag_lparen() -> i64 { return 41; } -fn token_tag_rparen() -> i64 { return 42; } -fn token_tag_semicolon() -> i64 { return 50; } -fn token_tag_underscore() -> i64 { return 53; } -fn token_tag_star() -> i64 { return 56; } -fn token_tag_plus() -> i64 { return 57; } -fn token_tag_minus() -> i64 { return 58; } -fn token_tag_slash() -> i64 { return 59; } -fn token_tag_percent() -> i64 { return 60; } - -fn token_text_tag(text: &Str) -> i64 { - if str_eq(text, "import") { return 1; } - if str_eq(text, "use") { return 2; } - if str_eq(text, "let") { return 3; } - if str_eq(text, "mut") { return 4; } - if str_eq(text, "with") { return 5; } - if str_eq(text, "fn") { return 6; } - if str_eq(text, "return") { return 7; } - if str_eq(text, "if") { return 8; } - if str_eq(text, "else") { return 9; } - if str_eq(text, "while") { return 10; } - if str_eq(text, "for") { return 11; } - if str_eq(text, "in") { return 12; } - if str_eq(text, "true") { return 13; } - if str_eq(text, "false") { return 14; } - if str_eq(text, "struct") { return 15; } - if str_eq(text, "enum") { return 16; } - if str_eq(text, "union") { return 17; } - if str_eq(text, "match") { return 18; } - if str_eq(text, "as") { return 19; } - if str_eq(text, "const") { return 21; } - if str_eq(text, "type") { return 22; } - if str_eq(text, "u8") { return 23; } - if str_eq(text, "i8") { return 24; } - if str_eq(text, "i32") { return 25; } - if str_eq(text, "u32") { return 26; } - if str_eq(text, "i64") { return 27; } - if str_eq(text, "u64") { return 28; } - if str_eq(text, "bool") { return 29; } - if str_eq(text, "void") { return 30; } - if str_eq(text, "ptr") { return 31; } - if str_eq(text, "_") { return 53; } - return 34; -} - -fn token_symbol_tag(ch: u8, ch2: u8) -> i64 { - if ch == 61 && ch2 == 61 { return 36; } - if ch == 61 && ch2 == 62 { return 49; } - if ch == 33 && ch2 == 61 { return 37; } - if ch == 58 && ch2 == 58 { return 47; } - if ch == 45 && ch2 == 62 { return 48; } - if ch == 46 && ch2 == 46 { return 51; } - if ch == 60 && ch2 == 61 { return 63; } - if ch == 62 && ch2 == 61 { return 64; } - if ch == 38 && ch2 == 38 { return 65; } - if ch == 124 && ch2 == 124 { return 66; } - if ch == 60 && ch2 == 60 { return 67; } - if ch == 62 && ch2 == 62 { return 68; } - if ch == 61 { return 35; } - if ch == 33 { return 38; } - if ch == 123 { return 39; } - if ch == 125 { return 40; } - if ch == 40 { return 41; } - if ch == 41 { return 42; } - if ch == 91 { return 43; } - if ch == 93 { return 44; } - if ch == 44 { return 45; } - if ch == 58 { return 46; } - if ch == 59 { return 50; } - if ch == 46 { return 52; } - if ch == 38 { return 54; } - if ch == 124 { return 55; } - if ch == 95 { return 53; } - if ch == 42 { return 56; } - if ch == 43 { return 57; } - if ch == 45 { return 58; } - if ch == 47 { return 59; } - if ch == 37 { return 60; } - if ch == 60 { return 61; } - if ch == 62 { return 62; } - if ch == 64 { return 69; } - return -1; + push_token(c, TK_STR, sb_text(sb), base + start, base + i); + return i; } -fn lex_next_record(src: &Str, pos: i64) -> TokenRecord { - let (_token, start, end) = lex_next_spanned(src, pos); - let length: i64 = str_len(src); - if start >= length { - return (token_tag_eof(), start, end, end); - } - let ch = str_char_at(src, start); - let ch2 = str_char_at(src, start + 1); - if char_is_digit(ch) { - return (token_tag_int(), start, end, end); - } - if ch == 34 { - return (token_tag_string(), start, end, end); - } - if is_ident_start(ch) { - let text = str_substr(src, start, end - start); - let tag = token_text_tag(&text); - return (tag, start, end, end); - } - if ch == 64 { - let macro_text = str_substr(src, start, end - start); - if str_eq(¯o_text, "@macro") { - return (20, start, end, end); +fn lex_at(c: i64, bytes: i64, start: i64, base: i64) -> i64 { + let mut is_macro = true; + let word: i64 = "macro"; + let mut k: i64 = 0; + while k < 5 { + if byte_at(bytes, start + 1 + k) != str_char_at(word, k) { + is_macro = false; } + k = k + 1; } - return (token_symbol_tag(ch, ch2), start, end, end); -} - -fn is_eof_token(tok: Token) -> bool { - let mut out = false; - match tok { - Token::Eof => out = true, - _ => out = false, + if is_macro { + push_token(c, TK_ATMACRO, 0, base + start, base + start + 6); + return start + 6; } - return out; + push_token(c, TK_AT, 0, base + start, base + start + 1); + return start + 1; } -fn count_tokens(src: &Str) -> i64 { - let len = str_len(src); - let mut pos: i64 = 0; - let mut count: i64 = 0; - let mut done = false; - while pos < len { - if done { - pos = len; +// Tokenize file `f`, appending its tokens (terminated by TK_EOF) to the +// token table. Returns the index of the file's first token. +fn lex_file(c: i64, f: i64) -> i64 { + let bytes = vget(cx(c, CX_BYTES), f); + let base = vget(cx(c, CX_BASES), f); + let first = tok_count(c); + let n = vlen(bytes); + let mut i: i64 = 0; + while i < n { + let b = vget(bytes, i); + let b2 = byte_at(bytes, i + 1); + if is_space_byte(b) { + i = i + 1; + } else if b == 47 && b2 == 47 { + while i < n && vget(bytes, i) != 10 { + i = i + 1; + } + } else if is_ident_start_byte(b) { + i = lex_ident(c, bytes, i, base); + } else if is_digit_byte(b) { + i = lex_int(c, bytes, i, base); + } else if b == 34 { + i = lex_string(c, bytes, i, base); + } else if b == 64 { + i = lex_at(c, bytes, i, base); } else { - let (tok, next) = lex_next(src, pos); - let eof = is_eof_token(tok); - if eof { - done = true; - } else { - count = count + 1; - pos = next; - if count > 100 { - done = true; - } + let (kind, width) = punct(b, b2); + if width == 0 { + fail_at(c, base + i, "E0001", "unexpected character"); } + push_token(c, kind, 0, base + i, base + i + width); + i = i + width; } } - return count; + push_token(c, TK_EOF, 0, base + n, base + n); + return first; } -fn dump_tokens(src: &Str) -> void { - let len = str_len(src); - let mut pos: i64 = 0; - let mut done = false; - while pos < len { - if done { - pos = len; - } else { - let (tok, next) = lex_next(src, pos); - let kind = token_kind_name(tok); - let is_eof = str_eq(&kind, "Eof"); - if is_eof { - let _c = kind; - done = true; - } else { - print(kind); - pos = next; - } - } - } +// Human-readable token kind, used by diagnostics and `trackc tokens`. +fn token_kind_text(kind: i64) -> i64 { + if kind == TK_EOF { return "end of file"; } + if kind == TK_INT { return "integer"; } + if kind == TK_STR { return "string"; } + if kind == TK_IDENT { return "identifier"; } + if kind >= TK_IMPORT && kind < TK_IMPORT + KW_COUNT { + return text_cat3("'", keyword_spelling(kind - TK_IMPORT), "'"); + } + if kind == TK_ATMACRO { return "'@macro'"; } + if kind == TK_AT { return "'@'"; } + if kind == TK_EQ { return "'='"; } + if kind == TK_EQEQ { return "'=='"; } + if kind == TK_NEQ { return "'!='"; } + if kind == TK_BANG { return "'!'"; } + if kind == TK_LBRACE { return "'{'"; } + if kind == TK_RBRACE { return "'}'"; } + if kind == TK_LPAREN { return "'('"; } + if kind == TK_RPAREN { return "')'"; } + if kind == TK_LBRACKET { return "'['"; } + if kind == TK_RBRACKET { return "']'"; } + if kind == TK_COMMA { return "','"; } + if kind == TK_COLON { return "':'"; } + if kind == TK_COLONCOLON { return "'::'"; } + if kind == TK_ARROW { return "'->'"; } + if kind == TK_FATARROW { return "'=>'"; } + if kind == TK_SEMI { return "';'"; } + if kind == TK_DOTDOT { return "'..'"; } + if kind == TK_DOT { return "'.'"; } + if kind == TK_UNDERSCORE { return "'_'"; } + if kind == TK_AMP { return "'&'"; } + if kind == TK_PIPE { return "'|'"; } + if kind == TK_STAR { return "'*'"; } + if kind == TK_PLUS { return "'+'"; } + if kind == TK_MINUS { return "'-'"; } + if kind == TK_SLASH { return "'/'"; } + if kind == TK_PERCENT { return "'%'"; } + if kind == TK_LT { return "'<'"; } + if kind == TK_GT { return "'>'"; } + if kind == TK_LTE { return "'<='"; } + if kind == TK_GTE { return "'>='"; } + if kind == TK_AMPAMP { return "'&&'"; } + if kind == TK_PIPEPIPE { return "'||'"; } + if kind == TK_SHL { return "'<<'"; } + return "'>>'"; +} + +// Register a source file and return its id. Positions of file `f` start at +// the end of the previous file plus one, so every byte has a unique +// global position. +fn add_source(c: i64, path: i64, src: i64) -> i64 { + let srcs = cx(c, CX_SRCS); + let bases = cx(c, CX_BASES); + let f = vlen(srcs); + let mut base: i64 = 0; + if f > 0 { + base = vget(bases, f - 1) + vlen(vget(cx(c, CX_BYTES), f - 1)) + 1; + } + vpush(srcs, src); + vpush(cx(c, CX_PATHS), path); + vpush(bases, base); + vpush(cx(c, CX_BYTES), text_bytes(src)); + return f; } diff --git a/compiler/src/lexer_test.trk b/compiler/src/lexer_test.trk deleted file mode 100644 index 16811e8..0000000 --- a/compiler/src/lexer_test.trk +++ /dev/null @@ -1,139 +0,0 @@ -// lexer_test.trk — native unit tests for the self-hosted lexer (v0.7) -// Run via `yard test` (discovers src/*_test.trk). Each `test_*` aborts on -// failure; `main` prints `ok` on success. Uses the same `count_tokens` / -// `lex_next` that `main.trk` uses, so the suite validates the real lexer. - -import "lexer"; - -fn assert_eq_i64(a: i64, b: i64) -> void { - if a != b { - abort("assert_eq_i64 failed"); - } -} - -fn assert_eq_bool(a: bool, b: bool) -> void { - if a != b { - abort("assert_eq_bool failed"); - } -} - -// --- helpers --- - -fn test_simple_let() -> void { - let src = "let x = 42;"; - let c = count_tokens(&src); - // Let Ident Eq Int Semicolon = 5 - assert_eq_i64(c, 5); -} - -fn test_import() -> void { - let src = "import \"std/io\";"; - let c = count_tokens(&src); - // Import StrLit Semicolon = 3 - assert_eq_i64(c, 3); -} - -fn test_fn() -> void { - let src = "fn main() -> void { }"; - let c = count_tokens(&src); - // Fn Ident LParen RParen Arrow TyVoid LBrace RBrace = 8 - assert_eq_i64(c, 8); -} - -fn test_if_else() -> void { - let src = "if true { } else { }"; - let c = count_tokens(&src); - // If True LBrace RBrace Else LBrace RBrace = 7 - assert_eq_i64(c, 7); -} - -fn test_ops() -> void { - let src = "x + y - z * w / v % u"; - let c = count_tokens(&src); - // Ident Plus Ident Minus Ident Star Ident Slash Ident Percent Ident = 11 - assert_eq_i64(c, 11); -} - -fn test_cmp() -> void { - let src = "a == b != c <= d >= e"; - let c = count_tokens(&src); - // Ident EqEq Ident Neq Ident Lte Ident Gte Ident = 9 - assert_eq_i64(c, 9); -} - -fn test_logic() -> void { - let src = "a && b || c"; - let c = count_tokens(&src); - // Ident AmpAmp Ident PipePipe Ident = 5 - assert_eq_i64(c, 5); -} - -fn test_shift() -> void { - let src = "a << b >> c"; - let c = count_tokens(&src); - // Ident Shl Ident Shr Ident = 5 - assert_eq_i64(c, 5); -} - -fn test_string_and_comment() -> void { - let src = "\"hello\" // comment\n let"; - let c = count_tokens(&src); - // StrLit Let = 2 (comment and whitespace skipped) - assert_eq_i64(c, 2); -} - -fn test_lex_next_kind() -> void { - let src = "let"; - let c = count_tokens(&src); - assert_eq_i64(c, 1); -} - -fn test_lex_next_spanned_skips_trivia() -> void { - let src = " // lead\nlet"; - let (tok, start, end) = lex_next_spanned(&src, 0); - let kind = token_kind_name(tok); - let is_let = str_eq(&kind, "Let"); - assert_eq_bool(is_let, true); - assert_eq_i64(start, 10); - assert_eq_i64(end, 13); -} - -fn test_parser_token_records() -> void { - let src = "let value = 42;"; - let let_record = lex_next_record(&src, 0); - assert_eq_i64(let_record.0, token_tag_let()); - assert_eq_i64(let_record.1, 0); - assert_eq_i64(let_record.2, 3); - - let ident_record = lex_next_record(&src, let_record.3); - assert_eq_i64(ident_record.0, token_tag_identifier()); - let eq_record = lex_next_record(&src, ident_record.3); - assert_eq_i64(eq_record.0, token_tag_eq()); - let int_record = lex_next_record(&src, eq_record.3); - assert_eq_i64(int_record.0, token_tag_int()); -} - -fn test_standalone_underscore_record() -> void { - let src = "_ name"; - let underscore = lex_next_record(&src, 0); - assert_eq_i64(underscore.0, token_tag_underscore()); - let name = lex_next_record(&src, underscore.3); - assert_eq_i64(name.0, token_tag_identifier()); -} - -fn main() -> void { - test_simple_let(); - test_import(); - test_fn(); - test_if_else(); - test_ops(); - test_cmp(); - test_logic(); - test_shift(); - test_string_and_comment(); - test_lex_next_kind(); - test_lex_next_spanned_skips_trivia(); - test_parser_token_records(); - test_standalone_underscore_record(); - print("ok"); -} diff --git a/compiler/src/loader.trk b/compiler/src/loader.trk new file mode 100644 index 0000000..1a65f4c --- /dev/null +++ b/compiler/src/loader.trk @@ -0,0 +1,134 @@ +// loader.trk — whole-program module loading. +// +// The root file and every local module it imports (transitively) are +// lexed, parsed, and appended to CX_ITEMS in the order the bootstrap +// checker visits them: an `import` of a not-yet-loaded module splices that +// module's items in before the importer's remaining items. Built-in +// namespaces (`std`, `std/io`, ...) have no source file. + +import "base"; +import "lexer"; +import "ast"; +import "parser"; + +fn is_builtin_module(path: i64) -> bool { + if text_eq(path, "std") { return true; } + if text_eq(path, "std/io") { return true; } + if text_eq(path, "std/fs") { return true; } + if text_eq(path, "std/sys") { return true; } + if text_eq(path, "std/mem") { return true; } + return text_eq(path, "math/vec"); +} + +// `a::b` → `a/b` +fn normalize_module_path(path: i64) -> i64 { + let bytes = text_bytes(path); + let sb = sb_new(); + let mut i: i64 = 0; + while i < vlen(bytes) { + let b = vget(bytes, i); + if b == 58 && vget(bytes, i + 1) == 58 { + sb_byte(sb, 47); + i = i + 2; + } else { + sb_byte(sb, b); + i = i + 1; + } + } + return sb_text(sb); +} + +fn join_path(dir: i64, rest: i64) -> i64 { + if text_len(dir) == 0 { + return rest; + } + if text_ends(dir, "/") { + return text_cat(dir, rest); + } + return text_cat3(dir, "/", rest); +} + +// Candidate files for `import "p"` from a file in `dir`: beside the +// importer first, then the bootstrap compiler's project-relative layout. +fn resolve_module(dir: i64, path: i64) -> i64 { + let p = normalize_module_path(path); + let file = text_cat(p, ".trk"); + let mut candidates = vnew(); + vpush(candidates, join_path(dir, file)); + vpush(candidates, join_path(dir, text_cat(p, "/mod.trk"))); + vpush(candidates, text_cat("src/", file)); + vpush(candidates, file); + vpush(candidates, text_cat("compiler/src/", file)); + vpush(candidates, text_cat("compiler/", file)); + vpush(candidates, text_cat3("src/", p, "/mod.trk")); + let mut i: i64 = 0; + while i < vlen(candidates) { + let cand = vget(candidates, i); + if path_exists(cand) { + return cand; + } + i = i + 1; + } + return 0; +} + +// Canonical key for a module file so `./a.trk` and `a.trk` load once. +fn module_key(path: i64) -> i64 { + if text_starts(path, "./") { + return str_substr(path, 2, text_len(path) - 2); + } + return path; +} + +fn module_loaded(c: i64, key: i64) -> bool { + let mods = cx(c, CX_MODS); + let mut i: i64 = 0; + while i < vlen(mods) { + if text_eq(vget(mods, i), key) { + return true; + } + i = i + 1; + } + return false; +} + +fn load_module(c: i64, path: i64) -> i64 { + vpush(cx(c, CX_MODS), module_key(path)); + if !path_exists(path) { + fail(c, "E0010", text_cat3("cannot read source file '", path, "'")); + } + let src = read_text(path); + let f = add_source(c, path, src); + let first = lex_file(c, f); + let items = parse_file(c, first); + let dir = path_dir(path); + let all = cx(c, CX_ITEMS); + let files = cx(c, CX_ITEMFILE); + let mut i: i64 = 0; + while i < list_len(c, items) { + let item = list_at(c, items, i); + if nk(c, item) == K_USE { + let mpath = na(c, item); + if !is_builtin_module(normalize_module_path(mpath)) { + let found = resolve_module(dir, mpath); + if found == 0 { + fail_at(c, npos(c, item), "E0011", text_cat3("Unknown module '", mpath, "'")); + } + if !module_loaded(c, module_key(found)) { + load_module(c, found); + } + } + } + vpush(all, item); + vpush(files, f); + i = i + 1; + } + return f; +} + +// Load `path` and everything it imports. Returns the root file id. +fn load_program(c: i64, path: i64) -> i64 { + let root = load_module(c, path); + vset(c, CX_MAINFILE, root); + return root; +} diff --git a/compiler/src/main.trk b/compiler/src/main.trk index 6afdbc7..1bf7c8b 100644 --- a/compiler/src/main.trk +++ b/compiler/src/main.trk @@ -1,19 +1,270 @@ -// main.trk — Demo driver for the self-hosted lexer (v0.7) -// Keeps `main` tiny; all heavy lifting lives in `lexer.trk`. -// Build via `yard` so artifacts land in `compiler/target/` (no root clutter). -// When invoked with an argument, dumps the token stream for that input -// (oracle mode for diffing against the Rust lexer). +// main.trk — trackc, the self-hosted Track compiler. +// +// trackc build [-o ] compile to a native executable +// trackc run [args...] build to a temporary file and run +// trackc check parse and ownership-check only +// trackc emit-c [-o ] write the generated C +// trackc tokens dump the token stream +// trackc --version +// +// Pipeline: loader (lexer + parser for every imported module) → checker → +// C emitter → the platform C compiler (`$CC`, default `cc`) with the +// runtime from runtime/track_runtime.c written beside the generated code. +import "base"; import "lexer"; +import "ast"; +import "parser"; +import "loader"; +import "symbols"; +import "checker"; +import "cgen"; +import "runtime_c"; -fn main() -> void { - let argc = os_args_count(); - if argc > 1 { - let src = os_arg(1); - dump_tokens(&src); - } else { - let src2 = "import \"std/io\"; fn main() -> void { let x = 42; // comment\n let s = \"hello\"; }"; - let c = count_tokens(&src2); - print(c); +fn trackc_version() -> i64 { return "1.0.0"; } + +fn usage() -> i64 { + err("trackc 1.0.0 — the self-hosted Track compiler\n\n"); + err("USAGE:\n"); + err(" trackc build [-o ] Compile to a native executable\n"); + err(" trackc run [args...] Compile and run\n"); + err(" trackc check Parse and ownership-check only\n"); + err(" trackc emit-c [-o ] Write the generated C\n"); + err(" trackc tokens Dump the token stream\n"); + err(" trackc --version Show version info\n"); + return 0; +} + +fn front_end(path: i64) -> i64 { + let c = cx_new(); + lex_init(c); + load_program(c, path); + collect_symbols(c); + check_program(c); + return c; +} + +// Value following `-o`, or 0. +fn output_flag() -> i64 { + let mut i: i64 = 2; + while i < arg_count() - 1 { + if text_eq(arg_text(i), "-o") { + return arg_text(i + 1); + } + i = i + 1; + } + return 0; +} + +// Wrap in single quotes for /bin/sh. +fn shell_quote(s: i64) -> i64 { + let bytes = text_bytes(s); + let b = sb_new(); + sb_byte(b, 39); + let mut i: i64 = 0; + while i < vlen(bytes) { + if vget(bytes, i) == 39 { + sb_add(b, "'\\''"); + } else { + sb_byte(b, vget(bytes, i)); + } + i = i + 1; + } + sb_byte(b, 39); + return sb_text(b); +} + +fn file_stem(path: i64) -> i64 { + let slash = last_slash(path); + let mut name = path; + if slash >= 0 { + name = str_substr(path, slash + 1, text_len(path) - slash - 1); + } + if text_ends(name, ".trk") { + return str_substr(name, 0, text_len(name) - 4); + } + return name; +} + +fn c_compiler() -> i64 { + if env_exists("CC") != 0 { + let cc: i64 = env_get("CC"); + if text_len(cc) > 0 { + return cc; + } + } + return "cc"; +} + +// Write `.c` and `.rt.c`, then compile them inside the output +// directory so the object files only record the base names; equal inputs +// therefore produce byte-identical executables in any directory. +fn compile_c(c_source: i64, exe: i64) -> i64 { + let rt = sb_new(); + runtime_source(rt); + let program_c = text_cat(exe, ".c"); + let runtime_file = text_cat(exe, ".rt.c"); + if write_text(program_c, c_source) != 0 { + err(text_cat3("trackc: cannot write '", program_c, "'\n")); + quit(1); + } + if write_text(runtime_file, sb_text(rt)) != 0 { + err(text_cat3("trackc: cannot write '", runtime_file, "'\n")); + quit(1); + } + let mut dir = path_dir(exe); + if text_len(dir) == 0 { + dir = "."; + } + let base = file_stem(exe); + let cmd = sb_new(); + sb_add(cmd, "cd "); + sb_add(cmd, shell_quote(dir)); + sb_add(cmd, " && "); + sb_add(cmd, c_compiler()); + sb_add(cmd, " -std=gnu99 -O2 -fwrapv -fno-strict-aliasing -fno-builtin -w -o "); + sb_add(cmd, shell_quote(base)); + sb_add(cmd, " "); + sb_add(cmd, shell_quote(text_cat(base, ".c"))); + sb_add(cmd, " "); + sb_add(cmd, shell_quote(text_cat(base, ".rt.c"))); + let status = shell(sb_text(cmd)); + if status != 0 { + err(text_cat3("trackc: C compiler failed (exit ", text_of_int(status), ")\n")); + quit(1); + } + return 0; +} + +fn require_file() -> i64 { + if arg_count() < 3 { + err("Error: expected a .trk file path\n"); + quit(1); + } + return arg_text(2); +} + +fn cmd_build(path: i64) -> i64 { + let c = front_end(path); + let mut exe = output_flag(); + if exe == 0 { + exe = file_stem(path); + } + compile_c(generate_c(c), exe); + out(text_cat3("✓ Built: ", exe, "\n")); + return 0; +} + +fn cmd_emit(path: i64) -> i64 { + let c = front_end(path); + let code = generate_c(c); + let dest = output_flag(); + if dest == 0 { + out(code); + return 0; + } + if write_text(dest, code) != 0 { + err(text_cat3("trackc: cannot write '", dest, "'\n")); + quit(1); + } + return 0; +} + +fn cmd_run(path: i64) -> i64 { + let c = front_end(path); + let mut tmp: i64 = "/tmp"; + if env_exists("TMPDIR") != 0 { + let t: i64 = env_get("TMPDIR"); + if text_len(t) > 0 { + tmp = t; + } + } + let exe = text_cat3(tmp, "/trackc_run_", text_of_int(clock_ms())); + compile_c(generate_c(c), exe); + let cmd = sb_new(); + sb_add(cmd, shell_quote(exe)); + let mut i: i64 = 3; + while i < arg_count() { + sb_add(cmd, " "); + sb_add(cmd, shell_quote(arg_text(i))); + i = i + 1; + } + let status = shell(sb_text(cmd)); + file_remove(exe); + file_remove(text_cat(exe, ".c")); + file_remove(text_cat(exe, ".rt.c")); + quit(status); + return 0; +} + +fn cmd_tokens(path: i64) -> i64 { + let c = cx_new(); + lex_init(c); + let f = add_source(c, path, read_text(path)); + let first = lex_file(c, f); + let mut t = first; + let mut going = true; + while going { + let kind = tok_kind(c, t); + let line = sb_new(); + sb_add(line, token_kind_text(kind)); + if kind == TK_INT { + sb_add(line, " "); + sb_int(line, tok_value(c, t)); + } else if kind == TK_IDENT { + sb_add(line, " "); + sb_add(line, name_text(c, tok_value(c, t))); + } else if kind == TK_STR { + sb_add(line, " "); + sb_add(line, c_string(tok_value(c, t))); + } + sb_byte(line, 10); + out(sb_text(line)); + if kind == TK_EOF { + going = false; + } + t = t + 1; + } + return 0; +} + +fn main() -> i64 { + if arg_count() < 2 { + usage(); + return 1; + } + let command = arg_text(1); + if text_eq(command, "--version") || text_eq(command, "-v") || text_eq(command, "version") { + out(text_cat3("trackc ", trackc_version(), "\n")); + return 0; + } + if text_eq(command, "--help") || text_eq(command, "-h") || text_eq(command, "help") { + usage(); + return 0; + } + if text_eq(command, "build") { + cmd_build(require_file()); + return 0; + } + if text_eq(command, "run") { + cmd_run(require_file()); + return 0; + } + if text_eq(command, "check") { + let path = require_file(); + front_end(path); + out(text_cat3("✓ Check passed: ", path, "\n")); + return 0; + } + if text_eq(command, "emit-c") { + cmd_emit(require_file()); + return 0; + } + if text_eq(command, "tokens") { + cmd_tokens(require_file()); + return 0; } + err(text_cat3("Unknown command: '", command, "'\n\n")); + usage(); + return 1; } diff --git a/compiler/src/parser.trk b/compiler/src/parser.trk index c64daa0..36f815d 100644 --- a/compiler/src/parser.trk +++ b/compiler/src/parser.trk @@ -1,320 +1,881 @@ -// parser.trk — Phase 3 recursive-descent parser subset. +// parser.trk — recursive-descent parser for the complete Track grammar. // -// Supported expressions: integers, strings, booleans, identifiers, -// parenthesized expressions, unary `!`/`-`, and arithmetic precedence. -// Supported statements: untyped `let`, `let mut`, `return`, and expression -// statements. Every result carries byte-precise error ranges. +// Grammar and precedence follow the bootstrap parser (`src/parser/`): +// expr := or (".." or)? +// or := and ("||" and)* +// and := comparison ("&&" comparison)* +// comparison := bitwise (("==" | "!=" | "<" | ">" | "<=" | ">=") bitwise)* +// bitwise := shift (("&" | "|") shift)* +// shift := additive (("<<" | ">>") additive)* +// additive := term (("+" | "-") term)* +// term := unary (("*" | "/" | "%") unary)* +// unary := ("-" | "!" | "*" | "&" "mut"?) unary | postfix +// postfix := primary ("." INT | "." IDENT "(" args ")" | "[" index "]" | "(" args ")")* +// Statements end with an optional ";". Struct literals are disabled inside +// `if`/`while`/`for` headers so `if x { ... }` parses as a block. +import "base"; import "lexer"; import "ast"; -import "source_span"; -import "diagnostic"; -// (node index, next byte, error start, error end, error code) -type ParserStep = (i64, i64, i64, i64, i64); -// (AST handle, error start, error end, error code) -type ParseResult = (ptr, i64, i64, i64); +fn p_tok(c: i64) -> i64 { return vget(c, CX_POS); } +fn p_kind(c: i64) -> i64 { return tok_kind(c, p_tok(c)); } +fn p_kind_at(c: i64, k: i64) -> i64 { + let t = p_tok(c) + k; + if t >= vget(c, CX_TEND) { + return TK_EOF; + } + return tok_kind(c, t); +} +fn p_value(c: i64) -> i64 { return tok_value(c, p_tok(c)); } +fn p_pos(c: i64) -> i64 { return tok_start(c, p_tok(c)); } +fn p_at(c: i64, kind: i64) -> bool { return p_kind(c) == kind; } +fn p_eof(c: i64) -> bool { return p_kind(c) == TK_EOF; } + +fn p_advance(c: i64) -> i64 { + let t = p_tok(c); + if tok_kind(c, t) != TK_EOF { + vset(c, CX_POS, t + 1); + } + return t; +} -fn parser_step_ok(node: i64, next: i64) -> ParserStep { - return (node, next, -1, -1, 0); +fn p_error(c: i64, expected: i64) -> i64 { + let message = text_cat3("expected ", expected, text_cat(", found ", token_kind_text(p_kind(c)))); + return fail_at(c, p_pos(c), "E0100", message); } -fn parser_step_error(next: i64, start: i64, end: i64, code: i64) -> ParserStep { - return (-1, next, start, end, code); +fn p_expect(c: i64, kind: i64) -> i64 { + if !p_at(c, kind) { + p_error(c, token_kind_text(kind)); + } + return p_advance(c); } -fn parser_step_failed(step: &ParserStep) -> bool { - return step.4 != 0; +fn p_accept(c: i64, kind: i64) -> bool { + if p_at(c, kind) { + p_advance(c); + return true; + } + return false; } -fn parser_step_copy(step: &ParserStep) -> ParserStep { - return (step.0, step.1, step.2, step.3, step.4); +fn p_ident(c: i64) -> i64 { + if !p_at(c, TK_IDENT) { + p_error(c, "identifier"); + } + let id = p_value(c); + p_advance(c); + return id; } -fn parse_error_name(code: i64) -> Str { - if code == 1 { return "P0001"; } - if code == 2 { return "P0002"; } - if code == 3 { return "P0003"; } - if code == 4 { return "P0004"; } - if code == 5 { return "P0005"; } - return "P0099"; +// IDENT ("::" IDENT)* interned as one qualified name. +fn p_qualified(c: i64) -> i64 { + let mut name = p_ident(c); + while p_at(c, TK_COLONCOLON) { + p_advance(c); + let sub = p_ident(c); + name = intern(c, text_cat3(name_text(c, name), "::", name_text(c, sub))); + } + return name; } -fn parse_error_message(code: i64) -> Str { - if code == 1 { return "expected expression"; } - if code == 2 { return "expected identifier after 'let'"; } - if code == 3 { return "expected '=' after binding name"; } - if code == 4 { return "expected ';' after statement"; } - if code == 5 { return "expected ')' after expression"; } - return "parser made no progress"; +// ------------------------------------------------------------------ types + +fn parse_type(c: i64) -> i64 { + let pos = p_pos(c); + let k = p_kind(c); + if k == TK_TY_U8 { p_advance(c); return node1(c, T_PRIM, PRIM_U8, pos); } + if k == TK_TY_I8 { p_advance(c); return node1(c, T_PRIM, PRIM_I8, pos); } + if k == TK_TY_I32 { p_advance(c); return node1(c, T_PRIM, PRIM_I32, pos); } + if k == TK_TY_U32 { p_advance(c); return node1(c, T_PRIM, PRIM_U32, pos); } + if k == TK_TY_I64 { p_advance(c); return node1(c, T_PRIM, PRIM_I64, pos); } + if k == TK_TY_U64 { p_advance(c); return node1(c, T_PRIM, PRIM_U64, pos); } + if k == TK_TY_BOOL { p_advance(c); return node1(c, T_PRIM, PRIM_BOOL, pos); } + if k == TK_TY_VOID { p_advance(c); return node1(c, T_PRIM, PRIM_VOID, pos); } + if k == TK_TY_PTR { + p_advance(c); + p_expect(c, TK_LT); + let inner = parse_type(c); + p_expect(c, TK_GT); + return node1(c, T_PTR, inner, pos); + } + if k == TK_AMP { + p_advance(c); + return node1(c, T_REF, parse_type(c), pos); + } + if k == TK_IDENT { + return node1(c, T_CUSTOM, p_ident(c), pos); + } + if k == TK_LBRACKET { + p_advance(c); + if p_accept(c, TK_RBRACKET) { + return node1(c, T_SLICE, parse_type(c), pos); + } + let elem = parse_type(c); + if p_accept(c, TK_RBRACKET) { + return node1(c, T_SLICE, elem, pos); + } + p_expect(c, TK_SEMI); + if !p_at(c, TK_INT) { + p_error(c, "array size"); + } + let size = p_value(c); + p_advance(c); + p_expect(c, TK_RBRACKET); + return node2(c, T_ARRAY, elem, size, pos); + } + if k == TK_LPAREN { + p_advance(c); + let mark = list_begin(c); + if !p_at(c, TK_RPAREN) { + list_item(c, parse_type(c)); + let mut more = true; + while more && p_at(c, TK_COMMA) { + p_advance(c); + if p_at(c, TK_RPAREN) { + more = false; + } else { + list_item(c, parse_type(c)); + } + } + } + p_expect(c, TK_RPAREN); + return node1(c, T_TUPLE, list_end(c, mark), pos); + } + return p_error(c, "type"); } -fn parse_error_label(code: i64) -> Str { - if code == 1 { return "expression required here"; } - if code == 2 { return "binding name required here"; } - if code == 3 { return "insert '=' here"; } - if code == 4 { return "terminate the statement here"; } - if code == 5 { return "close the parenthesized expression"; } - return "unexpected token"; +// ------------------------------------------------------------ expressions + +fn parse_expr(c: i64) -> i64 { + let pos = p_pos(c); + let left = parse_or(c); + // `a[s..]`: leave an open-ended `..` for the index parser. + if p_at(c, TK_DOTDOT) && p_kind_at(c, 1) != TK_RBRACKET { + p_advance(c); + let right = parse_or(c); + return node2(c, K_RANGE, left, right, pos); + } + return left; } -fn parse_primary(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let record = lex_next_record(source, pos); - let tag: i64 = record.0; - let start: i64 = record.1; - let end: i64 = record.2; - let next: i64 = record.3; +fn binary(c: i64, op: i64, left: i64, right: i64) -> i64 { + return node3(c, K_BIN, op, left, right, npos(c, left)); +} - if tag == token_tag_int() { - let text = str_substr(source, start, end - start); - let value = str_to_int(&text); - let node = ast_literal_node(ast_kind_int(), value, start, end); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, next); +fn parse_or(c: i64) -> i64 { + let mut left = parse_and(c); + while p_accept(c, TK_PIPEPIPE) { + left = binary(c, OP_OR, left, parse_and(c)); } - if tag == token_tag_string() { - let node = ast_literal_node(ast_kind_string(), 0, start + 1, end - 1); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, next); + return left; +} + +fn parse_and(c: i64) -> i64 { + let mut left = parse_comparison(c); + while p_accept(c, TK_AMPAMP) { + left = binary(c, OP_AND, left, parse_comparison(c)); } - if tag == token_tag_true() { - let node = ast_literal_node(ast_kind_bool(), 1, start, end); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, next); + return left; +} + +fn comparison_op(kind: i64) -> i64 { + if kind == TK_EQEQ { return OP_EQ; } + if kind == TK_NEQ { return OP_NEQ; } + if kind == TK_LT { return OP_LT; } + if kind == TK_GT { return OP_GT; } + if kind == TK_LTE { return OP_LTE; } + if kind == TK_GTE { return OP_GTE; } + return 0; +} + +fn parse_comparison(c: i64) -> i64 { + let mut left = parse_bitwise(c); + let mut op = comparison_op(p_kind(c)); + while op != 0 { + p_advance(c); + left = binary(c, op, left, parse_bitwise(c)); + op = comparison_op(p_kind(c)); } - if tag == token_tag_false() { - let node = ast_literal_node(ast_kind_bool(), 0, start, end); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, next); + return left; +} + +// `&` / `|` are bitwise only when not doubled (`& &x` stays a reference). +fn bitwise_op(c: i64) -> i64 { + let k = p_kind(c); + let next = p_kind_at(c, 1); + if k == TK_AMP && next != TK_AMP && next != TK_EOF { return OP_BAND; } + if k == TK_PIPE && next != TK_PIPE && next != TK_EOF { return OP_BOR; } + return 0; +} + +fn parse_bitwise(c: i64) -> i64 { + let mut left = parse_shift(c); + let mut op = bitwise_op(c); + while op != 0 { + p_advance(c); + left = binary(c, op, left, parse_shift(c)); + op = bitwise_op(c); } - if tag == token_tag_identifier() { - let node = ast_identifier_node(start, end); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, next); + return left; +} + +fn parse_shift(c: i64) -> i64 { + let mut left = parse_additive(c); + let mut going = true; + while going { + if p_accept(c, TK_SHL) { + left = binary(c, OP_SHL, left, parse_additive(c)); + } else if p_accept(c, TK_SHR) { + left = binary(c, OP_SHR, left, parse_additive(c)); + } else { + going = false; + } } - if tag == token_tag_lparen() { - let inner = parse_expression(source, ast, next); - if parser_step_failed(&inner) { - return parser_step_copy(&inner); + return left; +} + +fn parse_additive(c: i64) -> i64 { + let mut left = parse_term(c); + let mut going = true; + while going { + if p_accept(c, TK_PLUS) { + left = binary(c, OP_ADD, left, parse_term(c)); + } else if p_accept(c, TK_MINUS) { + left = binary(c, OP_SUB, left, parse_term(c)); + } else { + going = false; } - let close = lex_next_record(source, inner.1); - if close.0 != token_tag_rparen() { - return parser_step_error(close.3, close.1, close.2, 5); + } + return left; +} + +fn parse_term(c: i64) -> i64 { + let mut left = parse_unary(c); + let mut going = true; + while going { + if p_accept(c, TK_STAR) { + left = binary(c, OP_MUL, left, parse_unary(c)); + } else if p_accept(c, TK_SLASH) { + left = binary(c, OP_DIV, left, parse_unary(c)); + } else if p_accept(c, TK_PERCENT) { + left = binary(c, OP_MOD, left, parse_unary(c)); + } else { + going = false; } - return parser_step_ok(inner.0, close.3); } - return parser_step_error(next, start, end, 1); + return left; } -fn parse_unary(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let record = lex_next_record(source, pos); - let tag: i64 = record.0; - if tag == token_tag_bang() || tag == token_tag_minus() { - let operand = parse_unary(source, ast, record.3); - if parser_step_failed(&operand) { - return parser_step_copy(&operand); +fn parse_unary(c: i64) -> i64 { + let pos = p_pos(c); + if p_accept(c, TK_MINUS) { + return node2(c, K_UN, OP_NEG, parse_unary(c), pos); + } + if p_accept(c, TK_BANG) { + return node2(c, K_UN, OP_NOT, parse_unary(c), pos); + } + if p_accept(c, TK_STAR) { + return node2(c, K_UN, OP_DEREF, parse_unary(c), pos); + } + if p_accept(c, TK_AMP) { + p_accept(c, TK_MUT); + return node1(c, K_ADDR, parse_unary(c), pos); + } + return parse_postfix(c); +} + +// "(" expr ("," expr)* ")" — the opening paren is already consumed. +fn parse_args_into(c: i64) -> i64 { + if !p_at(c, TK_RPAREN) { + list_item(c, parse_expr(c)); + while p_accept(c, TK_COMMA) { + list_item(c, parse_expr(c)); } - let operand_index: i64 = operand.0; - let operand_end = ast_node_end(ast, operand_index); - let node = ast_unary_node(tag, operand_index, record.1, operand_end); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, operand.1); } - return parse_primary(source, ast, pos); + p_expect(c, TK_RPAREN); + return 0; } -fn parser_is_factor_operator(tag: i64) -> bool { - if tag == token_tag_star() { return true; } - if tag == token_tag_slash() { return true; } - return tag == token_tag_percent(); -} - -fn parser_is_term_operator(tag: i64) -> bool { - if tag == token_tag_plus() { return true; } - return tag == token_tag_minus(); -} - -fn parse_factor(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let initial = parse_unary(source, ast, pos); - if parser_step_failed(&initial) { - return parser_step_copy(&initial); - } - let mut left: i64 = initial.0; - let mut next: i64 = initial.1; - let mut parsing = true; - while parsing { - let operator = lex_next_record(source, next); - let operator_tag: i64 = operator.0; - if parser_is_factor_operator(operator_tag) { - let right_step = parse_unary(source, ast, operator.3); - if parser_step_failed(&right_step) { - return parser_step_copy(&right_step); +fn parse_postfix(c: i64) -> i64 { + let mut expr = parse_primary(c); + let mut going = true; + while going { + let pos = p_pos(c); + if p_accept(c, TK_DOT) { + if p_at(c, TK_INT) { + let index = p_value(c); + p_advance(c); + expr = node2(c, K_TINDEX, expr, index, pos); + } else { + let member = p_ident(c); + if p_accept(c, TK_LPAREN) { + // Uniform function call syntax: x.f(a) == f(x, a) + let mark = list_begin(c); + list_item(c, expr); + parse_args_into(c); + expr = node2(c, K_CALL, member, list_end(c, mark), npos(c, expr)); + } else { + expr = node2(c, K_FIELD, expr, member, pos); + } } - let right: i64 = right_step.0; - let start = ast_node_start(ast, left); - let end = ast_node_end(ast, right); - let node = ast_binary_node(operator_tag, left, right, start, end); - left = ast_push_node(ast, &node); - next = right_step.1; + } else if p_accept(c, TK_LBRACKET) { + expr = parse_index_suffix(c, expr, pos); + } else if p_at(c, TK_LPAREN) && nk(c, expr) == K_VAR { + p_advance(c); + let mark = list_begin(c); + parse_args_into(c); + expr = node2(c, K_CALL, na(c, expr), list_end(c, mark), npos(c, expr)); } else { - parsing = false; + going = false; + } + } + return expr; +} + +// After "[": index `e]`, slices `..e]`, `s..e]`, `s..]`. +fn parse_index_suffix(c: i64, target: i64, pos: i64) -> i64 { + if p_accept(c, TK_DOTDOT) { + let mut end: i64 = -1; + if !p_at(c, TK_RBRACKET) { + end = parse_expr(c); + } + p_expect(c, TK_RBRACKET); + return node3(c, K_SLICE, target, -1, end, pos); + } + let first = parse_expr(c); + if nk(c, first) == K_RANGE { + p_expect(c, TK_RBRACKET); + return node3(c, K_SLICE, target, na(c, first), nb(c, first), pos); + } + if p_accept(c, TK_DOTDOT) { + let mut end: i64 = -1; + if !p_at(c, TK_RBRACKET) { + end = parse_expr(c); } + p_expect(c, TK_RBRACKET); + return node3(c, K_SLICE, target, first, end, pos); } - return parser_step_ok(left, next); + p_expect(c, TK_RBRACKET); + return node2(c, K_INDEX, target, first, pos); } -fn parse_expression(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let initial = parse_factor(source, ast, pos); - if parser_step_failed(&initial) { - return parser_step_copy(&initial); +fn parse_primary(c: i64) -> i64 { + let pos = p_pos(c); + let k = p_kind(c); + if k == TK_INT { + let value = p_value(c); + p_advance(c); + return node1(c, K_INT, value, pos); + } + if k == TK_STR { + let text = p_value(c); + p_advance(c); + return node1(c, K_STR, text, pos); + } + if k == TK_TRUE { + p_advance(c); + return node1(c, K_BOOL, 1, pos); + } + if k == TK_FALSE { + p_advance(c); + return node1(c, K_BOOL, 0, pos); + } + if k == TK_IF { + return parse_if(c); + } + if k == TK_AT { + return parse_macro_call(c); } - let mut left: i64 = initial.0; - let mut next: i64 = initial.1; - let mut parsing = true; - while parsing { - let operator = lex_next_record(source, next); - let operator_tag: i64 = operator.0; - if parser_is_term_operator(operator_tag) { - let right_step = parse_factor(source, ast, operator.3); - if parser_step_failed(&right_step) { - return parser_step_copy(&right_step); + if k == TK_MATCH { + return parse_match(c); + } + if k == TK_IDENT { + let name = p_qualified(c); + if vget(c, CX_ALLOW) != 0 && p_at(c, TK_LBRACE) { + p_advance(c); + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + list_item(c, p_ident(c)); + p_expect(c, TK_COLON); + list_item(c, parse_expr(c)); + p_accept(c, TK_COMMA); } - let right: i64 = right_step.0; - let start = ast_node_start(ast, left); - let end = ast_node_end(ast, right); - let node = ast_binary_node(operator_tag, left, right, start, end); - left = ast_push_node(ast, &node); - next = right_step.1; + p_expect(c, TK_RBRACE); + return node2(c, K_SINIT, name, list_end(c, mark), pos); + } + return node1(c, K_VAR, name, pos); + } + if k == TK_LPAREN { + p_advance(c); + if p_accept(c, TK_RPAREN) { + return node1(c, K_TUPLE, 0, pos); + } + let first = parse_expr(c); + if p_accept(c, TK_COMMA) { + let mark = list_begin(c); + list_item(c, first); + while !p_at(c, TK_RPAREN) { + if p_eof(c) { + p_error(c, "')'"); + } + list_item(c, parse_expr(c)); + p_accept(c, TK_COMMA); + } + p_expect(c, TK_RPAREN); + return node1(c, K_TUPLE, list_end(c, mark), pos); + } + p_expect(c, TK_RPAREN); + return first; + } + if k == TK_LBRACKET { + p_advance(c); + let mark = list_begin(c); + if !p_at(c, TK_RBRACKET) { + list_item(c, parse_expr(c)); + while p_accept(c, TK_COMMA) { + list_item(c, parse_expr(c)); + } + } + p_expect(c, TK_RBRACKET); + return node1(c, K_ARRAY, list_end(c, mark), pos); + } + return p_error(c, "expression"); +} + +// "{" statement* "}" +fn parse_block(c: i64) -> i64 { + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + list_item(c, parse_statement(c)); + } + p_expect(c, TK_RBRACE); + return list_end(c, mark); +} + +// Parse an expression with struct literals disabled (loop/branch headers). +// Like the bootstrap parser, struct literals are re-enabled afterwards. +fn parse_header_expr(c: i64) -> i64 { + vset(c, CX_ALLOW, 0); + let e = parse_expr(c); + vset(c, CX_ALLOW, 1); + return e; +} + +fn parse_if(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_IF); + let cond = parse_header_expr(c); + let then_body = parse_block(c); + let mut else_body: i64 = 0; + if p_accept(c, TK_ELSE) { + if p_at(c, TK_IF) { + let mark = list_begin(c); + list_item(c, parse_if(c)); + else_body = list_end(c, mark); } else { - parsing = false; + else_body = parse_block(c); } } - return parser_step_ok(left, next); + return node3(c, K_IF, cond, then_body, else_body, pos); } -fn parse_let_statement(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let let_record = lex_next_record(source, pos); - let mut next: i64 = let_record.3; - let mut mutable = false; - let mut name_record = lex_next_record(source, next); - if name_record.0 == token_tag_mut() { - mutable = true; - next = name_record.3; - name_record = lex_next_record(source, next); +fn parse_macro_call(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_AT); + let name = p_ident(c); + let mut args: i64 = 0; + if p_accept(c, TK_LPAREN) { + let mark = list_begin(c); + parse_args_into(c); + args = list_end(c, mark); + } + let mut body: i64 = -1; + if p_at(c, TK_LBRACE) { + body = parse_block(c); } - if name_record.0 != token_tag_identifier() { - return parser_step_error(name_record.3, name_record.1, name_record.2, 2); + return node3(c, K_MACROCALL, name, args, body, pos); +} + +fn parse_match(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_MATCH); + let saved = vget(c, CX_ALLOW); + vset(c, CX_ALLOW, 0); + let target = parse_expr(c); + vset(c, CX_ALLOW, saved); + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + let pattern = parse_pattern(c); + let mut guard: i64 = -1; + if p_accept(c, TK_IF) { + guard = parse_expr(c); + } + p_expect(c, TK_FATARROW); + let body = parse_statement(c); + list_item(c, pattern); + list_item(c, guard); + list_item(c, body); + p_accept(c, TK_COMMA); + } + p_expect(c, TK_RBRACE); + return node2(c, K_MATCH, target, list_end(c, mark), pos); +} + +// --------------------------------------------------------------- patterns + +// "(" pattern ("," pattern)* ","? ")" — opening paren already consumed. +fn parse_pattern_list(c: i64) -> i64 { + let mark = list_begin(c); + if !p_at(c, TK_RPAREN) { + list_item(c, parse_pattern(c)); + let mut more = true; + while more && p_at(c, TK_COMMA) { + p_advance(c); + if p_at(c, TK_RPAREN) { + more = false; + } else { + list_item(c, parse_pattern(c)); + } + } } - let name_node = ast_identifier_node(name_record.1, name_record.2); - let name_index = ast_push_node(ast, &name_node); + p_expect(c, TK_RPAREN); + return list_end(c, mark); +} - let equals = lex_next_record(source, name_record.3); - if equals.0 != token_tag_eq() { - return parser_step_error(equals.3, equals.1, equals.2, 3); +fn parse_pattern(c: i64) -> i64 { + let pos = p_pos(c); + let k = p_kind(c); + if k == TK_UNDERSCORE { + p_advance(c); + return node0(c, P_WILD, pos); } - let initializer = parse_expression(source, ast, equals.3); - if parser_step_failed(&initializer) { - return parser_step_copy(&initializer); + if k == TK_INT || k == TK_STR || k == TK_TRUE || k == TK_FALSE { + return node1(c, P_LIT, parse_primary(c), pos); } - let semicolon = lex_next_record(source, initializer.1); - if semicolon.0 != token_tag_semicolon() { - return parser_step_error(semicolon.3, semicolon.1, semicolon.2, 4); + if k == TK_LPAREN { + p_advance(c); + return node1(c, P_TUPLE, parse_pattern_list(c), pos); } - let node = ast_let_node(name_index, initializer.0, mutable, let_record.1, semicolon.2); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, semicolon.3); + if k == TK_IDENT { + let first = p_ident(c); + if p_at(c, TK_COLONCOLON) { + p_advance(c); + let variant = p_ident(c); + // Only the first two segments are significant (`A::B::C` → A, B). + while p_accept(c, TK_COLONCOLON) { + p_ident(c); + } + let mut bindings: i64 = 0; + if p_accept(c, TK_LPAREN) { + bindings = parse_pattern_list(c); + } + return node3(c, P_VARIANT, first, variant, bindings, pos); + } + if p_accept(c, TK_LBRACE) { + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + let field_pos = p_pos(c); + let field = p_ident(c); + let mut sub: i64 = -1; + if p_accept(c, TK_COLON) { + sub = parse_pattern(c); + } else { + sub = node1(c, P_IDENT, field, field_pos); + } + list_item(c, field); + list_item(c, sub); + p_accept(c, TK_COMMA); + } + p_expect(c, TK_RBRACE); + return node2(c, P_STRUCT, first, list_end(c, mark), pos); + } + return node1(c, P_IDENT, first, pos); + } + return p_error(c, "pattern"); } -fn parse_return_statement(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let return_record = lex_next_record(source, pos); - let after_return = lex_next_record(source, return_record.3); - if after_return.0 == token_tag_semicolon() { - let empty_node = ast_return_node(-1, return_record.1, after_return.2); - let empty_index = ast_push_node(ast, &empty_node); - return parser_step_ok(empty_index, after_return.3); +// ------------------------------------------------------------- statements + +fn parse_statement(c: i64) -> i64 { + let stmt = parse_statement_inner(c); + p_accept(c, TK_SEMI); + return stmt; +} + +fn parse_statement_inner(c: i64) -> i64 { + let pos = p_pos(c); + let k = p_kind(c); + if k == TK_LET { return parse_let(c); } + if k == TK_WITH { return parse_with(c); } + if k == TK_FN { return parse_fn(c); } + if k == TK_IF { return parse_if(c); } + if k == TK_WHILE { + p_advance(c); + let cond = parse_header_expr(c); + return node2(c, K_WHILE, cond, parse_block(c), pos); } - let value = parse_expression(source, ast, return_record.3); - if parser_step_failed(&value) { - return parser_step_copy(&value); + if k == TK_FOR { + p_advance(c); + let var = p_ident(c); + p_expect(c, TK_IN); + let iter = parse_header_expr(c); + return node3(c, K_FOR, var, iter, parse_block(c), pos); + } + if k == TK_RETURN { + p_advance(c); + if p_at(c, TK_SEMI) || p_eof(c) { + return node1(c, K_RETURN, -1, pos); + } + return node1(c, K_RETURN, parse_expr(c), pos); } - let semicolon = lex_next_record(source, value.1); - if semicolon.0 != token_tag_semicolon() { - return parser_step_error(semicolon.3, semicolon.1, semicolon.2, 4); + if k == TK_IMPORT || k == TK_USE { return parse_import(c); } + if k == TK_CONST { + p_advance(c); + let name = p_ident(c); + p_expect(c, TK_EQ); + return node2(c, K_CONST, name, parse_expr(c), pos); } - let node = ast_return_node(value.0, return_record.1, semicolon.2); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, semicolon.3); + if k == TK_TYPE { + p_advance(c); + let name = p_ident(c); + p_expect(c, TK_EQ); + return node2(c, K_TYPEDEF, name, parse_type(c), pos); + } + if k == TK_STRUCT { return parse_struct(c); } + if k == TK_ATMACRO { return parse_macro_def(c); } + if k == TK_ENUM { return parse_enum(c); } + if k == TK_UNION { return parse_union(c); } + let expr = parse_expr(c); + if p_accept(c, TK_EQ) { + return node2(c, K_ASSIGN, expr, parse_expr(c), npos(c, expr)); + } + return expr; } -fn parse_statement(source: &Str, ast: &ptr, pos: i64) -> ParserStep { - let record = lex_next_record(source, pos); - if record.0 == token_tag_let() { - return parse_let_statement(source, ast, pos); +fn parse_let(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_LET); + let mut mutable: i64 = 0; + if p_accept(c, TK_MUT) { + mutable = 1; } - if record.0 == token_tag_return() { - return parse_return_statement(source, ast, pos); + if p_at(c, TK_LPAREN) { + let pattern = parse_pattern(c); + p_expect(c, TK_EQ); + return node3(c, K_LETD, pattern, mutable, parse_expr(c), pos); } - let value = parse_expression(source, ast, pos); - if parser_step_failed(&value) { - return parser_step_copy(&value); + let name = p_ident(c); + let mut ty: i64 = -1; + if p_accept(c, TK_COLON) { + ty = parse_type(c); } - let semicolon = lex_next_record(source, value.1); - if semicolon.0 != token_tag_semicolon() { - return parser_step_error(semicolon.3, semicolon.1, semicolon.2, 4); + p_expect(c, TK_EQ); + let value = parse_expr(c); + return node_new(c, K_LET, name, mutable, ty, value, -1, pos); +} + +fn parse_with(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_WITH); + let target = p_ident(c); + p_expect(c, TK_ARROW); + let lens = p_ident(c); + return node3(c, K_LENS, target, lens, parse_block(c), pos); +} + +// "(" (IDENT ":" type ("," IDENT ":" type)*)? ")" +fn parse_params(c: i64) -> i64 { + p_expect(c, TK_LPAREN); + let mark = list_begin(c); + if !p_at(c, TK_RPAREN) { + list_item(c, p_ident(c)); + p_expect(c, TK_COLON); + list_item(c, parse_type(c)); + while p_accept(c, TK_COMMA) { + list_item(c, p_ident(c)); + p_expect(c, TK_COLON); + list_item(c, parse_type(c)); + } } - let start = ast_node_start(ast, value.0); - let node = ast_expression_statement_node(value.0, start, semicolon.2); - let index = ast_push_node(ast, &node); - return parser_step_ok(index, semicolon.3); + p_expect(c, TK_RPAREN); + return list_end(c, mark); } -fn parse_subset(source: &Str) -> ParseResult { - let ast = ast_new(32); - let mut pos: i64 = 0; - let mut error_start: i64 = -1; - let mut error_end: i64 = -1; - let mut error_code: i64 = 0; - let mut parsing = true; - while parsing { - let record = lex_next_record(source, pos); - if record.0 == token_tag_eof() { - parsing = false; - } else { - let statement = parse_statement(source, &ast, pos); - if parser_step_failed(&statement) { - error_start = statement.2; - error_end = statement.3; - error_code = statement.4; - parsing = false; - } else { - let next: i64 = statement.1; - if next <= pos { - error_start = record.1; - error_end = record.2; - error_code = 99; - parsing = false; - } else { - pos = next; - } +fn parse_fn(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_FN); + let name = p_ident(c); + let mut generics: i64 = 0; + if p_accept(c, TK_LT) { + let mark = list_begin(c); + list_item(c, p_ident(c)); + while p_accept(c, TK_COMMA) { + list_item(c, p_ident(c)); + } + p_expect(c, TK_GT); + generics = list_end(c, mark); + } + let params = parse_params(c); + let mut ret: i64 = -1; + if p_accept(c, TK_ARROW) { + ret = parse_type(c); + } + let body = parse_block(c); + return node_new(c, K_FN, name, generics, params, ret, body, pos); +} + +fn parse_macro_def(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_ATMACRO); + let name = p_ident(c); + let params = parse_params(c); + let mut ret: i64 = -1; + if p_accept(c, TK_ARROW) { + ret = parse_type(c); + } + let body = parse_block(c); + return node_new(c, K_MACRODEF, name, 0, params, ret, body, pos); +} + +fn parse_import(c: i64) -> i64 { + let pos = p_pos(c); + p_advance(c); + let mut path: i64 = 0; + if p_at(c, TK_STR) { + path = p_value(c); + p_advance(c); + } else if p_at(c, TK_IDENT) { + path = name_text(c, p_qualified(c)); + } else { + p_error(c, "module path"); + } + let mut alias: i64 = -1; + if p_accept(c, TK_AS) { + alias = p_ident(c); + } + let mut imports: i64 = -1; + if p_accept(c, TK_COLONCOLON) { + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + let mut more = true; + while more && !p_at(c, TK_RBRACE) && !p_eof(c) { + list_item(c, p_ident(c)); + if !p_accept(c, TK_COMMA) { + more = false; } } + p_expect(c, TK_RBRACE); + imports = list_end(c, mark); } - return (ast, error_start, error_end, error_code); + return node3(c, K_USE, path, imports, alias, pos); } -fn parse_result_ast(result: &ParseResult) -> ptr { - return result.0; +fn parse_struct(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_STRUCT); + let name = p_ident(c); + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + let mut more = true; + while more && !p_at(c, TK_RBRACE) && !p_eof(c) { + list_item(c, p_ident(c)); + p_expect(c, TK_COLON); + list_item(c, parse_type(c)); + if !p_accept(c, TK_COMMA) { + more = false; + } + } + p_expect(c, TK_RBRACE); + return node2(c, K_STRUCT, name, list_end(c, mark), pos); +} + +fn parse_enum(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_ENUM); + let name = p_ident(c); + let mut underlying: i64 = -1; + if p_accept(c, TK_COLON) { + underlying = parse_type(c); + } + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + list_item(c, p_ident(c)); + let mut value: i64 = -1; + if p_accept(c, TK_EQ) { + value = parse_expr(c); + } + list_item(c, value); + p_accept(c, TK_COMMA); + } + p_expect(c, TK_RBRACE); + return node3(c, K_ENUM, name, underlying, list_end(c, mark), pos); } -fn parse_result_ok(result: &ParseResult) -> bool { - return result.3 == 0; +fn parse_union(c: i64) -> i64 { + let pos = p_pos(c); + p_expect(c, TK_UNION); + let name = p_ident(c); + if p_accept(c, TK_LPAREN) { + while !p_at(c, TK_RPAREN) { + if p_eof(c) { + p_error(c, "')'"); + } + p_advance(c); + } + p_expect(c, TK_RPAREN); + } + p_expect(c, TK_LBRACE); + let mark = list_begin(c); + while !p_at(c, TK_RBRACE) { + if p_eof(c) { + p_error(c, "'}'"); + } + list_item(c, p_ident(c)); + let mut payload: i64 = -1; + if p_accept(c, TK_LPAREN) { + payload = parse_type(c); + p_expect(c, TK_RPAREN); + } + list_item(c, payload); + p_accept(c, TK_COMMA); + } + p_expect(c, TK_RBRACE); + return node2(c, K_UNION, name, list_end(c, mark), pos); } -fn format_parse_error(source: &Str, path: &Str, result: &ParseResult) -> Str { - let code_value: i64 = result.3; - let span = source_span(source, result.1, result.2); - let code = parse_error_name(code_value); - let message = parse_error_message(code_value); - let label = parse_error_label(code_value); - return format_error(source, path, &code, &message, &label, span); +// Parse the tokens starting at `first` (one file) into a list of +// top-level statements. +fn parse_file(c: i64, first: i64) -> i64 { + vset(c, CX_POS, first); + let mut end = first; + while tok_kind(c, end) != TK_EOF { + end = end + 1; + } + vset(c, CX_TEND, end + 1); + vset(c, CX_ALLOW, 1); + let mark = list_begin(c); + while !p_eof(c) { + list_item(c, parse_statement(c)); + } + return list_end(c, mark); } diff --git a/compiler/src/runtime_c.trk b/compiler/src/runtime_c.trk new file mode 100644 index 0000000..7fb9f23 --- /dev/null +++ b/compiler/src/runtime_c.trk @@ -0,0 +1,1088 @@ +// runtime_c.trk — GENERATED by scripts/embed_runtime.sh from +// runtime/track_runtime.c. Do not edit by hand. + +import "base"; + +fn runtime_source(b: i64) -> i64 { + sb_line(b, "/*"); + sb_line(b, " * Track runtime (word ABI)."); + sb_line(b, " *"); + sb_line(b, " * Every function exported to Track takes and returns 64-bit words (`tk_w`):"); + sb_line(b, " * integers and booleans are plain values, strings are NUL-terminated"); + sb_line(b, " * `char*` pointers, and handles (files, Vec, collections, builders) are"); + sb_line(b, " * pointers to runtime-owned records. This single file is linked by both"); + sb_line(b, " * compilers: the Rust bootstrap compiler embeds it with `include_str!`, and"); + sb_line(b, " * the self-hosted compiler embeds a generated copy (`compiler/src/runtime_c.trk`,"); + sb_line(b, " * regenerated by `scripts/embed_runtime.sh`)."); + sb_line(b, " */"); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, "#include "); + sb_line(b, ""); + sb_line(b, "typedef int64_t tk_w;"); + sb_line(b, ""); + sb_line(b, "#define TK_PTR(w) ((void *)(uintptr_t)(w))"); + sb_line(b, "#define TK_STR(w) ((const char *)(uintptr_t)(w))"); + sb_line(b, "#define TK_WORD(p) ((tk_w)(uintptr_t)(p))"); + sb_line(b, ""); + sb_line(b, "/* ---------------------------------------------------------------- process */"); + sb_line(b, ""); + sb_line(b, "static int g_argc = 0;"); + sb_line(b, "static char **g_argv = NULL;"); + sb_line(b, ""); + sb_line(b, "/* Called from the generated `main` entry point before any Track code runs. */"); + sb_line(b, "tk_w track_runtime_init(tk_w argc, tk_w argv) {"); + sb_line(b, " g_argc = (int)argc;"); + sb_line(b, " g_argv = (char **)TK_PTR(argv);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w os_args_count(void) { return g_argc; }"); + sb_line(b, ""); + sb_line(b, "static char *tk_dup_n(const char *s, size_t n) {"); + sb_line(b, " char *out = (char *)malloc(n + 1);"); + sb_line(b, " if (!out) {"); + sb_line(b, " fprintf(stderr, \"\\nTrack Runtime Error: Out of memory allocation failure!\\n\");"); + sb_line(b, " exit(137);"); + sb_line(b, " }"); + sb_line(b, " if (n > 0) memcpy(out, s, n);"); + sb_line(b, " out[n] = '\\0';"); + sb_line(b, " return out;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "static char *tk_dup(const char *s) { return tk_dup_n(s ? s : \"\", s ? strlen(s) : 0); }"); + sb_line(b, ""); + sb_line(b, "tk_w os_arg(tk_w idx) {"); + sb_line(b, " if (g_argv && idx >= 0 && idx < g_argc) return TK_WORD(tk_dup(g_argv[idx]));"); + sb_line(b, " return TK_WORD(tk_dup(\"\"));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w sys_exit(tk_w code) {"); + sb_line(b, " fflush(stdout);"); + sb_line(b, " exit((int)code);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "static int is_valid_string_pointer(const void *ptr) {"); + sb_line(b, " if (!ptr || (uintptr_t)ptr < 0x10000) return 0;"); + sb_line(b, " int pfd[2];"); + sb_line(b, " if (pipe(pfd) < 0) return 0;"); + sb_line(b, " ssize_t res = write(pfd[1], ptr, 1);"); + sb_line(b, " close(pfd[0]);"); + sb_line(b, " close(pfd[1]);"); + sb_line(b, " return res == 1;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Fatal abort: print message and terminate the process. No unwinding —"); + sb_line(b, " * stack frames are simply discarded (linear cleanup is skipped by design). */"); + sb_line(b, "tk_w track_abort(tk_w msg) {"); + sb_line(b, " const char *s = TK_STR(msg);"); + sb_line(b, " fflush(stdout);"); + sb_line(b, " if (s && is_valid_string_pointer(s)) {"); + sb_line(b, " fprintf(stderr, \"Track abort: %s\\n\", s);"); + sb_line(b, " } else {"); + sb_line(b, " fprintf(stderr, \"Track abort\\n\");"); + sb_line(b, " }"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " exit(134);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Integer division by zero or INT64_MIN / -1: trap like a hardware divide"); + sb_line(b, " * fault (SIGFPE) without flushing buffered output. */"); + sb_line(b, "tk_w track_trap_div(void) {"); + sb_line(b, " raise(SIGFPE);"); + sb_line(b, " abort();"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w sys_exec(tk_w cmd) {"); + sb_line(b, " if (!cmd) return -1;"); + sb_line(b, " fflush(stdout);"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " int status = system(TK_STR(cmd));"); + sb_line(b, " if (status == -1) return -1;"); + sb_line(b, " if (WIFEXITED(status)) return WEXITSTATUS(status);"); + sb_line(b, " return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w process_spawn(tk_w cmd) { return sys_exec(cmd); }"); + sb_line(b, ""); + sb_line(b, "tk_w clock_ms(void) {"); + sb_line(b, " struct timespec ts;"); + sb_line(b, " clock_gettime(CLOCK_MONOTONIC, &ts);"); + sb_line(b, " return (tk_w)ts.tv_sec * 1000 + (tk_w)ts.tv_nsec / 1000000;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w env_get(tk_w key) {"); + sb_line(b, " const char *val = key ? getenv(TK_STR(key)) : NULL;"); + sb_line(b, " return TK_WORD(tk_dup(val ? val : \"\"));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w env_exists(tk_w key) { return key && getenv(TK_STR(key)) ? 1 : 0; }"); + sb_line(b, ""); + sb_line(b, "/* ----------------------------------------------------------------- memory */"); + sb_line(b, ""); + sb_line(b, "static size_t g_allocated_bytes = 0;"); + sb_line(b, "static size_t g_max_memory_limit = 536870912; /* 512MB default memory boundary limit */"); + sb_line(b, ""); + sb_line(b, "tk_w sys_set_memory_limit(tk_w bytes) {"); + sb_line(b, " if (bytes > 0) g_max_memory_limit = (size_t)bytes;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w sys_get_memory_used(void) { return (tk_w)g_allocated_bytes; }"); + sb_line(b, ""); + sb_line(b, "tk_w alloc(tk_w size_w) {"); + sb_line(b, " size_t size = (size_t)size_w;"); + sb_line(b, " if (size_w <= 0) return 0;"); + sb_line(b, " if (g_allocated_bytes + size > g_max_memory_limit) {"); + sb_line(b, " fprintf(stderr, \"\\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\\n\", g_allocated_bytes, size, g_max_memory_limit);"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " exit(137);"); + sb_line(b, " }"); + sb_line(b, " size_t *ptr = (size_t *)calloc(1, size + sizeof(size_t));"); + sb_line(b, " if (!ptr) {"); + sb_line(b, " fprintf(stderr, \"\\nTrack Runtime Error: Out of memory allocation failure!\\n\");"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " exit(137);"); + sb_line(b, " }"); + sb_line(b, " *ptr = size;"); + sb_line(b, " g_allocated_bytes += size;"); + sb_line(b, " return TK_WORD(ptr + 1);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w dealloc(tk_w p) {"); + sb_line(b, " if (!p) return 0;"); + sb_line(b, " size_t *raw = (size_t *)TK_PTR(p) - 1;"); + sb_line(b, " size_t size = *raw;"); + sb_line(b, " g_allocated_bytes = g_allocated_bytes >= size ? g_allocated_bytes - size : 0;"); + sb_line(b, " free(raw);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w mem_realloc(tk_w p, tk_w new_size_w) {"); + sb_line(b, " if (!p) return alloc(new_size_w);"); + sb_line(b, " if (new_size_w <= 0) return dealloc(p);"); + sb_line(b, " size_t new_size = (size_t)new_size_w;"); + sb_line(b, " size_t *raw = (size_t *)TK_PTR(p) - 1;"); + sb_line(b, " size_t old_size = *raw;"); + sb_line(b, " if (g_allocated_bytes - old_size + new_size > g_max_memory_limit) {"); + sb_line(b, " fprintf(stderr, \"\\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\\n\", g_allocated_bytes, new_size, g_max_memory_limit);"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " exit(137);"); + sb_line(b, " }"); + sb_line(b, " size_t *new_raw = (size_t *)realloc(raw, new_size + sizeof(size_t));"); + sb_line(b, " if (!new_raw) {"); + sb_line(b, " fprintf(stderr, \"\\nTrack Runtime Error: Out of memory reallocation failure!\\n\");"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " exit(137);"); + sb_line(b, " }"); + sb_line(b, " *new_raw = new_size;"); + sb_line(b, " g_allocated_bytes = g_allocated_bytes - old_size + new_size;"); + sb_line(b, " return TK_WORD(new_raw + 1);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Copy `count` words to a fresh heap cell (escaping tuples). */"); + sb_line(b, "tk_w track_words_copy(tk_w src, tk_w count) {"); + sb_line(b, " tk_w dst = alloc(count > 0 ? count * 8 : 8);"); + sb_line(b, " if (src && count > 0) memcpy(TK_PTR(dst), TK_PTR(src), (size_t)count * 8);"); + sb_line(b, " return dst;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ------------------------------------------------------------------ print */"); + sb_line(b, ""); + sb_line(b, "tk_w print(tk_w val) {"); + sb_line(b, " if (val >= 0x10000 && is_valid_string_pointer(TK_PTR(val))) {"); + sb_line(b, " const char *str = TK_STR(val);"); + sb_line(b, " int is_str = 1;"); + sb_line(b, " int len = 0;"); + sb_line(b, " while (len < 4096) {"); + sb_line(b, " unsigned char c = (unsigned char)str[len];"); + sb_line(b, " if (c == 0) break;"); + sb_line(b, " if (c < 9 || (c > 13 && c < 32) || c > 126) {"); + sb_line(b, " is_str = 0;"); + sb_line(b, " break;"); + sb_line(b, " }"); + sb_line(b, " len++;"); + sb_line(b, " }"); + sb_line(b, " if (is_str && len > 0) {"); + sb_line(b, " printf(\"%s\\n\", str);"); + sb_line(b, " return 0;"); + sb_line(b, " }"); + sb_line(b, " }"); + sb_line(b, " printf(\"%lld\\n\", (long long)val);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w println(tk_w val) { return print(val); }"); + sb_line(b, "tk_w print_int(tk_w val) { printf(\"%lld\\n\", (long long)val); return 0; }"); + sb_line(b, "tk_w print_hex(tk_w val) { printf(\"0x%llx\\n\", (unsigned long long)val); return 0; }"); + sb_line(b, "tk_w print_str(tk_w s) { printf(\"%s\\n\", s ? TK_STR(s) : \"\"); return 0; }"); + sb_line(b, ""); + sb_line(b, "tk_w print_err(tk_w s) {"); + sb_line(b, " if (s) {"); + sb_line(b, " fflush(stdout);"); + sb_line(b, " fprintf(stderr, \"%s\\n\", TK_STR(s));"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " }"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w eprint(tk_w s) { return print_err(s); }"); + sb_line(b, ""); + sb_line(b, "/* Unformatted output (no trailing newline, no int/string guessing). */"); + sb_line(b, "tk_w track_out(tk_w s) {"); + sb_line(b, " if (s) fputs(TK_STR(s), stdout);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_err(tk_w s) {"); + sb_line(b, " if (s) {"); + sb_line(b, " fflush(stdout);"); + sb_line(b, " fputs(TK_STR(s), stderr);"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " }"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w read_line(void) {"); + sb_line(b, " size_t cap = 128, len = 0;"); + sb_line(b, " char *buf = (char *)malloc(cap);"); + sb_line(b, " int c;"); + sb_line(b, " while (buf && (c = fgetc(stdin)) != EOF && c != '\\n') {"); + sb_line(b, " if (len + 1 >= cap) buf = (char *)realloc(buf, cap *= 2);"); + sb_line(b, " if (buf) buf[len++] = (char)c;"); + sb_line(b, " }"); + sb_line(b, " if (!buf) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " buf[len] = '\\0';"); + sb_line(b, " return TK_WORD(buf);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ---------------------------------------------------------------- strings */"); + sb_line(b, ""); + sb_line(b, "tk_w str_from_literal(tk_w raw) { return TK_WORD(tk_dup(TK_STR(raw))); }"); + sb_line(b, ""); + sb_line(b, "tk_w str_free(tk_w s) {"); + sb_line(b, " (void)s; /* literals and runtime strings share a representation; never freed */"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_len(tk_w s) { return s ? (tk_w)strlen(TK_STR(s)) : 0; }"); + sb_line(b, ""); + sb_line(b, "tk_w str_eq(tk_w a, tk_w b) {"); + sb_line(b, " if (!a || !b) return a == b ? 1 : 0;"); + sb_line(b, " return strcmp(TK_STR(a), TK_STR(b)) == 0 ? 1 : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_concat(tk_w a, tk_w b) {"); + sb_line(b, " size_t a_len = a ? strlen(TK_STR(a)) : 0;"); + sb_line(b, " size_t b_len = b ? strlen(TK_STR(b)) : 0;"); + sb_line(b, " char *out = tk_dup_n(\"\", 0);"); + sb_line(b, " out = (char *)realloc(out, a_len + b_len + 1);"); + sb_line(b, " if (!out) exit(137);"); + sb_line(b, " if (a_len) memcpy(out, TK_STR(a), a_len);"); + sb_line(b, " if (b_len) memcpy(out + a_len, TK_STR(b), b_len);"); + sb_line(b, " out[a_len + b_len] = '\\0';"); + sb_line(b, " return TK_WORD(out);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_find(tk_w s, tk_w sub) {"); + sb_line(b, " if (!s || !sub) return -1;"); + sb_line(b, " const char *p = strstr(TK_STR(s), TK_STR(sub));"); + sb_line(b, " return p ? (tk_w)(p - TK_STR(s)) : -1;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_contains(tk_w s, tk_w sub) {"); + sb_line(b, " if (!s || !sub) return 0;"); + sb_line(b, " return strstr(TK_STR(s), TK_STR(sub)) ? 1 : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_to_int(tk_w s) { return s ? atoll(TK_STR(s)) : 0; }"); + sb_line(b, ""); + sb_line(b, "/* Predicate companion for str_to_int: distinguishes a genuine \"0\" from a parse failure. */"); + sb_line(b, "tk_w str_is_int(tk_w s_w) {"); + sb_line(b, " const char *s = TK_STR(s_w);"); + sb_line(b, " if (!s || *s == '\\0') return 0;"); + sb_line(b, " const char *p = s;"); + sb_line(b, " if (*p == '+' || *p == '-') p++;"); + sb_line(b, " if (*p == '\\0') return 0;"); + sb_line(b, " for (; *p; p++) {"); + sb_line(b, " if (!isdigit((unsigned char)*p)) return 0;"); + sb_line(b, " }"); + sb_line(b, " return 1;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w int_to_str(tk_w val) {"); + sb_line(b, " char buf[32];"); + sb_line(b, " snprintf(buf, sizeof(buf), \"%lld\", (long long)val);"); + sb_line(b, " return TK_WORD(tk_dup(buf));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_starts_with(tk_w s, tk_w prefix) {"); + sb_line(b, " if (!s || !prefix) return 0;"); + sb_line(b, " size_t lp = strlen(TK_STR(prefix));"); + sb_line(b, " return strncmp(TK_STR(s), TK_STR(prefix), lp) == 0 ? 1 : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_ends_with(tk_w s, tk_w suffix) {"); + sb_line(b, " if (!s || !suffix) return 0;"); + sb_line(b, " size_t ls = strlen(TK_STR(s));"); + sb_line(b, " size_t lp = strlen(TK_STR(suffix));"); + sb_line(b, " if (lp > ls) return 0;"); + sb_line(b, " return strcmp(TK_STR(s) + ls - lp, TK_STR(suffix)) == 0 ? 1 : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_substr(tk_w s_w, tk_w start, tk_w len) {"); + sb_line(b, " const char *s = TK_STR(s_w);"); + sb_line(b, " if (!s || start < 0 || len <= 0) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " /* Bounded scan: never walk past the requested window of a long source. */"); + sb_line(b, " size_t avail = strnlen(s, (size_t)start + (size_t)len);"); + sb_line(b, " if ((size_t)start >= avail) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " return TK_WORD(tk_dup_n(s + start, avail - (size_t)start));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_trim(tk_w s_w) {"); + sb_line(b, " const char *s = TK_STR(s_w);"); + sb_line(b, " if (!s) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " while (*s && isspace((unsigned char)*s)) s++;"); + sb_line(b, " size_t len = strlen(s);"); + sb_line(b, " while (len > 0 && isspace((unsigned char)s[len - 1])) len--;"); + sb_line(b, " return TK_WORD(tk_dup_n(s, len));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w str_char_at(tk_w s_w, tk_w idx) {"); + sb_line(b, " const char *s = TK_STR(s_w);"); + sb_line(b, " if (!s || idx < 0) return 0;"); + sb_line(b, " size_t len = strnlen(s, (size_t)idx + 1);"); + sb_line(b, " if ((size_t)idx >= len) return 0;"); + sb_line(b, " return (unsigned char)s[idx];"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Copy a string's bytes into a collection for O(1) bounds-checked access. */"); + sb_line(b, "tk_w track_i64_collection_new(tk_w requested_cap);"); + sb_line(b, "tk_w track_i64_collection_push(tk_w handle, tk_w value);"); + sb_line(b, ""); + sb_line(b, "tk_w track_str_bytes(tk_w s_w) {"); + sb_line(b, " const unsigned char *s = (const unsigned char *)TK_STR(s_w);"); + sb_line(b, " size_t len = s ? strlen((const char *)s) : 0;"); + sb_line(b, " tk_w out = track_i64_collection_new((tk_w)len + 1);"); + sb_line(b, " for (size_t i = 0; i < len; i++) track_i64_collection_push(out, s[i]);"); + sb_line(b, " return out;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w char_is_digit(tk_w c) { return c >= 0 && c < 256 && isdigit((int)c) ? 1 : 0; }"); + sb_line(b, "tk_w char_is_alpha(tk_w c) { return c >= 0 && c < 256 && isalpha((int)c) ? 1 : 0; }"); + sb_line(b, "tk_w char_is_alphanumeric(tk_w c) { return c >= 0 && c < 256 && isalnum((int)c) ? 1 : 0; }"); + sb_line(b, "tk_w char_is_space(tk_w c) { return c >= 0 && c < 256 && isspace((int)c) ? 1 : 0; }"); + sb_line(b, "tk_w char_to_upper(tk_w c) { return c >= 0 && c < 256 ? (unsigned char)toupper((int)c) : c; }"); + sb_line(b, "tk_w char_to_lower(tk_w c) { return c >= 0 && c < 256 ? (unsigned char)tolower((int)c) : c; }"); + sb_line(b, ""); + sb_line(b, "/* ------------------------------------------------------------------ paths */"); + sb_line(b, ""); + sb_line(b, "tk_w path_basename(tk_w path_w) {"); + sb_line(b, " const char *path = TK_STR(path_w);"); + sb_line(b, " if (!path) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " const char *slash = strrchr(path, '/');"); + sb_line(b, " return TK_WORD(tk_dup(slash ? slash + 1 : path));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w path_ext(tk_w path_w) {"); + sb_line(b, " const char *path = TK_STR(path_w);"); + sb_line(b, " if (!path) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " const char *dot = strrchr(path, '.');"); + sb_line(b, " if (!dot || dot == path) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " return TK_WORD(tk_dup(dot + 1));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w path_join(tk_w a_w, tk_w b_w) {"); + sb_line(b, " const char *a = a_w ? TK_STR(a_w) : \"\";"); + sb_line(b, " const char *b = b_w ? TK_STR(b_w) : \"\";"); + sb_line(b, " size_t la = strlen(a), lb = strlen(b);"); + sb_line(b, " int slash = la > 0 && a[la - 1] != '/' && (lb == 0 || b[0] != '/');"); + sb_line(b, " char *out = tk_dup_n(a, la);"); + sb_line(b, " out = (char *)realloc(out, la + (size_t)slash + lb + 1);"); + sb_line(b, " if (!out) exit(137);"); + sb_line(b, " if (slash) out[la] = '/';"); + sb_line(b, " memcpy(out + la + slash, b, lb + 1);"); + sb_line(b, " return TK_WORD(out);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ------------------------------------------------------------------ files */"); + sb_line(b, ""); + sb_line(b, "tk_w file_open(tk_w path, tk_w mode) {"); + sb_line(b, " if (!path || !mode) return 0;"); + sb_line(b, " return TK_WORD(fopen(TK_STR(path), TK_STR(mode)));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w file_close(tk_w f) {"); + sb_line(b, " if (f) fclose((FILE *)TK_PTR(f));"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w file_read_all(tk_w handle) {"); + sb_line(b, " FILE *f = (FILE *)TK_PTR(handle);"); + sb_line(b, " if (!f || fseek(f, 0, SEEK_END) != 0) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " long end = ftell(f);"); + sb_line(b, " if (end < 0 || fseek(f, 0, SEEK_SET) != 0) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " char *buf = (char *)malloc((size_t)end + 1);"); + sb_line(b, " if (!buf) exit(137);"); + sb_line(b, " size_t got = fread(buf, 1, (size_t)end, f);"); + sb_line(b, " buf[got] = '\\0';"); + sb_line(b, " return TK_WORD(buf);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w file_write(tk_w handle, tk_w content) {"); + sb_line(b, " FILE *f = (FILE *)TK_PTR(handle);"); + sb_line(b, " if (!f || !content) return -1;"); + sb_line(b, " size_t len = strlen(TK_STR(content));"); + sb_line(b, " return fwrite(TK_STR(content), 1, len, f) == len ? 0 : -1;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w file_exists(tk_w path) { return path && access(TK_STR(path), F_OK) == 0 ? 1 : 0; }"); + sb_line(b, ""); + sb_line(b, "tk_w file_remove(tk_w path) { return path && unlink(TK_STR(path)) == 0 ? 0 : -1; }"); + sb_line(b, ""); + sb_line(b, "tk_w file_size(tk_w path) {"); + sb_line(b, " struct stat st;"); + sb_line(b, " if (!path || stat(TK_STR(path), &st) != 0) return -1;"); + sb_line(b, " return (tk_w)st.st_size;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w dir_exists(tk_w path) {"); + sb_line(b, " struct stat st;"); + sb_line(b, " return path && stat(TK_STR(path), &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w file_copy(tk_w src, tk_w dst) {"); + sb_line(b, " if (!src || !dst) return -1;"); + sb_line(b, " FILE *in = fopen(TK_STR(src), \"rb\");"); + sb_line(b, " if (!in) return -1;"); + sb_line(b, " FILE *out = fopen(TK_STR(dst), \"wb\");"); + sb_line(b, " if (!out) {"); + sb_line(b, " fclose(in);"); + sb_line(b, " return -1;"); + sb_line(b, " }"); + sb_line(b, " char buf[4096];"); + sb_line(b, " size_t n;"); + sb_line(b, " while ((n = fread(buf, 1, sizeof(buf), in)) > 0) fwrite(buf, 1, n, out);"); + sb_line(b, " fclose(in);"); + sb_line(b, " fclose(out);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Whole-file helpers: open, transfer, and close in one call. */"); + sb_line(b, "tk_w track_file_read(tk_w path) {"); + sb_line(b, " FILE *f = path ? fopen(TK_STR(path), \"rb\") : NULL;"); + sb_line(b, " if (!f) return TK_WORD(tk_dup(\"\"));"); + sb_line(b, " tk_w out = file_read_all(TK_WORD(f));"); + sb_line(b, " fclose(f);"); + sb_line(b, " return out;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_file_write_all(tk_w path, tk_w content) {"); + sb_line(b, " FILE *f = path ? fopen(TK_STR(path), \"wb\") : NULL;"); + sb_line(b, " if (!f) return -1;"); + sb_line(b, " tk_w status = file_write(TK_WORD(f), content);"); + sb_line(b, " if (fclose(f) != 0) status = -1;"); + sb_line(b, " return status;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ------------------------------------------------------------------- math */"); + sb_line(b, ""); + sb_line(b, "tk_w math_abs(tk_w x) { return x < 0 ? -x : x; }"); + sb_line(b, "tk_w math_min(tk_w a, tk_w b) { return a < b ? a : b; }"); + sb_line(b, "tk_w math_max(tk_w a, tk_w b) { return a > b ? a : b; }"); + sb_line(b, "tk_w math_clamp(tk_w v, tk_w lo, tk_w hi) { return v < lo ? lo : (v > hi ? hi : v); }"); + sb_line(b, "tk_w math_floor(tk_w x) { return x; }"); + sb_line(b, "tk_w math_ceil(tk_w x) { return x; }"); + sb_line(b, "tk_w math_round(tk_w x) { return x; }"); + sb_line(b, ""); + sb_line(b, "tk_w math_pow(tk_w base, tk_w exp) {"); + sb_line(b, " tk_w result = 1;"); + sb_line(b, " if (exp < 0) return 0;"); + sb_line(b, " while (exp > 0) {"); + sb_line(b, " if (exp & 1) result = (tk_w)((uint64_t)result * (uint64_t)base);"); + sb_line(b, " base = (tk_w)((uint64_t)base * (uint64_t)base);"); + sb_line(b, " exp >>= 1;"); + sb_line(b, " }"); + sb_line(b, " return result;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w math_sqrt(tk_w x) {"); + sb_line(b, " if (x <= 0) return 0;"); + sb_line(b, " uint64_t lo = 0, hi = 3037000499ULL; /* floor(sqrt(INT64_MAX)) */"); + sb_line(b, " while (lo < hi) {"); + sb_line(b, " uint64_t mid = (lo + hi + 1) / 2;"); + sb_line(b, " if (mid * mid <= (uint64_t)x) lo = mid; else hi = mid - 1;"); + sb_line(b, " }"); + sb_line(b, " return (tk_w)lo;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "static uint64_t g_rng_state = 0x853c49e65d8dbb29ULL;"); + sb_line(b, "tk_w math_random(void) {"); + sb_line(b, " uint64_t x = g_rng_state;"); + sb_line(b, " x ^= x << 13;"); + sb_line(b, " x ^= x >> 7;"); + sb_line(b, " x ^= x << 17;"); + sb_line(b, " g_rng_state = x;"); + sb_line(b, " return (tk_w)x;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w add(tk_w a, tk_w b) { return a + b; }"); + sb_line(b, "tk_w sum(tk_w a, tk_w b) { return a + b; }"); + sb_line(b, "tk_w sub(tk_w a, tk_w b) { return a - b; }"); + sb_line(b, ""); + sb_line(b, "/* -------------------------------------------------------------------- Vec */"); + sb_line(b, ""); + sb_line(b, "typedef struct {"); + sb_line(b, " tk_w *data;"); + sb_line(b, " tk_w len;"); + sb_line(b, " tk_w cap;"); + sb_line(b, "} TrackVec;"); + sb_line(b, ""); + sb_line(b, "tk_w vec_init(tk_w cap) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(alloc(sizeof(TrackVec)));"); + sb_line(b, " v->cap = cap > 0 ? cap : 16;"); + sb_line(b, " v->data = (tk_w *)TK_PTR(alloc(v->cap * (tk_w)sizeof(tk_w)));"); + sb_line(b, " v->len = 0;"); + sb_line(b, " return TK_WORD(v);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_push(tk_w h, tk_w val) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " if (!v) return 0;"); + sb_line(b, " if (v->len == v->cap) {"); + sb_line(b, " v->cap *= 2;"); + sb_line(b, " v->data = (tk_w *)TK_PTR(mem_realloc(TK_WORD(v->data), v->cap * (tk_w)sizeof(tk_w)));"); + sb_line(b, " }"); + sb_line(b, " v->data[v->len++] = val;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_get(tk_w h, tk_w idx) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " return v && idx >= 0 && idx < v->len ? v->data[idx] : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_set(tk_w h, tk_w idx, tk_w val) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " if (v && idx >= 0 && idx < v->len) v->data[idx] = val;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_pop(tk_w h) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " return v && v->len > 0 ? v->data[--v->len] : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_len(tk_w h) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " return v ? v->len : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_clear(tk_w h) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " if (v) v->len = 0;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_reserve(tk_w h, tk_w cap) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " if (v && cap > v->cap) {"); + sb_line(b, " v->data = (tk_w *)TK_PTR(mem_realloc(TK_WORD(v->data), cap * (tk_w)sizeof(tk_w)));"); + sb_line(b, " v->cap = cap;"); + sb_line(b, " }"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w vec_free(tk_w h) {"); + sb_line(b, " TrackVec *v = (TrackVec *)TK_PTR(h);"); + sb_line(b, " if (v) {"); + sb_line(b, " dealloc(TK_WORD(v->data));"); + sb_line(b, " dealloc(h);"); + sb_line(b, " }"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* --------------------------------------------- opaque 64-bit collections */"); + sb_line(b, ""); + sb_line(b, "/* Growable word vectors used by the self-hosted compiler. Track only carries"); + sb_line(b, " * the stable pointer handle; layout and capacity stay in the runtime. */"); + sb_line(b, "typedef struct {"); + sb_line(b, " tk_w *data;"); + sb_line(b, " tk_w len;"); + sb_line(b, " tk_w cap;"); + sb_line(b, "} TrackI64Collection;"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_new(tk_w requested_cap) {"); + sb_line(b, " tk_w cap = requested_cap > 0 ? requested_cap : 16;"); + sb_line(b, " TrackI64Collection *c = (TrackI64Collection *)malloc(sizeof(TrackI64Collection));"); + sb_line(b, " if (!c) exit(137);"); + sb_line(b, " c->data = (tk_w *)malloc((size_t)cap * sizeof(tk_w));"); + sb_line(b, " if (!c->data) exit(137);"); + sb_line(b, " c->len = 0;"); + sb_line(b, " c->cap = cap;"); + sb_line(b, " return TK_WORD(c);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_len(tk_w h) {"); + sb_line(b, " const TrackI64Collection *c = (const TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " return c ? c->len : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_push(tk_w h, tk_w value) {"); + sb_line(b, " TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " if (!c) return -1;"); + sb_line(b, " if (c->len == c->cap) {"); + sb_line(b, " c->cap *= 2;"); + sb_line(b, " c->data = (tk_w *)realloc(c->data, (size_t)c->cap * sizeof(tk_w));"); + sb_line(b, " if (!c->data) exit(137);"); + sb_line(b, " }"); + sb_line(b, " c->data[c->len] = value;"); + sb_line(b, " return c->len++;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_get(tk_w h, tk_w index) {"); + sb_line(b, " const TrackI64Collection *c = (const TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " if (!c || index < 0 || index >= c->len) return 0;"); + sb_line(b, " return c->data[index];"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_set(tk_w h, tk_w index, tk_w value) {"); + sb_line(b, " TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " if (!c || index < 0 || index >= c->len) return 0;"); + sb_line(b, " c->data[index] = value;"); + sb_line(b, " return 1;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_i64_collection_pop(tk_w h) {"); + sb_line(b, " TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " if (!c || c->len == 0) return 0;"); + sb_line(b, " return c->data[--c->len];"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Shrink to `len` elements (no-op when already shorter). */"); + sb_line(b, "tk_w track_i64_collection_truncate(tk_w h, tk_w len) {"); + sb_line(b, " TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h);"); + sb_line(b, " if (c && len >= 0 && len < c->len) c->len = len;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* --------------------------------------------------------- string builder */"); + sb_line(b, ""); + sb_line(b, "typedef struct {"); + sb_line(b, " char *data;"); + sb_line(b, " size_t len;"); + sb_line(b, " size_t cap;"); + sb_line(b, "} TrackStrBuf;"); + sb_line(b, ""); + sb_line(b, "static void sb_reserve(TrackStrBuf *b, size_t extra) {"); + sb_line(b, " if (b->len + extra + 1 <= b->cap) return;"); + sb_line(b, " while (b->len + extra + 1 > b->cap) b->cap *= 2;"); + sb_line(b, " b->data = (char *)realloc(b->data, b->cap);"); + sb_line(b, " if (!b->data) exit(137);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_new(void) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)malloc(sizeof(TrackStrBuf));"); + sb_line(b, " if (!b) exit(137);"); + sb_line(b, " b->cap = 256;"); + sb_line(b, " b->len = 0;"); + sb_line(b, " b->data = (char *)malloc(b->cap);"); + sb_line(b, " if (!b->data) exit(137);"); + sb_line(b, " b->data[0] = '\\0';"); + sb_line(b, " return TK_WORD(b);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_push(tk_w h, tk_w s) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h);"); + sb_line(b, " if (!b || !s) return 0;"); + sb_line(b, " size_t n = strlen(TK_STR(s));"); + sb_line(b, " sb_reserve(b, n);"); + sb_line(b, " memcpy(b->data + b->len, TK_STR(s), n + 1);"); + sb_line(b, " b->len += n;"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_push_byte(tk_w h, tk_w byte) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h);"); + sb_line(b, " if (!b) return 0;"); + sb_line(b, " sb_reserve(b, 1);"); + sb_line(b, " b->data[b->len++] = (char)byte;"); + sb_line(b, " b->data[b->len] = '\\0';"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_push_int(tk_w h, tk_w val) {"); + sb_line(b, " char buf[32];"); + sb_line(b, " snprintf(buf, sizeof(buf), \"%lld\", (long long)val);"); + sb_line(b, " return track_sb_push(h, TK_WORD(buf));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_len(tk_w h) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h);"); + sb_line(b, " return b ? (tk_w)b->len : 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Copy the builder contents into a new string. */"); + sb_line(b, "tk_w track_sb_str(tk_w h) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h);"); + sb_line(b, " return TK_WORD(b ? tk_dup_n(b->data, b->len) : tk_dup(\"\"));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_sb_clear(tk_w h) {"); + sb_line(b, " TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h);"); + sb_line(b, " if (b) {"); + sb_line(b, " b->len = 0;"); + sb_line(b, " b->data[0] = '\\0';"); + sb_line(b, " }"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ----------------------------------------------------------------- arrays */"); + sb_line(b, ""); + sb_line(b, "/* Array values are cells [length, elements...]; indexing is bounds checked. */"); + sb_line(b, "static void tk_bounds(tk_w a, tk_w i) {"); + sb_line(b, " if (!a) track_abort(TK_WORD(\"index into a null array\"));"); + sb_line(b, " if (i < 0 || i >= ((tk_w *)TK_PTR(a))[0]) track_abort(TK_WORD(\"array index out of bounds\"));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_array_get(tk_w a, tk_w i) {"); + sb_line(b, " tk_bounds(a, i);"); + sb_line(b, " return ((tk_w *)TK_PTR(a))[i + 1];"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w track_array_set(tk_w a, tk_w i, tk_w v) {"); + sb_line(b, " tk_bounds(a, i);"); + sb_line(b, " ((tk_w *)TK_PTR(a))[i + 1] = v;"); + sb_line(b, " return v;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* Copy of a[from..to]; `to` of -1 means the end of the array. */"); + sb_line(b, "tk_w track_array_slice(tk_w a, tk_w from, tk_w to) {"); + sb_line(b, " tk_w len = a ? ((tk_w *)TK_PTR(a))[0] : 0;"); + sb_line(b, " if (to < 0) to = len;"); + sb_line(b, " if (from < 0 || from > to || to > len) track_abort(TK_WORD(\"array slice out of bounds\"));"); + sb_line(b, " tk_w out = alloc((to - from + 1) * 8);"); + sb_line(b, " ((tk_w *)TK_PTR(out))[0] = to - from;"); + sb_line(b, " if (to > from) memcpy((tk_w *)TK_PTR(out) + 1, (tk_w *)TK_PTR(a) + 1 + from, (size_t)(to - from) * 8);"); + sb_line(b, " return out;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w array_len(tk_w a) { return a ? ((tk_w *)TK_PTR(a))[0] : 0; }"); + sb_line(b, ""); + sb_line(b, "/* A field access whose struct tag matched none of the declaring structs. */"); + sb_line(b, "tk_w track_bad_field(void) {"); + sb_line(b, " return track_abort(TK_WORD(\"field access on a value of the wrong struct type\"));"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "/* ---------------------------------------------------------------- network */"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_tcp_listen(tk_w port) {"); + sb_line(b, " int fd = socket(AF_INET, SOCK_STREAM, 0);"); + sb_line(b, " if (fd < 0) return -1;"); + sb_line(b, " int opt = 1;"); + sb_line(b, " setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));"); + sb_line(b, " struct sockaddr_in addr;"); + sb_line(b, " memset(&addr, 0, sizeof(addr));"); + sb_line(b, " addr.sin_family = AF_INET;"); + sb_line(b, " addr.sin_addr.s_addr = INADDR_ANY;"); + sb_line(b, " addr.sin_port = htons((uint16_t)port);"); + sb_line(b, " if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {"); + sb_line(b, " if (errno == EADDRINUSE) {"); + sb_line(b, " fprintf(stderr, \"Track Network Error: Port conflict! Port %d is already in use by another process.\\n\", (int)port);"); + sb_line(b, " } else {"); + sb_line(b, " fprintf(stderr, \"Track Network Error: Failed to bind TCP socket on port %d (errno: %d).\\n\", (int)port, errno);"); + sb_line(b, " }"); + sb_line(b, " fflush(stderr);"); + sb_line(b, " close(fd);"); + sb_line(b, " return -1;"); + sb_line(b, " }"); + sb_line(b, " if (listen(fd, 128) < 0) {"); + sb_line(b, " close(fd);"); + sb_line(b, " return -1;"); + sb_line(b, " }"); + sb_line(b, " return fd;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_accept(tk_w server_fd) {"); + sb_line(b, " if (server_fd < 0) return -1;"); + sb_line(b, " struct sockaddr_in client_addr;"); + sb_line(b, " socklen_t addrlen = sizeof(client_addr);"); + sb_line(b, " return accept((int)server_fd, (struct sockaddr *)&client_addr, &addrlen);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_connect(tk_w host, tk_w port) {"); + sb_line(b, " if (!host) return -1;"); + sb_line(b, " int fd = socket(AF_INET, SOCK_STREAM, 0);"); + sb_line(b, " if (fd < 0) return -1;"); + sb_line(b, " struct sockaddr_in addr;"); + sb_line(b, " memset(&addr, 0, sizeof(addr));"); + sb_line(b, " addr.sin_family = AF_INET;"); + sb_line(b, " addr.sin_port = htons((uint16_t)port);"); + sb_line(b, " if (inet_pton(AF_INET, TK_STR(host), &addr.sin_addr) <= 0 ||"); + sb_line(b, " connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {"); + sb_line(b, " close(fd);"); + sb_line(b, " return -1;"); + sb_line(b, " }"); + sb_line(b, " return fd;"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_send(tk_w fd, tk_w data, tk_w len) {"); + sb_line(b, " if (fd < 0 || !data) return -1;"); + sb_line(b, " return (tk_w)send((int)fd, TK_PTR(data), (size_t)len, 0);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_recv(tk_w fd, tk_w buf, tk_w max_len) {"); + sb_line(b, " if (fd < 0 || !buf) return -1;"); + sb_line(b, " return (tk_w)recv((int)fd, TK_PTR(buf), (size_t)max_len, 0);"); + sb_line(b, "}"); + sb_line(b, ""); + sb_line(b, "tk_w net_socket_close(tk_w fd) {"); + sb_line(b, " if (fd >= 0) close((int)fd);"); + sb_line(b, " return 0;"); + sb_line(b, "}"); + return 0; +} + +// Prototypes of every runtime function, for generated programs. +fn runtime_prototypes(b: i64) -> i64 { + sb_line(b, "tk_w track_runtime_init(tk_w, tk_w);"); + sb_line(b, "tk_w os_args_count(void);"); + sb_line(b, "tk_w os_arg(tk_w);"); + sb_line(b, "tk_w sys_exit(tk_w);"); + sb_line(b, "tk_w track_abort(tk_w);"); + sb_line(b, "tk_w track_trap_div(void);"); + sb_line(b, "tk_w sys_exec(tk_w);"); + sb_line(b, "tk_w process_spawn(tk_w);"); + sb_line(b, "tk_w clock_ms(void);"); + sb_line(b, "tk_w env_get(tk_w);"); + sb_line(b, "tk_w env_exists(tk_w);"); + sb_line(b, "tk_w sys_set_memory_limit(tk_w);"); + sb_line(b, "tk_w sys_get_memory_used(void);"); + sb_line(b, "tk_w alloc(tk_w);"); + sb_line(b, "tk_w dealloc(tk_w);"); + sb_line(b, "tk_w mem_realloc(tk_w, tk_w);"); + sb_line(b, "tk_w track_words_copy(tk_w, tk_w);"); + sb_line(b, "tk_w print(tk_w);"); + sb_line(b, "tk_w println(tk_w);"); + sb_line(b, "tk_w print_int(tk_w);"); + sb_line(b, "tk_w print_hex(tk_w);"); + sb_line(b, "tk_w print_str(tk_w);"); + sb_line(b, "tk_w print_err(tk_w);"); + sb_line(b, "tk_w eprint(tk_w);"); + sb_line(b, "tk_w track_out(tk_w);"); + sb_line(b, "tk_w track_err(tk_w);"); + sb_line(b, "tk_w read_line(void);"); + sb_line(b, "tk_w str_from_literal(tk_w);"); + sb_line(b, "tk_w str_free(tk_w);"); + sb_line(b, "tk_w str_len(tk_w);"); + sb_line(b, "tk_w str_eq(tk_w, tk_w);"); + sb_line(b, "tk_w str_concat(tk_w, tk_w);"); + sb_line(b, "tk_w str_find(tk_w, tk_w);"); + sb_line(b, "tk_w str_contains(tk_w, tk_w);"); + sb_line(b, "tk_w str_to_int(tk_w);"); + sb_line(b, "tk_w str_is_int(tk_w);"); + sb_line(b, "tk_w int_to_str(tk_w);"); + sb_line(b, "tk_w str_starts_with(tk_w, tk_w);"); + sb_line(b, "tk_w str_ends_with(tk_w, tk_w);"); + sb_line(b, "tk_w str_substr(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w str_trim(tk_w);"); + sb_line(b, "tk_w str_char_at(tk_w, tk_w);"); + sb_line(b, "tk_w track_str_bytes(tk_w);"); + sb_line(b, "tk_w char_is_digit(tk_w);"); + sb_line(b, "tk_w char_is_alpha(tk_w);"); + sb_line(b, "tk_w char_is_alphanumeric(tk_w);"); + sb_line(b, "tk_w char_is_space(tk_w);"); + sb_line(b, "tk_w char_to_upper(tk_w);"); + sb_line(b, "tk_w char_to_lower(tk_w);"); + sb_line(b, "tk_w path_basename(tk_w);"); + sb_line(b, "tk_w path_ext(tk_w);"); + sb_line(b, "tk_w path_join(tk_w, tk_w);"); + sb_line(b, "tk_w file_open(tk_w, tk_w);"); + sb_line(b, "tk_w file_close(tk_w);"); + sb_line(b, "tk_w file_read_all(tk_w);"); + sb_line(b, "tk_w file_write(tk_w, tk_w);"); + sb_line(b, "tk_w file_exists(tk_w);"); + sb_line(b, "tk_w file_remove(tk_w);"); + sb_line(b, "tk_w file_size(tk_w);"); + sb_line(b, "tk_w dir_exists(tk_w);"); + sb_line(b, "tk_w file_copy(tk_w, tk_w);"); + sb_line(b, "tk_w track_file_read(tk_w);"); + sb_line(b, "tk_w track_file_write_all(tk_w, tk_w);"); + sb_line(b, "tk_w math_abs(tk_w);"); + sb_line(b, "tk_w math_min(tk_w, tk_w);"); + sb_line(b, "tk_w math_max(tk_w, tk_w);"); + sb_line(b, "tk_w math_clamp(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w math_floor(tk_w);"); + sb_line(b, "tk_w math_ceil(tk_w);"); + sb_line(b, "tk_w math_round(tk_w);"); + sb_line(b, "tk_w math_pow(tk_w, tk_w);"); + sb_line(b, "tk_w math_sqrt(tk_w);"); + sb_line(b, "tk_w math_random(void);"); + sb_line(b, "tk_w add(tk_w, tk_w);"); + sb_line(b, "tk_w sum(tk_w, tk_w);"); + sb_line(b, "tk_w sub(tk_w, tk_w);"); + sb_line(b, "tk_w vec_init(tk_w);"); + sb_line(b, "tk_w vec_push(tk_w, tk_w);"); + sb_line(b, "tk_w vec_get(tk_w, tk_w);"); + sb_line(b, "tk_w vec_set(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w vec_pop(tk_w);"); + sb_line(b, "tk_w vec_len(tk_w);"); + sb_line(b, "tk_w vec_clear(tk_w);"); + sb_line(b, "tk_w vec_reserve(tk_w, tk_w);"); + sb_line(b, "tk_w vec_free(tk_w);"); + sb_line(b, "tk_w track_i64_collection_new(tk_w);"); + sb_line(b, "tk_w track_i64_collection_len(tk_w);"); + sb_line(b, "tk_w track_i64_collection_push(tk_w, tk_w);"); + sb_line(b, "tk_w track_i64_collection_get(tk_w, tk_w);"); + sb_line(b, "tk_w track_i64_collection_set(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w track_i64_collection_pop(tk_w);"); + sb_line(b, "tk_w track_i64_collection_truncate(tk_w, tk_w);"); + sb_line(b, "tk_w track_sb_new(void);"); + sb_line(b, "tk_w track_sb_push(tk_w, tk_w);"); + sb_line(b, "tk_w track_sb_push_byte(tk_w, tk_w);"); + sb_line(b, "tk_w track_sb_push_int(tk_w, tk_w);"); + sb_line(b, "tk_w track_sb_len(tk_w);"); + sb_line(b, "tk_w track_sb_str(tk_w);"); + sb_line(b, "tk_w track_sb_clear(tk_w);"); + sb_line(b, "tk_w track_array_get(tk_w, tk_w);"); + sb_line(b, "tk_w track_array_set(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w track_array_slice(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w array_len(tk_w);"); + sb_line(b, "tk_w track_bad_field(void);"); + sb_line(b, "tk_w net_socket_tcp_listen(tk_w);"); + sb_line(b, "tk_w net_socket_accept(tk_w);"); + sb_line(b, "tk_w net_socket_connect(tk_w, tk_w);"); + sb_line(b, "tk_w net_socket_send(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w net_socket_recv(tk_w, tk_w, tk_w);"); + sb_line(b, "tk_w net_socket_close(tk_w);"); + return 0; +} + +// Record the arity of every runtime function (NI_EXTERN). +fn register_runtime(c: i64) -> i64 { + ni_set(c, intern(c, "track_runtime_init"), NI_EXTERN, 2); + ni_set(c, intern(c, "os_args_count"), NI_EXTERN, 0); + ni_set(c, intern(c, "os_arg"), NI_EXTERN, 1); + ni_set(c, intern(c, "sys_exit"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_abort"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_trap_div"), NI_EXTERN, 0); + ni_set(c, intern(c, "sys_exec"), NI_EXTERN, 1); + ni_set(c, intern(c, "process_spawn"), NI_EXTERN, 1); + ni_set(c, intern(c, "clock_ms"), NI_EXTERN, 0); + ni_set(c, intern(c, "env_get"), NI_EXTERN, 1); + ni_set(c, intern(c, "env_exists"), NI_EXTERN, 1); + ni_set(c, intern(c, "sys_set_memory_limit"), NI_EXTERN, 1); + ni_set(c, intern(c, "sys_get_memory_used"), NI_EXTERN, 0); + ni_set(c, intern(c, "alloc"), NI_EXTERN, 1); + ni_set(c, intern(c, "dealloc"), NI_EXTERN, 1); + ni_set(c, intern(c, "mem_realloc"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_words_copy"), NI_EXTERN, 2); + ni_set(c, intern(c, "print"), NI_EXTERN, 1); + ni_set(c, intern(c, "println"), NI_EXTERN, 1); + ni_set(c, intern(c, "print_int"), NI_EXTERN, 1); + ni_set(c, intern(c, "print_hex"), NI_EXTERN, 1); + ni_set(c, intern(c, "print_str"), NI_EXTERN, 1); + ni_set(c, intern(c, "print_err"), NI_EXTERN, 1); + ni_set(c, intern(c, "eprint"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_out"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_err"), NI_EXTERN, 1); + ni_set(c, intern(c, "read_line"), NI_EXTERN, 0); + ni_set(c, intern(c, "str_from_literal"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_free"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_len"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_eq"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_concat"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_find"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_contains"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_to_int"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_is_int"), NI_EXTERN, 1); + ni_set(c, intern(c, "int_to_str"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_starts_with"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_ends_with"), NI_EXTERN, 2); + ni_set(c, intern(c, "str_substr"), NI_EXTERN, 3); + ni_set(c, intern(c, "str_trim"), NI_EXTERN, 1); + ni_set(c, intern(c, "str_char_at"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_str_bytes"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_is_digit"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_is_alpha"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_is_alphanumeric"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_is_space"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_to_upper"), NI_EXTERN, 1); + ni_set(c, intern(c, "char_to_lower"), NI_EXTERN, 1); + ni_set(c, intern(c, "path_basename"), NI_EXTERN, 1); + ni_set(c, intern(c, "path_ext"), NI_EXTERN, 1); + ni_set(c, intern(c, "path_join"), NI_EXTERN, 2); + ni_set(c, intern(c, "file_open"), NI_EXTERN, 2); + ni_set(c, intern(c, "file_close"), NI_EXTERN, 1); + ni_set(c, intern(c, "file_read_all"), NI_EXTERN, 1); + ni_set(c, intern(c, "file_write"), NI_EXTERN, 2); + ni_set(c, intern(c, "file_exists"), NI_EXTERN, 1); + ni_set(c, intern(c, "file_remove"), NI_EXTERN, 1); + ni_set(c, intern(c, "file_size"), NI_EXTERN, 1); + ni_set(c, intern(c, "dir_exists"), NI_EXTERN, 1); + ni_set(c, intern(c, "file_copy"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_file_read"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_file_write_all"), NI_EXTERN, 2); + ni_set(c, intern(c, "math_abs"), NI_EXTERN, 1); + ni_set(c, intern(c, "math_min"), NI_EXTERN, 2); + ni_set(c, intern(c, "math_max"), NI_EXTERN, 2); + ni_set(c, intern(c, "math_clamp"), NI_EXTERN, 3); + ni_set(c, intern(c, "math_floor"), NI_EXTERN, 1); + ni_set(c, intern(c, "math_ceil"), NI_EXTERN, 1); + ni_set(c, intern(c, "math_round"), NI_EXTERN, 1); + ni_set(c, intern(c, "math_pow"), NI_EXTERN, 2); + ni_set(c, intern(c, "math_sqrt"), NI_EXTERN, 1); + ni_set(c, intern(c, "math_random"), NI_EXTERN, 0); + ni_set(c, intern(c, "add"), NI_EXTERN, 2); + ni_set(c, intern(c, "sum"), NI_EXTERN, 2); + ni_set(c, intern(c, "sub"), NI_EXTERN, 2); + ni_set(c, intern(c, "vec_init"), NI_EXTERN, 1); + ni_set(c, intern(c, "vec_push"), NI_EXTERN, 2); + ni_set(c, intern(c, "vec_get"), NI_EXTERN, 2); + ni_set(c, intern(c, "vec_set"), NI_EXTERN, 3); + ni_set(c, intern(c, "vec_pop"), NI_EXTERN, 1); + ni_set(c, intern(c, "vec_len"), NI_EXTERN, 1); + ni_set(c, intern(c, "vec_clear"), NI_EXTERN, 1); + ni_set(c, intern(c, "vec_reserve"), NI_EXTERN, 2); + ni_set(c, intern(c, "vec_free"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_i64_collection_new"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_i64_collection_len"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_i64_collection_push"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_i64_collection_get"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_i64_collection_set"), NI_EXTERN, 3); + ni_set(c, intern(c, "track_i64_collection_pop"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_i64_collection_truncate"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_sb_new"), NI_EXTERN, 0); + ni_set(c, intern(c, "track_sb_push"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_sb_push_byte"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_sb_push_int"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_sb_len"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_sb_str"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_sb_clear"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_array_get"), NI_EXTERN, 2); + ni_set(c, intern(c, "track_array_set"), NI_EXTERN, 3); + ni_set(c, intern(c, "track_array_slice"), NI_EXTERN, 3); + ni_set(c, intern(c, "array_len"), NI_EXTERN, 1); + ni_set(c, intern(c, "track_bad_field"), NI_EXTERN, 0); + ni_set(c, intern(c, "net_socket_tcp_listen"), NI_EXTERN, 1); + ni_set(c, intern(c, "net_socket_accept"), NI_EXTERN, 1); + ni_set(c, intern(c, "net_socket_connect"), NI_EXTERN, 2); + ni_set(c, intern(c, "net_socket_send"), NI_EXTERN, 3); + ni_set(c, intern(c, "net_socket_recv"), NI_EXTERN, 3); + ni_set(c, intern(c, "net_socket_close"), NI_EXTERN, 1); + return 0; +} diff --git a/compiler/src/symbols.trk b/compiler/src/symbols.trk new file mode 100644 index 0000000..97847ca --- /dev/null +++ b/compiler/src/symbols.trk @@ -0,0 +1,234 @@ +// symbols.trk — program-wide declarations shared by the checker and the +// C emitter: functions, enum/union layouts, type aliases, and integer +// constants (evaluated in item order, as the bootstrap compiler does). + +import "base"; +import "ast"; + +// Compile-time integer evaluation. Returns (ok, value). +fn const_eval(c: i64, n: i64) -> (i64, i64) { + let k = nk(c, n); + if k == K_INT { + return (1, na(c, n)); + } + if k == K_BOOL { + return (1, na(c, n)); + } + if k == K_VAR { + let name = na(c, n); + if ni(c, name, NI_CONSTOK) == 1 { + return (1, ni(c, name, NI_CONSTVAL)); + } + return (0, 0); + } + if k == K_UN { + if na(c, n) != OP_NEG { + return (0, 0); + } + let (ok, v) = const_eval(c, nb(c, n)); + return (ok, 0 - v); + } + if k == K_BIN { + let op = na(c, n); + let (lok, l) = const_eval(c, nb(c, n)); + let (rok, r) = const_eval(c, nc(c, n)); + if lok == 0 || rok == 0 { + return (0, 0); + } + if op == OP_ADD { return (1, l + r); } + if op == OP_SUB { return (1, l - r); } + if op == OP_MUL { return (1, l * r); } + if op == OP_DIV && r != 0 { return (1, l / r); } + if op == OP_MOD && r != 0 { return (1, l % r); } + if op == OP_BAND { return (1, l & r); } + if op == OP_BOR { return (1, l | r); } + if op == OP_SHL { return (1, l << (r & 63)); } + if op == OP_SHR { return (1, l >> (r & 63)); } + return (0, 0); + } + return (0, 0); +} + +fn variant_name(c: i64, owner: i64, variant: i64) -> i64 { + return intern(c, text_cat3(name_text(c, owner), "::", name_text(c, variant))); +} + +// Record enum/union/alias/struct/function/constant declarations of every +// item. Later definitions of the same type name replace earlier ones. +fn collect_symbols(c: i64) -> i64 { + let items = cx(c, CX_ITEMS); + let mut i: i64 = 0; + while i < vlen(items) { + let n = vget(items, i); + let k = nk(c, n); + if k == K_CONST { + let (ok, v) = const_eval(c, nb(c, n)); + ni_set(c, na(c, n), NI_CONST, n); + if ok == 1 { + ni_set(c, na(c, n), NI_CONSTOK, 1); + ni_set(c, na(c, n), NI_CONSTVAL, v); + } + } else if k == K_ENUM { + let name = na(c, n); + ni_set(c, name, NI_TYPEKIND, 1); + ni_set(c, name, NI_TYPENODE, n); + let variants = nc(c, n); + let mut next: i64 = 0; + let mut j: i64 = 0; + while j < list_len(c, variants) { + let vname = variant_name(c, name, list_at(c, variants, j)); + let value = list_at(c, variants, j + 1); + let mut disc = next; + if value >= 0 { + let (ok, v) = const_eval(c, value); + if ok == 1 { + disc = v; + } + } + ni_set(c, vname, NI_VARIANT, disc); + ni_set(c, vname, NI_OWNER, name); + next = disc + 1; + j = j + 2; + } + } else if k == K_UNION { + let name = na(c, n); + ni_set(c, name, NI_TYPEKIND, 2); + ni_set(c, name, NI_TYPENODE, n); + ni_set(c, name, NI_PAYLOAD, 0); + let variants = nb(c, n); + let mut j: i64 = 0; + while j < list_len(c, variants) { + let vname = variant_name(c, name, list_at(c, variants, j)); + ni_set(c, vname, NI_VARIANT, j / 2); + ni_set(c, vname, NI_OWNER, name); + if list_at(c, variants, j + 1) >= 0 { + ni_set(c, name, NI_PAYLOAD, 1); + } + j = j + 2; + } + } else if k == K_TYPEDEF { + ni_set(c, na(c, n), NI_TYPEKIND, 4); + ni_set(c, na(c, n), NI_TYPENODE, nb(c, n)); + } else if k == K_STRUCT { + if ni(c, na(c, n), NI_TYPEKIND) != 3 { + vpush(cx(c, CX_STRUCTS), na(c, n)); + } + ni_set(c, na(c, n), NI_TYPEKIND, 3); + ni_set(c, na(c, n), NI_TYPENODE, n); + } else if k == K_FN || k == K_MACRODEF { + let name = na(c, n); + if ni(c, name, NI_FN) >= 0 { + fail_at(c, npos(c, n), "E0201", text_cat3("duplicate definition of function '", name_text(c, name), "'")); + } + ni_set(c, name, NI_FN, n); + } + i = i + 1; + } + return 0; +} + +// Discriminant of `Type::Variant`, or -1 when it is not a known variant. +fn variant_disc(c: i64, name: i64) -> i64 { + if ni(c, name, NI_OWNER) < 0 { + return -1; + } + return ni(c, name, NI_VARIANT); +} + +fn is_payload_union(c: i64, name: i64) -> bool { + return ni(c, name, NI_TYPEKIND) == 2 && ni(c, name, NI_PAYLOAD) == 1; +} + +// True when `Type::Variant` belongs to a union with payload variants +// (its values are heap cells `[tag, payload...]`). +fn is_payload_variant(c: i64, name: i64) -> bool { + let owner = ni(c, name, NI_OWNER); + if owner < 0 { + return false; + } + return is_payload_union(c, owner); +} + +// Number of words of a tuple type (resolving aliases), or -1. +fn tuple_arity(c: i64, ty: i64) -> i64 { + let mut t = ty; + let mut depth: i64 = 0; + while depth < 16 { + if t < 0 { + return -1; + } + if nk(c, t) == T_TUPLE { + return list_len(c, na(c, t)); + } + if nk(c, t) != T_CUSTOM { + return -1; + } + if ni(c, na(c, t), NI_TYPEKIND) != 4 { + return -1; + } + t = ni(c, na(c, t), NI_TYPENODE); + depth = depth + 1; + } + return -1; +} + +// Last `::` segment of a qualified name (the name itself if unqualified). +fn last_segment(c: i64, name: i64) -> i64 { + let text = name_text(c, name); + let bytes = text_bytes(text); + let mut i: i64 = vlen(bytes) - 1; + while i > 0 { + if vget(bytes, i) == 58 && vget(bytes, i - 1) == 58 { + return intern(c, str_substr(text, i + 1, vlen(bytes) - i - 1)); + } + i = i - 1; + } + return name; +} + +// Runtime tag in word 0 of a struct value (same hash as the Rust compiler). +fn struct_tag(c: i64, name: i64) -> i64 { + return text_hash(name_text(c, name)); +} + +// The K_STRUCT node declaring `name`, or -1. +fn struct_def(c: i64, name: i64) -> i64 { + if ni(c, name, NI_TYPEKIND) != 3 { + return -1; + } + return ni(c, name, NI_TYPENODE); +} + +// Index of `field` among the fields of struct node `s`, or -1. +fn struct_field_index(c: i64, s: i64, field: i64) -> i64 { + let fields = nb(c, s); + let mut i: i64 = 0; + while i < list_len(c, fields) { + if list_at(c, fields, i) == field { + return i / 2; + } + i = i + 2; + } + return -1; +} + +// Flattened (tag, index) pairs of every struct declaring `field`. +fn field_candidates(c: i64, field: i64) -> i64 { + let out = vnew(); + let structs = cx(c, CX_STRUCTS); + let mut i: i64 = 0; + while i < vlen(structs) { + let name = vget(structs, i); + let idx = struct_field_index(c, struct_def(c, name), field); + if idx >= 0 { + vpush(out, struct_tag(c, name)); + vpush(out, idx); + } + i = i + 1; + } + return out; +} + +fn is_qualified(c: i64, name: i64) -> bool { + return str_contains(name_text(c, name), "::") != 0; +} diff --git a/compiler/src/testing.trk b/compiler/src/testing.trk new file mode 100644 index 0000000..369ae74 --- /dev/null +++ b/compiler/src/testing.trk @@ -0,0 +1,45 @@ +// testing.trk — helpers for the native test suite (compiler/tests/). + +import "base"; +import "lexer"; +import "parser"; + +fn expect_i64(actual: i64, expected: i64, what: i64) -> i64 { + if actual != expected { + err(text_cat3("expected ", text_of_int(expected), text_cat3(", got ", text_of_int(actual), "\n"))); + track_abort(what); + } + return 0; +} + +fn expect_true(value: bool, what: i64) -> i64 { + if !value { + track_abort(what); + } + return 0; +} + +fn expect_text(actual: i64, expected: i64, what: i64) -> i64 { + if !text_eq(actual, expected) { + err(text_cat3("expected \"", expected, text_cat3("\", got \"", actual, "\"\n"))); + track_abort(what); + } + return 0; +} + +fn test_context() -> i64 { + let c = cx_new(); + lex_init(c); + return c; +} + +// Lex `text` as a file named test.trk; returns the first token index. +fn lex_text(c: i64, text: i64) -> i64 { + let f = add_source(c, "test.trk", text); + return lex_file(c, f); +} + +// Parse `text` and return its top-level statement list. +fn parse_text(c: i64, text: i64) -> i64 { + return parse_file(c, lex_text(c, text)); +} diff --git a/compiler/src/token.trk b/compiler/src/token.trk deleted file mode 100644 index d7d2856..0000000 --- a/compiler/src/token.trk +++ /dev/null @@ -1,15 +0,0 @@ -// token.trk — Token definitions for the Track self-hosted lexer (v0.7) -// Pure type definitions; no logic. Kept separate so `lexer.trk` can stay -// focused on scanning. Both files define the same `union Token` for -// codegen (yard links them as separate objects, types are nominally equal). - -union Token { - Import, Use, Let, Mut, With, Fn, Return, If, Else, While, For, In, True, False, - Struct, Enum, Union, Match, As, AtMacro, Const, TypeDef, At, - TyU8, TyI8, TyI32, TyU32, TyI64, TyU64, TyBool, TyVoid, TyPtr, - Int(i64), StrLit(Str), Ident(Str), - Eq, EqEq, Neq, Bang, LBrace, RBrace, LParen, RParen, LBracket, RBracket, - Comma, Colon, ColonColon, Arrow, FatArrow, Semicolon, DotDot, Dot, - Underscore, Amp, Pipe, Star, Plus, Minus, Slash, Percent, Lt, Gt, Lte, Gte, - AmpAmp, PipePipe, Shl, Shr, Eof, Unknown, -} diff --git a/compiler/tests/backend_test.trk b/compiler/tests/backend_test.trk new file mode 100644 index 0000000..65282ef --- /dev/null +++ b/compiler/tests/backend_test.trk @@ -0,0 +1,83 @@ +// Checker and C emitter on in-memory programs. Rejections exit the +// process, so they are covered by the Rust suite (tests/selfhost_tests.rs); +// here every program must be accepted. + +import "base"; +import "ast"; +import "symbols"; +import "checker"; +import "cgen"; +import "testing"; + +fn compile_text(text: i64) -> i64 { + let c = test_context(); + let items = parse_text(c, text); + let all = cx(c, CX_ITEMS); + let files = cx(c, CX_ITEMFILE); + let mut i: i64 = 0; + while i < list_len(c, items) { + vpush(all, list_at(c, items, i)); + vpush(files, 0); + i = i + 1; + } + collect_symbols(c); + check_program(c); + return c; +} + +fn contains(haystack: i64, needle: i64) -> bool { + return str_contains(haystack, needle) != 0; +} + +fn test_constants_and_enums_are_evaluated() -> i64 { + let c = compile_text("const A = 4; const B = A * 2 + 1; enum E { X = 5, Y, Z = 10 } fn main() -> void { print(B); }"); + expect_i64(ni(c, intern(c, "B"), NI_CONSTVAL), 9, "constant arithmetic"); + expect_i64(variant_disc(c, intern(c, "E::Y")), 6, "implicit discriminant follows explicit one"); + expect_i64(variant_disc(c, intern(c, "E::Z")), 10, "explicit discriminant"); + return 0; +} + +fn test_inference_tracks_copy_types() -> i64 { + let c = compile_text("fn id(x: T) -> T { return x; } fn main() -> void { let a = id(1); print(a); print(a); let s = (1, true); let (n, f) = s; print(n); print(n); }"); + expect_true(fn_known(c, intern(c, "id")), "generic function registered"); + return 0; +} + +fn test_emitted_c_shape() -> i64 { + let c = compile_text("union V { I(i64), N } fn add(a: i64, b: i64) -> i64 { return a + b; } fn main() -> void { let v = V::I(add(1, 2)); match v { V::I(x) => print(x), _ => print(0), } }"); + let code = generate_c(c); + expect_true(contains(code, "tk_w tk_add(tk_w a0, tk_w a1) {"), "function signature"); + expect_true(contains(code, "return (l0 + l1);"), "arithmetic return"); + expect_true(contains(code, "= alloc(16);"), "payload variant is a heap cell"); + expect_true(contains(code, "(TK_AT(t"), "match loads the tag"); + expect_true(contains(code, "int main(int argc, char **argv) {"), "C entry point"); + return 0; +} + +fn test_structs_and_arrays_lower_to_cells() -> i64 { + let c = compile_text("struct P { x: i64, y: i64 } struct Q { y: i64 } fn main() -> void { let mut p = P { y: 2, x: 1 }; p.x = 3; print(p.y); let a = [4, 5]; print(a[1]); }"); + let tag = struct_tag(c, intern(c, "P")); + expect_true(tag == text_hash("P"), "struct tag is the name hash"); + expect_i64(vlen(field_candidates(c, intern(c, "y"))), 4, "two structs declare y"); + let code = generate_c(c); + expect_true(contains(code, text_cat3("TK_AT(t0, 0) = ", text_of_int(tag), ";")), "tag in word 0"); + expect_true(contains(code, "TK_AT(t0, 1) = 1;"), "fields in declaration order"); + expect_true(contains(code, "track_bad_field()"), "shared field name dispatches on the tag"); + expect_true(contains(code, "track_array_get("), "bounds-checked indexing"); + return 0; +} + +fn test_string_literals_are_escaped() -> i64 { + expect_text(c_string("a\"b\\c?\n"), "TK_S(\"a\\\"b\\\\c\\?\\n\")", "C escapes"); + return 0; +} + +fn main() -> i64 { + test_constants_and_enums_are_evaluated(); + test_inference_tracks_copy_types(); + test_emitted_c_shape(); + test_structs_and_arrays_lower_to_cells(); + test_string_literals_are_escaped(); + print("ok"); + return 0; +} diff --git a/compiler/tests/lexer_test.trk b/compiler/tests/lexer_test.trk new file mode 100644 index 0000000..2f014ec --- /dev/null +++ b/compiler/tests/lexer_test.trk @@ -0,0 +1,96 @@ +import "base"; +import "lexer"; +import "testing"; + +fn kind_at(c: i64, first: i64, i: i64) -> i64 { + return tok_kind(c, first + i); +} + +fn test_keywords_identifiers_and_literals() -> i64 { + let c = test_context(); + let first = lex_text(c, "let mut letx = 42; import _ _a"); + expect_i64(kind_at(c, first, 0), TK_LET, "let keyword"); + expect_i64(kind_at(c, first, 1), TK_MUT, "mut keyword"); + expect_i64(kind_at(c, first, 2), TK_IDENT, "keyword prefix is an identifier"); + expect_text(name_text(c, tok_value(c, first + 2)), "letx", "identifier text"); + expect_i64(kind_at(c, first, 3), TK_EQ, "assignment"); + expect_i64(kind_at(c, first, 4), TK_INT, "integer"); + expect_i64(tok_value(c, first + 4), 42, "integer value"); + expect_i64(kind_at(c, first, 5), TK_SEMI, "semicolon"); + expect_i64(kind_at(c, first, 6), TK_IMPORT, "import keyword"); + expect_i64(kind_at(c, first, 7), TK_UNDERSCORE, "lone underscore"); + expect_i64(kind_at(c, first, 8), TK_IDENT, "underscore-prefixed identifier"); + expect_i64(kind_at(c, first, 9), TK_EOF, "end of file"); + return 0; +} + +fn test_primitive_types_are_keywords() -> i64 { + let c = test_context(); + let first = lex_text(c, "u8 i8 i32 u32 i64 u64 bool void ptr Str"); + expect_i64(kind_at(c, first, 0), TK_TY_U8, "u8"); + expect_i64(kind_at(c, first, 2), TK_TY_I32, "i32"); + expect_i64(kind_at(c, first, 4), TK_TY_I64, "i64"); + expect_i64(kind_at(c, first, 6), TK_TY_BOOL, "bool"); + expect_i64(kind_at(c, first, 7), TK_TY_VOID, "void"); + expect_i64(kind_at(c, first, 8), TK_TY_PTR, "ptr"); + expect_i64(kind_at(c, first, 9), TK_IDENT, "Str is an identifier"); + return 0; +} + +fn test_operators_take_longest_match() -> i64 { + let c = test_context(); + let first = lex_text(c, "== != <= >= << >> && || -> => :: .. . : & | < > ! ="); + expect_i64(kind_at(c, first, 0), TK_EQEQ, "=="); + expect_i64(kind_at(c, first, 1), TK_NEQ, "!="); + expect_i64(kind_at(c, first, 2), TK_LTE, "<="); + expect_i64(kind_at(c, first, 3), TK_GTE, ">="); + expect_i64(kind_at(c, first, 4), TK_SHL, "<<"); + expect_i64(kind_at(c, first, 5), TK_SHR, ">>"); + expect_i64(kind_at(c, first, 6), TK_AMPAMP, "&&"); + expect_i64(kind_at(c, first, 7), TK_PIPEPIPE, "||"); + expect_i64(kind_at(c, first, 8), TK_ARROW, "->"); + expect_i64(kind_at(c, first, 9), TK_FATARROW, "=>"); + expect_i64(kind_at(c, first, 10), TK_COLONCOLON, "::"); + expect_i64(kind_at(c, first, 11), TK_DOTDOT, ".."); + expect_i64(kind_at(c, first, 12), TK_DOT, "."); + expect_i64(kind_at(c, first, 13), TK_COLON, ":"); + expect_i64(kind_at(c, first, 14), TK_AMP, "&"); + expect_i64(kind_at(c, first, 15), TK_PIPE, "|"); + expect_i64(kind_at(c, first, 16), TK_LT, "<"); + expect_i64(kind_at(c, first, 17), TK_GT, ">"); + expect_i64(kind_at(c, first, 18), TK_BANG, "!"); + expect_i64(kind_at(c, first, 19), TK_EQ, "="); + return 0; +} + +fn test_strings_comments_and_positions() -> i64 { + let c = test_context(); + let first = lex_text(c, "// header\n \"a\\n\\\"b\\\\\" @macro @x"); + expect_i64(kind_at(c, first, 0), TK_STR, "string after comment"); + expect_text(tok_value(c, first), "a\n\"b\\", "escapes are decoded"); + expect_i64(tok_start(c, first), 12, "string start offset"); + expect_i64(tok_end(c, first), 22, "string end offset"); + expect_i64(kind_at(c, first, 1), TK_ATMACRO, "@macro"); + expect_i64(kind_at(c, first, 2), TK_AT, "@"); + expect_i64(kind_at(c, first, 3), TK_IDENT, "macro name"); + return 0; +} + +fn test_files_get_disjoint_positions() -> i64 { + let c = test_context(); + let a = lex_text(c, "fn"); + let b = lex_text(c, "fn"); + expect_true(tok_start(c, b) > tok_end(c, a), "second file starts after the first"); + expect_i64(file_of_pos(c, tok_start(c, b)), 1, "position maps back to its file"); + return 0; +} + +fn main() -> i64 { + test_keywords_identifiers_and_literals(); + test_primitive_types_are_keywords(); + test_operators_take_longest_match(); + test_strings_comments_and_positions(); + test_files_get_disjoint_positions(); + print("ok"); + return 0; +} diff --git a/compiler/tests/parser_test.trk b/compiler/tests/parser_test.trk index f6ba030..5dd8d33 100644 --- a/compiler/tests/parser_test.trk +++ b/compiler/tests/parser_test.trk @@ -1,101 +1,142 @@ -import "parser"; +import "base"; import "ast"; -import "lexer"; +import "parser"; +import "testing"; -fn assert_i64(actual: i64, expected: i64) -> void { - if actual != expected { - abort("parser integer assertion failed"); - } +fn item(c: i64, items: i64, i: i64) -> i64 { + return list_at(c, items, i); } -fn assert_bool(actual: bool, expected: bool) -> void { - if actual != expected { - abort("parser boolean assertion failed"); - } +fn name_of(c: i64, id: i64) -> i64 { + return name_text(c, id); } -fn assert_string(actual: &Str, expected: &Str) -> void { - if !str_eq(actual, expected) { - eprint(actual); - abort("parser string assertion failed"); - } +fn test_arithmetic_precedence_from_fixture() -> i64 { + let c = test_context(); + let items = parse_text(c, read_text("tests/fixtures/parser-subset.txt")); + expect_i64(list_len(c, items), 2, "two statements"); + let decl = item(c, items, 0); + expect_i64(nk(c, decl), K_LET, "let statement"); + expect_text(name_of(c, na(c, decl)), "answer", "binding name"); + // 40 + 2 * 1 => (+ 40 (* 2 1)) + let sum = nd(c, decl); + expect_i64(nk(c, sum), K_BIN, "binary sum"); + expect_i64(na(c, sum), OP_ADD, "addition at the root"); + expect_i64(na(c, nb(c, sum)), 40, "left operand"); + expect_i64(na(c, nc(c, sum)), OP_MUL, "multiplication binds tighter"); + expect_i64(nk(c, item(c, items, 1)), K_RETURN, "return statement"); + return 0; } -fn read_parser_fixture(path: &Str) -> Str { - let file = file_open(path, "r"); - return file_read_all(file); +fn test_logic_comparison_and_unary() -> i64 { + let c = test_context(); + let items = parse_text(c, "a || b && !c == -d;"); + let e = item(c, items, 0); + expect_i64(na(c, e), OP_OR, "|| is lowest"); + let rhs = nc(c, e); + expect_i64(na(c, rhs), OP_AND, "&& below comparison"); + let cmp = nc(c, rhs); + expect_i64(na(c, cmp), OP_EQ, "comparison"); + expect_i64(na(c, nb(c, cmp)), OP_NOT, "unary not"); + expect_i64(na(c, nc(c, cmp)), OP_NEG, "unary minus"); + return 0; } -fn test_let_return_and_precedence() -> void { - let source = read_parser_fixture("tests/fixtures/parser-subset.txt"); - let result = parse_subset(&source); - assert_bool(parse_result_ok(&result), true); - let ast = parse_result_ast(&result); - assert_i64(ast_node_count(&ast), 9); - - assert_i64(ast_node_kind(&ast, 4), ast_kind_binary()); - assert_i64(ast_node_value(&ast, 4), token_tag_star()); - assert_i64(ast_node_extra(&ast, 4), 2); - assert_i64(ast_node_auxiliary(&ast, 4), 3); - - assert_i64(ast_node_kind(&ast, 5), ast_kind_binary()); - assert_i64(ast_node_value(&ast, 5), token_tag_plus()); - assert_i64(ast_node_extra(&ast, 5), 1); - assert_i64(ast_node_auxiliary(&ast, 5), 4); +fn test_postfix_calls_and_tuples() -> i64 { + let c = test_context(); + let items = parse_text(c, "let t = (1, 2); x.f(3).0; g(a, b)"); + let tuple = nd(c, item(c, items, 0)); + expect_i64(nk(c, tuple), K_TUPLE, "tuple literal"); + expect_i64(list_len(c, na(c, tuple)), 2, "tuple arity"); + let index = item(c, items, 1); + expect_i64(nk(c, index), K_TINDEX, "tuple index"); + let call = na(c, index); + expect_i64(nk(c, call), K_CALL, "method call is a call"); + expect_text(name_of(c, na(c, call)), "f", "method name"); + expect_i64(list_len(c, nb(c, call)), 2, "receiver becomes the first argument"); + expect_i64(list_len(c, nb(c, item(c, items, 2))), 2, "plain call arguments"); + return 0; +} - assert_i64(ast_node_kind(&ast, 6), ast_kind_let()); - assert_i64(ast_node_value(&ast, 6), 0); - assert_i64(ast_node_auxiliary(&ast, 6), 5); - assert_i64(ast_node_kind(&ast, 8), ast_kind_return()); - assert_i64(ast_node_value(&ast, 8), 7); +fn test_struct_literals_are_disabled_in_headers() -> i64 { + let c = test_context(); + let items = parse_text(c, "if ready { go(); } else if later { wait(); } let p = Point { x: 1, y: 2 };"); + let branch = item(c, items, 0); + expect_i64(nk(c, branch), K_IF, "if statement"); + expect_i64(nk(c, na(c, branch)), K_VAR, "condition is a variable, not a struct literal"); + let chain = nc(c, branch); + expect_i64(list_len(c, chain), 1, "else-if is a nested if"); + expect_i64(nk(c, list_at(c, chain, 0)), K_IF, "nested if"); + let init = nd(c, item(c, items, 1)); + expect_i64(nk(c, init), K_SINIT, "struct literal outside headers"); + expect_i64(list_len(c, nb(c, init)), 4, "two field/value pairs"); + return 0; } -fn test_unary_bool_string_and_mutability() -> void { - let source = "let mut flag = !false; let text = \"ok\";"; - let result = parse_subset(&source); - assert_bool(parse_result_ok(&result), true); - let ast = parse_result_ast(&result); - assert_i64(ast_node_count(&ast), 7); - assert_i64(ast_node_kind(&ast, 2), ast_kind_unary()); - assert_i64(ast_node_value(&ast, 2), token_tag_bang()); - assert_i64(ast_node_kind(&ast, 3), ast_kind_let()); - assert_i64(ast_node_flags(&ast, 3), 1); - assert_i64(ast_node_kind(&ast, 5), ast_kind_string()); - assert_i64(ast_node_start(&ast, 5), 35); - assert_i64(ast_node_end(&ast, 5), 37); +fn test_functions_and_types() -> i64 { + let c = test_context(); + let items = parse_text(c, "fn pair(a: T, b: &ptr) -> (T, [i64; 4]) { return (a, b); }"); + let f = item(c, items, 0); + expect_i64(nk(c, f), K_FN, "function"); + expect_i64(list_len(c, nb(c, f)), 2, "generic parameters"); + expect_i64(list_len(c, nc(c, f)), 4, "two name/type pairs"); + let second = list_at(c, nc(c, f), 3); + expect_i64(nk(c, second), T_REF, "reference type"); + expect_i64(nk(c, na(c, second)), T_PTR, "pointer inside reference"); + let ret = nd(c, f); + expect_i64(nk(c, ret), T_TUPLE, "tuple return type"); + expect_i64(nk(c, list_at(c, na(c, ret), 1)), T_ARRAY, "array element type"); + expect_i64(nb(c, list_at(c, na(c, ret), 1)), 4, "array length"); + return 0; } -fn test_error_span_and_diagnostic() -> void { - let source = "let value = ;"; - let result = parse_subset(&source); - assert_bool(parse_result_ok(&result), false); - assert_i64(result.1, 12); - assert_i64(result.2, 13); - assert_i64(result.3, 1); - let actual = format_parse_error(&source, "broken.trk", &result); - let expected = "error[P0001]: expected expression\n --> broken.trk:1:13\n |\n1 | let value = ;\n | ^ expression required here"; - assert_string(&actual, &expected); +fn test_match_patterns_and_destructuring() -> i64 { + let c = test_context(); + let items = parse_text(c, "match v { Shape::Box(w, _) if w > 1 => big(), (1, x) => pair(x), \"s\" => s(), _ => 0, } let (a, b) = t;"); + let m = item(c, items, 0); + expect_i64(nk(c, m), K_MATCH, "match"); + let arms = nb(c, m); + expect_i64(list_len(c, arms), 12, "four (pattern, guard, body) arms"); + let variant = list_at(c, arms, 0); + expect_i64(nk(c, variant), P_VARIANT, "variant pattern"); + expect_text(name_of(c, na(c, variant)), "Shape", "union name"); + expect_text(name_of(c, nb(c, variant)), "Box", "variant name"); + expect_i64(list_len(c, nc(c, variant)), 2, "variant bindings"); + expect_true(list_at(c, arms, 1) >= 0, "guard parsed"); + expect_i64(nk(c, list_at(c, arms, 3)), P_TUPLE, "tuple pattern"); + expect_i64(nk(c, list_at(c, arms, 6)), P_LIT, "string literal pattern"); + expect_i64(nk(c, list_at(c, arms, 9)), P_WILD, "wildcard"); + expect_i64(nk(c, item(c, items, 1)), K_LETD, "destructuring let"); + return 0; } -fn test_parentheses_expression_statement_and_empty_return() -> void { - let source = "(1 + 2) * 3; return;"; - let result = parse_subset(&source); - assert_bool(parse_result_ok(&result), true); - let ast = parse_result_ast(&result); - assert_i64(ast_node_count(&ast), 7); - assert_i64(ast_node_kind(&ast, 2), ast_kind_binary()); - assert_i64(ast_node_value(&ast, 2), token_tag_plus()); - assert_i64(ast_node_kind(&ast, 4), ast_kind_binary()); - assert_i64(ast_node_value(&ast, 4), token_tag_star()); - assert_i64(ast_node_kind(&ast, 5), ast_kind_expression_statement()); - assert_i64(ast_node_kind(&ast, 6), ast_kind_return()); - assert_i64(ast_node_value(&ast, 6), -1); +fn test_declarations() -> i64 { + let c = test_context(); + let items = parse_text(c, "import \"std/io\" as console :: { print }; const N = 3; type Pair = (i64, i64); enum E : u8 { A = 1, B } union U { X(i64), Y } struct S { a: i64 } @macro bit(n: u32) -> u32 { return 1 << n; }"); + let use_item = item(c, items, 0); + expect_i64(nk(c, use_item), K_USE, "import"); + expect_text(na(c, use_item), "std/io", "module path"); + expect_text(name_of(c, nc(c, use_item)), "console", "alias"); + expect_i64(list_len(c, nb(c, use_item)), 1, "imported items"); + expect_i64(nk(c, item(c, items, 1)), K_CONST, "const"); + expect_i64(nk(c, item(c, items, 2)), K_TYPEDEF, "type alias"); + let e = item(c, items, 3); + expect_i64(nk(c, e), K_ENUM, "enum"); + expect_i64(list_len(c, nc(c, e)), 4, "two enum variants"); + expect_i64(nk(c, item(c, items, 4)), K_UNION, "union"); + expect_i64(nk(c, item(c, items, 5)), K_STRUCT, "struct"); + expect_i64(nk(c, item(c, items, 6)), K_MACRODEF, "macro definition"); + return 0; } -fn main() -> void { - test_let_return_and_precedence(); - test_unary_bool_string_and_mutability(); - test_error_span_and_diagnostic(); - test_parentheses_expression_statement_and_empty_return(); +fn main() -> i64 { + test_arithmetic_precedence_from_fixture(); + test_logic_comparison_and_unary(); + test_postfix_calls_and_tuples(); + test_struct_literals_are_disabled_in_headers(); + test_functions_and_types(); + test_match_patterns_and_destructuring(); + test_declarations(); print("ok"); + return 0; } diff --git a/docs/bootstrap.md b/docs/bootstrap.md new file mode 100644 index 0000000..88e295c --- /dev/null +++ b/docs/bootstrap.md @@ -0,0 +1,138 @@ +# Self-Hosting and the Bootstrap (v1.0.0) + +Track 1.0 is self-hosting: `trackc`, the compiler in `compiler/src/`, is +written entirely in Track and compiles itself to a fixed point. + +```sh +scripts/bootstrap.sh +# ==> stage 0: yard builds compiler/ (Rust bootstrap compiler) +# ==> stage 1: stage1 compiles compiler/src/main.trk +# ==> stage 2: stage2 compiles compiler/src/main.trk +# ✓ stage1 and stage2 emit identical C (18954 lines) +# ✓ stage2 and stage3 executables are byte-identical (209880 bytes) +``` + +| Stage | Compiler | Builds | +| :--- | :--- | :--- | +| 0 | Rust `yard` / `track` (Cranelift) | `compiler/` → `stage1/trackc` | +| 1 | `stage1/trackc` | `compiler/src/main.trk` → `stage2/trackc` | +| 2 | `stage2/trackc` | `compiler/src/main.trk` → `stage3/trackc` | + +The gate has two parts. Stage 1 (built by Rust) and stage 2 (built by +Track) must emit byte-identical C for the compiler, which shows the two +builds of `trackc` behave identically. Stage 2 and stage 3 executables +must then be byte-identical. CI runs the script on every push, and +`tests/selfhost_tests.rs` repeats the check under `cargo test`. + +## Using trackc + +```sh +trackc build hello.trk -o hello # native executable via the C compiler +trackc run hello.trk arg1 arg2 # build to a temporary file and run +trackc check hello.trk # parse + ownership check only +trackc emit-c hello.trk -o hello.c # inspect the generated C +trackc tokens hello.trk # token stream +``` + +`trackc` loads the root file and every local module it imports. Imports +resolve beside the importing file first (`/.trk`, +`//mod.trk`), then with the Rust compiler's project-relative +candidates (`src/`, the current directory, `compiler/src/`, `compiler/`). +`std`, `std/io`, `std/fs`, `std/sys`, `std/mem`, and `math/vec` are +built-in namespaces. + +## Pipeline + +| Module | Role | +| :--- | :--- | +| `base.trk` | word vectors, string builders, interning, the compiler context, diagnostics | +| `lexer.trk` | tokens as `(kind, value, start, end)` records; keywords interned first | +| `ast.trk` | node pool: seven words per node, child lists in a list pool | +| `parser.trk` | recursive descent over the full grammar, same precedence as `src/parser/` | +| `loader.trk` | whole-program loading in the order the Rust checker visits modules | +| `symbols.trk` | functions, enum/union layouts, aliases, integer constants | +| `checker.trk` | the ownership checker | +| `cgen.trk` | the C99 backend | +| `runtime_c.trk` | generated copy of `runtime/track_runtime.c` | +| `main.trk` | command-line driver | + +Every compiler table is a runtime-owned vector of 64-bit words referenced +by an `i64` handle, so handles are copy values for the ownership checker. +Names are interned once; per-name facts (function definition, variant +discriminant, current variable slot, C local binding) live in a +fixed-width side table indexed by name id, giving O(1) lookups without +hash maps. + +## Value model (the word ABI) + +Both compilers lower every Track value to one 64-bit word, and both link +the same runtime, `runtime/track_runtime.c`, where every exported function +takes and returns words: + +- integers and booleans are plain words; comparisons produce `0`/`1`; +- `&&` and `||` short-circuit and produce `0`/`1`; +- strings are NUL-terminated `char*`; references (`&x`) are the value itself; +- tuples are pointers to word arrays. A tuple literal lives in a slot of the + enclosing function; `return (a, b)` and returning a local that holds a + tuple literal copy it to the heap; +- enums are their discriminant (explicit `= value`, otherwise previous + 1); +- values of a union with any payload variant are heap cells + `[tag, payload...]`, and `match` loads the tag from the cell; +- division by zero (and `INT64_MIN / -1`) raises `SIGFPE`; shifts mask the + amount to 6 bits and `>>` is logical; arithmetic wraps. + +- structs are heap cells `[tag, fields...]` with fields in declaration + order; the tag is a hash of the struct name, so separately compiled + modules agree. `p.x` reads a field and `p.x = v` writes one; a field name + declared by several structs dispatches on the tag; +- arrays are heap cells `[length, elements...]`; `a[i]` and `a[i] = v` are + bounds checked (out of range aborts with status 134), `a[s..e]` / `a[s..]` + copy a slice, and `array_len(&a)` returns the length. + +## C backend contract + +`trackc build app.trk -o out/app` writes `out/app.c` (the program) and +`out/app.rt.c` (the runtime), then runs, from inside `out/`: + +```sh +$CC -std=gnu99 -O2 -fwrapv -fno-strict-aliasing -fno-builtin -w \ + -o app app.c app.rt.c +``` + +`$CC` defaults to `cc`. Compiling inside the output directory means object +files record only base names, so identical inputs give identical +executables wherever they are built. The generated C needs nothing but +``: all values are `tk_w` (`int64_t`), Track function `f` becomes +`tk_f`, locals are `lN`, temporaries `tN`, tuple slots `sN`, and every +counter restarts per function. Calls to functions that are neither Track +functions nor runtime functions are declared as externs from their first +call site. + +## The runtime + +`runtime/track_runtime.c` is the single source of truth. The Rust compiler +embeds it with `include_str!`; `scripts/embed_runtime.sh` regenerates +`compiler/src/runtime_c.trk`, which holds the source as Track string +literals plus each function's prototype and arity. CI runs +`scripts/embed_runtime.sh --check` so the two copies cannot drift. + +## Ownership checker + +`checker.trk` implements the same rules as `src/checker/` (see +[SPEC.md](../SPEC.md)): moves of non-copy values, borrows that freeze their +target while the reference is live, lens locks, the `if`/`else` merge rule, +no consumption of outer resources in loops, reference escape analysis, lens +escape analysis, forbidden manual frees, and the "did you mean" check for +misspelled calls. Instead of cloning maps at every branch it keeps a table +of variable slots (name, state, type, mutability, lens flag, the previous +slot with the same name, provenance) and snapshots slot states at +branches. `tests/selfhost_tests.rs` asserts that both checkers return the +same verdict on a shared set of accepted and rejected programs. + +## Testing + +```sh +cargo test --test selfhost_tests # fixed point, parity, verdicts, native tests +(cd compiler && yard test) # native tests built by the Rust compiler +(cd compiler && target/trackc run tests/parser_test.trk) # ... or by trackc +``` diff --git a/docs/ring.md b/docs/ring.md index 43c40d6..04ffb40 100644 --- a/docs/ring.md +++ b/docs/ring.md @@ -33,5 +33,5 @@ let count = ring_count(&r); ## Rules - Fixed capacity — no dynamic allocation -- Linear type — auto-freed when spent +- Linear type — moves are tracked by the checker - Lock-free single producer/consumer diff --git a/docs/roadmap.md b/docs/roadmap.md index 721f4bb..472aecf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ -# Track Language Roadmap: Path to Self-Bootstrapping (v0.1.6 → v0.9.0) +# Track Language Roadmap: Path to Self-Bootstrapping (v0.1.6 → v1.0.0) -This roadmap outlines the incremental milestone releases for the Track programming language and toolchain leading up to full compiler self-bootstrapping in **v0.9.0**. +This roadmap outlines the incremental milestone releases for the Track programming language and toolchain leading up to full compiler self-bootstrapping, reached in **v1.0.0**. --- @@ -14,8 +14,9 @@ This roadmap outlines the incremental milestone releases for the Track programmi [v0.5.0] Explicit Error Handling ✅ DONE [v0.6.0] Monomorphized Generics ✅ DONE [v0.7.0] Track Lexer in Track ✅ DONE -[v0.8.0] Track Parser, Checker & Codegen ⏳ PLANNED -[v0.9.0] Self-Bootstrapping Verified 🎯 MILESTONE +[v0.8.0] Track Parser, Checker & Codegen ✅ DONE +[v0.9.0] Self-Bootstrapping Verified ✅ DONE +[v1.0.0] Self-Hosted Release ✅ DONE ``` --- @@ -23,7 +24,7 @@ This roadmap outlines the incremental milestone releases for the Track programmi ## Milestone Breakdown ### v0.1.6 — Slices, Strings & Memory Primitives ✅ -- First-class slices (`[]T`) with `.len()` and sub-slicing `arr[start..end]`. +- First-class slices (`[]T`) with sub-slicing `arr[start..end]` (length via `array_len` since v1.0). - Owned heap string type (`Str`) and string formatting/concatenation. - Byte types (`u8`, `i8`) and safe reference borrows (`&T`). - Variable mutability enforcement (`let mut`). @@ -116,7 +117,7 @@ values that live on the call stack and are passed around explicitly. --- -### v0.8.0a — Native Compiler Foundations ⏳ IN PROGRESS +### v0.8.0a — Native Compiler Foundations ✅ - **Lexer parity gate** ✅: native lexer recognizes every host reserved word and primitive type before parser work begins; native tests prevent regressions. - **Source spans and diagnostics** ✅: normalized end-exclusive byte spans, @@ -125,30 +126,47 @@ values that live on the call stack and are passed around explicitly. - **AST, collections, and parser subset** ✅: compact source-ranged AST nodes, growable opaque 64-bit storage, arithmetic precedence, foundational statements, and structured parser errors are implemented and natively tested. -- **Backend contract**: the self-hosted compiler emits portable C as its first +- **Backend contract** ✅: the self-hosted compiler emits portable C as its first backend and invokes the platform C compiler through the existing explicit OS process API. Cranelift remains the bootstrap compiler backend, not a library - to reimplement in Track. + to reimplement in Track. See [bootstrap.md](bootstrap.md#c-backend-contract). - **Bootstrap build ordering and artifacts** ✅: Yard sorts module discovery, dependency resolution, build failures, link inputs, and cache metadata. Source-relative object paths prevent duplicate module basenames from racing or colliding with the runtime. Stage 2/3 binary identity remains the v0.9.0 gate. -### v0.8.0b — Native Parser ⏳ PLANNED -- **Native Parser**: - - Port AST representation (`union Expr`) and recursive descent parser to native Track code in `compiler/parser.trk`. - - Detailed compile-time error reporting with source code line/column highlighting. -### v0.8.0c — Native Checker ⏳ PLANNED -- **Native Checker**: - - Port `LinearChecker` borrow-checker, escape analysis, and type inference to Track in `compiler/checker.trk`. -### v0.8.0d — Native Codegen ⏳ PLANNED -- **Native Codegen**: - - Implement the portable C emitter in Track and invoke the system C compiler. +### v0.8.0b — Native Parser ✅ +- Full grammar in `compiler/src/parser.trk` over a node pool (`ast.trk`), with + line/column code frames for every lexer and parser error. +### v0.8.0c — Native Checker ✅ +- The ownership checker (moves, borrows, lenses, CFG merge, loop rules, escape + analysis) in `compiler/src/checker.trk`, verdict-for-verdict with the Rust checker. +### v0.8.0d — Native Codegen ✅ +- Portable C99 emitter (`compiler/src/cgen.trk`) on the shared word-ABI runtime + (`runtime/track_runtime.c`). --- -### v0.9.0 — Bootstrap Milestone (Stage 0 → Stage 1 → Stage 2 Verification) 🎯 +### v0.9.0 — Bootstrap Milestone (Stage 0 → Stage 1 → Stage 2 Verification) ✅ - **Stage 0 (Bootstrap)**: The Rust `track` compiler compiles `compiler/*.trk` → produces `track_stage1` executable. - **Stage 1 (Self-Compile)**: `track_stage1` compiles `compiler/*.trk` → produces `track_stage2` executable. - **Stage 2 (Verification)**: `track_stage2` compiles `compiler/*.trk` → produces `track_stage3` executable. -- **Verification Gate**: Assert byte-for-byte binary identity between `track_stage2` and `track_stage3` (`cmp track_stage2 track_stage3`). Full self-hosting achieved! +- **Verification Gate**: stage1 and stage2 emit byte-identical C, and `cmp track_stage2 track_stage3` passes. Run with `scripts/bootstrap.sh`. + +--- + +### v1.0.0 — Self-Hosted Release ✅ +- `trackc` ships `build`, `run`, `check`, `emit-c`, and `tokens`. +- One word-ABI runtime shared by both compilers; the Cranelift backend moved to + the same value model, fixing the miscompilations listed in the CHANGELOG. +- CI gates on the bootstrap fixed point and on behavioural parity between the + two compilers. + +- Structs, field access/update, struct patterns, and bounds-checked arrays + with slicing now have runtime representations in both compilers. + +### Next +- Emit deallocation at scope exit from the ownership analysis (linear values + are currently reclaimed at process exit). +- Generic structs (`struct Stack`), and retiring the Rust compiler once + `yard` itself is ported. diff --git a/docs/stdlib.md b/docs/stdlib.md index a3fd8cf..1525089 100644 --- a/docs/stdlib.md +++ b/docs/stdlib.md @@ -16,7 +16,7 @@ struct Vec { let mut v: Vec = vec_init(16); vec_push(&mut v, 42); let val = vec_get(&v, 0); -// v is automatically freed when spent (linear type) +// v is a linear type: the checker tracks every move ``` ## Memory @@ -25,7 +25,7 @@ let val = vec_get(&v, 0); // Allocate raw memory let buf: ptr = alloc(1024); -// buf is freed when spent — no manual free needed +// buf is linear — manual frees are rejected // Zero memory memset(buf, 0, 1024); @@ -71,7 +71,7 @@ let eq = str_eq(&a, &b); // Concatenate (allocates new buffer) let combined = str_concat(&a, &b); -// s is automatically freed when spent +// s is linear — the checker rejects use after a move ``` ## Dynamic Arrays (Your Own Vec) diff --git a/docs/v080a-foundations.md b/docs/v080a-foundations.md index 3a419ff..1a1230d 100644 --- a/docs/v080a-foundations.md +++ b/docs/v080a-foundations.md @@ -1,5 +1,9 @@ # v0.8.0a: Native Compiler Foundations +> Historical design record. The Phase 3 subset parser and six-word AST +> described below were replaced in v1.0.0 by the full parser and node pool +> documented in [bootstrap.md](bootstrap.md). + ## Decision record The Rust bootstrap compiler continues to use Cranelift. The self-hosted @@ -109,5 +113,5 @@ metadata after a clean build, and execution of the linked binary. These guarantees concern build ordering and artifact identity; byte-for-byte native binary reproducibility across toolchains or Stage 2/3 remains a separate gate. -The remaining v0.8.0a exit criterion is the native C-emitter interface and its -external compiler invocation contract. +The native C-emitter interface and its external compiler invocation contract +were completed in v1.0.0; see [bootstrap.md](bootstrap.md). diff --git a/examples/slice_and_types.trk b/examples/slice_and_types.trk index 1832b2a..2b99eea 100644 --- a/examples/slice_and_types.trk +++ b/examples/slice_and_types.trk @@ -12,5 +12,7 @@ fn main() -> void { let arr: [i64; 5] = [10, 20, 30, 40, 50]; let slice: []i64 = arr[1..4]; - print(slice); + print(array_len(&slice)); + print(slice[0]); + print(slice[2]); } diff --git a/examples/structs_arrays.trk b/examples/structs_arrays.trk new file mode 100644 index 0000000..b60ab44 --- /dev/null +++ b/examples/structs_arrays.trk @@ -0,0 +1,51 @@ +// v1.0: struct values, field access and update, struct patterns, and +// bounds-checked arrays with slicing. + +struct Point { x: i64, y: i64 } +struct Account { id: i64, balance: i64 } + +fn manhattan(p: &Point) -> i64 { + return p.x + p.y; +} + +fn quadrant(p: Point) -> i64 { + let mut q: i64 = 0; + match p { + Point { x: 0, y: 0 } => q = 0, + Point { x, y } if x > 0 && y > 0 => q = 1, + _ => q = 2, + } + return q; +} + +fn total(values: &[]i64) -> i64 { + let mut sum: i64 = 0; + for i in 0..array_len(values) { + sum = sum + values[i]; + } + return sum; +} + +fn main() -> void { + let mut p = Point { x: 3, y: 4 }; + print(manhattan(&p)); + p.y = 10; + print(p.y); + + let mut acct = Account { id: 7, balance: 100 }; + with acct -> a { + a.balance = a.balance - 25; + } + print(acct.balance); + + print(quadrant(Point { x: 0, y: 0 })); + print(quadrant(Point { x: 2, y: 5 })); + print(quadrant(Point { x: -1, y: 5 })); + + let mut xs = [5, 10, 15, 20]; + xs[0] = 1; + print(total(&xs)); + let tail = xs[2..]; + print(total(&tail)); + print(array_len(&tail)); +} diff --git a/runtime/track_runtime.c b/runtime/track_runtime.c new file mode 100644 index 0000000..405cd91 --- /dev/null +++ b/runtime/track_runtime.c @@ -0,0 +1,850 @@ +/* + * Track runtime (word ABI). + * + * Every function exported to Track takes and returns 64-bit words (`tk_w`): + * integers and booleans are plain values, strings are NUL-terminated + * `char*` pointers, and handles (files, Vec, collections, builders) are + * pointers to runtime-owned records. This single file is linked by both + * compilers: the Rust bootstrap compiler embeds it with `include_str!`, and + * the self-hosted compiler embeds a generated copy (`compiler/src/runtime_c.trk`, + * regenerated by `scripts/embed_runtime.sh`). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef int64_t tk_w; + +#define TK_PTR(w) ((void *)(uintptr_t)(w)) +#define TK_STR(w) ((const char *)(uintptr_t)(w)) +#define TK_WORD(p) ((tk_w)(uintptr_t)(p)) + +/* ---------------------------------------------------------------- process */ + +static int g_argc = 0; +static char **g_argv = NULL; + +/* Called from the generated `main` entry point before any Track code runs. */ +tk_w track_runtime_init(tk_w argc, tk_w argv) { + g_argc = (int)argc; + g_argv = (char **)TK_PTR(argv); + return 0; +} + +tk_w os_args_count(void) { return g_argc; } + +static char *tk_dup_n(const char *s, size_t n) { + char *out = (char *)malloc(n + 1); + if (!out) { + fprintf(stderr, "\nTrack Runtime Error: Out of memory allocation failure!\n"); + exit(137); + } + if (n > 0) memcpy(out, s, n); + out[n] = '\0'; + return out; +} + +static char *tk_dup(const char *s) { return tk_dup_n(s ? s : "", s ? strlen(s) : 0); } + +tk_w os_arg(tk_w idx) { + if (g_argv && idx >= 0 && idx < g_argc) return TK_WORD(tk_dup(g_argv[idx])); + return TK_WORD(tk_dup("")); +} + +tk_w sys_exit(tk_w code) { + fflush(stdout); + exit((int)code); + return 0; +} + +static int is_valid_string_pointer(const void *ptr) { + if (!ptr || (uintptr_t)ptr < 0x10000) return 0; + int pfd[2]; + if (pipe(pfd) < 0) return 0; + ssize_t res = write(pfd[1], ptr, 1); + close(pfd[0]); + close(pfd[1]); + return res == 1; +} + +/* Fatal abort: print message and terminate the process. No unwinding — + * stack frames are simply discarded (linear cleanup is skipped by design). */ +tk_w track_abort(tk_w msg) { + const char *s = TK_STR(msg); + fflush(stdout); + if (s && is_valid_string_pointer(s)) { + fprintf(stderr, "Track abort: %s\n", s); + } else { + fprintf(stderr, "Track abort\n"); + } + fflush(stderr); + exit(134); + return 0; +} + +/* Integer division by zero or INT64_MIN / -1: trap like a hardware divide + * fault (SIGFPE) without flushing buffered output. */ +tk_w track_trap_div(void) { + raise(SIGFPE); + abort(); + return 0; +} + +tk_w sys_exec(tk_w cmd) { + if (!cmd) return -1; + fflush(stdout); + fflush(stderr); + int status = system(TK_STR(cmd)); + if (status == -1) return -1; + if (WIFEXITED(status)) return WEXITSTATUS(status); + return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0); +} + +tk_w process_spawn(tk_w cmd) { return sys_exec(cmd); } + +tk_w clock_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (tk_w)ts.tv_sec * 1000 + (tk_w)ts.tv_nsec / 1000000; +} + +tk_w env_get(tk_w key) { + const char *val = key ? getenv(TK_STR(key)) : NULL; + return TK_WORD(tk_dup(val ? val : "")); +} + +tk_w env_exists(tk_w key) { return key && getenv(TK_STR(key)) ? 1 : 0; } + +/* ----------------------------------------------------------------- memory */ + +static size_t g_allocated_bytes = 0; +static size_t g_max_memory_limit = 536870912; /* 512MB default memory boundary limit */ + +tk_w sys_set_memory_limit(tk_w bytes) { + if (bytes > 0) g_max_memory_limit = (size_t)bytes; + return 0; +} + +tk_w sys_get_memory_used(void) { return (tk_w)g_allocated_bytes; } + +tk_w alloc(tk_w size_w) { + size_t size = (size_t)size_w; + if (size_w <= 0) return 0; + if (g_allocated_bytes + size > g_max_memory_limit) { + fprintf(stderr, "\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\n", g_allocated_bytes, size, g_max_memory_limit); + fflush(stderr); + exit(137); + } + size_t *ptr = (size_t *)calloc(1, size + sizeof(size_t)); + if (!ptr) { + fprintf(stderr, "\nTrack Runtime Error: Out of memory allocation failure!\n"); + fflush(stderr); + exit(137); + } + *ptr = size; + g_allocated_bytes += size; + return TK_WORD(ptr + 1); +} + +tk_w dealloc(tk_w p) { + if (!p) return 0; + size_t *raw = (size_t *)TK_PTR(p) - 1; + size_t size = *raw; + g_allocated_bytes = g_allocated_bytes >= size ? g_allocated_bytes - size : 0; + free(raw); + return 0; +} + +tk_w mem_realloc(tk_w p, tk_w new_size_w) { + if (!p) return alloc(new_size_w); + if (new_size_w <= 0) return dealloc(p); + size_t new_size = (size_t)new_size_w; + size_t *raw = (size_t *)TK_PTR(p) - 1; + size_t old_size = *raw; + if (g_allocated_bytes - old_size + new_size > g_max_memory_limit) { + fprintf(stderr, "\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\n", g_allocated_bytes, new_size, g_max_memory_limit); + fflush(stderr); + exit(137); + } + size_t *new_raw = (size_t *)realloc(raw, new_size + sizeof(size_t)); + if (!new_raw) { + fprintf(stderr, "\nTrack Runtime Error: Out of memory reallocation failure!\n"); + fflush(stderr); + exit(137); + } + *new_raw = new_size; + g_allocated_bytes = g_allocated_bytes - old_size + new_size; + return TK_WORD(new_raw + 1); +} + +/* Copy `count` words to a fresh heap cell (escaping tuples). */ +tk_w track_words_copy(tk_w src, tk_w count) { + tk_w dst = alloc(count > 0 ? count * 8 : 8); + if (src && count > 0) memcpy(TK_PTR(dst), TK_PTR(src), (size_t)count * 8); + return dst; +} + +/* ------------------------------------------------------------------ print */ + +tk_w print(tk_w val) { + if (val >= 0x10000 && is_valid_string_pointer(TK_PTR(val))) { + const char *str = TK_STR(val); + int is_str = 1; + int len = 0; + while (len < 4096) { + unsigned char c = (unsigned char)str[len]; + if (c == 0) break; + if (c < 9 || (c > 13 && c < 32) || c > 126) { + is_str = 0; + break; + } + len++; + } + if (is_str && len > 0) { + printf("%s\n", str); + return 0; + } + } + printf("%lld\n", (long long)val); + return 0; +} + +tk_w println(tk_w val) { return print(val); } +tk_w print_int(tk_w val) { printf("%lld\n", (long long)val); return 0; } +tk_w print_hex(tk_w val) { printf("0x%llx\n", (unsigned long long)val); return 0; } +tk_w print_str(tk_w s) { printf("%s\n", s ? TK_STR(s) : ""); return 0; } + +tk_w print_err(tk_w s) { + if (s) { + fflush(stdout); + fprintf(stderr, "%s\n", TK_STR(s)); + fflush(stderr); + } + return 0; +} + +tk_w eprint(tk_w s) { return print_err(s); } + +/* Unformatted output (no trailing newline, no int/string guessing). */ +tk_w track_out(tk_w s) { + if (s) fputs(TK_STR(s), stdout); + return 0; +} + +tk_w track_err(tk_w s) { + if (s) { + fflush(stdout); + fputs(TK_STR(s), stderr); + fflush(stderr); + } + return 0; +} + +tk_w read_line(void) { + size_t cap = 128, len = 0; + char *buf = (char *)malloc(cap); + int c; + while (buf && (c = fgetc(stdin)) != EOF && c != '\n') { + if (len + 1 >= cap) buf = (char *)realloc(buf, cap *= 2); + if (buf) buf[len++] = (char)c; + } + if (!buf) return TK_WORD(tk_dup("")); + buf[len] = '\0'; + return TK_WORD(buf); +} + +/* ---------------------------------------------------------------- strings */ + +tk_w str_from_literal(tk_w raw) { return TK_WORD(tk_dup(TK_STR(raw))); } + +tk_w str_free(tk_w s) { + (void)s; /* literals and runtime strings share a representation; never freed */ + return 0; +} + +tk_w str_len(tk_w s) { return s ? (tk_w)strlen(TK_STR(s)) : 0; } + +tk_w str_eq(tk_w a, tk_w b) { + if (!a || !b) return a == b ? 1 : 0; + return strcmp(TK_STR(a), TK_STR(b)) == 0 ? 1 : 0; +} + +tk_w str_concat(tk_w a, tk_w b) { + size_t a_len = a ? strlen(TK_STR(a)) : 0; + size_t b_len = b ? strlen(TK_STR(b)) : 0; + char *out = tk_dup_n("", 0); + out = (char *)realloc(out, a_len + b_len + 1); + if (!out) exit(137); + if (a_len) memcpy(out, TK_STR(a), a_len); + if (b_len) memcpy(out + a_len, TK_STR(b), b_len); + out[a_len + b_len] = '\0'; + return TK_WORD(out); +} + +tk_w str_find(tk_w s, tk_w sub) { + if (!s || !sub) return -1; + const char *p = strstr(TK_STR(s), TK_STR(sub)); + return p ? (tk_w)(p - TK_STR(s)) : -1; +} + +tk_w str_contains(tk_w s, tk_w sub) { + if (!s || !sub) return 0; + return strstr(TK_STR(s), TK_STR(sub)) ? 1 : 0; +} + +tk_w str_to_int(tk_w s) { return s ? atoll(TK_STR(s)) : 0; } + +/* Predicate companion for str_to_int: distinguishes a genuine "0" from a parse failure. */ +tk_w str_is_int(tk_w s_w) { + const char *s = TK_STR(s_w); + if (!s || *s == '\0') return 0; + const char *p = s; + if (*p == '+' || *p == '-') p++; + if (*p == '\0') return 0; + for (; *p; p++) { + if (!isdigit((unsigned char)*p)) return 0; + } + return 1; +} + +tk_w int_to_str(tk_w val) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)val); + return TK_WORD(tk_dup(buf)); +} + +tk_w str_starts_with(tk_w s, tk_w prefix) { + if (!s || !prefix) return 0; + size_t lp = strlen(TK_STR(prefix)); + return strncmp(TK_STR(s), TK_STR(prefix), lp) == 0 ? 1 : 0; +} + +tk_w str_ends_with(tk_w s, tk_w suffix) { + if (!s || !suffix) return 0; + size_t ls = strlen(TK_STR(s)); + size_t lp = strlen(TK_STR(suffix)); + if (lp > ls) return 0; + return strcmp(TK_STR(s) + ls - lp, TK_STR(suffix)) == 0 ? 1 : 0; +} + +tk_w str_substr(tk_w s_w, tk_w start, tk_w len) { + const char *s = TK_STR(s_w); + if (!s || start < 0 || len <= 0) return TK_WORD(tk_dup("")); + /* Bounded scan: never walk past the requested window of a long source. */ + size_t avail = strnlen(s, (size_t)start + (size_t)len); + if ((size_t)start >= avail) return TK_WORD(tk_dup("")); + return TK_WORD(tk_dup_n(s + start, avail - (size_t)start)); +} + +tk_w str_trim(tk_w s_w) { + const char *s = TK_STR(s_w); + if (!s) return TK_WORD(tk_dup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) len--; + return TK_WORD(tk_dup_n(s, len)); +} + +tk_w str_char_at(tk_w s_w, tk_w idx) { + const char *s = TK_STR(s_w); + if (!s || idx < 0) return 0; + size_t len = strnlen(s, (size_t)idx + 1); + if ((size_t)idx >= len) return 0; + return (unsigned char)s[idx]; +} + +/* Copy a string's bytes into a collection for O(1) bounds-checked access. */ +tk_w track_i64_collection_new(tk_w requested_cap); +tk_w track_i64_collection_push(tk_w handle, tk_w value); + +tk_w track_str_bytes(tk_w s_w) { + const unsigned char *s = (const unsigned char *)TK_STR(s_w); + size_t len = s ? strlen((const char *)s) : 0; + tk_w out = track_i64_collection_new((tk_w)len + 1); + for (size_t i = 0; i < len; i++) track_i64_collection_push(out, s[i]); + return out; +} + +tk_w char_is_digit(tk_w c) { return c >= 0 && c < 256 && isdigit((int)c) ? 1 : 0; } +tk_w char_is_alpha(tk_w c) { return c >= 0 && c < 256 && isalpha((int)c) ? 1 : 0; } +tk_w char_is_alphanumeric(tk_w c) { return c >= 0 && c < 256 && isalnum((int)c) ? 1 : 0; } +tk_w char_is_space(tk_w c) { return c >= 0 && c < 256 && isspace((int)c) ? 1 : 0; } +tk_w char_to_upper(tk_w c) { return c >= 0 && c < 256 ? (unsigned char)toupper((int)c) : c; } +tk_w char_to_lower(tk_w c) { return c >= 0 && c < 256 ? (unsigned char)tolower((int)c) : c; } + +/* ------------------------------------------------------------------ paths */ + +tk_w path_basename(tk_w path_w) { + const char *path = TK_STR(path_w); + if (!path) return TK_WORD(tk_dup("")); + const char *slash = strrchr(path, '/'); + return TK_WORD(tk_dup(slash ? slash + 1 : path)); +} + +tk_w path_ext(tk_w path_w) { + const char *path = TK_STR(path_w); + if (!path) return TK_WORD(tk_dup("")); + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) return TK_WORD(tk_dup("")); + return TK_WORD(tk_dup(dot + 1)); +} + +tk_w path_join(tk_w a_w, tk_w b_w) { + const char *a = a_w ? TK_STR(a_w) : ""; + const char *b = b_w ? TK_STR(b_w) : ""; + size_t la = strlen(a), lb = strlen(b); + int slash = la > 0 && a[la - 1] != '/' && (lb == 0 || b[0] != '/'); + char *out = tk_dup_n(a, la); + out = (char *)realloc(out, la + (size_t)slash + lb + 1); + if (!out) exit(137); + if (slash) out[la] = '/'; + memcpy(out + la + slash, b, lb + 1); + return TK_WORD(out); +} + +/* ------------------------------------------------------------------ files */ + +tk_w file_open(tk_w path, tk_w mode) { + if (!path || !mode) return 0; + return TK_WORD(fopen(TK_STR(path), TK_STR(mode))); +} + +tk_w file_close(tk_w f) { + if (f) fclose((FILE *)TK_PTR(f)); + return 0; +} + +tk_w file_read_all(tk_w handle) { + FILE *f = (FILE *)TK_PTR(handle); + if (!f || fseek(f, 0, SEEK_END) != 0) return TK_WORD(tk_dup("")); + long end = ftell(f); + if (end < 0 || fseek(f, 0, SEEK_SET) != 0) return TK_WORD(tk_dup("")); + char *buf = (char *)malloc((size_t)end + 1); + if (!buf) exit(137); + size_t got = fread(buf, 1, (size_t)end, f); + buf[got] = '\0'; + return TK_WORD(buf); +} + +tk_w file_write(tk_w handle, tk_w content) { + FILE *f = (FILE *)TK_PTR(handle); + if (!f || !content) return -1; + size_t len = strlen(TK_STR(content)); + return fwrite(TK_STR(content), 1, len, f) == len ? 0 : -1; +} + +tk_w file_exists(tk_w path) { return path && access(TK_STR(path), F_OK) == 0 ? 1 : 0; } + +tk_w file_remove(tk_w path) { return path && unlink(TK_STR(path)) == 0 ? 0 : -1; } + +tk_w file_size(tk_w path) { + struct stat st; + if (!path || stat(TK_STR(path), &st) != 0) return -1; + return (tk_w)st.st_size; +} + +tk_w dir_exists(tk_w path) { + struct stat st; + return path && stat(TK_STR(path), &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0; +} + +tk_w file_copy(tk_w src, tk_w dst) { + if (!src || !dst) return -1; + FILE *in = fopen(TK_STR(src), "rb"); + if (!in) return -1; + FILE *out = fopen(TK_STR(dst), "wb"); + if (!out) { + fclose(in); + return -1; + } + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) fwrite(buf, 1, n, out); + fclose(in); + fclose(out); + return 0; +} + +/* Whole-file helpers: open, transfer, and close in one call. */ +tk_w track_file_read(tk_w path) { + FILE *f = path ? fopen(TK_STR(path), "rb") : NULL; + if (!f) return TK_WORD(tk_dup("")); + tk_w out = file_read_all(TK_WORD(f)); + fclose(f); + return out; +} + +tk_w track_file_write_all(tk_w path, tk_w content) { + FILE *f = path ? fopen(TK_STR(path), "wb") : NULL; + if (!f) return -1; + tk_w status = file_write(TK_WORD(f), content); + if (fclose(f) != 0) status = -1; + return status; +} + +/* ------------------------------------------------------------------- math */ + +tk_w math_abs(tk_w x) { return x < 0 ? -x : x; } +tk_w math_min(tk_w a, tk_w b) { return a < b ? a : b; } +tk_w math_max(tk_w a, tk_w b) { return a > b ? a : b; } +tk_w math_clamp(tk_w v, tk_w lo, tk_w hi) { return v < lo ? lo : (v > hi ? hi : v); } +tk_w math_floor(tk_w x) { return x; } +tk_w math_ceil(tk_w x) { return x; } +tk_w math_round(tk_w x) { return x; } + +tk_w math_pow(tk_w base, tk_w exp) { + tk_w result = 1; + if (exp < 0) return 0; + while (exp > 0) { + if (exp & 1) result = (tk_w)((uint64_t)result * (uint64_t)base); + base = (tk_w)((uint64_t)base * (uint64_t)base); + exp >>= 1; + } + return result; +} + +tk_w math_sqrt(tk_w x) { + if (x <= 0) return 0; + uint64_t lo = 0, hi = 3037000499ULL; /* floor(sqrt(INT64_MAX)) */ + while (lo < hi) { + uint64_t mid = (lo + hi + 1) / 2; + if (mid * mid <= (uint64_t)x) lo = mid; else hi = mid - 1; + } + return (tk_w)lo; +} + +static uint64_t g_rng_state = 0x853c49e65d8dbb29ULL; +tk_w math_random(void) { + uint64_t x = g_rng_state; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + g_rng_state = x; + return (tk_w)x; +} + +tk_w add(tk_w a, tk_w b) { return a + b; } +tk_w sum(tk_w a, tk_w b) { return a + b; } +tk_w sub(tk_w a, tk_w b) { return a - b; } + +/* -------------------------------------------------------------------- Vec */ + +typedef struct { + tk_w *data; + tk_w len; + tk_w cap; +} TrackVec; + +tk_w vec_init(tk_w cap) { + TrackVec *v = (TrackVec *)TK_PTR(alloc(sizeof(TrackVec))); + v->cap = cap > 0 ? cap : 16; + v->data = (tk_w *)TK_PTR(alloc(v->cap * (tk_w)sizeof(tk_w))); + v->len = 0; + return TK_WORD(v); +} + +tk_w vec_push(tk_w h, tk_w val) { + TrackVec *v = (TrackVec *)TK_PTR(h); + if (!v) return 0; + if (v->len == v->cap) { + v->cap *= 2; + v->data = (tk_w *)TK_PTR(mem_realloc(TK_WORD(v->data), v->cap * (tk_w)sizeof(tk_w))); + } + v->data[v->len++] = val; + return 0; +} + +tk_w vec_get(tk_w h, tk_w idx) { + TrackVec *v = (TrackVec *)TK_PTR(h); + return v && idx >= 0 && idx < v->len ? v->data[idx] : 0; +} + +tk_w vec_set(tk_w h, tk_w idx, tk_w val) { + TrackVec *v = (TrackVec *)TK_PTR(h); + if (v && idx >= 0 && idx < v->len) v->data[idx] = val; + return 0; +} + +tk_w vec_pop(tk_w h) { + TrackVec *v = (TrackVec *)TK_PTR(h); + return v && v->len > 0 ? v->data[--v->len] : 0; +} + +tk_w vec_len(tk_w h) { + TrackVec *v = (TrackVec *)TK_PTR(h); + return v ? v->len : 0; +} + +tk_w vec_clear(tk_w h) { + TrackVec *v = (TrackVec *)TK_PTR(h); + if (v) v->len = 0; + return 0; +} + +tk_w vec_reserve(tk_w h, tk_w cap) { + TrackVec *v = (TrackVec *)TK_PTR(h); + if (v && cap > v->cap) { + v->data = (tk_w *)TK_PTR(mem_realloc(TK_WORD(v->data), cap * (tk_w)sizeof(tk_w))); + v->cap = cap; + } + return 0; +} + +tk_w vec_free(tk_w h) { + TrackVec *v = (TrackVec *)TK_PTR(h); + if (v) { + dealloc(TK_WORD(v->data)); + dealloc(h); + } + return 0; +} + +/* --------------------------------------------- opaque 64-bit collections */ + +/* Growable word vectors used by the self-hosted compiler. Track only carries + * the stable pointer handle; layout and capacity stay in the runtime. */ +typedef struct { + tk_w *data; + tk_w len; + tk_w cap; +} TrackI64Collection; + +tk_w track_i64_collection_new(tk_w requested_cap) { + tk_w cap = requested_cap > 0 ? requested_cap : 16; + TrackI64Collection *c = (TrackI64Collection *)malloc(sizeof(TrackI64Collection)); + if (!c) exit(137); + c->data = (tk_w *)malloc((size_t)cap * sizeof(tk_w)); + if (!c->data) exit(137); + c->len = 0; + c->cap = cap; + return TK_WORD(c); +} + +tk_w track_i64_collection_len(tk_w h) { + const TrackI64Collection *c = (const TrackI64Collection *)TK_PTR(h); + return c ? c->len : 0; +} + +tk_w track_i64_collection_push(tk_w h, tk_w value) { + TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h); + if (!c) return -1; + if (c->len == c->cap) { + c->cap *= 2; + c->data = (tk_w *)realloc(c->data, (size_t)c->cap * sizeof(tk_w)); + if (!c->data) exit(137); + } + c->data[c->len] = value; + return c->len++; +} + +tk_w track_i64_collection_get(tk_w h, tk_w index) { + const TrackI64Collection *c = (const TrackI64Collection *)TK_PTR(h); + if (!c || index < 0 || index >= c->len) return 0; + return c->data[index]; +} + +tk_w track_i64_collection_set(tk_w h, tk_w index, tk_w value) { + TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h); + if (!c || index < 0 || index >= c->len) return 0; + c->data[index] = value; + return 1; +} + +tk_w track_i64_collection_pop(tk_w h) { + TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h); + if (!c || c->len == 0) return 0; + return c->data[--c->len]; +} + +/* Shrink to `len` elements (no-op when already shorter). */ +tk_w track_i64_collection_truncate(tk_w h, tk_w len) { + TrackI64Collection *c = (TrackI64Collection *)TK_PTR(h); + if (c && len >= 0 && len < c->len) c->len = len; + return 0; +} + +/* --------------------------------------------------------- string builder */ + +typedef struct { + char *data; + size_t len; + size_t cap; +} TrackStrBuf; + +static void sb_reserve(TrackStrBuf *b, size_t extra) { + if (b->len + extra + 1 <= b->cap) return; + while (b->len + extra + 1 > b->cap) b->cap *= 2; + b->data = (char *)realloc(b->data, b->cap); + if (!b->data) exit(137); +} + +tk_w track_sb_new(void) { + TrackStrBuf *b = (TrackStrBuf *)malloc(sizeof(TrackStrBuf)); + if (!b) exit(137); + b->cap = 256; + b->len = 0; + b->data = (char *)malloc(b->cap); + if (!b->data) exit(137); + b->data[0] = '\0'; + return TK_WORD(b); +} + +tk_w track_sb_push(tk_w h, tk_w s) { + TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h); + if (!b || !s) return 0; + size_t n = strlen(TK_STR(s)); + sb_reserve(b, n); + memcpy(b->data + b->len, TK_STR(s), n + 1); + b->len += n; + return 0; +} + +tk_w track_sb_push_byte(tk_w h, tk_w byte) { + TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h); + if (!b) return 0; + sb_reserve(b, 1); + b->data[b->len++] = (char)byte; + b->data[b->len] = '\0'; + return 0; +} + +tk_w track_sb_push_int(tk_w h, tk_w val) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)val); + return track_sb_push(h, TK_WORD(buf)); +} + +tk_w track_sb_len(tk_w h) { + TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h); + return b ? (tk_w)b->len : 0; +} + +/* Copy the builder contents into a new string. */ +tk_w track_sb_str(tk_w h) { + TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h); + return TK_WORD(b ? tk_dup_n(b->data, b->len) : tk_dup("")); +} + +tk_w track_sb_clear(tk_w h) { + TrackStrBuf *b = (TrackStrBuf *)TK_PTR(h); + if (b) { + b->len = 0; + b->data[0] = '\0'; + } + return 0; +} + +/* ----------------------------------------------------------------- arrays */ + +/* Array values are cells [length, elements...]; indexing is bounds checked. */ +static void tk_bounds(tk_w a, tk_w i) { + if (!a) track_abort(TK_WORD("index into a null array")); + if (i < 0 || i >= ((tk_w *)TK_PTR(a))[0]) track_abort(TK_WORD("array index out of bounds")); +} + +tk_w track_array_get(tk_w a, tk_w i) { + tk_bounds(a, i); + return ((tk_w *)TK_PTR(a))[i + 1]; +} + +tk_w track_array_set(tk_w a, tk_w i, tk_w v) { + tk_bounds(a, i); + ((tk_w *)TK_PTR(a))[i + 1] = v; + return v; +} + +/* Copy of a[from..to]; `to` of -1 means the end of the array. */ +tk_w track_array_slice(tk_w a, tk_w from, tk_w to) { + tk_w len = a ? ((tk_w *)TK_PTR(a))[0] : 0; + if (to < 0) to = len; + if (from < 0 || from > to || to > len) track_abort(TK_WORD("array slice out of bounds")); + tk_w out = alloc((to - from + 1) * 8); + ((tk_w *)TK_PTR(out))[0] = to - from; + if (to > from) memcpy((tk_w *)TK_PTR(out) + 1, (tk_w *)TK_PTR(a) + 1 + from, (size_t)(to - from) * 8); + return out; +} + +tk_w array_len(tk_w a) { return a ? ((tk_w *)TK_PTR(a))[0] : 0; } + +/* A field access whose struct tag matched none of the declaring structs. */ +tk_w track_bad_field(void) { + return track_abort(TK_WORD("field access on a value of the wrong struct type")); +} + +/* ---------------------------------------------------------------- network */ + +tk_w net_socket_tcp_listen(tk_w port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons((uint16_t)port); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + if (errno == EADDRINUSE) { + fprintf(stderr, "Track Network Error: Port conflict! Port %d is already in use by another process.\n", (int)port); + } else { + fprintf(stderr, "Track Network Error: Failed to bind TCP socket on port %d (errno: %d).\n", (int)port, errno); + } + fflush(stderr); + close(fd); + return -1; + } + if (listen(fd, 128) < 0) { + close(fd); + return -1; + } + return fd; +} + +tk_w net_socket_accept(tk_w server_fd) { + if (server_fd < 0) return -1; + struct sockaddr_in client_addr; + socklen_t addrlen = sizeof(client_addr); + return accept((int)server_fd, (struct sockaddr *)&client_addr, &addrlen); +} + +tk_w net_socket_connect(tk_w host, tk_w port) { + if (!host) return -1; + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, TK_STR(host), &addr.sin_addr) <= 0 || + connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + return fd; +} + +tk_w net_socket_send(tk_w fd, tk_w data, tk_w len) { + if (fd < 0 || !data) return -1; + return (tk_w)send((int)fd, TK_PTR(data), (size_t)len, 0); +} + +tk_w net_socket_recv(tk_w fd, tk_w buf, tk_w max_len) { + if (fd < 0 || !buf) return -1; + return (tk_w)recv((int)fd, TK_PTR(buf), (size_t)max_len, 0); +} + +tk_w net_socket_close(tk_w fd) { + if (fd >= 0) close((int)fd); + return 0; +} diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..ce7a03a --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# Bootstrap the self-hosted Track compiler and verify the fixed point. +# +# stage 0 the Rust compiler (`yard`) builds compiler/ -> stage1 trackc +# stage 1 stage1 compiles compiler/src/main.trk -> stage2 trackc +# stage 2 stage2 compiles compiler/src/main.trk -> stage3 trackc +# +# The gate: stage1 and stage2 emit byte-identical C, and the stage2 and +# stage3 executables are byte-identical. Artifacts go to build/bootstrap/ +# (override with BOOTSTRAP_DIR). Set YARD to use a specific yard binary. +set -eu + +root=$(cd "$(dirname "$0")/.." && pwd) +out=${BOOTSTRAP_DIR:-"$root/build/bootstrap"} +yard=${YARD:-} + +if [ -z "$yard" ]; then + (cd "$root" && cargo build --release --quiet) + yard="$root/target/release/yard" +fi +# The stages run from other directories, so pin an absolute path. +case "$yard" in + /*) ;; + *) yard="$(pwd)/$yard" ;; +esac + +"$root/scripts/embed_runtime.sh" --check + +rm -rf "$out" +mkdir -p "$out/stage1" "$out/stage2" "$out/stage3" + +echo "==> stage 0: yard builds compiler/ (Rust bootstrap compiler)" +(cd "$root/compiler" && "$yard" build >/dev/null) +cp "$root/compiler/target/trackc" "$out/stage1/trackc" + +# Every stage compiles the same sources from the repository root, so the +# loader resolves identical module paths at every stage. +cd "$root" +echo "==> stage 1: stage1 compiles compiler/src/main.trk" +"$out/stage1/trackc" build compiler/src/main.trk -o "$out/stage2/trackc" >/dev/null +echo "==> stage 2: stage2 compiles compiler/src/main.trk" +"$out/stage2/trackc" build compiler/src/main.trk -o "$out/stage3/trackc" >/dev/null + +status=0 +if cmp -s "$out/stage2/trackc.c" "$out/stage3/trackc.c"; then + echo "✓ stage1 and stage2 emit identical C ($(wc -l < "$out/stage2/trackc.c") lines)" +else + echo "✗ stage1 and stage2 emitted different C" >&2 + status=1 +fi +if cmp -s "$out/stage2/trackc" "$out/stage3/trackc"; then + echo "✓ stage2 and stage3 executables are byte-identical ($(wc -c < "$out/stage2/trackc") bytes)" +else + echo "✗ stage2 and stage3 executables differ" >&2 + status=1 +fi +"$out/stage3/trackc" --version +exit $status diff --git a/scripts/embed_runtime.sh b/scripts/embed_runtime.sh new file mode 100755 index 0000000..b43754e --- /dev/null +++ b/scripts/embed_runtime.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# Regenerate compiler/src/runtime_c.trk from runtime/track_runtime.c. +# +# The self-hosted compiler writes the runtime next to every program it +# builds, so it carries the C source as Track string literals together with +# the prototype and arity of each exported function. +# +# scripts/embed_runtime.sh # rewrite the generated file +# scripts/embed_runtime.sh --check # fail if it is out of date +set -eu + +root=$(cd "$(dirname "$0")/.." && pwd) +src="$root/runtime/track_runtime.c" +out="$root/compiler/src/runtime_c.trk" +tmp="${out}.tmp.$$" + +awk ' +function esc(s) { + gsub(/\\/, "\\\\", s) + gsub(/"/, "\\\"", s) + return s +} +BEGIN { + print "// runtime_c.trk — GENERATED by scripts/embed_runtime.sh from" + print "// runtime/track_runtime.c. Do not edit by hand." + print "" + print "import \"base\";" + print "" + print "fn runtime_source(b: i64) -> i64 {" +} +{ + lines[NR] = $0 + print " sb_line(b, \"" esc($0) "\");" + if ($0 ~ /^tk_w [a-z_0-9]+\([^)]*\) \{/) { + name = $0 + sub(/^tk_w /, "", name) + sub(/\(.*/, "", name) + params = $0 + sub(/^[^(]*\(/, "", params) + sub(/\).*/, "", params) + if (params == "void") { + arity = 0 + } else { + arity = gsub(/,/, ",", params) + 1 + } + nfn++ + fname[nfn] = name + farity[nfn] = arity + } +} +END { + print " return 0;" + print "}" + print "" + print "// Prototypes of every runtime function, for generated programs." + print "fn runtime_prototypes(b: i64) -> i64 {" + for (i = 1; i <= nfn; i++) { + p = "" + for (j = 0; j < farity[i]; j++) { + p = p (j > 0 ? ", " : "") "tk_w" + } + if (p == "") p = "void" + print " sb_line(b, \"tk_w " fname[i] "(" p ");\");" + } + print " return 0;" + print "}" + print "" + print "// Record the arity of every runtime function (NI_EXTERN)." + print "fn register_runtime(c: i64) -> i64 {" + for (i = 1; i <= nfn; i++) { + print " ni_set(c, intern(c, \"" fname[i] "\"), NI_EXTERN, " farity[i] ");" + } + print " return 0;" + print "}" +} +' "$src" > "$tmp" + +if [ "${1:-}" = "--check" ]; then + if cmp -s "$tmp" "$out"; then + rm -f "$tmp" + echo "runtime_c.trk is up to date" + exit 0 + fi + rm -f "$tmp" + echo "compiler/src/runtime_c.trk is stale; run scripts/embed_runtime.sh" >&2 + exit 1 +fi +mv "$tmp" "$out" +echo "wrote $out" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 26e2b57..fab8811 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -29,6 +29,12 @@ pub enum Expr { index: usize, }, + /// `value.field` on a struct. + FieldAccess { + target: Box, + field: String, + }, + ArrayIndex { target: Box, index: Box, @@ -115,6 +121,11 @@ pub enum Expr { target: TrackType, }, + StructDef { + name: String, + fields: Vec<(String, TrackType)>, + }, + MacroDef { name: String, params: Vec<(String, TrackType)>, diff --git a/src/checker/mod.rs b/src/checker/mod.rs index d73756c..5d2fd9e 100644 --- a/src/checker/mod.rs +++ b/src/checker/mod.rs @@ -23,6 +23,10 @@ pub struct LinearChecker { pub current_return_type: Option, pub barebones: bool, pub loaded_modules: std::collections::HashSet, + /// Enum/union layouts and aliases for codegen, including imported modules. + pub type_info: crate::codegen::TypeInfo, + pub const_values: HashMap, + pub structs: HashMap>, } impl LinearChecker { @@ -70,6 +74,19 @@ impl LinearChecker { current_return_type: None, barebones: false, loaded_modules: std::collections::HashSet::new(), + type_info: crate::codegen::TypeInfo::default(), + const_values: HashMap::new(), + structs: HashMap::new(), + } + } + + /// A new binding is mutable only if declared `let mut`; it shadows any + /// earlier binding of the same name. + fn set_mutable(&mut self, name: &str, mutable: bool) { + if mutable { + self.mutables.insert(name.to_string()); + } else { + self.mutables.remove(name); } } @@ -414,14 +431,71 @@ impl LinearChecker { "path_join".to_string(), Some(TrackType::Custom("Str".to_string())), ); + + // Runtime support used by the self-hosted compiler (word ABI). + for name in [ + "track_i64_collection_new", + "track_i64_collection_len", + "track_i64_collection_push", + "track_i64_collection_get", + "track_i64_collection_set", + "track_i64_collection_pop", + "track_i64_collection_truncate", + "track_sb_new", + "track_sb_push", + "track_sb_push_byte", + "track_sb_push_int", + "track_sb_len", + "track_sb_str", + "track_sb_clear", + "track_str_bytes", + "track_file_read", + "track_file_write_all", + "track_out", + "track_err", + "track_words_copy", + "sys_exit", + "clock_ms", + "array_len", + ] { + self.functions + .insert(name.to_string(), Some(TrackType::I64)); + } } // end if !barebones — std package (import "std") still works via Use handling for stmt in program { match stmt { + Expr::ConstDef { name, value } => { + if let Some(v) = crate::codegen::eval_const(&self.const_values, value) { + self.const_values.insert(name.clone(), v); + self.type_info.consts.insert(name.clone(), v); + } + // Constants are module-level: visible to functions + // defined before them in the file. + self.declare(name.clone()); + if let Some(ty) = self.infer_type(value) { + self.types.insert(name.clone(), ty); + } + } + Expr::StructDef { name, fields } => { + self.structs.insert(name.clone(), fields.clone()); + self.type_info.structs.insert( + name.clone(), + fields.iter().map(|(f, _)| f.clone()).collect(), + ); + } Expr::TypeAlias { name, target } => { self.type_aliases.insert(name.clone(), target.clone()); + self.type_info.aliases.insert(name.clone(), target.clone()); } Expr::EnumDef { name, variants, .. } => { + for (vname, disc) in + crate::codegen::enum_discriminants(&self.const_values, variants) + { + self.type_info + .variants + .insert(format!("{}::{}", name, vname), disc); + } for (var_name, _) in variants { let fullname = format!("{}::{}", name, var_name); self.types @@ -431,6 +505,14 @@ impl LinearChecker { } Expr::UnionDef { name, variants } => { self.unions.insert(name.clone(), variants.clone()); + for (idx, (vname, ty)) in variants.iter().enumerate() { + self.type_info + .variants + .insert(format!("{}::{}", name, vname), idx as i64); + if ty.is_some() { + self.type_info.payload_unions.insert(name.clone()); + } + } for (var_name, ty_opt) in variants { let fullname = format!("{}::{}", name, var_name); if ty_opt.is_some() { @@ -536,6 +618,22 @@ impl LinearChecker { self.infer_type(target).map(|t| TrackType::Ref(Box::new(t))) } Expr::StructInitialization { ty_name, .. } => Some(TrackType::Custom(ty_name.clone())), + Expr::FieldAccess { target, field } => { + let ty = match self.infer_type(target)? { + TrackType::Ref(inner) => *inner, + other => other, + }; + match ty { + TrackType::Custom(name) => self + .structs + .get(&name)? + .iter() + .find(|(f, _)| f == field) + .map(|(_, t)| t.clone()), + _ => None, + } + } + Expr::StructDef { .. } => Some(TrackType::Void), Expr::FunctionCall { name, .. } => self.functions.get(name).cloned().flatten(), Expr::LensBlock { body, .. } => body.last().and_then(|e| self.infer_type(e)), Expr::IfElse { then_body, .. } => then_body.last().and_then(|e| self.infer_type(e)), @@ -567,7 +665,23 @@ impl LinearChecker { Expr::IntLiteral(_) | Expr::StringLiteral(_) | Expr::BoolLiteral(_) - | Expr::TypeAlias { .. } => Ok(()), + | Expr::TypeAlias { .. } + | Expr::StructDef { .. } => Ok(()), + + Expr::FieldAccess { target, field } => { + self.check_borrow(target)?; + if !self + .structs + .values() + .any(|fs| fs.iter().any(|(f, _)| f == field)) + { + return Err(format!( + "Compile Error: Unknown field '{}' (no struct declares it).", + field + )); + } + Ok(()) + } Expr::Variable(name) => { self.read_or_move(name)?; @@ -651,7 +765,17 @@ impl LinearChecker { Ok(()) } - Expr::StructInitialization { fields, .. } => { + Expr::StructInitialization { ty_name, fields } => { + if let Some(decl) = self.structs.get(ty_name) { + for (fname, _) in fields { + if !decl.iter().any(|(f, _)| f == fname) { + return Err(format!( + "Compile Error: Struct '{}' has no field '{}'.", + ty_name, fname + )); + } + } + } for (_, fval) in fields { self.reject_lens_escape(fval)?; self.check_expr(fval)?; @@ -720,9 +844,7 @@ impl LinearChecker { }; self.declare(name.clone()); - if *mutable { - self.mutables.insert(name.clone()); - } + self.set_mutable(name, *mutable); self.types.insert(name.clone(), final_ty.clone()); if matches!(final_ty, TrackType::Ref(_)) { @@ -740,6 +862,7 @@ impl LinearChecker { } => { self.enter_lens(target)?; self.declare(lens_name.clone()); + self.set_mutable(lens_name, false); self.lens_aliases.insert(lens_name.clone()); if let Some(ty) = self.types.get(target).cloned() { self.types.insert(lens_name.clone(), ty); @@ -767,6 +890,7 @@ impl LinearChecker { let pre_if = self.registry.clone(); let pre_if_types = self.types.clone(); let pre_if_borrows = self.borrows.clone(); + let pre_if_mutables = self.mutables.clone(); // Check then branch directly for stmt in then_body { @@ -776,6 +900,7 @@ impl LinearChecker { let then_end = std::mem::replace(&mut self.registry, pre_if.clone()); let _then_types = std::mem::replace(&mut self.types, pre_if_types.clone()); let then_end_borrows = std::mem::replace(&mut self.borrows, pre_if_borrows.clone()); + self.mutables = pre_if_mutables.clone(); // Check else branch directly for stmt in else_body { @@ -817,6 +942,7 @@ impl LinearChecker { self.registry = merged; self.types = merged_types; self.borrows = merged_borrows; + self.mutables = pre_if_mutables; self.update_borrow_states(); Ok(()) } @@ -829,6 +955,7 @@ impl LinearChecker { let pre_loop = self.registry.clone(); let pre_loop_types = self.types.clone(); let pre_loop_borrows = self.borrows.clone(); + let pre_loop_mutables = self.mutables.clone(); for stmt in body { self.check_expr(stmt)?; @@ -852,6 +979,7 @@ impl LinearChecker { self.registry = pre_loop; self.types = pre_loop_types; self.borrows = pre_loop_borrows; + self.mutables = pre_loop_mutables; self.update_borrow_states(); Ok(()) } @@ -867,8 +995,10 @@ impl LinearChecker { let pre_loop = self.registry.clone(); let pre_loop_types = self.types.clone(); let pre_loop_borrows = self.borrows.clone(); + let pre_loop_mutables = self.mutables.clone(); self.declare(var.clone()); + self.set_mutable(var, false); self.types.insert(var.clone(), elem_ty); for stmt in body { @@ -892,6 +1022,7 @@ impl LinearChecker { self.registry = pre_loop; self.types = pre_loop_types; self.borrows = pre_loop_borrows; + self.mutables = pre_loop_mutables; self.update_borrow_states(); Ok(()) } @@ -921,22 +1052,19 @@ impl LinearChecker { self.check_expr(value)?; // For simple variable assignment, check mutability and re-activate variable if let Expr::Variable(name) = target.as_ref() { - if !self.mutables.contains(name) && self.registry.contains_key(name) { + if !self.registry.contains_key(name) { + return Err(format!("Compile Error: Undeclared variable '{}'.", name)); + } + if !self.mutables.contains(name) { return Err(format!( "Compile Error: Cannot mutate immutable variable '{}'. Use 'let mut {}' to declare a mutable variable.", name, name )); } self.registry.insert(name.clone(), VariableState::Active); - if let Some(ty) = self.infer_type(value) { - self.types.insert(name.clone(), ty.clone()); - if matches!(ty, TrackType::Ref(_)) { - let prov = self.get_provenance(value); - self.borrows.insert(name.clone(), prov); - } else { - self.borrows.remove(name); - } - } + self.assign_types(name, value); + } else { + self.check_place(target)?; } self.update_borrow_states(); Ok(()) @@ -960,12 +1088,14 @@ impl LinearChecker { let saved_lens_aliases = std::mem::take(&mut self.lens_aliases); let saved_params = std::mem::take(&mut self.current_params); let saved_ret = self.current_return_type.clone(); + let saved_mutables = self.mutables.clone(); self.current_params = params.iter().map(|(n, _)| n.clone()).collect(); self.current_return_type = return_type.clone(); for (name, ty) in params { self.declare(name.clone()); + self.set_mutable(name, false); self.types.insert(name.clone(), ty.clone()); } @@ -1001,6 +1131,7 @@ impl LinearChecker { self.lens_aliases = saved_lens_aliases; self.current_params = saved_params; self.current_return_type = saved_ret; + self.mutables = saved_mutables; Ok(()) } @@ -1234,12 +1365,14 @@ impl LinearChecker { let saved_lens_aliases = std::mem::take(&mut self.lens_aliases); let saved_params = std::mem::take(&mut self.current_params); let saved_ret = self.current_return_type.clone(); + let saved_mutables = self.mutables.clone(); self.current_params = params.iter().map(|(n, _)| n.clone()).collect(); self.current_return_type = return_type.clone(); for (pname, pty) in params { self.declare(pname.clone()); + self.set_mutable(pname, false); self.types.insert(pname.clone(), pty.clone()); } @@ -1257,6 +1390,7 @@ impl LinearChecker { self.lens_aliases = saved_lens_aliases; self.current_params = saved_params; self.current_return_type = saved_ret; + self.mutables = saved_mutables; Ok(()) } @@ -1338,6 +1472,7 @@ impl LinearChecker { let saved_borrows = self.borrows.clone(); let saved_lens = self.lens_locked.clone(); let saved_lens_aliases = self.lens_aliases.clone(); + let saved_mutables = self.mutables.clone(); self.bind_pattern_variables(&arm.pattern, &target_ty, false)?; @@ -1352,12 +1487,52 @@ impl LinearChecker { self.borrows = saved_borrows; self.lens_locked = saved_lens; self.lens_aliases = saved_lens_aliases; + self.mutables = saved_mutables; } Ok(()) } } } + /// After `name = value`, the variable takes the value's type and + /// provenance. + fn assign_types(&mut self, name: &str, value: &Expr) { + if let Some(ty) = self.infer_type(value) { + self.types.insert(name.to_string(), ty.clone()); + if matches!(ty, TrackType::Ref(_)) { + let prov = self.get_provenance(value); + self.borrows.insert(name.to_string(), prov); + } else { + self.borrows.remove(name); + } + } + } + + /// Check an assignment target such as `p.x`, `t.0`, or `a[i]`: the root + /// variable must be usable and either `let mut` or a lens alias. + fn check_place(&mut self, place: &Expr) -> Result<(), String> { + match place { + Expr::FieldAccess { target, .. } | Expr::TupleIndex { target, .. } => { + self.check_place(target) + } + Expr::ArrayIndex { target, index } => { + self.check_borrow(index)?; + self.check_place(target) + } + Expr::Variable(name) => { + self.check_borrow(place)?; + if !self.mutables.contains(name) && !self.lens_aliases.contains(name) { + return Err(format!( + "Compile Error: Cannot mutate immutable variable '{}'. Use 'let mut {}' to declare a mutable variable.", + name, name + )); + } + Ok(()) + } + other => self.check_expr(other), + } + } + fn bind_pattern_variables( &mut self, pattern: &crate::ast::Pattern, @@ -1368,9 +1543,7 @@ impl LinearChecker { crate::ast::Pattern::Ident(name) => { self.declare(name.clone()); self.types.insert(name.clone(), target_type.clone()); - if mutable { - self.mutables.insert(name.clone()); - } + self.set_mutable(name, mutable); } crate::ast::Pattern::Wildcard | crate::ast::Pattern::Literal(_) => {} crate::ast::Pattern::Tuple(pats) => { @@ -1437,6 +1610,10 @@ impl LinearChecker { target: inner_target, .. } => self.get_provenance(inner_target), + Expr::FieldAccess { .. } => match place_root(target) { + Some(name) => vec![name.to_string()], + None => Vec::new(), + }, _ => self.get_provenance(target), }, Expr::Variable(name) => { @@ -1553,6 +1730,8 @@ impl LinearChecker { elements.iter().find_map(|e| self.find_lens_alias(e)) } Expr::TupleIndex { target, .. } => self.find_lens_alias(target), + // Reading a field yields the field's value, not the lens itself. + Expr::FieldAccess { .. } => None, Expr::FunctionCall { .. } => None, Expr::MacroCall { args, body, .. } => args .iter() @@ -1579,12 +1758,24 @@ impl LinearChecker { | Expr::EnumDef { .. } | Expr::UnionDef { .. } | Expr::TypeAlias { .. } + | Expr::StructDef { .. } | Expr::ForIn { .. } | Expr::Variable(_) => None, } } } +/// The variable at the base of `a.b.0[i]`, if any. +fn place_root(expr: &Expr) -> Option<&str> { + match expr { + Expr::Variable(name) => Some(name), + Expr::FieldAccess { target, .. } + | Expr::TupleIndex { target, .. } + | Expr::ArrayIndex { target, .. } => place_root(target), + _ => None, + } +} + fn is_comparison(op: &BinOp) -> bool { matches!( op, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index eb6a73c..84274b2 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -1,9 +1,20 @@ -use std::collections::HashMap; +//! Cranelift backend for the Rust bootstrap compiler. +//! +//! Every Track value is lowered to a single 64-bit machine word (the "word +//! ABI"): integers and booleans are sign/zero-normalised `i64`s, strings are +//! `char*` pointers, and tuples / tagged-union cells are pointers to arrays of +//! words. All Track functions take and return words, which keeps calls across +//! separately compiled modules well defined and matches the portable C backend +//! of the self-hosted compiler (`compiler/src/cgen.trk`) bit for bit. + +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::Path; use std::sync::Arc; -use cranelift_codegen::ir::{self, AbiParam, InstBuilder, StackSlotData, StackSlotKind, Value}; +use cranelift_codegen::ir::{ + self, AbiParam, InstBuilder, StackSlotData, StackSlotKind, Value, condcodes::IntCC, +}; use cranelift_codegen::isa; use cranelift_codegen::settings::{self, Configurable}; use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; @@ -13,15 +24,109 @@ use target_lexicon::Triple; use crate::ast::{BinOp, Expr, Pattern, TrackType, UnaryOp}; +const W: ir::Type = ir::types::I64; + +/// Type knowledge gathered by the checker across a module and everything it +/// imports. Codegen compiles one file at a time, so enum/union layouts and +/// type aliases defined in imported modules arrive through this table. +#[derive(Debug, Clone, Default)] +pub struct TypeInfo { + /// `Enum::Variant` / `Union::Variant` → discriminant. + pub variants: HashMap, + /// Unions with at least one payload variant; their values are heap cells + /// laid out as `[tag, payload...]`. + pub payload_unions: HashSet, + pub aliases: HashMap, + /// Integer constants, including those defined in imported modules. + pub consts: HashMap, + /// Struct name → field names in declaration order. + pub structs: HashMap>, +} + pub struct CodeGen { module_name: String, module: ObjectModule, fn_builder_ctx: FunctionBuilderContext, functions: HashMap, - variant_map: HashMap, + info: TypeInfo, consts: HashMap, } +/// Evaluate a compile-time constant expression (integer literals, negation, +/// and arithmetic over known constants). +pub fn eval_const(consts: &HashMap, expr: &Expr) -> Option { + match expr { + Expr::IntLiteral(v) => Some(*v), + Expr::BoolLiteral(b) => Some(if *b { 1 } else { 0 }), + Expr::UnaryOp { + op: UnaryOp::Neg, + expr, + } => eval_const(consts, expr).map(|v| v.wrapping_neg()), + Expr::BinaryOp { op, left, right } => { + let l = eval_const(consts, left)?; + let r = eval_const(consts, right)?; + match op { + BinOp::Add => Some(l.wrapping_add(r)), + BinOp::Sub => Some(l.wrapping_sub(r)), + BinOp::Mul => Some(l.wrapping_mul(r)), + BinOp::Div if r != 0 => Some(l.wrapping_div(r)), + BinOp::Mod if r != 0 => Some(l.wrapping_rem(r)), + BinOp::BitAnd => Some(l & r), + BinOp::BitOr => Some(l | r), + BinOp::Shl => Some(l.wrapping_shl((r & 63) as u32)), + BinOp::Shr => Some(((l as u64) >> (r & 63)) as i64), + _ => None, + } + } + Expr::Variable(name) => consts.get(name).copied(), + _ => None, + } +} + +/// Assign discriminants to enum variants: an explicit `= value` sets the +/// discriminant, otherwise it is one more than the previous variant. +pub fn enum_discriminants( + consts: &HashMap, + variants: &[(String, Option)], +) -> Vec<(String, i64)> { + let mut next = 0i64; + variants + .iter() + .map(|(name, value)| { + let disc = value + .as_ref() + .and_then(|v| eval_const(consts, v)) + .unwrap_or(next); + next = disc.wrapping_add(1); + (name.clone(), disc) + }) + .collect() +} + +/// Runtime tag stored in word 0 of every struct value: FNV-1a over the +/// name with 64-bit wrapping (identical in the self-hosted compiler). +pub fn struct_tag(name: &str) -> i64 { + let mut h: i64 = 2166136261; + for b in name.bytes() { + h ^= i64::from(b); + h = h.wrapping_mul(16777619); + } + h +} + +/// Number of words in a tuple type, resolving aliases. +fn tuple_len(aliases: &HashMap, ty: &TrackType) -> Option { + let mut current = ty; + for _ in 0..16 { + match current { + TrackType::Tuple(elems) => return Some(elems.len()), + TrackType::Custom(name) => current = aliases.get(name)?, + _ => return None, + } + } + None +} + impl CodeGen { pub fn create_default_isa() -> Arc { let mut flag_builder = settings::builder(); @@ -47,81 +152,88 @@ impl CodeGen { let object_builder = ObjectBuilder::new(isa, module_name, default_libcall_names()).unwrap(); let module = ObjectModule::new(object_builder); - let mut variant_map = HashMap::new(); - variant_map.insert("Red".to_string(), 0); - variant_map.insert("Green".to_string(), 1); - variant_map.insert("Blue".to_string(), 2); - variant_map.insert("Active".to_string(), 0); - variant_map.insert("Locked".to_string(), 1); - variant_map.insert("Spent".to_string(), 2); - variant_map.insert("Int".to_string(), 0); - variant_map.insert("Float".to_string(), 1); - variant_map.insert("Bool".to_string(), 2); - variant_map.insert("Ok".to_string(), 0); - variant_map.insert("Err".to_string(), 1); - Self { module_name: module_name.to_string(), module, fn_builder_ctx: FunctionBuilderContext::new(), functions: HashMap::new(), - variant_map, + info: TypeInfo::default(), consts: HashMap::new(), } } - /// Evaluate a compile-time constant expression (integer literals, - /// negation, and arithmetic over known constants). - fn eval_const(&self, expr: &Expr) -> Option { - match expr { - Expr::IntLiteral(v) => Some(*v), - Expr::BoolLiteral(b) => Some(if *b { 1 } else { 0 }), - Expr::UnaryOp { - op: UnaryOp::Neg, - expr, - } => self.eval_const(expr).map(|v| -v), - Expr::BinaryOp { op, left, right } => { - let l = self.eval_const(left)?; - let r = self.eval_const(right)?; - match op { - BinOp::Add => Some(l.wrapping_add(r)), - BinOp::Sub => Some(l.wrapping_sub(r)), - BinOp::Mul => Some(l.wrapping_mul(r)), - BinOp::Div if r != 0 => Some(l.wrapping_div(r)), - BinOp::Mod if r != 0 => Some(l.wrapping_rem(r)), - BinOp::BitAnd => Some(l & r), - BinOp::BitOr => Some(l | r), - BinOp::Shl => Some(l.wrapping_shl(r as u32)), - BinOp::Shr => Some(l.wrapping_shr(r as u32)), - _ => None, + /// Provide enum/union layouts and aliases discovered by the checker + /// (including imported modules). + pub fn set_type_info(&mut self, info: TypeInfo) { + self.consts + .extend(info.consts.iter().map(|(k, v)| (k.clone(), *v))); + self.info = info; + } + + fn signature( + &self, + name: &str, + params: &[(String, TrackType)], + ret: Option<&TrackType>, + ) -> ir::Signature { + let mut sig = self.module.make_signature(); + if name == "main" { + if params.is_empty() { + // C entry point: int main(int argc, char **argv) + sig.params.push(AbiParam::new(ir::types::I32)); + sig.params.push(AbiParam::new(W)); + } else { + for _ in params { + sig.params.push(AbiParam::new(W)); } } - Expr::Variable(name) => self.consts.get(name).copied(), - _ => None, + sig.returns.push(AbiParam::new(ir::types::I32)); + return sig; + } + for _ in params { + sig.params.push(AbiParam::new(W)); } + if let Some(rty) = ret + && *rty != TrackType::Void + { + sig.returns.push(AbiParam::new(W)); + } + sig } pub fn compile_program(&mut self, program: &[Expr]) { - // First pass: declare all functions, macros, enums, unions, and consts + // First pass: constants, type layouts, and function declarations. for expr in program { match expr { Expr::ConstDef { name, value } => { - if let Some(v) = self.eval_const(value) { + if let Some(v) = eval_const(&self.consts, value) { self.consts.insert(name.clone(), v); } } + Expr::TypeAlias { name, target } => { + self.info.aliases.insert(name.clone(), target.clone()); + } + Expr::StructDef { name, fields } => { + self.info.structs.insert( + name.clone(), + fields.iter().map(|(f, _)| f.clone()).collect(), + ); + } Expr::EnumDef { name, variants, .. } => { - for (idx, (vname, _)) in variants.iter().enumerate() { - self.variant_map.insert(vname.clone(), idx as i64); - self.variant_map - .insert(format!("{}::{}", name, vname), idx as i64); + for (vname, disc) in enum_discriminants(&self.consts, variants) { + self.info + .variants + .insert(format!("{}::{}", name, vname), disc); } } Expr::UnionDef { name, variants } => { - for (idx, (vname, _)) in variants.iter().enumerate() { - self.variant_map.insert(vname.clone(), idx as i64); - self.variant_map + for (idx, (vname, ty)) in variants.iter().enumerate() { + self.info + .variants .insert(format!("{}::{}", name, vname), idx as i64); + if ty.is_some() { + self.info.payload_unions.insert(name.clone()); + } } } Expr::FnDef { @@ -136,23 +248,7 @@ impl CodeGen { if !generics.is_empty() { continue; } - let mut sig = self.module.make_signature(); - for (_, pty) in params { - sig.params.push(AbiParam::new(track_type_to_cl(pty))); - } - if name == "main" { - sig.returns.push(AbiParam::new(ir::types::I32)); - } else if let Some(rty) = return_type - && *rty != TrackType::Void - { - sig.returns.push(AbiParam::new(track_type_to_cl(rty))); - } - - let func_id = self - .module - .declare_function(name, Linkage::Export, &sig) - .unwrap_or_else(|_| self.module.declare_anonymous_function(&sig).unwrap()); - self.functions.insert(name.clone(), func_id); + self.declare_track_fn(name, params, return_type.as_ref()); } Expr::MacroDef { name, @@ -160,72 +256,39 @@ impl CodeGen { return_type, .. } => { - let mut sig = self.module.make_signature(); - for (_, pty) in params { - sig.params.push(AbiParam::new(track_type_to_cl(pty))); - } - if name == "main" { - sig.returns.push(AbiParam::new(ir::types::I32)); - } else if let Some(rty) = return_type - && *rty != TrackType::Void - { - sig.returns.push(AbiParam::new(track_type_to_cl(rty))); - } - - let func_id = self - .module - .declare_function(name, Linkage::Export, &sig) - .unwrap_or_else(|_| self.module.declare_anonymous_function(&sig).unwrap()); - self.functions.insert(name.clone(), func_id); + self.declare_track_fn(name, params, return_type.as_ref()); } _ => {} } } - // Declare built-in print / printf if needed - let mut print_sig = self.module.make_signature(); - print_sig.params.push(AbiParam::new(ir::types::I64)); - let print_id = self - .module - .declare_function("print", Linkage::Import, &print_sig) - .unwrap_or_else(|_| self.module.declare_anonymous_function(&print_sig).unwrap()); - self.functions - .entry("print".to_string()) - .or_insert(print_id); - let mut has_main = false; // Second pass: define function bodies for expr in program { - if let Expr::FnDef { - name, - generics, - params, - return_type, - body, - } = expr - { - if !generics.is_empty() { - // Templates are handled via their monomorphized instances. - continue; - } - if name == "main" { - has_main = true; + match expr { + Expr::FnDef { + name, + generics, + params, + return_type, + body, + } if generics.is_empty() => { + has_main |= name == "main"; + let func_id = self.functions[name.as_str()]; + self.compile_fn(func_id, name, params, return_type.as_ref(), body, program); } - let func_id = *self.functions.get(name).unwrap(); - self.compile_fn(func_id, name, params, return_type.as_ref(), body, program); - } else if let Expr::MacroDef { - name, - params, - return_type, - body, - } = expr - { - if name == "main" { - has_main = true; + Expr::MacroDef { + name, + params, + return_type, + body, + } => { + has_main |= name == "main"; + let func_id = self.functions[name.as_str()]; + self.compile_fn(func_id, name, params, return_type.as_ref(), body, program); } - let func_id = *self.functions.get(name).unwrap(); - self.compile_fn(func_id, name, params, return_type.as_ref(), body, program); + _ => {} } } @@ -235,10 +298,24 @@ impl CodeGen { || self.module_name == "track_module" || self.module_name.ends_with("main")) { - self.synthesize_main(program); + self.compile_fn_inner(None, "main", &[], None, &[], program); } } + fn declare_track_fn( + &mut self, + name: &str, + params: &[(String, TrackType)], + ret: Option<&TrackType>, + ) { + let sig = self.signature(name, params, ret); + let func_id = self + .module + .declare_function(name, Linkage::Export, &sig) + .unwrap_or_else(|_| self.module.declare_anonymous_function(&sig).unwrap()); + self.functions.insert(name.to_string(), func_id); + } + fn compile_fn( &mut self, func_id: FuncId, @@ -247,79 +324,97 @@ impl CodeGen { return_type: Option<&TrackType>, body: &[Expr], program: &[Expr], + ) { + self.compile_fn_inner(Some(func_id), name, params, return_type, body, program); + } + + fn compile_fn_inner( + &mut self, + func_id: Option, + name: &str, + params: &[(String, TrackType)], + return_type: Option<&TrackType>, + body: &[Expr], + program: &[Expr], ) { let is_main = name == "main"; + let sig = self.signature(name, params, return_type); + let func_id = match func_id { + Some(id) => id, + None => self + .module + .declare_function("main", Linkage::Export, &sig) + .unwrap(), + }; let mut ctx = self.module.make_context(); - let mut sig = self.module.make_signature(); - for (_, pty) in params { - sig.params.push(AbiParam::new(track_type_to_cl(pty))); - } - if is_main { - sig.returns.push(AbiParam::new(ir::types::I32)); - } else if let Some(rty) = return_type - && *rty != TrackType::Void - { - sig.returns.push(AbiParam::new(track_type_to_cl(rty))); - } ctx.func.signature = sig; + let returns_value = is_main || return_type.is_some_and(|rty| *rty != TrackType::Void); let mut builder = FunctionBuilder::new(&mut ctx.func, &mut self.fn_builder_ctx); let entry_block = builder.create_block(); builder.append_block_params_for_function_params(entry_block); builder.switch_to_block(entry_block); - let mut var_map: HashMap = HashMap::new(); - let mut var_counter = 0u32; - - for (idx, (pname, pty)) in params.iter().enumerate() { - let var = Variable::from_u32(var_counter); - var_counter += 1; - builder.declare_var(var, track_type_to_cl(pty)); - let val = builder.block_params(entry_block)[idx]; - builder.def_var(var, val); - var_map.insert(pname.clone(), var); - } - let mut fn_ctx = FnContext { - var_map, - var_counter, + var_map: HashMap::new(), + var_counter: 0, functions: &mut self.functions, - variant_map: &self.variant_map, + info: &self.info, consts: &self.consts, + filled: false, + is_main, + returns_value, + stack_tuples: HashMap::new(), + tuple_params: HashMap::new(), }; - // If compiling main, compile top-level statements (global variables/consts) into entry block first + if is_main && params.is_empty() { + // Publish argc/argv to the runtime before any user code runs. + let argc = builder.block_params(entry_block)[0]; + let argv = builder.block_params(entry_block)[1]; + let argc = builder.ins().sextend(W, argc); + fn_ctx.call_named( + &mut builder, + &mut self.module, + "track_runtime_init", + &[argc, argv], + ); + } else { + for (idx, (pname, pty)) in params.iter().enumerate() { + let var = fn_ctx.new_var(); + builder.declare_var(var, W); + let val = builder.block_params(entry_block)[idx]; + builder.def_var(var, val); + fn_ctx.var_map.insert(pname.clone(), var); + if let Some(n) = tuple_len(&self.info.aliases, pty) { + fn_ctx.tuple_params.insert(pname.clone(), n); + } + } + } + + // Top-level statements (global lets) run at the start of main. if is_main { for top_stmt in program { + if fn_ctx.filled { + break; + } if !matches!(top_stmt, Expr::FnDef { .. } | Expr::MacroDef { .. }) { fn_ctx.compile_expr(&mut builder, &mut self.module, top_stmt); } } } - let mut last_val = None; - let mut has_return_stmt = false; - for stmt in body { - if builder.is_unreachable() { - break; - } - if matches!(stmt, Expr::Return { .. }) { - has_return_stmt = true; - } - last_val = fn_ctx.compile_expr(&mut builder, &mut self.module, stmt); - } + let last_val = fn_ctx.compile_block(&mut builder, &mut self.module, body); - if !builder.is_unreachable() && !has_return_stmt { + if !fn_ctx.filled { if is_main { let zero = builder.ins().iconst(ir::types::I32, 0); builder.ins().return_(&[zero]); - } else if return_type.is_none() || return_type == Some(&TrackType::Void) { - builder.ins().return_(&[]); - } else if let Some(v) = last_val { + } else if returns_value { + let v = last_val.unwrap_or_else(|| builder.ins().iconst(W, 0)); builder.ins().return_(&[v]); } else { - let default_ret = builder.ins().iconst(ir::types::I64, 0); - builder.ins().return_(&[default_ret]); + builder.ins().return_(&[]); } } @@ -329,51 +424,6 @@ impl CodeGen { self.module.clear_context(&mut ctx); } - fn synthesize_main(&mut self, program: &[Expr]) { - let mut sig = self.module.make_signature(); - sig.returns.push(AbiParam::new(ir::types::I32)); - - let main_id = self - .module - .declare_function("main", Linkage::Export, &sig) - .unwrap(); - - let mut ctx = self.module.make_context(); - ctx.func.signature = sig; - - let mut builder = FunctionBuilder::new(&mut ctx.func, &mut self.fn_builder_ctx); - let entry_block = builder.create_block(); - builder.switch_to_block(entry_block); - - let mut fn_ctx = FnContext { - var_map: HashMap::new(), - var_counter: 0, - functions: &mut self.functions, - variant_map: &self.variant_map, - consts: &self.consts, - }; - - let mut has_return_stmt = false; - for stmt in program { - if !matches!(stmt, Expr::FnDef { .. } | Expr::MacroDef { .. }) { - if matches!(stmt, Expr::Return { .. }) { - has_return_stmt = true; - } - fn_ctx.compile_expr(&mut builder, &mut self.module, stmt); - } - } - - if !builder.is_unreachable() && !has_return_stmt { - let ret_code = builder.ins().iconst(ir::types::I32, 0); - builder.ins().return_(&[ret_code]); - } - - builder.seal_all_blocks(); - builder.finalize(); - self.module.define_function(main_id, &mut ctx).unwrap(); - self.module.clear_context(&mut ctx); - } - pub fn write_object_file(self, output_path: &Path) -> Result<(), String> { let product = self.module.finish(); let bytes = product @@ -389,12 +439,27 @@ impl CodeGen { } } +struct Scope { + vars: HashMap, + stack_tuples: HashMap, + tuple_params: HashMap, +} + struct FnContext<'a> { var_map: HashMap, var_counter: u32, functions: &'a mut HashMap, - variant_map: &'a HashMap, + info: &'a TypeInfo, consts: &'a HashMap, + /// The current block already ends in a terminator; further statements in + /// the same lexical block are unreachable and skipped. + filled: bool, + is_main: bool, + returns_value: bool, + /// Locals bound directly to a tuple literal live in a stack slot and must + /// be copied to the heap when returned. + stack_tuples: HashMap, + tuple_params: HashMap, } impl<'a> FnContext<'a> { @@ -404,20 +469,249 @@ impl<'a> FnContext<'a> { var } + fn switch_to(&mut self, builder: &mut FunctionBuilder, block: ir::Block) { + builder.switch_to_block(block); + self.filled = false; + } + + fn jump(&mut self, builder: &mut FunctionBuilder, block: ir::Block, args: &[Value]) { + if !self.filled { + builder.ins().jump(block, args); + self.filled = true; + } + } + + /// Normalise a value to a full word (booleans and narrow parameters). + fn word(&self, builder: &mut FunctionBuilder, v: Value) -> Value { + let ty = builder.func.dfg.value_type(v); + if ty == W { + v + } else if ty == ir::types::I32 { + builder.ins().sextend(W, v) + } else { + builder.ins().uextend(W, v) + } + } + + fn bool_word(&self, builder: &mut FunctionBuilder, cc: IntCC, a: Value, b: Value) -> Value { + let c = builder.ins().icmp(cc, a, b); + builder.ins().uextend(W, c) + } + + fn zero(&self, builder: &mut FunctionBuilder) -> Value { + builder.ins().iconst(W, 0) + } + + fn func_ref( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + name: &str, + argc: usize, + ) -> ir::FuncRef { + let func_id = if let Some(&fid) = self.functions.get(name) { + fid + } else { + let mut sig = module.make_signature(); + for _ in 0..argc { + sig.params.push(AbiParam::new(W)); + } + sig.returns.push(AbiParam::new(W)); + let fid = module + .declare_function(name, Linkage::Import, &sig) + .unwrap_or_else(|_| module.declare_anonymous_function(&sig).unwrap()); + self.functions.insert(name.to_string(), fid); + fid + }; + module.declare_func_in_func(func_id, builder.func) + } + + /// Call a function by symbol name with word arguments, adapting to the + /// declared signature of locally defined functions. + fn call_named( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + name: &str, + args: &[Value], + ) -> Option { + let local_func = self.func_ref(builder, module, name, args.len()); + let sig_ref = builder.func.dfg.ext_funcs[local_func].signature; + let expected: Vec = builder.func.dfg.signatures[sig_ref] + .params + .iter() + .map(|p| p.value_type) + .collect(); + let mut fixed_args = Vec::with_capacity(expected.len()); + for (idx, &ty) in expected.iter().enumerate() { + let arg = match args.get(idx) { + Some(&a) => self.word(builder, a), + None => self.zero(builder), + }; + fixed_args.push(if ty == W { + arg + } else { + builder.ins().ireduce(ty, arg) + }); + } + let call_inst = builder.ins().call(local_func, &fixed_args); + let results = builder.inst_results(call_inst); + results.first().copied().map(|r| self.word(builder, r)) + } + + fn alloc_words( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + words: &[Value], + ) -> Value { + let size = builder.ins().iconst(W, (words.len().max(1) * 8) as i64); + let ptr = self + .call_named(builder, module, "alloc", &[size]) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + for (i, &val) in words.iter().enumerate() { + builder + .ins() + .store(ir::MemFlags::new(), val, ptr, (i * 8) as i32); + } + ptr + } + + fn copy_words_to_heap( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + src: Value, + count: usize, + ) -> Value { + let words: Vec = (0..count) + .map(|i| { + builder + .ins() + .load(W, ir::MemFlags::new(), src, (i * 8) as i32) + }) + .collect(); + self.alloc_words(builder, module, &words) + } + + /// (tag, field index) of every struct declaring `field`, by struct name. + fn field_candidates(&self, field: &str) -> Vec<(i64, usize)> { + let mut names: Vec<&String> = self.info.structs.keys().collect(); + names.sort(); + names + .into_iter() + .filter_map(|name| { + self.info.structs[name] + .iter() + .position(|f| f == field) + .map(|idx| (struct_tag(name), idx)) + }) + .collect() + } + + /// Load (`store == None`) or store a struct field. A field name shared + /// by several structs dispatches on the tag in word 0. + fn field_access( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + ptr: Value, + field: &str, + store: Option, + ) -> Value { + let candidates = self.field_candidates(field); + let access = |builder: &mut FunctionBuilder, idx: usize| -> Value { + let offset = ((idx + 1) * 8) as i32; + match store { + Some(v) => { + builder.ins().store(ir::MemFlags::new(), v, ptr, offset); + v + } + None => builder.ins().load(W, ir::MemFlags::new(), ptr, offset), + } + }; + if candidates.len() == 1 { + return access(builder, candidates[0].1); + } + let merge = builder.create_block(); + let result = builder.append_block_param(merge, W); + let tag = builder.ins().load(W, ir::MemFlags::new(), ptr, 0); + for (expected, idx) in candidates { + let hit = builder.create_block(); + let miss = builder.create_block(); + let want = builder.ins().iconst(W, expected); + let is_hit = builder.ins().icmp(IntCC::Equal, tag, want); + builder.ins().brif(is_hit, hit, &[], miss, &[]); + self.filled = true; + self.switch_to(builder, hit); + let v = access(builder, idx); + self.jump(builder, merge, &[v]); + self.switch_to(builder, miss); + } + let bad = self + .call_named(builder, module, "track_bad_field", &[]) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + self.jump(builder, merge, &[bad]); + self.switch_to(builder, merge); + result + } + + fn compile_block( + &mut self, + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + stmts: &[Expr], + ) -> Option { + // Bindings introduced inside a block go out of scope at its end. + let scope = self.enter_scope(); + let mut last = None; + for stmt in stmts { + if self.filled { + break; + } + last = self.compile_expr(builder, module, stmt); + } + self.exit_scope(scope); + last + } + + fn enter_scope(&self) -> Scope { + Scope { + vars: self.var_map.clone(), + stack_tuples: self.stack_tuples.clone(), + tuple_params: self.tuple_params.clone(), + } + } + + fn exit_scope(&mut self, scope: Scope) { + self.var_map = scope.vars; + self.stack_tuples = scope.stack_tuples; + self.tuple_params = scope.tuple_params; + } + + fn variant_key(&self, name: &str) -> Option { + self.info.variants.get(name).copied() + } + + fn is_payload_variant(&self, name: &str) -> bool { + name.split("::") + .next() + .is_some_and(|u| self.info.payload_unions.contains(u)) + } + fn compile_expr( &mut self, builder: &mut FunctionBuilder, module: &mut ObjectModule, expr: &Expr, ) -> Option { + if self.filled { + return None; + } match expr { - Expr::IntLiteral(val) => Some(builder.ins().iconst(ir::types::I64, *val)), + Expr::IntLiteral(val) => Some(builder.ins().iconst(W, *val)), - Expr::BoolLiteral(val) => Some( - builder - .ins() - .iconst(ir::types::I8, if *val { 1 } else { 0 }), - ), + Expr::BoolLiteral(val) => Some(builder.ins().iconst(W, if *val { 1 } else { 0 })), Expr::StringLiteral(s) => { let mut data_ctx = cranelift_module::DataDescription::new(); @@ -429,52 +723,113 @@ impl<'a> FnContext<'a> { module.define_data(data_id, &data_ctx).unwrap(); let local_data = module.declare_data_in_func(data_id, builder.func); - let ptr = builder.ins().symbol_value(ir::types::I64, local_data); - Some(ptr) + Some(builder.ins().symbol_value(W, local_data)) } Expr::Variable(name) => { - if name.contains("::") { - let variant_name = name.rsplit("::").next().unwrap_or(name); - let disc = self - .variant_map - .get(name) - .or_else(|| self.variant_map.get(variant_name)) - .copied() - .unwrap_or(0); - Some(builder.ins().iconst(ir::types::I64, disc)) - } else if let Some(&var) = self.var_map.get(name) { + if let Some(&var) = self.var_map.get(name) { Some(builder.use_var(var)) + } else if name.contains("::") { + let disc = self.variant_key(name).unwrap_or(0); + let tag = builder.ins().iconst(W, disc); + if self.is_payload_variant(name) { + let zero = self.zero(builder); + Some(self.alloc_words(builder, module, &[tag, zero])) + } else { + Some(tag) + } } else if let Some(&c) = self.consts.get(name) { - Some(builder.ins().iconst(ir::types::I64, c)) + Some(builder.ins().iconst(W, c)) } else { None } } Expr::LetDef { name, value, .. } => { - let val = self.compile_expr(builder, module, value)?; + let val = self + .compile_expr(builder, module, value) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; + } let var = self.new_var(); - let ty = builder.func.dfg.value_type(val); - builder.declare_var(var, ty); + builder.declare_var(var, W); builder.def_var(var, val); self.var_map.insert(name.clone(), var); + self.track_tuple_binding(name, value); Some(val) } Expr::Assign { target, value } => { let val = self.compile_expr(builder, module, value)?; - if let Expr::Variable(name) = &**target - && let Some(&var) = self.var_map.get(name) - { - builder.def_var(var, val); + let val = self.word(builder, val); + match &**target { + Expr::Variable(name) => { + if let Some(&var) = self.var_map.get(name) { + builder.def_var(var, val); + self.track_tuple_binding(name, value); + } + } + Expr::FieldAccess { target, field } => { + let ptr = self.compile_expr(builder, module, target)?; + self.field_access(builder, module, ptr, field, Some(val)); + } + Expr::TupleIndex { target, index } => { + let ptr = self.compile_expr(builder, module, target)?; + builder + .ins() + .store(ir::MemFlags::new(), val, ptr, (*index * 8) as i32); + } + Expr::ArrayIndex { target, index } => { + let arr = self.compile_expr(builder, module, target)?; + let idx = self.compile_expr(builder, module, index)?; + self.call_named(builder, module, "track_array_set", &[arr, idx, val]); + } + _ => {} } Some(val) } + Expr::BinaryOp { + op: op @ (BinOp::And | BinOp::Or), + left, + right, + } => { + // Short-circuit evaluation; result is a normalised 0/1 word. + let lhs = self.compile_expr(builder, module, left)?; + let zero = self.zero(builder); + let lhs_bool = self.bool_word(builder, IntCC::NotEqual, lhs, zero); + let rhs_block = builder.create_block(); + let merge = builder.create_block(); + let result = builder.append_block_param(merge, W); + if *op == BinOp::And { + builder + .ins() + .brif(lhs_bool, rhs_block, &[], merge, &[lhs_bool]); + } else { + builder + .ins() + .brif(lhs_bool, merge, &[lhs_bool], rhs_block, &[]); + } + self.filled = true; + self.switch_to(builder, rhs_block); + let rhs = self + .compile_expr(builder, module, right) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if !self.filled { + let zero = self.zero(builder); + let rhs_bool = self.bool_word(builder, IntCC::NotEqual, rhs, zero); + self.jump(builder, merge, &[rhs_bool]); + } + self.switch_to(builder, merge); + Some(result) + } + Expr::BinaryOp { op, left, right } => { let lhs = self.compile_expr(builder, module, left)?; let rhs = self.compile_expr(builder, module, right)?; + let lhs = self.word(builder, lhs); + let rhs = self.word(builder, rhs); let res = match op { BinOp::Add => builder.ins().iadd(lhs, rhs), @@ -482,46 +837,33 @@ impl<'a> FnContext<'a> { BinOp::Mul => builder.ins().imul(lhs, rhs), BinOp::Div => builder.ins().sdiv(lhs, rhs), BinOp::Mod => builder.ins().srem(lhs, rhs), - BinOp::Eq => builder.ins().icmp(ir::condcodes::IntCC::Equal, lhs, rhs), - BinOp::Neq => builder.ins().icmp(ir::condcodes::IntCC::NotEqual, lhs, rhs), - BinOp::Lt => builder - .ins() - .icmp(ir::condcodes::IntCC::SignedLessThan, lhs, rhs), - BinOp::Gt => { - builder - .ins() - .icmp(ir::condcodes::IntCC::SignedGreaterThan, lhs, rhs) - } - BinOp::Lte => { - builder - .ins() - .icmp(ir::condcodes::IntCC::SignedLessThanOrEqual, lhs, rhs) - } + BinOp::Eq => self.bool_word(builder, IntCC::Equal, lhs, rhs), + BinOp::Neq => self.bool_word(builder, IntCC::NotEqual, lhs, rhs), + BinOp::Lt => self.bool_word(builder, IntCC::SignedLessThan, lhs, rhs), + BinOp::Gt => self.bool_word(builder, IntCC::SignedGreaterThan, lhs, rhs), + BinOp::Lte => self.bool_word(builder, IntCC::SignedLessThanOrEqual, lhs, rhs), BinOp::Gte => { - builder - .ins() - .icmp(ir::condcodes::IntCC::SignedGreaterThanOrEqual, lhs, rhs) + self.bool_word(builder, IntCC::SignedGreaterThanOrEqual, lhs, rhs) } - BinOp::And => builder.ins().band(lhs, rhs), - BinOp::Or => builder.ins().bor(lhs, rhs), BinOp::BitAnd => builder.ins().band(lhs, rhs), BinOp::BitOr => builder.ins().bor(lhs, rhs), BinOp::Shl => builder.ins().ishl(lhs, rhs), BinOp::Shr => builder.ins().ushr(lhs, rhs), + BinOp::And | BinOp::Or => unreachable!(), }; Some(res) } Expr::UnaryOp { op, expr } => { let val = self.compile_expr(builder, module, expr)?; + let val = self.word(builder, val); match op { UnaryOp::Neg => Some(builder.ins().ineg(val)), // Logical NOT: (val == 0). Bitwise bnot would turn 1 into -2, // which is still truthy and inverts branch semantics. UnaryOp::Not => { - let ty = builder.func.dfg.value_type(val); - let zero = builder.ins().iconst(ty, 0); - Some(builder.ins().icmp(ir::condcodes::IntCC::Equal, val, zero)) + let zero = self.zero(builder); + Some(self.bool_word(builder, IntCC::Equal, val, zero)) } UnaryOp::Deref => Some(val), } @@ -534,92 +876,45 @@ impl<'a> FnContext<'a> { for arg in args { let v = self .compile_expr(builder, module, arg) - .unwrap_or_else(|| builder.ins().iconst(ir::types::I64, 0)); + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; + } arg_vals.push(v); } let clean_name = name.trim_start_matches('@'); - let mut target_name = if clean_name.contains("::") { - clean_name.split("::").last().unwrap() - } else { - clean_name - }; - if let Some(&disc) = self - .variant_map - .get(name) - .or_else(|| self.variant_map.get(target_name)) + // Variant constructors: `Union::Variant(payload...)`. + if clean_name.contains("::") + && let Some(disc) = self.variant_key(clean_name) { - return arg_vals - .first() - .copied() - .or_else(|| Some(builder.ins().iconst(ir::types::I64, disc))); + let tag = builder.ins().iconst(W, disc); + if self.is_payload_variant(clean_name) { + let mut words = vec![tag]; + words.extend(arg_vals.iter().map(|&v| self.word(builder, v))); + return Some(self.alloc_words(builder, module, &words)); + } + return Some(tag); } + let mut target_name = if clean_name.contains("::") { + clean_name.rsplit("::").next().unwrap() + } else { + clean_name + }; // Map Track-level builtin names to their C runtime symbols. if target_name == "abort" { target_name = "track_abort"; } - - let func_id = if let Some(&fid) = self.functions.get(name) { - fid - } else if let Some(&fid) = self.functions.get(clean_name) { - fid - } else if let Some(&fid) = self.functions.get(target_name) { - fid + let symbol = if self.functions.contains_key(name) { + name.as_str() + } else if self.functions.contains_key(clean_name) { + clean_name } else { - let mut sig = module.make_signature(); - for arg_val in &arg_vals { - let ty = builder.func.dfg.value_type(*arg_val); - sig.params.push(AbiParam::new(ty)); - } - sig.returns.push(AbiParam::new(ir::types::I64)); - let fid = module - .declare_function(target_name, Linkage::Import, &sig) - .unwrap_or_else(|_| module.declare_anonymous_function(&sig).unwrap()); - self.functions.insert(name.clone(), fid); - fid + target_name }; - - let local_func = module.declare_func_in_func(func_id, builder.func); - let sig_ref = builder.func.dfg.ext_funcs[local_func].signature; - - let mut fixed_args = Vec::new(); - for (idx, &arg_val) in arg_vals.iter().enumerate() { - let expected_ty_opt = builder.func.dfg.signatures[sig_ref] - .params - .get(idx) - .map(|p| p.value_type); - if let Some(expected_ty) = expected_ty_opt { - let actual_ty = builder.func.dfg.value_type(arg_val); - if actual_ty != expected_ty { - if actual_ty.bytes() > expected_ty.bytes() { - fixed_args.push(builder.ins().ireduce(expected_ty, arg_val)); - } else if actual_ty.bytes() < expected_ty.bytes() { - fixed_args.push(builder.ins().uextend(expected_ty, arg_val)); - } else { - fixed_args.push(arg_val); - } - } else { - fixed_args.push(arg_val); - } - } else { - fixed_args.push(arg_val); - } - } - - let call_inst = builder.ins().call(local_func, &fixed_args); - let results = builder.inst_results(call_inst); - if let Some(&res) = results.first() { - let res_ty = builder.func.dfg.value_type(res); - if res_ty == ir::types::I32 { - Some(builder.ins().uextend(ir::types::I64, res)) - } else { - Some(res) - } - } else { - None - } + self.call_named(builder, module, symbol, &arg_vals) } Expr::IfElse { @@ -628,42 +923,40 @@ impl<'a> FnContext<'a> { else_body, } => { let cond_val = self.compile_expr(builder, module, condition)?; + let cond_val = self.word(builder, cond_val); let then_block = builder.create_block(); let else_block = builder.create_block(); let merge_block = builder.create_block(); + let result = builder.append_block_param(merge_block, W); builder .ins() .brif(cond_val, then_block, &[], else_block, &[]); - - // Compile then block - builder.switch_to_block(then_block); - let mut then_val = None; - let then_returns = then_body.iter().any(|s| matches!(s, Expr::Return { .. })); - for stmt in then_body { - then_val = self.compile_expr(builder, module, stmt); - } - if !then_returns { - builder.ins().jump(merge_block, &[]); + self.filled = true; + + self.switch_to(builder, then_block); + let then_val = self.compile_block(builder, module, then_body); + if !self.filled { + let v = match then_val { + Some(v) => self.word(builder, v), + None => self.zero(builder), + }; + self.jump(builder, merge_block, &[v]); } - // Compile else block - builder.switch_to_block(else_block); - let mut else_val = None; - let else_returns = else_body.iter().any(|s| matches!(s, Expr::Return { .. })); - for stmt in else_body { - else_val = self.compile_expr(builder, module, stmt); - } - if !else_returns { - builder.ins().jump(merge_block, &[]); + self.switch_to(builder, else_block); + let else_val = self.compile_block(builder, module, else_body); + if !self.filled { + let v = match else_val { + Some(v) => self.word(builder, v), + None => self.zero(builder), + }; + self.jump(builder, merge_block, &[v]); } - // Merge block - builder.switch_to_block(merge_block); - builder.seal_block(merge_block); - - then_val.or(else_val) + self.switch_to(builder, merge_block); + Some(result) } Expr::WhileLoop { condition, body } => { @@ -671,29 +964,25 @@ impl<'a> FnContext<'a> { let body_block = builder.create_block(); let exit_block = builder.create_block(); - builder.ins().jump(header_block, &[]); - - // Header block - builder.switch_to_block(header_block); - let cond_val = self.compile_expr(builder, module, condition)?; - builder - .ins() - .brif(cond_val, body_block, &[], exit_block, &[]); + self.jump(builder, header_block, &[]); - // Body block - builder.switch_to_block(body_block); - builder.seal_block(body_block); - for stmt in body { - self.compile_expr(builder, module, stmt); + self.switch_to(builder, header_block); + let cond_val = self + .compile_expr(builder, module, condition) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if !self.filled { + let cond_val = self.word(builder, cond_val); + builder + .ins() + .brif(cond_val, body_block, &[], exit_block, &[]); + self.filled = true; } - builder.ins().jump(header_block, &[]); - - builder.seal_block(header_block); - // Exit block - builder.switch_to_block(exit_block); - builder.seal_block(exit_block); + self.switch_to(builder, body_block); + self.compile_block(builder, module, body); + self.jump(builder, header_block, &[]); + self.switch_to(builder, exit_block); None } @@ -702,37 +991,29 @@ impl<'a> FnContext<'a> { Expr::Range { start, end } => { let s = self .compile_expr(builder, module, start) - .unwrap_or_else(|| builder.ins().iconst(ir::types::I32, 0)); + .unwrap_or_else(|| builder.ins().iconst(W, 0)); let e = self .compile_expr(builder, module, end) - .unwrap_or_else(|| builder.ins().iconst(ir::types::I32, 0)); + .unwrap_or_else(|| builder.ins().iconst(W, 0)); (s, e) } _ => { - let s = builder.ins().iconst(ir::types::I32, 0); + let s = builder.ins().iconst(W, 0); let e = self .compile_expr(builder, module, iter) - .unwrap_or_else(|| builder.ins().iconst(ir::types::I32, 0)); + .unwrap_or_else(|| builder.ins().iconst(W, 0)); (s, e) } }; + if self.filled { + return None; + } + let start_val = self.word(builder, start_val); + let end_val = self.word(builder, end_val); - let loop_ty = builder.func.dfg.value_type(start_val); - let end_ty = builder.func.dfg.value_type(end_val); - let fixed_end_val = if end_ty != loop_ty { - if end_ty.bytes() > loop_ty.bytes() { - builder.ins().ireduce(loop_ty, end_val) - } else if end_ty.bytes() < loop_ty.bytes() { - builder.ins().uextend(loop_ty, end_val) - } else { - end_val - } - } else { - end_val - }; - + let loop_scope = self.enter_scope(); let loop_var = self.new_var(); - builder.declare_var(loop_var, loop_ty); + builder.declare_var(loop_var, W); builder.def_var(loop_var, start_val); self.var_map.insert(var.clone(), loop_var); @@ -740,93 +1021,83 @@ impl<'a> FnContext<'a> { let body_block = builder.create_block(); let exit_block = builder.create_block(); - builder.ins().jump(header_block, &[]); + self.jump(builder, header_block, &[]); - builder.switch_to_block(header_block); + self.switch_to(builder, header_block); let cur_val = builder.use_var(loop_var); - let cond_val = builder.ins().icmp( - ir::condcodes::IntCC::SignedLessThan, - cur_val, - fixed_end_val, - ); + let cond_val = builder.ins().icmp(IntCC::SignedLessThan, cur_val, end_val); builder .ins() .brif(cond_val, body_block, &[], exit_block, &[]); - - builder.switch_to_block(body_block); - builder.seal_block(body_block); - for stmt in body { - self.compile_expr(builder, module, stmt); + self.filled = true; + + self.switch_to(builder, body_block); + self.compile_block(builder, module, body); + if !self.filled { + let next_val = builder.ins().iadd_imm(cur_val, 1); + builder.def_var(loop_var, next_val); + self.jump(builder, header_block, &[]); } - let next_val = builder.ins().iadd_imm(cur_val, 1); - builder.def_var(loop_var, next_val); - builder.ins().jump(header_block, &[]); - - builder.seal_block(header_block); - builder.switch_to_block(exit_block); - builder.seal_block(exit_block); + self.switch_to(builder, exit_block); + self.exit_scope(loop_scope); None } Expr::Return { value } => { - let ret_ty = builder.func.signature.returns.first().map(|p| p.value_type); - if let Some(v_expr) = value { - // Returning a tuple must heap-allocate (stack slot would dangle). - let v_opt = if let Expr::TupleLiteral { elements } = v_expr.as_ref() { - let elem_count = elements.len(); - let size_val = builder - .ins() - .iconst(ir::types::I64, (elem_count * 8) as i64); - let alloc_fid = if let Some(&fid) = self.functions.get("alloc") { - fid - } else { - let mut sig = module.make_signature(); - sig.params.push(AbiParam::new(ir::types::I64)); - sig.returns.push(AbiParam::new(ir::types::I64)); - let fid = module - .declare_function("alloc", Linkage::Import, &sig) - .unwrap(); - self.functions.insert("alloc".to_string(), fid); - fid - }; - let local_alloc = module.declare_func_in_func(alloc_fid, builder.func); - let call_inst = builder.ins().call(local_alloc, &[size_val]); - let ptr = builder.inst_results(call_inst)[0]; - for (i, elem) in elements.iter().enumerate() { - if let Some(val) = self.compile_expr(builder, module, elem) { - let offset = (i * 8) as i32; - builder.ins().store(ir::MemFlags::new(), val, ptr, offset); - } - } - Some(ptr) - } else { - self.compile_expr(builder, module, v_expr) - }; - if let Some(mut v) = v_opt { - if let Some(expected_ty) = ret_ty { - let actual_ty = builder.func.dfg.value_type(v); - if actual_ty != expected_ty { - if actual_ty.bytes() > expected_ty.bytes() { - v = builder.ins().ireduce(expected_ty, v); - } else if actual_ty.bytes() < expected_ty.bytes() { - v = builder.ins().uextend(expected_ty, v); - } + let v = match value.as_deref() { + // Returning a tuple literal must heap-allocate (a stack slot would dangle). + Some(Expr::TupleLiteral { elements }) => { + let mut words = Vec::new(); + for elem in elements { + let v = self + .compile_expr(builder, module, elem) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; } + words.push(self.word(builder, v)); } - builder.ins().return_(&[v]); - } else if let Some(ty) = ret_ty { - let default_v = builder.ins().iconst(ty, 0); - builder.ins().return_(&[default_v]); - } else { - builder.ins().return_(&[]); + Some(self.alloc_words(builder, module, &words)) + } + Some(Expr::Variable(name)) + if self.stack_tuples.contains_key(name) + || self.tuple_params.contains_key(name) => + { + let count = self + .stack_tuples + .get(name) + .or_else(|| self.tuple_params.get(name)) + .copied() + .unwrap(); + let src = builder.use_var(self.var_map[name.as_str()]); + Some(self.copy_words_to_heap(builder, module, src, count)) } - } else if let Some(ty) = ret_ty { - let default_v = builder.ins().iconst(ty, 0); - builder.ins().return_(&[default_v]); + Some(v_expr) => self.compile_expr(builder, module, v_expr), + None => None, + }; + if self.filled { + return None; + } + if self.is_main { + let code = match v { + Some(v) => { + let v = self.word(builder, v); + builder.ins().ireduce(ir::types::I32, v) + } + None => builder.ins().iconst(ir::types::I32, 0), + }; + builder.ins().return_(&[code]); + } else if self.returns_value { + let v = match v { + Some(v) => self.word(builder, v), + None => self.zero(builder), + }; + builder.ins().return_(&[v]); } else { builder.ins().return_(&[]); } + self.filled = true; None } @@ -835,32 +1106,37 @@ impl<'a> FnContext<'a> { lens_name, body, } => { + let lens_scope = self.enter_scope(); if let Some(&var) = self.var_map.get(target) { self.var_map.insert(lens_name.clone(), var); } - let mut last = None; - for stmt in body { - last = self.compile_expr(builder, module, stmt); - } - last + let value = self.compile_block(builder, module, body); + self.exit_scope(lens_scope); + value } Expr::TupleLiteral { elements } => { // Fast, stack-allocated tuples for non-escaping locals. - // Heap allocation is used only when the tuple is directly - // returned (escaping the stack frame) — handled in the - // Return arm below. - let elem_count = elements.len(); + // Escaping tuples are copied to the heap by `Return`. + let mut words = Vec::new(); + for elem in elements { + let v = self + .compile_expr(builder, module, elem) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; + } + words.push(self.word(builder, v)); + } let slot = builder.create_sized_stack_slot(StackSlotData::new( StackSlotKind::ExplicitSlot, - (elem_count * 8) as u32, + (elements.len().max(1) * 8) as u32, )); - let ptr = builder.ins().stack_addr(ir::types::I64, slot, 0); - for (i, elem) in elements.iter().enumerate() { - if let Some(val) = self.compile_expr(builder, module, elem) { - let offset = (i * 8) as i32; - builder.ins().store(ir::MemFlags::new(), val, ptr, offset); - } + let ptr = builder.ins().stack_addr(W, slot, 0); + for (i, val) in words.into_iter().enumerate() { + builder + .ins() + .store(ir::MemFlags::new(), val, ptr, (i * 8) as i32); } Some(ptr) } @@ -868,10 +1144,7 @@ impl<'a> FnContext<'a> { Expr::TupleIndex { target, index } => { let ptr = self.compile_expr(builder, module, target)?; let offset = (*index * 8) as i32; - let val = builder - .ins() - .load(ir::types::I64, ir::MemFlags::new(), ptr, offset); - Some(val) + Some(builder.ins().load(W, ir::MemFlags::new(), ptr, offset)) } Expr::LetDestructure { pattern, value, .. } => { @@ -880,25 +1153,37 @@ impl<'a> FnContext<'a> { && let Pattern::Tuple(pats) = pattern && pats.len() == elements.len() { - for (pat, elem) in pats.iter().zip(elements.iter()) { - if let Some(val) = self.compile_expr(builder, module, elem) { - self.bind_pattern_codegen(builder, module, pat, val); + let mut vals = Vec::new(); + for elem in elements { + let v = self + .compile_expr(builder, module, elem) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; } + vals.push(v); + } + for (pat, val) in pats.iter().zip(vals) { + self.bind_pattern(builder, pat, val); } return None; } - if let Some(val) = self.compile_expr(builder, module, value) { - self.bind_pattern_codegen(builder, module, pattern, val); + if let Some(val) = self.compile_expr(builder, module, value) + && !self.filled + { + self.bind_pattern(builder, pattern, val); } None } Expr::Match { target, arms } => { let disc = self.compile_expr(builder, module, target)?; + let disc = self.word(builder, disc); let merge_block = builder.create_block(); - let mut last_val = None; + let result = builder.append_block_param(merge_block, W); for arm in arms { + let arm_scope = self.enter_scope(); let arm_block = builder.create_block(); let next_arm_block = builder.create_block(); @@ -910,49 +1195,107 @@ impl<'a> FnContext<'a> { builder .ins() .brif(matched, guard_block, &[], next_arm_block, &[]); + self.filled = true; + self.switch_to(builder, guard_block); - builder.switch_to_block(guard_block); - builder.seal_block(guard_block); - - self.bind_pattern_codegen(builder, module, &arm.pattern, disc); - - if let Some(guard_val) = self.compile_expr(builder, module, guard) { - matched = guard_val; - } else { - matched = builder.ins().iconst(ir::types::I8, 1); - } + self.bind_pattern(builder, &arm.pattern, disc); + matched = match self.compile_expr(builder, module, guard) { + Some(g) => self.word(builder, g), + None => builder.ins().iconst(W, 1), + }; } else { - self.bind_pattern_codegen(builder, module, &arm.pattern, disc); + self.bind_pattern(builder, &arm.pattern, disc); } builder .ins() .brif(matched, arm_block, &[], next_arm_block, &[]); + self.filled = true; + + self.switch_to(builder, arm_block); + let arm_val = self.compile_expr(builder, module, &arm.body); + if !self.filled { + let v = match arm_val { + Some(v) => self.word(builder, v), + None => self.zero(builder), + }; + self.jump(builder, merge_block, &[v]); + } - builder.switch_to_block(arm_block); - builder.seal_block(arm_block); - last_val = self.compile_expr(builder, module, &arm.body); - builder.ins().jump(merge_block, &[]); - - builder.switch_to_block(next_arm_block); - builder.seal_block(next_arm_block); + self.switch_to(builder, next_arm_block); + self.exit_scope(arm_scope); } - builder.ins().jump(merge_block, &[]); - builder.switch_to_block(merge_block); - builder.seal_block(merge_block); + let zero = self.zero(builder); + self.jump(builder, merge_block, &[zero]); + self.switch_to(builder, merge_block); + Some(result) + } - last_val + Expr::StructInitialization { ty_name, fields } => { + // Struct values are heap cells: [tag, fields in declaration order]. + let mut values = Vec::new(); + for (fname, fexpr) in fields { + let v = self + .compile_expr(builder, module, fexpr) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; + } + values.push((fname.clone(), self.word(builder, v))); + } + let order: Vec = match self.info.structs.get(ty_name) { + Some(decl) => decl.clone(), + None => fields.iter().map(|(f, _)| f.clone()).collect(), + }; + let mut words = vec![builder.ins().iconst(W, struct_tag(ty_name))]; + for fname in &order { + let v = match values.iter().find(|(f, _)| f == fname) { + Some((_, v)) => *v, + None => builder.ins().iconst(W, 0), + }; + words.push(v); + } + Some(self.alloc_words(builder, module, &words)) } - Expr::SliceIndex { target, start, .. } => { + Expr::FieldAccess { target, field } => { let ptr = self.compile_expr(builder, module, target)?; - if let Some(s) = start - && let Some(start_val) = self.compile_expr(builder, module, s) - { - return Some(builder.ins().iadd(ptr, start_val)); + Some(self.field_access(builder, module, ptr, field, None)) + } + + Expr::ArrayLiteral { elements } => { + // Arrays are heap cells: [length, elements...]. + let mut words = vec![builder.ins().iconst(W, elements.len() as i64)]; + for elem in elements { + let v = self + .compile_expr(builder, module, elem) + .unwrap_or_else(|| builder.ins().iconst(W, 0)); + if self.filled { + return None; + } + words.push(self.word(builder, v)); } - Some(ptr) + Some(self.alloc_words(builder, module, &words)) + } + + Expr::ArrayIndex { target, index } => { + let arr = self.compile_expr(builder, module, target)?; + let idx = self.compile_expr(builder, module, index)?; + self.call_named(builder, module, "track_array_get", &[arr, idx]) + } + + Expr::SliceIndex { target, start, end } => { + let arr = self.compile_expr(builder, module, target)?; + let from = match start { + Some(s) => self.compile_expr(builder, module, s)?, + None => builder.ins().iconst(W, 0), + }; + let to = match end { + Some(e) => self.compile_expr(builder, module, e)?, + None => builder.ins().iconst(W, -1), + }; + self.call_named(builder, module, "track_array_slice", &[arr, from, to]) } Expr::Range { start, end } => { @@ -965,44 +1308,69 @@ impl<'a> FnContext<'a> { } } - fn bind_pattern_codegen( - &mut self, - builder: &mut FunctionBuilder, - _module: &mut ObjectModule, - pattern: &Pattern, - val: Value, - ) { + fn track_tuple_binding(&mut self, name: &str, value: &Expr) { + if let Expr::TupleLiteral { elements } = value { + self.stack_tuples.insert(name.to_string(), elements.len()); + } else { + self.stack_tuples.remove(name); + } + self.tuple_params.remove(name); + } + + fn bind_pattern(&mut self, builder: &mut FunctionBuilder, pattern: &Pattern, val: Value) { match pattern { Pattern::Ident(name) => { let var = self.new_var(); - builder.declare_var(var, ir::types::I64); + builder.declare_var(var, W); builder.def_var(var, val); self.var_map.insert(name.clone(), var); + self.stack_tuples.remove(name); + self.tuple_params.remove(name); } Pattern::Wildcard | Pattern::Literal(_) => {} Pattern::Tuple(pats) => { for (i, p) in pats.iter().enumerate() { - let offset = (i * 8) as i32; - let elem_val = - builder - .ins() - .load(ir::types::I64, ir::MemFlags::new(), val, offset); - self.bind_pattern_codegen(builder, _module, p, elem_val); + let elem_val = builder + .ins() + .load(W, ir::MemFlags::new(), val, (i * 8) as i32); + self.bind_pattern(builder, p, elem_val); } } - Pattern::Variant { bindings, .. } => { - for p in bindings { - self.bind_pattern_codegen(builder, _module, p, val); + Pattern::Variant { + enum_or_union, + bindings, + .. + } => { + if self.info.payload_unions.contains(enum_or_union) { + for (i, p) in bindings.iter().enumerate() { + let payload = + builder + .ins() + .load(W, ir::MemFlags::new(), val, ((i + 1) * 8) as i32); + self.bind_pattern(builder, p, payload); + } + } else { + for p in bindings { + self.bind_pattern(builder, p, val); + } } } - Pattern::Struct { fields, .. } => { - for (_fname, p) in fields { - self.bind_pattern_codegen(builder, _module, p, val); + Pattern::Struct { name, fields } => { + let decl = self.info.structs.get(name).cloned().unwrap_or_default(); + for (fname, p) in fields { + if let Some(idx) = decl.iter().position(|f| f == fname) { + let field = + builder + .ins() + .load(W, ir::MemFlags::new(), val, ((idx + 1) * 8) as i32); + self.bind_pattern(builder, p, field); + } } } } } + /// Returns a 0/1 word that is non-zero when `pattern` matches `val`. fn compile_pattern_match( &mut self, builder: &mut FunctionBuilder, @@ -1011,52 +1379,69 @@ impl<'a> FnContext<'a> { val: Value, ) -> Value { match pattern { - Pattern::Ident(_) | Pattern::Wildcard => builder.ins().iconst(ir::types::I8, 1), + Pattern::Ident(_) | Pattern::Wildcard => builder.ins().iconst(W, 1), + Pattern::Struct { name, fields } => { + let Some(decl) = self.info.structs.get(name).cloned() else { + return builder.ins().iconst(W, 1); + }; + let tag = builder.ins().load(W, ir::MemFlags::new(), val, 0); + let want = builder.ins().iconst(W, struct_tag(name)); + let mut cond = self.bool_word(builder, IntCC::Equal, tag, want); + for (fname, p) in fields { + if let Some(idx) = decl.iter().position(|f| f == fname) { + let field = + builder + .ins() + .load(W, ir::MemFlags::new(), val, ((idx + 1) * 8) as i32); + let sub = self.compile_pattern_match(builder, module, p, field); + cond = builder.ins().band(cond, sub); + } + } + cond + } Pattern::Literal(lit_expr) => { - if let Some(lit_val) = self.compile_expr(builder, module, lit_expr) { - builder - .ins() - .icmp(ir::condcodes::IntCC::Equal, val, lit_val) - } else { - builder.ins().iconst(ir::types::I8, 1) + if let Expr::StringLiteral(_) = lit_expr.as_ref() { + let lit = self.compile_expr(builder, module, lit_expr).unwrap(); + let eq = self + .call_named(builder, module, "str_eq", &[val, lit]) + .unwrap(); + let zero = self.zero(builder); + return self.bool_word(builder, IntCC::NotEqual, eq, zero); + } + match self.compile_expr(builder, module, lit_expr) { + Some(lit_val) => { + let lit_val = self.word(builder, lit_val); + self.bool_word(builder, IntCC::Equal, val, lit_val) + } + None => builder.ins().iconst(W, 1), } } - Pattern::Variant { variant, .. } => { - let target_disc = self.variant_map.get(variant).copied().unwrap_or(0i64); - let expected = builder.ins().iconst(ir::types::I64, target_disc); - builder - .ins() - .icmp(ir::condcodes::IntCC::Equal, val, expected) + Pattern::Variant { + enum_or_union, + variant, + .. + } => { + let key = format!("{}::{}", enum_or_union, variant); + let target_disc = self.variant_key(&key).unwrap_or(0); + let expected = builder.ins().iconst(W, target_disc); + let tag = if self.info.payload_unions.contains(enum_or_union) { + builder.ins().load(W, ir::MemFlags::new(), val, 0) + } else { + val + }; + self.bool_word(builder, IntCC::Equal, tag, expected) } Pattern::Tuple(pats) => { - let mut cond = builder.ins().iconst(ir::types::I8, 1); + let mut cond = builder.ins().iconst(W, 1); for (i, p) in pats.iter().enumerate() { - let offset = (i * 8) as i32; - let elem_val = - builder - .ins() - .load(ir::types::I64, ir::MemFlags::new(), val, offset); + let elem_val = builder + .ins() + .load(W, ir::MemFlags::new(), val, (i * 8) as i32); let elem_cond = self.compile_pattern_match(builder, module, p, elem_val); cond = builder.ins().band(cond, elem_cond); } cond } - Pattern::Struct { .. } => builder.ins().iconst(ir::types::I8, 1), - } - } -} - -fn track_type_to_cl(ty: &TrackType) -> ir::Type { - match ty { - TrackType::U8 | TrackType::I8 => ir::types::I8, - TrackType::I32 | TrackType::U32 => ir::types::I32, - TrackType::I64 | TrackType::U64 => ir::types::I64, - TrackType::Bool => ir::types::I8, - TrackType::Void => ir::types::I32, - TrackType::Ptr(_) | TrackType::Ref(_) | TrackType::Slice(_) | TrackType::Tuple(_) => { - ir::types::I64 } - TrackType::Array(_, _) => ir::types::I64, - TrackType::Custom(_) => ir::types::I64, } } diff --git a/src/lib.rs b/src/lib.rs index e5fcb44..c715775 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,673 +11,19 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process; -pub const RUNTIME_C_SOURCE: &str = r#" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Math Extensions -double math_abs(double x) { return fabs(x); } -double math_min(double a, double b) { return a < b ? a : b; } -double math_max(double a, double b) { return a > b ? a : b; } -double math_pow(double base, double exp) { return pow(base, exp); } -double math_sqrt(double x) { return sqrt(x); } -double math_floor(double x) { return floor(x); } -double math_ceil(double x) { return ceil(x); } -double math_round(double x) { return round(x); } - -void print_err(const char* s) { - if (s) { - fprintf(stderr, "%s\n", s); - fflush(stderr); - } -} -void eprint(const char* s) { - print_err(s); -} -long long str_contains(const char* s, const char* sub_str) { - if (!s || !sub_str) return 0; - return strstr(s, sub_str) != NULL ? 1 : 0; -} - -static size_t g_allocated_bytes = 0; -static size_t g_max_memory_limit = 536870912; // 512MB default memory boundary limit - -void sys_set_memory_limit(long long bytes) { - if (bytes > 0) { - g_max_memory_limit = (size_t)bytes; - } -} - -long long sys_get_memory_used(void) { - return (long long)g_allocated_bytes; -} - -void* alloc(size_t size) { - if (size == 0) return NULL; - if (g_allocated_bytes + size > g_max_memory_limit) { - fprintf(stderr, "\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\n", g_allocated_bytes, size, g_max_memory_limit); - fflush(stderr); - exit(137); - } - size_t total = size + sizeof(size_t); - size_t* ptr = (size_t*)malloc(total); - if (!ptr) { - fprintf(stderr, "\nTrack Runtime Error: Out of memory allocation failure!\n"); - fflush(stderr); - exit(137); - } - *ptr = size; - g_allocated_bytes += size; - return (void*)(ptr + 1); -} - -void dealloc(void* ptr) { - if (!ptr) return; - size_t* raw = (size_t*)ptr - 1; - size_t size = *raw; - if (g_allocated_bytes >= size) { - g_allocated_bytes -= size; - } else { - g_allocated_bytes = 0; - } - free(raw); -} - -typedef struct { int* data; int len; int cap; } Vec; -Vec vec_init(int cap) { - Vec v; - size_t alloc_cap = cap > 0 ? (size_t)cap : 16; - v.data = (int*)alloc(alloc_cap * sizeof(int)); - v.len = 0; - v.cap = (int)alloc_cap; - return v; -} -void vec_push(Vec* v, int val) { - if (v && v->data && v->len < v->cap) { - v->data[v->len++] = val; - } -} -int vec_get(const Vec* v, int idx) { - if (v && v->data && idx >= 0 && idx < v->len) { - return v->data[idx]; - } - return 0; -} -void vec_set(Vec* v, int idx, int val) { - if (v && v->data && idx >= 0 && idx < v->len) { - v->data[idx] = val; - } -} -int vec_pop(Vec* v) { - if (v && v->len > 0) { - return v->data[--v->len]; - } - return 0; -} -void vec_free(Vec v) { - if (v.data) { - dealloc(v.data); - } -} -typedef struct { char* data; int len; } Str; -void str_free(Str s) { - if (s.data) { - free(s.data); - } -} -Str str_from_literal(const char* raw) { - Str s; - s.data = NULL; - s.len = 0; - if (!raw) return s; - s.len = (int)strlen(raw); - s.data = (char*)malloc((size_t)s.len + 1); - if (!s.data) return s; - memcpy(s.data, raw, (size_t)s.len + 1); - return s; -} -Str str_concat(const char* a, const char* b) { - Str s; - s.data = NULL; - s.len = 0; - size_t a_len = a ? strlen(a) : 0; - size_t b_len = b ? strlen(b) : 0; - if (a_len > SIZE_MAX - b_len - 1) return s; - size_t len = a_len + b_len; - s.data = (char*)malloc(len + 1); - if (!s.data) return s; - if (a_len > 0) memcpy(s.data, a, a_len); - if (b_len > 0) memcpy(s.data + a_len, b, b_len); - s.data[len] = '\0'; - s.len = (int)len; - return s; -} -void* file_open(const char* path, const char* mode) { - return (void*)fopen(path, mode); -} -void file_close(void* f) { - if (f) { - fclose((FILE*)f); - } -} -Str file_read_all(void* handle) { - Str s; - s.data = NULL; - s.len = 0; - FILE* f = (FILE*)handle; - if (!f) return s; - if (fseek(f, 0, SEEK_END) != 0) return s; - long end = ftell(f); - if (end < 0 || fseek(f, 0, SEEK_SET) != 0) return s; - size_t len = (size_t)end; - s.data = (char*)malloc(len + 1); - if (!s.data) return s; - size_t read = fread(s.data, 1, len, f); - if (read < len && ferror(f)) { - free(s.data); - s.data = NULL; - return s; - } - s.data[read] = '\0'; - s.len = (int)read; - return s; -} -int file_write(void* handle, const char* content) { - FILE* f = (FILE*)handle; - if (!f || !content) return -1; - size_t len = strlen(content); - return fwrite(content, 1, len, f) == len ? 0 : -1; -} -int file_exists(const char* path) { - return access(path, F_OK) == 0 ? 1 : 0; -} -long long clock_ms(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (long long)ts.tv_sec * 1000LL + (long long)ts.tv_nsec / 1000000LL; -} -void sys_exit(int code) { - exit(code); -} -static int is_valid_string_pointer(const void* ptr) { - if (!ptr || (uintptr_t)ptr < 0x10000) return 0; - int pfd[2]; - if (pipe(pfd) < 0) return 0; - ssize_t res = write(pfd[1], ptr, 1); - close(pfd[0]); - close(pfd[1]); - return res == 1; -} - -// Fatal abort: print message and terminate the process. -// No unwinding — stack frames are simply discarded (linear cleanup is skipped by design). -void track_abort(const char* msg) { - if (msg && is_valid_string_pointer(msg)) { - fprintf(stderr, "Track abort: %s\n", msg); - } else { - fprintf(stderr, "Track abort\n"); - } - fflush(stderr); - exit(134); -} - -void print(long long val) { - if (val >= 0x10000 && is_valid_string_pointer((const void*)(uintptr_t)val)) { - const char* str = (const char*)(uintptr_t)val; - int is_str = 1; - int len = 0; - while (len < 4096) { - unsigned char c = (unsigned char)str[len]; - if (c == 0) break; - if (c < 9 || (c > 13 && c < 32) || c > 126) { - is_str = 0; - break; - } - len++; - } - if (is_str && len > 0) { - printf("%s\n", str); - return; - } - } - printf("%lld\n", val); -} -long long add(long long a, long long b) { - return a + b; -} -long long sum(long long a, long long b) { - return a + b; -} -long long sub(long long a, long long b) { - return a - b; -} -long long str_find(const char* s, const char* sub_str) { - if (!s || !sub_str) return -1; - const char* p = strstr(s, sub_str); - return p ? (long long)(p - s) : -1; -} -long long str_to_int(const char* s) { - if (!s) return 0; - return atoll(s); -} -// Predicate companion for str_to_int: distinguishes a genuine "0" from a parse failure. -int str_is_int(const char* s) { - if (!s || *s == '\0') return 0; - const char* p = s; - if (*p == '+' || *p == '-') p++; - if (*p == '\0') return 0; - while (*p) { - if (!isdigit((unsigned char)*p)) return 0; - p++; - } - return 1; -} -Str int_to_str(long long val) { - char buf[64]; - snprintf(buf, sizeof(buf), "%lld", val); - Str s; - s.len = (int)strlen(buf); - s.data = (char*)malloc(s.len + 1); - memcpy(s.data, buf, s.len + 1); - return s; -} -int file_remove(const char* path) { - if (!path) return -1; - return unlink(path) == 0 ? 0 : -1; -} -long long file_size(const char* path) { - if (!path) return -1; - FILE* f = fopen(path, "rb"); - if (!f) return -1; - fseek(f, 0, SEEK_END); - long long sz = ftell(f); - fclose(f); - return sz; -} -long long math_clamp(long long val, long long min_val, long long max_val) { - if (val < min_val) return min_val; - if (val > max_val) return max_val; - return val; -} -static unsigned long long g_rng_state = 0x853c49e65d8dbb29ULL; -unsigned long long math_random(void) { - unsigned long long x = g_rng_state; - x ^= x << 13; - x ^= x >> 7; - x ^= x << 17; - g_rng_state = x; - return x; -} -int sys_exec(const char* cmd) { - if (!cmd) return -1; - return system(cmd); -} -Str env_get(const char* key) { - Str s; - s.data = NULL; - s.len = 0; - if (!key) return s; - const char* val = getenv(key); - if (val) { - s.len = (int)strlen(val); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, val, (size_t)s.len + 1); - } - return s; -} -int env_exists(const char* key) { - if (!key) return 0; - const char* val = getenv(key); - return val != NULL ? 1 : 0; -} -int net_socket_tcp_listen(int port) { - int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) return -1; - int opt = 1; - setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons((uint16_t)port); - if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - if (errno == EADDRINUSE) { - fprintf(stderr, "Track Network Error: Port conflict! Port %d is already in use by another process.\n", port); - } else { - fprintf(stderr, "Track Network Error: Failed to bind TCP socket on port %d (errno: %d).\n", port, errno); - } - fflush(stderr); - close(fd); - return -1; - } - if (listen(fd, 128) < 0) { - close(fd); - return -1; - } - return fd; -} -int net_socket_accept(int server_fd) { - if (server_fd < 0) return -1; - struct sockaddr_in client_addr; - socklen_t addrlen = sizeof(client_addr); - return accept(server_fd, (struct sockaddr*)&client_addr, &addrlen); -} -int net_socket_connect(const char* host, int port) { - if (!host) return -1; - int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) return -1; - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons((uint16_t)port); - if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) { - close(fd); - return -1; - } - if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - close(fd); - return -1; - } - return fd; -} -long long net_socket_send(int fd, const void* data, size_t len) { - if (fd < 0 || !data) return -1; - return (long long)send(fd, data, len, 0); -} -long long net_socket_recv(int fd, void* buf, size_t max_len) { - if (fd < 0 || !buf) return -1; - return (long long)recv(fd, buf, max_len, 0); -} -void net_socket_close(int fd) { - if (fd >= 0) close(fd); -} -static int g_argc = 0; -static char** g_argv = NULL; -int os_args_count(void) { - return g_argc; -} -Str os_arg(int idx) { - Str s; - s.data = NULL; - s.len = 0; - if (g_argv && idx >= 0 && idx < g_argc) { - const char* arg = g_argv[idx]; - s.len = (int)strlen(arg); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, arg, (size_t)s.len + 1); - } - return s; -} -int dir_exists(const char* path) { - if (!path) return 0; - struct stat st; - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - return 1; - } - return 0; -} -int file_copy(const char* src, const char* dst) { - if (!src || !dst) return -1; - FILE *in = fopen(src, "rb"); - if (!in) return -1; - FILE *out = fopen(dst, "wb"); - if (!out) { fclose(in); return -1; } - char buf[4096]; - size_t n; - while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { - fwrite(buf, 1, n, out); - } - fclose(in); - fclose(out); - return 0; -} -int process_spawn(const char* cmd) { - if (!cmd) return -1; - return system(cmd); -} - -// Char & Byte Operations -int char_is_digit(unsigned char c) { return isdigit(c) ? 1 : 0; } -int char_is_alpha(unsigned char c) { return isalpha(c) ? 1 : 0; } -int char_is_alphanumeric(unsigned char c) { return isalnum(c) ? 1 : 0; } -int char_is_space(unsigned char c) { return isspace(c) ? 1 : 0; } -unsigned char char_to_upper(unsigned char c) { return (unsigned char)toupper(c); } -unsigned char char_to_lower(unsigned char c) { return (unsigned char)tolower(c); } - -// String Extensions -int str_starts_with(const char* s, const char* prefix) { - if (!s || !prefix) return 0; - size_t len_s = strlen(s); - size_t len_p = strlen(prefix); - if (len_p > len_s) return 0; - return strncmp(s, prefix, len_p) == 0 ? 1 : 0; -} -int str_ends_with(const char* s, const char* suffix) { - if (!s || !suffix) return 0; - size_t len_s = strlen(s); - size_t len_p = strlen(suffix); - if (len_p > len_s) return 0; - return strcmp(s + len_s - len_p, suffix) == 0 ? 1 : 0; -} -Str str_substr(const char* s, long long start, long long len) { - Str res; - res.data = NULL; - res.len = 0; - if (!s || start < 0) return res; - size_t s_len = strlen(s); - if ((size_t)start >= s_len) return res; - size_t actual_len = (size_t)len; - if ((size_t)start + actual_len > s_len) { - actual_len = s_len - (size_t)start; - } - res.data = (char*)malloc(actual_len + 1); - memcpy(res.data, s + start, actual_len); - res.data[actual_len] = '\0'; - res.len = (int)actual_len; - return res; -} -Str str_trim(const char* s) { - Str res; - res.data = NULL; - res.len = 0; - if (!s) return res; - while (*s && isspace((unsigned char)*s)) s++; - if (*s == 0) { - res.data = (char*)calloc(1, 1); - return res; - } - const char* end = s + strlen(s) - 1; - while (end > s && isspace((unsigned char)*end)) end--; - size_t len = (size_t)(end - s + 1); - res.data = (char*)malloc(len + 1); - memcpy(res.data, s, len); - res.data[len] = '\0'; - res.len = (int)len; - return res; -} -unsigned char str_char_at(const char* s, long long idx) { - if (!s || idx < 0) return 0; - size_t len = strlen(s); - if ((size_t)idx >= len) return 0; - return (unsigned char)s[idx]; -} -unsigned int str_len(const char* s) { - if (!s) return 0; - return (unsigned int)strlen(s); -} -int str_eq(const char* a, const char* b) { - if (!a || !b) return a == b ? 1 : 0; - return strcmp(a, b) == 0 ? 1 : 0; -} - -// Memory & Vec Extensions -void* mem_realloc(void* ptr, size_t new_size) { - if (!ptr) return alloc(new_size); - if (new_size == 0) { - dealloc(ptr); - return NULL; - } - size_t* raw = (size_t*)ptr - 1; - size_t old_size = *raw; - if (g_allocated_bytes - old_size + new_size > g_max_memory_limit) { - fprintf(stderr, "\nTrack Runtime Error: Process memory boundary limit exceeded! (Allocated: %zu bytes, Requested: %zu bytes, Limit: %zu bytes)\n", g_allocated_bytes, new_size, g_max_memory_limit); - fflush(stderr); - exit(137); - } - size_t total = new_size + sizeof(size_t); - size_t* new_raw = (size_t*)realloc(raw, total); - if (!new_raw) { - fprintf(stderr, "\nTrack Runtime Error: Out of memory reallocation failure!\n"); - fflush(stderr); - exit(137); - } - *new_raw = new_size; - g_allocated_bytes = g_allocated_bytes - old_size + new_size; - return (void*)(new_raw + 1); -} -void vec_reserve(Vec* v, int cap) { - if (v && cap > v->cap) { - v->data = (int*)mem_realloc(v->data, (size_t)cap * sizeof(int)); - v->cap = cap; - } -} -void vec_clear(Vec* v) { - if (v) v->len = 0; -} -int vec_len(const Vec* v) { - return v ? v->len : 0; -} - -// Opaque 64-bit collection used by the self-hosted compiler. Track only -// carries the stable pointer handle; layout and capacity management stay in -// this bootstrap runtime until native generic collections are available. -typedef struct { - long long* data; - long long len; - long long cap; -} TrackI64Collection; - -void* track_i64_collection_new(long long requested_cap) { - long long cap = requested_cap > 0 ? requested_cap : 16; - if ((unsigned long long)cap > SIZE_MAX / sizeof(long long)) return NULL; - TrackI64Collection* collection = (TrackI64Collection*)alloc(sizeof(TrackI64Collection)); - if (!collection) return NULL; - collection->data = (long long*)alloc((size_t)cap * sizeof(long long)); - collection->len = 0; - collection->cap = cap; - return collection; -} - -long long track_i64_collection_len(const void* handle) { - const TrackI64Collection* collection = (const TrackI64Collection*)handle; - return collection ? collection->len : 0; -} - -long long track_i64_collection_push(void* handle, long long value) { - TrackI64Collection* collection = (TrackI64Collection*)handle; - if (!collection) return -1; - if (collection->len == collection->cap) { - if (collection->cap > (long long)(SIZE_MAX / sizeof(long long) / 2)) return -1; - long long next_cap = collection->cap * 2; - collection->data = (long long*)mem_realloc( - collection->data, - (size_t)next_cap * sizeof(long long) - ); - collection->cap = next_cap; - } - long long index = collection->len; - collection->data[index] = value; - collection->len += 1; - return index; -} - -long long track_i64_collection_get(const void* handle, long long index) { - const TrackI64Collection* collection = (const TrackI64Collection*)handle; - if (!collection || index < 0 || index >= collection->len) return 0; - return collection->data[index]; -} - -int track_i64_collection_set(void* handle, long long index, long long value) { - TrackI64Collection* collection = (TrackI64Collection*)handle; - if (!collection || index < 0 || index >= collection->len) return 0; - collection->data[index] = value; - return 1; -} - -// Path Extensions -Str path_basename(const char* path) { - Str s; - s.data = NULL; - s.len = 0; - if (!path) return s; - const char* last_slash = strrchr(path, '/'); - const char* name = last_slash ? last_slash + 1 : path; - s.len = (int)strlen(name); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, name, (size_t)s.len + 1); - return s; -} -Str path_ext(const char* path) { - Str s; - s.data = NULL; - s.len = 0; - if (!path) return s; - const char* dot = strrchr(path, '.'); - if (!dot || dot == path) return s; - s.len = (int)strlen(dot + 1); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, dot + 1, (size_t)s.len + 1); - return s; -} -Str path_join(const char* a, const char* b) { - Str s; - s.data = NULL; - s.len = 0; - if (!a && !b) return s; - if (!a) { - s.len = (int)strlen(b); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, b, (size_t)s.len + 1); - return s; - } - if (!b) { - s.len = (int)strlen(a); - s.data = (char*)malloc((size_t)s.len + 1); - memcpy(s.data, a, (size_t)s.len + 1); - return s; - } - size_t len_a = strlen(a); - size_t len_b = strlen(b); - int needs_slash = (len_a > 0 && a[len_a - 1] != '/' && (len_b == 0 || b[0] != '/')) ? 1 : 0; - size_t total = len_a + (size_t)needs_slash + len_b; - s.data = (char*)malloc(total + 1); - memcpy(s.data, a, len_a); - if (needs_slash) { - s.data[len_a] = '/'; - memcpy(s.data + len_a + 1, b, len_b); - } else { - memcpy(s.data + len_a, b, len_b); - } - s.data[total] = '\0'; - s.len = (int)total; - return s; -} -"#; +/// The Track runtime, shared with the self-hosted compiler (word ABI). +pub const RUNTIME_C_SOURCE: &str = include_str!("../runtime/track_runtime.c"); /// Compile source string through Lexer -> Parser -> Monomorphizer -> LinearChecker pipeline. pub fn compile_source(source: &str) -> Result, String> { + compile_source_with_info(source).map(|(program, _)| program) +} + +/// Like [`compile_source`], also returning the type layouts codegen needs +/// (enum discriminants, payload unions, aliases — including imports). +pub fn compile_source_with_info( + source: &str, +) -> Result<(Vec, codegen::TypeInfo), String> { let tokens = lexer::Lexer::tokenize(source)?; let mut p = parser::Parser::new(tokens, source.to_string()); let program = p.parse_program()?; @@ -685,7 +31,7 @@ pub fn compile_source(source: &str) -> Result, String> { mono::monomorphize(&mut program)?; let mut chk = checker::LinearChecker::new(); chk.check_program(&program)?; - Ok(program) + Ok((program, chk.type_info)) } /// Full build: source -> object file -> linked executable in specified directory. @@ -693,9 +39,10 @@ pub fn build_file_in_dir(filename: &str, out_dir: &Path) -> Result Compile to native executable"); eprintln!(" track run Compile and run"); diff --git a/src/mono.rs b/src/mono.rs index a6442b4..dce63fa 100644 --- a/src/mono.rs +++ b/src/mono.rs @@ -318,11 +318,34 @@ fn walk( } Expr::UnaryOp { expr, .. } | Expr::AddressOf { target: expr } - | Expr::ArrayIndex { target: expr, .. } - | Expr::SliceIndex { target: expr, .. } + | Expr::FieldAccess { target: expr, .. } | Expr::TupleIndex { target: expr, .. } => { walk(expr, env, templates, concrete_sigs, generated, in_progress)?; } + Expr::ArrayIndex { target, index } => { + walk( + target, + env, + templates, + concrete_sigs, + generated, + in_progress, + )?; + walk(index, env, templates, concrete_sigs, generated, in_progress)?; + } + Expr::SliceIndex { target, start, end } => { + walk( + target, + env, + templates, + concrete_sigs, + generated, + in_progress, + )?; + for bound in [start, end].into_iter().flatten() { + walk(bound, env, templates, concrete_sigs, generated, in_progress)?; + } + } Expr::BinaryOp { left, right, .. } => { walk(left, env, templates, concrete_sigs, generated, in_progress)?; walk(right, env, templates, concrete_sigs, generated, in_progress)?; diff --git a/src/parser/expr.rs b/src/parser/expr.rs index 20b6aeb..ed034f3 100644 --- a/src/parser/expr.rs +++ b/src/parser/expr.rs @@ -5,7 +5,9 @@ use crate::lexer::Token; impl Parser { pub fn parse_expr(&mut self) -> Result { let left = self.parse_or()?; - if self.peek() == Some(&Token::DotDot) { + // `a[s..]`: leave an open-ended `..` for the index parser. + let open_slice = matches!(self.tokens.get(self.pos + 1), Some((Token::RBracket, _))); + if self.peek() == Some(&Token::DotDot) && !open_slice { self.advance(); let right = self.parse_or()?; Ok(Expr::Range { @@ -218,6 +220,13 @@ impl Parser { } _ => { let method_name = self.expect_ident()?; + if self.peek() != Some(&Token::LParen) { + expr = Expr::FieldAccess { + target: Box::new(expr), + field: method_name, + }; + continue; + } self.expect(&Token::LParen)?; let mut args = vec![expr]; if self.peek() != Some(&Token::RParen) { diff --git a/src/parser/stmt.rs b/src/parser/stmt.rs index bd63a09..ba302fc 100644 --- a/src/parser/stmt.rs +++ b/src/parser/stmt.rs @@ -343,10 +343,7 @@ impl Parser { } } self.expect(&Token::RBrace)?; - Ok(Expr::TypeAlias { - name, - target: crate::ast::TrackType::Custom("Struct".to_string()), - }) + Ok(Expr::StructDef { name, fields }) } fn parse_macro_def(&mut self) -> Result { diff --git a/src/yard/builder.rs b/src/yard/builder.rs index 445b9e4..5d6a949 100644 --- a/src/yard/builder.rs +++ b/src/yard/builder.rs @@ -79,6 +79,11 @@ impl ParallelBuilder { let mut cache = BuildCache::load(&target_dir); + // An object depends on its own source, on declarations imported from + // other package modules (constants, enum layouts, aliases), and on + // the compiler itself, so the cache key covers all three. + let fingerprint = build_fingerprint(&trk_files)?; + // Prepare build tasks let mut tasks = Vec::new(); for trk_file in trk_files { @@ -105,14 +110,8 @@ impl ParallelBuilder { .map_err(|e| format!("Failed to create object directory: {}", e))?; } - let (hash, is_cached) = if !obj_path.exists() { - let hash = BuildCache::compute_hash(&source); - (hash, false) - } else { - let hash = BuildCache::compute_hash(&source); - let cached = cache.is_cached(&rel_path, &hash, &obj_path); - (hash, cached) - }; + let hash = BuildCache::compute_hash(&format!("{}\0{}", fingerprint, source)); + let is_cached = cache.is_cached(&rel_path, &hash, &obj_path); tasks.push(BuildTask { trk_file, @@ -189,6 +188,7 @@ impl ParallelBuilder { .unwrap_or("module"); let mut codegen = crate::codegen::CodeGen::new_with_isa(mod_name, target_isa.clone()); + codegen.set_type_info(checker.type_info.clone()); codegen.compile_program(&program); codegen.write_object_file(&task.obj_path)?; @@ -543,6 +543,30 @@ impl ParallelBuilder { } } +/// Hash of the compiler executable identity and every package source. +fn build_fingerprint(files: &[PathBuf]) -> Result { + let mut key = String::from(env!("CARGO_PKG_VERSION")); + if let Ok(exe) = std::env::current_exe() + && let Ok(meta) = fs::metadata(exe) + { + let modified = meta + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map_or(0, |d| d.as_nanos()); + key.push_str(&format!("\0{}\0{}", meta.len(), modified)); + } + for file in files { + let source = fs::read_to_string(file) + .map_err(|e| format!("Failed to read '{}': {}", file.display(), e))?; + key.push('\0'); + key.push_str(&file.to_string_lossy()); + key.push('\0'); + key.push_str(&BuildCache::compute_hash(&source)); + } + Ok(BuildCache::compute_hash(&key)) +} + fn copy_test_fixtures(source: &Path, destination: &Path) -> Result<(), String> { fs::create_dir_all(destination).map_err(|e| { format!( diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 0d39b44..1608166 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -63,8 +63,8 @@ fn test_cli_version_reports_0_6() { let stderr = String::from_utf8_lossy(&out.stderr); let combined = format!("{}{}", stdout, stderr); assert!( - combined.contains("0.7.0"), - "version output missing 0.7.0: {}", + combined.contains("1.0.0"), + "version output missing 1.0.0: {}", combined ); } diff --git a/tests/codegen_semantics_tests.rs b/tests/codegen_semantics_tests.rs new file mode 100644 index 0000000..71ce984 --- /dev/null +++ b/tests/codegen_semantics_tests.rs @@ -0,0 +1,291 @@ +// Runtime semantics of the Cranelift backend. Each case builds a program, +// runs it, and compares stdout. These cover miscompilations found while +// bootstrapping the self-hosted compiler. + +use std::env; +use std::fs; +use std::process::Command; +use track::build_file_in_dir; + +fn run(name: &str, source: &str, args: &[&str]) -> String { + let temp_dir = env::temp_dir().join(format!("track_sem_{}_{}", name, std::process::id())); + let _ = fs::remove_dir_all(&temp_dir); + fs::create_dir_all(&temp_dir).unwrap(); + let src_file = temp_dir.join(format!("{}.trk", name)); + fs::write(&src_file, source).unwrap(); + let exe = build_file_in_dir(src_file.to_str().unwrap(), &temp_dir).unwrap(); + let output = Command::new(&exe).args(args).output().unwrap(); + let _ = fs::remove_dir_all(&temp_dir); + assert!( + output.status.success(), + "{} exited with {:?}: {}", + name, + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).trim().to_string() +} + +#[test] +fn program_arguments_reach_os_args() { + let out = run( + "argv", + r#" + fn main() -> void { + print(os_args_count()); + let first = os_arg(1); + print(first); + } + "#, + &["hello"], + ); + assert_eq!(out, "2\nhello"); +} + +#[test] +fn logical_operators_short_circuit_and_normalise() { + let out = run( + "logic", + r#" + fn loud(v: i64) -> i64 { + print(v); + return v; + } + fn main() -> void { + let x: i64 = 1; + let y: i64 = 2; + if x && y { print(10); } else { print(11); } + if loud(0) && loud(5) { print(12); } + if loud(3) || loud(6) { print(13); } + let z = x && y; + print(z); + } + "#, + &[], + ); + assert_eq!(out, "10\n0\n3\n13\n1"); +} + +#[test] +fn return_inside_match_while_and_nested_if() { + let out = run( + "returns", + r#" + fn pick(x: i64) -> i64 { + match x { + 1 => return 10, + _ => return 20, + } + return 0; + } + fn first_over(limit: i64) -> i64 { + let mut i: i64 = 0; + while i < 100 { + if i * i > limit { + return i; + } + i = i + 1; + } + return -1; + } + fn nested(x: i64) -> i64 { + if x > 0 { + if x > 5 { return 7; } else { return 8; } + } + return 9; + } + fn loop_return(x: i64) -> i64 { + while x > 0 { + return 5; + } + return 6; + } + fn main() -> void { + print(pick(1)); print(pick(4)); + print(first_over(50)); + print(nested(10)); print(nested(2)); print(nested(-1)); + print(loop_return(1)); print(loop_return(0)); + } + "#, + &[], + ); + assert_eq!(out, "10\n20\n8\n7\n8\n9\n5\n6"); +} + +#[test] +fn if_and_match_produce_values() { + let out = run( + "values", + r#" + fn sign(x: i64) -> i64 { + let s = if x < 0 { -1 } else if x == 0 { 0 } else { 1 }; + return s; + } + fn name(x: i64) -> i64 { + let v = match x { 1 => 100, 2 => 200, _ => 300, }; + return v; + } + fn main() -> void { + print(sign(-4)); print(sign(0)); print(sign(9)); + print(name(2)); print(name(7)); + } + "#, + &[], + ); + assert_eq!(out, "-1\n0\n1\n200\n300"); +} + +#[test] +fn returned_tuple_variables_do_not_dangle() { + let out = run( + "tuples", + r#" + type Pair = (i64, i64); + fn make(a: i64) -> Pair { + let t = (a, a + 1); + return t; + } + fn clobber() -> i64 { + let z = (9, 9, 9, 9, 9, 9, 9, 9); + return z.0; + } + fn main() -> void { + let p = make(5); + let q = clobber(); + print(p.0); print(p.1); print(q); + } + "#, + &[], + ); + assert_eq!(out, "5\n6\n9"); +} + +#[test] +fn string_patterns_compare_contents() { + let out = run( + "strpat", + r#" + fn main() -> void { + let s = str_concat("he", "llo"); + match s { + "world" => print(0), + "hello" => print(1), + _ => print(2), + } + } + "#, + &[], + ); + assert_eq!(out, "1"); +} + +#[test] +fn union_payloads_and_enum_discriminants() { + let out = run( + "unions", + r#" + union Value { Int(i64), Flag(bool), Nothing } + enum Code { A = 5, B, C = 10 } + fn describe(v: Value) -> i64 { + let mut out: i64 = -1; + match v { + Value::Int(n) => out = n, + Value::Flag(f) => out = 1000 + f, + Value::Nothing => out = 0, + } + return out; + } + fn main() -> void { + print(describe(Value::Int(42))); + print(describe(Value::Flag(true))); + print(describe(Value::Nothing)); + print(Code::A); print(Code::B); print(Code::C); + } + "#, + &[], + ); + assert_eq!(out, "42\n1001\n0\n5\n6\n10"); +} + +#[test] +fn negative_runtime_results_keep_their_sign() { + let out = run( + "signs", + r#" + fn main() -> void { + let r = file_remove("/definitely/not/here/x"); + print(r); + let c = str_char_at("abc", 1); + print(c); + } + "#, + &[], + ); + assert_eq!(out, "-1\n98"); +} + +#[test] +fn block_bindings_do_not_leak_into_the_enclosing_scope() { + let out = run( + "scopes", + r#" + fn main() -> void { + let x = 10; + if true { + let x = 20; + print(x); + } + print(x); + match 1 { + x => print(x), + } + print(x); + } + "#, + &[], + ); + assert_eq!(out, "20\n10\n1\n10"); +} + +#[test] +fn structs_and_arrays_have_runtime_values() { + let out = run( + "structs", + r#" + struct Point { x: i64, y: i64 } + struct Size { w: i64, x: i64 } + fn make(n: i64) -> Point { let p = Point { y: n + 1, x: n }; return p; } + fn main() -> void { + let mut p = make(3); + print(p.x); print(p.y); + p.x = 10; + let s = Size { x: 8, w: 7 }; + print(p.x + s.x); + match p { Point { x: 10, y } => print(y), _ => print(0), } + let mut a = [1, 2, 3, 4]; + a[3] = 40; + let tail = a[2..]; + print(array_len(&tail)); print(tail[1]); + let mut t = (1, 2); + t.1 = 9; + print(t.1); + } + "#, + &[], + ); + assert_eq!(out, "3\n4\n18\n4\n2\n40\n9"); +} + +#[test] +fn array_index_out_of_bounds_aborts() { + let temp_dir = env::temp_dir().join(format!("track_sem_bounds_{}", std::process::id())); + let _ = fs::remove_dir_all(&temp_dir); + fs::create_dir_all(&temp_dir).unwrap(); + let src = temp_dir.join("bounds.trk"); + fs::write(&src, "fn main() -> void { let a = [1, 2]; print(a[2]); }").unwrap(); + let exe = build_file_in_dir(src.to_str().unwrap(), &temp_dir).unwrap(); + let output = Command::new(&exe).output().unwrap(); + let _ = fs::remove_dir_all(&temp_dir); + assert_eq!(output.status.code(), Some(134)); + assert!(String::from_utf8_lossy(&output.stderr).contains("array index out of bounds")); +} diff --git a/tests/selfhost_tests.rs b/tests/selfhost_tests.rs new file mode 100644 index 0000000..bf02248 --- /dev/null +++ b/tests/selfhost_tests.rs @@ -0,0 +1,386 @@ +// Self-hosted compiler (compiler/) against the Rust bootstrap compiler. +// +// trackc is built once with `yard` (stage 1). The tests then check the +// bootstrap fixed point, that both compilers produce programs with the same +// observable behaviour, and that both checkers accept and reject the same +// programs. + +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::OnceLock; + +fn repo() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn scratch(name: &str) -> PathBuf { + let dir = env::temp_dir().join(format!("track_selfhost_{}_{}", name, std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir +} + +/// Stage 1: trackc built by the Rust compiler. +fn trackc() -> &'static Path { + static TRACKC: OnceLock = OnceLock::new(); + TRACKC.get_or_init(|| { + let status = Command::new(env!("CARGO_BIN_EXE_yard")) + .arg("build") + .current_dir(repo().join("compiler")) + .output() + .expect("failed to run yard"); + assert!( + status.status.success(), + "yard build of compiler/ failed:\n{}{}", + String::from_utf8_lossy(&status.stdout), + String::from_utf8_lossy(&status.stderr) + ); + let built = repo().join("compiler/target/trackc"); + let copy = scratch("stage1").join("trackc"); + fs::copy(&built, ©).unwrap(); + copy + }) +} + +fn run_trackc(args: &[&str], cwd: &Path) -> Output { + Command::new(trackc()) + .args(args) + .current_dir(cwd) + .output() + .expect("failed to run trackc") +} + +fn run_exe(exe: &Path) -> (String, Option) { + let out = Command::new(exe).output().unwrap(); + ( + String::from_utf8_lossy(&out.stdout).to_string(), + out.status.code(), + ) +} + +#[test] +fn bootstrap_reaches_a_fixed_point() { + let dir = scratch("bootstrap"); + for stage in ["stage2", "stage3"] { + fs::create_dir_all(dir.join(stage)).unwrap(); + } + let stage2 = dir.join("stage2/trackc"); + let stage3 = dir.join("stage3/trackc"); + let out = run_trackc( + &[ + "build", + "compiler/src/main.trk", + "-o", + stage2.to_str().unwrap(), + ], + &repo(), + ); + assert!( + out.status.success(), + "stage1 failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let out = Command::new(&stage2) + .args([ + "build", + "compiler/src/main.trk", + "-o", + stage3.to_str().unwrap(), + ]) + .current_dir(repo()) + .output() + .unwrap(); + assert!( + out.status.success(), + "stage2 failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + + let c2 = fs::read(dir.join("stage2/trackc.c")).unwrap(); + let c3 = fs::read(dir.join("stage3/trackc.c")).unwrap(); + assert!(c2 == c3, "stage1 and stage2 emitted different C"); + let b2 = fs::read(&stage2).unwrap(); + let b3 = fs::read(&stage3).unwrap(); + assert!(b2 == b3, "stage2 and stage3 executables differ"); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn examples_behave_the_same_under_both_compilers() { + let dir = scratch("examples"); + let mut examples: Vec = fs::read_dir(repo().join("examples")) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| p.extension().is_some_and(|e| e == "trk")) + .collect(); + examples.sort(); + assert!(!examples.is_empty()); + for example in examples { + let stem = example.file_stem().unwrap().to_string_lossy().to_string(); + let host = track::build_file_in_dir(example.to_str().unwrap(), &dir); + let native_exe = dir.join(format!("{}_native", stem)); + let native = run_trackc( + &[ + "build", + example.to_str().unwrap(), + "-o", + native_exe.to_str().unwrap(), + ], + &dir, + ); + if stem.ends_with("_err") { + assert!( + host.is_err(), + "{}: host compiler accepted a rejection test", + stem + ); + assert!( + !native.status.success(), + "{}: trackc accepted a rejection test", + stem + ); + continue; + } + let host_exe = host.unwrap_or_else(|e| panic!("{}: host build failed: {}", stem, e)); + assert!( + native.status.success(), + "{}: trackc build failed: {}", + stem, + String::from_utf8_lossy(&native.stderr) + ); + assert_eq!( + run_exe(&host_exe), + run_exe(&native_exe), + "{}: behaviour differs", + stem + ); + } + let _ = fs::remove_dir_all(&dir); +} + +/// (name, source, accepted). Both checkers must agree with the expectation. +const VERDICTS: &[(&str, &str, bool)] = &[ + ( + "move_then_use", + "fn main() -> void { let s = str_concat(\"a\", \"b\"); let t = s; print(s); }", + false, + ), + ( + "copy_ints", + "fn main() -> void { let a: i64 = 1; let b = a; print(a); print(b); }", + true, + ), + ( + "borrow_does_not_move", + "fn f(x: &Str) -> i64 { return 0; } fn main() -> void { let s = str_concat(\"a\", \"b\"); f(&s); print(s); }", + true, + ), + ( + "frozen_by_reference", + "fn main() -> void { let s = str_concat(\"a\", \"b\"); let r = &s; let t = s; }", + false, + ), + ( + "lens_locks_target", + "fn main() -> void { let mut v = vec_init(4); with v -> l { let w = v; } }", + false, + ), + ( + "lens_cannot_escape", + "fn main() -> void { let mut v = vec_init(4); with v -> l { let w = l; } }", + false, + ), + ( + "inconsistent_if", + "fn eat(s: Str) -> void { } fn main() -> void { let s = str_concat(\"a\", \"b\"); if true { eat(s); } else { } }", + false, + ), + ( + "consistent_if", + "fn eat(s: Str) -> void { } fn main() -> void { let s = str_concat(\"a\", \"b\"); if true { eat(s); } else { eat(s); } }", + true, + ), + ( + "consume_in_loop", + "fn eat(s: Str) -> void { } fn main() -> void { let s = str_concat(\"a\", \"b\"); let mut i: i64 = 0; while i < 3 { eat(s); i = i + 1; } }", + false, + ), + ( + "return_local_ref", + "fn f() -> &i64 { let x = 5; return &x; }", + false, + ), + ( + "return_param_ref", + "fn f(x: &i64) -> &i64 { return x; }", + true, + ), + ( + "immutable_assign", + "fn main() -> void { let x = 1; x = 2; }", + false, + ), + ( + "mutable_assign", + "fn main() -> void { let mut x = 1; x = 2; print(x); }", + true, + ), + ( + "mut_does_not_leak_across_functions", + "fn a() -> void { let mut x = 1; x = 2; } fn b() -> void { let x = 1; x = 3; }", + false, + ), + ( + "shadowed_binding_is_immutable", + "fn main() -> void { let mut x = 1; let x = 2; x = 3; }", + false, + ), + ("assign_undeclared", "fn main() -> void { y = 3; }", false), + ( + "undeclared_use", + "fn main() -> void { print(nope); }", + false, + ), + ( + "forbidden_free", + "fn main() -> void { let s = str_concat(\"a\", \"b\"); str_free(s); }", + false, + ), + ( + "typo_suggestion", + "fn compute() -> i64 { return 1; } fn main() -> void { print(computee()); }", + false, + ), + ( + "tuple_index_bounds", + "fn main() -> void { let t = (1, 2); print(t.2); }", + false, + ), + ( + "const_before_use", + "fn main() -> void { print(LIMIT); } const LIMIT = 10;", + true, + ), + ( + "compile_error_macro", + "fn main() -> void { @compile_error(\"stop\"); }", + false, + ), + ( + "struct_field_update", + "struct P { x: i64 } fn main() -> void { let mut p = P { x: 1 }; p.x = 2; print(p.x); }", + true, + ), + ( + "immutable_field_update", + "struct P { x: i64 } fn main() -> void { let p = P { x: 1 }; p.x = 2; }", + false, + ), + ( + "lens_field_update", + "struct P { x: i64 } fn main() -> void { let mut p = P { x: 1 }; with p -> l { l.x = 5; } print(p.x); }", + true, + ), + ( + "unknown_struct_field", + "struct P { x: i64 } fn main() -> void { let p = P { z: 1 }; }", + false, + ), + ( + "unknown_field_access", + "struct P { x: i64 } fn main() -> void { let p = P { x: 1 }; print(p.q); }", + false, + ), + ( + "array_moved_by_value", + "fn main() -> void { let a = [1, 2]; let n = array_len(a); print(a[0]); }", + false, + ), + ( + "array_index_update", + "fn main() -> void { let mut a = [1, 2]; a[1] = 7; print(a[1]); }", + true, + ), +]; + +#[test] +fn checkers_agree_on_accepted_and_rejected_programs() { + let dir = scratch("verdicts"); + for (name, source, accepted) in VERDICTS { + let host = track::compile_source(source); + assert_eq!( + host.is_ok(), + *accepted, + "{}: host verdict: {:?}", + name, + host.err() + ); + let file = dir.join(format!("{}.trk", name)); + fs::write(&file, source).unwrap(); + let native = run_trackc(&["check", file.to_str().unwrap()], &dir); + assert_eq!( + native.status.success(), + *accepted, + "{}: trackc verdict: {}", + name, + String::from_utf8_lossy(&native.stderr) + ); + } + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn diagnostics_point_at_the_source() { + let dir = scratch("diagnostics"); + let file = dir.join("moved.trk"); + fs::write( + &file, + "fn main() -> void {\n let s = str_concat(\"a\", \"b\");\n let t = s;\n print(s);\n}\n", + ) + .unwrap(); + let out = run_trackc(&["check", "moved.trk"], &dir); + assert!(!out.status.success()); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.starts_with( + "error[E0200]: Use-after-free! Resource 's' is already spent.\n --> moved.trk:4:11\n" + ), + "unexpected diagnostic:\n{}", + stderr + ); + assert!( + stderr.contains("4 | print(s);"), + "missing source frame:\n{}", + stderr + ); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn native_test_suite_passes_under_trackc() { + let compiler = repo().join("compiler"); + let mut tests: Vec = fs::read_dir(compiler.join("tests")) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| p.extension().is_some_and(|e| e == "trk")) + .collect(); + tests.sort(); + for test in tests { + let rel = test + .strip_prefix(&compiler) + .unwrap() + .to_str() + .unwrap() + .to_string(); + let out = run_trackc(&["run", &rel], &compiler); + assert!( + out.status.success(), + "{} failed under trackc:\n{}{}", + rel, + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + } +} diff --git a/tests/yard_tests.rs b/tests/yard_tests.rs index 7250bb3..13ecba8 100644 --- a/tests/yard_tests.rs +++ b/tests/yard_tests.rs @@ -132,7 +132,7 @@ fn test_yard_version_reports() { String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr) ); - assert!(s.contains("0.7.0"), "yard version missing 0.7.0: {}", s); + assert!(s.contains("1.0.0"), "yard version missing 1.0.0: {}", s); } else { // fallback via cargo run let o = std::process::Command::new("cargo") @@ -144,7 +144,7 @@ fn test_yard_version_reports() { String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr) ); - assert!(s.contains("0.7.0"), "yard version missing: {}", s); + assert!(s.contains("1.0.0"), "yard version missing: {}", s); } }