diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 8a47456..abc42cc 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -2,8 +2,9 @@ * Helper functions for nix-js runtime */ -import type { NixValue, NixAttrs } from "./types"; +import type { NixValue, NixAttrs, NixBool } from "./types"; import { force_attrs, force_string } from "./type-assert"; +import { isAttrs } from "./builtins/type-check"; /** * Resolve a path (handles both absolute and relative paths) @@ -47,14 +48,9 @@ export const select = (obj: NixValue, key: NixValue): NixValue => { * @returns obj[key] if exists, otherwise default_val */ export const select_with_default = (obj: NixValue, key: NixValue, default_val: NixValue): NixValue => { - const forced_obj = force_attrs(obj); + const attrs = force_attrs(obj); const forced_key = force_string(key); - if (forced_obj === null || forced_obj === undefined) { - return default_val; - } - - const attrs = forced_obj; if (!(forced_key in attrs)) { return default_val; } @@ -62,6 +58,23 @@ export const select_with_default = (obj: NixValue, key: NixValue, default_val: N return attrs[forced_key]; }; +export const has_attr = (obj: NixValue, attrpath: NixValue[]): NixBool => { + if (!isAttrs(obj)) { + return false + } + let attrs = obj; + + for (const attr of attrpath.slice(0, -1)) { + const cur = attrs[force_string(attr)]; + if (!isAttrs(cur)) { + return false; + } + attrs = cur; + } + + return true; +}; + /** * Validate function parameters * Used for pattern matching in function parameters diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 5b52d03..a11e53d 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -5,7 +5,7 @@ */ import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk"; -import { select, select_with_default, validate_params, resolve_path } from "./helpers"; +import { select, select_with_default, validate_params, resolve_path, has_attr } from "./helpers"; import { op } from "./operators"; import { builtins, PRIMOP_METADATA } from "./builtins"; @@ -20,6 +20,7 @@ export const Nix = { is_thunk, IS_THUNK, + has_attr, select, select_with_default, validate_params, diff --git a/nix-js/src/bin/eval.rs b/nix-js/src/bin/eval.rs index 1acf282..d0caedf 100644 --- a/nix-js/src/bin/eval.rs +++ b/nix-js/src/bin/eval.rs @@ -10,7 +10,7 @@ fn main() -> Result<()> { } args.next(); let expr = args.next().unwrap(); - match Context::new().eval_code(&expr) { + match Context::new()?.eval_code(&expr) { Ok(value) => { println!("{value}"); Ok(()) diff --git a/nix-js/src/bin/repl.rs b/nix-js/src/bin/repl.rs index de7d3bc..3b37bc2 100644 --- a/nix-js/src/bin/repl.rs +++ b/nix-js/src/bin/repl.rs @@ -7,7 +7,7 @@ use nix_js::context::Context; fn main() -> Result<()> { let mut rl = DefaultEditor::new()?; - let mut context = Context::new(); + let mut context = Context::new()?; let re = Regex::new(r"^\s*([a-zA-Z_][a-zA-Z0-9_'-]*)\s*=(.*)$").unwrap(); loop { let readline = rl.readline("nix-js-repl> "); diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 008b0cb..11b498d 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -18,14 +18,20 @@ impl Compile for Ir { Ir::Float(float) => float.to_string(), Ir::Str(s) => { // Escape string for JavaScript - let escaped = s - .val - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\n', "\\n") - .replace('\r', "\\r") - .replace('\t', "\\t"); - format!("\"{}\"", escaped) + let mut escaped = String::with_capacity(s.val.len() + 2); + escaped.push('"'); + for c in s.val.chars() { + match c { + '\\' => escaped.push_str("\\\\"), + '\"' => escaped.push_str("\\\""), + '\n' => escaped.push_str("\\n"), + '\r' => escaped.push_str("\\r"), + '\t' => escaped.push_str("\\t"), + _ => escaped.push(c), + } + } + escaped.push('"'); + escaped } Ir::Path(p) => { // Path needs runtime resolution for interpolated paths @@ -88,8 +94,8 @@ impl Compile for BinOp { Leq => format!("Nix.op.lte({},{})", lhs, rhs), Geq => format!("Nix.op.gte({},{})", lhs, rhs), // Short-circuit operators: use JavaScript native && and || - And => format!("(Nix.force({}) && Nix.force({}))", lhs, rhs), - Or => format!("(Nix.force({}) || Nix.force({}))", lhs, rhs), + And => format!("Nix.force({}) && Nix.force({})", lhs, rhs), + Or => format!("Nix.force({}) || Nix.force({})", lhs, rhs), Impl => format!("(!Nix.force({}) || Nix.force({}))", lhs, rhs), Con => format!("Nix.op.concat({},{})", lhs, rhs), Upd => format!("Nix.op.update({},{})", lhs, rhs), @@ -202,13 +208,13 @@ impl Compile for Select { for (i, attr) in self.attrpath.iter().enumerate() { let is_last = i == attr_count - 1; - let has_default = self.default.is_some() && is_last; - result = match attr { Attr::Str(sym) => { let key = ctx.get_sym(*sym); - if has_default { - let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); + if let Some(default) = self.default + && is_last + { + let default_val = ctx.get_ir(default).compile(ctx); format!( "Nix.select_with_default({}, \"{}\", {})", result, key, default_val @@ -219,8 +225,10 @@ impl Compile for Select { } Attr::Dynamic(expr_id) => { let key = ctx.get_ir(*expr_id).compile(ctx); - if has_default { - let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); + if let Some(default) = self.default + && is_last + { + let default_val = ctx.get_ir(default).compile(ctx); format!( "Nix.select_with_default({}, {}, {})", result, key, default_val @@ -289,29 +297,16 @@ impl Compile for ConcatStrings { impl Compile for HasAttr { fn compile(&self, ctx: &Ctx) -> String { let lhs = ctx.get_ir(self.lhs).compile(ctx); - - // Build attrpath check - let mut current = format!("Nix.force({})", lhs); - - for attr in &self.rhs { - match attr { + let attrpath = self + .rhs + .iter() + .map(|attr| match attr { Attr::Str(sym) => { - let key = ctx.get_sym(*sym); - current = format!( - "(Nix.force({}) !== null && Nix.force({}) !== undefined && \"{}\" in Nix.force({}))", - current, current, key, current - ); + format!("\"{}\"", ctx.get_sym(*sym)) } - Attr::Dynamic(expr_id) => { - let key = ctx.get_ir(*expr_id).compile(ctx); - current = format!( - "(Nix.force({}) !== null && Nix.force({}) !== undefined && Nix.force({}) in Nix.force({}))", - current, current, key, current - ); - } - } - } - - current + Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx), + }) + .join(","); + format!("Nix.has_attr({lhs}, [{attrpath}])") } } diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index f540982..972c6c5 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -21,12 +21,6 @@ pub struct Context { runtime: Runtime, } -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - pub(crate) struct CtxPtr(NonNull); impl CtxPtr { pub(crate) unsafe fn as_ref(&self) -> &Ctx { @@ -38,23 +32,26 @@ impl CtxPtr { } impl Context { - pub fn new() -> Self { + pub fn new() -> Result { let mut ctx = Box::pin(Ctx::new()); let ptr = unsafe { CtxPtr(NonNull::new_unchecked(Pin::get_unchecked_mut(ctx.as_mut()))) }; - let runtime = Runtime::new(ptr); + let runtime = Runtime::new(ptr)?; - Self { ctx, runtime } + Ok(Self { ctx, runtime }) } pub fn eval_code(&mut self, expr: &str) -> Result { // Initialize `path_stack` with current directory for relative path resolution - let mut guard = PathDropGuard::new_cwd(self.ctx.as_mut()); + let mut guard = PathDropGuard::new_cwd(self.ctx.as_mut())?; let ctx = guard.as_ctx(); let root = rnix::Root::parse(expr); if !root.errors().is_empty() { return Err(Error::parse_error(root.errors().iter().join("; "))); } + + #[allow(clippy::unwrap_used)] + // Always `Some` since there is no parse error let root = ctx .as_mut() .downgrade_ctx() @@ -99,11 +96,12 @@ impl<'ctx> PathDropGuard<'ctx> { ctx.as_mut().project().path_stack.push(path); Self { ctx } } - pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Self { - let cwd = std::env::current_dir().unwrap(); + pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Result { + let cwd = std::env::current_dir() + .map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?; let virtual_file = cwd.join("__eval__.nix"); ctx.as_mut().project().path_stack.push(virtual_file); - Self { ctx } + Ok(Self { ctx }) } pub(crate) fn as_ctx<'a>(&'a mut self) -> &'a mut Pin<&'ctx mut Ctx> { &mut self.ctx @@ -185,24 +183,27 @@ impl Ctx { pub(crate) fn get_current_dir(&self) -> PathBuf { self.path_stack .last() - .unwrap() + .expect( + "path_stack should never be empty when get_current_dir is called. this is a bug", + ) .parent() - .unwrap() + .expect("path in path_stack should always have a parent dir. this is a bug") .to_path_buf() } } impl CodegenContext for Ctx { fn get_ir(&self, id: ExprId) -> &Ir { - self.irs.get(id.0).unwrap() + self.irs.get(id.0).expect("ExprId out of bounds") } fn get_sym(&self, id: SymId) -> &str { - self.symbols.resolve(id).unwrap() + self.symbols.resolve(id).expect("SymId out of bounds") } } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod test { use std::collections::BTreeMap; @@ -211,26 +212,42 @@ mod test { #[test] fn basic_eval() { - assert_eq!(Context::new().eval_code("1 + 1").unwrap(), Value::Int(2)); - assert_eq!(Context::new().eval_code("(x: x) 1").unwrap(), Value::Int(1)); assert_eq!( - Context::new().eval_code("(x: y: x - y) 2 1").unwrap(), + Context::new().unwrap().eval_code("1 + 1").unwrap(), + Value::Int(2) + ); + assert_eq!( + Context::new().unwrap().eval_code("(x: x) 1").unwrap(), Value::Int(1) ); assert_eq!( - Context::new().eval_code("rec { b = a; a = 1; }.b").unwrap(), + Context::new() + .unwrap() + .eval_code("(x: y: x - y) 2 1") + .unwrap(), Value::Int(1) ); assert_eq!( - Context::new().eval_code("let b = a; a = 1; in b").unwrap(), + Context::new() + .unwrap() + .eval_code("rec { b = a; a = 1; }.b") + .unwrap(), Value::Int(1) ); assert_eq!( - Context::new().eval_code("let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30").unwrap(), + Context::new() + .unwrap() + .eval_code("let b = a; a = 1; in b") + .unwrap(), + Value::Int(1) + ); + assert_eq!( + Context::new().unwrap().eval_code("let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30").unwrap(), Value::Int(832040) ); assert_eq!( Context::new() + .unwrap() .eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y") .unwrap(), Value::Int(2) @@ -272,7 +289,7 @@ mod test { ), ]; for (expr, expected) in tests { - assert_eq!(Context::new().eval_code(expr).unwrap(), expected); + assert_eq!(Context::new().unwrap().eval_code(expr).unwrap(), expected); } } @@ -281,18 +298,22 @@ mod test { // Test function with required parameters assert_eq!( Context::new() + .unwrap() .eval_code("({ a, b }: a + b) { a = 1; b = 2; }") .unwrap(), Value::Int(3) ); // Test missing required parameter should fail - let result = Context::new().eval_code("({ a, b }: a + b) { a = 1; }"); + let result = Context::new() + .unwrap() + .eval_code("({ a, b }: a + b) { a = 1; }"); assert!(result.is_err()); // Test all required parameters present assert_eq!( Context::new() + .unwrap() .eval_code("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }") .unwrap(), Value::Int(6) @@ -302,12 +323,15 @@ mod test { #[test] fn test_param_check_allowed() { // Test function without ellipsis - should reject unexpected arguments - let result = Context::new().eval_code("({ a, b }: a + b) { a = 1; b = 2; c = 3; }"); + let result = Context::new() + .unwrap() + .eval_code("({ a, b }: a + b) { a = 1; b = 2; c = 3; }"); assert!(result.is_err()); // Test function with ellipsis - should accept extra arguments assert_eq!( Context::new() + .unwrap() .eval_code("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }") .unwrap(), Value::Int(3) @@ -319,6 +343,7 @@ mod test { // Test function with default parameters assert_eq!( Context::new() + .unwrap() .eval_code("({ a, b ? 5 }: a + b) { a = 1; }") .unwrap(), Value::Int(6) @@ -327,6 +352,7 @@ mod test { // Test overriding default parameter assert_eq!( Context::new() + .unwrap() .eval_code("({ a, b ? 5 }: a + b) { a = 1; b = 10; }") .unwrap(), Value::Int(11) @@ -338,6 +364,7 @@ mod test { // Test function with @ pattern (alias) assert_eq!( Context::new() + .unwrap() .eval_code("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }") .unwrap(), Value::Int(3) @@ -349,6 +376,7 @@ mod test { // Test simple parameter (no pattern) should not have validation assert_eq!( Context::new() + .unwrap() .eval_code("(x: x.a + x.b) { a = 1; b = 2; }") .unwrap(), Value::Int(3) @@ -356,7 +384,7 @@ mod test { // Simple parameter accepts any argument assert_eq!( - Context::new().eval_code("(x: x) 42").unwrap(), + Context::new().unwrap().eval_code("(x: x) 42").unwrap(), Value::Int(42) ); } @@ -364,7 +392,7 @@ mod test { #[test] fn test_builtins_basic_access() { // Test that builtins identifier is accessible - let result = Context::new().eval_code("builtins").unwrap(); + let result = Context::new().unwrap().eval_code("builtins").unwrap(); // Should return an AttrSet with builtin functions assert!(matches!(result, Value::AttrSet(_))); } @@ -372,7 +400,10 @@ mod test { #[test] fn test_builtins_self_reference() { // Test builtins.builtins (self-reference as thunk) - let result = Context::new().eval_code("builtins.builtins").unwrap(); + let result = Context::new() + .unwrap() + .eval_code("builtins.builtins") + .unwrap(); assert!(matches!(result, Value::AttrSet(_))); } @@ -380,7 +411,10 @@ mod test { fn test_builtin_function_add() { // Test calling builtin function: builtins.add 1 2 assert_eq!( - Context::new().eval_code("builtins.add 1 2").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.add 1 2") + .unwrap(), Value::Int(3) ); } @@ -389,7 +423,10 @@ mod test { fn test_builtin_function_length() { // Test builtin with list: builtins.length [1 2 3] assert_eq!( - Context::new().eval_code("builtins.length [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.length [1 2 3]") + .unwrap(), Value::Int(3) ); } @@ -398,7 +435,10 @@ mod test { fn test_builtin_function_map() { // Test higher-order builtin: map (x: x * 2) [1 2 3] assert_eq!( - Context::new().eval_code("map (x: x * 2) [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("map (x: x * 2) [1 2 3]") + .unwrap(), Value::List(List::new( vec![Value::Int(2), Value::Int(4), Value::Int(6),] )) @@ -410,6 +450,7 @@ mod test { // Test predicate builtin: builtins.filter (x: x > 1) [1 2 3] assert_eq!( Context::new() + .unwrap() .eval_code("builtins.filter (x: x > 1) [1 2 3]") .unwrap(), Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) @@ -420,6 +461,7 @@ mod test { fn test_builtin_function_attrnames() { // Test builtins.attrNames { a = 1; b = 2; } let result = Context::new() + .unwrap() .eval_code("builtins.attrNames { a = 1; b = 2; }") .unwrap(); // Should return a list of attribute names @@ -434,7 +476,10 @@ mod test { fn test_builtin_function_head() { // Test builtins.head [1 2 3] assert_eq!( - Context::new().eval_code("builtins.head [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.head [1 2 3]") + .unwrap(), Value::Int(1) ); } @@ -443,7 +488,10 @@ mod test { fn test_builtin_function_tail() { // Test builtins.tail [1 2 3] assert_eq!( - Context::new().eval_code("builtins.tail [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.tail [1 2 3]") + .unwrap(), Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) ); } @@ -453,6 +501,7 @@ mod test { // Test builtins in let binding assert_eq!( Context::new() + .unwrap() .eval_code("let b = builtins; in b.add 5 3") .unwrap(), Value::Int(8) @@ -464,6 +513,7 @@ mod test { // Test builtins with 'with' expression assert_eq!( Context::new() + .unwrap() .eval_code("with builtins; add 10 20") .unwrap(), Value::Int(30) @@ -475,6 +525,7 @@ mod test { // Test nested function calls with builtins assert_eq!( Context::new() + .unwrap() .eval_code("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)") .unwrap(), Value::Int(11) // (2*3) + (10-5 = 6 + 5 = 11 @@ -485,27 +536,38 @@ mod test { fn test_builtin_type_checks() { // Test type checking functions assert_eq!( - Context::new().eval_code("builtins.isList [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.isList [1 2 3]") + .unwrap(), Value::Bool(true) ); assert_eq!( Context::new() + .unwrap() .eval_code("builtins.isAttrs { a = 1; }") .unwrap(), Value::Bool(true) ); assert_eq!( Context::new() + .unwrap() .eval_code("builtins.isFunction (x: x)") .unwrap(), Value::Bool(true) ); assert_eq!( - Context::new().eval_code("builtins.isNull null").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.isNull null") + .unwrap(), Value::Bool(true) ); assert_eq!( - Context::new().eval_code("builtins.isBool true").unwrap(), + Context::new() + .unwrap() + .eval_code("builtins.isBool true") + .unwrap(), Value::Bool(true) ); } @@ -515,6 +577,7 @@ mod test { // Test that user can shadow builtins (Nix allows this) assert_eq!( Context::new() + .unwrap() .eval_code("let builtins = { add = x: y: x - y; }; in builtins.add 5 3") .unwrap(), Value::Int(2) // Uses shadowed version @@ -526,6 +589,7 @@ mod test { // Test that builtins.builtins is lazy (thunk) // This should not cause infinite recursion let result = Context::new() + .unwrap() .eval_code("builtins.builtins.builtins.add 1 1") .unwrap(); assert_eq!(result, Value::Int(2)); @@ -534,27 +598,36 @@ mod test { // Free globals tests #[test] fn test_free_global_true() { - assert_eq!(Context::new().eval_code("true").unwrap(), Value::Bool(true)); + assert_eq!( + Context::new().unwrap().eval_code("true").unwrap(), + Value::Bool(true) + ); } #[test] fn test_free_global_false() { assert_eq!( - Context::new().eval_code("false").unwrap(), + Context::new().unwrap().eval_code("false").unwrap(), Value::Bool(false) ); } #[test] fn test_free_global_null() { - assert_eq!(Context::new().eval_code("null").unwrap(), Value::Null); + assert_eq!( + Context::new().unwrap().eval_code("null").unwrap(), + Value::Null + ); } #[test] fn test_free_global_map() { // Test free global function: map (x: x * 2) [1 2 3] assert_eq!( - Context::new().eval_code("map (x: x * 2) [1 2 3]").unwrap(), + Context::new() + .unwrap() + .eval_code("map (x: x * 2) [1 2 3]") + .unwrap(), Value::List(List::new( vec![Value::Int(2), Value::Int(4), Value::Int(6),] )) @@ -565,11 +638,11 @@ mod test { fn test_free_global_isnull() { // Test isNull function assert_eq!( - Context::new().eval_code("isNull null").unwrap(), + Context::new().unwrap().eval_code("isNull null").unwrap(), Value::Bool(true) ); assert_eq!( - Context::new().eval_code("isNull 5").unwrap(), + Context::new().unwrap().eval_code("isNull 5").unwrap(), Value::Bool(false) ); } @@ -579,12 +652,14 @@ mod test { // Test shadowing of free globals assert_eq!( Context::new() + .unwrap() .eval_code("let true = false; in true") .unwrap(), Value::Bool(false) ); assert_eq!( Context::new() + .unwrap() .eval_code("let map = x: y: x; in map 1 2") .unwrap(), Value::Int(1) @@ -596,6 +671,7 @@ mod test { // Test mixing free globals in expressions assert_eq!( Context::new() + .unwrap() .eval_code("if true then map (x: x + 1) [1 2] else []") .unwrap(), Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) @@ -607,6 +683,7 @@ mod test { // Test free globals in let bindings assert_eq!( Context::new() + .unwrap() .eval_code("let x = true; y = false; in x && y") .unwrap(), Value::Bool(false) @@ -616,7 +693,7 @@ mod test { // BigInt and numeric type tests #[test] fn test_bigint_precision() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); // Test large i64 values assert_eq!( @@ -641,7 +718,7 @@ mod test { #[test] fn test_int_float_distinction() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); // isInt tests assert_eq!( @@ -688,7 +765,7 @@ mod test { #[test] fn test_arithmetic_type_preservation() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); // int + int = int assert_eq!( @@ -717,7 +794,7 @@ mod test { #[test] fn test_integer_division() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); assert_eq!(ctx.eval_code("5 / 2").unwrap(), Value::Int(2)); @@ -735,7 +812,7 @@ mod test { #[test] fn test_builtin_arithmetic_with_bigint() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); // Test builtin add with large numbers assert_eq!( @@ -753,7 +830,7 @@ mod test { #[test] fn test_import_absolute_path() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let lib_path = temp_dir.path().join("nix_test_lib.nix"); @@ -766,7 +843,7 @@ mod test { #[test] fn test_import_nested() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); @@ -786,7 +863,7 @@ mod test { #[test] fn test_import_relative_path() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let subdir = temp_dir.path().join("subdir"); @@ -819,7 +896,7 @@ mod test { #[test] fn test_import_returns_function() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let func_path = temp_dir.path().join("nix_test_func.nix"); diff --git a/nix-js/src/context/downgrade.rs b/nix-js/src/context/downgrade.rs index bec8346..524de25 100644 --- a/nix-js/src/context/downgrade.rs +++ b/nix-js/src/context/downgrade.rs @@ -2,6 +2,7 @@ use std::pin::Pin; use hashbrown::HashMap; +use crate::codegen::CodegenContext; use crate::error::{Error, Result}; use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr}; @@ -65,7 +66,7 @@ impl DowngradeContext for DowngradeCtx<'_> { } fn get_sym(&self, id: SymId) -> &str { - self.ctx.symbols.resolve(id).unwrap() + self.ctx.get_sym(id) } fn lookup(&mut self, sym: SymId) -> Result { @@ -117,12 +118,20 @@ impl DowngradeContext for DowngradeCtx<'_> { fn extract_expr(&mut self, id: ExprId) -> Ir { let local_id = id.0 - self.ctx.irs.len(); - self.irs.get_mut(local_id).unwrap().take().unwrap() + self.irs + .get_mut(local_id) + .expect("ExprId out of bounds") + .take() + .expect("extract_expr called on an already extracted expr") } fn replace_expr(&mut self, id: ExprId, expr: Ir) { let local_id = id.0 - self.ctx.irs.len(); - let _ = self.irs.get_mut(local_id).unwrap().insert(expr); + let _ = self + .irs + .get_mut(local_id) + .expect("ExprId out of bounds") + .insert(expr); } #[allow(refining_impl_trait)] diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index d5e9972..a56d1a6 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::sync::Arc; use thiserror::Error; pub type Result = core::result::Result; @@ -11,6 +11,8 @@ pub enum ErrorKind { DowngradeError(String), #[error("error occurred during evaluation stage: {0}")] EvalError(String), + #[error("internal error occurred: {0}")] + InternalError(String), #[error("{0}")] Catchable(String), #[error("an unknown or unexpected error occurred")] @@ -21,7 +23,7 @@ pub enum ErrorKind { pub struct Error { pub kind: ErrorKind, pub span: Option, - pub source: Option>, + pub source: Option>, } impl std::fmt::Display for Error { @@ -101,7 +103,7 @@ impl Error { self } - pub fn with_source(mut self, source: Rc) -> Self { + pub fn with_source(mut self, source: Arc) -> Self { self.source = Some(source); self } @@ -115,6 +117,9 @@ impl Error { pub fn eval_error(msg: String) -> Self { Self::new(ErrorKind::EvalError(msg)) } + pub fn internal(msg: String) -> Self { + Self::new(ErrorKind::InternalError(msg)) + } pub fn catchable(msg: String) -> Self { Self::new(ErrorKind::Catchable(msg)) } diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index 09d1d6d..a3ec6c2 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -141,7 +141,9 @@ impl AttrSet { ) -> Result<()> { let mut path = path.into_iter(); // The last part of the path is the name of the attribute to be inserted. - let name = path.next_back().unwrap(); + let name = path + .next_back() + .expect("empty attrpath passed. this is a bug"); self._insert(path, name, value, ctx) } } diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 36a015d..59f2a15 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -1,3 +1,6 @@ +// Assume no parse error +#![allow(clippy::unwrap_used)] + use rnix::ast::{self, Expr, HasEntry}; use crate::error::{Error, Result}; diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index be21a4b..8624f96 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -1,3 +1,6 @@ +// Assume no parse error +#![allow(clippy::unwrap_used)] + use hashbrown::hash_map::Entry; use hashbrown::{HashMap, HashSet}; use rnix::ast; diff --git a/nix-js/src/lib.rs b/nix-js/src/lib.rs index f880c90..f90cb95 100644 --- a/nix-js/src/lib.rs +++ b/nix-js/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::unwrap_used)] + mod codegen; pub mod context; pub mod error; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 9836e22..83b6a35 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -144,7 +144,7 @@ pub(crate) struct Runtime { } impl Runtime { - pub(crate) fn new(ctx: CtxPtr) -> Self { + pub(crate) fn new(ctx: CtxPtr) -> Result { // Initialize V8 once static INIT: Once = Once::new(); INIT.call_once(|| { @@ -161,14 +161,14 @@ impl Runtime { let (is_thunk_symbol, primop_metadata_symbol) = { deno_core::scope!(scope, &mut js_runtime); - Self::get_symbols(scope) + Self::get_symbols(scope)? }; - Self { + Ok(Self { js_runtime, is_thunk_symbol, primop_metadata_symbol, - } + }) } pub(crate) fn eval(&mut self, script: String) -> Result { @@ -192,26 +192,45 @@ impl Runtime { } /// get (IS_THUNK, PRIMOP_METADATA) - fn get_symbols(scope: &ScopeRef) -> (v8::Global, v8::Global) { + fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global, v8::Global)> { let global = scope.get_current_context().global(scope); - let nix_key = v8::String::new(scope, "Nix").unwrap(); + let nix_key = v8::String::new(scope, "Nix") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; let nix_obj = global .get(scope, nix_key.into()) - .unwrap() + .ok_or_else(|| Error::internal("failed to get global Nix object".into()))? .to_object(scope) - .unwrap(); + .ok_or_else(|| { + Error::internal("failed to convert global Nix Value to object".into()) + })?; - let is_thunk_sym_key = v8::String::new(scope, "IS_THUNK").unwrap(); - let is_thunk_sym = nix_obj.get(scope, is_thunk_sym_key.into()).unwrap(); - let is_thunk = is_thunk_sym.try_cast::().unwrap(); + let is_thunk_sym_key = v8::String::new(scope, "IS_THUNK") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let is_thunk_sym = nix_obj + .get(scope, is_thunk_sym_key.into()) + .ok_or_else(|| Error::internal("failed to get IS_THUNK Symbol".into()))?; + let is_thunk = is_thunk_sym.try_cast::().map_err(|err| { + Error::internal(format!( + "failed to convert IS_THUNK Value to Symbol ({err})" + )) + })?; let is_thunk = v8::Global::new(scope, is_thunk); - let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA").unwrap(); - let primop_metadata_sym = nix_obj.get(scope, primop_metadata_sym_key.into()).unwrap(); - let primop_metadata = primop_metadata_sym.try_cast::().unwrap(); + let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let primop_metadata_sym = nix_obj + .get(scope, primop_metadata_sym_key.into()) + .ok_or_else(|| Error::internal("failed to get PRIMOP_METADATA Symbol".into()))?; + let primop_metadata = primop_metadata_sym + .try_cast::() + .map_err(|err| { + Error::internal(format!( + "failed to convert PRIMOP_METADATA Value to Symbol ({err})" + )) + })?; let primop_metadata = v8::Global::new(scope, primop_metadata); - (is_thunk, primop_metadata) + Ok((is_thunk, primop_metadata)) } } @@ -223,14 +242,17 @@ fn to_value<'a>( ) -> Value { match () { _ if val.is_big_int() => { - let (val, lossless) = val.to_big_int(scope).unwrap().i64_value(); + let (val, lossless) = val + .to_big_int(scope) + .expect("infallible conversion") + .i64_value(); if !lossless { panic!("BigInt value out of i64 range: conversion lost precision"); } Value::Int(val) } _ if val.is_number() => { - let val = val.to_number(scope).unwrap().value(); + let val = val.to_number(scope).expect("infallible conversion").value(); // number is always NixFloat Value::Float(val) } @@ -238,15 +260,15 @@ fn to_value<'a>( _ if val.is_false() => Value::Bool(false), _ if val.is_null() => Value::Null, _ if val.is_string() => { - let val = val.to_string(scope).unwrap(); + let val = val.to_string(scope).expect("infallible conversion"); Value::String(val.to_rust_string_lossy(scope)) } _ if val.is_array() => { - let val = val.try_cast::().unwrap(); + let val = val.try_cast::().expect("infallible conversion"); let len = val.length(); let list = (0..len) .map(|i| { - let val = val.get_index(scope, i).unwrap(); + let val = val.get_index(scope, i).expect("infallible index operation"); to_value(val, scope, is_thunk_symbol, primop_metadata_symbol) }) .collect(); @@ -264,15 +286,17 @@ fn to_value<'a>( return Value::Thunk; } - let val = val.to_object(scope).unwrap(); + let val = val.to_object(scope).expect("infallible conversion"); let keys = val .get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build()) - .unwrap(); + .expect("infallible operation"); let len = keys.length(); let attrs = (0..len) .map(|i| { - let key = keys.get_index(scope, i).unwrap(); - let val = val.get(scope, key).unwrap(); + let key = keys + .get_index(scope, i) + .expect("infallible index operation"); + let val = val.get(scope, key).expect("infallible operation"); let key = key.to_rust_string_lossy(scope); ( Symbol::new(key), @@ -291,7 +315,7 @@ fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymb return false; } - let obj = val.to_object(scope).unwrap(); + let obj = val.to_object(scope).expect("infallible conversion"); matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true()) } @@ -304,7 +328,7 @@ fn to_primop<'a>( return None; } - let obj = val.to_object(scope).unwrap(); + let obj = val.to_object(scope).expect("infallible conversion"); let metadata = obj.get(scope, symbol.into())?.to_object(scope)?; let name_key = v8::String::new(scope, "name")?; @@ -324,18 +348,19 @@ fn to_primop<'a>( } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod test { use super::*; use crate::context::Context; #[test] fn to_value_working() { - let mut ctx = Context::new(); + let mut ctx = Context::new().unwrap(); assert_eq!( ctx.eval_js( "({ - test: [1., 9223372036854775807n, true, false, 'hello world!'] - })" + test: [1., 9223372036854775807n, true, false, 'hello world!'] + })" .into(), ) .unwrap(), diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index bda1289..d3d2dea 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -39,8 +39,9 @@ impl Display for Symbol { } } -static REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_'-]*$").unwrap()); +static REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_'-]*$").expect("hardcoded regex is always valid") +}); impl Symbol { /// Checks if the symbol is a "normal" identifier that doesn't require quotes. fn normal(&self) -> bool {