Skip to content

Commit 32bbe3f

Browse files
committed
Fix VM-style compiler gaps (get_ref dispatch, field assign, method desugaring, match-arm flow) and Option/extern call-site typing so idiomatic programs and all examples build. Add integration tests, bytecode_vm example, refresh showcase/todo/http/text_summary, and update docs.
1 parent f61b9fc commit 32bbe3f

30 files changed

Lines changed: 974 additions & 190 deletions

.cursor/skills/writing-ion-code/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Import with paths like `import "stdlib/io.ion" as io;`:
150150

151151
No stdlib stdin/line input. For POSIX `read` on fd 0, see [examples/todo_demo/](../../../examples/todo_demo/).
152152

153-
Built-ins: `Vec<T>`, `String`, `Box<T>`, `Option<T>`, `Result<T, E>` (define enums in-file or import). `Vec::get` / `Vec::pop` move elements out; use `Vec::get_ref(&v, i)` for read-only in-function peek (`Option<&T>`, local only). `String` literals assign to `String`; use `String::from("...")` when a owned copy is needed.
153+
Built-ins: `Vec<T>`, `String`, `Box<T>`, `Option<T>`, `Result<T, E>` (define enums in-file or import). `Vec::get` / `Vec::pop` move elements out; use `Vec::get_ref(&v, i)` for read-only in-function peek (`Option<&T>`, local only). Match on `&Enum` from `get_ref` dispatches variants directly (no `*` deref). Struct field paths support `=` and `+=` on owned and `&mut` receivers. Nested generics such as `Vec<Vec<int>>` parse as consecutive `>` closings. String literals and `&String` coerce to `&str` at call sites.
154154

155155
## Build and verify
156156

.cursor/skills/writing-ion-code/references/verified-patterns.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,24 @@ s.push_str("hi");
4747
let lit: String = "hello";
4848
```
4949

50-
`Vec::get` / `Vec::pop` return `Option<T>` and **move** the element out. For read-only scans use `Vec::get_ref(&v, i)` which returns `Option<&T>` (local temporary only; cannot return or store). In `match Option::Some(x)`, `x` is `&T`: copy types bind by value; structs with owned fields bind as a pointer so nested `Vec` fields are not dropped. `Vec::set(&mut v, index, value)` returns `int` (0 = ok).
50+
`Vec::get` / `Vec::pop` return `Option<T>` and **move** the element out. For read-only scans use `Vec::get_ref(&v, i)` which returns `Option<&T>` (local temporary only; cannot return or store). In `match Option::Some(x)`, `x` is `&T`: copy types bind by value; structs and enums with non-copy fields bind as a pointer. Inner `match x` on `&Enum` dispatches variants without deref. `Vec::set(&mut v, index, value)` returns `int` (0 = ok). Method syntax (`v.push(x)`, `v.get_ref(i)`) desugars to `Vec::` builtins with correct receiver borrows.
51+
52+
## Struct field mutation
53+
54+
```ion
55+
struct VM { ip: int; stack: Vec<int>; }
56+
57+
fn step(vm: &mut VM) {
58+
vm.stack.push(1);
59+
vm.ip += 1;
60+
}
61+
```
62+
63+
Field paths are valid assignment targets on owned structs and `&mut` parameters.
64+
65+
## VM dispatch loop
66+
67+
See [tests/test_vm_execute.ion](../../../../tests/test_vm_execute.ion): `match vm.code.get_ref(vm.ip)` then inner `match op` on `&Op`, field updates, and `break` inside `match` within `loop`.
5168

5269
## Box
5370

@@ -68,10 +85,6 @@ let elem: int = arr[0]; // bounds checked unless unsafe
6885
## Borrowing (same function only)
6986

7087
```ion
71-
fn bump(x: &mut int) {
72-
*x = *x + 1;
73-
}
74-
7588
fn read_len(v: &Vec<int>) -> int {
7689
return v.len();
7790
}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2026-07
4+
5+
- **VM-style idioms**: `match` on `&Enum` from `Vec::get_ref`; struct field assignment and `+=` on owned/`&mut` paths; method desugaring (`vec.push`, `vec.get_ref`) with correct borrows; nested generic types (`Vec<Vec<int>>`); match-arm control-flow unification (`break`/`return` with value arms); `&str` call-site coercion; enum literals in `Vec::push`/`set` without double-wrapped C; `&mut Struct` field access via `->` in codegen. Integration tests and examples `bytecode_vm`, updated `showcase`, `todo_demo`, `http_server`, `text_summary`. Fix extern call typing so `&T` arguments match `&T` parameters (no erroneous copy-type ref stripping). Fix `Option<T>` match codegen to use the scrutinee type instead of the first registered monomorph. Former negative match-arm rvalue tests now pass as positive runs.
6+
37
## 2026-06
48

59
- **Readiness hardening**: beta compatibility and runtime ABI documents, a lightweight security policy, CLI/`ion-build` multi-error type diagnostics, sanitizer CI smoke (6 tests), and full integration harness `-Wall -Wextra -Werror` on Linux CI. Cgen warning-hygiene improvements (binding usage tracking and `(void)` silences, borrow/defer silences, string literal `.data`/`uint8_t*` casts, string `for...in` length casts), `String` runtime data as `uint8_t*`, and `CFLAGS`/`LDFLAGS` support in the integration harness.

ION_SPEC.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ Ion uses the following operators:
184184
- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`
185185
- Logical: `&&`, `||`, `!`
186186
- Bitwise: `&` (AND), `|` (OR), `^` (XOR), `<<` (left shift), `>>` (right shift)
187-
- Assignment: `=`, `+=` (compound assignment desugars to `x = x + e` for supported `+` types)
187+
- Assignment: `=`, `+=` (compound assignment desugars to `x = x + e` for supported `+` types). Assignment targets may be locals, index expressions, or field paths on owned structs or `&mut Struct` receivers (for example `vm.ip += 1`).
188188
- Type casting: `as` keyword for explicit type conversions
189189
- Field access: `.`
190190
- Address-of / borrow: `&` (borrow shared) and `&mut` (borrow exclusive)
@@ -1021,7 +1021,7 @@ Note that:
10211021
- `Vec<T>` is `Send` if `T: Send`.
10221022
- `Vec::new()` and `Vec::with_capacity()` infer `T` from a `let` type annotation when present (e.g. `let v: Vec<i32> = Vec::new()`).
10231023
- `Vec::get()` and `Vec::pop()` return `Option<T>` to handle out-of-bounds or empty cases. Both **move** the element out of the vector. To preserve vector length after a read-only scan, either use `Vec::get_ref()` (below) or copy fields and `Vec::set()` a rebuilt struct literal to put the value back (nested `Vec` fields still move on put-back).
1024-
- `Vec::get_ref()` returns `Option<&T>`: a **local, stack-only borrow** of an in-place element. It does not move or hollow the slot. The result is only valid as a short-lived binding within the current function (for example in a `match` arm). Match arms bind the element as `&T`; codegen uses `T*` for types with owned fields and copies by value for copy types, so repeated scans over `Vec<struct-with-nested-Vec>` do not double-free nested fields. It cannot be returned, stored in structs or enums, sent on channels, or cross `spawn`. While an `&T` from `get_ref` is active, the root owner of the vector (the binding behind `&Vec<T>`) is shared-borrowed: `&mut Vec<T>` on that owner, `Vec::set`, `Vec::push`, and `Vec::pop` on the same vector are rejected until the borrow ends. Out-of-range or negative indices yield `Option::None`. Nested inspection (`order.lines` then `get_ref`) follows the same root-owner borrow rules as field paths (Section 5.3).
1024+
- `Vec::get_ref()` returns `Option<&T>`: a **local, stack-only borrow** of an in-place element. It does not move or hollow the slot. The result is only valid as a short-lived binding within the current function (for example in a `match` arm). Match arms bind the element as `&T`; for enum elements, an inner `match` on that binding dispatches variants directly (no unary `*` deref). Copy fields in struct or enum variant patterns bind as `T`; non-copy fields bind as `&T`. Codegen uses `T*` for types with owned fields and copies by value for copy types, so repeated scans over `Vec<struct-with-nested-Vec>` do not double-free nested fields. It cannot be returned, stored in structs or enums, sent on channels, or cross `spawn`. While an `&T` from `get_ref` is active, the root owner of the vector (the binding behind `&Vec<T>`) is shared-borrowed: `&mut Vec<T>` on that owner, `Vec::set`, `Vec::push`, and `Vec::pop` on the same vector are rejected until the borrow ends. Out-of-range or negative indices yield `Option::None`. Nested inspection (`order.lines` then `get_ref`) follows the same root-owner borrow rules as field paths (Section 5.3). Field paths through `&Struct` that are already references (for example `order.lines` when `order: &Order`) are passed to `Vec` methods without an extra `&`.
10251025
- `Vec::set()` requires a mutable reference and returns an error code (0 for success, non-zero for failure). After shared borrows from `get_ref` end, `Vec::set` on the same index is allowed.
10261026

10271027
For cross-function or long-lived access, Ion still favors an **index/handle style**: helpers return indices or keys and callers re-index within their own function bodies.
@@ -1055,7 +1055,8 @@ Note that:
10551055
- `String::push_byte()` appends a single byte to an existing `String`.
10561056
- `==` and `!=` compare UTF-8 byte content (value equality), not pointer identity.
10571057

1058-
`&str` is always a **borrowed view** into existing UTF-8 data; it cannot be returned or stored in long-lived structures in ways that would violate the no-escape rule. The standard library intentionally avoids APIs that would expose `&str` values across function boundaries in ways that require complex lifetime reasoning (e.g., `String::as_str` methods that return borrowed views).
1058+
- `String::from` and stdlib APIs accepting `&str` also accept string literals and `&String` at call sites.
1059+
- `&str` is always a **borrowed view** into existing UTF-8 data; it cannot be returned or stored in long-lived structures in ways that would violate the no-escape rule. The standard library intentionally avoids APIs that would expose `&str` values across function boundaries in ways that require complex lifetime reasoning (e.g., `String::as_str` methods that return borrowed views).
10591060

10601061
#### 8.4 Channels
10611062

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ On Linux or macOS, use `./target/release/ion-build` and drop `.exe`. Windows cha
181181
| [spawn_channel/](examples/spawn_channel/) | `spawn` with cross-thread channels |
182182
| [channel_worker/](examples/channel_worker/) | Channel worker |
183183
| [worker_pool/](examples/worker_pool/) | Three workers with per-worker job/result channels |
184-
| [showcase/](examples/showcase/) | Mixed language features |
184+
| [showcase/](examples/showcase/) | Mixed language features (includes bytecode VM dispatch) |
185+
| [bytecode_vm/](examples/bytecode_vm/) | Stack VM: `get_ref` enum dispatch, field `+=`, method calls on `&mut` struct fields |
185186
| [access_log/](examples/access_log/) | Log parsing, spawn, channels, fmt/io |
186187
| [http_server/](examples/http_server/) | Sockets FFI, spawn per client, stdin `quit` to stop; see [http_server/README.md](examples/http_server/README.md) |
187188
| [text_summary/](examples/text_summary/) | `fs` read, string iteration, counts |
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Stack bytecode VM: idiomatic Ion patterns for interpreter-style loops.
2+
//
3+
// - Vec<Op> with method calls on struct fields (vm.code.push, vm.code.get_ref)
4+
// - match on &Op from get_ref (no unary * deref)
5+
// - struct field assignment and += on &mut VM
6+
// - break inside match within loop
7+
8+
enum Op { Push { val: int }; Add; Halt; }
9+
10+
struct VM {
11+
ip: int;
12+
stack: Vec<int>;
13+
code: Vec<Op>;
14+
}
15+
16+
fn execute(vm: &mut VM) -> int {
17+
loop {
18+
match vm.code.get_ref(vm.ip) {
19+
Option::Some(op) => {
20+
match op {
21+
Op::Push { val: v } => {
22+
vm.stack.push(v);
23+
vm.ip += 1;
24+
}
25+
Op::Add => {
26+
let b = match vm.stack.pop() {
27+
Option::Some(v) => { v; }
28+
Option::None => { return 0; }
29+
};
30+
let a = match vm.stack.pop() {
31+
Option::Some(v) => { v; }
32+
Option::None => { return 0; }
33+
};
34+
vm.stack.push(a + b);
35+
vm.ip += 1;
36+
}
37+
Op::Halt => { break; }
38+
};
39+
}
40+
Option::None => { break; }
41+
};
42+
}
43+
return 0;
44+
}
45+
46+
fn main() -> int {
47+
let mut vm: VM = VM {
48+
ip: 0,
49+
stack: Vec::new(),
50+
code: Vec::new(),
51+
};
52+
// Program: push 10, push 32, add -> 42, halt
53+
vm.code.push(Op::Push { val: 10 });
54+
vm.code.push(Op::Push { val: 32 });
55+
vm.code.push(Op::Add);
56+
vm.code.push(Op::Halt);
57+
execute(&mut vm);
58+
match vm.stack.pop() {
59+
Option::Some(n) => { return n; }
60+
Option::None => { return 1; }
61+
};
62+
}

examples/bytecode_vm/ion.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name = "bytecode_vm"
2+
main = "bytecode_vm.ion"
3+
output = "bytecode_vm"
4+
mode = "single"
5+
out_dir = "target"

examples/http_server/http_server.ion

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn dispatch_peer(peer_fd: int) {
8686
}
8787

8888
fn send_string_bytes(fd: int, text: String) {
89-
let nbytes: int = String::len(&text);
89+
let nbytes: int = text.len();
9090
unsafe {
9191
let _sent: int = send_sys(&fd, text.data, nbytes, 0);
9292
}
@@ -103,7 +103,7 @@ fn handle_client(client_fd: int) -> int {
103103
}
104104

105105
let body: String = http_body();
106-
let body_len: int = String::len(&body);
106+
let body_len: int = body.len();
107107
let digits: String = fmt::int_to_string(body_len);
108108

109109
let _sent1: int = send_sys(&client_fd, "HTTP/1.1 200 OK\r\n", 17, 0);

examples/showcase/showcase.ion

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fn vec_example() -> int {
8080
return 1;
8181
}
8282

83-
match numbers.get(0) {
83+
match numbers.get_ref(0) {
8484
Option::Some(value) => {
8585
if value != 10 {
8686
return 2;
@@ -373,7 +373,7 @@ fn complex_example() -> int {
373373
return 1;
374374
}
375375

376-
match numbers.get(0) {
376+
match numbers.get_ref(0) {
377377
Option::Some(value) => {
378378
if value != 1 {
379379
return 2;
@@ -384,7 +384,7 @@ fn complex_example() -> int {
384384
}
385385
};
386386

387-
match numbers.get(1) {
387+
match numbers.get_ref(1) {
388388
Option::Some(value) => {
389389
if value != 3 {
390390
return 4;
@@ -406,6 +406,70 @@ fn complex_example() -> int {
406406
return 0;
407407
}
408408

409+
// ============================================
410+
// 15. BYTECODE VM - get_ref dispatch, field mutation, method calls
411+
// ============================================
412+
413+
enum Op { Push { val: int }; Add; Halt; }
414+
415+
struct BytecodeVM {
416+
ip: int;
417+
stack: Vec<int>;
418+
code: Vec<Op>;
419+
}
420+
421+
fn run_bytecode(vm: &mut BytecodeVM) -> int {
422+
loop {
423+
match vm.code.get_ref(vm.ip) {
424+
Option::Some(op) => {
425+
match op {
426+
Op::Push { val: v } => {
427+
vm.stack.push(v);
428+
vm.ip += 1;
429+
}
430+
Op::Add => {
431+
let b = match vm.stack.pop() {
432+
Option::Some(v) => { v; }
433+
Option::None => { return 0; }
434+
};
435+
let a = match vm.stack.pop() {
436+
Option::Some(v) => { v; }
437+
Option::None => { return 0; }
438+
};
439+
vm.stack.push(a + b);
440+
vm.ip += 1;
441+
}
442+
Op::Halt => { break; }
443+
};
444+
}
445+
Option::None => { break; }
446+
};
447+
}
448+
return 0;
449+
}
450+
451+
fn bytecode_vm_example() -> int {
452+
let mut vm: BytecodeVM = BytecodeVM {
453+
ip: 0,
454+
stack: Vec::new(),
455+
code: Vec::new(),
456+
};
457+
vm.code.push(Op::Push { val: 10 });
458+
vm.code.push(Op::Push { val: 32 });
459+
vm.code.push(Op::Add);
460+
vm.code.push(Op::Halt);
461+
run_bytecode(&mut vm);
462+
match vm.stack.pop() {
463+
Option::Some(n) => {
464+
if n != 42 {
465+
return 1;
466+
}
467+
}
468+
Option::None => { return 2; }
469+
};
470+
return 0;
471+
}
472+
409473
// ============================================
410474
// MAIN - Run all examples
411475
// ============================================
@@ -462,5 +526,9 @@ fn main() -> int {
462526
return 11;
463527
}
464528

529+
if bytecode_vm_example() != 0 {
530+
return 12;
531+
}
532+
465533
return 0;
466534
}

examples/text_summary/text_summary.ion

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ fn main() -> int {
3535
match fs::read_to_string_result(path) {
3636
ReadResult::Ok(content) => {
3737
let text: String = content;
38-
let bytes: int = String::len(&text);
38+
let bytes: int = text.len();
3939
let counts: TextCounts = summarize_text(text);
4040

4141
io::println(String::from("file: sample.txt"));

0 commit comments

Comments
 (0)