diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 867a1c7..e515e54 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -23,7 +23,7 @@ import { op } from "./operators"; import { builtins, PRIMOP_METADATA } from "./builtins"; import { coerceToString, StringCoercionMode } from "./builtins/conversion"; import { HAS_CONTEXT } from "./string-context"; -import { IS_PATH, mkFunction } from "./types"; +import { IS_PATH, mkAttrs, mkFunction } from "./types"; import { forceBool } from "./type-assert"; export type NixRuntime = typeof Nix; @@ -52,6 +52,7 @@ export const Nix = { coerceToString, concatStringsWithContext, StringCoercionMode, + mkAttrs, mkFunction, pushContext, diff --git a/nix-js/runtime-ts/src/types.ts b/nix-js/runtime-ts/src/types.ts index 6537288..ab4982a 100644 --- a/nix-js/runtime-ts/src/types.ts +++ b/nix-js/runtime-ts/src/types.ts @@ -2,10 +2,11 @@ * Core TypeScript type definitions for nix-js runtime */ -import { IS_THUNK } from "./thunk"; -import { type StringWithContext, HAS_CONTEXT, isStringWithContext } from "./string-context"; +import { force, IS_THUNK } from "./thunk"; +import { type StringWithContext, HAS_CONTEXT, isStringWithContext, getStringContext } from "./string-context"; import { op } from "./operators"; import { forceAttrs } from "./type-assert"; +import { isString, typeOf } from "./builtins/type-check"; export { HAS_CONTEXT, isStringWithContext }; export type { StringWithContext }; @@ -73,6 +74,24 @@ export const mkFunction = ( return func; }; +export const mkAttrs = (attrs: NixAttrs, keys: NixValue[], values: NixValue[]): NixAttrs => { + const len = keys.length; + for (let i = 0; i < len; i++) { + const key = force(keys[i]); + if (key === null) { + continue; + } + if (!isString(key)) { + throw `Expected string, got ${typeOf(key)}` + } + if (isStringWithContext(key)) { + throw new TypeError(`the string '${key.value}' is not allowed to refer to a store path`) + } + attrs[key] = values[i]; + } + return attrs; +}; + /** * Interface for lazy thunk values * Thunks delay evaluation until forced diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index cec2772..b0bb598 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -353,24 +353,28 @@ impl Compile for AttrSet { attrs.push(format!("{}:{}", key.escape_quote(), value)); } - // FIXME: duplicated key - for (key_expr, value_expr) in &self.dyns { - let key = ctx.get_ir(*key_expr).compile(ctx); - let value_code = ctx.get_ir(*value_expr).compile(ctx); - - let value = if stack_trace_enabled { - let value_span = encode_span(ctx.get_ir(*value_expr).span(), ctx); - format!( - "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", - value_span, value_code - ) - } else { - value_code - }; - attrs.push(format!("[{}]:{}", key, value)); + if !self.dyns.is_empty() { + let (keys, vals) = self.dyns.iter().map(|&(key, val)| { + let key = ctx.get_ir(key).compile(ctx); + let val_expr = ctx.get_ir(val); + let val = val_expr.compile(ctx); + let span = val_expr.span(); + let val = if stack_trace_enabled { + let span = encode_span(span, ctx); + format!( + "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", + span, val + ) + } else { + val + }; + (key, val) + }).collect::<(Vec<_>, Vec<_>)>(); + format!("Nix.mkAttrs({{{}}},[{}],[{}])", attrs.join(","), keys.join(","), vals.join(",")) + } else { + format!("{{{}}}", attrs.join(",")) } - format!("{{{}}}", attrs.join(",")) } }