feat: thunk loop debugging
This commit is contained in:
@@ -53,9 +53,7 @@ export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||
const context = getStringContext(s);
|
||||
|
||||
if (context.size !== 1) {
|
||||
throw new Error(
|
||||
`context of string '${strValue}' must have exactly one element, but has ${context.size}`,
|
||||
);
|
||||
throw new Error(`context of string '${strValue}' must have exactly one element, but has ${context.size}`);
|
||||
}
|
||||
|
||||
const [encoded] = context;
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
import type { NixValue, NixString } from "../types";
|
||||
import { isStringWithContext } from "../types";
|
||||
import { force } from "../thunk";
|
||||
import {
|
||||
type NixStringContext,
|
||||
mkStringWithContext,
|
||||
addBuiltContext,
|
||||
} from "../string-context";
|
||||
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
|
||||
import { forceFunction } from "../type-assert";
|
||||
|
||||
const convertJsonToNix = (json: unknown): NixValue => {
|
||||
@@ -44,18 +40,14 @@ const convertJsonToNix = (json: unknown): NixValue => {
|
||||
export const fromJSON = (e: NixValue): NixValue => {
|
||||
const str = force(e);
|
||||
if (typeof str !== "string" && !isStringWithContext(str)) {
|
||||
throw new TypeError(
|
||||
`builtins.fromJSON: expected a string, got ${typeName(str)}`,
|
||||
);
|
||||
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeName(str)}`);
|
||||
}
|
||||
const jsonStr = isStringWithContext(str) ? str.value : str;
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
return convertJsonToNix(parsed);
|
||||
} catch (err) {
|
||||
throw new SyntaxError(
|
||||
`builtins.fromJSON: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
throw new SyntaxError(`builtins.fromJSON: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -80,11 +80,7 @@ const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] =>
|
||||
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, false, outContext));
|
||||
};
|
||||
|
||||
const nixValueToJson = (
|
||||
value: NixValue,
|
||||
seen = new Set<object>(),
|
||||
outContext?: NixStringContext,
|
||||
): any => {
|
||||
const nixValueToJson = (value: NixValue, seen = new Set<object>(), outContext?: NixStringContext): any => {
|
||||
const v = force(value);
|
||||
|
||||
if (v === null) return null;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Functional programming builtin functions
|
||||
*/
|
||||
|
||||
import { CatchableError, type NixValue } from "../types";
|
||||
import { CatchableError, HAS_CONTEXT, type NixValue } from "../types";
|
||||
import { force } from "../thunk";
|
||||
import { forceString } from "../type-assert";
|
||||
|
||||
@@ -15,8 +15,18 @@ export const seq =
|
||||
|
||||
export const deepSeq =
|
||||
(e1: NixValue) =>
|
||||
(e2: NixValue): never => {
|
||||
throw new Error("Not implemented: deepSeq");
|
||||
(e2: NixValue): NixValue => {
|
||||
const forced = force(e1);
|
||||
if (Array.isArray(forced)) {
|
||||
for (const val of forced) {
|
||||
deepSeq(val);
|
||||
}
|
||||
} else if (typeof forced === "object" && forced !== null && !(HAS_CONTEXT in forced)) {
|
||||
for (const [_, val] of Object.entries(forced)) {
|
||||
deepSeq(val);
|
||||
}
|
||||
}
|
||||
return e2;
|
||||
};
|
||||
|
||||
export const abort = (s: NixValue): never => {
|
||||
|
||||
@@ -73,11 +73,7 @@ const normalizeUrlInput = (
|
||||
const attrs = forceAttrs(args);
|
||||
const url = forceString(attrs.url);
|
||||
const hash =
|
||||
"sha256" in attrs
|
||||
? forceString(attrs.sha256)
|
||||
: "hash" in attrs
|
||||
? forceString(attrs.hash)
|
||||
: undefined;
|
||||
"sha256" in attrs ? forceString(attrs.sha256) : "hash" in attrs ? forceString(attrs.hash) : undefined;
|
||||
const name = "name" in attrs ? forceString(attrs.name) : undefined;
|
||||
const executable = "executable" in attrs ? forceBool(attrs.executable) : false;
|
||||
return { url, hash, name, executable };
|
||||
@@ -96,26 +92,14 @@ export const fetchurl = (args: NixValue): string => {
|
||||
|
||||
export const fetchTarball = (args: NixValue): string => {
|
||||
const { url, hash, name } = normalizeUrlInput(args);
|
||||
const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball(
|
||||
url,
|
||||
hash ?? null,
|
||||
name ?? null,
|
||||
);
|
||||
const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball(url, hash ?? null, name ?? null);
|
||||
return result.store_path;
|
||||
};
|
||||
|
||||
export const fetchGit = (args: NixValue): NixAttrs => {
|
||||
const forced = force(args);
|
||||
if (typeof forced === "string") {
|
||||
const result: FetchGitResult = Deno.core.ops.op_fetch_git(
|
||||
forced,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
);
|
||||
const result: FetchGitResult = Deno.core.ops.op_fetch_git(forced, null, null, false, false, false, null);
|
||||
return {
|
||||
outPath: result.out_path,
|
||||
rev: result.rev,
|
||||
@@ -206,12 +190,7 @@ export const fetchTree = (args: NixValue): NixAttrs => {
|
||||
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
|
||||
const owner = forceString(attrs.owner);
|
||||
const repo = forceString(attrs.repo);
|
||||
const rev =
|
||||
"rev" in attrs
|
||||
? forceString(attrs.rev)
|
||||
: "ref" in attrs
|
||||
? forceString(attrs.ref)
|
||||
: "HEAD";
|
||||
const rev = "rev" in attrs ? forceString(attrs.rev) : "ref" in attrs ? forceString(attrs.ref) : "HEAD";
|
||||
|
||||
const baseUrls: Record<string, string> = {
|
||||
github: "https://github.com",
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
* Type checking builtin functions
|
||||
*/
|
||||
|
||||
import type {
|
||||
NixAttrs,
|
||||
NixBool,
|
||||
NixFloat,
|
||||
NixFunction,
|
||||
NixInt,
|
||||
NixList,
|
||||
NixNull,
|
||||
NixStrictValue,
|
||||
NixString,
|
||||
NixValue,
|
||||
import {
|
||||
HAS_CONTEXT,
|
||||
type NixAttrs,
|
||||
type NixBool,
|
||||
type NixFloat,
|
||||
type NixFunction,
|
||||
type NixInt,
|
||||
type NixList,
|
||||
type NixNull,
|
||||
type NixString,
|
||||
type NixValue,
|
||||
} from "../types";
|
||||
import { force } from "../thunk";
|
||||
|
||||
export const isAttrs = (e: NixValue): e is NixAttrs => {
|
||||
const val = force(e);
|
||||
return typeof val === "object" && !Array.isArray(val) && val !== null;
|
||||
return typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val);
|
||||
};
|
||||
|
||||
export const isBool = (e: NixValue): e is NixBool => typeof force(e) === "boolean";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* All functionality is exported via the global `Nix` object
|
||||
*/
|
||||
|
||||
import { createThunk, force, isThunk, IS_THUNK } from "./thunk";
|
||||
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS } from "./thunk";
|
||||
import {
|
||||
select,
|
||||
selectWithDefault,
|
||||
@@ -30,6 +30,7 @@ export const Nix = {
|
||||
isThunk,
|
||||
IS_THUNK,
|
||||
HAS_CONTEXT,
|
||||
DEBUG_THUNKS,
|
||||
|
||||
call,
|
||||
hasAttr,
|
||||
|
||||
@@ -7,12 +7,7 @@ import type { NixValue, NixList, NixAttrs, NixString } from "./types";
|
||||
import { isStringWithContext } from "./types";
|
||||
import { force } from "./thunk";
|
||||
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
|
||||
import {
|
||||
getStringValue,
|
||||
getStringContext,
|
||||
mergeContexts,
|
||||
mkStringWithContext,
|
||||
} from "./string-context";
|
||||
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
|
||||
const isNixString = (v: unknown): v is NixString => {
|
||||
|
||||
@@ -27,7 +27,9 @@ export interface StringWithContext {
|
||||
}
|
||||
|
||||
export const isStringWithContext = (v: unknown): v is StringWithContext => {
|
||||
return typeof v === "object" && v !== null && HAS_CONTEXT in v && (v as StringWithContext)[HAS_CONTEXT] === true;
|
||||
return (
|
||||
typeof v === "object" && v !== null && HAS_CONTEXT in v && (v as StringWithContext)[HAS_CONTEXT] === true
|
||||
);
|
||||
};
|
||||
|
||||
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {
|
||||
|
||||
@@ -11,6 +11,10 @@ import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
|
||||
*/
|
||||
export const IS_THUNK = Symbol("is_thunk");
|
||||
|
||||
const forceStack: NixThunk[] = [];
|
||||
|
||||
export const DEBUG_THUNKS = { enabled: false };
|
||||
|
||||
/**
|
||||
* NixThunk class - represents a lazy, unevaluated expression
|
||||
*
|
||||
@@ -27,10 +31,23 @@ export class NixThunk implements NixThunkInterface {
|
||||
readonly [IS_THUNK] = true as const;
|
||||
func: (() => NixValue) | undefined;
|
||||
result: NixStrictValue | undefined;
|
||||
readonly label: string | undefined;
|
||||
readonly creationStack: string | undefined;
|
||||
|
||||
constructor(func: () => NixValue) {
|
||||
constructor(func: () => NixValue, label?: string) {
|
||||
this.func = func;
|
||||
this.result = undefined;
|
||||
this.label = label;
|
||||
if (DEBUG_THUNKS.enabled) {
|
||||
this.creationStack = new Error().stack?.split("\n").slice(2).join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.label) {
|
||||
return `«thunk ${this.label}»`;
|
||||
}
|
||||
return `«thunk»`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,33 +78,53 @@ export const force = (value: NixValue): NixStrictValue => {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Check if already evaluated or in blackhole state
|
||||
if (value.func === undefined) {
|
||||
// Blackhole: func is undefined but result is also undefined
|
||||
if (value.result === undefined) {
|
||||
throw new Error("infinite recursion encountered (blackhole)");
|
||||
const thunk = value as NixThunk;
|
||||
let msg = `infinite recursion encountered (blackhole) at ${thunk}\n`;
|
||||
msg += "Force chain (most recent first):\n";
|
||||
for (let i = forceStack.length - 1; i >= 0; i--) {
|
||||
const t = forceStack[i];
|
||||
msg += ` ${i + 1}. ${t}`;
|
||||
if (DEBUG_THUNKS.enabled && t.creationStack) {
|
||||
msg += `\n Created at:\n${t.creationStack
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n")}`;
|
||||
}
|
||||
msg += "\n";
|
||||
}
|
||||
if (DEBUG_THUNKS.enabled && thunk.creationStack) {
|
||||
msg += `\nBlackhole thunk created at:\n${thunk.creationStack
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n")}`;
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
// Already evaluated - return cached result
|
||||
return value.result;
|
||||
}
|
||||
|
||||
// Save func and enter blackhole state BEFORE calling func()
|
||||
const func = value.func;
|
||||
value.func = undefined;
|
||||
// result stays undefined - this is the blackhole state
|
||||
const thunk = value as NixThunk;
|
||||
const func = thunk.func!;
|
||||
thunk.func = undefined;
|
||||
|
||||
// Evaluate and cache
|
||||
forceStack.push(thunk);
|
||||
try {
|
||||
const result = force(func());
|
||||
value.result = result;
|
||||
|
||||
thunk.result = result;
|
||||
return result;
|
||||
} finally {
|
||||
forceStack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new thunk from a function
|
||||
* @param func - Function that produces a value when called
|
||||
* @param label - Optional label for debugging
|
||||
* @returns A new NixThunk wrapping the function
|
||||
*/
|
||||
export const createThunk = (func: () => NixValue): NixThunkInterface => {
|
||||
return new NixThunk(func);
|
||||
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
|
||||
return new NixThunk(func, label);
|
||||
};
|
||||
|
||||
6
nix-js/runtime-ts/src/types/global.d.ts
vendored
6
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -67,11 +67,7 @@ declare global {
|
||||
all_refs: boolean,
|
||||
name: string | null,
|
||||
): FetchGitResult;
|
||||
function op_fetch_hg(
|
||||
url: string,
|
||||
rev: string | null,
|
||||
name: string | null,
|
||||
): FetchHgResult;
|
||||
function op_fetch_hg(url: string, rev: string | null, name: string | null): FetchHgResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,13 @@ impl Ctx {
|
||||
.downgrade_ctx()
|
||||
.downgrade(root.tree().expr().unwrap())?;
|
||||
let code = self.get_ir(root).compile(self);
|
||||
let code = format!("Nix.force({})", code);
|
||||
|
||||
let debug_prefix = if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() {
|
||||
"Nix.DEBUG_THUNKS.enabled=true,"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let code = format!("({}Nix.force({}))", debug_prefix, code);
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("[DEBUG] generated code: {}", &code);
|
||||
Ok(code)
|
||||
|
||||
@@ -92,6 +92,8 @@ fn op_import<Ctx: RuntimeCtx>(
|
||||
let content = std::fs::read_to_string(&absolute_path)
|
||||
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("[DEBUG] compiling file: {}", absolute_path.display());
|
||||
let mut guard = ctx.push_path_stack(absolute_path);
|
||||
let ctx = guard.deref_mut();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user