feat: IS_PRIMOP

This commit is contained in:
2026-01-02 23:07:43 +08:00
parent f1670e8397
commit 073c95f2c3
7 changed files with 307 additions and 139 deletions

View File

@@ -1,6 +1,6 @@
use std::process::Command;
use std::path::Path;
use std::env; use std::env;
use std::path::Path;
use std::process::Command;
fn main() { fn main() {
let runtime_ts_dir = Path::new("runtime-ts"); let runtime_ts_dir = Path::new("runtime-ts");
@@ -17,7 +17,11 @@ fn main() {
if !runtime_ts_dir.join("node_modules").exists() { if !runtime_ts_dir.join("node_modules").exists() {
println!("Installing npm dependencies..."); println!("Installing npm dependencies...");
let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" }; let npm_cmd = if cfg!(target_os = "windows") {
"npm.cmd"
} else {
"npm"
};
let status = Command::new(npm_cmd) let status = Command::new(npm_cmd)
.arg("install") .arg("install")
.current_dir(runtime_ts_dir) .current_dir(runtime_ts_dir)
@@ -30,7 +34,11 @@ fn main() {
} }
println!("Running TypeScript type checking..."); println!("Running TypeScript type checking...");
let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" }; let npm_cmd = if cfg!(target_os = "windows") {
"npm.cmd"
} else {
"npm"
};
let status = Command::new(npm_cmd) let status = Command::new(npm_cmd)
.arg("run") .arg("run")
.arg("typecheck") .arg("typecheck")

View File

@@ -5,6 +5,98 @@
import { create_thunk } from '../thunk'; import { create_thunk } from '../thunk';
/**
* Symbol used to mark functions as primops (primitive operations)
* This is similar to IS_THUNK but for builtin functions
*/
export const IS_PRIMOP = Symbol('is_primop');
/**
* Metadata interface for primop functions
*/
export interface PrimopMetadata {
/** The name of the primop (e.g., "add", "map") */
name: string;
/** Total arity of the function (number of arguments it expects) */
arity: number;
/** Number of arguments already applied (for partial applications) */
applied: number;
}
/**
* Mark a function as a primop with metadata
* For curried functions, this recursively marks each layer
*
* @param func - The function to mark
* @param name - Name of the primop
* @param arity - Total number of arguments expected
* @param applied - Number of arguments already applied (default: 0)
* @returns The marked function
*/
export const markPrimop = <T extends Function>(
func: T,
name: string,
arity: number,
applied: number = 0
): T => {
// Mark this function as a primop
(func as any)[IS_PRIMOP] = {
name,
arity,
applied,
} as PrimopMetadata;
// If this is a curried function and not fully applied,
// wrap it to mark the next layer too
if (applied < arity - 1) {
const wrappedFunc = ((...args: any[]) => {
const result = func(...args);
// If result is a function, mark it as the next layer
if (typeof result === 'function') {
return markPrimop(result, name, arity, applied + args.length);
}
return result;
}) as any;
// Copy the primop metadata to the wrapper
wrappedFunc[IS_PRIMOP] = {
name,
arity,
applied,
} as PrimopMetadata;
return wrappedFunc as T;
}
return func;
};
/**
* Type guard to check if a value is a primop
* @param value - Value to check
* @returns true if value is marked as a primop
*/
export const is_primop = (value: unknown): value is Function & { [IS_PRIMOP]: PrimopMetadata } => {
return (
typeof value === 'function' &&
IS_PRIMOP in value &&
typeof value[IS_PRIMOP] === 'object' &&
value[IS_PRIMOP] !== null
);
};
/**
* Get primop metadata from a function
* @param func - Function to get metadata from
* @returns Metadata if function is a primop, undefined otherwise
*/
export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined => {
if (is_primop(func)) {
return func[IS_PRIMOP];
}
return undefined;
};
// Import all builtin categories // Import all builtin categories
import * as arithmetic from './arithmetic'; import * as arithmetic from './arithmetic';
import * as math from './math'; import * as math from './math';
@@ -24,144 +116,133 @@ import * as misc from './misc';
* All functions are curried for Nix semantics: * All functions are curried for Nix semantics:
* - Single argument functions: (a) => result * - Single argument functions: (a) => result
* - Multi-argument functions: (a) => (b) => result * - Multi-argument functions: (a) => (b) => result
*
* All primop functions are marked with IS_PRIMOP symbol for runtime introspection
*/ */
export const builtins: any = { export const builtins: any = {
// Arithmetic (curried binary functions) add: markPrimop(arithmetic.add, 'add', 2),
add: arithmetic.add, sub: markPrimop(arithmetic.sub, 'sub', 2),
sub: arithmetic.sub, mul: markPrimop(arithmetic.mul, 'mul', 2),
mul: arithmetic.mul, div: markPrimop(arithmetic.div, 'div', 2),
div: arithmetic.div, bitAnd: markPrimop(arithmetic.bitAnd, 'bitAnd', 2),
bitAnd: arithmetic.bitAnd, bitOr: markPrimop(arithmetic.bitOr, 'bitOr', 2),
bitOr: arithmetic.bitOr, bitXor: markPrimop(arithmetic.bitXor, 'bitXor', 2),
bitXor: arithmetic.bitXor, lessThan: markPrimop(arithmetic.lessThan, 'lessThan', 2),
lessThan: arithmetic.lessThan,
// Math ceil: markPrimop(math.ceil, 'ceil', 1),
ceil: math.ceil, floor: markPrimop(math.floor, 'floor', 1),
floor: math.floor,
// Type checking isAttrs: markPrimop(typeCheck.isAttrs, 'isAttrs', 1),
isAttrs: typeCheck.isAttrs, isBool: markPrimop(typeCheck.isBool, 'isBool', 1),
isBool: typeCheck.isBool, isFloat: markPrimop(typeCheck.isFloat, 'isFloat', 1),
isFloat: typeCheck.isFloat, isFunction: markPrimop(typeCheck.isFunction, 'isFunction', 1),
isFunction: typeCheck.isFunction, isInt: markPrimop(typeCheck.isInt, 'isInt', 1),
isInt: typeCheck.isInt, isList: markPrimop(typeCheck.isList, 'isList', 1),
isList: typeCheck.isList, isNull: markPrimop(typeCheck.isNull, 'isNull', 1),
isNull: typeCheck.isNull, isPath: markPrimop(typeCheck.isPath, 'isPath', 1),
isPath: typeCheck.isPath, isString: markPrimop(typeCheck.isString, 'isString', 1),
isString: typeCheck.isString, typeOf: markPrimop(typeCheck.typeOf, 'typeOf', 1),
typeOf: typeCheck.typeOf,
// List operations map: markPrimop(list.map, 'map', 2),
map: list.map, filter: markPrimop(list.filter, 'filter', 2),
filter: list.filter, length: markPrimop(list.length, 'length', 1),
length: list.length, head: markPrimop(list.head, 'head', 1),
head: list.head, tail: markPrimop(list.tail, 'tail', 1),
tail: list.tail, elem: markPrimop(list.elem, 'elem', 2),
elem: list.elem, elemAt: markPrimop(list.elemAt, 'elemAt', 2),
elemAt: list.elemAt, concatLists: markPrimop(list.concatLists, 'concatLists', 1),
concatLists: list.concatLists, concatMap: markPrimop(list.concatMap, 'concatMap', 2),
concatMap: list.concatMap, 'foldl\'': markPrimop(list.foldlPrime, 'foldl\'', 3),
'foldl\'': list.foldlPrime, sort: markPrimop(list.sort, 'sort', 2),
sort: list.sort, partition: markPrimop(list.partition, 'partition', 2),
partition: list.partition, genList: markPrimop(list.genList, 'genList', 2),
genList: list.genList, all: markPrimop(list.all, 'all', 2),
all: list.all, any: markPrimop(list.any, 'any', 2),
any: list.any,
// Attribute set operations attrNames: markPrimop(attrs.attrNames, 'attrNames', 1),
attrNames: attrs.attrNames, attrValues: markPrimop(attrs.attrValues, 'attrValues', 1),
attrValues: attrs.attrValues, getAttr: markPrimop(attrs.getAttr, 'getAttr', 2),
getAttr: attrs.getAttr, hasAttr: markPrimop(attrs.hasAttr, 'hasAttr', 2),
hasAttr: attrs.hasAttr, mapAttrs: markPrimop(attrs.mapAttrs, 'mapAttrs', 2),
mapAttrs: attrs.mapAttrs, listToAttrs: markPrimop(attrs.listToAttrs, 'listToAttrs', 1),
listToAttrs: attrs.listToAttrs, intersectAttrs: markPrimop(attrs.intersectAttrs, 'intersectAttrs', 2),
intersectAttrs: attrs.intersectAttrs, catAttrs: markPrimop(attrs.catAttrs, 'catAttrs', 2),
catAttrs: attrs.catAttrs, groupBy: markPrimop(attrs.groupBy, 'groupBy', 2),
groupBy: attrs.groupBy,
// String operations stringLength: markPrimop(string.stringLength, 'stringLength', 1),
stringLength: string.stringLength, substring: markPrimop(string.substring, 'substring', 3),
substring: string.substring, concatStringsSep: markPrimop(string.concatStringsSep, 'concatStringsSep', 2),
concatStringsSep: string.concatStringsSep, baseNameOf: markPrimop(string.baseNameOf, 'baseNameOf', 1),
baseNameOf: string.baseNameOf,
// Functional seq: markPrimop(functional.seq, 'seq', 2),
seq: functional.seq, deepSeq: markPrimop(functional.deepSeq, 'deepSeq', 2),
deepSeq: functional.deepSeq, abort: markPrimop(functional.abort, 'abort', 1),
abort: functional.abort, throw: markPrimop(functional.throwFunc, 'throw', 1),
throw: functional.throwFunc, trace: markPrimop(functional.trace, 'trace', 2),
trace: functional.trace, warn: markPrimop(functional.warn, 'warn', 2),
warn: functional.warn, break: markPrimop(functional.breakFunc, 'break', 1),
break: functional.breakFunc,
// I/O (unimplemented) import: markPrimop(io.importFunc, 'import', 1),
import: io.importFunc, scopedImport: markPrimop(io.scopedImport, 'scopedImport', 2),
scopedImport: io.scopedImport, fetchClosure: markPrimop(io.fetchClosure, 'fetchClosure', 1),
fetchClosure: io.fetchClosure, fetchGit: markPrimop(io.fetchGit, 'fetchGit', 1),
fetchGit: io.fetchGit, fetchTarball: markPrimop(io.fetchTarball, 'fetchTarball', 1),
fetchTarball: io.fetchTarball, fetchTree: markPrimop(io.fetchTree, 'fetchTree', 1),
fetchTree: io.fetchTree, fetchurl: markPrimop(io.fetchurl, 'fetchurl', 1),
fetchurl: io.fetchurl, readDir: markPrimop(io.readDir, 'readDir', 1),
readDir: io.readDir, readFile: markPrimop(io.readFile, 'readFile', 1),
readFile: io.readFile, readFileType: markPrimop(io.readFileType, 'readFileType', 1),
readFileType: io.readFileType, pathExists: markPrimop(io.pathExists, 'pathExists', 1),
pathExists: io.pathExists, path: markPrimop(io.path, 'path', 1),
path: io.path, toFile: markPrimop(io.toFile, 'toFile', 2),
toFile: io.toFile, toPath: markPrimop(io.toPath, 'toPath', 1),
toPath: io.toPath, filterSource: markPrimop(io.filterSource, 'filterSource', 2),
filterSource: io.filterSource, findFile: markPrimop(io.findFile, 'findFile', 2),
findFile: io.findFile, getEnv: markPrimop(io.getEnv, 'getEnv', 1),
getEnv: io.getEnv,
// Conversion (unimplemented) fromJSON: markPrimop(conversion.fromJSON, 'fromJSON', 1),
fromJSON: conversion.fromJSON, fromTOML: markPrimop(conversion.fromTOML, 'fromTOML', 1),
fromTOML: conversion.fromTOML, toJSON: markPrimop(conversion.toJSON, 'toJSON', 1),
toJSON: conversion.toJSON, toXML: markPrimop(conversion.toXML, 'toXML', 1),
toXML: conversion.toXML, toString: markPrimop(conversion.toString, 'toString', 1),
toString: conversion.toString,
// Miscellaneous (unimplemented) getContext: markPrimop(misc.getContext, 'getContext', 1),
getContext: misc.getContext, hasContext: markPrimop(misc.hasContext, 'hasContext', 1),
hasContext: misc.hasContext, hashFile: markPrimop(misc.hashFile, 'hashFile', 2),
hashFile: misc.hashFile, hashString: markPrimop(misc.hashString, 'hashString', 2),
hashString: misc.hashString, convertHash: markPrimop(misc.convertHash, 'convertHash', 2),
convertHash: misc.convertHash, unsafeDiscardOutputDependency: markPrimop(misc.unsafeDiscardOutputDependency, 'unsafeDiscardOutputDependency', 1),
unsafeDiscardOutputDependency: misc.unsafeDiscardOutputDependency, unsafeDiscardStringContext: markPrimop(misc.unsafeDiscardStringContext, 'unsafeDiscardStringContext', 1),
unsafeDiscardStringContext: misc.unsafeDiscardStringContext, unsafeGetAttrPos: markPrimop(misc.unsafeGetAttrPos, 'unsafeGetAttrPos', 2),
unsafeGetAttrPos: misc.unsafeGetAttrPos, addDrvOutputDependencies: markPrimop(misc.addDrvOutputDependencies, 'addDrvOutputDependencies', 2),
addDrvOutputDependencies: misc.addDrvOutputDependencies, compareVersions: markPrimop(misc.compareVersions, 'compareVersions', 2),
compareVersions: misc.compareVersions, dirOf: markPrimop(misc.dirOf, 'dirOf', 1),
dirOf: misc.dirOf, flakeRefToString: markPrimop(misc.flakeRefToString, 'flakeRefToString', 1),
flakeRefToString: misc.flakeRefToString, functionArgs: markPrimop(misc.functionArgs, 'functionArgs', 1),
functionArgs: misc.functionArgs, genericClosure: markPrimop(misc.genericClosure, 'genericClosure', 1),
genericClosure: misc.genericClosure, getFlake: markPrimop(misc.getFlake, 'getFlake', 1),
getFlake: misc.getFlake, match: markPrimop(misc.match, 'match', 2),
match: misc.match, outputOf: markPrimop(misc.outputOf, 'outputOf', 2),
outputOf: misc.outputOf, parseDrvName: markPrimop(misc.parseDrvName, 'parseDrvName', 1),
parseDrvName: misc.parseDrvName, parseFlakeName: markPrimop(misc.parseFlakeName, 'parseFlakeName', 1),
parseFlakeName: misc.parseFlakeName, placeholder: markPrimop(misc.placeholder, 'placeholder', 1),
placeholder: misc.placeholder, replaceStrings: markPrimop(misc.replaceStrings, 'replaceStrings', 3),
replaceStrings: misc.replaceStrings, split: markPrimop(misc.split, 'split', 2),
split: misc.split, splitVersion: markPrimop(misc.splitVersion, 'splitVersion', 1),
splitVersion: misc.splitVersion, traceVerbose: markPrimop(misc.traceVerbose, 'traceVerbose', 2),
traceVerbose: misc.traceVerbose, tryEval: markPrimop(misc.tryEval, 'tryEval', 1),
tryEval: misc.tryEval, zipAttrsWith: markPrimop(misc.zipAttrsWith, 'zipAttrsWith', 2),
zipAttrsWith: misc.zipAttrsWith,
// Meta - self-reference and constants builtins: create_thunk(() => builtins),
builtins: create_thunk(() => builtins), // Recursive reference
currentSystem: create_thunk(() => { currentSystem: create_thunk(() => {
throw 'Not implemented: currentSystem'; throw 'Not implemented: currentSystem';
}), }),
currentTime: create_thunk(() => Date.now()), currentTime: create_thunk(() => Date.now()),
// Constants (special keys)
false: false, false: false,
true: true, true: true,
null: null, null: null,
// Version information
langVersion: 6, langVersion: 6,
nixPath: [], nixPath: [],
nixVersion: 'NIX_JS_VERSION', nixVersion: 'NIX_JS_VERSION',

View File

@@ -7,7 +7,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 } from './helpers'; import { select, select_with_default, validate_params } from './helpers';
import { op } from './operators'; import { op } from './operators';
import { builtins } from './builtins'; import { builtins, IS_PRIMOP } from './builtins';
export type NixRuntime = typeof Nix; export type NixRuntime = typeof Nix;
@@ -26,6 +26,7 @@ export const Nix = {
op, op,
builtins, builtins,
IS_PRIMOP,
}; };
globalThis.Nix = Nix; globalThis.Nix = Nix;

View File

@@ -44,7 +44,6 @@ impl Default for Context {
"true", "true",
"false", "false",
"null", "null",
"abort", "abort",
"baseNameOf", "baseNameOf",
"break", "break",
@@ -681,10 +680,7 @@ mod test {
Value::Const(Const::Float(3.5)) Value::Const(Const::Float(3.5))
); );
assert_eq!( assert_eq!(ctx.eval("(-7) / 3").unwrap(), Value::Const(Const::Int(-2)));
ctx.eval("(-7) / 3").unwrap(),
Value::Const(Const::Int(-2))
);
} }
#[test] #[test]
@@ -700,8 +696,7 @@ mod test {
// Test builtin mul with large numbers // Test builtin mul with large numbers
assert_eq!( assert_eq!(
ctx.eval("builtins.mul 1000000000 1000000000") ctx.eval("builtins.mul 1000000000 1000000000").unwrap(),
.unwrap(),
Value::Const(Const::Int(1000000000000000000i64)) Value::Const(Const::Int(1000000000000000000i64))
); );
} }

View File

@@ -23,14 +23,17 @@ pub fn run(script: &str) -> Result<Value> {
struct RuntimeContext<'a, 'b> { struct RuntimeContext<'a, 'b> {
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>, scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>, is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
is_primop_symbol: Option<v8::Local<'a, v8::Symbol>>,
} }
impl<'a, 'b> RuntimeContext<'a, 'b> { impl<'a, 'b> RuntimeContext<'a, 'b> {
fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self { fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self {
let is_thunk_symbol = Self::get_is_thunk_symbol(scope); let is_thunk_symbol = Self::get_is_thunk_symbol(scope);
let is_primop_symbol = Self::get_is_primop_symbol(scope);
Self { Self {
scope, scope,
is_thunk_symbol, is_thunk_symbol,
is_primop_symbol,
} }
} }
@@ -50,6 +53,23 @@ impl<'a, 'b> RuntimeContext<'a, 'b> {
None None
} }
} }
fn get_is_primop_symbol(
scope: &v8::PinnedRef<'a, v8::HandleScope<'b>>,
) -> Option<v8::Local<'a, v8::Symbol>> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")?;
let nix_obj = global.get(scope, nix_key.into())?.to_object(scope)?;
let is_primop_sym_key = v8::String::new(scope, "IS_PRIMOP")?;
let is_primop_sym = nix_obj.get(scope, is_primop_sym_key.into())?;
if is_primop_sym.is_symbol() {
is_primop_sym.try_cast().ok()
} else {
None
}
}
} }
fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> { fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> {
@@ -127,8 +147,11 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
_ if val.is_number() => { _ if val.is_number() => {
let val = val.to_number(scope).unwrap().value(); let val = val.to_number(scope).unwrap().value();
// Heuristic: convert whole numbers to Int (for backward compatibility and JS interop) // Heuristic: convert whole numbers to Int (for backward compatibility and JS interop)
if val.is_finite() && val.fract() == 0.0 if val.is_finite()
&& val >= i64::MIN as f64 && val <= i64::MAX as f64 { && val.fract() == 0.0
&& val >= i64::MIN as f64
&& val <= i64::MAX as f64
{
Value::Const(Const::Int(val as i64)) Value::Const(Const::Int(val as i64))
} else { } else {
Value::Const(Const::Float(val)) Value::Const(Const::Float(val))
@@ -152,7 +175,15 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
.collect(); .collect();
Value::List(List::new(list)) Value::List(List::new(list))
} }
_ if val.is_function() => Value::Func, _ if val.is_function() => {
if let Some(name) = primop_app_name(val, ctx) {
Value::PrimOpApp(name)
} else if let Some(name) = primop_name(val, ctx) {
Value::PrimOp(name)
} else {
Value::Func
}
}
_ if val.is_object() => { _ if val.is_object() => {
if is_thunk(val, ctx) { if is_thunk(val, ctx) {
return Value::Thunk; return Value::Thunk;
@@ -193,6 +224,58 @@ fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
matches!(obj.get(scope, is_thunk_sym.into()), Some(v) if v.is_true()) matches!(obj.get(scope, is_thunk_sym.into()), Some(v) if v.is_true())
} }
/// Check if a function is a primop
fn primop_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeContext<'a, 'b>,
) -> Option<String> {
if !val.is_function() {
return None;
}
// Use cached IS_PRIMOP symbol from context
let is_primop_sym = ctx.is_primop_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, is_primop_sym.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(name_key) = v8::String::new(scope, "name")
&& let Some(name_val) = metadata_obj.get(scope, name_key.into())
{
Some(name_val.to_rust_string_lossy(scope))
} else {
None
}
}
/// Check if a primop is partially applied (has applied > 0)
fn primop_app_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeContext<'a, 'b>,
) -> Option<String> {
let name = primop_name(val, ctx)?;
// Get cached IS_PRIMOP symbol
let is_primop_sym = ctx.is_primop_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, is_primop_sym.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
}
}
#[test] #[test]
fn to_value_working() { fn to_value_working() {
assert_eq!( assert_eq!(

View File

@@ -185,9 +185,9 @@ pub enum Value {
/// A function (lambda). /// A function (lambda).
Func, Func,
/// A primitive (built-in) operation. /// A primitive (built-in) operation.
PrimOp, PrimOp(String),
/// A partially applied primitive operation. /// A partially applied primitive operation.
PrimOpApp, PrimOpApp(String),
/// A marker for a value that has been seen before during serialization, to break cycles. /// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures. /// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated, Repeated,
@@ -201,10 +201,10 @@ impl Display for Value {
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}"),
Thunk => write!(f, "<CODE>"), Thunk => write!(f, "«code»"),
Func => write!(f, "<LAMBDA>"), Func => write!(f, "«lambda»"),
PrimOp => write!(f, "<PRIMOP>"), PrimOp(name) => write!(f, "«primop {name}»"),
PrimOpApp => write!(f, "<PRIMOP-APP>"), PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
Repeated => write!(f, "<REPEATED>"), Repeated => write!(f, "<REPEATED>"),
} }
} }