Skip to content

Commit

Permalink
fix: change plugin-wasm to expose outputVarIds in the generated module (
Browse files Browse the repository at this point in the history
#482)

Fixes #481
  • Loading branch information
chrispcampbell authored May 9, 2024
1 parent 1a77eed commit 9c2f7d1
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 3 deletions.
23 changes: 21 additions & 2 deletions packages/plugin-wasm/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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<void> {
// 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<string> {
context.log('info', ' Generating WebAssembly module')

Expand Down Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions packages/plugin-wasm/src/var-names.ts
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 12 additions & 1 deletion tests/integration/impl-var-access/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down

0 comments on commit 9c2f7d1

Please sign in to comment.