Compare commits

..

4 Commits

Author SHA1 Message Date
e29e432328 feat: runtime error 2026-01-10 10:28:48 +08:00
cc53963df0 refactor: less unwrap 2026-01-10 10:28:48 +08:00
0376621982 refactor: flatten Ir::Const & Value::Const; add Ir::Builtin to represent
globally available builtins
2026-01-10 10:27:55 +08:00
9cfffc440f refactor: tidy 2026-01-09 17:57:22 +08:00
25 changed files with 622 additions and 539 deletions

View File

@@ -5,21 +5,21 @@
import type { NixValue } from "../types"; import type { NixValue } from "../types";
export const fromJSON = (e: NixValue): never => { export const fromJSON = (e: NixValue): never => {
throw "Not implemented: fromJSON"; throw new Error("Not implemented: fromJSON");
}; };
export const fromTOML = (e: NixValue): never => { export const fromTOML = (e: NixValue): never => {
throw "Not implemented: fromTOML"; throw new Error("Not implemented: fromTOML");
}; };
export const toJSON = (e: NixValue): never => { export const toJSON = (e: NixValue): never => {
throw "Not implemented: toJSON"; throw new Error("Not implemented: toJSON");
}; };
export const toXML = (e: NixValue): never => { export const toXML = (e: NixValue): never => {
throw "Not implemented: toXML"; throw new Error("Not implemented: toXML");
}; };
export const toString = (name: NixValue, s: NixValue): never => { export const toString = (name: NixValue, s: NixValue): never => {
throw "Not implemented: toString"; throw new Error("Not implemented: toString");
}; };

View File

@@ -1,9 +1,9 @@
import type { NixValue } from "../types"; import type { NixValue } from "../types";
export const derivation = (args: NixValue) => { export const derivation = (args: NixValue) => {
throw "Not implemented: derivation"; throw new Error("Not implemented: derivation");
}; };
export const derivationStrict = (args: NixValue) => { export const derivationStrict = (args: NixValue) => {
throw "Not implemented: derivationStrict"; throw new Error("Not implemented: derivationStrict");
}; };

View File

@@ -2,8 +2,9 @@
* Functional programming builtin functions * Functional programming builtin functions
*/ */
import type { NixValue } from "../types"; import { CatchableError, type NixValue } from "../types";
import { force } from "../thunk"; import { force } from "../thunk";
import { force_string } from "../type-assert";
export const seq = export const seq =
(e1: NixValue) => (e1: NixValue) =>
@@ -15,15 +16,15 @@ export const seq =
export const deepSeq = export const deepSeq =
(e1: NixValue) => (e1: NixValue) =>
(e2: NixValue): never => { (e2: NixValue): never => {
throw "Not implemented: deepSeq"; throw new Error("Not implemented: deepSeq");
}; };
export const abort = (s: NixValue): never => { export const abort = (s: NixValue): never => {
throw `evaluation aborted with the following error message: '${force(s)}'`; throw new Error(`evaluation aborted with the following error message: '${force(s)}'`);
}; };
export const throwFunc = (s: NixValue): never => { export const throwFunc = (s: NixValue): never => {
throw force(s); throw new CatchableError(force_string(s));
}; };
export const trace = (e1: NixValue, e2: NixValue): NixValue => { export const trace = (e1: NixValue, e2: NixValue): NixValue => {

View File

@@ -249,7 +249,7 @@ export const builtins: any = {
builtins: create_thunk(() => builtins), builtins: create_thunk(() => builtins),
currentSystem: create_thunk(() => { currentSystem: create_thunk(() => {
throw "Not implemented: currentSystem"; throw new Error("Not implemented: currentSystem");
}), }),
currentTime: create_thunk(() => Date.now()), currentTime: create_thunk(() => Date.now()),

View File

@@ -25,39 +25,39 @@ export const importFunc = (path: NixValue): NixValue => {
export const scopedImport = export const scopedImport =
(scope: NixValue) => (scope: NixValue) =>
(path: NixValue): never => { (path: NixValue): never => {
throw "Not implemented: scopedImport"; throw new Error("Not implemented: scopedImport");
}; };
export const storePath = (args: NixValue): never => { export const storePath = (args: NixValue): never => {
throw "Not implemented: storePath"; throw new Error("Not implemented: storePath");
}; };
export const fetchClosure = (args: NixValue): never => { export const fetchClosure = (args: NixValue): never => {
throw "Not implemented: fetchClosure"; throw new Error("Not implemented: fetchClosure");
}; };
export const fetchMercurial = (args: NixValue): never => { export const fetchMercurial = (args: NixValue): never => {
throw "Not implemented: fetchMercurial"; throw new Error("Not implemented: fetchMercurial");
}; };
export const fetchGit = (args: NixValue): never => { export const fetchGit = (args: NixValue): never => {
throw "Not implemented: fetchGit"; throw new Error("Not implemented: fetchGit");
}; };
export const fetchTarball = (args: NixValue): never => { export const fetchTarball = (args: NixValue): never => {
throw "Not implemented: fetchTarball"; throw new Error("Not implemented: fetchTarball");
}; };
export const fetchTree = (args: NixValue): never => { export const fetchTree = (args: NixValue): never => {
throw "Not implemented: fetchTree"; throw new Error("Not implemented: fetchTree");
}; };
export const fetchurl = (args: NixValue): never => { export const fetchurl = (args: NixValue): never => {
throw "Not implemented: fetchurl"; throw new Error("Not implemented: fetchurl");
}; };
export const readDir = (path: NixValue): never => { export const readDir = (path: NixValue): never => {
throw "Not implemented: readDir"; throw new Error("Not implemented: readDir");
}; };
export const readFile = (path: NixValue): string => { export const readFile = (path: NixValue): string => {
@@ -66,7 +66,7 @@ export const readFile = (path: NixValue): string => {
}; };
export const readFileType = (path: NixValue): never => { export const readFileType = (path: NixValue): never => {
throw "Not implemented: readFileType"; throw new Error("Not implemented: readFileType");
}; };
export const pathExists = (path: NixValue): boolean => { export const pathExists = (path: NixValue): boolean => {
@@ -75,27 +75,27 @@ export const pathExists = (path: NixValue): boolean => {
}; };
export const path = (args: NixValue): never => { export const path = (args: NixValue): never => {
throw "Not implemented: path"; throw new Error("Not implemented: path");
}; };
export const toFile = (name: NixValue, s: NixValue): never => { export const toFile = (name: NixValue, s: NixValue): never => {
throw "Not implemented: toFile"; throw new Error("Not implemented: toFile");
}; };
export const toPath = (name: NixValue, s: NixValue): never => { export const toPath = (name: NixValue, s: NixValue): never => {
throw "Not implemented: toPath"; throw new Error("Not implemented: toPath");
}; };
export const filterSource = (args: NixValue): never => { export const filterSource = (args: NixValue): never => {
throw "Not implemented: filterSource"; throw new Error("Not implemented: filterSource");
}; };
export const findFile = export const findFile =
(search: NixValue) => (search: NixValue) =>
(lookup: NixValue): never => { (lookup: NixValue): never => {
throw "Not implemented: findFile"; throw new Error("Not implemented: findFile");
}; };
export const getEnv = (s: NixValue): never => { export const getEnv = (s: NixValue): never => {
throw "Not implemented: getEnv"; throw new Error("Not implemented: getEnv");
}; };

View File

@@ -2,141 +2,155 @@
* Miscellaneous unimplemented builtin functions * Miscellaneous unimplemented builtin functions
*/ */
import type { NixValue } from "../types"; import { force } from "../thunk";
import { CatchableError } from "../types";
import type { NixBool, NixStrictValue, NixValue } from "../types";
export const addErrorContext = export const addErrorContext =
(e1: NixValue) => (e1: NixValue) =>
(e2: NixValue): never => { (e2: NixValue): never => {
throw "Not implemented: addErrorContext"; throw new Error("Not implemented: addErrorContext");
}; };
export const appendContext = export const appendContext =
(e1: NixValue) => (e1: NixValue) =>
(e2: NixValue): never => { (e2: NixValue): never => {
throw "Not implemented: appendContext"; throw new Error("Not implemented: appendContext");
}; };
export const getContext = (s: NixValue): never => { export const getContext = (s: NixValue): never => {
throw "Not implemented: getContext"; throw new Error("Not implemented: getContext");
}; };
export const hasContext = (s: NixValue): never => { export const hasContext = (s: NixValue): never => {
throw "Not implemented: hasContext"; throw new Error("Not implemented: hasContext");
}; };
export const hashFile = export const hashFile =
(type: NixValue) => (type: NixValue) =>
(p: NixValue): never => { (p: NixValue): never => {
throw "Not implemented: hashFile"; throw new Error("Not implemented: hashFile");
}; };
export const hashString = export const hashString =
(type: NixValue) => (type: NixValue) =>
(p: NixValue): never => { (p: NixValue): never => {
throw "Not implemented: hashString"; throw new Error("Not implemented: hashString");
}; };
export const convertHash = (args: NixValue): never => { export const convertHash = (args: NixValue): never => {
throw "Not implemented: convertHash"; throw new Error("Not implemented: convertHash");
}; };
export const unsafeDiscardOutputDependency = (s: NixValue): never => { export const unsafeDiscardOutputDependency = (s: NixValue): never => {
throw "Not implemented: unsafeDiscardOutputDependency"; throw new Error("Not implemented: unsafeDiscardOutputDependency");
}; };
export const unsafeDiscardStringContext = (s: NixValue): never => { export const unsafeDiscardStringContext = (s: NixValue): never => {
throw "Not implemented: unsafeDiscardStringContext"; throw new Error("Not implemented: unsafeDiscardStringContext");
}; };
export const unsafeGetAttrPos = (s: NixValue): never => { export const unsafeGetAttrPos = (s: NixValue): never => {
throw "Not implemented: unsafeGetAttrPos"; throw new Error("Not implemented: unsafeGetAttrPos");
}; };
export const addDrvOutputDependencies = (s: NixValue): never => { export const addDrvOutputDependencies = (s: NixValue): never => {
throw "Not implemented: addDrvOutputDependencies"; throw new Error("Not implemented: addDrvOutputDependencies");
}; };
export const compareVersions = export const compareVersions =
(s1: NixValue) => (s1: NixValue) =>
(s2: NixValue): never => { (s2: NixValue): never => {
throw "Not implemented: compareVersions"; throw new Error("Not implemented: compareVersions");
}; };
export const dirOf = (s: NixValue): never => { export const dirOf = (s: NixValue): never => {
throw "Not implemented: dirOf"; throw new Error("Not implemented: dirOf");
}; };
export const flakeRefToString = (attrs: NixValue): never => { export const flakeRefToString = (attrs: NixValue): never => {
throw "Not implemented: flakeRefToString"; throw new Error("Not implemented: flakeRefToString");
}; };
export const functionArgs = (f: NixValue): never => { export const functionArgs = (f: NixValue): never => {
throw "Not implemented: functionArgs"; throw new Error("Not implemented: functionArgs");
}; };
export const genericClosure = (args: NixValue): never => { export const genericClosure = (args: NixValue): never => {
throw "Not implemented: genericClosure"; throw new Error("Not implemented: genericClosure");
}; };
export const getFlake = (attrs: NixValue): never => { export const getFlake = (attrs: NixValue): never => {
throw "Not implemented: getFlake"; throw new Error("Not implemented: getFlake");
}; };
export const match = export const match =
(regex: NixValue) => (regex: NixValue) =>
(str: NixValue): never => { (str: NixValue): never => {
throw "Not implemented: match"; throw new Error("Not implemented: match");
}; };
export const outputOf = export const outputOf =
(drv: NixValue) => (drv: NixValue) =>
(out: NixValue): never => { (out: NixValue): never => {
throw "Not implemented: outputOf"; throw new Error("Not implemented: outputOf");
}; };
export const parseDrvName = (s: NixValue): never => { export const parseDrvName = (s: NixValue): never => {
throw "Not implemented: parseDrvName"; throw new Error("Not implemented: parseDrvName");
}; };
export const parseFlakeName = (s: NixValue): never => { export const parseFlakeName = (s: NixValue): never => {
throw "Not implemented: parseFlakeName"; throw new Error("Not implemented: parseFlakeName");
}; };
export const parseFlakeRef = (s: NixValue): never => { export const parseFlakeRef = (s: NixValue): never => {
throw "Not implemented: parseFlakeRef"; throw new Error("Not implemented: parseFlakeRef");
}; };
export const placeholder = (output: NixValue): never => { export const placeholder = (output: NixValue): never => {
throw "Not implemented: placeholder"; throw new Error("Not implemented: placeholder");
}; };
export const replaceStrings = export const replaceStrings =
(from: NixValue) => (from: NixValue) =>
(to: NixValue) => (to: NixValue) =>
(s: NixValue): never => { (s: NixValue): never => {
throw "Not implemented: replaceStrings"; throw new Error("Not implemented: replaceStrings");
}; };
export const split = (regex: NixValue, str: NixValue): never => { export const split = (regex: NixValue, str: NixValue): never => {
throw "Not implemented: split"; throw new Error("Not implemented: split");
}; };
export const splitVersion = (s: NixValue): never => { export const splitVersion = (s: NixValue): never => {
throw "Not implemented: splitVersion"; throw new Error("Not implemented: splitVersion");
}; };
export const traceVerbose = (e1: NixValue, e2: NixValue): never => { export const traceVerbose = (e1: NixValue, e2: NixValue): never => {
throw "Not implemented: traceVerbose"; throw new Error("Not implemented: traceVerbose");
}; };
export const tryEval = export const tryEval = (e: NixValue): { success: NixBool; value: NixStrictValue } => {
(e1: NixValue) => try {
(e2: NixValue): never => { return {
throw "Not implemented: tryEval"; success: true,
}; value: force(e),
};
} catch (err) {
if (err instanceof CatchableError) {
return {
success: false,
value: false,
};
} else {
throw err;
}
}
};
export const zipAttrsWith = export const zipAttrsWith =
(f: NixValue) => (f: NixValue) =>
(list: NixValue): never => { (list: NixValue): never => {
throw "Not implemented: zipAttrsWith"; throw new Error("Not implemented: zipAttrsWith");
}; };

View File

@@ -10,6 +10,7 @@ import type {
NixInt, NixInt,
NixList, NixList,
NixNull, NixNull,
NixStrictValue,
NixString, NixString,
NixValue, NixValue,
} from "../types"; } from "../types";
@@ -39,7 +40,7 @@ export const isList = (e: NixValue): e is NixList => Array.isArray(force(e));
export const isNull = (e: NixValue): e is NixNull => force(e) === null; export const isNull = (e: NixValue): e is NixNull => force(e) === null;
export const isPath = (e: NixValue): never => { export const isPath = (e: NixValue): never => {
throw "Not implemented: isPath"; throw new Error("Not implemented: isPath");
}; };
export const isString = (e: NixValue): e is NixString => typeof force(e) === "string"; export const isString = (e: NixValue): e is NixString => typeof force(e) === "string";

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

@@ -3,7 +3,7 @@
* Implements thunks for lazy evaluation of Nix expressions * Implements thunks for lazy evaluation of Nix expressions
*/ */
import type { NixValue, NixThunkInterface } from "./types"; import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
/** /**
* Symbol used to mark objects as thunks * Symbol used to mark objects as thunks
@@ -20,12 +20,12 @@ export const IS_THUNK = Symbol("is_thunk");
export class NixThunk implements NixThunkInterface { export class NixThunk implements NixThunkInterface {
[key: symbol]: any; [key: symbol]: any;
readonly [IS_THUNK] = true as const; readonly [IS_THUNK] = true as const;
func: (() => NixValue) | null; func: (() => NixValue) | undefined;
result: Exclude<NixValue, NixThunkInterface> | null; result: NixStrictValue | undefined;
constructor(func: () => NixValue) { constructor(func: () => NixValue) {
this.func = func; this.func = func;
this.result = null; this.result = undefined;
} }
} }
@@ -46,20 +46,20 @@ export const is_thunk = (value: unknown): value is NixThunkInterface => {
* @param value - Value to force (may be a thunk) * @param value - Value to force (may be a thunk)
* @returns The forced/evaluated value * @returns The forced/evaluated value
*/ */
export const force = (value: NixValue): Exclude<NixValue, NixThunkInterface> => { export const force = (value: NixValue): NixStrictValue => {
if (!is_thunk(value)) { if (!is_thunk(value)) {
return value; return value;
} }
// Already evaluated - return cached result // Already evaluated - return cached result
if (value.func === null) { if (value.func === undefined) {
return value.result!; return value.result!;
} }
// Evaluate and cache // Evaluate and cache
const result = force(value.func()); const result = force(value.func());
value.result = result; value.result = result;
value.func = null; value.func = undefined;
return result; return result;
}; };

View File

@@ -20,7 +20,7 @@ const typeName = (value: NixValue): string => {
if (typeof val === "object") return "attribute set"; if (typeof val === "object") return "attribute set";
throw new TypeError(`Unknown Nix type: ${typeof val}`); throw new TypeError(`Unknown Nix type: ${typeof val}`);
} };
/** /**
* Force a value and assert it's a list * Force a value and assert it's a list
@@ -52,7 +52,7 @@ export const force_function = (value: NixValue): NixFunction => {
*/ */
export const force_attrs = (value: NixValue): NixAttrs => { export const force_attrs = (value: NixValue): NixAttrs => {
const forced = force(value); const forced = force(value);
if (!isAttrs(forced)) { if (typeof forced !== "object" || Array.isArray(forced) || forced === null) {
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`); throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
} }
return forced; return forced;

View File

@@ -2,6 +2,8 @@
* Core TypeScript type definitions for nix-js runtime * Core TypeScript type definitions for nix-js runtime
*/ */
import { IS_THUNK } from "./thunk";
// Nix primitive types // Nix primitive types
export type NixInt = bigint; export type NixInt = bigint;
export type NixFloat = number; export type NixFloat = number;
@@ -20,9 +22,9 @@ export type NixFunction = (...args: any[]) => any;
* Thunks delay evaluation until forced * Thunks delay evaluation until forced
*/ */
export interface NixThunkInterface { export interface NixThunkInterface {
readonly [key: symbol]: true; // IS_THUNK marker readonly [IS_THUNK]: true;
func: (() => NixValue) | null; func: (() => NixValue) | undefined;
result: Exclude<NixValue, NixThunkInterface> | null; result: NixStrictValue | undefined;
} }
// Union of all Nix primitive types // Union of all Nix primitive types
@@ -34,6 +36,18 @@ export type NixPrimitive = NixNull | NixBool | NixInt | NixFloat | NixString;
*/ */
export type NixValue = NixPrimitive | NixList | NixAttrs | NixFunction | NixThunkInterface; export type NixValue = NixPrimitive | NixList | NixAttrs | NixFunction | NixThunkInterface;
export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
/**
* CatchableError: Error type thrown by `builtins.throw`
* This can be caught by `builtins.tryEval`
*/
export class CatchableError extends Error {
constructor(msg: string) {
super(msg);
}
}
// Operator function signatures // Operator function signatures
export type BinaryOp<T = NixValue, U = NixValue, R = NixValue> = (a: T, b: U) => R; export type BinaryOp<T = NixValue, U = NixValue, R = NixValue> = (a: T, b: U) => R;
export type UnaryOp<T = NixValue, R = NixValue> = (a: T) => R; export type UnaryOp<T = NixValue, R = NixValue> = (a: T) => R;

View File

@@ -1,7 +1,5 @@
import type { NixRuntime } from ".."; import type { NixRuntime } from "..";
export {};
declare global { declare global {
var Nix: NixRuntime; var Nix: NixRuntime;
namespace Deno { namespace Deno {

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

@@ -2,11 +2,11 @@ use itertools::Itertools as _;
use crate::ir::*; use crate::ir::*;
pub trait Compile<Ctx: CodegenContext> { pub(crate) trait Compile<Ctx: CodegenContext> {
fn compile(&self, ctx: &Ctx) -> String; fn compile(&self, ctx: &Ctx) -> String;
} }
pub trait CodegenContext { pub(crate) trait CodegenContext {
fn get_ir(&self, id: ExprId) -> &Ir; fn get_ir(&self, id: ExprId) -> &Ir;
fn get_sym(&self, id: SymId) -> &str; fn get_sym(&self, id: SymId) -> &str;
} }
@@ -14,22 +14,24 @@ pub trait CodegenContext {
impl<Ctx: CodegenContext> Compile<Ctx> for Ir { impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
fn compile(&self, ctx: &Ctx) -> String { fn compile(&self, ctx: &Ctx) -> String {
match self { match self {
Ir::Const(Const { val }) => match val { Ir::Int(int) => format!("{int}n"), // Generate BigInt literal
crate::value::Const::Null => "null".to_string(), Ir::Float(float) => float.to_string(),
crate::value::Const::Int(val) => format!("{}n", val), // Generate BigInt literal
crate::value::Const::Float(val) => val.to_string(),
crate::value::Const::Bool(val) => val.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
@@ -63,6 +65,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
format!("expr{}", expr_id.0) format!("expr{}", expr_id.0)
} }
Ir::Builtins(_) => "Nix.builtins".to_string(), Ir::Builtins(_) => "Nix.builtins".to_string(),
&Ir::Builtin(Builtin(name)) => format!("Nix.builtins[\"{}\"]", ctx.get_sym(name)),
Ir::ConcatStrings(x) => x.compile(ctx), Ir::ConcatStrings(x) => x.compile(ctx),
Ir::HasAttr(x) => x.compile(ctx), Ir::HasAttr(x) => x.compile(ctx),
&Ir::Assert(Assert { assertion, expr }) => { &Ir::Assert(Assert { assertion, expr }) => {
@@ -91,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),
@@ -205,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
@@ -222,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
@@ -292,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

@@ -8,7 +8,7 @@ use string_interner::DefaultStringInterner;
use crate::codegen::{CodegenContext, Compile}; use crate::codegen::{CodegenContext, Compile};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::ir::{DowngradeContext, ExprId, Ir, SymId}; use crate::ir::{Builtin, DowngradeContext, ExprId, Ir, SymId};
use crate::runtime::Runtime; use crate::runtime::Runtime;
use crate::value::Value; use crate::value::Value;
@@ -18,43 +18,58 @@ mod downgrade;
pub struct Context { pub struct Context {
ctx: Pin<Box<Ctx>>, ctx: Pin<Box<Ctx>>,
pub(crate) runtime: Runtime, runtime: Runtime,
} }
impl Default for Context { pub(crate) struct CtxPtr(NonNull<Ctx>);
fn default() -> Self { impl CtxPtr {
Self::new() pub(crate) unsafe fn as_ref(&self) -> &Ctx {
unsafe { self.0.as_ref() }
}
pub(crate) unsafe fn as_mut(&mut self) -> Pin<&mut Ctx> {
unsafe { Pin::new_unchecked(self.0.as_mut()) }
} }
} }
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 { 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("; ")));
} }
let root = ctx.as_mut().downgrade_ctx().downgrade(root.tree().expr().unwrap())?;
#[allow(clippy::unwrap_used)]
// Always `Some` since there is no parse error
let root = ctx
.as_mut()
.downgrade_ctx()
.downgrade(root.tree().expr().unwrap())?;
let code = ctx.get_ir(root).compile(Pin::get_ref(ctx.as_ref())); let code = ctx.get_ir(root).compile(Pin::get_ref(ctx.as_ref()));
let code = format!("Nix.force({})", code); let code = format!("Nix.force({})", code);
println!("[DEBUG] generated code: {}", &code); println!("[DEBUG] generated code: {}", &code);
self.runtime.eval(code) self.runtime.eval(code)
} }
#[allow(dead_code)]
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
self.runtime.eval(code)
}
} }
#[pin_project::pin_project(PinnedDrop)] #[pin_project::pin_project(PinnedDrop)]
pub struct Ctx { pub(crate) struct Ctx {
irs: Vec<Ir>, irs: Vec<Ir>,
symbols: DefaultStringInterner, symbols: DefaultStringInterner,
global: NonNull<HashMap<SymId, ExprId>>, global: NonNull<HashMap<SymId, ExprId>>,
@@ -72,22 +87,23 @@ impl PinnedDrop for Ctx {
} }
} }
pub struct PathDropGuard<'ctx> { pub(crate) struct PathDropGuard<'ctx> {
ctx: Pin<&'ctx mut Ctx>, ctx: Pin<&'ctx mut Ctx>,
} }
impl<'ctx> PathDropGuard<'ctx> { impl<'ctx> PathDropGuard<'ctx> {
pub fn new(path: PathBuf, mut ctx: Pin<&'ctx mut Ctx>) -> Self { pub(crate) fn new(path: PathBuf, mut ctx: Pin<&'ctx mut Ctx>) -> Self {
ctx.as_mut().project().path_stack.push(path); ctx.as_mut().project().path_stack.push(path);
Self { ctx } Self { ctx }
} }
pub 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 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
} }
} }
@@ -100,7 +116,7 @@ impl Drop for PathDropGuard<'_> {
impl Default for Ctx { impl Default for Ctx {
fn default() -> Self { fn default() -> Self {
use crate::ir::{Attr, Builtins, Select, ToIr}; use crate::ir::{Builtins, ToIr as _};
let mut symbols = DefaultStringInterner::new(); let mut symbols = DefaultStringInterner::new();
let mut irs = Vec::new(); let mut irs = Vec::new();
@@ -139,14 +155,9 @@ impl Default for Ctx {
for name in free_globals { for name in free_globals {
let name_sym = symbols.get_or_intern(name); let name_sym = symbols.get_or_intern(name);
let select_ir = Select { let id = ExprId(irs.len());
expr: builtins_expr, irs.push(Builtin(name_sym).to_ir());
attrpath: vec![Attr::Str(name_sym)], global.insert(name_sym, id);
default: None,
};
let select_expr = ExprId(irs.len());
irs.push(select_ir.to_ir());
global.insert(name_sym, select_expr);
} }
Self { Self {
@@ -160,98 +171,108 @@ impl Default for Ctx {
} }
impl Ctx { impl Ctx {
pub fn new() -> Self { pub(crate) fn new() -> Self {
Self::default() Self::default()
} }
pub fn downgrade_ctx<'a>(self: Pin<&'a mut Self>) -> DowngradeCtx<'a> { pub(crate) fn downgrade_ctx<'a>(self: Pin<&'a mut Self>) -> DowngradeCtx<'a> {
// SAFETY: `global` is readonly
let global_ref = unsafe { self.global.as_ref() }; let global_ref = unsafe { self.global.as_ref() };
DowngradeCtx::new(self, global_ref) DowngradeCtx::new(self, global_ref)
} }
pub 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;
use super::*; use super::*;
use crate::value::{AttrSet, Const, List, Symbol}; use crate::value::{AttrSet, List, Symbol};
#[test] #[test]
fn basic_eval() { fn basic_eval() {
assert_eq!( assert_eq!(
Context::new().eval_code("1 + 1").unwrap(), Context::new().unwrap().eval_code("1 + 1").unwrap(),
Value::Const(Const::Int(2)) Value::Int(2)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("(x: x) 1").unwrap(), Context::new().unwrap().eval_code("(x: x) 1").unwrap(),
Value::Const(Const::Int(1)) Value::Int(1)
);
assert_eq!(
Context::new().eval_code("(x: y: x - y) 2 1").unwrap(),
Value::Const(Const::Int(1))
);
assert_eq!(
Context::new().eval_code("rec { b = a; a = 1; }.b").unwrap(),
Value::Const(Const::Int(1))
);
assert_eq!(
Context::new().eval_code("let b = a; a = 1; in b").unwrap(),
Value::Const(Const::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(),
Value::Const(Const::Int(832040))
); );
assert_eq!( assert_eq!(
Context::new() Context::new()
.unwrap()
.eval_code("(x: y: x - y) 2 1")
.unwrap(),
Value::Int(1)
);
assert_eq!(
Context::new()
.unwrap()
.eval_code("rec { b = a; a = 1; }.b")
.unwrap(),
Value::Int(1)
);
assert_eq!(
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") .eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y")
.unwrap(), .unwrap(),
Value::Const(Const::Int(2)) Value::Int(2)
); );
} }
#[test] #[test]
fn test_binop() { fn test_binop() {
let tests = [ let tests = [
("1 + 1", Value::Const(Const::Int(2))), ("1 + 1", Value::Int(2)),
("2 - 1", Value::Const(Const::Int(1))), ("2 - 1", Value::Int(1)),
("1. * 1", Value::Const(Const::Float(1.))), ("1. * 1", Value::Float(1.)),
("1 / 1.", Value::Const(Const::Float(1.))), ("1 / 1.", Value::Float(1.)),
("1 == 1", Value::Const(Const::Bool(true))), ("1 == 1", Value::Bool(true)),
("1 != 1", Value::Const(Const::Bool(false))), ("1 != 1", Value::Bool(false)),
("2 < 1", Value::Const(Const::Bool(false))), ("2 < 1", Value::Bool(false)),
("2 > 1", Value::Const(Const::Bool(true))), ("2 > 1", Value::Bool(true)),
("1 <= 1", Value::Const(Const::Bool(true))), ("1 <= 1", Value::Bool(true)),
("1 >= 1", Value::Const(Const::Bool(true))), ("1 >= 1", Value::Bool(true)),
// Short-circuit evaluation: true || <expr> should not evaluate <expr> // Short-circuit evaluation: true || <expr> should not evaluate <expr>
("true || (1 / 0)", Value::Const(Const::Bool(true))), ("true || (1 / 0)", Value::Bool(true)),
("true && 1 == 0", Value::Const(Const::Bool(false))), ("true && 1 == 0", Value::Bool(false)),
( (
"[ 1 2 3 ] ++ [ 4 5 6 ]", "[ 1 2 3 ] ++ [ 4 5 6 ]",
Value::List(List::new( Value::List(List::new((1..=6).map(Value::Int).collect())),
(1..=6).map(Const::Int).map(Value::Const).collect(),
)),
), ),
( (
"{ a.b = 1; b = 2; } // { a.c = 2; }", "{ a.b = 1; b = 2; } // { a.c = 2; }",
@@ -260,15 +281,15 @@ mod test {
Symbol::from("a"), Symbol::from("a"),
Value::AttrSet(AttrSet::new(BTreeMap::from([( Value::AttrSet(AttrSet::new(BTreeMap::from([(
Symbol::from("c"), Symbol::from("c"),
Value::Const(Const::Int(2)), Value::Int(2),
)]))), )]))),
), ),
(Symbol::from("b"), Value::Const(Const::Int(2))), (Symbol::from("b"), Value::Int(2)),
]))), ]))),
), ),
]; ];
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);
} }
} }
@@ -277,36 +298,43 @@ 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::Const(Const::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::Const(Const::Int(6)) Value::Int(6)
); );
} }
#[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::Const(Const::Int(3)) Value::Int(3)
); );
} }
@@ -315,17 +343,19 @@ 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::Const(Const::Int(6)) Value::Int(6)
); );
// 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::Const(Const::Int(11)) Value::Int(11)
); );
} }
@@ -334,9 +364,10 @@ 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::Const(Const::Int(3)) Value::Int(3)
); );
} }
@@ -345,22 +376,23 @@ 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::Const(Const::Int(3)) Value::Int(3)
); );
// 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::Const(Const::Int(42)) Value::Int(42)
); );
} }
#[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(_)));
} }
@@ -368,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(_)));
} }
@@ -376,8 +411,11 @@ 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()
Value::Const(Const::Int(3)) .unwrap()
.eval_code("builtins.add 1 2")
.unwrap(),
Value::Int(3)
); );
} }
@@ -385,8 +423,11 @@ 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()
Value::Const(Const::Int(3)) .unwrap()
.eval_code("builtins.length [1 2 3]")
.unwrap(),
Value::Int(3)
); );
} }
@@ -394,12 +435,13 @@ 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()
Value::List(List::new(vec![ .unwrap()
Value::Const(Const::Int(2)), .eval_code("map (x: x * 2) [1 2 3]")
Value::Const(Const::Int(4)), .unwrap(),
Value::Const(Const::Int(6)), Value::List(List::new(
])) vec![Value::Int(2), Value::Int(4), Value::Int(6),]
))
); );
} }
@@ -408,12 +450,10 @@ 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::List(List::new(vec![Value::Int(2), Value::Int(3),]))
Value::Const(Const::Int(2)),
Value::Const(Const::Int(3)),
]))
); );
} }
@@ -421,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
@@ -435,8 +476,11 @@ 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()
Value::Const(Const::Int(1)) .unwrap()
.eval_code("builtins.head [1 2 3]")
.unwrap(),
Value::Int(1)
); );
} }
@@ -444,11 +488,11 @@ 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()
Value::List(List::new(vec![ .unwrap()
Value::Const(Const::Int(2)), .eval_code("builtins.tail [1 2 3]")
Value::Const(Const::Int(3)), .unwrap(),
])) Value::List(List::new(vec![Value::Int(2), Value::Int(3),]))
); );
} }
@@ -457,9 +501,10 @@ 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::Const(Const::Int(8)) Value::Int(8)
); );
} }
@@ -468,9 +513,10 @@ 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::Const(Const::Int(30)) Value::Int(30)
); );
} }
@@ -479,9 +525,10 @@ 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::Const(Const::Int(11)) // (2*3) + (10-5) = 6 + 5 = 11 Value::Int(11) // (2*3) + (10-5 = 6 + 5 = 11
); );
} }
@@ -489,28 +536,39 @@ 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()
Value::Const(Const::Bool(true)) .unwrap()
.eval_code("builtins.isList [1 2 3]")
.unwrap(),
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::Const(Const::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::Const(Const::Bool(true)) Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("builtins.isNull null").unwrap(), Context::new()
Value::Const(Const::Bool(true)) .unwrap()
.eval_code("builtins.isNull null")
.unwrap(),
Value::Bool(true)
); );
assert_eq!( assert_eq!(
Context::new().eval_code("builtins.isBool true").unwrap(), Context::new()
Value::Const(Const::Bool(true)) .unwrap()
.eval_code("builtins.isBool true")
.unwrap(),
Value::Bool(true)
); );
} }
@@ -519,9 +577,10 @@ 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::Const(Const::Int(2)) // Uses shadowed version Value::Int(2) // Uses shadowed version
); );
} }
@@ -530,33 +589,34 @@ 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::Const(Const::Int(2))); assert_eq!(result, Value::Int(2));
} }
// Free globals tests // Free globals tests
#[test] #[test]
fn test_free_global_true() { fn test_free_global_true() {
assert_eq!( assert_eq!(
Context::new().eval_code("true").unwrap(), Context::new().unwrap().eval_code("true").unwrap(),
Value::Const(Const::Bool(true)) 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::Const(Const::Bool(false)) Value::Bool(false)
); );
} }
#[test] #[test]
fn test_free_global_null() { fn test_free_global_null() {
assert_eq!( assert_eq!(
Context::new().eval_code("null").unwrap(), Context::new().unwrap().eval_code("null").unwrap(),
Value::Const(Const::Null) Value::Null
); );
} }
@@ -564,12 +624,13 @@ mod 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()
Value::List(List::new(vec![ .unwrap()
Value::Const(Const::Int(2)), .eval_code("map (x: x * 2) [1 2 3]")
Value::Const(Const::Int(4)), .unwrap(),
Value::Const(Const::Int(6)), Value::List(List::new(
])) vec![Value::Int(2), Value::Int(4), Value::Int(6),]
))
); );
} }
@@ -577,12 +638,12 @@ 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::Const(Const::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::Const(Const::Bool(false)) Value::Bool(false)
); );
} }
@@ -591,15 +652,17 @@ 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::Const(Const::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::Const(Const::Int(1)) Value::Int(1)
); );
} }
@@ -608,12 +671,10 @@ 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::List(List::new(vec![Value::Int(2), Value::Int(3),]))
Value::Const(Const::Int(2)),
Value::Const(Const::Int(3)),
]))
); );
} }
@@ -622,64 +683,65 @@ 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::Const(Const::Bool(false)) Value::Bool(false)
); );
} }
// 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!(
ctx.eval_code("9223372036854775807").unwrap(), ctx.eval_code("9223372036854775807").unwrap(),
Value::Const(Const::Int(9223372036854775807)) Value::Int(9223372036854775807)
); );
// Test negative large value // Test negative large value
// Can't use -9223372036854775808 since unary minus is actually desugared to (0 - <expr>) // Can't use -9223372036854775808 since unary minus is actually desugared to (0 - <expr>)
assert_eq!( assert_eq!(
ctx.eval_code("-9223372036854775807").unwrap(), ctx.eval_code("-9223372036854775807").unwrap(),
Value::Const(Const::Int(-9223372036854775807)) Value::Int(-9223372036854775807)
); );
// Test large number arithmetic // Test large number arithmetic
assert_eq!( assert_eq!(
ctx.eval_code("5000000000000000000 + 3000000000000000000") ctx.eval_code("5000000000000000000 + 3000000000000000000")
.unwrap(), .unwrap(),
Value::Const(Const::Int(8000000000000000000i64)) Value::Int(8000000000000000000i64)
); );
} }
#[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!(
ctx.eval_code("builtins.isInt 42").unwrap(), ctx.eval_code("builtins.isInt 42").unwrap(),
Value::Const(Const::Bool(true)) Value::Bool(true)
); );
assert_eq!( assert_eq!(
ctx.eval_code("builtins.isInt 42.0").unwrap(), ctx.eval_code("builtins.isInt 42.0").unwrap(),
Value::Const(Const::Bool(false)) Value::Bool(false)
); );
// isFloat tests // isFloat tests
assert_eq!( assert_eq!(
ctx.eval_code("builtins.isFloat 42").unwrap(), ctx.eval_code("builtins.isFloat 42").unwrap(),
Value::Const(Const::Bool(false)) Value::Bool(false)
); );
assert_eq!( assert_eq!(
ctx.eval_code("builtins.isFloat 42.5").unwrap(), ctx.eval_code("builtins.isFloat 42.5").unwrap(),
Value::Const(Const::Bool(true)) Value::Bool(true)
); );
assert_eq!( assert_eq!(
ctx.eval_code("builtins.isFloat 1.0").unwrap(), ctx.eval_code("builtins.isFloat 1.0").unwrap(),
Value::Const(Const::Bool(true)) Value::Bool(true)
); );
// typeOf tests // typeOf tests
@@ -697,13 +759,13 @@ mod test {
); );
// literal tests // literal tests
assert_eq!(ctx.eval_code("1").unwrap(), Value::Const(Const::Int(1))); assert_eq!(ctx.eval_code("1").unwrap(), Value::Int(1));
assert_eq!(ctx.eval_code("1.").unwrap(), Value::Const(Const::Float(1.))) assert_eq!(ctx.eval_code("1.").unwrap(), Value::Float(1.))
} }
#[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!(
@@ -732,55 +794,43 @@ 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::Const(Const::Int(2))); assert_eq!(ctx.eval_code("5 / 2").unwrap(), Value::Int(2));
assert_eq!(ctx.eval_code("7 / 3").unwrap(), Value::Const(Const::Int(2))); assert_eq!(ctx.eval_code("7 / 3").unwrap(), Value::Int(2));
assert_eq!( assert_eq!(ctx.eval_code("10 / 3").unwrap(), Value::Int(3));
ctx.eval_code("10 / 3").unwrap(),
Value::Const(Const::Int(3))
);
// Float division returns float // Float division returns float
assert_eq!( assert_eq!(ctx.eval_code("5 / 2.0").unwrap(), Value::Float(2.5));
ctx.eval_code("5 / 2.0").unwrap(),
Value::Const(Const::Float(2.5))
);
assert_eq!( assert_eq!(ctx.eval_code("7.0 / 2").unwrap(), Value::Float(3.5));
ctx.eval_code("7.0 / 2").unwrap(),
Value::Const(Const::Float(3.5))
);
assert_eq!( assert_eq!(ctx.eval_code("(-7) / 3").unwrap(), Value::Int(-2));
ctx.eval_code("(-7) / 3").unwrap(),
Value::Const(Const::Int(-2))
);
} }
#[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!(
ctx.eval_code("builtins.add 5000000000000000000 3000000000000000000") ctx.eval_code("builtins.add 5000000000000000000 3000000000000000000")
.unwrap(), .unwrap(),
Value::Const(Const::Int(8000000000000000000i64)) Value::Int(8000000000000000000i64)
); );
// Test builtin mul with large numbers // Test builtin mul with large numbers
assert_eq!( assert_eq!(
ctx.eval_code("builtins.mul 1000000000 1000000000").unwrap(), ctx.eval_code("builtins.mul 1000000000 1000000000").unwrap(),
Value::Const(Const::Int(1000000000000000000i64)) Value::Int(1000000000000000000i64)
); );
} }
#[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");
@@ -788,12 +838,12 @@ mod test {
std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap(); std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap();
let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display()); let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(8))); assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(8));
} }
#[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();
@@ -808,12 +858,12 @@ mod test {
std::fs::write(&main_path, main_content).unwrap(); std::fs::write(&main_path, main_content).unwrap();
let expr = format!(r#"(import "{}").result"#, main_path.display()); let expr = format!(r#"(import "{}").result"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(30))); assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(30));
} }
#[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");
@@ -827,32 +877,32 @@ mod test {
let main_path = temp_dir.path().join("main.nix"); let main_path = temp_dir.path().join("main.nix");
let main_content = r#" let main_content = r#"
let let
lib = import ./lib.nix; lib = import ./lib.nix;
helper = import ./subdir/helper.nix; helper = import ./subdir/helper.nix;
in { in {
result1 = lib.multiply 3 4; result1 = lib.multiply 3 4;
result2 = helper.subtract 10 3; result2 = helper.subtract 10 3;
} }
"#; "#;
std::fs::write(&main_path, main_content).unwrap(); std::fs::write(&main_path, main_content).unwrap();
let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display()); let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(12))); assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(12));
let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display()); let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(7))); assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(7));
} }
#[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");
std::fs::write(&func_path, "x: x * 2").unwrap(); std::fs::write(&func_path, "x: x * 2").unwrap();
let expr = format!(r#"(import "{}") 5"#, func_path.display()); let expr = format!(r#"(import "{}") 5"#, func_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(10))); assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(10));
} }
} }

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

@@ -4,7 +4,6 @@ use rnix::ast;
use string_interner::symbol::SymbolU32; use string_interner::symbol::SymbolU32;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::value::Const as PubConst;
use crate::value::format_symbol; use crate::value::format_symbol;
use nix_js_macros::ir; use nix_js_macros::ir;
@@ -44,8 +43,12 @@ pub trait DowngradeContext {
ir! { ir! {
Ir, Ir,
Int(i64),
Float(f64),
Str,
AttrSet, AttrSet,
List, List,
HasAttr, HasAttr,
BinOp, BinOp,
UnOp, UnOp,
@@ -54,8 +57,6 @@ ir! {
Call, Call,
Assert, Assert,
ConcatStrings, ConcatStrings,
Const,
Str,
Path, Path,
Func, Func,
Let, Let,
@@ -63,6 +64,7 @@ ir! {
ExprRef(ExprId), ExprRef(ExprId),
Thunk(ExprId), Thunk(ExprId),
Builtins, Builtins,
Builtin,
} }
impl AttrSet { impl AttrSet {
@@ -139,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)
} }
} }
@@ -354,18 +358,6 @@ pub struct ConcatStrings {
pub parts: Vec<ExprId>, pub parts: Vec<ExprId>,
} }
/// Represents a constant value (e.g., integer, float, boolean, null).
#[derive(Clone, Copy, Debug)]
pub struct Const {
pub val: PubConst,
}
impl<T: Into<PubConst>> From<T> for Const {
fn from(value: T) -> Self {
Self { val: value.into() }
}
}
/// Represents a simple, non-interpolated string literal. /// Represents a simple, non-interpolated string literal.
#[derive(Debug)] #[derive(Debug)]
pub struct Str { pub struct Str {
@@ -386,4 +378,4 @@ pub struct Builtins;
/// Represents an attribute in `builtins`. /// Represents an attribute in `builtins`.
#[derive(Debug)] #[derive(Debug)]
pub struct Builtin(pub String); pub struct Builtin(pub SymId);

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};
@@ -139,8 +142,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Literal { impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> { fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
Ok(ctx.new_expr(match self.kind() { Ok(ctx.new_expr(match self.kind() {
ast::LiteralKind::Integer(int) => Const::from(int.value().unwrap()).to_ir(), ast::LiteralKind::Integer(int) => Ir::Int(int.value().unwrap()),
ast::LiteralKind::Float(float) => Const::from(float.value().unwrap()).to_ir(), ast::LiteralKind::Float(float) => Ir::Float(float.value().unwrap()),
ast::LiteralKind::Uri(uri) => Str { ast::LiteralKind::Uri(uri) => Str {
val: uri.to_string(), val: uri.to_string(),
} }

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,8 +1,10 @@
pub mod codegen; #![warn(clippy::unwrap_used)]
mod codegen;
pub mod context; pub mod context;
pub mod error; pub mod error;
pub mod ir; pub mod ir;
pub mod runtime; mod runtime;
pub mod value; pub mod value;
#[global_allocator] #[global_allocator]

View File

@@ -1,89 +1,90 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell;
use std::pin::Pin; use std::pin::Pin;
use std::ptr::NonNull;
use std::sync::Once; use std::sync::Once;
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpDecl, OpState, RuntimeOptions, v8};
use deno_error::js_error_wrapper; use deno_error::JsErrorClass;
use crate::codegen::{CodegenContext, Compile}; use crate::codegen::{CodegenContext, Compile};
use crate::context::{Ctx, PathDropGuard}; use crate::context::{CtxPtr, PathDropGuard};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::ir::DowngradeContext; use crate::ir::DowngradeContext;
use crate::value::{AttrSet, Const, List, Symbol, Value}; use crate::value::{AttrSet, List, Symbol, Value};
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>; type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
type LocalValue<'a> = v8::Local<'a, v8::Value>;
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
fn runtime_extension(ctx: NonNull<Ctx>) -> Extension { fn runtime_extension(ctx: CtxPtr) -> Extension {
const ESM: &[ExtensionFileSource] = const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js"); &deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
const OPS: &[OpDecl] = &[
op_import(),
op_read_file(),
op_path_exists(),
op_resolve_path(),
];
Extension { Extension {
name: "nix_runtime", name: "nix_runtime",
esm_files: Cow::Borrowed(ESM), esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"), esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Owned(vec![ ops: Cow::Borrowed(OPS),
op_import(),
op_read_file(),
op_path_exists(),
op_resolve_path(),
]),
op_state_fn: Some(Box::new(move |state| { op_state_fn: Some(Box::new(move |state| {
state.put(RefCell::new(ctx)); state.put(ctx);
})), })),
enabled: true, enabled: true,
..Default::default() ..Default::default()
} }
} }
#[derive(Debug)] mod private {
pub struct SimpleErrorWrapper(String); use deno_error::js_error_wrapper;
#[allow(dead_code)]
#[derive(Debug)]
pub struct SimpleErrorWrapper(pub(crate) String);
impl std::fmt::Display for SimpleErrorWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for SimpleErrorWrapper {}
impl std::fmt::Display for SimpleErrorWrapper { js_error_wrapper!(SimpleErrorWrapper, NixError, "Error");
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f) impl From<String> for NixError {
fn from(value: String) -> Self {
NixError(SimpleErrorWrapper(value))
}
}
impl From<&str> for NixError {
fn from(value: &str) -> Self {
NixError(SimpleErrorWrapper(value.to_string()))
}
} }
} }
use private::NixError;
impl std::error::Error for SimpleErrorWrapper {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
fn description(&self) -> &str {
&self.0
}
}
impl From<String> for NixError {
fn from(value: String) -> Self {
NixError(SimpleErrorWrapper(value))
}
}
js_error_wrapper!(SimpleErrorWrapper, NixError, "EvalError");
#[deno_core::op2] #[deno_core::op2]
#[string] #[string]
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> { fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
let mut ptr = state.borrow::<RefCell<NonNull<Ctx>>>().borrow_mut(); let ptr = state.borrow_mut::<CtxPtr>();
let ctx = unsafe { Pin::new_unchecked(ptr.as_mut()) }; let ctx = unsafe { ptr.as_mut() };
let current_dir = ctx.get_current_dir(); let current_dir = ctx.get_current_dir();
let absolute_path = current_dir let mut absolute_path = current_dir
.join(&path) .join(&path)
.canonicalize() .canonicalize()
.map_err(|e| -> NixError { format!("Failed to resolve path {}: {}", path, e).into() })?; .map_err(|e| format!("Failed to resolve path {}: {}", path, e))?;
if absolute_path.is_dir() {
absolute_path.push("default.nix")
}
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx); let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
let ctx = guard.as_ctx(); let ctx = guard.as_ctx();
let content = std::fs::read_to_string(&absolute_path).map_err(|e| -> NixError { let content = std::fs::read_to_string(&absolute_path)
format!("Failed to read {}: {}", absolute_path.display(), e).into() .map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
})?;
let root = rnix::Root::parse(&content); let root = rnix::Root::parse(&content);
if !root.errors().is_empty() { if !root.errors().is_empty() {
@@ -95,15 +96,12 @@ fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result
.into()); .into());
} }
let expr = root let expr = root.tree().expr().ok_or("No expression in file")?;
.tree()
.expr()
.ok_or_else(|| -> NixError { "No expression in file".to_string().into() })?;
let expr_id = ctx let expr_id = ctx
.as_mut() .as_mut()
.downgrade_ctx() .downgrade_ctx()
.downgrade(expr) .downgrade(expr)
.map_err(|e| -> NixError { format!("Downgrade error: {}", e).into() })?; .map_err(|e| format!("Downgrade error: {}", e))?;
Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref()))) Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref())))
} }
@@ -111,8 +109,7 @@ fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result
#[deno_core::op2] #[deno_core::op2]
#[string] #[string]
fn op_read_file(#[string] path: String) -> std::result::Result<String, NixError> { fn op_read_file(#[string] path: String) -> std::result::Result<String, NixError> {
std::fs::read_to_string(&path) Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?)
.map_err(|e| -> NixError { format!("Failed to read {}: {}", path, e).into() })
} }
#[deno_core::op2(fast)] #[deno_core::op2(fast)]
@@ -126,7 +123,7 @@ fn op_resolve_path(
state: &mut OpState, state: &mut OpState,
#[string] path: String, #[string] path: String,
) -> std::result::Result<String, NixError> { ) -> std::result::Result<String, NixError> {
let ptr = state.borrow::<RefCell<NonNull<Ctx>>>().borrow(); let ptr = state.borrow::<CtxPtr>();
let ctx = unsafe { ptr.as_ref() }; let ctx = unsafe { ptr.as_ref() };
// If already absolute, return as-is // If already absolute, return as-is
@@ -137,21 +134,21 @@ fn op_resolve_path(
// Resolve relative path against current file directory (or CWD) // Resolve relative path against current file directory (or CWD)
let current_dir = ctx.get_current_dir(); let current_dir = ctx.get_current_dir();
current_dir Ok(current_dir
.join(&path) .join(&path)
.canonicalize() .canonicalize()
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.map_err(|e| -> NixError { format!("Failed to resolve path {}: {}", path, e).into() }) .map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
} }
pub struct Runtime { pub(crate) struct Runtime {
js_runtime: JsRuntime, js_runtime: JsRuntime,
is_thunk_symbol: v8::Global<v8::Symbol>, is_thunk_symbol: v8::Global<v8::Symbol>,
primop_metadata_symbol: v8::Global<v8::Symbol>, primop_metadata_symbol: v8::Global<v8::Symbol>,
} }
impl Runtime { impl Runtime {
pub fn new(ctx: NonNull<Ctx>) -> 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(|| {
@@ -168,21 +165,21 @@ 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 fn eval(&mut self, script: String) -> Result<Value> { pub(crate) fn eval(&mut self, script: String) -> Result<Value> {
let global_value = self let global_value = self
.js_runtime .js_runtime
.execute_script("<eval>", script) .execute_script("<eval>", script)
.map_err(|e| Error::eval_error(format!("Execution error: {:?}", e)))?; .map_err(|e| Error::eval_error(format!("{}", e.get_message())))?;
// Retrieve scope from JsRuntime // Retrieve scope from JsRuntime
deno_core::scope!(scope, self.js_runtime); deno_core::scope!(scope, self.js_runtime);
@@ -190,77 +187,100 @@ impl Runtime {
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol); let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol); let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
Ok(to_value(local_value, scope, is_thunk_symbol, primop_metadata_symbol)) Ok(to_value(
local_value,
scope,
is_thunk_symbol,
primop_metadata_symbol,
))
} }
/// get (IS_THUNK, PRIMOP_METADATA) /// get (IS_THUNK, PRIMOP_METADATA)
fn get_symbols( fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global<v8::Symbol>, v8::Global<v8::Symbol>)> {
scope: &v8::PinnedRef<'_, v8::HandleScope<'_>>,
) -> (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))
} }
} }
fn to_value<'a>( fn to_value<'a>(
val: v8::Local<'a, v8::Value>, val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>, scope: &ScopeRef<'a, '_>,
is_thunk_symbol: v8::Local<'a, v8::Symbol>, is_thunk_symbol: LocalSymbol<'a>,
primop_metadata_symbol: v8::Local<'a, v8::Symbol>, primop_metadata_symbol: LocalSymbol<'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::Const(Const::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::Const(Const::Float(val)) Value::Float(val)
} }
_ if val.is_true() => Value::Const(Const::Bool(true)), _ if val.is_true() => Value::Bool(true),
_ if val.is_false() => Value::Const(Const::Bool(false)), _ if val.is_false() => Value::Bool(false),
_ if val.is_null() => Value::Const(Const::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();
Value::List(List::new(list)) Value::List(List::new(list))
} }
_ if val.is_function() => { _ if val.is_function() => {
if let Some(name) = primop_app_name(val, scope, primop_metadata_symbol) { if let Some(primop) = to_primop(val, scope, primop_metadata_symbol) {
Value::PrimOpApp(name) primop
} else if let Some(name) = primop_name(val, scope, primop_metadata_symbol) {
Value::PrimOp(name)
} else { } else {
Value::Func Value::Func
} }
@@ -270,17 +290,22 @@ 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), to_value(val, scope, is_thunk_symbol, primop_metadata_symbol)) (
Symbol::new(key),
to_value(val, scope, is_thunk_symbol, primop_metadata_symbol),
)
}) })
.collect(); .collect();
Value::AttrSet(AttrSet::new(attrs)) Value::AttrSet(AttrSet::new(attrs))
@@ -289,61 +314,54 @@ fn to_value<'a>(
} }
} }
fn is_thunk<'a>(val: v8::Local<'a, v8::Value>, scope: &ScopeRef<'a, '_>, symbol: v8::Local<'a, v8::Symbol>) -> bool { fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() { if !val.is_object() {
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())
} }
fn primop_name<'a>(val: v8::Local<'a, v8::Value>, scope: &ScopeRef<'a, '_>, symbol: v8::Local<'a, v8::Symbol>) -> Option<String> { fn to_primop<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<Value> {
if !val.is_function() { if !val.is_function() {
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)?;
if let Some(metadata) = obj.get(scope, symbol.into()) let name_key = v8::String::new(scope, "name")?;
&& let Some(metadata_obj) = metadata.to_object(scope) let name = metadata
&& let Some(name_key) = v8::String::new(scope, "name") .get(scope, name_key.into())?
&& let Some(name_val) = metadata_obj.get(scope, name_key.into()) .to_rust_string_lossy(scope);
{
Some(name_val.to_rust_string_lossy(scope)) let applied_key = v8::String::new(scope, "applied")?;
let applied_val = metadata.get(scope, applied_key.into())?;
let applied = applied_val.to_number(scope)?.value();
if applied == 0.0 {
Some(Value::PrimOp(name))
} else { } else {
None Some(Value::PrimOpApp(name))
}
}
fn primop_app_name<'a>(val: v8::Local<'a, v8::Value>, scope: &ScopeRef<'a, '_>, symbol: v8::Local<'a, v8::Symbol>) -> Option<String> {
let name = primop_name(val, scope, symbol)?;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, symbol.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(applied_key) = v8::String::new(scope, "applied")
&& let Some(applied_val) = metadata_obj.get(scope, applied_key.into())
&& let Some(applied_num) = applied_val.to_number(scope)
&& applied_num.value() > 0.0
{
Some(name)
} else {
None
} }
} }
#[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.runtime.eval( ctx.eval_js(
"({ "({
test: [1., 9223372036854775807n, true, false, 'hello world!'] test: [1., 9223372036854775807n, true, false, 'hello world!']
})" })"
@@ -353,10 +371,10 @@ mod test {
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([( Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
Symbol::from("test"), Symbol::from("test"),
Value::List(List::new(vec![ Value::List(List::new(vec![
Value::Const(Const::Float(1.)), Value::Float(1.),
Value::Const(Const::Int(9223372036854775807)), Value::Int(9223372036854775807),
Value::Const(Const::Bool(true)), Value::Bool(true),
Value::Const(Const::Bool(false)), Value::Bool(false),
Value::String("hello world!".to_string()) Value::String("hello world!".to_string())
])) ]))
)]))) )])))

View File

@@ -9,49 +9,6 @@ use std::sync::LazyLock;
use derive_more::{Constructor, IsVariant, Unwrap}; use derive_more::{Constructor, IsVariant, Unwrap};
use regex::Regex; use regex::Regex;
/// Represents a constant, primitive value in Nix.
#[derive(Debug, Clone, Copy, PartialEq, IsVariant, Unwrap)]
pub enum Const {
/// A boolean value (`true` or `false`).
Bool(bool),
/// A 64-bit signed integer.
Int(i64),
/// A 64-bit floating-point number.
Float(f64),
/// The `null` value.
Null,
}
impl Display for Const {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Const::*;
match self {
Int(x) => write!(f, "{x}"),
Float(x) => write!(f, "{x}"),
Bool(x) => write!(f, "{x}"),
Null => write!(f, "null"),
}
}
}
impl From<bool> for Const {
fn from(value: bool) -> Self {
Const::Bool(value)
}
}
impl From<i64> for Const {
fn from(value: i64) -> Self {
Const::Int(value)
}
}
impl From<f64> for Const {
fn from(value: f64) -> Self {
Const::Float(value)
}
}
/// Represents a Nix symbol, which is used as a key in attribute sets. /// Represents a Nix symbol, which is used as a key in attribute sets.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)]
pub struct Symbol(String); pub struct Symbol(String);
@@ -82,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 {
@@ -172,8 +130,14 @@ impl Display for List {
/// Represents any possible Nix value that can be returned from an evaluation. /// Represents any possible Nix value that can be returned from an evaluation.
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)] #[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
pub enum Value { pub enum Value {
/// A constant value (int, float, bool, null). /// An integer value.
Const(Const), Int(i64),
/// An floating-point value.
Float(f64),
/// An boolean value.
Bool(bool),
/// An null value.
Null,
/// A string value. /// A string value.
String(String), String(String),
/// An attribute set. /// An attribute set.
@@ -197,7 +161,10 @@ impl Display for Value {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*; use Value::*;
match self { match self {
Const(x) => write!(f, "{x}"), &Int(x) => write!(f, "{x}"),
&Float(x) => write!(f, "{x}"),
&Bool(x) => write!(f, "{x}"),
Null => write!(f, "null"),
String(x) => write!(f, r#""{x}""#), String(x) => write!(f, r#""{x}""#),
AttrSet(x) => write!(f, "{x}"), AttrSet(x) => write!(f, "{x}"),
List(x) => write!(f, "{x}"), List(x) => write!(f, "{x}"),