Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update build and plugin packages to support JS code generation #487

Merged
merged 28 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
50a45d6
docs: correct typo in sir example README
chrispcampbell May 5, 2024
6c5ae81
fix: remove plugin-wasm from hello-world example
chrispcampbell May 5, 2024
86f6d07
fix: add genFormat property to UserConfig and replace pre/postGenerat…
chrispcampbell May 5, 2024
a3d4a47
fix: update templates to allow for either JS or C code generation
chrispcampbell May 5, 2024
7767d57
fix: update create package to support creating JS-only project
chrispcampbell May 8, 2024
100a288
fix: update plugin-wasm to fail if it is included when genFormat is n…
chrispcampbell May 8, 2024
78923a9
fix: update integration tests to use postGenerateCode
chrispcampbell May 8, 2024
57365d3
fix: sketch out changes to plugin-worker to support importing JS model
chrispcampbell May 8, 2024
9619226
fix: checkpoint work on plugin-worker
chrispcampbell May 8, 2024
b297d8c
Merge branch 'main' into chris/479-build-js
chrispcampbell May 11, 2024
f3f7b79
Merge branch 'main' into chris/479-build-js
chrispcampbell May 23, 2024
af3c84a
fix: update comments in templates
chrispcampbell May 23, 2024
2d11591
fix: restore deprecation warning for --genc
chrispcampbell May 23, 2024
fa70608
fix: rename variable for clarity
chrispcampbell May 23, 2024
da51d46
fix: update preprocessModel call to work with recent changes
chrispcampbell May 23, 2024
d34f38f
fix: correct call to postGenerateCode
chrispcampbell May 23, 2024
97ec08d
docs: update API docs for build package
chrispcampbell May 23, 2024
0ef16fd
test: update JS-level integration tests to test both JS and Wasm cases
chrispcampbell May 23, 2024
7dd3851
fix: update build package to copy `generated-model.js` to prepDir
chrispcampbell May 23, 2024
1b181e2
fix: port fixes from issue 461 (preserve output var names from spec) …
chrispcampbell May 23, 2024
6942dda
fix: remove now unused file in plugin-worker
chrispcampbell May 23, 2024
e016cbb
fix: use commas instead of brackets in subscript lists in outputVarId…
chrispcampbell May 23, 2024
ac42535
fix: update plugin-{check,config,wasm} to use comma as separator inst…
chrispcampbell May 23, 2024
998cbb4
test: update impl-var-access test to include a 2D subscripted output …
chrispcampbell May 23, 2024
d67c1fb
test: change ext-control-params test to read FINAL TIME from csv file
chrispcampbell May 23, 2024
2985bef
Revert "test: change ext-control-params test to read FINAL TIME from …
chrispcampbell May 23, 2024
67de020
fix: allow FINAL TIME to be defined as a non-constant
chrispcampbell May 23, 2024
bd65ea4
fix: remove initialTime and finalTime properties from JsModelFunction…
chrispcampbell May 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/hello-world/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
6 changes: 1 addition & 5 deletions examples/hello-world/sde.config.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/sir/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
27 changes: 24 additions & 3 deletions examples/template-default/sde.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],

Expand All @@ -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')]
}),
Expand Down
28 changes: 25 additions & 3 deletions examples/template-minimal/sde.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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
Expand Down
22 changes: 12 additions & 10 deletions packages/build/docs/interfaces/Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -100,42 +100,44 @@ 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

`Promise`<`void`\>

___

### 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).

___

Expand Down
10 changes: 10 additions & 0 deletions packages/build/docs/interfaces/ResolvedConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
11 changes: 11 additions & 0 deletions packages/build/docs/interfaces/UserConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)[]
Expand Down
7 changes: 7 additions & 0 deletions packages/build/src/_shared/resolved-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 46 additions & 26 deletions packages/build/src/build/impl/gen-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`)
Expand Down Expand Up @@ -165,36 +178,43 @@ 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<void> {
log('verbose', ' Generating C code')
async function generateCode(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise<void> {
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'
// The default error message from `spawnChild` is not very informative, so the
// 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)
}
15 changes: 15 additions & 0 deletions packages/build/src/config/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,28 @@ 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,
prepDir,
modelFiles,
modelInputPaths,
watchPaths,
genFormat,
sdeDir,
sdeCmdPath
}
Expand Down
8 changes: 8 additions & 0 deletions packages/build/src/config/user-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading