├── Cargo.toml ├── README.md ├── rust-toolchain.toml ├── rustfmt.toml ├── src ├── api.rs ├── compiler.rs ├── lib.rs └── vm │ ├── mod.rs │ ├── stack.rs │ ├── tape.rs │ └── util.rs └── tests └── basic.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yurt" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [profile.dev] 9 | opt-level = 2 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YURT 2 | 3 | YUniversal RunTime 4 | 5 | ## Goals 6 | 7 | - High portability 8 | - Support for reentrant control flow (exceptions, async, effect handlers, etc.) 9 | - Sandboxed execution 10 | - Capability-driven 11 | - Memory safety (programs cannot perform ACE) 12 | - Performance, where doing so does not sacrifice other goals 13 | - Small 14 | - Good intra-module FFI 15 | - Eval (modules can build and invoke modules) 16 | 17 | ## Non-goals 18 | 19 | - JIT/AoT compilation 20 | 21 | ## Design 22 | 23 | - Threaded interpreter 24 | - Safe wrapper API for code generator 25 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 150 2 | attr_fn_like_width = 140 3 | combine_control_expr = true 4 | fn_single_line = true 5 | match_arm_blocks = false 6 | match_block_trailing_comma = true 7 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use crate::vm::{ 4 | util::{incr_offset, AsRepr}, 5 | Data, ReadWrite, Runtime, Stack, State, Tape, TapeOffset, 6 | }; 7 | 8 | pub struct Module { 9 | pub(crate) tape: Tape, 10 | pub(crate) funcs: Vec, 11 | } 12 | 13 | impl Module { 14 | pub fn build() -> ModuleBuilder { ModuleBuilder::default() } 15 | 16 | pub fn call(&self, f: FuncId, input: I) -> Result { 17 | let func = &self.funcs[f.0]; 18 | 19 | let input_repr = I::repr(); 20 | let output_repr = O::repr(); 21 | 22 | if input_repr != func.input { 23 | Err(format!( 24 | "Tried to call function with input type of {:?} with input type {:?}", 25 | func.input, input_repr 26 | )) 27 | } else if output_repr != func.output { 28 | Err(format!( 29 | "Tried to call function with output type of {:?} with output type {:?}", 30 | func.output, output_repr 31 | )) 32 | } else { 33 | let mut stack = Stack::new(1024); 34 | let mut rt = Runtime::default(); 35 | unsafe { 36 | let mut stack = stack.ptr(); 37 | 38 | // ABI is return space, then arg 39 | let output_range = incr_offset(0, &output_repr); 40 | let input_range = incr_offset(output_range.end, &input_repr); 41 | stack.write(input, input_range.start); 42 | 43 | self.tape.ptr().exec_at(func.addr, State::init(&mut rt), stack); 44 | Ok(rt.last_stack_ptr().unwrap().read(output_range.start)) 45 | } 46 | } 47 | } 48 | 49 | pub fn show_symbols(&self) { 50 | for (addr, addr_to, desc) in &self.tape.symbols { 51 | println!("0x{addr:03X}..0x{addr_to:03X} | {desc}"); 52 | } 53 | } 54 | } 55 | 56 | pub struct FuncInfo { 57 | pub(crate) input: Repr, 58 | pub(crate) output: Repr, 59 | pub(crate) addr: TapeOffset, 60 | } 61 | 62 | #[derive(Default)] 63 | pub struct ModuleBuilder { 64 | pub(crate) funcs: Vec, 65 | } 66 | 67 | impl ModuleBuilder { 68 | pub fn build_func Expr>(&mut self, input: Repr, output: Repr, f: F) -> FuncId { 69 | let id = FuncId(self.funcs.len()); 70 | self.funcs.push(Func { 71 | input, 72 | output, 73 | body: f(Expr::Local(0)), 74 | }); 75 | id 76 | } 77 | } 78 | 79 | #[derive(Copy, Clone)] 80 | pub struct FuncId(usize); 81 | 82 | pub struct Func { 83 | pub(crate) input: Repr, 84 | pub(crate) output: Repr, 85 | pub(crate) body: Expr, 86 | } 87 | 88 | #[derive(Debug, Clone)] 89 | pub enum Cond { 90 | IsTrue, 91 | NotZero, 92 | } 93 | 94 | impl Cond { 95 | pub fn if_then_else(self, pred: Expr, a: Expr, b: Expr) -> Expr { Expr::IfElse(self, Box::new(pred), Box::new(a), Box::new(b)) } 96 | } 97 | 98 | #[derive(Debug, Clone)] 99 | pub enum Expr { 100 | Bool(bool), 101 | U64(u64), 102 | 103 | Local(usize), 104 | FieldAccess(Box, usize), 105 | Add(Box, Box), 106 | Mul(Box, Box), 107 | Then(Box, Box), 108 | Scope(Vec, Box), 109 | IfElse(Cond, Box, Box, Box), 110 | } 111 | 112 | impl Expr { 113 | pub fn field(self, key: usize) -> Self { Self::FieldAccess(Box::new(self), key) } 114 | 115 | pub fn add(self, other: Self) -> Self { Self::Add(Box::new(self), Box::new(other)) } 116 | pub fn mul(self, other: Self) -> Self { Self::Mul(Box::new(self), Box::new(other)) } 117 | 118 | pub fn then(self, other: Self) -> Self { Self::Then(Box::new(self), Box::new(other)) } 119 | 120 | pub fn scope) -> Expr>(inputs: Vec, f: F) -> Self { 121 | let body = f((0..inputs.len()).map(Self::Local).collect()); 122 | Self::Scope(inputs, Box::new(body)) 123 | } 124 | } 125 | 126 | #[derive(Clone, Debug, PartialEq)] 127 | pub enum Repr { 128 | U64, 129 | Bool, 130 | Tuple(Vec), 131 | } 132 | 133 | impl Repr { 134 | pub fn size(&self) -> usize { 135 | match self { 136 | Self::U64 => u64::BYTES, 137 | Self::Bool => bool::BYTES, 138 | Self::Tuple(fields) => fields.iter().map(|f| f.size()).sum(), 139 | } 140 | } 141 | 142 | pub fn align(&self) -> usize { 143 | match self { 144 | Self::U64 => u64::ALIGN, 145 | Self::Bool => bool::ALIGN, 146 | Self::Tuple(fields) => fields.iter().map(|f| f.align()).max().unwrap_or(1), // Align of an empty tuple is 1 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/compiler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{Cond, Expr, Func, FuncInfo, Module, ModuleBuilder, Repr}, 3 | vm::{ 4 | util::{incr_offset, AsRepr, ReadWrite}, 5 | Data, FixupTapeOffset, Tape, TapeOffset, 6 | }, 7 | }; 8 | use core::fmt; 9 | 10 | enum StackItemKind { 11 | // local_id 12 | Local(usize), 13 | Temporary, 14 | } 15 | 16 | struct StackItem { 17 | kind: StackItemKind, 18 | repr: Repr, 19 | // TODO: Make a `StackAddr` type 20 | offset: usize, 21 | } 22 | 23 | // Stack grows upward. Items in the current frame extend *past* stack pointer 24 | // | caller frame | [i32] [i64] ... 25 | // ^ 26 | // | stack pointer 27 | #[derive(Default)] 28 | struct StackLog { 29 | // (offset, _) 30 | items: Vec, 31 | // In stack frame 32 | cur_offset: usize, 33 | // Of stack frame (i.e: after compilation, can be used to determine the overflow check length) 34 | max_offset: usize, 35 | } 36 | 37 | #[derive(Copy, Clone, PartialEq)] 38 | struct StackIdx(usize); 39 | 40 | impl StackLog { 41 | pub fn with_push(&mut self, kind: StackItemKind, repr: Repr, f: F) -> R 42 | where 43 | F: FnOnce(&mut Self, StackIdx) -> R, 44 | { 45 | let item_idx = self.items.len(); 46 | let old_offset = self.cur_offset; 47 | let offset_range = incr_offset(self.cur_offset, &repr); 48 | 49 | // Apply new item 50 | self.items.push(StackItem { 51 | kind, 52 | repr, 53 | offset: offset_range.start, 54 | }); 55 | self.cur_offset = offset_range.end; 56 | self.max_offset = self.max_offset.max(self.cur_offset); 57 | 58 | // Invoke inner 59 | let r = f(self, StackIdx(item_idx)); 60 | 61 | // Return to old state 62 | self.cur_offset = old_offset; 63 | self.items.pop(); 64 | r 65 | } 66 | 67 | pub fn get_local(&self, local: usize) -> StackIdx { 68 | self.items 69 | .iter() 70 | .enumerate() 71 | .rev() 72 | .find(|(_, item)| matches!(&item.kind, StackItemKind::Local(l) if *l == local)) 73 | .map(|(idx, _)| StackIdx(idx)) 74 | .expect("No such local") 75 | } 76 | 77 | pub fn get(&self, idx: StackIdx) -> &StackItem { &self.items[idx.0] } 78 | 79 | // Like `with_push`, except the expression is permitted to be derived from some local instead 80 | pub fn with_expr(&mut self, tape: &mut Tape, expr: Expr, f: F) -> Result 81 | where 82 | F: FnOnce(&mut Tape, &mut Self, (StackIdx, Path)) -> R, 83 | { 84 | // Try to extract the expression as a path 85 | if let Some(path) = expr.try_as_path(self) { 86 | Ok(f(tape, self, path)) 87 | } else { 88 | // Path extraction failed, evaluate the expression into a temporary 89 | self.with_push(StackItemKind::Temporary, expr.derive_repr(self), |stack, expr_out| { 90 | expr.compile(tape, stack, expr_out)?; 91 | Ok(f(tape, stack, (expr_out, Path::All))) 92 | }) 93 | } 94 | } 95 | } 96 | 97 | // Represents a path into a repr 98 | #[derive(Debug)] 99 | pub enum Path { 100 | All, // The entire data structure 101 | Field(usize, Box), // A field within a tuple 102 | } 103 | 104 | impl Expr { 105 | fn compile(self, tape: &mut Tape, stack: &mut StackLog, output: StackIdx) -> Result<(), String> { 106 | fn copy(tape: &mut Tape, stack: &StackLog, (src, src_path): (StackIdx, Path), (dst, dst_path): (StackIdx, Path)) { 107 | if src == dst { 108 | return; 109 | } // Don't need to copy if src and dst are the same 110 | 111 | let (src, dst) = (stack.get(src), stack.get(dst)); 112 | let src_repr = src.repr.resolve_path(&src_path); 113 | let dst_repr = dst.repr.resolve_path(&dst_path); 114 | assert_eq!(src_repr, dst_repr, "Cannot perform copy between different reprs"); 115 | 116 | let (src, dst, sz) = ( 117 | src.offset + src.repr.resolve_path_offset(&src_path), 118 | dst.offset + dst.repr.resolve_path_offset(&dst_path), 119 | src_repr.size(), 120 | ); 121 | 122 | let symbol = format!("copy {src:3} -[x{sz:2}]-> {dst:3}"); 123 | 124 | match (src, dst, sz) { 125 | (src, dst, sz @ 8) => drop(tape.push_op(symbol, (src, dst), |(src, dst), tape, regs, stack| unsafe { stack.copy(src, dst, 8) })), 126 | (src, dst, sz) => drop(tape.push_op(symbol, (src, dst, sz), |(src, dst, sz), tape, regs, stack| unsafe { 127 | stack.copy(src, dst, sz) 128 | })), 129 | } 130 | } 131 | 132 | fn literal_op(tape: &mut Tape, stack: &mut StackLog, val: A, (dst, dst_path): (StackIdx, Path)) 133 | where 134 | [(); A::BYTES]:, 135 | { 136 | let dst = stack.get(dst); 137 | let dst_repr = dst.repr.resolve_path(&dst_path); 138 | assert_eq!(dst_repr, &A::repr(), "Destination repr does not match literal"); 139 | 140 | let dst = dst.offset + dst.repr.resolve_path_offset(&dst_path); 141 | 142 | let symbol = format!("litr {val} -> {dst:3}"); 143 | 144 | match dst { 145 | dst => drop(tape.push_op(symbol, (val, dst), move |(val, dst), tape, regs, stack| unsafe { 146 | stack.write(val, dst); 147 | })), 148 | } 149 | } 150 | 151 | fn binary_op( 152 | tape: &mut Tape, 153 | stack: &mut StackLog, 154 | f: F, 155 | (src0, src0_path): (StackIdx, Path), 156 | (src1, src1_path): (StackIdx, Path), 157 | (dst, dst_path): (StackIdx, Path), 158 | ) where 159 | F: Fn(A, B) -> C + Copy, 160 | [(); A::BYTES]:, 161 | [(); B::BYTES]:, 162 | [(); C::BYTES]:, 163 | { 164 | let (src0, src1, dst) = (stack.get(src0), stack.get(src1), stack.get(dst)); 165 | let src0_repr = src0.repr.resolve_path(&src0_path); 166 | let src1_repr = src1.repr.resolve_path(&src1_path); 167 | let dst_repr = dst.repr.resolve_path(&dst_path); 168 | assert_eq!( 169 | (src0_repr, src1_repr, dst_repr), 170 | (&A::repr(), &B::repr(), &C::repr()), 171 | "Source/destination reprs do not match for binary op" 172 | ); 173 | 174 | let (src0, src1, dst) = ( 175 | src0.offset + src0.repr.resolve_path_offset(&src0_path), 176 | src1.offset + src1.repr.resolve_path_offset(&src1_path), 177 | dst.offset + dst.repr.resolve_path_offset(&dst_path), 178 | ); 179 | 180 | let symbol = format!("binary {src0:3}, {src1:3} -> {dst:3} ({})", core::any::type_name::()); 181 | 182 | match (src0, src1, dst) { 183 | (src0 @ 8, src1 @ 16, dst @ 32) => { 184 | tape.push_op(symbol, (), move |(), tape, regs, stack| unsafe { 185 | let a = stack.read(8); 186 | let b = stack.read(16); 187 | stack.write(f(a, b), 32); 188 | }); 189 | }, 190 | (src0, src1, dst) => { 191 | tape.push_op(symbol, (src0, src1, dst), move |(src0, src1, dst), tape, regs, stack| unsafe { 192 | let a = stack.read(src0); 193 | let b = stack.read(src1); 194 | stack.write(f(a, b), dst); 195 | }); 196 | }, 197 | } 198 | } 199 | 200 | #[must_use] 201 | fn jump(tape: &mut Tape, stack: &mut StackLog) -> FixupTapeOffset { 202 | let symbol = format!("jump"); 203 | 204 | // This offset will be fixed up later! 205 | tape.push_op(symbol, (0,), move |(rel,), tape, regs, stack| unsafe { 206 | tape.jump_rel(rel); 207 | }) 208 | .1 209 | .0 210 | } 211 | 212 | #[must_use] 213 | fn jump_if( 214 | tape: &mut Tape, 215 | stack: &mut StackLog, 216 | f: F, 217 | (src, src_path): (StackIdx, Path), 218 | ) -> FixupTapeOffset 219 | where 220 | F: Fn(A) -> bool + Copy, 221 | [(); A::BYTES]:, 222 | { 223 | let src = stack.get(src); 224 | let src_repr = src.repr.resolve_path(&src_path); 225 | assert_eq!(src_repr, &A::repr(), "Condition must have matching repr"); 226 | 227 | let src = src.offset + src.repr.resolve_path_offset(&src_path); 228 | 229 | let symbol = format!("jump_if {src}"); 230 | 231 | match src { 232 | // This offset will be fixed up later! 233 | src => 234 | tape.push_op(symbol, (src, 0), move |(src, rel), tape, regs, stack| unsafe { 235 | if f(stack.read(src)) { 236 | tape.jump_rel(rel); 237 | } 238 | }) 239 | .1 240 | .1, 241 | } 242 | } 243 | 244 | let output_repr = &stack.get(output).repr; 245 | let self_repr = self.derive_repr(stack); 246 | assert_eq!( 247 | output_repr, &self_repr, 248 | "Output slot repr {output_repr:?} did not match expression repr {self_repr:?}" 249 | ); 250 | 251 | Ok(match self { 252 | Expr::Bool(x) => literal_op(tape, stack, x, (output, Path::All)), 253 | Expr::U64(x) => literal_op(tape, stack, x, (output, Path::All)), 254 | Expr::Local(local) => copy(tape, stack, (stack.get_local(local), Path::All), (output, Path::All)), 255 | Expr::Add(a, b) => stack.with_expr(tape, *a, move |tape, stack, a_slot| { 256 | stack.with_expr(tape, *b, move |tape, stack, b_slot| { 257 | binary_op(tape, stack, >::add, a_slot, b_slot, (output, Path::All)); 258 | }) 259 | })??, 260 | Expr::Mul(a, b) => stack.with_expr(tape, *a, move |tape, stack, a_slot| { 261 | stack.with_expr(tape, *b, move |tape, stack, b_slot| { 262 | binary_op(tape, stack, >::mul, a_slot, b_slot, (output, Path::All)); 263 | }) 264 | })??, 265 | Expr::FieldAccess(x, key) => stack.with_expr(tape, *x, move |tape, stack, x_slot| { 266 | copy(tape, stack, x_slot, (output, Path::All)); 267 | })?, 268 | Expr::IfElse(cond, pred, a, b) => stack.with_expr(tape, *pred, move |tape, stack, pred_slot| { 269 | // cond 270 | let true_fixup = match cond { 271 | Cond::IsTrue => jump_if(tape, stack, |x: bool| x, pred_slot), 272 | Cond::NotZero => jump_if(tape, stack, |x: u64| x != 0, pred_slot), 273 | }; 274 | let post_cond = tape.next_addr(); 275 | 276 | // false 277 | b.compile(tape, stack, output)?; 278 | let end_fixup = jump(tape, stack); 279 | let post_b = tape.next_addr(); 280 | 281 | // true 282 | a.compile(tape, stack, output)?; 283 | let end = tape.next_addr(); 284 | 285 | tape.fixup(true_fixup, post_cond.offset_to(post_b)); 286 | tape.fixup(end_fixup, post_b.offset_to(end)); 287 | Ok::<(), String>(()) 288 | })??, 289 | expr => todo!("{expr:?}"), 290 | }) 291 | } 292 | 293 | // If possible, try to reuse existing stack data to derive this expression 294 | fn try_as_path(&self, stack: &StackLog) -> Option<(StackIdx, Path)> { 295 | match self { 296 | Expr::Bool(_) | Expr::U64(_) => None, 297 | 298 | Expr::Local(local) => Some((stack.get_local(*local), Path::All)), 299 | Expr::FieldAccess(x, key) => { 300 | let (x, path) = x.try_as_path(stack)?; 301 | Some((x, Path::Field(*key, Box::new(path)))) 302 | }, 303 | Expr::Add(..) | Expr::Mul(..) => None, 304 | expr => todo!("{expr:?}"), 305 | } 306 | } 307 | 308 | fn derive_repr(&self, stack: &StackLog) -> Repr { 309 | match self { 310 | Expr::Bool(_) => Repr::Bool, 311 | Expr::U64(_) => Repr::U64, 312 | Expr::Local(local) => stack.get(stack.get_local(*local)).repr.clone(), 313 | Expr::FieldAccess(x, key) => 314 | if let Repr::Tuple(mut fields) = x.derive_repr(stack) { 315 | fields.remove(*key) 316 | } else { 317 | panic!("Field access on non-tuple not permitted") 318 | }, 319 | Expr::Add(a, b) | Expr::Mul(a, b) => { 320 | let a_repr = a.derive_repr(stack); 321 | let b_repr = b.derive_repr(stack); 322 | assert_eq!(a_repr, Repr::U64); 323 | assert_eq!(b_repr, Repr::U64); 324 | Repr::U64 325 | }, 326 | Expr::IfElse(cond, pred, a, b) => { 327 | match cond { 328 | Cond::IsTrue => assert_eq!(pred.derive_repr(stack), Repr::Bool, "Branch conditional must have a bool repr"), 329 | Cond::NotZero => assert_eq!(pred.derive_repr(stack), Repr::U64, "Branch conditional must have a u64 repr"), 330 | } 331 | let a_repr = a.derive_repr(stack); 332 | let b_repr = b.derive_repr(stack); 333 | assert_eq!(a_repr, b_repr, "Reprs of branches must be equivalent"); 334 | a_repr 335 | }, 336 | expr => todo!("{expr:?}"), 337 | } 338 | } 339 | } 340 | 341 | impl Repr { 342 | pub fn resolve_path(&self, path: &Path) -> &Repr { 343 | match (self, path) { 344 | (this, Path::All) => this, 345 | (Self::Tuple(fields), Path::Field(key, path)) => 346 | if *key >= fields.len() { 347 | panic!("Tuple field key {key} is invalid for {self:?}") 348 | } else { 349 | fields[*key].resolve_path(path) 350 | }, 351 | (repr, path) => todo!("resolve path for {repr:?} and {path:?}"), 352 | } 353 | } 354 | 355 | pub fn resolve_path_offset(&self, path: &Path) -> usize { 356 | match (self, path) { 357 | (_, Path::All) => 0, 358 | (Self::Tuple(fields), Path::Field(key, path)) => 359 | if *key >= fields.len() { 360 | panic!("Tuple field key {key} is invalid for {self:?}") 361 | } else { 362 | let mut offset = 0; 363 | for field in &fields[..*key] { 364 | offset = incr_offset(offset, field).end; 365 | } 366 | incr_offset(offset, &fields[*key]).start + fields[*key].resolve_path_offset(path) 367 | }, 368 | (repr, path) => todo!("resolve path offset for {repr:?} and {path:?}"), 369 | } 370 | } 371 | } 372 | 373 | impl Func { 374 | pub fn compile(self, tape: &mut Tape) -> Result { 375 | let addr = tape.next_addr(); 376 | StackLog::default().with_push(StackItemKind::Temporary, self.output.clone(), |stack, output| { 377 | stack.with_push(StackItemKind::Local(0), self.input.clone(), |stack, _| { 378 | self.body.compile(tape, stack, output) 379 | }) 380 | })?; 381 | tape.push_exit(); 382 | Ok(FuncInfo { 383 | input: self.input, 384 | output: self.output, 385 | addr, 386 | }) 387 | } 388 | } 389 | 390 | impl ModuleBuilder { 391 | pub fn compile(self) -> Result { 392 | let mut tape = Tape::default(); 393 | 394 | let funcs = self.funcs.into_iter().map(|func| func.compile(&mut tape)).collect::>()?; 395 | 396 | Ok(Module { tape, funcs }) 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature( 2 | explicit_tail_calls, 3 | maybe_uninit_uninit_array_transpose, 4 | maybe_uninit_array_assume_init, 5 | generic_const_exprs, 6 | inline_const, 7 | core_intrinsics, 8 | int_roundings, 9 | abi_vectorcall 10 | )] 11 | #![allow(incomplete_features, unused_mut, unused_variables)] 12 | 13 | pub mod api; 14 | pub mod compiler; 15 | pub mod vm; 16 | 17 | use core::{marker::PhantomData, mem::MaybeUninit}; 18 | 19 | // pub unsafe fn cont(mut tape: TapePtr, mut regs: Regs, mut stack: StackPtr) { 20 | // become tape.read::()(tape, regs, stack) 21 | // } 22 | 23 | // pub unsafe fn add_reg(mut tape: TapePtr, mut regs: Regs, mut stack: StackPtr) { 24 | // regs.0 += regs.1; 25 | 26 | // become tape.read::()(tape, regs, stack) 27 | // } 28 | 29 | // pub unsafe fn add(mut tape: TapePtr, mut regs: Regs, mut stack: StackPtr) { 30 | // let a = stack.pop::(); 31 | // let b = stack.pop::(); 32 | // stack.push::<_, false>(a + b); 33 | 34 | // become tape.read::()(tape, regs, stack) 35 | // } 36 | 37 | // pub fn ret(tape: TapePtr, regs: Regs, stack: StackPtr) {} 38 | 39 | // pub unsafe fn branch(mut tape: TapePtr, mut regs: Regs, mut stack: StackPtr) { 40 | // let offset = tape.read::(); 41 | // if stack.read_at::(0) > 0 { 42 | // tape.jump_rel(offset as isize); 43 | // } 44 | // become tape.read::()(tape, regs, stack); 45 | // } 46 | 47 | // pub unsafe fn skip_if(mut tape: TapePtr, mut regs: Regs, mut stack: StackPtr) { 48 | // if stack.read_at::(0) > 0 { 49 | // tape.read::(); 50 | // } 51 | // become tape.read::()(tape, regs, stack); 52 | // } 53 | -------------------------------------------------------------------------------- /src/vm/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub mod stack; 4 | pub mod tape; 5 | pub mod util; 6 | 7 | pub use self::{ 8 | stack::{Stack, StackPtr}, 9 | tape::{FixupTapeOffset, Tape, TapeFn, TapeOffset, TapePtr}, 10 | util::{Data, ReadWrite}, 11 | }; 12 | 13 | #[derive(Copy, Clone, Default)] 14 | pub struct Regs(u64); 15 | 16 | pub struct State<'rt, 'tape, 'stack> { 17 | regs: Regs, 18 | rt: &'rt mut Runtime<'tape, 'stack>, 19 | } 20 | 21 | impl<'rt, 'tape, 'stack> State<'rt, 'tape, 'stack> { 22 | pub fn init(rt: &'rt mut Runtime<'tape, 'stack>) -> Self { Self { regs: Regs::default(), rt } } 23 | } 24 | 25 | #[derive(Default)] 26 | pub struct Runtime<'tape, 'stack> { 27 | pub(crate) last_state: Option<(TapePtr<'tape>, Regs, StackPtr<'stack>)>, 28 | } 29 | 30 | impl<'tape, 'stack> Runtime<'tape, 'stack> { 31 | fn save_state(&mut self, tape: TapePtr<'tape>, regs: Regs, stack: StackPtr<'stack>) { self.last_state = Some((tape, regs, stack)); } 32 | 33 | pub fn last_stack_ptr(&mut self) -> Option<&mut StackPtr<'stack>> { self.last_state.as_mut().map(|(_, _, stack)| stack) } 34 | } 35 | -------------------------------------------------------------------------------- /src/vm/stack.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub struct Stack { 4 | bytes: Vec>, 5 | } 6 | 7 | impl Stack { 8 | pub fn new(cap: usize) -> Self { 9 | Self { 10 | bytes: vec![MaybeUninit::uninit(); cap], 11 | } 12 | } 13 | 14 | pub fn ptr(&mut self) -> StackPtr { 15 | StackPtr { 16 | ptr: { 17 | let ptr = self.bytes.as_mut_ptr(); 18 | let align_offset = ptr.align_offset(StackPtr::FRAME_ALIGN); 19 | assert!(align_offset < self.bytes.len()); 20 | unsafe { ptr.offset(align_offset as isize) } 21 | }, 22 | phantom: PhantomData, 23 | } 24 | } 25 | } 26 | 27 | // Grows down 28 | #[repr(transparent)] 29 | pub struct StackPtr<'stack> { 30 | ptr: *mut MaybeUninit, 31 | phantom: PhantomData<&'stack mut Stack>, 32 | } 33 | 34 | impl<'stack> StackPtr<'stack> { 35 | pub const FRAME_ALIGN: usize = 16; 36 | 37 | #[inline(always)] 38 | pub(crate) fn ptr(&self) -> *const MaybeUninit { self.ptr } 39 | #[inline(always)] 40 | pub(crate) fn ptr_mut(&mut self) -> *mut MaybeUninit { self.ptr } 41 | 42 | #[inline(always)] 43 | pub unsafe fn enter_stack_frame(&mut self, frame_start: usize, stack_end: *mut MaybeUninit) { 44 | self.ptr = self.ptr.offset(frame_start as isize); 45 | } 46 | 47 | #[inline(always)] 48 | pub unsafe fn read(&self, offset: usize) -> T { T::read(self, offset) } 49 | #[inline(always)] 50 | pub unsafe fn write(&mut self, x: T, offset: usize) { x.write(self, offset) } 51 | 52 | #[inline(always)] 53 | pub unsafe fn copy(&mut self, src: usize, dst: usize, len: usize) { 54 | core::ptr::copy_nonoverlapping(self.ptr.offset(src as isize), self.ptr.offset(dst as isize), len); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/vm/tape.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub type TapeFn = for<'rt, 'tape, 'stack> fn(TapePtr<'tape>, State<'rt, 'tape, 'stack>, StackPtr<'stack>); 4 | 5 | // TODO: Rename to `CodeAddr` 6 | #[derive(Debug, Copy, Clone)] 7 | pub struct TapeOffset(usize); 8 | 9 | impl TapeOffset { 10 | pub fn offset_to(&self, other: Self) -> isize { other.0 as isize - self.0 as isize } 11 | } 12 | 13 | #[derive(Debug, Copy, Clone)] 14 | pub struct FixupTapeOffset(TapeOffset, PhantomData); 15 | 16 | #[derive(Default)] 17 | pub struct Tape { 18 | // `MaybeUninit` is used because it may carry provenance, which is important for function pointers 19 | // See [https://github.com/rust-lang/miri/issues/2926] 20 | code: Vec>, 21 | pub(crate) symbols: Vec<(usize, usize, String)>, 22 | } 23 | 24 | impl Tape { 25 | pub fn next_addr(&self) -> TapeOffset { TapeOffset(self.code.len()) } 26 | 27 | pub fn push(&mut self, data: T) -> TapeOffset 28 | where 29 | [(); T::BYTES]:, 30 | { 31 | let offset = self.next_addr(); 32 | self.code.extend_from_slice(data.to_bytes().as_ref()); 33 | offset 34 | } 35 | 36 | pub fn ptr(&self) -> TapePtr { TapePtr(self.code.as_ptr(), PhantomData) } 37 | 38 | /// Fixup a specific address with a new value. 39 | // Ensure that types match (TODO: check this somehow?!) 40 | pub fn fixup(&mut self, addr: FixupTapeOffset, data: T) 41 | where 42 | [(); T::BYTES]:, 43 | { 44 | let bytes = data.to_bytes(); 45 | self.code[addr.0 .0..][..bytes.len()].clone_from_slice(bytes.as_ref()); 46 | } 47 | 48 | // Returns (instr addr, args addr) 49 | #[track_caller] 50 | pub fn push_op(&mut self, symbol: String, args: A, _f: F) -> (TapeOffset, A::Addresses) 51 | where 52 | A: Args, 53 | F: for<'rt, 'tape, 'stack> Fn(A, &mut TapePtr<'tape>, &mut State<'rt, 'tape, 'stack>, &mut StackPtr<'stack>) + Copy, 54 | { 55 | fn perform<'rt, 'tape, 'stack, A, F>(mut tape: TapePtr<'tape>, mut state: State<'rt, 'tape, 'stack>, mut stack: StackPtr<'stack>) 56 | where 57 | A: Args, 58 | F: for<'rt1, 'tape1, 'stack1> Fn(A, &mut TapePtr<'tape1>, &mut State<'rt1, 'tape1, 'stack1>, &mut StackPtr<'stack1>) + Copy, 59 | { 60 | let f = unsafe { core::mem::transmute::<_, &F>(&()) }; 61 | 62 | let args = unsafe { A::from_tape(&mut tape) }; 63 | 64 | f(args, &mut tape, &mut state, &mut stack); 65 | 66 | let next_instr = unsafe { tape.read::() }; 67 | become next_instr(tape, state, stack); 68 | } 69 | 70 | // Doesn't work yet :( 71 | // const { 72 | // if core::mem::size_of::() != 0 { 73 | // panic!("Can only perform operations that have no environment"); 74 | // } 75 | // } 76 | 77 | /// ```compile_fail 78 | /// use yurt::vm::Tape; 79 | /// let mut tape = Tape::default(); 80 | /// let i = 0; 81 | /// tape.push_op((), |(), _, _, _| { 82 | /// let _ = &i; 83 | /// }); 84 | /// ``` 85 | trait NoCapture: Sized { 86 | const ASSERT: () = assert!(core::mem::size_of::() == 0, "Can only perform operations that have no environment"); 87 | } 88 | 89 | impl NoCapture for F {} 90 | 91 | #[allow(clippy::let_unit_value)] 92 | let _ = ::ASSERT; 93 | 94 | let pre = self.code.len(); 95 | 96 | let instr_addr = self.push(perform:: as TapeFn); 97 | let arg_addrs = args.push(self); 98 | 99 | self.symbols.push((pre, self.code.len(), symbol)); 100 | 101 | (instr_addr, arg_addrs) 102 | } 103 | 104 | pub fn push_exit(&mut self) { 105 | fn exit<'rt, 'tape, 'stack>(tape: TapePtr<'tape>, state: State<'rt, 'tape, 'stack>, stack: StackPtr<'stack>) { 106 | state.rt.save_state(tape, state.regs, stack); 107 | } 108 | 109 | let pre = self.code.len(); 110 | self.push(exit as TapeFn); 111 | self.symbols.push((pre, self.code.len(), "exit".to_string())); 112 | } 113 | } 114 | 115 | pub trait Args { 116 | type Addresses; 117 | 118 | fn push(self, tape: &mut Tape) -> Self::Addresses; 119 | /// Tape data at the pointer head must be valid for these args in sequence. 120 | unsafe fn from_tape(tape: &mut TapePtr) -> Self; 121 | } 122 | 123 | macro_rules! impl_args_for_tuple { 124 | () => { impl_args_for_tuple!(~); }; 125 | ($head:ident $($X:ident)*) => { 126 | impl_args_for_tuple!($($X)*); 127 | impl_args_for_tuple!(~ $head $($X)*); 128 | }; 129 | (~ $($X:ident)*) => { 130 | #[allow(unused_variables, non_snake_case)] 131 | impl<$($X: Data),*> Args for ($($X,)*) where $([(); $X::BYTES]:,)* { 132 | type Addresses = ($(FixupTapeOffset<$X>,)*); 133 | fn push(self, tape: &mut Tape) -> Self::Addresses { 134 | let ($($X,)*) = self; 135 | ($(FixupTapeOffset(tape.push($X), PhantomData),)*) 136 | } 137 | unsafe fn from_tape(tape: &mut TapePtr) -> Self { 138 | ($($X::from_bytes(*tape.read_bytes()),)*) 139 | } 140 | } 141 | }; 142 | } 143 | 144 | impl_args_for_tuple!(A_ B_ C_ D_ E_ F_); 145 | 146 | #[repr(transparent)] 147 | pub struct TapePtr<'tape>(*const MaybeUninit, PhantomData<&'tape Tape>); 148 | 149 | impl<'tape> TapePtr<'tape> { 150 | /// SAFETY: Offset must be valid for tape. 151 | #[inline(always)] 152 | pub unsafe fn jump_rel(&mut self, offset: isize) { self.0 = self.0.offset(offset); } 153 | /// SAFETY: `TapePtr` must have at least `N` bytes to read left. 154 | #[inline(always)] 155 | pub unsafe fn read_bytes(&mut self) -> &'tape [MaybeUninit; N] { 156 | let r = &*(self.0 as *const [MaybeUninit; N]); 157 | self.0 = self.0.offset(N as isize); 158 | r 159 | } 160 | 161 | /// SAFETY: `TapePtr` must have data exactly corresponding to this type. 162 | #[inline(always)] 163 | pub unsafe fn read(&mut self) -> T 164 | where 165 | [(); T::BYTES]:, 166 | { 167 | T::from_bytes(*self.read_bytes()) 168 | } 169 | 170 | #[inline(always)] 171 | pub unsafe fn exec_at<'rt, 'stack>(&mut self, offset: TapeOffset, state: State<'rt, 'tape, 'stack>, stack: StackPtr<'stack>) { 172 | let mut tape = Self(self.0.offset(offset.0 as isize), PhantomData); 173 | tape.read::()(tape, state, stack); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/vm/util.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::api::Repr; 3 | use core::ops::Range; 4 | 5 | // Take an existing stack offset and return the offset range for the given repr to be placed on the stack. 6 | pub fn incr_offset(offset: usize, repr: &Repr) -> Range { 7 | let start = offset.next_multiple_of(repr.align()); 8 | start..(start + repr.size()) 9 | } 10 | 11 | pub trait AsRepr { 12 | fn repr() -> Repr; 13 | } 14 | 15 | pub trait Data { 16 | const BYTES: usize; 17 | const ALIGN: usize; 18 | 19 | fn to_bytes(&self) -> [MaybeUninit; Self::BYTES] 20 | where 21 | [(); Self::BYTES]:; 22 | /// Bytes must be valid for this type. 23 | unsafe fn from_bytes(bytes: [MaybeUninit; Self::BYTES]) -> Self; 24 | } 25 | 26 | pub trait StackSafe: Sized { 27 | const ASSERT: () = assert!( 28 | core::mem::align_of::() <= StackPtr::FRAME_ALIGN, 29 | "Stack-safe aligns must have an alignment <= the minimum stack frame alignment" 30 | ); 31 | } 32 | 33 | pub trait ReadWrite { 34 | unsafe fn write(&self, stack: &mut StackPtr, offset: usize); 35 | unsafe fn read(stack: &StackPtr, offset: usize) -> Self; 36 | } 37 | 38 | macro_rules! tape_data_int { 39 | ($($T:ty = $N:expr),* $(,)?) => { 40 | $( 41 | impl Data for $T { 42 | const BYTES: usize = $N; 43 | const ALIGN: usize = core::mem::align_of::<$T>(); 44 | #[inline(always)] 45 | fn to_bytes(&self) -> [MaybeUninit; Self::BYTES] { MaybeUninit::new(self.to_ne_bytes()).transpose() } 46 | #[inline(always)] 47 | unsafe fn from_bytes(bytes: [MaybeUninit; Self::BYTES]) -> Self { 48 | <$T>::from_ne_bytes(MaybeUninit::array_assume_init(bytes)) 49 | } 50 | } 51 | 52 | impl StackSafe for $T {} 53 | 54 | impl ReadWrite for $T { 55 | #[inline(always)] 56 | unsafe fn write(&self, stack: &mut StackPtr, offset: usize) { 57 | stack.ptr_mut().offset(offset as isize).cast::().write(*self); 58 | } 59 | #[inline(always)] 60 | unsafe fn read(stack: &StackPtr, offset: usize) -> Self { 61 | stack.ptr().offset(offset as isize).cast::().read() 62 | } 63 | } 64 | )* 65 | }; 66 | } 67 | 68 | tape_data_int!( 69 | u8 = 1, 70 | u16 = 2, 71 | u32 = 4, 72 | u64 = 8, 73 | u128 = 16, 74 | usize = core::mem::size_of::(), 75 | i8 = 1, 76 | i16 = 2, 77 | i32 = 4, 78 | i64 = 8, 79 | i128 = 16, 80 | isize = core::mem::size_of::(), 81 | ); 82 | 83 | impl AsRepr for u64 { 84 | fn repr() -> Repr { Repr::U64 } 85 | } 86 | 87 | impl AsRepr for bool { 88 | fn repr() -> Repr { Repr::Bool } 89 | } 90 | 91 | impl Data for TapeFn { 92 | const BYTES: usize = core::mem::size_of::(); 93 | const ALIGN: usize = core::mem::align_of::(); 94 | 95 | #[inline(always)] 96 | fn to_bytes(&self) -> [MaybeUninit; Self::BYTES] { MaybeUninit::new((*self as usize).to_ne_bytes()).transpose() } 97 | #[inline(always)] 98 | unsafe fn from_bytes(bytes: [MaybeUninit; Self::BYTES]) -> Self { core::mem::transmute(bytes) } 99 | } 100 | 101 | impl Data for bool { 102 | const BYTES: usize = 1; 103 | const ALIGN: usize = core::mem::align_of::(); 104 | #[inline(always)] 105 | fn to_bytes(&self) -> [MaybeUninit; Self::BYTES] { [MaybeUninit::new(*self as u8)] } 106 | #[inline(always)] 107 | unsafe fn from_bytes(bytes: [MaybeUninit; Self::BYTES]) -> Self { MaybeUninit::array_assume_init(bytes)[0] > 0 } 108 | } 109 | 110 | impl StackSafe for bool {} 111 | 112 | impl ReadWrite for bool { 113 | #[inline(always)] 114 | unsafe fn write(&self, stack: &mut StackPtr, offset: usize) { stack.ptr_mut().offset(offset as isize).cast::().write(*self as u8); } 115 | #[inline(always)] 116 | unsafe fn read(stack: &StackPtr, offset: usize) -> Self { stack.ptr().offset(offset as isize).cast::().read() > 0 } 117 | } 118 | 119 | macro_rules! impl_push_pop_for_tuple { 120 | () => { impl_push_pop_for_tuple!(~); }; 121 | ($head:ident $($X:ident)*) => { 122 | impl_push_pop_for_tuple!($($X)*); 123 | impl_push_pop_for_tuple!(~ $head $($X)*); 124 | }; 125 | (~ $($X:ident)*) => { 126 | #[allow(unused_variables, non_snake_case)] 127 | impl<$($X: ReadWrite + AsRepr),*> ReadWrite for ($($X,)*) { 128 | #[inline(always)] 129 | #[allow(unused_assignments)] 130 | unsafe fn write(&self, stack: &mut StackPtr, mut offset: usize) { 131 | let ($($X,)*) = self; 132 | $( 133 | // TODO: Faster implementation without invoking $X::repr() 134 | let field_range = incr_offset(offset, &$X::repr()); 135 | let $X = $X.write(stack, field_range.start); 136 | offset = field_range.end; 137 | )* 138 | } 139 | #[inline(always)] 140 | #[allow(unused_assignments)] 141 | unsafe fn read(stack: &StackPtr, mut offset: usize) -> Self { 142 | $( 143 | // TODO: Faster implementation without invoking $X::repr() 144 | let field_range = incr_offset(offset, &$X::repr()); 145 | let $X = $X::read(stack, field_range.start); 146 | offset = field_range.end; 147 | )* 148 | ($($X,)*) 149 | } 150 | } 151 | 152 | impl<$($X: AsRepr),*> AsRepr for ($($X,)*) { 153 | fn repr() -> Repr { Repr::Tuple(vec![$($X::repr(),)*]) } 154 | } 155 | }; 156 | } 157 | 158 | impl_push_pop_for_tuple!(A_ B_ C_ D_ E_ F_); 159 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use yurt::{ 2 | api::{Cond, Expr, Module, Repr}, 3 | vm::{Runtime, Stack, State, Tape}, 4 | }; 5 | 6 | #[test] 7 | fn procedure() { 8 | let mut module = Module::build(); 9 | let add_mul = module.build_func(Repr::Tuple(vec![Repr::U64, Repr::U64, Repr::U64]), Repr::U64, |arg| { 10 | Cond::NotZero.if_then_else( 11 | Expr::U64(1), 12 | arg.clone().field(0).mul(arg.clone().field(1)).add(arg.field(2)), 13 | Expr::U64(42), 14 | ) 15 | }); 16 | let module = module.compile().unwrap(); 17 | 18 | module.show_symbols(); 19 | 20 | assert_eq!(module.call::<_, u64>(add_mul, (3u64, 5u64, 1u64)).unwrap(), 16); 21 | } 22 | --------------------------------------------------------------------------------