diff --git a/biome.json b/biome.json deleted file mode 100644 index 3ca5744..0000000 --- a/biome.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "includes": ["**", "!!**/dist"] - }, - "formatter": { - "enabled": true, - "formatWithErrors": true, - "attributePosition": "auto", - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 110, - "lineEnding": "lf" - }, - "linter": { - "rules": { - "style": { - "useNamingConvention": { - "level": "warn", - "options": { - "strictCase": false, - "conventions": [ - { - "selector": { "kind": "objectLiteralProperty" }, - "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] - }, - { - "selector": { "kind": "typeProperty" }, - "formats": ["camelCase", "snake_case"] - } - ] - } - } - } - } - }, - "overrides": [ - { - "includes": ["**/global.d.ts"], - "linter": { - "rules": { - "style": { - "useNamingConvention": "off" - } - } - } - } - ], - "javascript": { - "formatter": { - "arrowParentheses": "always", - "bracketSameLine": false, - "bracketSpacing": true, - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "semicolons": "always", - "trailingCommas": "all" - } - }, - "json": { - "formatter": { - "trailingCommas": "none" - } - } -} diff --git a/fix-builtins/src/lib.rs b/fix-builtins/src/lib.rs index c9bef75..8d649ba 100644 --- a/fix-builtins/src/lib.rs +++ b/fix-builtins/src/lib.rs @@ -247,6 +247,9 @@ pub enum PrimOpPhase { CallFunctor1, CallFunctor2, + ImportFinalize, + ScopedImportFinalize, + Illegal, } diff --git a/fix-codegen/src/disassembler.rs b/fix-codegen/src/disassembler.rs index 62d3629..7ce53fc 100644 --- a/fix-codegen/src/disassembler.rs +++ b/fix-codegen/src/disassembler.rs @@ -98,7 +98,11 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { self.read_u32(); } Builtins => {} - ReplBinding | ScopedImportBinding => { + ReplBinding => { + self.read_u32(); + } + ScopedImportBinding => { + self.read_u32(); self.read_u32(); } } @@ -420,7 +424,11 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { ("ConcatStrings", format!("count={} force={}", count, force)) } Op::CoerceToString => ("CoerceToString", String::new()), - Op::ResolvePath => ("ResolvePath", String::new()), + Op::ResolvePath => { + let dir_id = self.read_u32(); + let dir = self.ctx.resolve_string(dir_id); + ("ResolvePath", format!("dir={:?}", dir)) + } Op::Assert => { let raw_idx = self.read_u32(); let span_id = self.read_u32(); @@ -447,9 +455,10 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { ("LoadReplBinding", format!("{:?}", name)) } Op::LoadScopedBinding => { + let slot = self.read_u32(); let idx = self.read_u32(); let name = self.ctx.resolve_string(idx); - ("LoadScopedBinding", format!("{:?}", name)) + ("LoadScopedBinding", format!("slot={} {:?}", slot, name)) } Op::Return => ("Return", String::new()), Op::Illegal => ("Illegal", String::new()), diff --git a/fix-codegen/src/lib.rs b/fix-codegen/src/lib.rs index 8463128..9eb57fc 100644 --- a/fix-codegen/src/lib.rs +++ b/fix-codegen/src/lib.rs @@ -16,6 +16,8 @@ pub trait BytecodeContext { fn get_code(&self) -> &[u8]; fn get_code_mut(&mut self) -> &mut Vec; fn add_constant(&mut self, val: Const) -> u32; + fn current_source_dir(&mut self) -> StringId; + fn current_scope_slot(&self) -> Option; } #[repr(u8)] @@ -232,6 +234,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } ScopedImportBinding(id) => { self.emit_u8(OperandType::ScopedImportBinding as u8); + let slot = self + .ctx + .current_scope_slot() + .expect("ScopedImportBinding outside scoped compilation"); + self.emit_u32(slot); self.emit_str_id(id); } } @@ -423,6 +430,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { &Ir::Path(p) => { self.emit_expr(p); self.emit_op(Op::ResolvePath); + let dir_id = self.ctx.current_source_dir(); + self.emit_str_id(dir_id); } &Ir::If { cond, consq, alter } => { self.emit_expr(cond); @@ -548,6 +557,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } &Ir::ScopedImportBinding(name) => { self.emit_op(Op::LoadScopedBinding); + let slot = self + .ctx + .current_scope_slot() + .expect("ScopedImportBinding outside scoped compilation"); + self.emit_u32(slot); self.emit_str_id(name); } Ir::WithLookup { sym, namespaces } => { @@ -592,6 +606,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::PushString); self.emit_str_id(id); self.emit_op(Op::ResolvePath); + let dir_id = self.ctx.current_source_dir(); + self.emit_str_id(dir_id); } Thunk(id) => { let (layer, local) = self.resolve_thunk(id); @@ -615,6 +631,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { } ScopedImportBinding(name) => { self.emit_op(Op::LoadScopedBinding); + let slot = self + .ctx + .current_scope_slot() + .expect("ScopedImportBinding outside scoped compilation"); + self.emit_u32(slot); self.emit_str_id(name); } } diff --git a/fix-vm/src/bytecode_reader.rs b/fix-vm/src/bytecode_reader.rs index fe4de5d..84c274c 100644 --- a/fix-vm/src/bytecode_reader.rs +++ b/fix-vm/src/bytecode_reader.rs @@ -122,8 +122,9 @@ impl<'a> BytecodeReader<'a> { OperandData::ReplBinding(id) } OperandType::ScopedImportBinding => { - let id = self.read_string_id(); - OperandData::ScopedImportBinding(id) + let slot_id = self.read_u32(); + let name = self.read_string_id(); + OperandData::ScopedImportBinding { slot_id, name } } } } diff --git a/fix-vm/src/dispatch_tailcall.rs b/fix-vm/src/dispatch_tailcall.rs index ff60bc4..50a9113 100644 --- a/fix-vm/src/dispatch_tailcall.rs +++ b/fix-vm/src/dispatch_tailcall.rs @@ -7,6 +7,7 @@ use crate::{Break, BytecodeReader, Step, Vm, VmRuntimeCtx}; pub(crate) enum TailResult { YieldFuel(u32), Done, + LoadFile, } pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn( @@ -37,6 +38,7 @@ macro_rules! tail_dispatch_after { ($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{ match $result { Step::Continue(()) | Step::Break(Break::Force) => {} + Step::Break(Break::LoadFile) => return TailResult::LoadFile, Step::Break(Break::Done) => return TailResult::Done, } let new_pc: u32 = $new_pc; @@ -183,7 +185,7 @@ tail_fn!(op_jump, (reader)); tail_fn!(op_coerce_to_string, (reader, mc)); tail_fn!(op_concat_strings, (ctx, reader, mc)); -tail_fn!(op_resolve_path, (ctx)); +tail_fn!(op_resolve_path, (ctx, reader, mc)); tail_fn!(op_assert, (ctx, reader, mc)); @@ -193,7 +195,7 @@ tail_fn!(op_load_builtins, ()); tail_fn!(op_load_builtin, (reader)); tail_fn!(op_load_repl_binding, (reader)); -tail_fn!(op_load_scoped_binding, (reader)); +tail_fn!(op_load_scoped_binding, (ctx, reader, mc)); macro_rules! table { ($($variant:ident => $fn:ident),* $(,)?) => { diff --git a/fix-vm/src/instructions/misc.rs b/fix-vm/src/instructions/misc.rs index c3c246c..59895b8 100644 --- a/fix-vm/src/instructions/misc.rs +++ b/fix-vm/src/instructions/misc.rs @@ -1,9 +1,12 @@ +use std::path::{Component, PathBuf}; + use fix_builtins::BuiltinId; use fix_common::StringId; +use fix_error::Error; use num_enum::TryFromPrimitive; -use crate::value::{NixString, StrictValue}; -use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx}; +use crate::value::{AttrSet, NixString, StrictValue}; +use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt}; impl<'gc> crate::Vm<'gc> { #[inline(always)] @@ -31,9 +34,37 @@ impl<'gc> crate::Vm<'gc> { } #[inline(always)] - pub(crate) fn op_load_scoped_binding(&mut self, reader: &mut BytecodeReader<'_>) -> Step { - let _name = reader.read_string_id(); - todo!("LoadScopedBinding"); + pub(crate) fn op_load_scoped_binding( + &mut self, + ctx: &impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + _mc: &gc_arena::Mutation<'gc>, + ) -> Step { + let slot_id = reader.read_u32(); + let name = reader.read_string_id(); + let scope = match self.scope_slots.get(slot_id as usize).copied() { + Some(s) => s, + None => { + return self.finish_err(Error::eval_error(format!( + "internal: invalid scope slot {slot_id}" + ))); + } + }; + let Some(attrs) = scope.as_gc::() else { + return self.finish_err(Error::eval_error( + "internal: scope slot is not an attrset", + )); + }; + match attrs.lookup(name) { + Some(val) => { + self.push(val); + Step::Continue(()) + } + None => self.finish_err(Error::eval_error(format!( + "scoped binding '{}' not found", + ctx.resolve_string(name) + ))), + } } #[inline(always)] @@ -58,8 +89,6 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, _mc: &gc_arena::Mutation<'gc>, ) -> Step { - use crate::VmRuntimeCtxExt; - let count = reader.read_u16() as usize; let _force_string = reader.read_u8() != 0; @@ -85,7 +114,64 @@ impl<'gc> crate::Vm<'gc> { } #[inline(always)] - pub(crate) fn op_resolve_path(&mut self, _ctx: &mut impl VmRuntimeCtx) -> Step { - todo!("implement ResolvePath"); + pub(crate) fn op_resolve_path( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &gc_arena::Mutation<'gc>, + ) -> Step { + let path_val = self.force_and_retry::(reader, mc)?; + let dir_id = reader.read_string_id(); + let path = match ctx.get_string(path_val) { + Some(s) => s.to_owned(), + None => { + return self.finish_err(Error::eval_error(format!( + "expected a string for path, got {}", + path_val.ty() + ))); + } + }; + let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) { + Ok(s) => s, + Err(e) => return self.finish_err(e), + }; + let sid = ctx.intern_string(resolved); + self.push(Value::new_inline(sid)); + Step::Continue(()) } } + +/// Resolve a Nix path literal against `current_dir`. +/// +/// Mirrors nix-js's `op_resolve_path`: absolute paths returned as-is, `~/X` +/// expanded against `$HOME`, otherwise joined onto `current_dir`. The result +/// is normalized by removing `.` components and resolving `..` lexically +/// (no symlink resolution). +fn resolve_path_str(current_dir: &str, path: &str) -> Result> { + let raw = if path.starts_with('/') { + return Ok(path.to_owned()); + } else if let Some(rest) = path.strip_prefix("~/") { + #[allow(deprecated)] + let mut dir = std::env::home_dir() + .ok_or_else(|| Error::eval_error("home dir not defined"))?; + dir.push(rest); + dir + } else { + let mut dir = PathBuf::from(current_dir); + dir.push(path); + dir + }; + let mut normalized = PathBuf::new(); + for component in raw.components() { + match component { + Component::Prefix(p) => normalized.push(p.as_os_str()), + Component::RootDir => normalized.push("/"), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(c) => normalized.push(c), + } + } + Ok(normalized.to_string_lossy().into_owned()) +} diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 61b4b4c..793da57 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -77,13 +77,25 @@ pub trait VmRuntimeCtx { pub trait VmCode { fn bytecode(&self) -> &[u8]; - fn compile( + fn compile_with_scope( &mut self, source: Source, + extra_scope: Option, ctx: &mut impl VmRuntimeCtx, ) -> fix_error::Result; } +/// Extra scope passed to a re-entrant compile from inside a running VM. +/// +/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin), +/// but the variant is kept open so REPL bindings could later land here too. +pub enum ExtraScope { + ScopedImport { + keys: HashSet, + slot_id: u32, + }, +} + trait VmRuntimeCtxExt: VmRuntimeCtx { fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; fn get_string_id<'a, 'gc: 'a>( @@ -197,6 +209,7 @@ impl ConvertValueWithSeen for T { enum Break { Force, Done, + LoadFile, } type Step = std::ops::ControlFlow; @@ -214,6 +227,7 @@ pub struct Vm<'gc> { env: GcEnv<'gc>, import_cache: HashMap>, + scope_slots: Vec>, builtins: Value<'gc>, empty_list: Value<'gc>, @@ -224,9 +238,24 @@ pub struct Vm<'gc> { #[collect(require_static)] result: Option>, + #[collect(require_static)] + pending_load: Option, + functor_sym: StringId, } +#[derive(Debug)] +pub(crate) struct PendingLoad { + pub path: PathBuf, + pub scope: Option, +} + +#[derive(Debug)] +pub(crate) struct PendingScope { + pub keys: HashSet, + pub slot_id: u32, +} + enum OperandData { Const(StaticValue), BigInt(i64), @@ -234,7 +263,7 @@ enum OperandData { BuiltinConst(StringId), Builtins, ReplBinding(StringId), - ScopedImportBinding(StringId), + ScopedImportBinding { slot_id: u32, name: StringId }, } impl OperandData { @@ -260,7 +289,17 @@ impl OperandData { .unwrap(), Builtins => root.builtins, ReplBinding(_id) => todo!(), - ScopedImportBinding(_id) => todo!(), + ScopedImportBinding { slot_id, name } => { + #[allow(clippy::unwrap_used)] + let scope = root + .scope_slots + .get(slot_id as usize) + .expect("invalid scope slot"); + #[allow(clippy::unwrap_used)] + let attrs = scope.as_gc::().expect("scope must be attrset"); + #[allow(clippy::unwrap_used)] + attrs.lookup(name).expect("scoped binding not found") + } } } } @@ -334,6 +373,7 @@ impl<'gc> Vm<'gc> { env: Gc::new(mc, RefLock::new(Env::empty())), import_cache: HashMap::new(), + scope_slots: Vec::new(), builtins, empty_list: Value::new_gc(Gc::new(mc, List::default())), @@ -342,6 +382,7 @@ impl<'gc> Vm<'gc> { force_mode, result: None, + pending_load: None, functor_sym: ctx.intern_string("__functor"), } @@ -541,6 +582,7 @@ struct CallFrame<'gc> { enum Action { Continue { pc: usize }, Done(Result), + LoadFile(PendingLoad), } enum NixNum { @@ -573,6 +615,24 @@ impl Vm<'_> { } } } + Action::LoadFile(load) => { + let source = match Source::new_file(load.path) { + Ok(src) => src, + Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))), + }; + let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport { + keys: s.keys, + slot_id: s.slot_id, + }); + let new_ip = match code.compile_with_scope(source, extra_scope, runtime) { + Ok(ip) => ip, + Err(err) => break Err(err), + }; + pc = new_ip.0; + arena.mutate_root(|mc, root| { + root.env = Gc::new(mc, RefLock::new(Env::empty())); + }); + } Action::Done(done) => break done, } } @@ -604,6 +664,11 @@ impl<'gc> Vm<'gc> { TailResult::Done => { Action::Done(self.result.take().expect("TailResult::Done without result")) } + TailResult::LoadFile => Action::LoadFile( + self.pending_load + .take() + .expect("TailResult::LoadFile without pending_load"), + ), } } } @@ -689,7 +754,7 @@ impl<'gc> Vm<'gc> { ConcatStrings => self.op_concat_strings(ctx, &mut reader, mc), CoerceToString => self.op_coerce_to_string(&mut reader, mc), - ResolvePath => self.op_resolve_path(ctx), + ResolvePath => self.op_resolve_path(ctx, &mut reader, mc), Assert => self.op_assert(ctx, &mut reader, mc), @@ -699,7 +764,7 @@ impl<'gc> Vm<'gc> { LoadBuiltin => self.op_load_builtin(&mut reader), LoadReplBinding => self.op_load_repl_binding(&mut reader), - LoadScopedBinding => self.op_load_scoped_binding(&mut reader), + LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader, mc), Illegal => unreachable!(), }; @@ -709,6 +774,13 @@ impl<'gc> Vm<'gc> { Step::Break(Break::Done) => { return Action::Done(self.result.take().expect("Break::Done without result")); } + Step::Break(Break::LoadFile) => { + return Action::LoadFile( + self.pending_load + .take() + .expect("Break::LoadFile without pending_load"), + ); + } } } } diff --git a/fix-vm/src/primops/io.rs b/fix-vm/src/primops/io.rs index 8b13789..621c5a2 100644 --- a/fix-vm/src/primops/io.rs +++ b/fix-vm/src/primops/io.rs @@ -1 +1,181 @@ +use std::path::PathBuf; +use fix_builtins::PrimOpPhase; +use fix_common::StringId; +use fix_error::Error; +use gc_arena::{Gc, Mutation}; +use hashbrown::HashSet; + +use crate::bytecode_reader::BytecodeReader; +use crate::value::*; +use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; + +impl<'gc> Vm<'gc> { + pub(crate) fn primop_import( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> Step { + // stack: [path] + let path_val = self.force_and_retry::(reader, mc)?; + let path_str = match ctx.get_string(path_val) { + Some(s) => s.to_owned(), + None => { + return self.finish_err(Error::eval_error(format!( + "expected a path string, got {}", + path_val.ty() + ))); + } + }; + let abs = match resolve_import_target(&path_str) { + Ok(p) => p, + Err(e) => return self.finish_err(e), + }; + + if let Some(&cached) = self.import_cache.get(&abs) { + return self.return_from_primop(cached, reader); + } + + // Stash the resolved path on the stack as a string-id so the + // finalizer can use it as the cache key. The slot we pop here was + // freed by `force_and_retry`, so we simply push. + let path_sid = ctx.intern_string(abs.to_string_lossy()); + self.push(Value::new_inline(path_sid)); + self.call_stack.push(CallFrame { + pc: PrimOpPhase::ImportFinalize.ip() as usize, + thunk: None, + env: self.env, + }); + + self.pending_load = Some(PendingLoad { + path: abs, + scope: None, + }); + Step::Break(Break::LoadFile) + } + + pub(crate) fn primop_import_finalize( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + ) -> Step { + // stack: [path_sid, return_value] + let val = self.pop(); + #[allow(clippy::unwrap_used)] + let path_sid = self.pop().as_inline::().unwrap(); + // The cache key is keyed by the absolute path string we interned in + // `primop_import`. Resolve it back to the host PathBuf. + let path_str = ctx.resolve_string(path_sid).to_owned(); + self.import_cache.insert(PathBuf::from(path_str), val); + self.push(val); + let Some(CallFrame { + pc: ret_pc, + thunk: _, + env, + }) = self.call_stack.pop() + else { + unreachable!() + }; + reader.set_pc(ret_pc); + // FIXME: + // self.call_depth -= 1; + self.env = env; + Step::Continue(()) + } + + pub(crate) fn primop_scoped_import( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> Step { + // stack: [scope, path] + let (scope_attrs, path_val) = + self.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; + let path_str = match ctx.get_string(path_val) { + Some(s) => s.to_owned(), + None => { + return self.finish_err(Error::eval_error(format!( + "expected a path string, got {}", + path_val.ty() + ))); + } + }; + let abs = match resolve_import_target(&path_str) { + Ok(p) => p, + Err(e) => return self.finish_err(e), + }; + + let keys: HashSet = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); + let slot_id = self.scope_slots.len() as u32; + self.scope_slots.push(Value::new_gc(scope_attrs)); + + self.call_stack.push(CallFrame { + pc: PrimOpPhase::ScopedImportFinalize.ip() as usize, + thunk: None, + env: self.env, + }); + + self.pending_load = Some(PendingLoad { + path: abs, + scope: Some(PendingScope { keys, slot_id }), + }); + Step::Break(Break::LoadFile) + } + + pub(crate) fn primop_scoped_import_finalize( + &mut self, + _ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, + ) -> Step { + // stack: [return_value] + // We intentionally do NOT pop the slot from `scope_slots` so that + // closures or thunks created inside the imported file can still + // resolve their scope after `scopedImport` returns. + let val = self.pop(); + self.return_from_primop(val, reader) + } + + pub(crate) fn primop_path_exists( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> Step { + let path_val = self.force_and_retry::(reader, mc)?; + let path = match ctx.get_string(path_val) { + Some(s) => s.to_owned(), + None => { + return self.finish_err(Error::eval_error(format!( + "expected a path string, got {}", + path_val.ty() + ))); + } + }; + let must_be_dir = path.ends_with('/') || path.ends_with("/."); + let p = std::path::Path::new(&path); + let exists = if must_be_dir { + std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false) + } else { + std::fs::symlink_metadata(p).is_ok() + }; + self.return_from_primop(Value::new_inline(exists), reader) + } +} + +/// Convert the user-supplied path string into an absolute, dotted-segment +/// resolved `PathBuf` and append `default.nix` if the target is a directory. +fn resolve_import_target(path: &str) -> Result> { + let mut abs = PathBuf::from(path); + if !abs.is_absolute() { + return Err(Error::eval_error(format!( + "import: expected an absolute path, got '{path}'" + ))); + } + if abs.is_dir() { + abs.push("default.nix"); + } + Ok(abs) +} diff --git a/fix-vm/src/primops/mod.rs b/fix-vm/src/primops/mod.rs index b4295aa..a0689d8 100644 --- a/fix-vm/src/primops/mod.rs +++ b/fix-vm/src/primops/mod.rs @@ -50,10 +50,18 @@ impl<'gc> Vm<'gc> { CallFunctor1 => self.primop_call_functor_1(reader, mc), CallFunctor2 => self.primop_call_functor_2(reader, mc), + Import => self.primop_import(ctx, reader, mc), + ImportFinalize => self.primop_import_finalize(ctx, reader), + ScopedImport => self.primop_scoped_import(ctx, reader, mc), + ScopedImportFinalize => self.primop_scoped_import_finalize(ctx, reader, mc), + + PathExists => self.primop_path_exists(ctx, reader, mc), + phase => todo!("primop phase {phase:?}"), } } + #[inline(always)] fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step { self.push(val); let Some(CallFrame { diff --git a/fix/src/lib.rs b/fix/src/lib.rs index 06755a7..80cb3a5 100644 --- a/fix/src/lib.rs +++ b/fix/src/lib.rs @@ -34,6 +34,7 @@ pub struct CodeState { pub spans: Vec<(usize, rnix::TextRange)>, pub thunk_count: usize, pub global_env: HashMap, + pub current_scope_slot: Option, } pub struct Evaluator { @@ -67,6 +68,7 @@ impl Evaluator { thunk_count: 0, bytecode, global_env, + current_scope_slot: None, }, } } @@ -150,16 +152,22 @@ impl VmCode for CodeState { fn bytecode(&self) -> &[u8] { &self.bytecode } - fn compile( + fn compile_with_scope( &mut self, source: Source, + extra_scope: Option, runtime: &mut impl VmRuntimeCtx, ) -> Result { let mut compiler = CompilerCtx { code: self, runtime, }; - compiler.compile_bytecode(source, None) + let extra = extra_scope.map(|s| match s { + fix_vm::ExtraScope::ScopedImport { keys, slot_id } => { + ExtraScope::ScopedImport { keys, slot_id } + } + }); + compiler.compile_bytecode(source, extra) } } @@ -180,9 +188,18 @@ impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> { source: Source, extra_scope: Option, ) -> Result { - let root = self.downgrade(source, extra_scope)?; - let ip = fix_codegen::compile_bytecode(root.as_ref(), self); - Ok(ip) + let prev_scope_slot = self.code.current_scope_slot; + self.code.current_scope_slot = match &extra_scope { + Some(ExtraScope::ScopedImport { slot_id, .. }) => Some(*slot_id), + _ => None, + }; + let result = (|| -> Result { + let root = self.downgrade(source, extra_scope)?; + let ip = fix_codegen::compile_bytecode(root.as_ref(), self); + Ok(ip) + })(); + self.code.current_scope_slot = prev_scope_slot; + result } fn downgrade(&mut self, source: Source, extra_scope: Option) -> Result { @@ -258,6 +275,22 @@ impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> { }; 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 current_scope_slot(&self) -> Option { + self.code.current_scope_slot + } } #[derive(Default)] @@ -410,8 +443,8 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir> .alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into())); } } - Scope::ScopedImport(scoped_bindings) => { - if scoped_bindings.contains(&sym) { + Scope::ScopedImport { keys, .. } => { + if keys.contains(&sym) { return Ok(self .bump .alloc(GhostCell::new(MaybeThunk::ScopedImportBinding(sym)).into())); @@ -598,21 +631,27 @@ impl<'id, 'ir> ThunkScope<'id, 'ir> { enum Scope<'ctx, 'id, 'ir> { Global(&'ctx HashMap), Repl(&'ctx HashSet), - ScopedImport(HashSet), + ScopedImport { + keys: HashSet, + slot_id: u32, + }, Let(HashMap>), Param { sym: StringId, abs_layer: u8 }, } -enum ExtraScope<'ctx> { +pub enum ExtraScope<'ctx> { Repl(&'ctx HashSet), - ScopedImport(HashSet), + ScopedImport { + keys: HashSet, + slot_id: u32, + }, } impl<'ctx> From> for Scope<'ctx, '_, '_> { fn from(value: ExtraScope<'ctx>) -> Self { use ExtraScope::*; match value { - ScopedImport(scope) => Scope::ScopedImport(scope), + ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id }, Repl(scope) => Scope::Repl(scope), } }