interpreter: initial vm

This commit is contained in:
Guillem L. Jara
2026-05-23 20:03:32 +02:00
parent 91361d44dc
commit d4842f7aa9
7 changed files with 246 additions and 96 deletions
Generated
+1
View File
@@ -504,6 +504,7 @@ dependencies = [
"bumpalo",
"color-eyre",
"either",
"hashbrown 0.17.1",
"indexmap",
"memchr",
"parser",
+1
View File
@@ -13,6 +13,7 @@ tracing.workspace = true
parser.workspace = true
bumpalo.workspace = true
allocator-api2.workspace = true
hashbrown.workspace = true
indexmap = "2.14.0"
[lints]
+74 -53
View File
@@ -10,9 +10,7 @@
//! emitting bytecode without an intermediate AST because AWK contextual
//! shenanigans; _even_ if it was possible, good luck maintaining that.
#![allow(dead_code)]
mod lower;
pub mod lower;
pub use lower::test_interpreter;
@@ -20,23 +18,23 @@ use std::fmt::{Debug, Display};
#[derive(Clone, Copy, Debug)]
#[repr(transparent)]
struct NonLocal(u16);
pub struct NonLocal(pub u16);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
struct Reg(u16);
pub struct Reg(pub u16);
#[derive(Clone, Copy, Debug)]
#[repr(transparent)]
struct Label(u16);
pub struct Label(u16);
#[derive(Clone, Copy, Debug)]
#[repr(transparent)]
struct ArgCount(u16);
pub struct ArgCount(u16);
#[repr(u8, align(1))]
#[derive(Clone, Copy, Debug)]
enum OpCode {
pub enum OpCode {
// Unary operations
Record,
Negation,
@@ -63,10 +61,12 @@ enum OpCode {
Modulo,
// Intrinsic operations
Load,
LoadUser,
LoadBultin,
LoadConst,
Copy,
Store,
StoreUser,
StoreBuiltin,
IntrinsicCall,
UserCall,
IndirectCall,
@@ -80,26 +80,36 @@ const _: () = const { assert!(size_of::<Instruction>() <= 8) };
#[derive(Clone, Copy)]
#[repr(C, align(8))]
struct Instruction {
opcode: OpCode,
hint: Hint,
args: Arguments,
pub struct Instruction {
pub opcode: OpCode,
pub hint: Hint,
pub args: Arguments,
}
#[derive(Clone, Copy)]
#[repr(C, align(2))]
union Arguments {
unary_local: (Reg, Reg),
binary_local: (Reg, Reg, Reg),
load_store: (Reg, NonLocal),
jump: Label,
ret: Reg,
branch: (Reg, Label, Label),
br_if: (Reg, Label),
call: (Reg, NonLocal, ArgCount),
ind_call: (Reg, Reg, ArgCount),
pub union Arguments {
unary_local: UnaryArg,
binary_local: BinaryArg,
load_store: LoadStoreArg,
jump: JumpArg,
ret: RetArg,
branch: BranchArg,
br_if: BrIfArg,
call: CallArgs,
ind_call: IndCallArgs,
}
pub type UnaryArg = (Reg, Reg);
pub type BinaryArg = (Reg, Reg, Reg);
pub type LoadStoreArg = (Reg, NonLocal);
pub type JumpArg = Label;
pub type RetArg = Reg;
pub type BranchArg = (Reg, Label, Label);
pub type BrIfArg = (Reg, Label);
pub type CallArgs = (Reg, NonLocal, ArgCount);
pub type IndCallArgs = (Reg, Reg, ArgCount);
impl Instruction {
fn unary(opcode: impl Into<OpCode>, dest: Reg, src: &impl HintedReg) -> Self {
let opcode = opcode.into();
@@ -180,6 +190,24 @@ impl Instruction {
hint: Hint::None,
}
}
pub fn get_unary(&self) -> Option<&UnaryArg> {
self.opcode
.is_unary()
.then(|| unsafe { &self.args.unary_local })
}
pub fn get_binary(&self) -> Option<&BinaryArg> {
self.opcode
.is_binary()
.then(|| unsafe { &self.args.binary_local })
}
pub fn get_load_store(&self) -> Option<&LoadStoreArg> {
self.opcode
.is_load_store()
.then(|| unsafe { &self.args.load_store })
}
}
impl OpCode {
@@ -213,7 +241,14 @@ impl OpCode {
}
fn is_load_store(self) -> bool {
matches!(self, Self::Load | Self::Store | Self::LoadConst)
matches!(
self,
Self::LoadUser
| Self::LoadBultin
| Self::LoadConst
| Self::StoreUser
| Self::StoreBuiltin
)
}
fn is_jump(self) -> bool {
@@ -231,7 +266,7 @@ impl OpCode {
#[repr(u8, align(1))]
#[derive(Clone, Copy, Debug)]
enum Hint {
pub enum Hint {
None = 0,
UnboxedFloat64,
UnboxedLhsFloat64,
@@ -247,35 +282,15 @@ impl Debug for Instruction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Instruction::{:?}", self.opcode)?;
match self.opcode {
OpCode::Record
| OpCode::Negation
| OpCode::ToInt
| OpCode::Negative
| OpCode::Concat
| OpCode::Copy => {
op if op.is_unary() => {
let (dest, data) = unsafe { &self.args.unary_local };
write!(f, "({dest:?}, {data:?})")
}
OpCode::Eq
| OpCode::NEq
| OpCode::Gt
| OpCode::Lt
| OpCode::LtE
| OpCode::GtE
| OpCode::And
| OpCode::Or
| OpCode::Matches
| OpCode::MatchesNot
| OpCode::Add
| OpCode::Subtract
| OpCode::Multiply
| OpCode::Divide
| OpCode::Raise
| OpCode::Modulo => {
op if op.is_binary() => {
let (dest, lhs, rhs) = unsafe { &self.args.binary_local };
write!(f, "({dest:?}, {lhs:?}, {rhs:?})")
}
OpCode::Load | OpCode::LoadConst | OpCode::Store => {
op if op.is_load_store() => {
let (dest, src) = unsafe { &self.args.load_store };
write!(f, "({dest:?}, {src:?})")
}
@@ -323,14 +338,18 @@ impl Display for Instruction {
let (dest, lhs, rhs) = unsafe { &self.args.binary_local };
write!(f, "{dest} <- {op} {lhs}, {rhs}")
}
op @ (OpCode::Load | OpCode::Store) => {
op @ (OpCode::LoadUser | OpCode::StoreUser) => {
let (dest, src) = unsafe { &self.args.load_store };
write!(f, "{dest} <- {op} global[{src}]")
write!(f, "{dest} <- {op} user[{src}]")
}
op @ OpCode::LoadConst => {
let (dest, src) = unsafe { &self.args.load_store };
write!(f, "{dest} <- {op} mem[{src}]")
}
op @ (OpCode::StoreBuiltin | OpCode::LoadBultin) => {
let (dest, src) = unsafe { &self.args.load_store };
write!(f, "{dest} <- {op} intrinsic[{src}]")
}
op @ OpCode::BrIf => {
let (cond, label) = unsafe { self.args.br_if };
write!(f, "{op} {cond}, {label}")
@@ -375,9 +394,11 @@ impl Display for OpCode {
Self::Divide => "div",
Self::Raise => "pow",
Self::Modulo => "mod",
Self::Load => "vload",
Self::LoadUser => "vload",
Self::LoadBultin => "iload",
Self::LoadConst => "cload",
Self::Store => "vstore",
Self::StoreUser => "vstore",
Self::StoreBuiltin => "istore",
Self::Copy => "cpy",
Self::IntrinsicCall => "icall",
Self::UserCall => "ucall",
+23 -18
View File
@@ -9,20 +9,21 @@ use std::mem::forget;
use bumpalo::{Bump, collections::Vec};
use indexmap::IndexSet;
use parser::{Atom, BinaryOperator, Expr, ExprNode, UnaryOperator};
use parser::{Atom, BinaryOperator, Expr, ExprNode, UnaryOperator, Variable};
use crate::ir::{Hint, HintedReg, Instruction, Label, NonLocal, OpCode, Reg};
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Value(f64); // TODO: use NaN-boxing.
use crate::{
ir::{Hint, HintedReg, Instruction, Label, NonLocal, OpCode, Reg},
vm::{ExecMode, Interpreter, SymbolTable, Value},
};
#[derive(Debug)]
struct Code<'a> {
arena: &'a Bump,
bc: Bytecode<'a>,
consts: &'a mut IndexSet<Value>,
free_regs: Vec<'a, Reg>,
reg_pointer: u16,
pub struct Code<'arena> {
pub arena: &'arena Bump,
pub bc: Bytecode<'arena>,
pub consts: IndexSet<Value>,
pub symbols: SymbolTable<'arena>,
free_regs: Vec<'arena, Reg>,
pub reg_pointer: u16,
}
#[must_use]
@@ -38,10 +39,10 @@ impl Code<'_> {
fn lower_expr_into(&mut self, expr: &Expr, dest: Reg) -> Hint {
match expr {
Expr::Leaf(atom) => match atom {
Atom::Variable(var) => {
let src = NonLocal(crate::index_for_global(var));
Atom::Variable(Variable::User(ident)) => {
let src = self.symbols.register_user_var(ident, self.arena);
self.bc
.emit(Instruction::load_store(OpCode::Load, dest, src));
.emit(Instruction::load_store(OpCode::LoadUser, dest, src));
}
&Atom::Number(n) => {
let src = self.register_const(Value(n));
@@ -106,8 +107,8 @@ impl Code<'_> {
}
#[derive(Debug, Clone)]
struct Bytecode<'a> {
code: Vec<'a, Instruction>,
pub struct Bytecode<'a> {
pub code: Vec<'a, Instruction>,
}
#[derive(Clone, Debug)]
@@ -167,13 +168,17 @@ pub fn test_interpreter(expr: &Expr<'_>) -> impl Display {
let mut c = Code {
arena: &bump,
bc: Bytecode::new_in(&bump),
consts: &mut IndexSet::new(),
consts: IndexSet::new(),
symbols: SymbolTable::new_in(&bump),
reg_pointer: 0,
free_regs: Vec::new_in(&bump),
};
let result = c.lower_expr(expr);
forget(result);
c.to_string()
let code = c.to_string();
let mut vm = Interpreter::new(ExecMode::Uu, c);
vm.run();
format!("{code}---\n{vm:#?}")
}
impl From<UnaryOperator> for OpCode {
+3 -25
View File
@@ -3,34 +3,12 @@
// For the full copyright and license information, please view the LICENSE
// files that was distributed with this source code.
mod ir;
#![allow(dead_code)]
use color_eyre::eyre::Result;
use either::Either;
pub(crate) mod ir;
mod vm;
pub use ir::test_interpreter;
use parser::Variable;
pub enum BuiltinCommand {}
pub enum BuiltinVar {}
pub type Command<'a> = Either<BuiltinCommand, &'a str>;
// pub type Variable<'a> = Either<BuiltinVar, &'a str>;
#[derive(Debug)]
pub struct Interpreter;
impl Interpreter {
#[tracing::instrument]
pub fn run(self) -> Result<Option<i32>> {
todo!()
}
pub fn eval_expression(&mut self) {}
}
fn index_for_global(_var: &Variable) -> u16 {
1 // TODO
}
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
+14
View File
@@ -0,0 +1,14 @@
use std::ptr::NonNull;
// / NaN-boxed value
union AValue {
float: f64,
ptr: NonNull<()>,
}
impl AValue {
fn get_float(&self) -> f64 {
let float = unsafe { self.float };
if !float.is_nan() { float } else { todo!() }
}
}
+130
View File
@@ -0,0 +1,130 @@
use bumpalo::{Bump, collections::Vec};
use hashbrown::{DefaultHashBuilder, HashMap};
use indexmap::{IndexMap, IndexSet};
use parser::Identifier;
use crate::ir::{
NonLocal, OpCode, Reg,
lower::{Bytecode, Code},
};
#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct Value(pub f64); // TODO: use NaN-boxing.
#[derive(Debug)]
pub enum ExecMode {
Uu,
Gnu,
Posix,
}
#[derive(Debug)]
pub struct Interpreter<'a> {
arena: &'a Bump,
bc: Bytecode<'a>,
program_counter: usize,
registers: Registers<'a>,
symbols: SymbolTable<'a>,
consts: IndexSet<Value>,
compat: ExecMode,
}
#[derive(Debug)]
pub struct Registers<'a>(Vec<'a, Value>);
#[derive(Debug)]
pub struct SymbolTable<'a> {
user: IndexMap<Identifier<'a>, Value>,
// separate table for cheap invalidation. It's an arena _visibly shrugs_.
records: HashMap<usize, Value, DefaultHashBuilder, &'a Bump>,
// etc
}
impl<'a> Interpreter<'a> {
pub fn new(compat: ExecMode, code: Code<'a>) -> Self {
Self {
arena: code.arena,
bc: code.bc,
program_counter: 0,
registers: Registers(bumpalo::vec![in code.arena; Value(0.); 8]),
symbols: code.symbols,
consts: code.consts,
compat,
}
}
}
impl<'a> SymbolTable<'a> {
pub fn new_in(arena: &'a Bump) -> Self {
Self {
user: IndexMap::new(),
records: HashMap::new_in(arena),
}
}
fn lookup_user_var(&self, var: NonLocal) -> &Value {
self.user.get_index(var.0 as _).unwrap().1
}
fn write_user_val(&mut self, var: NonLocal, value: &Value) {
*self.user.get_index_mut(var.0 as _).unwrap().1 = Value::clone(value);
}
pub fn register_user_var(&mut self, var: &Identifier, bump: &'a Bump) -> NonLocal {
if let Some(index) = self.user.get_index_of(var) {
NonLocal(index as _)
} else {
let ident = Identifier {
namespace: bump.alloc_str(var.namespace),
literal: bump.alloc_str(var.literal),
};
NonLocal(self.user.insert_full(ident, Value(0.)).0 as _)
}
}
}
impl Interpreter<'_> {
pub fn run(&mut self) {
while let Some(instr) = self.bc.code.get(self.program_counter) {
match instr {
ix if let Some(&(dest, src)) = ix.get_unary() => {}
ix if let Some(&(dest, lhs, rhs)) = ix.get_binary() => {
let lhs = self.registers.read(lhs);
let rhs = self.registers.read(rhs);
let val = match ix.opcode {
OpCode::Add => Value(lhs.0 + rhs.0),
OpCode::Subtract => Value(lhs.0 - rhs.0),
OpCode::Multiply => Value(lhs.0 * rhs.0),
OpCode::Divide => Value(lhs.0 / rhs.0),
_ => todo!(),
};
self.registers.write(dest, &val);
}
ix if let Some(&(dest, src)) = ix.get_load_store() => match ix.opcode {
OpCode::LoadConst => self
.registers
.write(dest, self.consts.get_index(src.0 as _).unwrap()),
OpCode::LoadUser => {
self.registers
.write(dest, self.symbols.lookup_user_var(src));
}
OpCode::StoreUser => {
self.symbols.write_user_val(src, self.registers.read(dest));
}
_ => todo!(),
},
_ => todo!(),
}
self.program_counter += 1;
}
}
}
impl Registers<'_> {
fn read(&self, src: Reg) -> &Value {
&self.0[src.0 as usize]
}
fn write(&mut self, dest: Reg, src: &Value) {
self.0[dest.0 as usize] = Value::clone(src);
}
}