@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@sdeverywhere/compile",
3
- "version": "0.7.28",
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.3",
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",
@@ -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 exception.
130
+ // A string that cannot be converted throws an error.
131
131
  export let cdbl = x => {
132
132
  function throwError() {
133
- throw new Error(`ERROR: cannot convert "${x}" to a number`)
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
- return cell != null ? cdbl(cell.v) : null
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
- let value = '0.0'
84
+ if (data[r] == null || data[r][c] == null || data[r][c] === '') {
85
+ return null
86
+ }
76
87
  try {
77
- value = data[r] != null && data[r][c] != null ? cdbl(data[r][c]) : null
78
- } catch (error) {
79
- console.error(`${error.message} in ${dataPathname}`)
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`)
@@ -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 the model
21
- * (used for resolving data files for `GET DIRECT SUBSCRIPT`).
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 []
@@ -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
- let timeValue = getCellValue(timeCol, timeRow)
93
- let dataValue = getCellValue(dataCol, dataRow)
94
- while (timeValue != null && dataValue != null) {
95
- lookupData = listConcat(lookupData, `${timeValue}, ${dataValue}`, true)
96
- lookupSize++
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, cartesianProductOf, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
3
+ import { canonicalName, newDepreciationVarName, newFixedDelayVarName } from '../_shared/helpers.js'
4
4
 
5
- import { hasMapping, indexNamesForSubscript, isDimension, isIndex, sub } from '../_shared/subscript.js'
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, get all combinations of the LHS subscripts that map to the subscripts/dimensions
971
- // in the RHS variable reference. For example:
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 an array of strings representing the possible subset of combinations, like this:
975
- // _a1,_c2,_b1
976
- // _a1,_c2,_b2
977
- // _a2,_c2,_b1
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
- // - get all combinations of RHS subscripts that can be accepted by that RHS instance
982
- // (build an array of strings, e.g., ['_a1,_c1,_b1', '_a1,_c1,_b1', ...])
983
- // - see if any of the LHS subscript combos match any of the RHS subscript combos; if
984
- // so, then add the RHS `refId` to the array of variables referenced by the LHS
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: Get all combinations of the LHS subscripts that map to the subscripts/dimensions
1016
- // in the RHS variable reference. Here `rhsSubIds` is the array of parsed subscript/dimension
1017
- // IDs that appear in the RHS variable reference. We figure out which LHS subscripts/dimensions
1018
- // are relevant for the RHS subscripts/dimensions given the context of the LHS variable (which
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: Build an array of mapped LHS subscript combos (one string of comma-separated
1025
- // subscript IDs for each combo)
1026
- const mappedLhsSubIdsPerPosition = mappedLhsSubIds.map(indexNamesForSubscript)
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 (isDimension(subId) && (rhsText.includes('GET DIRECT DATA') || rhsText.includes('GET DIRECT LOOKUPS'))) {
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 the mdl file.
36
- * The dat and xlsx files referenced by the spec will be relative to this directory.
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 the mdl file.
138
- * The dat, xlsx, and csv files referenced by the model will be relative to this directory.
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