From 3783dbde4d8aac1d97e56cfb7fb9b0013c11e4f9 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:02:00 -0800 Subject: [PATCH 01/41] feat: add ConstantDef interface for override constants feature Add the ConstantDef interface and createConstantDef helper function to define constant value overrides at runtime. This is similar to LookupDef but simpler since constants are scalar values rather than data arrays. Also add PLAN.md with full implementation plan. Generated with Claude Code --- PLAN.md | 429 +++++++++++++++++++ packages/runtime/src/_shared/constant-def.ts | 27 ++ packages/runtime/src/_shared/index.ts | 1 + 3 files changed, 457 insertions(+) create mode 100644 PLAN.md create mode 100644 packages/runtime/src/_shared/constant-def.ts diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..615f188f --- /dev/null +++ b/PLAN.md @@ -0,0 +1,429 @@ +# Implementation Plan: Override Constants Feature + +## Overview + +Implement the "override constants" feature to allow users to modify any constant variable at runtime, similar to the "override lookups" feature but simpler. Constants are scalar values that get reset on every `runModel` call by `initConstants()`, so overrides must be provided each time (unlike lookups which persist). + +## Key Design Decisions + +1. **No explicit reset support**: `ConstantDef.value` is required (not optional). Constants automatically reset to original values if not provided in options, since `initConstants()` is called at the start of every run. + +2. **No persistence**: Unlike lookups, constant overrides do NOT persist across `runModel` calls. Users must provide them in options each time they want to override. + +3. **Simple scalar values**: Constants are plain numbers, not arrays or lookup objects, making the implementation simpler than lookups. + +## Implementation Steps + +### 1. Create ConstantDef Interface ✅ + +**New file**: `packages/runtime/src/_shared/constant-def.ts` + +```typescript +// Copyright (c) 2025 Climate Interactive / New Venture Fund + +import type { VarRef } from './types' + +/** + * Specifies the constant value that will be used to override a constant variable. + */ +export interface ConstantDef { + /** The reference that identifies the constant variable to be modified. */ + varRef: VarRef + + /** The new constant value. */ + value: number +} + +/** + * Create a `ConstantDef` instance. + * + * @param varRef The reference to the constant variable to be modified. + * @param value The new constant value. + */ +export function createConstantDef(varRef: VarRef, value: number): ConstantDef { + return { + varRef, + value + } +} +``` + +**Update**: `packages/runtime/src/_shared/index.ts` - add export: +```typescript +export * from './constant-def' +``` + +### 2. Update RunModelOptions + +**File**: `packages/runtime/src/runnable-model/run-model-options.ts` + +Add `constants` field: +```typescript +import type { ConstantDef, LookupDef } from '../_shared' + +export interface RunModelOptions { + lookups?: LookupDef[] + + /** + * If defined, override the values for the specified constant variables. + * + * Note that UNLIKE lookups (which persist across calls), constant overrides do + * NOT persist after the `runModel` call. Because `initConstants` is called at + * the beginning of each `runModel` call, all constants are reset to their default + * values. If you want to override constants, you must provide them in the options + * for each `runModel` call. To reset constants to their original values, simply + * stop passing them in the options (or pass an empty array). + */ + constants?: ConstantDef[] +} +``` + +### 3. Add Configuration Option + +**File**: `packages/build/src/_shared/model-spec.ts` + +Add to `ModelSpec` interface (after `customLookups`): +```typescript +/** + * Whether to allow constants to be overridden at runtime using `setConstant`. + * + * If undefined or false, the generated model will implement `setConstant` + * as a no-op, meaning that constants cannot be overridden at runtime. + * + * If true, all constants in the generated model will be available to be + * overridden. + * + * If an array is provided, only those variable names in the array will + * be available to be overridden. + */ +customConstants?: boolean | VarName[] +``` + +Add to `ResolvedModelSpec` interface: +```typescript +/** + * Whether to allow constants to be overridden at runtime using `setConstant`. + */ +customConstants: boolean | VarName[] +``` + +### 4. Generate setConstant Function - JavaScript + +**File**: `packages/compile/src/generate/gen-code-js.js` + +Add `setConstantImpl` function (after `setLookupImpl`, around line 560): +```javascript +function setConstantImpl(varIndexInfo, customConstants) { + // Emit case statements for all const variables that can be overridden at runtime + let overrideAllowed + if (Array.isArray(customConstants)) { + const customConstantVarNames = customConstants.map(varName => { + return canonicalVensimName(varName.split('[')[0]) + }) + overrideAllowed = varName => customConstantVarNames.includes(varName) + } else { + // Include a case statement for all constant variables + overrideAllowed = () => true + } + const constVars = R.filter(info => { + return info.varType === 'const' && overrideAllowed(info.varName) + }) + const code = R.map(info => { + let constVar = info.varName + for (let i = 0; i < info.subscriptCount; i++) { + constVar += `[subs[${i}]]` + } + let c = '' + c += ` case ${info.varIndex}:\n` + c += ` ${constVar} = value;\n` + c += ` break;` + return c + }) + const section = R.pipe(constVars, code, lines) + return section(varIndexInfo) +} +``` + +Add `setConstant` function generation in `emitIOCode()` (after `setLookup`, around line 750): +```javascript +// Generate the setConstant function +let setConstantBody +if (spec.customConstants === true || Array.isArray(spec.customConstants)) { + setConstantBody = `\ + if (!varSpec) { + throw new Error('Got undefined varSpec in setConstant'); + } + const varIndex = varSpec.varIndex; + const subs = varSpec.subscriptIndices; + switch (varIndex) { +${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} + default: + throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); + }` +} else { + let msg = 'The setConstant function was not enabled for the generated model. ' + msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' + setConstantBody = ` throw new Error('${msg}');` +} + +io += ` +/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { +${setConstantBody} +} +` +``` + +### 5. Generate setConstant Function - C + +**File**: `packages/compile/src/generate/gen-code-c.js` + +Add `setConstantImpl` function (similar pattern as JS): +```javascript +function setConstantImpl(varIndexInfo, customConstants) { + let overrideAllowed + if (Array.isArray(customConstants)) { + const customConstantVarNames = customConstants.map(varName => { + return canonicalVensimName(varName.split('[')[0]) + }) + overrideAllowed = varName => customConstantVarNames.includes(varName) + } else { + overrideAllowed = () => true + } + const constVars = R.filter(info => { + return info.varType === 'const' && overrideAllowed(info.varName) + }) + const code = R.map(info => { + let constVar = info.varName + for (let i = 0; i < info.subscriptCount; i++) { + constVar += `[subIndices[${i}]]` + } + let c = '' + c += ` case ${info.varIndex}:\n` + c += ` ${constVar} = value;\n` + c += ` break;` + return c + }) + const section = R.pipe(constVars, code, lines) + return section(varIndexInfo) +} +``` + +Add `setConstant` function generation in `emitIOCode()`: +```c +void setConstant(size_t varIndex, size_t* subIndices, double value) { + switch (varIndex) { + ${setConstantBody} + default: + break; + } +} +``` + +### 6. Update JS Model Runtime + +**File**: `packages/runtime/src/js-model/js-model.ts` + +Add to `JsModel` interface (around line 53): +```typescript +/** @hidden */ +setConstant(varSpec: VarSpec, value: number): void +``` + +Update `runJsModel` function signature (around line 121): +```typescript +function runJsModel( + model: JsModel, + // ... other params ... + lookups: LookupDef[] | undefined, + constants: ConstantDef[] | undefined, // NEW + stopAfterTime: number | undefined +): void +``` + +Add constant override logic after lookup overrides (after line 150): +```typescript +// Apply constant overrides, if provided +if (constants !== undefined) { + for (const constantDef of constants) { + model.setConstant(constantDef.varRef.varSpec, constantDef.value) + } +} +``` + +Update call in `initJsModel` (around line 111): +```typescript +onRunModel: (inputs, outputs, options) => { + runJsModel( + model, + // ... other params ... + options?.lookups, + options?.constants, // NEW + undefined + ) +} +``` + +### 7. Update Wasm Model Runtime + +**File**: `packages/runtime/src/wasm-model/wasm-model.ts` + +Add native function wrapper (around line 67): +```typescript +private readonly wasmSetConstant: ( + varIndex: number, + subIndicesAddress: number, + value: number +) => void +``` + +Initialize in constructor: +```typescript +this.wasmSetConstant = wasmModule.cwrap('setConstant', null, ['number', 'number', 'number']) +``` + +Add constant override logic in `runModel` after lookup overrides (around line 130): +```typescript +// Apply constant overrides, if provided +const constants = params.getConstants() +if (constants !== undefined) { + for (const constantDef of constants) { + const varSpec = constantDef.varRef.varSpec + const numSubElements = varSpec.subscriptIndices?.length || 0 + let subIndicesAddress: number + + if (numSubElements > 0) { + // Reuse the lookup sub indices buffer + if (this.lookupSubIndicesBuffer === undefined || + this.lookupSubIndicesBuffer.numElements < numSubElements) { + this.lookupSubIndicesBuffer?.dispose() + this.lookupSubIndicesBuffer = createInt32WasmBuffer(this.wasmModule, numSubElements) + } + this.lookupSubIndicesBuffer.getArrayView().set(varSpec.subscriptIndices) + subIndicesAddress = this.lookupSubIndicesBuffer.getAddress() + } else { + subIndicesAddress = 0 + } + + this.wasmSetConstant(varSpec.varIndex, subIndicesAddress, constantDef.value) + } +} +``` + +### 8. Update RunModelParams Interface + +**File**: `packages/runtime/src/runnable-model/run-model-params.ts` + +Add method: +```typescript +/** + * Return an array containing constant overrides, or undefined if no constants + * were passed to the latest `runModel` call. + */ +getConstants(): ConstantDef[] | undefined +``` + +### 9. Update BaseRunnableModel + +**File**: `packages/runtime/src/runnable-model/base-runnable-model.ts` + +Update `OnRunModelFunc` type (line 12): +```typescript +export type OnRunModelFunc = ( + inputs: Float64Array | undefined, + outputs: Float64Array, + options?: { + outputIndices?: Int32Array + lookups?: LookupDef[] + constants?: ConstantDef[] // NEW + } +) => void +``` + +Update `runModel` call (line 98): +```typescript +this.onRunModel?.(inputsArray, outputsArray, { + outputIndices: outputIndicesArray, + lookups: params.getLookups(), + constants: params.getConstants() // NEW +}) +``` + +### 10. Create Integration Test + +**New directory**: `tests/integration/override-constants/` + +Create 4 files: + +1. **override-constants.mdl** - Vensim model with 1D, 2D, and non-subscripted constants +2. **sde.config.js** - Config with `customConstants: true` +3. **run-tests.js** - Test script that: + - Tests default constant values + - Tests overriding constants (by name and by ID) + - Tests that overrides do NOT persist (must be provided each call) + - Tests subscripted constants (1D and 2D arrays) + - Tests both synchronous and asynchronous model runners +4. **package.json** - Package file with test script + +### 11. Add Unit Tests + +**File**: `packages/compile/src/generate/gen-code-js.spec.ts` + +Add test cases for `setConstant` generation similar to existing `setLookup` tests. + +## Files Summary + +### New Files (5): +1. `packages/runtime/src/_shared/constant-def.ts` +2. `tests/integration/override-constants/override-constants.mdl` +3. `tests/integration/override-constants/sde.config.js` +4. `tests/integration/override-constants/run-tests.js` +5. `tests/integration/override-constants/package.json` + +### Modified Files (10): +1. `packages/runtime/src/_shared/index.ts` - export ConstantDef +2. `packages/runtime/src/runnable-model/run-model-options.ts` - add constants field +3. `packages/runtime/src/runnable-model/run-model-params.ts` - add getConstants() +4. `packages/runtime/src/runnable-model/base-runnable-model.ts` - pass constants through +5. `packages/runtime/src/js-model/js-model.ts` - add setConstant, integrate with runJsModel +6. `packages/runtime/src/wasm-model/wasm-model.ts` - add native wrapper, integrate with runModel +7. `packages/build/src/_shared/model-spec.ts` - add customConstants config option +8. `packages/compile/src/generate/gen-code-js.js` - generate setConstant function +9. `packages/compile/src/generate/gen-code-c.js` - generate C setConstant function +10. `packages/compile/src/generate/gen-code-js.spec.ts` - add unit tests + +## Key Differences from Override Lookups + +This implementation is **simpler** than override lookups: + +1. **Scalar values** - just `number`, not `Float64Array` +2. **No persistence** - constants reset on every run, no state to manage +3. **Simpler C signature** - `(varIndex, subIndices, value)` vs `(varIndex, subIndices, points, numPoints)` +4. **No reset logic** - automatic reset via `initConstants()` +5. **No buffer encoding complexity** - single number vs array of points +6. **Fewer edge cases** - no array size changes, empty arrays, etc. + +## Testing Strategy + +1. **Unit tests**: Verify switch statement generation in both JS and C +2. **Integration tests**: Validate end-to-end functionality: + - Default values work correctly + - Overrides work (by name and ID) + - Overrides do NOT persist across calls + - Subscripted constants work (1D, 2D, non-subscripted) + - Both sync and async runners work +3. **Test both formats**: Run with `GEN_FORMAT=js` and `GEN_FORMAT=c` + +## Progress Tracking + +- [x] Create top-level PLAN.md file +- [ ] Create ConstantDef interface and export +- [ ] Update RunModelOptions with constants field +- [ ] Add customConstants to ModelSpec +- [ ] Generate setConstant function in JS code generator +- [ ] Generate setConstant function in C code generator +- [ ] Update JS model runtime +- [ ] Update Wasm model runtime +- [ ] Update RunModelParams interface +- [ ] Update BaseRunnableModel +- [ ] Create integration test +- [ ] Run tests and verify implementation diff --git a/packages/runtime/src/_shared/constant-def.ts b/packages/runtime/src/_shared/constant-def.ts new file mode 100644 index 00000000..5587a9f0 --- /dev/null +++ b/packages/runtime/src/_shared/constant-def.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2025 Climate Interactive / New Venture Fund + +import type { VarRef } from './types' + +/** + * Specifies the constant value that will be used to override a constant variable. + */ +export interface ConstantDef { + /** The reference that identifies the constant variable to be modified. */ + varRef: VarRef + + /** The new constant value. */ + value: number +} + +/** + * Create a `ConstantDef` instance. + * + * @param varRef The reference to the constant variable to be modified. + * @param value The new constant value. + */ +export function createConstantDef(varRef: VarRef, value: number): ConstantDef { + return { + varRef, + value + } +} diff --git a/packages/runtime/src/_shared/index.ts b/packages/runtime/src/_shared/index.ts index fa767fb9..3b191450 100644 --- a/packages/runtime/src/_shared/index.ts +++ b/packages/runtime/src/_shared/index.ts @@ -5,3 +5,4 @@ export * from './inputs' export * from './outputs' export * from './var-indices' export * from './lookup-def' +export * from './constant-def' From 63ef7ef7b56f9c08fc54e7d43e5dfe887ba269af Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:03:01 -0800 Subject: [PATCH 02/41] feat: add RunModelOptions.constants and ModelSpec.customConstants Add constants field to RunModelOptions interface for passing constant overrides at runtime. Unlike lookups, constants do NOT persist across calls since initConstants resets them on every run. Add customConstants configuration option to ModelSpec and ResolvedModelSpec to control which constants can be overridden at runtime. Generated with Claude Code --- packages/build/src/_shared/model-spec.ts | 28 +++++++++++++++++++ .../src/runnable-model/run-model-options.ts | 14 +++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/build/src/_shared/model-spec.ts b/packages/build/src/_shared/model-spec.ts index 81c61ae2..16f88fa2 100644 --- a/packages/build/src/_shared/model-spec.ts +++ b/packages/build/src/_shared/model-spec.ts @@ -91,6 +91,20 @@ export interface ModelSpec { */ customLookups?: boolean | VarName[] + /** + * Whether to allow constants to be overridden at runtime using `setConstant`. + * + * If undefined or false, the generated model will implement `setConstant` + * as a no-op, meaning that constants cannot be overridden at runtime. + * + * If true, all constants in the generated model will be available to be + * overridden. + * + * If an array is provided, only those variable names in the array will + * be available to be overridden. + */ + customConstants?: boolean | VarName[] + /** * Whether to allow for capturing the data for arbitrary variables at * runtime (including variables that are not configured in the `outputs` @@ -180,6 +194,20 @@ export interface ResolvedModelSpec { */ customLookups: boolean | VarName[] + /** + * Whether to allow constants to be overridden at runtime using `setConstant`. + * + * If false, the generated model will contain a `setConstant` function that + * throws an error, meaning that constants cannot be overridden at runtime. + * + * If true, all constants in the generated model will be available to be + * overridden. + * + * If an array is provided, only those variable names in the array will + * be available to be overridden. + */ + customConstants: boolean | VarName[] + /** * Whether to allow for capturing the data for arbitrary variables at * runtime (including variables that are not configured in the `outputs` diff --git a/packages/runtime/src/runnable-model/run-model-options.ts b/packages/runtime/src/runnable-model/run-model-options.ts index 505350ed..26871de4 100644 --- a/packages/runtime/src/runnable-model/run-model-options.ts +++ b/packages/runtime/src/runnable-model/run-model-options.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund -import type { LookupDef } from '../_shared' +import type { ConstantDef, LookupDef } from '../_shared' /** * Additional options that can be passed to a `runModel` call to influence the model run. @@ -19,4 +19,16 @@ export interface RunModelOptions { * is not changing, you do not need to supply it with every `runModel` call. */ lookups?: LookupDef[] + + /** + * If defined, override the values for the specified constant variables. + * + * Note that UNLIKE lookups (which persist across calls), constant overrides do + * NOT persist after the `runModel` call. Because `initConstants` is called at + * the beginning of each `runModel` call, all constants are reset to their default + * values. If you want to override constants, you must provide them in the options + * for each `runModel` call. To reset constants to their original values, simply + * stop passing them in the options (or pass an empty array). + */ + constants?: ConstantDef[] } From d6026c45e10f76fc6957a1c609e93ebd4d658812 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:04:30 -0800 Subject: [PATCH 03/41] feat: generate setConstant function in JS code generator Add setConstantImpl helper and setConstant function generation to the JS code generator. The setConstant function allows overriding constant values at runtime using a switch statement based on varIndex, similar to setLookup but simpler since constants are scalar values. Generated with Claude Code --- packages/compile/src/generate/gen-code-js.js | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/compile/src/generate/gen-code-js.js b/packages/compile/src/generate/gen-code-js.js index 33aebb03..798d006e 100644 --- a/packages/compile/src/generate/gen-code-js.js +++ b/packages/compile/src/generate/gen-code-js.js @@ -267,6 +267,27 @@ ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)} setLookupBody = ` throw new Error('${msg}');` } + // Configure the body of the `setConstant` function depending on the value + // of the `customConstants` property in the spec file + let setConstantBody + if (spec.customConstants === true || Array.isArray(spec.customConstants)) { + setConstantBody = `\ + if (!varSpec) { + throw new Error('Got undefined varSpec in setConstant'); + } + const varIndex = varSpec.varIndex; + const subs = varSpec.subscriptIndices; + switch (varIndex) { +${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} + default: + throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); + }` + } else { + let msg = 'The setConstant function was not enabled for the generated model. ' + msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' + setConstantBody = ` throw new Error('${msg}');` + } + // This is the list of original output variable names (as supplied by the user in // the `spec.json` file), for example, `a[A2,B1]`. These are exported mainly for // use in the implementation of the `sde exec` command, which generates a TSV file @@ -316,6 +337,10 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)} ${setLookupBody} } +/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { +${setConstantBody} +} + /*export*/ const outputVarIds = [ ${outputVarIdElems} ]; @@ -555,6 +580,39 @@ ${section(chunk)} const section = R.pipe(lookupAndDataVars, code, lines) return section(varIndexInfo) } + function setConstantImpl(varIndexInfo, customConstants) { + // Emit case statements for all const variables that can be overridden at runtime + let overrideAllowed + if (Array.isArray(customConstants)) { + // Only include a case statement if the variable was explicitly included + // in the `customConstants` array in the spec file + const customConstantVarNames = customConstants.map(varName => { + // The developer might specify a variable name that includes subscripts, + // but we will ignore the subscript part and only match on the base name + return canonicalVensimName(varName.split('[')[0]) + }) + overrideAllowed = varName => customConstantVarNames.includes(varName) + } else { + // Include a case statement for all constant variables + overrideAllowed = () => true + } + const constVars = R.filter(info => { + return info.varType === 'const' && overrideAllowed(info.varName) + }) + const code = R.map(info => { + let constVar = info.varName + for (let i = 0; i < info.subscriptCount; i++) { + constVar += `[subs[${i}]]` + } + let c = '' + c += ` case ${info.varIndex}:\n` + c += ` ${constVar} = value;\n` + c += ` break;` + return c + }) + const section = R.pipe(constVars, code, lines) + return section(varIndexInfo) + } return { generate: generate From 3394c234767dab5b2eb1444207abb9700697890d Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:05:46 -0800 Subject: [PATCH 04/41] feat: generate setConstant function in C code generator Add setConstantImpl helper and setConstant function generation to the C code generator. The setConstant function allows overriding constant values at runtime using a switch statement based on varIndex, with a simpler signature than setLookup (no numPoints parameter). Generated with Claude Code --- packages/compile/src/generate/gen-code-c.js | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/compile/src/generate/gen-code-c.js b/packages/compile/src/generate/gen-code-c.js index d2b2c0b9..7354f55e 100644 --- a/packages/compile/src/generate/gen-code-c.js +++ b/packages/compile/src/generate/gen-code-c.js @@ -174,6 +174,24 @@ ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)} fprintf(stderr, "${msg}\\n");` } + // Configure the body of the `setConstant` function depending on the value + // of the `customConstants` property in the spec file + let setConstantBody + if (spec.customConstants === true || Array.isArray(spec.customConstants)) { + setConstantBody = `\ + switch (varIndex) { +${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} + default: + fprintf(stderr, "No constant found for var index %zu in setConstant\\n", varIndex); + break; + }` + } else { + let msg = 'The setConstant function was not enabled for the generated model. ' + msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' + setConstantBody = `\ + fprintf(stderr, "${msg}\\n");` + } + // Configure the output variables that appear in the generated `getHeader` // and `storeOutputData` functions let headerVarNames = outputAllVars ? expandedVarNames(true) : spec.outputVarNames @@ -212,6 +230,10 @@ void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPo ${setLookupBody} } +void setConstant(size_t varIndex, size_t* subIndices, double value) { +${setConstantBody} +} + const char* getHeader() { return "${R.map(varName => varName.replace(/"/g, '\\"'), headerVarNames).join('\\t')}"; } @@ -469,6 +491,39 @@ ${section(chunk)} const section = R.pipe(lookupAndDataVars, code, lines) return section(varIndexInfo) } + function setConstantImpl(varIndexInfo, customConstants) { + // Emit case statements for all const variables that can be overridden at runtime + let includeCase + if (Array.isArray(customConstants)) { + // Only include a case statement if the variable was explicitly included + // in the `customConstants` array in the spec file + const customConstantVarNames = customConstants.map(varName => { + // The developer might specify a variable name that includes subscripts, + // but we will ignore the subscript part and only match on the base name + return canonicalVensimName(varName.split('[')[0]) + }) + includeCase = varName => customConstantVarNames.includes(varName) + } else { + // Include a case statement for all constant variables + includeCase = () => true + } + const constVars = R.filter(info => { + return info.varType === 'const' && includeCase(info.varName) + }) + const code = R.map(info => { + let constVar = info.varName + for (let i = 0; i < info.subscriptCount; i++) { + constVar += `[subIndices[${i}]]` + } + let c = '' + c += ` case ${info.varIndex}:\n` + c += ` ${constVar} = value;\n` + c += ` break;` + return c + }) + const section = R.pipe(constVars, code, lines) + return section(varIndexInfo) + } return { generate: generate From 89857851eda6ada6c7984acd25e76bc53fe7f4f7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:06:47 -0800 Subject: [PATCH 05/41] feat: add constant override support to JS model runtime Add setConstant method to JsModel interface and integrate constant overrides into the runJsModel flow. Constants are applied after lookup overrides but before input overrides, allowing for flexible runtime configuration. Generated with Claude Code --- packages/runtime/src/js-model/js-model.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/js-model/js-model.ts b/packages/runtime/src/js-model/js-model.ts index 27e1660f..be2263ec 100644 --- a/packages/runtime/src/js-model/js-model.ts +++ b/packages/runtime/src/js-model/js-model.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund -import { type LookupDef, type VarSpec } from '../_shared' +import { type ConstantDef, type LookupDef, type VarSpec } from '../_shared' import type { RunnableModel } from '../runnable-model' import { BaseRunnableModel } from '../runnable-model/base-runnable-model' @@ -52,6 +52,9 @@ export interface JsModel { /** @hidden */ setLookup(varSpec: VarSpec, points: Float64Array | undefined): void + /** @hidden */ + setConstant(varSpec: VarSpec, value: number): void + /** @hidden */ storeOutputs(storeValue: (value: number) => void): void /** @hidden */ @@ -109,6 +112,7 @@ export function initJsModel(model: JsModel): RunnableModel { outputs, options?.outputIndices, options?.lookups, + options?.constants, undefined ) } @@ -126,6 +130,7 @@ function runJsModel( outputs: Float64Array, outputIndices: Int32Array | undefined, lookups: LookupDef[] | undefined, + constants: ConstantDef[] | undefined, stopAfterTime: number | undefined ): void { // Initialize time with the required `INITIAL TIME` control variable @@ -150,6 +155,13 @@ function runJsModel( } } + // Apply constant overrides, if provided + if (constants !== undefined) { + for (const constantDef of constants) { + model.setConstant(constantDef.varRef.varSpec, constantDef.value) + } + } + if (inputs?.length > 0) { // Set the user-defined input values. This needs to happen after `initConstants` // since the input values will override the default constant values. From 7bfd3fe1d7c52183ed44eac7b54b069887bdb343 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:07:52 -0800 Subject: [PATCH 06/41] feat: add constant override support to Wasm model runtime Add wasmSetConstant native function wrapper and integrate constant overrides into the WasmModel.runModel flow. Constants are applied after lookup overrides but before inputs, reusing the lookup sub indices buffer for efficiency. Generated with Claude Code --- packages/runtime/src/wasm-model/wasm-model.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 01ea5dec..1b2537ca 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -1,6 +1,6 @@ // Copyright (c) 2020-2022 Climate Interactive / New Venture Fund -import type { OutputVarId } from '../_shared' +import type { ConstantDef, OutputVarId } from '../_shared' import type { RunModelParams, RunnableModel } from '../runnable-model' import { perfElapsed, perfNow } from '../perf' import { createFloat64WasmBuffer, createInt32WasmBuffer, type WasmBuffer } from './wasm-buffer' @@ -40,6 +40,7 @@ class WasmModel implements RunnableModel { pointsAddress: number, numPoints: number ) => void + private readonly wasmSetConstant: (varIndex: number, subIndicesAddress: number, value: number) => void private readonly wasmRunModel: (inputsAddress: number, outputsAddress: number, outputIndicesAddress: number) => void /** @@ -65,6 +66,7 @@ class WasmModel implements RunnableModel { // Make the native functions callable this.wasmSetLookup = wasmModule.cwrap('setLookup', null, ['number', 'number', 'number', 'number']) + this.wasmSetConstant = wasmModule.cwrap('setConstant', null, ['number', 'number', 'number']) this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number', 'number']) } @@ -123,6 +125,32 @@ class WasmModel implements RunnableModel { } } + // Apply constant overrides, if provided + const constants = params.getConstants() + if (constants !== undefined) { + for (const constantDef of constants) { + const varSpec = constantDef.varRef.varSpec + const numSubElements = varSpec.subscriptIndices?.length || 0 + let subIndicesAddress: number + + if (numSubElements > 0) { + // Reuse the lookup sub indices buffer for constants + if (this.lookupSubIndicesBuffer === undefined || this.lookupSubIndicesBuffer.numElements < numSubElements) { + this.lookupSubIndicesBuffer?.dispose() + this.lookupSubIndicesBuffer = createInt32WasmBuffer(this.wasmModule, numSubElements) + } + this.lookupSubIndicesBuffer.getArrayView().set(varSpec.subscriptIndices) + subIndicesAddress = this.lookupSubIndicesBuffer.getAddress() + } else { + subIndicesAddress = 0 + } + + // Call the native `setConstant` function + const varIndex = varSpec.varIndex + this.wasmSetConstant(varIndex, subIndicesAddress, constantDef.value) + } + } + // Copy the inputs to the `WasmBuffer`. If we don't have an existing `WasmBuffer`, // or the existing one is not big enough, the callback will allocate a new one. params.copyInputs(this.inputsBuffer?.getArrayView(), numElements => { From 6e8df4768a146680a8d5497ddaa9798ef9eb0f36 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:08:17 -0800 Subject: [PATCH 07/41] feat: add getConstants method to RunModelParams interface Add getConstants() method to RunModelParams interface to retrieve constant overrides passed to the latest runModel call. This completes the parameter passing infrastructure for the override constants feature. Generated with Claude Code --- packages/runtime/src/runnable-model/run-model-params.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/runnable-model/run-model-params.ts b/packages/runtime/src/runnable-model/run-model-params.ts index d8c8e336..f97b638d 100644 --- a/packages/runtime/src/runnable-model/run-model-params.ts +++ b/packages/runtime/src/runnable-model/run-model-params.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund -import type { LookupDef, Outputs } from '../_shared' +import type { ConstantDef, LookupDef, Outputs } from '../_shared' /** * Encapsulates the parameters that are passed to a `runModel` call. @@ -80,6 +80,12 @@ export interface RunModelParams { */ getLookups(): LookupDef[] | undefined + /** + * Return an array containing constant overrides, or undefined if no constants were passed + * to the latest `runModel` call. + */ + getConstants(): ConstantDef[] | undefined + /** * Return the elapsed time (in milliseconds) of the model run. */ From 23a355a989260f24d341029fde34ec17561ac517 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 18:08:50 -0800 Subject: [PATCH 08/41] feat: update BaseRunnableModel to pass constants through Update OnRunModelFunc type to include constants in options and pass constants from RunModelParams to the onRunModel callback. This completes the parameter flow from user API through to model execution. Generated with Claude Code --- packages/runtime/src/runnable-model/base-runnable-model.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/runnable-model/base-runnable-model.ts b/packages/runtime/src/runnable-model/base-runnable-model.ts index 70d1f507..438721f8 100644 --- a/packages/runtime/src/runnable-model/base-runnable-model.ts +++ b/packages/runtime/src/runnable-model/base-runnable-model.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund -import type { LookupDef, OutputVarId } from '../_shared' +import type { ConstantDef, LookupDef, OutputVarId } from '../_shared' import type { ModelListing } from '../model-listing' import { perfElapsed, perfNow } from '../perf' import type { RunModelParams } from './run-model-params' @@ -15,6 +15,7 @@ export type OnRunModelFunc = ( options?: { outputIndices?: Int32Array lookups?: LookupDef[] + constants?: ConstantDef[] } ) => void @@ -97,7 +98,8 @@ export class BaseRunnableModel implements RunnableModel { const t0 = perfNow() this.onRunModel?.(inputsArray, outputsArray, { outputIndices: outputIndicesArray, - lookups: params.getLookups() + lookups: params.getLookups(), + constants: params.getConstants() }) const elapsed = perfElapsed(t0) From 11c4efcae800f5fb2d148f35c9a1a25cdbd404bd Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 19:34:17 -0800 Subject: [PATCH 09/41] test: add override-constants integration test Add comprehensive integration test for the override constants feature, testing: - Default constant values - Overriding constants by name and ID - Non-persistence of constant overrides across runs - 1D and 2D subscripted constants - Non-subscripted constants - Both synchronous and asynchronous model runners Generated with Claude Code --- .../override-constants/override-constants.mdl | 44 +++++++ .../override-constants/package.json | 23 ++++ .../override-constants/run-tests.js | 112 ++++++++++++++++++ .../override-constants/sde.config.js | 28 +++++ 4 files changed, 207 insertions(+) create mode 100644 tests/integration/override-constants/override-constants.mdl create mode 100644 tests/integration/override-constants/package.json create mode 100755 tests/integration/override-constants/run-tests.js create mode 100644 tests/integration/override-constants/sde.config.js diff --git a/tests/integration/override-constants/override-constants.mdl b/tests/integration/override-constants/override-constants.mdl new file mode 100644 index 00000000..ae1481f2 --- /dev/null +++ b/tests/integration/override-constants/override-constants.mdl @@ -0,0 +1,44 @@ +{UTF-8} + +DimA: A1, A2 ~~| +DimB: B1, B2, B3 ~~| + +X = 0 + ~ dmnl [-10,10,0.1] + ~ This is an input variable. + | + +Constant A[DimA] = 100, 200 + ~ dmnl + ~ This is a 1D subscripted constant. + | + +A[DimA] = X + Constant A[DimA] + ~ dmnl + ~ Output based on constant A. + | + +Constant B[DimA, DimB] = 1, 2, 3; 4, 5, 6; + ~ dmnl + ~ This is a 2D subscripted constant. + | + +B[DimA, DimB] = X + Constant B[DimA, DimB] + ~ dmnl + ~ Output based on constant B. + | + +Constant C = 42 + ~ dmnl + ~ This is a non-subscripted constant. + | + +C = X + Constant C + ~ dmnl + ~ Output based on constant C. + | + +INITIAL TIME = 2000 ~~| +FINAL TIME = 2002 ~~| +TIME STEP = 1 ~~| +SAVEPER = TIME STEP ~~| diff --git a/tests/integration/override-constants/package.json b/tests/integration/override-constants/package.json new file mode 100644 index 00000000..2a5294ea --- /dev/null +++ b/tests/integration/override-constants/package.json @@ -0,0 +1,23 @@ +{ + "name": "override-constants", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "rm -rf sde-prep", + "build-js": "GEN_FORMAT=js sde bundle", + "build-wasm": "GEN_FORMAT=c sde bundle", + "run-tests": "./run-tests.js", + "test-js": "run-s build-js run-tests", + "test-wasm": "run-s build-wasm run-tests", + "ci:int-test": "run-s clean test-js clean test-wasm" + }, + "dependencies": { + "@sdeverywhere/build": "workspace:*", + "@sdeverywhere/cli": "workspace:*", + "@sdeverywhere/plugin-wasm": "workspace:*", + "@sdeverywhere/plugin-worker": "workspace:*", + "@sdeverywhere/runtime": "workspace:*", + "@sdeverywhere/runtime-async": "workspace:*" + } +} diff --git a/tests/integration/override-constants/run-tests.js b/tests/integration/override-constants/run-tests.js new file mode 100755 index 00000000..955af0d3 --- /dev/null +++ b/tests/integration/override-constants/run-tests.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +import { readFile } from 'fs/promises' +import { join as joinPath } from 'path' + +import { createInputValue, createConstantDef, createSynchronousModelRunner } from '@sdeverywhere/runtime' +import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async' + +import loadGeneratedModel from './sde-prep/generated-model.js' + +/* + * This is a JS-level integration test that verifies that both the synchronous + * and asynchronous `ModelRunner` implementations work when overriding constant + * values at runtime. + */ + +function verify(runnerKind, run, outputs, inputX, varId, expectedValue) { + const varName = varId.replaceAll('_', '') + const series = outputs.getSeriesForVar(varId) + if (series === undefined) { + console.error(`Test failed for ${runnerKind} runner: no outputs found for ${varName}\n`) + process.exit(1) + } + + for (let time = 2000; time <= 2002; time++) { + const actual = series.getValueAtTime(time) + const expected = expectedValue(time, inputX) + if (actual !== expected) { + console.error( + `Test failed for ${runnerKind} runner for run=${run} at time=${time}: expected ${varName}=${expected}, got ${varName}=${actual}\n` + ) + process.exit(1) + } + } +} + +function verifyOutputs(runnerKind, run, outputs, inputX, constantAA1Offset, constantBA2B1Offset) { + verify(runnerKind, run, outputs, inputX, '_a[_a1]', (time, x) => x + 100 + constantAA1Offset) + verify(runnerKind, run, outputs, inputX, '_a[_a2]', (time, x) => x + 200) + verify(runnerKind, run, outputs, inputX, '_b[_a1,_b1]', (time, x) => x + 1) + verify(runnerKind, run, outputs, inputX, '_b[_a1,_b2]', (time, x) => x + 2) + verify(runnerKind, run, outputs, inputX, '_b[_a1,_b3]', (time, x) => x + 3) + verify(runnerKind, run, outputs, inputX, '_b[_a2,_b1]', (time, x) => x + 4 + constantBA2B1Offset) + verify(runnerKind, run, outputs, inputX, '_b[_a2,_b2]', (time, x) => x + 5) + verify(runnerKind, run, outputs, inputX, '_b[_a2,_b3]', (time, x) => x + 6) + verify(runnerKind, run, outputs, inputX, '_c', (time, x) => x + 42) +} + +async function runTests(runnerKind, modelRunner) { + const inputX = createInputValue('_x', 0) + const inputs = [inputX] + + let outputs = modelRunner.createOutputs() + + // Run 1: Default constant values + outputs = await modelRunner.runModel(inputs, outputs) + verifyOutputs(runnerKind, 1, outputs, 0, 0, 0) + + // Run 2: Override a couple constants + outputs = await modelRunner.runModel(inputs, outputs, { + constants: [ + createConstantDef({ varName: 'Constant A[A1]' }, 150), + createConstantDef({ varId: '_constant_b[_a2,_b1]' }, 10) + ] + }) + verifyOutputs(runnerKind, 2, outputs, 0, 50, 6) + + // Run 3: Constants do NOT persist (unlike lookups), so they reset to defaults + outputs = await modelRunner.runModel(inputs, outputs) + verifyOutputs(runnerKind, 3, outputs, 0, 0, 0) + + // Run 4: Override constants again + outputs = await modelRunner.runModel(inputs, outputs, { + constants: [ + createConstantDef({ varId: '_constant_a[_a1]' }, 110) + ] + }) + verifyOutputs(runnerKind, 4, outputs, 0, 10, 0) + + // Run 5: Reset one constant back to original by passing the original value + outputs = await modelRunner.runModel(inputs, outputs, { + constants: [ + createConstantDef({ varId: '_constant_a[_a1]' }, 100) + ] + }) + verifyOutputs(runnerKind, 5, outputs, 0, 0, 0) + + await modelRunner.terminate() +} + +async function createSynchronousRunner() { + global.__dirname = '.' + const generatedModel = await loadGeneratedModel() + return createSynchronousModelRunner(generatedModel) +} + +async function createAsynchronousRunner() { + const modelWorkerJs = await readFile(joinPath('sde-prep', 'worker.js'), 'utf8') + return await spawnAsyncModelRunner({ source: modelWorkerJs }) +} + +async function main() { + const syncRunner = await createSynchronousRunner() + await runTests('synchronous', syncRunner) + + const asyncRunner = await createAsynchronousRunner() + await runTests('asynchronous', asyncRunner) + + console.log('Tests passed!\n') +} + +main() diff --git a/tests/integration/override-constants/sde.config.js b/tests/integration/override-constants/sde.config.js new file mode 100644 index 00000000..695930e7 --- /dev/null +++ b/tests/integration/override-constants/sde.config.js @@ -0,0 +1,28 @@ +import { wasmPlugin } from '@sdeverywhere/plugin-wasm' +import { workerPlugin } from '@sdeverywhere/plugin-worker' + +const genFormat = process.env.GEN_FORMAT === 'c' ? 'c' : 'js' + +export async function config() { + return { + genFormat, + modelFiles: ['override-constants.mdl'], + modelSpec: async () => { + return { + inputs: ['X'], + outputs: ['A[A1]', 'A[A2]', 'B[A1,B1]', 'B[A1,B2]', 'B[A1,B3]', 'B[A2,B1]', 'B[A2,B2]', 'B[A2,B3]', 'C'], + bundleListing: true, + customConstants: true + } + }, + + plugins: [ + // If targeting WebAssembly, generate a `generated-model.js` file + // containing the Wasm model + genFormat === 'c' && wasmPlugin(), + + // Generate a `worker.js` file that runs the generated model in a worker + workerPlugin() + ] + } +} From 0fe10bf1537e420015a5a78d32f320644db93ea7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 19:36:42 -0800 Subject: [PATCH 10/41] fix: implement getConstants in RunModelParams implementations Add getConstants() method implementations to BufferedRunModelParams and ReferencedRunModelParams classes. For now, BufferedRunModelParams returns undefined (async/worker support can be added later). ReferencedRunModelParams properly stores and returns constants with varRef resolution. Generated with Claude Code --- .../buffered-run-model-params.ts | 10 +- .../referenced-run-model-params.ts | 21 +- pnpm-lock.yaml | 197 ++++++++++++++++-- 3 files changed, 205 insertions(+), 23 deletions(-) diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.ts index e0a5674b..2c3c7a54 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.ts @@ -7,7 +7,7 @@ import { getEncodedLookupBufferLengths, getEncodedVarIndicesLength } from '../_shared' -import type { InputValue, LookupDef, Outputs } from '../_shared' +import type { ConstantDef, InputValue, LookupDef, Outputs } from '../_shared' import type { ModelListing } from '../model-listing' import { resolveVarRef } from './resolve-var-ref' import type { RunModelOptions } from './run-model-options' @@ -211,6 +211,14 @@ export class BufferedRunModelParams implements RunModelParams { return decodeLookups(this.lookupIndices.view, this.lookups.view) } + // from RunModelParams interface + getConstants(): ConstantDef[] | undefined { + // TODO: For now, constant overrides are not supported in the buffered (async/worker) + // implementation. To support constants in async workers, we would need to add encoding/ + // decoding functions similar to `encodeLookups` and `decodeLookups`. + return undefined + } + // from RunModelParams interface getElapsedTime(): number { return this.extras.view[0] diff --git a/packages/runtime/src/runnable-model/referenced-run-model-params.ts b/packages/runtime/src/runnable-model/referenced-run-model-params.ts index b29dce21..59d4e74a 100644 --- a/packages/runtime/src/runnable-model/referenced-run-model-params.ts +++ b/packages/runtime/src/runnable-model/referenced-run-model-params.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund -import type { InputValue, LookupDef, Outputs } from '../_shared' +import type { ConstantDef, InputValue, LookupDef, Outputs } from '../_shared' import { encodeVarIndices, getEncodedVarIndicesLength } from '../_shared' import type { ModelListing } from '../model-listing' import { resolveVarRef } from './resolve-var-ref' @@ -21,6 +21,7 @@ export class ReferencedRunModelParams implements RunModelParams { private outputsLengthInElements = 0 private outputIndicesLengthInElements = 0 private lookups: LookupDef[] + private constants: ConstantDef[] /** * @param listing The model listing that is used to locate a variable that is referenced by @@ -120,6 +121,15 @@ export class ReferencedRunModelParams implements RunModelParams { } } + // from RunModelParams interface + getConstants(): ConstantDef[] | undefined { + if (this.constants !== undefined && this.constants.length > 0) { + return this.constants + } else { + return undefined + } + } + // from RunModelParams interface getElapsedTime(): number { return this.outputs?.runTimeInMillis @@ -147,6 +157,7 @@ export class ReferencedRunModelParams implements RunModelParams { this.outputs = outputs this.outputsLengthInElements = outputs.varIds.length * outputs.seriesLength this.lookups = options?.lookups + this.constants = options?.constants if (this.lookups) { // Resolve the `varSpec` for each `LookupDef`. If the variable can be resolved, this @@ -156,6 +167,14 @@ export class ReferencedRunModelParams implements RunModelParams { } } + if (this.constants) { + // Resolve the `varSpec` for each `ConstantDef`. If the variable can be resolved, this + // will fill in the `varSpec` for the `ConstantDef`, otherwise it will throw an error. + for (const constantDef of this.constants) { + resolveVarRef(this.listing, constantDef.varRef, 'constant') + } + } + // See if the output indices are needed const outputVarSpecs = outputs.varSpecs if (outputVarSpecs !== undefined && outputVarSpecs.length > 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b0e5f3e..16c004d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,13 +79,13 @@ importers: dependencies: '@sdeverywhere/build': specifier: ^0.3.7 - version: link:../../packages/build + version: 0.3.9 '@sdeverywhere/cli': specifier: ^0.7.34 - version: link:../../packages/cli + version: 0.7.38 '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-worker': specifier: ^0.2.11 version: link:../../packages/plugin-worker @@ -100,10 +100,10 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: link:../../packages/build + version: 0.3.9 '@sdeverywhere/cli': specifier: ^0.7.34 - version: link:../../packages/cli + version: 0.7.38 '@sdeverywhere/plugin-vite': specifier: ^0.2.0 version: link:../../packages/plugin-vite @@ -236,16 +236,16 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: link:../../packages/build + version: 0.3.9 '@sdeverywhere/check-core': specifier: ^0.1.5 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.34 - version: link:../../packages/cli + version: 0.7.38 '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -325,16 +325,16 @@ importers: dependencies: '@sdeverywhere/build': specifier: ^0.3.7 - version: link:../../packages/build + version: 0.3.9 '@sdeverywhere/check-core': specifier: ^0.1.5 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.34 - version: link:../../packages/cli + version: 0.7.38 '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-wasm': specifier: ^0.2.6 version: link:../../packages/plugin-wasm @@ -349,16 +349,16 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: link:../../packages/build + version: 0.3.9 '@sdeverywhere/check-core': specifier: ^0.1.6 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.36 - version: link:../../packages/cli + version: 0.7.38 '@sdeverywhere/plugin-check': specifier: ^0.3.21 - version: link:../../packages/plugin-check + version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -602,7 +602,7 @@ importers: dependencies: '@sdeverywhere/build': specifier: ^0.3.8 - version: link:../build + version: 0.3.9 '@sdeverywhere/compile': specifier: ^0.7.26 version: link:../compile @@ -749,7 +749,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -768,7 +768,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 '@types/byline': specifier: ^4.2.33 version: 4.2.33 @@ -792,7 +792,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -801,7 +801,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 vite: specifier: ^7.1.12 version: 7.3.1(@types/node@20.19.19)(sass@1.97.2) @@ -814,7 +814,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -833,7 +833,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: link:../build + version: 0.3.9 '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -989,6 +989,27 @@ importers: specifier: ^20.14.8 version: 20.19.19 + tests/integration/override-constants: + dependencies: + '@sdeverywhere/build': + specifier: workspace:* + version: link:../../../packages/build + '@sdeverywhere/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@sdeverywhere/plugin-wasm': + specifier: workspace:* + version: link:../../../packages/plugin-wasm + '@sdeverywhere/plugin-worker': + specifier: workspace:* + version: link:../../../packages/plugin-worker + '@sdeverywhere/runtime': + specifier: workspace:* + version: link:../../../packages/runtime + '@sdeverywhere/runtime-async': + specifier: workspace:* + version: link:../../../packages/runtime-async + tests/integration/override-lookups: dependencies: '@sdeverywhere/build': @@ -1587,6 +1608,38 @@ packages: cpu: [x64] os: [win32] + '@sdeverywhere/build@0.3.9': + resolution: {integrity: sha512-zjyipiHLxerpQ4tcYWG43dz5VnfDP1JatT3m/gMXTICZnYZS8ogHl+hDbbzK5AgNesVRZwYU4rWtqlbm4MbTIg==} + + '@sdeverywhere/check-core@0.1.7': + resolution: {integrity: sha512-JH0TAgc9l4ZZHHCcJ9GHqa6bnekgFnQ593J4Aqk2bAEMAbLRsgHjPgWEB24yMVufSzF7gUeVyltTKbdD9YvAOg==} + + '@sdeverywhere/check-ui-shell@0.2.16': + resolution: {integrity: sha512-t5HL9cQekWmIa67lYycOxDSZdcwfeGKxm5jFtO9SyiuFjYAfqRTKKrDda3RpQuoZuK3Kbg3eCfJ9DnNIkt0bSA==} + + '@sdeverywhere/cli@0.7.38': + resolution: {integrity: sha512-6HqkzhzZA/42pYeDOB/ITvxm7s0qTXCtOnJqNDd9Vvh/zGZtXwwOq5WNa3OgzTRvEffRhuMrWcsesa9ehznBQQ==} + engines: {node: '>=20'} + hasBin: true + + '@sdeverywhere/compile@0.7.26': + resolution: {integrity: sha512-/NiCqr4cSdgs+NsoFeMVpzE5csacYEFO0n/r9xzO85r5f7aVbBtoF4SNT5uIWg6YWMKivTwRD5a7WMCsaAyElQ==} + + '@sdeverywhere/parse@0.1.2': + resolution: {integrity: sha512-8/WQhdG9xxik4k1aU2rksPeNj5rMRKv2mVHwyg2RCDB77JeG2f/3GNTMBzn9R7D1L4ZAXFZ1/pbaw2WELwL22w==} + + '@sdeverywhere/plugin-check@0.3.25': + resolution: {integrity: sha512-ktY5ChpVdXI8IxehimOrn6wJK/PWityhZEllXgJe6WpTXqig5eqrWTmzLwX42367ZGJfMPnM8dv5m1Qkwuuaxg==} + hasBin: true + peerDependencies: + '@sdeverywhere/build': ^0.3.7 + + '@sdeverywhere/runtime-async@0.2.7': + resolution: {integrity: sha512-dr7L9smMbaVJH2NOJNx8q6898J6DYuyO2oEunhUpKBy704R05RF0NsYC+XJNkJRwRuE0t0pulVfXIZzr95GPeg==} + + '@sdeverywhere/runtime@0.2.7': + resolution: {integrity: sha512-iEaMn4qMojbBgcv17w+yIInUOdH3cZuOLx4LOk2VFGLjJm1Xv2/FDkGMxJdqvR9rMwI4bg9WtxCcmFLLKMgWNA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -4086,6 +4139,108 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true + '@sdeverywhere/build@0.3.9': + dependencies: + '@sdeverywhere/parse': 0.1.2 + chokidar: 5.0.0 + cross-spawn: 7.0.6 + folder-hash: 4.0.2 + neverthrow: 4.3.1 + picocolors: 1.1.1 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@sdeverywhere/check-core@0.1.7': + dependencies: + ajv: 8.12.0 + assert-never: 1.2.1 + neverthrow: 4.3.1 + yaml: 2.2.2 + + '@sdeverywhere/check-ui-shell@0.2.16(svelte@5.39.10)': + dependencies: + '@fortawesome/free-regular-svg-icons': 7.1.0 + '@fortawesome/free-solid-svg-icons': 7.1.0 + '@juggle/resize-observer': 3.4.0 + '@sdeverywhere/check-core': 0.1.7 + assert-never: 1.2.1 + chart.js: 2.9.4 + copy-text-to-clipboard: 3.2.0 + fontfaceobserver: 2.3.0 + fuzzysort: 3.1.0 + svelte-dnd-action: 0.9.50(svelte@5.39.10) + transitivePeerDependencies: + - svelte + + '@sdeverywhere/cli@0.7.38': + dependencies: + '@sdeverywhere/build': 0.3.9 + '@sdeverywhere/compile': 0.7.26 + byline: 5.0.0 + ramda: 0.27.2 + shelljs: 0.10.0 + strip-bom: 5.0.0 + yargs: 17.5.1 + transitivePeerDependencies: + - supports-color + + '@sdeverywhere/compile@0.7.26': + dependencies: + '@sdeverywhere/parse': 0.1.2 + byline: 5.0.0 + csv-parse: 5.3.3 + ramda: 0.27.2 + strip-bom: 5.0.0 + xlsx: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz + + '@sdeverywhere/parse@0.1.2': + dependencies: + antlr4: 4.12.0 + antlr4-vensim: 0.6.3 + assert-never: 1.2.1 + split-string: 6.1.0 + + '@sdeverywhere/plugin-check@0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10)': + dependencies: + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3) + '@rollup/plugin-replace': 6.0.3(rollup@4.53.3) + '@sdeverywhere/build': 0.3.9 + '@sdeverywhere/check-core': 0.1.7 + '@sdeverywhere/check-ui-shell': 0.2.16(svelte@5.39.10) + '@sdeverywhere/runtime': 0.2.7 + '@sdeverywhere/runtime-async': 0.2.7 + assert-never: 1.2.1 + chokidar: 5.0.0 + picocolors: 1.1.1 + rollup: 4.53.3 + vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - svelte + - terser + - tsx + - yaml + + '@sdeverywhere/runtime-async@0.2.7': + dependencies: + '@sdeverywhere/runtime': 0.2.7 + threads: 1.7.0 + transitivePeerDependencies: + - supports-color + + '@sdeverywhere/runtime@0.2.7': + dependencies: + neverthrow: 2.7.1 + '@standard-schema/spec@1.1.0': {} '@storybook/addon-docs@10.1.11(@types/react@19.2.2)(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': From 9eb2f07d2c27f635ee3caa0e7e1de05e50a425f0 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 9 Jan 2026 19:37:25 -0800 Subject: [PATCH 11/41] fix: add setConstant to MockJsModel and remove unused import Add setConstant method to MockJsModel to fix interface implementation error. Remove unused ConstantDef import from wasm-model.ts. Generated with Claude Code --- packages/runtime/src/js-model/_mocks/mock-js-model.ts | 9 +++++++++ packages/runtime/src/wasm-model/wasm-model.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/js-model/_mocks/mock-js-model.ts b/packages/runtime/src/js-model/_mocks/mock-js-model.ts index f4797576..cfe48526 100644 --- a/packages/runtime/src/js-model/_mocks/mock-js-model.ts +++ b/packages/runtime/src/js-model/_mocks/mock-js-model.ts @@ -119,6 +119,15 @@ export class MockJsModel implements JsModel { this.lookups.set(varId, new JsModelLookup(numPoints, points)) } + // from JsModel interface + setConstant(varSpec: VarSpec, value: number): void { + const varId = this.varIdForSpec(varSpec) + if (varId === undefined) { + throw new Error(`No constant variable found for spec ${varSpec}`) + } + this.vars.set(varId, value) + } + // from JsModel interface storeOutputs(storeValue: (value: number) => void): void { for (const varId of this.outputVarIds) { diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 1b2537ca..59f1ca86 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -1,6 +1,6 @@ // Copyright (c) 2020-2022 Climate Interactive / New Venture Fund -import type { ConstantDef, OutputVarId } from '../_shared' +import type { OutputVarId } from '../_shared' import type { RunModelParams, RunnableModel } from '../runnable-model' import { perfElapsed, perfNow } from '../perf' import { createFloat64WasmBuffer, createInt32WasmBuffer, type WasmBuffer } from './wasm-buffer' From ceabb7e76d10c46eda5b9dbb030d002637c307c1 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 08:46:45 -0800 Subject: [PATCH 12/41] feat: add constant encoding/decoding for async/worker support Implement encoding/decoding functions for constants to enable async/worker model runners to override constants at runtime. This includes: - Add getEncodedConstantBufferLengths(), encodeConstants(), and decodeConstants() functions in var-indices.ts - Update BufferedRunModelParams with constant buffer sections and encoding/decoding - Update PLAN.md with implementation details and progress tracking Constants are encoded with simpler structure than lookups (no offset/length metadata needed, just sequential values). The buffer format uses two sections: constantIndices (metadata) and constants (values). Generated with Claude Code --- PLAN.md | 125 +++++++++++---- packages/runtime/src/_shared/var-indices.ts | 149 ++++++++++++++++++ .../buffered-run-model-params.ts | 69 +++++++- 3 files changed, 308 insertions(+), 35 deletions(-) diff --git a/PLAN.md b/PLAN.md index 615f188f..418f8344 100644 --- a/PLAN.md +++ b/PLAN.md @@ -322,7 +322,64 @@ Add method: getConstants(): ConstantDef[] | undefined ``` -### 9. Update BaseRunnableModel +### 9. Add Constant Encoding/Decoding for Async/Worker Support + +**File**: `packages/runtime/src/_shared/var-indices.ts` + +Add three new functions for encoding/decoding constants (similar to lookup encoding): + +1. `getEncodedConstantBufferLengths(constantDefs: ConstantDef[])` + - Returns `{ constantIndicesLength, constantsLength }` + - Format for constantIndices buffer: + - constant count + - constantN var index + - constantN subscript count + - constantN sub1 index, sub2 index, ... subM index + - (repeat for each constant) + - Format for constants buffer: + - constantN value + - (repeat for each constant) + +2. `encodeConstants(constantDefs: ConstantDef[], constantIndicesArray: Int32Array, constantsArray: Float64Array)` + - Writes constant metadata to indices buffer + - Writes constant values to constants buffer + +3. `decodeConstants(constantIndicesArray: Int32Array, constantsArray: Float64Array): ConstantDef[]` + - Reconstructs ConstantDef instances from buffers + +### 10. Update BufferedRunModelParams for Async/Worker Support + +**File**: `packages/runtime/src/runnable-model/buffered-run-model-params.ts` + +1. Add two new buffer sections: + ```typescript + /** The constant values section of the `encoded` buffer. */ + private readonly constants = new Float64Section() + + /** The constant indices section of the `encoded` buffer. */ + private readonly constantIndices = new Int32Section() + ``` + +2. Update header length constant (line 16): + ```typescript + const headerLengthInElements = 20 // Was 16, add 4 for constants sections + ``` + +3. In `updateFromParams()`: + - Compute constant buffer lengths using `getEncodedConstantBufferLengths()` + - Add constant sections to memory layout calculation + - Update header to include constant section offsets/lengths + - Encode constants using `encodeConstants()` + +4. In `updateFromEncodedBuffer()`: + - Read constant section offsets/lengths from header + - Rebuild constant section views + +5. In `getConstants()`: + - Replace TODO with actual implementation + - Return `decodeConstants(this.constantIndices.view, this.constants.view)` + +### 11. Update BaseRunnableModel **File**: `packages/runtime/src/runnable-model/base-runnable-model.ts` @@ -348,7 +405,7 @@ this.onRunModel?.(inputsArray, outputsArray, { }) ``` -### 10. Create Integration Test +### 12. Create Integration Test **New directory**: `tests/integration/override-constants/` @@ -364,7 +421,7 @@ Create 4 files: - Tests both synchronous and asynchronous model runners 4. **package.json** - Package file with test script -### 11. Add Unit Tests +### 13. Add Unit Tests **File**: `packages/compile/src/generate/gen-code-js.spec.ts` @@ -379,28 +436,36 @@ Add test cases for `setConstant` generation similar to existing `setLookup` test 4. `tests/integration/override-constants/run-tests.js` 5. `tests/integration/override-constants/package.json` -### Modified Files (10): +### Modified Files (12): 1. `packages/runtime/src/_shared/index.ts` - export ConstantDef -2. `packages/runtime/src/runnable-model/run-model-options.ts` - add constants field -3. `packages/runtime/src/runnable-model/run-model-params.ts` - add getConstants() -4. `packages/runtime/src/runnable-model/base-runnable-model.ts` - pass constants through -5. `packages/runtime/src/js-model/js-model.ts` - add setConstant, integrate with runJsModel -6. `packages/runtime/src/wasm-model/wasm-model.ts` - add native wrapper, integrate with runModel -7. `packages/build/src/_shared/model-spec.ts` - add customConstants config option -8. `packages/compile/src/generate/gen-code-js.js` - generate setConstant function -9. `packages/compile/src/generate/gen-code-c.js` - generate C setConstant function -10. `packages/compile/src/generate/gen-code-js.spec.ts` - add unit tests +2. `packages/runtime/src/_shared/var-indices.ts` - add constant encoding/decoding functions +3. `packages/runtime/src/runnable-model/run-model-options.ts` - add constants field +4. `packages/runtime/src/runnable-model/run-model-params.ts` - add getConstants() +5. `packages/runtime/src/runnable-model/base-runnable-model.ts` - pass constants through +6. `packages/runtime/src/runnable-model/buffered-run-model-params.ts` - implement constant encoding/decoding +7. `packages/runtime/src/runnable-model/referenced-run-model-params.ts` - implement getConstants() +8. `packages/runtime/src/js-model/js-model.ts` - add setConstant, integrate with runJsModel +9. `packages/runtime/src/wasm-model/wasm-model.ts` - add native wrapper, integrate with runModel +10. `packages/build/src/_shared/model-spec.ts` - add customConstants config option +11. `packages/compile/src/generate/gen-code-js.js` - generate setConstant function +12. `packages/compile/src/generate/gen-code-c.js` - generate C setConstant function ## Key Differences from Override Lookups -This implementation is **simpler** than override lookups: +This implementation has some similarities and differences compared to override lookups: + +### Similarities: +1. **Encoding/decoding for async support** - both need buffer encoding for worker threads +2. **VarRef resolution** - both use the same varRef pattern for identifying variables +3. **Subscript handling** - both support subscripted variables (1D, 2D arrays) -1. **Scalar values** - just `number`, not `Float64Array` +### Differences (Constants are simpler): +1. **Scalar values** - just `number`, not `Float64Array` of points 2. **No persistence** - constants reset on every run, no state to manage 3. **Simpler C signature** - `(varIndex, subIndices, value)` vs `(varIndex, subIndices, points, numPoints)` 4. **No reset logic** - automatic reset via `initConstants()` -5. **No buffer encoding complexity** - single number vs array of points -6. **Fewer edge cases** - no array size changes, empty arrays, etc. +5. **Simpler buffer encoding** - one value per constant vs variable-length point arrays +6. **No offset tracking** - constants buffer is sequential values, no offset/length metadata needed ## Testing Strategy @@ -415,15 +480,17 @@ This implementation is **simpler** than override lookups: ## Progress Tracking -- [x] Create top-level PLAN.md file -- [ ] Create ConstantDef interface and export -- [ ] Update RunModelOptions with constants field -- [ ] Add customConstants to ModelSpec -- [ ] Generate setConstant function in JS code generator -- [ ] Generate setConstant function in C code generator -- [ ] Update JS model runtime -- [ ] Update Wasm model runtime -- [ ] Update RunModelParams interface -- [ ] Update BaseRunnableModel -- [ ] Create integration test -- [ ] Run tests and verify implementation +- [x] 1. Create top-level PLAN.md file +- [x] 2. Create ConstantDef interface and export +- [x] 3. Update RunModelOptions with constants field +- [x] 4. Add customConstants to ModelSpec +- [x] 5. Generate setConstant function in JS code generator +- [x] 6. Generate setConstant function in C code generator +- [x] 7. Update JS model runtime +- [x] 8. Update Wasm model runtime +- [x] 9. Update RunModelParams interface +- [x] 10. Add constant encoding/decoding for async/worker support +- [x] 11. Update BufferedRunModelParams with constant encoding/decoding +- [x] 12. Update BaseRunnableModel +- [x] 13. Create integration test +- [ ] 14. Run tests and verify implementation diff --git a/packages/runtime/src/_shared/var-indices.ts b/packages/runtime/src/_shared/var-indices.ts index 67aa733e..a113cc83 100644 --- a/packages/runtime/src/_shared/var-indices.ts +++ b/packages/runtime/src/_shared/var-indices.ts @@ -1,5 +1,6 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund +import type { ConstantDef } from './constant-def' import type { LookupDef } from './lookup-def' import type { VarSpec } from './types' @@ -251,3 +252,151 @@ export function decodeLookups(lookupIndicesArray: Int32Array, lookupsArray: Floa return lookupDefs } + +/** + * Return the lengths of the arrays that are required to store the constant values + * and indices for the given `ConstantDef` instances. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantDefs The `ConstantDef` instances to encode. + */ +export function getEncodedConstantBufferLengths(constantDefs: ConstantDef[]): { + constantIndicesLength: number + constantsLength: number +} { + // The constants buffer includes all constant values for the provided constant overrides + // (added sequentially, one value per constant). The constant indices buffer has the + // following format: + // constant count + // constantN var index + // constantN subscript count + // constantN sub1 index + // constantN sub2 index + // ... + // constantN subM index + // ... (repeat for each constant) + + // Start with one element for the total constant variable count + let constantIndicesLength = 1 + let constantsLength = 0 + + for (const constantDef of constantDefs) { + // Ensure that the var spec has already been resolved + const varSpec = constantDef.varRef.varSpec + if (varSpec === undefined) { + throw new Error('Cannot compute constant buffer lengths until all constant var specs are defined') + } + + // Include one element for the variable index and one for the subscript count + constantIndicesLength += 2 + + // Include one element for each subscript + const subCount = varSpec.subscriptIndices?.length || 0 + constantIndicesLength += subCount + + // Add one element for the constant value + constantsLength += 1 + } + + return { + constantIndicesLength, + constantsLength + } +} + +/** + * Encode constant values and indices to the given arrays. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantDefs The `ConstantDef` instances to encode. + * @param constantIndicesArray The view on the constant indices buffer. + * @param constantsArray The view on the constant values buffer. + */ +export function encodeConstants( + constantDefs: ConstantDef[], + constantIndicesArray: Int32Array, + constantsArray: Float64Array +): void { + // Write the constant variable count + let ci = 0 + constantIndicesArray[ci++] = constantDefs.length + + // Write the indices and values for each constant + let constantDataOffset = 0 + for (const constantDef of constantDefs) { + // Write the constant variable index + const varSpec = constantDef.varRef.varSpec + constantIndicesArray[ci++] = varSpec.varIndex + + // Write the subscript count + const subs = varSpec.subscriptIndices + const subCount = subs?.length || 0 + constantIndicesArray[ci++] = subCount + + // Write the subscript indices + for (let i = 0; i < subCount; i++) { + constantIndicesArray[ci++] = subs[i] + } + + // Write the constant value + constantsArray[constantDataOffset++] = constantDef.value + } +} + +/** + * Decode constant values and indices from the given buffer views and return the + * reconstructed `ConstantDef` instances. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantIndicesArray The view on the constant indices buffer. + * @param constantsArray The view on the constant values buffer. + */ +export function decodeConstants( + constantIndicesArray: Int32Array, + constantsArray: Float64Array +): ConstantDef[] { + const constantDefs: ConstantDef[] = [] + let ci = 0 + + // Read the constant variable count + const constantCount = constantIndicesArray[ci++] + + // Read the metadata for each variable from the constant indices buffer + for (let i = 0; i < constantCount; i++) { + // Read the constant variable index + const varIndex = constantIndicesArray[ci++] + + // Read the subscript count + const subCount = constantIndicesArray[ci++] + + // Read the subscript indices + const subscriptIndices: number[] = subCount > 0 ? Array(subCount) : undefined + for (let subIndex = 0; subIndex < subCount; subIndex++) { + subscriptIndices[subIndex] = constantIndicesArray[ci++] + } + + // Create a `VarSpec` for the variable + const varSpec: VarSpec = { + varIndex, + subscriptIndices + } + + // Read the constant value + const value = constantsArray[i] + + constantDefs.push({ + varRef: { + varSpec + }, + value + }) + } + + return constantDefs +} diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.ts index 2c3c7a54..1f5d1424 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.ts @@ -1,9 +1,12 @@ // Copyright (c) 2024 Climate Interactive / New Venture Fund import { + decodeConstants, decodeLookups, + encodeConstants, encodeLookups, encodeVarIndices, + getEncodedConstantBufferLengths, getEncodedLookupBufferLengths, getEncodedVarIndicesLength } from '../_shared' @@ -13,7 +16,7 @@ import { resolveVarRef } from './resolve-var-ref' import type { RunModelOptions } from './run-model-options' import type { RunModelParams } from './run-model-params' -const headerLengthInElements = 16 +const headerLengthInElements = 20 const extrasLengthInElements = 1 interface Section { @@ -74,6 +77,8 @@ export class BufferedRunModelParams implements RunModelParams { * outputIndices * lookups (data) * lookupIndices + * constants (values) + * constantIndices */ private encoded: ArrayBuffer @@ -101,6 +106,12 @@ export class BufferedRunModelParams implements RunModelParams { /** The lookup indices section of the `encoded` buffer. */ private readonly lookupIndices = new Int32Section() + /** The constant values section of the `encoded` buffer. */ + private readonly constants = new Float64Section() + + /** The constant indices section of the `encoded` buffer. */ + private readonly constantIndices = new Int32Section() + /** * @param listing The model listing that is used to locate a variable that is referenced by * name or identifier. If undefined, variables cannot be referenced by name or identifier, @@ -213,10 +224,13 @@ export class BufferedRunModelParams implements RunModelParams { // from RunModelParams interface getConstants(): ConstantDef[] | undefined { - // TODO: For now, constant overrides are not supported in the buffered (async/worker) - // implementation. To support constants in async workers, we would need to add encoding/ - // decoding functions similar to `encodeLookups` and `decodeLookups`. - return undefined + if (this.constantIndices.lengthInElements === 0) { + return undefined + } + + // Reconstruct the `ConstantDef` instances using the data from the constant values and + // indices buffers + return decodeConstants(this.constantIndices.view, this.constants.view) } // from RunModelParams interface @@ -290,6 +304,26 @@ export class BufferedRunModelParams implements RunModelParams { lookupIndicesLengthInElements = 0 } + // Determine the number of elements in the constant values and indices sections + let constantsLengthInElements: number + let constantIndicesLengthInElements: number + if (options?.constants !== undefined && options.constants.length > 0) { + // Resolve the `varSpec` for each `ConstantDef`. If the variable can be resolved, this + // will fill in the `varSpec` for the `ConstantDef`, otherwise it will throw an error. + for (const constantDef of options.constants) { + resolveVarRef(this.listing, constantDef.varRef, 'constant') + } + + // Compute the required lengths + const encodedLengths = getEncodedConstantBufferLengths(options.constants) + constantsLengthInElements = encodedLengths.constantsLength + constantIndicesLengthInElements = encodedLengths.constantIndicesLength + } else { + // Don't use the constant values and indices buffers when constant overrides are not provided + constantsLengthInElements = 0 + constantIndicesLengthInElements = 0 + } + // Compute the byte offset and byte length of each section let byteOffset = 0 function section(kind: 'float64' | 'int32', lengthInElements: number): number { @@ -311,6 +345,8 @@ export class BufferedRunModelParams implements RunModelParams { const outputIndicesOffsetInBytes = section('int32', outputIndicesLengthInElements) const lookupsOffsetInBytes = section('float64', lookupsLengthInElements) const lookupIndicesOffsetInBytes = section('int32', lookupIndicesLengthInElements) + const constantsOffsetInBytes = section('float64', constantsLengthInElements) + const constantIndicesOffsetInBytes = section('int32', constantIndicesLengthInElements) // Get the total byte length const requiredLengthInBytes = byteOffset @@ -341,6 +377,10 @@ export class BufferedRunModelParams implements RunModelParams { headerView[headerIndex++] = lookupsLengthInElements headerView[headerIndex++] = lookupIndicesOffsetInBytes headerView[headerIndex++] = lookupIndicesLengthInElements + headerView[headerIndex++] = constantsOffsetInBytes + headerView[headerIndex++] = constantsLengthInElements + headerView[headerIndex++] = constantIndicesOffsetInBytes + headerView[headerIndex++] = constantIndicesLengthInElements // Update the views // TODO: We can avoid recreating the views every time if buffer and section offset/length @@ -351,6 +391,8 @@ export class BufferedRunModelParams implements RunModelParams { this.outputIndices.update(this.encoded, outputIndicesOffsetInBytes, outputIndicesLengthInElements) this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) + this.constants.update(this.encoded, constantsOffsetInBytes, constantsLengthInElements) + this.constantIndices.update(this.encoded, constantIndicesOffsetInBytes, constantIndicesLengthInElements) // Copy the input values into the internal buffer // TODO: Throw an error if inputs.length is less than number of inputs declared @@ -378,6 +420,11 @@ export class BufferedRunModelParams implements RunModelParams { if (lookupIndicesLengthInElements > 0) { encodeLookups(options.lookups, this.lookupIndices.view, this.lookups.view) } + + // Copy the constant values and indices into the internal buffers, if needed + if (constantIndicesLengthInElements > 0) { + encodeConstants(options.constants, this.constantIndices.view, this.constants.view) + } } /** @@ -415,6 +462,10 @@ export class BufferedRunModelParams implements RunModelParams { const lookupsLengthInElements = headerView[headerIndex++] const lookupIndicesOffsetInBytes = headerView[headerIndex++] const lookupIndicesLengthInElements = headerView[headerIndex++] + const constantsOffsetInBytes = headerView[headerIndex++] + const constantsLengthInElements = headerView[headerIndex++] + const constantIndicesOffsetInBytes = headerView[headerIndex++] + const constantIndicesLengthInElements = headerView[headerIndex++] // Verify that the buffer is long enough to contain all sections const extrasLengthInBytes = extrasLengthInElements * Float64Array.BYTES_PER_ELEMENT @@ -423,6 +474,8 @@ export class BufferedRunModelParams implements RunModelParams { const outputIndicesLengthInBytes = outputIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT const lookupsLengthInBytes = lookupsLengthInElements * Float64Array.BYTES_PER_ELEMENT const lookupIndicesLengthInBytes = lookupIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT + const constantsLengthInBytes = constantsLengthInElements * Float64Array.BYTES_PER_ELEMENT + const constantIndicesLengthInBytes = constantIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT const requiredLengthInBytes = headerLengthInBytes + extrasLengthInBytes + @@ -430,7 +483,9 @@ export class BufferedRunModelParams implements RunModelParams { outputsLengthInBytes + outputIndicesLengthInBytes + lookupsLengthInBytes + - lookupIndicesLengthInBytes + lookupIndicesLengthInBytes + + constantsLengthInBytes + + constantIndicesLengthInBytes if (buffer.byteLength < requiredLengthInBytes) { throw new Error('Buffer must be long enough to contain sections declared in header') } @@ -442,5 +497,7 @@ export class BufferedRunModelParams implements RunModelParams { this.outputIndices.update(this.encoded, outputIndicesOffsetInBytes, outputIndicesLengthInElements) this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) + this.constants.update(this.encoded, constantsOffsetInBytes, constantsLengthInElements) + this.constantIndices.update(this.encoded, constantIndicesOffsetInBytes, constantIndicesLengthInElements) } } From a8e0ed6fc8dd788ab00d912ce55c0486ef9684bf Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 09:28:19 -0800 Subject: [PATCH 13/41] fix: add customConstants to ResolvedModelSpec and spec.json Update build-once.ts to properly include customConstants when creating the resolved model spec and spec.json file. This is needed for the compile package to generate the correct setConstant function body. - Add customConstants to specJson with default value of false - Add customConstants resolution logic similar to customLookups - Include customConstants in the returned ResolvedModelSpec Generated with Claude Code --- packages/build/src/build/impl/build-once.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/build/src/build/impl/build-once.ts b/packages/build/src/build/impl/build-once.ts index ecf1f2ad..154f6111 100644 --- a/packages/build/src/build/impl/build-once.ts +++ b/packages/build/src/build/impl/build-once.ts @@ -76,8 +76,9 @@ export async function buildOnce( outputVarNames: modelSpec.outputVarNames, externalDatfiles: modelSpec.datFiles, bundleListing: modelSpec.bundleListing, - customLookups: modelSpec.customLookups, - customOutputs: modelSpec.customOutputs, + customLookups: modelSpec.customLookups || false, + customConstants: modelSpec.customConstants || false, + customOutputs: modelSpec.customOutputs || false, ...modelSpec.options } const specPath = joinPath(config.prepDir, 'spec.json') @@ -227,6 +228,13 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec { customLookups = false } + let customConstants: boolean | VarName[] + if (modelSpec.customConstants !== undefined) { + customConstants = modelSpec.customConstants + } else { + customConstants = false + } + let customOutputs: boolean | VarName[] if (modelSpec.customOutputs !== undefined) { customOutputs = modelSpec.customOutputs @@ -242,6 +250,7 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec { datFiles: modelSpec.datFiles || [], bundleListing: modelSpec.bundleListing === true, customLookups, + customConstants, customOutputs, options: modelSpec.options } From 83e8d71f47a908e6ce79d368b59d92cbbfa0db03 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 09:31:09 -0800 Subject: [PATCH 14/41] fix: add customConstants to plugin-config processor Add customConstants field to ModelOptions interface and processor output. This ensures customConstants from config is passed through to the model spec. Note: Currently experiencing a TypeScript build error that needs investigation. The ModelSpec interface has customConstants defined, but TypeScript is not recognizing it during the plugin-config build. Generated with Claude Code --- packages/plugin-config/src/context.ts | 1 + packages/plugin-config/src/processor.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/plugin-config/src/context.ts b/packages/plugin-config/src/context.ts index 045dd7fb..89a63135 100644 --- a/packages/plugin-config/src/context.ts +++ b/packages/plugin-config/src/context.ts @@ -19,6 +19,7 @@ export interface ModelOptions { readonly datFiles: string[] readonly bundleListing: boolean readonly customLookups: boolean + readonly customConstants: boolean readonly customOutputs: boolean } diff --git a/packages/plugin-config/src/processor.ts b/packages/plugin-config/src/processor.ts index 884b413b..274848df 100644 --- a/packages/plugin-config/src/processor.ts +++ b/packages/plugin-config/src/processor.ts @@ -136,6 +136,7 @@ async function processModelConfig(buildContext: BuildContext, options: ConfigPro datFiles: modelOptions.datFiles, bundleListing: modelOptions.bundleListing, customLookups: modelOptions.customLookups, + customConstants: modelOptions.customConstants, customOutputs: modelOptions.customOutputs, options: options.spec } From 3ccacfaff49aac5997c831b384172714f7490bcc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 10:47:44 -0800 Subject: [PATCH 15/41] fix: move customConstants code before customLookups instead of after --- packages/build/src/_shared/model-spec.ts | 40 ++++---- packages/build/src/build/impl/build-once.ts | 18 ++-- packages/compile/src/generate/gen-code-c.js | 90 +++++++++--------- packages/compile/src/generate/gen-code-js.js | 96 ++++++++++---------- packages/plugin-config/src/context.ts | 8 +- packages/plugin-config/src/processor.ts | 2 +- 6 files changed, 128 insertions(+), 126 deletions(-) diff --git a/packages/build/src/_shared/model-spec.ts b/packages/build/src/_shared/model-spec.ts index 16f88fa2..c946f76c 100644 --- a/packages/build/src/_shared/model-spec.ts +++ b/packages/build/src/_shared/model-spec.ts @@ -78,32 +78,32 @@ export interface ModelSpec { bundleListing?: boolean /** - * Whether to allow lookups to be overridden at runtime using `setLookup`. + * Whether to allow constants to be overridden at runtime using `setConstant`. * - * If undefined or false, the generated model will implement `setLookup` - * as a no-op, meaning that lookups cannot be overridden at runtime. + * If undefined or false, the generated model will implement `setConstant` + * as a no-op, meaning that constants cannot be overridden at runtime. * - * If true, all lookups in the generated model will be available to be + * If true, all constants in the generated model will be available to be * overridden. * * If an array is provided, only those variable names in the array will * be available to be overridden. */ - customLookups?: boolean | VarName[] + customConstants?: boolean | VarName[] /** - * Whether to allow constants to be overridden at runtime using `setConstant`. + * Whether to allow lookups to be overridden at runtime using `setLookup`. * - * If undefined or false, the generated model will implement `setConstant` - * as a no-op, meaning that constants cannot be overridden at runtime. + * If undefined or false, the generated model will implement `setLookup` + * as a no-op, meaning that lookups cannot be overridden at runtime. * - * If true, all constants in the generated model will be available to be + * If true, all lookups in the generated model will be available to be * overridden. * * If an array is provided, only those variable names in the array will * be available to be overridden. */ - customConstants?: boolean | VarName[] + customLookups?: boolean | VarName[] /** * Whether to allow for capturing the data for arbitrary variables at @@ -181,32 +181,32 @@ export interface ResolvedModelSpec { bundleListing: boolean /** - * Whether to allow lookups to be overridden at runtime using `setLookup`. + * Whether to allow constants to be overridden at runtime using `setConstant`. * - * If false, the generated model will contain a `setLookup` function that - * throws an error, meaning that lookups cannot be overridden at runtime. + * If false, the generated model will contain a `setConstant` function that + * throws an error, meaning that constants cannot be overridden at runtime. * - * If true, all lookups in the generated model will be available to be + * If true, all constants in the generated model will be available to be * overridden. * * If an array is provided, only those variable names in the array will * be available to be overridden. */ - customLookups: boolean | VarName[] + customConstants: boolean | VarName[] /** - * Whether to allow constants to be overridden at runtime using `setConstant`. + * Whether to allow lookups to be overridden at runtime using `setLookup`. * - * If false, the generated model will contain a `setConstant` function that - * throws an error, meaning that constants cannot be overridden at runtime. + * If false, the generated model will contain a `setLookup` function that + * throws an error, meaning that lookups cannot be overridden at runtime. * - * If true, all constants in the generated model will be available to be + * If true, all lookups in the generated model will be available to be * overridden. * * If an array is provided, only those variable names in the array will * be available to be overridden. */ - customConstants: boolean | VarName[] + customLookups: boolean | VarName[] /** * Whether to allow for capturing the data for arbitrary variables at diff --git a/packages/build/src/build/impl/build-once.ts b/packages/build/src/build/impl/build-once.ts index 154f6111..5845a262 100644 --- a/packages/build/src/build/impl/build-once.ts +++ b/packages/build/src/build/impl/build-once.ts @@ -76,8 +76,8 @@ export async function buildOnce( outputVarNames: modelSpec.outputVarNames, externalDatfiles: modelSpec.datFiles, bundleListing: modelSpec.bundleListing, - customLookups: modelSpec.customLookups || false, customConstants: modelSpec.customConstants || false, + customLookups: modelSpec.customLookups || false, customOutputs: modelSpec.customOutputs || false, ...modelSpec.options } @@ -221,13 +221,6 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec { outputSpecs = [] } - let customLookups: boolean | VarName[] - if (modelSpec.customLookups !== undefined) { - customLookups = modelSpec.customLookups - } else { - customLookups = false - } - let customConstants: boolean | VarName[] if (modelSpec.customConstants !== undefined) { customConstants = modelSpec.customConstants @@ -235,6 +228,13 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec { customConstants = false } + let customLookups: boolean | VarName[] + if (modelSpec.customLookups !== undefined) { + customLookups = modelSpec.customLookups + } else { + customLookups = false + } + let customOutputs: boolean | VarName[] if (modelSpec.customOutputs !== undefined) { customOutputs = modelSpec.customOutputs @@ -249,8 +249,8 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec { outputs: outputSpecs, datFiles: modelSpec.datFiles || [], bundleListing: modelSpec.bundleListing === true, - customLookups, customConstants, + customLookups, customOutputs, options: modelSpec.options } diff --git a/packages/compile/src/generate/gen-code-c.js b/packages/compile/src/generate/gen-code-c.js index 7354f55e..43929ea0 100644 --- a/packages/compile/src/generate/gen-code-c.js +++ b/packages/compile/src/generate/gen-code-c.js @@ -146,6 +146,24 @@ ${chunkedFunctions('evalLevels', Model.levelVars(), ' // Evaluate levels.')}` // Input/output section // function emitIOCode() { + // Configure the body of the `setConstant` function depending on the value + // of the `customConstants` property in the spec file + let setConstantBody + if (spec.customConstants === true || Array.isArray(spec.customConstants)) { + setConstantBody = `\ + switch (varIndex) { +${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} + default: + fprintf(stderr, "No constant found for var index %zu in setConstant\\n", varIndex); + break; + }` + } else { + let msg = 'The setConstant function was not enabled for the generated model. ' + msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' + setConstantBody = `\ + fprintf(stderr, "${msg}\\n");` + } + // Configure the body of the `setLookup` function depending on the value // of the `customLookups` property in the spec file // TODO: The fprintf calls should be replaced with a mechanism that throws @@ -174,24 +192,6 @@ ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)} fprintf(stderr, "${msg}\\n");` } - // Configure the body of the `setConstant` function depending on the value - // of the `customConstants` property in the spec file - let setConstantBody - if (spec.customConstants === true || Array.isArray(spec.customConstants)) { - setConstantBody = `\ - switch (varIndex) { -${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} - default: - fprintf(stderr, "No constant found for var index %zu in setConstant\\n", varIndex); - break; - }` - } else { - let msg = 'The setConstant function was not enabled for the generated model. ' - msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' - setConstantBody = `\ - fprintf(stderr, "${msg}\\n");` - } - // Configure the output variables that appear in the generated `getHeader` // and `storeOutputData` functions let headerVarNames = outputAllVars ? expandedVarNames(true) : spec.outputVarNames @@ -457,71 +457,71 @@ ${section(chunk)} } return inputVars.join('\n') } - function setLookupImpl(varIndexInfo, customLookups) { - // Emit case statements for all lookups and data variables that can be overridden - // at runtime + function setConstantImpl(varIndexInfo, customConstants) { + // Emit case statements for all const variables that can be overridden at runtime let includeCase - if (Array.isArray(customLookups)) { + if (Array.isArray(customConstants)) { // Only include a case statement if the variable was explicitly included - // in the `customLookups` array in the spec file - const customLookupVarNames = customLookups.map(varName => { + // in the `customConstants` array in the spec file + const customConstantVarNames = customConstants.map(varName => { // The developer might specify a variable name that includes subscripts, // but we will ignore the subscript part and only match on the base name return canonicalVensimName(varName.split('[')[0]) }) - includeCase = varName => customLookupVarNames.includes(varName) + includeCase = varName => customConstantVarNames.includes(varName) } else { - // Include a case statement for all lookup and data variables + // Include a case statement for all constant variables includeCase = () => true } - const lookupAndDataVars = R.filter(info => { - return (info.varType === 'lookup' || info.varType === 'data') && includeCase(info.varName) + const constVars = R.filter(info => { + return info.varType === 'const' && includeCase(info.varName) }) const code = R.map(info => { - let lookupVar = info.varName + let constVar = info.varName for (let i = 0; i < info.subscriptCount; i++) { - lookupVar += `[subIndices[${i}]]` + constVar += `[subIndices[${i}]]` } let c = '' c += ` case ${info.varIndex}:\n` - c += ` pLookup = &${lookupVar};\n` + c += ` ${constVar} = value;\n` c += ` break;` return c }) - const section = R.pipe(lookupAndDataVars, code, lines) + const section = R.pipe(constVars, code, lines) return section(varIndexInfo) } - function setConstantImpl(varIndexInfo, customConstants) { - // Emit case statements for all const variables that can be overridden at runtime + function setLookupImpl(varIndexInfo, customLookups) { + // Emit case statements for all lookups and data variables that can be overridden + // at runtime let includeCase - if (Array.isArray(customConstants)) { + if (Array.isArray(customLookups)) { // Only include a case statement if the variable was explicitly included - // in the `customConstants` array in the spec file - const customConstantVarNames = customConstants.map(varName => { + // in the `customLookups` array in the spec file + const customLookupVarNames = customLookups.map(varName => { // The developer might specify a variable name that includes subscripts, // but we will ignore the subscript part and only match on the base name return canonicalVensimName(varName.split('[')[0]) }) - includeCase = varName => customConstantVarNames.includes(varName) + includeCase = varName => customLookupVarNames.includes(varName) } else { - // Include a case statement for all constant variables + // Include a case statement for all lookup and data variables includeCase = () => true } - const constVars = R.filter(info => { - return info.varType === 'const' && includeCase(info.varName) + const lookupAndDataVars = R.filter(info => { + return (info.varType === 'lookup' || info.varType === 'data') && includeCase(info.varName) }) const code = R.map(info => { - let constVar = info.varName + let lookupVar = info.varName for (let i = 0; i < info.subscriptCount; i++) { - constVar += `[subIndices[${i}]]` + lookupVar += `[subIndices[${i}]]` } let c = '' c += ` case ${info.varIndex}:\n` - c += ` ${constVar} = value;\n` + c += ` pLookup = &${lookupVar};\n` c += ` break;` return c }) - const section = R.pipe(constVars, code, lines) + const section = R.pipe(lookupAndDataVars, code, lines) return section(varIndexInfo) } diff --git a/packages/compile/src/generate/gen-code-js.js b/packages/compile/src/generate/gen-code-js.js index 798d006e..a01a4563 100644 --- a/packages/compile/src/generate/gen-code-js.js +++ b/packages/compile/src/generate/gen-code-js.js @@ -241,6 +241,27 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels' function emitIOCode() { mode = 'io' + // Configure the body of the `setConstant` function depending on the value + // of the `customConstants` property in the spec file + let setConstantBody + if (spec.customConstants === true || Array.isArray(spec.customConstants)) { + setConstantBody = `\ + if (!varSpec) { + throw new Error('Got undefined varSpec in setConstant'); + } + const varIndex = varSpec.varIndex; + const subs = varSpec.subscriptIndices; + switch (varIndex) { +${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} + default: + throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); + }` + } else { + let msg = 'The setConstant function was not enabled for the generated model. ' + msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' + setConstantBody = ` throw new Error('${msg}');` + } + // Configure the body of the `setLookup` function depending on the value // of the `customLookups` property in the spec file let setLookupBody @@ -267,27 +288,6 @@ ${setLookupImpl(Model.varIndexInfo(), spec.customLookups)} setLookupBody = ` throw new Error('${msg}');` } - // Configure the body of the `setConstant` function depending on the value - // of the `customConstants` property in the spec file - let setConstantBody - if (spec.customConstants === true || Array.isArray(spec.customConstants)) { - setConstantBody = `\ - if (!varSpec) { - throw new Error('Got undefined varSpec in setConstant'); - } - const varIndex = varSpec.varIndex; - const subs = varSpec.subscriptIndices; - switch (varIndex) { -${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} - default: - throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); - }` - } else { - let msg = 'The setConstant function was not enabled for the generated model. ' - msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' - setConstantBody = ` throw new Error('${msg}');` - } - // This is the list of original output variable names (as supplied by the user in // the `spec.json` file), for example, `a[A2,B1]`. These are exported mainly for // use in the implementation of the `sde exec` command, which generates a TSV file @@ -546,71 +546,71 @@ ${section(chunk)} } return inputVars } - function setLookupImpl(varIndexInfo, customLookups) { - // Emit case statements for all lookups and data variables that can be overridden - // at runtime + function setConstantImpl(varIndexInfo, customConstants) { + // Emit case statements for all const variables that can be overridden at runtime let overrideAllowed - if (Array.isArray(customLookups)) { + if (Array.isArray(customConstants)) { // Only include a case statement if the variable was explicitly included - // in the `customLookups` array in the spec file - const customLookupVarNames = customLookups.map(varName => { + // in the `customConstants` array in the spec file + const customConstantVarNames = customConstants.map(varName => { // The developer might specify a variable name that includes subscripts, // but we will ignore the subscript part and only match on the base name return canonicalVensimName(varName.split('[')[0]) }) - overrideAllowed = varName => customLookupVarNames.includes(varName) + overrideAllowed = varName => customConstantVarNames.includes(varName) } else { - // Include a case statement for all lookup and data variables + // Include a case statement for all constant variables overrideAllowed = () => true } - const lookupAndDataVars = R.filter(info => { - return (info.varType === 'lookup' || info.varType === 'data') && overrideAllowed(info.varName) + const constVars = R.filter(info => { + return info.varType === 'const' && overrideAllowed(info.varName) }) const code = R.map(info => { - let lookupVar = info.varName + let constVar = info.varName for (let i = 0; i < info.subscriptCount; i++) { - lookupVar += `[subs[${i}]]` + constVar += `[subs[${i}]]` } let c = '' c += ` case ${info.varIndex}:\n` - c += ` lookup = ${lookupVar};\n` + c += ` ${constVar} = value;\n` c += ` break;` return c }) - const section = R.pipe(lookupAndDataVars, code, lines) + const section = R.pipe(constVars, code, lines) return section(varIndexInfo) } - function setConstantImpl(varIndexInfo, customConstants) { - // Emit case statements for all const variables that can be overridden at runtime + function setLookupImpl(varIndexInfo, customLookups) { + // Emit case statements for all lookups and data variables that can be overridden + // at runtime let overrideAllowed - if (Array.isArray(customConstants)) { + if (Array.isArray(customLookups)) { // Only include a case statement if the variable was explicitly included - // in the `customConstants` array in the spec file - const customConstantVarNames = customConstants.map(varName => { + // in the `customLookups` array in the spec file + const customLookupVarNames = customLookups.map(varName => { // The developer might specify a variable name that includes subscripts, // but we will ignore the subscript part and only match on the base name return canonicalVensimName(varName.split('[')[0]) }) - overrideAllowed = varName => customConstantVarNames.includes(varName) + overrideAllowed = varName => customLookupVarNames.includes(varName) } else { - // Include a case statement for all constant variables + // Include a case statement for all lookup and data variables overrideAllowed = () => true } - const constVars = R.filter(info => { - return info.varType === 'const' && overrideAllowed(info.varName) + const lookupAndDataVars = R.filter(info => { + return (info.varType === 'lookup' || info.varType === 'data') && overrideAllowed(info.varName) }) const code = R.map(info => { - let constVar = info.varName + let lookupVar = info.varName for (let i = 0; i < info.subscriptCount; i++) { - constVar += `[subs[${i}]]` + lookupVar += `[subs[${i}]]` } let c = '' c += ` case ${info.varIndex}:\n` - c += ` ${constVar} = value;\n` + c += ` lookup = ${lookupVar};\n` c += ` break;` return c }) - const section = R.pipe(constVars, code, lines) + const section = R.pipe(lookupAndDataVars, code, lines) return section(varIndexInfo) } diff --git a/packages/plugin-config/src/context.ts b/packages/plugin-config/src/context.ts index 89a63135..2bcb6925 100644 --- a/packages/plugin-config/src/context.ts +++ b/packages/plugin-config/src/context.ts @@ -18,8 +18,8 @@ export interface ModelOptions { readonly graphDefaultMaxTime: number readonly datFiles: string[] readonly bundleListing: boolean - readonly customLookups: boolean readonly customConstants: boolean + readonly customLookups: boolean readonly customOutputs: boolean } @@ -175,10 +175,11 @@ export function createConfigContext(buildContext: BuildContext, configDir: strin const datFiles = origDatFiles.map(f => joinPath(relative(prepDir, projDir), f)) // Read other boolean properties from `model.csv` - // TODO: If customLookups is true, see if there is a `config/custom-lookups.csv` file + // TODO: If customConstants is true, see if there is a `config/custom-constants.csv` file // and if so, make an array of variable names instead of setting a boolean in `spec.json`. - // (Same thing for customOutputs.) + // (Same thing for customLookups and customOutputs.) const bundleListing = modelCsv['bundle listing'] === 'true' + const customConstants = modelCsv['custom constants'] === 'true' const customLookups = modelCsv['custom lookups'] === 'true' const customOutputs = modelCsv['custom outputs'] === 'true' @@ -187,6 +188,7 @@ export function createConfigContext(buildContext: BuildContext, configDir: strin graphDefaultMaxTime, datFiles, bundleListing, + customConstants, customLookups, customOutputs } diff --git a/packages/plugin-config/src/processor.ts b/packages/plugin-config/src/processor.ts index 274848df..d09159d3 100644 --- a/packages/plugin-config/src/processor.ts +++ b/packages/plugin-config/src/processor.ts @@ -135,8 +135,8 @@ async function processModelConfig(buildContext: BuildContext, options: ConfigPro outputs: context.getOrderedOutputs(), datFiles: modelOptions.datFiles, bundleListing: modelOptions.bundleListing, - customLookups: modelOptions.customLookups, customConstants: modelOptions.customConstants, + customLookups: modelOptions.customLookups, customOutputs: modelOptions.customOutputs, options: options.spec } From 58010f2b1bed103753f4fc59ef437f3f010a2930 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 11:00:09 -0800 Subject: [PATCH 16/41] build: revert to lock file from main branch This reverts the change in 0fe10bf1537e420015a5a78d32f320644db93ea7 that caused local dependencies to be using published versions instead. --- pnpm-lock.yaml | 199 ++++++------------------------------------------- 1 file changed, 22 insertions(+), 177 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16c004d0..957e4043 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,13 +79,13 @@ importers: dependencies: '@sdeverywhere/build': specifier: ^0.3.7 - version: 0.3.9 + version: link:../../packages/build '@sdeverywhere/cli': specifier: ^0.7.34 - version: 0.7.38 + version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) + version: link:../../packages/plugin-check '@sdeverywhere/plugin-worker': specifier: ^0.2.11 version: link:../../packages/plugin-worker @@ -100,10 +100,10 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: 0.3.9 + version: link:../../packages/build '@sdeverywhere/cli': specifier: ^0.7.34 - version: 0.7.38 + version: link:../../packages/cli '@sdeverywhere/plugin-vite': specifier: ^0.2.0 version: link:../../packages/plugin-vite @@ -236,16 +236,16 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: 0.3.9 + version: link:../../packages/build '@sdeverywhere/check-core': specifier: ^0.1.5 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.34 - version: 0.7.38 + version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) + version: link:../../packages/plugin-check '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -325,16 +325,16 @@ importers: dependencies: '@sdeverywhere/build': specifier: ^0.3.7 - version: 0.3.9 + version: link:../../packages/build '@sdeverywhere/check-core': specifier: ^0.1.5 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.34 - version: 0.7.38 + version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) + version: link:../../packages/plugin-check '@sdeverywhere/plugin-wasm': specifier: ^0.2.6 version: link:../../packages/plugin-wasm @@ -349,16 +349,16 @@ importers: version: 9.37.0 '@sdeverywhere/build': specifier: ^0.3.7 - version: 0.3.9 + version: link:../../packages/build '@sdeverywhere/check-core': specifier: ^0.1.6 version: link:../../packages/check-core '@sdeverywhere/cli': specifier: ^0.7.36 - version: 0.7.38 + version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.21 - version: 0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) + version: link:../../packages/plugin-check '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -601,8 +601,8 @@ importers: packages/cli: dependencies: '@sdeverywhere/build': - specifier: ^0.3.8 - version: 0.3.9 + specifier: ^0.3.9 + version: link:../build '@sdeverywhere/compile': specifier: ^0.7.26 version: link:../compile @@ -749,7 +749,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -768,7 +768,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build '@types/byline': specifier: ^4.2.33 version: 4.2.33 @@ -792,7 +792,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -801,7 +801,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build vite: specifier: ^7.1.12 version: 7.3.1(@types/node@20.19.19)(sass@1.97.2) @@ -814,7 +814,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -833,7 +833,7 @@ importers: devDependencies: '@sdeverywhere/build': specifier: '*' - version: 0.3.9 + version: link:../build '@types/node': specifier: ^20.14.8 version: 20.19.19 @@ -989,27 +989,6 @@ importers: specifier: ^20.14.8 version: 20.19.19 - tests/integration/override-constants: - dependencies: - '@sdeverywhere/build': - specifier: workspace:* - version: link:../../../packages/build - '@sdeverywhere/cli': - specifier: workspace:* - version: link:../../../packages/cli - '@sdeverywhere/plugin-wasm': - specifier: workspace:* - version: link:../../../packages/plugin-wasm - '@sdeverywhere/plugin-worker': - specifier: workspace:* - version: link:../../../packages/plugin-worker - '@sdeverywhere/runtime': - specifier: workspace:* - version: link:../../../packages/runtime - '@sdeverywhere/runtime-async': - specifier: workspace:* - version: link:../../../packages/runtime-async - tests/integration/override-lookups: dependencies: '@sdeverywhere/build': @@ -1608,38 +1587,6 @@ packages: cpu: [x64] os: [win32] - '@sdeverywhere/build@0.3.9': - resolution: {integrity: sha512-zjyipiHLxerpQ4tcYWG43dz5VnfDP1JatT3m/gMXTICZnYZS8ogHl+hDbbzK5AgNesVRZwYU4rWtqlbm4MbTIg==} - - '@sdeverywhere/check-core@0.1.7': - resolution: {integrity: sha512-JH0TAgc9l4ZZHHCcJ9GHqa6bnekgFnQ593J4Aqk2bAEMAbLRsgHjPgWEB24yMVufSzF7gUeVyltTKbdD9YvAOg==} - - '@sdeverywhere/check-ui-shell@0.2.16': - resolution: {integrity: sha512-t5HL9cQekWmIa67lYycOxDSZdcwfeGKxm5jFtO9SyiuFjYAfqRTKKrDda3RpQuoZuK3Kbg3eCfJ9DnNIkt0bSA==} - - '@sdeverywhere/cli@0.7.38': - resolution: {integrity: sha512-6HqkzhzZA/42pYeDOB/ITvxm7s0qTXCtOnJqNDd9Vvh/zGZtXwwOq5WNa3OgzTRvEffRhuMrWcsesa9ehznBQQ==} - engines: {node: '>=20'} - hasBin: true - - '@sdeverywhere/compile@0.7.26': - resolution: {integrity: sha512-/NiCqr4cSdgs+NsoFeMVpzE5csacYEFO0n/r9xzO85r5f7aVbBtoF4SNT5uIWg6YWMKivTwRD5a7WMCsaAyElQ==} - - '@sdeverywhere/parse@0.1.2': - resolution: {integrity: sha512-8/WQhdG9xxik4k1aU2rksPeNj5rMRKv2mVHwyg2RCDB77JeG2f/3GNTMBzn9R7D1L4ZAXFZ1/pbaw2WELwL22w==} - - '@sdeverywhere/plugin-check@0.3.25': - resolution: {integrity: sha512-ktY5ChpVdXI8IxehimOrn6wJK/PWityhZEllXgJe6WpTXqig5eqrWTmzLwX42367ZGJfMPnM8dv5m1Qkwuuaxg==} - hasBin: true - peerDependencies: - '@sdeverywhere/build': ^0.3.7 - - '@sdeverywhere/runtime-async@0.2.7': - resolution: {integrity: sha512-dr7L9smMbaVJH2NOJNx8q6898J6DYuyO2oEunhUpKBy704R05RF0NsYC+XJNkJRwRuE0t0pulVfXIZzr95GPeg==} - - '@sdeverywhere/runtime@0.2.7': - resolution: {integrity: sha512-iEaMn4qMojbBgcv17w+yIInUOdH3cZuOLx4LOk2VFGLjJm1Xv2/FDkGMxJdqvR9rMwI4bg9WtxCcmFLLKMgWNA==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -4139,108 +4086,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true - '@sdeverywhere/build@0.3.9': - dependencies: - '@sdeverywhere/parse': 0.1.2 - chokidar: 5.0.0 - cross-spawn: 7.0.6 - folder-hash: 4.0.2 - neverthrow: 4.3.1 - picocolors: 1.1.1 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - supports-color - - '@sdeverywhere/check-core@0.1.7': - dependencies: - ajv: 8.12.0 - assert-never: 1.2.1 - neverthrow: 4.3.1 - yaml: 2.2.2 - - '@sdeverywhere/check-ui-shell@0.2.16(svelte@5.39.10)': - dependencies: - '@fortawesome/free-regular-svg-icons': 7.1.0 - '@fortawesome/free-solid-svg-icons': 7.1.0 - '@juggle/resize-observer': 3.4.0 - '@sdeverywhere/check-core': 0.1.7 - assert-never: 1.2.1 - chart.js: 2.9.4 - copy-text-to-clipboard: 3.2.0 - fontfaceobserver: 2.3.0 - fuzzysort: 3.1.0 - svelte-dnd-action: 0.9.50(svelte@5.39.10) - transitivePeerDependencies: - - svelte - - '@sdeverywhere/cli@0.7.38': - dependencies: - '@sdeverywhere/build': 0.3.9 - '@sdeverywhere/compile': 0.7.26 - byline: 5.0.0 - ramda: 0.27.2 - shelljs: 0.10.0 - strip-bom: 5.0.0 - yargs: 17.5.1 - transitivePeerDependencies: - - supports-color - - '@sdeverywhere/compile@0.7.26': - dependencies: - '@sdeverywhere/parse': 0.1.2 - byline: 5.0.0 - csv-parse: 5.3.3 - ramda: 0.27.2 - strip-bom: 5.0.0 - xlsx: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz - - '@sdeverywhere/parse@0.1.2': - dependencies: - antlr4: 4.12.0 - antlr4-vensim: 0.6.3 - assert-never: 1.2.1 - split-string: 6.1.0 - - '@sdeverywhere/plugin-check@0.3.25(@sdeverywhere/build@0.3.9)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10)': - dependencies: - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3) - '@rollup/plugin-replace': 6.0.3(rollup@4.53.3) - '@sdeverywhere/build': 0.3.9 - '@sdeverywhere/check-core': 0.1.7 - '@sdeverywhere/check-ui-shell': 0.2.16(svelte@5.39.10) - '@sdeverywhere/runtime': 0.2.7 - '@sdeverywhere/runtime-async': 0.2.7 - assert-never: 1.2.1 - chokidar: 5.0.0 - picocolors: 1.1.1 - rollup: 4.53.3 - vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - svelte - - terser - - tsx - - yaml - - '@sdeverywhere/runtime-async@0.2.7': - dependencies: - '@sdeverywhere/runtime': 0.2.7 - threads: 1.7.0 - transitivePeerDependencies: - - supports-color - - '@sdeverywhere/runtime@0.2.7': - dependencies: - neverthrow: 2.7.1 - '@standard-schema/spec@1.1.0': {} '@storybook/addon-docs@10.1.11(@types/react@19.2.2)(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': From 1e165d96ded2a4741440be58b81130de04e0c16e Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 11:07:52 -0800 Subject: [PATCH 17/41] build: update lock file to include override-constants test package --- pnpm-lock.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 957e4043..be17dee1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -989,6 +989,27 @@ importers: specifier: ^20.14.8 version: 20.19.19 + tests/integration/override-constants: + dependencies: + '@sdeverywhere/build': + specifier: workspace:* + version: link:../../../packages/build + '@sdeverywhere/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@sdeverywhere/plugin-wasm': + specifier: workspace:* + version: link:../../../packages/plugin-wasm + '@sdeverywhere/plugin-worker': + specifier: workspace:* + version: link:../../../packages/plugin-worker + '@sdeverywhere/runtime': + specifier: workspace:* + version: link:../../../packages/runtime + '@sdeverywhere/runtime-async': + specifier: workspace:* + version: link:../../../packages/runtime-async + tests/integration/override-lookups: dependencies: '@sdeverywhere/build': From ad463e4f14e859e5afd6b6eee20f9cd43f824197 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Sat, 10 Jan 2026 16:11:33 -0800 Subject: [PATCH 18/41] fix: implement constant overrides in C runtime --- packages/cli/src/c/model.c | 38 ++++++- packages/cli/src/c/sde.h | 4 +- packages/compile/src/generate/gen-code-js.js | 1 + packages/runtime/src/wasm-model/wasm-model.ts | 99 +++++++++++++++---- 4 files changed, 121 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 7c48a785..56805da5 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -80,6 +80,39 @@ char* run_model(const char* inputs) { return outputData; } +/** + * Set constant overrides from the given buffers. + * + * The `constantIndices` buffer contains the variable indices and subscript indices + * for each constant to override. The format is: + * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] + * + * The `constantValues` buffer contains the corresponding values for each constant. + */ +void setConstantsFromBuffer(int32_t* constantIndices, double* constantValues) { + if (constantIndices == NULL || constantValues == NULL) { + return; + } + + size_t indexBufferOffset = 0; + size_t valueBufferOffset = 0; + size_t constantCount = (size_t)constantIndices[indexBufferOffset++]; + + for (size_t i = 0; i < constantCount; i++) { + size_t varIndex = (size_t)constantIndices[indexBufferOffset++]; + size_t subCount = (size_t)constantIndices[indexBufferOffset++]; + size_t* subIndices; + if (subCount > 0) { + subIndices = (size_t*)(constantIndices + indexBufferOffset); + indexBufferOffset += subCount; + } else { + subIndices = NULL; + } + double value = constantValues[valueBufferOffset++]; + setConstant(varIndex, subIndices, value); + } +} + /** * Run the model, reading inputs from the given `inputs` buffer, and writing outputs * to the given `outputs` buffer. @@ -103,10 +136,13 @@ char* run_model(const char* inputs) { * (where tN is the last time in the range), the second variable outputs will begin, * and so on. */ -void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices) { +void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices, int32_t* constantIndices, double* constantValues) { outputBuffer = outputs; outputIndexBuffer = outputIndices; initConstants(); + if (constantIndices != NULL && constantValues != NULL) { + setConstantsFromBuffer(constantIndices, constantValues); + } setInputsFromBuffer(inputs); initLevels(); run(); diff --git a/packages/cli/src/c/sde.h b/packages/cli/src/c/sde.h index 4d3e583c..603ec731 100644 --- a/packages/cli/src/c/sde.h +++ b/packages/cli/src/c/sde.h @@ -55,7 +55,8 @@ double getInitialTime(void); double getFinalTime(void); double getSaveper(void); char* run_model(const char* inputs); -void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices); +void setConstantsFromBuffer(int32_t* constantIndices, double* constantValues); +void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices, int32_t* constantIndices, double* constantValues); void run(void); void startOutput(void); void outputVar(double value); @@ -67,6 +68,7 @@ void initLevels(void); void setInputs(const char* inputData); void setInputsFromBuffer(double *inputData); void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints); +void setConstant(size_t varIndex, size_t* subIndices, double value); void evalAux(void); void evalLevels(void); void storeOutputData(void); diff --git a/packages/compile/src/generate/gen-code-js.js b/packages/compile/src/generate/gen-code-js.js index a01a4563..54b72fcb 100644 --- a/packages/compile/src/generate/gen-code-js.js +++ b/packages/compile/src/generate/gen-code-js.js @@ -664,6 +664,7 @@ export default async function () { setTime, setInputs, setLookup, + setConstant, storeOutputs, storeOutput, diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 59f1ca86..2975c7cb 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -33,6 +33,8 @@ class WasmModel implements RunnableModel { private outputIndicesBuffer: WasmBuffer private lookupDataBuffer: WasmBuffer private lookupSubIndicesBuffer: WasmBuffer + private constantIndicesBuffer: WasmBuffer + private constantValuesBuffer: WasmBuffer private readonly wasmSetLookup: ( varIndex: number, @@ -40,8 +42,13 @@ class WasmModel implements RunnableModel { pointsAddress: number, numPoints: number ) => void - private readonly wasmSetConstant: (varIndex: number, subIndicesAddress: number, value: number) => void - private readonly wasmRunModel: (inputsAddress: number, outputsAddress: number, outputIndicesAddress: number) => void + private readonly wasmRunModel: ( + inputsAddress: number, + outputsAddress: number, + outputIndicesAddress: number, + constantIndicesAddress: number, + constantValuesAddress: number + ) => void /** * @param wasmModule The `WasmModule` that provides access to the native functions. @@ -66,8 +73,13 @@ class WasmModel implements RunnableModel { // Make the native functions callable this.wasmSetLookup = wasmModule.cwrap('setLookup', null, ['number', 'number', 'number', 'number']) - this.wasmSetConstant = wasmModule.cwrap('setConstant', null, ['number', 'number', 'number']) - this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, ['number', 'number', 'number']) + this.wasmRunModel = wasmModule.cwrap('runModelWithBuffers', null, [ + 'number', + 'number', + 'number', + 'number', + 'number' + ]) } // from RunnableModel interface @@ -125,30 +137,71 @@ class WasmModel implements RunnableModel { } } - // Apply constant overrides, if provided + // Prepare constant overrides buffer, if provided + let constantIndicesBuffer: WasmBuffer + let constantValuesBuffer: WasmBuffer const constants = params.getConstants() - if (constants !== undefined) { + if (constants !== undefined && constants.length > 0) { + // Calculate the size needed for the constantIndices buffer: + // count (1) + for each constant: varIndex (1) + subCount (1) + subIndices (variable) + let totalIndicesSize = 1 // for count + for (const constantDef of constants) { + const numSubElements = constantDef.varRef.varSpec.subscriptIndices?.length || 0 + totalIndicesSize += 2 + numSubElements // varIndex + subCount + subIndices + } + + // Allocate the constantIndices buffer + if ( + this.constantIndicesBuffer === undefined || + this.constantIndicesBuffer.numElements < totalIndicesSize + ) { + this.constantIndicesBuffer?.dispose() + this.constantIndicesBuffer = createInt32WasmBuffer(this.wasmModule, totalIndicesSize) + } + + // Allocate the constantValues buffer (one value per constant) + const numConstants = constants.length + if (this.constantValuesBuffer === undefined || this.constantValuesBuffer.numElements < numConstants) { + this.constantValuesBuffer?.dispose() + this.constantValuesBuffer = createFloat64WasmBuffer(this.wasmModule, numConstants) + } + + // Build the constantIndices and constantValues buffers + const indicesView = this.constantIndicesBuffer.getArrayView() + const valuesView = this.constantValuesBuffer.getArrayView() + let indicesOffset = 0 + let valuesOffset = 0 + + // Write count + indicesView[indicesOffset++] = numConstants + + // Write each constant's data for (const constantDef of constants) { const varSpec = constantDef.varRef.varSpec const numSubElements = varSpec.subscriptIndices?.length || 0 - let subIndicesAddress: number + // Write varIndex + indicesView[indicesOffset++] = varSpec.varIndex + + // Write subCount + indicesView[indicesOffset++] = numSubElements + + // Write subIndices if (numSubElements > 0) { - // Reuse the lookup sub indices buffer for constants - if (this.lookupSubIndicesBuffer === undefined || this.lookupSubIndicesBuffer.numElements < numSubElements) { - this.lookupSubIndicesBuffer?.dispose() - this.lookupSubIndicesBuffer = createInt32WasmBuffer(this.wasmModule, numSubElements) + for (let i = 0; i < numSubElements; i++) { + indicesView[indicesOffset++] = varSpec.subscriptIndices[i] } - this.lookupSubIndicesBuffer.getArrayView().set(varSpec.subscriptIndices) - subIndicesAddress = this.lookupSubIndicesBuffer.getAddress() - } else { - subIndicesAddress = 0 } - // Call the native `setConstant` function - const varIndex = varSpec.varIndex - this.wasmSetConstant(varIndex, subIndicesAddress, constantDef.value) + // Write value + valuesView[valuesOffset++] = constantDef.value } + + constantIndicesBuffer = this.constantIndicesBuffer + constantValuesBuffer = this.constantValuesBuffer + } else { + constantIndicesBuffer = undefined + constantValuesBuffer = undefined } // Copy the inputs to the `WasmBuffer`. If we don't have an existing `WasmBuffer`, @@ -187,7 +240,9 @@ class WasmModel implements RunnableModel { this.wasmRunModel( this.inputsBuffer?.getAddress() || 0, this.outputsBuffer.getAddress(), - outputIndicesBuffer?.getAddress() || 0 + outputIndicesBuffer?.getAddress() || 0, + constantIndicesBuffer?.getAddress() || 0, + constantValuesBuffer?.getAddress() || 0 ) const elapsed = perfElapsed(t0) @@ -209,6 +264,12 @@ class WasmModel implements RunnableModel { this.outputIndicesBuffer?.dispose() this.outputIndicesBuffer = undefined + this.constantIndicesBuffer?.dispose() + this.constantIndicesBuffer = undefined + + this.constantValuesBuffer?.dispose() + this.constantValuesBuffer = undefined + // TODO: Dispose the `WasmModule` too? } } From eee9844b2eff22ff8f5b79734e1ad3e3eb2480f0 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 13 Jan 2026 16:24:20 -0800 Subject: [PATCH 19/41] fix: move constant override functions before lookup override functions --- packages/cli/src/c/model.c | 68 ++-- packages/cli/src/c/sde.h | 1 - packages/compile/src/generate/gen-code-c.js | 8 +- packages/compile/src/generate/gen-code-js.js | 10 +- packages/runtime/src/_shared/constant-def.ts | 3 +- packages/runtime/src/_shared/index.ts | 2 +- packages/runtime/src/_shared/var-indices.ts | 293 +++++++++--------- .../src/js-model/_mocks/mock-js-model.ts | 14 +- packages/runtime/src/js-model/js-model.ts | 25 +- .../src/runnable-model/base-runnable-model.ts | 6 +- .../buffered-run-model-params.ts | 98 +++--- .../referenced-run-model-params.ts | 32 +- .../src/runnable-model/run-model-options.ts | 24 +- .../src/runnable-model/run-model-params.ts | 12 +- 14 files changed, 297 insertions(+), 299 deletions(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 56805da5..72e98e63 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -80,39 +80,6 @@ char* run_model(const char* inputs) { return outputData; } -/** - * Set constant overrides from the given buffers. - * - * The `constantIndices` buffer contains the variable indices and subscript indices - * for each constant to override. The format is: - * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] - * - * The `constantValues` buffer contains the corresponding values for each constant. - */ -void setConstantsFromBuffer(int32_t* constantIndices, double* constantValues) { - if (constantIndices == NULL || constantValues == NULL) { - return; - } - - size_t indexBufferOffset = 0; - size_t valueBufferOffset = 0; - size_t constantCount = (size_t)constantIndices[indexBufferOffset++]; - - for (size_t i = 0; i < constantCount; i++) { - size_t varIndex = (size_t)constantIndices[indexBufferOffset++]; - size_t subCount = (size_t)constantIndices[indexBufferOffset++]; - size_t* subIndices; - if (subCount > 0) { - subIndices = (size_t*)(constantIndices + indexBufferOffset); - indexBufferOffset += subCount; - } else { - subIndices = NULL; - } - double value = constantValues[valueBufferOffset++]; - setConstant(varIndex, subIndices, value); - } -} - /** * Run the model, reading inputs from the given `inputs` buffer, and writing outputs * to the given `outputs` buffer. @@ -141,7 +108,7 @@ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices outputIndexBuffer = outputIndices; initConstants(); if (constantIndices != NULL && constantValues != NULL) { - setConstantsFromBuffer(constantIndices, constantValues); + setConstantsFromBuffers(constantIndices, constantValues); } setInputsFromBuffer(inputs); initLevels(); @@ -150,6 +117,39 @@ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices outputIndexBuffer = NULL; } +/** + * Set constant overrides from the given buffers. + * + * The `constantIndices` buffer contains the variable indices and subscript indices + * for each constant to override. The format is: + * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] + * + * The `constantValues` buffer contains the corresponding values for each constant. + */ +void setConstantsFromBuffers(int32_t* constantIndices, double* constantValues) { + if (constantIndices == NULL || constantValues == NULL) { + return; + } + + size_t indexBufferOffset = 0; + size_t valueBufferOffset = 0; + size_t constantCount = (size_t)constantIndices[indexBufferOffset++]; + + for (size_t i = 0; i < constantCount; i++) { + size_t varIndex = (size_t)constantIndices[indexBufferOffset++]; + size_t subCount = (size_t)constantIndices[indexBufferOffset++]; + size_t* subIndices; + if (subCount > 0) { + subIndices = (size_t*)(constantIndices + indexBufferOffset); + indexBufferOffset += subCount; + } else { + subIndices = NULL; + } + double value = constantValues[valueBufferOffset++]; + setConstant(varIndex, subIndices, value); + } +} + void run() { #ifdef PERF_TEST clock_gettime(CLOCK_MONOTONIC, &startTime); diff --git a/packages/cli/src/c/sde.h b/packages/cli/src/c/sde.h index 603ec731..d12bb257 100644 --- a/packages/cli/src/c/sde.h +++ b/packages/cli/src/c/sde.h @@ -55,7 +55,6 @@ double getInitialTime(void); double getFinalTime(void); double getSaveper(void); char* run_model(const char* inputs); -void setConstantsFromBuffer(int32_t* constantIndices, double* constantValues); void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices, int32_t* constantIndices, double* constantValues); void run(void); void startOutput(void); diff --git a/packages/compile/src/generate/gen-code-c.js b/packages/compile/src/generate/gen-code-c.js index 43929ea0..22c47bbf 100644 --- a/packages/compile/src/generate/gen-code-c.js +++ b/packages/compile/src/generate/gen-code-c.js @@ -226,14 +226,14 @@ void setInputsFromBuffer(double* inputData) { ${inputsFromBufferImpl()} } -void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) { -${setLookupBody} -} - void setConstant(size_t varIndex, size_t* subIndices, double value) { ${setConstantBody} } +void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) { +${setLookupBody} +} + const char* getHeader() { return "${R.map(varName => varName.replace(/"/g, '\\"'), headerVarNames).join('\\t')}"; } diff --git a/packages/compile/src/generate/gen-code-js.js b/packages/compile/src/generate/gen-code-js.js index 54b72fcb..324c93d1 100644 --- a/packages/compile/src/generate/gen-code-js.js +++ b/packages/compile/src/generate/gen-code-js.js @@ -333,14 +333,14 @@ ${customOutputSection(Model.varIndexInfo(), spec.customOutputs)} return `\ /*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}} -/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) { -${setLookupBody} -} - /*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { ${setConstantBody} } +/*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) { +${setLookupBody} +} + /*export*/ const outputVarIds = [ ${outputVarIdElems} ]; @@ -663,8 +663,8 @@ export default async function () { setTime, setInputs, - setLookup, setConstant, + setLookup, storeOutputs, storeOutput, diff --git a/packages/runtime/src/_shared/constant-def.ts b/packages/runtime/src/_shared/constant-def.ts index 5587a9f0..46ecb805 100644 --- a/packages/runtime/src/_shared/constant-def.ts +++ b/packages/runtime/src/_shared/constant-def.ts @@ -3,7 +3,8 @@ import type { VarRef } from './types' /** - * Specifies the constant value that will be used to override a constant variable. + * Specifies the constant value that will be used to override a constant in a + * generated model. */ export interface ConstantDef { /** The reference that identifies the constant variable to be modified. */ diff --git a/packages/runtime/src/_shared/index.ts b/packages/runtime/src/_shared/index.ts index 3b191450..5011715d 100644 --- a/packages/runtime/src/_shared/index.ts +++ b/packages/runtime/src/_shared/index.ts @@ -4,5 +4,5 @@ export * from './types' export * from './inputs' export * from './outputs' export * from './var-indices' -export * from './lookup-def' export * from './constant-def' +export * from './lookup-def' diff --git a/packages/runtime/src/_shared/var-indices.ts b/packages/runtime/src/_shared/var-indices.ts index a113cc83..9cc6b3e2 100644 --- a/packages/runtime/src/_shared/var-indices.ts +++ b/packages/runtime/src/_shared/var-indices.ts @@ -69,6 +69,151 @@ export function encodeVarIndices(varSpecs: VarSpec[], indicesArray: Int32Array): } } +/** + * Return the lengths of the arrays that are required to store the constant values + * and indices for the given `ConstantDef` instances. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantDefs The `ConstantDef` instances to encode. + */ +export function getEncodedConstantBufferLengths(constantDefs: ConstantDef[]): { + constantIndicesLength: number + constantsLength: number +} { + // The constants buffer includes all constant values for the provided constant overrides + // (added sequentially, one value per constant). The constant indices buffer has the + // following format: + // constant count + // constantN var index + // constantN subscript count + // constantN sub1 index + // constantN sub2 index + // ... + // constantN subM index + // ... (repeat for each constant) + + // Start with one element for the total constant variable count + let constantIndicesLength = 1 + let constantsLength = 0 + + for (const constantDef of constantDefs) { + // Ensure that the var spec has already been resolved + const varSpec = constantDef.varRef.varSpec + if (varSpec === undefined) { + throw new Error('Cannot compute constant buffer lengths until all constant var specs are defined') + } + + // Include one element for the variable index and one for the subscript count + constantIndicesLength += 2 + + // Include one element for each subscript + const subCount = varSpec.subscriptIndices?.length || 0 + constantIndicesLength += subCount + + // Add one element for the constant value + constantsLength += 1 + } + + return { + constantIndicesLength, + constantsLength + } +} + +/** + * Encode constant values and indices to the given arrays. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantDefs The `ConstantDef` instances to encode. + * @param constantIndicesArray The view on the constant indices buffer. + * @param constantsArray The view on the constant values buffer. + */ +export function encodeConstants( + constantDefs: ConstantDef[], + constantIndicesArray: Int32Array, + constantsArray: Float64Array +): void { + // Write the constant variable count + let ci = 0 + constantIndicesArray[ci++] = constantDefs.length + + // Write the indices and values for each constant + let constantDataOffset = 0 + for (const constantDef of constantDefs) { + // Write the constant variable index + const varSpec = constantDef.varRef.varSpec + constantIndicesArray[ci++] = varSpec.varIndex + + // Write the subscript count + const subs = varSpec.subscriptIndices + const subCount = subs?.length || 0 + constantIndicesArray[ci++] = subCount + + // Write the subscript indices + for (let i = 0; i < subCount; i++) { + constantIndicesArray[ci++] = subs[i] + } + + // Write the constant value + constantsArray[constantDataOffset++] = constantDef.value + } +} + +/** + * Decode constant values and indices from the given buffer views and return the + * reconstructed `ConstantDef` instances. + * + * @hidden This is not part of the public API; it is exposed here for use by + * the synchronous and asynchronous model runner implementations. + * + * @param constantIndicesArray The view on the constant indices buffer. + * @param constantsArray The view on the constant values buffer. + */ +export function decodeConstants(constantIndicesArray: Int32Array, constantsArray: Float64Array): ConstantDef[] { + const constantDefs: ConstantDef[] = [] + let ci = 0 + + // Read the constant variable count + const constantCount = constantIndicesArray[ci++] + + // Read the metadata for each variable from the constant indices buffer + for (let i = 0; i < constantCount; i++) { + // Read the constant variable index + const varIndex = constantIndicesArray[ci++] + + // Read the subscript count + const subCount = constantIndicesArray[ci++] + + // Read the subscript indices + const subscriptIndices: number[] = subCount > 0 ? Array(subCount) : undefined + for (let subIndex = 0; subIndex < subCount; subIndex++) { + subscriptIndices[subIndex] = constantIndicesArray[ci++] + } + + // Create a `VarSpec` for the variable + const varSpec: VarSpec = { + varIndex, + subscriptIndices + } + + // Read the constant value + const value = constantsArray[i] + + constantDefs.push({ + varRef: { + varSpec + }, + value + }) + } + + return constantDefs +} + /** * Return the lengths of the arrays that are required to store the lookup data * and indices for the given `LookupDef` instances. @@ -252,151 +397,3 @@ export function decodeLookups(lookupIndicesArray: Int32Array, lookupsArray: Floa return lookupDefs } - -/** - * Return the lengths of the arrays that are required to store the constant values - * and indices for the given `ConstantDef` instances. - * - * @hidden This is not part of the public API; it is exposed here for use by - * the synchronous and asynchronous model runner implementations. - * - * @param constantDefs The `ConstantDef` instances to encode. - */ -export function getEncodedConstantBufferLengths(constantDefs: ConstantDef[]): { - constantIndicesLength: number - constantsLength: number -} { - // The constants buffer includes all constant values for the provided constant overrides - // (added sequentially, one value per constant). The constant indices buffer has the - // following format: - // constant count - // constantN var index - // constantN subscript count - // constantN sub1 index - // constantN sub2 index - // ... - // constantN subM index - // ... (repeat for each constant) - - // Start with one element for the total constant variable count - let constantIndicesLength = 1 - let constantsLength = 0 - - for (const constantDef of constantDefs) { - // Ensure that the var spec has already been resolved - const varSpec = constantDef.varRef.varSpec - if (varSpec === undefined) { - throw new Error('Cannot compute constant buffer lengths until all constant var specs are defined') - } - - // Include one element for the variable index and one for the subscript count - constantIndicesLength += 2 - - // Include one element for each subscript - const subCount = varSpec.subscriptIndices?.length || 0 - constantIndicesLength += subCount - - // Add one element for the constant value - constantsLength += 1 - } - - return { - constantIndicesLength, - constantsLength - } -} - -/** - * Encode constant values and indices to the given arrays. - * - * @hidden This is not part of the public API; it is exposed here for use by - * the synchronous and asynchronous model runner implementations. - * - * @param constantDefs The `ConstantDef` instances to encode. - * @param constantIndicesArray The view on the constant indices buffer. - * @param constantsArray The view on the constant values buffer. - */ -export function encodeConstants( - constantDefs: ConstantDef[], - constantIndicesArray: Int32Array, - constantsArray: Float64Array -): void { - // Write the constant variable count - let ci = 0 - constantIndicesArray[ci++] = constantDefs.length - - // Write the indices and values for each constant - let constantDataOffset = 0 - for (const constantDef of constantDefs) { - // Write the constant variable index - const varSpec = constantDef.varRef.varSpec - constantIndicesArray[ci++] = varSpec.varIndex - - // Write the subscript count - const subs = varSpec.subscriptIndices - const subCount = subs?.length || 0 - constantIndicesArray[ci++] = subCount - - // Write the subscript indices - for (let i = 0; i < subCount; i++) { - constantIndicesArray[ci++] = subs[i] - } - - // Write the constant value - constantsArray[constantDataOffset++] = constantDef.value - } -} - -/** - * Decode constant values and indices from the given buffer views and return the - * reconstructed `ConstantDef` instances. - * - * @hidden This is not part of the public API; it is exposed here for use by - * the synchronous and asynchronous model runner implementations. - * - * @param constantIndicesArray The view on the constant indices buffer. - * @param constantsArray The view on the constant values buffer. - */ -export function decodeConstants( - constantIndicesArray: Int32Array, - constantsArray: Float64Array -): ConstantDef[] { - const constantDefs: ConstantDef[] = [] - let ci = 0 - - // Read the constant variable count - const constantCount = constantIndicesArray[ci++] - - // Read the metadata for each variable from the constant indices buffer - for (let i = 0; i < constantCount; i++) { - // Read the constant variable index - const varIndex = constantIndicesArray[ci++] - - // Read the subscript count - const subCount = constantIndicesArray[ci++] - - // Read the subscript indices - const subscriptIndices: number[] = subCount > 0 ? Array(subCount) : undefined - for (let subIndex = 0; subIndex < subCount; subIndex++) { - subscriptIndices[subIndex] = constantIndicesArray[ci++] - } - - // Create a `VarSpec` for the variable - const varSpec: VarSpec = { - varIndex, - subscriptIndices - } - - // Read the constant value - const value = constantsArray[i] - - constantDefs.push({ - varRef: { - varSpec - }, - value - }) - } - - return constantDefs -} diff --git a/packages/runtime/src/js-model/_mocks/mock-js-model.ts b/packages/runtime/src/js-model/_mocks/mock-js-model.ts index cfe48526..72544e7b 100644 --- a/packages/runtime/src/js-model/_mocks/mock-js-model.ts +++ b/packages/runtime/src/js-model/_mocks/mock-js-model.ts @@ -110,22 +110,22 @@ export class MockJsModel implements JsModel { } // from JsModel interface - setLookup(varSpec: VarSpec, points: Float64Array | undefined): void { + setConstant(varSpec: VarSpec, value: number): void { const varId = this.varIdForSpec(varSpec) if (varId === undefined) { - throw new Error(`No lookup variable found for spec ${varSpec}`) + throw new Error(`No constant variable found for spec ${varSpec}`) } - const numPoints = points ? points.length / 2 : 0 - this.lookups.set(varId, new JsModelLookup(numPoints, points)) + this.vars.set(varId, value) } // from JsModel interface - setConstant(varSpec: VarSpec, value: number): void { + setLookup(varSpec: VarSpec, points: Float64Array | undefined): void { const varId = this.varIdForSpec(varSpec) if (varId === undefined) { - throw new Error(`No constant variable found for spec ${varSpec}`) + throw new Error(`No lookup variable found for spec ${varSpec}`) } - this.vars.set(varId, value) + const numPoints = points ? points.length / 2 : 0 + this.lookups.set(varId, new JsModelLookup(numPoints, points)) } // from JsModel interface diff --git a/packages/runtime/src/js-model/js-model.ts b/packages/runtime/src/js-model/js-model.ts index be2263ec..abb19a74 100644 --- a/packages/runtime/src/js-model/js-model.ts +++ b/packages/runtime/src/js-model/js-model.ts @@ -50,10 +50,10 @@ export interface JsModel { setInputs(inputValue: (index: number) => number): void /** @hidden */ - setLookup(varSpec: VarSpec, points: Float64Array | undefined): void + setConstant(varSpec: VarSpec, value: number): void /** @hidden */ - setConstant(varSpec: VarSpec, value: number): void + setLookup(varSpec: VarSpec, points: Float64Array | undefined): void /** @hidden */ storeOutputs(storeValue: (value: number) => void): void @@ -111,8 +111,8 @@ export function initJsModel(model: JsModel): RunnableModel { inputs, outputs, options?.outputIndices, - options?.lookups, options?.constants, + options?.lookups, undefined ) } @@ -129,8 +129,8 @@ function runJsModel( inputs: Float64Array | undefined, outputs: Float64Array, outputIndices: Int32Array | undefined, - lookups: LookupDef[] | undefined, constants: ConstantDef[] | undefined, + lookups: LookupDef[] | undefined, stopAfterTime: number | undefined ): void { // Initialize time with the required `INITIAL TIME` control variable @@ -148,13 +148,6 @@ function runJsModel( // Initialize constants to their default values model.initConstants() - // Apply lookup overrides, if provided - if (lookups !== undefined) { - for (const lookupDef of lookups) { - model.setLookup(lookupDef.varRef.varSpec, lookupDef.points) - } - } - // Apply constant overrides, if provided if (constants !== undefined) { for (const constantDef of constants) { @@ -162,9 +155,17 @@ function runJsModel( } } + // Apply lookup overrides, if provided + if (lookups !== undefined) { + for (const lookupDef of lookups) { + model.setLookup(lookupDef.varRef.varSpec, lookupDef.points) + } + } + if (inputs?.length > 0) { // Set the user-defined input values. This needs to happen after `initConstants` - // since the input values will override the default constant values. + // and the `setConstant` override calls, since the input values will override + // the default and custom constant values. model.setInputs(index => inputs[index]) } diff --git a/packages/runtime/src/runnable-model/base-runnable-model.ts b/packages/runtime/src/runnable-model/base-runnable-model.ts index 438721f8..c32ee63f 100644 --- a/packages/runtime/src/runnable-model/base-runnable-model.ts +++ b/packages/runtime/src/runnable-model/base-runnable-model.ts @@ -14,8 +14,8 @@ export type OnRunModelFunc = ( outputs: Float64Array, options?: { outputIndices?: Int32Array - lookups?: LookupDef[] constants?: ConstantDef[] + lookups?: LookupDef[] } ) => void @@ -98,8 +98,8 @@ export class BaseRunnableModel implements RunnableModel { const t0 = perfNow() this.onRunModel?.(inputsArray, outputsArray, { outputIndices: outputIndicesArray, - lookups: params.getLookups(), - constants: params.getConstants() + constants: params.getConstants(), + lookups: params.getLookups() }) const elapsed = perfElapsed(t0) diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.ts index 1f5d1424..0f30b97c 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.ts @@ -75,10 +75,10 @@ export class BufferedRunModelParams implements RunModelParams { * inputs * outputs * outputIndices - * lookups (data) - * lookupIndices * constants (values) * constantIndices + * lookups (data) + * lookupIndices */ private encoded: ArrayBuffer @@ -100,18 +100,18 @@ export class BufferedRunModelParams implements RunModelParams { /** The output indices section of the `encoded` buffer. */ private readonly outputIndices = new Int32Section() - /** The lookup data section of the `encoded` buffer. */ - private readonly lookups = new Float64Section() - - /** The lookup indices section of the `encoded` buffer. */ - private readonly lookupIndices = new Int32Section() - /** The constant values section of the `encoded` buffer. */ private readonly constants = new Float64Section() /** The constant indices section of the `encoded` buffer. */ private readonly constantIndices = new Int32Section() + /** The lookup data section of the `encoded` buffer. */ + private readonly lookups = new Float64Section() + + /** The lookup indices section of the `encoded` buffer. */ + private readonly lookupIndices = new Int32Section() + /** * @param listing The model listing that is used to locate a variable that is referenced by * name or identifier. If undefined, variables cannot be referenced by name or identifier, @@ -212,25 +212,25 @@ export class BufferedRunModelParams implements RunModelParams { } // from RunModelParams interface - getLookups(): LookupDef[] | undefined { - if (this.lookupIndices.lengthInElements === 0) { + getConstants(): ConstantDef[] | undefined { + if (this.constantIndices.lengthInElements === 0) { return undefined } - // Reconstruct the `LookupDef` instances using the data from the lookup data and + // Reconstruct the `ConstantDef` instances using the data from the constant values and // indices buffers - return decodeLookups(this.lookupIndices.view, this.lookups.view) + return decodeConstants(this.constantIndices.view, this.constants.view) } // from RunModelParams interface - getConstants(): ConstantDef[] | undefined { - if (this.constantIndices.lengthInElements === 0) { + getLookups(): LookupDef[] | undefined { + if (this.lookupIndices.lengthInElements === 0) { return undefined } - // Reconstruct the `ConstantDef` instances using the data from the constant values and + // Reconstruct the `LookupDef` instances using the data from the lookup data and // indices buffers - return decodeConstants(this.constantIndices.view, this.constants.view) + return decodeLookups(this.lookupIndices.view, this.lookups.view) } // from RunModelParams interface @@ -284,26 +284,6 @@ export class BufferedRunModelParams implements RunModelParams { outputIndicesLengthInElements = 0 } - // Determine the number of elements in the lookup data and indices sections - let lookupsLengthInElements: number - let lookupIndicesLengthInElements: number - if (options?.lookups !== undefined && options.lookups.length > 0) { - // Resolve the `varSpec` for each `LookupDef`. If the variable can be resolved, this - // will fill in the `varSpec` for the `LookupDef`, otherwise it will throw an error. - for (const lookupDef of options.lookups) { - resolveVarRef(this.listing, lookupDef.varRef, 'lookup') - } - - // Compute the required lengths - const encodedLengths = getEncodedLookupBufferLengths(options.lookups) - lookupsLengthInElements = encodedLengths.lookupsLength - lookupIndicesLengthInElements = encodedLengths.lookupIndicesLength - } else { - // Don't use the lookup data and indices buffers when lookup overrides are not provided - lookupsLengthInElements = 0 - lookupIndicesLengthInElements = 0 - } - // Determine the number of elements in the constant values and indices sections let constantsLengthInElements: number let constantIndicesLengthInElements: number @@ -324,6 +304,26 @@ export class BufferedRunModelParams implements RunModelParams { constantIndicesLengthInElements = 0 } + // Determine the number of elements in the lookup data and indices sections + let lookupsLengthInElements: number + let lookupIndicesLengthInElements: number + if (options?.lookups !== undefined && options.lookups.length > 0) { + // Resolve the `varSpec` for each `LookupDef`. If the variable can be resolved, this + // will fill in the `varSpec` for the `LookupDef`, otherwise it will throw an error. + for (const lookupDef of options.lookups) { + resolveVarRef(this.listing, lookupDef.varRef, 'lookup') + } + + // Compute the required lengths + const encodedLengths = getEncodedLookupBufferLengths(options.lookups) + lookupsLengthInElements = encodedLengths.lookupsLength + lookupIndicesLengthInElements = encodedLengths.lookupIndicesLength + } else { + // Don't use the lookup data and indices buffers when lookup overrides are not provided + lookupsLengthInElements = 0 + lookupIndicesLengthInElements = 0 + } + // Compute the byte offset and byte length of each section let byteOffset = 0 function section(kind: 'float64' | 'int32', lengthInElements: number): number { @@ -343,10 +343,10 @@ export class BufferedRunModelParams implements RunModelParams { const inputsOffsetInBytes = section('float64', inputsLengthInElements) const outputsOffsetInBytes = section('float64', outputsLengthInElements) const outputIndicesOffsetInBytes = section('int32', outputIndicesLengthInElements) - const lookupsOffsetInBytes = section('float64', lookupsLengthInElements) - const lookupIndicesOffsetInBytes = section('int32', lookupIndicesLengthInElements) const constantsOffsetInBytes = section('float64', constantsLengthInElements) const constantIndicesOffsetInBytes = section('int32', constantIndicesLengthInElements) + const lookupsOffsetInBytes = section('float64', lookupsLengthInElements) + const lookupIndicesOffsetInBytes = section('int32', lookupIndicesLengthInElements) // Get the total byte length const requiredLengthInBytes = byteOffset @@ -373,14 +373,14 @@ export class BufferedRunModelParams implements RunModelParams { headerView[headerIndex++] = outputsLengthInElements headerView[headerIndex++] = outputIndicesOffsetInBytes headerView[headerIndex++] = outputIndicesLengthInElements - headerView[headerIndex++] = lookupsOffsetInBytes - headerView[headerIndex++] = lookupsLengthInElements - headerView[headerIndex++] = lookupIndicesOffsetInBytes - headerView[headerIndex++] = lookupIndicesLengthInElements headerView[headerIndex++] = constantsOffsetInBytes headerView[headerIndex++] = constantsLengthInElements headerView[headerIndex++] = constantIndicesOffsetInBytes headerView[headerIndex++] = constantIndicesLengthInElements + headerView[headerIndex++] = lookupsOffsetInBytes + headerView[headerIndex++] = lookupsLengthInElements + headerView[headerIndex++] = lookupIndicesOffsetInBytes + headerView[headerIndex++] = lookupIndicesLengthInElements // Update the views // TODO: We can avoid recreating the views every time if buffer and section offset/length @@ -389,10 +389,10 @@ export class BufferedRunModelParams implements RunModelParams { this.extras.update(this.encoded, extrasOffsetInBytes, extrasLengthInElements) this.outputs.update(this.encoded, outputsOffsetInBytes, outputsLengthInElements) this.outputIndices.update(this.encoded, outputIndicesOffsetInBytes, outputIndicesLengthInElements) - this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) - this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) this.constants.update(this.encoded, constantsOffsetInBytes, constantsLengthInElements) this.constantIndices.update(this.encoded, constantIndicesOffsetInBytes, constantIndicesLengthInElements) + this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) + this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) // Copy the input values into the internal buffer // TODO: Throw an error if inputs.length is less than number of inputs declared @@ -416,15 +416,15 @@ export class BufferedRunModelParams implements RunModelParams { encodeVarIndices(outputVarSpecs, this.outputIndices.view) } - // Copy the lookup data and indices into the internal buffers, if needed - if (lookupIndicesLengthInElements > 0) { - encodeLookups(options.lookups, this.lookupIndices.view, this.lookups.view) - } - // Copy the constant values and indices into the internal buffers, if needed if (constantIndicesLengthInElements > 0) { encodeConstants(options.constants, this.constantIndices.view, this.constants.view) } + + // Copy the lookup data and indices into the internal buffers, if needed + if (lookupIndicesLengthInElements > 0) { + encodeLookups(options.lookups, this.lookupIndices.view, this.lookups.view) + } } /** diff --git a/packages/runtime/src/runnable-model/referenced-run-model-params.ts b/packages/runtime/src/runnable-model/referenced-run-model-params.ts index 59d4e74a..5a1df168 100644 --- a/packages/runtime/src/runnable-model/referenced-run-model-params.ts +++ b/packages/runtime/src/runnable-model/referenced-run-model-params.ts @@ -20,8 +20,8 @@ export class ReferencedRunModelParams implements RunModelParams { private outputs: Outputs private outputsLengthInElements = 0 private outputIndicesLengthInElements = 0 - private lookups: LookupDef[] private constants: ConstantDef[] + private lookups: LookupDef[] /** * @param listing The model listing that is used to locate a variable that is referenced by @@ -113,18 +113,18 @@ export class ReferencedRunModelParams implements RunModelParams { } // from RunModelParams interface - getLookups(): LookupDef[] | undefined { - if (this.lookups !== undefined && this.lookups.length > 0) { - return this.lookups + getConstants(): ConstantDef[] | undefined { + if (this.constants !== undefined && this.constants.length > 0) { + return this.constants } else { return undefined } } // from RunModelParams interface - getConstants(): ConstantDef[] | undefined { - if (this.constants !== undefined && this.constants.length > 0) { - return this.constants + getLookups(): LookupDef[] | undefined { + if (this.lookups !== undefined && this.lookups.length > 0) { + return this.lookups } else { return undefined } @@ -156,16 +156,8 @@ export class ReferencedRunModelParams implements RunModelParams { this.inputs = inputs this.outputs = outputs this.outputsLengthInElements = outputs.varIds.length * outputs.seriesLength - this.lookups = options?.lookups this.constants = options?.constants - - if (this.lookups) { - // Resolve the `varSpec` for each `LookupDef`. If the variable can be resolved, this - // will fill in the `varSpec` for the `LookupDef`, otherwise it will throw an error. - for (const lookupDef of this.lookups) { - resolveVarRef(this.listing, lookupDef.varRef, 'lookup') - } - } + this.lookups = options?.lookups if (this.constants) { // Resolve the `varSpec` for each `ConstantDef`. If the variable can be resolved, this @@ -175,6 +167,14 @@ export class ReferencedRunModelParams implements RunModelParams { } } + if (this.lookups) { + // Resolve the `varSpec` for each `LookupDef`. If the variable can be resolved, this + // will fill in the `varSpec` for the `LookupDef`, otherwise it will throw an error. + for (const lookupDef of this.lookups) { + resolveVarRef(this.listing, lookupDef.varRef, 'lookup') + } + } + // See if the output indices are needed const outputVarSpecs = outputs.varSpecs if (outputVarSpecs !== undefined && outputVarSpecs.length > 0) { diff --git a/packages/runtime/src/runnable-model/run-model-options.ts b/packages/runtime/src/runnable-model/run-model-options.ts index 26871de4..28edd03f 100644 --- a/packages/runtime/src/runnable-model/run-model-options.ts +++ b/packages/runtime/src/runnable-model/run-model-options.ts @@ -6,6 +6,18 @@ import type { ConstantDef, LookupDef } from '../_shared' * Additional options that can be passed to a `runModel` call to influence the model run. */ export interface RunModelOptions { + /** + * If defined, override the values for the specified constant variables. + * + * Note that constant overrides do not persist after the `runModel` call. Because + * `initConstants` is called at the beginning of each `runModel` call, all constants + * are reset to their default values before each model run. If you want to override + * constants, you must provide them in the options for each `runModel` call. To + * reset constants to their original values, simply stop passing them in the options + * (or pass an empty array). + */ + constants?: ConstantDef[] + /** * If defined, override the data for the specified lookups and/or data variables. * @@ -19,16 +31,4 @@ export interface RunModelOptions { * is not changing, you do not need to supply it with every `runModel` call. */ lookups?: LookupDef[] - - /** - * If defined, override the values for the specified constant variables. - * - * Note that UNLIKE lookups (which persist across calls), constant overrides do - * NOT persist after the `runModel` call. Because `initConstants` is called at - * the beginning of each `runModel` call, all constants are reset to their default - * values. If you want to override constants, you must provide them in the options - * for each `runModel` call. To reset constants to their original values, simply - * stop passing them in the options (or pass an empty array). - */ - constants?: ConstantDef[] } diff --git a/packages/runtime/src/runnable-model/run-model-params.ts b/packages/runtime/src/runnable-model/run-model-params.ts index f97b638d..6c935b96 100644 --- a/packages/runtime/src/runnable-model/run-model-params.ts +++ b/packages/runtime/src/runnable-model/run-model-params.ts @@ -74,18 +74,18 @@ export interface RunModelParams { */ storeOutputs(array: Float64Array): void - /** - * Return an array containing lookup overrides, or undefined if no lookups were passed to - * the latest `runModel` call. - */ - getLookups(): LookupDef[] | undefined - /** * Return an array containing constant overrides, or undefined if no constants were passed * to the latest `runModel` call. */ getConstants(): ConstantDef[] | undefined + /** + * Return an array containing lookup overrides, or undefined if no lookups were passed to + * the latest `runModel` call. + */ + getLookups(): LookupDef[] | undefined + /** * Return the elapsed time (in milliseconds) of the model run. */ From b893f0c4147dd4f3962a1b395aa28cbc72acc49f Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 13 Jan 2026 16:28:35 -0800 Subject: [PATCH 20/41] docs: format PLAN.md --- PLAN.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/PLAN.md b/PLAN.md index 418f8344..c27263fb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -49,6 +49,7 @@ export function createConstantDef(varRef: VarRef, value: number): ConstantDef { ``` **Update**: `packages/runtime/src/_shared/index.ts` - add export: + ```typescript export * from './constant-def' ``` @@ -58,6 +59,7 @@ export * from './constant-def' **File**: `packages/runtime/src/runnable-model/run-model-options.ts` Add `constants` field: + ```typescript import type { ConstantDef, LookupDef } from '../_shared' @@ -83,6 +85,7 @@ export interface RunModelOptions { **File**: `packages/build/src/_shared/model-spec.ts` Add to `ModelSpec` interface (after `customLookups`): + ```typescript /** * Whether to allow constants to be overridden at runtime using `setConstant`. @@ -100,6 +103,7 @@ customConstants?: boolean | VarName[] ``` Add to `ResolvedModelSpec` interface: + ```typescript /** * Whether to allow constants to be overridden at runtime using `setConstant`. @@ -112,6 +116,7 @@ customConstants: boolean | VarName[] **File**: `packages/compile/src/generate/gen-code-js.js` Add `setConstantImpl` function (after `setLookupImpl`, around line 560): + ```javascript function setConstantImpl(varIndexInfo, customConstants) { // Emit case statements for all const variables that can be overridden at runtime @@ -145,6 +150,7 @@ function setConstantImpl(varIndexInfo, customConstants) { ``` Add `setConstant` function generation in `emitIOCode()` (after `setLookup`, around line 750): + ```javascript // Generate the setConstant function let setConstantBody @@ -178,6 +184,7 @@ ${setConstantBody} **File**: `packages/compile/src/generate/gen-code-c.js` Add `setConstantImpl` function (similar pattern as JS): + ```javascript function setConstantImpl(varIndexInfo, customConstants) { let overrideAllowed @@ -209,6 +216,7 @@ function setConstantImpl(varIndexInfo, customConstants) { ``` Add `setConstant` function generation in `emitIOCode()`: + ```c void setConstant(size_t varIndex, size_t* subIndices, double value) { switch (varIndex) { @@ -224,23 +232,26 @@ void setConstant(size_t varIndex, size_t* subIndices, double value) { **File**: `packages/runtime/src/js-model/js-model.ts` Add to `JsModel` interface (around line 53): + ```typescript /** @hidden */ setConstant(varSpec: VarSpec, value: number): void ``` Update `runJsModel` function signature (around line 121): + ```typescript function runJsModel( model: JsModel, // ... other params ... lookups: LookupDef[] | undefined, - constants: ConstantDef[] | undefined, // NEW + constants: ConstantDef[] | undefined, // NEW stopAfterTime: number | undefined ): void ``` Add constant override logic after lookup overrides (after line 150): + ```typescript // Apply constant overrides, if provided if (constants !== undefined) { @@ -251,13 +262,14 @@ if (constants !== undefined) { ``` Update call in `initJsModel` (around line 111): + ```typescript onRunModel: (inputs, outputs, options) => { runJsModel( model, // ... other params ... options?.lookups, - options?.constants, // NEW + options?.constants, // NEW undefined ) } @@ -268,6 +280,7 @@ onRunModel: (inputs, outputs, options) => { **File**: `packages/runtime/src/wasm-model/wasm-model.ts` Add native function wrapper (around line 67): + ```typescript private readonly wasmSetConstant: ( varIndex: number, @@ -277,11 +290,13 @@ private readonly wasmSetConstant: ( ``` Initialize in constructor: + ```typescript this.wasmSetConstant = wasmModule.cwrap('setConstant', null, ['number', 'number', 'number']) ``` Add constant override logic in `runModel` after lookup overrides (around line 130): + ```typescript // Apply constant overrides, if provided const constants = params.getConstants() @@ -293,8 +308,7 @@ if (constants !== undefined) { if (numSubElements > 0) { // Reuse the lookup sub indices buffer - if (this.lookupSubIndicesBuffer === undefined || - this.lookupSubIndicesBuffer.numElements < numSubElements) { + if (this.lookupSubIndicesBuffer === undefined || this.lookupSubIndicesBuffer.numElements < numSubElements) { this.lookupSubIndicesBuffer?.dispose() this.lookupSubIndicesBuffer = createInt32WasmBuffer(this.wasmModule, numSubElements) } @@ -314,6 +328,7 @@ if (constants !== undefined) { **File**: `packages/runtime/src/runnable-model/run-model-params.ts` Add method: + ```typescript /** * Return an array containing constant overrides, or undefined if no constants @@ -352,6 +367,7 @@ Add three new functions for encoding/decoding constants (similar to lookup encod **File**: `packages/runtime/src/runnable-model/buffered-run-model-params.ts` 1. Add two new buffer sections: + ```typescript /** The constant values section of the `encoded` buffer. */ private readonly constants = new Float64Section() @@ -361,8 +377,9 @@ Add three new functions for encoding/decoding constants (similar to lookup encod ``` 2. Update header length constant (line 16): + ```typescript - const headerLengthInElements = 20 // Was 16, add 4 for constants sections + const headerLengthInElements = 20 // Was 16, add 4 for constants sections ``` 3. In `updateFromParams()`: @@ -384,6 +401,7 @@ Add three new functions for encoding/decoding constants (similar to lookup encod **File**: `packages/runtime/src/runnable-model/base-runnable-model.ts` Update `OnRunModelFunc` type (line 12): + ```typescript export type OnRunModelFunc = ( inputs: Float64Array | undefined, @@ -391,17 +409,18 @@ export type OnRunModelFunc = ( options?: { outputIndices?: Int32Array lookups?: LookupDef[] - constants?: ConstantDef[] // NEW + constants?: ConstantDef[] // NEW } ) => void ``` Update `runModel` call (line 98): + ```typescript this.onRunModel?.(inputsArray, outputsArray, { outputIndices: outputIndicesArray, lookups: params.getLookups(), - constants: params.getConstants() // NEW + constants: params.getConstants() // NEW }) ``` @@ -430,6 +449,7 @@ Add test cases for `setConstant` generation similar to existing `setLookup` test ## Files Summary ### New Files (5): + 1. `packages/runtime/src/_shared/constant-def.ts` 2. `tests/integration/override-constants/override-constants.mdl` 3. `tests/integration/override-constants/sde.config.js` @@ -437,6 +457,7 @@ Add test cases for `setConstant` generation similar to existing `setLookup` test 5. `tests/integration/override-constants/package.json` ### Modified Files (12): + 1. `packages/runtime/src/_shared/index.ts` - export ConstantDef 2. `packages/runtime/src/_shared/var-indices.ts` - add constant encoding/decoding functions 3. `packages/runtime/src/runnable-model/run-model-options.ts` - add constants field @@ -455,11 +476,13 @@ Add test cases for `setConstant` generation similar to existing `setLookup` test This implementation has some similarities and differences compared to override lookups: ### Similarities: + 1. **Encoding/decoding for async support** - both need buffer encoding for worker threads 2. **VarRef resolution** - both use the same varRef pattern for identifying variables 3. **Subscript handling** - both support subscripted variables (1D, 2D arrays) ### Differences (Constants are simpler): + 1. **Scalar values** - just `number`, not `Float64Array` of points 2. **No persistence** - constants reset on every run, no state to manage 3. **Simpler C signature** - `(varIndex, subIndices, value)` vs `(varIndex, subIndices, points, numPoints)` From 33001b2fd304172aac2f0b4085e9229e57da57b4 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 13 Jan 2026 16:38:08 -0800 Subject: [PATCH 21/41] fix: correct order of constants and lookups in updateFromEncodedBuffer --- .../src/runnable-model/buffered-run-model-params.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.ts index 0f30b97c..ca60502d 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.ts @@ -458,14 +458,14 @@ export class BufferedRunModelParams implements RunModelParams { const outputsLengthInElements = headerView[headerIndex++] const outputIndicesOffsetInBytes = headerView[headerIndex++] const outputIndicesLengthInElements = headerView[headerIndex++] - const lookupsOffsetInBytes = headerView[headerIndex++] - const lookupsLengthInElements = headerView[headerIndex++] - const lookupIndicesOffsetInBytes = headerView[headerIndex++] - const lookupIndicesLengthInElements = headerView[headerIndex++] const constantsOffsetInBytes = headerView[headerIndex++] const constantsLengthInElements = headerView[headerIndex++] const constantIndicesOffsetInBytes = headerView[headerIndex++] const constantIndicesLengthInElements = headerView[headerIndex++] + const lookupsOffsetInBytes = headerView[headerIndex++] + const lookupsLengthInElements = headerView[headerIndex++] + const lookupIndicesOffsetInBytes = headerView[headerIndex++] + const lookupIndicesLengthInElements = headerView[headerIndex++] // Verify that the buffer is long enough to contain all sections const extrasLengthInBytes = extrasLengthInElements * Float64Array.BYTES_PER_ELEMENT @@ -495,9 +495,9 @@ export class BufferedRunModelParams implements RunModelParams { this.inputs.update(this.encoded, inputsOffsetInBytes, inputsLengthInElements) this.outputs.update(this.encoded, outputsOffsetInBytes, outputsLengthInElements) this.outputIndices.update(this.encoded, outputIndicesOffsetInBytes, outputIndicesLengthInElements) - this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) - this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) this.constants.update(this.encoded, constantsOffsetInBytes, constantsLengthInElements) this.constantIndices.update(this.encoded, constantIndicesOffsetInBytes, constantIndicesLengthInElements) + this.lookups.update(this.encoded, lookupsOffsetInBytes, lookupsLengthInElements) + this.lookupIndices.update(this.encoded, lookupIndicesOffsetInBytes, lookupIndicesLengthInElements) } } From 35174ce62d736f1c24b4e52b49812a6e7bbea7cc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 13 Jan 2026 16:41:21 -0800 Subject: [PATCH 22/41] test: add tests for constant override encode/decode --- .../buffered-run-model-params.spec.ts | 55 ++++++++++++++++++- .../referenced-run-model-params.spec.ts | 50 ++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.spec.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.spec.ts index 2a71e57d..cfbb758d 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.spec.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' -import { Outputs, createLookupDef, type LookupDef } from '../_shared' +import { Outputs, createConstantDef, createLookupDef, type ConstantDef, type LookupDef } from '../_shared' import { BufferedRunModelParams } from './buffered-run-model-params' import { ModelListing } from '../model-listing' @@ -330,7 +330,58 @@ describe('BufferedRunModelParams', () => { expect(outputs.runTimeInMillis).toBe(42) }) - it('should copy lookups', () => { + it('should copy constant overrides', () => { + const listing = new ModelListing(JSON.parse(listingJson)) + + const inputs = [1, 2, 3] + const outputs = new Outputs(['_x', '_y'], 2000, 2002, 1) + + const constants: ConstantDef[] = [ + // Reference the first variable by name + createConstantDef({ varName: 'A' }, 42), + // Reference the second variable by ID + createConstantDef({ varId: '_b' }, 100) + ] + + const runnerParams = new BufferedRunModelParams(listing) + const workerParams = new BufferedRunModelParams(listing) + + // Run once without providing constants + runnerParams.updateFromParams(inputs, outputs) + workerParams.updateFromEncodedBuffer(runnerParams.getEncodedBuffer()) + + // Verify that constants array is undefined + expect(workerParams.getConstants()).toBeUndefined() + + // Run again with constants + runnerParams.updateFromParams(inputs, outputs, { constants }) + workerParams.updateFromEncodedBuffer(runnerParams.getEncodedBuffer()) + + // Verify that constants array on the worker side contains the expected values + expect(workerParams.getConstants()).toEqual([ + createConstantDef({ varSpec: { varIndex: 1 } }, 42), + createConstantDef({ varSpec: { varIndex: 2 } }, 100) + ]) + + // Run again without constants + runnerParams.updateFromParams(inputs, outputs) + workerParams.updateFromEncodedBuffer(runnerParams.getEncodedBuffer()) + + // Verify that constants array is undefined + expect(workerParams.getConstants()).toBeUndefined() + + // Run again with a constant referenced by spec + const constantBySpec = createConstantDef({ varSpec: listing.varSpecs.get('_a') }, 999) + runnerParams.updateFromParams(inputs, outputs, { + constants: [constantBySpec] + }) + workerParams.updateFromEncodedBuffer(runnerParams.getEncodedBuffer()) + + // Verify that constants array on the worker side contains the expected values + expect(workerParams.getConstants()).toEqual([constantBySpec]) + }) + + it('should copy lookup overrides', () => { const listing = new ModelListing(JSON.parse(listingJson)) const inputs = [1, 2, 3] diff --git a/packages/runtime/src/runnable-model/referenced-run-model-params.spec.ts b/packages/runtime/src/runnable-model/referenced-run-model-params.spec.ts index 7abe1e56..a49e0841 100644 --- a/packages/runtime/src/runnable-model/referenced-run-model-params.spec.ts +++ b/packages/runtime/src/runnable-model/referenced-run-model-params.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' -import { Outputs, createLookupDef, type LookupDef } from '../_shared' +import { Outputs, createConstantDef, createLookupDef, type ConstantDef, type LookupDef } from '../_shared' import { ReferencedRunModelParams } from './referenced-run-model-params' import { ModelListing } from '../model-listing' @@ -222,7 +222,53 @@ describe('ReferencedRunModelParams', () => { expect(outputs.getSeriesForVar('_y').points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) }) - it('should copy lookups', () => { + it('should copy constant overrides', () => { + const listing = new ModelListing(JSON.parse(listingJson)) + + const inputs = [1, 2, 3] + const outputs = new Outputs(['_x', '_y'], 2000, 2002, 1) + + const constants: ConstantDef[] = [ + // Reference the first variable by name + createConstantDef({ varName: 'A' }, 42), + // Reference the second variable by ID + createConstantDef({ varId: '_b' }, 100) + ] + + const params = new ReferencedRunModelParams(listing) + + // Run once without providing constants + params.updateFromParams(inputs, outputs) + + // Verify that constants array is undefined + expect(params.getConstants()).toBeUndefined() + + // Run again with constants + params.updateFromParams(inputs, outputs, { constants }) + + // Verify that constants array contains the expected values + expect(params.getConstants()).toEqual([ + createConstantDef({ varName: 'A', varSpec: { varIndex: 1 } }, 42), + createConstantDef({ varId: '_b', varSpec: { varIndex: 2 } }, 100) + ]) + + // Run again without constants + params.updateFromParams(inputs, outputs) + + // Verify that constants array is undefined + expect(params.getConstants()).toBeUndefined() + + // Run again with a constant referenced by spec + const constantBySpec = createConstantDef({ varSpec: listing.varSpecs.get('_a') }, 999) + params.updateFromParams(inputs, outputs, { + constants: [constantBySpec] + }) + + // Verify that constants array contains the expected values + expect(params.getConstants()).toEqual([constantBySpec]) + }) + + it('should copy lookup overrides', () => { const listing = new ModelListing(JSON.parse(listingJson)) const inputs = [1, 2, 3] From 10c222ed7f5cee9b479bcf759e13acb655ee235d Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 14 Jan 2026 17:56:40 -0800 Subject: [PATCH 23/41] build: update vitest and related packages to fix storybook tests --- package.json | 7 +- packages/check-ui-shell/vitest.config.js | 3 +- pnpm-lock.yaml | 265 +++++++++++++++-------- 3 files changed, 183 insertions(+), 92 deletions(-) diff --git a/package.json b/package.json index 8f4d950b..08233519 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ "@playwright/test": "^1.56.1", "@typescript-eslint/eslint-plugin": "^8.46.0", "@typescript-eslint/parser": "^8.46.0", - "@vitest/browser": "^4.0.16", - "@vitest/coverage-v8": "^4.0.16", + "@vitest/browser": "^4.0.17", + "@vitest/browser-playwright": "^4.0.17", + "@vitest/coverage-v8": "^4.0.17", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-eslint-comments": "^3.2.0", @@ -42,7 +43,7 @@ "typedoc": "0.25.0", "typedoc-plugin-markdown": "3.16.0", "typescript": "^5.2.2", - "vitest": "^4.0.16" + "vitest": "^4.0.17" }, "pnpm": { "peerDependencyRules": { diff --git a/packages/check-ui-shell/vitest.config.js b/packages/check-ui-shell/vitest.config.js index 96dc28b3..b5e831ae 100644 --- a/packages/check-ui-shell/vitest.config.js +++ b/packages/check-ui-shell/vitest.config.js @@ -2,6 +2,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig, mergeConfig } from 'vitest/config' +import { playwright } from '@vitest/browser-playwright' import { storybookTest } from '@storybook/addon-vitest/vitest-plugin' @@ -36,7 +37,7 @@ export default defineConfig(() => name: 'storybook', browser: { enabled: true, - provider: 'playwright', + provider: playwright(), headless: true, instances: [{ browser: 'chromium' }] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be17dee1..0e8c01d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,11 +21,14 @@ importers: specifier: ^8.46.0 version: 8.46.0(eslint@9.37.0)(typescript@5.2.2) '@vitest/browser': - specifier: ^4.0.16 - version: 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + specifier: ^4.0.17 + version: 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/browser-playwright': + specifier: ^4.0.17 + version: 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) '@vitest/coverage-v8': - specifier: ^4.0.16 - version: 4.0.16(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + specifier: ^4.0.17 + version: 4.0.17(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(vitest@4.0.17) eslint: specifier: ^9.37.0 version: 9.37.0 @@ -72,8 +75,8 @@ importers: specifier: ^5.2.2 version: 5.2.2 vitest: - specifier: ^4.0.16 - version: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + specifier: ^4.0.17 + version: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) examples/hello-world: dependencies: @@ -85,7 +88,7 @@ importers: version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.26(@sdeverywhere/build@packages+build)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-worker': specifier: ^0.2.11 version: link:../../packages/plugin-worker @@ -245,7 +248,7 @@ importers: version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.26(@sdeverywhere/build@packages+build)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -334,7 +337,7 @@ importers: version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.18 - version: link:../../packages/plugin-check + version: 0.3.26(@sdeverywhere/build@packages+build)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-wasm': specifier: ^0.2.6 version: link:../../packages/plugin-wasm @@ -358,7 +361,7 @@ importers: version: link:../../packages/cli '@sdeverywhere/plugin-check': specifier: ^0.3.21 - version: link:../../packages/plugin-check + version: 0.3.26(@sdeverywhere/build@packages+build)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10) '@sdeverywhere/plugin-config': specifier: ^0.2.8 version: link:../../packages/plugin-config @@ -560,7 +563,7 @@ importers: version: 5.0.10(@storybook/svelte@10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10))(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.10)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) '@storybook/addon-vitest': specifier: ^10.1.11 - version: 10.1.11(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(@vitest/runner@4.0.16)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + version: 10.1.11(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.17) '@storybook/svelte': specifier: ^10.1.11 version: 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10) @@ -724,7 +727,7 @@ importers: version: link:../check-core '@sdeverywhere/check-ui-shell': specifier: ^0.2.16 - version: link:../check-ui-shell + version: 0.2.17(svelte@5.39.10) '@sdeverywhere/runtime': specifier: ^0.2.7 version: link:../runtime @@ -1608,6 +1611,24 @@ packages: cpu: [x64] os: [win32] + '@sdeverywhere/check-core@0.1.7': + resolution: {integrity: sha512-JH0TAgc9l4ZZHHCcJ9GHqa6bnekgFnQ593J4Aqk2bAEMAbLRsgHjPgWEB24yMVufSzF7gUeVyltTKbdD9YvAOg==} + + '@sdeverywhere/check-ui-shell@0.2.17': + resolution: {integrity: sha512-srWjxD+BN2sLtOjizG++M72pEJ1qcroUEMxs0GtJ4uLVBcyueP7BAXN6ylE7bH1u+jVmZnuC/bzSuCUXht2yEA==} + + '@sdeverywhere/plugin-check@0.3.26': + resolution: {integrity: sha512-m9pcIXtxMAoUMzEim4GeLeUgk5ppBMnlDMddYQhPMDxTNcDz1bmdRsPfNVYhw1r4ulLOLGJI3mtNPB29teeVvA==} + hasBin: true + peerDependencies: + '@sdeverywhere/build': ^0.3.7 + + '@sdeverywhere/runtime-async@0.2.7': + resolution: {integrity: sha512-dr7L9smMbaVJH2NOJNx8q6898J6DYuyO2oEunhUpKBy704R05RF0NsYC+XJNkJRwRuE0t0pulVfXIZzr95GPeg==} + + '@sdeverywhere/runtime@0.2.7': + resolution: {integrity: sha512-iEaMn4qMojbBgcv17w+yIInUOdH3cZuOLx4LOk2VFGLjJm1Xv2/FDkGMxJdqvR9rMwI4bg9WtxCcmFLLKMgWNA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1865,16 +1886,22 @@ packages: resolution: {integrity: sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/browser@4.0.16': - resolution: {integrity: sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==} + '@vitest/browser-playwright@4.0.17': + resolution: {integrity: sha512-CE9nlzslHX6Qz//MVrjpulTC9IgtXTbJ+q7Rx1HD+IeSOWv4NHIRNHPA6dB4x01d9paEqt+TvoqZfmgq40DxEQ==} + peerDependencies: + playwright: '*' + vitest: 4.0.17 + + '@vitest/browser@4.0.17': + resolution: {integrity: sha512-cgf2JZk2fv5or3efmOrRJe1V9Md89BPgz4ntzbf84yAb+z2hW6niaGFinl9aFzPZ1q3TGfWZQWZ9gXTFThs2Qw==} peerDependencies: - vitest: 4.0.16 + vitest: 4.0.17 - '@vitest/coverage-v8@4.0.16': - resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==} + '@vitest/coverage-v8@4.0.17': + resolution: {integrity: sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==} peerDependencies: - '@vitest/browser': 4.0.16 - vitest: 4.0.16 + '@vitest/browser': 4.0.17 + vitest: 4.0.17 peerDependenciesMeta: '@vitest/browser': optional: true @@ -1882,8 +1909,8 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.0.16': - resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + '@vitest/expect@4.0.17': + resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} @@ -1896,8 +1923,8 @@ packages: vite: optional: true - '@vitest/mocker@4.0.16': - resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} + '@vitest/mocker@4.0.17': + resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -1910,26 +1937,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.0.16': - resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + '@vitest/pretty-format@4.0.17': + resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} - '@vitest/runner@4.0.16': - resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} + '@vitest/runner@4.0.17': + resolution: {integrity: sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==} - '@vitest/snapshot@4.0.16': - resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} + '@vitest/snapshot@4.0.17': + resolution: {integrity: sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.0.16': - resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} + '@vitest/spy@4.0.17': + resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.0.16': - resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@vitest/utils@4.0.17': + resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2659,10 +2686,6 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} @@ -3591,18 +3614,18 @@ packages: vite: optional: true - vitest@4.0.16: - resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} + vitest@4.0.17: + resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.16 - '@vitest/browser-preview': 4.0.16 - '@vitest/browser-webdriverio': 4.0.16 - '@vitest/ui': 4.0.16 + '@vitest/browser-playwright': 4.0.17 + '@vitest/browser-preview': 4.0.17 + '@vitest/browser-webdriverio': 4.0.17 + '@vitest/ui': 4.0.17 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -4107,6 +4130,68 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true + '@sdeverywhere/check-core@0.1.7': + dependencies: + ajv: 8.12.0 + assert-never: 1.2.1 + neverthrow: 4.3.1 + yaml: 2.2.2 + + '@sdeverywhere/check-ui-shell@0.2.17(svelte@5.39.10)': + dependencies: + '@fortawesome/free-regular-svg-icons': 7.1.0 + '@fortawesome/free-solid-svg-icons': 7.1.0 + '@juggle/resize-observer': 3.4.0 + '@sdeverywhere/check-core': 0.1.7 + assert-never: 1.2.1 + chart.js: 2.9.4 + copy-text-to-clipboard: 3.2.0 + fontfaceobserver: 2.3.0 + fuzzysort: 3.1.0 + svelte-dnd-action: 0.9.50(svelte@5.39.10) + transitivePeerDependencies: + - svelte + + '@sdeverywhere/plugin-check@0.3.26(@sdeverywhere/build@packages+build)(@types/node@20.19.19)(sass@1.97.2)(svelte@5.39.10)': + dependencies: + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3) + '@rollup/plugin-replace': 6.0.3(rollup@4.53.3) + '@sdeverywhere/build': link:packages/build + '@sdeverywhere/check-core': 0.1.7 + '@sdeverywhere/check-ui-shell': 0.2.17(svelte@5.39.10) + '@sdeverywhere/runtime': 0.2.7 + '@sdeverywhere/runtime-async': 0.2.7 + assert-never: 1.2.1 + chokidar: 5.0.0 + picocolors: 1.1.1 + rollup: 4.53.3 + vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - svelte + - terser + - tsx + - yaml + + '@sdeverywhere/runtime-async@0.2.7': + dependencies: + '@sdeverywhere/runtime': 0.2.7 + threads: 1.7.0 + transitivePeerDependencies: + - supports-color + + '@sdeverywhere/runtime@0.2.7': + dependencies: + neverthrow: 2.7.1 + '@standard-schema/spec@1.1.0': {} '@storybook/addon-docs@10.1.11(@types/react@19.2.2)(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': @@ -4150,15 +4235,16 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@storybook/addon-vitest@10.1.11(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(@vitest/runner@4.0.16)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@storybook/addon-vitest@10.1.11(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.17)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@vitest/browser': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/runner': 4.0.16 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/browser-playwright': 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/runner': 4.0.17 + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) transitivePeerDependencies: - react - react-dom @@ -4428,16 +4514,29 @@ snapshots: '@typescript-eslint/types': 8.46.0 eslint-visitor-keys: 4.2.1 - '@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/browser-playwright@4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17)': dependencies: - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/utils': 4.0.16 + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + playwright: 1.56.1 + tinyrainbow: 3.0.3 + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17)': + dependencies: + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + '@vitest/utils': 4.0.17 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -4445,24 +4544,21 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@4.0.16(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/coverage-v8@4.0.17(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(vitest@4.0.17)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.16 + '@vitest/utils': 4.0.17 ast-v8-to-istanbul: 0.3.10 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 magicast: 0.5.1 obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) optionalDependencies: - '@vitest/browser': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) - transitivePeerDependencies: - - supports-color + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) '@vitest/expect@3.2.4': dependencies: @@ -4472,12 +4568,12 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.0.16': + '@vitest/expect@4.0.17': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 chai: 6.2.2 tinyrainbow: 3.0.3 @@ -4485,13 +4581,13 @@ snapshots: dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) - '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': dependencies: - '@vitest/spy': 4.0.16 + '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -4501,18 +4597,18 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.0.16': + '@vitest/pretty-format@4.0.17': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.16': + '@vitest/runner@4.0.17': dependencies: - '@vitest/utils': 4.0.16 + '@vitest/utils': 4.0.17 pathe: 2.0.3 - '@vitest/snapshot@4.0.16': + '@vitest/snapshot@4.0.17': dependencies: - '@vitest/pretty-format': 4.0.16 + '@vitest/pretty-format': 4.0.17 magic-string: 0.30.21 pathe: 2.0.3 @@ -4520,7 +4616,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.0.16': {} + '@vitest/spy@4.0.17': {} '@vitest/utils@3.2.4': dependencies: @@ -4528,9 +4624,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.0.16': + '@vitest/utils@4.0.17': dependencies: - '@vitest/pretty-format': 4.0.16 + '@vitest/pretty-format': 4.0.17 tinyrainbow: 3.0.3 acorn-jsx@5.3.2(acorn@8.15.0): @@ -5230,14 +5326,6 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -6093,15 +6181,15 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) - vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2): + vitest@4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2): dependencies: - '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/pretty-format': 4.0.16 - '@vitest/runner': 4.0.16 - '@vitest/snapshot': 4.0.16 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 + '@vitest/expect': 4.0.17 + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + '@vitest/pretty-format': 4.0.17 + '@vitest/runner': 4.0.17 + '@vitest/snapshot': 4.0.17 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 es-module-lexer: 1.7.0 expect-type: 1.2.2 magic-string: 0.30.21 @@ -6117,6 +6205,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.19 + '@vitest/browser-playwright': 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) transitivePeerDependencies: - jiti - less From eb9a780c7cb05c563e1faf9581ddad5e6cd9e121 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 14 Jan 2026 17:59:24 -0800 Subject: [PATCH 24/41] test: add tests for constant override handling in model runners --- packages/runtime-async/tests/runner.spec.ts | 63 +++++++++++++++--- .../src/js-model/_mocks/mock-js-model.ts | 16 +++-- .../synchronous-model-runner.spec.ts | 66 ++++++++++++++++--- .../src/wasm-model/_mocks/mock-wasm-module.ts | 48 +++++++++++++- 4 files changed, 169 insertions(+), 24 deletions(-) diff --git a/packages/runtime-async/tests/runner.spec.ts b/packages/runtime-async/tests/runner.spec.ts index 21298127..4bbb3364 100644 --- a/packages/runtime-async/tests/runner.spec.ts +++ b/packages/runtime-async/tests/runner.spec.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { ModelRunner } from '@sdeverywhere/runtime' -import { ModelListing, createInputValue, createLookupDef } from '@sdeverywhere/runtime' +import { ModelListing, createConstantDef, createInputValue, createLookupDef } from '@sdeverywhere/runtime' import { spawnAsyncModelRunner } from '../src/runner' @@ -31,6 +31,14 @@ const listingJson = ` { "id": "_output_2_data", "index": 5 + }, + { + "id": "_constant_1", + "index": 6 + }, + { + "id": "_constant_2", + "index": 7 } ] } @@ -58,9 +66,12 @@ function createMockJsModel() { finalTime: endTime, outputVarIds: ['_output_1', '_output_2'], listingJson: \`${listingJson}\`, - onEvalAux: (vars, lookups) => { + onEvalAux: (vars, constants, lookups) => { const time = vars.get('_time') - if (lookups.size > 0) { + // Get constant values, defaulting to 1 and 4 + const constant1 = constants?.get('_constant_1') ?? 1 + const constant2 = constants?.get('_constant_2') ?? 4 + if (lookups && lookups.size > 0) { const lookup1 = lookups.get('_output_1_data') const lookup2 = lookups.get('_output_2_data') // expect(lookup1).toBeDefined() @@ -68,8 +79,8 @@ function createMockJsModel() { vars.set('_output_1', lookup1.getValueForX(time, 'interpolate')) vars.set('_output_2', lookup2.getValueForX(time, 'interpolate')) } else { - vars.set('_output_1', time - startTime + 1) - vars.set('_output_2', time - startTime + 4) + vars.set('_output_1', time - startTime + constant1) + vars.set('_output_2', time - startTime + constant2) vars.set('_x', time - startTime + 7) } } @@ -93,8 +104,11 @@ async function createMockWasmModule() { finalTime: endTime, outputVarIds: ['_output_1', '_output_2'], listingJson: \`${listingJson}\`, - onRunModel: (inputs, outputs, lookups, outputIndices) => { - if (lookups.size > 0) { + onRunModel: (inputs, outputs, constants, lookups, outputIndices) => { + // Get constant values, defaulting to 1 and 4 + const constant1 = constants?.get('_constant_1') ?? 1 + const constant2 = constants?.get('_constant_2') ?? 4 + if (lookups && lookups.size > 0) { // Pretend that outputs are derived from lookup data const lookup1 = lookups.get('_output_1_data') const lookup2 = lookups.get('_output_2_data') @@ -107,10 +121,10 @@ async function createMockWasmModule() { } else { if (outputIndices === undefined) { // Store 3 values for the _output_1, and 3 for _output_2 - outputs.set([1, 2, 3, 4, 5, 6]) + outputs.set([constant1, constant1 + 1, constant1 + 2, constant2, constant2 + 1, constant2 + 2]) } else { // Store 3 values for each of the three variables - outputs.set([7, 8, 9, 4, 5, 6, 1, 2, 3]) + outputs.set([7, 8, 9, constant2, constant2 + 1, constant2 + 2, constant1, constant1 + 1, constant1 + 2]) } } } @@ -210,6 +224,37 @@ describe.each([ expect(outputs.getSeriesForVar('_output_2')!.points).toEqual(lookup2Points) }) + it('should run the model (with constant overrides)', async () => { + const inputs = [createInputValue('_input_1', 7), createInputValue('_input_2', 8), createInputValue('_input_3', 9)] + let outputs = runner.createOutputs() + + // Run once without constant overrides + outputs = await runner.runModel(inputs, outputs) + + // Verify that outputs contain the original values + expect(outputs.getSeriesForVar('_output_1')!.points).toEqual([p(2000, 1), p(2001, 2), p(2002, 3)]) + expect(outputs.getSeriesForVar('_output_2')!.points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) + + // Run again, this time with constant overrides + outputs = await runner.runModel(inputs, outputs, { + constants: [ + createConstantDef({ varId: '_constant_1' }, 100), + createConstantDef({ varId: '_constant_2' }, 400) + ] + }) + + // Verify that outputs contain the values using the overridden constants + expect(outputs.getSeriesForVar('_output_1')!.points).toEqual([p(2000, 100), p(2001, 101), p(2002, 102)]) + expect(outputs.getSeriesForVar('_output_2')!.points).toEqual([p(2000, 400), p(2001, 401), p(2002, 402)]) + + // Run again without constant overrides + outputs = await runner.runModel(inputs, outputs) + + // Verify that the constant overrides are NOT in effect (they do NOT persist like lookups) + expect(outputs.getSeriesForVar('_output_1')!.points).toEqual([p(2000, 1), p(2001, 2), p(2002, 3)]) + expect(outputs.getSeriesForVar('_output_2')!.points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) + }) + it('should run the model in a worker (when output var specs are included)', async () => { const listing = new ModelListing(JSON.parse(listingJson)) const inputs = [7, 8, 9] diff --git a/packages/runtime/src/js-model/_mocks/mock-js-model.ts b/packages/runtime/src/js-model/_mocks/mock-js-model.ts index 72544e7b..a84185ab 100644 --- a/packages/runtime/src/js-model/_mocks/mock-js-model.ts +++ b/packages/runtime/src/js-model/_mocks/mock-js-model.ts @@ -10,7 +10,11 @@ import { JsModelLookup } from '../js-model-lookup' * @hidden This type is not part of the public API; it is exposed only for use in * tests in the runtime-async package. */ -export type OnEvalAux = (vars: Map, lookups: Map) => void +export type OnEvalAux = ( + vars: Map, + constants: Map | undefined, + lookups: Map +) => void /** * @hidden This type is not part of the public API; it is exposed only for use in @@ -35,6 +39,7 @@ export class MockJsModel implements JsModel { private readonly finalTime: number private readonly vars: Map = new Map() + private readonly constants: Map = new Map() private readonly lookups: Map = new Map() private fns: JsModelFunctions @@ -115,7 +120,7 @@ export class MockJsModel implements JsModel { if (varId === undefined) { throw new Error(`No constant variable found for spec ${varSpec}`) } - this.vars.set(varId, value) + this.constants.set(varId, value) } // from JsModel interface @@ -145,14 +150,17 @@ export class MockJsModel implements JsModel { } // from JsModel interface - initConstants(): void {} + initConstants(): void { + // Clear all constant overrides (they don't persist across runs) + this.constants.clear() + } // from JsModel interface initLevels(): void {} // from JsModel interface evalAux(): void { - this.onEvalAux?.(this.vars, this.lookups) + this.onEvalAux?.(this.vars, this.constants.size > 0 ? this.constants : undefined, this.lookups) } // from JsModel interface diff --git a/packages/runtime/src/model-runner/synchronous-model-runner.spec.ts b/packages/runtime/src/model-runner/synchronous-model-runner.spec.ts index 84ba456a..60289aea 100644 --- a/packages/runtime/src/model-runner/synchronous-model-runner.spec.ts +++ b/packages/runtime/src/model-runner/synchronous-model-runner.spec.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { createInputValue, createLookupDef } from '../_shared' +import { createConstantDef, createInputValue, createLookupDef } from '../_shared' import { ModelListing } from '../model-listing' @@ -39,6 +39,14 @@ const listingJson = ` { "id": "_x", "index": 5 + }, + { + "id": "_constant_1", + "index": 6 + }, + { + "id": "_constant_2", + "index": 7 } ] } @@ -50,9 +58,12 @@ function createMockJsModel(): MockJsModel { finalTime: endTime, outputVarIds: ['_output_1', '_output_2'], listingJson, - onEvalAux: (vars, lookups) => { + onEvalAux: (vars, constants, lookups) => { const time = vars.get('_time') - if (lookups.size > 0) { + // Get constant values, defaulting to 1 and 4 + const constant1 = constants?.get('_constant_1') ?? 1 + const constant2 = constants?.get('_constant_2') ?? 4 + if (lookups && lookups.size > 0) { const lookup1 = lookups.get('_output_1_data') const lookup2 = lookups.get('_output_2_data') expect(lookup1).toBeDefined() @@ -60,8 +71,8 @@ function createMockJsModel(): MockJsModel { vars.set('_output_1', lookup1.getValueForX(time, 'interpolate')) vars.set('_output_2', lookup2.getValueForX(time, 'interpolate')) } else { - vars.set('_output_1', time - startTime + 1) - vars.set('_output_2', time - startTime + 4) + vars.set('_output_1', time - startTime + constant1) + vars.set('_output_2', time - startTime + constant2) vars.set('_x', time - startTime + 7) } } @@ -74,13 +85,17 @@ function createMockWasmModule(): MockWasmModule { finalTime: endTime, outputVarIds: ['_output_1', '_output_2'], listingJson, - onRunModel: (inputs, outputs, lookups, outputIndices) => { + onRunModel: (inputs, outputs, constants, lookups, outputIndices) => { // Verify inputs if (inputs.length > 0) { expect(inputs).toEqual(new Float64Array([7, 8, 9])) } - if (lookups.size > 0) { + // Get constant values, defaulting to 1 and 4 + const constant1 = constants?.get('_constant_1') ?? 1 + const constant2 = constants?.get('_constant_2') ?? 4 + + if (lookups && lookups.size > 0) { // Pretend that outputs are derived from lookup data const lookup1 = lookups.get('_output_1_data') const lookup2 = lookups.get('_output_2_data') @@ -93,7 +108,7 @@ function createMockWasmModule(): MockWasmModule { } else { if (outputIndices === undefined) { // Store 3 values for the _output_1, and 3 for _output_2 - outputs.set([1, 2, 3, 4, 5, 6]) + outputs.set([constant1, constant1 + 1, constant1 + 2, constant2, constant2 + 1, constant2 + 2]) } else { // Verify output indices expect(outputIndices).toEqual( @@ -109,7 +124,7 @@ function createMockWasmModule(): MockWasmModule { ]) ) // Store 3 values for each of the three variables - outputs.set([7, 8, 9, 4, 5, 6, 1, 2, 3]) + outputs.set([7, 8, 9, constant2, constant2 + 1, constant2 + 2, constant1, constant1 + 1, constant1 + 2]) } } } @@ -173,6 +188,39 @@ describe.each([ expect(outOutputs.getSeriesForVar('_output_2').points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) }) + it('should run the model (with constant overrides)', async () => { + const inputs = [createInputValue('_input_1', 7), createInputValue('_input_2', 8), createInputValue('_input_3', 9)] + let outputs = runner.createOutputs() + + // Run once without constant overrides + outputs = await runner.runModel(inputs, outputs) + + // Verify that outputs contain the original values + expect(outputs.getSeriesForVar('_output_1').points).toEqual([p(2000, 1), p(2001, 2), p(2002, 3)]) + expect(outputs.getSeriesForVar('_output_2').points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) + + // Run again, this time with constant overrides + outputs = await runner.runModel(inputs, outputs, { + constants: [ + // Reference the first constant by name (adds 100 instead of 1) + createConstantDef({ varName: 'constant 1' }, 100), + // Reference the second constant by ID (adds 400 instead of 4) + createConstantDef({ varId: '_constant_2' }, 400) + ] + }) + + // Verify that outputs contain the values using the overridden constants + expect(outputs.getSeriesForVar('_output_1').points).toEqual([p(2000, 100), p(2001, 101), p(2002, 102)]) + expect(outputs.getSeriesForVar('_output_2').points).toEqual([p(2000, 400), p(2001, 401), p(2002, 402)]) + + // Run again without constant overrides + outputs = await runner.runModel(inputs, outputs) + + // Verify that the constant overrides are NOT in effect (they do NOT persist like lookups) + expect(outputs.getSeriesForVar('_output_1').points).toEqual([p(2000, 1), p(2001, 2), p(2002, 3)]) + expect(outputs.getSeriesForVar('_output_2').points).toEqual([p(2000, 4), p(2001, 5), p(2002, 6)]) + }) + it('should run the model (with lookup overrides)', async () => { const inputs = [createInputValue('_input_1', 7), createInputValue('_input_2', 8), createInputValue('_input_3', 9)] let outputs = runner.createOutputs() diff --git a/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts b/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts index 5c3674c4..4eca2fcd 100644 --- a/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts +++ b/packages/runtime/src/wasm-model/_mocks/mock-wasm-module.ts @@ -12,6 +12,7 @@ import type { WasmModule } from '../wasm-module' export type OnRunModel = ( inputs: Float64Array, outputs: Float64Array, + constants: Map | undefined, lookups: Map, outputIndices?: Int32Array ) => void @@ -48,6 +49,7 @@ export class MockWasmModule implements WasmModule { private readonly allocs: Map = new Map() private readonly lookups: Map = new Map() + private readonly constants: Map = new Map() public readonly onRunModel: OnRunModel @@ -104,11 +106,53 @@ export class MockWasmModule implements WasmModule { this.lookups.set(varId, new JsModelLookup(numPoints, points)) } case 'runModelWithBuffers': - return (inputsAddress: number, outputsAddress: number, outputIndicesAddress: number) => { + return ( + inputsAddress: number, + outputsAddress: number, + outputIndicesAddress: number, + constantIndicesAddress: number, + constantValuesAddress: number + ) => { const inputs = this.getHeapView('float64', inputsAddress) as Float64Array const outputs = this.getHeapView('float64', outputsAddress) as Float64Array const outputIndices = this.getHeapView('int32', outputIndicesAddress) as Int32Array - this.onRunModel(inputs, outputs, this.lookups, outputIndices) + + // Decode constant buffers if provided + this.constants.clear() + if (constantIndicesAddress !== 0 && constantValuesAddress !== 0) { + const constantIndices = this.getHeapView('int32', constantIndicesAddress) as Int32Array + const constantValues = this.getHeapView('float64', constantValuesAddress) as Float64Array + + // Read count + const numConstants = constantIndices[0] + let indicesOffset = 1 + let valuesOffset = 0 + + // Decode each constant + for (let i = 0; i < numConstants; i++) { + const varIndex = constantIndices[indicesOffset++] + const subCount = constantIndices[indicesOffset++] + // TODO: We skip subscript indices for now, but we should test them here too + indicesOffset += subCount + + const varId = this.varIdForSpec({ varIndex }) + if (varId) { + this.constants.set(varId, constantValues[valuesOffset++]) + } + } + } + + // Run the model + this.onRunModel( + inputs, + outputs, + this.constants.size > 0 ? this.constants : undefined, + this.lookups, + outputIndices + ) + + // Clear constants after the run (they don't persist) + this.constants.clear() } default: throw new Error(`Unhandled call to cwrap with function name '${fname}'`) From 99235227bc1060c6573a514c184dda10fe597360 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 14 Jan 2026 20:40:42 -0800 Subject: [PATCH 25/41] test: add tests for setConstant to mirror what was done for setLookup --- .../compile/src/generate/gen-code-c.spec.ts | 58 +++++++++++++++++ .../compile/src/generate/gen-code-js.spec.ts | 63 +++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/packages/compile/src/generate/gen-code-c.spec.ts b/packages/compile/src/generate/gen-code-c.spec.ts index bcfb8f4f..ff8a5fd3 100644 --- a/packages/compile/src/generate/gen-code-c.spec.ts +++ b/packages/compile/src/generate/gen-code-c.spec.ts @@ -21,6 +21,7 @@ function readInlineModelAndGenerateC( directDataSpec?: DirectDataSpec inputVarNames?: string[] outputVarNames?: string[] + customConstants?: boolean | string[] customLookups?: boolean | string[] customOutputs?: boolean | string[] } @@ -33,6 +34,7 @@ function readInlineModelAndGenerateC( const spec = { inputVarNames: opts?.inputVarNames, outputVarNames: opts?.outputVarNames, + customConstants: opts?.customConstants, customLookups: opts?.customLookups, customOutputs: opts?.customOutputs } @@ -267,6 +269,10 @@ void setInputsFromBuffer(double* inputData) { _input = inputData[0]; } +void setConstant(size_t varIndex, size_t* subIndices, double value) { + fprintf(stderr, "The setConstant function was not enabled for the generated model. Set the customConstants property in the spec/config file to allow for overriding constants at runtime.\\n"); +} + void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints) { Lookup** pLookup = NULL; switch (varIndex) { @@ -359,6 +365,58 @@ void storeOutput(size_t varIndex, size_t* subIndices) { `) }) + it('should generate setConstant that reports error when customConstants is disabled', () => { + const mdl = ` + DimA: A1, A2 ~~| + x[DimA] = 10, 20 ~~| + y = 42 ~~| + INITIAL TIME = 0 ~~| + FINAL TIME = 2 ~~| + TIME STEP = 1 ~~| + SAVEPER = 1 ~~| + ` + const code = readInlineModelAndGenerateC(mdl, { + inputVarNames: [], + outputVarNames: ['x[A1]', 'y'] + }) + expect(code).toMatch(`\ +void setConstant(size_t varIndex, size_t* subIndices, double value) { + fprintf(stderr, "The setConstant function was not enabled for the generated model. Set the customConstants property in the spec/config file to allow for overriding constants at runtime.\\n"); +}`) + }) + + it('should generate setConstant that includes a subset of cases when customConstants is an array', () => { + const mdl = ` + DimA: A1, A2 ~~| + x[DimA] = 10, 20 ~~| + y = 42 ~~| + z = 99 ~~| + INITIAL TIME = 0 ~~| + FINAL TIME = 2 ~~| + TIME STEP = 1 ~~| + SAVEPER = 1 ~~| + ` + const code = readInlineModelAndGenerateC(mdl, { + inputVarNames: [], + outputVarNames: ['x[A1]', 'y', 'z'], + customConstants: ['x[A1]', 'z'] + }) + expect(code).toMatch(`\ +void setConstant(size_t varIndex, size_t* subIndices, double value) { + switch (varIndex) { + case 5: + _x[subIndices[0]] = value; + break; + case 7: + _z = value; + break; + default: + fprintf(stderr, "No constant found for var index %zu in setConstant\\n", varIndex); + break; + } +}`) + }) + it('should generate setLookup that reports error when customLookups is disabled', () => { const mdl = ` x = 1 ~~| diff --git a/packages/compile/src/generate/gen-code-js.spec.ts b/packages/compile/src/generate/gen-code-js.spec.ts index 8bd59ca6..c2058b65 100644 --- a/packages/compile/src/generate/gen-code-js.spec.ts +++ b/packages/compile/src/generate/gen-code-js.spec.ts @@ -22,6 +22,7 @@ function readInlineModelAndGenerateJS( inputVarNames?: string[] outputVarNames?: string[] bundleListing?: boolean + customConstants?: boolean | string[] customLookups?: boolean | string[] customOutputs?: boolean | string[] } @@ -35,6 +36,7 @@ function readInlineModelAndGenerateJS( inputVarNames: opts?.inputVarNames, outputVarNames: opts?.outputVarNames, bundleListing: opts?.bundleListing, + customConstants: opts?.customConstants, customLookups: opts?.customLookups, customOutputs: opts?.customOutputs } @@ -454,6 +456,10 @@ function evalAux0() { _input = valueAtIndex(0); } +/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { + throw new Error('The setConstant function was not enabled for the generated model. Set the customConstants property in the spec/config file to allow for overriding constants at runtime.'); +} + /*export*/ function setLookup(varSpec /*: VarSpec*/, points /*: Float64Array | undefined*/) { if (!varSpec) { throw new Error('Got undefined varSpec in setLookup'); @@ -693,6 +699,7 @@ export default async function () { setTime, setInputs, + setConstant, setLookup, storeOutputs, @@ -723,6 +730,62 @@ export default async function () { expect(code).toMatch(`/*export*/ const modelListing = undefined;`) }) + it('should generate setConstant that throws error when customConstants is disabled', () => { + const mdl = ` + DimA: A1, A2 ~~| + x[DimA] = 10, 20 ~~| + y = 42 ~~| + INITIAL TIME = 0 ~~| + FINAL TIME = 2 ~~| + TIME STEP = 1 ~~| + SAVEPER = 1 ~~| + ` + const code = readInlineModelAndGenerateJS(mdl, { + inputVarNames: [], + outputVarNames: ['x[A1]', 'y'] + }) + expect(code).toMatch(`\ +/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { + throw new Error('The setConstant function was not enabled for the generated model. Set the customConstants property in the spec/config file to allow for overriding constants at runtime.'); +}`) + }) + + it('should generate setConstant that includes a subset of cases when customConstants is an array', () => { + const mdl = ` + DimA: A1, A2 ~~| + x[DimA] = 10, 20 ~~| + y = 42 ~~| + z = 99 ~~| + INITIAL TIME = 0 ~~| + FINAL TIME = 2 ~~| + TIME STEP = 1 ~~| + SAVEPER = 1 ~~| + ` + const code = readInlineModelAndGenerateJS(mdl, { + inputVarNames: [], + outputVarNames: ['x[A1]', 'y', 'z'], + customConstants: ['x[A1]', 'z'] + }) + expect(code).toMatch(`\ +/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { + if (!varSpec) { + throw new Error('Got undefined varSpec in setConstant'); + } + const varIndex = varSpec.varIndex; + const subs = varSpec.subscriptIndices; + switch (varIndex) { + case 5: + _x[subs[0]] = value; + break; + case 7: + _z = value; + break; + default: + throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); + } +}`) + }) + it('should generate setLookup that throws error when customLookups is disabled', () => { const mdl = ` x = 1 ~~| From bb0aaa8c9d1f8a152d4eda9b0cf9a68f2a6dd8cc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 14 Jan 2026 20:40:57 -0800 Subject: [PATCH 26/41] test: add tests for constant buffer encode/decode --- .../runtime/src/_shared/var-indices.spec.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/runtime/src/_shared/var-indices.spec.ts b/packages/runtime/src/_shared/var-indices.spec.ts index 9d4bda7b..fae5b4a0 100644 --- a/packages/runtime/src/_shared/var-indices.spec.ts +++ b/packages/runtime/src/_shared/var-indices.spec.ts @@ -2,12 +2,16 @@ import { describe, expect, it } from 'vitest' +import { createConstantDef, type ConstantDef } from './constant-def' import { createLookupDef, type LookupDef } from './lookup-def' import type { VarSpec } from './types' import { + decodeConstants, decodeLookups, + encodeConstants, encodeLookups, encodeVarIndices, + getEncodedConstantBufferLengths, getEncodedLookupBufferLengths, getEncodedVarIndicesLength } from './var-indices' @@ -62,6 +66,68 @@ describe('encodeVarIndices', () => { }) }) +const constantDefs: ConstantDef[] = [ + createConstantDef({ varSpec: { varIndex: 1 } }, 42), + createConstantDef({ varSpec: { varIndex: 2, subscriptIndices: [1, 2] } }, 100), + createConstantDef({ varSpec: { varIndex: 3 } }, 3.14) +] + +describe('getEncodedConstantBufferLengths', () => { + it('should return the correct length', () => { + const { constantIndicesLength, constantsLength } = getEncodedConstantBufferLengths(constantDefs) + expect(constantIndicesLength).toBe(9) + expect(constantsLength).toBe(3) + }) +}) + +describe('encodeConstants and decodeConstants', () => { + it('should encode and decode the correct values', () => { + const constantIndices = new Int32Array(11) + const constantValues = new Float64Array(5) + encodeConstants(constantDefs, constantIndices, constantValues) + + expect(constantIndices).toEqual( + new Int32Array([ + 3, // constant count + + 1, // var0 index + 0, // var0 subscript count + + 2, // var1 index + 2, // var1 subscript count + 1, // var1 sub0 index + 2, // var1 sub1 index + + 3, // var2 index + 0, // var2 subscript count + + // zero padding + 0, + 0 + ]) + ) + + expect(constantValues).toEqual( + new Float64Array([ + // var0 value + 42, + + // var1 value + 100, + + // var2 value + 3.14, + + // zero padding + 0, 0 + ]) + ) + + const decodedConstantDefs = decodeConstants(constantIndices, constantValues) + expect(decodedConstantDefs).toEqual(constantDefs) + }) +}) + const p = (x: number, y: number) => ({ x, y }) const lookupDefs: LookupDef[] = [ createLookupDef({ varSpec: { varIndex: 1 } }, [p(0, 0), p(1, 1)]), From f83d7afc3116beb917c95ef3512d3d6c9f910ab1 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 15 Jan 2026 09:26:15 -0800 Subject: [PATCH 27/41] fix: prettier --- packages/runtime-async/tests/runner.spec.ts | 5 +---- packages/runtime/src/wasm-model/wasm-model.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/runtime-async/tests/runner.spec.ts b/packages/runtime-async/tests/runner.spec.ts index 4bbb3364..6a3f4eda 100644 --- a/packages/runtime-async/tests/runner.spec.ts +++ b/packages/runtime-async/tests/runner.spec.ts @@ -237,10 +237,7 @@ describe.each([ // Run again, this time with constant overrides outputs = await runner.runModel(inputs, outputs, { - constants: [ - createConstantDef({ varId: '_constant_1' }, 100), - createConstantDef({ varId: '_constant_2' }, 400) - ] + constants: [createConstantDef({ varId: '_constant_1' }, 100), createConstantDef({ varId: '_constant_2' }, 400)] }) // Verify that outputs contain the values using the overridden constants diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 2975c7cb..47cd0f8b 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -151,10 +151,7 @@ class WasmModel implements RunnableModel { } // Allocate the constantIndices buffer - if ( - this.constantIndicesBuffer === undefined || - this.constantIndicesBuffer.numElements < totalIndicesSize - ) { + if (this.constantIndicesBuffer === undefined || this.constantIndicesBuffer.numElements < totalIndicesSize) { this.constantIndicesBuffer?.dispose() this.constantIndicesBuffer = createInt32WasmBuffer(this.wasmModule, totalIndicesSize) } From 61a762d4d1aec524e5e079db8a90e070e00a5f49 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 15 Jan 2026 09:36:04 -0800 Subject: [PATCH 28/41] test: update plugin-config tests to check custom constants --- packages/plugin-config/src/__tests__/config1/model.csv | 4 ++-- packages/plugin-config/src/__tests__/config2/model.csv | 4 ++-- packages/plugin-config/src/__tests__/config3/model.csv | 4 ++-- packages/plugin-config/src/processor.spec.ts | 3 +++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/plugin-config/src/__tests__/config1/model.csv b/packages/plugin-config/src/__tests__/config1/model.csv index e78d93c7..73d17eab 100644 --- a/packages/plugin-config/src/__tests__/config1/model.csv +++ b/packages/plugin-config/src/__tests__/config1/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,200,Data1.dat;Data2.dat,false,false,false +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,200,Data1.dat;Data2.dat,false,false,false,false diff --git a/packages/plugin-config/src/__tests__/config2/model.csv b/packages/plugin-config/src/__tests__/config2/model.csv index 5422e220..b7c9d2ea 100644 --- a/packages/plugin-config/src/__tests__/config2/model.csv +++ b/packages/plugin-config/src/__tests__/config2/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,200,,true,true,true +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,200,,true,true,true,true diff --git a/packages/plugin-config/src/__tests__/config3/model.csv b/packages/plugin-config/src/__tests__/config3/model.csv index 5422e220..b7c9d2ea 100644 --- a/packages/plugin-config/src/__tests__/config3/model.csv +++ b/packages/plugin-config/src/__tests__/config3/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,200,,true,true,true +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,200,,true,true,true,true diff --git a/packages/plugin-config/src/processor.spec.ts b/packages/plugin-config/src/processor.spec.ts index bc1e95af..eaf3ab9f 100644 --- a/packages/plugin-config/src/processor.spec.ts +++ b/packages/plugin-config/src/processor.spec.ts @@ -74,6 +74,7 @@ const specJson1 = `\ "../Data2.dat" ], "bundleListing": false, + "customConstants": false, "customLookups": false, "customOutputs": false }\ @@ -94,6 +95,7 @@ const specJson2 = `\ "../Data2.dat" ], "bundleListing": false, + "customConstants": false, "customLookups": false, "customOutputs": false, "directData": { @@ -113,6 +115,7 @@ const specJson3 = `\ ], "externalDatfiles": [], "bundleListing": true, + "customConstants": true, "customLookups": true, "customOutputs": true }\ From 87f7aa88147321e427eb51646caf88a44dd172fe Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 15 Jan 2026 11:20:25 -0800 Subject: [PATCH 29/41] docs: update API docs --- packages/build/docs/interfaces/ModelSpec.md | 17 ++++++++++++++ .../docs/interfaces/ResolvedModelSpec.md | 17 ++++++++++++++ .../docs/functions/createConstantDef.md | 18 +++++++++++++++ .../runtime/docs/interfaces/ConstantDef.md | 22 +++++++++++++++++++ .../docs/interfaces/RunModelOptions.md | 15 +++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 packages/runtime/docs/functions/createConstantDef.md create mode 100644 packages/runtime/docs/interfaces/ConstantDef.md diff --git a/packages/build/docs/interfaces/ModelSpec.md b/packages/build/docs/interfaces/ModelSpec.md index 135d4a7e..cd3b8d88 100644 --- a/packages/build/docs/interfaces/ModelSpec.md +++ b/packages/build/docs/interfaces/ModelSpec.md @@ -53,6 +53,23 @@ if it is needed. ___ +### customConstants + + `Optional` **customConstants**: `boolean` \| `string`[] + +Whether to allow constants to be overridden at runtime using `setConstant`. + +If undefined or false, the generated model will implement `setConstant` +as a no-op, meaning that constants cannot be overridden at runtime. + +If true, all constants in the generated model will be available to be +overridden. + +If an array is provided, only those variable names in the array will +be available to be overridden. + +___ + ### customLookups `Optional` **customLookups**: `boolean` \| `string`[] diff --git a/packages/build/docs/interfaces/ResolvedModelSpec.md b/packages/build/docs/interfaces/ResolvedModelSpec.md index a1cc884d..2d9a723e 100644 --- a/packages/build/docs/interfaces/ResolvedModelSpec.md +++ b/packages/build/docs/interfaces/ResolvedModelSpec.md @@ -72,6 +72,23 @@ if it is needed. ___ +### customConstants + + **customConstants**: `boolean` \| `string`[] + +Whether to allow constants to be overridden at runtime using `setConstant`. + +If false, the generated model will contain a `setConstant` function that +throws an error, meaning that constants cannot be overridden at runtime. + +If true, all constants in the generated model will be available to be +overridden. + +If an array is provided, only those variable names in the array will +be available to be overridden. + +___ + ### customLookups **customLookups**: `boolean` \| `string`[] diff --git a/packages/runtime/docs/functions/createConstantDef.md b/packages/runtime/docs/functions/createConstantDef.md new file mode 100644 index 00000000..2e61ae6e --- /dev/null +++ b/packages/runtime/docs/functions/createConstantDef.md @@ -0,0 +1,18 @@ +[@sdeverywhere/runtime](../index.md) / createConstantDef + +# Function: createConstantDef + +**createConstantDef**(`varRef`, `value`): [`ConstantDef`](../interfaces/ConstantDef.md) + +Create a `ConstantDef` instance. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `varRef` | [`VarRef`](../interfaces/VarRef.md) | The reference to the constant variable to be modified. | +| `value` | `number` | The new constant value. | + +#### Returns + +[`ConstantDef`](../interfaces/ConstantDef.md) diff --git a/packages/runtime/docs/interfaces/ConstantDef.md b/packages/runtime/docs/interfaces/ConstantDef.md new file mode 100644 index 00000000..e0d7b5f4 --- /dev/null +++ b/packages/runtime/docs/interfaces/ConstantDef.md @@ -0,0 +1,22 @@ +[@sdeverywhere/runtime](../index.md) / ConstantDef + +# Interface: ConstantDef + +Specifies the constant value that will be used to override a constant in a +generated model. + +## Properties + +### varRef + + **varRef**: [`VarRef`](VarRef.md) + +The reference that identifies the constant variable to be modified. + +___ + +### value + + **value**: `number` + +The new constant value. diff --git a/packages/runtime/docs/interfaces/RunModelOptions.md b/packages/runtime/docs/interfaces/RunModelOptions.md index dd5369ae..7f2307ed 100644 --- a/packages/runtime/docs/interfaces/RunModelOptions.md +++ b/packages/runtime/docs/interfaces/RunModelOptions.md @@ -6,6 +6,21 @@ Additional options that can be passed to a `runModel` call to influence the mode ## Properties +### constants + + `Optional` **constants**: [`ConstantDef`](ConstantDef.md)[] + +If defined, override the values for the specified constant variables. + +Note that constant overrides do not persist after the `runModel` call. Because +`initConstants` is called at the beginning of each `runModel` call, all constants +are reset to their default values before each model run. If you want to override +constants, you must provide them in the options for each `runModel` call. To +reset constants to their original values, simply stop passing them in the options +(or pass an empty array). + +___ + ### lookups `Optional` **lookups**: [`LookupDef`](LookupDef.md)[] From 2d3d85caf04338318d1a5047ac57c27648de8ca3 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 15 Jan 2026 11:35:53 -0800 Subject: [PATCH 30/41] fix: include `custom constants` column in model.csv files --- examples/sir/config/model.csv | 4 ++-- examples/template-jquery/config/model.csv | 4 ++-- examples/template-svelte/config/model.csv | 4 ++-- packages/build/tests/build-prod/build-prod.spec.ts | 6 +++++- packages/create/src/step-config.ts | 3 ++- packages/plugin-config/template-config/model.csv | 4 ++-- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/sir/config/model.csv b/examples/sir/config/model.csv index debd9f54..9614c9f1 100644 --- a/examples/sir/config/model.csv +++ b/examples/sir/config/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,100,,false,false,false +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,100,,false,false,false,false diff --git a/examples/template-jquery/config/model.csv b/examples/template-jquery/config/model.csv index debd9f54..9614c9f1 100644 --- a/examples/template-jquery/config/model.csv +++ b/examples/template-jquery/config/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,100,,false,false,false +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,100,,false,false,false,false diff --git a/examples/template-svelte/config/model.csv b/examples/template-svelte/config/model.csv index debd9f54..9614c9f1 100644 --- a/examples/template-svelte/config/model.csv +++ b/examples/template-svelte/config/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,100,,false,false,false +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,100,,false,false,false,false diff --git a/packages/build/tests/build-prod/build-prod.spec.ts b/packages/build/tests/build-prod/build-prod.spec.ts index bef5873e..0ac7b005 100644 --- a/packages/build/tests/build-prod/build-prod.spec.ts +++ b/packages/build/tests/build-prod/build-prod.spec.ts @@ -101,6 +101,7 @@ describe('build in production mode', () => { expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }]) expect(resolvedModelSpec!.datFiles).toEqual([]) expect(resolvedModelSpec!.bundleListing).toBe(false) + expect(resolvedModelSpec!.customConstants).toBe(false) expect(resolvedModelSpec!.customLookups).toBe(false) expect(resolvedModelSpec!.customOutputs).toBe(false) }) @@ -144,11 +145,12 @@ describe('build in production mode', () => { expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }]) expect(resolvedModelSpec!.datFiles).toEqual([]) expect(resolvedModelSpec!.bundleListing).toBe(true) + expect(resolvedModelSpec!.customConstants).toBe(false) expect(resolvedModelSpec!.customLookups).toEqual(['lookup1']) expect(resolvedModelSpec!.customOutputs).toEqual(['output1']) }) - it('should resolve model spec (when boolean is provided for customLookups and customOutputs)', async () => { + it('should resolve model spec (when boolean is provided for customConstants, customLookups, and customOutputs)', async () => { let resolvedModelSpec: ResolvedModelSpec const userConfig: UserConfig = { genFormat: 'c', @@ -161,6 +163,7 @@ describe('build in production mode', () => { inputs: ['Y'], outputs: ['Z'], bundleListing: true, + customConstants: true, customLookups: true, customOutputs: true } @@ -182,6 +185,7 @@ describe('build in production mode', () => { expect(result.value.exitCode).toBe(0) expect(resolvedModelSpec!).toBeDefined() expect(resolvedModelSpec!.bundleListing).toBe(true) + expect(resolvedModelSpec!.customConstants).toEqual(true) expect(resolvedModelSpec!.customLookups).toEqual(true) expect(resolvedModelSpec!.customOutputs).toEqual(true) }) diff --git a/packages/create/src/step-config.ts b/packages/create/src/step-config.ts index 07d46a06..2dae919d 100644 --- a/packages/create/src/step-config.ts +++ b/packages/create/src/step-config.ts @@ -193,11 +193,12 @@ export async function chooseGenConfig(projDir: string, mdlPath: string): Promise // Disable bundled listing and customization features by default const bundleListing = 'false' + const customConstants = 'false' const customLookups = 'false' const customOutputs = 'false' // Add line and write out updated `model.csv` - const modelCsvLine = `${initialTime},${finalTime},${datPart},${bundleListing},${customLookups},${customOutputs}` + const modelCsvLine = `${initialTime},${finalTime},${datPart},${bundleListing},${customConstants},${customLookups},${customOutputs}` const newModelCsvContent = `${modelCsvHeader}\n${modelCsvLine}\n` await writeFile(modelCsvFile, newModelCsvContent) diff --git a/packages/plugin-config/template-config/model.csv b/packages/plugin-config/template-config/model.csv index a5d2eea1..9657ea5d 100644 --- a/packages/plugin-config/template-config/model.csv +++ b/packages/plugin-config/template-config/model.csv @@ -1,2 +1,2 @@ -graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs -0,100,Data1.dat;Data2.dat,false,false,false +graph default min time,graph default max time,model dat files,bundle listing,custom constants,custom lookups,custom outputs +0,100,Data1.dat;Data2.dat,false,false,false,false From 4d533dac3dbbbfc84f521fd87566c60a3f8fc0bc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 15 Jan 2026 11:36:09 -0800 Subject: [PATCH 31/41] fix: move setConstantsFromBuffers before runModelWithBuffers --- packages/cli/src/c/model.c | 66 +++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 72e98e63..d7b1ba50 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -67,6 +67,39 @@ double getSaveper() { return _saveper; } +/** + * Set constant overrides from the given buffers. + * + * The `constantIndices` buffer contains the variable indices and subscript indices + * for each constant to override. The format is: + * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] + * + * The `constantValues` buffer contains the corresponding values for each constant. + */ +void setConstantsFromBuffers(int32_t* constantIndices, double* constantValues) { + if (constantIndices == NULL || constantValues == NULL) { + return; + } + + size_t indexBufferOffset = 0; + size_t valueBufferOffset = 0; + size_t constantCount = (size_t)constantIndices[indexBufferOffset++]; + + for (size_t i = 0; i < constantCount; i++) { + size_t varIndex = (size_t)constantIndices[indexBufferOffset++]; + size_t subCount = (size_t)constantIndices[indexBufferOffset++]; + size_t* subIndices; + if (subCount > 0) { + subIndices = (size_t*)(constantIndices + indexBufferOffset); + indexBufferOffset += subCount; + } else { + subIndices = NULL; + } + double value = constantValues[valueBufferOffset++]; + setConstant(varIndex, subIndices, value); + } +} + char* run_model(const char* inputs) { // run_model does everything necessary to run the model with the given inputs. // It may be called multiple times. Call finish() after all runs are complete. @@ -117,39 +150,6 @@ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices outputIndexBuffer = NULL; } -/** - * Set constant overrides from the given buffers. - * - * The `constantIndices` buffer contains the variable indices and subscript indices - * for each constant to override. The format is: - * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] - * - * The `constantValues` buffer contains the corresponding values for each constant. - */ -void setConstantsFromBuffers(int32_t* constantIndices, double* constantValues) { - if (constantIndices == NULL || constantValues == NULL) { - return; - } - - size_t indexBufferOffset = 0; - size_t valueBufferOffset = 0; - size_t constantCount = (size_t)constantIndices[indexBufferOffset++]; - - for (size_t i = 0; i < constantCount; i++) { - size_t varIndex = (size_t)constantIndices[indexBufferOffset++]; - size_t subCount = (size_t)constantIndices[indexBufferOffset++]; - size_t* subIndices; - if (subCount > 0) { - subIndices = (size_t*)(constantIndices + indexBufferOffset); - indexBufferOffset += subCount; - } else { - subIndices = NULL; - } - double value = constantValues[valueBufferOffset++]; - setConstant(varIndex, subIndices, value); - } -} - void run() { #ifdef PERF_TEST clock_gettime(CLOCK_MONOTONIC, &startTime); From 5902a0ce24266660d7d0e6f3c52765a0b6db665c Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 20 Jan 2026 17:32:30 -0800 Subject: [PATCH 32/41] docs: add param docs for runModelWithBuffers --- packages/cli/src/c/model.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index d7b1ba50..51bc6d9a 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -135,6 +135,23 @@ char* run_model(const char* inputs) { * by the output value for that variable at t1, and so on. After the value for tN * (where tN is the last time in the range), the second variable outputs will begin, * and so on. + * + * @param inputs The required buffer that contains the model input values (see above + * for details on the expected format). + * @param outputs The required buffer that will receive the model output values (see + * above for details on the expected format). + * @param outputIndices An optional buffer that contains the indices of the output + * variables to store. Pass NULL if using the default outputs. This is typically + * only used by the JS-level runtime package. For more details on the expected format, + * see the `Outputs` API in that package. + * @param constantIndices An optional buffer that contains the indices of the constants + * to override. Pass NULL if not overriding any constants. This is typically only + * used by the JS-level runtime package. For more details on the expected format, see + * the `RunModelParams` API in that package. + * @param constantValues An optional buffer that contains the values of the constants + * to override. Pass NULL if not overriding any constants. This is typically only + * used by the JS-level runtime package. For more details on the expected format, see + * the `RunModelParams` API in that package. */ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices, int32_t* constantIndices, double* constantValues) { outputBuffer = outputs; From 4a2562cd3ed1ce92c4adf5f3b8e045c5112e878d Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 20 Jan 2026 17:33:26 -0800 Subject: [PATCH 33/41] build: update lock file --- pnpm-lock.yaml | 194 ++++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 82 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8de36380..2c26b43a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,11 +21,14 @@ importers: specifier: ^8.46.0 version: 8.46.0(eslint@9.37.0)(typescript@5.2.2) '@vitest/browser': - specifier: ^4.0.16 - version: 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + specifier: ^4.0.17 + version: 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/browser-playwright': + specifier: ^4.0.17 + version: 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) '@vitest/coverage-v8': - specifier: ^4.0.16 - version: 4.0.16(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + specifier: ^4.0.17 + version: 4.0.17(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(vitest@4.0.17) eslint: specifier: ^9.37.0 version: 9.37.0 @@ -72,8 +75,8 @@ importers: specifier: ^5.2.2 version: 5.2.2 vitest: - specifier: ^4.0.16 - version: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + specifier: ^4.0.17 + version: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) examples/hello-world: dependencies: @@ -560,7 +563,7 @@ importers: version: 5.0.10(@storybook/svelte@10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10))(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.10)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) '@storybook/addon-vitest': specifier: ^10.1.11 - version: 10.1.11(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(@vitest/runner@4.0.16)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) + version: 10.1.11(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.17) '@storybook/svelte': specifier: ^10.1.11 version: 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(svelte@5.39.10) @@ -989,6 +992,27 @@ importers: specifier: ^20.14.8 version: 20.19.19 + tests/integration/override-constants: + dependencies: + '@sdeverywhere/build': + specifier: workspace:* + version: link:../../../packages/build + '@sdeverywhere/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@sdeverywhere/plugin-wasm': + specifier: workspace:* + version: link:../../../packages/plugin-wasm + '@sdeverywhere/plugin-worker': + specifier: workspace:* + version: link:../../../packages/plugin-worker + '@sdeverywhere/runtime': + specifier: workspace:* + version: link:../../../packages/runtime + '@sdeverywhere/runtime-async': + specifier: workspace:* + version: link:../../../packages/runtime-async + tests/integration/override-lookups: dependencies: '@sdeverywhere/build': @@ -1844,16 +1868,22 @@ packages: resolution: {integrity: sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/browser@4.0.16': - resolution: {integrity: sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==} + '@vitest/browser-playwright@4.0.17': + resolution: {integrity: sha512-CE9nlzslHX6Qz//MVrjpulTC9IgtXTbJ+q7Rx1HD+IeSOWv4NHIRNHPA6dB4x01d9paEqt+TvoqZfmgq40DxEQ==} peerDependencies: - vitest: 4.0.16 + playwright: '*' + vitest: 4.0.17 - '@vitest/coverage-v8@4.0.16': - resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==} + '@vitest/browser@4.0.17': + resolution: {integrity: sha512-cgf2JZk2fv5or3efmOrRJe1V9Md89BPgz4ntzbf84yAb+z2hW6niaGFinl9aFzPZ1q3TGfWZQWZ9gXTFThs2Qw==} peerDependencies: - '@vitest/browser': 4.0.16 - vitest: 4.0.16 + vitest: 4.0.17 + + '@vitest/coverage-v8@4.0.17': + resolution: {integrity: sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==} + peerDependencies: + '@vitest/browser': 4.0.17 + vitest: 4.0.17 peerDependenciesMeta: '@vitest/browser': optional: true @@ -1861,8 +1891,8 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.0.16': - resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + '@vitest/expect@4.0.17': + resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} @@ -1875,8 +1905,8 @@ packages: vite: optional: true - '@vitest/mocker@4.0.16': - resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} + '@vitest/mocker@4.0.17': + resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -1889,26 +1919,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.0.16': - resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + '@vitest/pretty-format@4.0.17': + resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} - '@vitest/runner@4.0.16': - resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} + '@vitest/runner@4.0.17': + resolution: {integrity: sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==} - '@vitest/snapshot@4.0.16': - resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} + '@vitest/snapshot@4.0.17': + resolution: {integrity: sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.0.16': - resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} + '@vitest/spy@4.0.17': + resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.0.16': - resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@vitest/utils@4.0.17': + resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2638,10 +2668,6 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} @@ -3570,18 +3596,18 @@ packages: vite: optional: true - vitest@4.0.16: - resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} + vitest@4.0.17: + resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.16 - '@vitest/browser-preview': 4.0.16 - '@vitest/browser-webdriverio': 4.0.16 - '@vitest/ui': 4.0.16 + '@vitest/browser-playwright': 4.0.17 + '@vitest/browser-preview': 4.0.17 + '@vitest/browser-webdriverio': 4.0.17 + '@vitest/ui': 4.0.17 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -4129,15 +4155,16 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@storybook/addon-vitest@10.1.11(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(@vitest/runner@4.0.16)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@storybook/addon-vitest@10.1.11(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.17)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@vitest/browser': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/runner': 4.0.16 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/browser-playwright': 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/runner': 4.0.17 + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) transitivePeerDependencies: - react - react-dom @@ -4407,16 +4434,29 @@ snapshots: '@typescript-eslint/types': 8.46.0 eslint-visitor-keys: 4.2.1 - '@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/browser-playwright@4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17)': dependencies: - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/utils': 4.0.16 + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + playwright: 1.56.1 + tinyrainbow: 3.0.3 + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17)': + dependencies: + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + '@vitest/utils': 4.0.17 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -4424,24 +4464,21 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@4.0.16(@vitest/browser@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/coverage-v8@4.0.17(@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17))(vitest@4.0.17)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.16 + '@vitest/utils': 4.0.17 ast-v8-to-istanbul: 0.3.10 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 magicast: 0.5.1 obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@20.19.19)(sass@1.97.2) + vitest: 4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2) optionalDependencies: - '@vitest/browser': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2)) - transitivePeerDependencies: - - supports-color + '@vitest/browser': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) '@vitest/expect@3.2.4': dependencies: @@ -4451,12 +4488,12 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.0.16': + '@vitest/expect@4.0.17': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 chai: 6.2.2 tinyrainbow: 3.0.3 @@ -4468,9 +4505,9 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) - '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': + '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))': dependencies: - '@vitest/spy': 4.0.16 + '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -4480,18 +4517,18 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.0.16': + '@vitest/pretty-format@4.0.17': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.16': + '@vitest/runner@4.0.17': dependencies: - '@vitest/utils': 4.0.16 + '@vitest/utils': 4.0.17 pathe: 2.0.3 - '@vitest/snapshot@4.0.16': + '@vitest/snapshot@4.0.17': dependencies: - '@vitest/pretty-format': 4.0.16 + '@vitest/pretty-format': 4.0.17 magic-string: 0.30.21 pathe: 2.0.3 @@ -4499,7 +4536,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.0.16': {} + '@vitest/spy@4.0.17': {} '@vitest/utils@3.2.4': dependencies: @@ -4507,9 +4544,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.0.16': + '@vitest/utils@4.0.17': dependencies: - '@vitest/pretty-format': 4.0.16 + '@vitest/pretty-format': 4.0.17 tinyrainbow: 3.0.3 acorn-jsx@5.3.2(acorn@8.15.0): @@ -5209,14 +5246,6 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -6072,15 +6101,15 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@20.19.19)(sass@1.97.2) - vitest@4.0.16(@types/node@20.19.19)(sass@1.97.2): + vitest@4.0.17(@types/node@20.19.19)(@vitest/browser-playwright@4.0.17)(sass@1.97.2): dependencies: - '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) - '@vitest/pretty-format': 4.0.16 - '@vitest/runner': 4.0.16 - '@vitest/snapshot': 4.0.16 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 + '@vitest/expect': 4.0.17 + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2)) + '@vitest/pretty-format': 4.0.17 + '@vitest/runner': 4.0.17 + '@vitest/snapshot': 4.0.17 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 es-module-lexer: 1.7.0 expect-type: 1.2.2 magic-string: 0.30.21 @@ -6096,6 +6125,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.19 + '@vitest/browser-playwright': 4.0.17(playwright@1.56.1)(vite@7.3.1(@types/node@20.19.19)(sass@1.97.2))(vitest@4.0.17) transitivePeerDependencies: - jiti - less From 7bb6df60380cdaa6159c947c04dc7c4952ccf113 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 20 Jan 2026 17:42:19 -0800 Subject: [PATCH 34/41] docs: clarify docs for runModelWithBuffers --- packages/cli/src/c/model.c | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 51bc6d9a..3eac8e15 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -136,22 +136,29 @@ char* run_model(const char* inputs) { * (where tN is the last time in the range), the second variable outputs will begin, * and so on. * - * @param inputs The required buffer that contains the model input values (see above - * for details on the expected format). - * @param outputs The required buffer that will receive the model output values (see - * above for details on the expected format). + * For the optional `outputIndices` and `constantIndices` buffers, the expected format + * is: + * [count, varIndex1, subCount1, subIndex1_1, ..., varIndex2, subCount2, ...] + * where `count` is the number of variables to store, `varIndexN` is the index of the + * variable to store (from the model listing file), `subCountN` is the number of + * subscripts for that variable, and `subIndexN_M` is the index of the subscript + * at the Mth position for that variable. + * + * @param inputs The required buffer that contains the model input values. See above + * for details on the expected format. + * @param outputs The required buffer that will receive the model output values. See + * above for details on the expected format. * @param outputIndices An optional buffer that contains the indices of the output * variables to store. Pass NULL if using the default outputs. This is typically - * only used by the JS-level runtime package. For more details on the expected format, - * see the `Outputs` API in that package. + * only used by the JS-level runtime package. See above for details on the expected + * format. * @param constantIndices An optional buffer that contains the indices of the constants - * to override. Pass NULL if not overriding any constants. This is typically only - * used by the JS-level runtime package. For more details on the expected format, see - * the `RunModelParams` API in that package. + * to override. Pass NULL if not overriding any constants. (This is typically only + * used by the JS-level runtime package. See above for details on the expected format. * @param constantValues An optional buffer that contains the values of the constants * to override. Pass NULL if not overriding any constants. This is typically only - * used by the JS-level runtime package. For more details on the expected format, see - * the `RunModelParams` API in that package. + * used by the JS-level runtime package. Each value in the buffer corresponds to the + * value of the constant at the corresponding index. */ void runModelWithBuffers(double* inputs, double* outputs, int32_t* outputIndices, int32_t* constantIndices, double* constantValues) { outputBuffer = outputs; From 910e0963e529a5d091819c878dae96795d6b7eb7 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Fri, 23 Jan 2026 16:37:31 -0800 Subject: [PATCH 35/41] fix: remove PLAN.md I added this to an issue comment for posterity --- PLAN.md | 519 -------------------------------------------------------- 1 file changed, 519 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index c27263fb..00000000 --- a/PLAN.md +++ /dev/null @@ -1,519 +0,0 @@ -# Implementation Plan: Override Constants Feature - -## Overview - -Implement the "override constants" feature to allow users to modify any constant variable at runtime, similar to the "override lookups" feature but simpler. Constants are scalar values that get reset on every `runModel` call by `initConstants()`, so overrides must be provided each time (unlike lookups which persist). - -## Key Design Decisions - -1. **No explicit reset support**: `ConstantDef.value` is required (not optional). Constants automatically reset to original values if not provided in options, since `initConstants()` is called at the start of every run. - -2. **No persistence**: Unlike lookups, constant overrides do NOT persist across `runModel` calls. Users must provide them in options each time they want to override. - -3. **Simple scalar values**: Constants are plain numbers, not arrays or lookup objects, making the implementation simpler than lookups. - -## Implementation Steps - -### 1. Create ConstantDef Interface ✅ - -**New file**: `packages/runtime/src/_shared/constant-def.ts` - -```typescript -// Copyright (c) 2025 Climate Interactive / New Venture Fund - -import type { VarRef } from './types' - -/** - * Specifies the constant value that will be used to override a constant variable. - */ -export interface ConstantDef { - /** The reference that identifies the constant variable to be modified. */ - varRef: VarRef - - /** The new constant value. */ - value: number -} - -/** - * Create a `ConstantDef` instance. - * - * @param varRef The reference to the constant variable to be modified. - * @param value The new constant value. - */ -export function createConstantDef(varRef: VarRef, value: number): ConstantDef { - return { - varRef, - value - } -} -``` - -**Update**: `packages/runtime/src/_shared/index.ts` - add export: - -```typescript -export * from './constant-def' -``` - -### 2. Update RunModelOptions - -**File**: `packages/runtime/src/runnable-model/run-model-options.ts` - -Add `constants` field: - -```typescript -import type { ConstantDef, LookupDef } from '../_shared' - -export interface RunModelOptions { - lookups?: LookupDef[] - - /** - * If defined, override the values for the specified constant variables. - * - * Note that UNLIKE lookups (which persist across calls), constant overrides do - * NOT persist after the `runModel` call. Because `initConstants` is called at - * the beginning of each `runModel` call, all constants are reset to their default - * values. If you want to override constants, you must provide them in the options - * for each `runModel` call. To reset constants to their original values, simply - * stop passing them in the options (or pass an empty array). - */ - constants?: ConstantDef[] -} -``` - -### 3. Add Configuration Option - -**File**: `packages/build/src/_shared/model-spec.ts` - -Add to `ModelSpec` interface (after `customLookups`): - -```typescript -/** - * Whether to allow constants to be overridden at runtime using `setConstant`. - * - * If undefined or false, the generated model will implement `setConstant` - * as a no-op, meaning that constants cannot be overridden at runtime. - * - * If true, all constants in the generated model will be available to be - * overridden. - * - * If an array is provided, only those variable names in the array will - * be available to be overridden. - */ -customConstants?: boolean | VarName[] -``` - -Add to `ResolvedModelSpec` interface: - -```typescript -/** - * Whether to allow constants to be overridden at runtime using `setConstant`. - */ -customConstants: boolean | VarName[] -``` - -### 4. Generate setConstant Function - JavaScript - -**File**: `packages/compile/src/generate/gen-code-js.js` - -Add `setConstantImpl` function (after `setLookupImpl`, around line 560): - -```javascript -function setConstantImpl(varIndexInfo, customConstants) { - // Emit case statements for all const variables that can be overridden at runtime - let overrideAllowed - if (Array.isArray(customConstants)) { - const customConstantVarNames = customConstants.map(varName => { - return canonicalVensimName(varName.split('[')[0]) - }) - overrideAllowed = varName => customConstantVarNames.includes(varName) - } else { - // Include a case statement for all constant variables - overrideAllowed = () => true - } - const constVars = R.filter(info => { - return info.varType === 'const' && overrideAllowed(info.varName) - }) - const code = R.map(info => { - let constVar = info.varName - for (let i = 0; i < info.subscriptCount; i++) { - constVar += `[subs[${i}]]` - } - let c = '' - c += ` case ${info.varIndex}:\n` - c += ` ${constVar} = value;\n` - c += ` break;` - return c - }) - const section = R.pipe(constVars, code, lines) - return section(varIndexInfo) -} -``` - -Add `setConstant` function generation in `emitIOCode()` (after `setLookup`, around line 750): - -```javascript -// Generate the setConstant function -let setConstantBody -if (spec.customConstants === true || Array.isArray(spec.customConstants)) { - setConstantBody = `\ - if (!varSpec) { - throw new Error('Got undefined varSpec in setConstant'); - } - const varIndex = varSpec.varIndex; - const subs = varSpec.subscriptIndices; - switch (varIndex) { -${setConstantImpl(Model.varIndexInfo(), spec.customConstants)} - default: - throw new Error(\`No constant found for var index \${varIndex} in setConstant\`); - }` -} else { - let msg = 'The setConstant function was not enabled for the generated model. ' - msg += 'Set the customConstants property in the spec/config file to allow for overriding constants at runtime.' - setConstantBody = ` throw new Error('${msg}');` -} - -io += ` -/*export*/ function setConstant(varSpec /*: VarSpec*/, value /*: number*/) { -${setConstantBody} -} -` -``` - -### 5. Generate setConstant Function - C - -**File**: `packages/compile/src/generate/gen-code-c.js` - -Add `setConstantImpl` function (similar pattern as JS): - -```javascript -function setConstantImpl(varIndexInfo, customConstants) { - let overrideAllowed - if (Array.isArray(customConstants)) { - const customConstantVarNames = customConstants.map(varName => { - return canonicalVensimName(varName.split('[')[0]) - }) - overrideAllowed = varName => customConstantVarNames.includes(varName) - } else { - overrideAllowed = () => true - } - const constVars = R.filter(info => { - return info.varType === 'const' && overrideAllowed(info.varName) - }) - const code = R.map(info => { - let constVar = info.varName - for (let i = 0; i < info.subscriptCount; i++) { - constVar += `[subIndices[${i}]]` - } - let c = '' - c += ` case ${info.varIndex}:\n` - c += ` ${constVar} = value;\n` - c += ` break;` - return c - }) - const section = R.pipe(constVars, code, lines) - return section(varIndexInfo) -} -``` - -Add `setConstant` function generation in `emitIOCode()`: - -```c -void setConstant(size_t varIndex, size_t* subIndices, double value) { - switch (varIndex) { - ${setConstantBody} - default: - break; - } -} -``` - -### 6. Update JS Model Runtime - -**File**: `packages/runtime/src/js-model/js-model.ts` - -Add to `JsModel` interface (around line 53): - -```typescript -/** @hidden */ -setConstant(varSpec: VarSpec, value: number): void -``` - -Update `runJsModel` function signature (around line 121): - -```typescript -function runJsModel( - model: JsModel, - // ... other params ... - lookups: LookupDef[] | undefined, - constants: ConstantDef[] | undefined, // NEW - stopAfterTime: number | undefined -): void -``` - -Add constant override logic after lookup overrides (after line 150): - -```typescript -// Apply constant overrides, if provided -if (constants !== undefined) { - for (const constantDef of constants) { - model.setConstant(constantDef.varRef.varSpec, constantDef.value) - } -} -``` - -Update call in `initJsModel` (around line 111): - -```typescript -onRunModel: (inputs, outputs, options) => { - runJsModel( - model, - // ... other params ... - options?.lookups, - options?.constants, // NEW - undefined - ) -} -``` - -### 7. Update Wasm Model Runtime - -**File**: `packages/runtime/src/wasm-model/wasm-model.ts` - -Add native function wrapper (around line 67): - -```typescript -private readonly wasmSetConstant: ( - varIndex: number, - subIndicesAddress: number, - value: number -) => void -``` - -Initialize in constructor: - -```typescript -this.wasmSetConstant = wasmModule.cwrap('setConstant', null, ['number', 'number', 'number']) -``` - -Add constant override logic in `runModel` after lookup overrides (around line 130): - -```typescript -// Apply constant overrides, if provided -const constants = params.getConstants() -if (constants !== undefined) { - for (const constantDef of constants) { - const varSpec = constantDef.varRef.varSpec - const numSubElements = varSpec.subscriptIndices?.length || 0 - let subIndicesAddress: number - - if (numSubElements > 0) { - // Reuse the lookup sub indices buffer - if (this.lookupSubIndicesBuffer === undefined || this.lookupSubIndicesBuffer.numElements < numSubElements) { - this.lookupSubIndicesBuffer?.dispose() - this.lookupSubIndicesBuffer = createInt32WasmBuffer(this.wasmModule, numSubElements) - } - this.lookupSubIndicesBuffer.getArrayView().set(varSpec.subscriptIndices) - subIndicesAddress = this.lookupSubIndicesBuffer.getAddress() - } else { - subIndicesAddress = 0 - } - - this.wasmSetConstant(varSpec.varIndex, subIndicesAddress, constantDef.value) - } -} -``` - -### 8. Update RunModelParams Interface - -**File**: `packages/runtime/src/runnable-model/run-model-params.ts` - -Add method: - -```typescript -/** - * Return an array containing constant overrides, or undefined if no constants - * were passed to the latest `runModel` call. - */ -getConstants(): ConstantDef[] | undefined -``` - -### 9. Add Constant Encoding/Decoding for Async/Worker Support - -**File**: `packages/runtime/src/_shared/var-indices.ts` - -Add three new functions for encoding/decoding constants (similar to lookup encoding): - -1. `getEncodedConstantBufferLengths(constantDefs: ConstantDef[])` - - Returns `{ constantIndicesLength, constantsLength }` - - Format for constantIndices buffer: - - constant count - - constantN var index - - constantN subscript count - - constantN sub1 index, sub2 index, ... subM index - - (repeat for each constant) - - Format for constants buffer: - - constantN value - - (repeat for each constant) - -2. `encodeConstants(constantDefs: ConstantDef[], constantIndicesArray: Int32Array, constantsArray: Float64Array)` - - Writes constant metadata to indices buffer - - Writes constant values to constants buffer - -3. `decodeConstants(constantIndicesArray: Int32Array, constantsArray: Float64Array): ConstantDef[]` - - Reconstructs ConstantDef instances from buffers - -### 10. Update BufferedRunModelParams for Async/Worker Support - -**File**: `packages/runtime/src/runnable-model/buffered-run-model-params.ts` - -1. Add two new buffer sections: - - ```typescript - /** The constant values section of the `encoded` buffer. */ - private readonly constants = new Float64Section() - - /** The constant indices section of the `encoded` buffer. */ - private readonly constantIndices = new Int32Section() - ``` - -2. Update header length constant (line 16): - - ```typescript - const headerLengthInElements = 20 // Was 16, add 4 for constants sections - ``` - -3. In `updateFromParams()`: - - Compute constant buffer lengths using `getEncodedConstantBufferLengths()` - - Add constant sections to memory layout calculation - - Update header to include constant section offsets/lengths - - Encode constants using `encodeConstants()` - -4. In `updateFromEncodedBuffer()`: - - Read constant section offsets/lengths from header - - Rebuild constant section views - -5. In `getConstants()`: - - Replace TODO with actual implementation - - Return `decodeConstants(this.constantIndices.view, this.constants.view)` - -### 11. Update BaseRunnableModel - -**File**: `packages/runtime/src/runnable-model/base-runnable-model.ts` - -Update `OnRunModelFunc` type (line 12): - -```typescript -export type OnRunModelFunc = ( - inputs: Float64Array | undefined, - outputs: Float64Array, - options?: { - outputIndices?: Int32Array - lookups?: LookupDef[] - constants?: ConstantDef[] // NEW - } -) => void -``` - -Update `runModel` call (line 98): - -```typescript -this.onRunModel?.(inputsArray, outputsArray, { - outputIndices: outputIndicesArray, - lookups: params.getLookups(), - constants: params.getConstants() // NEW -}) -``` - -### 12. Create Integration Test - -**New directory**: `tests/integration/override-constants/` - -Create 4 files: - -1. **override-constants.mdl** - Vensim model with 1D, 2D, and non-subscripted constants -2. **sde.config.js** - Config with `customConstants: true` -3. **run-tests.js** - Test script that: - - Tests default constant values - - Tests overriding constants (by name and by ID) - - Tests that overrides do NOT persist (must be provided each call) - - Tests subscripted constants (1D and 2D arrays) - - Tests both synchronous and asynchronous model runners -4. **package.json** - Package file with test script - -### 13. Add Unit Tests - -**File**: `packages/compile/src/generate/gen-code-js.spec.ts` - -Add test cases for `setConstant` generation similar to existing `setLookup` tests. - -## Files Summary - -### New Files (5): - -1. `packages/runtime/src/_shared/constant-def.ts` -2. `tests/integration/override-constants/override-constants.mdl` -3. `tests/integration/override-constants/sde.config.js` -4. `tests/integration/override-constants/run-tests.js` -5. `tests/integration/override-constants/package.json` - -### Modified Files (12): - -1. `packages/runtime/src/_shared/index.ts` - export ConstantDef -2. `packages/runtime/src/_shared/var-indices.ts` - add constant encoding/decoding functions -3. `packages/runtime/src/runnable-model/run-model-options.ts` - add constants field -4. `packages/runtime/src/runnable-model/run-model-params.ts` - add getConstants() -5. `packages/runtime/src/runnable-model/base-runnable-model.ts` - pass constants through -6. `packages/runtime/src/runnable-model/buffered-run-model-params.ts` - implement constant encoding/decoding -7. `packages/runtime/src/runnable-model/referenced-run-model-params.ts` - implement getConstants() -8. `packages/runtime/src/js-model/js-model.ts` - add setConstant, integrate with runJsModel -9. `packages/runtime/src/wasm-model/wasm-model.ts` - add native wrapper, integrate with runModel -10. `packages/build/src/_shared/model-spec.ts` - add customConstants config option -11. `packages/compile/src/generate/gen-code-js.js` - generate setConstant function -12. `packages/compile/src/generate/gen-code-c.js` - generate C setConstant function - -## Key Differences from Override Lookups - -This implementation has some similarities and differences compared to override lookups: - -### Similarities: - -1. **Encoding/decoding for async support** - both need buffer encoding for worker threads -2. **VarRef resolution** - both use the same varRef pattern for identifying variables -3. **Subscript handling** - both support subscripted variables (1D, 2D arrays) - -### Differences (Constants are simpler): - -1. **Scalar values** - just `number`, not `Float64Array` of points -2. **No persistence** - constants reset on every run, no state to manage -3. **Simpler C signature** - `(varIndex, subIndices, value)` vs `(varIndex, subIndices, points, numPoints)` -4. **No reset logic** - automatic reset via `initConstants()` -5. **Simpler buffer encoding** - one value per constant vs variable-length point arrays -6. **No offset tracking** - constants buffer is sequential values, no offset/length metadata needed - -## Testing Strategy - -1. **Unit tests**: Verify switch statement generation in both JS and C -2. **Integration tests**: Validate end-to-end functionality: - - Default values work correctly - - Overrides work (by name and ID) - - Overrides do NOT persist across calls - - Subscripted constants work (1D, 2D, non-subscripted) - - Both sync and async runners work -3. **Test both formats**: Run with `GEN_FORMAT=js` and `GEN_FORMAT=c` - -## Progress Tracking - -- [x] 1. Create top-level PLAN.md file -- [x] 2. Create ConstantDef interface and export -- [x] 3. Update RunModelOptions with constants field -- [x] 4. Add customConstants to ModelSpec -- [x] 5. Generate setConstant function in JS code generator -- [x] 6. Generate setConstant function in C code generator -- [x] 7. Update JS model runtime -- [x] 8. Update Wasm model runtime -- [x] 9. Update RunModelParams interface -- [x] 10. Add constant encoding/decoding for async/worker support -- [x] 11. Update BufferedRunModelParams with constant encoding/decoding -- [x] 12. Update BaseRunnableModel -- [x] 13. Create integration test -- [ ] 14. Run tests and verify implementation From c5978c2f415df2da36ac585a5d634a54cc47952b Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:19:30 -0800 Subject: [PATCH 36/41] fix: update order of params passed to runModelWithBuffers call from JS level --- packages/runtime/src/wasm-model/wasm-model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index f7050c96..34e99980 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -243,8 +243,8 @@ class WasmModel implements RunnableModel { 0, this.outputsBuffer.getAddress(), outputIndicesBuffer?.getAddress() || 0, - constantIndicesBuffer?.getAddress() || 0, - constantValuesBuffer?.getAddress() || 0 + constantValuesBuffer?.getAddress() || 0, + constantIndicesBuffer?.getAddress() || 0 ) const elapsed = perfElapsed(t0) From 9fc0780e65dafebdf8130829edabed7209124c9a Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:30:50 -0800 Subject: [PATCH 37/41] fix: update C runModel to pass NULL for constant override params --- packages/cli/src/c/model.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/c/model.c b/packages/cli/src/c/model.c index 9c51c3fd..e65c1d27 100644 --- a/packages/cli/src/c/model.c +++ b/packages/cli/src/c/model.c @@ -123,7 +123,7 @@ void setConstantOverridesFromBuffers(double* constantValues, int32_t* constantIn * values. See above for details on the expected format. */ void runModel(double* inputs, double* outputs) { - runModelWithBuffers(inputs, NULL, outputs, NULL); + runModelWithBuffers(inputs, NULL, outputs, NULL, NULL, NULL); } /** From 843c1c90f7732c020e8408df15aecf3e3b47f7bc Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:41:51 -0800 Subject: [PATCH 38/41] fix: move setConstant before setLookup in header to be consistent with order of generated model --- packages/cli/src/c/sde.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/c/sde.h b/packages/cli/src/c/sde.h index 002e35bb..1abc28a2 100644 --- a/packages/cli/src/c/sde.h +++ b/packages/cli/src/c/sde.h @@ -66,8 +66,8 @@ void finish(void); void initConstants(void); void initLevels(void); void setInputs(double* inputValues, int32_t* inputIndices); -void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints); void setConstant(size_t varIndex, size_t* subIndices, double value); +void setLookup(size_t varIndex, size_t* subIndices, double* points, size_t numPoints); void evalAux(void); void evalLevels(void); void storeOutputData(void); From 02f2b6510a0fdab5b69771e1a3fcbe837da9c1a0 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:42:23 -0800 Subject: [PATCH 39/41] fix: move constant-related code before lookup-related code --- .../src/runnable-model/buffered-run-model-params.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/runnable-model/buffered-run-model-params.ts b/packages/runtime/src/runnable-model/buffered-run-model-params.ts index ca60502d..7ada326c 100644 --- a/packages/runtime/src/runnable-model/buffered-run-model-params.ts +++ b/packages/runtime/src/runnable-model/buffered-run-model-params.ts @@ -472,20 +472,20 @@ export class BufferedRunModelParams implements RunModelParams { const inputsLengthInBytes = inputsLengthInElements * Float64Array.BYTES_PER_ELEMENT const outputsLengthInBytes = outputsLengthInElements * Float64Array.BYTES_PER_ELEMENT const outputIndicesLengthInBytes = outputIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT - const lookupsLengthInBytes = lookupsLengthInElements * Float64Array.BYTES_PER_ELEMENT - const lookupIndicesLengthInBytes = lookupIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT const constantsLengthInBytes = constantsLengthInElements * Float64Array.BYTES_PER_ELEMENT const constantIndicesLengthInBytes = constantIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT + const lookupsLengthInBytes = lookupsLengthInElements * Float64Array.BYTES_PER_ELEMENT + const lookupIndicesLengthInBytes = lookupIndicesLengthInElements * Int32Array.BYTES_PER_ELEMENT const requiredLengthInBytes = headerLengthInBytes + extrasLengthInBytes + inputsLengthInBytes + outputsLengthInBytes + outputIndicesLengthInBytes + - lookupsLengthInBytes + - lookupIndicesLengthInBytes + constantsLengthInBytes + - constantIndicesLengthInBytes + constantIndicesLengthInBytes + + lookupsLengthInBytes + + lookupIndicesLengthInBytes if (buffer.byteLength < requiredLengthInBytes) { throw new Error('Buffer must be long enough to contain sections declared in header') } From 189547f05ffdfedb999763f18b6011bee3de1e71 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:42:34 -0800 Subject: [PATCH 40/41] fix: move values-related code before indices-related code --- packages/runtime/src/wasm-model/wasm-model.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/wasm-model/wasm-model.ts b/packages/runtime/src/wasm-model/wasm-model.ts index 34e99980..bdd1e2ed 100644 --- a/packages/runtime/src/wasm-model/wasm-model.ts +++ b/packages/runtime/src/wasm-model/wasm-model.ts @@ -266,12 +266,12 @@ class WasmModel implements RunnableModel { this.outputIndicesBuffer?.dispose() this.outputIndicesBuffer = undefined - this.constantIndicesBuffer?.dispose() - this.constantIndicesBuffer = undefined - this.constantValuesBuffer?.dispose() this.constantValuesBuffer = undefined + this.constantIndicesBuffer?.dispose() + this.constantIndicesBuffer = undefined + // TODO: Dispose the `WasmModule` too? } } From 3a2cce57759d9a57f55d1dbc4508855623958979 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Wed, 4 Feb 2026 14:45:50 -0800 Subject: [PATCH 41/41] fix: update main.c to pass NULL for constant override params --- packages/cli/src/c/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/c/main.c b/packages/cli/src/c/main.c index ad7b0463..2d07f888 100644 --- a/packages/cli/src/c/main.c +++ b/packages/cli/src/c/main.c @@ -111,7 +111,7 @@ int main(int argc, char** argv) { double* outputBuffer = (double*)malloc(numOutputs * numSavePoints * sizeof(double)); // Run the model with the sparse input arrays and output buffer - runModelWithBuffers(inputValues, inputIndices, outputBuffer, NULL); + runModelWithBuffers(inputValues, inputIndices, outputBuffer, NULL, NULL, NULL); if (!suppress_data_output) { if (raw_output) {