"use strict"; const unreachable = require("@joepie91/unreachable")("jsNix"); const NoChange = require("../astformer/actions/no-change"); const { NixAttributeIdentifier, NixAttributeSet, NixBinding } = require("./util/nix-types"); function isAttributeSet(node) { return (node.type === "NixAttributeSet"); } function mergeAttributeSets(a, b) { return NixAttributeSet([ ... a.bind, ... b.bind ]); } function mergeBindings(name, a, b) { let attributes = mergeAttributeSets(unpackBindingValue(a), unpackBindingValue(b)); return NixBinding( [ NixAttributeIdentifier(name) ], attributes ); } function unpackBindingValue(binding) { // binding -> value for top-level attribute of the binding if (binding.attrpath.attr.length > 1) { // Nested attribute path syntax, synthesize a new attribute set with the first attribute snipped off - this new attribute set represents the *value* of the binding return NixAttributeSet([ NixBinding(binding.attrpath.attr.slice(1), binding.expression) ]); } else { return binding.expression; } } /* FIXME: Cases to test/verify: { a = 4; a.b = 5; } { a = {}; a.b = 5; } { a.c = 4; a.b = 5; } { a = 4; a = 5; } */ module.exports = { name: "desugar-attrsets", visitors: { NixAttributeSet: (node) => { let neededDesugaring = false; let newStaticBindings = {}; let dynamicBindings = []; for (let binding of node.bind) { if (binding.type === "NixBinding") { if (binding.attrpath.attr.length > 1) { neededDesugaring = true; } let firstAttribute = binding.attrpath.attr[0]; let isStaticAttribute = firstAttribute.type === "NixAttributeIdentifier"; if (isStaticAttribute) { let attributeName = firstAttribute.name; let existingBinding = newStaticBindings[attributeName]; if (existingBinding == null) { newStaticBindings[attributeName] = NixBinding( [ firstAttribute ], unpackBindingValue(binding) ); } else { if (isAttributeSet(existingBinding.expression) && isAttributeSet(binding.expression)) { neededDesugaring = true; newStaticBindings[attributeName] = mergeBindings(attributeName, existingBinding, binding); } else { throw new Error(`Key '${attributeName}' was specified twice, but this is only allowed when both values are an attribute set`); } } } else { // FIXME: Needs runtime check, need to *always* construct objects at runtime when dynamic bindings are involved // FIXME: Still just desugar here? And expect another transformer to deal with the runtime checks // desugar but do not merge, dynamic bindings cannot be combined with attribute paths on the same key dynamicBindings.push(binding); } } else { unreachable(`unrecognized binding type: ${binding.type}`); } } let staticBindingList = Object.entries(newStaticBindings).map(([ _key, value ]) => { return value; }); // We only check this here because there are multiple things that could cause a need for desugaring, and this is simpler for now than having a separate 'check first' implementation if (neededDesugaring) { return NixAttributeSet([ ... staticBindingList, ... dynamicBindings ], node.recursive); } else { return NoChange; } } } };