PageSourceSearch

https://global-plastics-tool.org/js/visitors.js

js global-plastics-tool.org collected 2026-09-26 01:14:31 UTC 23,493 bytes, 743 lines download raw bytes

1/**
2 * Template for the ANTLR visitors.
3 * 
4 * @license BSD, see LICENSE.md
5 */
6
7import {CONSUMPTION_ATTRS, EOL_ATTRS} from "const";
8
9// eslint-disable-next-line no-undef
10let toolkit = null;
11
12try {
13    toolkit = PlasticsLang.getToolkit();
14} catch (e) {
15    alert('Sorry, we could could not initialize app. Your browser may be out of date.');
16    console.log(e);
17}
18
19
20/**
21 * Logic to interpret a plastics language script.
22 *
23 * @license BSD, see LICENSE.md.
24 * @note Thanks to https://snorristurluson.github.io/AntlrCalc/ for lambda formulation whose website
25 *      content is under MIT License (https://github.com/snorristurluson/snorristurluson.github.io/
26 *      blob/master/LICENSE). See README.md for other helpful resources related to this language and
27 *      ANTLR. In particular, interested developers should look at https://github.com/bkiers/
28 *      tiny-language-antlr4.
29 */
30
31
32/**
33 * Visitor which compiles the PT program to JS lambdas.
34 *
35 * Visitor which compiles the PT program to JS lambdas, returning a list of
36 * lambdas which take in a State object and can be run in series one per
37 * program frame render.
38 */
39class CompileVisitor extends toolkit.PlasticsLangVisitor {
40    /**
41     * Visit a number node with interpretation of number modifiers.
42     *
43     * @ctx ANTLR context.
44     * @return Lambda which takes in a State object and returns a float
45     *     corresponding to number value (literal) intended by the user.
46     */
47    visitNumber(ctx) {
48        const self = this;
49
50        const raw = ctx.getText();
51
52        const signMultiplier = raw.includes("-") ? -1 : 1;
53
54        const bodyRawText = ctx.getChild(ctx.getChildCount() - 1).getText();
55        const bodyParsed = signMultiplier * parseFloat(bodyRawText);
56
57        return (state) => {
58            const retVal = bodyParsed;
59            return retVal;
60        };
61    }
62
63    /**
64     * Visit expression which adds or subtracts two other expressions.
65     *
66     * @ctx ANTLR context.
67     * @return Lambda which takes in a State object and returns a float.
68     */
69    visitAdditionExpression(ctx) {
70        const self = this;
71
72        const priorExpression = ctx.getChild(0).accept(self);
73        const opFunc = ctx.op.text === "+" ? (a, b) => a + b : (a, b) => a - b;
74        const afterExpression = ctx.getChild(2).accept(self);
75
76        return (state) => {
77            return opFunc(priorExpression(state), afterExpression(state));
78        };
79    }
80
81    /**
82     * Visit expression which resolves to a single number.
83     *
84     * @ctx ANTLR context.
85     * @return Lambda which takes in a State object and returns a float.
86     */
87    visitSimpleExpression(ctx) {
88        const self = this;
89        return ctx.getChild(0).accept(self);
90    }
91
92    /**
93     * Visit expression which is inside parantheses.
94     *
95     * @ctx ANTLR context.
96     * @return Lambda which takes in a State object and returns a float.
97     */
98    visitParenExpression(ctx) {
99        const self = this;
100        return ctx.getChild(1).accept(self);
101    }
102
103    /**
104     * Visit expression which raises to a power.
105     *
106     * @ctx ANTLR context.
107     * @return Lambda which takes in a State object and returns a float.
108     */
109    visitPowExpression(ctx) {
110        const self = this;
111
112        const priorExpression = ctx.getChild(0).accept(self);
113        const opFunc = (a, b) => Math.pow(a, b);
114        const afterExpression = ctx.getChild(2).accept(self);
115
116        return (state) => {
117            return opFunc(priorExpression(state), afterExpression(state));
118        };
119    }
120
121    /**
122     * Visit expression which multiplies or divides two other expressions.
123     *
124     * @ctx ANTLR context.
125     * @return Lambda which takes in a State object and returns a float.
126     */
127    visitMultiplyExpression(ctx) {
128        const self = this;
129
130        const priorExpression = ctx.getChild(0).accept(self);
131        let opFunc = null;
132        if (ctx.op.text === "*") {
133            opFunc = (a, b) => a * b;
134        } else if (ctx.op.text === "/") {
135            opFunc = (a, b) => a / b;
136        }
137        const afterExpression = ctx.getChild(2).accept(self);
138
139        return (state) => {
140            return opFunc(priorExpression(state), afterExpression(state));
141        };
142    }
143
144    /**
145     * Visit an expression containing a variable identifier.
146     *
147     * @param ctx ANTLR context
148     * @returns Lambda which takes in a State and returns the value of the identifier.
149     */
150    visitSimpleIdentifier(ctx) {
151        const self = this;
152
153        const raw = ctx.getText();
154
155        return (state) => {
156            return self._getValue(raw, state);
157        };
158    }
159
160    /**
161     * Determine how long the lifecycle is for a set of goods.
162     *
163     * @param ctx ANTLR context
164     * @returns Function taking state and resolving to the mean lifecycle duration for the set of
165     *      goods in years.
166     */
167    visitLifecycleExpression(ctx) {
168        const self = this;
169
170        return ctx.getChild(0).accept(self);
171    }
172
173    /**
174     * Draw a value randomly from a normal distribution.
175     *
176     * @param ctx ANTLR context
177     * @returns Function taking state and resolving to the value drawn from the distribution.
178     */
179    visitDrawNormalExpression(ctx) {
180        const self = this;
181
182        const meanFuture = ctx.getChild(5).accept(self);
183        const stdFuture = ctx.getChild(8).accept(self);
184
185        return (state) => {
186            const meanValue = meanFuture(state);
187            const stdValue = stdFuture(state);
188            const generator = d3.randomNormal(meanValue, stdValue);
189            return generator();
190        };
191    }
192
193    /**
194     * Draw a value randomly from a uniform distribution.
195     *
196     * @param ctx ANTLR context
197     * @returns Function taking state and resolving to the value drawn from the distribution.
198     */
199    visitDrawUniformExpression(ctx) {
200        const self = this;
201
202        const lowValueFuture = ctx.getChild(3).accept(self);
203        const highValueFuture = ctx.getChild(5).accept(self);
204
205        return (state) => {
206            const lowValue = lowValueFuture(state);
207            const highValue = highValueFuture(state);
208            const generator = d3.randomUniform(lowValue, highValue);
209            return generator();
210        };
211    }
212
213    /**
214     * Repeat an expression multiple times, summing or taking the product of all results.
215     *
216     * @param ctx ANTLR context
217     * @returns Function taking state and resolving to result after repetition.
218     */
219    visitRepeatExpression(ctx) {
220        const self = this;
221
222        const summarizationStrategy = ctx.getChild(0).getText();
223        const countFuture = ctx.getChild(1).accept(self);
224        const expressionFuture = ctx.getChild(4).accept(self);
225
226        const reduceStrategy = {
227            "product": {"acc": (a, b, length) => a * b, "scale": (x, length) => x},
228            "sum": {"acc": (a, b, length) => a + b, "scale": (x, length) => x},
229            "average": {"acc": (a, b, length) => a + b, "scale": (x, length) => x / length},
230        }[summarizationStrategy];
231
232        return (state) => {
233            const count = countFuture(state);
234
235            const values = [];
236            for (let i = 0; i < count; i++) {
237                values.push(expressionFuture(state));
238            }
239
240            const numValues = values.length;
241            if (numValues == 0) {
242                return 0;
243            } else {
244                const accStrategy = reduceStrategy["acc"];
245                const scaleStrategy = reduceStrategy["scale"];
246                const accumulated = values.reduce((a, b) => accStrategy(a, b, numValues));
247                return scaleStrategy(accumulated, numValues);
248            }
249        };
250    }
251
252    /**
253     * Determine how long the lifecycle is for a set of goods.
254     *
255     * @param ctx ANTLR context
256     * @returns The mean lifecycle duration for the set of goods in years.
257     */
258    visitLifecycle(ctx) {
259        const self = this;
260
261        const numIdentifiers = Math.ceil((ctx.getChildCount() - 4) / 2.0);
262        const identifiers = [];
263        for (let i = 0; i < numIdentifiers; i++) {
264            const childIndex = i * 2 + 3;
265            const identifier = ctx.getChild(childIndex).getText();
266            if (!identifier.startsWith("out.")) {
267                throw "Identifier for lifecycle must be in out.";
268            }
269            identifiers.push(identifier);
270        }
271
272        const getVarName = (varFullName) => {
273            const varPieces = varFullName.split(".");
274            const varName = varPieces[varPieces.length - 1];
275            return varName;
276        };
277
278        const makeHas = (target) => {
279            return (varFullName) => {
280                const varName = getVarName(varFullName);
281                return target.includes(varName);
282            };
283        };
284        const wasteIdentifiers = identifiers.filter(makeHas(EOL_ATTRS));
285        const consumptionIdentifiers = identifiers.filter(makeHas(CONSUMPTION_ATTRS));
286
287        if (wasteIdentifiers.length > 0 && consumptionIdentifiers.length > 0) {
288            throw "Cannot mix lifetimes of waste and consumption";
289        }
290
291        const nonMatched = identifiers.filter((x) => !wasteIdentifiers.includes(x))
292            .filter((x) => !consumptionIdentifiers.includes(x));
293
294        if (nonMatched.length > 0) {
295            throw "Could not find lifetimes for " + nonMatched[0];
296        }
297
298        const getLifecycleForWaste = (state) => {
299            return self._getValue("in.recyclingDelay", state);
300        };
301
302        const getLifecycleForConsumption = (state) => {
303            const getLeverName = (identifier) => {
304                return getVarName(identifier).replace("MT", "Lifecycle");
305            };
306
307            const lifetimes = consumptionIdentifiers.map(getLeverName)
308                .map((x) => "in." + x)
309                .map((x) => self._getValue(x, state));
310
311            const weights = consumptionIdentifiers.map((x) => {
312                return self._getValue(x, state);
313            });
314
315            const numIdentifiers = consumptionIdentifiers.length;
316            let runningTotal = 0;
317            let totalWeights = 0;
318            for (let i = 0; i < numIdentifiers; i++) {
319                runningTotal += lifetimes[i] * weights[i];
320                totalWeights += weights[i];
321            }
322            return runningTotal / totalWeights;
323        };
324
325        return (state) => {
326            if (wasteIdentifiers.length > 0) {
327                return getLifecycleForWaste(state);
328            } else {
329                return getLifecycleForConsumption(state);
330            }
331        };
332    }
333
334    /**
335     * Interpret a line of code which defines a local variable.
336     *
337     * @param ctx ANTLR context.
338     * @returns Local variable definition lambda.
339     */
340    visitDefinition(ctx) {
341        const self = this;
342
343        const name = ctx.getChild(1).getText();
344        if (name.indexOf(".") != -1) {
345            throw "Cannot make new variables with periods in the name.";
346        }
347
348        const expression = ctx.getChild(3).accept(self);
349
350        return (state) => {
351            const result = expression(state);
352            const localVars = state.get("local");
353            if (localVars.has(name)) {
354                throw name + " was defined multiple times.";
355            }
356            localVars.set(name, result);
357        };
358    }
359
360    /**
361     * Interpret code which assigns a new value to a variable.
362     *
363     * @param ctx ANTLR context.
364     * @returns Lambda which assigns a variable.
365     */
366    visitAssignment(ctx) {
367        const self = this;
368
369        const name = ctx.getChild(0).getText();
370        const expression = ctx.getChild(2).accept(self);
371
372        return (state) => {
373            const result = expression(state);
374            self._setValue(name, result, state);
375        };
376    }
377
378    /**
379     * Visit a command node.
380     *
381     * @return Array with functions which execute the user's command.
382     */
383    visitCommand(ctx) {
384        const self = this;
385        const instructions = ctx.getChild(0).accept(self);
386
387        return instructions;
388    }
389
390    /**
391     * Visit a program node and all children.
392     *
393     * @return Array with functions which execute the user's commands.
394     */
395    visitProgram(ctx) {
396        const self = this;
397
398        const instructions = [];
399
400        const numCommands = ctx.getChildCount() / 2;
401        for (let i = 0; i < numCommands; i++) {
402            const newInstructions = ctx.getChild(i * 2).accept(self);
403            instructions.push(newInstructions);
404        }
405
406        return (state) => {
407            instructions.forEach((instruction) => instruction(state));
408        };
409    }
410
411    /**
412     * Visit the condition of a conditional statement.
413     *
414     * @param ctx ANTLR context.
415     * @returns Lambda to evaluate the condition.
416     */
417    visitCondition(ctx) {
418        const self = this;
419
420        const priorExpression = ctx.getChild(0).accept(self);
421        let opFunc = null;
422        if (ctx.op.text === "==") {
423            opFunc = (a, b) => a == b;
424        } else if (ctx.op.text === "!=") {
425            opFunc = (a, b) => a != b;
426        } else if (ctx.op.text === "<") {
427            opFunc = (a, b) => a < b;
428        } else if (ctx.op.text === ">") {
429            opFunc = (a, b) => a > b;
430        } else if (ctx.op.text === ">=") {
431            opFunc = (a, b) => a >= b;
432        } else if (ctx.op.text === "<=") {
433            opFunc = (a, b) => a <= b;
434        }
435        const afterExpression = ctx.getChild(2).accept(self);
436
437        return (state) => {
438            return opFunc(priorExpression(state), afterExpression(state));
439        };
440    }
441
442    /**
443     * Visit a conditional statement similar to a Python turninary operation.
444     *
445     * @param ctx ANTLR context.
446     * @returns Lambda to evaluate the conditional.
447     */
448    visitConditional(ctx) {
449        const self = this;
450
451        const condition = ctx.cond.accept(self);
452        const positive = ctx.pos.accept(self);
453        const negative = ctx.neg.accept(self);
454
455        return (state) => {
456            if (condition(state)) {
457                return positive(state);
458            } else {
459                return negative(state);
460            }
461        };
462    }
463
464    /**
465     * Visit a statement which ensures a value is within a certain range, setting it to the max if
466     * too high or the minimum if too low.
467     *
468     * @param ctx ANTLR context
469     * @param opFunc Function taking in the value to cap and the cap level, returning the value
470     *      after applying the cap.
471     * @returns Lambda to evaluate the cap command.
472     */
473    visitCallCap(ctx, opFunc) {
474        const self = this;
475
476        const identifier = ctx.operand.getText();
477        const limitExpression = ctx.limit.accept(self);
478
479        return (state) => {
480            const val = opFunc(
481                self._getValue(identifier, state),
482                limitExpression(state),
483            );
484            self._setValue(identifier, val, state);
485        };
486    }
487
488    /**
489     * Ensure a value is at or above a certain value.
490     *
491     * @param ctx ANTLR context.
492     * @returns Lambda to enforce the minimum.
493     */
494    visitCallMin(ctx) {
495        const self = this;
496        return self.visitCallCap(ctx, (a, b) => a < b ? a : b);
497    }
498
499    /**
500     * Ensure a value is at or below a certain value.
501     *
502     * @param ctx ANTLR context.
503     * @returns Lambda to enforce the maximum.
504     */
505    visitCallMax(ctx) {
506        const self = this;
507        return self.visitCallCap(ctx, (a, b) => a > b ? a : b);
508    }
509
510    /**
511     * Evaluate a two sided bound, ensuring a value is within a certain range by setting it to the
512     * max if too high or the minimum if too low.
513     *
514     * @param ctx ANTLR context.
515     * @returns Lambda to enforce the bounds.
516     */
517    visitCallBound(ctx) {
518        const self = this;
519
520        const identifier = ctx.operand.getText();
521        const lowerExpression = ctx.lower.accept(self);
522        const upperExpression = ctx.upper.accept(self);
523
524        return (state) => {
525            const getBoundValue = () => {
526                const operand = self._getValue(identifier, state);
527                const lower = lowerExpression(state);
528                const upper = upperExpression(state);
529
530                if (operand > upper) {
531                    return upper;
532                } else if (operand < lower) {
533                    return lower;
534                } else {
535                    return operand;
536                }
537            };
538
539            const newValue = getBoundValue();
540            self._setValue(identifier, newValue, state);
541        };
542    }
543
544    /**
545     * Distribute a value across variables using the current variable values themselves as weights.
546     *
547     * @param ctx ANTLR context.
548     * @returns Lambda which performs the distribution.
549     */
550    visitDistributeDirect(ctx) {
551        const self = this;
552        const numIdentifiers = Math.ceil((ctx.getChildCount() - 6) / 2.0);
553        return self.visitDistributeStrategy(ctx, numIdentifiers, false);
554    }
555
556    /**
557     * Distribute a value across variables using custom weights.
558     *
559     * @param ctx ANTLR context.
560     * @returns Lambda which performs the distribution.
561     */
562    visitDistributeIndirect(ctx) {
563        const self = this;
564        const numIdentifiers = Math.ceil((ctx.getChildCount() - 6) / 4.0);
565        return self.visitDistributeStrategy(ctx, numIdentifiers, true);
566    }
567
568    /**
569     * Helper function to execute a distribute command.
570     *
571     * @param ctx ANTLR context.
572     * @param numIdentifiers The number of identifiers to which the value is being distributed.
573     * @param hasBy True if custom weights and false if using the variable prior values as weights.
574     * @returns Lambda for fulfilling the distribution action.
575     */
576    visitDistributeStrategy(ctx, numIdentifiers, hasBy) {
577        const self = this;
578
579        const elementsPerIdentifier = hasBy ? 4 : 2;
580
581        const valueExpression = ctx.value.accept(self);
582        const methodName = ctx.method.text;
583        const identifiers = [];
584        for (let i = 0; i < numIdentifiers; i++) {
585            const childIndex = i * elementsPerIdentifier + 4;
586            const scaleIndex = childIndex + 2;
587            const identifier = ctx.getChild(childIndex).getText();
588            const getIdVal = (state) => self._getValue(identifier, state);
589            const scaleGetter = hasBy ? ctx.getChild(scaleIndex).accept(self) : getIdVal;
590            identifiers.push({"identifier": identifier, "scale": scaleGetter});
591        }
592
593        const isLinear = methodName === "linearly";
594
595        return (state) => {
596            const valueToDistribute = valueExpression(state);
597
598            if (Math.abs(valueToDistribute) < 1e-7) {
599                return;
600            }
601
602            const totalTargetsValue = identifiers.map((identifierPair) => {
603                const scaleGetter = identifierPair["scale"];
604                return scaleGetter(state);
605            }).reduce((a, b) => a + b);
606
607            const getChangeProportional = (scaleValue) => {
608                return scaleValue / totalTargetsValue * valueToDistribute;
609            };
610
611            const getChangeLinear = (beforeValue) => {
612                return 1 / identifiers.length * valueToDistribute;
613            };
614
615            const isLinearEffective = isLinear || totalTargetsValue <= 0;
616
617            const getChange = isLinearEffective ? getChangeLinear : getChangeProportional;
618
619            identifiers.forEach((identifierPair) => {
620                const identifier = identifierPair["identifier"];
621                const scaleGetter = identifierPair["scale"];
622                const beforeValue = self._getValue(identifier, state);
623                const scaleValue = scaleGetter(state);
624                const change = getChange(scaleValue);
625                const newValue = beforeValue + change;
626                if (isLinearEffective || beforeValue != 0) {
627                    self._setValue(identifier, newValue, state);
628                }
629            });
630        };
631    }
632
633    /**
634     *
635     *
636     * @param ctx
637     * @returns
638     */
639    visitInspect(ctx) {
640        const self = this;
641
642        const valueText = ctx.value.getText();
643        const valueExpression = ctx.value.accept(self);
644
645        return (state) => {
646            const value = valueExpression(state);
647            state.get("inspect").push({"name": valueText, "value": value});
648        };
649    }
650
651    /**
652     * Generate a lambda to evaluate a change over time.
653     *
654     * @param ctx ANTLR context.
655     * @returns Lambda which performs the change over time.
656     */
657    visitTarget(ctx) {
658        const self = this;
659
660        const subject = ctx.subject.getText();
661        const valueExpression = ctx.value.accept(self);
662        const startYear = ctx.startyear.accept(self);
663        const endYear = ctx.endyear.accept(self);
664
665        return (state) => {
666            const value = valueExpression(state);
667
668            const startYearRealized = startYear(state);
669            const endYearRealized = endYear(state);
670
671            if (startYearRealized >= endYearRealized) {
672                throw "Start year must be earlier than end year for change.";
673            }
674
675            const currentYear = state.get("meta").get("year");
676
677            const pastEnd = currentYear > endYearRealized;
678            const effectiveCurrentYear = pastEnd ? endYearRealized : currentYear;
679
680            const slope = value / (endYearRealized - startYearRealized);
681            const change = slope * (effectiveCurrentYear - startYearRealized);
682            const oldValue = self._getValue(subject, state);
683            const newValue = oldValue + change;
684
685            self._setValue(subject, newValue, state);
686        };
687    }
688
689    /**
690     * Get the value of an identifier.
691     *
692     * @param raw Identifier name which may include nesting like out.nafta.
693     * @param state The state object in which to find this value.
694     * @returns The value of the identifier.
695     */
696    _getValue(raw, state) {
697        const resolved = raw.indexOf(".") == -1 ? "local." + raw : raw;
698        const pieces = resolved.split(".");
699        let value = state;
700        pieces.forEach((piece) => {
701            if (!value.has(piece)) {
702                throw "Could not find " + piece + " (" + raw + ")";
703            }
704            value = value.get(piece);
705        });
706        return value;
707    }
708
709    /**
710     * Set the value of an identifier.
711     *
712     * @param name Identifier name which may include nesting like out.nafta.
713     * @param result The value to assign.
714     * @param state The state object in which to find this value.
715     */
716    _setValue(name, result, state) {
717        const self = this;
718
719
720        const resolved = name.indexOf(".") == -1 ? "local." + name : name;
721        const pieces = resolved.split(".");
722
723        let container = state;
724        pieces.slice(0, -1).forEach((piece) => {
725            if (!container.has(piece)) {
726                throw "Could not find " + piece + " (" + name + ")";
727            }
728            container = container.get(piece);
729        });
730
731        const finalPiece = pieces[pieces.length - 1];
732        if (!container.has(finalPiece)) {
733            throw "Could not find " + finalPiece + " (" + name + ")";
734        }
735
736        container.set(finalPiece, result);
737    }
738}
739
740
741
742// eslint-disable-next-line no-undef
743export {toolkit, CompileVisitor};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.