diff --git a/packages/plugin-wasm/src/plugin.ts b/packages/plugin-wasm/src/plugin.ts index 4b267b0f..93d133a4 100644 --- a/packages/plugin-wasm/src/plugin.ts +++ b/packages/plugin-wasm/src/plugin.ts @@ -1,13 +1,16 @@ // Copyright (c) 2022 Climate Interactive / New Venture Fund -import { existsSync } from 'fs' +import { existsSync, mkdirSync } from 'fs' +import { writeFile } from 'fs/promises' + import { basename, dirname, join as joinPath } from 'path' import { findUp } from 'find-up' -import type { BuildContext, Plugin } from '@sdeverywhere/build' +import type { BuildContext, ModelSpec, Plugin } from '@sdeverywhere/build' import type { WasmPluginOptions } from './options' +import { sdeNameForVensimVarName } from './var-names' export function wasmPlugin(options?: WasmPluginOptions): Plugin { return new WasmPlugin(options) @@ -16,6 +19,20 @@ export function wasmPlugin(options?: WasmPluginOptions): Plugin { class WasmPlugin implements Plugin { constructor(private readonly options?: WasmPluginOptions) {} + async preGenerate(context: BuildContext, modelSpec: ModelSpec): Promise { + // Ensure that the build directory exists before we generate a file into it + const buildDir = joinPath(context.config.prepDir, 'build') + if (!existsSync(buildDir)) { + mkdirSync(buildDir, { recursive: true }) + } + + // Write a file that will be folded into the generated Wasm module + const outputVarsFile = joinPath(buildDir, 'processed_outputs.js') + const outputVarIds = modelSpec.outputs.map(o => sdeNameForVensimVarName(o.varName)) + const content = `Module["outputVarIds"] = ${JSON.stringify(outputVarIds)};` + await writeFile(outputVarsFile, content) + } + async postGenerateC(context: BuildContext, cContent: string): Promise { context.log('info', ' Generating WebAssembly module') @@ -99,6 +116,8 @@ async function buildWasm( addInput('macros.c') addInput('model.c') addInput('vensim.c') + addArg('--pre-js') + addArg('build/processed_outputs.js') addArg('-Ibuild') addArg('-o') addArg(outputJsPath) diff --git a/packages/plugin-wasm/src/var-names.ts b/packages/plugin-wasm/src/var-names.ts new file mode 100644 index 00000000..60ca16f0 --- /dev/null +++ b/packages/plugin-wasm/src/var-names.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2024 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/tests/integration/impl-var-access/run-tests.js b/tests/integration/impl-var-access/run-tests.js index 9493e686..a47a4d9a 100755 --- a/tests/integration/impl-var-access/run-tests.js +++ b/tests/integration/impl-var-access/run-tests.js @@ -99,12 +99,23 @@ async function createSynchronousRunner() { // fix the generated `wasm-model.js` file so that it works for either ESM or CommonJS. global.__dirname = '.' + // Load the generated Wasm module and verify that it exposes `outputVarIds` const wasmModule = await initWasm() - const wasmResult = initWasmModelAndBuffers(wasmModule, 1, ['_z', '_d[_a1]']) + const actualVarIds = wasmModule.outputVarIds || [] + const expectedVarIds = ['_z', '_d[_a1]'] + if (actualVarIds.length !== expectedVarIds.length || !actualVarIds.every((v, i) => v === expectedVarIds[i])) { + throw new Error( + `Test failed: outputVarIds [${actualVarIds}] in generated Wasm module don't match expected values [${expectedVarIds}]` + ) + } + + // Initialize the synchronous `ModelRunner` that drives the Wasm model + const wasmResult = initWasmModelAndBuffers(wasmModule, 1, wasmModule.outputVarIds) return createWasmModelRunner(wasmResult) } async function createAsynchronousRunner() { + // Initialize the asynchronous `ModelRunner` that drives the Wasm model const modelWorkerJs = await readFile(joinPath('sde-prep', 'worker.js'), 'utf8') return await spawnAsyncModelRunner({ source: modelWorkerJs }) }