@sdeverywhere/compile 0.7.31 → 0.7.33
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/README.md +2 -2
- package/package.json +6 -3
- package/src/_shared/helpers.js +11 -4
- package/src/_shared/model-spec.js +174 -0
- package/src/_shared/normalize-model-spec.js +32 -0
- package/src/_shared/read-dat.js +10 -3
- package/src/_shared/xlsx.js +60 -45
- package/src/generate/gen-code.js +10 -12
- package/src/generate/gen-equation.js +5 -1
- package/src/generate/gen-expr.js +171 -17
- package/src/index.js +20 -5
- package/src/model/analyze-cycles.js +208 -0
- package/src/model/model.js +73 -1
- package/src/model/read-equations.js +27 -8
- package/src/model/read-variables.js +19 -14
- package/src/model/toposort.js +83 -1
- package/src/parse-and-generate.js +50 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sdeverywhere/compile
|
|
2
2
|
|
|
3
|
-
This package contains the core [SDEverywhere](https://github.com/climateinteractive/SDEverywhere) compiler that takes a Vensim model as input and generates C code as output.
|
|
3
|
+
This package contains the core [SDEverywhere](https://github.com/climateinteractive/SDEverywhere) compiler that takes a Vensim or Stella model as input and generates JavaScript or C code as output.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ More usage details will be included here at a later time when the interfaces sta
|
|
|
32
32
|
## Documentation
|
|
33
33
|
|
|
34
34
|
The `compile` package is currently treated as an implementation detail of the `cli` package.
|
|
35
|
-
As such, there is no
|
|
35
|
+
As such, there is no generated API documentation at this time, but we hope to expose a public API once the interfaces stabilize.
|
|
36
36
|
|
|
37
37
|
## License
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.33",
|
|
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
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"dependencies": {
|
|
8
|
-
"@sdeverywhere/parse": "^0.1.
|
|
9
|
+
"@sdeverywhere/parse": "^0.1.6",
|
|
9
10
|
"byline": "^5.0.0",
|
|
10
11
|
"csv-parse": "^5.3.3",
|
|
11
12
|
"fflate": "^0.8.3",
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
"url": "https://github.com/climateinteractive/SDEverywhere/issues"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"build": "tsup",
|
|
27
30
|
"lint": "eslint . --max-warnings 0",
|
|
28
31
|
"prettier:check": "prettier --check .",
|
|
29
32
|
"prettier:fix": "prettier --write .",
|
|
@@ -32,6 +35,6 @@
|
|
|
32
35
|
"test": "vitest run",
|
|
33
36
|
"test:watch": "vitest --hideSkippedTests",
|
|
34
37
|
"test:ci": "vitest run",
|
|
35
|
-
"ci:build": "run-s lint prettier:check type-check test:ci"
|
|
38
|
+
"ci:build": "run-s clean lint prettier:check type-check build test:ci"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/_shared/helpers.js
CHANGED
|
@@ -37,11 +37,18 @@ export function resetHelperState() {
|
|
|
37
37
|
resetXlsxCache()
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Format a model variable or subscript/dimension name into a valid C identifier.
|
|
42
|
+
*
|
|
43
|
+
* In the case where you have a full variable name that includes subscripts/dimensions
|
|
44
|
+
* (e.g., 'Variable name[DimA,B2]'), use `canonicalVensimName` to convert the base
|
|
45
|
+
* variable name and subscript/dimension parts to canonical form indepdendently.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('./model-spec.js').VarName} name The variable name as used in the
|
|
48
|
+
* modeling tool.
|
|
49
|
+
* @return {import('./model-spec.js').VarId} The canonical variable identifier.
|
|
50
|
+
*/
|
|
40
51
|
export let canonicalName = name => {
|
|
41
|
-
// Format a model variable or subscript/dimension name into a valid C identifier.
|
|
42
|
-
// In the case where you have a full variable name that includes subscripts/dimensions
|
|
43
|
-
// (e.g., 'Variable name[DimA,B2]'), use `canonicalVensimName` to convert the
|
|
44
|
-
// base variable name and subscript/dimension parts to canonical form indepdendently.
|
|
45
52
|
return canonicalId(name)
|
|
46
53
|
}
|
|
47
54
|
export let decanonicalize = name => {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A variable name as used in the modeling tool, for example `Some Var[DimA]` as used in
|
|
5
|
+
* a Vensim model.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {string} VarName
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A variable identifier in the canonical format used internally by SDEverywhere, for
|
|
12
|
+
* example `_some_var`. These are derived from a `VarName` by lowercasing the name and
|
|
13
|
+
* replacing special characters with underscores.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {string} VarId
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A dimension (subscript range) identifier in the canonical format used internally by
|
|
20
|
+
* SDEverywhere, for example `_dima`.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {string} DimId
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Describes a `dat` file that provides data for exogenous data variables in the model.
|
|
27
|
+
*
|
|
28
|
+
* This can either be:
|
|
29
|
+
* - a plain file name (relative to the model directory), for example `data.dat`, or
|
|
30
|
+
* - an object with a single key/value pair, where the key is a prefix that is prepended
|
|
31
|
+
* to each variable name read from the file, and the value is the file name, for
|
|
32
|
+
* example `{ "prefix ": "data.dat" }`.
|
|
33
|
+
*
|
|
34
|
+
* @typedef {string | { [varNamePrefix: string]: string }} DatFileSpec
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Describes a model (e.g., a Vensim mdl file) and the input/output variables that should
|
|
39
|
+
* be included in the model generated by SDEverywhere.
|
|
40
|
+
*
|
|
41
|
+
* This is the type of the object that is parsed from a `spec.json` file (as passed to the
|
|
42
|
+
* `sde generate` command using the `--spec` argument) and that is accepted by the
|
|
43
|
+
* `parseAndGenerate` function.
|
|
44
|
+
*
|
|
45
|
+
* All properties are optional. If neither `inputVarNames` nor `outputVarNames` is
|
|
46
|
+
* provided, the generated model will include all variables from the model and will not
|
|
47
|
+
* allow any inputs to be set at runtime.
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object} ModelSpec
|
|
50
|
+
*
|
|
51
|
+
* @property {VarName[]} [inputVarNames] The input variables for the model, using the
|
|
52
|
+
* variable names as they appear in the modeling tool.
|
|
53
|
+
*
|
|
54
|
+
* When this is provided, only the listed variables can be set at runtime, and any
|
|
55
|
+
* variables that are not needed to compute the configured `outputVarNames` will be pruned
|
|
56
|
+
* from the generated model.
|
|
57
|
+
*
|
|
58
|
+
* @property {VarName[]} [outputVarNames] The output variables for the model, using the
|
|
59
|
+
* variable names as they appear in the modeling tool.
|
|
60
|
+
*
|
|
61
|
+
* It is customary to include `Time` as the first output variable.
|
|
62
|
+
*
|
|
63
|
+
* When this is provided, only the listed variables (plus the variables needed to compute
|
|
64
|
+
* them) will be included in the generated model.
|
|
65
|
+
*
|
|
66
|
+
* @property {DatFileSpec[]} [datFiles] The `dat` files that provide the data for exogenous
|
|
67
|
+
* data variables in the model.
|
|
68
|
+
*
|
|
69
|
+
* Each entry is resolved relative to the model directory (i.e., the directory that is
|
|
70
|
+
* passed using the `--datadir` argument, which defaults to the directory that contains the
|
|
71
|
+
* model file).
|
|
72
|
+
*
|
|
73
|
+
* @property {{ [dataTag: string]: string }} [directData] The mapping of data tag to
|
|
74
|
+
* Excel workbook file name, used to resolve the data for `GET DIRECT DATA`,
|
|
75
|
+
* `GET DIRECT CONSTANTS`, and `GET DIRECT LOOKUPS` calls in the model.
|
|
76
|
+
*
|
|
77
|
+
* Each key is the tag that appears in the model equation (for example, `?data`), and each
|
|
78
|
+
* value is the name of an `xlsx` file that is resolved relative to the model directory.
|
|
79
|
+
*
|
|
80
|
+
* @property {{ [dimName: string]: string }} [dimensionFamilies] The mapping of dimension
|
|
81
|
+
* name to family name, used when SDEverywhere cannot infer the family for a dimension
|
|
82
|
+
* from the model alone.
|
|
83
|
+
*
|
|
84
|
+
* Both the keys and the values use the dimension names as they appear in the modeling tool
|
|
85
|
+
* (they are converted to canonical form when the spec file is read).
|
|
86
|
+
*
|
|
87
|
+
* @property {{ [varId: VarId]: DimId | DimId[] }} [specialSeparationDims] The mapping of
|
|
88
|
+
* variable identifier to the dimension(s) on which that variable should be separated
|
|
89
|
+
* into one variable instance per subscript.
|
|
90
|
+
*
|
|
91
|
+
* Separating a variable is sometimes necessary to break a dependency cycle that would
|
|
92
|
+
* otherwise prevent the model from being evaluated. Each value can be either a single
|
|
93
|
+
* dimension identifier or an array of dimension identifiers.
|
|
94
|
+
*
|
|
95
|
+
* @property {(DimId | DimId[])[]} [separateAllVarsWithDims] The dimensions for which all
|
|
96
|
+
* variables should be separated into one variable instance per subscript.
|
|
97
|
+
*
|
|
98
|
+
* This is a convenience alternative to `specialSeparationDims` that avoids the need to
|
|
99
|
+
* list each affected variable. Each entry can be either a single dimension identifier or
|
|
100
|
+
* an array of dimension identifiers; a variable is separated only if every dimension in
|
|
101
|
+
* the entry appears on the left-hand side of its equation.
|
|
102
|
+
*
|
|
103
|
+
* @property {boolean} [bundleListing] Whether to bundle a model listing with the generated
|
|
104
|
+
* model.
|
|
105
|
+
*
|
|
106
|
+
* If undefined, defaults to false.
|
|
107
|
+
*
|
|
108
|
+
* When this is true, a model listing will be bundled with the generated model to allow the
|
|
109
|
+
* `runtime` package to resolve variables that are referenced by name or identifier. This
|
|
110
|
+
* listing will increase the size of the generated model, so it is recommended to set this
|
|
111
|
+
* to true only if it is needed.
|
|
112
|
+
*
|
|
113
|
+
* @property {boolean | VarName[]} [customConstants] Whether to allow constants to be
|
|
114
|
+
* overridden at runtime using `setConstant`.
|
|
115
|
+
*
|
|
116
|
+
* If undefined or false, the generated model will implement `setConstant` as a no-op,
|
|
117
|
+
* meaning that constants cannot be overridden at runtime.
|
|
118
|
+
*
|
|
119
|
+
* If true, all constants in the generated model will be available to be overridden.
|
|
120
|
+
*
|
|
121
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
122
|
+
* overridden.
|
|
123
|
+
*
|
|
124
|
+
* @property {boolean | VarName[]} [customLookups] Whether to allow lookups to be
|
|
125
|
+
* overridden at runtime using `setLookup`.
|
|
126
|
+
*
|
|
127
|
+
* If undefined or false, the generated model will implement `setLookup` as a no-op,
|
|
128
|
+
* meaning that lookups cannot be overridden at runtime.
|
|
129
|
+
*
|
|
130
|
+
* If true, all lookups in the generated model will be available to be overridden.
|
|
131
|
+
*
|
|
132
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
133
|
+
* overridden.
|
|
134
|
+
*
|
|
135
|
+
* @property {boolean | VarName[]} [customOutputs] Whether to allow for capturing the data
|
|
136
|
+
* for arbitrary variables at runtime (including variables that are not configured in the
|
|
137
|
+
* `outputVarNames` array).
|
|
138
|
+
*
|
|
139
|
+
* If undefined or false, the generated model will implement `storeOutput` as a no-op,
|
|
140
|
+
* meaning that the data for arbitrary variables cannot be captured at runtime.
|
|
141
|
+
*
|
|
142
|
+
* If true, all variables in the generated model will be available to be captured at
|
|
143
|
+
* runtime.
|
|
144
|
+
*
|
|
145
|
+
* If an array is provided, only those variable names in the array will be available to be
|
|
146
|
+
* captured at runtime.
|
|
147
|
+
*
|
|
148
|
+
* @property {DatFileSpec[]} [externalDatfiles] The `dat` files that provide the data for
|
|
149
|
+
* exogenous data variables in the model.
|
|
150
|
+
*
|
|
151
|
+
* DEPRECATED: Use `datFiles` instead. This property is still honored (but is ignored if
|
|
152
|
+
* `datFiles` is also provided) and will be removed in a future release.
|
|
153
|
+
*
|
|
154
|
+
* @property {string} [name] An optional descriptive name for the model.
|
|
155
|
+
*
|
|
156
|
+
* This is not currently used by SDEverywhere, but is allowed (and is included in many
|
|
157
|
+
* existing `spec.json` files) as a way to document what the model is.
|
|
158
|
+
*
|
|
159
|
+
* @property {VarId[]} [inputVars] The input variable identifiers for the model, in
|
|
160
|
+
* canonical form.
|
|
161
|
+
*
|
|
162
|
+
* This is derived from `inputVarNames` while the model is being read, and is not intended
|
|
163
|
+
* to be set in a `spec.json` file.
|
|
164
|
+
*
|
|
165
|
+
* @property {VarId[]} [outputVars] The output variable identifiers for the model, in
|
|
166
|
+
* canonical form.
|
|
167
|
+
*
|
|
168
|
+
* This is derived from `outputVarNames` while the model is being read, and is not intended
|
|
169
|
+
* to be set in a `spec.json` file.
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
// Note that this module only declares types (as JSDoc typedefs), so this empty export
|
|
173
|
+
// is needed to make it a module (otherwise the typedefs would be declared globally).
|
|
174
|
+
export {}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize the given model spec so that the rest of the compile package only needs
|
|
5
|
+
* to work with the preferred property names.
|
|
6
|
+
*
|
|
7
|
+
* Some `spec.json` properties have been renamed over time. For each renamed property,
|
|
8
|
+
* this copies the value from the deprecated property to the preferred one, unless the
|
|
9
|
+
* preferred property is already defined (in which case the preferred one wins). The
|
|
10
|
+
* deprecated properties are left in place so that the spec object is unchanged from
|
|
11
|
+
* the caller's point of view.
|
|
12
|
+
*
|
|
13
|
+
* Note that the given spec object is modified in place (and returned for convenience),
|
|
14
|
+
* which is consistent with how the spec object is treated elsewhere in this package.
|
|
15
|
+
* This function is idempotent, so it is safe to call it more than once on the same spec.
|
|
16
|
+
*
|
|
17
|
+
* @template {import('./model-spec.js').ModelSpec | undefined} T
|
|
18
|
+
* @param {T} spec The model spec to normalize, or undefined.
|
|
19
|
+
* @return {T} The same spec object that was provided.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeModelSpec(spec) {
|
|
22
|
+
if (spec === undefined) {
|
|
23
|
+
return spec
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The `externalDatfiles` property was renamed to `datFiles`
|
|
27
|
+
if (spec.datFiles === undefined && spec.externalDatfiles !== undefined) {
|
|
28
|
+
spec.datFiles = spec.externalDatfiles
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return spec
|
|
32
|
+
}
|
package/src/_shared/read-dat.js
CHANGED
|
@@ -5,14 +5,21 @@ import * as R from 'ramda'
|
|
|
5
5
|
import B from './bufx.js'
|
|
6
6
|
import { canonicalVensimName } from './helpers.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The datasets read from external `dat` files, keyed by variable identifier. Each
|
|
10
|
+
* dataset is a map of time to value.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Map<import('./model-spec.js').VarId, Map<number, number>>} ExtData
|
|
13
|
+
*/
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Read a Vensim `dat` file with static data and return a Map.
|
|
10
17
|
* Each dataset consists of a key (the variable name in the canonical
|
|
11
18
|
* format used by SDE) and a map of time/value pairs.
|
|
12
19
|
*
|
|
13
|
-
* @param pathname The absolute path to the dat file.
|
|
14
|
-
* @param prefix An optional prefix string prepended to var names.
|
|
15
|
-
* @return A Map containing the datasets.
|
|
20
|
+
* @param {string} pathname The absolute path to the dat file.
|
|
21
|
+
* @param {string} [prefix] An optional prefix string prepended to var names.
|
|
22
|
+
* @return {Promise<ExtData>} A promise that resolves with a Map containing the datasets.
|
|
16
23
|
*/
|
|
17
24
|
export async function readDat(pathname, prefix = '') {
|
|
18
25
|
let log = new Map()
|
package/src/_shared/xlsx.js
CHANGED
|
@@ -278,10 +278,22 @@ function getAttr(attrs, name) {
|
|
|
278
278
|
return end < 0 ? undefined : attrs.slice(start, end)
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Normalize line endings the way a conformant XML parser (and SheetJS) does:
|
|
283
|
+
* `\r\n` and lone `\r` both become `\n`. Applied after entity decoding so a
|
|
284
|
+
* CR encoded as ` ` is normalized too.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} s The text to normalize.
|
|
287
|
+
* @returns The normalized string.
|
|
288
|
+
*/
|
|
289
|
+
function normalizeEol(s) {
|
|
290
|
+
return s.indexOf('\r') === -1 ? s : s.replace(/\r\n?/g, '\n')
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
/**
|
|
282
294
|
* Decode the standard XML entities (`<`, `>`, `&`, `"`,
|
|
283
295
|
* `'`) along with numeric character references (`&#NN;` and `&#xNN;`)
|
|
284
|
-
* in the given text
|
|
296
|
+
* in the given text, and normalize line endings to `\n`.
|
|
285
297
|
*
|
|
286
298
|
* @param {string} s The raw text from an XML element body or attribute.
|
|
287
299
|
* @returns The decoded string.
|
|
@@ -289,20 +301,22 @@ function getAttr(attrs, name) {
|
|
|
289
301
|
function decodeXmlText(s) {
|
|
290
302
|
// Fast path: most cell text contains no entities, so skip the regex chain
|
|
291
303
|
if (s.indexOf('&') === -1) {
|
|
292
|
-
return s
|
|
304
|
+
return normalizeEol(s)
|
|
293
305
|
}
|
|
294
306
|
|
|
295
307
|
// Decode the named entities, then decimal and hex numeric refs, and finally
|
|
296
308
|
// `&` — leaving `&` last avoids accidentally producing `<` etc.
|
|
297
309
|
// from a literal `&lt;` in the source
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
return normalizeEol(
|
|
311
|
+
s
|
|
312
|
+
.replace(/</g, '<')
|
|
313
|
+
.replace(/>/g, '>')
|
|
314
|
+
.replace(/"/g, '"')
|
|
315
|
+
.replace(/'/g, "'")
|
|
316
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
|
|
317
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
|
|
318
|
+
.replace(/&/g, '&')
|
|
319
|
+
)
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
//
|
|
@@ -392,6 +406,19 @@ function parseWorkbookRels(xml) {
|
|
|
392
406
|
return rels
|
|
393
407
|
}
|
|
394
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Extract the text content of the `<v>` element in a cell body, tolerating
|
|
411
|
+
* attributes on the tag (e.g. `<v xml:space="preserve">`). Returns undefined
|
|
412
|
+
* when there is no `<v>` element (e.g. an uncalculated formula cell).
|
|
413
|
+
*
|
|
414
|
+
* @param {string} body The inner XML of a `<c>` element.
|
|
415
|
+
* @returns The raw text between `<v...>` and `</v>`, or undefined.
|
|
416
|
+
*/
|
|
417
|
+
function getVText(body) {
|
|
418
|
+
const m = /<v\b[^>]*>([\s\S]*?)<\/v>/.exec(body)
|
|
419
|
+
return m ? m[1] : undefined
|
|
420
|
+
}
|
|
421
|
+
|
|
395
422
|
/**
|
|
396
423
|
* Scan a worksheet's XML and build a sparse cell map shaped like the SheetJS
|
|
397
424
|
* worksheet object: `{ [cellRef]: { v }, '!ref': 'A1:Z99' }`. Skips empty
|
|
@@ -429,16 +456,7 @@ function parseSheetXml(xml, sharedStrings) {
|
|
|
429
456
|
const t = getAttr(attrs, 't')
|
|
430
457
|
|
|
431
458
|
let value
|
|
432
|
-
if (t === '
|
|
433
|
-
// Shared string: <v>N</v> where N indexes sharedStrings
|
|
434
|
-
const vStart = body.indexOf('<v>')
|
|
435
|
-
if (vStart < 0) {
|
|
436
|
-
continue
|
|
437
|
-
}
|
|
438
|
-
const vEnd = body.indexOf('</v>', vStart + 3)
|
|
439
|
-
const idx = parseInt(body.slice(vStart + 3, vEnd), 10)
|
|
440
|
-
value = sharedStrings[idx]
|
|
441
|
-
} else if (t === 'inlineStr') {
|
|
459
|
+
if (t === 'inlineStr') {
|
|
442
460
|
// Inline string: <is><t>...</t></is>
|
|
443
461
|
const tStart = body.indexOf('<t')
|
|
444
462
|
if (tStart < 0) {
|
|
@@ -447,38 +465,35 @@ function parseSheetXml(xml, sharedStrings) {
|
|
|
447
465
|
const tOpenEnd = body.indexOf('>', tStart)
|
|
448
466
|
const tEnd = body.indexOf('</t>', tOpenEnd)
|
|
449
467
|
value = decodeXmlText(body.slice(tOpenEnd + 1, tEnd))
|
|
450
|
-
} else if (t === 'str') {
|
|
451
|
-
// Formula result as string: <v>...</v>
|
|
452
|
-
const vStart = body.indexOf('<v>')
|
|
453
|
-
if (vStart < 0) {
|
|
454
|
-
continue
|
|
455
|
-
}
|
|
456
|
-
const vEnd = body.indexOf('</v>', vStart + 3)
|
|
457
|
-
value = decodeXmlText(body.slice(vStart + 3, vEnd))
|
|
458
|
-
} else if (t === 'b') {
|
|
459
|
-
// Boolean: <v>0</v> or <v>1</v>
|
|
460
|
-
const vStart = body.indexOf('<v>')
|
|
461
|
-
if (vStart < 0) {
|
|
462
|
-
continue
|
|
463
|
-
}
|
|
464
|
-
value = body.charCodeAt(vStart + 3) === 49 // '1'
|
|
465
468
|
} else if (t === 'e') {
|
|
466
469
|
// Error cell, skip
|
|
467
470
|
continue
|
|
468
471
|
} else {
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
// skip the cell so the
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
// The remaining cell types carry their value in a <v> element, which
|
|
473
|
+
// may have attributes (e.g. <v xml:space="preserve">). If <v> is
|
|
474
|
+
// missing (e.g. an uncalculated formula), skip the cell so the
|
|
475
|
+
// caller's missing-cell handling kicks in.
|
|
476
|
+
const vText = getVText(body)
|
|
477
|
+
if (vText === undefined) {
|
|
474
478
|
continue
|
|
475
479
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
+
if (t === 's') {
|
|
481
|
+
// Shared string: <v>N</v> where N indexes sharedStrings
|
|
482
|
+
value = sharedStrings[parseInt(vText, 10)]
|
|
483
|
+
} else if (t === 'str') {
|
|
484
|
+
// Formula result as string: <v>...</v>
|
|
485
|
+
value = decodeXmlText(vText)
|
|
486
|
+
} else if (t === 'b') {
|
|
487
|
+
// Boolean: <v>0</v> or <v>1</v>
|
|
488
|
+
value = vText.charCodeAt(0) === 49 // '1'
|
|
489
|
+
} else {
|
|
490
|
+
// Numeric (t === 'n' or absent)
|
|
491
|
+
const num = +vText
|
|
492
|
+
if (Number.isNaN(num)) {
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
value = num
|
|
480
496
|
}
|
|
481
|
-
value = num
|
|
482
497
|
}
|
|
483
498
|
|
|
484
499
|
// Store the cell under its A1 ref, matching the SheetJS sheet shape
|
package/src/generate/gen-code.js
CHANGED
|
@@ -4,23 +4,21 @@ import { generateJS } from './gen-code-js.js'
|
|
|
4
4
|
/**
|
|
5
5
|
* Generate code from the given parsed model.
|
|
6
6
|
*
|
|
7
|
-
* @param {
|
|
7
|
+
* @param {import('../parse-and-generate.js').ParsedModel} parsedModel The parsed model structure.
|
|
8
8
|
* @param {Object} opts The options that control code generation.
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* analysis will be available.
|
|
16
|
-
* @param {Map<string, any>} opts.extData The map of datasets from external `.dat` files.
|
|
17
|
-
* @param {Map<string, any>} opts.directData The mapping of dataset name used in a
|
|
9
|
+
* @param {import('../_shared/model-spec.js').ModelSpec} opts.spec The parsed `spec.json` object.
|
|
10
|
+
* @param {import('../parse-and-generate.js').GenerateOperation[]} opts.operations The array
|
|
11
|
+
* of operations to perform.
|
|
12
|
+
* @param {import('../_shared/read-dat.js').ExtData} [opts.extData] The map of datasets from
|
|
13
|
+
* external `.dat` files.
|
|
14
|
+
* @param {Map<string, any>} [opts.directData] The mapping of dataset name used in a
|
|
18
15
|
* `GET DIRECT DATA` call (e.g., `?data`) to the tabular data contained in the loaded
|
|
19
16
|
* data file.
|
|
20
|
-
* @param {string} opts.modelDirname The absolute path to the directory containing data
|
|
17
|
+
* @param {string} [opts.modelDirname] The absolute path to the directory containing data
|
|
21
18
|
* (dat, xlsx, csv) files that are referenced by the model. This path is used for
|
|
22
19
|
* resolving data files for `GET DIRECT SUBSCRIPT` calls.
|
|
23
|
-
* @
|
|
20
|
+
* @param {string} [opts.varname] The variable name passed to the `sde causes` command.
|
|
21
|
+
* @returns {string} A string containing the generated code.
|
|
24
22
|
*/
|
|
25
23
|
export function generateCode(parsedModel, opts) {
|
|
26
24
|
// Note that the two `generate` functions perform the same steps (other than the
|
|
@@ -128,6 +128,9 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Keep a buffer of code that will be included before all subscript loops
|
|
132
|
+
const preLoopLines = []
|
|
133
|
+
|
|
131
134
|
// Keep a buffer of code that will be included before the innermost loop
|
|
132
135
|
const preInnerLoopLines = []
|
|
133
136
|
|
|
@@ -145,6 +148,7 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
|
|
|
145
148
|
cLhs,
|
|
146
149
|
loopIndexVars,
|
|
147
150
|
arrayIndexVars,
|
|
151
|
+
emitPreLoop: s => preLoopLines.push(s),
|
|
148
152
|
emitPreInnerLoop: s => preInnerLoopLines.push(s),
|
|
149
153
|
emitPreFormula: s => preFormulaLines.push(s),
|
|
150
154
|
emitPostFormula: s => postFormulaLines.push(s),
|
|
@@ -161,7 +165,7 @@ export function generateEquation(variable, mode, extData, directData, modelDir,
|
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
// Combine all lines of comments and code into a single array
|
|
164
|
-
return [comment, ...openLoops, ...preFormulaLines, formula, ...postFormulaLines, ...closeLoops]
|
|
168
|
+
return [comment, ...preLoopLines, ...openLoops, ...preFormulaLines, formula, ...postFormulaLines, ...closeLoops]
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
/**
|