implement import & scopedImport (WIP, ResolvePath resolves to string)

This commit is contained in:
2026-05-05 15:18:31 +08:00
parent eb59f4fb67
commit 32f0434881
9 changed files with 441 additions and 30 deletions
+3
View File
@@ -247,6 +247,9 @@ pub enum PrimOpPhase {
CallFunctor1,
CallFunctor2,
ImportFinalize,
ScopedImportFinalize,
Illegal,
}
+12 -3
View File
@@ -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()),
+21
View File
@@ -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);
}
}
+3 -2
View File
@@ -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 }
}
}
}
+94 -9
View File
@@ -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
View File
@@ -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"),
);
}
}
}
}
+168
View File
@@ -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)
}
+7
View File
@@ -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:?}"),
}
}
+49 -8
View File
@@ -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 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),
}
}