import { isValidFeatureName , Context , CompiledExecutable } from "sqrl" ; import { DefaultDict } from "../jslib/DefaultDict" ; interface Options { includeCost ? : boolean ; reverseEdges ? : boolean ; clusterRules ? : boolean ; clusterFeatures ? : boolean ; fillColor ? : string ; includeSlots ? : boolean ; } interface FeatureEdges { [ feature ] : Set < string > ; } interface FeatureNode { name : string ; feature : boolean ; rule : boolean ; cost : number ; recursiveCost : number ; } export function generateDotFile ( ctx , compiled , features = [ ] , depends = [ ] , ignore = [ ] , options = { clusterFeatures : false , clusterRules : false , includeCost : false , reverseEdges : false , fillColor : "" , includeSlots : false , } ) { const { includeCost , includeSlots , reverseEdges , clusterRules , clusterFeatures , } = options ; const ignoreSet = new Set ( ignore ) ; const nodes = { } ; const forward = new DefaultDict ( ( ) => new Set ( ) ) ; const reverse = new DefaultDict ( ( ) => new Set ( ) ) ; const slotNames = compiled . getSlotNames ( ) ; const slotLoad = compiled . getSlotLoad ( ) ; slotNames . forEach ( ( slotName , idx ) => { slotLoad [ idx ] . forEach ( ( loadIdx ) => { const feature = slotNames [ loadIdx ] ; forward [ slotName ] . add ( feature ) ; reverse [ feature ] . add ( slotName ) ; } ) ; nodes [ slotName ] = { name : slotName , feature : false , rule : false , cost : 0 , recursiveCost : 0 , } ; } ) ; Object . entries ( compiled . getFeatureDocs ( ) ) . forEach ( ( [ name , doc ] ) => { nodes [ name ] = { name , feature : true , rule : false , cost : doc . cost , recursiveCost : doc . recursiveCost , } ; } ) ; Object . entries ( compiled . getRuleSpecs ( ) ) . forEach ( ( [ name , spec ] ) => { nodes [ name ] . rule = true ; } ) ; function recurse ( name , edges , recurseSeen ) { if ( ignoreSet . has ( name ) ) { return ; } else if ( ! nodes . hasOwnProperty ( name ) ) { ctx . warn ( { } , "Missing node for dot file:: %s" , name ) ; return ; } recurseSeen . add ( name ) ; edges [ name ] . forEach ( ( feature ) => recurse ( feature , edges , recurseSeen ) ) ; } const seen = new Set ( ) ; const seenDepends = new Set ( ) ; for ( const feature of features ) { recurse ( feature , forward , seen ) ; } for ( const feature of depends ) { recurse ( feature , reverse , seenDepends ) ; } const render = new Set ( [ ... seen , ... seenDepends ] ) ; let rv = `digraph features {\n` ; const renderNodes = Array . from ( render ) . sort ( ) . filter ( ( name ) => ! ignoreSet . has ( name ) ) . map ( ( name ) => nodes [ name ] ) ; function quoteName ( name ) { return isValidFeatureName ( name ) ? name : JSON . stringify ( name ) ; } function nodesToDot ( filteredNodes , color , subgraph ) { let nodesDot = filteredNodes . map ( ( node ) => { let label = node . name ; if ( ! includeSlots && ! isValidFeatureName ( node . name ) ) { return `${ quoteName ( node . name ) } [label="" style="filled" color="#aaaaaa" fillcolor="#eeeeee"];\n` ; } if ( includeCost ) { label += "\n$" + node . recursiveCost . toFixed ( 2 ) ; label = label . trim ( ) ; } const fill = "#ffffff" ; const style = "" ; return `${ quoteName ( node . name ) } [label=${ JSON . stringify ( label ) } color="${ color }" style=${ JSON . stringify ( style ) } fillcolor=${ JSON . stringify ( fill ) }];\n` ; } ) . join ( "" ) ; if ( subgraph ) { nodesDot = `subgraph ${ subgraph } {\n${ nodesDot }}\n` ; } return nodesDot ; } rv += nodesToDot ( renderNodes . filter ( ( node ) => node . rule ) , "red" , clusterRules ? "cluster_rules" : null ) ; rv += nodesToDot ( renderNodes . filter ( ( node ) => ! node . rule ) , "blue" , clusterFeatures ? "cluster_features" : null ) ; renderNodes . forEach ( ( node ) => { Array . from ( forward [ node . name ] ) . sort ( ) . forEach ( ( feature ) => { if ( render . has ( feature ) && ! ignoreSet . has ( feature ) ) { if ( reverseEdges ) { rv += `${ quoteName ( node . name ) } -> ${ quoteName ( feature ) };\n` ; } else { rv += `${ quoteName ( feature ) } -> ${ quoteName ( node . name ) };\n` ; } } } ) ; } ) ; rv += `}\n` ; return rv ; }