Compare commits
4 Commits
23950da6ea
...
e29e432328
| Author | SHA1 | Date | |
|---|---|---|---|
|
e29e432328
|
|||
|
cc53963df0
|
|||
|
0376621982
|
|||
|
9cfffc440f
|
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
2
nix-js/runtime-ts/src/types/global.d.ts
vendored
2
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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> ");
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)]
|
||||||
impl std::fmt::Display for SimpleErrorWrapper {
|
#[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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
std::fmt::Debug::fmt(self, f)
|
std::fmt::Display::fmt(&self.0, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl std::error::Error for SimpleErrorWrapper {}
|
||||||
|
|
||||||
impl std::error::Error for SimpleErrorWrapper {
|
js_error_wrapper!(SimpleErrorWrapper, NixError, "Error");
|
||||||
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 {
|
impl From<String> for NixError {
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
NixError(SimpleErrorWrapper(value))
|
NixError(SimpleErrorWrapper(value))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
impl From<&str> for NixError {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
NixError(SimpleErrorWrapper(value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
use private::NixError;
|
||||||
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())
|
||||||
]))
|
]))
|
||||||
)])))
|
)])))
|
||||||
|
|||||||
@@ -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}"),
|
||||||
|
|||||||
Reference in New Issue
Block a user