diff --git a/nix-js/runtime-ts/src/builtins/functional.ts b/nix-js/runtime-ts/src/builtins/functional.ts index d92f5e2..8a5dad9 100644 --- a/nix-js/runtime-ts/src/builtins/functional.ts +++ b/nix-js/runtime-ts/src/builtins/functional.ts @@ -5,6 +5,7 @@ import { CatchableError, HAS_CONTEXT, type NixValue } from "../types"; import { force } from "../thunk"; import { coerceToString, StringCoercionMode } from "./conversion"; +import { printValue } from "../print"; export const seq = (e1: NixValue) => @@ -40,7 +41,7 @@ export const throwFunc = (s: NixValue): never => { export const trace = (e1: NixValue) => (e2: NixValue): NixValue => { - console.log(`trace: ${coerceToString(e1, StringCoercionMode.Base)}`); + console.error(`trace: ${printValue(e1)}`); return e2; }; diff --git a/nix-js/runtime-ts/src/print.ts b/nix-js/runtime-ts/src/print.ts new file mode 100644 index 0000000..1a7018d --- /dev/null +++ b/nix-js/runtime-ts/src/print.ts @@ -0,0 +1,107 @@ +import { isThunk, IS_CYCLE } from "./thunk"; +import { isStringWithContext } from "./string-context"; +import { isNixPath, type NixValue } from "./types"; +import { is_primop, get_primop_metadata } from "./builtins/index"; + +export const printValue = (value: NixValue, seen: WeakSet = new WeakSet()): string => { + if (isThunk(value)) { + return "«thunk»"; + } + + if (value === null) { + return "null"; + } + + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + + if (typeof value === "bigint") { + return value.toString(); + } + + if (typeof value === "number") { + return value.toString(); + } + + if (typeof value === "string") { + return printString(value); + } + + if (typeof value === "function") { + if (is_primop(value)) { + const meta = get_primop_metadata(value); + if (meta && meta.applied > 0) { + return ""; + } + return ""; + } + return ""; + } + + if (typeof value === "object") { + if (IS_CYCLE in value && (value as any)[IS_CYCLE] === true) { + return "«repeated»"; + } + + if (seen.has(value)) { + return "«repeated»"; + } + seen.add(value); + + if (isNixPath(value)) { + return value.value; + } + + if (isStringWithContext(value)) { + return printString(value.value); + } + + if (Array.isArray(value)) { + const items = value.map((v) => printValue(v, seen)).join(" "); + return `[ ${items} ]`; + } + + const entries = Object.entries(value) + .map(([k, v]) => `${printSymbol(k)} = ${printValue(v, seen)};`) + .join(" "); + return `{${entries ? ` ${entries} ` : " "}}`; + } + + return ""; +}; + +const printString = (s: string): string => { + let result = '"'; + for (const c of s) { + switch (c) { + case "\\": + result += "\\\\"; + break; + case '"': + result += '\\"'; + break; + case "\n": + result += "\\n"; + break; + case "\r": + result += "\\r"; + break; + case "\t": + result += "\\t"; + break; + default: + result += c; + } + } + return result + '"'; +}; + +const SYMBOL_REGEX = /^[a-zA-Z_][a-zA-Z0-9_'-]*$/; + +const printSymbol = (s: string): string => { + if (SYMBOL_REGEX.test(s)) { + return s; + } + return printString(s); +}; diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index ff9c8eb..304164d 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -149,7 +149,7 @@ export const CYCLE_MARKER = { [IS_CYCLE]: true }; * Deeply force a value, handling cycles by returning a special marker. * Uses WeakSet to track seen objects and avoid infinite recursion. * Returns a fully forced value where thunks are replaced with their results. - * Cyclic references are replaced with CYCLE_MARKER. + * Cyclic references are replaced with CYCLE_MARKER, preserving the container type. */ export const forceDeepSafe = (value: NixValue, seen: WeakSet = new WeakSet()): NixStrictValue => { const forced = force(value); @@ -159,6 +159,9 @@ export const forceDeepSafe = (value: NixValue, seen: WeakSet = new WeakS } if (seen.has(forced)) { + if (Array.isArray(forced)) { + return [CYCLE_MARKER]; + } return CYCLE_MARKER; } seen.add(forced); diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index ad3ca89..e385b14 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -125,6 +125,24 @@ impl Display for AttrSet { } } +impl AttrSet { + pub fn display_compat(&self) -> AttrSetCompatDisplay<'_> { + AttrSetCompatDisplay(self) + } +} + +pub struct AttrSetCompatDisplay<'a>(&'a AttrSet); + +impl Display for AttrSetCompatDisplay<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{{")?; + for (k, v) in self.0.data.iter() { + write!(f, " {k} = {};", v.display_compat())?; + } + write!(f, " }}") + } +} + /// Represents a Nix list, which is a vector of values. #[derive(Constructor, Default, Clone, Debug, PartialEq)] pub struct List { @@ -153,6 +171,24 @@ impl Display for List { } } +impl List { + pub fn display_compat(&self) -> ListCompatDisplay<'_> { + ListCompatDisplay(self) + } +} + +pub struct ListCompatDisplay<'a>(&'a List); + +impl Display for ListCompatDisplay<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "[ ")?; + for v in self.0.data.iter() { + write!(f, "{} ", v.display_compat())?; + } + write!(f, "]") + } +} + /// Represents any possible Nix value that can be returned from an evaluation. #[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)] pub enum Value { @@ -218,3 +254,45 @@ impl Display for Value { } } } + +impl Value { + pub fn display_compat(&self) -> ValueCompatDisplay<'_> { + ValueCompatDisplay(self) + } +} + +pub struct ValueCompatDisplay<'a>(&'a Value); + +impl Display for ValueCompatDisplay<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + use Value::*; + match self.0 { + &Int(x) => write!(f, "{x}"), + &Float(x) => write!(f, "{x}"), + &Bool(x) => write!(f, "{x}"), + Null => write!(f, "null"), + String(x) => { + write!(f, "\"")?; + for c in x.chars() { + match c { + '\\' => write!(f, "\\\\")?, + '"' => write!(f, "\\\"")?, + '\n' => write!(f, "\\n")?, + '\r' => write!(f, "\\r")?, + '\t' => write!(f, "\\t")?, + c => write!(f, "{c}")?, + } + } + write!(f, "\"") + } + Path(x) => write!(f, "{x}"), + AttrSet(x) => write!(f, "{}", x.display_compat()), + List(x) => write!(f, "{}", x.display_compat()), + Thunk => write!(f, "«thunk»"), + Func => write!(f, ""), + PrimOp(_) => write!(f, ""), + PrimOpApp(_) => write!(f, ""), + Repeated => write!(f, "«repeated»"), + } + } +} diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 8653661..e564e53 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -38,7 +38,7 @@ fn read_expected(name: &str) -> String { } fn format_value(value: &Value) -> String { - value.to_string() + value.display_compat().to_string() } macro_rules! eval_okay_test {