diff --git a/fix-codegen/src/disassembler.rs b/fix-codegen/src/disassembler.rs index 5aba5c5..08e3d65 100644 --- a/fix-codegen/src/disassembler.rs +++ b/fix-codegen/src/disassembler.rs @@ -79,19 +79,23 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { #[inline(always)] fn read_operand_data(&mut self) { + use OperandType::*; let tag = self.read_u8(); let ty = OperandType::try_from_primitive(tag).expect("invalid operand type"); match ty { - OperandType::Const => { + Const => { self.read_u32(); } - OperandType::Local => { + BigInt => { + self.read_i64(); + } + Local => { self.read_u8(); self.read_u32(); } - OperandType::Builtins => {} - OperandType::BigInt => { - self.read_i64(); + Builtins => {} + ReplBinding | ScopedImportBinding | WithLookup => { + self.read_u32(); } } } diff --git a/fix-codegen/src/lib.rs b/fix-codegen/src/lib.rs index b6f571a..6fdae83 100644 --- a/fix-codegen/src/lib.rs +++ b/fix-codegen/src/lib.rs @@ -1,6 +1,4 @@ -use std::ops::Deref; - -use fix_builtins::BuiltinId; +use fix_builtins::{BUILTINS, BuiltinId}; use fix_common::StringId; use fix_ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind}; use hashbrown::HashMap; @@ -116,9 +114,12 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> { #[derive(Debug, Clone, Copy, TryFromPrimitive)] pub enum OperandType { Const, + BigInt, Local, Builtins, - BigInt, + ReplBinding, + ScopedImportBinding, + WithLookup, } pub enum Const { @@ -126,6 +127,7 @@ pub enum Const { Float(f64), Bool(bool), String(StringId), + Path(StringId), PrimOp { id: BuiltinId, arity: u8, @@ -143,9 +145,12 @@ pub enum AttrKeyType { pub enum InlineOperand { Const(Const), + BigInt(i64), Local { layer: u8, local: u32 }, Builtins, - BigInt(i64), + ReplBinding(StringId), + ScopedImportBinding(StringId), + WithLookup(StringId), } pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr { @@ -164,9 +169,9 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } #[must_use] - fn inline_maybe_thunk(&self, val: MaybeThunk) -> InlineOperand { + fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand { use MaybeThunk::*; - match val { + match *val { Int(x) => { if let Ok(x) = x.try_into() { InlineOperand::Const(Const::Smi(x)) @@ -178,37 +183,55 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { Bool(b) => InlineOperand::Const(Const::Bool(b)), Null => InlineOperand::Const(Const::Null), Str(id) => InlineOperand::Const(Const::String(id)), + Path(id) => InlineOperand::Const(Const::String(id)), Thunk(id) => { let (layer, local) = self.resolve_thunk(id); InlineOperand::Local { layer, local } } - Arg { layer } => InlineOperand::Local { - layer, - local: 0, + Arg { layer } => InlineOperand::Local { layer, local: 0 }, + Builtin(id) => { + let (_, arity) = BUILTINS[id as usize]; + InlineOperand::Const(Const::PrimOp { id, arity, dispatch_ip: id.entry_phase().ip() }) }, - _ => todo!(), + Builtins => InlineOperand::Builtins, + ReplBinding(id) => InlineOperand::ReplBinding(id), + ScopedImportBinding(id) => InlineOperand::ScopedImportBinding(id), + WithLookup(id) => InlineOperand::WithLookup(id), } } - fn emit_maybe_thunk(&mut self, val: MaybeThunk) { + fn emit_maybe_thunk(&mut self, val: &MaybeThunk) { + use InlineOperand::*; let operand = self.inline_maybe_thunk(val); match operand { - InlineOperand::Const(val) => { + Const(val) => { let idx = self.ctx.add_constant(val); self.emit_u8(OperandType::Const as u8); self.emit_u32(idx); } - InlineOperand::Local { layer, local } => { + BigInt(val) => { + self.emit_u8(OperandType::BigInt as u8); + self.emit_i64(val); + } + Local { layer, local } => { self.emit_u8(OperandType::Local as u8); self.emit_u8(layer); self.emit_u32(local); } - InlineOperand::Builtins => { + Builtins => { self.emit_u8(OperandType::Builtins as u8); } - InlineOperand::BigInt(val) => { - self.emit_u8(OperandType::BigInt as u8); - self.emit_i64(val); + ReplBinding(id) => { + self.emit_u8(OperandType::ReplBinding as u8); + self.emit_str_id(id); + } + ScopedImportBinding(id) => { + self.emit_u8(OperandType::ScopedImportBinding as u8); + self.emit_str_id(id); + } + WithLookup(id) => { + self.emit_u8(OperandType::WithLookup as u8); + self.emit_str_id(id); } } } @@ -315,7 +338,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize { - match ir.deref() { + match ir { Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body), Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body), Ir::If { cond, consq, alter } => { @@ -361,7 +384,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { ir: RawIrRef<'ir>, out: &mut Vec<(ThunkId, RawIrRef<'ir>)>, ) { - match ir.deref() { + match ir { Ir::With { thunks, body, .. } => { for &(id, inner) in thunks.iter() { out.push((id, inner)); @@ -422,7 +445,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } fn emit_toplevel(&mut self, ir: RawIrRef<'_>) { - match ir.deref() { + match ir { &Ir::TopLevel { body, ref thunks } => { let with_thunk_count = self.count_with_thunks(body); let total_slots = thunks.len() + with_thunk_count; @@ -468,11 +491,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } fn emit_expr(&mut self, ir: RawIrRef<'_>) { - match ir.deref() { + match ir { &Ir::Int(x) => { - if x <= i32::MAX as i64 { + if let Ok(x) = x.try_into() { self.emit_op(Op::PushSmi); - self.emit_i32(x as i32); + self.emit_i32(x); } else { self.emit_op(Op::PushBigInt); self.emit_i64(x); @@ -567,10 +590,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } => { self.emit_select(expr, attrpath, default, span); } - &Ir::Thunk(id) => { - let (layer, local) = self.resolve_thunk(id); - self.emit_load(layer, local); - } Ir::Builtins => { self.emit_op(Op::LoadBuiltins); } @@ -580,7 +599,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } &Ir::BuiltinConst(id) => { self.emit_select( - RawIrRef(&Ir::Builtins), + &Ir::Builtins, &[Attr::Str(id, TextRange::default())], None, TextRange::default(), @@ -642,6 +661,60 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::LookupWith); self.emit_str_id(name); } + &Ir::MaybeThunk(thunk) => { + use MaybeThunk::*; + match *thunk { + Int(x) => { + if let Ok(x) = x.try_into() { + self.emit_op(Op::PushSmi); + self.emit_i32(x); + } else { + self.emit_op(Op::PushBigInt); + self.emit_i64(x); + } + } + Float(x) => { + self.emit_op(Op::PushFloat); + self.emit_f64(x); + } + Bool(true) => self.emit_op(Op::PushTrue), + Bool(false) => self.emit_op(Op::PushFalse), + Null => self.emit_op(Op::PushNull), + Str(id) => { + self.emit_op(Op::PushString); + self.emit_str_id(id); + } + Path(id) => { + self.emit_op(Op::PushString); + self.emit_str_id(id); + self.emit_op(Op::ResolvePath); + } + Thunk(id) => { + let (layer, local) = self.resolve_thunk(id); + self.emit_load(layer, local); + } + Arg { layer } => self.emit_load(layer, 0), + Builtin(id) => { + self.emit_op(Op::LoadBuiltin); + self.emit_u8(id as u8); + } + Builtins => self.emit_op(Op::LoadBuiltins), + ReplBinding(name) => { + self.emit_op(Op::LoadReplBinding); + self.emit_str_id(name); + } + ScopedImportBinding(name) => { + self.emit_op(Op::LoadScopedBinding); + self.emit_str_id(name); + } + WithLookup(name) => { + // TODO: specialize shallow with lookups + self.emit_op(Op::PrepareWith); + self.emit_op(Op::LookupWith); + self.emit_str_id(name); + } + } + } } } @@ -739,11 +812,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } } - fn emit_func( + fn emit_func<'ir>( &mut self, - thunks: &[(ThunkId, RawIrRef<'_>)], - param: &Option>, - body: RawIrRef<'_>, + thunks: &[(ThunkId, RawIrRef<'ir>)], + param: &Option>, + body: RawIrRef<'ir>, ) { let with_thunk_count = self.count_with_thunks(body); let total_slots = thunks.len() + with_thunk_count; @@ -793,8 +866,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn emit_attrset( &mut self, - stcs: &fix_ir::HashMap<'_, StringId, (MaybeThunk, TextRange)>, - dyns: &[(RawIrRef<'_>, MaybeThunk, TextRange)], + stcs: &fix_ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>, + dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)], ) { if stcs.is_empty() && dyns.is_empty() { self.emit_op(Op::MakeEmptyAttrs); @@ -913,7 +986,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn emit_with( &mut self, - namespace: MaybeThunk, + namespace: &MaybeThunk, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)], ) { diff --git a/fix-ir/src/downgrade.rs b/fix-ir/src/downgrade.rs index 66e4b71..f7665e2 100644 --- a/fix-ir/src/downgrade.rs +++ b/fix-ir/src/downgrade.rs @@ -39,12 +39,12 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T, E: std::fmt::Display> } pub trait DowngradeContext<'id: 'ir, 'ir> { - fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir>; - fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> MaybeThunk; + fn new_expr(&self, expr: Ir<'ir, GhostRef<'id, 'ir>>) -> IrRef<'id, 'ir>; + fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> GhostMaybeThunkRef<'id, 'ir>; fn intern_string(&mut self, sym: impl AsRef) -> StringId; fn resolve_sym(&self, id: StringId) -> Symbol<'_>; - fn lookup(&self, sym: StringId, span: TextRange) -> Result; + fn lookup(&self, sym: StringId, span: TextRange) -> Result>; fn get_current_source(&self) -> Source; @@ -229,7 +229,8 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo let span = self.syntax().text_range(); let text = self.ident_token().require(ctx, span)?.to_string(); let sym = ctx.intern_string(text); - ctx.lookup(sym, span).map(|thunk| thunk.to_ir(ctx)) + ctx.lookup(sym, span) + .map(|thunk| ctx.new_expr(Ir::MaybeThunk(thunk))) } } @@ -835,8 +836,8 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) - } struct FinalizedAttrSet<'id, 'ir> { - stcs: HashMap<'ir, StringId, (MaybeThunk, TextRange)>, - dyns: Vec<'ir, (IrRef<'id, 'ir>, MaybeThunk, TextRange)>, + stcs: HashMap<'ir, StringId, (GhostMaybeThunkRef<'id, 'ir>, TextRange)>, + dyns: Vec<'ir, (IrRef<'id, 'ir>, GhostMaybeThunkRef<'id, 'ir>, TextRange)>, } fn downgrade_attrs<'id, 'ir>( @@ -1074,7 +1075,7 @@ where F: FnOnce( &mut Ctx, &[StringId], - &[(IrRef<'id, 'ir>, MaybeThunk, TextRange)], + &[(IrRef<'id, 'ir>, GhostMaybeThunkRef<'id, 'ir>, TextRange)], ) -> Result>, { let mut pending = PendingAttrSet::new_in(ctx.bump()); @@ -1090,7 +1091,7 @@ where let vals = { let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump()); for sym in &keys { - temp.push(finalized.stcs.get(sym).expect("WTF").0.to_ir(ctx)); + temp.push(ctx.new_expr(Ir::MaybeThunk(finalized.stcs.get(sym).expect("WTF").0))); } temp }; @@ -1101,7 +1102,7 @@ where fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>( entries: &[ast::Entry], ctx: &mut Ctx, -) -> Result> { +) -> Result, TextRange)>> { let mut inherit_lookups = HashMap::new_in(ctx.bump()); for entry in entries { if let ast::Entry::Inherit(inherit) = entry @@ -1141,7 +1142,7 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( pending: PendingAttrSet, - inherit_lookups: &HashMap, + inherit_lookups: &HashMap, TextRange)>, ctx: &mut Ctx, ) -> Result> { let mut stcs = HashMap::new_in(ctx.bump()); @@ -1169,7 +1170,7 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( value: PendingValue, - inherit_lookups: &HashMap, + inherit_lookups: &HashMap, TextRange)>, ctx: &mut Ctx, ) -> Result> { match value { @@ -1185,9 +1186,10 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW } PendingValue::InheritScope(sym, span) => { if let Some(&(expr, _)) = inherit_lookups.get(&sym) { - Ok(expr.to_ir(ctx)) + Ok(ctx.new_expr(Ir::MaybeThunk(expr))) } else { - ctx.lookup(sym, span).map(|val| val.to_ir(ctx)) + ctx.lookup(sym, span) + .map(|val| ctx.new_expr(Ir::MaybeThunk(val))) } } PendingValue::Set(set) => { diff --git a/fix-ir/src/lib.rs b/fix-ir/src/lib.rs index 15ee0c2..355b130 100644 --- a/fix-ir/src/lib.rs +++ b/fix-ir/src/lib.rs @@ -1,5 +1,5 @@ use std::hash::Hash; -use std::ops::Deref; +use std::marker::PhantomData; use bumpalo::Bump; use bumpalo::collections::Vec; @@ -10,56 +10,36 @@ use num_enum::TryFromPrimitive as _; use rnix::{TextRange, ast}; use string_interner::DefaultStringInterner; -use crate::downgrade::DowngradeContext; - pub mod downgrade; pub type HashMap<'ir, K, V> = hashbrown::HashMap; -#[repr(transparent)] -#[derive(Clone, Copy)] -pub struct IrRef<'id, 'ir>(&'ir GhostCell<'id, Ir<'ir, Self>>); - -impl<'id, 'ir> IrRef<'id, 'ir> { - pub fn new(ir: &'ir GhostCell<'id, Ir<'ir, Self>>) -> Self { - Self(ir) - } - - pub fn alloc(bump: &'ir Bump, ir: Ir<'ir, Self>) -> Self { - Self(bump.alloc(GhostCell::new(ir))) - } - - pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> { - self.0.borrow(token) - } +pub type IrRef<'id, 'ir> = as RefExt<'ir>>::IrRef; +pub type RawIrRef<'ir> = as RefExt<'ir>>::IrRef; +pub type GhostMaybeThunkRef<'id, 'ir> = as RefExt<'ir>>::MaybeThunkRef; +impl<'id, 'ir> Ir<'ir, GhostRef<'id, 'ir>> { /// Freeze a mutable IR reference into a read-only one, consuming the /// `GhostToken` to prevent any further mutation. - /// - /// # Safety - /// The transmute is sound because: - /// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T` - /// - `IrRef<'id, 'ir>` is `#[repr(transparent)]` over - /// `&'ir GhostCell<'id, Ir<'ir, Self>>` - /// - `RawIrRef<'ir>` is `#[repr(transparent)]` over `&'ir Ir<'ir, Self>` - /// - `Ir<'ir, Ref>` is `#[repr(C)]` and both ref types are pointer-sized - /// - /// Consuming the `GhostToken` guarantees no `borrow_mut` calls can occur - /// afterwards, so the shared `&Ir` references from `RawIrRef::Deref` can - /// never alias with mutable references. - pub fn freeze(self, _token: GhostToken<'id>) -> RawIrRef<'ir> { - unsafe { std::mem::transmute(self) } - } -} - -#[repr(transparent)] -#[derive(Clone, Copy, Debug)] -pub struct RawIrRef<'ir>(pub &'ir Ir<'ir, Self>); - -impl<'ir> Deref for RawIrRef<'ir> { - type Target = Ir<'ir, RawIrRef<'ir>>; - fn deref(&self) -> &Self::Target { - self.0 + pub fn freeze(this: IrRef<'id, 'ir>, _: GhostToken<'id>) -> RawIrRef<'ir> { + // SAFETY: The transmute is sound because: + // - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`, so + // `&'ir GhostCell<'id, T>` and `&'ir T` have identical layout. + // - `Ir<'ir, R>` is `#[repr(C)]`, and for every field that depends on + // `R`, instantiating `R = GhostRef<'id, 'ir>` vs `R = RawRef<'ir>` + // produces types of identical layout: + // - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<…>>` vs `&'ir Ir<…>` + // - `R::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>` + // vs `&'ir MaybeThunk` + // - `R::Ref>` (used in `ConcatStrings::parts`) reduces + // to the same case as `R::IrRef` + // - Therefore `IrRef<'id, 'ir>` and `RawIrRef<'ir>` are both + // pointer-sized references with the same layout. + // + // Consuming the `GhostToken` guarantees no `borrow_mut` calls can + // occur afterwards, so the shared `&Ir` references reachable from a + // `RawIrRef<'ir>` can never alias with mutable references. + unsafe { std::mem::transmute::, RawIrRef<'ir>>(this) } } } @@ -81,102 +61,108 @@ pub enum MaybeThunk { WithLookup(StringId), } -impl MaybeThunk { - fn to_ir<'id, 'ir>(self, ctx: &mut impl DowngradeContext<'id, 'ir>) -> IrRef<'id, 'ir> { - use MaybeThunk::*; - let ir = match self { - Int(x) => Ir::Int(x), - Float(x) => Ir::Float(x), - Bool(x) => Ir::Bool(x), - Null => Ir::Null, - Str(x) => Ir::Str(x), - Path(x) => Ir::Path(ctx.new_expr(Ir::Str(x))), - Thunk(x) => Ir::Thunk(x), - Arg { layer } => Ir::Arg { layer }, - Builtin(x) => Ir::Builtin(x), - Builtins => Ir::Builtins, - ReplBinding(x) => Ir::ReplBinding(x), - ScopedImportBinding(x) => Ir::ScopedImportBinding(x), - WithLookup(x) => Ir::WithLookup(x), - }; - ctx.new_expr(ir) - } + +pub trait Ref<'ir> { + type Ref + where + T: 'ir; +} + +pub trait RefExt<'ir>: Ref<'ir> { + type Ir; + type IrRef; + type MaybeThunkRef; +} +impl<'ir, T: Ref<'ir> + 'ir> RefExt<'ir> for T { + type Ir = Ir<'ir, Self>; + type IrRef = Self::Ref; + type MaybeThunkRef = Self::Ref; +} + +pub struct GhostRef<'id, 'ir>(PhantomData<&'ir GhostCell<'id, ()>>); +pub struct RawRef<'ir>(PhantomData<&'ir ()>); + +impl<'id, 'ir> Ref<'ir> for GhostRef<'id, 'ir> { + type Ref = &'ir GhostCell<'id, T>; +} +impl<'ir> Ref<'ir> for RawRef<'ir> { + type Ref = &'ir T; } #[repr(C)] #[derive(Debug)] -pub enum Ir<'ir, Ref> { +pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> { Int(i64), Float(f64), Bool(bool), Null, Str(StringId), - Path(Ref), + Path(R::IrRef), AttrSet { - stcs: HashMap<'ir, StringId, (MaybeThunk, TextRange)>, - dyns: Vec<'ir, (Ref, MaybeThunk, TextRange)>, + stcs: HashMap<'ir, StringId, (R::MaybeThunkRef, TextRange)>, + dyns: Vec<'ir, (R::IrRef, R::MaybeThunkRef, TextRange)>, }, List { - items: Vec<'ir, MaybeThunk>, + items: Vec<'ir, R::MaybeThunkRef>, }, ConcatStrings { - parts: Vec<'ir, Ref>, + parts: Vec<'ir, R::Ref>>, force_string: bool, }, // OPs UnOp { - rhs: Ref, + rhs: R::IrRef, kind: UnOpKind, }, BinOp { - lhs: Ref, - rhs: Ref, + lhs: R::IrRef, + rhs: R::IrRef, kind: BinOpKind, }, HasAttr { - lhs: Ref, - rhs: Vec<'ir, Attr>, + lhs: R::IrRef, + rhs: Vec<'ir, Attr>, }, Select { - expr: Ref, - attrpath: Vec<'ir, Attr>, - default: Option, + expr: R::IrRef, + attrpath: Vec<'ir, Attr>, + default: Option, span: TextRange, }, // Conditionals If { - cond: Ref, - consq: Ref, - alter: Ref, + cond: R::IrRef, + consq: R::IrRef, + alter: R::IrRef, }, Assert { - assertion: Ref, - expr: Ref, + assertion: R::IrRef, + expr: R::IrRef, assertion_raw: String, span: TextRange, }, With { - namespace: MaybeThunk, - body: Ref, - thunks: Vec<'ir, (ThunkId, Ref)>, + namespace: R::MaybeThunkRef, + body: R::IrRef, + thunks: Vec<'ir, (ThunkId, R::IrRef)>, }, WithLookup(StringId), // Function related Func { - body: Ref, + body: R::IrRef, param: Option>, - thunks: Vec<'ir, (ThunkId, Ref)>, + thunks: Vec<'ir, (ThunkId, R::IrRef)>, }, Arg { layer: u8, }, Call { - func: Ref, - arg: MaybeThunk, + func: R::IrRef, + arg: R::MaybeThunkRef, span: TextRange, }, @@ -187,10 +173,10 @@ pub enum Ir<'ir, Ref> { // Misc TopLevel { - body: Ref, - thunks: Vec<'ir, (ThunkId, Ref)>, + body: R::IrRef, + thunks: Vec<'ir, (ThunkId, R::IrRef)>, }, - Thunk(ThunkId), + MaybeThunk(R::MaybeThunkRef), CurPos(TextRange), ReplBinding(StringId), ScopedImportBinding(StringId), @@ -299,7 +285,7 @@ pub struct Param<'ir> { pub fn new_global_env( strings: &mut DefaultStringInterner, -) -> hashbrown::HashMap>> { +) -> hashbrown::HashMap>> { let mut global_env = hashbrown::HashMap::new(); let builtins_sym = StringId(strings.get_or_intern("builtins")); global_env.insert(builtins_sym, Ir::Builtins); diff --git a/fix-vm/src/bytecode_reader.rs b/fix-vm/src/bytecode_reader.rs index abe35f8..31cd856 100644 --- a/fix-vm/src/bytecode_reader.rs +++ b/fix-vm/src/bytecode_reader.rs @@ -103,15 +103,27 @@ impl<'a> BytecodeReader<'a> { let id = self.read_u32(); OperandData::Const(ctx.get_const(id)) } + OperandType::BigInt => { + let val = self.read_i64(); + OperandData::BigInt(val) + } OperandType::Local => { let layer = self.read_u8(); let idx = self.read_u32(); OperandData::Local { layer, idx } } OperandType::Builtins => OperandData::Builtins, - OperandType::BigInt => { - let val = self.read_i64(); - OperandData::BigInt(val) + OperandType::ReplBinding => { + let id = self.read_string_id(); + OperandData::ReplBinding(id) + } + OperandType::ScopedImportBinding => { + let id = self.read_string_id(); + OperandData::ScopedImportBinding(id) + } + OperandType::WithLookup => { + let id = self.read_string_id(); + OperandData::WithLookup(id) } } } diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 58bbaaf..f477bb2 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -227,16 +227,21 @@ pub struct Vm<'gc> { pub(crate) enum OperandData { Const(StaticValue), + BigInt(i64), Local { layer: u8, idx: u32 }, Builtins, - BigInt(i64), + ReplBinding(StringId), + ScopedImportBinding(StringId), + WithLookup(StringId), } impl OperandData { pub(crate) fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &Vm<'gc>) -> Value<'gc> { + use OperandData::*; match *self { - OperandData::Const(sv) => sv.into(), - OperandData::Local { layer, idx } => { + Const(sv) => sv.into(), + BigInt(val) => Value::new_gc(Gc::new(mc, val)), + Local { layer, idx } => { let mut cur = root.env; for _ in 0..layer { let prev = cur.borrow().prev.expect("env chain too short"); @@ -244,8 +249,10 @@ impl OperandData { } cur.borrow().locals[idx as usize] } - OperandData::Builtins => root.builtins, - OperandData::BigInt(val) => Value::new_gc(Gc::new(mc, val)), + Builtins => root.builtins, + ReplBinding(_id) => todo!(), + ScopedImportBinding(_id) => todo!(), + WithLookup(_id) => todo!(), } } } diff --git a/fix/src/lib.rs b/fix/src/lib.rs index 69b26c6..f9b5176 100644 --- a/fix/src/lib.rs +++ b/fix/src/lib.rs @@ -8,7 +8,7 @@ use fix_codegen::{BytecodeContext, InstructionPtr, Op}; use fix_common::{StringId, Symbol}; use fix_error::{Error, Result, Source}; use fix_ir::downgrade::{Downgrade as _, DowngradeContext}; -use fix_ir::{Ir, IrRef, MaybeThunk, RawIrRef, ThunkId}; +use fix_ir::{GhostMaybeThunkRef, GhostRef, Ir, IrRef, MaybeThunk, RawIrRef, RawRef, ThunkId}; use fix_vm::{ForceMode, StaticValue, Vm, VmCode, VmContext, VmRuntimeCtx}; use ghost_cell::{GhostCell, GhostToken}; use hashbrown::{HashMap, HashSet}; @@ -30,7 +30,7 @@ pub struct CodeState { pub sources: Vec, pub spans: Vec<(usize, rnix::TextRange)>, pub thunk_count: usize, - pub global_env: HashMap>>, + pub global_env: HashMap>>, } pub struct Evaluator { @@ -85,13 +85,13 @@ impl Evaluator { source: Source, scope: &HashSet, ) -> Result { - self.do_eval(source, Some(Scope::Repl(scope)), ForceMode::Shallow) + self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow) } fn do_eval<'ctx>( &'ctx mut self, source: Source, - extra_scope: Option>, + extra_scope: Option>, force_mode: ForceMode, ) -> Result { let ip = { @@ -175,14 +175,14 @@ impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> { fn compile_bytecode( &mut self, source: Source, - extra_scope: Option, + extra_scope: Option, ) -> Result { let root = self.downgrade(source, extra_scope)?; let ip = fix_codegen::compile_bytecode(root.as_ref(), self); Ok(ip) } - fn downgrade(&mut self, source: Source, extra_scope: Option) -> Result { + fn downgrade(&mut self, source: Source, extra_scope: Option) -> Result { tracing::debug!("Parsing Nix expression"); self.code.sources.push(source.clone()); @@ -202,7 +202,7 @@ impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> { token, self.runtime, &self.code.global_env, - extra_scope, + extra_scope.map(Into::into), &mut self.code.thunk_count, source, ); @@ -245,6 +245,7 @@ impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> { Float(x) => StaticValue::new_float(x), Bool(x) => StaticValue::new_inline(x), String(x) => StaticValue::new_inline(x), + Path(_) => todo!(), PrimOp { id, arity, @@ -310,34 +311,20 @@ struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> { token: GhostToken<'id>, runtime: &'ctx mut R, source: Source, - scopes: Vec>, + scopes: Vec>, with_scope_count: u32, arg_count: u32, thunk_count: &'ctx mut usize, thunk_scopes: Vec>, } -fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool { - !matches!( - ir.borrow(token), - Ir::Builtin(_) - | Ir::Builtins - | Ir::Int(_) - | Ir::Float(_) - | Ir::Bool(_) - | Ir::Null - | Ir::Str(_) - | Ir::Thunk(_) - ) -} - impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> { fn new( bump: &'ir Bump, token: GhostToken<'id>, runtime: &'ctx mut R, - global: &'ctx HashMap>>, - extra_scope: Option>, + global: &'ctx HashMap>>, + extra_scope: Option>, thunk_count: &'ctx mut usize, source: Source, ) -> Self { @@ -360,23 +347,29 @@ impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> { impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir, R> { - fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> { - IrRef::new(self.bump.alloc(GhostCell::new(expr))) + fn new_expr(&self, expr: Ir<'ir, GhostRef<'id, 'ir>>) -> IrRef<'id, 'ir> { + self.bump.alloc(GhostCell::new(expr)) } - fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> MaybeThunk { + fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> GhostMaybeThunkRef<'id, 'ir> { use MaybeThunk::*; - match *ir.borrow(&self.token) { - Ir::Builtin(x) => return Builtin(x), - Ir::Int(x) => return Int(x), - Ir::Float(x) => return Float(x), - Ir::Bool(x) => return Bool(x), - Ir::Str(x) => return Str(x), - Ir::Thunk(x) => return Thunk(x), - Ir::Arg { layer } => return Arg { layer }, - Ir::Builtins => return Builtins, - Ir::Null => return Null, - _ => (), + let expr = (|| { + let expr = match *ir.borrow(&self.token) { + Ir::Builtin(x) => Builtin(x), + Ir::Int(x) => Int(x), + Ir::Float(x) => Float(x), + Ir::Bool(x) => Bool(x), + Ir::Str(x) => Str(x), + Ir::Arg { layer } => Arg { layer }, + Ir::Builtins => Builtins, + Ir::Null => Null, + Ir::MaybeThunk(thunk) => return Some(thunk), + _ => return None, + }; + Some(self.bump.alloc(GhostCell::new(expr))) + })(); + if let Some(thunk) = expr { + return thunk; } let id = ThunkId(*self.thunk_count); *self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow"); @@ -384,7 +377,7 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> .last_mut() .expect("no active cache scope") .add_binding(id, ir, &self.token); - Thunk(id) + self.bump.alloc(GhostCell::new(Thunk(id))) } fn intern_string(&mut self, sym: impl AsRef) -> StringId { @@ -395,7 +388,7 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> self.runtime.resolve_string(id).into() } - fn lookup(&self, sym: StringId, span: rnix::TextRange) -> Result { + fn lookup(&self, sym: StringId, span: rnix::TextRange) -> Result> { for scope in self.scopes.iter().rev() { match scope { &Scope::Global(global_scope) => { @@ -408,22 +401,26 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> Ir::Null => Null, _ => unreachable!("globals should only contain leaf IR nodes"), }; - return Ok(val); + return Ok(self.bump.alloc(GhostCell::new(val))); } } &Scope::Repl(repl_bindings) => { if repl_bindings.contains(&sym) { - return Ok(MaybeThunk::ReplBinding(sym)); + return Ok(self + .bump + .alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)))); } } Scope::ScopedImport(scoped_bindings) => { if scoped_bindings.contains(&sym) { - return Ok(MaybeThunk::ScopedImportBinding(sym)); + return Ok(self + .bump + .alloc(GhostCell::new(MaybeThunk::ScopedImportBinding(sym)))); } } Scope::Let(let_scope) => { if let Some(&expr) = let_scope.get(&sym) { - return Ok(MaybeThunk::Thunk(expr)); + return Ok(expr); } } &Scope::Param { @@ -434,14 +431,14 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> let layers: u8 = self.thunk_scopes.len().try_into().expect("scope too deep!"); let layer = layers - abs_layer; - return Ok(MaybeThunk::Arg { layer }); + return Ok(self.bump.alloc(GhostCell::new(MaybeThunk::Arg { layer }))); } } } } if self.with_scope_count > 0 { - Ok(MaybeThunk::WithLookup(sym)) + Ok(self.bump.alloc(GhostCell::new(MaybeThunk::WithLookup(sym)))) } else { Err(Error::downgrade_error( format!("'{}' not found", self.resolve_sym(sym)), @@ -464,11 +461,14 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> .thunk_count .checked_add(keys.len()) .expect("thunk id overflow"); - let iter = keys - .iter() - .enumerate() - .map(|(offset, &key)| (key, ThunkId(base + offset))); - self.scopes.push(Scope::Let(iter.collect())); + let scope = { + let mut scope = HashMap::new(); + for (offset, &key) in keys.iter().enumerate() { + scope.insert(key, &*self.bump.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(base + offset))))); + } + scope + }; + self.scopes.push(Scope::Let(scope)); let (vals, ret) = { let mut guard = ScopeGuard { ctx: self }; f(guard.as_ctx())? @@ -538,8 +538,10 @@ impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> { .pop() .expect("no thunk scope left???") .bindings; - let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks }); - Ok(ir.freeze(self.token)) + Ok(Ir::freeze( + self.new_expr(Ir::TopLevel { body, thunks }), + self.token, + )) } } @@ -563,14 +565,29 @@ impl<'id, 'ir> ThunkScope<'id, 'ir> { } } -enum Scope<'ctx> { - Global(&'ctx HashMap>>), +enum Scope<'ctx, 'id, 'ir> { + Global(&'ctx HashMap>>), Repl(&'ctx HashSet), ScopedImport(HashSet), - Let(HashMap), + Let(HashMap>), Param { sym: StringId, abs_layer: u8 }, } +enum ExtraScope<'ctx> { + Repl(&'ctx HashSet), + ScopedImport(HashSet), +} + +impl<'ctx> From> for Scope<'ctx, '_, '_> { + fn from(value: ExtraScope<'ctx>) -> Self { + use ExtraScope::*; + match value { + ScopedImport(scope) => Scope::ScopedImport(scope), + Repl(scope) => Scope::Repl(scope), + } + } +} + struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> { ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>, } @@ -593,15 +610,31 @@ struct OwnedIr { } impl OwnedIr { + /// # Safety + /// `ir` must be an allocation backed by `bump`. The reference's + /// lifetime is extended to `'static` as a placeholder; the stored IR + /// must only be re-borrowed via [`OwnedIr::as_ref`], which narrows + /// the lifetime back to that of the `&self` borrow. Moving `bump` + /// into the struct keeps the underlying allocation live for the + /// lifetime of the `OwnedIr`. unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self { Self { _bump: bump, + // SAFETY: see function docs - caller guarantees `ir` is in `bump`, + // and the `'static` lifetime is a placeholder narrowed by `as_ref`. ir: unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) }, } } - fn as_ref(&self) -> RawIrRef<'_> { - self.ir + fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> { + // SAFETY: narrows the placeholder `'static` lifetime stored in + // `self.ir` down to `'ir = &'ir self`. Lifetime shortening is + // logically sound for covariant positions; the transmute is only + // needed because `RawRef<'ir>` carries `'ir` through a GAT + // (`Ref::Ref`), which prevents the compiler from inferring + // covariance automatically. The bump arena that backs the IR is + // owned by `self._bump`, so the data is live for at least `'ir`. + unsafe { std::mem::transmute::, RawIrRef<'ir>>(self.ir) } } }