diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index fc534b7..d3ffbd9 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -46,8 +46,24 @@ Dependency tracking for imported derivations may be incomplete.`, export const scopedImport = (scope: NixValue) => - (path: NixValue): never => { - throw new Error("Not implemented: scopedImport"); + (path: NixValue): NixValue => { + const scopeAttrs = forceAttrs(scope); + const scopeKeys = Object.keys(scopeAttrs); + + const context: NixStringContext = new Set(); + const pathStr = coerceToPath(path, context); + + if (context.size > 0) { + console.warn( + `[WARN] scopedImport: Path has string context which is not yet fully tracked. +Dependency tracking for imported derivations may be incomplete.`, + ); + } + + const code = Deno.core.ops.op_scoped_import(pathStr, scopeKeys); + + const scopedFunc = Function(`return (${code})`)(); + return scopedFunc(scopeAttrs); }; export const storePath = (pathArg: NixValue): StringWithContext => { diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 88441c4..8ca6859 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -23,11 +23,13 @@ import { op } from "./operators"; import { builtins, PRIMOP_METADATA } from "./builtins"; import { coerceToString, StringCoercionMode } from "./builtins/conversion"; import { HAS_CONTEXT } from "./string-context"; -import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS } from "./types"; +import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS, NixValue } from "./types"; import { forceBool } from "./type-assert"; export type NixRuntime = typeof Nix; +const replBindings: Record = {}; + /** * The global Nix runtime object */ @@ -67,6 +69,12 @@ export const Nix = { op, builtins, PRIMOP_METADATA, + + replBindings, + setReplBinding: (name: string, value: NixValue) => { + replBindings[name] = value; + }, + getReplBinding: (name: string) => replBindings[name], }; globalThis.Nix = Nix; diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 0667c43..46b972c 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -37,6 +37,7 @@ declare global { namespace ops { function op_resolve_path(currentDir: string, path: string): string; function op_import(path: string): string; + function op_scoped_import(path: string, scopeKeys: string[]): string; function op_read_file(path: string): string; function op_read_file_type(path: string): string; function op_read_dir(path: string): Record; diff --git a/nix-js/src/bin/repl.rs b/nix-js/src/bin/repl.rs index a53daa3..6951e45 100644 --- a/nix-js/src/bin/repl.rs +++ b/nix-js/src/bin/repl.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use hashbrown::HashSet; use nix_js::context::Context; use nix_js::error::Source; use rustyline::DefaultEditor; @@ -9,6 +10,7 @@ fn main() -> Result<()> { let mut rl = DefaultEditor::new()?; let mut context = Context::new()?; + let mut scope = HashSet::new(); const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$"); loop { let readline = rl.readline("nix-js-repl> "); @@ -18,16 +20,24 @@ fn main() -> Result<()> { continue; } let _ = rl.add_history_entry(line.as_str()); - if let Some([Some(_), Some(_ident), Some(_expr)]) = RE.exec(&line) { - eprintln!("Error: binding not implemented yet"); - continue; - // if expr.is_empty() { - // eprintln!("Error: missing expression after '='"); - // continue; - // } - // if let Err(err) = context.add_binding(ident, expr) { - // eprintln!("Error: {}", err); - // } + if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) { + if let Some(expr) = rest.strip_prefix('=') { + let expr = expr.trim_start(); + if expr.is_empty() { + eprintln!("Error: missing expression after '='"); + continue; + } + match context.add_binding(ident, expr, &mut scope) { + Ok(value) => println!("{} = {}", ident, value), + Err(err) => eprintln!("{:?}", miette::Report::new(*err)), + } + } else { + let src = Source::new_repl(line)?; + match context.eval_repl(src, &scope) { + Ok(value) => println!("{value}"), + Err(err) => eprintln!("{:?}", miette::Report::new(*err)), + } + } } else { let src = Source::new_repl(line)?; match context.eval_shallow(src) { diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 7b52c56..13103a3 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -116,7 +116,7 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String { code!(&mut buf, ctx; "Nix.builtins.storeDir=" quoted(ctx.get_store_dir()) - ";const currentDir=" + ";const __currentDir=" quoted(&ctx.get_current_dir().display().to_string()) ";return " expr @@ -125,6 +125,28 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String { buf.into_string() } +pub(crate) fn compile_scoped(expr: &Ir, ctx: &impl CodegenContext) -> String { + let mut buf = CodeBuffer::with_capacity(8192); + + code!(&mut buf, ctx; "((__scope)=>{"); + + if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() { + code!(&mut buf, ctx; "Nix.DEBUG_THUNKS.enabled=true;"); + } + + code!(&mut buf, ctx; + "Nix.builtins.storeDir=" + quoted(ctx.get_store_dir()) + ";const __currentDir=" + quoted(&ctx.get_current_dir().display().to_string()) + ";return " + expr + "})" + ); + + buf.into_string() +} + trait Compile { fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer); } @@ -216,7 +238,7 @@ impl Compile for Ir { code!(buf, ctx; quoted(&s.val)); } Ir::Path(p) => { - code!(buf, ctx; "Nix.resolvePath(currentDir," ctx.get_ir(p.expr) ")"); + code!(buf, ctx; "Nix.resolvePath(__currentDir," ctx.get_ir(p.expr) ")"); } Ir::If(x) => x.compile(ctx, buf), Ir::BinOp(x) => x.compile(ctx, buf), @@ -275,6 +297,20 @@ impl Compile for Ir { ")" ); } + &Ir::ReplBinding(ReplBinding { inner: name, .. }) => { + code!(buf, ctx; + "Nix.getReplBinding(" + quoted(ctx.get_sym(name)) + ")" + ); + } + &Ir::ScopedImportBinding(ScopedImportBinding { inner: name, .. }) => { + code!(buf, ctx; + "__scope[" + quoted(ctx.get_sym(name)) + "]" + ); + } } } } diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 10b98cb..e09905c 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::ptr::NonNull; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use itertools::Itertools as _; use rnix::TextRange; use string_interner::DefaultStringInterner; @@ -9,8 +9,8 @@ use string_interner::DefaultStringInterner; use crate::codegen::{CodegenContext, compile}; use crate::error::{Error, Result, Source}; use crate::ir::{ - Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, Null, SymId, Thunk, - ToIr as _, + Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, Null, ReplBinding, + ScopedImportBinding, SymId, Thunk, ToIr as _, }; use crate::runtime::{Runtime, RuntimeContext}; use crate::store::{Store, StoreBackend, StoreConfig}; @@ -47,18 +47,38 @@ impl Context { eval!(eval_shallow, "Nix.forceShallow({})"); eval!(eval_deep, "Nix.forceDeep({})"); - pub fn compile(&mut self, source: Source) -> Result { - self.ctx.compile(source) + pub fn eval_repl<'a>(&'a mut self, source: Source, scope: &'a HashSet) -> Result { + tracing::info!("Starting evaluation"); + + tracing::debug!("Compiling code"); + let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?; + + tracing::debug!("Executing JavaScript"); + self.runtime.eval(format!("Nix.forceShallow({})", code), &mut self.ctx) } - #[allow(dead_code)] - pub(crate) fn eval_js(&mut self, code: String) -> Result { - self.runtime.eval(code, &mut self.ctx) + pub fn compile(&mut self, source: Source) -> Result { + self.ctx.compile(source, None) } pub fn get_store_dir(&self) -> &str { self.ctx.get_store_dir() } + + pub fn add_binding<'a>(&'a mut self, name: &str, expr: &str, scope: &'a mut HashSet) -> Result { + let source = Source::new_repl(expr.to_string())?; + let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?; + + let sym = self.ctx.symbols.get_or_intern(name); + + let eval_and_store = format!( + "(()=>{{const __v=Nix.forceShallow({});Nix.setReplBinding(\"{}\",__v);return __v}})()", + code, name + ); + + scope.insert(sym); + self.runtime.eval(eval_and_store, &mut self.ctx) + } } pub(crate) struct Ctx { @@ -166,9 +186,12 @@ impl Ctx { }) } - pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> { + fn downgrade_ctx<'a>( + &'a mut self, + extra_scope: Option>, + ) -> DowngradeCtx<'a> { let global_ref = unsafe { self.global.as_ref() }; - DowngradeCtx::new(self, global_ref) + DowngradeCtx::new(self, global_ref, extra_scope) } pub(crate) fn get_current_dir(&self) -> &Path { @@ -190,7 +213,7 @@ impl Ctx { self.sources.get(id).expect("source not found").clone() } - fn compile(&mut self, source: Source) -> Result { + fn compile<'a>(&'a mut self, source: Source, extra_scope: Option>) -> Result { tracing::debug!("Parsing Nix expression"); self.sources.push(source.clone()); @@ -204,7 +227,7 @@ impl Ctx { #[allow(clippy::unwrap_used)] let root = self - .downgrade_ctx() + .downgrade_ctx(extra_scope) .downgrade(root.tree().expr().unwrap())?; tracing::debug!("Generating JavaScript code"); @@ -212,6 +235,40 @@ impl Ctx { tracing::debug!("Generated code: {}", &code); Ok(code) } + + pub(crate) fn compile_scoped( + &mut self, + source: Source, + scope: Vec, + ) -> Result { + use crate::codegen::compile_scoped; + + tracing::debug!("Parsing Nix expression for scoped import"); + + self.sources.push(source.clone()); + + let root = rnix::Root::parse(&source.src); + if !root.errors().is_empty() { + let error_msg = root.errors().iter().join("; "); + let err = Error::parse_error(error_msg).with_source(source); + return Err(err); + } + + let scope = Scope::ScopedImport( + scope + .into_iter() + .map(|k| self.symbols.get_or_intern(k)) + .collect(), + ); + + #[allow(clippy::unwrap_used)] + let root = self.downgrade_ctx(Some(scope)).downgrade(root.tree().expr().unwrap())?; + + tracing::debug!("Generating JavaScript code for scoped import"); + let code = compile_scoped(self.get_ir(root), self); + tracing::debug!("Generated scoped code: {}", &code); + Ok(code) + } } impl CodegenContext for Ctx { @@ -245,8 +302,11 @@ impl RuntimeContext for Ctx { fn add_source(&mut self, source: Source) { self.sources.push(source); } - fn compile_code(&mut self, source: Source) -> Result { - self.compile(source) + fn compile(&mut self, source: Source) -> Result { + self.compile(source, None) + } + fn compile_scoped(&mut self, source: Source, scope: Vec) -> Result { + self.compile_scoped(source, scope) } fn get_source(&self, id: usize) -> Source { self.get_source(id) @@ -258,6 +318,8 @@ impl RuntimeContext for Ctx { enum Scope<'ctx> { Global(&'ctx HashMap), + Repl(&'ctx HashSet), + ScopedImport(HashSet), Let(HashMap), Param(SymId, ExprId), With(ExprId), @@ -288,9 +350,15 @@ pub struct DowngradeCtx<'ctx> { } impl<'ctx> DowngradeCtx<'ctx> { - fn new(ctx: &'ctx mut Ctx, global: &'ctx HashMap) -> Self { + fn new( + ctx: &'ctx mut Ctx, + global: &'ctx HashMap, + extra_scope: Option>, + ) -> Self { Self { - scopes: vec![Scope::Global(global)], + scopes: std::iter::once(Scope::Global(global)) + .chain(extra_scope) + .collect(), irs: vec![], arg_id: 0, thunk_scopes: vec![Vec::new()], @@ -364,6 +432,16 @@ impl DowngradeContext for DowngradeCtx<'_> { return Ok(expr); } } + &Scope::Repl(repl_bindings) => { + if repl_bindings.contains(&sym) { + return Ok(self.new_expr(ReplBinding { inner: sym, span }.to_ir())); + } + } + Scope::ScopedImport(scoped_bindings) => { + if scoped_bindings.contains(&sym) { + return Ok(self.new_expr(ScopedImportBinding { inner: sym, span }.to_ir())); + } + } Scope::Let(let_scope) => { if let Some(&expr) = let_scope.get(&sym) { return Ok(self.new_expr(Thunk { inner: expr, span }.to_ir())); diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index ba9e08d..7574a00 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -71,6 +71,8 @@ ir! { Builtins, Builtin(SymId), CurPos, + ReplBinding(SymId), + ScopedImportBinding(SymId), } #[repr(transparent)] diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index d915aca..9e22c10 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -18,7 +18,8 @@ type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>; pub(crate) trait RuntimeContext: 'static { fn get_current_dir(&self) -> &Path; fn add_source(&mut self, path: Source); - fn compile_code(&mut self, source: Source) -> Result; + fn compile(&mut self, source: Source) -> Result; + fn compile_scoped(&mut self, source: Source, scope: Vec) -> Result; fn get_source(&self, id: usize) -> Source; fn get_store(&self) -> &dyn Store; } @@ -44,6 +45,7 @@ fn runtime_extension() -> Extension { &deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js"); let mut ops = vec![ op_import::(), + op_scoped_import::(), op_read_file(), op_read_file_type(), op_read_dir(), @@ -446,29 +448,3 @@ fn to_primop<'a>( Some(Value::PrimOpApp(name)) } } - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod test { - use super::*; - use crate::context::Context; - - #[test] - fn to_value_working() { - let mut ctx = Context::new().unwrap(); - const EXPR: &str = "({ test: [1., 9223372036854775807n, true, false, 'hello world!'] })"; - assert_eq!( - ctx.eval_js(EXPR.into()).unwrap(), - Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([( - Symbol::from("test"), - Value::List(List::new(vec![ - Value::Float(1.), - Value::Int(9223372036854775807), - Value::Bool(true), - Value::Bool(false), - Value::String("hello world!".to_string()) - ])) - )]))) - ); - } -} diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index 7af84c1..5e72d19 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -1,7 +1,7 @@ use std::path::{Component, Path, PathBuf}; use std::sync::Arc; -use hashbrown::hash_map::{HashMap, Entry}; +use hashbrown::hash_map::{Entry, HashMap}; use deno_core::OpState; use regex::Regex; @@ -59,7 +59,7 @@ pub(super) fn op_import( .into(), }; ctx.add_source(source.clone()); - return Ok(ctx.compile_code(source).map_err(|err| err.to_string())?); + return Ok(ctx.compile(source).map_err(|err| err.to_string())?); } else { return Err(format!("Corepkg not found: {}", corepkg_name).into()); } @@ -83,7 +83,37 @@ pub(super) fn op_import( tracing::debug!("Compiling file"); ctx.add_source(source.clone()); - Ok(ctx.compile_code(source).map_err(|err| err.to_string())?) + Ok(ctx.compile(source).map_err(|err| err.to_string())?) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_scoped_import( + state: &mut OpState, + #[string] path: String, + #[serde] scope: Vec, +) -> std::result::Result { + let _span = tracing::info_span!("op_scoped_import", path = %path).entered(); + let ctx: &mut Ctx = state.get_ctx_mut(); + + let current_dir = ctx.get_current_dir(); + let mut absolute_path = current_dir.join(&path); + + if absolute_path.is_dir() { + absolute_path.push("default.nix") + } + + tracing::info!("Scoped importing file: {}", absolute_path.display()); + + let source = Source::new_file(absolute_path.clone()) + .map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?; + + tracing::debug!("Compiling file for scoped import"); + ctx.add_source(source.clone()); + + Ok(ctx + .compile_scoped(source, scope) + .map_err(|err| err.to_string())?) } #[deno_core::op2] diff --git a/nix-js/tests/derivation.rs b/nix-js/tests/derivation.rs index 6742679..cec8a17 100644 --- a/nix-js/tests/derivation.rs +++ b/nix-js/tests/derivation.rs @@ -7,8 +7,9 @@ use utils::{eval_deep, eval_deep_result}; #[test] fn derivation_minimal() { - let result = - eval_deep(r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#); + let result = eval_deep( + r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#, + ); match result { Value::AttrSet(attrs) => { @@ -78,7 +79,8 @@ fn derivation_to_string() { #[test] fn derivation_missing_name() { - let result = eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#); + let result = + eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 0118ea1..7b76bf0 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -181,10 +181,7 @@ eval_okay_test!( ); eval_okay_test!(r#if); eval_okay_test!(ind_string); -eval_okay_test!( - #[ignore = "not implemented: scopedImport"] - import -); +eval_okay_test!(import); eval_okay_test!(inherit_attr_pos); eval_okay_test!( #[ignore = "__overrides is not supported"]