use bumpalo::Bump; use fix_bytecode::{Const, InstructionPtr, Op, PrimOpPhase}; use fix_error::{Error, Result, Source}; use fix_lang::{StringId, Symbol}; use fix_runtime::{StaticValue, VmCode, VmRuntimeCtx}; use ghost_cell::{GhostCell, GhostToken}; use hashbrown::{HashMap, HashSet}; use string_interner::DefaultStringInterner; use crate::BytecodeContext; use crate::ir::downgrade::{Downgrade as _, DowngradeContext}; use crate::ir::{ GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef, ThunkId, }; pub struct CodeState { pub bytecode: Vec, pub sources: Vec, pub spans: Vec<(usize, rnix::TextRange)>, pub thunk_count: usize, pub global_env: HashMap, } impl CodeState { pub fn new(strings: &mut DefaultStringInterner) -> Self { let global_env = crate::ir::new_global_env(strings); let mut bytecode = Vec::with_capacity(PrimOpPhase::Illegal as usize * 2); for phase in 0..=PrimOpPhase::Illegal as u8 { bytecode.push(Op::DispatchPrimOp as u8); bytecode.push(phase); } Self { sources: Vec::new(), spans: Vec::new(), thunk_count: 0, bytecode, global_env, } } pub fn compile_bytecode<'ctx>( &'ctx mut self, source: Source, extra_scope: Option>, runtime: &'ctx mut impl VmRuntimeCtx, ) -> Result { let mut compiler = CompilerCtx { code: self, runtime, }; compiler.compile_bytecode(source, extra_scope) } } impl VmCode for CodeState { fn bytecode(&self) -> &[u8] { &self.bytecode } fn compile_with_scope( &mut self, source: Source, extra_scope: Option, runtime: &mut impl VmRuntimeCtx, ) -> Result { let extra = extra_scope.map(|s| match s { fix_runtime::ExtraScope::ScopedImport { keys, slot_id } => { ExtraScope::ScopedImport { keys, slot_id } } }); CodeState::compile_bytecode(self, source, extra, runtime) } } struct CompilerCtx<'a, R: VmRuntimeCtx> { code: &'a mut CodeState, runtime: &'a mut R, } impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> { fn compile_bytecode( &mut self, source: Source, extra_scope: Option, ) -> Result { let root = self.downgrade(source, extra_scope)?; let ip = crate::compile_bytecode(root.as_ref(), self); Ok(ip) } fn downgrade(&mut self, source: Source, extra_scope: Option) -> Result { tracing::debug!("Parsing Nix expression"); self.code.sources.push(source.clone()); let root = rnix::Root::parse(&source.src); handle_parse_error(root.errors(), source.clone()).map_or(Ok(()), Err)?; tracing::debug!("Downgrading Nix expression"); let expr = root .tree() .expr() .ok_or_else(|| Error::parse_error("unexpected EOF".into()))?; let bump = Bump::new(); GhostToken::new(|token| { let downgrade_ctx = DowngradeCtx::new( &bump, token, self.runtime, &self.code.global_env, extra_scope.map(Into::into), &mut self.code.thunk_count, source, ); let ir = downgrade_ctx.downgrade_toplevel(expr)?; let ir = unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) }; Ok(OwnedIr { _bump: bump, ir }) }) } } impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> { fn intern_string(&mut self, s: &str) -> StringId { self.runtime.intern_string(s) } fn register_span(&mut self, range: rnix::TextRange) -> u32 { let id = self.code.spans.len(); let source_id = self .code .sources .len() .checked_sub(1) .expect("current_source not set"); self.code.spans.push((source_id, range)); id as u32 } fn get_code(&self) -> &[u8] { &self.code.bytecode } fn get_code_mut(&mut self) -> &mut Vec { &mut self.code.bytecode } fn add_constant(&mut self, val: Const) -> u32 { use Const::*; let val = match val { Smi(x) => StaticValue::new_inline(x), Float(x) => StaticValue::new_float(x), Bool(x) => StaticValue::new_inline(x), String(x) => StaticValue::new_inline(x), Path(x) => StaticValue::new_inline(fix_runtime::Path(x)), PrimOp { id, arity, dispatch_ip, } => StaticValue::new_primop(id, arity, dispatch_ip), Null => StaticValue::default(), }; self.runtime.add_const(val) } fn current_source_dir(&mut self) -> StringId { let dir = self .code .sources .last() .expect("current_source not set") .get_dir() .to_string_lossy() .into_owned(); self.runtime.intern_string(dir) } } fn parse_error_span(error: &rnix::ParseError) -> Option { use rnix::ParseError::*; match error { Unexpected(range) | UnexpectedExtra(range) | UnexpectedWanted(_, range, _) | UnexpectedDoubleBind(range) | DuplicatedArgs(range, _) => Some(*range), _ => None, } } fn handle_parse_error<'a>( errors: impl IntoIterator, source: Source, ) -> Option> { for err in errors { if let Some(span) = parse_error_span(err) { return Some( Error::parse_error(err.to_string()) .with_source(source) .with_span(span), ); } } None } struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> { bump: &'ir Bump, token: GhostToken<'id>, runtime: &'ctx mut R, source: Source, scopes: Vec>, with_stack: Vec>, thunk_count: &'ctx mut usize, thunk_scopes: Vec>, } 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>, thunk_count: &'ctx mut usize, source: Source, ) -> Self { Self { bump, token, runtime, source, scopes: std::iter::once(Scope::Global(global)) .chain(extra_scope) .collect(), thunk_count, with_stack: Vec::new(), thunk_scopes: vec![ThunkScope::new_in(bump)], } } } impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir, R> { fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> { self.bump.alloc(GhostCell::new(expr).into()) } fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> { use MaybeThunk::*; 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).into())) })(); 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"); self.thunk_scopes .last_mut() .expect("no active cache scope") .add_binding(id, ir); self.bump.alloc(GhostCell::new(Thunk(id)).into()) } fn intern_string(&mut self, sym: impl AsRef) -> StringId { self.runtime.intern_string(sym) } fn resolve_sym(&self, id: StringId) -> Symbol<'_> { self.runtime.resolve_string(id).into() } fn lookup( &mut self, sym: StringId, span: rnix::TextRange, ) -> Result> { for scope in self.scopes.iter().rev() { match scope { &Scope::Global(global_scope) => { if let Some(expr) = global_scope.get(&sym) { return Ok(expr.into()); } } &Scope::Repl(repl_bindings) => { if repl_bindings.contains(&sym) { return Ok(self .bump .alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into())); } } &Scope::ScopedImport { ref keys, slot_id } => { if keys.contains(&sym) { return Ok(self.bump.alloc( GhostCell::new(MaybeThunk::ScopedImportBinding { sym, slot_id }).into(), )); } } Scope::Let(let_scope) => { if let Some(&expr) = let_scope.get(&sym) { return Ok(expr.into()); } } &Scope::Param { sym: param_sym, abs_layer, } => { if param_sym == sym { let layers: u8 = self.thunk_scopes.len().try_into().expect("scope too deep!"); let layer = layers - abs_layer; return Ok(self .bump .alloc(GhostCell::new(MaybeThunk::Arg { layer }).into())); } } } } if !self.with_stack.is_empty() { let id = ThunkId(*self.thunk_count); *self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow"); let mut namespaces = bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump); namespaces.extend(self.with_stack.iter().rev().copied()); let body = self .bump .alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into()); self.thunk_scopes .last_mut() .expect("no active thunk scope") .add_binding(id, body); Ok(self .bump .alloc(GhostCell::new(MaybeThunk::Thunk(id)).into())) } else { Err(Error::downgrade_error( format!("'{}' not found", self.resolve_sym(sym)), self.get_current_source(), span, )) } } fn get_current_source(&self) -> Source { self.source.clone() } fn with_let_scope(&mut self, keys: &[StringId], f: F) -> Result where F: FnOnce( &mut Self, ) -> Result<( bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>, Ret, )>, { let base = *self.thunk_count; *self.thunk_count = self .thunk_count .checked_add(keys.len()) .expect("thunk id overflow"); let handles = (base..base + keys.len()) .map(|id| { &*self .bump .alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id)))) }) .collect::>(); let scope = keys.iter().copied().zip(handles.iter().copied()).collect(); self.scopes.push(Scope::Let(scope)); let (vals, ret) = { f(self)? }; self.scopes.pop(); assert_eq!(keys.len(), vals.len()); let scope = self.thunk_scopes.last_mut().expect("no active thunk scope"); for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() { let thunk = *val.borrow(&self.token); *handle.borrow_mut(&mut self.token) = thunk; let id = ThunkId(base + i); let ir_ref = self .bump .alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into()); scope.add_binding(id, ir_ref); } Ok(ret) } fn with_param_scope(&mut self, sym: StringId, f: F) -> Ret where F: FnOnce(&mut Self) -> Ret, { self.scopes.push(Scope::Param { sym, abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"), }); let mut guard = ScopeGuard { ctx: self }; f(guard.as_ctx()) } fn with_with_scope(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret where F: FnOnce(&mut Self) -> Ret, { self.with_stack.push(namespace); let ret = f(self); self.with_stack.pop(); ret } fn with_thunk_scope( &mut self, f: F, ) -> ( Ret, bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>, ) where F: FnOnce(&mut Self) -> Ret, { if self.thunk_scopes.len() == u8::MAX as usize { panic!("scope too deep!"); } self.thunk_scopes.push(ThunkScope::new_in(self.bump)); let ret = f(self); ( ret, self.thunk_scopes .pop() .expect("no thunk scope left???") .bindings, ) } fn bump(&self) -> &'ir bumpalo::Bump { self.bump } } impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> { fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result> { let body = root.downgrade(&mut self)?; let thunks = self .thunk_scopes .pop() .expect("no thunk scope left???") .bindings; Ok(Ir::freeze( self.new_expr(Ir::TopLevel { body, thunks }), self.token, )) } } struct ThunkScope<'id, 'ir> { bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>, } impl<'id, 'ir> ThunkScope<'id, 'ir> { fn new_in(bump: &'ir Bump) -> Self { Self { bindings: bumpalo::collections::Vec::new_in(bump), } } fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) { self.bindings.push((id, ir)); } } enum Scope<'ctx, 'id, 'ir> { Global(&'ctx HashMap), Repl(&'ctx HashSet), ScopedImport { keys: HashSet, #[allow(dead_code)] slot_id: u32, }, Let(HashMap>), Param { sym: StringId, abs_layer: u8, }, } pub enum ExtraScope<'ctx> { Repl(&'ctx HashSet), ScopedImport { keys: HashSet, slot_id: u32, }, } impl<'ctx> From> for Scope<'ctx, '_, '_> { fn from(value: ExtraScope<'ctx>) -> Self { use ExtraScope::*; match value { ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id }, Repl(scope) => Scope::Repl(scope), } } } struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> { ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>, } impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> { fn drop(&mut self) { self.ctx.scopes.pop(); } } impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> { fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> { self.ctx } } struct OwnedIr { _bump: Bump, ir: RawIrRef<'static>, } impl OwnedIr { fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> { unsafe { std::mem::transmute::, RawIrRef<'ir>>(self.ir) } } }