Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions crates/mq-wasm/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -716,6 +746,23 @@ pub async fn run(code: &str, content: &str, options: JsValue) -> Result<String,
})
}

/// Lists the VM bytecode `code` compiles to, without running it.
#[wasm_bindgen(js_name=dumpBytecode, skip_typescript)]
pub async fn dump_bytecode(code: &str) -> Result<JsValue, JsValue> {
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<String, JsValue> {
let token_arena = mq_lang::Shared::new(mq_lang::SharedCell::new(mq_lang::Arena::new(1024)));
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions packages/mq-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ Format mq code.

Returns: `Promise<string>` - 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<BytecodeDump>` - Phases, chunks, instructions and constants

#### `diagnostics(code)`

Get diagnostics for mq code.
Expand Down
11 changes: 11 additions & 0 deletions packages/mq-web/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import init, {
Options,
Diagnostic,
BytecodeDump,
DefinedValue,
InlayHint,
HoverResult,
Expand All @@ -11,6 +12,7 @@ import init, {
interface WasmModule {
run: (code: string, content: string, options: Options) => Promise<string>;
toAst: (code: string) => Promise<string>;
dumpBytecode: (code: string) => Promise<BytecodeDump>;
format: (code: string) => Promise<string>;
diagnostics: (
code: string,
Expand Down Expand Up @@ -50,6 +52,7 @@ async function initWasm(): Promise<WasmModule> {
wasmModule = {
run: wasmImport.run,
toAst: wasmImport.toAst,
dumpBytecode: wasmImport.dumpBytecode,
format: wasmImport.format,
diagnostics: wasmImport.diagnostics,
inlayHints: wasmImport.inlayHints,
Expand Down Expand Up @@ -95,6 +98,14 @@ export async function toAst(code: string): Promise<string> {
return await wasm.toAst(code);
}

/**
* List the VM bytecode that mq code compiles to, without running it.
*/
export async function dumpBytecode(code: string): Promise<BytecodeDump> {
const wasm = await initWasm();
return await wasm.dumpBytecode(code);
}

/**
* Format mq code
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/mq-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions packages/mq-web/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
run,
format,
toAst,
dumpBytecode,
toHtml,
htmlToMarkdown,
diagnostics,
Expand Down Expand Up @@ -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");
Expand Down
Loading