diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8466928 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "oxide" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7eb8961 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "oxide" +version = "0.1.0" +edition = "2024" +description = "The Oxide programming language: a friendlier Rust" +license = "MIT" + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdf2830 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Oxide + +**A friendlier Rust.** Oxide keeps the ideas that make Rust great — ownership, +borrowing, memory safety without garbage collection, immutability by default — +and removes the parts that make it hard to learn: lifetime annotations, the +struct/impl split, sized-integer soup, and format-macro ceremony. + +```oxide +class Point { + x: float, + y: float, + + func dist(&self, other: &Point) -> float { + let dx = self.x - other.x; + let dy = self.y - other.y; + return (dx * dx + dy * dy).sqrt(); + } +} + +func main() { + let a = Point { x: 0.0, y: 0.0 }; + let b = Point { x: 3.0, y: 4.0 }; + println("distance: {a.dist(&b)}"); +} +``` + +## What Oxide is + +- **Ownership and borrow checking, always.** Moves, `&` shared borrows, and + `&mut` exclusive borrows work like Rust's. The difference: lifetimes are + *always inferred* — there is no `'a` syntax anywhere in the language. +- **Classes.** Data and methods live in one block. No inheritance; + composition now, traits later. +- **Simple types.** `int`, `float`, `bool`, `string`. Sized types can come + later when the systems-programming story needs them. +- **String interpolation.** `println("hello {name}")` with real embedded + expressions — no macros required. +- **Compiled to a bytecode VM.** The `oxide` toolchain (written in Rust) + compiles `.ox` files to bytecode and runs them on a custom VM. + +## Status + +**Milestone 5 complete — the borrow checker is live.** The full v1 +pipeline runs: lexer → parser → type checker → **borrow checker** → +bytecode compiler → stack VM. Ownership and borrowing work exactly as +the design doc promises, with zero lifetime annotations: + +- **Moves**: assigning, passing, or returning an owned value (string, + list, class) transfers ownership; using the source afterwards is a + compile error with a `.clone()` hint. Copy types (`int`, `float`, + `bool`) just copy. +- **Borrows**: `&x` shares, `&mut x` gives exclusive access; parameters + can take `&[int]`, `&mut Counter`, etc. Aliasing XOR mutation is + enforced, and borrows end at their **last use** (non-lexical), not at + scope end. +- **Errors show both sites** (§8.4): the conflict, where the borrow + began, and a concrete fix — + +``` +error: cannot modify `scores` while it is borrowed + --> demo.ox:4:5 + | +4 | scores[0] = 100; + | ^^^^^^^^^^^^^^^^ + --> demo.ox:3:18 + | +3 | let first = &scores; + | ^^^^^^ the borrow happens here + = help: finish using the borrow before modifying the value +``` + +`examples/tour.ox` — one program exercising the *entire* v1 grammar — +now runs end-to-end. Remaining spec gaps: returning references and +whole-value assignment through `&mut` are rejected with restructure +hints (both are candidates for a follow-up milestone), and `unsafe` +does not exist by design. + +## Layout + +| Path | Purpose | +|------|---------| +| `docs/design.md` | The v1 language design document | +| `src/lexer.rs` | Hand-written lexer (interpolated strings, nested comments) | +| `src/parser.rs` | Recursive-descent + Pratt parser producing the AST | +| `src/ast.rs` / `src/pretty.rs` | AST definitions and tree printer | +| `src/check.rs` / `src/tir.rs` | Type checker lowering the AST to typed IR | +| `src/compile.rs` / `src/bytecode.rs` | Typed IR → stack bytecode, disassembler | +| `src/vm.rs` / `src/value.rs` | The stack VM and runtime values | +| `src/diagnostics.rs` | Span → line/col mapping and caret rendering | +| `examples/hello.ox` | Expressions, variables, `if`, lists, built-ins | +| `examples/functions.ox` | Functions, recursion, and loops | +| `examples/classes.ox` | Classes, methods, composition, `to_str` | +| `examples/borrow.ox` | Moves, clones, `&`/`&mut` borrows, NLL | +| `examples/tour.ox` | One program exercising the entire v1 grammar — runs | + +## Toolchain + +``` +oxide run main.ox # compile and execute +oxide build main.ox # compile, write bytecode disassembly to main.oxb +oxide check main.ox # parse the file and report syntax errors +oxide ast main.ox # parse and print the AST +``` diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..1550e68 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,426 @@ +# Oxide Language Design — v1 + +*Status: draft. This document specifies the v1 core language: variables, +functions, classes, control flow, and the ownership/borrowing model. Features +explicitly deferred to later versions are collected in [§12](#12-roadmap).* + +--- + +## 1. Philosophy + +Oxide is a Rust-family language with one design goal: **keep Rust's safety +model, remove Rust's learning curve.** + +The guiding principles, in priority order: + +1. **Memory safety is non-negotiable.** Oxide has full ownership semantics + and a borrow checker. There is no garbage collector and no `unsafe` + escape hatch in v1. +2. **The compiler works for you, not the reverse.** Lifetimes are always + inferred. If a program's borrows are too complex to infer, the compiler + rejects it *with a suggestion to restructure* — it never asks the + programmer to write an annotation, because the annotation syntax does not + exist. +3. **One obvious way.** One integer type, one string type, one way to attach + methods to data. Choices Rust makes you make (`i32` vs `i64`, `String` vs + `&str`, where to put the `impl`) are made for you. +4. **Convenience over ceremony.** Small, cheap values copy implicitly. + String formatting is built into string literals. Numeric widening from + `int` to `float` in arithmetic is implicit. +5. **Rust programmers feel at home.** The syntax skeleton — `func` (Rust's + `fn`), `let`, `let mut`, `match` (future), braces, `&`/`&mut` — is + Rust's. Oxide diverges only where Rust is noisy, hard, or implicit in a + confusing way (e.g. Oxide requires an explicit `return`). + +### Non-goals for v1 + +- Competing with Rust on runtime performance (Oxide runs on a bytecode VM). +- `unsafe`, FFI, inline assembly, or systems-level control. +- Full trait/generics machinery (planned, see roadmap). + +--- + +## 2. Hello, world + +```oxide +func main() { + println("Hello, world!"); +} +``` + +Execution starts at `func main()`. Source files use the `.ox` extension and +UTF-8 encoding. + +--- + +## 3. Lexical structure + +- **Comments:** `// line comment` and `/* block comment */` (nestable). +- **Identifiers:** `[A-Za-z_][A-Za-z0-9_]*`. Convention: `snake_case` for + variables and functions, `PascalCase` for classes. +- **Semicolons:** required after every statement. Blocks are not values in + general; the one exception is `if` used as an expression (§10), where + each branch's final expression (no semicolon) is that branch's value. + Functions never return implicitly — see §6. +- **Keywords (v1):** `func`, `let`, `mut`, `class`, `if`, `else`, `while`, + `for`, `in`, `loop`, `break`, `continue`, `return`, `true`, `false`, + `self`, `and`, `or`, `not`. + +Reserved for future use: `enum`, `match`, `trait`, `impl`, `pub`, `mod`, +`use`, `const`, `static`, `async`, `await` — and `fn`, so the compiler can +greet Rust muscle memory with "did you mean `func`?". + +### Literals + +| Kind | Examples | +|------|----------| +| int | `0`, `42`, `1_000_000`, `0xFF`, `0b1010` | +| float | `3.14`, `0.5`, `1e9`, `2.5e-3` | +| bool | `true`, `false` | +| string | `"hello"`, `"line\n"`, `"x = {x}"` (see §9) | + +--- + +## 4. Types + +V1 has four primitive types and user-defined classes: + +| Type | Meaning | Semantics | +|------|---------|-----------| +| `int` | 64-bit signed integer | Copy | +| `float` | 64-bit IEEE-754 | Copy | +| `bool` | `true` / `false` | Copy | +| `string` | heap-allocated, growable UTF-8 text | Owned (moves) | +| `ClassName` | user-defined class instance | Owned (moves) | +| `[T]` | growable list of `T` | Owned (moves) | +| `()` | unit — the type of "nothing" | Copy | + +Notes: + +- There is exactly **one** integer type and **one** float type. Sized types + (`int32`, `uint8`, …) are deferred until Oxide targets systems use cases. +- There is exactly **one** string type. Rust's `String`/`&str` split becomes + `string` and `&string`. +- `[T]` is the built-in list (Rust's `Vec`): `let xs: [int] = [1, 2, 3];`. + It is the only generic-shaped type in v1 and is compiler-built-in; user + generics are deferred. +- Integer arithmetic panics on overflow. Integer division by zero panics. +- Mixed `int`/`float` arithmetic implicitly widens the `int` operand to + `float` (principle 4). All other conversions are explicit via built-ins: + `float(n)`, `int(f)` (truncates), `str(x)`. + +--- + +## 5. Variables + +```oxide +let x = 5; // immutable, type inferred (int) +let mut count = 0; // mutable +let name: string = "oxide"; // optional annotation +count = count + 1; // ok: count is mut +x = 10; // ERROR: x is not mutable +``` + +- `let` bindings are immutable by default; `let mut` opts into mutation. +- Local types are inferred; annotations are optional and checked. +- Shadowing is allowed, as in Rust: `let x = x + 1;`. +- Assigning to a `let` binding, or mutating through it, is a compile error. + +--- + +## 6. Functions + +```oxide +func add(a: int, b: int) -> int { + return a + b; +} + +func greet(name: &string) { // no -> means returns () + println("hi {name}"); +} +``` + +- Parameter types and return types are **required** on function signatures + (like Rust — signatures are the API surface and the inference boundary). +- **Returns are always explicit:** `return expr;` is the only way a + function produces a value. Unlike Rust, there is no tail-expression + return — a dangling final expression is a compile error with the hint + "add `return`". A function with a `-> T` must `return` on every path. +- A function with no `->` returns `()`; falling off the end of its body is + allowed, and bare `return;` exits early. +- Parameters follow ownership rules: `name: string` takes ownership, + `name: &string` borrows, `name: &mut string` borrows exclusively (§8). +- No overloading, no default arguments, no variadics in v1. + +--- + +## 7. Classes + +A class is a Rust struct with its `impl` block inlined: fields first, then +methods, in one declaration. **There is no inheritance.** Polymorphism +arrives with traits in a later version; until then, use composition. + +```oxide +class Counter { + count: int, + label: string, + + func new(label: string) -> Counter { + return Counter { count: 0, label: label }; + } + + func increment(&mut self) { + self.count = self.count + 1; + } + + func report(&self) -> string { + return "{self.label}: {self.count}"; + } +} + +func main() { + let mut c = Counter::new("clicks"); + c.increment(); + c.increment(); + println(c.report()); // clicks: 2 +} +``` + +Rules: + +- **Construction:** the literal form `ClassName { field: value, ... }` must + set every field. By convention a `new` associated function wraps it. +- **Methods** take `&self` (read), `&mut self` (modify), or `self` + (consume). A method with no `self` parameter is an *associated function*, + called as `ClassName::func(...)` — the only place `::` appears in v1. +- **Field access and method calls** both use `.`. +- Calling a `&mut self` method requires the receiver to be mutable + (`let mut`), exactly as in Rust. +- All fields and methods are public in v1; visibility control comes with + modules. + +--- + +## 8. Ownership and borrowing + +This is Oxide's core. The rules are Rust's, specified here in full. The one +deliberate difference: **lifetimes exist only inside the compiler.** There +is no annotation syntax, so every rule below must be checkable by inference +alone. + +### 8.1 Ownership + +1. Every value has exactly one owner (a variable, field, or list element). +2. When the owner goes out of scope, the value is dropped. +3. Assignment, argument passing, and returning **move** ownership for owned + types (`string`, classes, lists). After a move, the source variable is + unusable — using it is a compile error. + +```oxide +let a = "hello"; +let b = a; // move: a no longer usable +println(b); // ok +println(a); // ERROR: `a` was moved to `b` +``` + +4. **Copy types** (`int`, `float`, `bool`, `()`) are duplicated instead of + moved; the source stays usable. *(Convenience principle: classes whose + fields are all Copy may be auto-promoted to Copy in a future version; + v1 keeps this conservative — only primitives copy.)* + +### 8.2 Borrows + +A borrow is a reference that does not own: `&x` (shared) or `&mut x` +(exclusive). Dereferencing is automatic on field access and method calls; +explicit `*` exists for assignment through a `&mut`. + +The two laws, identical to Rust's: + +1. **Aliasing XOR mutation.** At any program point a value has either any + number of live `&` borrows, **or** exactly one live `&mut` borrow — + never both. +2. **No dangling.** A borrow must not outlive the value it points to. + +Additional rules that follow: + +- While any borrow is live, the owner cannot be moved or dropped. +- While a `&mut` is live, the owner cannot be read or written except + through that borrow. +- Taking `&mut x` requires `x` to be a `mut` binding. +- Borrow liveness is **non-lexical**: a borrow ends at its last use, not at + the end of its scope (Rust's NLL behavior — this makes many "friendly" + programs compile that scope-based checking would reject). + +```oxide +let mut s = "hi"; +let r = &s; // shared borrow begins +println(r); // last use of r — borrow ends here (non-lexical) +s.push("!"); // ok: needs &mut s, no shared borrow live anymore +``` + +### 8.3 Inferred lifetimes + +Function signatures never mention lifetimes. The compiler applies these +inference rules; programs they cannot cover are **rejected, not annotated**: + +1. A returned reference is assumed to borrow from **the** reference + parameter if there is exactly one, or from `&self` / `&mut self` in a + method. (Rust's elision rules.) +2. If a function takes multiple reference parameters and returns a + reference, the returned borrow is conservatively assumed to come from + **any** of them (i.e., the caller must keep all of them alive). If that + conservative assumption makes a caller fail to check, the diagnostic + suggests restructuring — e.g. returning an owned value or an index. +3. References cannot be stored in class fields in v1. This single + restriction eliminates every case where Rust would require a struct + lifetime annotation. Classes own their data; share by borrowing at the + call site or cloning. + +```oxide +func longest(a: &string, b: &string) -> &string { // legal: rule 2 + return if a.len() > b.len() { a } else { b }; +} + +class Parser { + source: &string, // ERROR: classes cannot hold references. + // help: store the string itself: `source: string` +} +``` + +### 8.4 The error-message contract + +Borrow-checker diagnostics are a feature, not an afterthought. Every +ownership/borrow error must state, in this order: (1) what rule was broken +in plain language, (2) the two source locations in conflict, (3) a concrete +suggested fix (`clone the value`, `end the first borrow earlier`, `return +an owned string instead`). Never Rust jargon like "does not live long +enough" without a location and a fix. + +--- + +## 9. Strings and interpolation + +- `string` is heap-allocated, growable, UTF-8. Methods (v1 built-ins): + `len()`, `push(s)`, `contains(s)`, `split(sep)`, `trim()`, `clone()`. +- `+` concatenates two strings (consuming the left operand, like Rust). +- **Interpolation:** inside any string literal, `{expr}` embeds any + expression whose type is printable (`int`, `float`, `bool`, `string`, or + a class with a `to_str(&self) -> string` method). `{{` and `}}` escape + literal braces. Interpolated expressions **borrow**; they never move. + +```oxide +let n = 3; +let who = "world"; +println("hello {who}, {n} + 1 = {n + 1}"); +``` + +There are no format macros; interpolation is the formatting system. + +--- + +## 10. Control flow and expressions + +All Rust-style: no parentheses around conditions, braces mandatory. `if` +is usable as an expression; loops and function bodies are statements only: + +```oxide +let grade = if score >= 90 { "A" } else { "B" }; // if is an expression + +while count < 10 { count = count + 1; } + +for x in [1, 2, 3] { println("{x}"); } // for-in over lists (by move) +for x in &xs { ... } // or by borrow + +for i in 0..10 { ... } // half-open range +loop { if done { break; } } // infinite loop; break/continue +``` + +Operators, tightest first: unary `- not & &mut *` · `* / %` · `+ -` · +`== != < <= > >=` · `and` · `or` · assignment. Boolean operators are the +words `and` / `or` / `not` (friendlier than `&&`/`||`/`!`, and frees `&` +to mean only "borrow"). Both operators short-circuit. Comparison operators +do not chain (`a < b < c` is a compile error with a hint to use `and`). + +--- + +## 11. Errors and panics + +V1 has no `Result`, no exceptions. Failures **panic**: print a message with +a stack trace and terminate with a nonzero exit code. + +- Runtime panics: integer overflow, divide by zero, list index out of + bounds, explicit `panic("msg")`. +- `assert(cond)` / `assert(cond, "msg")` built-ins panic when false. + +`Option`/`Result` with `?` are the flagship v2 feature and will arrive with +enums (§12); the standard library is deliberately tiny until then so that +few APIs need fallibility. + +--- + +## 12. Roadmap + +Deferred deliberately, in rough priority order: + +| Version | Feature | Notes | +|---------|---------|-------| +| v2 | Enums + `match` | Sum types with exhaustive matching | +| v2 | `Option`/`Result` + `?` | Replaces panics as the error model | +| v3 | Generics | User-defined `func max(...)`, generic classes | +| v3 | Traits | Simple traits only: methods + bounds. No associated types, no HRTBs | +| v4 | Modules + visibility | `mod`, `use`, `pub`; multi-file programs | +| v4 | Maps and more stdlib | `{K: V}` built-in map type | +| later | Copy auto-promotion | Classes of all-Copy fields become Copy | +| later | References in fields | Only if inference can stay annotation-free | +| later | Closures | Capture analysis interacts with borrows; needs care | +| later | Sized numeric types, FFI | If/when systems use cases appear | + +--- + +## 13. Implementation plan + +Toolchain written in **Rust**; execution on a custom **bytecode VM**. + +``` +.ox source + │ lexer (hand-written, produces token stream w/ spans) + ▼ +tokens + │ parser (recursive descent + Pratt for expressions → AST) + ▼ +AST + │ resolver (name resolution, scope building) + │ type checker (local inference; signatures required) + │ borrow checker (move tracking + NLL-style liveness on a CFG) + ▼ +checked AST + │ compiler (→ stack-based bytecode, constant pool) + ▼ +bytecode (.oxb) + │ VM (stack machine; call frames; drop on scope exit) + ▼ +result +``` + +Key decisions: + +- **Stack-based VM** (simpler to compile to than register-based); values + are NaN-boxed or a tagged enum — decide at implementation time. +- Because the borrow checker statically guarantees unique ownership, the VM + needs **no GC**: owned heap values are freed when their owner's stack + slot is popped or overwritten by a move. +- **Spans everywhere.** Every token, AST node, and bytecode instruction + carries a source span so runtime panics and borrow errors always point at + source (the §8.4 contract depends on this). +- CLI: `oxide run`, `oxide build`, `oxide check` (stops after borrow + checking — the fast feedback loop). + +### Milestone order + +1. Lexer + parser + AST pretty-printer for the full v1 grammar. +2. `oxide run` end-to-end for expressions/variables/`println` (type checker + + compiler + VM, no borrow checker yet). +3. Functions and control flow. +4. Classes and methods. +5. **Borrow checker** (moves first, then borrows with NLL liveness). +6. Diagnostics polish to meet the §8.4 contract. diff --git a/examples/borrow.ox b/examples/borrow.ox new file mode 100644 index 0000000..6f9adda --- /dev/null +++ b/examples/borrow.ox @@ -0,0 +1,84 @@ +// Milestone 5: ownership and borrowing — Rust's rules, zero annotations. +// Everything here passes the borrow checker; the commented-out lines are +// the programs it rejects (each with a two-location error and a fix). + +class Inventory { + items: [string], + count: int, + + func new() -> Inventory { + return Inventory { items: [], count: 0 }; + } + + func total(&self) -> int { + return self.count; + } + + func restock(&mut self, n: int) { + self.count = self.count + n; + } + + func to_str(&self) -> string { + return "inventory of {self.count}"; + } +} + +// Shared borrow: reads without taking ownership. +func sum(xs: &[int]) -> int { + let mut total = 0; + for x in xs { + total = total + *x; + } + return total; +} + +// Exclusive borrow: mutates the caller's data in place. +func double_all(xs: &mut [int]) { + for i in 0..xs.len() { + xs[i] = xs[i] * 2; + } +} + +func audit(inv: &Inventory) -> string { + return "audit: {inv}"; +} + +func receive(inv: &mut Inventory, n: int) { + inv.restock(n); +} + +func main() { + // Moves: ownership transfers, the source is dead afterwards. + let greeting = "hello"; + let owned = greeting; + // println(greeting); // error: cannot use `greeting`: it was moved + + // Clones make it explicit when you really want two copies. + let copy = owned.clone(); + println("{owned} / {copy}"); + + // Borrows read without consuming... + let mut nums = [1, 2, 3, 4]; + println("sum = {sum(&nums)}"); + println("still mine: {nums.len()} items"); + + // ...and &mut borrows mutate in place. + double_all(&mut nums); + println("doubled: [{nums[0]}, {nums[1]}, {nums[2]}, {nums[3]}]"); + + // Aliasing XOR mutation, with non-lexical lifetimes: this shared + // borrow ends at its last use, so the mutation after it is fine. + let peek = &nums; + println("first = {peek[0]}"); + nums[0] = 99; + // ...but flipping those two lines is an error: + // let peek = &nums; + // nums[0] = 99; // error: cannot modify `nums` while borrowed + // println("{peek[0]}"); + + let mut inv = Inventory::new(); + receive(&mut inv, 10); + receive(&mut inv, 5); + println(audit(&inv)); + println("total = {inv.total()}"); +} diff --git a/examples/classes.ox b/examples/classes.ox new file mode 100644 index 0000000..4a7287e --- /dev/null +++ b/examples/classes.ox @@ -0,0 +1,101 @@ +// Milestone 4: classes — fields + methods in one block, no inheritance. + +class Point { + x: float, + y: float, + + func new(x: float, y: float) -> Point { + return Point { x: x, y: y }; + } + + func dist(&self, other: &Point) -> float { + let dx = self.x - other.x; + let dy = self.y - other.y; + return float_sqrt(dx * dx + dy * dy); + } + + func scale(&mut self, factor: float) { + self.x = self.x * factor; + self.y = self.y * factor; + } + + func to_str(&self) -> string { + return "({self.x}, {self.y})"; + } +} + +class Counter { + count: int, + label: string, + + func new(label: string) -> Counter { + return Counter { count: 0, label: label }; + } + + func increment(&mut self) { + self.count = self.count + 1; + } + + func to_str(&self) -> string { + return "{self.label}: {self.count}"; + } +} + +// Composition, not inheritance: a Rect owns two Points. +class Rect { + origin: Point, + corner: Point, + + func area(&self) -> float { + let w = self.corner.x - self.origin.x; + let h = self.corner.y - self.origin.y; + return w * h; + } + + func translate(&mut self, dx: float, dy: float) { + self.origin.x = self.origin.x + dx; + self.origin.y = self.origin.y + dy; + self.corner.x = self.corner.x + dx; + self.corner.y = self.corner.y + dy; + } +} + +// Newton's method — no float sqrt built-in yet. +func float_sqrt(x: float) -> float { + if x <= 0.0 { + return 0.0; + } + let mut guess = x / 2.0; + for i in 0..20 { + guess = (guess + x / guess) / 2.0; + } + return guess; +} + +func main() { + let mut p = Point::new(3.0, 4.0); + println("p = {p}"); // uses Point::to_str + p.scale(2.0); + println("scaled = {p}"); + + let mut clicks = Counter::new("clicks"); + clicks.increment(); + clicks.increment(); + clicks.increment(); + println(clicks); // println accepts to_str classes too + + let mut r = Rect { + origin: Point::new(0.0, 0.0), + corner: Point::new(4.0, 2.5), + }; + println("area = {r.area()}"); + r.translate(1.0, 1.0); + println("origin after move = {r.origin}"); + + let points = [Point::new(0.0, 0.0), Point::new(3.0, 4.0), Point::new(6.0, 8.0)]; + let mut total = 0.0; + for i in 1..3 { + total = total + points[i].dist(&points[i - 1]); + } + println("path length = {total}"); +} diff --git a/examples/functions.ox b/examples/functions.ox new file mode 100644 index 0000000..f5c27af --- /dev/null +++ b/examples/functions.ox @@ -0,0 +1,58 @@ +// Milestone 3: user-defined functions, recursion, and loops. + +func fib(n: int) -> int { + if n < 2 { + return n; + } + return fib(n - 1) + fib(n - 2); +} + +func classify(n: int) -> string { + if n % 15 == 0 { + return "FizzBuzz"; + } else if n % 3 == 0 { + return "Fizz"; + } else if n % 5 == 0 { + return "Buzz"; + } + return str(n); +} + +func total(xs: [int]) -> int { + let mut sum = 0; + for x in xs { + sum = sum + x; + } + return sum; +} + +func first_prime_after(n: int) -> int { + let mut candidate = n + 1; + loop { + let mut is_prime = candidate > 1; + let mut d = 2; + while d * d <= candidate { + if candidate % d == 0 { + is_prime = false; + break; + } + d = d + 1; + } + if is_prime { + return candidate; + } + candidate = candidate + 1; + } +} + +func main() { + println("fib(20) = {fib(20)}"); + + for i in 1..16 { + println(classify(i)); + } + + let xs = [3, 1, 4, 1, 5, 9, 2, 6]; + println("total = {total(xs)}"); + println("next prime after 100 = {first_prime_after(100)}"); +} diff --git a/examples/hello.ox b/examples/hello.ox new file mode 100644 index 0000000..1c991c9 --- /dev/null +++ b/examples/hello.ox @@ -0,0 +1,42 @@ +// Everything `oxide run` executes today (milestone 2): variables, +// expressions, string interpolation, `if`, lists, and the built-ins. + +func main() { + let name = "world"; + println("Hello, {name}!"); + + // Immutable by default; `let mut` opts into mutation. + let mut count = 0; + count = count + 1; + count = count + 1; + println("count = {count}"); + + // One int type, one float type; mixed arithmetic widens implicitly. + let pi = 3.14159; + let tau = pi * 2; + let almost = int(tau) + 1; + println("tau = {tau}, almost = {almost}, exact = {float(almost) - tau}"); + + // `if` is an expression. + let score = 87; + let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }; + println("score {score} gets grade {grade}"); + + // Lists: literals, indexing, element assignment, len(). + let mut ys = [10, 20, 30]; + ys[0] = ys[1] + ys[2]; + println("ys = [{ys[0]}, {ys[1]}, {ys[2]}], len = {ys.len()}"); + + // Booleans use words, comparisons don't chain. + let in_range = score >= 0 and score <= 100; + println("valid score: {in_range}, shouty: {not in_range or score > 50}"); + + // Strings: concat with +, escapes, braces. + let brand = "Oxide" + " " + "v0.1"; + println("{brand}: {{friendlier}} \"rust\", len {brand.len()}"); + + assert(count == 2, "count should be 2"); + if count > 100 { + panic("unreachable"); + } +} diff --git a/examples/tour.ox b/examples/tour.ox new file mode 100644 index 0000000..8d7deb5 --- /dev/null +++ b/examples/tour.ox @@ -0,0 +1,138 @@ +// A tour of the Oxide v1 grammar. This file exercises every syntactic +// construct the milestone-1 parser supports; `oxide check examples/tour.ox` +// must report syntax OK. + +/* Block comments /* nest */ properly. */ + +class Point { + x: float, + y: float, + + func new(x: float, y: float) -> Point { + return Point { x: x, y: y }; + } + + func dist(&self, other: &Point) -> float { + let dx = self.x - other.x; + let dy = self.y - other.y; + return (dx * dx + dy * dy).sqrt(); + } + + func scale(&mut self, factor: float) { + self.x = self.x * factor; + self.y = self.y * factor; + } + + func into_label(self) -> string { + return "({self.x}, {self.y})"; + } +} + +class Counter { + count: int, + label: string, + + func new(label: string) -> Counter { + return Counter { count: 0, label: label }; + } + + func increment(&mut self) { + self.count = self.count + 1; + } + + func report(&self) -> string { + return "{self.label}: {self.count}"; + } +} + +func classify(score: int) -> string { + // `if` as an expression, with else-if chains as statements below. + let grade = if score >= 90 { "A" } else { "B" }; + if score >= 90 { + println("excellent"); + } else if score >= 50 { + println("passing"); + } else { + println("needs work"); + } + return grade; +} + +func sum(xs: &[int]) -> int { + let mut total = 0; + for x in xs { + total = total + *x; + } + return total; +} + +func numerics() { + let a = 1_000_000; + let b = 0xFF + 0b1010; + let c = 3.14 * 1e9 + 2.5e-3; + let mixed = a + 1; + let widened = float(mixed) / c; + println("a={a} b={b} widened={widened}"); +} + +func loops_and_ranges() { + let mut i = 0; + while i < 10 { + i = i + 1; + if i % 2 == 0 { + continue; + } + } + for n in 0..5 { + println("n = {n}"); + } + loop { + i = i - 1; + if i == 0 { + break; + } + } +} + +func lists() { + let xs: [int] = [1, 2, 3]; + let grid: [[int]] = [[1, 2], [3, 4]]; + let mut ys = [10, 20, 30]; + ys[0] = ys[1] + grid[1][0]; + let empty: [string] = []; + println("first = {ys[0]}, len = {empty.len()}"); +} + +func booleans(flag: bool, score: int) -> bool { + let ok = flag and score >= 0 or not flag; + return ok != false; +} + +func strings(name: &string) -> string { + let greeting = "hello, {name}!"; + let escaped = "tab\there, brace {{literal}}, quote \"quoted\""; + let nested = "sum = {sum(&[1, 2, 3]) + 1}"; + return greeting + " " + escaped + " " + nested; +} + +func main() { + let origin = Point::new(0.0, 0.0); + let mut p = Point::new(3.0, 4.0); + p.scale(2.0); + println("dist = {origin.dist(&p)}"); + println(p.into_label()); + + let mut counter = Counter::new("clicks"); + counter.increment(); + counter.increment(); + println(counter.report()); + + let grade = classify(95); + println("grade = {grade}"); + + numerics(); + loops_and_ranges(); + lists(); + println("ok = {booleans(true, 10)}"); + println(strings(&"world")); +} diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..beca64a --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,292 @@ +//! AST for the v1 grammar. Every node carries a `Span`. + +use crate::span::Span; + +#[derive(Debug, Clone)] +pub struct Program { + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub enum Item { + Func(Func), + Class(Class), +} + +#[derive(Debug, Clone)] +pub struct Func { + pub name: Ident, + pub params: Vec, + /// None means the function returns `()` (design doc §6). + pub return_type: Option, + pub body: Block, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Class { + pub name: Ident, + pub fields: Vec, + pub methods: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Field { + pub name: Ident, + pub ty: Type, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum Param { + /// `self`, `&self`, or `&mut self` — only legal as a first parameter + /// inside a class. + SelfParam { + kind: SelfKind, + span: Span, + }, + Normal { + name: Ident, + ty: Type, + span: Span, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SelfKind { + /// `self` — consumes the instance. + Owned, + /// `&self` + Ref, + /// `&mut self` + RefMut, +} + +#[derive(Debug, Clone)] +pub struct Ident { + pub name: String, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Type { + pub kind: TypeKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum TypeKind { + Int, + Float, + Bool, + Str, + Unit, + /// A user-defined class name. + Named(String), + /// `[T]` + List(Box), + /// `&T` + Ref(Box), + /// `&mut T` + RefMut(Box), +} + +#[derive(Debug, Clone)] +pub struct Block { + pub stmts: Vec, + /// Trailing expression without a semicolon. Only meaningful for blocks + /// in expression position (`if` branches, §10); in a function body the + /// parser accepts it, and later phases reject it with "add `return`" + /// (§6) — parsing it keeps the error message good. + pub tail: Option>, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Stmt { + pub kind: StmtKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum StmtKind { + /// `let [mut] name [: ty] = value;` + Let { + mutable: bool, + name: Ident, + ty: Option, + value: Expr, + }, + /// `lhs = value;` — lhs must be a place (name, field, index, deref). + Assign { + target: Expr, + value: Expr, + }, + /// An expression used for effect: `c.increment();` + Expr(Expr), + /// `return;` / `return expr;` + Return(Option), + /// `if` used as a statement (may still have else-if chains). + If(IfExpr), + While { + cond: Expr, + body: Block, + }, + /// `for pat in iterable { ... }` + For { + var: Ident, + iterable: Expr, + body: Block, + }, + Loop { + body: Block, + }, + Break, + Continue, +} + +#[derive(Debug, Clone)] +pub struct IfExpr { + pub cond: Box, + pub then_block: Block, + /// `else { ... }` or `else if ...` (nested IfExpr in a one-stmt block). + pub else_block: Option, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct Expr { + pub kind: ExprKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum ExprKind { + Int(i64), + Float(f64), + Bool(bool), + /// Interpolated string: parsed segments. + Str(Vec), + /// A variable reference, including `self`. + Var(String), + /// `[a, b, c]` + List(Vec), + /// `start..end` (half-open) + Range { + start: Box, + end: Box, + }, + Unary { + op: UnaryOp, + operand: Box, + }, + Binary { + op: BinaryOp, + lhs: Box, + rhs: Box, + }, + /// `callee(args)` — callee is a plain function name in v1. + Call { + callee: Ident, + args: Vec, + }, + /// `Class::assoc(args)` — the only place `::` appears (§7). + AssocCall { + class: Ident, + func: Ident, + args: Vec, + }, + /// `recv.method(args)` + MethodCall { + recv: Box, + method: Ident, + args: Vec, + }, + /// `recv.field` + FieldAccess { + recv: Box, + field: Ident, + }, + /// `list[index]` + Index { + recv: Box, + index: Box, + }, + /// `ClassName { field: value, ... }` + ClassLiteral { + class: Ident, + fields: Vec<(Ident, Expr)>, + }, + /// `if` in expression position — both branches required (§10). + If(IfExpr), +} + +#[derive(Debug, Clone)] +pub enum StrPart { + Text(String), + Expr(Box), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + /// `-x` + Neg, + /// `not x` + Not, + /// `&x` + Ref, + /// `&mut x` + RefMut, + /// `*x` + Deref, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + Add, + Sub, + Mul, + Div, + Rem, + Eq, + NotEq, + Lt, + LtEq, + Gt, + GtEq, + And, + Or, +} + +impl UnaryOp { + pub fn symbol(self) -> &'static str { + match self { + UnaryOp::Neg => "-", + UnaryOp::Not => "not", + UnaryOp::Ref => "&", + UnaryOp::RefMut => "&mut", + UnaryOp::Deref => "*", + } + } +} + +impl BinaryOp { + pub fn symbol(self) -> &'static str { + match self { + BinaryOp::Add => "+", + BinaryOp::Sub => "-", + BinaryOp::Mul => "*", + BinaryOp::Div => "/", + BinaryOp::Rem => "%", + BinaryOp::Eq => "==", + BinaryOp::NotEq => "!=", + BinaryOp::Lt => "<", + BinaryOp::LtEq => "<=", + BinaryOp::Gt => ">", + BinaryOp::GtEq => ">=", + BinaryOp::And => "and", + BinaryOp::Or => "or", + } + } +} diff --git a/src/borrowck.rs b/src/borrowck.rs new file mode 100644 index 0000000..8be67c6 --- /dev/null +++ b/src/borrowck.rs @@ -0,0 +1,744 @@ +//! The borrow checker (design doc §8). +//! +//! Runs after type checking, over the typed IR. Each function is +//! flattened into a linear event stream (with structured markers for +//! branches and loops), then analyzed: +//! +//! 1. **Liveness pre-pass** — for every reference-typed local, find its +//! last use. A borrow held by a reference ends at that reference's +//! last use, not at scope end (§8.2's non-lexical rule). A use inside +//! a loop that the reference was created *before* extends the borrow +//! to the whole loop (the next iteration will read it again). +//! 2. **State walk** — simulate the events tracking, per local: moved-ness +//! and, for references, the set of variables they borrow from +//! (provenance). At each event the §8 laws are enforced: no use after +//! move, no move/write while borrowed, aliasing XOR mutation. +//! +//! Branches are analyzed on both arms and merged conservatively (moved in +//! either arm counts as moved). Loop bodies are analyzed, merged with the +//! entry state, and re-analyzed when anything changed — catching +//! iteration-carried use-after-move. + +use crate::diagnostics::Diagnostic; +use crate::span::Span; +use crate::tir::*; +use crate::ty::Ty; +use std::collections::{HashMap, HashSet}; + +pub fn borrowck(program: &TProgram) -> Result<(), Diagnostic> { + for f in &program.funcs { + check_func(f)?; + } + Ok(()) +} + +#[derive(Debug, Clone)] +enum Ev { + /// Read of a variable that is not a move (Copy value or reference). + Use { + slot: usize, + span: Span, + }, + Move { + slot: usize, + span: Span, + }, + /// A borrow of `root` (or, if `root` is itself a reference, of + /// everything it points at). `dest` is the reference local this + /// borrow flows into, if any; otherwise it is a temporary borrow + /// lasting until the end of the statement. + Borrow { + root: usize, + mutable: bool, + dest: Option, + span: Span, + }, + /// Copying a reference (`let r2 = r;`, passing `r` along). + RefCopy { + from: usize, + dest: Option, + span: Span, + }, + /// A local comes into existence (Let / loop variable). + Decl { + slot: usize, + }, + /// Whole-variable assignment. + Write { + slot: usize, + span: Span, + }, + /// Assignment through a projection (`x.f = ..`, `xs[i] = ..`); + /// `through_ref` when the path goes through a reference root. + WriteProj { + root: usize, + through_ref: bool, + span: Span, + }, + StmtEnd, + IfStart, + IfElse, + IfEnd, + LoopStart, + LoopEnd, +} + +// ── flattening ────────────────────────────────────────────────────── + +struct Flattener<'a> { + f: &'a TFunc, + events: Vec, +} + +impl<'a> Flattener<'a> { + fn is_ref(&self, slot: usize) -> bool { + self.f.locals[slot].ty.is_ref() + } + + fn block(&mut self, b: &TBlock) { + for s in &b.stmts { + self.stmt(s); + } + if let Some(tail) = &b.tail { + self.expr(tail, None); + self.events.push(Ev::StmtEnd); + } + } + + /// Like `block`, but the tail expression's borrows flow into `dest` + /// (for `let r = if .. { &x } else { &y };`). + fn value_block(&mut self, b: &TBlock, dest: Option) { + for s in &b.stmts { + self.stmt(s); + } + if let Some(tail) = &b.tail { + self.expr(tail, dest); + } + } + + fn stmt(&mut self, s: &TStmt) { + match s { + TStmt::Let { slot, value } => { + let dest = self.is_ref(*slot).then_some(*slot); + self.expr(value, dest); + self.events.push(Ev::Decl { slot: *slot }); + self.events.push(Ev::StmtEnd); + } + TStmt::AssignPlace { place, value, span } => { + let bare = place.projs.is_empty(); + let dest = (bare && self.is_ref(place.root)).then_some(place.root); + self.expr(value, dest); + self.place_projs(place); + if bare { + self.events.push(Ev::Write { + slot: place.root, + span: *span, + }); + } else { + let through_ref = self.is_ref(place.root); + self.events.push(Ev::WriteProj { + root: place.root, + through_ref, + span: *span, + }); + } + self.events.push(Ev::StmtEnd); + } + TStmt::Expr(e) => { + self.expr(e, None); + self.events.push(Ev::StmtEnd); + } + TStmt::If { + cond, + then_b, + else_b, + } => { + self.expr(cond, None); + self.events.push(Ev::StmtEnd); + self.events.push(Ev::IfStart); + self.block(then_b); + self.events.push(Ev::IfElse); + if let Some(else_b) = else_b { + self.block(else_b); + } + self.events.push(Ev::IfEnd); + } + TStmt::While { cond, body } => { + self.events.push(Ev::LoopStart); + self.expr(cond, None); + self.events.push(Ev::StmtEnd); + self.block(body); + self.events.push(Ev::LoopEnd); + } + TStmt::Loop { body, .. } => { + self.events.push(Ev::LoopStart); + self.block(body); + self.events.push(Ev::LoopEnd); + } + TStmt::ForRange { + var_slot, + end_slot, + start, + end, + body, + } => { + self.expr(start, None); + self.expr(end, None); + self.events.push(Ev::Decl { slot: *end_slot }); + self.events.push(Ev::StmtEnd); + self.events.push(Ev::LoopStart); + self.events.push(Ev::Decl { slot: *var_slot }); + self.block(body); + self.events.push(Ev::LoopEnd); + } + TStmt::ForList { + var_slot, + list_slot, + idx_slot, + list, + by_ref, + body, + } => { + let dest = self.is_ref(*list_slot).then_some(*list_slot); + self.expr(list, dest); + self.events.push(Ev::Decl { slot: *list_slot }); + self.events.push(Ev::Decl { slot: *idx_slot }); + self.events.push(Ev::StmtEnd); + self.events.push(Ev::LoopStart); + // Each iteration reads the list and rebinds the variable. + self.events.push(Ev::Use { + slot: *list_slot, + span: list.span, + }); + if *by_ref && self.is_ref(*var_slot) { + self.events.push(Ev::RefCopy { + from: *list_slot, + dest: Some(*var_slot), + span: list.span, + }); + } + self.events.push(Ev::Decl { slot: *var_slot }); + self.block(body); + self.events.push(Ev::LoopEnd); + } + TStmt::Break(_) | TStmt::Continue(_) => {} + TStmt::Return(value) => { + if let Some(v) = value { + self.expr(v, None); + } + self.events.push(Ev::StmtEnd); + } + } + } + + /// Emit the index-expression events of a place's projections. + fn place_projs(&mut self, place: &TPlace) { + for p in &place.projs { + if let TProj::Index(idx) = p { + self.expr(idx, None); + } + } + } + + fn expr(&mut self, e: &TExpr, dest: Option) { + match &e.kind { + TExprKind::LoadPlace { place, mode } => { + self.place_projs(place); + let root = place.root; + match mode { + PlaceMode::Copy => { + if self.is_ref(root) { + self.events.push(Ev::RefCopy { + from: root, + dest, + span: place.span, + }); + } else { + self.events.push(Ev::Use { + slot: root, + span: place.span, + }); + } + } + PlaceMode::Move => { + self.events.push(Ev::Move { + slot: root, + span: place.span, + }); + } + PlaceMode::Borrow => { + self.events.push(Ev::Borrow { + root, + mutable: false, + dest, + span: place.span, + }); + } + PlaceMode::BorrowMut => { + self.events.push(Ev::Borrow { + root, + mutable: true, + dest, + span: place.span, + }); + } + } + } + TExprKind::BorrowTemp { value, .. } => self.expr(value, None), + TExprKind::Deref(inner) => self.expr(inner, dest), + TExprKind::GetFieldTemp { recv, .. } => self.expr(recv, None), + TExprKind::IndexTemp { recv, index } => { + self.expr(recv, None); + self.expr(index, None); + } + TExprKind::Str(parts) => { + for p in parts { + if let TStrPart::Expr(inner) = p { + self.expr(inner, None); + } + } + } + TExprKind::List(items) | TExprKind::NewObject(items) => { + for i in items { + self.expr(i, None); + } + } + TExprKind::NegI(i) + | TExprKind::NegF(i) + | TExprKind::Not(i) + | TExprKind::IntToFloat(i) + | TExprKind::FloatToInt(i) + | TExprKind::ToStr(i) + | TExprKind::Len(i) + | TExprKind::CloneVal(i) + | TExprKind::Sqrt(i) + | TExprKind::Println(i) + | TExprKind::Panic(i) => self.expr(i, None), + TExprKind::Binary { lhs, rhs, .. } | TExprKind::Logic { lhs, rhs, .. } => { + self.expr(lhs, None); + self.expr(rhs, None); + } + TExprKind::Assert { cond, msg } => { + self.expr(cond, None); + self.expr(msg, None); + } + TExprKind::CallUser { args, .. } => { + for a in args { + self.expr(a, None); + } + } + TExprKind::If { + cond, + then_b, + else_b, + } => { + self.expr(cond, None); + self.events.push(Ev::IfStart); + self.value_block(then_b, dest); + self.events.push(Ev::IfElse); + self.value_block(else_b, dest); + self.events.push(Ev::IfEnd); + } + TExprKind::Int(_) | TExprKind::Float(_) | TExprKind::Bool(_) => {} + } + } +} + +// ── analysis ──────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq)] +enum MoveState { + Valid, + Moved(Span), +} + +#[derive(Clone)] +struct State { + moves: Vec, + declared: Vec, + /// For reference locals: the set of (non-reference) variables they + /// borrow from, plus where the borrow was created. + prov: Vec>, + borrow_span: Vec>, +} + +impl State { + fn merge(&mut self, other: &State) { + for i in 0..self.moves.len() { + if let MoveState::Moved(s) = other.moves[i] + && self.moves[i] == MoveState::Valid + { + self.moves[i] = MoveState::Moved(s); + } + self.declared[i] |= other.declared[i]; + let extra: Vec = other.prov[i].difference(&self.prov[i]).copied().collect(); + self.prov[i].extend(extra); + if self.borrow_span[i].is_none() { + self.borrow_span[i] = other.borrow_span[i]; + } + } + } +} + +struct Analyzer<'a> { + f: &'a TFunc, + events: &'a [Ev], + /// Matching close-marker index for each IfStart/LoopStart. + matches: HashMap, // IfStart -> (IfElse, IfEnd); LoopStart -> (end, end) + /// For each ref slot: last event index where it is used. + last_use: Vec, + state: State, + /// Temporary borrows within the current statement. + temps: Vec<(HashSet, bool, Span, Option)>, +} + +fn check_func(f: &TFunc) -> Result<(), Diagnostic> { + let mut fl = Flattener { + f, + events: Vec::new(), + }; + fl.block(&f.body); + let events = fl.events; + + // Match markers. + let mut matches: HashMap = HashMap::new(); + { + let mut stack: Vec = Vec::new(); + let mut else_of: HashMap = HashMap::new(); + for (i, ev) in events.iter().enumerate() { + match ev { + Ev::IfStart | Ev::LoopStart => stack.push(i), + Ev::IfElse => { + let open = *stack.last().expect("balanced markers"); + else_of.insert(open, i); + } + Ev::IfEnd | Ev::LoopEnd => { + let open = stack.pop().expect("balanced markers"); + let mid = else_of.get(&open).copied().unwrap_or(i); + matches.insert(open, (mid, i)); + } + _ => {} + } + } + } + + // Liveness pre-pass for reference locals: last use, extended to the + // end of any loop the reference was created before. + let num = f.locals.len(); + let mut decl_at = vec![usize::MAX; num]; + let mut raw_uses: Vec<(usize, usize)> = Vec::new(); // (slot, event idx) + let mut loop_stack: Vec = Vec::new(); + let mut loop_extents: Vec<(usize, usize)> = Vec::new(); + for (i, ev) in events.iter().enumerate() { + match ev { + Ev::LoopStart => loop_stack.push(i), + Ev::LoopEnd => { + let s = loop_stack.pop().expect("balanced loops"); + loop_extents.push((s, i)); + } + Ev::Decl { slot } => { + if decl_at[*slot] == usize::MAX { + decl_at[*slot] = i; + } + } + Ev::Use { slot, .. } | Ev::Move { slot, .. } | Ev::Write { slot, .. } => { + raw_uses.push((*slot, i)); + } + Ev::RefCopy { from, dest, .. } => { + raw_uses.push((*from, i)); + if let Some(d) = dest { + raw_uses.push((*d, i)); + } + } + Ev::Borrow { root, dest, .. } => { + raw_uses.push((*root, i)); + if let Some(d) = dest { + raw_uses.push((*d, i)); + } + } + Ev::WriteProj { root, .. } => raw_uses.push((*root, i)), + _ => {} + } + } + let mut last_use = vec![0usize; num]; + for (slot, at) in &raw_uses { + let mut effective = *at; + for (ls, le) in &loop_extents { + // Used inside a loop the variable was declared before: the + // next iteration reads it again, so it stays live to the end. + if at > ls && at < le && decl_at[*slot] < *ls { + effective = effective.max(*le); + } + } + last_use[*slot] = last_use[*slot].max(effective); + } + + let state = State { + moves: vec![MoveState::Valid; num], + declared: (0..num).map(|s| s < f.num_params).collect(), + prov: vec![HashSet::new(); num], + borrow_span: vec![None; num], + }; + let mut an = Analyzer { + f, + events: &events, + matches, + last_use, + state, + temps: Vec::new(), + }; + an.run(0, events.len())?; + Ok(()) +} + +impl<'a> Analyzer<'a> { + fn name(&self, slot: usize) -> &str { + &self.f.locals[slot].name + } + + fn is_ref(&self, slot: usize) -> bool { + self.f.locals[slot].ty.is_ref() + } + + fn is_mut_ref(&self, slot: usize) -> bool { + matches!(self.f.locals[slot].ty, Ty::RefMut(_)) + } + + /// The variables a borrow of `root` actually loans: `root` itself, or + /// what it points at when `root` is a reference. + fn effective_roots(&self, root: usize) -> HashSet { + if self.is_ref(root) { + self.state.prov[root].clone() + } else { + let mut s = HashSet::new(); + s.insert(root); + s + } + } + + /// All loans covering `target` at event `i`, excluding those held by + /// `exempt` (a reference re-used through itself). + fn loans_on( + &self, + target: usize, + i: usize, + exempt: Option, + ) -> Vec<(bool, Span, Option)> { + let mut found = Vec::new(); + for slot in 0..self.f.locals.len() { + if !self.is_ref(slot) || !self.state.declared[slot] || Some(slot) == exempt { + continue; + } + if i <= self.last_use[slot] && self.state.prov[slot].contains(&target) { + found.push(( + self.is_mut_ref(slot), + self.state.borrow_span[slot].unwrap_or(self.f.locals[slot].span), + Some(slot), + )); + } + } + for (roots, mutable, span, holder) in &self.temps { + if *holder == exempt && exempt.is_some() { + continue; + } + if roots.contains(&target) { + found.push((*mutable, *span, *holder)); + } + } + found + } + + fn run(&mut self, mut i: usize, end: usize) -> Result<(), Diagnostic> { + while i < end { + match &self.events[i] { + Ev::Use { slot, span } => { + self.check_not_moved(*slot, *span, "use")?; + if !self.is_ref(*slot) { + for (mutable, bspan, _) in self.loans_on(*slot, i, None) { + if mutable { + return Err(Diagnostic::error( + format!( + "cannot use `{}` while it is mutably borrowed", + self.name(*slot) + ), + *span, + ) + .with_related("the `&mut` borrow happens here", bspan) + .with_help("finish using the borrow first, or narrow its scope")); + } + } + } + } + Ev::Move { slot, span } => { + self.check_not_moved(*slot, *span, "move")?; + let loans = self.loans_on(*slot, i, None); + if let Some((_, bspan, _)) = loans.first() { + return Err(Diagnostic::error( + format!("cannot move `{}` while it is borrowed", self.name(*slot)), + *span, + ) + .with_related("the borrow happens here", *bspan) + .with_help("finish using the borrow first, or clone the value")); + } + self.state.moves[*slot] = MoveState::Moved(*span); + } + Ev::Borrow { + root, + mutable, + dest, + span, + } => { + self.check_not_moved(*root, *span, "borrow")?; + let eff = self.effective_roots(*root); + let exempt = if self.is_ref(*root) { + Some(*root) + } else { + None + }; + for target in &eff { + let loans = self.loans_on(*target, i, exempt); + for (existing_mut, bspan, _) in &loans { + if *mutable || *existing_mut { + let (msg, label) = if *mutable { + ( + format!( + "cannot borrow `{}` as mutable: it is already borrowed", + self.name(*target) + ), + "the other borrow happens here", + ) + } else { + ( + format!( + "cannot borrow `{}`: it is already mutably borrowed", + self.name(*target) + ), + "the `&mut` borrow happens here", + ) + }; + return Err(Diagnostic::error(msg, *span) + .with_related(label, *bspan) + .with_help( + "a value can have many `&` borrows or one `&mut` borrow, never both (aliasing XOR mutation)", + )); + } + } + } + match dest { + Some(d) => { + self.state.prov[*d].extend(eff.iter().copied()); + if self.state.borrow_span[*d].is_none() { + self.state.borrow_span[*d] = Some(*span); + } + } + None => self.temps.push((eff, *mutable, *span, exempt)), + } + } + Ev::RefCopy { from, dest, span } => { + self.check_not_moved(*from, *span, "use")?; + if let Some(d) = dest { + let roots = self.state.prov[*from].clone(); + self.state.prov[*d].extend(roots); + if self.state.borrow_span[*d].is_none() { + self.state.borrow_span[*d] = + self.state.borrow_span[*from].or(Some(*span)); + } + } + } + Ev::Decl { slot } => { + self.state.declared[*slot] = true; + self.state.moves[*slot] = MoveState::Valid; + } + Ev::Write { slot, span } => { + for (_, bspan, holder) in self.loans_on(*slot, i, Some(*slot)) { + if holder != Some(*slot) { + return Err(Diagnostic::error( + format!( + "cannot assign to `{}` while it is borrowed", + self.name(*slot) + ), + *span, + ) + .with_related("the borrow happens here", bspan) + .with_help("finish using the borrow before assigning")); + } + } + self.state.moves[*slot] = MoveState::Valid; + } + Ev::WriteProj { + root, + through_ref, + span, + } => { + self.check_not_moved(*root, *span, "modify")?; + let (targets, exempt) = if *through_ref { + (self.effective_roots(*root), Some(*root)) + } else { + (self.effective_roots(*root), None) + }; + for target in &targets { + for (_, bspan, holder) in self.loans_on(*target, i, exempt) { + if holder == Some(*root) { + continue; + } + return Err(Diagnostic::error( + format!( + "cannot modify `{}` while it is borrowed", + self.name(*target) + ), + *span, + ) + .with_related("the borrow happens here", bspan) + .with_help("finish using the borrow before modifying the value")); + } + } + } + Ev::StmtEnd => self.temps.clear(), + Ev::IfStart => { + let (mid, close) = self.matches[&i]; + let snapshot = self.state.clone(); + self.run(i + 1, mid)?; + let then_state = self.state.clone(); + self.state = snapshot; + self.run(mid + 1, close)?; + self.state.merge(&then_state); + i = close; + } + Ev::LoopStart => { + let (_, close) = self.matches[&i]; + let entry = self.state.clone(); + self.run(i + 1, close)?; + let mut merged = entry.clone(); + merged.merge(&self.state); + if !states_eq(&merged, &entry) { + self.state = merged; + self.run(i + 1, close)?; + } + i = close; + } + Ev::IfElse | Ev::IfEnd | Ev::LoopEnd => {} + } + i += 1; + } + Ok(()) + } + + fn check_not_moved(&self, slot: usize, span: Span, verb: &str) -> Result<(), Diagnostic> { + if let MoveState::Moved(mspan) = self.state.moves[slot] { + let name = self.name(slot); + return Err(Diagnostic::error( + format!("cannot {verb} `{name}`: it was moved"), + span, + ) + .with_related(format!("`{name}` was moved here"), mspan) + .with_help(format!( + "after a move the original is gone (§8.1); clone it instead: `{name}.clone()`" + ))); + } + Ok(()) + } +} + +fn states_eq(a: &State, b: &State) -> bool { + a.moves == b.moves && a.declared == b.declared && a.prov == b.prov +} diff --git a/src/bytecode.rs b/src/bytecode.rs new file mode 100644 index 0000000..57a54dc --- /dev/null +++ b/src/bytecode.rs @@ -0,0 +1,188 @@ +//! Bytecode for the stack-based Oxide VM (design doc §13). +//! +//! Every instruction carries a parallel source span (`Chunk::spans`) so +//! runtime panics point at real source — the §8.4 diagnostics contract +//! applies to runtime errors too. + +use crate::span::Span; +use crate::value::Value; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Op { + /// Push `consts[idx]`. + Const(u16), + Pop, + Dup, + LoadLocal(u16), + StoreLocal(u16), + /// Unconditional jump to an absolute instruction index. + Jump(u32), + /// Pop a bool; jump when false. + JumpIfFalse(u32), + + // Integer arithmetic (checked: overflow and /0 panic). + AddI, + SubI, + MulI, + DivI, + RemI, + NegI, + + // Float arithmetic. + AddF, + SubF, + MulF, + DivF, + RemF, + NegF, + + /// Pop two strings, push their concatenation. + Concat, + /// Pop N values (all strings), push the concatenation — interpolation. + ConcatN(u16), + + Not, + /// Same-type equality. + Eq, + Ne, + LtI, + LeI, + GtI, + GeI, + LtF, + LeF, + GtF, + GeF, + + IntToFloat, + /// Truncate toward zero; panics if the float is out of `int` range. + FloatToInt, + ToStr, + + /// Pop N values, push a list of them. + MakeList(u16), + /// Pop N field values, push a new object. + MakeObject(u16), + /// Pop an object, push its field at the index. + GetField(u16), + /// Pop value then object; store the value into the object's field. + SetField(u16), + /// Pop index then list, push the element. + Index, + /// Pop value, index, then list; store the value into the element. + SetIndex, + /// Pop a list or string, push its length. + Len, + /// Pop a value, push a fully independent deep copy (`.clone()`). + CloneVal, + /// Pop a float, push its square root (`.sqrt()`). + SqrtF, + + /// Pop a string, print it followed by a newline. + Print, + /// Pop message (string) then condition (bool); panic when false. + Assert, + /// Pop a string, panic with it. + Panic, + + /// Push `()`. + Unit, + /// Call `funcs[idx]`: pops its arguments into the new frame's locals. + Call(u16), + /// Pop the return value, pop the frame, push the value for the caller. + Return, + Halt, +} + +/// A whole compiled program: one chunk per function. +#[derive(Debug, Default)] +pub struct CompiledProgram { + pub funcs: Vec, + /// Index of `main` in `funcs`. + pub main: usize, +} + +impl CompiledProgram { + pub fn disassemble(&self) -> String { + let mut out = String::new(); + for (i, chunk) in self.funcs.iter().enumerate() { + let main_note = if i == self.main { + " ; entry point" + } else { + "" + }; + out.push_str(&format!("\nfunc {} (#{i}){main_note}\n", chunk.name)); + out.push_str(&chunk.disassemble()); + } + out + } +} + +#[derive(Debug, Default)] +pub struct Chunk { + pub name: String, + pub code: Vec, + /// Source span for each instruction, parallel to `code`. + pub spans: Vec, + pub consts: Vec, + /// Parameters occupy local slots `0..num_params`. + pub num_params: usize, + pub num_locals: usize, +} + +impl Chunk { + pub fn emit(&mut self, op: Op, span: Span) -> usize { + self.code.push(op); + self.spans.push(span); + self.code.len() - 1 + } + + pub fn add_const(&mut self, value: Value) -> u16 { + // Reuse identical constants (floats compare by bits via PartialEq + // being fine here: NaN consts never dedupe, which is harmless). + if let Some(i) = self.consts.iter().position(|c| c == &value) { + return i as u16; + } + let idx = self.consts.len(); + assert!(idx <= u16::MAX as usize, "constant pool overflow"); + self.consts.push(value); + idx as u16 + } + + pub fn patch_jump(&mut self, at: usize) { + let target = self.code.len() as u32; + match &mut self.code[at] { + Op::Jump(t) | Op::JumpIfFalse(t) => *t = target, + other => panic!("patch_jump on non-jump {other:?}"), + } + } + + /// Human-readable disassembly (used by `oxide build`). + pub fn disassemble(&self) -> String { + let mut out = String::new(); + out.push_str(&format!( + "; {} params, {} locals, {} constants\n", + self.num_params, + self.num_locals, + self.consts.len() + )); + for (i, c) in self.consts.iter().enumerate() { + out.push_str(&format!("; const #{i} = {}\n", preview(c))); + } + for (i, op) in self.code.iter().enumerate() { + let extra = match op { + Op::Const(idx) => format!(" ; {}", preview(&self.consts[*idx as usize])), + _ => String::new(), + }; + out.push_str(&format!("{i:04} {op:?}{extra}\n")); + } + out + } +} + +fn preview(v: &Value) -> String { + match v { + Value::Str(s) => format!("{s:?}"), + other => other.display(), + } +} diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..6923fe2 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,2162 @@ +//! Type checker: lowers the AST to typed IR (milestones 2–5: functions, +//! loops, classes, references, variables, expressions, built-ins). +//! +//! Classes are lowered structurally: each method becomes a plain function +//! named `Class::method` whose local slot 0 is `self`, and field access +//! becomes indexed access in declaration order. +//! +//! Milestone 5 adds places and access modes: every variable access (or a +//! projection from one — fields, elements, derefs) lowers to `LoadPlace` +//! with a mode (copy / move / borrow / borrow-mut) chosen by context: +//! read-only uses (interpolation, `==`, `len()`, `&self` receivers) +//! borrow, everything else copies or moves by type. The borrow checker +//! (borrowck.rs) enforces §8's rules over those modes; this file enforces +//! the structural rules: no references inside lists or class fields, no +//! references to references, no returning references, no moving out of +//! fields/elements/references (with `.clone()` hints), and mutation paths +//! (`&mut` requires a mutable root or a `&mut` deref on the way). + +use crate::ast::{ + self, BinaryOp, Block, Expr, ExprKind, Item, Param, Program, SelfKind, Stmt, StmtKind, UnaryOp, +}; +use crate::diagnostics::Diagnostic; +use crate::span::Span; +use crate::tir::*; +use crate::ty::Ty; +use std::collections::{HashMap, HashSet}; + +const BUILTINS: &[&str] = &["println", "str", "int", "float", "assert", "panic"]; + +#[derive(Debug, Clone)] +struct FuncSig { + idx: usize, + /// `self` is not included in `params`. + params: Vec<(String, Ty)>, + ret: Ty, + /// Some(..) for methods; None for free and associated functions. + self_kind: Option, +} + +#[derive(Debug, Clone)] +struct ClassInfo { + /// Fields in declaration order. + fields: Vec<(String, Ty)>, + /// Methods and associated functions. + methods: HashMap, +} + +struct Env { + funcs: HashMap, + classes: HashMap, +} + +impl Env { + /// The `to_str(&self) -> string` method of a class, if it has a + /// conforming one (used for interpolation and `str()`). + fn to_str_method(&self, class: &str) -> Option<&FuncSig> { + let sig = self.classes.get(class)?.methods.get("to_str")?; + let conforms = matches!(sig.self_kind, Some(SelfKind::Ref)) + && sig.params.is_empty() + && sig.ret == Ty::Str; + conforms.then_some(sig) + } +} + +/// `self` kind (if any) plus the named parameters of a signature. +type LoweredParams = (Option, Vec<(String, Ty)>); + +enum Todo<'a> { + Free(&'a ast::Func), + Method { class: &'a str, func: &'a ast::Func }, +} + +/// Where a surface type appears — reference legality differs (§8.3). +#[derive(Clone, Copy, PartialEq)] +enum TypePos { + Param, + Return, + Local, + Field, + ListElem, +} + +pub fn check_program(program: &Program) -> Result { + // Pass 0: class names (field/param types may reference any class). + let mut class_names: HashSet = HashSet::new(); + for item in &program.items { + if let Item::Class(c) = item + && !class_names.insert(c.name.name.clone()) + { + return Err(Diagnostic::error( + format!("a class named `{}` is already defined", c.name.name), + c.name.span, + )); + } + } + + // Pass 1: signatures, assigning every function (free or method) an + // index in item order so call order never matters. + let mut env = Env { + funcs: HashMap::new(), + classes: HashMap::new(), + }; + let mut todo: Vec = Vec::new(); + for item in &program.items { + match item { + Item::Func(f) => { + let name = &f.name.name; + if BUILTINS.contains(&name.as_str()) { + return Err(Diagnostic::error( + format!("cannot define `{name}`: it is a built-in function"), + f.name.span, + )); + } + if env.funcs.contains_key(name) { + return Err(Diagnostic::error( + format!("a function named `{name}` is already defined"), + f.name.span, + )); + } + if class_names.contains(name) { + return Err(Diagnostic::error( + format!("`{name}` is already the name of a class"), + f.name.span, + )); + } + if name == "main" { + if !f.params.is_empty() { + return Err(Diagnostic::error( + "`main` cannot take parameters", + f.name.span, + )); + } + if let Some(rt) = &f.return_type { + return Err(Diagnostic::error("`main` cannot return a value", rt.span)); + } + } + let (self_kind, params) = lower_params(f, &class_names)?; + if self_kind.is_some() { + return Err(Diagnostic::error( + "`self` is only allowed in class methods", + f.name.span, + )); + } + let ret = lower_ret(f, &class_names)?; + env.funcs.insert( + name.clone(), + FuncSig { + idx: todo.len(), + params, + ret, + self_kind: None, + }, + ); + todo.push(Todo::Free(f)); + } + Item::Class(c) => { + let mut fields: Vec<(String, Ty)> = Vec::new(); + for field in &c.fields { + if fields.iter().any(|(n, _)| n == &field.name.name) { + return Err(Diagnostic::error( + format!( + "class `{}` already has a field named `{}`", + c.name.name, field.name.name + ), + field.name.span, + )); + } + fields.push(( + field.name.name.clone(), + lower_type(&field.ty, &class_names, TypePos::Field)?, + )); + } + let mut methods: HashMap = HashMap::new(); + for m in &c.methods { + if methods.contains_key(&m.name.name) { + return Err(Diagnostic::error( + format!( + "class `{}` already has a method named `{}`", + c.name.name, m.name.name + ), + m.name.span, + )); + } + let (self_kind, params) = lower_params(m, &class_names)?; + let ret = lower_ret(m, &class_names)?; + methods.insert( + m.name.name.clone(), + FuncSig { + idx: todo.len(), + params, + ret, + self_kind, + }, + ); + todo.push(Todo::Method { + class: &c.name.name, + func: m, + }); + } + env.classes + .insert(c.name.name.clone(), ClassInfo { fields, methods }); + } + } + } + + let Some(main_sig) = env.funcs.get("main") else { + return Err(Diagnostic::error( + "no `func main()` found — execution starts there", + Span::new(0, 0), + )); + }; + let main = main_sig.idx; + + // Pass 2: check bodies in index order. + let mut funcs = Vec::new(); + for t in &todo { + match t { + Todo::Free(f) => { + let sig = env.funcs[&f.name.name].clone(); + funcs.push(check_body(f, &f.name.name, &sig, None, &env)?); + } + Todo::Method { class, func } => { + let sig = env.classes[*class].methods[&func.name.name].clone(); + let mangled = format!("{class}::{}", func.name.name); + funcs.push(check_body(func, &mangled, &sig, Some(class), &env)?); + } + } + } + Ok(TProgram { funcs, main }) +} + +fn lower_params(f: &ast::Func, class_names: &HashSet) -> Result { + let mut self_kind = None; + let mut params = Vec::new(); + for p in &f.params { + match p { + Param::SelfParam { kind, .. } => self_kind = Some(*kind), + Param::Normal { name, ty, .. } => { + params.push(( + name.name.clone(), + lower_type(ty, class_names, TypePos::Param)?, + )); + } + } + } + Ok((self_kind, params)) +} + +fn lower_ret(f: &ast::Func, class_names: &HashSet) -> Result { + match &f.return_type { + Some(t) => lower_type(t, class_names, TypePos::Return), + None => Ok(Ty::Unit), + } +} + +fn lower_type( + t: &ast::Type, + class_names: &HashSet, + pos: TypePos, +) -> Result { + use ast::TypeKind::*; + match &t.kind { + Int => Ok(Ty::Int), + Float => Ok(Ty::Float), + Bool => Ok(Ty::Bool), + Str => Ok(Ty::Str), + Unit => Ok(Ty::Unit), + List(inner) => Ok(Ty::List(Box::new(lower_type( + inner, + class_names, + TypePos::ListElem, + )?))), + Named(name) => { + if class_names.contains(name) { + Ok(Ty::Class(name.clone())) + } else { + Err(Diagnostic::error(format!("unknown type `{name}`"), t.span) + .with_help(format!("define it first: `class {name} {{ ... }}`"))) + } + } + Ref(inner) | RefMut(inner) => { + let mutable = matches!(t.kind, RefMut(_)); + match pos { + TypePos::Return => { + return Err(Diagnostic::error( + "returning references is not supported yet", + t.span, + ) + .with_help("return an owned value instead (clone it if needed)")); + } + TypePos::Field => { + // The parser already rejects these; belt and braces. + return Err(Diagnostic::error( + "classes cannot hold references (§8.3)", + t.span, + )); + } + TypePos::ListElem => { + return Err(Diagnostic::error( + "lists own their elements — element types cannot be references", + t.span, + ) + .with_help("store owned values (clone them if needed)")); + } + TypePos::Param | TypePos::Local => {} + } + if matches!(inner.kind, Ref(_) | RefMut(_)) { + return Err(Diagnostic::error( + "references to references are not supported", + t.span, + ) + .with_help("use the reference directly")); + } + let inner_ty = lower_type(inner, class_names, TypePos::ListElem)?; + if mutable && inner_ty.is_copy() { + return Err(Diagnostic::error( + format!("`&mut {inner_ty}`: copyable values cannot be borrowed mutably"), + t.span, + ) + .with_help("take the value and return the new one instead")); + } + Ok(if mutable { + Ty::RefMut(Box::new(inner_ty)) + } else { + Ty::Ref(Box::new(inner_ty)) + }) + } + } +} + +fn check_body( + f: &ast::Func, + lowered_name: &str, + sig: &FuncSig, + class: Option<&str>, + env: &Env, +) -> Result { + let mut checker = Checker { + env, + scopes: vec![Vec::new()], + locals: Vec::new(), + ret: sig.ret.clone(), + loops: Vec::new(), + self_kind: sig.self_kind, + }; + if sig.self_kind.is_some() { + let class = class.expect("methods always have a class"); + checker.declare_full( + "self", + Ty::Class(class.to_string()), + false, + true, + f.name.span, + ); + } + for (pname, pty) in &sig.params { + checker.declare_full(pname, pty.clone(), false, true, f.name.span); + } + let body = checker.stmt_block(&f.body)?; + if checker.ret != Ty::Unit && !always_exits(&body) { + return Err(Diagnostic::error( + format!( + "not every path through `{lowered_name}` returns a `{}`", + checker.ret + ), + f.name.span, + ) + .with_help("add a `return ;` at the end (or make every branch return)")); + } + let num_params = sig.params.len() + usize::from(sig.self_kind.is_some()); + Ok(TFunc { + name: lowered_name.to_string(), + num_params, + num_locals: checker.locals.len(), + ret: sig.ret.clone(), + body, + span: f.span, + locals: checker.locals, + }) +} + +/// Does this block unconditionally leave the function (return, panic, or +/// loop forever)? +fn always_exits(b: &TBlock) -> bool { + b.stmts.iter().any(stmt_exits) +} + +fn stmt_exits(s: &TStmt) -> bool { + match s { + TStmt::Return(_) => true, + TStmt::Expr(e) => matches!(e.kind, TExprKind::Panic(_)), + TStmt::If { + then_b, + else_b: Some(else_b), + .. + } => always_exits(then_b) && always_exits(else_b), + TStmt::Loop { has_break, .. } => !*has_break, + _ => false, + } +} + +#[derive(Debug, Clone)] +struct ScopeEntry { + slot: usize, + name: String, +} + +/// Why a place cannot be mutated (or Ok if it can). +#[derive(Debug, Clone, PartialEq)] +enum MutBlock { + Ok, + NotMut(String), + IsParam(String), + SelfShared, + SharedRef, +} + +/// A lowered place plus everything needed to pick an access mode. +struct PlaceCk { + place: TPlace, + ty: Ty, + mut_block: MutBlock, + root_name: String, + has_deref: bool, + has_proj: bool, + root_is_self: bool, +} + +struct Checker<'a> { + env: &'a Env, + /// Lexical scopes, innermost last (name → slot). Slots are never + /// reused; shadowing gets a fresh slot. + scopes: Vec>, + /// Metadata per slot, indexed by slot number. + locals: Vec, + /// Current function's return type. + ret: Ty, + /// One entry per enclosing loop: has a `break` bound to it? + loops: Vec, + /// Some(..) when checking a method body. + self_kind: Option, +} + +impl<'a> Checker<'a> { + fn lookup(&self, name: &str) -> Option { + self.scopes + .iter() + .rev() + .flat_map(|s| s.iter().rev()) + .find(|l| l.name == name) + .map(|l| l.slot) + } + + fn meta(&self, slot: usize) -> &LocalMeta { + &self.locals[slot] + } + + fn declare(&mut self, name: &str, ty: Ty, mutable: bool, span: Span) -> usize { + self.declare_full(name, ty, mutable, false, span) + } + + fn declare_full( + &mut self, + name: &str, + ty: Ty, + mutable: bool, + is_param: bool, + span: Span, + ) -> usize { + let slot = self.locals.len(); + self.locals.push(LocalMeta { + name: name.to_string(), + ty, + mutable, + is_param, + span, + }); + self.scopes + .last_mut() + .expect("scope stack is never empty inside a function") + .push(ScopeEntry { + slot, + name: name.to_string(), + }); + slot + } + + /// A slot for a compiler-internal temporary; `<...>` can't collide + /// with user identifiers. + fn hidden_slot(&mut self, purpose: &str, ty: Ty, span: Span) -> usize { + self.declare(&format!("<{purpose}>"), ty, true, span) + } + + // ── places ─────────────────────────────────────────────────────── + + /// Lower a place expression (a variable, or field/index/deref + /// projections from one). Returns None when `e` is not a place. + fn lower_place(&mut self, e: &Expr) -> Result, Diagnostic> { + match &e.kind { + ExprKind::Var(name) => { + let Some(slot) = self.lookup(name) else { + return Err(unknown_variable(name, e.span)); + }; + let meta = self.meta(slot).clone(); + let is_self = name == "self"; + let mut_block = if is_self { + match self.self_kind { + Some(SelfKind::RefMut | SelfKind::Owned) => MutBlock::Ok, + Some(SelfKind::Ref) => MutBlock::SelfShared, + None => unreachable!("`self` resolved outside a method"), + } + } else if meta.is_param { + MutBlock::IsParam(name.clone()) + } else if !meta.mutable { + MutBlock::NotMut(name.clone()) + } else { + MutBlock::Ok + }; + Ok(Some(PlaceCk { + place: TPlace { + root: slot, + projs: Vec::new(), + span: e.span, + }, + ty: meta.ty, + mut_block, + root_name: name.clone(), + has_deref: false, + has_proj: false, + root_is_self: is_self, + })) + } + ExprKind::FieldAccess { recv, field } => { + let Some(mut p) = self.lower_place(recv)? else { + return Ok(None); + }; + self.auto_deref(&mut p); + let Ty::Class(cname) = p.ty.clone() else { + return Err(Diagnostic::error( + format!("`{}` has no fields — it is not a class", p.ty), + field.span, + )); + }; + let (idx, fty) = self.field_of(&cname, field)?; + p.place.projs.push(TProj::Field(idx)); + p.place.span = e.span; + p.ty = fty; + p.has_proj = true; + Ok(Some(p)) + } + ExprKind::Index { recv, index } => { + let Some(mut p) = self.lower_place(recv)? else { + return Ok(None); + }; + self.auto_deref(&mut p); + let Ty::List(elem) = p.ty.clone() else { + return Err(Diagnostic::error( + format!("`{}` cannot be indexed — only lists can", p.ty), + e.span, + )); + }; + let index = self.expect_ty(index, &Ty::Int, "list indices are `int`")?; + p.place.projs.push(TProj::Index(index)); + p.place.span = e.span; + p.ty = *elem; + p.has_proj = true; + Ok(Some(p)) + } + ExprKind::Unary { + op: UnaryOp::Deref, + operand, + } => { + let Some(mut p) = self.lower_place(operand)? else { + return Ok(None); + }; + if !p.ty.is_ref() { + return Err(Diagnostic::error( + format!("cannot dereference a `{}` — it is not a reference", p.ty), + e.span, + )); + } + self.auto_deref(&mut p); + p.place.span = e.span; + p.has_proj = true; + Ok(Some(p)) + } + _ => Ok(None), + } + } + + /// Look through a reference: push a Deref projection and update the + /// mutation path (`&mut` re-enables mutation; `&` blocks it). + fn auto_deref(&self, p: &mut PlaceCk) { + while p.ty.is_ref() { + let mutable = matches!(p.ty, Ty::RefMut(_)); + p.ty = p.ty.deref_target().clone(); + p.place.projs.push(TProj::Deref); + p.has_deref = true; + p.has_proj = true; + p.mut_block = if mutable { + MutBlock::Ok + } else { + MutBlock::SharedRef + }; + } + } + + fn require_mut(&self, p: &PlaceCk, action: &str, span: Span) -> Result<(), Diagnostic> { + match &p.mut_block { + MutBlock::Ok => Ok(()), + MutBlock::NotMut(name) => Err(Diagnostic::error( + format!("cannot {action}: `{name}` is not mutable"), + span, + ) + .with_help(format!("declare it with `let mut {name} = ...`"))), + MutBlock::IsParam(name) => Err(Diagnostic::error( + format!("cannot {action}: `{name}` is a parameter"), + span, + ) + .with_help(format!( + "parameters are immutable; copy it first: `let mut {name} = {name};` — or take `&mut {}`", + self.meta(self.lookup(name).expect("param exists")).ty + ))), + MutBlock::SelfShared => Err(Diagnostic::error( + format!("cannot {action} through `&self`"), + span, + ) + .with_help("take `&mut self` in this method")), + MutBlock::SharedRef => Err(Diagnostic::error( + format!("cannot {action} through a shared `&` reference"), + span, + ) + .with_help("take a `&mut` reference instead")), + } + } + + /// Read a place as a value: copy if Copy, move if a bare owned root, + /// error (with a clone hint) when moving out of a projection. + fn read_place(&self, p: PlaceCk, span: Span) -> Result { + if p.ty.is_copy() { + return Ok(TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode: PlaceMode::Copy, + }, + span, + }); + } + if !p.has_proj { + if p.root_is_self && matches!(self.self_kind, Some(SelfKind::Ref | SelfKind::RefMut)) { + return Err( + Diagnostic::error("cannot move `self` out of a borrowed method", span) + .with_help("clone what you need: `self.clone()` or a field's `.clone()`"), + ); + } + return Ok(TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode: PlaceMode::Move, + }, + span, + }); + } + let what = if p.has_deref { + "a reference".to_string() + } else if p.place.projs.iter().any(|pr| matches!(pr, TProj::Index(_))) { + "a list".to_string() + } else { + "a field".to_string() + }; + Err( + Diagnostic::error(format!("cannot move a `{}` out of {what}", p.ty), span) + .with_help("clone it instead: append `.clone()`"), + ) + } + + /// Check an expression in a read-only context (interpolation, `==`, + /// `len()`, `&self` receivers): places of owned types are borrowed + /// rather than moved. + fn expr_read_only(&mut self, e: &Expr) -> Result { + if let Some(p) = self.lower_place(e)? { + let mode = if p.ty.is_copy() || p.ty.is_ref() { + PlaceMode::Copy + } else { + PlaceMode::Borrow + }; + return Ok(TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode, + }, + span: e.span, + }); + } + self.expr(e) + } + + // ── blocks ─────────────────────────────────────────────────────── + + fn stmt_block(&mut self, block: &Block) -> Result { + self.scopes.push(Vec::new()); + let result = self.stmt_block_inner(block); + self.scopes.pop(); + result + } + + fn stmt_block_inner(&mut self, block: &Block) -> Result { + let mut stmts = Vec::new(); + for stmt in &block.stmts { + stmts.push(self.stmt(stmt)?); + } + if let Some(tail) = &block.tail { + if let ExprKind::If(if_expr) = &tail.kind { + let cond = self.bool_cond(&if_expr.cond)?; + let then_b = self.stmt_block(&if_expr.then_block)?; + let else_b = match &if_expr.else_block { + Some(b) => Some(self.stmt_block(b)?), + None => None, + }; + stmts.push(TStmt::If { + cond, + then_b, + else_b, + }); + } else { + let checked = self.expr(tail)?; + if checked.ty == Ty::Unit { + return Err(Diagnostic::error( + "missing `;` after this statement", + checked.span, + )); + } + return Err(Diagnostic::error( + "this expression's value goes nowhere", + checked.span, + ) + .with_help( + "Oxide has no implicit return: write `return ;` to return it, or add `;` to discard it", + )); + } + } + Ok(TBlock { stmts, tail: None }) + } + + fn value_block(&mut self, block: &Block) -> Result<(TBlock, Ty, Span), Diagnostic> { + self.scopes.push(Vec::new()); + let result = (|| { + let mut stmts = Vec::new(); + for stmt in &block.stmts { + stmts.push(self.stmt(stmt)?); + } + let Some(tail) = &block.tail else { + return Err(Diagnostic::error("this branch must end with a value", block.span) + .with_help( + "an `if` used as an expression needs each branch to end with an expression (no `;`)", + )); + }; + let checked = self.expr(tail)?; + let ty = checked.ty.clone(); + let span = checked.span; + Ok(( + TBlock { + stmts, + tail: Some(Box::new(checked)), + }, + ty, + span, + )) + })(); + self.scopes.pop(); + result + } + + // ── statements ─────────────────────────────────────────────────── + + fn stmt(&mut self, stmt: &Stmt) -> Result { + match &stmt.kind { + StmtKind::Let { + mutable, + name, + ty, + value, + } => { + // An annotated empty list is the one place inference needs + // the annotation's help: `let xs: [int] = [];`. + if let (Some(annotation), ExprKind::List(elements)) = (ty, &value.kind) + && elements.is_empty() + { + let want = self.ty_of(annotation, TypePos::Local)?; + if !matches!(want, Ty::List(_)) { + return Err(Diagnostic::error( + format!("`[]` is a list, but `{}` is declared `{want}`", name.name), + value.span, + )); + } + let slot = self.declare(&name.name, want.clone(), *mutable, name.span); + return Ok(TStmt::Let { + slot, + value: TExpr { + kind: TExprKind::List(Vec::new()), + ty: want, + span: value.span, + }, + }); + } + let value = self.expr(value)?; + if value.ty == Ty::Unit { + return Err(Diagnostic::error( + format!( + "`{}` would have no value — this expression produces nothing", + name.name + ), + value.span, + )); + } + let value = match ty { + Some(annotation) => { + let want = self.ty_of(annotation, TypePos::Local)?; + self.coerce(value, &want).map_err(|d| { + d.with_help(format!( + "`{}` is declared `{}` but the value has a different type", + name.name, want + )) + })? + } + None => value, + }; + let slot = self.declare(&name.name, value.ty.clone(), *mutable, name.span); + Ok(TStmt::Let { slot, value }) + } + StmtKind::Assign { target, value } => self.assign(target, value, stmt.span), + StmtKind::Expr(e) => { + let checked = self.expr(e)?; + Ok(TStmt::Expr(checked)) + } + StmtKind::Return(value) => match (value, self.ret.clone()) { + (None, Ty::Unit) => Ok(TStmt::Return(None)), + (None, ret) => Err(Diagnostic::error( + format!("this function must return a `{ret}`"), + stmt.span, + ) + .with_help(format!("write `return <{ret} value>;`"))), + (Some(e), Ty::Unit) => Err(Diagnostic::error( + "this function has no return type, but `return` carries a value", + e.span, + ) + .with_help("declare one: `func name(...) -> `, or write `return;`")), + (Some(e), ret) => { + let value = self.expr(e)?; + let value = self + .coerce(value, &ret) + .map_err(|d| d.with_help(format!("this function returns `{ret}`")))?; + Ok(TStmt::Return(Some(value))) + } + }, + StmtKind::If(if_expr) => { + let cond = self.bool_cond(&if_expr.cond)?; + let then_b = self.stmt_block(&if_expr.then_block)?; + let else_b = match &if_expr.else_block { + Some(b) => Some(self.stmt_block(b)?), + None => None, + }; + Ok(TStmt::If { + cond, + then_b, + else_b, + }) + } + StmtKind::While { cond, body } => { + let cond = self.bool_cond(cond)?; + self.loops.push(false); + let body = self.stmt_block(body)?; + self.loops.pop(); + Ok(TStmt::While { cond, body }) + } + StmtKind::Loop { body } => { + self.loops.push(false); + let body = self.stmt_block(body)?; + let has_break = self.loops.pop().expect("loop entry pushed above"); + Ok(TStmt::Loop { body, has_break }) + } + StmtKind::For { + var, + iterable, + body, + } => self.for_stmt(var, iterable, body), + StmtKind::Break => match self.loops.last_mut() { + Some(flag) => { + *flag = true; + Ok(TStmt::Break(stmt.span)) + } + None => Err(Diagnostic::error("`break` outside of a loop", stmt.span)), + }, + StmtKind::Continue => { + if self.loops.is_empty() { + return Err(Diagnostic::error("`continue` outside of a loop", stmt.span)); + } + Ok(TStmt::Continue(stmt.span)) + } + } + } + + fn for_stmt( + &mut self, + var: &ast::Ident, + iterable: &Expr, + body: &Block, + ) -> Result { + self.scopes.push(Vec::new()); + self.loops.push(false); + let result = (|| { + if let ExprKind::Range { start, end } = &iterable.kind { + let start = self.expect_ty(start, &Ty::Int, "range bounds are `int`")?; + let end = self.expect_ty(end, &Ty::Int, "range bounds are `int`")?; + let var_slot = self.declare(&var.name, Ty::Int, false, var.span); + let end_slot = self.hidden_slot("range-end", Ty::Int, iterable.span); + let body = self.stmt_block_inner(body)?; + return Ok(TStmt::ForRange { + var_slot, + end_slot, + start, + end, + body, + }); + } + let list = self.expr(iterable)?; + let (elem_ty, by_ref) = match &list.ty { + Ty::List(elem) => ((**elem).clone(), false), + Ty::Ref(inner) | Ty::RefMut(inner) => match &**inner { + Ty::List(elem) => ((**elem).clone(), true), + other => { + return Err(Diagnostic::error( + format!("cannot iterate over a `&{other}`"), + list.span, + )); + } + }, + other => { + return Err(Diagnostic::error( + format!("cannot iterate over a `{other}`"), + list.span, + ) + .with_help( + "`for` iterates lists (`for x in xs`) or ranges (`for i in 0..10`)", + )); + } + }; + let var_ty = if by_ref { + Ty::Ref(Box::new(elem_ty)) + } else { + elem_ty + }; + let var_slot = self.declare(&var.name, var_ty, false, var.span); + let list_slot = self.hidden_slot("for-list", list.ty.clone(), iterable.span); + let idx_slot = self.hidden_slot("for-index", Ty::Int, iterable.span); + let body = self.stmt_block_inner(body)?; + Ok(TStmt::ForList { + var_slot, + list_slot, + idx_slot, + list, + by_ref, + body, + }) + })(); + self.loops.pop(); + self.scopes.pop(); + result + } + + fn assign(&mut self, target: &Expr, value: &Expr, span: Span) -> Result { + if let ExprKind::Var(name) = &target.kind + && name == "self" + { + return Err( + Diagnostic::error("cannot assign to `self` itself", target.span) + .with_help("assign to its fields instead: `self.field = ...;`"), + ); + } + let Some(p) = self.lower_place(target)? else { + return Err( + Diagnostic::error("this expression cannot be assigned to", target.span) + .with_help("assignable places: variables, fields, and list elements"), + ); + }; + if matches!(p.place.projs.last(), Some(TProj::Deref)) { + return Err(Diagnostic::error( + "cannot replace the whole value behind a reference yet", + target.span, + ) + .with_help("assign to its fields or elements instead (`ref.field = ...`)")); + } + self.require_mut(&p, &format!("assign to `{}`", p.root_name), target.span)?; + let value = self.expr(value)?; + let value = self + .coerce(value, &p.ty) + .map_err(|d| d.with_help(format!("this place holds a `{}`", p.ty)))?; + Ok(TStmt::AssignPlace { + place: p.place, + value, + span, + }) + } + + // ── expressions ────────────────────────────────────────────────── + + fn bool_cond(&mut self, cond: &Expr) -> Result { + self.expect_ty(cond, &Ty::Bool, "conditions are `bool`") + } + + fn expect_ty(&mut self, e: &Expr, want: &Ty, why: &str) -> Result { + let checked = self.expr(e)?; + if &checked.ty == want { + Ok(checked) + } else { + Err(Diagnostic::error( + format!("expected `{want}` here, found `{}`", checked.ty), + checked.span, + ) + .with_help(why.to_string())) + } + } + + /// Coerce `expr` to `want`, inserting int→float widening when allowed. + fn coerce(&self, expr: TExpr, want: &Ty) -> Result { + if &expr.ty == want { + return Ok(expr); + } + if expr.ty == Ty::Int && *want == Ty::Float { + let span = expr.span; + return Ok(TExpr { + kind: TExprKind::IntToFloat(Box::new(expr)), + ty: Ty::Float, + span, + }); + } + Err(Diagnostic::error( + format!("expected `{want}`, found `{}`", expr.ty), + expr.span, + )) + } + + fn ty_of(&self, t: &ast::Type, pos: TypePos) -> Result { + let class_names: HashSet = self.env.classes.keys().cloned().collect(); + lower_type(t, &class_names, pos) + } + + fn field_of(&self, class: &str, field: &ast::Ident) -> Result<(usize, Ty), Diagnostic> { + let info = self + .env + .classes + .get(class) + .expect("class types come from the env"); + match info.fields.iter().position(|(n, _)| n == &field.name) { + Some(i) => Ok((i, info.fields[i].1.clone())), + None => { + let names: Vec<&str> = info.fields.iter().map(|(n, _)| n.as_str()).collect(); + Err(Diagnostic::error( + format!("class `{class}` has no field `{}`", field.name), + field.span, + ) + .with_help(if names.is_empty() { + format!("`{class}` has no fields") + } else { + format!("available fields: {}", names.join(", ")) + })) + } + } + } + + /// Turn a checked expression into a string value for interpolation / + /// `println` / `str()`: strings pass through, printables get ToStr, + /// classes (possibly behind references) use their `to_str` method. + fn stringify(&self, checked: TExpr) -> Result { + let target = checked.ty.deref_target().clone(); + if target == Ty::Str { + return Ok(checked); + } + if target.is_printable() { + let span = checked.span; + return Ok(TExpr { + kind: TExprKind::ToStr(Box::new(checked)), + ty: Ty::Str, + span, + }); + } + if let Ty::Class(name) = &target { + if let Some(sig) = self.env.to_str_method(name) { + let span = checked.span; + return Ok(TExpr { + kind: TExprKind::CallUser { + func: sig.idx, + args: vec![checked], + }, + ty: Ty::Str, + span, + }); + } + return Err(Diagnostic::error( + format!("`{name}` cannot be turned into a string"), + checked.span, + ) + .with_help(format!( + "add a `func to_str(&self) -> string {{ ... }}` method to `{name}`" + ))); + } + Err(Diagnostic::error( + format!("expected something printable here, found `{}`", checked.ty), + checked.span, + ) + .with_help("printable types: int, float, bool, string, and classes with `to_str`")) + } + + fn printable_as_str(&mut self, e: &Expr) -> Result { + let checked = self.expr_read_only(e)?; + self.stringify(checked) + } + + fn expr(&mut self, e: &Expr) -> Result { + let span = e.span; + // Any place expression in plain value position: copy or move. + let placelike = matches!( + e.kind, + ExprKind::Var(_) | ExprKind::FieldAccess { .. } | ExprKind::Index { .. } + ) || matches!( + &e.kind, + ExprKind::Unary { + op: UnaryOp::Deref, + .. + } + ); + if placelike && let Some(p) = self.lower_place(e)? { + return self.read_place(p, span); + } + match &e.kind { + ExprKind::Int(v) => Ok(TExpr { + kind: TExprKind::Int(*v), + ty: Ty::Int, + span, + }), + ExprKind::Float(v) => Ok(TExpr { + kind: TExprKind::Float(*v), + ty: Ty::Float, + span, + }), + ExprKind::Bool(v) => Ok(TExpr { + kind: TExprKind::Bool(*v), + ty: Ty::Bool, + span, + }), + ExprKind::Str(parts) => { + let mut checked = Vec::new(); + for part in parts { + match part { + ast::StrPart::Text(t) => checked.push(TStrPart::Text(t.clone())), + ast::StrPart::Expr(inner) => { + let inner = self.printable_as_str(inner)?; + checked.push(TStrPart::Expr(inner)); + } + } + } + Ok(TExpr { + kind: TExprKind::Str(checked), + ty: Ty::Str, + span, + }) + } + ExprKind::Var(_) => unreachable!("handled as a place above"), + ExprKind::List(elements) => { + if elements.is_empty() { + return Err(Diagnostic::error( + "cannot infer the element type of an empty list", + span, + ) + .with_help("annotate the binding: `let xs: [int] = [];`")); + } + let mut checked: Vec = Vec::new(); + let mut elem_ty: Option = None; + for el in elements { + let el = self.expr(el)?; + if el.ty.is_ref() { + return Err(Diagnostic::error( + "lists own their elements — references cannot be stored in them", + el.span, + ) + .with_help("clone the value instead: `x.clone()`")); + } + match &elem_ty { + None => elem_ty = Some(el.ty.clone()), + Some(t) if *t == el.ty => {} + Some(t) => { + if *t == Ty::Float && el.ty == Ty::Int { + let span = el.span; + checked.push(TExpr { + kind: TExprKind::IntToFloat(Box::new(el)), + ty: Ty::Float, + span, + }); + continue; + } + if *t == Ty::Int && el.ty == Ty::Float { + for prev in checked.iter_mut() { + widen_in_place(prev); + } + elem_ty = Some(Ty::Float); + checked.push(el); + continue; + } + return Err(Diagnostic::error( + format!( + "list elements must all have the same type: expected `{t}`, found `{}`", + el.ty + ), + el.span, + )); + } + } + checked.push(el); + } + let elem_ty = elem_ty.expect("non-empty list has an element type"); + Ok(TExpr { + kind: TExprKind::List(checked), + ty: Ty::List(Box::new(elem_ty)), + span, + }) + } + ExprKind::Unary { op, operand } => self.unary(*op, operand, span), + ExprKind::Binary { op, lhs, rhs } => self.binary(*op, lhs, rhs, span), + ExprKind::Index { recv, index } => { + // Non-place receiver (a temporary). + let recv = self.expr(recv)?; + let target = recv.ty.deref_target().clone(); + let Ty::List(elem_ty) = target else { + return Err(Diagnostic::error( + format!("`{}` cannot be indexed — only lists can", recv.ty), + recv.span, + )); + }; + let index = self.expect_ty(index, &Ty::Int, "list indices are `int`")?; + Ok(TExpr { + kind: TExprKind::IndexTemp { + recv: Box::new(recv), + index: Box::new(index), + }, + ty: *elem_ty, + span, + }) + } + ExprKind::FieldAccess { recv, field } => { + // Non-place receiver (a temporary). + let recv = self.expr(recv)?; + let target = recv.ty.deref_target().clone(); + let Ty::Class(cname) = target else { + return Err(Diagnostic::error( + format!("`{}` has no fields — it is not a class", recv.ty), + field.span, + )); + }; + let (field_idx, field_ty) = self.field_of(&cname, field)?; + Ok(TExpr { + kind: TExprKind::GetFieldTemp { + recv: Box::new(recv), + field_idx, + }, + ty: field_ty, + span, + }) + } + ExprKind::MethodCall { recv, method, args } => { + self.method_call(recv, method, args, span) + } + ExprKind::Call { callee, args } => self.call(callee, args, span), + ExprKind::AssocCall { class, func, args } => { + let Some(info) = self.env.classes.get(&class.name) else { + return Err(Diagnostic::error( + format!("unknown class `{}`", class.name), + class.span, + )); + }; + let Some(sig) = info.methods.get(&func.name).cloned() else { + return Err(Diagnostic::error( + format!("class `{}` has no function `{}`", class.name, func.name), + func.span, + )); + }; + if sig.self_kind.is_some() { + return Err(Diagnostic::error( + format!( + "`{}::{}` is a method, not an associated function", + class.name, func.name + ), + span, + ) + .with_help(format!( + "call it on an instance: `value.{}(...)`", + func.name + ))); + } + let checked_args = + self.check_args(&format!("{}::{}", class.name, func.name), &sig, args, span)?; + Ok(TExpr { + kind: TExprKind::CallUser { + func: sig.idx, + args: checked_args, + }, + ty: sig.ret, + span, + }) + } + ExprKind::ClassLiteral { class, fields } => { + let Some(info) = self.env.classes.get(&class.name).cloned() else { + return Err(Diagnostic::error( + format!("unknown class `{}`", class.name), + class.span, + ) + .with_help(format!("define it first: `class {} {{ ... }}`", class.name))); + }; + let mut values: Vec> = info.fields.iter().map(|_| None).collect(); + for (fname, fvalue) in fields { + let Some(idx) = info.fields.iter().position(|(n, _)| n == &fname.name) else { + let names: Vec<&str> = + info.fields.iter().map(|(n, _)| n.as_str()).collect(); + return Err(Diagnostic::error( + format!("class `{}` has no field `{}`", class.name, fname.name), + fname.span, + ) + .with_help(format!("available fields: {}", names.join(", ")))); + }; + if values[idx].is_some() { + return Err(Diagnostic::error( + format!("field `{}` is set twice", fname.name), + fname.span, + )); + } + let want = info.fields[idx].1.clone(); + // An empty list literal takes its type from the field. + let value = match &fvalue.kind { + ExprKind::List(els) if els.is_empty() && matches!(want, Ty::List(_)) => { + TExpr { + kind: TExprKind::List(Vec::new()), + ty: want.clone(), + span: fvalue.span, + } + } + _ => self.expr(fvalue)?, + }; + let value = self.coerce(value, &want).map_err(|d| { + d.with_help(format!( + "field `{}` of `{}` is `{want}`", + fname.name, class.name + )) + })?; + values[idx] = Some(value); + } + let missing: Vec<&str> = info + .fields + .iter() + .zip(&values) + .filter(|(_, v)| v.is_none()) + .map(|((n, _), _)| n.as_str()) + .collect(); + if !missing.is_empty() { + return Err(Diagnostic::error( + format!( + "missing field{} in `{}` literal: {}", + if missing.len() == 1 { "" } else { "s" }, + class.name, + missing.join(", ") + ), + span, + ) + .with_help("every field must be set when constructing a class")); + } + let values: Vec = values + .into_iter() + .map(|v| v.expect("checked above")) + .collect(); + Ok(TExpr { + kind: TExprKind::NewObject(values), + ty: Ty::Class(class.name.clone()), + span, + }) + } + ExprKind::Range { .. } => Err(Diagnostic::error("a range can only be iterated", span) + .with_help("write it directly in the loop header: `for i in 0..10 { ... }`")), + ExprKind::If(if_expr) => { + let cond = self.bool_cond(&if_expr.cond)?; + let else_block = if_expr + .else_block + .as_ref() + .expect("parser guarantees `else` on if-expressions"); + let (then_b, then_ty, _) = self.value_block(&if_expr.then_block)?; + let (else_b, else_ty, else_span) = self.value_block(else_block)?; + let (then_b, else_b, ty) = + unify_branches(then_b, then_ty, else_b, else_ty, else_span)?; + Ok(TExpr { + kind: TExprKind::If { + cond: Box::new(cond), + then_b, + else_b, + }, + ty, + span, + }) + } + } + } + + fn method_call( + &mut self, + recv: &Expr, + method: &ast::Ident, + args: &[Expr], + span: Span, + ) -> Result { + // Universal methods on strings/lists/classes first. + let recv_ty = self.peek_place_ty(recv)?; + let target = recv_ty.as_ref().map(|t| t.deref_target().clone()); + + match (method.name.as_str(), &target) { + ("len", Some(Ty::Str | Ty::List(_))) => { + if !args.is_empty() { + return Err(Diagnostic::error("`len()` takes no arguments", span)); + } + let recv = self.expr_read_only(recv)?; + return Ok(TExpr { + kind: TExprKind::Len(Box::new(recv)), + ty: Ty::Int, + span, + }); + } + ("sqrt", Some(Ty::Float)) => { + if !args.is_empty() { + return Err(Diagnostic::error("`sqrt()` takes no arguments", span)); + } + let recv = self.expr_read_only(recv)?; + let recv = auto_deref_value(recv); + return Ok(TExpr { + kind: TExprKind::Sqrt(Box::new(recv)), + ty: Ty::Float, + span, + }); + } + ("clone", Some(t @ (Ty::Str | Ty::List(_) | Ty::Class(_)))) => { + if !args.is_empty() { + return Err(Diagnostic::error("`clone()` takes no arguments", span)); + } + let owned = t.clone(); + let recv = self.expr_read_only(recv)?; + return Ok(TExpr { + kind: TExprKind::CloneVal(Box::new(recv)), + ty: owned, + span, + }); + } + _ => {} + } + + let Some(Ty::Class(cname)) = target else { + return match target { + Some(Ty::Str) => Err(Diagnostic::error( + format!("string method `{}` is not available yet", method.name), + method.span, + ) + .with_help("`len()` and `clone()` work today; the rest comes later")), + Some(t) => Err(Diagnostic::error( + format!("`{t}` has no method `{}`", method.name), + method.span, + )), + None => Err(Diagnostic::error("cannot call a method here", method.span)), + }; + }; + let info = self + .env + .classes + .get(&cname) + .expect("class types come from the env"); + let Some(sig) = info.methods.get(&method.name).cloned() else { + return Err(Diagnostic::error( + format!("class `{cname}` has no method `{}`", method.name), + method.span, + )); + }; + let Some(self_kind) = sig.self_kind else { + return Err(Diagnostic::error( + format!( + "`{cname}::{}` is an associated function, not a method", + method.name + ), + span, + ) + .with_help(format!("call it as `{cname}::{}(...)`", method.name))); + }; + + // Build the receiver argument according to the method's self kind. + let checked_recv = if let Some(p) = self.lower_place(recv)? { + match self_kind { + SelfKind::Ref => { + let mode = if p.ty.is_ref() { + PlaceMode::Copy + } else { + PlaceMode::Borrow + }; + TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode, + }, + span: recv.span, + } + } + SelfKind::RefMut => match &p.ty { + // Calling through an existing `&mut` reborrows it. + Ty::RefMut(_) => TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode: PlaceMode::Copy, + }, + span: recv.span, + }, + Ty::Ref(_) => { + return Err(Diagnostic::error( + format!( + "cannot call `{}` through a shared `&` reference (it takes `&mut self`)", + method.name + ), + recv.span, + ) + .with_help("take a `&mut` reference instead")); + } + _ => { + self.require_mut( + &p, + &format!("call `{}` (it takes `&mut self`)", method.name), + recv.span, + )?; + TExpr { + ty: p.ty.clone(), + kind: TExprKind::LoadPlace { + place: p.place, + mode: PlaceMode::BorrowMut, + }, + span: recv.span, + } + } + }, + SelfKind::Owned => { + if p.ty.is_ref() || p.has_deref { + return Err(Diagnostic::error( + format!( + "cannot take ownership through a reference to call `{}` (it takes `self`)", + method.name + ), + recv.span, + ) + .with_help("clone the value first: `.clone()`")); + } + self.read_place(p, recv.span)? + } + } + } else { + // Temporary receiver: fine for every self kind. + self.expr(recv)? + }; + + let mut checked_args = + self.check_args(&format!("{cname}::{}", method.name), &sig, args, span)?; + checked_args.insert(0, checked_recv); + Ok(TExpr { + kind: TExprKind::CallUser { + func: sig.idx, + args: checked_args, + }, + ty: sig.ret, + span, + }) + } + + /// The type an expression would have, looking at places without + /// consuming anything (used to route method calls). + fn peek_place_ty(&mut self, e: &Expr) -> Result, Diagnostic> { + if let Some(p) = self.lower_place(e)? { + return Ok(Some(p.ty)); + } + // Not a place: type it for routing. Checking twice is fine — the + // checker is pure. + Ok(Some(self.expr(e)?.ty)) + } + + /// Check a call's arguments against a signature (self excluded), + /// enforcing reference discipline at the boundary. + fn check_args( + &mut self, + name: &str, + sig: &FuncSig, + args: &[Expr], + span: Span, + ) -> Result, Diagnostic> { + if args.len() != sig.params.len() { + return Err(Diagnostic::error( + format!( + "`{name}` takes {} argument(s), got {}", + sig.params.len(), + args.len() + ), + span, + )); + } + let mut checked = Vec::new(); + for (arg, (pname, pty)) in args.iter().zip(&sig.params) { + let arg_t = match pty { + Ty::Ref(inner) => { + let a = self.expr(arg)?; + match &a.ty { + Ty::Ref(t) | Ty::RefMut(t) if t == inner => a, + t if *t == **inner => { + return Err(Diagnostic::error( + format!("parameter `{pname}` of `{name}` is `&{inner}`, but this passes ownership"), + a.span, + ) + .with_help("borrow it: put `&` in front of the argument")); + } + _ => { + return Err(Diagnostic::error( + format!("expected `&{inner}`, found `{}`", a.ty), + a.span, + ) + .with_help(format!("parameter `{pname}` of `{name}` is `&{inner}`"))); + } + } + } + Ty::RefMut(inner) => { + let a = self.expr(arg)?; + match &a.ty { + Ty::RefMut(t) if t == inner => a, + Ty::Ref(t) if t == inner => { + return Err(Diagnostic::error( + format!("parameter `{pname}` of `{name}` needs `&mut {inner}`, but this is a shared `&`"), + a.span, + ) + .with_help("borrow it mutably: `&mut ...`")); + } + t if *t == **inner => { + return Err(Diagnostic::error( + format!("parameter `{pname}` of `{name}` is `&mut {inner}`, but this passes ownership"), + a.span, + ) + .with_help("borrow it mutably: put `&mut` in front of the argument")); + } + _ => { + return Err(Diagnostic::error( + format!("expected `&mut {inner}`, found `{}`", a.ty), + a.span, + )); + } + } + } + owned => { + let a = self.expr(arg)?; + if a.ty.is_ref() && a.ty.deref_target() == owned { + return Err(Diagnostic::error( + format!("parameter `{pname}` of `{name}` takes ownership of a `{owned}`, but this is a reference"), + a.span, + ) + .with_help("clone it: append `.clone()` — or pass the value itself")); + } + self.coerce(a, owned).map_err(|d| { + d.with_help(format!("parameter `{pname}` of `{name}` is `{owned}`")) + })? + } + }; + checked.push(arg_t); + } + Ok(checked) + } + + fn unary(&mut self, op: UnaryOp, operand: &Expr, span: Span) -> Result { + match op { + UnaryOp::Neg => { + let operand = self.expr(operand)?; + match operand.ty { + Ty::Int => Ok(TExpr { + kind: TExprKind::NegI(Box::new(operand)), + ty: Ty::Int, + span, + }), + Ty::Float => Ok(TExpr { + kind: TExprKind::NegF(Box::new(operand)), + ty: Ty::Float, + span, + }), + ref ty => Err(Diagnostic::error( + format!("cannot negate a `{ty}`"), + operand.span, + )), + } + } + UnaryOp::Not => { + let operand = self.expect_ty(operand, &Ty::Bool, "`not` works on `bool`")?; + Ok(TExpr { + kind: TExprKind::Not(Box::new(operand)), + ty: Ty::Bool, + span, + }) + } + UnaryOp::Ref | UnaryOp::RefMut => { + let mutable = op == UnaryOp::RefMut; + if let Some(p) = self.lower_place(operand)? { + if p.ty.is_ref() { + return Err(Diagnostic::error( + "references to references are not supported", + span, + ) + .with_help("the value is already a reference — use it directly")); + } + if mutable { + if p.ty.is_copy() { + return Err(Diagnostic::error( + format!("cannot mutably borrow a copyable `{}`", p.ty), + span, + ) + .with_help( + "assign a new value instead of mutating through a reference", + )); + } + self.require_mut(&p, &format!("borrow `{}` mutably", p.root_name), span)?; + } + let inner = p.ty.clone(); + let mode = if mutable { + PlaceMode::BorrowMut + } else { + PlaceMode::Borrow + }; + return Ok(TExpr { + kind: TExprKind::LoadPlace { + place: p.place, + mode, + }, + ty: if mutable { + Ty::RefMut(Box::new(inner)) + } else { + Ty::Ref(Box::new(inner)) + }, + span, + }); + } + // Borrow of a temporary: the runtime value keeps it alive. + let value = self.expr(operand)?; + if value.ty.is_ref() { + return Err(Diagnostic::error( + "references to references are not supported", + span, + )); + } + if mutable && value.ty.is_copy() { + return Err(Diagnostic::error( + format!("cannot mutably borrow a copyable `{}`", value.ty), + span, + )); + } + let inner = value.ty.clone(); + Ok(TExpr { + kind: TExprKind::BorrowTemp { + value: Box::new(value), + mutable, + }, + ty: if mutable { + Ty::RefMut(Box::new(inner)) + } else { + Ty::Ref(Box::new(inner)) + }, + span, + }) + } + UnaryOp::Deref => { + // Place derefs are handled in expr(); this is `*temp`. + let value = self.expr(operand)?; + let (Ty::Ref(inner) | Ty::RefMut(inner)) = value.ty.clone() else { + return Err(Diagnostic::error( + format!( + "cannot dereference a `{}` — it is not a reference", + value.ty + ), + span, + )); + }; + Ok(TExpr { + kind: TExprKind::Deref(Box::new(value)), + ty: *inner, + span, + }) + } + } + } + + fn binary( + &mut self, + op: BinaryOp, + lhs: &Expr, + rhs: &Expr, + span: Span, + ) -> Result { + use BinaryOp::*; + + if matches!(op, And | Or) { + let lhs = self.expect_ty(lhs, &Ty::Bool, "`and`/`or` work on `bool`")?; + let rhs = self.expect_ty(rhs, &Ty::Bool, "`and`/`or` work on `bool`")?; + return Ok(TExpr { + kind: TExprKind::Logic { + is_and: op == And, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + ty: Ty::Bool, + span, + }); + } + + // Equality reads both sides without consuming them. + if matches!(op, Eq | NotEq) { + let lhs = auto_deref_value(self.expr_read_only(lhs)?); + let rhs = auto_deref_value(self.expr_read_only(rhs)?); + let (lhs, rhs, ty) = widen_pair(lhs, rhs); + if ty.is_none() && lhs.ty != rhs.ty { + return Err(Diagnostic::error( + format!("cannot compare `{}` with `{}`", lhs.ty, rhs.ty), + span, + )); + } + if matches!(lhs.ty, Ty::Unit | Ty::List(_) | Ty::Class(_)) { + return Err(Diagnostic::error( + format!( + "`{}` values cannot be compared with `{}`", + lhs.ty, + if op == Eq { "==" } else { "!=" } + ), + span, + ) + .with_help("compare individual fields or elements instead")); + } + let top = if op == Eq { TBinOp::Eq } else { TBinOp::Ne }; + return Ok(TExpr { + kind: TExprKind::Binary { + op: top, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + ty: Ty::Bool, + span, + }); + } + + let lhs = self.expr(lhs)?; + + // String concatenation: consumes the left side (§9), reads the right. + if op == Add && lhs.ty.deref_target() == &Ty::Str { + let lhs = auto_deref_value(lhs); + let rhs = auto_deref_value(self.expr_read_only(rhs)?); + if rhs.ty != Ty::Str { + return Err(Diagnostic::error( + format!("cannot concatenate a string with a `{}`", rhs.ty), + rhs.span, + ) + .with_help("use interpolation instead: \"text {value}\"")); + } + return Ok(TExpr { + kind: TExprKind::Binary { + op: TBinOp::Concat, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + ty: Ty::Str, + span, + }); + } + let rhs = self.expr(rhs)?; + if op == Add && rhs.ty.deref_target() == &Ty::Str { + return Err(Diagnostic::error( + format!("cannot concatenate a `{}` with a string", lhs.ty), + lhs.span, + ) + .with_help("use interpolation instead: \"text {value}\"")); + } + + // Ordering and arithmetic: numeric only, with widening. + let (lhs, rhs, numeric_ty) = widen_pair(lhs, rhs); + let Some(numeric_ty) = numeric_ty else { + let hint_ref = lhs.ty.is_ref() || rhs.ty.is_ref(); + let mut d = Diagnostic::error( + format!( + "`{}` is not defined between `{}` and `{}`", + op.symbol(), + lhs.ty, + rhs.ty + ), + span, + ); + if hint_ref { + d = d.with_help("dereference the reference first: `*x`"); + } + return Err(d); + }; + let is_float = numeric_ty == Ty::Float; + let (top, result_ty) = match op { + Add => ( + if is_float { TBinOp::AddF } else { TBinOp::AddI }, + numeric_ty, + ), + Sub => ( + if is_float { TBinOp::SubF } else { TBinOp::SubI }, + numeric_ty, + ), + Mul => ( + if is_float { TBinOp::MulF } else { TBinOp::MulI }, + numeric_ty, + ), + Div => ( + if is_float { TBinOp::DivF } else { TBinOp::DivI }, + numeric_ty, + ), + Rem => ( + if is_float { TBinOp::RemF } else { TBinOp::RemI }, + numeric_ty, + ), + Lt => (if is_float { TBinOp::LtF } else { TBinOp::LtI }, Ty::Bool), + LtEq => (if is_float { TBinOp::LeF } else { TBinOp::LeI }, Ty::Bool), + Gt => (if is_float { TBinOp::GtF } else { TBinOp::GtI }, Ty::Bool), + GtEq => (if is_float { TBinOp::GeF } else { TBinOp::GeI }, Ty::Bool), + And | Or | Eq | NotEq => unreachable!("handled above"), + }; + Ok(TExpr { + kind: TExprKind::Binary { + op: top, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + ty: result_ty, + span, + }) + } + + fn call( + &mut self, + callee: &ast::Ident, + args: &[Expr], + span: Span, + ) -> Result { + let argc = args.len(); + let arity = |want: &str| { + Diagnostic::error( + format!("`{}` expects {want}, got {argc} argument(s)", callee.name), + span, + ) + }; + match callee.name.as_str() { + "println" => { + let [arg] = args else { + return Err(arity("one string")); + }; + let arg = self.printable_as_str(arg)?; + Ok(TExpr { + kind: TExprKind::Println(Box::new(arg)), + ty: Ty::Unit, + span, + }) + } + "str" => { + let [arg] = args else { + return Err(arity("one value")); + }; + self.printable_as_str(arg) + } + "float" => { + let [arg] = args else { + return Err(arity("one int")); + }; + let arg = self.expect_ty(arg, &Ty::Int, "`float(n)` converts an int to a float")?; + Ok(TExpr { + kind: TExprKind::IntToFloat(Box::new(arg)), + ty: Ty::Float, + span, + }) + } + "int" => { + let [arg] = args else { + return Err(arity("one float")); + }; + let arg = + self.expect_ty(arg, &Ty::Float, "`int(f)` truncates a float to an int")?; + Ok(TExpr { + kind: TExprKind::FloatToInt(Box::new(arg)), + ty: Ty::Int, + span, + }) + } + "panic" => { + let [arg] = args else { + return Err(arity("one string")); + }; + let arg = self.printable_as_str(arg)?; + Ok(TExpr { + kind: TExprKind::Panic(Box::new(arg)), + ty: Ty::Unit, + span, + }) + } + "assert" => { + let (cond, msg) = match args { + [cond] => (cond, None), + [cond, msg] => (cond, Some(msg)), + _ => return Err(arity("a condition and an optional message")), + }; + let cond = + self.expect_ty(cond, &Ty::Bool, "the first argument is the condition")?; + let msg = match msg { + Some(m) => self.printable_as_str(m)?, + // Empty message → the VM prints the bare "assertion + // failed" without a trailing colon. + None => TExpr { + kind: TExprKind::Str(vec![TStrPart::Text(String::new())]), + ty: Ty::Str, + span, + }, + }; + Ok(TExpr { + kind: TExprKind::Assert { + cond: Box::new(cond), + msg: Box::new(msg), + }, + ty: Ty::Unit, + span, + }) + } + name => { + let Some(sig) = self.env.funcs.get(name).cloned() else { + let help = if self.env.classes.contains_key(name) { + format!( + "`{name}` is a class — construct it with `{name} {{ ... }}` or call `{name}::func(...)`" + ) + } else { + "built-ins: println, str, int, float, assert, panic — or define it: `func name(...) { ... }`" + .to_string() + }; + return Err(Diagnostic::error( + format!("unknown function `{name}`"), + callee.span, + ) + .with_help(help)); + }; + let checked_args = self.check_args(name, &sig, args, span)?; + Ok(TExpr { + kind: TExprKind::CallUser { + func: sig.idx, + args: checked_args, + }, + ty: sig.ret, + span, + }) + } + } + } +} + +/// Rewrite a checked expression's type through one reference layer. Sound +/// because a reference and its target have the same runtime representation. +fn auto_deref_value(mut e: TExpr) -> TExpr { + if e.ty.is_ref() { + e.ty = e.ty.deref_target().clone(); + } + e +} + +/// If exactly one side is int and the other float, widen the int side. +/// Returns the common numeric type when both sides end up numeric. +fn widen_pair(lhs: TExpr, rhs: TExpr) -> (TExpr, TExpr, Option) { + match (&lhs.ty, &rhs.ty) { + (Ty::Int, Ty::Int) => (lhs, rhs, Some(Ty::Int)), + (Ty::Float, Ty::Float) => (lhs, rhs, Some(Ty::Float)), + (Ty::Int, Ty::Float) => { + let span = lhs.span; + let lhs = TExpr { + kind: TExprKind::IntToFloat(Box::new(lhs)), + ty: Ty::Float, + span, + }; + (lhs, rhs, Some(Ty::Float)) + } + (Ty::Float, Ty::Int) => { + let span = rhs.span; + let rhs = TExpr { + kind: TExprKind::IntToFloat(Box::new(rhs)), + ty: Ty::Float, + span, + }; + (lhs, rhs, Some(Ty::Float)) + } + _ => (lhs, rhs, None), + } +} + +fn widen_in_place(e: &mut TExpr) { + if e.ty == Ty::Int { + let span = e.span; + let inner = std::mem::replace( + e, + TExpr { + kind: TExprKind::Bool(false), + ty: Ty::Float, + span, + }, + ); + *e = TExpr { + kind: TExprKind::IntToFloat(Box::new(inner)), + ty: Ty::Float, + span, + }; + } +} + +/// Unify the branch types of an if-expression, widening int→float when the +/// branches disagree only in that way. +fn unify_branches( + then_b: TBlock, + then_ty: Ty, + else_b: TBlock, + else_ty: Ty, + else_span: Span, +) -> Result<(TBlock, TBlock, Ty), Diagnostic> { + if then_ty == else_ty { + return Ok((then_b, else_b, then_ty)); + } + let widen_tail = |mut b: TBlock| -> TBlock { + if let Some(tail) = b.tail.take() { + let span = tail.span; + b.tail = Some(Box::new(TExpr { + kind: TExprKind::IntToFloat(tail), + ty: Ty::Float, + span, + })); + } + b + }; + match (&then_ty, &else_ty) { + (Ty::Int, Ty::Float) => Ok((widen_tail(then_b), else_b, Ty::Float)), + (Ty::Float, Ty::Int) => Ok((then_b, widen_tail(else_b), Ty::Float)), + _ => Err(Diagnostic::error( + format!("the branches of this `if` expression disagree: `{then_ty}` vs `{else_ty}`"), + else_span, + ) + .with_help("both branches must produce the same type of value")), + } +} + +fn unknown_variable(name: &str, span: Span) -> Diagnostic { + Diagnostic::error(format!("cannot find `{name}` in this scope"), span) + .with_help(format!("declare it first: `let {name} = ...;`")) +} diff --git a/src/compile.rs b/src/compile.rs new file mode 100644 index 0000000..b8c6269 --- /dev/null +++ b/src/compile.rs @@ -0,0 +1,478 @@ +//! Typed IR → bytecode. The checker and borrow checker resolved all type +//! and ownership questions, so this is a direct translation; the only +//! cleverness is jump patching — for `if`, short-circuit `and`/`or`, and +//! loop `break`/`continue`. +//! +//! Access modes vanish here: a move, copy, or borrow of a heap value all +//! compile to the same load (heap values are shared `Rc`s at runtime; the +//! borrow checker made the difference unobservable). `Deref` projections +//! compile to nothing. + +use crate::bytecode::{Chunk, CompiledProgram, Op}; +use crate::span::Span; +use crate::tir::*; +use crate::value::Value; + +pub fn compile(program: &TProgram) -> CompiledProgram { + let funcs = program.funcs.iter().map(compile_func).collect(); + CompiledProgram { + funcs, + main: program.main, + } +} + +fn compile_func(f: &TFunc) -> Chunk { + let mut c = Compiler { + chunk: Chunk { + name: f.name.clone(), + num_params: f.num_params, + num_locals: f.num_locals, + ..Chunk::default() + }, + loops: Vec::new(), + }; + c.block(&f.body); + // Implicit `return ();` at the end of every function body. For + // functions with a return type the checker proved this unreachable. + let end = Span::new(f.span.end.saturating_sub(1), f.span.end); + c.chunk.emit(Op::Unit, end); + c.chunk.emit(Op::Return, end); + c.chunk +} + +struct LoopCtx { + /// Jump indices to patch to the loop's exit. + breaks: Vec, + /// Jump indices to patch to the loop's continue point (condition + /// re-check or increment step). + continues: Vec, +} + +struct Compiler { + chunk: Chunk, + loops: Vec, +} + +impl Compiler { + fn block(&mut self, block: &TBlock) { + for stmt in &block.stmts { + self.stmt(stmt); + } + if let Some(tail) = &block.tail { + self.expr(tail); + } + } + + /// Push the value of a place: load the root, apply projections. + fn load_place(&mut self, place: &TPlace) { + self.chunk + .emit(Op::LoadLocal(place.root as u16), place.span); + for proj in &place.projs { + match proj { + TProj::Field(idx) => { + self.chunk.emit(Op::GetField(*idx as u16), place.span); + } + TProj::Index(idx) => { + self.expr(idx); + self.chunk.emit(Op::Index, place.span); + } + TProj::Deref => {} + } + } + } + + fn stmt(&mut self, stmt: &TStmt) { + match stmt { + TStmt::Let { slot, value } => { + self.expr(value); + self.chunk.emit(Op::StoreLocal(*slot as u16), value.span); + } + TStmt::AssignPlace { place, value, span } => { + // Split off the final projection: everything before it is + // the container to mutate. + let real_projs: Vec<&TProj> = place + .projs + .iter() + .filter(|p| !matches!(p, TProj::Deref)) + .collect(); + if real_projs.is_empty() { + // Bare root (derefs compile to nothing anyway). + self.expr(value); + self.chunk.emit(Op::StoreLocal(place.root as u16), *span); + return; + } + // Load the container: root + all projections except the + // last non-deref one. + self.chunk + .emit(Op::LoadLocal(place.root as u16), place.span); + let mut remaining = real_projs.len(); + let mut last: Option<&TProj> = None; + for proj in &place.projs { + if !matches!(proj, TProj::Deref) { + remaining -= 1; + if remaining == 0 { + last = Some(proj); + break; + } + } + match proj { + TProj::Field(idx) => { + self.chunk.emit(Op::GetField(*idx as u16), place.span); + } + TProj::Index(idx) => { + self.expr(idx); + self.chunk.emit(Op::Index, place.span); + } + TProj::Deref => {} + } + } + match last.expect("non-empty projections have a last") { + TProj::Field(idx) => { + self.expr(value); + self.chunk.emit(Op::SetField(*idx as u16), *span); + } + TProj::Index(idx) => { + self.expr(idx); + self.expr(value); + self.chunk.emit(Op::SetIndex, *span); + } + TProj::Deref => unreachable!("checker rejects deref-final assignment"), + } + } + TStmt::Expr(e) => { + self.expr(e); + if e.pushes_value() { + self.chunk.emit(Op::Pop, e.span); + } + } + TStmt::If { + cond, + then_b, + else_b, + } => { + self.expr(cond); + let to_else = self.chunk.emit(Op::JumpIfFalse(0), cond.span); + self.block(then_b); + match else_b { + Some(else_b) => { + let to_end = self.chunk.emit(Op::Jump(0), cond.span); + self.chunk.patch_jump(to_else); + self.block(else_b); + self.chunk.patch_jump(to_end); + } + None => self.chunk.patch_jump(to_else), + } + } + TStmt::While { cond, body } => { + let start = self.chunk.code.len() as u32; + self.expr(cond); + let to_end = self.chunk.emit(Op::JumpIfFalse(0), cond.span); + self.loops.push(LoopCtx { + breaks: vec![to_end], + continues: Vec::new(), + }); + self.block(body); + self.chunk.emit(Op::Jump(start), cond.span); + self.finish_loop(start); + } + TStmt::Loop { body, .. } => { + let start = self.chunk.code.len() as u32; + self.loops.push(LoopCtx { + breaks: Vec::new(), + continues: Vec::new(), + }); + self.block(body); + self.chunk.emit(Op::Jump(start), Span::new(0, 0)); + self.finish_loop(start); + } + TStmt::ForRange { + var_slot, + end_slot, + start, + end, + body, + } => { + let span = start.span; + self.expr(start); + self.chunk.emit(Op::StoreLocal(*var_slot as u16), span); + self.expr(end); + self.chunk.emit(Op::StoreLocal(*end_slot as u16), end.span); + + let check = self.chunk.code.len() as u32; + self.chunk.emit(Op::LoadLocal(*var_slot as u16), span); + self.chunk.emit(Op::LoadLocal(*end_slot as u16), span); + self.chunk.emit(Op::LtI, span); + let to_end = self.chunk.emit(Op::JumpIfFalse(0), span); + + self.loops.push(LoopCtx { + breaks: vec![to_end], + continues: Vec::new(), + }); + self.block(body); + + let incr = self.chunk.code.len() as u32; + self.chunk.emit(Op::LoadLocal(*var_slot as u16), span); + let one = self.chunk.add_const(Value::Int(1)); + self.chunk.emit(Op::Const(one), span); + self.chunk.emit(Op::AddI, span); + self.chunk.emit(Op::StoreLocal(*var_slot as u16), span); + self.chunk.emit(Op::Jump(check), span); + self.finish_loop(incr); + } + TStmt::ForList { + var_slot, + list_slot, + idx_slot, + list, + body, + .. + } => { + let span = list.span; + self.expr(list); + self.chunk.emit(Op::StoreLocal(*list_slot as u16), span); + let zero = self.chunk.add_const(Value::Int(0)); + self.chunk.emit(Op::Const(zero), span); + self.chunk.emit(Op::StoreLocal(*idx_slot as u16), span); + + let check = self.chunk.code.len() as u32; + self.chunk.emit(Op::LoadLocal(*idx_slot as u16), span); + self.chunk.emit(Op::LoadLocal(*list_slot as u16), span); + self.chunk.emit(Op::Len, span); + self.chunk.emit(Op::LtI, span); + let to_end = self.chunk.emit(Op::JumpIfFalse(0), span); + + self.chunk.emit(Op::LoadLocal(*list_slot as u16), span); + self.chunk.emit(Op::LoadLocal(*idx_slot as u16), span); + self.chunk.emit(Op::Index, span); + self.chunk.emit(Op::StoreLocal(*var_slot as u16), span); + + self.loops.push(LoopCtx { + breaks: vec![to_end], + continues: Vec::new(), + }); + self.block(body); + + let incr = self.chunk.code.len() as u32; + self.chunk.emit(Op::LoadLocal(*idx_slot as u16), span); + let one = self.chunk.add_const(Value::Int(1)); + self.chunk.emit(Op::Const(one), span); + self.chunk.emit(Op::AddI, span); + self.chunk.emit(Op::StoreLocal(*idx_slot as u16), span); + self.chunk.emit(Op::Jump(check), span); + self.finish_loop(incr); + } + TStmt::Break(span) => { + let at = self.chunk.emit(Op::Jump(0), *span); + self.loops + .last_mut() + .expect("checker rejects break outside loops") + .breaks + .push(at); + } + TStmt::Continue(span) => { + let at = self.chunk.emit(Op::Jump(0), *span); + self.loops + .last_mut() + .expect("checker rejects continue outside loops") + .continues + .push(at); + } + TStmt::Return(value) => { + let span = value.as_ref().map(|v| v.span).unwrap_or(Span::new(0, 0)); + match value { + Some(v) => self.expr(v), + None => { + self.chunk.emit(Op::Unit, span); + } + } + self.chunk.emit(Op::Return, span); + } + } + } + + /// Patch this loop's `break`s to the current position and its + /// `continue`s to `continue_target`, then pop the loop context. + fn finish_loop(&mut self, continue_target: u32) { + let ctx = self.loops.pop().expect("finish_loop pairs with a push"); + for at in ctx.breaks { + self.chunk.patch_jump(at); + } + for at in ctx.continues { + match &mut self.chunk.code[at] { + Op::Jump(t) => *t = continue_target, + other => panic!("continue patched onto non-jump {other:?}"), + } + } + } + + fn expr(&mut self, e: &TExpr) { + let span = e.span; + match &e.kind { + TExprKind::Int(v) => { + let idx = self.chunk.add_const(Value::Int(*v)); + self.chunk.emit(Op::Const(idx), span); + } + TExprKind::Float(v) => { + let idx = self.chunk.add_const(Value::Float(*v)); + self.chunk.emit(Op::Const(idx), span); + } + TExprKind::Bool(v) => { + let idx = self.chunk.add_const(Value::Bool(*v)); + self.chunk.emit(Op::Const(idx), span); + } + TExprKind::Str(parts) => { + if let [TStrPart::Text(t)] = parts.as_slice() { + let idx = self.chunk.add_const(Value::str(t.clone())); + self.chunk.emit(Op::Const(idx), span); + return; + } + for part in parts { + match part { + TStrPart::Text(t) => { + let idx = self.chunk.add_const(Value::str(t.clone())); + self.chunk.emit(Op::Const(idx), span); + } + TStrPart::Expr(inner) => self.expr(inner), + } + } + self.chunk.emit(Op::ConcatN(parts.len() as u16), span); + } + TExprKind::LoadPlace { place, .. } => self.load_place(place), + TExprKind::BorrowTemp { value, .. } => self.expr(value), + TExprKind::Deref(inner) => self.expr(inner), + TExprKind::GetFieldTemp { recv, field_idx } => { + self.expr(recv); + self.chunk.emit(Op::GetField(*field_idx as u16), span); + } + TExprKind::IndexTemp { recv, index } => { + self.expr(recv); + self.expr(index); + self.chunk.emit(Op::Index, span); + } + TExprKind::List(elements) => { + for el in elements { + self.expr(el); + } + self.chunk.emit(Op::MakeList(elements.len() as u16), span); + } + TExprKind::NegI(inner) => { + self.expr(inner); + self.chunk.emit(Op::NegI, span); + } + TExprKind::NegF(inner) => { + self.expr(inner); + self.chunk.emit(Op::NegF, span); + } + TExprKind::Not(inner) => { + self.expr(inner); + self.chunk.emit(Op::Not, span); + } + TExprKind::Binary { op, lhs, rhs } => { + self.expr(lhs); + self.expr(rhs); + let op = match op { + TBinOp::AddI => Op::AddI, + TBinOp::SubI => Op::SubI, + TBinOp::MulI => Op::MulI, + TBinOp::DivI => Op::DivI, + TBinOp::RemI => Op::RemI, + TBinOp::AddF => Op::AddF, + TBinOp::SubF => Op::SubF, + TBinOp::MulF => Op::MulF, + TBinOp::DivF => Op::DivF, + TBinOp::RemF => Op::RemF, + TBinOp::Concat => Op::Concat, + TBinOp::Eq => Op::Eq, + TBinOp::Ne => Op::Ne, + TBinOp::LtI => Op::LtI, + TBinOp::LeI => Op::LeI, + TBinOp::GtI => Op::GtI, + TBinOp::GeI => Op::GeI, + TBinOp::LtF => Op::LtF, + TBinOp::LeF => Op::LeF, + TBinOp::GtF => Op::GtF, + TBinOp::GeF => Op::GeF, + }; + self.chunk.emit(op, span); + } + TExprKind::Logic { is_and, lhs, rhs } => { + self.expr(lhs); + self.chunk.emit(Op::Dup, span); + if *is_and { + let short = self.chunk.emit(Op::JumpIfFalse(0), span); + self.chunk.emit(Op::Pop, span); + self.expr(rhs); + self.chunk.patch_jump(short); + } else { + self.chunk.emit(Op::Not, span); + let short = self.chunk.emit(Op::JumpIfFalse(0), span); + self.chunk.emit(Op::Pop, span); + self.expr(rhs); + self.chunk.patch_jump(short); + } + } + TExprKind::IntToFloat(inner) => { + self.expr(inner); + self.chunk.emit(Op::IntToFloat, span); + } + TExprKind::FloatToInt(inner) => { + self.expr(inner); + self.chunk.emit(Op::FloatToInt, span); + } + TExprKind::ToStr(inner) => { + self.expr(inner); + self.chunk.emit(Op::ToStr, span); + } + TExprKind::Len(inner) => { + self.expr(inner); + self.chunk.emit(Op::Len, span); + } + TExprKind::CloneVal(inner) => { + self.expr(inner); + self.chunk.emit(Op::CloneVal, span); + } + TExprKind::Sqrt(inner) => { + self.expr(inner); + self.chunk.emit(Op::SqrtF, span); + } + TExprKind::CallUser { func, args } => { + for arg in args { + self.expr(arg); + } + self.chunk.emit(Op::Call(*func as u16), span); + } + TExprKind::NewObject(fields) => { + for field in fields { + self.expr(field); + } + self.chunk.emit(Op::MakeObject(fields.len() as u16), span); + } + TExprKind::Println(arg) => { + self.expr(arg); + self.chunk.emit(Op::Print, span); + } + TExprKind::Assert { cond, msg } => { + self.expr(cond); + self.expr(msg); + self.chunk.emit(Op::Assert, span); + } + TExprKind::Panic(msg) => { + self.expr(msg); + self.chunk.emit(Op::Panic, span); + } + TExprKind::If { + cond, + then_b, + else_b, + } => { + self.expr(cond); + let to_else = self.chunk.emit(Op::JumpIfFalse(0), cond.span); + self.block(then_b); + let to_end = self.chunk.emit(Op::Jump(0), span); + self.chunk.patch_jump(to_else); + self.block(else_b); + self.chunk.patch_jump(to_end); + } + } + } +} diff --git a/src/diagnostics.rs b/src/diagnostics.rs new file mode 100644 index 0000000..170da45 --- /dev/null +++ b/src/diagnostics.rs @@ -0,0 +1,136 @@ +//! Diagnostic rendering. The design doc (§8.4) commits every error to: +//! plain-language rule, precise location(s), and a concrete suggested fix. +//! This module owns the "precise location" part: mapping byte spans to +//! line/column and rendering the offending source line with a caret. + +use crate::span::Span; +use std::fmt::Write as _; + +#[derive(Debug, Clone)] +pub struct Diagnostic { + pub message: String, + pub span: Span, + /// Optional "help: ..." line with a concrete suggested fix. + pub help: Option, + /// Extra labeled locations (the §8.4 contract: borrow errors show both + /// conflicting sites). Rendered as additional caret snippets. + pub related: Vec<(String, Span)>, +} + +impl Diagnostic { + pub fn error(message: impl Into, span: Span) -> Self { + Diagnostic { + message: message.into(), + span, + help: None, + related: Vec::new(), + } + } + + pub fn with_help(mut self, help: impl Into) -> Self { + self.help = Some(help.into()); + self + } + + pub fn with_related(mut self, label: impl Into, span: Span) -> Self { + self.related.push((label.into(), span)); + self + } +} + +/// A source file plus the line index needed to resolve spans. +pub struct SourceFile { + pub name: String, + pub text: String, + /// Byte offset at which each line starts. + line_starts: Vec, +} + +impl SourceFile { + pub fn new(name: impl Into, text: impl Into) -> Self { + let text = text.into(); + let mut line_starts = vec![0]; + for (i, b) in text.bytes().enumerate() { + if b == b'\n' { + line_starts.push(i + 1); + } + } + SourceFile { + name: name.into(), + text, + line_starts, + } + } + + /// 1-based (line, column) for a byte offset. Column counts characters, + /// not bytes, so multi-byte UTF-8 doesn't skew carets. + pub fn line_col(&self, offset: usize) -> (usize, usize) { + let line = match self.line_starts.binary_search(&offset) { + Ok(l) => l, + Err(l) => l - 1, + }; + let line_start = self.line_starts[line]; + let col = self.text[line_start..offset.min(self.text.len())] + .chars() + .count(); + (line + 1, col + 1) + } + + fn line_text(&self, line: usize) -> &str { + let start = self.line_starts[line - 1]; + let end = self + .line_starts + .get(line) + .map(|&e| e.saturating_sub(1)) + .unwrap_or(self.text.len()); + &self.text[start..end.max(start)] + } + + /// One caret snippet: source line with an underline and an optional + /// trailing label. + fn snippet(&self, span: Span, label: Option<&str>) -> String { + let (line, col) = self.line_col(span.start); + let mut out = String::new(); + let src = self.line_text(line); + let gutter = line.to_string(); + let _ = writeln!(out, "{:>width$} |", "", width = gutter.len()); + let _ = writeln!(out, "{gutter} | {src}"); + let (end_line, end_col) = self.line_col(span.end); + let width = if end_line == line && end_col > col { + end_col - col + } else { + 1 + }; + let _ = writeln!( + out, + "{:>gw$} | {:>pad$}{}{}", + "", + "", + "^".repeat(width.max(1)), + label.map(|l| format!(" {l}")).unwrap_or_default(), + gw = gutter.len(), + pad = col - 1 + ); + out + } + + /// Render a diagnostic in the classic `file:line:col` + caret style, + /// including any related locations (§8.4: both sites of a conflict). + pub fn render(&self, diag: &Diagnostic) -> String { + let (line, col) = self.line_col(diag.span.start); + let mut out = String::new(); + let _ = writeln!(out, "error: {}", diag.message); + let _ = writeln!(out, " --> {}:{}:{}", self.name, line, col); + out.push_str(&self.snippet(diag.span, None)); + for (label, span) in &diag.related { + let (rline, rcol) = self.line_col(span.start); + let _ = writeln!(out, " --> {}:{}:{}", self.name, rline, rcol); + out.push_str(&self.snippet(*span, Some(label))); + } + let gutter_len = line.to_string().len(); + if let Some(help) = &diag.help { + let _ = writeln!(out, "{:>gw$} = help: {help}", "", gw = gutter_len); + } + out + } +} diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..0eb842b --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,571 @@ +//! Hand-written lexer (design doc §3, §13). +//! +//! Notable behaviors: +//! - `&mut` is fused into a single token: `&` followed by the `mut` keyword +//! is unambiguous in every grammar position. +//! - String literals are lexed into segments: literal text and `{expr}` +//! interpolation holes. The hole's raw source text is captured (with its +//! absolute byte offset) and re-parsed by the parser, so the lexer stays +//! ignorant of expression grammar. `{{`/`}}` escape literal braces. +//! - Block comments nest. +//! - Reserved keywords (`fn`, `enum`, `match`, ...) lex to an error with a +//! help message rather than becoming identifiers. + +use crate::diagnostics::Diagnostic; +use crate::span::Span; +use crate::token::{StringSegment, Token, TokenKind}; + +pub struct Lexer<'src> { + src: &'src str, + bytes: &'src [u8], + pos: usize, +} + +impl<'src> Lexer<'src> { + pub fn new(src: &'src str) -> Self { + Lexer { + src, + bytes: src.as_bytes(), + pos: 0, + } + } + + /// Lex the whole input. Returns all tokens (ending with Eof) or the + /// first lexical error. + pub fn tokenize(mut self) -> Result, Diagnostic> { + let mut tokens = Vec::new(); + loop { + let tok = self.next_token()?; + let is_eof = tok.kind == TokenKind::Eof; + tokens.push(tok); + if is_eof { + return Ok(tokens); + } + } + } + + fn peek(&self) -> Option { + self.bytes.get(self.pos).copied() + } + + fn peek2(&self) -> Option { + self.bytes.get(self.pos + 1).copied() + } + + fn bump(&mut self) -> Option { + let b = self.peek()?; + self.pos += 1; + Some(b) + } + + fn skip_trivia(&mut self) -> Result<(), Diagnostic> { + loop { + match self.peek() { + Some(b' ' | b'\t' | b'\r' | b'\n') => { + self.pos += 1; + } + Some(b'/') if self.peek2() == Some(b'/') => { + while let Some(b) = self.peek() { + if b == b'\n' { + break; + } + self.pos += 1; + } + } + Some(b'/') if self.peek2() == Some(b'*') => { + let start = self.pos; + self.pos += 2; + let mut depth = 1usize; + while depth > 0 { + match (self.peek(), self.peek2()) { + (Some(b'/'), Some(b'*')) => { + depth += 1; + self.pos += 2; + } + (Some(b'*'), Some(b'/')) => { + depth -= 1; + self.pos += 2; + } + (Some(_), _) => self.pos += 1, + (None, _) => { + return Err(Diagnostic::error( + "this block comment is never closed", + Span::new(start, start + 2), + ) + .with_help("add `*/` to close it")); + } + } + } + } + _ => return Ok(()), + } + } + } + + fn next_token(&mut self) -> Result { + self.skip_trivia()?; + let start = self.pos; + let Some(b) = self.peek() else { + return Ok(Token { + kind: TokenKind::Eof, + span: Span::new(start, start), + }); + }; + + let kind = match b { + b'0'..=b'9' => return self.number(), + b'"' => return self.string(), + b'a'..=b'z' | b'A'..=b'Z' | b'_' => return self.ident_or_keyword(), + b'(' => self.single(TokenKind::LParen), + b')' => self.single(TokenKind::RParen), + b'{' => self.single(TokenKind::LBrace), + b'}' => self.single(TokenKind::RBrace), + b'[' => self.single(TokenKind::LBracket), + b']' => self.single(TokenKind::RBracket), + b',' => self.single(TokenKind::Comma), + b';' => self.single(TokenKind::Semi), + b'+' => self.single(TokenKind::Plus), + b'*' => self.single(TokenKind::Star), + b'/' => self.single(TokenKind::Slash), + b'%' => self.single(TokenKind::Percent), + b':' => self.pair_or(b':', TokenKind::ColonColon, TokenKind::Colon), + b'.' => self.pair_or(b'.', TokenKind::DotDot, TokenKind::Dot), + b'=' => self.pair_or(b'=', TokenKind::EqEq, TokenKind::Eq), + b'<' => self.pair_or(b'=', TokenKind::LtEq, TokenKind::Lt), + b'>' => self.pair_or(b'=', TokenKind::GtEq, TokenKind::Gt), + b'-' => self.pair_or(b'>', TokenKind::Arrow, TokenKind::Minus), + b'&' => { + self.pos += 1; + // Fuse `&mut` (with any whitespace between) into one token. + let save = self.pos; + self.skip_trivia()?; + if self.src[self.pos..].starts_with("mut") + && !matches!( + self.bytes.get(self.pos + 3), + Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') + ) + { + self.pos += 3; + return Ok(Token { + kind: TokenKind::AmpMut, + span: Span::new(start, self.pos), + }); + } + self.pos = save; + return Ok(Token { + kind: TokenKind::Amp, + span: Span::new(start, self.pos), + }); + } + b'!' => { + if self.peek2() == Some(b'=') { + self.pos += 2; + return Ok(Token { + kind: TokenKind::NotEq, + span: Span::new(start, self.pos), + }); + } + return Err(Diagnostic::error( + "unexpected character `!`", + Span::new(start, start + 1), + ) + .with_help("Oxide spells boolean negation `not`; `!=` is still not-equals")); + } + b'|' if self.peek2() == Some(b'|') => { + return Err( + Diagnostic::error("unexpected `||`", Span::new(start, start + 2)) + .with_help("Oxide spells boolean OR as the word `or`"), + ); + } + _ => { + let ch_len = utf8_len(b); + let ch = &self.src[start..(start + ch_len).min(self.src.len())]; + return Err(Diagnostic::error( + format!("unexpected character `{ch}`"), + Span::new(start, start + ch_len), + )); + } + }; + Ok(Token { + kind, + span: Span::new(start, self.pos), + }) + } + + fn single(&mut self, kind: TokenKind) -> TokenKind { + self.pos += 1; + kind + } + + /// If the next-next byte is `second`, produce `pair`, else `single`. + fn pair_or(&mut self, second: u8, pair: TokenKind, single: TokenKind) -> TokenKind { + if self.peek2() == Some(second) { + self.pos += 2; + pair + } else { + self.pos += 1; + single + } + } + + fn ident_or_keyword(&mut self) -> Result { + let start = self.pos; + while let Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') = self.peek() { + self.pos += 1; + } + let text = &self.src[start..self.pos]; + let span = Span::new(start, self.pos); + match TokenKind::keyword(text) { + Some(Ok(kind)) => Ok(Token { kind, span }), + Some(Err(help)) => Err(Diagnostic::error( + format!("`{text}` is not a valid name here"), + span, + ) + .with_help(help)), + None => Ok(Token { + kind: TokenKind::Ident(text.to_string()), + span, + }), + } + } + + fn number(&mut self) -> Result { + let start = self.pos; + + // Hex / binary + if self.peek() == Some(b'0') && matches!(self.peek2(), Some(b'x' | b'X' | b'b' | b'B')) { + let radix_char = self.peek2().unwrap(); + let radix = if radix_char == b'x' || radix_char == b'X' { + 16 + } else { + 2 + }; + self.pos += 2; + let digits_start = self.pos; + while let Some(b) = self.peek() { + if b == b'_' || (b as char).is_digit(radix) { + self.pos += 1; + } else { + break; + } + } + let raw: String = self.src[digits_start..self.pos] + .chars() + .filter(|&c| c != '_') + .collect(); + let span = Span::new(start, self.pos); + if raw.is_empty() { + return Err( + Diagnostic::error("this numeric literal has no digits", span).with_help( + format!( + "write digits after `0{}`, e.g. `0{}1010`", + radix_char as char, radix_char as char + ), + ), + ); + } + let value = i64::from_str_radix(&raw, radix).map_err(|_| { + Diagnostic::error("integer literal is too large for `int` (64-bit)", span) + })?; + return Ok(Token { + kind: TokenKind::Int(value), + span, + }); + } + + // Decimal int / float + while let Some(b'0'..=b'9' | b'_') = self.peek() { + self.pos += 1; + } + let mut is_float = false; + // A `.` starts a fraction only if followed by a digit ­— `0..10` must + // stay int, DotDot, int; `x.method()` must stay int, Dot, ident. + if self.peek() == Some(b'.') && matches!(self.peek2(), Some(b'0'..=b'9')) { + is_float = true; + self.pos += 1; + while let Some(b'0'..=b'9' | b'_') = self.peek() { + self.pos += 1; + } + } + if matches!(self.peek(), Some(b'e' | b'E')) { + let mut ahead = self.pos + 1; + if matches!(self.bytes.get(ahead), Some(b'+' | b'-')) { + ahead += 1; + } + if matches!(self.bytes.get(ahead), Some(b'0'..=b'9')) { + is_float = true; + self.pos = ahead + 1; + while let Some(b'0'..=b'9' | b'_') = self.peek() { + self.pos += 1; + } + } + } + + let span = Span::new(start, self.pos); + let raw: String = self.src[start..self.pos] + .chars() + .filter(|&c| c != '_') + .collect(); + if is_float { + let value: f64 = raw + .parse() + .map_err(|_| Diagnostic::error("invalid float literal", span))?; + Ok(Token { + kind: TokenKind::Float(value), + span, + }) + } else { + let value: i64 = raw.parse().map_err(|_| { + Diagnostic::error("integer literal is too large for `int` (64-bit)", span) + .with_help("the maximum `int` is 9_223_372_036_854_775_807") + })?; + Ok(Token { + kind: TokenKind::Int(value), + span, + }) + } + } + + fn string(&mut self) -> Result { + let start = self.pos; + self.pos += 1; // opening quote + let mut segments: Vec = Vec::new(); + let mut text = String::new(); + + loop { + match self.bump() { + None => { + return Err(Diagnostic::error( + "this string is never closed", + Span::new(start, start + 1), + ) + .with_help("add a closing `\"`")); + } + Some(b'"') => break, + Some(b'\\') => { + let esc_pos = self.pos - 1; + match self.bump() { + Some(b'n') => text.push('\n'), + Some(b't') => text.push('\t'), + Some(b'r') => text.push('\r'), + Some(b'\\') => text.push('\\'), + Some(b'"') => text.push('"'), + Some(b'0') => text.push('\0'), + other => { + let shown = other.map(|c| (c as char).to_string()).unwrap_or_default(); + return Err(Diagnostic::error( + format!("unknown escape sequence `\\{shown}`"), + Span::new(esc_pos, self.pos), + ) + .with_help("supported escapes: \\n \\t \\r \\\\ \\\" \\0")); + } + } + } + Some(b'{') => { + if self.peek() == Some(b'{') { + self.pos += 1; + text.push('{'); + continue; + } + // Interpolation hole: capture raw expression source up to + // the matching close brace (tracking nested braces and + // nested string literals). + if !text.is_empty() { + segments.push(StringSegment::Text(std::mem::take(&mut text))); + } + let expr_start = self.pos; + let mut depth = 1usize; + loop { + match self.peek() { + None => { + return Err(Diagnostic::error( + "this `{` interpolation is never closed", + Span::new(expr_start - 1, expr_start), + ) + .with_help("add a matching `}` inside the string")); + } + Some(b'{') => { + depth += 1; + self.pos += 1; + } + Some(b'}') => { + depth -= 1; + self.pos += 1; + if depth == 0 { + break; + } + } + Some(b'"') => { + // A nested string inside the expression: skip + // it wholesale (it may contain braces). + self.pos += 1; + loop { + match self.bump() { + None => { + return Err(Diagnostic::error( + "unclosed string inside interpolation", + Span::new(expr_start, self.pos), + )); + } + Some(b'\\') => { + self.pos += 1; + } + Some(b'"') => break, + Some(_) => {} + } + } + } + Some(_) => self.pos += 1, + } + } + let source = self.src[expr_start..self.pos - 1].to_string(); + if source.trim().is_empty() { + return Err(Diagnostic::error( + "empty interpolation `{}`", + Span::new(expr_start - 1, self.pos), + ) + .with_help("put an expression inside the braces, or escape with `{{}}`")); + } + segments.push(StringSegment::Expr { + source, + offset: expr_start, + }); + } + Some(b'}') => { + if self.peek() == Some(b'}') { + self.pos += 1; + text.push('}'); + } else { + return Err(Diagnostic::error( + "stray `}` in string", + Span::new(self.pos - 1, self.pos), + ) + .with_help("write `}}` for a literal `}`")); + } + } + Some(b) => { + // Copy the full UTF-8 character, not just one byte. + let ch_len = utf8_len(b); + let ch_start = self.pos - 1; + self.pos = ch_start + ch_len; + text.push_str(&self.src[ch_start..self.pos]); + } + } + } + + if !text.is_empty() || segments.is_empty() { + segments.push(StringSegment::Text(text)); + } + Ok(Token { + kind: TokenKind::Str(segments), + span: Span::new(start, self.pos), + }) + } +} + +fn utf8_len(first_byte: u8) -> usize { + match first_byte { + 0x00..=0x7F => 1, + 0xC0..=0xDF => 2, + 0xE0..=0xEF => 3, + _ => 4, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::token::TokenKind::*; + + fn kinds(src: &str) -> Vec { + Lexer::new(src) + .tokenize() + .unwrap() + .into_iter() + .map(|t| t.kind) + .collect() + } + + #[test] + fn numbers() { + assert_eq!( + kinds("42 1_000 0xFF 0b1010 6.25 1e9 2.5e-3"), + vec![ + Int(42), + Int(1000), + Int(255), + Int(10), + Float(6.25), + Float(1e9), + Float(2.5e-3), + Eof + ] + ); + } + + #[test] + fn range_is_not_a_float() { + assert_eq!(kinds("0..10"), vec![Int(0), DotDot, Int(10), Eof]); + } + + #[test] + fn amp_mut_fuses() { + assert_eq!( + kinds("&mut x & y"), + vec![AmpMut, Ident("x".into()), Amp, Ident("y".into()), Eof] + ); + // `&mutable` must NOT fuse: `mut` is a prefix of the identifier. + assert_eq!(kinds("&mutable"), vec![Amp, Ident("mutable".into()), Eof]); + } + + #[test] + fn interpolation_segments() { + let toks = kinds(r#""a {x + 1} b {{literal}}""#); + let Str(segs) = &toks[0] else { + panic!("expected string") + }; + assert_eq!(segs.len(), 3); + assert!(matches!(&segs[0], crate::token::StringSegment::Text(t) if t == "a ")); + assert!( + matches!(&segs[1], crate::token::StringSegment::Expr { source, .. } if source == "x + 1") + ); + assert!(matches!(&segs[2], crate::token::StringSegment::Text(t) if t == " b {literal}")); + } + + #[test] + fn nested_braces_and_strings_in_interpolation() { + let toks = kinds(r#""{ Point { x: 1 }.report("}") }""#); + let Str(segs) = &toks[0] else { + panic!("expected string") + }; + assert_eq!(segs.len(), 1); + assert!(matches!( + &segs[0], + crate::token::StringSegment::Expr { source, .. } + if source.contains("Point { x: 1 }") && source.contains(r#""}""#) + )); + } + + #[test] + fn fn_keyword_is_rejected_with_hint() { + let err = Lexer::new("fn main() {}").tokenize().unwrap_err(); + assert!(err.help.unwrap().contains("func")); + } + + #[test] + fn reserved_keywords_rejected() { + let err = Lexer::new("let match = 1;").tokenize().unwrap_err(); + assert!(err.help.unwrap().contains("reserved")); + } + + #[test] + fn rust_boolean_ops_get_hints() { + let err = Lexer::new("a || b").tokenize().unwrap_err(); + assert!(err.help.unwrap().contains("`or`")); + } + + #[test] + fn nested_block_comments() { + assert_eq!(kinds("1 /* a /* b */ c */ 2"), vec![Int(1), Int(2), Eof]); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4fc8efe --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +//! The Oxide language toolchain. +//! +//! Pipeline (design doc §13): +//! source → lexer → parser (AST) → checker (typed IR) → compiler +//! (bytecode) → VM. + +pub mod ast; +pub mod borrowck; +pub mod bytecode; +pub mod check; +pub mod compile; +pub mod diagnostics; +pub mod lexer; +pub mod parser; +pub mod pretty; +pub mod span; +pub mod tir; +pub mod token; +pub mod ty; +pub mod value; +pub mod vm; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e839fe6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +use oxide::diagnostics::{Diagnostic, SourceFile}; +use oxide::{borrowck, check, compile, parser, pretty, vm}; +use std::process::ExitCode; + +const USAGE: &str = "\ +The Oxide programming language + +Usage: + oxide run compile and execute + oxide build compile and write bytecode disassembly to + oxide check parse the file and report syntax errors + oxide ast parse the file and print its AST + +Note: `run`/`build` currently compile the milestone-2 subset (a single +`func main` with variables, expressions, `if`, lists, and built-ins); +constructs from later milestones are reported with the milestone that +delivers them."; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let (cmd, path) = match args.as_slice() { + [cmd, path] if matches!(cmd.as_str(), "check" | "ast" | "run" | "build") => { + (cmd.as_str(), path.as_str()) + } + _ => { + eprintln!("{USAGE}"); + return ExitCode::FAILURE; + } + }; + + let text = match std::fs::read_to_string(path) { + Ok(t) => t, + Err(e) => { + eprintln!("error: cannot read `{path}`: {e}"); + return ExitCode::FAILURE; + } + }; + let file = SourceFile::new(path, text); + + let program = match parser::parse(&file.text) { + Ok(p) => p, + Err(diag) => return report(&file, &diag), + }; + + match cmd { + "ast" => { + print!("{}", pretty::print_program(&program)); + ExitCode::SUCCESS + } + "check" => { + println!("{path}: syntax OK"); + ExitCode::SUCCESS + } + "run" | "build" => { + let checked = match check::check_program(&program) { + Ok(c) => c, + Err(diag) => return report(&file, &diag), + }; + if let Err(diag) = borrowck::borrowck(&checked) { + return report(&file, &diag); + } + let compiled = compile::compile(&checked); + if cmd == "build" { + let out_path = std::path::Path::new(path).with_extension("oxb"); + let dis = format!( + "; oxide bytecode (provisional text format)\n; source: {path}\n{}", + compiled.disassemble() + ); + if let Err(e) = std::fs::write(&out_path, dis) { + eprintln!("error: cannot write `{}`: {e}", out_path.display()); + return ExitCode::FAILURE; + } + println!("wrote {}", out_path.display()); + return ExitCode::SUCCESS; + } + let mut stdout = std::io::stdout().lock(); + match vm::run(&compiled, &mut stdout) { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + drop(stdout); + let diag = Diagnostic::error( + format!("panic: {} (in `{}`)", err.message, err.frame_name), + err.span, + ); + eprint!("{}", file.render(&diag)); + // Call stack, innermost caller first. + for (name, span) in err.trace.iter().rev() { + let (line, col) = file.line_col(span.start); + eprintln!(" called from `{name}` at {}:{line}:{col}", file.name); + } + // Rust uses 101 for panics; Oxide follows suit. + ExitCode::from(101) + } + } + } + _ => unreachable!(), + } +} + +fn report(file: &SourceFile, diag: &Diagnostic) -> ExitCode { + eprint!("{}", file.render(diag)); + ExitCode::FAILURE +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..e00f5da --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,1208 @@ +//! Recursive-descent parser with Pratt-style expression parsing +//! (design doc §13). First error wins; recovery comes later. + +use crate::ast::*; +use crate::diagnostics::Diagnostic; +use crate::lexer::Lexer; +use crate::span::Span; +use crate::token::{StringSegment, Token, TokenKind}; + +pub fn parse(source: &str) -> Result { + let tokens = Lexer::new(source).tokenize()?; + Parser::new(tokens).program() +} + +struct Parser { + tokens: Vec, + pos: usize, + /// Class-literal restriction (§7 / Rust's rule): inside `if`/`while` + /// conditions and `for` iterables, `Name { ... }` is not parsed as a + /// class literal — the `{` starts the body instead. Parentheses reset it. + allow_class_literal: bool, +} + +impl Parser { + fn new(tokens: Vec) -> Self { + Parser { + tokens, + pos: 0, + allow_class_literal: true, + } + } + + // ── token helpers ──────────────────────────────────────────────── + + fn peek(&self) -> &Token { + &self.tokens[self.pos.min(self.tokens.len() - 1)] + } + + fn peek_kind(&self) -> &TokenKind { + &self.peek().kind + } + + fn peek2_kind(&self) -> &TokenKind { + &self.tokens[(self.pos + 1).min(self.tokens.len() - 1)].kind + } + + fn bump(&mut self) -> Token { + let tok = self.tokens[self.pos.min(self.tokens.len() - 1)].clone(); + if self.pos < self.tokens.len() - 1 { + self.pos += 1; + } + tok + } + + fn at(&self, kind: &TokenKind) -> bool { + self.peek_kind() == kind + } + + fn eat(&mut self, kind: &TokenKind) -> bool { + if self.at(kind) { + self.bump(); + true + } else { + false + } + } + + fn expect(&mut self, kind: TokenKind, context: &str) -> Result { + if self.at(&kind) { + Ok(self.bump()) + } else { + let found = self.peek(); + Err(Diagnostic::error( + format!( + "expected {} {context}, found {}", + kind.describe(), + found.kind.describe() + ), + found.span, + )) + } + } + + fn expect_ident(&mut self, context: &str) -> Result { + match self.peek_kind().clone() { + TokenKind::Ident(name) => { + let tok = self.bump(); + Ok(Ident { + name, + span: tok.span, + }) + } + other => Err(Diagnostic::error( + format!("expected a name {context}, found {}", other.describe()), + self.peek().span, + )), + } + } + + // ── items ──────────────────────────────────────────────────────── + + fn program(&mut self) -> Result { + let mut items = Vec::new(); + while !self.at(&TokenKind::Eof) { + items.push(self.item()?); + } + Ok(Program { items }) + } + + fn item(&mut self) -> Result { + match self.peek_kind() { + TokenKind::Func => Ok(Item::Func(self.func(false)?)), + TokenKind::Class => Ok(Item::Class(self.class()?)), + other => Err(Diagnostic::error( + format!( + "expected `func` or `class` at the top level, found {}", + other.describe() + ), + self.peek().span, + ) + .with_help("all code lives inside functions; statements cannot appear at file scope")), + } + } + + fn func(&mut self, in_class: bool) -> Result { + let start = self.expect(TokenKind::Func, "to start a function")?.span; + let name = self.expect_ident("after `func`")?; + self.expect(TokenKind::LParen, "to start the parameter list")?; + + let mut params = Vec::new(); + while !self.at(&TokenKind::RParen) { + if !params.is_empty() { + self.expect(TokenKind::Comma, "between parameters")?; + if self.at(&TokenKind::RParen) { + break; // trailing comma + } + } + params.push(self.param(in_class, params.is_empty())?); + } + self.expect(TokenKind::RParen, "to close the parameter list")?; + + let return_type = if self.eat(&TokenKind::Arrow) { + Some(self.ty()?) + } else { + None + }; + let body = self.block()?; + let span = start.to(body.span); + Ok(Func { + name, + params, + return_type, + body, + span, + }) + } + + fn param(&mut self, in_class: bool, is_first: bool) -> Result { + // self / &self / &mut self + let self_kind = match (self.peek_kind(), self.peek2_kind()) { + (TokenKind::SelfKw, _) => Some((SelfKind::Owned, 1)), + (TokenKind::Amp, TokenKind::SelfKw) => Some((SelfKind::Ref, 2)), + (TokenKind::AmpMut, TokenKind::SelfKw) => Some((SelfKind::RefMut, 2)), + _ => None, + }; + if let Some((kind, len)) = self_kind { + let start = self.peek().span; + let mut end = start; + for _ in 0..len { + end = self.bump().span; + } + let span = start.to(end); + if !in_class { + return Err(Diagnostic::error( + "`self` parameters are only allowed in class methods", + span, + )); + } + if !is_first { + return Err(Diagnostic::error( + "`self` must be the first parameter", + span, + )); + } + return Ok(Param::SelfParam { kind, span }); + } + + let name = self.expect_ident("for this parameter")?; + self.expect(TokenKind::Colon, "after the parameter name") + .map_err(|d| d.with_help("parameter types are required, e.g. `count: int`"))?; + let ty = self.ty()?; + let span = name.span.to(ty.span); + Ok(Param::Normal { name, ty, span }) + } + + fn class(&mut self) -> Result { + let start = self.expect(TokenKind::Class, "to start a class")?.span; + let name = self.expect_ident("after `class`")?; + self.expect(TokenKind::LBrace, "to open the class body")?; + + let mut fields = Vec::new(); + let mut methods = Vec::new(); + while !self.at(&TokenKind::RBrace) { + if self.at(&TokenKind::Func) { + methods.push(self.func(true)?); + } else { + // Field: `name: type,` + let fname = self.expect_ident("for a field (or `func` for a method)")?; + if !methods.is_empty() { + return Err(Diagnostic::error( + format!("field `{}` appears after the class's methods", fname.name), + fname.span, + ) + .with_help("declare all fields before the first method (§7)")); + } + self.expect(TokenKind::Colon, "after the field name")?; + let ty = self.ty()?; + let span = fname.span.to(ty.span); + if let TypeKind::Ref(_) | TypeKind::RefMut(_) = ty.kind { + return Err(Diagnostic::error( + "classes cannot hold references (§8.3)", + ty.span, + ) + .with_help("store the value itself, and borrow at the call site instead")); + } + fields.push(Field { + name: fname, + ty, + span, + }); + // Comma required between fields; optional before a method + // or the closing brace. + if !self.eat(&TokenKind::Comma) + && !self.at(&TokenKind::RBrace) + && !self.at(&TokenKind::Func) + { + return Err(Diagnostic::error( + format!( + "expected `,` after field, found {}", + self.peek_kind().describe() + ), + self.peek().span, + )); + } + } + } + let end = self + .expect(TokenKind::RBrace, "to close the class body")? + .span; + Ok(Class { + name, + fields, + methods, + span: start.to(end), + }) + } + + // ── types ──────────────────────────────────────────────────────── + + fn ty(&mut self) -> Result { + let tok = self.peek().clone(); + match tok.kind { + TokenKind::Amp => { + self.bump(); + let inner = self.ty()?; + let span = tok.span.to(inner.span); + Ok(Type { + kind: TypeKind::Ref(Box::new(inner)), + span, + }) + } + TokenKind::AmpMut => { + self.bump(); + let inner = self.ty()?; + let span = tok.span.to(inner.span); + Ok(Type { + kind: TypeKind::RefMut(Box::new(inner)), + span, + }) + } + TokenKind::LBracket => { + self.bump(); + let inner = self.ty()?; + let end = self + .expect(TokenKind::RBracket, "to close the list type")? + .span; + Ok(Type { + kind: TypeKind::List(Box::new(inner)), + span: tok.span.to(end), + }) + } + TokenKind::LParen => { + self.bump(); + let end = self + .expect(TokenKind::RParen, "— `()` is the only parenthesized type")? + .span; + Ok(Type { + kind: TypeKind::Unit, + span: tok.span.to(end), + }) + } + TokenKind::Ident(name) => { + self.bump(); + let kind = match name.as_str() { + "int" => TypeKind::Int, + "float" => TypeKind::Float, + "bool" => TypeKind::Bool, + "string" => TypeKind::Str, + // Friendly rejections for Rust muscle memory. + "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" + | "isize" => { + return Err(Diagnostic::error( + format!("`{name}` is not an Oxide type"), + tok.span, + ) + .with_help("Oxide has a single integer type: `int`")); + } + "f32" | "f64" => { + return Err(Diagnostic::error( + format!("`{name}` is not an Oxide type"), + tok.span, + ) + .with_help("Oxide has a single float type: `float`")); + } + "String" | "str" => { + return Err(Diagnostic::error( + format!("`{name}` is not an Oxide type"), + tok.span, + ) + .with_help("Oxide has a single string type: `string`")); + } + _ => TypeKind::Named(name), + }; + Ok(Type { + kind, + span: tok.span, + }) + } + other => Err(Diagnostic::error( + format!("expected a type, found {}", other.describe()), + tok.span, + )), + } + } + + // ── statements & blocks ────────────────────────────────────────── + + fn block(&mut self) -> Result { + let start = self.expect(TokenKind::LBrace, "to open a block")?.span; + let mut stmts = Vec::new(); + let mut tail = None; + + while !self.at(&TokenKind::RBrace) && !self.at(&TokenKind::Eof) { + match self.peek_kind() { + TokenKind::Let => stmts.push(self.let_stmt()?), + TokenKind::Return => { + let start = self.bump().span; + let value = if self.at(&TokenKind::Semi) { + None + } else { + Some(self.expr()?) + }; + let end = self.expect(TokenKind::Semi, "after `return`")?.span; + stmts.push(Stmt { + kind: StmtKind::Return(value), + span: start.to(end), + }); + } + TokenKind::Break => { + let s = self.bump().span; + let e = self.expect(TokenKind::Semi, "after `break`")?.span; + stmts.push(Stmt { + kind: StmtKind::Break, + span: s.to(e), + }); + } + TokenKind::Continue => { + let s = self.bump().span; + let e = self.expect(TokenKind::Semi, "after `continue`")?.span; + stmts.push(Stmt { + kind: StmtKind::Continue, + span: s.to(e), + }); + } + TokenKind::While => { + let s = self.bump().span; + let cond = self.condition()?; + let body = self.block()?; + let span = s.to(body.span); + stmts.push(Stmt { + kind: StmtKind::While { cond, body }, + span, + }); + } + TokenKind::Loop => { + let s = self.bump().span; + let body = self.block()?; + let span = s.to(body.span); + stmts.push(Stmt { + kind: StmtKind::Loop { body }, + span, + }); + } + TokenKind::For => { + let s = self.bump().span; + let var = self.expect_ident("as the loop variable")?; + self.expect(TokenKind::In, "after the loop variable")?; + let iterable = self.condition()?; + let body = self.block()?; + let span = s.to(body.span); + stmts.push(Stmt { + kind: StmtKind::For { + var, + iterable, + body, + }, + span, + }); + } + TokenKind::If => { + let if_expr = self.if_expr()?; + // An `if` immediately before `}` is the block's value + // (an if-expression in tail position, §10). Anywhere + // else it is an if-statement. + if self.at(&TokenKind::RBrace) { + let span = if_expr.span; + tail = Some(Box::new(Expr { + kind: ExprKind::If(if_expr), + span, + })); + } else { + let span = if_expr.span; + stmts.push(Stmt { + kind: StmtKind::If(if_expr), + span, + }); + } + } + _ => { + // Expression, assignment, or a tail expression. + let expr = self.expr()?; + if self.at(&TokenKind::Eq) { + self.check_place(&expr)?; + self.bump(); + let value = self.expr()?; + let end = self.expect(TokenKind::Semi, "after the assignment")?.span; + let span = expr.span.to(end); + stmts.push(Stmt { + kind: StmtKind::Assign { + target: expr, + value, + }, + span, + }); + } else if self.eat(&TokenKind::Semi) { + let span = expr.span; + stmts.push(Stmt { + kind: StmtKind::Expr(expr), + span, + }); + } else if self.at(&TokenKind::RBrace) { + tail = Some(Box::new(expr)); + } else { + return Err(Diagnostic::error( + format!( + "expected `;` after this statement, found {}", + self.peek_kind().describe() + ), + self.peek().span, + )); + } + } + } + } + let end = self.expect(TokenKind::RBrace, "to close the block")?.span; + Ok(Block { + stmts, + tail, + span: start.to(end), + }) + } + + fn let_stmt(&mut self) -> Result { + let start = self.expect(TokenKind::Let, "to start a binding")?.span; + let mutable = self.eat(&TokenKind::Mut); + let name = self.expect_ident("after `let`")?; + let ty = if self.eat(&TokenKind::Colon) { + Some(self.ty()?) + } else { + None + }; + self.expect(TokenKind::Eq, "in the `let` binding") + .map_err(|d| d.with_help("every `let` needs an initial value: `let x = ...;`"))?; + let value = self.expr()?; + let end = self + .expect(TokenKind::Semi, "after the `let` binding")? + .span; + Ok(Stmt { + kind: StmtKind::Let { + mutable, + name, + ty, + value, + }, + span: start.to(end), + }) + } + + /// Assignment targets must be places: a variable, field, index, or deref. + fn check_place(&self, expr: &Expr) -> Result<(), Diagnostic> { + match &expr.kind { + ExprKind::Var(_) + | ExprKind::FieldAccess { .. } + | ExprKind::Index { .. } + | ExprKind::Unary { + op: UnaryOp::Deref, .. + } => Ok(()), + _ => Err( + Diagnostic::error("this expression cannot be assigned to", expr.span).with_help( + "only variables, fields, list elements, and `*ref` can appear left of `=`", + ), + ), + } + } + + /// Parse a condition / for-iterable: class literals are not allowed at + /// the top level here (the `{` would be ambiguous with the body). + fn condition(&mut self) -> Result { + let saved = self.allow_class_literal; + self.allow_class_literal = false; + let result = self.expr(); + self.allow_class_literal = saved; + result + } + + fn if_expr(&mut self) -> Result { + let start = self.expect(TokenKind::If, "to start an `if`")?.span; + let cond = self.condition()?; + let then_block = self.block()?; + let mut span = start.to(then_block.span); + let else_block = if self.eat(&TokenKind::Else) { + if self.at(&TokenKind::If) { + // `else if ...` desugars to `else { if ... }`. + let nested = self.if_expr()?; + let nested_span = nested.span; + span = span.to(nested_span); + Some(Block { + stmts: Vec::new(), + tail: Some(Box::new(Expr { + kind: ExprKind::If(nested), + span: nested_span, + })), + span: nested_span, + }) + } else { + let b = self.block()?; + span = span.to(b.span); + Some(b) + } + } else { + None + }; + Ok(IfExpr { + cond: Box::new(cond), + then_block, + else_block, + span, + }) + } + + // ── expressions (Pratt) ────────────────────────────────────────── + + pub(crate) fn expr(&mut self) -> Result { + self.range_expr() + } + + /// `a..b` — lowest precedence, non-associative. + fn range_expr(&mut self) -> Result { + let start = self.or_expr()?; + if self.at(&TokenKind::DotDot) { + self.bump(); + let end = self.or_expr()?; + let span = start.span.to(end.span); + return Ok(Expr { + kind: ExprKind::Range { + start: Box::new(start), + end: Box::new(end), + }, + span, + }); + } + Ok(start) + } + + fn or_expr(&mut self) -> Result { + let mut lhs = self.and_expr()?; + while self.at(&TokenKind::Or) { + self.bump(); + let rhs = self.and_expr()?; + let span = lhs.span.to(rhs.span); + lhs = Expr { + kind: ExprKind::Binary { + op: BinaryOp::Or, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + span, + }; + } + Ok(lhs) + } + + fn and_expr(&mut self) -> Result { + let mut lhs = self.comparison()?; + while self.at(&TokenKind::And) { + self.bump(); + let rhs = self.comparison()?; + let span = lhs.span.to(rhs.span); + lhs = Expr { + kind: ExprKind::Binary { + op: BinaryOp::And, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + span, + }; + } + Ok(lhs) + } + + fn comparison_op(&self) -> Option { + match self.peek_kind() { + TokenKind::EqEq => Some(BinaryOp::Eq), + TokenKind::NotEq => Some(BinaryOp::NotEq), + TokenKind::Lt => Some(BinaryOp::Lt), + TokenKind::LtEq => Some(BinaryOp::LtEq), + TokenKind::Gt => Some(BinaryOp::Gt), + TokenKind::GtEq => Some(BinaryOp::GtEq), + _ => None, + } + } + + /// Comparisons do not chain (§10): `a < b < c` is a hard error. + fn comparison(&mut self) -> Result { + let lhs = self.additive()?; + let Some(op) = self.comparison_op() else { + return Ok(lhs); + }; + let op_span = self.bump().span; + let rhs = self.additive()?; + if let Some(second) = self.comparison_op() { + return Err(Diagnostic::error( + format!( + "comparison operators cannot be chained: `{}` after `{}`", + second.symbol(), + op.symbol() + ), + self.peek().span, + ) + .with_help(format!( + "split it with `and`, e.g. `a {} b and b {} c`", + op.symbol(), + second.symbol() + ))); + } + let _ = op_span; + let span = lhs.span.to(rhs.span); + Ok(Expr { + kind: ExprKind::Binary { + op, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + span, + }) + } + + fn additive(&mut self) -> Result { + let mut lhs = self.multiplicative()?; + loop { + let op = match self.peek_kind() { + TokenKind::Plus => BinaryOp::Add, + TokenKind::Minus => BinaryOp::Sub, + _ => break, + }; + self.bump(); + let rhs = self.multiplicative()?; + let span = lhs.span.to(rhs.span); + lhs = Expr { + kind: ExprKind::Binary { + op, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + span, + }; + } + Ok(lhs) + } + + fn multiplicative(&mut self) -> Result { + let mut lhs = self.unary()?; + loop { + let op = match self.peek_kind() { + TokenKind::Star => BinaryOp::Mul, + TokenKind::Slash => BinaryOp::Div, + TokenKind::Percent => BinaryOp::Rem, + _ => break, + }; + self.bump(); + let rhs = self.unary()?; + let span = lhs.span.to(rhs.span); + lhs = Expr { + kind: ExprKind::Binary { + op, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }, + span, + }; + } + Ok(lhs) + } + + fn unary(&mut self) -> Result { + let op = match self.peek_kind() { + TokenKind::Minus => Some(UnaryOp::Neg), + TokenKind::Not => Some(UnaryOp::Not), + TokenKind::Amp => Some(UnaryOp::Ref), + TokenKind::AmpMut => Some(UnaryOp::RefMut), + TokenKind::Star => Some(UnaryOp::Deref), + _ => None, + }; + if let Some(op) = op { + let start = self.bump().span; + let operand = self.unary()?; + let span = start.to(operand.span); + return Ok(Expr { + kind: ExprKind::Unary { + op, + operand: Box::new(operand), + }, + span, + }); + } + self.postfix() + } + + /// `.field`, `.method(args)`, `[index]` — left-associative postfix ops. + fn postfix(&mut self) -> Result { + let mut expr = self.primary()?; + loop { + if self.at(&TokenKind::Dot) { + self.bump(); + let name = self.expect_ident("after `.`")?; + if self.at(&TokenKind::LParen) { + let (args, end) = self.call_args()?; + let span = expr.span.to(end); + expr = Expr { + kind: ExprKind::MethodCall { + recv: Box::new(expr), + method: name, + args, + }, + span, + }; + } else { + let span = expr.span.to(name.span); + expr = Expr { + kind: ExprKind::FieldAccess { + recv: Box::new(expr), + field: name, + }, + span, + }; + } + } else if self.at(&TokenKind::LBracket) { + self.bump(); + let index = self.expr_with_class_literals()?; + let end = self.expect(TokenKind::RBracket, "to close the index")?.span; + let span = expr.span.to(end); + expr = Expr { + kind: ExprKind::Index { + recv: Box::new(expr), + index: Box::new(index), + }, + span, + }; + } else { + return Ok(expr); + } + } + } + + /// Parse an expression with the class-literal restriction lifted + /// (inside (), [], call args — anywhere re-bracketed). + fn expr_with_class_literals(&mut self) -> Result { + let saved = self.allow_class_literal; + self.allow_class_literal = true; + let result = self.expr(); + self.allow_class_literal = saved; + result + } + + fn call_args(&mut self) -> Result<(Vec, Span), Diagnostic> { + self.expect(TokenKind::LParen, "to start the arguments")?; + let mut args = Vec::new(); + while !self.at(&TokenKind::RParen) { + if !args.is_empty() { + self.expect(TokenKind::Comma, "between arguments")?; + if self.at(&TokenKind::RParen) { + break; // trailing comma + } + } + args.push(self.expr_with_class_literals()?); + } + let end = self + .expect(TokenKind::RParen, "to close the arguments")? + .span; + Ok((args, end)) + } + + fn primary(&mut self) -> Result { + let tok = self.peek().clone(); + match tok.kind { + TokenKind::Int(v) => { + self.bump(); + Ok(Expr { + kind: ExprKind::Int(v), + span: tok.span, + }) + } + TokenKind::Float(v) => { + self.bump(); + Ok(Expr { + kind: ExprKind::Float(v), + span: tok.span, + }) + } + TokenKind::True => { + self.bump(); + Ok(Expr { + kind: ExprKind::Bool(true), + span: tok.span, + }) + } + TokenKind::False => { + self.bump(); + Ok(Expr { + kind: ExprKind::Bool(false), + span: tok.span, + }) + } + TokenKind::SelfKw => { + self.bump(); + Ok(Expr { + kind: ExprKind::Var("self".into()), + span: tok.span, + }) + } + TokenKind::Str(segments) => { + self.bump(); + let parts = parse_string_segments(&segments)?; + Ok(Expr { + kind: ExprKind::Str(parts), + span: tok.span, + }) + } + TokenKind::If => { + let if_expr = self.if_expr()?; + if if_expr.else_block.is_none() { + return Err(Diagnostic::error( + "an `if` used as an expression needs an `else` branch", + if_expr.span, + ) + .with_help("without `else` there is no value when the condition is false")); + } + let span = if_expr.span; + Ok(Expr { + kind: ExprKind::If(if_expr), + span, + }) + } + TokenKind::LParen => { + self.bump(); + let inner = self.expr_with_class_literals()?; + self.expect(TokenKind::RParen, "to close the parenthesized expression")?; + Ok(inner) + } + TokenKind::LBracket => { + self.bump(); + let mut elements = Vec::new(); + while !self.at(&TokenKind::RBracket) { + if !elements.is_empty() { + self.expect(TokenKind::Comma, "between list elements")?; + if self.at(&TokenKind::RBracket) { + break; // trailing comma + } + } + elements.push(self.expr_with_class_literals()?); + } + let end = self.expect(TokenKind::RBracket, "to close the list")?.span; + Ok(Expr { + kind: ExprKind::List(elements), + span: tok.span.to(end), + }) + } + TokenKind::Ident(name) => { + self.bump(); + let ident = Ident { + name, + span: tok.span, + }; + // `Name::assoc(...)` + if self.at(&TokenKind::ColonColon) { + self.bump(); + let func = self.expect_ident("after `::`")?; + let (args, end) = self.call_args().map_err(|d| { + d.with_help( + "`::` is only used to call associated functions: `Class::new(...)`", + ) + })?; + let span = ident.span.to(end); + return Ok(Expr { + kind: ExprKind::AssocCall { + class: ident, + func, + args, + }, + span, + }); + } + // `name(...)` + if self.at(&TokenKind::LParen) { + let (args, end) = self.call_args()?; + let span = ident.span.to(end); + return Ok(Expr { + kind: ExprKind::Call { + callee: ident, + args, + }, + span, + }); + } + // `Name { field: value }` + if self.at(&TokenKind::LBrace) && self.allow_class_literal { + self.bump(); + let mut fields = Vec::new(); + while !self.at(&TokenKind::RBrace) { + if !fields.is_empty() { + self.expect(TokenKind::Comma, "between fields")?; + if self.at(&TokenKind::RBrace) { + break; // trailing comma + } + } + let fname = self.expect_ident("for this field")?; + self.expect(TokenKind::Colon, "after the field name")?; + let value = self.expr_with_class_literals()?; + fields.push((fname, value)); + } + let end = self + .expect(TokenKind::RBrace, "to close the class literal")? + .span; + let span = ident.span.to(end); + return Ok(Expr { + kind: ExprKind::ClassLiteral { + class: ident, + fields, + }, + span, + }); + } + Ok(Expr { + kind: ExprKind::Var(ident.name), + span: ident.span, + }) + } + other => Err(Diagnostic::error( + format!("expected an expression, found {}", other.describe()), + tok.span, + )), + } + } +} + +/// Parse the expression holes captured by the lexer inside an interpolated +/// string. Each hole's source is re-lexed and parsed on its own; token spans +/// are shifted by the hole's byte offset so errors point into the original +/// string literal. +fn parse_string_segments(segments: &[StringSegment]) -> Result, Diagnostic> { + let mut parts = Vec::new(); + for seg in segments { + match seg { + StringSegment::Text(t) => parts.push(StrPart::Text(t.clone())), + StringSegment::Expr { source, offset } => { + let mut tokens = Lexer::new(source).tokenize().map_err(|mut d| { + d.span = d.span.offset(*offset); + d + })?; + for t in &mut tokens { + t.span = t.span.offset(*offset); + } + let mut sub = Parser::new(tokens); + let expr = sub.expr()?; + if !sub.at(&TokenKind::Eof) { + return Err(Diagnostic::error( + format!( + "unexpected {} after the interpolated expression", + sub.peek_kind().describe() + ), + sub.peek().span, + )); + } + parts.push(StrPart::Expr(Box::new(expr))); + } + } + } + Ok(parts) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_ok(src: &str) -> Program { + match parse(src) { + Ok(p) => p, + Err(d) => panic!("parse failed: {} (span {})", d.message, d.span), + } + } + + fn parse_err(src: &str) -> Diagnostic { + parse(src).expect_err("expected a parse error") + } + + #[test] + fn hello_world() { + let p = parse_ok(r#"func main() { println("Hello, world!"); }"#); + assert_eq!(p.items.len(), 1); + } + + #[test] + fn func_with_params_and_return() { + let p = parse_ok("func add(a: int, b: int) -> int { return a + b; }"); + let Item::Func(f) = &p.items[0] else { panic!() }; + assert_eq!(f.name.name, "add"); + assert_eq!(f.params.len(), 2); + assert!(f.return_type.is_some()); + } + + #[test] + fn class_with_fields_and_methods() { + let p = parse_ok( + "class Counter { + count: int, + label: string, + + func new(label: string) -> Counter { + return Counter { count: 0, label: label }; + } + func increment(&mut self) { self.count = self.count + 1; } + func report(&self) -> string { return \"{self.label}: {self.count}\"; } + }", + ); + let Item::Class(c) = &p.items[0] else { + panic!() + }; + assert_eq!(c.fields.len(), 2); + assert_eq!(c.methods.len(), 3); + assert!(matches!( + c.methods[1].params[0], + Param::SelfParam { + kind: SelfKind::RefMut, + .. + } + )); + } + + #[test] + fn precedence() { + let p = parse_ok("func f() -> bool { return 1 + 2 * 3 == 7 and not false; }"); + let Item::Func(f) = &p.items[0] else { panic!() }; + let StmtKind::Return(Some(e)) = &f.body.stmts[0].kind else { + panic!() + }; + // Top node must be `and`. + let ExprKind::Binary { + op: BinaryOp::And, + lhs, + .. + } = &e.kind + else { + panic!("expected `and` at the top, got {:?}", e.kind) + }; + // lhs: (1 + (2*3)) == 7 + assert!(matches!( + lhs.kind, + ExprKind::Binary { + op: BinaryOp::Eq, + .. + } + )); + } + + #[test] + fn chained_comparison_is_rejected() { + let d = parse_err("func f() { let x = 1 < 2 < 3; }"); + assert!(d.help.unwrap().contains("and")); + } + + #[test] + fn if_as_expression_requires_else() { + parse_ok(r#"func f(score: int) -> string { return if score >= 90 { "A" } else { "B" }; }"#); + let d = parse_err("func f() { let x = if true { 1 }; }"); + assert!(d.message.contains("`else`")); + } + + #[test] + fn class_literal_not_allowed_in_condition() { + // `Point { ... }` after `if` must not parse as a class literal — + // here it parses as var `p`, then `{` opens the body. + parse_ok("func f(p: bool) { if p { return; } }"); + // In parens it is allowed: + parse_ok("class P { x: int, } func f() { if (P { x: 1 }).x == 1 { return; } }"); + } + + #[test] + fn ranges_and_for() { + let p = parse_ok("func f(xs: [int]) { for i in 0..10 { } for x in &xs { } }"); + let Item::Func(f) = &p.items[0] else { panic!() }; + let StmtKind::For { iterable, .. } = &f.body.stmts[0].kind else { + panic!() + }; + assert!(matches!(iterable.kind, ExprKind::Range { .. })); + } + + #[test] + fn assoc_call_and_postfix() { + parse_ok("class C { func new() -> C { return C {}; } } func main() { let c = C::new(); }"); + parse_ok("func f(xs: [[int]]) { let v = xs[0][1] + xs[1].len(); }"); + } + + #[test] + fn interpolated_string_expressions_parse() { + let p = parse_ok(r#"func f(n: int) { println("n = {n}, next = {n + 1}"); }"#); + let Item::Func(f) = &p.items[0] else { panic!() }; + let StmtKind::Expr(e) = &f.body.stmts[0].kind else { + panic!() + }; + let ExprKind::Call { args, .. } = &e.kind else { + panic!() + }; + let ExprKind::Str(parts) = &args[0].kind else { + panic!() + }; + let exprs = parts + .iter() + .filter(|p| matches!(p, StrPart::Expr(_))) + .count(); + assert_eq!(exprs, 2); + } + + #[test] + fn bad_interpolation_error_points_into_string() { + let src = r#"func f() { println("x = {1 +}"); }"#; + let d = parse_err(src); + // The error span must fall inside the string literal in the file. + let str_start = src.find('"').unwrap(); + assert!(d.span.start > str_start); + } + + #[test] + fn class_field_after_method_rejected() { + let d = parse_err("class C { func m(&self) { } x: int, }"); + assert!(d.help.unwrap().contains("before")); + } + + #[test] + fn reference_field_rejected() { + let d = parse_err("class C { s: &string, }"); + assert!(d.message.contains("references")); + } + + #[test] + fn rust_types_get_hints() { + let d = parse_err("func f(x: i32) { }"); + assert!(d.help.unwrap().contains("`int`")); + } + + #[test] + fn assignment_targets_checked() { + parse_ok("func f(xs: [int]) { xs[0] = 1; }"); + let d = parse_err("func f() { 1 + 2 = 3; }"); + assert!(d.message.contains("cannot be assigned")); + } + + #[test] + fn top_level_statement_rejected() { + let d = parse_err("let x = 1;"); + assert!(d.help.unwrap().contains("inside functions")); + } + + #[test] + fn self_outside_class_rejected() { + let d = parse_err("func f(&self) { }"); + assert!(d.message.contains("class methods")); + } +} diff --git a/src/pretty.rs b/src/pretty.rs new file mode 100644 index 0000000..12c6a6f --- /dev/null +++ b/src/pretty.rs @@ -0,0 +1,281 @@ +//! AST pretty-printer: renders the tree in an indented, human-readable +//! form for `oxide ast`. Output is stable and diff-friendly so it can back +//! snapshot tests later. + +use crate::ast::*; + +pub fn print_program(program: &Program) -> String { + let mut p = Printer::default(); + p.line("Program"); + p.indented(|p| { + for item in &program.items { + match item { + Item::Func(f) => p.func(f), + Item::Class(c) => p.class(c), + } + } + }); + p.out +} + +#[derive(Default)] +struct Printer { + out: String, + depth: usize, +} + +impl Printer { + fn line(&mut self, text: impl AsRef) { + for _ in 0..self.depth { + self.out.push_str(" "); + } + self.out.push_str(text.as_ref()); + self.out.push('\n'); + } + + fn indented(&mut self, f: impl FnOnce(&mut Self)) { + self.depth += 1; + f(self); + self.depth -= 1; + } + + fn func(&mut self, f: &Func) { + let params: Vec = f + .params + .iter() + .map(|p| match p { + Param::SelfParam { kind, .. } => match kind { + SelfKind::Owned => "self".to_string(), + SelfKind::Ref => "&self".to_string(), + SelfKind::RefMut => "&mut self".to_string(), + }, + Param::Normal { name, ty, .. } => format!("{}: {}", name.name, type_str(ty)), + }) + .collect(); + let ret = f + .return_type + .as_ref() + .map(|t| format!(" -> {}", type_str(t))) + .unwrap_or_default(); + self.line(format!( + "Func {}({}){}", + f.name.name, + params.join(", "), + ret + )); + self.indented(|p| p.block(&f.body)); + } + + fn class(&mut self, c: &Class) { + self.line(format!("Class {}", c.name.name)); + self.indented(|p| { + for field in &c.fields { + p.line(format!( + "Field {}: {}", + field.name.name, + type_str(&field.ty) + )); + } + for method in &c.methods { + p.func(method); + } + }); + } + + fn block(&mut self, b: &Block) { + self.line("Block"); + self.indented(|p| { + for stmt in &b.stmts { + p.stmt(stmt); + } + if let Some(tail) = &b.tail { + p.line("Tail"); + p.indented(|p| p.expr(tail)); + } + }); + } + + fn stmt(&mut self, s: &Stmt) { + match &s.kind { + StmtKind::Let { + mutable, + name, + ty, + value, + } => { + let mut_str = if *mutable { " mut" } else { "" }; + let ty_str = ty + .as_ref() + .map(|t| format!(": {}", type_str(t))) + .unwrap_or_default(); + self.line(format!("Let{} {}{}", mut_str, name.name, ty_str)); + self.indented(|p| p.expr(value)); + } + StmtKind::Assign { target, value } => { + self.line("Assign"); + self.indented(|p| { + p.expr(target); + p.expr(value); + }); + } + StmtKind::Expr(e) => { + self.line("ExprStmt"); + self.indented(|p| p.expr(e)); + } + StmtKind::Return(value) => { + self.line("Return"); + if let Some(v) = value { + self.indented(|p| p.expr(v)); + } + } + StmtKind::If(if_expr) => self.if_expr(if_expr, "IfStmt"), + StmtKind::While { cond, body } => { + self.line("While"); + self.indented(|p| { + p.expr(cond); + p.block(body); + }); + } + StmtKind::For { + var, + iterable, + body, + } => { + self.line(format!("For {}", var.name)); + self.indented(|p| { + p.expr(iterable); + p.block(body); + }); + } + StmtKind::Loop { body } => { + self.line("Loop"); + self.indented(|p| p.block(body)); + } + StmtKind::Break => self.line("Break"), + StmtKind::Continue => self.line("Continue"), + } + } + + fn if_expr(&mut self, i: &IfExpr, label: &str) { + self.line(label); + self.indented(|p| { + p.line("Cond"); + p.indented(|p| p.expr(&i.cond)); + p.line("Then"); + p.indented(|p| p.block(&i.then_block)); + if let Some(else_block) = &i.else_block { + p.line("Else"); + p.indented(|p| p.block(else_block)); + } + }); + } + + fn expr(&mut self, e: &Expr) { + match &e.kind { + ExprKind::Int(v) => self.line(format!("Int {v}")), + ExprKind::Float(v) => self.line(format!("Float {v}")), + ExprKind::Bool(v) => self.line(format!("Bool {v}")), + ExprKind::Var(name) => self.line(format!("Var {name}")), + ExprKind::Str(parts) => { + self.line("Str"); + self.indented(|p| { + for part in parts { + match part { + StrPart::Text(t) => p.line(format!("Text {t:?}")), + StrPart::Expr(e) => { + p.line("Interp"); + p.indented(|p| p.expr(e)); + } + } + } + }); + } + ExprKind::List(elements) => { + self.line("List"); + self.indented(|p| { + for el in elements { + p.expr(el); + } + }); + } + ExprKind::Range { start, end } => { + self.line("Range"); + self.indented(|p| { + p.expr(start); + p.expr(end); + }); + } + ExprKind::Unary { op, operand } => { + self.line(format!("Unary {}", op.symbol())); + self.indented(|p| p.expr(operand)); + } + ExprKind::Binary { op, lhs, rhs } => { + self.line(format!("Binary {}", op.symbol())); + self.indented(|p| { + p.expr(lhs); + p.expr(rhs); + }); + } + ExprKind::Call { callee, args } => { + self.line(format!("Call {}", callee.name)); + self.indented(|p| { + for a in args { + p.expr(a); + } + }); + } + ExprKind::AssocCall { class, func, args } => { + self.line(format!("AssocCall {}::{}", class.name, func.name)); + self.indented(|p| { + for a in args { + p.expr(a); + } + }); + } + ExprKind::MethodCall { recv, method, args } => { + self.line(format!("MethodCall .{}", method.name)); + self.indented(|p| { + p.expr(recv); + for a in args { + p.expr(a); + } + }); + } + ExprKind::FieldAccess { recv, field } => { + self.line(format!("Field .{}", field.name)); + self.indented(|p| p.expr(recv)); + } + ExprKind::Index { recv, index } => { + self.line("Index"); + self.indented(|p| { + p.expr(recv); + p.expr(index); + }); + } + ExprKind::ClassLiteral { class, fields } => { + self.line(format!("ClassLiteral {}", class.name)); + self.indented(|p| { + for (name, value) in fields { + p.line(format!("Field {}", name.name)); + p.indented(|p| p.expr(value)); + } + }); + } + ExprKind::If(if_expr) => self.if_expr(if_expr, "IfExpr"), + } + } +} + +fn type_str(t: &Type) -> String { + match &t.kind { + TypeKind::Int => "int".into(), + TypeKind::Float => "float".into(), + TypeKind::Bool => "bool".into(), + TypeKind::Str => "string".into(), + TypeKind::Unit => "()".into(), + TypeKind::Named(n) => n.clone(), + TypeKind::List(inner) => format!("[{}]", type_str(inner)), + TypeKind::Ref(inner) => format!("&{}", type_str(inner)), + TypeKind::RefMut(inner) => format!("&mut {}", type_str(inner)), + } +} diff --git a/src/span.rs b/src/span.rs new file mode 100644 index 0000000..4411be1 --- /dev/null +++ b/src/span.rs @@ -0,0 +1,33 @@ +//! Byte-offset source spans. Every token and AST node carries one so that +//! diagnostics (and later, runtime panics) always point at source. + +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Span { + pub fn new(start: usize, end: usize) -> Self { + Span { start, end } + } + + /// The smallest span covering both `self` and `other`. + pub fn to(self, other: Span) -> Span { + Span::new(self.start.min(other.start), self.end.max(other.end)) + } + + /// Shift the span by `offset` bytes (used when re-parsing interpolated + /// string segments, whose text was lexed relative to the segment start). + pub fn offset(self, offset: usize) -> Span { + Span::new(self.start + offset, self.end + offset) + } +} + +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}..{}", self.start, self.end) + } +} diff --git a/src/tir.rs b/src/tir.rs new file mode 100644 index 0000000..b93eb42 --- /dev/null +++ b/src/tir.rs @@ -0,0 +1,267 @@ +//! Typed IR: the checker's output and the input to the borrow checker and +//! the bytecode compiler. +//! +//! Compared to the AST, names are resolved to local slots, every expression +//! carries its type, implicit int→float widenings are explicit +//! `IntToFloat` nodes, operators are split by operand type, and — new with +//! milestone 5 — every access to a variable (or a projection from one) is +//! a `LoadPlace` with an explicit **mode**: copy, move, or borrow. The +//! borrow checker reads those modes; the compiler ignores them (at runtime +//! a borrow of a heap value is the same `Rc`). + +use crate::span::Span; +use crate::ty::Ty; + +#[derive(Debug)] +pub struct TProgram { + pub funcs: Vec, + /// Index of `main` in `funcs`. + pub main: usize, +} + +#[derive(Debug)] +pub struct TFunc { + pub name: String, + /// Parameters occupy local slots `0..num_params`. + pub num_params: usize, + pub num_locals: usize, + pub ret: Ty, + pub body: TBlock, + pub span: Span, + /// Metadata for every local slot (borrow-checker diagnostics). + pub locals: Vec, +} + +#[derive(Debug, Clone)] +pub struct LocalMeta { + pub name: String, + pub ty: Ty, + pub mutable: bool, + pub is_param: bool, + pub span: Span, +} + +/// A place: a storage location rooted at a local, with projections. +#[derive(Debug)] +pub struct TPlace { + pub root: usize, + pub projs: Vec, + pub span: Span, +} + +#[derive(Debug)] +pub enum TProj { + Field(usize), + Index(TExpr), + /// Looking through a reference. No runtime effect (a ref is the value). + Deref, +} + +/// How a place is accessed (design doc §8). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlaceMode { + /// Duplicate a Copy value (or a shared reference). + Copy, + /// Take ownership. The checker guarantees the place is a bare root of + /// an owned type. + Move, + /// `&place` (or an implicit read-only use: interpolation, `==`, `len`, + /// `&self` receivers). + Borrow, + /// `&mut place` (or an implicit `&mut self` receiver). + BorrowMut, +} + +#[derive(Debug)] +pub struct TBlock { + pub stmts: Vec, + /// Only present for value blocks (if-expression branches). + pub tail: Option>, +} + +#[derive(Debug)] +pub enum TStmt { + Let { + slot: usize, + value: TExpr, + }, + /// Assignment to a place: bare root, or through field/index + /// projections (possibly behind `&mut` derefs). + AssignPlace { + place: TPlace, + value: TExpr, + span: Span, + }, + /// Expression for effect. + Expr(TExpr), + If { + cond: TExpr, + then_b: TBlock, + else_b: Option, + }, + While { + cond: TExpr, + body: TBlock, + }, + Loop { + body: TBlock, + /// Whether any `break` binds to this loop — a breakless `loop` + /// never falls through, which return-path analysis relies on. + has_break: bool, + }, + /// `for var in list { ... }` — `list_slot`/`idx_slot` are hidden + /// locals holding the list and the current position. + ForList { + var_slot: usize, + list_slot: usize, + idx_slot: usize, + list: TExpr, + /// True when iterating by reference (`for x in &xs` or a `&[T]` + /// value): the list is borrowed for the loop, not consumed. + by_ref: bool, + body: TBlock, + }, + /// `for var in start..end { ... }` — `end_slot` is a hidden local. + ForRange { + var_slot: usize, + end_slot: usize, + start: TExpr, + end: TExpr, + body: TBlock, + }, + Break(Span), + Continue(Span), + /// `return;` / `return expr;` + Return(Option), +} + +#[derive(Debug)] +pub struct TExpr { + pub kind: TExprKind, + pub ty: Ty, + pub span: Span, +} + +#[derive(Debug)] +pub enum TExprKind { + Int(i64), + Float(f64), + Bool(bool), + /// Interpolated string: pieces are concatenated at runtime. + Str(Vec), + /// Access a place with an explicit mode. + LoadPlace { + place: TPlace, + mode: PlaceMode, + }, + /// `&expr` / `&mut expr` of a non-place (temporary) expression. The + /// runtime value keeps the temporary alive, so no loan is needed. + BorrowTemp { + value: Box, + mutable: bool, + }, + /// `*expr` of a non-place expression (runtime no-op). + Deref(Box), + /// Projection from a temporary (e.g. `Point::new(1,2).x`). + GetFieldTemp { + recv: Box, + field_idx: usize, + }, + IndexTemp { + recv: Box, + index: Box, + }, + List(Vec), + NegI(Box), + NegF(Box), + Not(Box), + Binary { + op: TBinOp, + lhs: Box, + rhs: Box, + }, + /// Short-circuiting `and` / `or`. + Logic { + is_and: bool, + lhs: Box, + rhs: Box, + }, + IntToFloat(Box), + FloatToInt(Box), + ToStr(Box), + /// `.len()` on a (possibly borrowed) string or list. + Len(Box), + /// `.clone()` — deep copy of a string, list, or class instance. + CloneVal(Box), + /// `.sqrt()` on a float. + Sqrt(Box), + /// Call to a user-defined function (index into `TProgram::funcs`). + /// Pushes exactly one value — unit functions push `()`. Methods are + /// lowered to functions whose slot 0 is `self`. + CallUser { + func: usize, + args: Vec, + }, + /// `ClassName { ... }` — field values in declaration order. + NewObject(Vec), + /// `println(s)` — pushes nothing (type is Unit). + Println(Box), + /// `assert(cond, msg)` — msg is synthesized when omitted. + Assert { + cond: Box, + msg: Box, + }, + /// `panic(msg)` + Panic(Box), + /// `if` in value position: both branches push a value of `ty`. + If { + cond: Box, + then_b: TBlock, + else_b: TBlock, + }, +} + +impl TExpr { + /// Does evaluating this expression leave a value on the stack? + /// (The unit built-ins are the only expressions that push nothing; + /// unit-returning user calls still push `()`.) + pub fn pushes_value(&self) -> bool { + !matches!( + self.kind, + TExprKind::Println(_) | TExprKind::Assert { .. } | TExprKind::Panic(_) + ) + } +} + +#[derive(Debug)] +pub enum TStrPart { + Text(String), + /// Already wrapped in ToStr / to_str calls by the checker. + Expr(TExpr), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TBinOp { + AddI, + SubI, + MulI, + DivI, + RemI, + AddF, + SubF, + MulF, + DivF, + RemF, + Concat, + /// Same-type equality (int, float, bool, string). + Eq, + Ne, + LtI, + LeI, + GtI, + GeI, + LtF, + LeF, + GtF, + GeF, +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..697fa84 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,187 @@ +//! Token definitions for the v1 grammar (design doc §3). + +use crate::span::Span; +use std::fmt; + +#[derive(Debug, Clone, PartialEq)] +pub struct Token { + pub kind: TokenKind, + pub span: Span, +} + +/// One piece of an interpolated string literal: either literal text (with +/// escapes already processed) or the raw source of an embedded expression. +/// The expression text is re-lexed and parsed by the parser; `offset` is +/// the byte position of the expression's first character in the original +/// file, so sub-spans can be mapped back to real source locations. +#[derive(Debug, Clone, PartialEq)] +pub enum StringSegment { + Text(String), + Expr { source: String, offset: usize }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum TokenKind { + // Literals + Int(i64), + Float(f64), + Str(Vec), + True, + False, + + Ident(String), + + // Keywords (design doc §3) + Func, + Let, + Mut, + Class, + If, + Else, + While, + For, + In, + Loop, + Break, + Continue, + Return, + SelfKw, + And, + Or, + Not, + + // Punctuation and operators + LParen, + RParen, + LBrace, + RBrace, + LBracket, + RBracket, + Comma, + Colon, + ColonColon, + Semi, + Dot, + DotDot, + Arrow, // -> + Plus, + Minus, + Star, + Slash, + Percent, + Amp, // & + AmpMut, // `&mut` fused by the lexer: unambiguous, one token + Eq, // = + EqEq, + NotEq, // != + Lt, + LtEq, + Gt, + GtEq, + + Eof, +} + +impl TokenKind { + /// Keyword lookup, including reserved words we reject with a hint. + /// Returns Err(help) for reserved-for-future keywords. + pub fn keyword(ident: &str) -> Option> { + use TokenKind::*; + let tok = match ident { + "func" => Func, + "let" => Let, + "mut" => Mut, + "class" => Class, + "if" => If, + "else" => Else, + "while" => While, + "for" => For, + "in" => In, + "loop" => Loop, + "break" => Break, + "continue" => Continue, + "return" => Return, + "self" => SelfKw, + "and" => And, + "or" => Or, + "not" => Not, + "true" => True, + "false" => False, + "fn" => { + return Some(Err("Oxide spells this keyword `func`".into())); + } + "enum" | "match" | "trait" | "impl" | "pub" | "mod" | "use" | "const" | "static" + | "async" | "await" => { + return Some(Err(format!( + "`{ident}` is reserved for a future version of Oxide and cannot be used as a name" + ))); + } + _ => return None, + }; + Some(Ok(tok)) + } + + /// Human-readable name for error messages. + pub fn describe(&self) -> String { + use TokenKind::*; + match self { + Int(_) => "an integer literal".into(), + Float(_) => "a float literal".into(), + Str(_) => "a string literal".into(), + True => "`true`".into(), + False => "`false`".into(), + Ident(name) => format!("identifier `{name}`"), + Func => "`func`".into(), + Let => "`let`".into(), + Mut => "`mut`".into(), + Class => "`class`".into(), + If => "`if`".into(), + Else => "`else`".into(), + While => "`while`".into(), + For => "`for`".into(), + In => "`in`".into(), + Loop => "`loop`".into(), + Break => "`break`".into(), + Continue => "`continue`".into(), + Return => "`return`".into(), + SelfKw => "`self`".into(), + And => "`and`".into(), + Or => "`or`".into(), + Not => "`not`".into(), + LParen => "`(`".into(), + RParen => "`)`".into(), + LBrace => "`{`".into(), + RBrace => "`}`".into(), + LBracket => "`[`".into(), + RBracket => "`]`".into(), + Comma => "`,`".into(), + Colon => "`:`".into(), + ColonColon => "`::`".into(), + Semi => "`;`".into(), + Dot => "`.`".into(), + DotDot => "`..`".into(), + Arrow => "`->`".into(), + Plus => "`+`".into(), + Minus => "`-`".into(), + Star => "`*`".into(), + Slash => "`/`".into(), + Percent => "`%`".into(), + Amp => "`&`".into(), + AmpMut => "`&mut`".into(), + Eq => "`=`".into(), + EqEq => "`==`".into(), + NotEq => "`!=`".into(), + Lt => "`<`".into(), + LtEq => "`<=`".into(), + Gt => "`>`".into(), + GtEq => "`>=`".into(), + Eof => "end of file".into(), + } + } +} + +impl fmt::Display for TokenKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.describe()) + } +} diff --git a/src/ty.rs b/src/ty.rs new file mode 100644 index 0000000..cc5ea08 --- /dev/null +++ b/src/ty.rs @@ -0,0 +1,63 @@ +//! Semantic types (design doc §4, §8). Distinct from `ast::Type`, which is +//! surface syntax; the checker lowers surface types into these. + +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ty { + Int, + Float, + Bool, + Str, + Unit, + List(Box), + /// A user-defined class, identified by name (class names are unique). + Class(String), + /// `&T` — shared borrow. + Ref(Box), + /// `&mut T` — exclusive borrow. + RefMut(Box), +} + +impl Ty { + /// Types that can appear in string interpolation and `str(...)` (§9) + /// without help. Classes become printable via a `to_str` method — the + /// checker handles that case separately. + pub fn is_printable(&self) -> bool { + matches!(self, Ty::Int | Ty::Float | Ty::Bool | Ty::Str) + } + + /// Copy types duplicate instead of moving (§8.1 rule 4). Shared + /// references are Copy (like Rust); `&mut` is not. + pub fn is_copy(&self) -> bool { + matches!(self, Ty::Int | Ty::Float | Ty::Bool | Ty::Unit | Ty::Ref(_)) + } + + /// Strip any reference wrapper: `&T` / `&mut T` → `T`. + pub fn deref_target(&self) -> &Ty { + match self { + Ty::Ref(inner) | Ty::RefMut(inner) => inner, + other => other, + } + } + + pub fn is_ref(&self) -> bool { + matches!(self, Ty::Ref(_) | Ty::RefMut(_)) + } +} + +impl fmt::Display for Ty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Ty::Int => write!(f, "int"), + Ty::Float => write!(f, "float"), + Ty::Bool => write!(f, "bool"), + Ty::Str => write!(f, "string"), + Ty::Unit => write!(f, "()"), + Ty::List(inner) => write!(f, "[{inner}]"), + Ty::Class(name) => write!(f, "{name}"), + Ty::Ref(inner) => write!(f, "&{inner}"), + Ty::RefMut(inner) => write!(f, "&mut {inner}"), + } + } +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..c39c326 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,83 @@ +//! Runtime values. +//! +//! Heap values (strings, lists, objects) are reference-counted and shared: +//! loading a local clones an `Rc`, and a borrow *is* the same `Rc`. The +//! borrow checker guarantees programs cannot observe the sharing — a +//! moved-from binding can never be read again, and aliasing-xor-mutation +//! holds statically — so these are exactly the "moves without copying" +//! the design doc promises, implemented as cheap pointer sharing. + +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Clone)] +pub enum Value { + Int(i64), + Float(f64), + Bool(bool), + Str(Rc), + List(Rc>>), + /// A class instance: fields in declaration order. + Object(Rc>>), + Unit, +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::Int(a), Value::Int(b)) => a == b, + (Value::Float(a), Value::Float(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Str(a), Value::Str(b)) => a == b, + (Value::List(a), Value::List(b)) => *a.borrow() == *b.borrow(), + (Value::Object(a), Value::Object(b)) => *a.borrow() == *b.borrow(), + (Value::Unit, Value::Unit) => true, + _ => false, + } + } +} + +impl Value { + pub fn str(s: impl Into) -> Value { + Value::Str(Rc::new(s.into())) + } + + /// The `.clone()` method: a fully independent deep copy. + pub fn deep_clone(&self) -> Value { + match self { + Value::Str(s) => Value::Str(Rc::new((**s).clone())), + Value::List(items) => Value::List(Rc::new(RefCell::new( + items.borrow().iter().map(|v| v.deep_clone()).collect(), + ))), + Value::Object(fields) => Value::Object(Rc::new(RefCell::new( + fields.borrow().iter().map(|v| v.deep_clone()).collect(), + ))), + other => other.clone(), + } + } + + /// Render for `println` / `str()` / interpolation. Floats always show + /// a decimal point so the int/float distinction stays visible. + pub fn display(&self) -> String { + match self { + Value::Int(v) => v.to_string(), + Value::Float(v) => { + if v.is_finite() && v.fract() == 0.0 && v.abs() < 1e15 { + format!("{v:.1}") + } else { + v.to_string() + } + } + Value::Bool(v) => v.to_string(), + Value::Str(s) => (**s).clone(), + Value::List(items) => { + let inner: Vec = items.borrow().iter().map(|v| v.display()).collect(); + format!("[{}]", inner.join(", ")) + } + // Classes render via their `to_str` method; the checker never + // lets a bare object reach a display context. + Value::Object(_) => "".to_string(), + Value::Unit => "()".to_string(), + } + } +} diff --git a/src/vm.rs b/src/vm.rs new file mode 100644 index 0000000..817da68 --- /dev/null +++ b/src/vm.rs @@ -0,0 +1,397 @@ +//! The Oxide stack VM. Executes a `CompiledProgram` with one call frame +//! per active function; runtime failures (overflow, division by zero, +//! index out of bounds, explicit panics, stack overflow) surface as +//! `RuntimeError` with the source span of the faulting instruction plus +//! the call stack. + +use crate::bytecode::{CompiledProgram, Op}; +use crate::span::Span; +use crate::value::Value; +use std::io::Write; + +/// Recursion limit: a friendly panic beats blowing the host stack. +const MAX_FRAMES: usize = 10_000; + +#[derive(Debug)] +pub struct RuntimeError { + pub message: String, + pub span: Span, + /// Call stack at the moment of the panic, outermost first: + /// (function name, call-site span). The innermost (faulting) function + /// is named by `frame_name`. + pub trace: Vec<(String, Span)>, + pub frame_name: String, +} + +struct Frame { + func: usize, + ip: usize, + locals: Vec, +} + +pub fn run(program: &CompiledProgram, out: &mut W) -> Result<(), RuntimeError> { + let main = &program.funcs[program.main]; + let mut frames = vec![Frame { + func: program.main, + ip: 0, + locals: vec![Value::Unit; main.num_locals], + }]; + let mut stack: Vec = Vec::with_capacity(64); + + macro_rules! frame { + () => { + frames.last_mut().expect("at least one frame while running") + }; + } + + loop { + let func_idx = frame!().func; + let chunk = &program.funcs[func_idx]; + let ip = frame!().ip; + if ip >= chunk.code.len() { + // Every function ends with an explicit Return; reaching here + // means a compiler bug rather than a user error. + unreachable!("fell off the end of `{}`", chunk.name); + } + + macro_rules! fail { + ($($arg:tt)*) => {{ + let trace = frames[..frames.len() - 1] + .iter() + .map(|f| { + let c = &program.funcs[f.func]; + // f.ip was already advanced past the Call op. + (c.name.clone(), c.spans[f.ip.saturating_sub(1)]) + }) + .collect(); + return Err(RuntimeError { + message: format!($($arg)*), + span: chunk.spans[ip], + trace, + frame_name: chunk.name.clone(), + }); + }}; + } + macro_rules! pop { + () => { + stack.pop().expect("stack underflow: compiler bug") + }; + } + macro_rules! int_bin { + ($method:ident, $verb:literal) => {{ + let (b, a) = (pop!(), pop!()); + let (Value::Int(a), Value::Int(b)) = (a, b) else { + unreachable!() + }; + match a.$method(b) { + Some(v) => stack.push(Value::Int(v)), + None => fail!("integer overflow while {} {a} and {b}", $verb), + } + }}; + } + macro_rules! float_bin { + ($op:tt) => {{ + let (b, a) = (pop!(), pop!()); + let (Value::Float(a), Value::Float(b)) = (a, b) else { unreachable!() }; + stack.push(Value::Float(a $op b)); + }}; + } + macro_rules! cmp_int { + ($op:tt) => {{ + let (b, a) = (pop!(), pop!()); + let (Value::Int(a), Value::Int(b)) = (a, b) else { unreachable!() }; + stack.push(Value::Bool(a $op b)); + }}; + } + macro_rules! cmp_float { + ($op:tt) => {{ + let (b, a) = (pop!(), pop!()); + let (Value::Float(a), Value::Float(b)) = (a, b) else { unreachable!() }; + stack.push(Value::Bool(a $op b)); + }}; + } + + match chunk.code[ip] { + Op::Const(idx) => stack.push(chunk.consts[idx as usize].clone()), + Op::Unit => stack.push(Value::Unit), + Op::Pop => { + pop!(); + } + Op::Dup => { + let top = stack.last().expect("stack underflow: compiler bug").clone(); + stack.push(top); + } + Op::LoadLocal(slot) => { + let v = frame!().locals[slot as usize].clone(); + stack.push(v); + } + Op::StoreLocal(slot) => { + let v = pop!(); + frame!().locals[slot as usize] = v; + } + Op::Jump(target) => { + frame!().ip = target as usize; + continue; + } + Op::JumpIfFalse(target) => { + let Value::Bool(cond) = pop!() else { + unreachable!() + }; + if !cond { + frame!().ip = target as usize; + continue; + } + } + + Op::AddI => int_bin!(checked_add, "adding"), + Op::SubI => int_bin!(checked_sub, "subtracting"), + Op::MulI => int_bin!(checked_mul, "multiplying"), + Op::DivI => { + let (b, a) = (pop!(), pop!()); + let (Value::Int(a), Value::Int(b)) = (a, b) else { + unreachable!() + }; + if b == 0 { + fail!("division by zero"); + } + match a.checked_div(b) { + Some(v) => stack.push(Value::Int(v)), + None => fail!("integer overflow while dividing {a} by {b}"), + } + } + Op::RemI => { + let (b, a) = (pop!(), pop!()); + let (Value::Int(a), Value::Int(b)) = (a, b) else { + unreachable!() + }; + if b == 0 { + fail!("division by zero (in `%`)"); + } + match a.checked_rem(b) { + Some(v) => stack.push(Value::Int(v)), + None => fail!("integer overflow in `%` of {a} and {b}"), + } + } + Op::NegI => { + let Value::Int(v) = pop!() else { + unreachable!() + }; + match v.checked_neg() { + Some(v) => stack.push(Value::Int(v)), + None => fail!("integer overflow while negating {v}"), + } + } + + Op::AddF => float_bin!(+), + Op::SubF => float_bin!(-), + Op::MulF => float_bin!(*), + Op::DivF => float_bin!(/), + Op::RemF => float_bin!(%), + Op::NegF => { + let Value::Float(v) = pop!() else { + unreachable!() + }; + stack.push(Value::Float(-v)); + } + + Op::Concat => { + let (b, a) = (pop!(), pop!()); + let (Value::Str(a), Value::Str(b)) = (a, b) else { + unreachable!() + }; + stack.push(Value::str(format!("{a}{b}"))); + } + Op::ConcatN(n) => { + let at = stack.len() - n as usize; + let mut result = String::new(); + for v in stack.drain(at..) { + let Value::Str(s) = v else { unreachable!() }; + result.push_str(&s); + } + stack.push(Value::str(result)); + } + + Op::Not => { + let Value::Bool(v) = pop!() else { + unreachable!() + }; + stack.push(Value::Bool(!v)); + } + Op::Eq => { + let (b, a) = (pop!(), pop!()); + stack.push(Value::Bool(a == b)); + } + Op::Ne => { + let (b, a) = (pop!(), pop!()); + stack.push(Value::Bool(a != b)); + } + Op::LtI => cmp_int!(<), + Op::LeI => cmp_int!(<=), + Op::GtI => cmp_int!(>), + Op::GeI => cmp_int!(>=), + Op::LtF => cmp_float!(<), + Op::LeF => cmp_float!(<=), + Op::GtF => cmp_float!(>), + Op::GeF => cmp_float!(>=), + + Op::IntToFloat => { + let Value::Int(v) = pop!() else { + unreachable!() + }; + stack.push(Value::Float(v as f64)); + } + Op::FloatToInt => { + let Value::Float(v) = pop!() else { + unreachable!() + }; + if v.is_nan() { + fail!("cannot convert NaN to int"); + } + let truncated = v.trunc(); + if truncated < i64::MIN as f64 || truncated > i64::MAX as f64 { + fail!("{v} is out of range for int"); + } + stack.push(Value::Int(truncated as i64)); + } + Op::ToStr => { + let v = pop!(); + stack.push(Value::str(v.display())); + } + + Op::MakeList(n) => { + let at = stack.len() - n as usize; + let items: Vec = stack.drain(at..).collect(); + stack.push(Value::List(std::rc::Rc::new(std::cell::RefCell::new( + items, + )))); + } + Op::MakeObject(n) => { + let at = stack.len() - n as usize; + let fields: Vec = stack.drain(at..).collect(); + stack.push(Value::Object(std::rc::Rc::new(std::cell::RefCell::new( + fields, + )))); + } + Op::GetField(idx) => { + let Value::Object(obj) = pop!() else { + unreachable!() + }; + let v = obj.borrow()[idx as usize].clone(); + stack.push(v); + } + Op::SetField(idx) => { + let (value, obj) = (pop!(), pop!()); + let Value::Object(obj) = obj else { + unreachable!() + }; + obj.borrow_mut()[idx as usize] = value; + } + Op::Index => { + let (idx, list) = (pop!(), pop!()); + let (Value::List(items), Value::Int(i)) = (list, idx) else { + unreachable!() + }; + let items = items.borrow(); + if i < 0 || i as usize >= items.len() { + fail!( + "index {i} is out of bounds for a list of length {}", + items.len() + ); + } + stack.push(items[i as usize].clone()); + } + Op::SetIndex => { + let (value, idx, list) = (pop!(), pop!(), pop!()); + let (Value::Int(i), Value::List(items)) = (idx, list) else { + unreachable!() + }; + let mut items = items.borrow_mut(); + if i < 0 || i as usize >= items.len() { + fail!( + "index {i} is out of bounds for a list of length {}", + items.len() + ); + } + items[i as usize] = value; + } + Op::CloneVal => { + let v = pop!(); + stack.push(v.deep_clone()); + } + Op::SqrtF => { + let Value::Float(v) = pop!() else { + unreachable!() + }; + stack.push(Value::Float(v.sqrt())); + } + Op::Len => { + let v = pop!(); + let len = match v { + Value::Str(s) => s.chars().count() as i64, + Value::List(items) => items.borrow().len() as i64, + _ => unreachable!(), + }; + stack.push(Value::Int(len)); + } + + Op::Print => { + let Value::Str(s) = pop!() else { + unreachable!() + }; + // A failed write (e.g. stdout piped into `head`, pipe now + // closed) ends execution quietly, like any well-behaved + // CLI on SIGPIPE. + if writeln!(out, "{s}").is_err() { + return Ok(()); + } + } + Op::Assert => { + let (msg, cond) = (pop!(), pop!()); + let (Value::Bool(cond), Value::Str(msg)) = (cond, msg) else { + unreachable!() + }; + if !cond { + if msg.is_empty() { + fail!("assertion failed"); + } + fail!("assertion failed: {msg}"); + } + } + Op::Panic => { + let Value::Str(msg) = pop!() else { + unreachable!() + }; + fail!("{msg}"); + } + + Op::Call(idx) => { + if frames.len() >= MAX_FRAMES { + fail!("stack overflow: recursion deeper than {MAX_FRAMES} calls"); + } + let callee = &program.funcs[idx as usize]; + let mut locals = vec![Value::Unit; callee.num_locals]; + for slot in (0..callee.num_params).rev() { + locals[slot] = pop!(); + } + frame!().ip = ip + 1; // resume after the call + frames.push(Frame { + func: idx as usize, + ip: 0, + locals, + }); + continue; + } + Op::Return => { + let value = pop!(); + frames.pop(); + if frames.is_empty() { + return Ok(()); + } + stack.push(value); + continue; + } + Op::Halt => return Ok(()), + } + frame!().ip = ip + 1; + } +} diff --git a/tests/parse_examples.rs b/tests/parse_examples.rs new file mode 100644 index 0000000..05ad231 --- /dev/null +++ b/tests/parse_examples.rs @@ -0,0 +1,54 @@ +//! Integration tests: the example programs must parse, and the CLI's +//! diagnostics must render with correct line/column info. + +use oxide::diagnostics::SourceFile; +use oxide::parser; +use oxide::pretty; + +#[test] +fn tour_example_parses() { + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/tour.ox")) + .expect("examples/tour.ox must exist"); + let file = SourceFile::new("examples/tour.ox", src); + let program = match parser::parse(&file.text) { + Ok(p) => p, + Err(d) => panic!("tour.ox failed to parse:\n{}", file.render(&d)), + }; + // Sanity: everything in the file made it into the AST. + assert_eq!(program.items.len(), 10, "expected 2 classes + 8 functions"); + + // The pretty-printer must cover every node kind without panicking, and + // produce one line per AST node (loose lower bound). + let printed = pretty::print_program(&program); + assert!(printed.lines().count() > 100); + assert!(printed.starts_with("Program")); +} + +#[test] +fn diagnostics_point_at_the_right_line() { + let src = "func main() {\n let x = 1 < 2 < 3;\n}\n"; + let file = SourceFile::new("bad.ox", src); + let diag = parser::parse(&file.text).expect_err("chained comparison must be rejected"); + let rendered = file.render(&diag); + assert!( + rendered.contains("bad.ox:2:"), + "wrong location:\n{rendered}" + ); + assert!( + rendered.contains("let x = 1 < 2 < 3;"), + "source line missing:\n{rendered}" + ); + assert!(rendered.contains("help:"), "missing help:\n{rendered}"); +} + +#[test] +fn fn_keyword_diagnostic_is_friendly() { + let src = "fn main() {}\n"; + let file = SourceFile::new("rusty.ox", src); + let diag = parser::parse(&file.text).expect_err("`fn` must be rejected"); + let rendered = file.render(&diag); + assert!( + rendered.contains("func"), + "expected a `func` hint:\n{rendered}" + ); +} diff --git a/tests/run_programs.rs b/tests/run_programs.rs new file mode 100644 index 0000000..f4fc975 --- /dev/null +++ b/tests/run_programs.rs @@ -0,0 +1,1110 @@ +//! End-to-end tests for `oxide run`: source in, stdout (or a diagnostic / +//! runtime panic) out. + +use oxide::{borrowck, check, compile, parser, vm}; + +/// Compile and run `src`, returning captured stdout. +fn run(src: &str) -> Result { + let program = parser::parse(src).map_err(|d| format!("parse: {}", d.message))?; + let checked = check::check_program(&program).map_err(|d| { + format!( + "check: {}{}", + d.message, + d.help.map(|h| format!(" ({h})")).unwrap_or_default() + ) + })?; + borrowck::borrowck(&checked).map_err(|d| { + format!( + "borrow: {}{}", + d.message, + d.help.map(|h| format!(" ({h})")).unwrap_or_default() + ) + })?; + let chunk = compile::compile(&checked); + let mut out = Vec::new(); + vm::run(&chunk, &mut out).map_err(|e| format!("panic: {}", e.message))?; + Ok(String::from_utf8(out).expect("VM output is UTF-8")) +} + +fn run_ok(src: &str) -> String { + run(src).expect("program should run") +} + +fn run_err(src: &str) -> String { + run(src).expect_err("program should fail") +} + +#[test] +fn hello_world() { + assert_eq!( + run_ok(r#"func main() { println("Hello, world!"); }"#), + "Hello, world!\n" + ); +} + +#[test] +fn arithmetic_precedence_and_widening() { + let out = run_ok( + r#"func main() { + println("{1 + 2 * 3}"); + println("{(1 + 2) * 3}"); + println("{7 / 2} {7 % 2}"); + println("{1 + 0.5}"); + println("{10 / 4.0}"); + println("{-3 + -2}"); + }"#, + ); + assert_eq!(out, "7\n9\n3 1\n1.5\n2.5\n-5\n"); +} + +#[test] +fn floats_always_show_a_decimal_point() { + assert_eq!( + run_ok(r#"func main() { println("{2.0 * 10}"); }"#), + "20.0\n" + ); +} + +#[test] +fn mutation_and_shadowing() { + let out = run_ok( + r#"func main() { + let mut x = 1; + x = x + 1; + let y = 10; + let y = y + 1; // shadowing, like Rust + println("{x} {y}"); + }"#, + ); + assert_eq!(out, "2 11\n"); +} + +#[test] +fn if_expression_with_branch_widening() { + let out = run_ok( + r#"func main() { + let v = if true { 1 } else { 2.5 }; + println("{v}"); + let grade = if 95 >= 90 { "A" } else { "B" }; + println("{grade}"); + }"#, + ); + assert_eq!(out, "1.0\nA\n"); +} + +#[test] +fn if_statements_and_logic() { + let out = run_ok( + r#"func main() { + let x = 5; + if x > 3 and x < 10 { + println("mid"); + } else { + println("out"); + } + if x == 99 or not (x == 5) { + println("no"); + } + }"#, + ); + assert_eq!(out, "mid\n"); +} + +#[test] +fn lists_index_store_and_len() { + let out = run_ok( + r#"func main() { + let mut xs = [1, 2, 3]; + xs[1] = xs[0] * 10; + let empty: [string] = []; + println("{xs[1]} {xs.len()} {empty.len()}"); + let nested = [[1, 2], [3]]; + println("{nested[0][1]} {nested[1].len()}"); + }"#, + ); + assert_eq!(out, "10 3 0\n2 1\n"); +} + +#[test] +fn interpolation_and_string_ops() { + let out = run_ok( + r#"func main() { + let name = "ox"; + println("{name}ide has {name.len() + 2} letters, {{escaped}}"); + let joined = "a" + "b" + "c"; + println("{joined == "abc"} {str(42) + "!"}"); + }"#, + ); + assert_eq!(out, "oxide has 4 letters, {escaped}\ntrue 42!\n"); +} + +#[test] +fn conversions() { + let out = run_ok( + r#"func main() { + println("{int(3.9)} {int(-3.9)} {float(2) / 8}"); + }"#, + ); + assert_eq!(out, "3 -3 0.25\n"); +} + +#[test] +fn early_return_halts() { + let out = run_ok( + r#"func main() { + println("before"); + if true { + return; + } + println("after"); + }"#, + ); + assert_eq!(out, "before\n"); +} + +// ── runtime panics ────────────────────────────────────────────────── + +#[test] +fn overflow_panics() { + let err = run_err("func main() { let x = 9_223_372_036_854_775_807 + 1; }"); + assert!(err.contains("overflow"), "{err}"); +} + +#[test] +fn division_by_zero_panics() { + let err = run_err("func main() { let zero = 0; let x = 1 / zero; }"); + assert!(err.contains("division by zero"), "{err}"); +} + +#[test] +fn index_out_of_bounds_panics() { + let err = run_err("func main() { let xs = [1]; println(\"{xs[5]}\"); }"); + assert!(err.contains("out of bounds"), "{err}"); +} + +#[test] +fn assert_and_panic() { + let err = run_err(r#"func main() { assert(1 == 2, "math broke"); }"#); + assert_eq!(err, "panic: assertion failed: math broke"); + let err = run_err(r#"func main() { assert(false); }"#); + assert_eq!(err, "panic: assertion failed"); + let err = run_err(r#"func main() { panic("boom {1 + 1}"); }"#); + assert_eq!(err, "panic: boom 2"); +} + +// ── check-time errors ─────────────────────────────────────────────── + +#[test] +fn assignment_to_immutable_is_rejected() { + let err = run_err("func main() { let x = 1; x = 2; }"); + assert!( + err.contains("not mutable") && err.contains("let mut"), + "{err}" + ); +} + +#[test] +fn unknown_variable_is_rejected() { + let err = run_err("func main() { println(\"{nope}\"); }"); + assert!(err.contains("cannot find `nope`"), "{err}"); +} + +#[test] +fn string_plus_int_suggests_interpolation() { + let err = run_err(r#"func main() { let s = "n = " + 1; }"#); + assert!(err.contains("interpolation"), "{err}"); +} + +#[test] +fn no_implicit_return() { + let err = run_err("func main() { 1 + 2 }"); + assert!(err.contains("no implicit return"), "{err}"); +} + +#[test] +fn type_mismatch_in_assignment() { + let err = run_err(r#"func main() { let mut x = 1; x = "hi"; }"#); + assert!(err.contains("expected `int`"), "{err}"); +} + +#[test] +fn condition_must_be_bool() { + let err = run_err("func main() { if 1 { println(\"x\"); } }"); + assert!(err.contains("expected `bool`"), "{err}"); +} + +#[test] +fn structural_reference_limits() { + let err = run_err("func main() { let r = 0..10; }"); + assert!(err.contains("for"), "{err}"); + let err = run_err("func f() -> &string { } func main() { }"); + assert!(err.contains("owned value"), "{err}"); + let err = run_err("func f(x: &&int) { } func main() { }"); + assert!(err.contains("references to references"), "{err}"); + let err = run_err("func f(x: &mut int) { } func main() { }"); + assert!(err.contains("copyable"), "{err}"); + let err = run_err(r#"func main() { let s = "x"; let xs = [&s]; }"#); + assert!(err.contains("own their elements"), "{err}"); +} + +#[test] +fn missing_main_is_reported() { + let err = run_err("func helper(x: int) -> int { return x; }"); + assert!(err.contains("main"), "{err}"); +} + +// ── milestone 3: functions ────────────────────────────────────────── + +#[test] +fn function_calls_and_recursion() { + let out = run_ok( + r#" + func fib(n: int) -> int { + if n < 2 { + return n; + } + return fib(n - 1) + fib(n - 2); + } + func main() { + println("{fib(10)}"); + }"#, + ); + assert_eq!(out, "55\n"); +} + +#[test] +fn mutual_recursion_and_call_order() { + // `is_even` calls `is_odd` before it is defined — two-pass collection. + let out = run_ok( + r#" + func is_even(n: int) -> bool { + if n == 0 { return true; } + return is_odd(n - 1); + } + func is_odd(n: int) -> bool { + if n == 0 { return false; } + return is_even(n - 1); + } + func main() { + println("{is_even(10)} {is_odd(10)}"); + }"#, + ); + assert_eq!(out, "true false\n"); +} + +#[test] +fn unit_functions_and_early_return() { + let out = run_ok( + r#" + func shout(msg: string) { + if msg.len() == 0 { + return; + } + println("{msg}!"); + } + func main() { + shout("hi"); + shout(""); + shout("bye"); + }"#, + ); + assert_eq!(out, "hi!\nbye!\n"); +} + +#[test] +fn arguments_widen_and_lists_pass() { + let out = run_ok( + r#" + func avg(a: float, b: float) -> float { + return (a + b) / 2.0; + } + func total(xs: [int]) -> int { + let mut sum = 0; + for x in xs { + sum = sum + x; + } + return sum; + } + func main() { + println("{avg(1, 2)} {total([1, 2, 3, 4])}"); + }"#, + ); + assert_eq!(out, "1.5 10\n"); +} + +#[test] +fn not_all_paths_return_is_rejected() { + let err = run_err("func f(x: int) -> int { if x > 0 { return 1; } } func main() { }"); + assert!(err.contains("not every path"), "{err}"); +} + +#[test] +fn diverging_paths_count_as_returning() { + run_ok( + r#" + func f(x: int) -> int { + if x > 0 { return 1; } else { panic("negative"); } + } + func g(x: int) -> int { + loop { + if x > 0 { return x; } + } + } + func main() { println("{f(1) + g(2)}"); }"#, + ); +} + +#[test] +fn parameters_are_immutable() { + let err = run_err("func f(x: int) { x = 1; } func main() { }"); + assert!( + err.contains("parameter") && err.contains("let mut x = x"), + "{err}" + ); +} + +#[test] +fn arity_and_type_errors() { + let err = run_err("func f(x: int) -> int { return x; } func main() { let y = f(1, 2); }"); + assert!(err.contains("takes 1 argument"), "{err}"); + let err = run_err(r#"func f(x: int) -> int { return x; } func main() { let y = f("no"); }"#); + assert!(err.contains("parameter `x`"), "{err}"); + let err = run_err("func println() { } func main() { }"); + assert!(err.contains("built-in"), "{err}"); + let err = run_err("func f() { } func f() { } func main() { }"); + assert!(err.contains("already defined"), "{err}"); +} + +#[test] +fn stack_overflow_is_a_panic() { + let err = run_err("func f(n: int) -> int { return f(n + 1); } func main() { let x = f(0); }"); + assert!(err.contains("stack overflow"), "{err}"); +} + +// ── milestone 3: loops ────────────────────────────────────────────── + +#[test] +fn while_loop() { + let out = run_ok( + r#"func main() { + let mut n = 3; + while n > 0 { + println("{n}"); + n = n - 1; + } + }"#, + ); + assert_eq!(out, "3\n2\n1\n"); +} + +#[test] +fn for_over_range_and_list() { + let out = run_ok( + r#"func main() { + let mut sum = 0; + for i in 0..5 { + sum = sum + i; + } + let mut prod = 1; + for x in [2, 3, 4] { + prod = prod * x; + } + println("{sum} {prod}"); + }"#, + ); + assert_eq!(out, "10 24\n"); +} + +#[test] +fn break_and_continue() { + let out = run_ok( + r#"func main() { + let mut found = 0; + for i in 0..100 { + if i % 2 == 0 { + continue; + } + if i > 6 { + break; + } + found = found + i; + } + println("{found}"); + + let mut n = 0; + loop { + n = n + 1; + if n == 4 { break; } + } + println("{n}"); + }"#, + ); + assert_eq!(out, "9\n4\n"); // 1 + 3 + 5 +} + +#[test] +fn nested_loops_bind_break_to_innermost() { + let out = run_ok( + r#"func main() { + let mut count = 0; + for i in 0..3 { + for j in 0..10 { + if j == 2 { break; } + count = count + 1; + } + } + println("{count}"); + }"#, + ); + assert_eq!(out, "6\n"); +} + +#[test] +fn loop_var_scoping_and_empty_ranges() { + let out = run_ok( + r#"func main() { + let i = 99; + for i in 0..2 { + println("{i}"); + } + println("{i}"); + for x in 5..5 { + println("never"); + } + }"#, + ); + assert_eq!(out, "0\n1\n99\n"); +} + +#[test] +fn break_outside_loop_is_rejected() { + let err = run_err("func main() { break; }"); + assert!(err.contains("outside of a loop"), "{err}"); + let err = run_err("func main() { continue; }"); + assert!(err.contains("outside of a loop"), "{err}"); +} + +#[test] +fn example_hello_runs() { + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/hello.ox")) + .expect("examples/hello.ox must exist"); + let out = run_ok(&src); + assert!(out.starts_with("Hello, world!\n"), "{out}"); + assert!(out.contains("count = 2"), "{out}"); + assert!(out.contains("grade B"), "{out}"); + assert!(out.contains("ys = [50, 20, 30], len = 3"), "{out}"); +} + +// ── milestone 4: classes ──────────────────────────────────────────── + +const COUNTER: &str = r#" +class Counter { + count: int, + label: string, + + func new(label: string) -> Counter { + return Counter { count: 0, label: label }; + } + func increment(&mut self) { self.count = self.count + 1; } + func get(&self) -> int { return self.count; } + func to_str(&self) -> string { return "{self.label}: {self.count}"; } +} +"#; + +#[test] +fn construct_fields_and_methods() { + let out = run_ok(&format!( + r#"{COUNTER} + func main() {{ + let mut c = Counter::new("hits"); + c.increment(); + c.increment(); + println("{{c.get()}} {{c.count}} {{c.label}}"); + }}"# + )); + assert_eq!(out, "2 2 hits\n"); +} + +#[test] +fn to_str_powers_interpolation_and_println() { + let out = run_ok(&format!( + r#"{COUNTER} + func main() {{ + let mut c = Counter::new("n"); + c.increment(); + println("as interp: {{c}}"); + println(c); + println(str(c) + "!"); + }}"# + )); + assert_eq!(out, "as interp: n: 1\nn: 1\nn: 1!\n"); +} + +#[test] +fn field_assignment_and_composition() { + let out = run_ok( + r#" + class Point { x: float, y: float, } + class Rect { + origin: Point, + corner: Point, + func width(&self) -> float { return self.corner.x - self.origin.x; } + } + func main() { + let mut r = Rect { + origin: Point { x: 1.0, y: 1.0 }, + corner: Point { x: 4.0, y: 3.0 }, + }; + r.origin.x = 0.0; + println("{r.width()}"); + }"#, + ); + assert_eq!(out, "4.0\n"); +} + +#[test] +fn classes_in_lists_and_as_params() { + let out = run_ok( + r#" + class P { v: int, } + func double(p: P) -> int { return p.v * 2; } + func main() { + let ps = [P { v: 1 }, P { v: 2 }, P { v: 3 }]; + let mut sum = 0; + for p in ps { + sum = sum + double(p); + } + println("{sum}"); + }"#, + ); + assert_eq!(out, "12\n"); +} + +#[test] +fn owned_self_method() { + let out = run_ok( + r#" + class Wrapper { + v: int, + func unwrap(self) -> int { return self.v; } + } + func main() { + println("{Wrapper { v: 7 }.unwrap()}"); + }"#, + ); + assert_eq!(out, "7\n"); +} + +#[test] +fn mut_self_requires_mutable_receiver() { + let err = run_err(&format!( + r#"{COUNTER} + func main() {{ + let c = Counter::new("x"); + c.increment(); + }}"# + )); + assert!( + err.contains("&mut self") && err.contains("let mut"), + "{err}" + ); +} + +#[test] +fn ref_self_cannot_mutate() { + let err = run_err( + r#" + class C { + v: int, + func bad(&self) { self.v = 1; } + } + func main() { }"#, + ); + assert!(err.contains("&self") && err.contains("&mut self"), "{err}"); +} + +#[test] +fn class_literal_field_errors() { + let err = run_err("class P { x: int, y: int, } func main() { let p = P { x: 1 }; }"); + assert!(err.contains("missing field") && err.contains("y"), "{err}"); + let err = run_err("class P { x: int, } func main() { let p = P { x: 1, z: 2 }; }"); + assert!( + err.contains("no field `z`") && err.contains("available fields: x"), + "{err}" + ); + let err = run_err("class P { x: int, } func main() { let p = P { x: 1, x: 2 }; }"); + assert!(err.contains("set twice"), "{err}"); + let err = run_err(r#"class P { x: int, } func main() { let p = P { x: "s" }; }"#); + assert!(err.contains("field `x` of `P` is `int`"), "{err}"); +} + +#[test] +fn method_vs_associated_function_mixups() { + let err = run_err(&format!( + r#"{COUNTER} + func main() {{ let c = Counter::new("x"); let n = Counter::get(); }}"# + )); + assert!(err.contains("method") && err.contains("instance"), "{err}"); + let err = run_err(&format!( + r#"{COUNTER} + func main() {{ let c = Counter::new("x"); let d = c.new("y"); }}"# + )); + assert!( + err.contains("associated function") && err.contains("Counter::new"), + "{err}" + ); +} + +#[test] +fn unknown_class_and_field_errors() { + let err = run_err("func main() { let p = Nope { x: 1 }; }"); + assert!(err.contains("unknown class `Nope`"), "{err}"); + let err = run_err("class P { x: int, } func main() { let p = P { x: 1 }; let y = p.z; }"); + assert!(err.contains("no field `z`"), "{err}"); + let err = run_err("func f(p: Nope) { } func main() { }"); + assert!(err.contains("unknown type `Nope`"), "{err}"); +} + +#[test] +fn class_without_to_str_is_not_printable() { + let err = run_err("class P { x: int, } func main() { println(P { x: 1 }); }"); + assert!(err.contains("to_str"), "{err}"); +} + +#[test] +fn classes_cannot_be_compared() { + let err = run_err( + "class P { x: int, } func main() { let a = P { x: 1 }; let b = P { x: 1 }; let e = a == b; }", + ); + assert!(err.contains("cannot be compared"), "{err}"); +} + +#[test] +fn class_returned_from_function_and_stack_trace_names() { + let err = run_err( + r#" + class Safe { + limit: int, + func check(&self, v: int) { + if v > self.limit { + panic("over limit"); + } + } + } + func main() { + let s = Safe { limit: 10 }; + s.check(11); + }"#, + ); + assert_eq!(err, "panic: over limit"); +} + +#[test] +fn duplicate_definitions_rejected() { + let err = run_err("class C { x: int, } class C { y: int, } func main() { }"); + assert!(err.contains("already defined"), "{err}"); + let err = run_err("class C { func m(&self) { } func m(&self) { } } func main() { }"); + assert!(err.contains("already has a method"), "{err}"); + let err = run_err("class C { x: int, } func C() { } func main() { }"); + assert!(err.contains("already the name of a class"), "{err}"); +} + +#[test] +fn example_classes_runs() { + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/classes.ox")) + .expect("examples/classes.ox must exist"); + let out = run_ok(&src); + assert!(out.contains("scaled = (6.0, 8.0)"), "{out}"); + assert!(out.contains("clicks: 3"), "{out}"); + assert!(out.contains("area = 10.0"), "{out}"); + assert!(out.contains("path length = 10.0"), "{out}"); +} + +// ── milestone 5: moves ────────────────────────────────────────────── + +#[test] +fn use_after_move_is_rejected() { + let err = run_err( + r#"func main() { + let a = "hello"; + let b = a; + println(a); + }"#, + ); + assert!(err.contains("moved") && err.contains("clone"), "{err}"); +} + +#[test] +fn copy_types_do_not_move() { + let out = run_ok( + r#"func main() { + let a = 5; + let b = a; + println("{a} {b}"); + }"#, + ); + assert_eq!(out, "5 5\n"); +} + +#[test] +fn passing_owned_arg_moves() { + let err = run_err( + r#"func eat(s: string) { } + func main() { + let s = "x"; + eat(s); + println(s); + }"#, + ); + assert!(err.contains("moved"), "{err}"); +} + +#[test] +fn clone_avoids_the_move() { + let out = run_ok( + r#"func eat(s: string) { } + func main() { + let s = "x"; + eat(s.clone()); + println(s); + let mut xs = [1, 2]; + let ys = xs.clone(); + xs[0] = 99; + println("{xs[0]} {ys[0]}"); + }"#, + ); + assert_eq!(out, "x\n99 1\n"); +} + +#[test] +fn reassignment_revives_a_moved_variable() { + let out = run_ok( + r#"func main() { + let mut s = "one"; + let t = s; + s = "two"; + println("{s} {t}"); + }"#, + ); + assert_eq!(out, "two one\n"); +} + +#[test] +fn move_in_one_branch_counts() { + let err = run_err( + r#"func main() { + let s = "x"; + if true { + let t = s; + } + println(s); + }"#, + ); + assert!(err.contains("moved"), "{err}"); +} + +#[test] +fn iteration_carried_move_is_caught() { + let err = run_err( + r#"func main() { + let s = "x"; + for i in 0..3 { + let t = s; + } + }"#, + ); + assert!(err.contains("moved"), "{err}"); +} + +#[test] +fn moving_out_of_projections_needs_clone() { + let err = run_err( + r#"class P { name: string, } + func main() { + let p = P { name: "x" }; + let n = p.name; + }"#, + ); + assert!( + err.contains("out of a field") && err.contains("clone"), + "{err}" + ); + let err = run_err( + r#"func main() { + let xs = ["a"]; + let s = xs[0]; + }"#, + ); + assert!( + err.contains("out of a list") && err.contains("clone"), + "{err}" + ); +} + +#[test] +fn for_by_value_consumes_the_list() { + let err = run_err( + r#"func main() { + let xs = [1, 2]; + for x in xs { } + println("{xs.len()}"); + }"#, + ); + assert!(err.contains("moved"), "{err}"); +} + +// ── milestone 5: borrows ──────────────────────────────────────────── + +#[test] +fn shared_borrows_and_ref_params() { + let out = run_ok( + r#"func total(xs: &[int]) -> int { + let mut sum = 0; + for x in xs { + sum = sum + *x; + } + return sum; + } + func main() { + let xs = [1, 2, 3]; + println("{total(&xs)} {total(&xs)} {xs.len()}"); + }"#, + ); + assert_eq!(out, "6 6 3\n"); +} + +#[test] +fn mut_ref_param_mutates_caller_data() { + let out = run_ok( + r#"func fill(xs: &mut [int], v: int) { + xs[0] = v; + } + func bump(c: &mut Counter) { + c.n = c.n + 1; + } + class Counter { n: int, } + func main() { + let mut xs = [0, 0]; + fill(&mut xs, 42); + let mut c = Counter { n: 0 }; + bump(&mut c); + bump(&mut c); + println("{xs[0]} {c.n}"); + }"#, + ); + assert_eq!(out, "42 2\n"); +} + +#[test] +fn aliasing_xor_mutation() { + // Two shared borrows: fine. + run_ok( + r#"func main() { + let s = "x"; + let a = &s; + let b = &s; + println("{a} {b}"); + }"#, + ); + // Shared + mutable: rejected. + let err = run_err( + r#"func main() { + let mut xs = [1]; + let r = &xs; + let m = &mut xs; + println("{r.len()}"); + }"#, + ); + assert!(err.contains("already borrowed"), "{err}"); + // Two mutable: rejected. + let err = run_err( + r#"func main() { + let mut xs = [1]; + let a = &mut xs; + let b = &mut xs; + fill(a); + fill(b); + } + func fill(xs: &mut [int]) { xs[0] = 1; }"#, + ); + assert!(err.contains("already") && err.contains("borrow"), "{err}"); +} + +#[test] +fn nll_borrows_end_at_last_use() { + // The shared borrow of `s` ends at its last use, so the later + // mutation is fine (§8.2's non-lexical rule). + run_ok( + r#"func main() { + let mut s = "hi"; + let r = &s; + println(r); + s = "world"; + println(s); + }"#, + ); +} + +#[test] +fn borrow_still_live_blocks_mutation() { + let err = run_err( + r#"func main() { + let mut s = "hi"; + let r = &s; + s = "world"; + println(r); + }"#, + ); + assert!(err.contains("borrowed"), "{err}"); +} + +#[test] +fn cannot_move_while_borrowed() { + let err = run_err( + r#"func main() { + let s = "x"; + let r = &s; + let t = s; + println(r); + }"#, + ); + assert!(err.contains("move") && err.contains("borrowed"), "{err}"); +} + +#[test] +fn borrow_created_before_loop_extends_through_it() { + let err = run_err( + r#"func main() { + let mut s = "x"; + let r = &s; + for i in 0..3 { + s = "y"; + println(r); + } + }"#, + ); + assert!(err.contains("borrowed"), "{err}"); +} + +#[test] +fn mut_borrow_blocks_reads() { + let err = run_err( + r#"func touch(m: &mut [int], n: int) { m[0] = n; } + func main() { + let mut xs = [1]; + let m = &mut xs; + let n = xs.len(); + touch(m, n); + }"#, + ); + assert!( + err.contains("mutably borrowed") || err.contains("already"), + "{err}" + ); +} + +#[test] +fn mut_methods_through_mut_refs() { + let out = run_ok( + r#"class Counter { + n: int, + func bump(&mut self) { self.n = self.n + 1; } + func get(&self) -> int { return self.n; } + } + func twice(c: &mut Counter) { + c.bump(); + c.bump(); + } + func main() { + let mut c = Counter { n: 0 }; + twice(&mut c); + println("{c.get()}"); + }"#, + ); + assert_eq!(out, "2\n"); +} + +#[test] +fn mut_method_through_shared_ref_rejected() { + let err = run_err( + r#"class C { + n: int, + func bump(&mut self) { self.n = self.n + 1; } + } + func f(c: &C) { c.bump(); } + func main() { }"#, + ); + assert!(err.contains("shared") && err.contains("&mut"), "{err}"); +} + +#[test] +fn borrow_argument_discipline() { + let err = run_err( + r#"func f(xs: &[int]) { } + func main() { let xs = [1]; f(xs); }"#, + ); + assert!(err.contains("borrow it"), "{err}"); + let err = run_err( + r#"func f(xs: [int]) { } + func main() { let xs = [1]; f(&xs); }"#, + ); + assert!(err.contains("takes ownership"), "{err}"); + let err = run_err( + r#"func f(xs: &mut [int]) { } + func main() { let mut xs = [1]; f(&xs); }"#, + ); + assert!(err.contains("&mut"), "{err}"); + let err = run_err( + r#"func f(xs: &mut [int]) { } + func main() { let xs = [1]; f(&mut xs); }"#, + ); + assert!(err.contains("not mutable"), "{err}"); +} + +#[test] +fn refs_to_temporaries_are_fine() { + let out = run_ok( + r#"func total(xs: &[int]) -> int { + let mut sum = 0; + for x in xs { sum = sum + *x; } + return sum; + } + func main() { + println("{total(&[1, 2, 3])}"); + }"#, + ); + assert_eq!(out, "6\n"); +} + +#[test] +fn interpolation_borrows_do_not_move() { + let out = run_ok( + r#"func main() { + let s = "keep"; + println("{s} and {s} again"); + println(s); + }"#, + ); + assert_eq!(out, "keep and keep again\nkeep\n"); +} + +#[test] +fn example_tour_runs_end_to_end() { + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/tour.ox")) + .expect("examples/tour.ox must exist"); + let out = run_ok(&src); + assert!(out.contains("dist = 10"), "{out}"); + assert!(out.contains("clicks: 2"), "{out}"); + assert!(out.contains("grade = A"), "{out}"); + assert!(out.contains("excellent"), "{out}"); +} + +#[test] +fn example_borrow_runs() { + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/borrow.ox")) + .expect("examples/borrow.ox must exist"); + let out = run_ok(&src); + assert!(out.contains("sum = 10"), "{out}"); + assert!(out.contains("doubled: [2, 4, 6, 8]"), "{out}"); + assert!(out.contains("total = 15"), "{out}"); +}