"use strict"; const assert = require("assert"); const types = require("@babel/types"); const template = require("@babel/template").default; const unpackExpression = require("./_unpack-expression"); const NoChange = require("../astformer/actions/no-change"); const printAst = require("../print-ast"); // FIXME: Add expression parens! let tmplCallLazy = template(` %%wrapper%%() `); let tmplLazyWrapper = template(`( $$jsNix$memoize(() => %%expression%%) )`); let tmplLazyWrapperRecursive = template(`( function() { return %%expression%%; } )`); let tmplExtend = template(`( $$jsNix$extend(this ?? {}, %%object%%) )`); let tmplScopeWrapper = template(`( (() => { %%bindings%%; return %%object%%; })() )`); // FIXME: Verify that this always works, and that we don't need `var` for hoisting! let tmplRecursiveBinding = template(` const %%name%% = %%expression%%; `); function lazyEvaluationWrapper(args) { let { recursive, ... rest } = args; // printAst(rest.expression); let wrapper = (recursive) ? tmplLazyWrapperRecursive : tmplLazyWrapper; return unpackExpression(wrapper(rest)); } function isDynamicBinding(binding) { return binding.attrpath.attr[0].type !== "NixAttributeIdentifier"; } 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 unpackExpression(tmplCallLazy({ wrapper: 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 hasDynamicBindings = node.bind.some((binding) => isDynamicBinding(binding)); let bindings = node.bind.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: lazyEvaluationWrapper({ recursive: isRecursive, expression: binding.expression }) }; }); if (hasDynamicBindings) { throw new Error(`UNIMPLEMENTED: Dynamic bindings are not supported yet`); } else if (isRecursive) { return unpackExpression(tmplScopeWrapper({ bindings: bindings.map(({ name, expression }) => { return tmplRecursiveBinding({ name: name, expression: expression }); }), object: types.objectExpression(bindings.map(({ name }) => { return types.objectProperty( types.stringLiteral(name), types.identifier(name) ); })) })); } else { let object = types.objectExpression(bindings.map(({ name, expression }) => { return types.objectProperty( types.stringLiteral(name), // TODO: Remove the isRecursive distinction here once heap-of-vars works expression ); })); // Only do prototypal inheritance for `this` if we're dealing with nested recursive attribute sets if (isRecursive && getContext("attributeSets_inRecursiveSet") === true) { return unpackExpression(tmplExtend({ object: object })); } else { return object; } } }); }, } };