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