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.

176 lines
4.9 KiB
JavaScript

"use strict";
const assert = require("assert");
const types = require("@babel/types");
const template = require("@babel/template").default;
const splitFilter = require("split-filter");
const unpackExpression = require("./util/unpack-expression");
const templateExpression = require("./util/template-expression");
const NoChange = require("../astformer/actions/no-change");
const printAst = require("../print-ast");
const lazyWrapper = require("./templates/lazy-wrapper");
const callLazyWrapper = require("./templates/call-lazy-wrapper");
const objectLiteral = require("./templates/object-literal");
// TODO: Optimize lazy evaluation wrappers by only unpacking them selectively when used in an actual expression; in particular, that avoids the "wrapper that just calls another wrapper" overhead when passing attributes as function arguments
// TODO: Change to a prototype-based scope object chain? This should produce more consistent output regardless of whether a given set of bindings is static vs. dynamic and recursive vs. non-recursive
let tmplAssertKeys = template(`
$assertUniqueKeys( %%keyList%% )
`);
let tmplObjectNormal = templateExpression(`
(() => {
%%keyAssertion%%;
return %%object%%;
})()
`);
let tmplScopeWrapper = templateExpression(`(
(() => {
%%bindings%%;
return %%object%%;
})()
)`);
let tmplDynamicScopeWrapper = templateExpression(`(
(() => {
%%keyAssertion%%;
let $attributes = {};
with ($attributes) {
/* Static and overrides */
Object.assign($attributes, {
%%bindings%%;
});
}
return $attributes;
})()
)`);
// FIXME: Verify that this always works, and that we don't need `var` for hoisting!
let tmplRecursiveBinding = template(`
const %%name%% = %%expression%%;
`);
function isDynamicBinding(binding) {
return binding.attrpath.attr[0].type !== "NixAttributeIdentifier";
}
function objectNormal(bindings) {
return tmplObjectNormal({
object: objectLiteral(bindings.map(({ name, expression }) => {
return [ name, expression ];
})),
keyAssertion: bindings.some((binding) => typeof binding.name !== "string")
// Only needed when dealing with dynamic keys
? assertKeys(bindings.map(({ name }) => implicitStringLiteral(name)))
: null
});
}
function objectRecursiveStatic(bindings) {
return tmplScopeWrapper({
bindings: bindings.map(({ name, expression }) => {
return tmplRecursiveBinding({
name: name,
expression: expression
});
}),
object: objectLiteral(bindings.map(({ name }) => {
assert(typeof name === "string");
return [ name, types.identifier(name) ];
}))
});
}
function objectRecursiveDynamic(bindings) {
throw new Error(`UNIMPLEMENTED: Dynamic bindings are not supported yet`);
return tmplDynamicScopeWrapper({
});
}
function implicitStringLiteral(node) {
if (typeof node === "string") {
return types.stringLiteral(node);
} else {
return node;
}
}
function assertKeys(keys) {
return tmplAssertKeys({ keyList: types.arrayExpression(keys) });
}
module.exports = {
name: "attribute-sets",
visitors: {
Program: (_node, { setContext }) => {
setContext(null, "attributeSets_inRecursiveSet", false);
return NoChange;
},
NixAttributeSelection: (_node, { defer }) => {
// TODO: Optimize dynamic attribute access by translating builtins.getAttr to dynamic property access at compile time? This requires scope analysis!
return defer((node) => {
assert(node.attrpath.type === "NixAttributePath");
return node.attrpath.attr.reduce((last, identifier) => {
assert(identifier.type === "NixAttributeIdentifier");
return callLazyWrapper(types.memberExpression(last, types.identifier(identifier.name)));
}, node.expression);
});
},
NixAttributeSet: (node, { defer, setContext, getContext }) => {
// if (node.recursive) {
// setContext(null, "attributeSets_inRecursiveSet", true);
// }
return defer((node) => {
let isRecursive = node.recursive;
// let [ dynamicNodes, staticNodes ] = splitFilter(node.bind, (binding) => isDynamicBinding(binding));
// let staticBindings = staticNodes.map((binding) => {
// assert(binding.attrpath.attr.length === 1); // Nested attributes should have been desugared by this point
// return {
// name: binding.attrpath.attr[0].name,
// expression: lazyWrapper(binding.expression)
// };
// });
let bindings = node.bind.map((binding) => {
assert(binding.attrpath.attr.length === 1); // Nested attributes should have been desugared by this point
return {
name: (isDynamicBinding(binding))
? binding.attrpath.attr[0]
: binding.attrpath.attr[0].name,
expression: lazyWrapper(binding.expression)
};
});
let hasDynamicBindings = bindings.some((binding) => typeof binding.name !== "string");
if (isRecursive) {
if (hasDynamicBindings) {
return objectRecursiveDynamic(bindings);
} else {
return objectRecursiveStatic(bindings);
}
} else {
return objectNormal(bindings);
}
});
},
}
};