refactor: less unwrap

This commit is contained in:
2026-01-09 21:18:40 +08:00
parent 0376621982
commit cc53963df0
14 changed files with 274 additions and 138 deletions

View File

@@ -2,8 +2,9 @@
* Helper functions for nix-js runtime * 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 { force_attrs, force_string } from "./type-assert";
import { isAttrs } from "./builtins/type-check";
/** /**
* Resolve a path (handles both absolute and relative paths) * 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 * @returns obj[key] if exists, otherwise default_val
*/ */
export const select_with_default = (obj: NixValue, key: NixValue, default_val: NixValue): NixValue => { 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); 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)) { if (!(forced_key in attrs)) {
return default_val; return default_val;
} }
@@ -62,6 +58,23 @@ export const select_with_default = (obj: NixValue, key: NixValue, default_val: N
return attrs[forced_key]; 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 * Validate function parameters
* Used for pattern matching in function parameters * Used for pattern matching in function parameters

View File

@@ -5,7 +5,7 @@
*/ */
import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk"; 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 { op } from "./operators";
import { builtins, PRIMOP_METADATA } from "./builtins"; import { builtins, PRIMOP_METADATA } from "./builtins";
@@ -20,6 +20,7 @@ export const Nix = {
is_thunk, is_thunk,
IS_THUNK, IS_THUNK,
has_attr,
select, select,
select_with_default, select_with_default,
validate_params, validate_params,

View File

@@ -10,7 +10,7 @@ fn main() -> Result<()> {
} }
args.next(); args.next();
let expr = args.next().unwrap(); let expr = args.next().unwrap();
match Context::new().eval_code(&expr) { match Context::new()?.eval_code(&expr) {
Ok(value) => { Ok(value) => {
println!("{value}"); println!("{value}");
Ok(()) Ok(())

View File

@@ -7,7 +7,7 @@ use nix_js::context::Context;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut rl = DefaultEditor::new()?; 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(); let re = Regex::new(r"^\s*([a-zA-Z_][a-zA-Z0-9_'-]*)\s*=(.*)$").unwrap();
loop { loop {
let readline = rl.readline("nix-js-repl> "); let readline = rl.readline("nix-js-repl> ");

View File

@@ -18,14 +18,20 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
Ir::Float(float) => float.to_string(), Ir::Float(float) => float.to_string(),
Ir::Str(s) => { Ir::Str(s) => {
// Escape string for JavaScript // Escape string for JavaScript
let escaped = s let mut escaped = String::with_capacity(s.val.len() + 2);
.val escaped.push('"');
.replace('\\', "\\\\") for c in s.val.chars() {
.replace('"', "\\\"") match c {
.replace('\n', "\\n") '\\' => escaped.push_str("\\\\"),
.replace('\r', "\\r") '\"' => escaped.push_str("\\\""),
.replace('\t', "\\t"); '\n' => escaped.push_str("\\n"),
format!("\"{}\"", escaped) '\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
_ => escaped.push(c),
}
}
escaped.push('"');
escaped
} }
Ir::Path(p) => { Ir::Path(p) => {
// Path needs runtime resolution for interpolated paths // Path needs runtime resolution for interpolated paths
@@ -88,8 +94,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for BinOp {
Leq => format!("Nix.op.lte({},{})", lhs, rhs), Leq => format!("Nix.op.lte({},{})", lhs, rhs),
Geq => format!("Nix.op.gte({},{})", lhs, rhs), Geq => format!("Nix.op.gte({},{})", lhs, rhs),
// Short-circuit operators: use JavaScript native && and || // Short-circuit operators: use JavaScript native && and ||
And => format!("(Nix.force({}) && Nix.force({}))", lhs, rhs), And => format!("Nix.force({}) && Nix.force({})", lhs, rhs),
Or => format!("(Nix.force({}) || Nix.force({}))", lhs, rhs), Or => format!("Nix.force({}) || Nix.force({})", lhs, rhs),
Impl => format!("(!Nix.force({}) || Nix.force({}))", lhs, rhs), Impl => format!("(!Nix.force({}) || Nix.force({}))", lhs, rhs),
Con => format!("Nix.op.concat({},{})", lhs, rhs), Con => format!("Nix.op.concat({},{})", lhs, rhs),
Upd => format!("Nix.op.update({},{})", lhs, rhs), Upd => format!("Nix.op.update({},{})", lhs, rhs),
@@ -202,13 +208,13 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Select {
for (i, attr) in self.attrpath.iter().enumerate() { for (i, attr) in self.attrpath.iter().enumerate() {
let is_last = i == attr_count - 1; let is_last = i == attr_count - 1;
let has_default = self.default.is_some() && is_last;
result = match attr { result = match attr {
Attr::Str(sym) => { Attr::Str(sym) => {
let key = ctx.get_sym(*sym); let key = ctx.get_sym(*sym);
if has_default { if let Some(default) = self.default
let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); && is_last
{
let default_val = ctx.get_ir(default).compile(ctx);
format!( format!(
"Nix.select_with_default({}, \"{}\", {})", "Nix.select_with_default({}, \"{}\", {})",
result, key, default_val result, key, default_val
@@ -219,8 +225,10 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Select {
} }
Attr::Dynamic(expr_id) => { Attr::Dynamic(expr_id) => {
let key = ctx.get_ir(*expr_id).compile(ctx); let key = ctx.get_ir(*expr_id).compile(ctx);
if has_default { if let Some(default) = self.default
let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); && is_last
{
let default_val = ctx.get_ir(default).compile(ctx);
format!( format!(
"Nix.select_with_default({}, {}, {})", "Nix.select_with_default({}, {}, {})",
result, key, default_val result, key, default_val
@@ -289,29 +297,16 @@ impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
impl<Ctx: CodegenContext> Compile<Ctx> for HasAttr { impl<Ctx: CodegenContext> Compile<Ctx> for HasAttr {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
let lhs = ctx.get_ir(self.lhs).compile(ctx); let lhs = ctx.get_ir(self.lhs).compile(ctx);
let attrpath = self
// Build attrpath check .rhs
let mut current = format!("Nix.force({})", lhs); .iter()
.map(|attr| match attr {
for attr in &self.rhs {
match attr {
Attr::Str(sym) => { Attr::Str(sym) => {
let key = ctx.get_sym(*sym); format!("\"{}\"", ctx.get_sym(*sym))
current = format!(
"(Nix.force({}) !== null && Nix.force({}) !== undefined && \"{}\" in Nix.force({}))",
current, current, key, current
);
} }
Attr::Dynamic(expr_id) => { Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx),
let key = ctx.get_ir(*expr_id).compile(ctx); })
current = format!( .join(",");
"(Nix.force({}) !== null && Nix.force({}) !== undefined && Nix.force({}) in Nix.force({}))", format!("Nix.has_attr({lhs}, [{attrpath}])")
current, current, key, current
);
}
}
}
current
} }
} }

View File

@@ -21,12 +21,6 @@ pub struct Context {
runtime: Runtime, runtime: Runtime,
} }
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct CtxPtr(NonNull<Ctx>); pub(crate) struct CtxPtr(NonNull<Ctx>);
impl CtxPtr { impl CtxPtr {
pub(crate) unsafe fn as_ref(&self) -> &Ctx { pub(crate) unsafe fn as_ref(&self) -> &Ctx {
@@ -38,23 +32,26 @@ impl CtxPtr {
} }
impl Context { impl Context {
pub fn new() -> Self { pub fn new() -> Result<Self> {
let mut ctx = Box::pin(Ctx::new()); let mut ctx = Box::pin(Ctx::new());
let ptr = unsafe { CtxPtr(NonNull::new_unchecked(Pin::get_unchecked_mut(ctx.as_mut()))) }; 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<Value> { pub fn eval_code(&mut self, expr: &str) -> Result<Value> {
// Initialize `path_stack` with current directory for relative path resolution // 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 ctx = guard.as_ctx();
let root = rnix::Root::parse(expr); let root = rnix::Root::parse(expr);
if !root.errors().is_empty() { if !root.errors().is_empty() {
return Err(Error::parse_error(root.errors().iter().join("; "))); return Err(Error::parse_error(root.errors().iter().join("; ")));
} }
#[allow(clippy::unwrap_used)]
// Always `Some` since there is no parse error
let root = ctx let root = ctx
.as_mut() .as_mut()
.downgrade_ctx() .downgrade_ctx()
@@ -99,11 +96,12 @@ impl<'ctx> PathDropGuard<'ctx> {
ctx.as_mut().project().path_stack.push(path); ctx.as_mut().project().path_stack.push(path);
Self { ctx } Self { ctx }
} }
pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Self { pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Result<Self> {
let cwd = std::env::current_dir().unwrap(); let cwd = std::env::current_dir()
.map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?;
let virtual_file = cwd.join("__eval__.nix"); let virtual_file = cwd.join("__eval__.nix");
ctx.as_mut().project().path_stack.push(virtual_file); 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> { pub(crate) fn as_ctx<'a>(&'a mut self) -> &'a mut Pin<&'ctx mut Ctx> {
&mut self.ctx &mut self.ctx
@@ -185,24 +183,27 @@ impl Ctx {
pub(crate) fn get_current_dir(&self) -> PathBuf { pub(crate) fn get_current_dir(&self) -> PathBuf {
self.path_stack self.path_stack
.last() .last()
.unwrap() .expect(
"path_stack should never be empty when get_current_dir is called. this is a bug",
)
.parent() .parent()
.unwrap() .expect("path in path_stack should always have a parent dir. this is a bug")
.to_path_buf() .to_path_buf()
} }
} }
impl CodegenContext for Ctx { impl CodegenContext for Ctx {
fn get_ir(&self, id: ExprId) -> &Ir { 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 { fn get_sym(&self, id: SymId) -> &str {
self.symbols.resolve(id).unwrap() self.symbols.resolve(id).expect("SymId out of bounds")
} }
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test { mod test {
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -211,26 +212,42 @@ mod test {
#[test] #[test]
fn basic_eval() { 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!( 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) Value::Int(1)
); );
assert_eq!( 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) Value::Int(1)
); );
assert_eq!( 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) Value::Int(1)
); );
assert_eq!( 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) Value::Int(832040)
); );
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y") .eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y")
.unwrap(), .unwrap(),
Value::Int(2) Value::Int(2)
@@ -272,7 +289,7 @@ mod test {
), ),
]; ];
for (expr, expected) in tests { 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 // Test function with required parameters
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("({ a, b }: a + b) { a = 1; b = 2; }") .eval_code("({ a, b }: a + b) { a = 1; b = 2; }")
.unwrap(), .unwrap(),
Value::Int(3) Value::Int(3)
); );
// Test missing required parameter should fail // 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()); assert!(result.is_err());
// Test all required parameters present // Test all required parameters present
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }") .eval_code("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }")
.unwrap(), .unwrap(),
Value::Int(6) Value::Int(6)
@@ -302,12 +323,15 @@ mod test {
#[test] #[test]
fn test_param_check_allowed() { fn test_param_check_allowed() {
// Test function without ellipsis - should reject unexpected arguments // 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()); assert!(result.is_err());
// Test function with ellipsis - should accept extra arguments // Test function with ellipsis - should accept extra arguments
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }") .eval_code("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }")
.unwrap(), .unwrap(),
Value::Int(3) Value::Int(3)
@@ -319,6 +343,7 @@ mod test {
// Test function with default parameters // Test function with default parameters
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("({ a, b ? 5 }: a + b) { a = 1; }") .eval_code("({ a, b ? 5 }: a + b) { a = 1; }")
.unwrap(), .unwrap(),
Value::Int(6) Value::Int(6)
@@ -327,6 +352,7 @@ mod test {
// Test overriding default parameter // Test overriding default parameter
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("({ a, b ? 5 }: a + b) { a = 1; b = 10; }") .eval_code("({ a, b ? 5 }: a + b) { a = 1; b = 10; }")
.unwrap(), .unwrap(),
Value::Int(11) Value::Int(11)
@@ -338,6 +364,7 @@ mod test {
// Test function with @ pattern (alias) // Test function with @ pattern (alias)
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }") .eval_code("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }")
.unwrap(), .unwrap(),
Value::Int(3) Value::Int(3)
@@ -349,6 +376,7 @@ mod test {
// Test simple parameter (no pattern) should not have validation // Test simple parameter (no pattern) should not have validation
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("(x: x.a + x.b) { a = 1; b = 2; }") .eval_code("(x: x.a + x.b) { a = 1; b = 2; }")
.unwrap(), .unwrap(),
Value::Int(3) Value::Int(3)
@@ -356,7 +384,7 @@ mod test {
// Simple parameter accepts any argument // Simple parameter accepts any argument
assert_eq!( assert_eq!(
Context::new().eval_code("(x: x) 42").unwrap(), Context::new().unwrap().eval_code("(x: x) 42").unwrap(),
Value::Int(42) Value::Int(42)
); );
} }
@@ -364,7 +392,7 @@ mod test {
#[test] #[test]
fn test_builtins_basic_access() { fn test_builtins_basic_access() {
// Test that builtins identifier is accessible // 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 // Should return an AttrSet with builtin functions
assert!(matches!(result, Value::AttrSet(_))); assert!(matches!(result, Value::AttrSet(_)));
} }
@@ -372,7 +400,10 @@ mod test {
#[test] #[test]
fn test_builtins_self_reference() { fn test_builtins_self_reference() {
// Test builtins.builtins (self-reference as thunk) // 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(_))); assert!(matches!(result, Value::AttrSet(_)));
} }
@@ -380,7 +411,10 @@ mod test {
fn test_builtin_function_add() { fn test_builtin_function_add() {
// Test calling builtin function: builtins.add 1 2 // Test calling builtin function: builtins.add 1 2
assert_eq!( assert_eq!(
Context::new().eval_code("builtins.add 1 2").unwrap(), Context::new()
.unwrap()
.eval_code("builtins.add 1 2")
.unwrap(),
Value::Int(3) Value::Int(3)
); );
} }
@@ -389,7 +423,10 @@ mod test {
fn test_builtin_function_length() { fn test_builtin_function_length() {
// Test builtin with list: builtins.length [1 2 3] // Test builtin with list: builtins.length [1 2 3]
assert_eq!( 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) Value::Int(3)
); );
} }
@@ -398,7 +435,10 @@ mod test {
fn test_builtin_function_map() { fn test_builtin_function_map() {
// Test higher-order builtin: map (x: x * 2) [1 2 3] // Test higher-order builtin: map (x: x * 2) [1 2 3]
assert_eq!( 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( Value::List(List::new(
vec![Value::Int(2), Value::Int(4), Value::Int(6),] 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] // Test predicate builtin: builtins.filter (x: x > 1) [1 2 3]
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("builtins.filter (x: x > 1) [1 2 3]") .eval_code("builtins.filter (x: x > 1) [1 2 3]")
.unwrap(), .unwrap(),
Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) Value::List(List::new(vec![Value::Int(2), Value::Int(3),]))
@@ -420,6 +461,7 @@ mod test {
fn test_builtin_function_attrnames() { fn test_builtin_function_attrnames() {
// Test builtins.attrNames { a = 1; b = 2; } // Test builtins.attrNames { a = 1; b = 2; }
let result = Context::new() let result = Context::new()
.unwrap()
.eval_code("builtins.attrNames { a = 1; b = 2; }") .eval_code("builtins.attrNames { a = 1; b = 2; }")
.unwrap(); .unwrap();
// Should return a list of attribute names // Should return a list of attribute names
@@ -434,7 +476,10 @@ mod test {
fn test_builtin_function_head() { fn test_builtin_function_head() {
// Test builtins.head [1 2 3] // Test builtins.head [1 2 3]
assert_eq!( 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) Value::Int(1)
); );
} }
@@ -443,7 +488,10 @@ mod test {
fn test_builtin_function_tail() { fn test_builtin_function_tail() {
// Test builtins.tail [1 2 3] // Test builtins.tail [1 2 3]
assert_eq!( 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),])) Value::List(List::new(vec![Value::Int(2), Value::Int(3),]))
); );
} }
@@ -453,6 +501,7 @@ mod test {
// Test builtins in let binding // Test builtins in let binding
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("let b = builtins; in b.add 5 3") .eval_code("let b = builtins; in b.add 5 3")
.unwrap(), .unwrap(),
Value::Int(8) Value::Int(8)
@@ -464,6 +513,7 @@ mod test {
// Test builtins with 'with' expression // Test builtins with 'with' expression
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("with builtins; add 10 20") .eval_code("with builtins; add 10 20")
.unwrap(), .unwrap(),
Value::Int(30) Value::Int(30)
@@ -475,6 +525,7 @@ mod test {
// Test nested function calls with builtins // Test nested function calls with builtins
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)") .eval_code("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)")
.unwrap(), .unwrap(),
Value::Int(11) // (2*3) + (10-5 = 6 + 5 = 11 Value::Int(11) // (2*3) + (10-5 = 6 + 5 = 11
@@ -485,27 +536,38 @@ mod test {
fn test_builtin_type_checks() { fn test_builtin_type_checks() {
// Test type checking functions // Test type checking functions
assert_eq!( 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) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("builtins.isAttrs { a = 1; }") .eval_code("builtins.isAttrs { a = 1; }")
.unwrap(), .unwrap(),
Value::Bool(true) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("builtins.isFunction (x: x)") .eval_code("builtins.isFunction (x: x)")
.unwrap(), .unwrap(),
Value::Bool(true) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("builtins.isNull null").unwrap(), Context::new()
.unwrap()
.eval_code("builtins.isNull null")
.unwrap(),
Value::Bool(true) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("builtins.isBool true").unwrap(), Context::new()
.unwrap()
.eval_code("builtins.isBool true")
.unwrap(),
Value::Bool(true) Value::Bool(true)
); );
} }
@@ -515,6 +577,7 @@ mod test {
// Test that user can shadow builtins (Nix allows this) // Test that user can shadow builtins (Nix allows this)
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("let builtins = { add = x: y: x - y; }; in builtins.add 5 3") .eval_code("let builtins = { add = x: y: x - y; }; in builtins.add 5 3")
.unwrap(), .unwrap(),
Value::Int(2) // Uses shadowed version Value::Int(2) // Uses shadowed version
@@ -526,6 +589,7 @@ mod test {
// Test that builtins.builtins is lazy (thunk) // Test that builtins.builtins is lazy (thunk)
// This should not cause infinite recursion // This should not cause infinite recursion
let result = Context::new() let result = Context::new()
.unwrap()
.eval_code("builtins.builtins.builtins.add 1 1") .eval_code("builtins.builtins.builtins.add 1 1")
.unwrap(); .unwrap();
assert_eq!(result, Value::Int(2)); assert_eq!(result, Value::Int(2));
@@ -534,27 +598,36 @@ mod test {
// Free globals tests // Free globals tests
#[test] #[test]
fn test_free_global_true() { 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] #[test]
fn test_free_global_false() { fn test_free_global_false() {
assert_eq!( assert_eq!(
Context::new().eval_code("false").unwrap(), Context::new().unwrap().eval_code("false").unwrap(),
Value::Bool(false) Value::Bool(false)
); );
} }
#[test] #[test]
fn test_free_global_null() { 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] #[test]
fn test_free_global_map() { fn test_free_global_map() {
// Test free global function: map (x: x * 2) [1 2 3] // Test free global function: map (x: x * 2) [1 2 3]
assert_eq!( 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( Value::List(List::new(
vec![Value::Int(2), Value::Int(4), Value::Int(6),] vec![Value::Int(2), Value::Int(4), Value::Int(6),]
)) ))
@@ -565,11 +638,11 @@ mod test {
fn test_free_global_isnull() { fn test_free_global_isnull() {
// Test isNull function // Test isNull function
assert_eq!( assert_eq!(
Context::new().eval_code("isNull null").unwrap(), Context::new().unwrap().eval_code("isNull null").unwrap(),
Value::Bool(true) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("isNull 5").unwrap(), Context::new().unwrap().eval_code("isNull 5").unwrap(),
Value::Bool(false) Value::Bool(false)
); );
} }
@@ -579,12 +652,14 @@ mod test {
// Test shadowing of free globals // Test shadowing of free globals
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("let true = false; in true") .eval_code("let true = false; in true")
.unwrap(), .unwrap(),
Value::Bool(false) Value::Bool(false)
); );
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("let map = x: y: x; in map 1 2") .eval_code("let map = x: y: x; in map 1 2")
.unwrap(), .unwrap(),
Value::Int(1) Value::Int(1)
@@ -596,6 +671,7 @@ mod test {
// Test mixing free globals in expressions // Test mixing free globals in expressions
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("if true then map (x: x + 1) [1 2] else []") .eval_code("if true then map (x: x + 1) [1 2] else []")
.unwrap(), .unwrap(),
Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) Value::List(List::new(vec![Value::Int(2), Value::Int(3),]))
@@ -607,6 +683,7 @@ mod test {
// Test free globals in let bindings // Test free globals in let bindings
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("let x = true; y = false; in x && y") .eval_code("let x = true; y = false; in x && y")
.unwrap(), .unwrap(),
Value::Bool(false) Value::Bool(false)
@@ -616,7 +693,7 @@ mod test {
// BigInt and numeric type tests // BigInt and numeric type tests
#[test] #[test]
fn test_bigint_precision() { fn test_bigint_precision() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
// Test large i64 values // Test large i64 values
assert_eq!( assert_eq!(
@@ -641,7 +718,7 @@ mod test {
#[test] #[test]
fn test_int_float_distinction() { fn test_int_float_distinction() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
// isInt tests // isInt tests
assert_eq!( assert_eq!(
@@ -688,7 +765,7 @@ mod test {
#[test] #[test]
fn test_arithmetic_type_preservation() { fn test_arithmetic_type_preservation() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
// int + int = int // int + int = int
assert_eq!( assert_eq!(
@@ -717,7 +794,7 @@ mod test {
#[test] #[test]
fn test_integer_division() { 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)); assert_eq!(ctx.eval_code("5 / 2").unwrap(), Value::Int(2));
@@ -735,7 +812,7 @@ mod test {
#[test] #[test]
fn test_builtin_arithmetic_with_bigint() { fn test_builtin_arithmetic_with_bigint() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
// Test builtin add with large numbers // Test builtin add with large numbers
assert_eq!( assert_eq!(
@@ -753,7 +830,7 @@ mod test {
#[test] #[test]
fn test_import_absolute_path() { fn test_import_absolute_path() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let lib_path = temp_dir.path().join("nix_test_lib.nix"); let lib_path = temp_dir.path().join("nix_test_lib.nix");
@@ -766,7 +843,7 @@ mod test {
#[test] #[test]
fn test_import_nested() { fn test_import_nested() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
@@ -786,7 +863,7 @@ mod test {
#[test] #[test]
fn test_import_relative_path() { fn test_import_relative_path() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let subdir = temp_dir.path().join("subdir"); let subdir = temp_dir.path().join("subdir");
@@ -819,7 +896,7 @@ mod test {
#[test] #[test]
fn test_import_returns_function() { fn test_import_returns_function() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let func_path = temp_dir.path().join("nix_test_func.nix"); let func_path = temp_dir.path().join("nix_test_func.nix");

View File

@@ -2,6 +2,7 @@ use std::pin::Pin;
use hashbrown::HashMap; use hashbrown::HashMap;
use crate::codegen::CodegenContext;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr}; 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 { 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<ExprId> { fn lookup(&mut self, sym: SymId) -> Result<ExprId> {
@@ -117,12 +118,20 @@ impl DowngradeContext for DowngradeCtx<'_> {
fn extract_expr(&mut self, id: ExprId) -> Ir { fn extract_expr(&mut self, id: ExprId) -> Ir {
let local_id = id.0 - self.ctx.irs.len(); 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) { fn replace_expr(&mut self, id: ExprId, expr: Ir) {
let local_id = id.0 - self.ctx.irs.len(); 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)] #[allow(refining_impl_trait)]

View File

@@ -1,4 +1,4 @@
use std::rc::Rc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
@@ -11,6 +11,8 @@ pub enum ErrorKind {
DowngradeError(String), DowngradeError(String),
#[error("error occurred during evaluation stage: {0}")] #[error("error occurred during evaluation stage: {0}")]
EvalError(String), EvalError(String),
#[error("internal error occurred: {0}")]
InternalError(String),
#[error("{0}")] #[error("{0}")]
Catchable(String), Catchable(String),
#[error("an unknown or unexpected error occurred")] #[error("an unknown or unexpected error occurred")]
@@ -21,7 +23,7 @@ pub enum ErrorKind {
pub struct Error { pub struct Error {
pub kind: ErrorKind, pub kind: ErrorKind,
pub span: Option<rnix::TextRange>, pub span: Option<rnix::TextRange>,
pub source: Option<Rc<str>>, pub source: Option<Arc<str>>,
} }
impl std::fmt::Display for Error { impl std::fmt::Display for Error {
@@ -101,7 +103,7 @@ impl Error {
self self
} }
pub fn with_source(mut self, source: Rc<str>) -> Self { pub fn with_source(mut self, source: Arc<str>) -> Self {
self.source = Some(source); self.source = Some(source);
self self
} }
@@ -115,6 +117,9 @@ impl Error {
pub fn eval_error(msg: String) -> Self { pub fn eval_error(msg: String) -> Self {
Self::new(ErrorKind::EvalError(msg)) Self::new(ErrorKind::EvalError(msg))
} }
pub fn internal(msg: String) -> Self {
Self::new(ErrorKind::InternalError(msg))
}
pub fn catchable(msg: String) -> Self { pub fn catchable(msg: String) -> Self {
Self::new(ErrorKind::Catchable(msg)) Self::new(ErrorKind::Catchable(msg))
} }

View File

@@ -141,7 +141,9 @@ impl AttrSet {
) -> Result<()> { ) -> Result<()> {
let mut path = path.into_iter(); let mut path = path.into_iter();
// The last part of the path is the name of the attribute to be inserted. // 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) self._insert(path, name, value, ctx)
} }
} }

View File

@@ -1,3 +1,6 @@
// Assume no parse error
#![allow(clippy::unwrap_used)]
use rnix::ast::{self, Expr, HasEntry}; use rnix::ast::{self, Expr, HasEntry};
use crate::error::{Error, Result}; use crate::error::{Error, Result};

View File

@@ -1,3 +1,6 @@
// Assume no parse error
#![allow(clippy::unwrap_used)]
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use rnix::ast; use rnix::ast;

View File

@@ -1,3 +1,5 @@
#![warn(clippy::unwrap_used)]
mod codegen; mod codegen;
pub mod context; pub mod context;
pub mod error; pub mod error;

View File

@@ -144,7 +144,7 @@ pub(crate) struct Runtime {
} }
impl Runtime { impl Runtime {
pub(crate) fn new(ctx: CtxPtr) -> Self { pub(crate) fn new(ctx: CtxPtr) -> Result<Self> {
// Initialize V8 once // Initialize V8 once
static INIT: Once = Once::new(); static INIT: Once = Once::new();
INIT.call_once(|| { INIT.call_once(|| {
@@ -161,14 +161,14 @@ impl Runtime {
let (is_thunk_symbol, primop_metadata_symbol) = { let (is_thunk_symbol, primop_metadata_symbol) = {
deno_core::scope!(scope, &mut js_runtime); deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope) Self::get_symbols(scope)?
}; };
Self { Ok(Self {
js_runtime, js_runtime,
is_thunk_symbol, is_thunk_symbol,
primop_metadata_symbol, primop_metadata_symbol,
} })
} }
pub(crate) fn eval(&mut self, script: String) -> Result<Value> { pub(crate) fn eval(&mut self, script: String) -> Result<Value> {
@@ -192,26 +192,45 @@ impl Runtime {
} }
/// get (IS_THUNK, PRIMOP_METADATA) /// get (IS_THUNK, PRIMOP_METADATA)
fn get_symbols(scope: &ScopeRef) -> (v8::Global<v8::Symbol>, v8::Global<v8::Symbol>) { fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global<v8::Symbol>, v8::Global<v8::Symbol>)> {
let global = scope.get_current_context().global(scope); 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 let nix_obj = global
.get(scope, nix_key.into()) .get(scope, nix_key.into())
.unwrap() .ok_or_else(|| Error::internal("failed to get global Nix object".into()))?
.to_object(scope) .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_key = v8::String::new(scope, "IS_THUNK")
let is_thunk_sym = nix_obj.get(scope, is_thunk_sym_key.into()).unwrap(); .ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let is_thunk = is_thunk_sym.try_cast::<v8::Symbol>().unwrap(); 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::<v8::Symbol>().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 is_thunk = v8::Global::new(scope, is_thunk);
let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA").unwrap(); let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA")
let primop_metadata_sym = nix_obj.get(scope, primop_metadata_sym_key.into()).unwrap(); .ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let primop_metadata = primop_metadata_sym.try_cast::<v8::Symbol>().unwrap(); 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::<v8::Symbol>()
.map_err(|err| {
Error::internal(format!(
"failed to convert PRIMOP_METADATA Value to Symbol ({err})"
))
})?;
let primop_metadata = v8::Global::new(scope, primop_metadata); 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 { ) -> Value {
match () { match () {
_ if val.is_big_int() => { _ 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 { if !lossless {
panic!("BigInt value out of i64 range: conversion lost precision"); panic!("BigInt value out of i64 range: conversion lost precision");
} }
Value::Int(val) Value::Int(val)
} }
_ if val.is_number() => { _ 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 // number is always NixFloat
Value::Float(val) Value::Float(val)
} }
@@ -238,15 +260,15 @@ fn to_value<'a>(
_ if val.is_false() => Value::Bool(false), _ if val.is_false() => Value::Bool(false),
_ if val.is_null() => Value::Null, _ if val.is_null() => Value::Null,
_ if val.is_string() => { _ 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)) Value::String(val.to_rust_string_lossy(scope))
} }
_ if val.is_array() => { _ if val.is_array() => {
let val = val.try_cast::<v8::Array>().unwrap(); let val = val.try_cast::<v8::Array>().expect("infallible conversion");
let len = val.length(); let len = val.length();
let list = (0..len) let list = (0..len)
.map(|i| { .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) to_value(val, scope, is_thunk_symbol, primop_metadata_symbol)
}) })
.collect(); .collect();
@@ -264,15 +286,17 @@ fn to_value<'a>(
return Value::Thunk; return Value::Thunk;
} }
let val = val.to_object(scope).unwrap(); let val = val.to_object(scope).expect("infallible conversion");
let keys = val let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build()) .get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
.unwrap(); .expect("infallible operation");
let len = keys.length(); let len = keys.length();
let attrs = (0..len) let attrs = (0..len)
.map(|i| { .map(|i| {
let key = keys.get_index(scope, i).unwrap(); let key = keys
let val = val.get(scope, key).unwrap(); .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); let key = key.to_rust_string_lossy(scope);
( (
Symbol::new(key), Symbol::new(key),
@@ -291,7 +315,7 @@ fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymb
return false; 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()) matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
} }
@@ -304,7 +328,7 @@ fn to_primop<'a>(
return None; 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 metadata = obj.get(scope, symbol.into())?.to_object(scope)?;
let name_key = v8::String::new(scope, "name")?; let name_key = v8::String::new(scope, "name")?;
@@ -324,13 +348,14 @@ fn to_primop<'a>(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test { mod test {
use super::*; use super::*;
use crate::context::Context; use crate::context::Context;
#[test] #[test]
fn to_value_working() { fn to_value_working() {
let mut ctx = Context::new(); let mut ctx = Context::new().unwrap();
assert_eq!( assert_eq!(
ctx.eval_js( ctx.eval_js(
"({ "({

View File

@@ -39,8 +39,9 @@ impl Display for Symbol {
} }
} }
static REGEX: LazyLock<Regex> = static REGEX: LazyLock<Regex> = LazyLock::new(|| {
LazyLock::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_'-]*$").unwrap()); Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_'-]*$").expect("hardcoded regex is always valid")
});
impl Symbol { impl Symbol {
/// Checks if the symbol is a "normal" identifier that doesn't require quotes. /// Checks if the symbol is a "normal" identifier that doesn't require quotes.
fn normal(&self) -> bool { fn normal(&self) -> bool {