From 18b0873e74facea772e56f59a1ba4470ebb1fdd6 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Thu, 23 May 2024 19:09:26 -0700 Subject: [PATCH] fix: update build and plugin packages to support JS code generation (#487) Fixes #479 --- examples/hello-world/package.json | 1 - examples/hello-world/sde.config.js | 6 +- examples/sir/README.md | 2 +- examples/template-default/sde.config.js | 27 ++++++- examples/template-minimal/sde.config.js | 28 +++++++- packages/build/docs/interfaces/Plugin.md | 22 +++--- .../build/docs/interfaces/ResolvedConfig.md | 10 +++ packages/build/docs/interfaces/UserConfig.md | 11 +++ packages/build/src/_shared/resolved-config.ts | 7 ++ packages/build/src/build/impl/gen-model.ts | 72 ++++++++++++------- packages/build/src/config/config-loader.ts | 15 ++++ packages/build/src/config/user-config.ts | 8 +++ packages/build/src/plugin/plugin.ts | 18 ++--- packages/build/tests/build/build-prod.spec.ts | 26 +++---- packages/cli/src/sde-generate.js | 6 +- packages/compile/src/generate/gen-code-js.js | 67 +++++++++-------- .../compile/src/generate/gen-code-js.spec.ts | 72 +++++++++++++------ packages/create/bin/create-sde.js | 2 +- packages/create/src/index.ts | 23 ++++-- packages/create/src/step-code-format.ts | 59 +++++++++++++++ packages/create/src/step-config.ts | 9 ++- packages/create/src/step-emsdk.ts | 2 +- packages/plugin-check/src/var-names.ts | 2 +- packages/plugin-config/src/var-names.ts | 2 +- packages/plugin-wasm/src/plugin.ts | 8 ++- packages/plugin-wasm/src/var-names.ts | 2 +- packages/plugin-worker/src/var-names.ts | 46 ------------ .../src/js-model/js-model-functions.ts | 2 - packages/runtime/src/js-model/js-model.ts | 2 - pnpm-lock.yaml | 3 - .../ext-control-params/package.json | 9 +-- .../ext-control-params/sde.config.js | 8 ++- .../impl-var-access-no-time/package.json | 9 +-- .../impl-var-access-no-time/sde.config.js | 20 ++++-- .../impl-var-access/impl-var-access.mdl | 7 +- .../integration/impl-var-access/package.json | 9 +-- .../integration/impl-var-access/run-tests.js | 5 +- .../integration/impl-var-access/sde.config.js | 22 ++++-- tests/integration/saveper/package.json | 9 +-- tests/integration/saveper/sde.config.js | 8 ++- 40 files changed, 439 insertions(+), 227 deletions(-) create mode 100644 packages/create/src/step-code-format.ts delete mode 100644 packages/plugin-worker/src/var-names.ts diff --git a/examples/hello-world/package.json b/examples/hello-world/package.json index 084522d8..a0b4caa1 100644 --- a/examples/hello-world/package.json +++ b/examples/hello-world/package.json @@ -15,7 +15,6 @@ "@sdeverywhere/build": "^0.3.4", "@sdeverywhere/cli": "^0.7.23", "@sdeverywhere/plugin-check": "^0.3.5", - "@sdeverywhere/plugin-wasm": "^0.2.3", "@sdeverywhere/plugin-worker": "^0.2.3", "vite": "^4.4.9" } diff --git a/examples/hello-world/sde.config.js b/examples/hello-world/sde.config.js index 4ed5ff81..de344b24 100644 --- a/examples/hello-world/sde.config.js +++ b/examples/hello-world/sde.config.js @@ -1,5 +1,4 @@ import { checkPlugin } from '@sdeverywhere/plugin-check' -import { wasmPlugin } from '@sdeverywhere/plugin-wasm' import { workerPlugin } from '@sdeverywhere/plugin-worker' export async function config() { @@ -15,10 +14,7 @@ export async function config() { }, plugins: [ - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), - - // Generate a `worker.js` file that runs the Wasm model in a worker + // Generate a `worker.js` file that runs the generated model in a worker workerPlugin(), // Run model check diff --git a/examples/sir/README.md b/examples/sir/README.md index 8dcafb77..d7361423 100644 --- a/examples/sir/README.md +++ b/examples/sir/README.md @@ -1,6 +1,6 @@ # sir -This example directory contains the class SIR (Susceptible-Infectious-Recovered) model of +This example directory contains the classic SIR (Susceptible-Infectious-Recovered) model of infectious disease. It is intended to demonstrate the use of the `@sdeverywhere/create` package to quickly set up a new project that uses the provided config files to generate a simple web application. diff --git a/examples/template-default/sde.config.js b/examples/template-default/sde.config.js index 9d317c19..224f08d8 100644 --- a/examples/template-default/sde.config.js +++ b/examples/template-default/sde.config.js @@ -13,8 +13,27 @@ const packagePath = (...parts) => joinPath(__dirname, 'packages', ...parts) const appPath = (...parts) => packagePath('app', ...parts) const corePath = (...parts) => packagePath('core', ...parts) +// +// NOTE: This template can generate a model as a WebAssembly module (runs faster, +// but requires additional tools to be installed) or in pure JavaScript format (runs +// slower, but is simpler to build). Regardless of which approach you choose, the +// same APIs (e.g., `ModelRunner`) can be used to run the generated model. +// +// If `genFormat` is 'c', the sde compiler will generate C code, but `plugin-wasm` +// is needed to convert the C code to a WebAssembly module, in which case +// the Emscripten SDK must be installed (the `@sdeverywhere/create` package can +// help with this; see "Quick Start" instructions). +// +// If `genFormat` is 'js', the sde compiler will generate JavaScript code that runs +// in the browser or in Node.js without the additional Emscripten build step. +// +const genFormat = 'js' + export async function config() { return { + // Specify the format of the generated code, either 'js' or 'c' + genFormat, + // Specify the Vensim model to read modelFiles: ['MODEL_NAME.mdl'], @@ -34,10 +53,12 @@ export async function config() { }), plugins: [ - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // If targeting WebAssembly, generate a `generated-model.js` file + // containing the Wasm model + genFormat === 'c' && wasmPlugin(), - // Generate a `worker.js` file that runs the Wasm model in a worker + // Generate a `worker.js` file that runs the model asynchronously on a + // worker thread for improved responsiveness workerPlugin({ outputPaths: [corePath('src', 'model', 'generated', 'worker.js')] }), diff --git a/examples/template-minimal/sde.config.js b/examples/template-minimal/sde.config.js index 894d26c2..2e2b7247 100644 --- a/examples/template-minimal/sde.config.js +++ b/examples/template-minimal/sde.config.js @@ -2,8 +2,28 @@ import { checkPlugin } from '@sdeverywhere/plugin-check' import { wasmPlugin } from '@sdeverywhere/plugin-wasm' import { workerPlugin } from '@sdeverywhere/plugin-worker' +// +// NOTE: This template can generate a model as a WebAssembly module (runs faster, +// but requires additional tools to be installed) or in pure JavaScript format (runs +// slower, but is simpler to build). Regardless of which approach you choose, the +// same APIs (e.g., `ModelRunner`) can be used to run the generated model. +// +// If `genFormat` is 'c', the sde compiler will generate C code, but `plugin-wasm` +// is needed to convert the C code to a WebAssembly module, in which case +// the Emscripten SDK must be installed (the `@sdeverywhere/create` package can +// help with this; see "Quick Start" instructions). +// +// If `genFormat` is 'js', the sde compiler will generate JavaScript code that runs +// in the browser or in Node.js without the additional Emscripten build step. +// +const genFormat = 'js' + export async function config() { return { + // Specify the format of the generated code, either 'js' or 'c' + genFormat, + + // Specify the Vensim model to read modelFiles: ['MODEL_NAME.mdl'], modelSpec: async () => { @@ -24,10 +44,12 @@ export async function config() { }, plugins: [ - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // If targeting WebAssembly, generate a `generated-model.js` file + // containing the Wasm model + genFormat === 'c' && wasmPlugin(), - // Generate a `worker.js` file that runs the Wasm model in a worker + // Generate a `worker.js` file that runs the model asynchronously on a + // worker thread for improved responsiveness workerPlugin(), // Run model check diff --git a/packages/build/docs/interfaces/Plugin.md b/packages/build/docs/interfaces/Plugin.md index d4fbb947..2aefa250 100644 --- a/packages/build/docs/interfaces/Plugin.md +++ b/packages/build/docs/interfaces/Plugin.md @@ -13,8 +13,8 @@ listed below: - preGenerate - preProcessMdl - postProcessMdl - - preGenerateC - - postGenerateC + - preGenerateCode + - postGenerateCode - postGenerate - postBuild - watch (only called once after initial build steps when mode==development) @@ -100,17 +100,18 @@ The modified mdl file content (if postprocessing was needed). ___ -### preGenerateC +### preGenerateCode -`Optional` **preGenerateC**(`context`): `Promise`<`void`\> +`Optional` **preGenerateCode**(`context`, `format`): `Promise`<`void`\> -Called before SDE generates a C file from the mdl file. +Called before SDE generates a JS or C file from the mdl file. #### Parameters | Name | Type | Description | | :------ | :------ | :------ | | `context` | [`BuildContext`](../classes/BuildContext.md) | The build context (for logging, etc). | +| `format` | ``"js"`` \| ``"c"`` | The generated code format, either 'js' or 'c'. | #### Returns @@ -118,24 +119,25 @@ Called before SDE generates a C file from the mdl file. ___ -### postGenerateC +### postGenerateCode -`Optional` **postGenerateC**(`context`, `cContent`): `Promise`<`string`\> +`Optional` **postGenerateCode**(`context`, `format`, `content`): `Promise`<`string`\> -Called after SDE generates a C file from the mdl file. +Called after SDE generates a JS or C file from the mdl file. #### Parameters | Name | Type | Description | | :------ | :------ | :------ | | `context` | [`BuildContext`](../classes/BuildContext.md) | The build context (for logging, etc). | -| `cContent` | `string` | The resulting C file content. | +| `format` | ``"js"`` \| ``"c"`` | The generated code format, either 'js' or 'c'. | +| `content` | `string` | The resulting JS or C file content. | #### Returns `Promise`<`string`\> -The modified C file content (if postprocessing was needed). +The modified JS or C file content (if postprocessing was needed). ___ diff --git a/packages/build/docs/interfaces/ResolvedConfig.md b/packages/build/docs/interfaces/ResolvedConfig.md index 1ad4a8f2..8b9b43a2 100644 --- a/packages/build/docs/interfaces/ResolvedConfig.md +++ b/packages/build/docs/interfaces/ResolvedConfig.md @@ -57,3 +57,13 @@ ___ Paths to files that when changed will trigger a rebuild in watch mode. These can be paths to files or glob patterns (relative to the project directory). + +___ + +### genFormat + + **genFormat**: ``"js"`` \| ``"c"`` + +The code format to generate. If 'js', the model will be compiled to a JavaScript +file. If 'c', the model will be compiled to a C file (in which case an additional +plugin will be needed to convert the C code to a WebAssembly module). diff --git a/packages/build/docs/interfaces/UserConfig.md b/packages/build/docs/interfaces/UserConfig.md index 295a54b8..a306ce43 100644 --- a/packages/build/docs/interfaces/UserConfig.md +++ b/packages/build/docs/interfaces/UserConfig.md @@ -53,6 +53,17 @@ If left undefined, this will resolve to the `modelFiles` array. ___ +### genFormat + + `Optional` **genFormat**: ``"js"`` \| ``"c"`` + +The code format to generate. If 'js', the model will be compiled to a JavaScript +file. If 'c', the model will be compiled to a C file (in which case an additional +plugin will be needed to convert the C code to a WebAssembly module). If undefined, +defaults to 'js'. + +___ + ### plugins `Optional` **plugins**: [`Plugin`](Plugin.md)[] diff --git a/packages/build/src/_shared/resolved-config.ts b/packages/build/src/_shared/resolved-config.ts index 1839e6aa..f24b9ce4 100644 --- a/packages/build/src/_shared/resolved-config.ts +++ b/packages/build/src/_shared/resolved-config.ts @@ -42,6 +42,13 @@ export interface ResolvedConfig { */ watchPaths: string[] + /** + * The code format to generate. If 'js', the model will be compiled to a JavaScript + * file. If 'c', the model will be compiled to a C file (in which case an additional + * plugin will be needed to convert the C code to a WebAssembly module). + */ + genFormat: 'js' | 'c' + /** * The path to the `@sdeverywhere/cli` package. This is currently only used to get * access to the files in the `src/c` directory. diff --git a/packages/build/src/build/impl/gen-model.ts b/packages/build/src/build/impl/gen-model.ts index fadfeca2..43ef8432 100644 --- a/packages/build/src/build/impl/gen-model.ts +++ b/packages/build/src/build/impl/gen-model.ts @@ -58,22 +58,35 @@ export async function generateModel(context: BuildContext, plugins: Plugin[]): P } } - // Generate the C file + // Generate the JS or C file for (const plugin of plugins) { - if (plugin.preGenerateC) { - await plugin.preGenerateC(context) + if (plugin.preGenerateCode) { + await plugin.preGenerateCode(context, config.genFormat) } } - await generateC(context, config.sdeDir, sdeCmdPath, prepDir) + await generateCode(context, config.sdeDir, sdeCmdPath, prepDir) + const generatedCodeFile = `processed.${config.genFormat}` + const generatedCodePath = joinPath(prepDir, 'build', generatedCodeFile) for (const plugin of plugins) { - if (plugin.postGenerateC) { - const cPath = joinPath(prepDir, 'build', 'processed.c') - let cContent = await readFile(cPath, 'utf8') - cContent = await plugin.postGenerateC(context, cContent) - await writeFile(cPath, cContent) + if (plugin.postGenerateCode) { + let generatedCodeContent = await readFile(generatedCodePath, 'utf8') + generatedCodeContent = await plugin.postGenerateCode(context, config.genFormat, generatedCodeContent) + await writeFile(generatedCodePath, generatedCodeContent) } } + if (config.genFormat === 'js') { + // When generating JS code, copy the generated JS file to the `staged/model` + // directory, because that's where plugin-worker expects to find it, but also + // set it up to be copied to the `prepDir`, which is where other code expects + // to find it + // TODO: Maybe we can change plugin-worker to use the one in `prepDir`, and/or + // add a build config setting to allow for customizing the output location + const outputJsFile = 'generated-model.js' + const stagedOutputJsPath = context.prepareStagedFile('model', outputJsFile, prepDir, outputJsFile) + await copyFile(generatedCodePath, stagedOutputJsPath) + } + const t1 = performance.now() const elapsed = ((t1 - t0) / 1000).toFixed(1) log('info', `Done generating model (${elapsed}s)`) @@ -165,16 +178,19 @@ async function flattenMdls( } /** - * Generate a C file from the `processed.mdl` file. + * Generate a JS or C file from the `processed.mdl` file. */ -async function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise { - log('verbose', ' Generating C code') +async function generateCode(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise { + const genFormat = context.config.genFormat + const genFormatName = genFormat.toUpperCase() + log('verbose', ` Generating ${genFormatName} code`) - // Use SDE to generate both a C version of the model (`--genc`) AND a JSON list of all model + // Use SDE to generate both a JS/C version of the model (`--outformat`) AND a JSON list of all model // dimensions and variables (`--list`) const command = sdeCmdPath - const gencArgs = ['generate', '--genc', '--list', '--spec', 'spec.json', 'processed'] - const gencOutput = await context.spawnChild(prepDir, command, gencArgs, { + const outFormat = `--outformat=${genFormat}` + const genCmdArgs = ['generate', outFormat, '--list', '--spec', 'spec.json', 'processed'] + const genCmdOutput = await context.spawnChild(prepDir, command, genCmdArgs, { // By default, ignore lines that start with "WARNING: Data for" since these are often harmless // TODO: Don't filter by default, but make it configurable // ignoredMessageFilter: 'WARNING: Data for' @@ -182,19 +198,23 @@ async function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: stri // following allows us to throw our own error ignoreError: true }) - if (gencOutput.exitCode !== 0) { - throw new Error(`Failed to generate C code: 'sde generate' command failed (code=${gencOutput.exitCode})`) + if (genCmdOutput.exitCode !== 0) { + throw new Error( + `Failed to generate ${genFormatName} code: 'sde generate' command failed (code=${genCmdOutput.exitCode})` + ) } - // Copy SDE's supporting C files into the build directory - const buildDir = joinPath(prepDir, 'build') - const sdeCDir = joinPath(sdeDir, 'src', 'c') - const files = await readdir(sdeCDir) - const copyOps = [] - for (const file of files) { - if (file.endsWith('.c') || file.endsWith('.h')) { - copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file))) + if (genFormat === 'c') { + // Copy SDE's supporting C files into the build directory + const buildDir = joinPath(prepDir, 'build') + const sdeCDir = joinPath(sdeDir, 'src', 'c') + const files = await readdir(sdeCDir) + const copyOps = [] + for (const file of files) { + if (file.endsWith('.c') || file.endsWith('.h')) { + copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file))) + } } + await Promise.all(copyOps) } - await Promise.all(copyOps) } diff --git a/packages/build/src/config/config-loader.ts b/packages/build/src/config/config-loader.ts index 91e87122..ee4ea709 100644 --- a/packages/build/src/config/config-loader.ts +++ b/packages/build/src/config/config-loader.ts @@ -157,6 +157,20 @@ function resolveUserConfig( watchPaths = modelFiles } + // Validate the code generation format + const rawGenFormat = userConfig.genFormat || 'js' + let genFormat: 'js' | 'c' + switch (rawGenFormat) { + case 'js': + genFormat = 'js' + break + case 'c': + genFormat = 'c' + break + default: + throw new Error(`The configured genFormat value is invalid; must be either 'js' or 'c'`) + } + return { mode, rootDir, @@ -164,6 +178,7 @@ function resolveUserConfig( modelFiles, modelInputPaths, watchPaths, + genFormat, sdeDir, sdeCmdPath } diff --git a/packages/build/src/config/user-config.ts b/packages/build/src/config/user-config.ts index 8440cb96..1d9d12b5 100644 --- a/packages/build/src/config/user-config.ts +++ b/packages/build/src/config/user-config.ts @@ -40,6 +40,14 @@ export interface UserConfig { */ watchPaths?: string[] + /** + * The code format to generate. If 'js', the model will be compiled to a JavaScript + * file. If 'c', the model will be compiled to a C file (in which case an additional + * plugin will be needed to convert the C code to a WebAssembly module). If undefined, + * defaults to 'js'. + */ + genFormat?: 'js' | 'c' + /** * The array of plugins that are used to customize the build process. These will be * executed in the order defined here. diff --git a/packages/build/src/plugin/plugin.ts b/packages/build/src/plugin/plugin.ts index 925f8fcd..3ce1f4bc 100644 --- a/packages/build/src/plugin/plugin.ts +++ b/packages/build/src/plugin/plugin.ts @@ -16,8 +16,8 @@ import type { BuildContext } from '../context/context' * - preGenerate * - preProcessMdl * - postProcessMdl - * - preGenerateC - * - postGenerateC + * - preGenerateCode + * - postGenerateCode * - postGenerate * - postBuild * - watch (only called once after initial build steps when mode==development) @@ -58,20 +58,22 @@ export interface Plugin { postProcessMdl?(context: BuildContext, mdlContent: string): Promise /** - * Called before SDE generates a C file from the mdl file. + * Called before SDE generates a JS or C file from the mdl file. * * @param context The build context (for logging, etc). + * @param format The generated code format, either 'js' or 'c'. */ - preGenerateC?(context: BuildContext): Promise + preGenerateCode?(context: BuildContext, format: 'js' | 'c'): Promise /** - * Called after SDE generates a C file from the mdl file. + * Called after SDE generates a JS or C file from the mdl file. * * @param context The build context (for logging, etc). - * @param cContent The resulting C file content. - * @return The modified C file content (if postprocessing was needed). + * @param format The generated code format, either 'js' or 'c'. + * @param content The resulting JS or C file content. + * @return The modified JS or C file content (if postprocessing was needed). */ - postGenerateC?(context: BuildContext, cContent: string): Promise + postGenerateCode?(context: BuildContext, format: 'js' | 'c', content: string): Promise /** * Called after the "generate model" process has completed (but before the staged diff --git a/packages/build/tests/build/build-prod.spec.ts b/packages/build/tests/build/build-prod.spec.ts index 67e25577..00ebbcfb 100644 --- a/packages/build/tests/build/build-prod.spec.ts +++ b/packages/build/tests/build/build-prod.spec.ts @@ -33,12 +33,12 @@ const plugin = (num: number, calls: string[]) => { record('postProcessMdl') return mdlContent }, - preGenerateC: async () => { - record('preGenerateC') + preGenerateCode: async (_, format) => { + record(`preGenerateCode ${format}`) }, - postGenerateC: async (_, cContent) => { - record('postGenerateC') - return cContent + postGenerateCode: async (_, format, content) => { + record(`postGenerateCode ${format}`) + return content }, postGenerate: async () => { record('postGenerate') @@ -60,6 +60,7 @@ describe('build in production mode', () => { const calls: string[] = [] const userConfig: UserConfig = { + genFormat: 'c', rootDir: resolvePath(__dirname, '..'), prepDir: resolvePath(__dirname, 'sde-prep'), modelFiles: [], @@ -119,10 +120,10 @@ describe('build in production mode', () => { 'plugin 2: preProcessMdl', 'plugin 1: postProcessMdl', 'plugin 2: postProcessMdl', - 'plugin 1: preGenerateC', - 'plugin 2: preGenerateC', - 'plugin 1: postGenerateC', - 'plugin 2: postGenerateC', + 'plugin 1: preGenerateCode js', + 'plugin 2: preGenerateCode js', + 'plugin 1: postGenerateCode js', + 'plugin 2: postGenerateCode js', 'plugin 1: postGenerate', 'plugin 2: postGenerate', 'plugin 1: postBuild', @@ -163,8 +164,8 @@ describe('build in production mode', () => { it('in preGenerate', async () => verify('preGenerate')) it('in preProcessMdl', async () => verify('preProcessMdl')) it('in postProcessMdl', async () => verify('postProcessMdl')) - it('in preGenerateC', async () => verify('preGenerateC')) - it('in postGenerateC', async () => verify('postGenerateC')) + it('in preGenerateCode', async () => verify('preGenerateCode')) + it('in postGenerateCode', async () => verify('postGenerateCode')) it('in postGenerate', async () => verify('postGenerate')) it('in postBuild', async () => verify('postBuild')) }) @@ -237,6 +238,7 @@ describe('build in production mode', () => { } const userConfig: UserConfig = { + genFormat: 'c', rootDir: resolvePath(__dirname, '..'), prepDir: resolvePath(__dirname, 'sde-prep'), modelFiles: [resolvePath(__dirname, '..', '_shared', 'sample.mdl')], @@ -251,7 +253,7 @@ describe('build in production mode', () => { } // TODO: This error message isn't helpful, but it's due to the fact that - // the `generateC` function spawns an `sde` process rather than calling + // the `generateCode` function spawns an `sde` process rather than calling // into the compiler directly. Once we improve it to call into the // compiler, the error message here should be the one from `readDat`. expect(result.error.message).toBe(`Failed to generate C code: 'sde generate' command failed (code=1)`) diff --git a/packages/cli/src/sde-generate.js b/packages/cli/src/sde-generate.js index 9a01dd55..930d3581 100644 --- a/packages/cli/src/sde-generate.js +++ b/packages/cli/src/sde-generate.js @@ -83,9 +83,9 @@ export let generate = async (model, opts) => { operations.push('generateJS') } if (opts.genc || opts.outformat === 'c') { - // if (opts.genc) { - // console.warn(`WARNING: --genc option is deprecated for the 'sde generate' command; use --outformat=c instead`) - // } + if (opts.genc) { + console.warn(`WARNING: --genc option is deprecated for the 'sde generate' command; use --outformat=c instead`) + } operations.push('generateC') } if (opts.list) { diff --git a/packages/compile/src/generate/gen-code-js.js b/packages/compile/src/generate/gen-code-js.js index 8201a312..c3f75686 100644 --- a/packages/compile/src/generate/gen-code-js.js +++ b/packages/compile/src/generate/gen-code-js.js @@ -1,6 +1,6 @@ import * as R from 'ramda' -import { asort, lines, strlist, mapIndexed } from '../_shared/helpers.js' +import { asort, canonicalVensimName, lines, strlist, mapIndexed } from '../_shared/helpers.js' import { sub, allDimensions, allMappings, subscriptFamilies } from '../_shared/subscript.js' import Model from '../model/model.js' @@ -16,14 +16,7 @@ let codeGenerator = (parsedModel, opts) => { // Set to 'decl', 'init-lookups', 'eval', etc depending on the section being generated. let mode = '' // Set to true to output all variables when there is no model run spec. - let outputAllVars - if (spec.outputVars && spec.outputVars.length > 0) { - outputAllVars = false - } else if (spec.outputVarNames && spec.outputVarNames.length > 0) { - outputAllVars = false - } else { - outputAllVars = true - } + let outputAllVars = spec.outputVarNames === undefined || spec.outputVarNames.length === 0 // Function to generate a section of the code let generateSection = R.map(v => { return generateEquation(v, mode, extData, directData, modelDirname, 'js') @@ -94,38 +87,37 @@ function initControlParamsIfNeeded() { throw new Error('Must call setModelFunctions() before running the model'); } - // We currently require INITIAL TIME, FINAL TIME, and TIME STEP to be - // defined as constant values. Some models may define SAVEPER in terms - // of TIME STEP, which means that the compiler may treat it as an aux, - // not as a constant. We call initConstants() to ensure that we have - // initial values for these control parameters. + // We currently require INITIAL TIME and TIME STEP to be defined + // as constant values. Some models may define SAVEPER in terms of + // TIME STEP (or FINAL TIME in terms of INITIAL TIME), which means + // that the compiler may treat them as an aux, not as a constant. + // We call initConstants() to ensure that we have initial values + // for these control parameters. initConstants(); if (_initial_time === undefined) { throw new Error('INITIAL TIME must be defined as a constant value'); } - if (_final_time === undefined) { - throw new Error('FINAL TIME must be defined as a constant value'); - } if (_time_step === undefined) { throw new Error('TIME STEP must be defined as a constant value'); } - if (_saveper === undefined) { - // If _saveper is undefined after calling initConstants(), it means it - // is defined as an aux, in which case we perform an initial step of - // the run loop in order to initialize that value. First, set the - // time and initial function context. + if (_final_time === undefined || _saveper === undefined) { + // If _final_time or _saveper is undefined after calling initConstants(), + // it means one or both is defined as an aux, in which case we perform + // an initial step of the run loop in order to initialize the value(s). + // First, set the time and initial function context. setTime(_initial_time); fns.setContext({ - initialTime: _initial_time, - finalTime: _final_time, timeStep: _time_step, currentTime: _time }); - // Perform initial step to initialize _saveper + // Perform initial step to initialize _final_time and/or _saveper initLevels(); evalAux(); + if (_final_time === undefined) { + throw new Error('FINAL TIME must be defined'); + } if (_saveper === undefined) { throw new Error('SAVEPER must be defined'); } @@ -246,13 +238,28 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels' // Input/output section // function emitIOCode() { - const outputVarIds = outputAllVars ? expandedVarNames() : spec.outputVars - const outputVarNames = outputAllVars ? expandedVarNames(true) : spec.outputVars - const outputVarIdElems = outputVarIds.map(id => `'${id}'`).join(',\n ') + mode = 'io' + + // 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 + // with a header line that includes the original variable names for all outputs. + const outputVarNames = outputAllVars ? expandedVarNames(true) : spec.outputVarNames const outputVarNameElems = outputVarNames .map(name => `'${Model.vensimName(name).replace(/'/g, `\\'`)}'`) .join(',\n ') - mode = 'io' + + // This is the list of output variable identifiers (in canonical format), for + // example, `_a[_a2,_b2]`. These are exported for use in the runtime package + // for having a canonical identifier associated with the data for each output. + const outputVarIds = outputVarNames.map(canonicalVensimName) + const outputVarIdElems = outputVarIds.map(id => `'${id}'`).join(',\n ') + + // This is the list of output variable access declarations, which are in valid + // C code format, with subscripts mapped to C index form, for example, + // `_a[1][0]`. These are used in the implementation of `storeOutputs`. + const outputVarAccesses = outputAllVars ? expandedVarNames() : spec.outputVars + return `\ /*export*/ function setInputs(valueAtIndex /*: (index: number) => number*/) {${inputsFromBufferImpl()}} @@ -265,7 +272,7 @@ ${chunkedFunctions('evalLevels', true, Model.levelVars(), ' // Evaluate levels' ]; /*export*/ function storeOutputs(storeValue /*: (value: number) => void*/) { -${specOutputSection(outputVarIds)} +${specOutputSection(outputVarAccesses)} } /*export*/ function storeOutput(varSpec /*: VarSpec*/, storeValue /*: (value: number) => void*/) { diff --git a/packages/compile/src/generate/gen-code-js.spec.ts b/packages/compile/src/generate/gen-code-js.spec.ts index 4940c303..bb1128ee 100644 --- a/packages/compile/src/generate/gen-code-js.spec.ts +++ b/packages/compile/src/generate/gen-code-js.spec.ts @@ -164,19 +164,25 @@ function runJsModel(model: JsModel, inputs: number[], outputs: number[]) { describe('generateJS (Vensim -> JS)', () => { it('should generate code for a simple model', () => { const mdl = ` + DimA: A1, A2 ~~| + DimB: B1, B2 ~~| input = 1 ~~| x = input ~~| y = :NOT: x ~~| z = ABS(y) ~~| w = WITH LOOKUP(x, ( [(0,0)-(2,2)], (0,0),(0.1,0.01),(0.5,0.7),(1,1),(1.5,1.2),(2,1.3) )) ~~| + a[DimA] = 0, 1 ~~| + b[DimA, DimB] = 5 ~~| ` const code = readInlineModelAndGenerateJS(mdl, { inputVarNames: ['input'], - outputVarNames: ['x', 'y', 'z', 'w'] + outputVarNames: ['a[A1]', 'b[A2,B1]', 'x', 'y', 'z', 'w'] }) expect(code).toEqual(`\ // Model variables let __lookup1; +let _a = multiDimArray([2]); +let _b = multiDimArray([2, 2]); let _input; let _w; let _x; @@ -184,7 +190,8 @@ let _y; let _z; // Array dimensions - +const _dima = [0, 1]; +const _dimb = [0, 1]; // Dimension mappings @@ -210,38 +217,37 @@ function initControlParamsIfNeeded() { throw new Error('Must call setModelFunctions() before running the model'); } - // We currently require INITIAL TIME, FINAL TIME, and TIME STEP to be - // defined as constant values. Some models may define SAVEPER in terms - // of TIME STEP, which means that the compiler may treat it as an aux, - // not as a constant. We call initConstants() to ensure that we have - // initial values for these control parameters. + // We currently require INITIAL TIME and TIME STEP to be defined + // as constant values. Some models may define SAVEPER in terms of + // TIME STEP (or FINAL TIME in terms of INITIAL TIME), which means + // that the compiler may treat them as an aux, not as a constant. + // We call initConstants() to ensure that we have initial values + // for these control parameters. initConstants(); if (_initial_time === undefined) { throw new Error('INITIAL TIME must be defined as a constant value'); } - if (_final_time === undefined) { - throw new Error('FINAL TIME must be defined as a constant value'); - } if (_time_step === undefined) { throw new Error('TIME STEP must be defined as a constant value'); } - if (_saveper === undefined) { - // If _saveper is undefined after calling initConstants(), it means it - // is defined as an aux, in which case we perform an initial step of - // the run loop in order to initialize that value. First, set the - // time and initial function context. + if (_final_time === undefined || _saveper === undefined) { + // If _final_time or _saveper is undefined after calling initConstants(), + // it means one or both is defined as an aux, in which case we perform + // an initial step of the run loop in order to initialize the value(s). + // First, set the time and initial function context. setTime(_initial_time); fns.setContext({ - initialTime: _initial_time, - finalTime: _final_time, timeStep: _time_step, currentTime: _time }); - // Perform initial step to initialize _saveper + // Perform initial step to initialize _final_time and/or _saveper initLevels(); evalAux(); + if (_final_time === undefined) { + throw new Error('FINAL TIME must be defined'); + } if (_saveper === undefined) { throw new Error('SAVEPER must be defined'); } @@ -316,6 +322,16 @@ function initData() { } function initConstants0() { + // a[DimA] = 0,1 + _a[0] = 0.0; + // a[DimA] = 0,1 + _a[1] = 1.0; + // b[DimA,DimB] = 5 + for (let i = 0; i < 2; i++) { + for (let j = 0; j < 2; j++) { + _b[i][j] = 5.0; + } + } // input = 1 _input = 1.0; } @@ -356,6 +372,8 @@ function evalAux0() { } /*export*/ const outputVarIds = [ + '_a[_a1]', + '_b[_a2,_b1]', '_x', '_y', '_z', @@ -363,6 +381,8 @@ function evalAux0() { ]; /*export*/ const outputVarNames = [ + 'a[A1]', + 'b[A2,B1]', 'x', 'y', 'z', @@ -370,6 +390,8 @@ function evalAux0() { ]; /*export*/ function storeOutputs(storeValue /*: (value: number) => void*/) { + storeValue(_a[0]); + storeValue(_b[1][0]); storeValue(_x); storeValue(_y); storeValue(_z); @@ -379,18 +401,24 @@ function evalAux0() { /*export*/ function storeOutput(varSpec /*: VarSpec*/, storeValue /*: (value: number) => void*/) { switch (varSpec.varIndex) { case 1: - storeValue(_input); + storeValue(_a[varSpec.subscriptIndices[0]]); break; case 2: - storeValue(_x); + storeValue(_b[varSpec.subscriptIndices[0]][varSpec.subscriptIndices[1]]); break; case 3: - storeValue(_w); + storeValue(_input); break; case 4: - storeValue(_y); + storeValue(_x); break; case 5: + storeValue(_w); + break; + case 6: + storeValue(_y); + break; + case 7: storeValue(_z); break; default: diff --git a/packages/create/bin/create-sde.js b/packages/create/bin/create-sde.js index 20b69186..a8f5b9db 100755 --- a/packages/create/bin/create-sde.js +++ b/packages/create/bin/create-sde.js @@ -3,7 +3,7 @@ const currentVersion = process.versions.node const requiredMajorVersion = parseInt(currentVersion.split('.')[0], 10) -const minimumMajorVersion = 14 +const minimumMajorVersion = 18 if (requiredMajorVersion < minimumMajorVersion) { console.error(`Node.js v${currentVersion} is not supported by SDEverywhere.`) diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index 6f8a4e62..3daa5ab8 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -8,6 +8,7 @@ import prompts from 'prompts' import detectPackageManager from 'which-pm-runs' import yargs from 'yargs-parser' +import { chooseCodeFormat } from './step-code-format' import { chooseGenConfig, generateCheckYaml, updateSdeConfig } from './step-config' import { chooseInstallDeps } from './step-deps' import { chooseProjectDir } from './step-directory' @@ -38,12 +39,19 @@ export async function main(): Promise { // Prompt the user to select an mdl file const mdlPath = await chooseMdlFile(projDir) + console.log() + + // Prompt the user to select a code generation format + const genFormat = await chooseCodeFormat() + console.log() // Update the `sde.config.js` file to use the chosen mdl file and // generate a sample `.check.yaml` file - await updateSdeConfig(projDir, mdlPath) - await generateCheckYaml(projDir, mdlPath) - console.log() + if (!args.dryRun) { + await updateSdeConfig(projDir, mdlPath, genFormat) + await generateCheckYaml(projDir, mdlPath) + console.log() + } // If the user chose the default template, offer to set up CSV files if (templateName === 'template-default' && !args.dryRun) { @@ -51,9 +59,12 @@ export async function main(): Promise { console.log() } - // Prompt the user to install Emscripten SDK - await chooseInstallEmsdk(projDir, args) - console.log() + // If the user chose C as the code generation format, prompt the user to + // install Emscripten SDK + if (genFormat === 'c') { + await chooseInstallEmsdk(projDir, args) + console.log() + } // Prompt the user to install dependencies await chooseInstallDeps(projDir, args, pkgManager) diff --git a/packages/create/src/step-code-format.ts b/packages/create/src/step-code-format.ts new file mode 100644 index 00000000..5817b293 --- /dev/null +++ b/packages/create/src/step-code-format.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2024 Climate Interactive / New Venture Fund + +import { bold, dim, green } from 'kleur/colors' +import ora from 'ora' +import prompts from 'prompts' + +const promptMessage = `Would you like your project to use WebAssembly?` + +const noDesc = `\ +* Choose this if you want to get started using SDEverywhere quickly +and you don't want to install the Emscripten SDK. +* This will generate a model that uses JavaScript code only, which +doesn't require extra build tools, but runs slower than WebAssembly.` +const yesDesc = `\ +* Choose this if you want this script to install the Emscripten SDK +for you, or if you already have it installed. +* This will generate a model that uses WebAssembly, which requires an +additional build step, but runs faster than pure JavaScript code.` + +const FORMATS = [ + { + title: 'No, generate a JavaScript model', + description: noDesc, + value: 'js' + }, + { + title: 'Yes, generate a WebAssembly model', + description: yesDesc, + value: 'c' + } +] + +export async function chooseCodeFormat(): Promise { + // Prompt the user + const options = await prompts( + [ + { + type: 'select', + name: 'format', + message: promptMessage, + choices: FORMATS + } + ], + { + onCancel: () => { + ora().info(dim('Operation cancelled.')) + process.exit(0) + } + } + ) + + const target = options.format === 'c' ? 'WebAssembly' : 'JavaScript' + const successMessage = green( + `Configuring your project to generate ${target}. See "${bold('sde.config.js')}" for details.` + ) + ora(successMessage).succeed() + + return options.format +} diff --git a/packages/create/src/step-config.ts b/packages/create/src/step-config.ts index d44a7db9..1d29743d 100644 --- a/packages/create/src/step-config.ts +++ b/packages/create/src/step-config.ts @@ -47,11 +47,14 @@ const sampleCheckContent = `\ - gt: 0 ` -export async function updateSdeConfig(projDir: string, mdlPath: string): Promise { +export async function updateSdeConfig(projDir: string, mdlPath: string, genFormat: string): Promise { // Read the `sde.config.js` file from the template const configPath = joinPath(projDir, 'sde.config.js') let configText = await readFile(configPath, 'utf8') + // Set the code generation format to the chosen format + configText = configText.replace(`const genFormat = 'js'`, `const genFormat = '${genFormat}'`) + // Replace instances of `MODEL_NAME.mdl` with the path to the chosen mdl file configText = configText.replaceAll('MODEL_NAME.mdl', mdlPath) @@ -418,8 +421,10 @@ async function readModelVars(projDir: string, mdlPath: string): Promise sdeNameForVensimName(x)) - id += `[${subscripts.join('][')}]` + id += `[${subscripts.join(',')}]` } return id diff --git a/packages/plugin-config/src/var-names.ts b/packages/plugin-config/src/var-names.ts index 763a36f9..711f34a2 100644 --- a/packages/plugin-config/src/var-names.ts +++ b/packages/plugin-config/src/var-names.ts @@ -42,7 +42,7 @@ export function sdeNameForVensimVarName(varName: string): string { let id = sdeNameForVensimName(m[1]) if (m[2]) { const subscripts = m[2].split(',').map(x => sdeNameForVensimName(x)) - id += `[${subscripts.join('][')}]` + id += `[${subscripts.join(',')}]` } return id diff --git a/packages/plugin-wasm/src/plugin.ts b/packages/plugin-wasm/src/plugin.ts index 8787b49c..6eac342e 100644 --- a/packages/plugin-wasm/src/plugin.ts +++ b/packages/plugin-wasm/src/plugin.ts @@ -36,7 +36,11 @@ Module["outputVarIds"] = ${JSON.stringify(outputVarIds)}; await writeFile(preJsFile, content) } - async postGenerateC(context: BuildContext, cContent: string): Promise { + async postGenerateCode(context: BuildContext, format: 'js' | 'c', content: string): Promise { + if (format !== 'c') { + throw new Error("When using plugin-wasm, you must set `genFormat` to 'c' in your `sde.config.js` file") + } + context.log('info', ' Generating WebAssembly module') // If `outputJsPath` is undefined, write `generated-model.js` to the prep dir @@ -59,7 +63,7 @@ Module["outputVarIds"] = ${JSON.stringify(outputVarIds)}; // context.log('info', ' Done!') - return cContent + return content } } diff --git a/packages/plugin-wasm/src/var-names.ts b/packages/plugin-wasm/src/var-names.ts index 60ca16f0..b4bb649e 100644 --- a/packages/plugin-wasm/src/var-names.ts +++ b/packages/plugin-wasm/src/var-names.ts @@ -39,7 +39,7 @@ export function sdeNameForVensimVarName(varName: string): string { let id = sdeNameForVensimName(m[1]) if (m[2]) { const subscripts = m[2].split(',').map(x => sdeNameForVensimName(x)) - id += `[${subscripts.join('][')}]` + id += `[${subscripts.join(',')}]` } return id diff --git a/packages/plugin-worker/src/var-names.ts b/packages/plugin-worker/src/var-names.ts deleted file mode 100644 index c3ea560a..00000000 --- a/packages/plugin-worker/src/var-names.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2022 Climate Interactive / New Venture Fund - -/** - * Helper function that converts a Vensim variable or subscript name - * into a valid C identifier as used by SDE. - * TODO: Import helper function from `compile` package instead - */ -function sdeNameForVensimName(name: string): string { - return ( - '_' + - name - .trim() - .replace(/"/g, '_') - .replace(/\s+!$/g, '!') - .replace(/\s/g, '_') - .replace(/,/g, '_') - .replace(/-/g, '_') - .replace(/\./g, '_') - .replace(/\$/g, '_') - .replace(/'/g, '_') - .replace(/&/g, '_') - .replace(/%/g, '_') - .replace(/\//g, '_') - .replace(/\|/g, '_') - .toLowerCase() - ) -} - -/** - * Helper function that converts a Vensim variable name (possibly containing - * subscripts) into a valid C identifier as used by SDE. - * TODO: Import helper function from `compile` package instead - */ -export function sdeNameForVensimVarName(varName: string): string { - const m = varName.match(/([^[]+)(?:\[([^\]]+)\])?/) - if (!m) { - throw new Error(`Invalid Vensim name: ${varName}`) - } - let id = sdeNameForVensimName(m[1]) - if (m[2]) { - const subscripts = m[2].split(',').map(x => sdeNameForVensimName(x)) - id += `[${subscripts.join('][')}]` - } - - return id -} diff --git a/packages/runtime/src/js-model/js-model-functions.ts b/packages/runtime/src/js-model/js-model-functions.ts index 65992a8f..21fb41b8 100644 --- a/packages/runtime/src/js-model/js-model-functions.ts +++ b/packages/runtime/src/js-model/js-model-functions.ts @@ -19,8 +19,6 @@ const _NA_ = -Number.MAX_VALUE * `JsModel` implementations. */ export interface JsModelFunctionContext { - initialTime: number - finalTime: number timeStep: number currentTime: number } diff --git a/packages/runtime/src/js-model/js-model.ts b/packages/runtime/src/js-model/js-model.ts index 42ea16ab..0b376b33 100644 --- a/packages/runtime/src/js-model/js-model.ts +++ b/packages/runtime/src/js-model/js-model.ts @@ -102,8 +102,6 @@ function runJsModel( // Configure the functions. The function context makes the control variable values // available to certain functions that depend on those values. const fnContext: JsModelFunctionContext = { - initialTime, - finalTime, timeStep, currentTime: time } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12213eef..09aad364 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,6 @@ importers: '@sdeverywhere/plugin-check': specifier: ^0.3.5 version: link:../../packages/plugin-check - '@sdeverywhere/plugin-wasm': - specifier: ^0.2.3 - version: link:../../packages/plugin-wasm '@sdeverywhere/plugin-worker': specifier: ^0.2.3 version: link:../../packages/plugin-worker diff --git a/tests/integration/ext-control-params/package.json b/tests/integration/ext-control-params/package.json index 8d312501..35346dff 100644 --- a/tests/integration/ext-control-params/package.json +++ b/tests/integration/ext-control-params/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "clean": "rm -rf sde-prep", - "build": "sde bundle", - "dev": "sde dev", + "build-js": "GEN_FORMAT=js sde bundle", + "build-wasm": "GEN_FORMAT=c sde bundle", "run-tests": "./run-tests.js", - "test": "run-s build run-tests", - "ci:int-test": "run-s clean test" + "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:*", diff --git a/tests/integration/ext-control-params/sde.config.js b/tests/integration/ext-control-params/sde.config.js index 1b2aa0c7..63c1f539 100644 --- a/tests/integration/ext-control-params/sde.config.js +++ b/tests/integration/ext-control-params/sde.config.js @@ -1,8 +1,11 @@ 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: ['ext-control-params.mdl'], modelSpec: async () => { @@ -22,8 +25,9 @@ export async function config() { } }, - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // 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() diff --git a/tests/integration/impl-var-access-no-time/package.json b/tests/integration/impl-var-access-no-time/package.json index 2783ed76..50588262 100644 --- a/tests/integration/impl-var-access-no-time/package.json +++ b/tests/integration/impl-var-access-no-time/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "clean": "rm -rf sde-prep", - "build": "sde bundle", - "dev": "sde dev", + "build-js": "GEN_FORMAT=js sde bundle", + "build-wasm": "GEN_FORMAT=c sde bundle", "run-tests": "./run-tests.js", - "test": "run-s build run-tests", - "ci:int-test": "run-s clean test" + "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:*", diff --git a/tests/integration/impl-var-access-no-time/sde.config.js b/tests/integration/impl-var-access-no-time/sde.config.js index a78c94db..a13eab5a 100644 --- a/tests/integration/impl-var-access-no-time/sde.config.js +++ b/tests/integration/impl-var-access-no-time/sde.config.js @@ -1,8 +1,11 @@ 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: ['impl-var-access-no-time.mdl'], modelSpec: async () => { @@ -16,15 +19,20 @@ export async function config() { plugins: [ // Include a custom plugin that applies post-processing steps { - postGenerateC: (_, cContent) => { - // Edit the generated C code so that it enables the `SDE_USE_OUTPUT_INDICES` flag; this is - // required in order to access impl (non-exported) model variables - return cContent.replace('#define SDE_USE_OUTPUT_INDICES 0', '#define SDE_USE_OUTPUT_INDICES 1') + postGenerateCode: (_, format, content) => { + if (format === 'c') { + // Edit the generated C code so that it enables the `SDE_USE_OUTPUT_INDICES` flag; this is + // required in order to access impl (non-exported) model variables + return content.replace('#define SDE_USE_OUTPUT_INDICES 0', '#define SDE_USE_OUTPUT_INDICES 1') + } else { + return content + } } }, - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // 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() diff --git a/tests/integration/impl-var-access/impl-var-access.mdl b/tests/integration/impl-var-access/impl-var-access.mdl index 6d883c92..91b9f77b 100644 --- a/tests/integration/impl-var-access/impl-var-access.mdl +++ b/tests/integration/impl-var-access/impl-var-access.mdl @@ -40,7 +40,12 @@ C[DimA, DimB] = A[DimA] + B[DimB] D[DimA] = X + SUM(C[DimA, DimB!]) ~ dmnl - ~ This is an output variable. + ~ This is a 1D subscripted output variable. + | + +E[DimA, DimB] = A[DimA] + B[DimB] + ~ dmnl + ~ This is a 2D subscripted output variable. | INITIAL TIME = 2000 ~~| diff --git a/tests/integration/impl-var-access/package.json b/tests/integration/impl-var-access/package.json index 344e1d00..d56418ef 100644 --- a/tests/integration/impl-var-access/package.json +++ b/tests/integration/impl-var-access/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "clean": "rm -rf sde-prep", - "build": "sde bundle", - "dev": "sde dev", + "build-js": "GEN_FORMAT=js sde bundle", + "build-wasm": "GEN_FORMAT=c sde bundle", "run-tests": "./run-tests.js", - "test": "run-s build run-tests", - "ci:int-test": "run-s clean test" + "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:*", diff --git a/tests/integration/impl-var-access/run-tests.js b/tests/integration/impl-var-access/run-tests.js index 7400bd27..9e6df6bc 100755 --- a/tests/integration/impl-var-access/run-tests.js +++ b/tests/integration/impl-var-access/run-tests.js @@ -43,6 +43,9 @@ function verifyDeclaredOutputs(runnerKind, outputs, inputX) { // D[DimA] = X + SUM(C[DimA, DimB!]) // D[A1] = X + (A[A1] + B[B1]) + (A[A1] + B[B2]) + (A[A1] + B[B3]) verify(runnerKind, outputs, inputX, '_d[_a1]', (_, inputX) => inputX + 1 + 100 + 1 + 200 + 1 + 300) + + // E[DimA, DimB] = D[DimA] + B[DimB] + verify(runnerKind, outputs, inputX, '_e[_a2,_b1]', () => 2 + 100) } function verifyImplOutputs(runnerKind, outputs, inputX) { @@ -102,7 +105,7 @@ async function createSynchronousRunner() { // Load the generated model and verify that it exposes `outputVarIds` const generatedModel = await loadGeneratedModel() const actualVarIds = generatedModel.outputVarIds || [] - const expectedVarIds = ['_z', '_d[_a1]'] + const expectedVarIds = ['_z', '_d[_a1]', '_e[_a2,_b1]'] if (actualVarIds.length !== expectedVarIds.length || !actualVarIds.every((v, i) => v === expectedVarIds[i])) { throw new Error( `Test failed: outputVarIds [${actualVarIds}] in generated model don't match expected values [${expectedVarIds}]` diff --git a/tests/integration/impl-var-access/sde.config.js b/tests/integration/impl-var-access/sde.config.js index adc1e039..f526d709 100644 --- a/tests/integration/impl-var-access/sde.config.js +++ b/tests/integration/impl-var-access/sde.config.js @@ -1,14 +1,17 @@ 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: ['impl-var-access.mdl'], modelSpec: async () => { return { inputs: [{ varName: 'X', defaultValue: 0, minValue: -10, maxValue: 10 }], - outputs: [{ varName: 'Z' }, { varName: 'D[A1]' }], + outputs: [{ varName: 'Z' }, { varName: 'D[A1]' }, { varName: 'E[A2,B1]' }], datFiles: [] } }, @@ -16,15 +19,20 @@ export async function config() { plugins: [ // Include a custom plugin that applies post-processing steps { - postGenerateC: (_, cContent) => { - // Edit the generated C code so that it enables the `SDE_USE_OUTPUT_INDICES` flag; this is - // required in order to access impl (non-exported) model variables - return cContent.replace('#define SDE_USE_OUTPUT_INDICES 0', '#define SDE_USE_OUTPUT_INDICES 1') + postGenerateCode: (_, format, content) => { + if (format === 'c') { + // Edit the generated C code so that it enables the `SDE_USE_OUTPUT_INDICES` flag; this is + // required in order to access impl (non-exported) model variables + return content.replace('#define SDE_USE_OUTPUT_INDICES 0', '#define SDE_USE_OUTPUT_INDICES 1') + } else { + return content + } } }, - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // 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() diff --git a/tests/integration/saveper/package.json b/tests/integration/saveper/package.json index 439ec506..8e4fc694 100644 --- a/tests/integration/saveper/package.json +++ b/tests/integration/saveper/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "clean": "rm -rf sde-prep", - "build": "sde bundle", - "dev": "sde dev", + "build-js": "GEN_FORMAT=js sde bundle", + "build-wasm": "GEN_FORMAT=c sde bundle", "run-tests": "./run-tests.js", - "test": "run-s build run-tests", - "ci:int-test": "run-s clean test" + "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:*", diff --git a/tests/integration/saveper/sde.config.js b/tests/integration/saveper/sde.config.js index bfb1db56..10d1de37 100644 --- a/tests/integration/saveper/sde.config.js +++ b/tests/integration/saveper/sde.config.js @@ -1,8 +1,11 @@ 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: ['saveper.mdl'], modelSpec: async () => { @@ -14,8 +17,9 @@ export async function config() { }, plugins: [ - // Generate a `generated-model.js` file containing the Wasm model - wasmPlugin(), + // 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()