diff --git a/.github/workflows/wasm-test.yaml b/.github/workflows/wasm-test.yaml index 93b2f12..baf81ed 100644 --- a/.github/workflows/wasm-test.yaml +++ b/.github/workflows/wasm-test.yaml @@ -32,6 +32,16 @@ jobs: uses: actions/setup-node@v3 with: node-version: 20.x + - name: Install rust nightly toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.79 + override: true + components: clippy, rustfmt + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.22 - name: Install dependencies run: npm install @@ -39,6 +49,15 @@ jobs: - name: Build run: npm run build + - name: Test + run: npm run test + + - name: Rust example e2e tests + run: cd examples/rust && cargo test -r + + - name: Go example e2e tests + run: cd examples/go && go mod tidy && go run main.go + - name: Publish Dry Run if: "startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-')" # Since this command will not exit with non-zero code when file missing, diff --git a/wasm/examples/browser/kcl.wasm b/wasm/examples/browser/kcl.wasm old mode 100644 new mode 100755 index 77e7cae..402baca Binary files a/wasm/examples/browser/kcl.wasm and b/wasm/examples/browser/kcl.wasm differ diff --git a/wasm/examples/browser/src/main.ts b/wasm/examples/browser/src/main.ts index 5eb162c..7a2c776 100644 --- a/wasm/examples/browser/src/main.ts +++ b/wasm/examples/browser/src/main.ts @@ -1,7 +1,8 @@ -import { load, invokeKCLRun } from "@kcl-lang/wasm-lib"; +import { load, invokeKCLRun, invokeKCLFmt } from "@kcl-lang/wasm-lib"; + +const inst = await load(); async function main() { - const inst = await load(); const result = invokeKCLRun(inst, { filename: "test.k", source: ` @@ -11,6 +12,14 @@ schema Person: p = Person {name = "Alice"}`, }); console.log(result); + const fmtResult = invokeKCLFmt(inst, { + source: ` +schema Person: + name: str + +p = Person {name = "Alice"}`, + }); + console.log(fmtResult); } main(); diff --git a/wasm/examples/browser/tsconfig.json b/wasm/examples/browser/tsconfig.json index 9c7495e..aa0421f 100644 --- a/wasm/examples/browser/tsconfig.json +++ b/wasm/examples/browser/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "target": "es2022", - "module": "commonjs", - "moduleResolution": "node", + "module": "es2022", "lib": ["es2022", "dom"], + "moduleResolution": "node", "sourceMap": true, "outDir": "./dist", "rootDir": "./src", diff --git a/wasm/examples/go/main.go b/wasm/examples/go/main.go index f1f5645..c18ef09 100644 --- a/wasm/examples/go/main.go +++ b/wasm/examples/go/main.go @@ -19,4 +19,11 @@ func main() { panic(err) } fmt.Println(result) + result, err = m.Fmt(&module.FmtOptions{ + Source: "a = 1", + }) + if err != nil { + panic(err) + } + fmt.Println(result) } diff --git a/wasm/examples/go/pkg/module/module.go b/wasm/examples/go/pkg/module/module.go index e69648b..c676d81 100644 --- a/wasm/examples/go/pkg/module/module.go +++ b/wasm/examples/go/pkg/module/module.go @@ -14,6 +14,10 @@ type RunOptions struct { Source string } +type FmtOptions struct { + Source string +} + type KCLModule struct { Instance *wasmtime.Instance Store *wasmtime.Store @@ -21,6 +25,7 @@ type KCLModule struct { KclMalloc *wasmtime.Func KclFree *wasmtime.Func KclRun *wasmtime.Func + KclFmt *wasmtime.Func } func New(path string) (*KCLModule, error) { @@ -55,6 +60,7 @@ func New(path string) (*KCLModule, error) { malloc := instance.GetFunc(store, "kcl_malloc") free := instance.GetFunc(store, "kcl_free") run := instance.GetFunc(store, "kcl_run") + fmt := instance.GetFunc(store, "kcl_fmt") return &KCLModule{ Instance: instance, Store: store, @@ -62,6 +68,7 @@ func New(path string) (*KCLModule, error) { KclMalloc: malloc, KclFree: free, KclRun: run, + KclFmt: fmt, }, nil } @@ -106,3 +113,34 @@ func (m *KCLModule) Run(opts *RunOptions) (string, error) { return result, nil } + +func (m *KCLModule) Fmt(opts *FmtOptions) (string, error) { + sourcePtr, sourceLen, err := copyStringToWasmMemory(m.Store, m.KclMalloc, m.Memory, opts.Source) + if err != nil { + return "", err + } + defer func() { + err := freeMemory(m.Store, m.KclFree, sourcePtr, sourceLen) + if err != nil { + fmt.Println("Failed to free source memory:", err) + } + }() + + resultPtr, err := m.KclFmt.Call(m.Store, sourcePtr) + if err != nil { + return "", err + } + + result, _, err := copyCStrFromWasmMemory(m.Store, m.Memory, resultPtr.(int32)) + if err != nil { + return "", err + } + defer func() { + err := freeMemory(m.Store, m.KclFree, resultPtr.(int32), int32(len(result))) + if err != nil { + fmt.Println("Failed to free result memory:", err) + } + }() + + return result, nil +} diff --git a/wasm/examples/node/src/main.ts b/wasm/examples/node/src/main.ts index 5eb162c..88503eb 100644 --- a/wasm/examples/node/src/main.ts +++ b/wasm/examples/node/src/main.ts @@ -1,6 +1,6 @@ -import { load, invokeKCLRun } from "@kcl-lang/wasm-lib"; +import { load, invokeKCLRun, invokeKCLFmt } from "@kcl-lang/wasm-lib"; -async function main() { +async function run() { const inst = await load(); const result = invokeKCLRun(inst, { filename: "test.k", @@ -13,4 +13,17 @@ p = Person {name = "Alice"}`, console.log(result); } -main(); +async function fmt() { + const inst = await load(); + const result = invokeKCLFmt(inst, { + source: ` +schema Person: + name: str + +p = Person {name = "Alice"}`, + }); + console.log(result); +} + +run(); +fmt(); diff --git a/wasm/examples/rust/src/lib.rs b/wasm/examples/rust/src/lib.rs index 0358c1d..f4a9436 100644 --- a/wasm/examples/rust/src/lib.rs +++ b/wasm/examples/rust/src/lib.rs @@ -1,6 +1,8 @@ -use std::path::Path; +#[cfg(test)] +mod tests; use anyhow::Result; +use std::path::Path; use wasmtime::*; use wasmtime_wasi::{ preview1::{self, WasiP1Ctx}, @@ -19,11 +21,17 @@ impl State { } } +#[derive(Debug)] pub struct RunOptions { pub filename: String, pub source: String, } +#[derive(Debug)] +pub struct FmtOptions { + pub source: String, +} + pub struct KCLModule { pub instance: Instance, store: Store, @@ -31,6 +39,8 @@ pub struct KCLModule { malloc: TypedFunc, free: TypedFunc<(i32, i32), ()>, run: TypedFunc<(i32, i32), i32>, + fmt: TypedFunc, + runtime_err: TypedFunc<(i32, i32), i32>, } impl KCLModule { @@ -54,6 +64,8 @@ impl KCLModule { let malloc = instance.get_typed_func::(&mut store, "kcl_malloc")?; let free = instance.get_typed_func::<(i32, i32), ()>(&mut store, "kcl_free")?; let run = instance.get_typed_func::<(i32, i32), i32>(&mut store, "kcl_run")?; + let fmt = instance.get_typed_func::(&mut store, "kcl_fmt")?; + let runtime_err = instance.get_typed_func::<(i32, i32), i32>(&mut store, "kcl_runtime_err")?; Ok(KCLModule { instance, store, @@ -61,6 +73,8 @@ impl KCLModule { malloc, free, run, + fmt, + runtime_err, }) } @@ -70,10 +84,39 @@ impl KCLModule { copy_string_to_wasm_memory(&mut self.store, &self.malloc, self.memory, &opts.filename)?; let (source_ptr, source_len) = copy_string_to_wasm_memory(&mut self.store, &self.malloc, self.memory, &opts.source)?; - let result_ptr = self.run.call(&mut self.store, (filename_ptr, source_ptr))?; + let runtime_err_len = 1024; + let (runtime_err_ptr, _) = malloc_bytes_from_wasm_memory(&mut self.store, &self.malloc, runtime_err_len)?; + let result_str = match self.run.call(&mut self.store, (filename_ptr, source_ptr)) { + Ok(result_ptr) => { + let (result_str, result_len) = + copy_cstr_from_wasm_memory(&mut self.store, self.memory, result_ptr as usize)?; + free_memory(&mut self.store, &self.free, result_ptr, result_len)?; + result_str + }, + Err(err) => { + self.runtime_err.call(&mut self.store, (runtime_err_ptr, runtime_err_len))?; + let (runtime_err_str, runtime_err_len) = + copy_cstr_from_wasm_memory(&mut self.store, self.memory, runtime_err_ptr as usize)?; + free_memory(&mut self.store, &self.free, runtime_err_ptr, runtime_err_len)?; + if runtime_err_str.is_empty() { + return Err(err) + } else { + runtime_err_str + } + }, + }; + free_memory(&mut self.store, &self.free, filename_ptr, filename_len)?; + free_memory(&mut self.store, &self.free, source_ptr, source_len)?; + Ok(result_str) + } + + /// Run with the wasm module and options. + pub fn fmt(&mut self, opts: &FmtOptions) -> Result { + let (source_ptr, source_len) = + copy_string_to_wasm_memory(&mut self.store, &self.malloc, self.memory, &opts.source)?; + let result_ptr = self.fmt.call(&mut self.store, source_ptr)?; let (result_str, result_len) = copy_cstr_from_wasm_memory(&mut self.store, self.memory, result_ptr as usize)?; - free_memory(&mut self.store, &self.free, filename_ptr, filename_len)?; free_memory(&mut self.store, &self.free, source_ptr, source_len)?; free_memory(&mut self.store, &self.free, result_ptr, result_len)?; @@ -98,6 +141,15 @@ fn copy_string_to_wasm_memory( Ok((ptr, length as usize)) } +fn malloc_bytes_from_wasm_memory( + store: &mut Store, + malloc: &TypedFunc, + length: i32, +) -> Result<(i32, usize)> { + let ptr = malloc.call(&mut *store, length)?; + Ok((ptr, length as usize)) +} + fn copy_cstr_from_wasm_memory( store: &mut Store, memory: Memory, diff --git a/wasm/examples/rust/src/tests.rs b/wasm/examples/rust/src/tests.rs new file mode 100644 index 0000000..54ceae4 --- /dev/null +++ b/wasm/examples/rust/src/tests.rs @@ -0,0 +1,68 @@ +use crate::{FmtOptions, KCLModule, RunOptions}; +use anyhow::Result; + +const WASM_PATH: &str = "../../kcl.wasm"; +const BENCH_COUNT: usize = 20; + +#[test] +fn test_run() -> Result<()> { + let opts = RunOptions { + filename: "test.k".to_string(), + source: "a = 1".to_string(), + }; + let mut module = KCLModule::from_path(WASM_PATH)?; + for _ in 0..BENCH_COUNT { + let result = module.run(&opts)?; + println!("{}", result); + } + Ok(()) +} + +#[test] +fn test_run_parse_error() -> Result<()> { + let opts = RunOptions { + filename: "test.k".to_string(), + source: "a = ".to_string(), + }; + let mut module = KCLModule::from_path(WASM_PATH)?; + let result = module.run(&opts)?; + println!("{}", result); + Ok(()) +} + +#[test] +fn test_run_type_error() -> Result<()> { + let opts = RunOptions { + filename: "test.k".to_string(), + source: "a: str = 1".to_string(), + }; + let mut module = KCLModule::from_path(WASM_PATH)?; + let result = module.run(&opts)?; + println!("{}", result); + Ok(()) +} + +#[test] +fn test_run_runtime_error() -> Result<()> { + let opts = RunOptions { + filename: "test.k".to_string(), + source: "a = [][0]".to_string(), + }; + let mut module = KCLModule::from_path(WASM_PATH)?; + let result = module.run(&opts)?; + println!("{}", result); + Ok(()) +} + +#[test] +fn test_fmt() -> Result<()> { + let opts = FmtOptions { + source: "a = 1".to_string(), + }; + let mut module = KCLModule::from_path(WASM_PATH)?; + for _ in 0..BENCH_COUNT { + let result = module.fmt(&opts)?; + println!("{}", result); + } + Ok(()) +} diff --git a/wasm/jest.config.ts b/wasm/jest.config.ts new file mode 100644 index 0000000..b6ae57b --- /dev/null +++ b/wasm/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; + +const config: Config = { + verbose: true, + transform: { "^.+\\.ts?$": "ts-jest" }, + testEnvironment: "node", + testRegex: "/tests/.*\\.(test|spec)?\\.(ts|tsx)$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], +}; + +export default config; diff --git a/wasm/kcl.wasm b/wasm/kcl.wasm old mode 100644 new mode 100755 index 8e713d4..402baca Binary files a/wasm/kcl.wasm and b/wasm/kcl.wasm differ diff --git a/wasm/package.json b/wasm/package.json index df9b0b9..81387d5 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@kcl-lang/wasm-lib", - "version": "0.10.0-rc.1", + "version": "0.10.0-rc.2", "description": "KCL WASM module", "files": [ "kcl.wasm", @@ -14,7 +14,8 @@ }, "scripts": { "build": "tsc --build", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "jest" }, "license": "Apache-2.0", "dependencies": { @@ -22,8 +23,12 @@ "prettier": "^2.8.4" }, "devDependencies": { + "@types/jest": "^29.5.12", "@types/node": "^20.11.0", + "jest": "^29.7.0", "prettier": "^2.8.4", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "typescript": "^5.3.3" }, "bundleDependencies": [ diff --git a/wasm/src/index.ts b/wasm/src/index.ts index d6e95a7..53a1538 100644 --- a/wasm/src/index.ts +++ b/wasm/src/index.ts @@ -1,6 +1,7 @@ import { init, WASI, MemFS } from "@wasmer/wasi"; const RUN_FUNCTION_NAME = "kcl_run"; const FMT_FUNCTION_NAME = "kcl_fmt"; +const RUNTIME_ERR_FUNCTION_NAME = "kcl_runtime_err"; export interface KCLWasmLoadOptions { /** @@ -68,14 +69,8 @@ export interface FmtOptions { export async function load(opts?: KCLWasmLoadOptions) { await init(); const options = opts ?? {}; - // preopen everything - let preopens: Record = { - "/": "/", - }; - const w = new WASI({ env: options.env ?? {}, - preopens: preopens, fs: options.fs, }); @@ -130,15 +125,27 @@ export function invokeKCLRun( instance, opts.source ); - const resultPtr = exports[RUN_FUNCTION_NAME](filenamePtr, sourcePtr); - const [resultStr, resultPtrLength] = copyCStrFromWasmMemory( - instance, - resultPtr - ); - exports.kcl_free(filenamePtr, filenamePtrLength); - exports.kcl_free(sourcePtr, sourcePtrLength); - exports.kcl_free(resultPtr, resultPtrLength); - return resultStr; + let result; + try { + const resultPtr = exports[RUN_FUNCTION_NAME](filenamePtr, sourcePtr); + const [resultStr, resultPtrLength] = copyCStrFromWasmMemory( + instance, + resultPtr + ); + exports.kcl_free(resultPtr, resultPtrLength); + result = resultStr; + } catch (error) { + let runtimeErrPtrLength = 1024; + let runtimeErrPtr = exports.kcl_malloc(runtimeErrPtrLength); + exports[RUNTIME_ERR_FUNCTION_NAME](runtimeErrPtr, runtimeErrPtrLength); + const [runtimeErrStr, _] = copyCStrFromWasmMemory(instance, runtimeErrPtr); + exports.kcl_free(runtimeErrPtr, runtimeErrPtrLength); + result = "ERROR:" + runtimeErrStr; + } finally { + exports.kcl_free(filenamePtr, filenamePtrLength); + exports.kcl_free(sourcePtr, sourcePtrLength); + } + return result; } /** diff --git a/wasm/tests/fmt.test.ts b/wasm/tests/fmt.test.ts new file mode 100644 index 0000000..acc1807 --- /dev/null +++ b/wasm/tests/fmt.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "@jest/globals"; +import { load, invokeKCLFmt } from "../src/"; + +test("fmt", async () => { + const inst = await load(); + const result = invokeKCLFmt(inst, { + source: ` +schema Person: + name: str + +p = Person {name = "Alice"}`, + }); + expect(result).toBe(`schema Person: + name: str + +p = Person {name = "Alice"} +`); +}); diff --git a/wasm/tests/run.test.ts b/wasm/tests/run.test.ts new file mode 100644 index 0000000..fdf2958 --- /dev/null +++ b/wasm/tests/run.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "@jest/globals"; +import { load, invokeKCLRun } from "../src"; + +test("run", async () => { + const inst = await load(); + const result = invokeKCLRun(inst, { + filename: "test.k", + source: ` +schema Person: + name: str + +p = Person {name = "Alice"}`, + }); + expect(result).toBe("p:\n name: Alice"); +}); diff --git a/wasm/tests/run_error.test.ts b/wasm/tests/run_error.test.ts new file mode 100644 index 0000000..4cec226 --- /dev/null +++ b/wasm/tests/run_error.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "@jest/globals"; +import { load, invokeKCLRun } from "../src"; + +test("run parse error test", async () => { + const inst = await load(); + const result = invokeKCLRun(inst, { + filename: "test.k", + source: ` +a = +`, + }); + expect(result).toBe(`ERROR:error[E1001]: InvalidSyntax +---> File test.k:2:4: expected one of ["identifier", "literal", "(", "[", "{"] got newline + + +error[E1001]: InvalidSyntax +---> File test.k:2:4: expected one of ["identifier", "literal", "(", "[", "{"] got newline + +`); +}); + +test("run type error test", async () => { + const inst = await load(); + const result = invokeKCLRun(inst, { + filename: "test.k", + source: ` +a: str = 1 +`, + }); + expect(result).toBe(`ERROR: +error[E2G22]: TypeError +---> File test.k:2:1: expected str, got int(1) + +`); +}); + +test("run runtime error test", async () => { + const inst = await load(); + const result = invokeKCLRun(inst, { + filename: "test.k", + source: ` +a = [][0] +`, + }); + expect(result).toBe("ERROR:list index out of range: 0"); +});