"use strict"; const assert = require("assert"); const types = require("@babel/types"); const template = require("@babel/template").default; const templateExpression = require("./util/template-expression"); const NoChange = require("../astformer/actions/no-change"); 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); } }); }, } };