389 lines
10 KiB
Rust
389 lines
10 KiB
Rust
//! # IR Instructions
|
|
//!
|
|
//! This module defines the set of instructions used in the Intermediate Representation (IR).
|
|
//! These instructions are designed to be easy to generate from a high-level AST and
|
|
//! easy to lower into VM-specific bytecode.
|
|
|
|
use crate::common::spans::Span;
|
|
use crate::ir_vm::types::{ConstId, TypeId};
|
|
use crate::ir_core::ids::FunctionId;
|
|
|
|
/// An `Instruction` combines an instruction's behavior (`kind`) with its
|
|
/// source code location (`span`) for debugging and error reporting.
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct Instruction {
|
|
pub kind: InstrKind,
|
|
/// The location in the original source code that generated this instruction.
|
|
pub span: Option<Span>,
|
|
}
|
|
|
|
impl Instruction {
|
|
/// Creates a new instruction with an optional source span.
|
|
pub fn new(kind: InstrKind, span: Option<Span>) -> Self {
|
|
Self { kind, span }
|
|
}
|
|
}
|
|
|
|
/// A `Label` represents a destination for a jump instruction.
|
|
/// During the assembly phase, labels are resolved into actual memory offsets.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
|
pub struct Label(pub String);
|
|
|
|
/// The various types of operations that can be performed in the IR.
|
|
///
|
|
/// The IR uses a stack-based model, similar to the final Prometeu ByteCode.
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub enum InstrKind {
|
|
/// Does nothing.
|
|
Nop,
|
|
/// Terminates program execution.
|
|
Halt,
|
|
|
|
// --- Literals ---
|
|
// These instructions push a constant value from the pool onto the stack.
|
|
|
|
/// Pushes a constant from the pool onto the stack.
|
|
PushConst(ConstId),
|
|
/// Pushes a bounded value (0..0xFFFF) onto the stack.
|
|
PushBounded(u32),
|
|
/// Pushes a boolean onto the stack.
|
|
PushBool(bool),
|
|
/// Pushes a `null` value onto the stack.
|
|
PushNull,
|
|
|
|
// --- Stack Operations ---
|
|
|
|
/// Removes the top value from the stack.
|
|
Pop,
|
|
/// Duplicates the top value on the stack.
|
|
Dup,
|
|
/// Swaps the top two values on the stack.
|
|
Swap,
|
|
|
|
// --- Arithmetic ---
|
|
// These take two values from the stack and push the result.
|
|
|
|
/// Addition: `a + b`
|
|
Add,
|
|
/// Subtraction: `a - b`
|
|
Sub,
|
|
/// Multiplication: `a * b`
|
|
Mul,
|
|
/// Division: `a / b`
|
|
Div,
|
|
/// Negation: `-a` (takes one value)
|
|
Neg,
|
|
|
|
// --- Logical/Comparison ---
|
|
|
|
/// Equality: `a == b`
|
|
Eq,
|
|
/// Inequality: `a != b`
|
|
Neq,
|
|
/// Less than: `a < b`
|
|
Lt,
|
|
/// Greater than: `a > b`
|
|
Gt,
|
|
/// Less than or equal: `a <= b`
|
|
Lte,
|
|
/// Greater than or equal: `a >= b`
|
|
Gte,
|
|
/// Logical AND: `a && b`
|
|
And,
|
|
/// Logical OR: `a || b`
|
|
Or,
|
|
/// Logical NOT: `!a`
|
|
Not,
|
|
|
|
// --- Bitwise Operations ---
|
|
|
|
/// Bitwise AND: `a & b`
|
|
BitAnd,
|
|
/// Bitwise OR: `a | b`
|
|
BitOr,
|
|
/// Bitwise XOR: `a ^ b`
|
|
BitXor,
|
|
/// Shift Left: `a << b`
|
|
Shl,
|
|
/// Shift Right: `a >> b`
|
|
Shr,
|
|
|
|
// --- Variable Access ---
|
|
|
|
/// Retrieves a value from a local variable slot and pushes it onto the stack.
|
|
LocalLoad { slot: u32 },
|
|
/// Pops a value from the stack and stores it in a local variable slot.
|
|
LocalStore { slot: u32 },
|
|
/// Retrieves a value from a global variable slot and pushes it onto the stack.
|
|
GetGlobal(u32),
|
|
/// Pops a value from the stack and stores it in a global variable slot.
|
|
SetGlobal(u32),
|
|
|
|
// --- Control Flow ---
|
|
|
|
/// Unconditionally jumps to the specified label.
|
|
Jmp(Label),
|
|
/// Pops a boolean from the stack. If false, jumps to the specified label.
|
|
JmpIfFalse(Label),
|
|
/// Defines a location that can be jumped to. Does not emit code by itself.
|
|
Label(Label),
|
|
/// Calls a function by ID with the specified number of arguments.
|
|
/// Arguments should be pushed onto the stack before calling.
|
|
Call { func_id: FunctionId, arg_count: u32 },
|
|
/// Returns from the current function. The return value (if any) should be on top of the stack.
|
|
Ret,
|
|
|
|
// --- OS / System ---
|
|
|
|
/// Triggers a system call (e.g., drawing to the screen, reading input).
|
|
Syscall(u32),
|
|
/// Special instruction to synchronize with the hardware frame clock.
|
|
FrameSync,
|
|
|
|
// --- HIP / Memory ---
|
|
|
|
/// Allocates memory on the heap.
|
|
Alloc { type_id: TypeId, slots: u32 },
|
|
/// Reads from heap at gate + offset. Pops gate, pushes value.
|
|
GateLoad { offset: u32 },
|
|
/// Writes to heap at gate + offset. Pops gate and value.
|
|
GateStore { offset: u32 },
|
|
|
|
// --- Scope Markers ---
|
|
GateBeginPeek,
|
|
GateEndPeek,
|
|
GateBeginBorrow,
|
|
GateEndBorrow,
|
|
GateBeginMutate,
|
|
GateEndMutate,
|
|
|
|
// --- Reference Counting ---
|
|
|
|
/// Increments the reference count of a gate handle on the stack.
|
|
/// Stack: [..., Gate(g)] -> [..., Gate(g)]
|
|
GateRetain,
|
|
/// Decrements the reference count of a gate handle and pops it from the stack.
|
|
/// Stack: [..., Gate(g)] -> [...]
|
|
GateRelease,
|
|
}
|
|
|
|
/// List of instructions that are sensitive to Reference Counting (RC).
|
|
/// These instructions must trigger retain/release operations on gate handles.
|
|
pub const RC_SENSITIVE_OPS: &[&str] = &[
|
|
"LocalStore",
|
|
"GateStore",
|
|
"GateLoad",
|
|
"Pop",
|
|
"Ret",
|
|
"FrameSync",
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::ir_vm::types::{ConstId, TypeId};
|
|
|
|
#[test]
|
|
fn test_instr_kind_is_cloneable() {
|
|
let instr = InstrKind::Alloc { type_id: TypeId(1), slots: 2 };
|
|
let cloned = instr.clone();
|
|
match cloned {
|
|
InstrKind::Alloc { type_id, slots } => {
|
|
assert_eq!(type_id, TypeId(1));
|
|
assert_eq!(slots, 2);
|
|
}
|
|
_ => panic!("Clone failed"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_isa_surface_snapshot() {
|
|
// This test ensures that the instruction set surface remains stable.
|
|
// If you add/remove/change instructions, this test will fail,
|
|
// prompting an explicit review of the ISA change.
|
|
let instructions = vec![
|
|
InstrKind::Nop,
|
|
InstrKind::Halt,
|
|
InstrKind::PushConst(ConstId(0)),
|
|
InstrKind::PushBounded(0),
|
|
InstrKind::PushBool(true),
|
|
InstrKind::PushNull,
|
|
InstrKind::Pop,
|
|
InstrKind::Dup,
|
|
InstrKind::Swap,
|
|
InstrKind::Add,
|
|
InstrKind::Sub,
|
|
InstrKind::Mul,
|
|
InstrKind::Div,
|
|
InstrKind::Neg,
|
|
InstrKind::Eq,
|
|
InstrKind::Neq,
|
|
InstrKind::Lt,
|
|
InstrKind::Gt,
|
|
InstrKind::Lte,
|
|
InstrKind::Gte,
|
|
InstrKind::And,
|
|
InstrKind::Or,
|
|
InstrKind::Not,
|
|
InstrKind::BitAnd,
|
|
InstrKind::BitOr,
|
|
InstrKind::BitXor,
|
|
InstrKind::Shl,
|
|
InstrKind::Shr,
|
|
InstrKind::LocalLoad { slot: 0 },
|
|
InstrKind::LocalStore { slot: 0 },
|
|
InstrKind::GetGlobal(0),
|
|
InstrKind::SetGlobal(0),
|
|
InstrKind::Jmp(Label("target".to_string())),
|
|
InstrKind::JmpIfFalse(Label("target".to_string())),
|
|
InstrKind::Label(Label("target".to_string())),
|
|
InstrKind::Call { func_id: FunctionId(0), arg_count: 0 },
|
|
InstrKind::Ret,
|
|
InstrKind::Syscall(0),
|
|
InstrKind::FrameSync,
|
|
InstrKind::Alloc { type_id: TypeId(0), slots: 0 },
|
|
InstrKind::GateLoad { offset: 0 },
|
|
InstrKind::GateStore { offset: 0 },
|
|
InstrKind::GateBeginPeek,
|
|
InstrKind::GateEndPeek,
|
|
InstrKind::GateBeginBorrow,
|
|
InstrKind::GateEndBorrow,
|
|
InstrKind::GateBeginMutate,
|
|
InstrKind::GateEndMutate,
|
|
InstrKind::GateRetain,
|
|
InstrKind::GateRelease,
|
|
];
|
|
|
|
let serialized = serde_json::to_string_pretty(&instructions).unwrap();
|
|
|
|
// This is a "lock" on the ISA surface.
|
|
// If the structure of InstrKind changes, the serialization will change.
|
|
let expected_json = r#"[
|
|
"Nop",
|
|
"Halt",
|
|
{
|
|
"PushConst": 0
|
|
},
|
|
{
|
|
"PushBounded": 0
|
|
},
|
|
{
|
|
"PushBool": true
|
|
},
|
|
"PushNull",
|
|
"Pop",
|
|
"Dup",
|
|
"Swap",
|
|
"Add",
|
|
"Sub",
|
|
"Mul",
|
|
"Div",
|
|
"Neg",
|
|
"Eq",
|
|
"Neq",
|
|
"Lt",
|
|
"Gt",
|
|
"Lte",
|
|
"Gte",
|
|
"And",
|
|
"Or",
|
|
"Not",
|
|
"BitAnd",
|
|
"BitOr",
|
|
"BitXor",
|
|
"Shl",
|
|
"Shr",
|
|
{
|
|
"LocalLoad": {
|
|
"slot": 0
|
|
}
|
|
},
|
|
{
|
|
"LocalStore": {
|
|
"slot": 0
|
|
}
|
|
},
|
|
{
|
|
"GetGlobal": 0
|
|
},
|
|
{
|
|
"SetGlobal": 0
|
|
},
|
|
{
|
|
"Jmp": "target"
|
|
},
|
|
{
|
|
"JmpIfFalse": "target"
|
|
},
|
|
{
|
|
"Label": "target"
|
|
},
|
|
{
|
|
"Call": {
|
|
"func_id": 0,
|
|
"arg_count": 0
|
|
}
|
|
},
|
|
"Ret",
|
|
{
|
|
"Syscall": 0
|
|
},
|
|
"FrameSync",
|
|
{
|
|
"Alloc": {
|
|
"type_id": 0,
|
|
"slots": 0
|
|
}
|
|
},
|
|
{
|
|
"GateLoad": {
|
|
"offset": 0
|
|
}
|
|
},
|
|
{
|
|
"GateStore": {
|
|
"offset": 0
|
|
}
|
|
},
|
|
"GateBeginPeek",
|
|
"GateEndPeek",
|
|
"GateBeginBorrow",
|
|
"GateEndBorrow",
|
|
"GateBeginMutate",
|
|
"GateEndMutate",
|
|
"GateRetain",
|
|
"GateRelease"
|
|
]"#;
|
|
assert_eq!(serialized, expected_json);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_ref_leakage_in_instr_names() {
|
|
// Enforce the rule that "Ref" must never refer to HIP memory in ir_vm.
|
|
// The snapshot test above already locks the names, but this test
|
|
// explicitly asserts the absence of the "Ref" substring in HIP-related instructions.
|
|
let instructions = [
|
|
"GateLoad", "GateStore", "Alloc",
|
|
"GateBeginPeek", "GateEndPeek",
|
|
"GateBeginBorrow", "GateEndBorrow",
|
|
"GateBeginMutate", "GateEndMutate",
|
|
"GateRetain", "GateRelease"
|
|
];
|
|
|
|
for name in instructions {
|
|
assert!(!name.contains("Ref"), "Instruction {} contains forbidden 'Ref' terminology", name);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rc_sensitive_list_exists() {
|
|
// Required by PR-06: Documentation test or unit assertion that the RC-sensitive list exists
|
|
assert!(!RC_SENSITIVE_OPS.is_empty(), "RC-sensitive instructions list must not be empty");
|
|
|
|
let expected = ["LocalStore", "GateStore", "GateLoad", "Pop", "Ret", "FrameSync"];
|
|
for op in expected {
|
|
assert!(RC_SENSITIVE_OPS.contains(&op), "RC-sensitive list must contain {}", op);
|
|
}
|
|
}
|
|
}
|