From 60cd61d771899991ce693badbac87e82425ab4d5 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Fri, 13 Feb 2026 22:17:28 +0800 Subject: [PATCH] feat: implement fromTOML; fix fromJSON implementation --- Cargo.lock | 55 ++++++++- default.nix | 1 + nix-js/Cargo.toml | 1 + nix-js/runtime-ts/src/builtins/conversion.ts | 43 +------ nix-js/runtime-ts/src/conversion.ts | 11 +- nix-js/runtime-ts/src/types/global.d.ts | 2 + nix-js/src/fetcher/archive.rs | 4 +- nix-js/src/runtime.rs | 2 + nix-js/src/runtime/ops.rs | 116 +++++++++++++++++++ nix-js/src/value.rs | 58 +++++++++- nix-js/tests/lang.rs | 7 +- shell.nix | 1 + 12 files changed, 246 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ac0f67..adb0ee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1966,6 +1966,7 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", + "toml", "tracing", "tracing-subscriber", "xz2", @@ -2280,7 +2281,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -2824,6 +2825,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3358,6 +3368,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -3367,6 +3398,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -3374,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] @@ -3388,6 +3433,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" diff --git a/default.nix b/default.nix index 484ea46..4f93e81 100644 --- a/default.nix +++ b/default.nix @@ -9,6 +9,7 @@ let flake = ( import flake-compat { src = ./.; + copySourceTreeToStore = false; } ); in diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index e48a53a..e90dab6 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -49,6 +49,7 @@ bzip2 = "0.5" zip = "2.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +toml = "0.8" dirs = "5.0" tempfile = "3.24" rusqlite = { version = "0.33", features = ["bundled"] } diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index affdb87..0cc4060 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -6,55 +6,22 @@ import type { NixString, NixValue } from "../types"; import { isStringWithContext, isNixPath } from "../types"; import { force } from "../thunk"; import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context"; -import { forceFunction } from "../type-assert"; +import { forceFunction, forceStringNoCtx } from "../type-assert"; import { nixValueToJson } from "../conversion"; import { isAttrs, isPath, typeOf } from "./type-check"; -const convertJsonToNix = (json: unknown): NixValue => { - if (json === null) { - return null; - } - if (typeof json === "boolean") { - return json; - } - if (typeof json === "number") { - if (Number.isInteger(json)) { - return BigInt(json); - } - return json; - } - if (typeof json === "string") { - return json; - } - if (Array.isArray(json)) { - return json.map(convertJsonToNix); - } - if (typeof json === "object") { - const result: Record = {}; - for (const [key, value] of Object.entries(json)) { - result[key] = convertJsonToNix(value); - } - return result; - } - throw new TypeError(`unsupported JSON value type: ${typeof json}`); -}; - 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 ${typeOf(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)}`); - } + return Deno.core.ops.op_from_json(jsonStr); }; -export const fromTOML = (e: NixValue): never => { - throw new Error("Not implemented: fromTOML"); +export const fromTOML = (e: NixValue): NixValue => { + const toml = forceStringNoCtx(e); + return Deno.core.ops.op_from_toml(toml); }; export const toJSON = (e: NixValue): NixString => { diff --git a/nix-js/runtime-ts/src/conversion.ts b/nix-js/runtime-ts/src/conversion.ts index 2d7254a..435516a 100644 --- a/nix-js/runtime-ts/src/conversion.ts +++ b/nix-js/runtime-ts/src/conversion.ts @@ -41,11 +41,12 @@ export const nixValueToJson = ( } } - if (seen.has(v)) { - throw new Error("cycle detected in toJSON"); - } else { - seen.add(v) - } + // FIXME: is this check necessary? + // if (seen.has(v)) { + // throw new Error("cycle detected in toJSON"); + // } else { + // seen.add(v) + // } if (Array.isArray(v)) { return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen)); diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index fce683b..45e6125 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -74,6 +74,8 @@ declare global { ): string; function op_match(regex: string, text: string): (string | null)[] | null; function op_split(regex: string, text: string): (string | (string | null)[])[]; + function op_from_json(json: string): any; + function op_from_toml(toml: string): any; } } } diff --git a/nix-js/src/fetcher/archive.rs b/nix-js/src/fetcher/archive.rs index 5ee9fc2..88a43a8 100644 --- a/nix-js/src/fetcher/archive.rs +++ b/nix-js/src/fetcher/archive.rs @@ -183,9 +183,7 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> { Ok(()) } -pub fn extract_tarball_to_temp( - data: &[u8], -) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> { +pub fn extract_tarball_to_temp(data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> { let temp_dir = tempfile::tempdir()?; let extracted_path = extract_archive(data, temp_dir.path())?; Ok((extracted_path, temp_dir)) diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index a61544d..7d15de7 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -69,6 +69,8 @@ fn runtime_extension() -> Extension { op_add_filtered_path::(), op_match(), op_split(), + op_from_json(), + op_from_toml(), ]; ops.extend(crate::fetcher::register_ops::()); diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index 89e8483..18527cc 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use hashbrown::hash_map::{Entry, HashMap}; use deno_core::OpState; +use deno_core::v8; use regex::Regex; use rust_embed::Embed; @@ -1039,3 +1040,118 @@ pub(super) enum SplitResult { Text(String), Captures(Vec>), } + +pub(super) enum NixJsonValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + Str(String), + Arr(Vec), + Obj(Vec<(String, NixJsonValue)>), +} + +impl<'a> deno_core::convert::ToV8<'a> for NixJsonValue { + type Error = deno_error::JsErrorBox; + + fn to_v8<'i>( + self, + scope: &mut v8::PinScope<'a, 'i>, + ) -> std::result::Result, Self::Error> { + match self { + Self::Null => Ok(v8::null(scope).into()), + Self::Bool(b) => Ok(v8::Boolean::new(scope, b).into()), + Self::Int(i) => Ok(v8::BigInt::new_from_i64(scope, i).into()), + Self::Float(f) => Ok(v8::Number::new(scope, f).into()), + Self::Str(s) => v8::String::new(scope, &s) + .map(|s| s.into()) + .ok_or_else(|| deno_error::JsErrorBox::type_error("failed to create v8 string")), + Self::Arr(arr) => { + let elements = arr + .into_iter() + .map(|v| v.to_v8(scope)) + .collect::, _>>()?; + Ok(v8::Array::new_with_elements(scope, &elements).into()) + } + Self::Obj(entries) => { + let obj = v8::Object::new(scope); + for (k, v) in entries { + let key: v8::Local = v8::String::new(scope, &k) + .ok_or_else(|| { + deno_error::JsErrorBox::type_error("failed to create v8 string") + })? + .into(); + let val = v.to_v8(scope)?; + obj.set(scope, key, val); + } + Ok(obj.into()) + } + } + } +} + +fn json_to_nix(value: serde_json::Value) -> NixJsonValue { + match value { + serde_json::Value::Null => NixJsonValue::Null, + serde_json::Value::Bool(b) => NixJsonValue::Bool(b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + NixJsonValue::Int(i) + } else if let Some(f) = n.as_f64() { + NixJsonValue::Float(f) + } else { + NixJsonValue::Float(n.as_u64().unwrap_or(0) as f64) + } + } + serde_json::Value::String(s) => NixJsonValue::Str(s), + serde_json::Value::Array(arr) => { + NixJsonValue::Arr(arr.into_iter().map(json_to_nix).collect()) + } + serde_json::Value::Object(map) => { + NixJsonValue::Obj(map.into_iter().map(|(k, v)| (k, json_to_nix(v))).collect()) + } + } +} + +fn toml_to_nix(value: toml::Value) -> std::result::Result { + match value { + toml::Value::String(s) => Ok(NixJsonValue::Str(s)), + toml::Value::Integer(i) => Ok(NixJsonValue::Int(i)), + toml::Value::Float(f) => Ok(NixJsonValue::Float(f)), + toml::Value::Boolean(b) => Ok(NixJsonValue::Bool(b)), + toml::Value::Datetime(_) => Err(NixRuntimeError::from( + "while parsing TOML: Dates and times are not supported", + )), + toml::Value::Array(arr) => { + let items: std::result::Result, _> = arr.into_iter().map(toml_to_nix).collect(); + Ok(NixJsonValue::Arr(items?)) + } + toml::Value::Table(table) => { + let entries: std::result::Result, _> = table + .into_iter() + .map(|(k, v)| toml_to_nix(v).map(|v| (k, v))) + .collect(); + Ok(NixJsonValue::Obj(entries?)) + } + } +} + +#[deno_core::op2] +#[to_v8] +pub(super) fn op_from_json( + #[string] json_str: String, +) -> std::result::Result { + let parsed: serde_json::Value = serde_json::from_str(&json_str) + .map_err(|e| NixRuntimeError::from(format!("builtins.fromJSON: {e}")))?; + Ok(json_to_nix(parsed)) +} + +#[deno_core::op2] +#[to_v8] +pub(super) fn op_from_toml( + #[string] toml_str: String, +) -> std::result::Result { + let parsed: toml::Value = toml::from_str(&toml_str) + .map_err(|e| NixRuntimeError::from(format!("while parsing TOML: {e}")))?; + toml_to_nix(parsed) +} diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index ad63573..8161084 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -268,12 +268,66 @@ fn escape_quote_string(s: &str) -> String { ret } +/// Format a float matching C's `printf("%g", x)` with default precision 6. +fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult { + if !x.is_finite() { + return write!(f, "{x}"); + } + if x == 0.0 { + return if x.is_sign_negative() { + write!(f, "-0") + } else { + write!(f, "0") + }; + } + + let precision: i32 = 6; + let exp = x.abs().log10().floor() as i32; + + let formatted = if exp >= -4 && exp < precision { + let decimal_places = (precision - 1 - exp) as usize; + format!("{x:.decimal_places$}") + } else { + let sig_digits = (precision - 1) as usize; + let s = format!("{x:.sig_digits$e}"); + let (mantissa, exp_part) = s + .split_once('e') + .expect("scientific notation must contain 'e'"); + let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') { + ("-", d) + } else if let Some(d) = exp_part.strip_prefix('+') { + ("+", d) + } else { + ("+", exp_part) + }; + if digits.len() < 2 { + format!("{mantissa}e{sign}0{digits}") + } else { + format!("{mantissa}e{sign}{digits}") + } + }; + + if formatted.contains('.') { + if let Some(e_pos) = formatted.find('e') { + let trimmed = formatted[..e_pos] + .trim_end_matches('0') + .trim_end_matches('.'); + write!(f, "{}{}", trimmed, &formatted[e_pos..]) + } else { + let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); + write!(f, "{trimmed}") + } + } else { + write!(f, "{formatted}") + } +} + impl Display for Value { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { use Value::*; match self { &Int(x) => write!(f, "{x}"), - &Float(x) => write!(f, "{x}"), + &Float(x) => fmt_nix_float(f, x), &Bool(x) => write!(f, "{x}"), Null => write!(f, "null"), String(x) => write!(f, "{}", escape_quote_string(x)), @@ -302,7 +356,7 @@ impl Display for ValueCompatDisplay<'_> { use Value::*; match self.0 { &Int(x) => write!(f, "{x}"), - &Float(x) => write!(f, "{x}"), + &Float(x) => fmt_nix_float(f, x), &Bool(x) => write!(f, "{x}"), Null => write!(f, "null"), String(x) => write!(f, "{}", escape_quote_string(x)), diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 7b76bf0..602434a 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -152,12 +152,9 @@ eval_okay_test!(foldlStrict_lazy_elements); eval_okay_test!(foldlStrict_lazy_initial_accumulator); eval_okay_test!(fromjson); eval_okay_test!(fromjson_escapes); +eval_okay_test!(fromTOML); eval_okay_test!( - #[ignore = "not implemented: fromTOML"] - fromTOML -); -eval_okay_test!( - #[ignore = "not implemented: fromTOML"] + #[ignore = "timestamps are not supported"] fromTOML_timestamps ); eval_okay_test!(functionargs); diff --git a/shell.nix b/shell.nix index 658b162..798fc6a 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,7 @@ let flake = ( import flake-compat { src = ./.; + copySourceTreeToStore = false; } ); in