You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

105 lines
3.2 KiB
JavaScript

"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;
}
}
}
};