"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 ;
}
}
/ * F I X M E : C a s e s t o t e s t / v e r i f y :
{ 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 ;
}
}
}
} ;