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