Skip to main content

vihaco_cpu/
instruction.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4use vihaco::Instruction;
5use vihaco::program::{Type, Value};
6use vihaco_parser::{BareToken, Ident};
7
8/// Runtime bytecode instructions.
9///
10/// Source text parses into the separate [`SurfaceInstruction`] enum below.
11/// Keeping the source and runtime forms separate lets patterns carry symbolic
12/// names and surface types until a resolver converts them to runtime values.
13/// Runtime variant order remains stable because it determines derived opcodes.
14#[derive(Debug, Clone, PartialEq, Instruction)]
15#[instruction(width = 16)]
16pub enum RuntimeInstruction {
17    // no-ops
18    /// span <file:file_id> <start:u32> <end:u32>
19    /// `span 0 1 2` — three space-separated u32s.
20    Span(u32, u32, u32),
21
22    /// Label definition.
23    Label,
24
25    /// `func_start <name>` — marks function entry. `<name>` is symbolic and
26    /// orchestrator-resolved; the unit variant carries no payload.
27    FunctionStart,
28    /// `func_end <name>` — marks function exit (debug only).
29    FunctionEnd,
30
31    /// `breakpoint`. Must precede `Branch` (whose token `br` would be a
32    /// prefix of `breakpoint`).
33    Breakpoint,
34
35    // control flows
36    /// `br <target>` — symbolic. Deferred to orchestrator.
37    Branch(u32),
38
39    /// `cond_br <true_target>, <false_target>` — symbolic. Deferred.
40    ConditionalBranch(u32, u32),
41
42    /// `ret` (bare) is the form real `.sst` uses; numeric `ret <n>` has no
43    /// precedent so we defer. Orchestrator emits `Return(0)` for bare `ret`.
44    Return(u32),
45
46    /// `call_indirect`. **Must precede `Call`** for the prefix check.
47    IndirectCall,
48
49    /// `call <arity>, <addr>` — symbolic addr. Deferred.
50    Call(u32, u32),
51
52    /// `halt` — stop execution.
53    Halt,
54
55    // traps / IO
56    /// `print` — write top-of-stack to stdout.
57    Print,
58
59    // memory operations
60    /// `load.<type> <address>` — two fields with single-space separator.
61    Load(Type, u32),
62    /// `store.<type> <address>`.
63    Store(Type, u32),
64
65    /// `dup`.
66    Dup,
67
68    /// `heap_alloc <n>`.
69    HeapAlloc(u32),
70
71    /// `get_item`. Must precede `Ge` (token `ge` ⊂ `get_item`).
72    GetItem,
73
74    /// `heap_dealloc` — pops a HeapRef and marks the slot dead, returning it
75    /// to the free list for reuse by the next `heap_alloc`.
76    HeapDealloc,
77
78    /// `const.<type> <literal>` — numeric/bool only here. `.str`/`.fn_ref`/
79    /// `.heap_ref` are orchestrator-handled.
80    Const(Value),
81
82    // arithmetic operations
83    Add(Type),
84    Sub(Type),
85    Mul(Type),
86    Div(Type),
87    Rem(Type),
88    Neg(Type),
89
90    // integer / bitwise operations
91    Shl(Type),
92    Shr(Type),
93    Rol(Type),
94    Ror(Type),
95    BitAnd(Type),
96    BitOr(Type),
97    BitXor(Type),
98
99    // boolean operations
100    Not,
101    And,
102    Or,
103    Xor,
104
105    // comparison operations
106    Eq(Type),
107    Ne(Type),
108    Lt(Type),
109    Gt(Type),
110    Le(Type),
111    Ge(Type),
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, vihaco_parser_derive::Parse)]
115#[syntax_class(type)]
116pub enum SurfaceType {
117    #[pattern = "`undef`"]
118    Undefined,
119    #[pattern = "`str`"]
120    String,
121    #[pattern = "`bool`"]
122    Bool,
123    #[pattern = "`i64`"]
124    I64,
125    #[pattern = "`u32`"]
126    U32,
127    #[pattern = "`u64`"]
128    U64,
129    #[pattern = "`f64`"]
130    F64,
131    #[pattern = "`fn_ref`"]
132    FunctionRef,
133    #[pattern = "`heap_ref`"]
134    HeapRef,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, vihaco_parser_derive::Parse)]
138#[syntax_class(value)]
139pub enum SurfaceValue {
140    #[pattern = "$0"]
141    Quoted(vihaco_parser::QuotedString),
142    #[pattern = "$0"]
143    Bare(BareToken),
144}
145
146#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)]
147#[syntax_class(instruction, head = "cpu")]
148pub enum SurfaceInstruction {
149    // no-ops
150    /// span <file:file_id> <start:u32> <end:u32>
151    /// `span 0 1 2` — three space-separated u32s.
152    #[pattern = "'span $0 $1 $2"]
153    Span(u32, u32, u32),
154
155    /// Label definition.
156    #[pattern = "'label `@` $0"]
157    Label(Ident),
158
159    /// `func_start <name>` — marks function entry. `<name>` is symbolic and
160    /// orchestrator-resolved; the unit variant carries no payload.
161    #[pattern = "'func_start"]
162    FunctionStart,
163    /// `func_end <name>` — marks function exit (debug only).
164    #[pattern = "'func_end"]
165    FunctionEnd,
166
167    /// `breakpoint`. Must precede `Branch` (whose token `br` would be a
168    /// prefix of `breakpoint`).
169    Breakpoint,
170
171    // control flows
172    /// `br <target>` — symbolic. Deferred to orchestrator.
173    #[pattern = "'br `@` $0"]
174    Branch(Ident),
175
176    /// `cond_br <true_target>, <false_target>` — symbolic. Deferred.
177    #[pattern = "'cond_br `@` $0 `,` `@` $1"]
178    ConditionalBranch(Ident, Ident),
179
180    /// `ret` (bare) is the form real `.sst` uses; numeric `ret <n>` has no
181    /// precedent so we defer. Orchestrator emits `Return(0)` for bare `ret`.
182    #[pattern = "'ret"]
183    Return,
184
185    /// `call_indirect`. **Must precede `Call`** for the prefix check.
186    #[pattern = "'call_indirect"]
187    IndirectCall,
188
189    /// `call <arity>, <addr>` — symbolic addr. Deferred.
190    Call(u32, Ident),
191
192    /// `halt` — stop execution.
193    Halt,
194
195    // traps / IO
196    /// `print` — write top-of-stack to stdout.
197    Print,
198
199    // memory operations
200    /// `load.<type> <address>` — two fields with single-space separator.
201    Load(SurfaceType, u32),
202
203    /// `store.<type> <address>`.
204    Store(SurfaceType, u32),
205
206    /// `dup`.
207    Dup,
208
209    /// `heap_alloc <n>`.
210    #[pattern = "'heap_alloc $0"]
211    HeapAlloc(u32),
212
213    /// `get_item`. Must precede `Ge` (token `ge` ⊂ `get_item`).
214    #[pattern = "'get_item"]
215    GetItem,
216
217    /// `heap_dealloc` — pops a HeapRef and marks the slot dead, returning it
218    /// to the free list for reuse by the next `heap_alloc`.
219    #[pattern = "'heap_dealloc"]
220    HeapDealloc,
221
222    /// `const.<type> <literal>` — numeric/bool only here. `.str`/`.fn_ref`/
223    /// `.heap_ref` are orchestrator-handled.
224    Const(SurfaceType, SurfaceValue),
225
226    // arithmetic operations
227    Add(SurfaceType),
228    Sub(SurfaceType),
229    Mul(SurfaceType),
230    Div(SurfaceType),
231    Rem(SurfaceType),
232    Neg(SurfaceType),
233
234    // integer / bitwise operations
235    Shl(SurfaceType),
236    Shr(SurfaceType),
237    Rol(SurfaceType),
238    Ror(SurfaceType),
239    #[pattern = "'bitand $0"]
240    BitAnd(SurfaceType),
241    #[pattern = "'bitor $0"]
242    BitOr(SurfaceType),
243    #[pattern = "'bitxor $0"]
244    BitXor(SurfaceType),
245
246    // boolean operations
247    Not,
248    And,
249    Or,
250    Xor,
251
252    // comparison operations
253    Eq(SurfaceType),
254    Ne(SurfaceType),
255    Lt(SurfaceType),
256    Gt(SurfaceType),
257    Le(SurfaceType),
258    Ge(SurfaceType),
259}
260
261impl<T: Into<Value>> From<T> for RuntimeInstruction {
262    fn from(value: T) -> Self {
263        RuntimeInstruction::Const(value.into())
264    }
265}
266
267impl vihaco::CanonicalInstructionSyntax for RuntimeInstruction {
268    fn variants() -> &'static [vihaco::CanonicalInstructionVariantSyntax] {
269        &[
270            vihaco::CanonicalInstructionVariantSyntax {
271                mnemonic: "cpu::const_i64",
272                operands: &[vihaco::OperandKind::I64],
273            },
274            vihaco::CanonicalInstructionVariantSyntax {
275                mnemonic: "cpu::const_f64",
276                operands: &[vihaco::OperandKind::F64],
277            },
278            vihaco::CanonicalInstructionVariantSyntax {
279                mnemonic: "cpu::const_bool",
280                operands: &[vihaco::OperandKind::Bool],
281            },
282            vihaco::CanonicalInstructionVariantSyntax {
283                mnemonic: "cpu::const_u64",
284                operands: &[vihaco::OperandKind::NonNegativeU64],
285            },
286            vihaco::CanonicalInstructionVariantSyntax {
287                mnemonic: "cpu::fn_ref",
288                operands: &[vihaco::OperandKind::Symbol],
289            },
290            vihaco::CanonicalInstructionVariantSyntax {
291                mnemonic: "cpu::call_direct",
292                operands: &[vihaco::OperandKind::Symbol],
293            },
294        ]
295    }
296}
297
298#[cfg(test)]
299#[allow(clippy::approx_constant)]
300mod parse_tests {
301    use super::{BareToken, SurfaceInstruction, SurfaceType, SurfaceValue};
302    use chumsky::Parser as _;
303    use vihaco_parser::Parse;
304
305    fn parse(input: &str) -> SurfaceInstruction {
306        SurfaceInstruction::parser()
307            .parse(input)
308            .into_result()
309            .unwrap_or_else(|e| panic!("parse({input:?}) failed: {e:?}"))
310    }
311
312    fn parse_type(input: &str) -> SurfaceType {
313        SurfaceType::parser()
314            .parse(input)
315            .into_result()
316            .unwrap_or_else(|e| panic!("parse_type({input:?}) failed: {e:?}"))
317    }
318
319    macro_rules! assert_parses {
320        ($input:literal, $pattern:pat $(if $guard:expr)?) => {
321            assert!(
322                matches!(parse($input), $pattern $(if $guard)?),
323                "input {:?} parsed to the wrong variant or operands",
324                $input
325            );
326        };
327    }
328
329    #[test]
330    fn parses_unit_variants() {
331        assert_parses!("cpu::halt", SurfaceInstruction::Halt);
332        assert_parses!("cpu::print", SurfaceInstruction::Print);
333        assert_parses!("cpu::dup", SurfaceInstruction::Dup);
334        assert_parses!("cpu::breakpoint", SurfaceInstruction::Breakpoint);
335        assert_parses!(
336            "cpu::label @loop",
337            SurfaceInstruction::Label(name) if name.as_str() == "loop"
338        );
339        assert_parses!("cpu::func_start", SurfaceInstruction::FunctionStart);
340        assert_parses!("cpu::func_end", SurfaceInstruction::FunctionEnd);
341        assert_parses!("cpu::get_item", SurfaceInstruction::GetItem);
342        assert_parses!("cpu::not", SurfaceInstruction::Not);
343        assert_parses!("cpu::and", SurfaceInstruction::And);
344        assert_parses!("cpu::or", SurfaceInstruction::Or);
345        assert_parses!("cpu::xor", SurfaceInstruction::Xor);
346        assert_parses!("cpu::call_indirect", SurfaceInstruction::IndirectCall);
347        assert_parses!("cpu::ret", SurfaceInstruction::Return);
348    }
349
350    #[test]
351    fn parses_surface_types() {
352        for (input, expected) in [
353            ("undef", SurfaceType::Undefined),
354            ("str", SurfaceType::String),
355            ("bool", SurfaceType::Bool),
356            ("i64", SurfaceType::I64),
357            ("u32", SurfaceType::U32),
358            ("u64", SurfaceType::U64),
359            ("f64", SurfaceType::F64),
360            ("fn_ref", SurfaceType::FunctionRef),
361            ("heap_ref", SurfaceType::HeapRef),
362        ] {
363            assert_eq!(parse_type(input), expected, "input {input:?}");
364        }
365    }
366
367    #[test]
368    fn parses_typed_operations() {
369        assert_parses!("cpu::add i64", SurfaceInstruction::Add(SurfaceType::I64));
370        assert_parses!("cpu::sub f64", SurfaceInstruction::Sub(SurfaceType::F64));
371        assert_parses!("cpu::mul u32", SurfaceInstruction::Mul(SurfaceType::U32));
372        assert_parses!("cpu::div u64", SurfaceInstruction::Div(SurfaceType::U64));
373        assert_parses!("cpu::rem i64", SurfaceInstruction::Rem(SurfaceType::I64));
374        assert_parses!("cpu::neg f64", SurfaceInstruction::Neg(SurfaceType::F64));
375        assert_parses!("cpu::lt i64", SurfaceInstruction::Lt(SurfaceType::I64));
376        assert_parses!("cpu::eq i64", SurfaceInstruction::Eq(SurfaceType::I64));
377        assert_parses!("cpu::ne u64", SurfaceInstruction::Ne(SurfaceType::U64));
378        assert_parses!("cpu::gt u32", SurfaceInstruction::Gt(SurfaceType::U32));
379        assert_parses!("cpu::le f64", SurfaceInstruction::Le(SurfaceType::F64));
380        assert_parses!("cpu::ge f64", SurfaceInstruction::Ge(SurfaceType::F64));
381        assert_parses!(
382            "cpu::bitand i64",
383            SurfaceInstruction::BitAnd(SurfaceType::I64)
384        );
385        assert_parses!(
386            "cpu::bitor u64",
387            SurfaceInstruction::BitOr(SurfaceType::U64)
388        );
389        assert_parses!(
390            "cpu::bitxor u32",
391            SurfaceInstruction::BitXor(SurfaceType::U32)
392        );
393        assert_parses!("cpu::shl u64", SurfaceInstruction::Shl(SurfaceType::U64));
394        assert_parses!("cpu::shr i64", SurfaceInstruction::Shr(SurfaceType::I64));
395        assert_parses!("cpu::rol u32", SurfaceInstruction::Rol(SurfaceType::U32));
396        assert_parses!("cpu::ror u64", SurfaceInstruction::Ror(SurfaceType::U64));
397    }
398
399    #[test]
400    fn parses_load_store() {
401        assert_parses!(
402            "cpu::load i64, 7",
403            SurfaceInstruction::Load(SurfaceType::I64, 7)
404        );
405        assert_parses!(
406            "cpu::store f64, 42",
407            SurfaceInstruction::Store(SurfaceType::F64, 42)
408        );
409    }
410
411    #[test]
412    fn parses_heap_alloc() {
413        assert_parses!("cpu::heap_alloc 5", SurfaceInstruction::HeapAlloc(5));
414    }
415
416    #[test]
417    fn parses_span() {
418        assert_parses!("cpu::span 0 1 2", SurfaceInstruction::Span(0, 1, 2));
419    }
420
421    #[test]
422    fn parses_const_numeric_flavors() {
423        assert_parses!(
424            "cpu::const i64, 42",
425            SurfaceInstruction::Const(SurfaceType::I64, value)
426                if value == SurfaceValue::Bare(BareToken("42".to_owned()))
427        );
428        assert_parses!(
429            "cpu::const u64, 7",
430            SurfaceInstruction::Const(SurfaceType::U64, value)
431                if value == SurfaceValue::Bare(BareToken("7".to_owned()))
432        );
433        assert_parses!(
434            "cpu::const u32, 3",
435            SurfaceInstruction::Const(SurfaceType::U32, value)
436                if value == SurfaceValue::Bare(BareToken("3".to_owned()))
437        );
438        assert_parses!(
439            "cpu::const f64, 3.14",
440            SurfaceInstruction::Const(SurfaceType::F64, value)
441                if value == SurfaceValue::Bare(BareToken("3.14".to_owned()))
442        );
443        assert_parses!(
444            "cpu::const bool, true",
445            SurfaceInstruction::Const(SurfaceType::Bool, value)
446                if value == SurfaceValue::Bare(BareToken("true".to_owned()))
447        );
448    }
449
450    #[test]
451    fn parses_const_quoted_string() {
452        assert_parses!(
453            "cpu::const str, \"hello world\"",
454            SurfaceInstruction::Const(SurfaceType::String, SurfaceValue::Quoted(value))
455                if value.as_str() == "hello world"
456        );
457    }
458
459    #[test]
460    fn parses_symbolic_control_flow() {
461        assert_parses!(
462            "cpu::br @body",
463            SurfaceInstruction::Branch(target) if target.as_str() == "body"
464        );
465        assert_parses!(
466            "cpu::cond_br @then, @else",
467            SurfaceInstruction::ConditionalBranch(then_target, else_target)
468                if then_target.as_str() == "then" && else_target.as_str() == "else"
469        );
470        assert_parses!(
471            "cpu::call 2, main",
472            SurfaceInstruction::Call(2, target) if target.as_str() == "main"
473        );
474    }
475
476    #[test]
477    fn rejects_malformed_quoted_value_instead_of_treating_it_as_bare() {
478        assert!(
479            SurfaceInstruction::parser()
480                .parse("cpu::const str, \"unterminated")
481                .has_errors()
482        );
483    }
484
485    #[test]
486    fn rejects_legacy_runtime_instruction_syntax() {
487        assert!(
488            SurfaceInstruction::parser()
489                .parse("const.i64 42")
490                .has_errors()
491        );
492        assert!(SurfaceInstruction::parser().parse("br @body").has_errors());
493    }
494}