feat: implement coerceToString
This commit is contained in:
5 files changed
+184
-7
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
||||
@@ -22,13 +22,16 @@
|
||||
"rustfmt"
|
||||
"rust-analyzer"
|
||||
])
|
||||
cargo-outdated
|
||||
lldb
|
||||
valgrind
|
||||
claude-code
|
||||
hyperfine
|
||||
|
||||
nodejs
|
||||
nodePackages.npm
|
||||
biome
|
||||
|
||||
claude-code
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Conversion and serialization builtin functions (unimplemented)
|
||||
* Conversion and serialization builtin functions
|
||||
*/
|
||||
|
||||
import type { NixValue } from "../types";
|
||||
import { force } from "../thunk";
|
||||
|
||||
export const fromJSON = (e: NixValue): never => {
|
||||
throw new Error("Not implemented: fromJSON");
|
||||
@@ -20,6 +21,176 @@ export const toXML = (e: NixValue): never => {
|
||||
throw new Error("Not implemented: toXML");
|
||||
};
|
||||
|
||||
export const toString = (name: NixValue, s: NixValue): never => {
|
||||
throw new Error("Not implemented: toString");
|
||||
/**
|
||||
* String coercion modes control which types can be coerced to strings
|
||||
*
|
||||
* - Base: Only strings are allowed (no coercion)
|
||||
* - Interpolation: Used in string interpolation "${expr}" - allows strings and integers
|
||||
* - ToString: Used in builtins.toString - allows all types (bools, floats, null, lists, etc.)
|
||||
*/
|
||||
export enum StringCoercionMode {
|
||||
Base = 0,
|
||||
Interpolation = 1,
|
||||
ToString = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get human-readable type names for error messages
|
||||
*/
|
||||
const typeName = (value: NixValue): string => {
|
||||
const val = force(value);
|
||||
|
||||
if (typeof val === "bigint") return "int";
|
||||
if (typeof val === "number") return "float";
|
||||
if (typeof val === "boolean") return "boolean";
|
||||
if (typeof val === "string") return "string";
|
||||
if (val === null) return "null";
|
||||
if (Array.isArray(val)) return "list";
|
||||
if (typeof val === "function") return "lambda";
|
||||
if (typeof val === "object") return "attribute set";
|
||||
|
||||
return `unknown type`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Coerce a Nix value to a string according to the specified mode.
|
||||
* This implements the same behavior as Lix's EvalState::coerceToString.
|
||||
*
|
||||
* @param value - The value to coerce
|
||||
* @param mode - The coercion mode (controls which types are allowed)
|
||||
* @param copyToStore - If true, paths should be copied to the Nix store (not implemented yet)
|
||||
* @returns The string representation of the value
|
||||
* @throws TypeError if the value cannot be coerced in the given mode
|
||||
*
|
||||
* Coercion rules by type:
|
||||
* - String: Always returns as-is
|
||||
* - Path: Returns the path string (copyToStore not implemented yet)
|
||||
* - Integer: Only in Interpolation or ToString mode
|
||||
* - Float: Only in ToString mode
|
||||
* - Boolean: Only in ToString mode (true → "1", false → "")
|
||||
* - Null: Only in ToString mode (→ "")
|
||||
* - List: Only in ToString mode (recursively coerce elements, join with spaces)
|
||||
* - Attrs: Check for __toString method or outPath attribute
|
||||
* - Function: Never coercible (throws error)
|
||||
*/
|
||||
export const coerceToString = (
|
||||
value: NixValue,
|
||||
mode: StringCoercionMode = StringCoercionMode.ToString,
|
||||
copyToStore: boolean = false,
|
||||
): string => {
|
||||
const v = force(value);
|
||||
|
||||
// Strings are always returned as-is, regardless of mode
|
||||
if (typeof v === "string") {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Attribute sets can define custom string conversion via __toString method
|
||||
// or may have an outPath attribute (for derivations and paths)
|
||||
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
||||
// First, try the __toString method if present
|
||||
// This allows custom types to define their own string representation
|
||||
if ("__toString" in v) {
|
||||
// Force the method in case it's a thunk
|
||||
const toStringMethod = force(v["__toString"]);
|
||||
if (typeof toStringMethod === "function") {
|
||||
// Call the method with self as argument
|
||||
const result = force(toStringMethod(v));
|
||||
if (typeof result !== "string") {
|
||||
throw new TypeError(`__toString returned ${typeName(result)} instead of string`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no __toString, try outPath (used for derivations and store paths)
|
||||
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
|
||||
if ("outPath" in v) {
|
||||
// Recursively coerce the outPath value (it might itself be an attrs with __toString)
|
||||
return coerceToString(v["outPath"], mode, copyToStore);
|
||||
}
|
||||
|
||||
// Attribute sets without __toString or outPath cannot be coerced
|
||||
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||
}
|
||||
|
||||
// Integer coercion is allowed in Interpolation and ToString modes
|
||||
// This enables string interpolation like "value: ${42}"
|
||||
if (mode >= StringCoercionMode.Interpolation) {
|
||||
if (typeof v === "bigint") {
|
||||
return v.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// The following types are only coercible in ToString mode (builtins.toString)
|
||||
if (mode >= StringCoercionMode.ToString) {
|
||||
// Booleans: true → "1", false → ""
|
||||
// This is for shell scripting convenience (same as null)
|
||||
if (typeof v === "boolean") {
|
||||
return v ? "1" : "";
|
||||
}
|
||||
|
||||
// Floats are converted using JavaScript's default toString
|
||||
if (typeof v === "number") {
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
// Null becomes empty string (for shell scripting convenience)
|
||||
if (v === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Lists are recursively converted and joined with spaces
|
||||
// We cannot use Array.join() directly because of special spacing rules:
|
||||
// - Elements are recursively coerced to strings
|
||||
// - Spaces are added between elements, BUT:
|
||||
// * No space is added after an element if it's an empty list
|
||||
// * The last element never gets a trailing space
|
||||
//
|
||||
// Examples:
|
||||
// - [ 1 2 3 ] → "1 2 3"
|
||||
// - [ 1 [ ] 2 ] → "1 2" (empty list doesn't add space)
|
||||
// - [ 1 [ [ ] ] 2 ] → "1 2" (nested empty list is not itself empty, so adds space)
|
||||
// - [ [ 1 2 ] [ 3 4 ] ] → "1 2 3 4" (nested lists flatten)
|
||||
if (Array.isArray(v)) {
|
||||
let result = "";
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
const item = v[i];
|
||||
// Recursively convert element to string
|
||||
const str = coerceToString(item, mode, copyToStore);
|
||||
result += str;
|
||||
|
||||
// Add space after this element if:
|
||||
// 1. It's not the last element, AND
|
||||
// 2. The element is not an empty list
|
||||
//
|
||||
// Note: We check if the ELEMENT is an empty list, not if its
|
||||
// string representation is empty.
|
||||
// For example, [[]] is not an empty list (length 1), so it gets
|
||||
// a trailing space even though its toString is "".
|
||||
if (i < v.length - 1) {
|
||||
const forcedItem = force(item);
|
||||
if (!Array.isArray(forcedItem) || forcedItem.length !== 0) {
|
||||
result += " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||
};
|
||||
|
||||
/**
|
||||
* builtins.toString - Convert a value to a string
|
||||
*
|
||||
* This is the public builtin function exposed to Nix code.
|
||||
* It uses ToString mode, which allows coercing all types except functions.
|
||||
*
|
||||
* @param value - The value to convert to a string
|
||||
* @returns The string representation
|
||||
*/
|
||||
export const toString = (value: NixValue): string => {
|
||||
return coerceToString(value, StringCoercionMode.ToString, false);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { createThunk, force, is_thunk, IS_THUNK } from "./thunk";
|
||||
import { select, selectWithDefault, validateParams, resolvePath, hasAttr } from "./helpers";
|
||||
import { op } from "./operators";
|
||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
|
||||
export type NixRuntime = typeof Nix;
|
||||
|
||||
@@ -25,6 +26,8 @@ export const Nix = {
|
||||
selectWithDefault,
|
||||
validateParams,
|
||||
resolvePath,
|
||||
coerceToString,
|
||||
StringCoercionMode,
|
||||
|
||||
op,
|
||||
builtins,
|
||||
|
||||
@@ -292,8 +292,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
||||
.iter()
|
||||
.map(|part| {
|
||||
let compiled = ctx.get_ir(*part).compile(ctx);
|
||||
// TODO: coerce to string
|
||||
format!("String(Nix.force({}))", compiled)
|
||||
// TODO: copyToStore
|
||||
format!("Nix.coerceToString({}, Nix.StringCoercionMode.Interpolation, false)", compiled)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
Reference in new issue
Block a user