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.