From f9ef67539938ed949a17022df05707a2c06c558a Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 17 Aug 2021 11:21:25 -0700 Subject: [PATCH] perf: optimize IF THEN ELSE for cases where condition expression resolves to a constant (#103) Fixes #102 --- .github/workflows/build.yaml | 2 +- models/prune/prune.dat | 230 ++++++++++++++++++++++ models/prune/prune.mdl | 371 +++++++++++++++++++++++++++++++---- models/prune/prune.vdfx | Bin 11211 -> 0 bytes models/prune/prune_check.sh | 34 +++- models/prune/prune_data.dat | 24 +-- models/prune/prune_spec.json | 14 +- src/EquationGen.js | 31 +++ src/EquationReader.js | 29 +++ src/ExprReader.js | 204 +++++++++++++++++++ src/Model.js | 43 +++- 11 files changed, 915 insertions(+), 67 deletions(-) delete mode 100755 models/prune/prune.vdfx create mode 100644 src/ExprReader.js diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c3a300db..f9fc2f29 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,4 +26,4 @@ jobs: - name: Run Tests run: | npm run prettier:check - npm test + npm run test diff --git a/models/prune/prune.dat b/models/prune/prune.dat index 2182a8c4..75e5ec48 100644 --- a/models/prune/prune.dat +++ b/models/prune/prune.dat @@ -22,6 +22,10 @@ B1 Totals 8 135.714 9 150 10 170 +Constant Partial 1 +0 1 +Constant Partial 2 +0 2 D Totals 0 11000 1 11200 @@ -60,6 +64,10 @@ E2 Values 10 8400 FINAL TIME 0 10 +Initial Partial[C1] +0 1 +Initial Partial[C2] +0 2 INITIAL TIME 0 0 Input 1 @@ -116,6 +124,18 @@ Look2 Value at t1 8 1 9 1 10 1 +Partial[C2] +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 SAVEPER 0 1 1 1 @@ -140,6 +160,216 @@ Simple Totals 8 4242.86 9 4400 10 5500 +Test 1 F +0 2 +Test 1 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 1 T +0 1 +Test 10 Cond +0 1 +Test 10 F +0 2 +Test 10 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 10 T +0 1 +Test 11 Cond +0 0 +Test 11 F +0 2 +Test 11 Result +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 +Test 11 T +0 1 +Test 12 Cond +0 1 +Test 12 F +0 2 +Test 12 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 12 T +0 1 +Test 2 F +0 2 +Test 2 Result +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 +Test 2 T +0 1 +Test 3 F +0 2 +Test 3 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 3 T +0 1 +Test 4 Cond +0 0 +Test 4 F +0 2 +Test 4 Result +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 +Test 4 T +0 1 +Test 5 Cond +0 1 +Test 5 F +0 2 +Test 5 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 5 T +0 1 +Test 6 Cond +0 0 +Test 6 F +0 2 +Test 6 Result +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 +Test 6 T +0 1 +Test 7 Cond +0 1 +Test 7 F +0 2 +Test 7 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 7 T +0 1 +Test 8 Cond +0 0 +Test 8 F +0 2 +Test 8 Result +0 2 +1 2 +2 2 +3 2 +4 2 +5 2 +6 2 +7 2 +8 2 +9 2 +10 2 +Test 8 T +0 1 +Test 9 Cond +0 1 +Test 9 F +0 2 +Test 9 Result +0 1 +1 1 +2 1 +3 1 +4 1 +5 1 +6 1 +7 1 +8 1 +9 1 +10 1 +Test 9 T +0 1 TIME STEP 0 1 With Look1 at t1 diff --git a/models/prune/prune.mdl b/models/prune/prune.mdl index 7203ff05..10e726b9 100644 --- a/models/prune/prune.mdl +++ b/models/prune/prune.mdl @@ -1,119 +1,366 @@ {UTF-8} - DimA: A1, A2, A3 - ~~| + ~ + ~ | DimB: B1, B2, B3 - ~~| + ~ + ~ | DimC: C1, C2 - ~~| + ~ + ~ | DimD: D1, D2 -> DimC - ~~| + ~ + ~ | DimE: E1, E2 - ~~| + ~ + ~ | Simple 1 - ~~| + ~ dmnl + ~ | Simple 2 - ~~| + ~ dmnl + ~ | A Values[DimA] - ~~| + ~ dmnl + ~ | BC Values[DimB,DimC] - ~~| + ~ dmnl + ~ | D Values[DimD] - ~~| - -E Values[E1] - ~~| + ~ dmnl + ~ | +E Values[E1] ~~| E Values[E2] - ~~| + ~ dmnl + ~ | Look1((0,0),(1,1),(2,2)) - ~~| + ~ dmnl + ~ | Look2((0,0),(1,1),(2,2)) - ~~| + ~ dmnl + ~ | -Input 1 = 10.0 - ~~| +Input 1 = 10 + ~ dmnl + ~ | -Input 2 = 20.0 - ~~| +Input 2 = 20 + ~ dmnl + ~ | -Input 3 = 30.0 - ~~| +Input 3 = 30 + ~ dmnl + ~ | Simple Totals = Simple 1 + Simple 2 - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | A Totals = SUM( A Values[DimA!] ) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | B1 Totals = SUM( BC Values[B1,DimC!] ) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | D Totals = SUM( D Values[DimD!] ) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | E1 Values = E Values[E1] - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | E2 Values = E Values[E2] - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | Input 1 and 2 Total = Input 1 + Input 2 - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | Input 2 and 3 Total = Input 2 + Input 3 - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | Look1 Value at t1 = Look1(1) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | Look2 Value at t1 = Look2(1) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | With Look1 at t1 = WITH LOOKUP ( 1, ([(0,0)-(2,2)],(0,0),(1,1),(2,2)) ) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | With Look2 at t1 = WITH LOOKUP ( 1, ([(0,0)-(2,2)],(0,0),(1,1),(2,2)) ) - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | Constant Partial 1 = 1 - ~~| + ~ dmnl + ~ | Constant Partial 2 = 2 - ~~| + ~ dmnl + ~ | Initial Partial[C1] = - INITIAL( Constant Partial 1 ) - ~~| - + INITIAL( Constant Partial 1 ) ~~| Initial Partial[C2] = INITIAL( Constant Partial 2 ) - ~~| + ~ dmnl + ~ | Partial[C2] = Initial Partial[C2] - ~~| + ~ dmnl + ~ ~ :SUPPLEMENTARY + | + +Test 1 T = 1 + ~ dmnl + ~ | + +Test 1 F = 2 + ~ dmnl + ~ | + +Test 1 Result = IF THEN ELSE(Input 1 = 10, Test 1 T, Test 1 F) + ~ dmnl + ~ Should not be eliminated because "Input 1" is listed as an input in the \ + spec file. + ~ :SUPPLEMENTARY + | + +Test 2 T = 1 + ~ dmnl + ~ | + +Test 2 F = 2 + ~ dmnl + ~ | + +Test 2 Result = IF THEN ELSE(0, Test 2 T, Test 2 F) + ~ dmnl + ~ Only "Test 2 F" should be generated. + ~ :SUPPLEMENTARY + | + +Test 3 T = 1 + ~ dmnl + ~ | + +Test 3 F = 2 + ~ dmnl + ~ | + +Test 3 Result = IF THEN ELSE(1, Test 3 T, Test 3 F) + ~ dmnl + ~ Only "Test 3 T" should be generated. + ~ :SUPPLEMENTARY + | + +Test 4 Cond = 0 + ~ dmnl + ~ | + +Test 4 T = 1 + ~ dmnl + ~ | + +Test 4 F = 2 + ~ dmnl + ~ | + +Test 4 Result = IF THEN ELSE(Test 4 Cond, Test 4 T, Test 4 F) + ~ dmnl + ~ Only "Test 4 F" should be generated. + ~ :SUPPLEMENTARY + | + +Test 5 Cond = 1 + ~ dmnl + ~ | + +Test 5 T = 1 + ~ dmnl + ~ | + +Test 5 F = 2 + ~ dmnl + ~ | + +Test 5 Result = IF THEN ELSE(Test 5 Cond, Test 5 T, Test 5 F) + ~ dmnl + ~ Only "Test 5 T" should be generated. + ~ :SUPPLEMENTARY + | + +Test 6 Cond = 0 + ~ dmnl + ~ | + +Test 6 T = 1 + ~ dmnl + ~ | + +Test 6 F = 2 + ~ dmnl + ~ | + +Test 6 Result = IF THEN ELSE(Test 6 Cond = 1, Test 6 T, Test 6 F) + ~ dmnl + ~ Only "Test 6 F" should be generated. + ~ :SUPPLEMENTARY + | + +Test 7 Cond = 1 + ~ dmnl + ~ | + +Test 7 T = 1 + ~ dmnl + ~ | + +Test 7 F = 2 + ~ dmnl + ~ | + +Test 7 Result = IF THEN ELSE(Test 7 Cond = 1, Test 7 T, Test 7 F) + ~ dmnl + ~ Only "Test 7 T" should be generated. + ~ :SUPPLEMENTARY + | + +Test 8 Cond = 0 + ~ dmnl + ~ | + +Test 8 T = 1 + ~ dmnl + ~ | + +Test 8 F = 2 + ~ dmnl + ~ | + +Test 8 Result = IF THEN ELSE(Test 8 Cond > 0, Test 8 T, Test 8 F) + ~ dmnl + ~ Only "Test 8 F" should be generated. + ~ :SUPPLEMENTARY + | + +Test 9 Cond = 1 + ~ dmnl + ~ | + +Test 9 T = 1 + ~ dmnl + ~ | + +Test 9 F = 2 + ~ dmnl + ~ | + +Test 9 Result = IF THEN ELSE(Test 9 Cond > 0, Test 9 T, Test 9 F) + ~ dmnl + ~ Only "Test 9 T" should be generated. + ~ :SUPPLEMENTARY + | + +Test 10 Cond = 1 + ~ dmnl + ~ | + +Test 10 T = 1 + ~ dmnl + ~ | + +Test 10 F = 2 + ~ dmnl + ~ | + +Test 10 Result = IF THEN ELSE(ABS(Test 10 Cond), Test 10 T, Test 10 F) + ~ dmnl + ~ Should not be eliminated because condition contains function call. + ~ :SUPPLEMENTARY + | + +Test 11 Cond = 0 + ~ dmnl + ~ | + +Test 11 T = 1 + ~ dmnl + ~ | + +Test 11 F = 2 + ~ dmnl + ~ | + +Test 11 Result = IF THEN ELSE(Test 11 Cond :AND: ABS(Test 11 Cond), Test 11 T, Test 11 F\ + ) + ~ dmnl + ~ Only "Test 11 F" should be generated. + ~ :SUPPLEMENTARY + | + +Test 12 Cond = 1 + ~ dmnl + ~ | + +Test 12 T = 1 + ~ dmnl + ~ | + +Test 12 F = 2 + ~ dmnl + ~ | + +Test 12 Result = IF THEN ELSE(Test 12 Cond :OR: ABS(Test 12 Cond), Test 12 T, Test 12 F\ + ) + ~ dmnl + ~ Only "Test 12 T" should be generated. + ~ :SUPPLEMENTARY + | ******************************************************** .Control @@ -141,3 +388,43 @@ TIME STEP = 1 ~ Month [0,?] ~ The time step for the simulation. | + +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*View 1 +$192-192-192,0,Times New Roman|12||0-0-0|0-0-0|0-0-255|-1--1--1|255-255-255|96,96,100,0 +///---\\\ +:L<%^E!@ +1:prune.vdfx +4:Time +5:A Values[DimA] +6:A1 +6:B1 +6:C1 +6:D1 +6:E1 +9:prune +19:100,0 +24:0 +25:10 +26:10 +13:prune_data.vdfx +15:0,0,0,0,0,0 +27:0, +34:0, +42:0 +72:0 +73:0 +95:0 +96:0 +97:0 +77:0 +78:0 +93:0 +94:0 +92:0 +91:0 +90:0 +87:0 +75: +43: diff --git a/models/prune/prune.vdfx b/models/prune/prune.vdfx deleted file mode 100755 index 592d0f0fd4ede9fa567ac9ca2cb75f6d528ad189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11211 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 qJPsk%ncflgJ3&|!92V^RvHB+jaVd5 diff --git a/models/prune/prune_check.sh b/models/prune/prune_check.sh index b3173483..24d3d4ad 100755 --- a/models/prune/prune_check.sh +++ b/models/prune/prune_check.sh @@ -7,7 +7,7 @@ C_FILE=build/prune.c function expect_present { grep -q "$1" $C_FILE if [[ $? != 0 ]]; then - echo "ERROR: Did not find string '$1' this is expected to appear in $C_FILE" + echo "ERROR: Did not find string '$1' that is expected to appear in $C_FILE" exit 1 fi } @@ -43,6 +43,18 @@ expect_present "_constant_partial_1" expect_present "_constant_partial_2" expect_present "_initial_partial" expect_present "_partial" +expect_present "_test_1_result = _IF_THEN_ELSE(_input_1 == 10.0, _test_1_t, _test_1_f);" +expect_present "_test_2_result = _test_2_f;" +expect_present "_test_3_result = _test_3_t;" +expect_present "_test_4_result = _test_4_f;" +expect_present "_test_5_result = _test_5_t;" +expect_present "_test_6_result = _test_6_f;" +expect_present "_test_7_result = _test_7_t;" +expect_present "_test_8_result = _test_8_f;" +expect_present "_test_9_result = _test_9_t;" +expect_present "_test_10_result = _IF_THEN_ELSE(_ABS(_test_10_cond), _test_10_t, _test_10_f);" +expect_present "_test_11_result = _test_11_f;" +expect_present "_test_12_result = _test_12_t;" # Verify that unreferenced variables do not appear in the generated C file expect_not_present "_input_3" @@ -56,5 +68,25 @@ expect_not_present "__lookup2" expect_not_present "_look2" expect_not_present "_look2_value_at_t1" expect_not_present "_with_look2_at_t1" +expect_not_present "_test_2_cond" +expect_not_present "_test_2_t" +expect_not_present "_test_3_cond" +expect_not_present "_test_3_f" +expect_not_present "_test_4_cond" +expect_not_present "_test_4_t" +expect_not_present "_test_5_cond" +expect_not_present "_test_5_f" +expect_not_present "_test_6_cond" +expect_not_present "_test_6_t" +expect_not_present "_test_7_cond" +expect_not_present "_test_7_f" +expect_not_present "_test_8_cond" +expect_not_present "_test_8_t" +expect_not_present "_test_9_cond" +expect_not_present "_test_9_f" +expect_not_present "_test_11_cond" +expect_not_present "_test_11_t" +expect_not_present "_test_12_cond" +expect_not_present "_test_12_f" echo "All validation checks passed!" diff --git a/models/prune/prune_data.dat b/models/prune/prune_data.dat index d9f2ba6b..66629fab 100644 --- a/models/prune/prune_data.dat +++ b/models/prune/prune_data.dat @@ -1,15 +1,3 @@ -Simple 1 -0 1000 -1 2000 -2 3000 -9 4000 -10 5000 -Simple 2 -0 100 -1 200 -2 300 -9 400 -10 500 A Values[A1] 0 0 1 10 @@ -88,3 +76,15 @@ E Values[E2] 2 8200 9 8300 10 8400 +Simple 1 +0 1000 +1 2000 +2 3000 +9 4000 +10 5000 +Simple 2 +0 100 +1 200 +2 300 +9 400 +10 500 diff --git a/models/prune/prune_spec.json b/models/prune/prune_spec.json index 0aa2f1fa..0a027077 100644 --- a/models/prune/prune_spec.json +++ b/models/prune/prune_spec.json @@ -9,7 +9,19 @@ "B1 Totals", "Look1 Value at t1", "With Look1 at t1", - "Partial[C2]" + "Partial[C2]", + "Test 1 Result", + "Test 2 Result", + "Test 3 Result", + "Test 4 Result", + "Test 5 Result", + "Test 6 Result", + "Test 7 Result", + "Test 8 Result", + "Test 9 Result", + "Test 10 Result", + "Test 11 Result", + "Test 12 Result" ], "externalDatfiles": ["prune_data.dat"] } diff --git a/src/EquationGen.js b/src/EquationGen.js index c10c26e1..441ad209 100644 --- a/src/EquationGen.js +++ b/src/EquationGen.js @@ -2,6 +2,7 @@ import path from 'path' import R from 'ramda' import XLSX from 'xlsx' import { ModelLexer, ModelParser } from 'antlr4-vensim' +import ExprReader from './ExprReader.js' import ModelReader from './ModelReader.js' import ModelLHSReader from './ModelLHSReader.js' import LoopIndexVars from './LoopIndexVars.js' @@ -691,6 +692,10 @@ export default class EquationGen extends ModelReader { } else if (fn === '_ACTIVE_INITIAL') { // Only emit the eval-time initialization without the function call for ACTIVE INITIAL. super.visitCall(ctx) + } else if (fn === '_IF_THEN_ELSE') { + // Conditional expressions are handled specially in `visitExprList`. + super.visitCall(ctx) + this.callStack.pop() } else if (isSmoothFunction(fn)) { // For smooth functions, replace the entire call with the expansion variable generated earlier. let smoothVar = Model.varWithRefId(this.var.smoothVarRefId) @@ -778,6 +783,32 @@ export default class EquationGen extends ModelReader { exprs[0].accept(this) this.setArgIndex(1) this.vsoOrder = this.cVarOrConst(exprs[1]) + } else if (fn === '_IF_THEN_ELSE') { + // See if the condition expression was previously determined to resolve to a + // compile-time constant. If so, we only need to emit code for one branch. + const condText = ctx.expr(0).getText() + const condValue = Model.getConstantExprValue(condText) + if (condValue !== undefined) { + if (condValue !== 0) { + // Emit only the "if true" branch + this.setArgIndex(1) + ctx.expr(1).accept(this) + } else { + // Emit only the "if false" branch + this.setArgIndex(2) + ctx.expr(2).accept(this) + } + } else { + // Emit a normal if/else with both branches + this.emit(fn) + this.emit('(') + for (let i = 0; i < exprs.length; i++) { + if (i > 0) this.emit(', ') + this.setArgIndex(i) + exprs[i].accept(this) + } + this.emit(')') + } } else { // Ordinary expression lists are completely emitted with comma delimiters. for (let i = 0; i < exprs.length; i++) { diff --git a/src/EquationReader.js b/src/EquationReader.js index 2ed4c5cf..9f37863d 100644 --- a/src/EquationReader.js +++ b/src/EquationReader.js @@ -1,6 +1,7 @@ import antlr4 from 'antlr4' import { ModelLexer, ModelParser } from 'antlr4-vensim' import R from 'ramda' +import ExprReader from './ExprReader.js' import Model from './Model.js' import ModelReader from './ModelReader.js' import VariableReader from './VariableReader.js' @@ -172,6 +173,34 @@ export default class EquationReader extends ModelReader { tab: args[1], startCell: args[2] } + } else if (fn === '_IF_THEN_ELSE') { + // Evaluate the condition expression of the `IF THEN ELSE`. If it resolves + // to a compile-time constant, we only need to visit one branch, which means + // that no references will be recorded for the other branch, therefore allowing + // it to be skipped in the unused reference elimination phase and during the + // final code generation phase. + const condText = ctx.expr(0).getText() + const exprReader = new ExprReader() + const condExpr = exprReader.read(condText) + if (condExpr.constantValue !== undefined) { + // Record the conditional expression and its constant value so that + // it can be accessed later by EquationGen. We need to record it + // this way because any variables referenced by the expression may + // be removed during the unused reference elimination phase. + Model.addConstantExpr(condText, condExpr.constantValue) + if (condExpr.constantValue !== 0) { + // Only visit the "if true" branch + this.setArgIndex(1) + ctx.expr(1).accept(this) + } else { + // Only visit the "if false" branch + this.setArgIndex(2) + ctx.expr(2).accept(this) + } + } else { + // Visit the condition and both branches + super.visitExprList(ctx) + } } else { // Keep track of all function names referenced in this expression. Note that lookup // variables are sometimes function-like, so they will be included here. This will be diff --git a/src/ExprReader.js b/src/ExprReader.js new file mode 100644 index 00000000..82accb08 --- /dev/null +++ b/src/ExprReader.js @@ -0,0 +1,204 @@ +import antlr4 from 'antlr4' +import { ModelLexer, ModelParser, ModelVisitor } from 'antlr4-vensim' +import { canonicalName } from './Helpers.js' +import Model from './Model.js' + +/** + * Reads an expression and determines if it resolves to a constant numeric value. + * This depends on having access to the model variables, so this should be used + * only after the `readVariables` process has been completed and the spec file + * has been loaded. + */ +export default class ExprReader extends ModelVisitor { + constructor() { + super() + } + + read(exprText) { + let chars = new antlr4.InputStream(exprText) + let lexer = new ModelLexer(chars) + let tokens = new antlr4.CommonTokenStream(lexer) + let parser = new ModelParser(tokens) + parser.buildParseTrees = true + let expr = parser.expr() + expr.accept(this) + + return { + constantValue: this.constantValue + } + } + + // Constants + + visitConst(ctx) { + const constantValue = parseFloat(ctx.Const().getText()) + if (!Number.isNaN(constantValue)) { + this.constantValue = constantValue + } else { + this.constantValue = undefined + } + } + visitConstList() { + this.constantValue = undefined + } + + // Function calls and variables + + visitCall() { + // Treat function calls as non-constant (can't easily determine if they + // resolve to a constant) + this.constantValue = undefined + } + visitExprList() { + // Treat function calls as non-constant (can't easily determine if they + // resolve to a constant) + this.constantValue = undefined + } + visitVar(ctx) { + // Determine whether this variable has a constant value + const varName = ctx.Id().getText().trim() + const cName = canonicalName(varName) + const v = Model.varWithName(cName) + const modelFormula = v?.modelFormula?.trim() || '' + const isNumber = modelFormula.match(/^[+-]?\d+(\.\d+)?$/) + const canBeOverridden = Model.isInputVar(cName) + if (v && isNumber && !canBeOverridden) { + const numValue = parseFloat(modelFormula) + if (!Number.isNaN(numValue)) { + this.constantValue = numValue + } else { + this.constantValue = undefined + } + } else { + this.constantValue = undefined + } + } + + // Lookups + + visitLookup() { + this.constantValue = undefined + } + visitLookupCall() { + this.constantValue = undefined + } + + // Unary operators + + visitNegative(ctx) { + super.visitNegative(ctx) + if (this.constantValue !== undefined) { + this.constantValue = -this.constantValue + } + } + visitPositive(ctx) { + super.visitPositive(ctx) + if (this.constantValue !== undefined) { + this.constantValue = +this.constantValue + } + } + visitNot(ctx) { + super.visitNot(ctx) + if (this.constantValue !== undefined) { + this.constantValue = !this.constantValue + } + } + + // Binary operators + + visitBinaryArgs(ctx, combine) { + ctx.expr(0).accept(this) + const constantValue0 = this.constantValue + ctx.expr(1).accept(this) + const constantValue1 = this.constantValue + if (constantValue0 !== undefined && constantValue1 !== undefined) { + this.constantValue = combine(constantValue0, constantValue1) + } else { + this.constantValue = undefined + } + } + + visitPower(ctx) { + this.visitBinaryArgs(ctx, (a, b) => Math.pow(a, b)) + } + visitMulDiv(ctx) { + this.visitBinaryArgs(ctx, (a, b) => { + if (ctx.op.type === ModelLexer.Star) { + return a * b + } else { + return a / b + } + }) + } + visitAddSub(ctx) { + this.visitBinaryArgs(ctx, (a, b) => { + if (ctx.op.type === ModelLexer.Plus) { + return a + b + } else { + return a - b + } + }) + } + visitRelational(ctx) { + this.visitBinaryArgs(ctx, (a, b) => { + if (ctx.op.type === ModelLexer.Less) { + return a < b ? 1 : 0 + } else if (ctx.op.type === ModelLexer.Greater) { + return a > b ? 1 : 0 + } else if (ctx.op.type === ModelLexer.LessEqual) { + return a <= b ? 1 : 0 + } else { + return a >= b ? 1 : 0 + } + }) + } + visitEquality(ctx) { + this.visitBinaryArgs(ctx, (a, b) => { + if (ctx.op.type === ModelLexer.Equal) { + return a === b ? 1 : 0 + } else { + return a !== b ? 1 : 0 + } + }) + } + visitAnd(ctx) { + // For AND expressions, we don't need both sides to have a constant value; as + // long as one side is known to be false, then the expression resolves to false + ctx.expr(0).accept(this) + const constantValue0 = this.constantValue + ctx.expr(1).accept(this) + const constantValue1 = this.constantValue + if (constantValue0 !== undefined && constantValue1 !== undefined) { + this.constantValue = constantValue0 && constantValue1 + } else if (constantValue0 !== undefined && !constantValue0) { + this.constantValue = 0 + } else if (constantValue1 !== undefined && !constantValue1) { + this.constantValue = 0 + } else { + this.constantValue = undefined + } + } + visitOr(ctx) { + // For OR expressions, we don't need both sides to have a constant value; as + // long as one side is known to be true, then the expression resolves to true + ctx.expr(0).accept(this) + const constantValue0 = this.constantValue + ctx.expr(1).accept(this) + const constantValue1 = this.constantValue + if (constantValue0 !== undefined && constantValue1 !== undefined) { + this.constantValue = constantValue0 || constantValue1 + } else if (constantValue0 !== undefined && constantValue0) { + this.constantValue = 1 + } else if (constantValue1 !== undefined && constantValue1) { + this.constantValue = 1 + } else { + this.constantValue = undefined + } + } + + // Tokens + + visitParens(ctx) { + super.visitParens(ctx) + } +} diff --git a/src/Model.js b/src/Model.js index e19a881b..f61038c8 100644 --- a/src/Model.js +++ b/src/Model.js @@ -23,6 +23,8 @@ import { import { decanonicalize, isIterable, listConcat, strlist, vlog, vsort } from './Helpers.js' let variables = [] +let inputVars = [] +let constantExprs = new Map() // Also keep variables in a map (with `varName` as key) for faster lookup const variablesByName = new Map() @@ -43,6 +45,21 @@ function read(parseTree, spec, extData, directData, modelDirname) { readSubscriptRanges(parseTree, spec.dimensionFamilies, spec.indexFamilies, modelDirname) // Read variables from the model parse tree. readVariables(parseTree, specialSeparationDims, directData) + if (spec) { + // If the spec file contains `input/outputVarNames` (with full Vensim variable names) + // convert those to C names first. Otherwise, use `input/outputNames` which are already + // assumed to be valid C names. + if (spec.inputVarNames) { + spec.inputVars = R.map(cName, spec.inputVarNames) + } + if (spec.outputVarNames) { + spec.outputVars = R.map(cName, spec.outputVarNames) + } + // Save the input vars locally so that they can be referenced by `isInputVar`. + if (spec.inputVars) { + inputVars = spec.inputVars + } + } // Analyze model equations to fill in more details about variables. analyze() // Check that all input and output vars in the spec actually exist in the model. @@ -235,16 +252,6 @@ function checkSpecVars(spec, extData) { } if (spec) { - // If the spec file contains `input/outputVarNames` (with full Vensim variable names) - // convert those to C names first. Otherwise, use `input/outputNames` which are already - // assumed to be valid C names. - if (spec.inputVarNames) { - spec.inputVars = R.map(cName, spec.inputVarNames) - } - if (spec.outputVarNames) { - spec.outputVars = R.map(cName, spec.outputVarNames) - } - check(spec.inputVars, 'input') check(spec.outputVars, 'output') } @@ -648,6 +655,19 @@ function cName(vensimVarName) { // This function requires model analysis to be completed first when the variable has subscripts. return new VarNameReader().read(vensimVarName) } +function isInputVar(varName) { + // Return true if the given variable (in canonical form) is included in the list of + // input variables in the spec file. + return inputVars.includes(varName) +} +function addConstantExpr(exprText, constantValue) { + // Record the constant value for the given expression in a map for later lookup. + constantExprs.set(exprText, constantValue) +} +function getConstantExprValue(exprText) { + // Return the constant value for the given expression if one was recorded. + return constantExprs.get(exprText) +} // // Helpers for getting lists of vars // @@ -975,6 +995,7 @@ function printDepsGraph(graph, varType) { } } export default { + addConstantExpr, addEquation, addNonAtoAVar, addVariable, @@ -985,7 +1006,9 @@ export default { dataVars, expansionFlags, filterVar, + getConstantExprValue, initVars, + isInputVar, isNonAtoAName, levelVars, lookupVars,