1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
use crate::nan_value::NanValue;
use super::symbol::VmSymbolTable;
/// A compiled function chunk — bytecode + metadata.
#[derive(Debug, Clone)]
pub struct FnChunk {
pub name: String,
pub arity: u8,
pub local_count: u16,
pub code: Vec<u8>,
pub constants: Vec<NanValue>,
/// Declared effects (e.g. `! [Console.print, Http]`). Empty for pure functions.
pub effects: Vec<u32>,
/// Conservatively classified "thin" function: likely to return without
/// creating any frame-local heap survivors or dirtying globals.
pub thin: bool,
/// Narrow wrapper-like helper that borrows the caller young region and
/// skips ordinary-return handoff as long as it stays out of yard/handoff.
pub parent_thin: bool,
/// Leaf function: no CALL_KNOWN or CALL_VALUE in bytecode (only builtins
/// and opcodes). When also thin and args-only (local_count == arity),
/// can be called without pushing a CallFrame.
pub leaf: bool,
/// Source file path for this function (empty for synthetic/unknown).
pub source_file: String,
/// Run-length encoded line table: `(bytecode_offset, source_line)`.
/// Sorted by offset. Lookup: find last entry where offset <= target ip.
pub line_table: Vec<(u16, u16)>,
}
/// Minimal call frame: 16 bytes of metadata, no closure/upvalue fields.
#[derive(Debug, Clone)]
pub struct CallFrame {
/// Index into `CodeStore::functions`.
pub fn_id: u32,
/// Current instruction pointer (byte offset into `FnChunk::code`).
pub ip: u32,
/// Base pointer: index into VM stack where this frame's locals start.
pub bp: u32,
/// Number of local slots (params + local bindings).
pub local_count: u16,
/// Arena length at function entry; allocations above this mark are local
/// to the frame unless promoted on return/tail-call.
pub arena_mark: u32,
/// Yard length at function entry; reused TCO frames compact this suffix
/// so loop-carried survivors do not accumulate across iterations.
pub yard_base: u32,
/// Current yard suffix owned by this frame iteration. Reused TCO frames
/// may advance this mark so older carried survivors become the shared
/// prefix for the next iteration.
pub yard_mark: u32,
/// Handoff length at function entry; ordinary returns compact this suffix
/// so helper results can survive into the caller without polluting stable.
pub handoff_mark: u32,
/// Whether this frame stored a young-region value into globals.
pub globals_dirty: bool,
/// Whether ordinary returns introduced caller-yard survivors that should
/// be pruned on the next tail-call boundary.
pub yard_dirty: bool,
/// Whether helper returns introduced handoff survivors that should be
/// pruned on the next boundary of this frame.
pub handoff_dirty: bool,
/// Conservatively classified as cheap enough for a fast return path.
pub thin: bool,
/// Uses the caller young region as its allocation lane and skips
/// ordinary-return handoff while it remains a pure wrapper frame.
pub parent_thin: bool,
}
/// All compiled bytecode for a program.
#[derive(Debug, Clone)]
pub struct CodeStore {
pub functions: Vec<FnChunk>,
/// Map from function name to index in `functions`.
pub fn_index: std::collections::HashMap<String, u32>,
/// Compile-time-known symbol table for functions, builtins, effects, and other names.
pub(crate) symbols: VmSymbolTable,
/// Per-record-type field slot lookup: (type_id, field_symbol_id) -> field_idx.
pub(crate) record_field_slots: std::collections::HashMap<(u32, u32), u8>,
}
impl Default for CodeStore {
fn default() -> Self {
Self::new()
}
}
impl CodeStore {
pub fn new() -> Self {
CodeStore {
functions: Vec::new(),
fn_index: std::collections::HashMap::new(),
symbols: VmSymbolTable::default(),
record_field_slots: std::collections::HashMap::new(),
}
}
pub fn add_function(&mut self, chunk: FnChunk) -> u32 {
let id = self.functions.len() as u32;
self.fn_index.insert(chunk.name.clone(), id);
self.functions.push(chunk);
id
}
pub fn get(&self, id: u32) -> &FnChunk {
&self.functions[id as usize]
}
pub fn find(&self, name: &str) -> Option<u32> {
self.fn_index.get(name).copied()
}
pub fn register_record_fields(&mut self, type_id: u32, field_symbol_ids: &[u32]) {
for (field_idx, symbol_id) in field_symbol_ids.iter().copied().enumerate() {
self.record_field_slots
.insert((type_id, symbol_id), field_idx as u8);
}
}
/// Resolve a bytecode position to (source_file, source_line).
/// Returns None if line table is empty or fn_id is invalid.
pub fn resolve_source_location(&self, fn_id: u32, ip: u32) -> Option<(&str, u16)> {
let chunk = self.functions.get(fn_id as usize)?;
if chunk.line_table.is_empty() {
return None;
}
// Binary search: find last entry where offset <= ip
let ip16 = ip as u16;
let idx = match chunk
.line_table
.binary_search_by_key(&ip16, |&(off, _)| off)
{
Ok(i) => i,
Err(0) => return None,
Err(i) => i - 1,
};
let (_, line) = chunk.line_table[idx];
let file = if chunk.source_file.is_empty() {
None
} else {
Some(chunk.source_file.as_str())
};
Some((file.unwrap_or(""), line))
}
}
/// Source location resolved from line table (cold-path only).
#[derive(Debug, Default, Clone)]
pub struct VmSourceLoc {
pub file: String,
pub line: u16,
pub fn_name: String,
}
/// VM runtime error.
#[derive(Debug)]
pub enum VmError {
/// Runtime error with message and optional source line.
Runtime { msg: String, line: u16 },
/// Type error (e.g. adding int + string).
Type { msg: String, line: u16 },
/// Non-exhaustive match at source line.
MatchFail(u16),
/// Stack underflow (bug in compiler).
StackUnderflow,
}
impl VmError {
pub fn runtime(msg: impl Into<String>) -> Self {
VmError::Runtime {
msg: msg.into(),
line: 0,
}
}
pub fn type_err(msg: impl Into<String>) -> Self {
VmError::Type {
msg: msg.into(),
line: 0,
}
}
/// Attach resolved source location (cold path).
pub fn with_location(self, loc: Option<VmSourceLoc>) -> Self {
let Some(loc) = loc else { return self };
if loc.line == 0 {
return self;
}
match self {
VmError::Runtime { msg, line: 0 } => VmError::Runtime {
msg,
line: loc.line,
},
VmError::Type { msg, line: 0 } => VmError::Type {
msg,
line: loc.line,
},
other => other,
}
}
}
impl std::fmt::Display for VmError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VmError::Runtime { msg, line } if *line > 0 => {
write!(f, "Runtime error [line {}]: {}", line, msg)
}
VmError::Runtime { msg, .. } => write!(f, "Runtime error: {}", msg),
VmError::Type { msg, line } if *line > 0 => {
write!(f, "Type error [line {}]: {}", line, msg)
}
VmError::Type { msg, .. } => write!(f, "Type error: {}", msg),
VmError::MatchFail(line) => write!(f, "Non-exhaustive match at line {}", line),
VmError::StackUnderflow => write!(f, "Internal error: stack underflow"),
}
}
}
impl std::error::Error for VmError {}