@sdeverywhere/compile 0.7.28 → 0.7.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/_shared/helpers.js +2 -2
- package/src/generate/direct-data-helpers.js +18 -6
- package/src/generate/gen-code.js +3 -2
- package/src/generate/gen-direct-const.js +5 -0
- package/src/generate/gen-equation.js +6 -0
- package/src/generate/gen-expr.js +92 -0
- package/src/generate/gen-lookup-from-direct.js +14 -7
- package/src/model/read-equations-expand.js +72 -0
- package/src/model/read-equations.js +40 -49
- package/src/model/read-variables.js +7 -1
- package/src/parse-and-generate.js +5 -5
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.30",
|
|
4
4
|
"description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@sdeverywhere/parse": "^0.1.
|
|
8
|
+
"@sdeverywhere/parse": "^0.1.4",
|
|
9
9
|
"byline": "^5.0.0",
|
|
10
10
|
"csv-parse": "^5.3.3",
|
|
11
11
|
"ramda": "^0.27.0",
|
package/src/_shared/helpers.js
CHANGED
|
@@ -127,10 +127,10 @@ export let listConcat = (a, x, addSpaces = false) => {
|
|
|
127
127
|
}
|
|
128
128
|
// Convert a number or string into a C double constant string.
|
|
129
129
|
// A blank string is converted to zero, following Excel.
|
|
130
|
-
// A string that cannot be converted throws an
|
|
130
|
+
// A string that cannot be converted throws an error.
|
|
131
131
|
export let cdbl = x => {
|
|
132
132
|
function throwError() {
|
|
133
|
-
throw new Error(`
|
|
133
|
+
throw new Error(`Cannot convert "${x}" to a number`)
|
|
134
134
|
}
|
|
135
135
|
let s = '0.0'
|
|
136
136
|
if (typeof x === 'number') {
|
|
@@ -48,7 +48,16 @@ function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
|
|
|
48
48
|
if (sheet) {
|
|
49
49
|
return (c, r) => {
|
|
50
50
|
let cell = sheet[XLSX.utils.encode_cell({ c, r })]
|
|
51
|
-
|
|
51
|
+
if (cell == null || cell.v === '') {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return cdbl(cell.v)
|
|
56
|
+
} catch (_error) {
|
|
57
|
+
// Return null when the cell value cannot be converted to a number;
|
|
58
|
+
// the caller will treat this as the end of data.
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
} else {
|
|
54
63
|
throw new Error(`Direct ${dataKind} worksheet ${tab} in ${dataSource} ${fileOrTag} not found`)
|
|
@@ -72,13 +81,16 @@ function handleCsvFile(file, dataPathname, delimiter, dataKind) {
|
|
|
72
81
|
let data = readCsv(dataPathname, delimiter)
|
|
73
82
|
if (data) {
|
|
74
83
|
return (c, r) => {
|
|
75
|
-
|
|
84
|
+
if (data[r] == null || data[r][c] == null || data[r][c] === '') {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
76
87
|
try {
|
|
77
|
-
|
|
78
|
-
} catch (
|
|
79
|
-
|
|
88
|
+
return cdbl(data[r][c])
|
|
89
|
+
} catch (_error) {
|
|
90
|
+
// Return null when the cell value cannot be converted to a number;
|
|
91
|
+
// the caller will treat this as the end of data.
|
|
92
|
+
return null
|
|
80
93
|
}
|
|
81
|
-
return value
|
|
82
94
|
}
|
|
83
95
|
} else {
|
|
84
96
|
throw new Error(`Direct ${dataKind} file ${file} could not be read`)
|
package/src/generate/gen-code.js
CHANGED
|
@@ -17,8 +17,9 @@ import { generateJS } from './gen-code-js.js'
|
|
|
17
17
|
* @param {Map<string, any>} opts.directData The mapping of dataset name used in a
|
|
18
18
|
* `GET DIRECT DATA` call (e.g., `?data`) to the tabular data contained in the loaded
|
|
19
19
|
* data file.
|
|
20
|
-
* @param {string} opts.modelDirname The path to the directory containing
|
|
21
|
-
* (
|
|
20
|
+
* @param {string} opts.modelDirname The absolute path to the directory containing data
|
|
21
|
+
* (dat, xlsx, csv) files that are referenced by the model. This path is used for
|
|
22
|
+
* resolving data files for `GET DIRECT SUBSCRIPT` calls.
|
|
22
23
|
* @returns A string containing the generated code.
|
|
23
24
|
*/
|
|
24
25
|
export function generateCode(parsedModel, opts) {
|
|
@@ -82,7 +82,12 @@ export function generateDirectConstInit(variable, directData, modelDir) {
|
|
|
82
82
|
for (let i = 0; i < cellOffsets.length; i++) {
|
|
83
83
|
let rowOffset = cellOffsets[i][0] ? cellOffsets[i][0] : 0
|
|
84
84
|
let colOffset = cellOffsets[i][1] ? cellOffsets[i][1] : 0
|
|
85
|
+
// Use 0.0 as a fallback when the cell is missing, empty, or contains a non-numeric value.
|
|
86
|
+
// (Vensim raises an error in this case, but SDE has historically tolerated invalid cells.)
|
|
85
87
|
let dataValue = getCellValue(startCol + colOffset, startRow + rowOffset)
|
|
88
|
+
if (dataValue == null) {
|
|
89
|
+
dataValue = '0.0'
|
|
90
|
+
}
|
|
86
91
|
let lhs = `${variable.varName}${lhsSubscripts[i] || ''}`
|
|
87
92
|
lines.push(` ${lhs} = ${dataValue};`)
|
|
88
93
|
}
|
|
@@ -116,6 +116,12 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
|
|
|
116
116
|
// Emit decl/init code for the lookup
|
|
117
117
|
const lookupDef = generateLookupFromPoints(variable, mode, /*copy=*/ false, cLhs, loopIndexVars, outFormat)
|
|
118
118
|
if (lookupDef.length > 0) {
|
|
119
|
+
if (mode === 'decl') {
|
|
120
|
+
// When declaring a lookup, even if the lookup variable includes dimensions (i.e., is
|
|
121
|
+
// partially apply-to-all), the data variable declarations should not be inside for loops,
|
|
122
|
+
// so we omit them in this case
|
|
123
|
+
return [...lookupDef]
|
|
124
|
+
}
|
|
119
125
|
return [...openLoops, ...lookupDef, ...closeLoops]
|
|
120
126
|
} else {
|
|
121
127
|
return []
|
package/src/generate/gen-expr.js
CHANGED
|
@@ -387,6 +387,12 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
387
387
|
}
|
|
388
388
|
return generateAllocateAvailableCall(callExpr, ctx)
|
|
389
389
|
|
|
390
|
+
case '_ALLOCATE_BY_PRIORITY':
|
|
391
|
+
if (ctx.outFormat === 'js') {
|
|
392
|
+
throw new Error(`${callExpr.fnName} function not yet implemented for JS code gen`)
|
|
393
|
+
}
|
|
394
|
+
return generateAllocateByPriorityCall(callExpr, ctx)
|
|
395
|
+
|
|
390
396
|
case '_ELMCOUNT':
|
|
391
397
|
case '_SIZE': {
|
|
392
398
|
// Emit the size of the dimension in place of the dimension name. Note that Vensim uses
|
|
@@ -400,8 +406,11 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
case '_GET_DIRECT_CONSTANTS':
|
|
409
|
+
case '_GET_XLS_CONSTANTS':
|
|
403
410
|
case '_GET_DIRECT_DATA':
|
|
411
|
+
case '_GET_XLS_DATA':
|
|
404
412
|
case '_GET_DIRECT_LOOKUPS':
|
|
413
|
+
case '_GET_XLS_LOOKUPS':
|
|
405
414
|
// These functions are handled at a higher level, so we should not get here
|
|
406
415
|
throw new Error(`Unexpected function '${fnId}' in code gen for '${ctx.variable.modelLHS}'`)
|
|
407
416
|
|
|
@@ -941,6 +950,88 @@ function generateAllocateAvailableCall(callExpr, ctx) {
|
|
|
941
950
|
return `${tmpVarId}[${allocDimId}[${allocLoopIndexVar}]]`
|
|
942
951
|
}
|
|
943
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Generate C/JS code for an `ALLOCATE BY PRIORITY` function call.
|
|
955
|
+
*
|
|
956
|
+
* @param {*} callExpr The function call expression from the parsed model.
|
|
957
|
+
* @param {GenExprContext} ctx The context used when generating code for the expression.
|
|
958
|
+
* @return {string} The generated C/JS code.
|
|
959
|
+
*/
|
|
960
|
+
function generateAllocateByPriorityCall(callExpr, ctx) {
|
|
961
|
+
function validateArg(index, name) {
|
|
962
|
+
const arg = callExpr.args[index]
|
|
963
|
+
if (arg.kind === 'variable-ref') {
|
|
964
|
+
return arg
|
|
965
|
+
} else {
|
|
966
|
+
throw new Error(`ALLOCATE BY PRIORITY argument '${name}' must be a variable reference`)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Given a C/JS variable reference string (e.g., '_var[i][j]'), return that
|
|
971
|
+
// string without the last N array index parts
|
|
972
|
+
function cVarRefWithoutLastIndices(arg, count) {
|
|
973
|
+
const varRef = ctx.cVarRef(arg)
|
|
974
|
+
const origIndexParts = Model.splitRefId(varRef).subscripts
|
|
975
|
+
if (origIndexParts < count) {
|
|
976
|
+
throw new Error(`ALLOCATE BY PRIORITY argument '${arg}' should have at least ${count} subscripts`)
|
|
977
|
+
}
|
|
978
|
+
const newIndexParts = origIndexParts.slice(0, -count)
|
|
979
|
+
if (newIndexParts.length > 0) {
|
|
980
|
+
return `${arg.varId}${newIndexParts.map(x => `[${x}]`).join('')}`
|
|
981
|
+
} else {
|
|
982
|
+
return arg.varId
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Process the request argument. Only include subscripts up until the last one;
|
|
987
|
+
// the implementation function will iterate over the requesters array.
|
|
988
|
+
const reqArg = validateArg(0, 'req')
|
|
989
|
+
const reqRef = cVarRefWithoutLastIndices(reqArg, 1)
|
|
990
|
+
|
|
991
|
+
// Process the priority argument. Only include subscripts up until the
|
|
992
|
+
// last one; the implementation function will iterate over the priorities
|
|
993
|
+
// array.
|
|
994
|
+
const priorityArg = validateArg(1, 'priority')
|
|
995
|
+
const priorityRef = cVarRefWithoutLastIndices(priorityArg, 1)
|
|
996
|
+
|
|
997
|
+
// Process the size argument
|
|
998
|
+
const sizeArg = generateExpr(callExpr.args[2], ctx)
|
|
999
|
+
|
|
1000
|
+
// Process the width argument
|
|
1001
|
+
const widthArg = generateExpr(callExpr.args[3], ctx)
|
|
1002
|
+
|
|
1003
|
+
// Process the supply argument
|
|
1004
|
+
const supplyArg = generateExpr(callExpr.args[4], ctx)
|
|
1005
|
+
|
|
1006
|
+
// The `ALLOCATE BY PRIORITY` function iterates over the last subscript in its first
|
|
1007
|
+
// argument, allocating the available quantity according to the priority values given
|
|
1008
|
+
// in the second argument. The `readEquation` process will have already verified that
|
|
1009
|
+
// the last dimension of both arguments matches the last dimension of the LHS.
|
|
1010
|
+
const allocDimId = reqArg.subscriptRefs[reqArg.subscriptRefs.length - 1].subId
|
|
1011
|
+
const allocLoopIndexVar = ctx.loopIndexVars.index(allocDimId)
|
|
1012
|
+
|
|
1013
|
+
// Generate the code that is emitted before the entire block (before any loops are opened)
|
|
1014
|
+
const tmpVarId = newTmpVarName()
|
|
1015
|
+
const numRequesters = sub(allocDimId).size
|
|
1016
|
+
switch (ctx.outFormat) {
|
|
1017
|
+
case 'c':
|
|
1018
|
+
ctx.emitPreInnerLoop(
|
|
1019
|
+
` double* ${tmpVarId} = _ALLOCATE_BY_PRIORITY(${reqRef}, ${priorityRef}, ${sizeArg}, ${widthArg}, ${supplyArg}, ${numRequesters});`
|
|
1020
|
+
)
|
|
1021
|
+
break
|
|
1022
|
+
case 'js':
|
|
1023
|
+
ctx.emitPreInnerLoop(
|
|
1024
|
+
` let ${tmpVarId} = fns.ALLOCATE_BY_PRIORITY(${reqRef}, ${priorityRef}, ${sizeArg}, ${widthArg}, ${supplyArg}, ${numRequesters});`
|
|
1025
|
+
)
|
|
1026
|
+
break
|
|
1027
|
+
default:
|
|
1028
|
+
throw new Error(`Unhandled output format '${ctx.outFormat}'`)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Generate the RHS expression used in the inner loop
|
|
1032
|
+
return `${tmpVarId}[${allocDimId}[${allocLoopIndexVar}]]`
|
|
1033
|
+
}
|
|
1034
|
+
|
|
944
1035
|
/**
|
|
945
1036
|
* Recursively traverse the given expression and call the function when visiting a variable ref.
|
|
946
1037
|
*
|
|
@@ -970,6 +1061,7 @@ function visitVariableRefs(expr, onVarRef) {
|
|
|
970
1061
|
break
|
|
971
1062
|
|
|
972
1063
|
case 'lookup-call':
|
|
1064
|
+
visitVariableRefs(expr.varRef, onVarRef)
|
|
973
1065
|
visitVariableRefs(expr.arg, onVarRef)
|
|
974
1066
|
break
|
|
975
1067
|
|
|
@@ -89,14 +89,21 @@ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell,
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
// Read time/value pairs, matching Vensim's behavior:
|
|
93
|
+
// - Stop reading when the first non-numeric time value is encountered. This
|
|
94
|
+
// allows additional content (e.g., labels) to follow the data in the row or column.
|
|
95
|
+
// - Skip pairs with a non-numeric data value, but continue reading subsequent pairs.
|
|
96
|
+
while (true) {
|
|
97
|
+
const timeValue = getCellValue(timeCol, timeRow)
|
|
98
|
+
if (timeValue == null) {
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
const dataValue = getCellValue(dataCol, dataRow)
|
|
102
|
+
if (dataValue != null) {
|
|
103
|
+
lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
|
|
104
|
+
lookupSize++
|
|
105
|
+
}
|
|
97
106
|
nextCell()
|
|
98
|
-
dataValue = getCellValue(dataCol, dataRow)
|
|
99
|
-
timeValue = getCellValue(timeCol, timeRow)
|
|
100
107
|
}
|
|
101
108
|
if (lookupSize === 0) {
|
|
102
109
|
throw new Error(`Empty lookup data array for ${varLhs}`)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { indexNamesForSubscript } from '../_shared/subscript.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given the array of LHS subscript/dimension IDs (already mapped to correspond to the
|
|
5
|
+
* RHS positions) and a set of RHS variable instances, return the refIds of the RHS
|
|
6
|
+
* instances whose subscript combinations overlap with the LHS combinations at every
|
|
7
|
+
* position.
|
|
8
|
+
*
|
|
9
|
+
* Conceptually, this is equivalent to checking whether any combination in the LHS
|
|
10
|
+
* cartesian product matches any combination in a given RHS instance's cartesian
|
|
11
|
+
* product. But because positions in a cartesian product are independent, we only
|
|
12
|
+
* need to check that each position has at least one index in common between the
|
|
13
|
+
* LHS and RHS index sets. This reduces the complexity of the check from
|
|
14
|
+
* O(product of dimension sizes) to O(sum of dimension sizes).
|
|
15
|
+
*
|
|
16
|
+
* For example, suppose DimA={A1,A2} and DimB={B1,B2}, the LHS accesses `[DimA,DimB]`,
|
|
17
|
+
* and we want to check whether it overlaps with a RHS variable instance `_x[_dima,_b1]`.
|
|
18
|
+
* The full cartesian products look like this:
|
|
19
|
+
* LHS combos: { (A1,B1), (A1,B2), (A2,B1), (A2,B2) }
|
|
20
|
+
* RHS combos: { (A1,B1), (A2,B1) }
|
|
21
|
+
* The two sets share (A1,B1) and (A2,B1), so there is a match. But we don't need
|
|
22
|
+
* to enumerate either set — we can check each position independently:
|
|
23
|
+
* position 0: LHS {A1,A2} ∩ RHS {A1,A2} = {A1,A2} (non-empty)
|
|
24
|
+
* position 1: LHS {B1,B2} ∩ RHS {B1} = {B1} (non-empty)
|
|
25
|
+
* Every position has at least one index in common, so we know a full-combo match
|
|
26
|
+
* must exist (pick any shared index at each position, e.g., (A2,B1), and it is in
|
|
27
|
+
* both products). Conversely, if any position has an empty intersection, no full
|
|
28
|
+
* combo can match — for example, if instead the LHS accessed `[A1,DimB]` (a specific
|
|
29
|
+
* index at position 0) and the RHS instance were `_x[_a2,_dimb]`, position 0 would
|
|
30
|
+
* give LHS {A1} ∩ RHS {A2} = ∅ and we could stop immediately.
|
|
31
|
+
*
|
|
32
|
+
* @param {string[]} mappedLhsSubIds The array of LHS subscript/dimension IDs at each
|
|
33
|
+
* position, mapped to correspond to the RHS variable reference positions.
|
|
34
|
+
* @param {{ subscripts: string[], refId: string }[]} rhsVarInstances The array of RHS
|
|
35
|
+
* variable instances to filter.
|
|
36
|
+
* @returns {string[]} A sorted array of refIds for the RHS instances whose subscripts
|
|
37
|
+
* overlap with the LHS at every position.
|
|
38
|
+
*/
|
|
39
|
+
export function matchingRhsRefIds(mappedLhsSubIds, rhsVarInstances) {
|
|
40
|
+
// Build a Set of LHS index names for each position for quick lookup
|
|
41
|
+
const lhsIndexSets = mappedLhsSubIds.map(id => new Set(indexNamesForSubscript(id)))
|
|
42
|
+
|
|
43
|
+
// For each RHS variable instance, check if there is overlap at every subscript
|
|
44
|
+
// position between the LHS and RHS index sets
|
|
45
|
+
const rhsRefIds = []
|
|
46
|
+
for (const rhsVarInstance of rhsVarInstances) {
|
|
47
|
+
let matches = true
|
|
48
|
+
for (let i = 0; i < rhsVarInstance.subscripts.length; i++) {
|
|
49
|
+
const rhsIndices = indexNamesForSubscript(rhsVarInstance.subscripts[i])
|
|
50
|
+
let hasOverlap = false
|
|
51
|
+
for (const id of rhsIndices) {
|
|
52
|
+
if (lhsIndexSets[i].has(id)) {
|
|
53
|
+
hasOverlap = true
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!hasOverlap) {
|
|
58
|
+
matches = false
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (matches) {
|
|
63
|
+
rhsRefIds.push(rhsVarInstance.refId)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return the sorted array of relevant refIds
|
|
68
|
+
// TODO: Sorting is not essential here, but the legacy reader sorted so we will keep
|
|
69
|
+
// that behavior now to avoid invalidating tests. Later we should remove this `sort`
|
|
70
|
+
// call and update the tests accordingly.
|
|
71
|
+
return rhsRefIds.sort()
|
|
72
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseVensimModel } from '@sdeverywhere/parse'
|
|
2
2
|
|
|
3
|
-
import { canonicalName,
|
|
3
|
+
import { canonicalName, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
|
|
4
4
|
|
|
5
|
-
import { hasMapping,
|
|
5
|
+
import { hasMapping, isDimension, isIndex, sub } from '../_shared/subscript.js'
|
|
6
6
|
|
|
7
7
|
import Model from './model.js'
|
|
8
8
|
import { generateDelayVariables } from './read-equation-fn-delay.js'
|
|
@@ -11,6 +11,7 @@ import { generateNpvVariables } from './read-equation-fn-npv.js'
|
|
|
11
11
|
import { generateSmoothVariables } from './read-equation-fn-smooth.js'
|
|
12
12
|
import { generateTrendVariables } from './read-equation-fn-trend.js'
|
|
13
13
|
import { generateLookup } from './read-equation-fn-with-lookup.js'
|
|
14
|
+
import { matchingRhsRefIds } from './read-equations-expand.js'
|
|
14
15
|
import { readVariables } from './read-variables.js'
|
|
15
16
|
|
|
16
17
|
class Context {
|
|
@@ -480,6 +481,11 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
480
481
|
validateCallArgs(callExpr, 3)
|
|
481
482
|
break
|
|
482
483
|
|
|
484
|
+
case '_ALLOCATE_BY_PRIORITY':
|
|
485
|
+
validateCallDepth(callExpr, context)
|
|
486
|
+
validateCallArgs(callExpr, 5)
|
|
487
|
+
break
|
|
488
|
+
|
|
483
489
|
case '_DELAY1':
|
|
484
490
|
case '_DELAY1I':
|
|
485
491
|
case '_DELAY3':
|
|
@@ -522,7 +528,8 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
522
528
|
generateGameVariables(v, callExpr, context)
|
|
523
529
|
break
|
|
524
530
|
|
|
525
|
-
case '_GET_DIRECT_CONSTANTS':
|
|
531
|
+
case '_GET_DIRECT_CONSTANTS':
|
|
532
|
+
case '_GET_XLS_CONSTANTS':
|
|
526
533
|
validateCallDepth(callExpr, context)
|
|
527
534
|
validateCallArgs(callExpr, 3)
|
|
528
535
|
validateCallArgType(callExpr, 0, 'string')
|
|
@@ -536,10 +543,11 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
536
543
|
startCell: callExpr.args[2].text
|
|
537
544
|
}
|
|
538
545
|
break
|
|
539
|
-
}
|
|
540
546
|
|
|
541
547
|
case '_GET_DIRECT_DATA':
|
|
548
|
+
case '_GET_XLS_DATA':
|
|
542
549
|
case '_GET_DIRECT_LOOKUPS':
|
|
550
|
+
case '_GET_XLS_LOOKUPS':
|
|
543
551
|
validateCallDepth(callExpr, context)
|
|
544
552
|
validateCallArgs(callExpr, 4)
|
|
545
553
|
validateCallArgType(callExpr, 0, 'string')
|
|
@@ -877,6 +885,9 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
877
885
|
}
|
|
878
886
|
}
|
|
879
887
|
continue
|
|
888
|
+
} else if (callExpr.fnId === '_ALLOCATE_BY_PRIORITY') {
|
|
889
|
+
// TODO: Throw an error if the last dimension of arg0 does not match last dimension of LHS
|
|
890
|
+
// TODO: Throw an error if the last dimension of arg1 does not match last dimension of LHS
|
|
880
891
|
}
|
|
881
892
|
|
|
882
893
|
context.setArgIndex(index, argModes[index])
|
|
@@ -967,21 +978,26 @@ function expandedRefIdsForVar(lhsVariable, rhsBaseRefId, rhsSubIds) {
|
|
|
967
978
|
// it must be non-apply-to-all. The goal now is to determine which instances (refIds) are
|
|
968
979
|
// relevant for the given `lhsVariable` context.
|
|
969
980
|
//
|
|
970
|
-
// First,
|
|
971
|
-
//
|
|
981
|
+
// First, determine the set of LHS subscript indices accessed at each position of the RHS
|
|
982
|
+
// variable reference. For example:
|
|
972
983
|
// y[DimA,DimB,DimC] :EXCEPT: [DimA,DimB,C1] = x[DimA,DimC,DimB]
|
|
973
984
|
// In this case the `DimC` on the RHS is only "accessed" by `C2` from the LHS, so we would
|
|
974
|
-
// build
|
|
975
|
-
// _a1,
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
// _a2,_c2,_b2
|
|
985
|
+
// build a per-position set of accessed indices, like this:
|
|
986
|
+
// position 0 (DimA on RHS): { _a1, _a2 }
|
|
987
|
+
// position 1 (DimC on RHS): { _c2 }
|
|
988
|
+
// position 2 (DimB on RHS): { _b1, _b2 }
|
|
979
989
|
//
|
|
980
|
-
// Then, for each RHS variable instance
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
990
|
+
// Then, for each RHS variable instance, check whether every subscript position has at
|
|
991
|
+
// least one index in common between the LHS set and the indices that the RHS instance
|
|
992
|
+
// accepts at that position. If so, add the RHS `refId` to the array of variables
|
|
993
|
+
// referenced by the LHS.
|
|
994
|
+
//
|
|
995
|
+
// Conceptually this is equivalent to checking whether any combination in the LHS
|
|
996
|
+
// cartesian product matches any combination in the RHS cartesian product, but we can
|
|
997
|
+
// avoid computing the cartesian products explicitly because positions in a cartesian
|
|
998
|
+
// product are independent: if every position has at least one element in common, then
|
|
999
|
+
// there exists a full combination that matches. This reduces the complexity from
|
|
1000
|
+
// O(product of dimension sizes) to O(sum of dimension sizes).
|
|
985
1001
|
//
|
|
986
1002
|
// In the following examples, suppose the referenced RHS variable is non-apply-to-all and
|
|
987
1003
|
// has two instances:
|
|
@@ -1012,43 +1028,18 @@ function expandedRefIdsForVar(lhsVariable, rhsBaseRefId, rhsSubIds) {
|
|
|
1012
1028
|
// _x[_dima,_c2,_dimb]
|
|
1013
1029
|
//
|
|
1014
1030
|
|
|
1015
|
-
// Step 1:
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
// are relevant for the RHS subscripts/dimensions given the context of the LHS variable
|
|
1019
|
-
// may have been separated/expanded).
|
|
1031
|
+
// Step 1: Resolve the LHS subscript/dimension at each position of the RHS variable
|
|
1032
|
+
// reference. Here `rhsSubIds` is the array of parsed subscript/dimension IDs that
|
|
1033
|
+
// appear in the RHS variable reference. We figure out which LHS subscripts/dimensions
|
|
1034
|
+
// are relevant for the RHS subscripts/dimensions given the context of the LHS variable
|
|
1035
|
+
// (which may have been separated/expanded).
|
|
1020
1036
|
const lhsSubRefs = lhsVariable.parsedEqn.lhs.varDef.subscriptRefs
|
|
1021
1037
|
const lhsSubIds = lhsSubRefs?.map(subRef => subRef.subId) || []
|
|
1022
1038
|
const mappedLhsSubIds = rhsSubIds.map(rhsSubId => resolveRhsSubOrDim(lhsVariable, lhsSubIds, rhsSubId))
|
|
1023
1039
|
|
|
1024
|
-
// Step 2:
|
|
1025
|
-
//
|
|
1026
|
-
|
|
1027
|
-
const mappedLhsCombos = cartesianProductOf(mappedLhsSubIdsPerPosition).map(combo => combo.join(','))
|
|
1028
|
-
|
|
1029
|
-
// Step 3: For each RHS variable instance, get all combinations of RHS subscripts that can
|
|
1030
|
-
// be accepted by that particular RHS instance
|
|
1031
|
-
const rhsRefIds = []
|
|
1032
|
-
for (const rhsVarInstance of rhsVarInstances) {
|
|
1033
|
-
// Build RHS subscript combos (one string of comma-separated subscript IDs for each combo)
|
|
1034
|
-
const rhsVarInstanceSubIdsPerPosition = rhsVarInstance.subscripts.map(indexNamesForSubscript)
|
|
1035
|
-
const rhsCombos = cartesianProductOf(rhsVarInstanceSubIdsPerPosition).map(combo => combo.join(','))
|
|
1036
|
-
|
|
1037
|
-
// See if any of the LHS subscript combos match any of the RHS subscript combos
|
|
1038
|
-
for (const lhsCombo of mappedLhsCombos) {
|
|
1039
|
-
if (rhsCombos.includes(lhsCombo)) {
|
|
1040
|
-
// There was a match; add the refId and break out of the inner loop
|
|
1041
|
-
rhsRefIds.push(rhsVarInstance.refId)
|
|
1042
|
-
break
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Return the sorted array of relevant refIds
|
|
1048
|
-
// TODO: Sorting is not essential here, but the legacy reader sorted so we will keep that
|
|
1049
|
-
// behavior now to avoid invalidating tests. Later we should remove this `sort` call and
|
|
1050
|
-
// update the tests accordingly.
|
|
1051
|
-
return rhsRefIds.sort()
|
|
1040
|
+
// Step 2: Find the RHS variable instances whose subscripts overlap with the LHS
|
|
1041
|
+
// subscripts at every position
|
|
1042
|
+
return matchingRhsRefIds(mappedLhsSubIds, rhsVarInstances)
|
|
1052
1043
|
}
|
|
1053
1044
|
|
|
1054
1045
|
/**
|
|
@@ -197,7 +197,13 @@ function subscriptPositionsToExpand(subIds, exceptSubIdSets, separationDims, rhs
|
|
|
197
197
|
|
|
198
198
|
if (!expand) {
|
|
199
199
|
// Direct data vars with subscripts are separated because we generate a lookup for each index
|
|
200
|
-
if (
|
|
200
|
+
if (
|
|
201
|
+
isDimension(subId) &&
|
|
202
|
+
(rhsText.includes('GET DIRECT DATA') ||
|
|
203
|
+
rhsText.includes('GET XLS DATA') ||
|
|
204
|
+
rhsText.includes('GET DIRECT LOOKUPS') ||
|
|
205
|
+
rhsText.includes('GET XLS LOOKUPS'))
|
|
206
|
+
) {
|
|
201
207
|
expand = true
|
|
202
208
|
}
|
|
203
209
|
}
|
|
@@ -32,8 +32,8 @@ import { generateCode } from './generate/gen-code.js'
|
|
|
32
32
|
* @param {string[]} operations The set of operations to perform; can include 'generateC', 'generateJS',
|
|
33
33
|
* 'printVarList', 'printRefIdTest', 'convertNames'. If the array is empty, the model will be
|
|
34
34
|
* read but no operation will be performed.
|
|
35
|
-
* @param {string} modelDirname The absolute path to the directory containing
|
|
36
|
-
*
|
|
35
|
+
* @param {string} modelDirname The absolute path to the directory containing data (dat, xlsx, csv)
|
|
36
|
+
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
37
37
|
* @param {string} modelName The model name (without the mdl extension).
|
|
38
38
|
* @param {string} buildDir The output directory where the C or list files will be written.
|
|
39
39
|
* @param {string} [varname] The variable name passed to the 'sde causes' command.
|
|
@@ -134,8 +134,8 @@ export function printNames(namesPathname, operation) {
|
|
|
134
134
|
*
|
|
135
135
|
* @param {string} input The string containing the model text.
|
|
136
136
|
* @param {string} modelKind The kind of model to parse, either 'vensim' or 'xmile'.
|
|
137
|
-
* @param {string} modelDir The absolute path to the directory containing
|
|
138
|
-
*
|
|
137
|
+
* @param {string} modelDir The absolute path to the directory containing data (dat, xlsx, csv)
|
|
138
|
+
* files that are referenced by the model. These files will be resolved relative to this directory.
|
|
139
139
|
* @param {Object} [options] The options that control parsing.
|
|
140
140
|
* @param {boolean} options.sort Whether to sort definitions alphabetically in the preprocess step.
|
|
141
141
|
* @return {*} A parsed tree representation of the model.
|
|
@@ -147,7 +147,7 @@ export function parseModel(input, modelKind, modelDir, options) {
|
|
|
147
147
|
if (modelDir) {
|
|
148
148
|
parseContext = {
|
|
149
149
|
getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell /*, prefix*/) {
|
|
150
|
-
// Resolve the CSV file relative the model directory
|
|
150
|
+
// Resolve the CSV file relative to the model directory
|
|
151
151
|
const csvPath = path.resolve(modelDir, fileName)
|
|
152
152
|
|
|
153
153
|
// Read the subscripts from the CSV file
|