feat: implement coerceToString
This commit is contained in:
@@ -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": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -22,13 +22,16 @@
|
|||||||
"rustfmt"
|
"rustfmt"
|
||||||
"rust-analyzer"
|
"rust-analyzer"
|
||||||
])
|
])
|
||||||
|
cargo-outdated
|
||||||
lldb
|
lldb
|
||||||
valgrind
|
valgrind
|
||||||
claude-code
|
hyperfine
|
||||||
|
|
||||||
nodejs
|
nodejs
|
||||||
nodePackages.npm
|
nodePackages.npm
|
||||||
biome
|
biome
|
||||||
|
|
||||||
|
claude-code
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Conversion and serialization builtin functions (unimplemented)
|
* Conversion and serialization builtin functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue } from "../types";
|
import type { NixValue } from "../types";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
|
||||||
export const fromJSON = (e: NixValue): never => {
|
export const fromJSON = (e: NixValue): never => {
|
||||||
throw new Error("Not implemented: fromJSON");
|
throw new Error("Not implemented: fromJSON");
|
||||||
@@ -20,6 +21,176 @@ export const toXML = (e: NixValue): never => {
|
|||||||
throw new Error("Not implemented: toXML");
|
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 { select, selectWithDefault, validateParams, resolvePath, hasAttr } from "./helpers";
|
||||||
import { op } from "./operators";
|
import { op } from "./operators";
|
||||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
|
||||||
export type NixRuntime = typeof Nix;
|
export type NixRuntime = typeof Nix;
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export const Nix = {
|
|||||||
selectWithDefault,
|
selectWithDefault,
|
||||||
validateParams,
|
validateParams,
|
||||||
resolvePath,
|
resolvePath,
|
||||||
|
coerceToString,
|
||||||
|
StringCoercionMode,
|
||||||
|
|
||||||
op,
|
op,
|
||||||
builtins,
|
builtins,
|
||||||
|
|||||||
@@ -292,8 +292,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|part| {
|
.map(|part| {
|
||||||
let compiled = ctx.get_ir(*part).compile(ctx);
|
let compiled = ctx.get_ir(*part).compile(ctx);
|
||||||
// TODO: coerce to string
|
// TODO: copyToStore
|
||||||
format!("String(Nix.force({}))", compiled)
|
format!("Nix.coerceToString({}, Nix.StringCoercionMode.Interpolation, false)", compiled)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user