-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
974 lines (901 loc) · 50 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
var _ = require('lodash'),
extend = require('extend'), // NB: skips undefined values, unlike lodash extend (or lodash assign)
graphlib = require('graphlib'),
jsonschema = require('jsonschema'),
colors = require('colors'),
MersenneTwister = require('mersennetwister'),
SubgraphSearch = require('./subgraph').SubgraphSearch
function isArray (obj) {
return Object.prototype.toString.call(obj) === '[object Array]'
}
/**
* Represents a graph grammar.
* @constructor
* @param {Object} json A description of the graph grammar, matching the [JSON schema]{@link https://ihh.github.io/graphgram/schema_doc.html}.
* @param {Object} [opts] Options
* @param {Boolean} [opts.canonical] If true, the grammar JSON must be strictly canonical: both LHS and RHS must have full `node` and `edge` sections
*/
function Grammar (json, opts) {
if (opts)
extend (this, opts)
if (!this.matcher)
this.matcher = new Matcher()
this.rules = []
if (json) {
this.validate (json)
extend (this, json)
}
this.init()
}
Grammar.prototype.registerRhsLabelFunction = function (funcName, func, schema) {
this.matcher.rhsLabelFunc['$' + funcName] = { func, schema }
}
Grammar.prototype.registerRhsLabelExecFunction = function (funcName, command, description, quoteFlag) {
const execSync = require('child_process').execSync
const argDesc = 'Argument to \'' + command + '\''
const warn = this.warn.bind(this)
this.registerRhsLabelFunction (funcName,
function() {
let args = _.flattenDeep(Array.prototype.slice.call(arguments,0));
if (quoteFlag)
args = '"' + args.join(' ').replace(/(["'$`\\])/g,'\\$1') + '"'
let cmd = command + ' ' + args
warn(colors.yellow(cmd));
return execSync(cmd).toString()
},
{ description, oneOf: [{ description: argDesc, type: 'string' }, { type: 'array', items: { description: argDesc, type: 'string' }}] })
}
/**
* @typedef {Object} EvolveReturnValue
* @property {Integer} iterations The number of times a transformation rule was applied
* @property {Graph} graph The transformed [graphlib]{@link https://www.npmjs.com/package/graphlib} graph
*/
/**
* @function{Grammar#evolve}
* @description Evolve a graph using a graph grammar.
* @param {Object} [opts] Options influencing the way the grammar is applied.
* @param {Object} [opts.rnd] Random number generator. Default is [MersenneTwister]{@link https://www.npmjs.com/package/mersennetwister}
* @param {Integer} [opts.seed] Seed for random number generator.
* @param {Graph} [opts.graph] Initial [graphlib]{@link https://www.npmjs.com/package/graphlib} graph. Default is a graph containing a single node whose ID is given by `opts.start`
* @param {String} [opts.start] Default label for the starting node of the initial graph. Default is to use the `start` property of the grammar.
* @param {Integer} [opts.limit] Maximum number of transformation rules to apply. Default is to use the limits specified by the grammar or subgrammars, if any
* @param {Integer} [opts.stage] Apply a specific stage of the grammar. GraphGram's grammars can consist of multiple subgrammars. Default is to apply all of them, one after the other.
* @param {Integer} [opts.verbose] Verbosity level. 0 is quiet (the default), 1 is the default for the CLI, 2+ is loud.
* @returns {EvolveReturnValue} An object containing the results of the grammar application.
*/
// main API method to evolve a graph using the grammar
Grammar.prototype.evolve = function (opts) {
var grammar = this
opts = opts || {}
var mt = opts.rnd || new MersenneTwister (opts.seed)
var context = new Context ({ grammar: this, rnd: mt }, { verbose: opts.verbose })
if (opts.graph) {
context.graph = opts.graph
context.warn (colors.yellow ("Initial grammar has " + context.graph.nodes().length + " nodes, " + context.graph.edges().length + " edges"))
context.findNextIds()
} else {
context.graph = new graphlib.Graph()
context.addNode (grammar.start)
context.warn (colors.yellow ("Initializing grammar with " + grammar.start))
}
var graph = context.graph
var limit = typeof(opts.limit) !== 'undefined' ? parseInt(opts.limit) : this.limit
var iterations = 0
if (this.stages) {
// staged grammar: iterate over subgrammars
this.stages.forEach (function (subgrammar, stage) {
var subLimit = limit
if (typeof(subgrammar.limit) !== 'undefined' && (typeof(subLimit) === 'undefined' || subgrammar.limit < subLimit))
subLimit = subgrammar.limit
if (subLimit == 0 || (typeof(opts.stage) !== 'undefined' && stage != parseInt(opts.stage))) {
context.warn (colors.cyan ("Skipping " + subgrammar.displayName + " of " + grammar.displayName))
return
}
context.warn (colors.cyan ("Entering " + subgrammar.displayName + " of " + grammar.displayName))
var info = subgrammar.evolve ({ graph: graph,
verbose: opts.verbose,
limit: subLimit,
rnd: mt })
iterations += info.iterations
if (typeof(limit) === 'number')
limit -= info.iterations
})
} else {
// not a staged grammar: iterate over rules
for (iterations = 0; typeof(limit) === 'undefined' || iterations < limit; ++iterations) {
context.updateIteration (iterations)
// sample a rule application
var site = context.sampleRuleSite()
if (!site)
break
// apply the rule
context.applyRuleAtSite (site)
}
}
return { iterations, graph }
}
/**
* @function{Grammar#toDot}
* @description Generate a GraphViz dot-format description of a graph.
* @param {Object} graph A graphlib graph.
* @returns {string} The graph in GraphViz dot format.
*/
// Convert graphlib graph to graphviz dot format
Grammar.prototype.toDot = function (graph) {
return [(graph.isDirected() ? "digraph" : "graph") + " G {"]
.concat (graph.nodes().map (function (id) {
return ' ' + id + Grammar.prototype.dotAttrs(graph.node(id)) + ';'
})).concat (graph.edges().map (function (edge) {
return ' ' + edge.v + (graph.isDirected() ? ' -> ' : ' -- ') + edge.w + Grammar.prototype.dotAttrs(graph.edge(edge)) + ';'
})).concat (['}',''])
.join("\n")
}
Grammar.prototype.dotAttrs = function (label) {
var attrText = ''
if (typeof(label) === 'string') attrText = ' [label="' + label + '"]'
else if (typeof(label) === 'object' && label.dot)
attrText = ' [' + Object.keys(label.dot).map((a) => (a + '="' + label.dot[a] + '"')).join(',') + ']'
return attrText
}
// private validation & setup methods (yes I know they're not really private)
Grammar.prototype.makeGraphSchema = function (lhs) {
var canonical = this.canonical
var labelSchema = lhs
? { description: 'A query expression for matching a label on the left-hand side of a subgraph transformation rule.', '$ref': '#/definitions/lhs_label' }
: { description: 'A recipe for generating a label on the right-hand side of a subgraph transformation rule.', '$ref': '#/definitions/rhs_label' }
var headTailSchema = function(desc) { return { '$ref': (canonical ? '#/definitions/identifier_list' : '#/definitions/identifier_or_list') } }
return {
description: lhs
? 'This block specifies a pattern with which to match the subgraph on the left-hand side of the transformation rule.'
: 'This block specifies the subgraph generated on the right-hand side of the transformation rule, replacing the matched subgraph on the left-hand side.',
oneOf: (canonical
? []
: [{ description: 'An array of node labels. The ' + (lhs ? 'matched' : 'replacement') + ' subgraph is a chain of nodes.' + (lhs ? '' : ' The `head` and `tail` properties will be automatically set to (respectively) the first and last nodes on the left-hand side of the rule.'), type: 'array', items: { description: 'A node label.', type: 'string' } },
{ description: 'A single node label. The ' + (lhs ? 'matched' : 'replacement') + ' subgraph contains exactly one node.' + (lhs ? 'The string can be a regular expression, to be matched against the node label.' : ' The `head` and `tail` properties will automatically be set to (respectively) the first and last nodes on the left-hand side of the rule.'), type: 'string' }])
.concat ([
{ type: 'object',
description: 'A full description, including nodes and edges, of the subgraph to be ' + (lhs ? 'matched.' : 'used for replacement. If the `node` block is absent, it will be copied from the left-hand side.'),
required: (canonical || lhs) ? ['node'] : [],
additionalProperties: false,
properties: {
node: {
description: 'The set of nodes in the ' + (lhs ? 'matching' : 'replacement') + ' subgraph.',
type: 'array',
minItems: 1,
items: {
oneOf: (canonical
? []
: [{ description: 'A node label.' + (lhs ? 'This pattern will match any node that has the corresponding string label.' : ''), type: 'string' },
{ description: 'A (node ID, node label) pair; both are strings. ' + (lhs ? 'The ID can be used to reference the node elsewhere in the rule.' : 'The ID either refers to a node that was matched on the left-hand side of the rule, or is completely new.'), type: 'array', minItems: 2, maxItems: 2, items: { type: 'string' } }]
.concat (lhs ? [] : [
{ type: 'object',
description: 'An incremental update to a node in the matched subgraph.',
additionalProperties: false,
required: ['id','update'],
properties:
{ id: { description: 'A node identifier that refers to a node that was matched on the left-hand side of the rule.', '$ref': '#/definitions/identifier' },
update: extend ({}, labelSchema, {description:'An update to the existing node label, whose properties will be copied over to the existing node label using Lodash `assign` semantics. It follows that this update will typically be an object rather than another type of value (though the schema also allows string-valued, array-valued, or numeric values here).'}),
head: headTailSchema('The identifier(s) of the node, or nodes, whose incoming edges will be replaced by incoming edges to this node. (The identifiers are as defined in the `node` block.)'),
tail: headTailSchema('The identifier(s) of the node, or nodes, whose outgoing edges will be replaced by outgoing edges from this node. (The identifiers are as defined in the `node` block.)') } } ]))
.concat ([
{ type: 'object',
description: lhs ? 'A pattern for matching a node in a subgraph.' : 'A description of a node in the replacement subgraph.',
additionalProperties: false,
properties: extend ({
id: { description: lhs ? 'A node identifier that can be used to reference the node elsewhere in the rule.' : 'A node identifier that either refers to a node that was matched on the left-hand side of the rule, or is completely new.', '$ref': '#/definitions/identifier' },
label: labelSchema,
}, lhs ? {
strict: { description: 'If true, then any graph node that matches this pattern rule cannot have any neighbors that are not also in the subgraph defined by the pattern rule', type: 'boolean' }
} : {
head: headTailSchema('The identifier(s) of the node, or nodes, whose incoming edges will be replaced by incoming edges to this node. (The identifiers are as defined in the `node` block.)'), // if an lhs node is specified here, incoming edges to that lhs node will be attached
tail: headTailSchema('The identifier(s) of the node, or nodes, whose outgoing edges will be replaced by outgoing edges from this node. (The identifiers are as defined in the `node` block.)') // if an lhs node is specified here, outgoing edges from that lhs node will be attached
})
}
])
}
},
edge: {
description: 'The set of edges in the ' + (lhs ? 'matching subgraph. Note that, unless the `induce` property is set (within this rule or at a higher level in the grammar), this match is permissive rather than strict: the subgraph is allowed to contain more edges than specified here. In contrast, if `induce` is set, then ONLY the edges in this subgraph are allowed for a match.' : 'replacement subgraph. (Note that under some circumstances, edges will be automatically added even if not specified here. Specifically, if the `node` property is array-valued, then a chain of edges will be added automatically between consecutive nodes in the list.)'),
type: 'array',
items: {
description: 'An edge being ' + (lhs ? 'matched on the left' : 'added on the right') + '-hand side of a transformation rule. '
+ (lhs
? 'Note that the edge may be specified as an array of the form `[v,w,label,id]` or as an object with those properties; the two are functionally equivalent. `v` and `w` represent source and target node IDs, respectively; `label` is a query expression to match edge labels; and `id` is a temporary identifier for the edge. `label` and `id` are optional.'
: 'The edge may be specified in a variety of ways, but the basic idea is to either specify a source and target (`v,w`) or an identifier referencing an existing edge (`id`), and then to replace the label completely (`label`) or update it incrementally (`update`). Specifically the edge can be a `[v,w,label]` tuple (where `v` and `w` are the source and target of the new edge), as an `{id,label}` object (where `id` is the ID of an edge introduced on the left-hand side of the transformation rule; in this and the `[v,w,label]` tuple the label is optional, and will be copied from an existing edge if one exists), as an `{id,update}` object (where `update` represents an incremental update to the existing edge label), as a `{v,w,update}` object, as a string (which will be interpreted as the ID of an existing edge), or as a `{v,w,label}` object.'),
anyOf: (canonical
? []
: [{ type: 'array',
description: 'A tuple describing an edge being ' + (lhs ? 'matched on the left' : 'added on the right') + '-hand side of a transformation rule.',
minItems: 2,
maxItems: lhs ? 4 : 3,
items: [ { description: 'The source node of the edge, using the node numbering or naming scheme defined in the `node` block.', type: ['string','number'] }, // v
{ description: 'The target node of the edge, using the node numbering or naming scheme defined in the `node` block.', type: ['string','number'] }, // w
extend ({}, labelSchema, { description: lhs ? 'A query expression for matching the edge label.' : 'An expression for the replacement edge label.' }) ] // label
.concat (lhs ? [{description: 'A temporary identifier for the edge being matched. This is temporary in the sense that it is defined only while the transformation rule is being applied.', type:'string'}] : []) }] // id
.concat (lhs ? [] : [
{ type: 'object',
description: 'An object describing an edge being added on the right-hand side of a transformation rule.',
additionalProperties: false,
required: ['id'],
properties: { id: { description: 'An edge identifer from the subgraph matched on the left-hand side of the transformation rule.', '$ref': '#/definitions/identifier' },
label: labelSchema }
},
{ type: 'object',
description: 'An object describing an incremental update to an edge on the right-hand side of a transformation rule.',
additionalProperties: false,
required: ['id','update'],
properties: { id: { '$ref': '#/definitions/identifier' },
update: extend ({}, labelSchema, {description:'A incremental update to the existing edge label.'}) }
},
{ type: 'object',
description: 'An object describing an incremental update to an edge on the right-hand side of a transformation rule.',
additionalProperties: false,
required: ['v','w','update'],
properties: { v: { description: 'The source node of the edge, using the node identifiers defined in the `node` block.', '$ref': '#/definitions/identifier' },
w: { description: 'The target node of the edge, using the node identifiers defined in the `node` block.', '$ref': '#/definitions/identifier' },
update: extend ({}, labelSchema, {description:'A incremental update to the existing edge label.'}) }
},
{ description: 'An edge identifer from the subgraph matched on the left-hand side of the transformation rule. The edge will be copied unmodified.', type: 'string' }]))
.concat ([
{ type: 'object',
description: 'An object describing an edge being ' + (lhs ? 'matched on the left' : 'added on the right') + '-hand side of a transformation rule.',
additionalProperties: false,
required: ['v','w'],
properties: extend ({
v: { description: 'The source node of the edge, using the node naming scheme defined in the `node` block.', '$ref': '#/definitions/identifier' },
w: { description: 'The target node of the edge, using the node naming scheme defined in the `node` block.', '$ref': '#/definitions/identifier' },
label: extend ({}, labelSchema, { description: lhs ? 'A query expression for matching the edge label.' : 'The new edge label.' })
}, lhs ? { id: { description: 'A temporary identifier for the edge being matched. This is temporary in the sense that it is defined only while the transformation rule is being applied.', '$ref': '#/definitions/identifier' } } : {})
}
])
}
}
}
}
])
}
}
Grammar.prototype.makeGrammarSchema = function (topLevel, staged) {
return extend ({
description: (topLevel ? (staged ? 'A top-level grammar, consisting of one or more stages.' : 'A top-level grammar, consisting of just one stage.') : 'A subgrammar, corresponding to an individual stage of graph transformation.'),
type: 'object',
required: (staged ? ['stages'] : ['rules']),
additionalProperties: false,
properties: extend
(staged
? { stages: { description: 'The successive stages to be applied. Each stage is a separate subgrammar of transformations.', type: 'array', minItems: 1, items: { '$ref': '#/definitions/subgrammar' } } }
: { rules: { '$ref': '#/definitions/rules' } },
{ name: { description: 'The name of this ' + (topLevel ? 'grammar.' : 'subgrammar.'), type: 'string' },
limit: { description: 'The maximum number of rule applications.', type: 'number' },
induced: { description: 'Default value of the `induced` parameter, which governs the specificty of subgraph-matching. Can be overridden by the `induced` parameter for stages and rules.', type: 'boolean' } },
topLevel ? {start:{ description: 'The start node label for the default initial graph.' }} : {})
})
}
/**
* @function{Grammar#makeSchema}
* @description Get the JSON schema for a graph grammar.
* @returns {Object} The JSON schema.
*/
Grammar.prototype.makeSchema = function() {
return {
description: "Specification of a `graphgram` stochastic grammar for graph transformation.",
oneOf: [ this.makeGrammarSchema(true,false),
this.makeGrammarSchema(true,true) ],
definitions: {
identifier: {
description: 'An identifier',
type: 'string',
pattern: '^[a-zA-Z_0-9]+$'
},
identifier_list: { type: 'array', items: { '$ref': '#/definitions/identifier' } },
identifier_or_list: {
oneOf: [{ type: 'string' }, { '$ref': '#/definitions/identifier_list' }]
},
lhs_label: this.matcher.makeLhsLabelSchema ({ '$ref': '#/definitions/lhs_label' }),
rhs_label: this.matcher.makeRhsLabelSchema ({ '$ref': '#/definitions/rhs_label' }),
rules: {
description: 'The list of subgraph transformation rules in this grammar.',
type: 'array',
items: {
description: 'An individual subgraph transformation rule. Each rule proceeds by matching a subgraph whose pattern is specified on the left-hand side (the `lhs`), and replacing it with a subgraph specified on the right-hand side (`rhs`). There are various different syntactical forms depending on the complexity and topology of the matching and replacement subgraphs, and whether properties (like edges and labels) are to be changed or copied over unmodified.',
type: 'object',
required: ['lhs','rhs'],
additionalProperties: false,
properties: {
name: { description: 'The name of this rule.', type: 'string' },
lhs: this.makeGraphSchema(true),
rhs: this.makeGraphSchema(false),
induced: { description: 'If true, then the subgraph induced by the nodes on the left-hand side of the rule must exactly match the subgraph as specified in the rule: no additional nodes within the subgraph are allowed.', type: 'boolean' }, // if true, match lhs using induced subgraph search
condition: { description: 'A string that will be passed to JavaScript\'s `eval` for evaluation, to test whether the match should proceed. Use $id.label for labels, $id.match[n] for n\'th matching group, $$iter for iteration#, $$graph for graph.', type: 'string' }, // eval'd string. Use $id.label for labels, $id.match[n] for n'th matching group, $$iter for iteration#, $$graph for graph
weight: { description: 'A probabilistic weight that is used to determine which rules should be randomly applied, in the event that multiple patterns match.', type: ['string','number'] }, // string is eval'd using same expansions as 'condition'
limit: { description: 'The maximum number of times this rule can be used to transform the graph.', type: 'number' },
type: { description: 'If a `type` is specified, then any `limit` specified for this rule is interpreted as the maximum number of times any rule with the same `type` can be used.', type: 'string' },
delay: { description: 'The minimum number of times another transformation rule must be applied to the graph before this rule can be used.', type: 'number' },
}
}
},
subgrammar: this.makeGrammarSchema(false,false)
}
}
}
Grammar.prototype.validate = function (json) {
var validator = new jsonschema.Validator()
var schema = this.makeSchema()
var result = validator.validate (json, schema, {nestedErrors: true})
if (result.errors.length) {
var errs = result.errors.map (function (ve) { return ve.stack }).join("\n")
throw new Error ("Schema validation error:\n" + errs)
}
}
Grammar.prototype.warn = function() {
if (this.verbose)
console.warn.apply (console, arguments)
}
Grammar.prototype.warnVerbose = function() {
if (this.verbose > 1)
console.warn.apply (console, arguments)
}
Grammar.prototype.init = function() {
var grammar = this
this.displayName = this.name || this.displayName || 'graph-grammar'
if (this.stages)
this.stages = this.stages.map ((subgrammar, n) => new Grammar (extend ( { induced: this.induced },
subgrammar),
{ matcher: this.matcher,
displayName: this.displayName + ' stage #' + n }))
this.rules.forEach (function (rule, n) {
rule.countType = rule.type || String(n)
rule.displayName = rule.name || ('rule #' + n)
var linkHeadTailRhs = false, headOrTail = {}
function checkGraph (prop, nodeIdFactory, lhsInfo) {
var isNodeId = {}, isEdgeId = {}
var name = rule.displayName + ' ' + prop
// do some auto-expansion of syntactic sugar
if (!grammar.canonical) {
if (typeof(rule[prop]) === 'string') {
// a string expands to a single node, and triggers automatic linking of head & tail
linkHeadTailRhs = true
rule[prop] = { node: [{ id: 'a', label: rule[prop] }] }
} else if (isArray(rule[prop])) {
// an array of strings expands to a chain of nodes, and triggers automatic linking of head & tail
linkHeadTailRhs = true
rule[prop] = { node: rule[prop].map (function (label, n) { return { id: nodeIdFactory(n), label: label, strict: prop === 'lhs' ? (n == 0 || n == rule[prop].length - 1) : undefined } }),
edge: rule[prop].slice(1).map (function (_label, n) { return [ nodeIdFactory(n), nodeIdFactory(n+1) ] }) }
}
}
// now we have a graph
var graph = rule[prop]
// more syntactic sugar
if (!grammar.canonical) {
// if node is missing on rhs, copy it from lhs
if (typeof(graph.node) === 'undefined')
graph.node = _.cloneDeep (rule.lhs.node)
graph.node = graph.node.map (function (node, n) {
// if a node is a string or number, interpret it as a label, and auto-assign it an ID
// if a node is a 2-tuple, interpret it as an [id,label] pair
if (isArray(node))
return { id: node[0], label: node[1] }
else if (typeof(node) === 'object') {
if (typeof(node.id) === 'undefined')
node.id = nodeIdFactory(n)
// if a node doesn't have a 'label' but has the ID of an LHS node, then copy the label over
// or, if it has an '{update:X}', create the corresponding label expression '{$assign:[{$eval:"$id.label"},X]}'
if (typeof(node.label) === 'undefined' && lhsInfo && lhsInfo.isNodeId[node.id])
node.label = node.update
? grammar.matcher.makeLabelUpdate (node.id, node.update)
: grammar.matcher.makeLabelEval (node.id)
delete node.update
return node
}
return { id: nodeIdFactory(n), label: node }
})
if (prop === 'rhs' && linkHeadTailRhs
&& !(rule.lhs.node.length == 1 && graph.node.length == 1 && rule.lhs.node[0].id === graph.node[0].id)) {
graph.node[0].head = rule.lhs.node[0].id
graph.node[graph.node.length - 1].tail = rule.lhs.node[rule.lhs.node.length - 1].id
}
graph.node.forEach (function (node) {
// if head/tail is a string, convert it to a single-element array
if (typeof(node.head) === 'string') node.head = [node.head]
if (typeof(node.tail) === 'string') node.tail = [node.tail]
})
if (graph.edge)
graph.edge = graph.edge.map (function (edge) {
// if an edge is a string, interpret it as an id
// if an edge is a 2-, 3- or 4-tuple, interpret it as [v,w,label,id]
var e = typeof(edge) === 'string' ? { id: edge } : (isArray(edge) ? { v: edge[0], w: edge[1], label: edge[2], id: edge[3] } : edge)
e.v = typeof(e.v) !== 'undefined' && String (e.v)
e.w = typeof(e.w) !== 'undefined' && String (e.w)
e.id = e.id && String (e.id)
// if an edge doesn't have a 'label' but has the ID of an LHS edge, then copy the label over
// or, if it has an '{assign:X}', create the corresponding label expression '{$assign:[{$eval:"$id.label"},X]}'
if (e.id && lhsInfo) {
if (lhsInfo.isEdgeId[e.id]) {
if (typeof(e.label) === 'undefined')
e.label = e.update
? grammar.matcher.makeLabelUpdate (e.id, e.update)
: grammar.matcher.makeLabelEval (e.id)
// if v or w is undefined, copy those over
if (typeof(e.v) === 'undefined') e.v = lhsInfo.isEdgeId[e.id].v
if (typeof(e.w) === 'undefined') e.w = lhsInfo.isEdgeId[e.id].w
}
// remove noncanonical sugary properties from RHS edges
delete e.id
delete e.update
}
return e
})
}
// all done with syntactic sugar
// now validate IDs
var heads = {}, tails = {}
function checkNodeId (id, desc, isNodeId) {
if (!isNodeId[id]) throw new Error ("In " + name + ", " + desc + ": " + id + " is not a node ID")
}
function countNodeId (id, linkType, count, rhsNodeId) {
checkNodeId (id, 'node ' + rhsNodeId + ' ' + linkType, lhsInfo.isNodeId)
if (count[id])
throw new Error ("In " + name + ": " + id + " appears as " + linkType + " for more than one rhs node")
count[id] = true
if (id !== rhsNodeId)
headOrTail[id] = linkType
}
graph.node.forEach (function (node) {
if (isNodeId[node.id]) throw new Error ("In " + name + ": duplicate node ID " + node.id)
isNodeId[node.id] = node
if (lhsInfo) {
if (node.head)
node.head.forEach (function (head) { countNodeId (head, 'head', heads, node.id) })
if (node.tail)
node.tail.forEach (function (tail) { countNodeId (tail, 'tail', tails, node.id) })
}
})
if (graph.edge)
graph.edge.forEach (function (edge) {
checkNodeId (edge.v, 'edge.v', isNodeId)
checkNodeId (edge.w, 'edge.w', isNodeId)
if (edge.id) {
if (isNodeId[edge.id] || isEdgeId[edge.id]) throw new Error ("In " + name + ": duplicate edge ID " + edge.id)
isEdgeId[edge.id] = edge
}
})
return { isNodeId, isEdgeId }
}
var lhsInfo = checkGraph ('lhs', (n) => String.fromCharCode(97+n))
var rhsInfo = checkGraph ('rhs', (n) => String(n), lhsInfo)
Object.keys(headOrTail).forEach (function (id) {
if (rhsInfo.isNodeId[id])
throw new Error ("In " + rule.displayName + ": lhs node " + id + " is listed as " + headOrTail[id] + " for an rhs node, but is also an rhs node ID")
})
})
this.rules.forEach (function (rule) {
rule.lhsGraph = grammar.makeGraph (rule.lhs)
})
}
Grammar.prototype.canonicalGraphJson = function (graph, lhs) {
return { node: graph.node, edge: graph.edge }
}
/**
* @function{Grammar#canonicalJson}
* @description Returns the canonical JSON for a graph grammar.
* @returns {Object} The JSON representation of the grammar.
*/
Grammar.prototype.canonicalJson = function() {
var grammar = this
var grammarProps = ['name','limit','induced','start']
var ruleProps = ['name','induced','condition','weight','limit','type','delay']
return extend (_.zipObject (grammarProps, grammarProps.map ((prop) => grammar[prop])),
{ stages: this.stages && this.stages.map ((subgrammar) => subgrammar.canonicalJson()),
rules: this.stages ? undefined : this.rules.map ((rule) => {
return extend ({ lhs: grammar.canonicalGraphJson (rule.lhs),
rhs: grammar.canonicalGraphJson (rule.rhs) },
_.zipObject (ruleProps, ruleProps.map ((prop) => rule[prop]))) }) })
}
Grammar.prototype.makeGraph = function (json) {
var graph = new graphlib.Graph()
json.node.forEach (function (node) {
graph.setNode (node.id, node.label)
})
if (json.edge)
json.edge.forEach (function (edge) {
graph.setEdge (edge.v, edge.w, edge.label)
})
return graph
}
// Context is an object that wraps the current grammar, graph, and iteration & does the actual graph-rewriting
function Context (json, opts) {
extend (this, json, { nextNodeId: 1 })
this.matcher = this.grammar.matcher
this.warnVerbose = this.grammar.warnVerbose
this.warn = this.grammar.warn
this.verbose = this.grammar.verbose
this.ruleCount = {}
this.grammar.rules.forEach ((rule) => { this.ruleCount[rule.countType] = 0 })
extend (this, opts)
}
Context.prototype.findNextIds = function() {
var context = this
this.graph.nodes().forEach (function (id) {
var n = parseInt (id)
if (n) context.nextNodeId = Math.max (context.nextNodeId, n + 1)
})
}
Context.prototype.updateIteration = function (iter) {
this.iter = iter
var keys = ['iter','graph'], base = {}
keys.forEach ((key) => { base['$'+key] = this[key] })
this.matcher.setBaseContext (base)
this.warn (colors.cyan ("Iteration " + (iter+1) + " of " + this.grammar.displayName))
}
// main method for matching a rule
Context.prototype.sampleRuleSite = function() {
var context = this, grammar = this.grammar, graph = this.graph
// find all sites at which a rule matches
var labelMatch = this.matcher.labelMatch.bind (this.matcher)
var nodes = graph.nodes(), edges = graph.edges(), sites = []
grammar.rules.forEach (function (rule, n) {
if (rule.limit && context.ruleCount[rule.countType] >= rule.limit)
return
if (rule.delay && context.iter < rule.delay)
return
var isomorphs = new SubgraphSearch (graph, rule.lhsGraph, { labelMatch }).isomorphisms
isomorphs.forEach (function (isomorph) {
var mismatch = false
rule.lhs.node.forEach (function (node) {
mismatch = mismatch || (node.strict && rule.lhsGraph.neighbors(node.id).length != graph.neighbors(isomorph.assign[node.id]).length)
})
if (rule.induced || (typeof(rule.induced) === 'undefined' && grammar.induced))
rule.lhs.node.forEach (function (iNode) {
var si = iNode.id, gi = isomorph.assign[si]
rule.lhs.node.forEach (function (jNode) {
var sj = jNode.id, gj = isomorph.assign[sj]
mismatch = mismatch || (graph.hasEdge(gi,gj) && !subgraph.hasEdge(si,sj))
})
})
if (!mismatch) {
if (rule.lhs.edge)
rule.lhs.edge.forEach (function (edge, n) {
if (edge.id) {
isomorph.match[edge.id] = isomorph.edgeMatch[n]
isomorph.label[edge.id] = graph.edge (isomorph.assign[edge.v], isomorph.assign[edge.w])
}
})
if (context.evalCond (isomorph, rule)) {
var weight = context.evalWeight (isomorph, rule)
context.warnVerbose ('Found match ' + context.nodeList(isomorph.assign,colors.red) + ' to ' + rule.displayName + ' with weight ' + colors.blue(weight))
sites.push ({ weight, isomorph, rule })
}
}
})
})
// sample a rule application site
var totalWeight = sites.reduce (function (total, s) { return total + s.weight }, 0)
var w = this.rnd.rnd() * totalWeight, m = -1
while (w > 0 && ++m < sites.length - 1)
w -= sites[m].weight
context.warnVerbose(colors.blue("Total weight is " + totalWeight + "; " + (totalWeight > 0 ? ("sampled " + sites[m].rule.displayName) : "no rules to apply")))
return totalWeight > 0 ? sites[m] : false
}
// method for rewriting the graph using a matched rule
Context.prototype.applyRuleAtSite = function (site) {
var context = this, graph = this.graph, isomorph = site.isomorph, rule = site.rule, oldId = isomorph.assign
++this.ruleCount[rule.countType]
var newLabel = {}, newId = {}
rule.rhs.node.forEach (function (node) {
newLabel[node.id] = context.newLabel (isomorph, node.label)
newId[node.id] = context.addNode (newLabel[node.id])
})
context.warn ("Replacing nodes " + context.nodeList(oldId,colors.red) + " with " + context.nodeList(newId,colors.green))
if (rule.lhs.edge)
rule.lhs.edge.forEach (function (edge) {
graph.removeEdge (oldId[edge.v], oldId[edge.w])
})
rule.lhs.node.forEach (function (node) {
if (newId[node.id]) {
context.reattachOutgoing (oldId[node.id], newId[node.id])
context.reattachIncoming (oldId[node.id], newId[node.id])
} else {
var headId, tailId
rule.rhs.node.forEach (function (rhsNode) {
if (rhsNode.head && rhsNode.head.indexOf(node.id) >= 0) headId = rhsNode.id
if (rhsNode.tail && rhsNode.tail.indexOf(node.id) >= 0) tailId = rhsNode.id
})
if (headId)
context.reattachIncoming (oldId[node.id], newId[headId])
if (tailId)
context.reattachOutgoing (oldId[node.id], newId[tailId])
}
graph.removeNode (oldId[node.id])
})
if (rule.rhs.edge)
rule.rhs.edge.forEach (function (edge) {
var label = context.newLabel (isomorph, edge.label)
context.addEdge (newId[edge.v], newId[edge.w], label)
context.warn ("Adding edge " + context.edgeDesc(newId[edge.v],newId[edge.w],label,colors.green,colors.green))
})
}
Context.prototype.addNode = function (label) {
var id = String (this.nextNodeId++)
this.graph.setNode (id, label)
return id
}
Context.prototype.addEdge = function (src, dest, label) {
this.graph.setEdge (src, dest, label)
}
Context.prototype.evalCond = function (isomorph, rule) {
return this.evalMatchExpr (isomorph, rule.condition, true)
}
Context.prototype.evalWeight = function (isomorph, rule) {
return this.evalMatchExpr (isomorph, rule.weight, 1)
}
Context.prototype.evalMatchExpr = function (isomorph, expr, defaultVal) {
if (typeof(expr) === 'undefined')
return defaultVal
return this.matcher.evalMatchExpr (isomorph, expr)
}
Context.prototype.newLabel = function (isomorph, expr) {
return this.matcher.newLabel (isomorph, expr)
}
Context.prototype.reattachIncoming = function (oldId, newId) {
var context = this, graph = this.graph
var incoming = graph.predecessors(oldId)
if (incoming)
incoming.forEach (function (pred) {
var label = graph.edge (pred, oldId)
context.addEdge (pred, newId, label)
graph.removeEdge (pred, oldId)
context.warnVerbose ("Replacing incoming edge " + context.edgeDesc(pred,oldId,label,colors.green,colors.red) + " with " + context.edgeDesc(pred,newId,label,colors.green,colors.green))
})
}
Context.prototype.reattachOutgoing = function (oldId, newId) {
var context = this, graph = this.graph
var outgoing = graph.successors(oldId)
if (outgoing)
outgoing.forEach (function (succ) {
var label = graph.edge (oldId, succ)
context.addEdge (newId, succ, label)
graph.removeEdge (oldId, succ)
context.warnVerbose ("Replacing outgoing edge " + context.edgeDesc(oldId,succ,label,colors.red,colors.green) + " with " + context.edgeDesc(newId,succ,label,colors.green,colors.green))
})
}
Context.prototype.labelString = function (label) {
return typeof(label) === 'object' ? JSON.stringify(label) : label
}
Context.prototype.nodeList = function (assign, color) {
var context = this
return '{' + Object.keys(assign).map((id) => (id+':'+context.nodeDesc(assign[id],color))).join (",") + '}'
}
Context.prototype.nodeDesc = function (id, color, rev) {
color = color.bind (colors)
var idText = color(id), labelText = colors.inverse(color(this.labelString(this.graph.node(id))))
return rev ? (labelText + idText) : (idText + labelText)
}
Context.prototype.edgeDesc = function (src, dest, label, srcColor, destColor) {
var srcDesc = this.nodeDesc(src,srcColor), destDesc = this.nodeDesc(dest,destColor,true)
return srcDesc + colors.yellow('-') + (label ? colors.inverse(colors.yellow(this.labelString(label))) : '') + colors.yellow('>') + destDesc
}
// Matcher is an object that encapsulates the JSON query language
// It could, in theory, be overridden to use a different query language
// (you'd also need to provide a JSON schema for that language, or just use a permissive schema)
function Matcher() {
extend (this, {
// special terms used in lhs label expressions
equalsKey: '$equals',
containsKey: '$contains',
findKey: '$find', // recursive descent search
testKey: '$test', // defines a function that is then applied to the label, yielding truthy/falsy
notKey: '$not',
andKey: '$and',
orKey: '$or',
// special terms used in rhs label expressions
evalKey: '$eval',
extendKey: '$extend', // skips undefined values
assignKey: '$assign', // does not skip undefined values
mergeKey: '$merge', // is recursive
// plugins
rhsLabelFunc: {}
})
}
Matcher.prototype.mapObject = function (obj, f) {
var result = {}
Object.keys(obj).forEach (function (k) { result[k] = f(obj[k]) })
return result
}
Matcher.prototype.setBaseContext = function (baseContext) {
this.baseContext = baseContext
}
Matcher.prototype.makeExtendedContext = function (isomorph) {
var extendedContext = extend ({}, this.baseContext)
Object.keys(isomorph.assign).forEach (function (id) {
extendedContext[id] = { id: isomorph.assign[id] }
})
Object.keys(isomorph.match).forEach (function (id) {
extendedContext[id] = extend (extendedContext[id] || {},
{ label: isomorph.label[id],
match: isomorph.match[id].match })
})
return extendedContext
}
// schema for label queries (used in 'label' on the LHS of rules)
Matcher.prototype.makeLhsLabelSchema = function (ref) {
let descRef = function (desc) { return extend ({}, ref, { description: desc }) }
return {
description: 'A query for matching a label in a graph entity (i.e. a node or edge).',
oneOf: [{ description: 'This query will match any label that has exactly the specified type and value.', type: ['string','number','boolean','array'] },
{ type: 'object',
description: 'A compound query expression that is formed by combining, or modifying, one or more constituent query expressions.',
maxProperties: 1,
additionalProperties: false,
properties: {
'$equals': descRef('The label object must exactly match the given query expression, with no additional properties.'),
'$contains': descRef('The label object must match the given query expression, but is allowed to contain additional properties.'),
'$find': descRef('A recursive descent search of the label object must find an element that matches the given query expression.'),
'$not': descRef('The label must NOT match the given query expression.'),
'$and': { description: 'The label must match all of the query expressions given in the list.', type: 'array', minItems: 1, items: ref },
'$or': { description: 'The label must match one of the query expressions given in the list.', type: 'array', minItems: 1, items: ref },
'$test': { description: 'A flexible user-defined query. The text value of this query expression, when `eval`\'d, defines a JavaScript function which, when called with the label as its sole argument, must return a truthy value.', type: 'string' }
}
},
{ type: 'object',
description: 'The label must be a JSON object, it must have a key that matches the given key (which can be any string that does not begin with a "$" character), and the value must match the query expression associated with that key.',
additionalProperties: false,
patternProperties: {
'^[^$].*$': ref
}
}]
}
}
// queries
Matcher.prototype.labelMatch = function (gLabel, sLabel, opts) {
var labelMatch = this.labelMatch.bind (this)
opts = opts || {}
if (typeof(sLabel) === 'undefined')
return { match: gLabel }
var typesMatch = (typeof(sLabel) === typeof(gLabel))
if (typeof(sLabel) === 'string') {
var match
return typesMatch && (match = new RegExp('^'+sLabel+'$').exec (gLabel)) && { match: match.slice(0) }
} else if (isArray(sLabel)) {
if (!isArray(gLabel) || sLabel.length !== gLabel.length)
return false
var allMatch = true
var match = sLabel.map (function (s, n) {
var m
if (allMatch) {
m = labelMatch (gLabel[n], s, opts)
if (!m) allMatch = false
}
return m && m.match
})
return allMatch && { match }
} else if (typeof(sLabel) === 'object') {
var match = {}, clauseMatch
if (sLabel[this.equalsKey])
return labelMatch (gLabel, sLabel[this.equalsKey], extend ({}, opts, {exact:true}))
if (sLabel[this.containsKey])
return labelMatch (gLabel, sLabel[this.containsKey], extend ({}, opts, {exact:false}))
if (sLabel[this.notKey])
return !labelMatch (gLabel, sLabel[this.notKey], opts) && {}
if (sLabel[this.andKey])
return sLabel[this.andKey].reduce (function (allTrue, clause) {
return allTrue && (clauseMatch = labelMatch (gLabel, clause, opts)) && extend (match, clauseMatch.match)
}, true) && { match }
if (sLabel[this.orKey])
return sLabel[this.orKey].reduce (function (anyTrue, clause) {
return anyTrue || ((clauseMatch = labelMatch (gLabel, clause, opts)) && ((match = clauseMatch.match) || true))
}, false) && { match }
if (sLabel[this.findKey]) {
var findLabel = sLabel[this.findKey]
function find (g) {
var result = labelMatch (g, findLabel, opts)
if (isArray(g))
result = g.reduce (function (r, c) { return r || find(c) }, result)
else if (typeof(g) === 'object')
result = Object.keys(g).reduce (function (r, k) { return r || find(g[k]) }, result)
return result
}
return find (gLabel)
}
if (sLabel[this.testKey])
return (eval(sLabel[this.testKey]) (gLabel)) && {}
var allMatch = typesMatch && (!opts.exact || Object.keys(gLabel).length == Object.keys(sLabel).length)
Object.keys(sLabel).forEach (function (k) {
if (allMatch) {
var m = labelMatch (gLabel[k], sLabel[k], opts)
if (m)
match[k] = m.match
else
allMatch = false
}
})
return allMatch && { match }
} else
return typesMatch && gLabel === sLabel && { match: gLabel }
}
// schema for label evaluations (used in 'condition' & 'weight' on LHS of rules, and 'label' on RHS of rules)
Matcher.prototype.makeRhsLabelSchema = function (ref) {
const rhsLabelKeys = Object.keys(this.rhsLabelFunc).sort()
return {
oneOf: [{ description: 'Generates a label with exactly the specified type and value. For string-valued labels, or arrays that include strings, the string may include substrings of the form `${id.label}` where `id` is an identifier that has been assigned, during this transformation rule, to a previously referenced node or edge.', type: ['string','number','boolean','array'] },
{ type: 'object',
description: 'Generates a label using a functional expression that typically involves evaluating a string as JavaScript. The JavaScript may refer to the labels of nodes or edges that have previously been assigned IDs, using the syntax `$id.label`.',
maxProperties: 1,
additionalProperties: false,
properties: extend ({
'$eval': { description: 'Generates a label by evaluating a JavaScript string.', type: ['string','array','object'] },
'$extend': { description: 'Generates a label by applying `extend` to its arguments, where the semantics of `extend` skip undefined values.', type: 'array', minItems: 2, items: ref },
'$assign': { description: 'Generates a label by applying the Lodash `assign` function to its arguments, where the semantics of `assign` do *not* skip undefined values.', type: 'array', minItems: 2, items: ref },
'$merge': { description: 'Generates a label by applying the Lodash `merge` function to its arguments, i.e. performing a recursive traversal and attempting to merge at each level.', type: 'array', minItems: 2, items: ref }
},
_.zipObject (rhsLabelKeys, rhsLabelKeys.map((key)=>this.rhsLabelFunc[key].schema)))
},
{ type: 'object',
description: 'Generates a JSON object with the given key(s) (which can be any string that does not begin with a "$" character), mapping to value(s) which are themselves label expressions.',
additionalProperties: false,
patternProperties: {
'^[^$].*$': ref
}
}]
}
}
// direct evaluation syntax e.g. "$id.label + 'some_extra_text'"
Matcher.prototype.evalMatchExpr = function (isomorph, expr) {
var evalMatchExprForIsomorph = this.evalMatchExpr.bind (this, isomorph)
if (typeof(expr) === 'undefined')
return defaultVal
else if (typeof(expr) === 'string') {
var extendedContext = this.makeExtendedContext(isomorph)
var defs = Object.keys(extendedContext).map (function (key) {
return '$' + key + '=' + JSON.stringify(extendedContext[key]) + ';'
}).join('')
return eval(defs + expr)
} else if (isArray(expr))
return expr.map (evalMatchExprForIsomorph)
else if (typeof(expr) === 'object')
return this.mapObject (expr, evalMatchExprForIsomorph)
else
return expr
}
// expansion syntax e.g. "${id.label}some_extra_text"
Matcher.prototype.newLabel = function (isomorph, expr) {
var newLabelForIsomorph = this.newLabel.bind (this, isomorph)
if (typeof(expr) === 'string') {
var extendedContext = this.makeExtendedContext(isomorph)
return expr.replace (/\${([a-zA-Z_0-9\.\$\[\]]+)}/g, function (_m, v) {
return eval ('extendedContext.' + v) || ''
})
} else if (isArray(expr))
return expr.map (newLabelForIsomorph)
else if (typeof(expr) === 'object') {
if (Object.keys(expr).length === 1 && Object.keys(expr)[0][0] === '$') {
const key = Object.keys(expr)[0]
const value = expr[key]
if (key === this.evalKey)
return this.evalMatchExpr (isomorph, value)
if (key === this.extendKey)
return extend.apply (null, value.map (newLabelForIsomorph))
if (key === this.assignKey)
return _.assign.apply (null, value.map (newLabelForIsomorph))
if (key === this.mergeKey)
return _.merge.apply (null, value.map (newLabelForIsomorph))
if (this.rhsLabelFunc[key])
return this.rhsLabelFunc[key].func.apply (null, isArray(value) ? value.map (newLabelForIsomorph) : [newLabelForIsomorph(value)])
// unknown '$func' key... fall through to default case
}
return this.mapObject (expr, newLabelForIsomorph)
} else
return expr
}
// method yielding the direct evaluation syntax for a given identifer's label
Matcher.prototype.makeLabelEval = function (id) {
var result = {}
result[this.evalKey] = '$' + id + '.label'
return result
}
// method yielding syntax to update a given identifer's label
Matcher.prototype.makeLabelUpdate = function (id, update) {
var result = {}
result[this.assignKey] = [this.makeLabelEval(id), update]
return result
}
module.exports = { Grammar }