WASM Compilation

Status: NOT IMPLEMENTED

This document describes a planned LSP WASM build that does not exist. The LSP server has not been compiled to WASM.

The Playground currently uses a separate runtime WASM module at website/playground-wasm/ with run_ori() and format_ori() exports. That module provides direct evaluation and formatting via JavaScript calls — it is not an LSP server and does not implement the LSP protocol.

Compiling the LSP server to WebAssembly for browser-based Playground integration.

Build Process

Prerequisites

# Install wasm-pack
cargo install wasm-pack

# Add WASM target
rustup target add wasm32-unknown-unknown

Build Command

# From tools/ori-lsp/
wasm-pack build --target web --out-dir ../../playground/wasm-lsp/pkg

Output:

playground/wasm-lsp/pkg/
├── ori_lsp.js           # JS glue code
├── ori_lsp.d.ts         # TypeScript types
├── ori_lsp_bg.wasm      # WASM binary
└── package.json

JavaScript API

WASM Exports

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct WasmLanguageServer {
    inner: OriLanguageServer,
}

#[wasm_bindgen]
impl WasmLanguageServer {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self {
            inner: OriLanguageServer::new_wasm(),
        }
    }

    /// Handle an incoming JSON-RPC message from the client
    #[wasm_bindgen]
    pub fn handle_message(&mut self, message: &str) -> Option<String> {
        let request: Message = serde_json::from_str(message).ok()?;
        let response = self.inner.handle(request);
        response.map(|r| serde_json::to_string(&r).unwrap())
    }

    /// Get pending notifications (diagnostics, etc.)
    #[wasm_bindgen]
    pub fn get_notifications(&mut self) -> Vec<JsValue> {
        self.inner
            .drain_notifications()
            .into_iter()
            .map(|n| JsValue::from_str(&serde_json::to_string(&n).unwrap()))
            .collect()
    }

    /// Open a document
    #[wasm_bindgen]
    pub fn open_document(&mut self, uri: &str, text: &str) {
        self.inner.open_document(uri, text);
    }

    /// Update document content
    #[wasm_bindgen]
    pub fn update_document(&mut self, uri: &str, text: &str) {
        self.inner.update_document(uri, text);
    }

    /// Close a document
    #[wasm_bindgen]
    pub fn close_document(&mut self, uri: &str) {
        self.inner.close_document(uri);
    }

    /// Format a document (returns formatted text)
    #[wasm_bindgen]
    pub fn format(&self, uri: &str) -> Option<String> {
        self.inner.format(uri)
    }

    /// Get hover info at position
    #[wasm_bindgen]
    pub fn hover(&self, uri: &str, line: u32, character: u32) -> Option<String> {
        let result = self.inner.hover(uri, line, character)?;
        Some(serde_json::to_string(&result).unwrap())
    }

    /// Get diagnostics for a document
    #[wasm_bindgen]
    pub fn get_diagnostics(&self, uri: &str) -> String {
        let diagnostics = self.inner.get_diagnostics(uri);
        serde_json::to_string(&diagnostics).unwrap()
    }
}

TypeScript Usage

import init, { WasmLanguageServer } from './pkg/ori_lsp.js';

async function createServer(): Promise<WasmLanguageServer> {
    await init();
    return new WasmLanguageServer();
}

// Usage
const server = await createServer();

// Open document
server.open_document('file:///main.ori', 'let x = 42');

// Get diagnostics
const diagnosticsJson = server.get_diagnostics('file:///main.ori');
const diagnostics = JSON.parse(diagnosticsJson);

// Format
const formatted = server.format('file:///main.ori');

// Hover
const hoverJson = server.hover('file:///main.ori', 0, 4);
if (hoverJson) {
    const hover = JSON.parse(hoverJson);
    console.log(hover.contents);
}

// Update on edit
server.update_document('file:///main.ori', 'let x: int = 42');

Monaco Integration

Language Client

Create a lightweight LSP-like client for Monaco:

import * as monaco from 'monaco-editor';
import { WasmLanguageServer } from './pkg/ori_lsp.js';

class OriLanguageClient {
    private server: WasmLanguageServer;
    private diagnosticsCallback: (uri: string, diagnostics: any[]) => void;

    constructor(server: WasmLanguageServer) {
        this.server = server;
    }

    onDiagnostics(callback: (uri: string, diagnostics: any[]) => void) {
        this.diagnosticsCallback = callback;
    }

    openDocument(model: monaco.editor.ITextModel) {
        const uri = model.uri.toString();
        this.server.open_document(uri, model.getValue());
        this.updateDiagnostics(uri);
    }

    updateDocument(model: monaco.editor.ITextModel) {
        const uri = model.uri.toString();
        this.server.update_document(uri, model.getValue());
        this.updateDiagnostics(uri);
    }

    closeDocument(model: monaco.editor.ITextModel) {
        const uri = model.uri.toString();
        this.server.close_document(uri);
    }

    format(model: monaco.editor.ITextModel): monaco.editor.ISingleEditOperation[] | null {
        const uri = model.uri.toString();
        const formatted = this.server.format(uri);
        if (!formatted) return null;

        return [{
            range: model.getFullModelRange(),
            text: formatted,
        }];
    }

    hover(model: monaco.editor.ITextModel, position: monaco.Position): monaco.languages.Hover | null {
        const uri = model.uri.toString();
        const hoverJson = this.server.hover(uri, position.lineNumber - 1, position.column - 1);
        if (!hoverJson) return null;

        const hover = JSON.parse(hoverJson);
        return {
            contents: [{ value: hover.contents.value }],
        };
    }

    private updateDiagnostics(uri: string) {
        const diagnosticsJson = this.server.get_diagnostics(uri);
        const diagnostics = JSON.parse(diagnosticsJson);
        if (this.diagnosticsCallback) {
            this.diagnosticsCallback(uri, diagnostics);
        }
    }
}

Monaco Provider Registration

// Register formatting provider
monaco.languages.registerDocumentFormattingEditProvider('ori', {
    provideDocumentFormattingEdits(model) {
        return client.format(model);
    }
});

// Register hover provider
monaco.languages.registerHoverProvider('ori', {
    provideHover(model, position) {
        return client.hover(model, position);
    }
});

// Wire up diagnostics to Monaco markers
client.onDiagnostics((uri, diagnostics) => {
    const model = monaco.editor.getModel(monaco.Uri.parse(uri));
    if (!model) return;

    const markers = diagnostics.map(d => ({
        severity: d.severity === 1 ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning,
        startLineNumber: d.range.start.line + 1,
        startColumn: d.range.start.character + 1,
        endLineNumber: d.range.end.line + 1,
        endColumn: d.range.end.character + 1,
        message: d.message,
        source: 'ori',
    }));

    monaco.editor.setModelMarkers(model, 'ori', markers);
});

// Wire up model events
editor.onDidChangeModelContent(() => {
    client.updateDocument(editor.getModel());
});

Size Optimization

Release Build

# Cargo.toml
[profile.release]
lto = true
opt-level = 's'  # Optimize for size
codegen-units = 1

wasm-opt

Post-process with wasm-opt for further reduction:

wasm-opt -Os -o ori_lsp_opt.wasm ori_lsp_bg.wasm

Code Splitting

If the full LSP is too large, consider splitting:

ori_lsp_core.wasm     # Diagnostics only (smallest)
ori_lsp_format.wasm   # + Formatting
ori_lsp_full.wasm     # + Hover, completion, etc.

Async Considerations

WASM is single-threaded. Avoid blocking operations:

// BAD: Blocks the main thread
pub fn compute_expensive(&self) -> String {
    // Long computation...
}

// GOOD: Return immediately, poll for results
#[wasm_bindgen]
pub fn start_computation(&mut self, id: u32) {
    self.pending.insert(id, Computation::new());
}

#[wasm_bindgen]
pub fn poll_computation(&mut self, id: u32) -> Option<String> {
    self.pending.get_mut(&id)?.poll()
}

For diagnostic debouncing, use JavaScript timers:

let debounceTimer: number | null = null;

function onDocumentChange() {
    if (debounceTimer) {
        clearTimeout(debounceTimer);
    }
    debounceTimer = setTimeout(() => {
        client.updateDocument(editor.getModel());
    }, 100);
}

Testing

WASM-specific tests

#[cfg(target_arch = "wasm32")]
mod wasm_tests {
    use super::*;
    use wasm_bindgen_test::*;

    wasm_bindgen_test_configure!(run_in_browser);

    #[wasm_bindgen_test]
    fn test_format() {
        let server = WasmLanguageServer::new();
        server.open_document("file:///test.ori", "let x=1");

        let formatted = server.format("file:///test.ori").unwrap();
        assert_eq!(formatted, "let x = 1\n");
    }
}

Run with:

wasm-pack test --headless --firefox