diff --git a/crates/mq-wasm/src/script.rs b/crates/mq-wasm/src/script.rs index b91e8532c..01b9d87bd 100644 --- a/crates/mq-wasm/src/script.rs +++ b/crates/mq-wasm/src/script.rs @@ -35,6 +35,36 @@ export interface HoverResult { content: string; } +export interface BytecodeLocation { + line: number; + column: number; +} + +export interface BytecodeInstruction { + pc: number; + opcode: string; + operands?: string; + location: BytecodeLocation | null; +} + +export interface BytecodeChunk { + index: number; + local_count: number; + locals: string[]; + upvalues: string[]; + instructions: BytecodeInstruction[]; + constants: string[]; +} + +export interface BytecodePhase { + name: string; + chunks: BytecodeChunk[]; +} + +export interface BytecodeDump { + phases: BytecodePhase[]; +} + export interface Options { isUpdate: boolean, inputFormat: 'markdown' | 'text' | 'mdx' | 'html' | 'null' | 'raw' | null, @@ -716,6 +746,23 @@ pub async fn run(code: &str, content: &str, options: JsValue) -> Result Result { + let resolver = WasmModuleResolver::new(); + resolver.initialize().await; + resolver.preload_modules(code).await; + + let mut engine = mq_lang::Engine::new(resolver); + engine.load_builtin_module(); + + let compiled = engine.compile(code).map_err(|e| JsValue::from_str(&format!("{}", e)))?; + let dump = engine + .dump_bytecode(&compiled) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + serde_wasm_bindgen::to_value(&dump).map_err(|e| JsValue::from_str(&format!("Failed to serialize bytecode: {}", e))) +} + #[wasm_bindgen(js_name=toAst)] pub async fn to_ast(code: &str) -> Result { let token_arena = mq_lang::Shared::new(mq_lang::SharedCell::new(mq_lang::Arena::new(1024))); @@ -1321,6 +1368,18 @@ mod tests { } #[allow(unused)] + #[wasm_bindgen_test] + async fn test_dump_bytecode_lists_phases() { + let dump = dump_bytecode("1 + 2").await.unwrap(); + let phases = js_sys::Reflect::get(&dump, &JsValue::from_str("phases")).unwrap(); + assert_eq!(js_sys::Array::from(&phases).length(), 1); + } + + #[wasm_bindgen_test] + async fn test_dump_bytecode_rejects_syntax_error() { + assert!(dump_bytecode("1 +").await.is_err()); + } + #[wasm_bindgen_test] async fn test_script_run_simple() { let result = run( diff --git a/packages/mq-web/README.md b/packages/mq-web/README.md index 447137740..4d5ad842e 100644 --- a/packages/mq-web/README.md +++ b/packages/mq-web/README.md @@ -70,6 +70,14 @@ Format mq code. Returns: `Promise` - The formatted code +#### `dumpBytecode(code)` + +List the VM bytecode that mq code compiles to, without running it. + +- `code`: string - The mq code to compile + +Returns: `Promise` - Phases, chunks, instructions and constants + #### `diagnostics(code)` Get diagnostics for mq code. diff --git a/packages/mq-web/src/core.ts b/packages/mq-web/src/core.ts index 0b85e85a6..73b8fbd0f 100644 --- a/packages/mq-web/src/core.ts +++ b/packages/mq-web/src/core.ts @@ -1,6 +1,7 @@ import init, { Options, Diagnostic, + BytecodeDump, DefinedValue, InlayHint, HoverResult, @@ -11,6 +12,7 @@ import init, { interface WasmModule { run: (code: string, content: string, options: Options) => Promise; toAst: (code: string) => Promise; + dumpBytecode: (code: string) => Promise; format: (code: string) => Promise; diagnostics: ( code: string, @@ -50,6 +52,7 @@ async function initWasm(): Promise { wasmModule = { run: wasmImport.run, toAst: wasmImport.toAst, + dumpBytecode: wasmImport.dumpBytecode, format: wasmImport.format, diagnostics: wasmImport.diagnostics, inlayHints: wasmImport.inlayHints, @@ -95,6 +98,14 @@ export async function toAst(code: string): Promise { return await wasm.toAst(code); } +/** + * List the VM bytecode that mq code compiles to, without running it. + */ +export async function dumpBytecode(code: string): Promise { + const wasm = await initWasm(); + return await wasm.dumpBytecode(code); +} + /** * Format mq code */ diff --git a/packages/mq-web/src/index.ts b/packages/mq-web/src/index.ts index 1babe5df5..841e806b4 100644 --- a/packages/mq-web/src/index.ts +++ b/packages/mq-web/src/index.ts @@ -12,10 +12,15 @@ */ // Re-export everything from core and types -export { run, format, diagnostics, inlayHints, definedValues, hover, toAst, toHtml, htmlToMarkdown, clearHttpCache, clearAllHttpCache } from "./core.js"; +export { run, format, diagnostics, inlayHints, definedValues, hover, toAst, dumpBytecode, toHtml, htmlToMarkdown, clearHttpCache, clearAllHttpCache } from "./core.js"; export type { Options, + BytecodeDump, + BytecodePhase, + BytecodeChunk, + BytecodeInstruction, + BytecodeLocation, Diagnostic, InlayHint, HoverResult, diff --git a/packages/mq-web/test/index.test.ts b/packages/mq-web/test/index.test.ts index 409d044df..1bf06a22b 100644 --- a/packages/mq-web/test/index.test.ts +++ b/packages/mq-web/test/index.test.ts @@ -3,6 +3,7 @@ import { run, format, toAst, + dumpBytecode, toHtml, htmlToMarkdown, diagnostics, @@ -62,6 +63,22 @@ describe("toAst", () => { }); }); +describe("dumpBytecode", () => { + it("lists the bytecode of a query without running it", async () => { + const dump = await dumpBytecode("def f(x): x + 1; | f(1)"); + expect(dump.phases[0].name).toBe("main"); + const opcodes = dump.phases[0].chunks.flatMap((chunk) => + chunk.instructions.map((instruction) => instruction.opcode), + ); + expect(opcodes).toContain("Return"); + expect(opcodes).not.toContain("StmtBoundary"); + }); + + it("rejects a syntax error", async () => { + await expect(dumpBytecode("1 +")).rejects.toBeDefined(); + }); +}); + describe("toHtml", () => { it("converts heading to HTML", async () => { const result = await toHtml("# Hello\n\nWorld");