implement import & scopedImport (WIP, ResolvePath resolves to string, import not working)
This commit is contained in:
@@ -247,6 +247,9 @@ pub enum PrimOpPhase {
|
||||
CallFunctor1,
|
||||
CallFunctor2,
|
||||
|
||||
ImportFinalize,
|
||||
ScopedImportFinalize,
|
||||
|
||||
Illegal,
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -16,6 +16,8 @@ pub trait BytecodeContext {
|
||||
fn get_code(&self) -> &[u8];
|
||||
fn get_code_mut(&mut self) -> &mut Vec<u8>;
|
||||
fn add_constant(&mut self, val: Const) -> u32;
|
||||
fn current_source_dir(&mut self) -> StringId;
|
||||
fn current_scope_slot(&self) -> Option<u32>;
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,36 @@ 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<'_>,
|
||||
) -> 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::<AttrSet>() 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 +88,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 +113,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::<StrictValue>(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<String, Box<Error>> {
|
||||
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())
|
||||
}
|
||||
|
||||
+81
-5
@@ -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<ExtraScope>,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
) -> fix_error::Result<InstructionPtr>;
|
||||
}
|
||||
|
||||
/// 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<StringId>,
|
||||
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<T: VmRuntimeCtx> ConvertValueWithSeen for T {
|
||||
enum Break {
|
||||
Force,
|
||||
Done,
|
||||
LoadFile,
|
||||
}
|
||||
|
||||
type Step = std::ops::ControlFlow<Break>;
|
||||
@@ -214,6 +227,11 @@ pub struct Vm<'gc> {
|
||||
env: GcEnv<'gc>,
|
||||
|
||||
import_cache: HashMap<PathBuf, Value<'gc>>,
|
||||
/// Stack of attrsets, one per `scopedImport` invocation. Indexed by
|
||||
/// `slot_id` baked into the bytecode at compile time. Slots are never
|
||||
/// freed — they live for the lifetime of the [`Vm`] (i.e. one
|
||||
/// `Vm::run` call).
|
||||
scope_slots: Vec<Value<'gc>>,
|
||||
|
||||
builtins: Value<'gc>,
|
||||
empty_list: Value<'gc>,
|
||||
@@ -224,9 +242,27 @@ pub struct Vm<'gc> {
|
||||
#[collect(require_static)]
|
||||
result: Option<Result<fix_common::Value>>,
|
||||
|
||||
/// Set by `import` / `scopedImport` primops just before they yield
|
||||
/// `Break::LoadFile`. The outer [`Vm::run`] loop consumes it via
|
||||
/// `Action::LoadFile`.
|
||||
#[collect(require_static)]
|
||||
pending_load: Option<PendingLoad>,
|
||||
|
||||
functor_sym: StringId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PendingLoad {
|
||||
pub path: PathBuf,
|
||||
pub scope: Option<PendingScope>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PendingScope {
|
||||
pub keys: HashSet<StringId>,
|
||||
pub slot_id: u32,
|
||||
}
|
||||
|
||||
enum OperandData {
|
||||
Const(StaticValue),
|
||||
BigInt(i64),
|
||||
@@ -234,7 +270,7 @@ enum OperandData {
|
||||
BuiltinConst(StringId),
|
||||
Builtins,
|
||||
ReplBinding(StringId),
|
||||
ScopedImportBinding(StringId),
|
||||
ScopedImportBinding { slot_id: u32, name: StringId },
|
||||
}
|
||||
|
||||
impl OperandData {
|
||||
@@ -260,7 +296,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::<AttrSet>().expect("scope must be attrset");
|
||||
#[allow(clippy::unwrap_used)]
|
||||
attrs.lookup(name).expect("scoped binding not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +380,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 +389,7 @@ impl<'gc> Vm<'gc> {
|
||||
force_mode,
|
||||
|
||||
result: None,
|
||||
pending_load: None,
|
||||
|
||||
functor_sym: ctx.intern_string("__functor"),
|
||||
}
|
||||
@@ -541,6 +589,7 @@ struct CallFrame<'gc> {
|
||||
enum Action {
|
||||
Continue { pc: usize },
|
||||
Done(Result<fix_common::Value>),
|
||||
LoadFile(PendingLoad),
|
||||
}
|
||||
|
||||
enum NixNum {
|
||||
@@ -573,6 +622,21 @@ 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;
|
||||
}
|
||||
Action::Done(done) => break done,
|
||||
}
|
||||
}
|
||||
@@ -604,6 +668,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 +758,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 +768,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),
|
||||
|
||||
Illegal => unreachable!(),
|
||||
};
|
||||
@@ -709,6 +778,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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,169 @@
|
||||
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::<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),
|
||||
};
|
||||
|
||||
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<'_>,
|
||||
_mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack: [path_sid, return_value]
|
||||
let val = self.pop();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let path_sid = self.pop().as_inline::<StringId>().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.return_from_primop(val, reader)
|
||||
}
|
||||
|
||||
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<AttrSet>, 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<StringId> = 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::<StrictValue>(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<PathBuf, Box<Error>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ 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, mc),
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
+52
-11
@@ -34,6 +34,9 @@ pub struct CodeState {
|
||||
pub spans: Vec<(usize, rnix::TextRange)>,
|
||||
pub thunk_count: usize,
|
||||
pub global_env: HashMap<StringId, MaybeThunk>,
|
||||
/// Set during a compilation pass when the code is being compiled under a
|
||||
/// `scopedImport` scope. Read by [`CompilerCtx::current_scope_slot`].
|
||||
pub current_scope_slot: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Evaluator {
|
||||
@@ -67,6 +70,7 @@ impl Evaluator {
|
||||
thunk_count: 0,
|
||||
bytecode,
|
||||
global_env,
|
||||
current_scope_slot: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -150,16 +154,22 @@ impl VmCode for CodeState {
|
||||
fn bytecode(&self) -> &[u8] {
|
||||
&self.bytecode
|
||||
}
|
||||
fn compile(
|
||||
fn compile_with_scope(
|
||||
&mut self,
|
||||
source: Source,
|
||||
extra_scope: Option<fix_vm::ExtraScope>,
|
||||
runtime: &mut impl VmRuntimeCtx,
|
||||
) -> Result<InstructionPtr> {
|
||||
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 +190,18 @@ impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
|
||||
source: Source,
|
||||
extra_scope: Option<ExtraScope>,
|
||||
) -> Result<InstructionPtr> {
|
||||
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<InstructionPtr> {
|
||||
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<ExtraScope>) -> Result<OwnedIr> {
|
||||
@@ -258,6 +277,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<u32> {
|
||||
self.code.current_scope_slot
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -410,8 +445,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 +633,27 @@ impl<'id, 'ir> ThunkScope<'id, 'ir> {
|
||||
enum Scope<'ctx, 'id, 'ir> {
|
||||
Global(&'ctx HashMap<StringId, MaybeThunk>),
|
||||
Repl(&'ctx HashSet<StringId>),
|
||||
ScopedImport(HashSet<StringId>),
|
||||
ScopedImport {
|
||||
keys: HashSet<StringId>,
|
||||
slot_id: u32,
|
||||
},
|
||||
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
|
||||
Param { sym: StringId, abs_layer: u8 },
|
||||
}
|
||||
|
||||
enum ExtraScope<'ctx> {
|
||||
pub enum ExtraScope<'ctx> {
|
||||
Repl(&'ctx HashSet<StringId>),
|
||||
ScopedImport(HashSet<StringId>),
|
||||
ScopedImport {
|
||||
keys: HashSet<StringId>,
|
||||
slot_id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'ctx> From<ExtraScope<'ctx>> 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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user