Skip to content
Merged
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
53 changes: 52 additions & 1 deletion crate/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"

[dependencies]
fink = { git = "https://github.com/fink-lang/fink.git", tag = "v0.1.0", default-features = false }
fink = { git = "https://github.com/fink-lang/fink.git", tag = "v0.3.0", default-features = false, features = ["compiler", "wat"] }
wasm-bindgen = "0.2"

[profile.release]
Expand Down
22 changes: 22 additions & 0 deletions crate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,28 @@ fn serialize_children(node: &ast::Node) -> String {
parts.join(",")
}

// ---------------------------------------------------------------------------
// Compiler: Fink source → WASM binary
// ---------------------------------------------------------------------------

/// Compile Fink source to a WASM binary.
/// Returns the raw bytes on success, or throws a JS error on failure.
#[wasm_bindgen]
pub fn compile(src: &str) -> Result<Vec<u8>, JsValue> {
use fink::ast::build_index;
use fink::parser::parse;
use fink::passes::closure_lifting::lift_all;
use fink::passes::cps::transform::lower_expr;
use fink::passes::wasm::codegen::codegen;

let r = parse(src).map_err(|e| JsValue::from_str(&e.message))?;
let ast_index = build_index(&r);
let cps = lower_expr(&r.root);
let (lifted, resolved) = lift_all(cps, &ast_index);
let result = codegen(&lifted, &resolved, &ast_index);
Ok(result.wasm)
}

// ---------------------------------------------------------------------------

/// Stateful parsed document - parse once, query many times.
Expand Down
18 changes: 14 additions & 4 deletions src/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// Placeholder compiler — real Fink→WASM codegen is not ready yet.
// Returns null until a compile(src) → Uint8Array entry point is available.
// Fink → WASM compiler. The compile() function is backed by the playground
// WASM crate (fink_playground_wasm). Call setCompileModule() once the crate
// is initialised (see main.ts) so compile() can delegate to it.

export async function compile(_src: string): Promise<Uint8Array | null> {
return null
type WasmModule = { compile: (src: string) => Uint8Array }

let wasmMod: WasmModule | null = null

export function setCompileModule(mod: WasmModule): void {
wasmMod = mod
}

export async function compile(src: string): Promise<Uint8Array | null> {
if (!wasmMod) return null
return wasmMod.compile(src)
}
19 changes: 13 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// Analysis (semantic tokens, diagnostics, go-to-def, references) uses the
// playground WASM crate loaded via dynamic import at runtime.
//
// Code execution uses the WASI shim (wasi-shim.ts) running in a sandboxed
// iframe. The compiler slot is a placeholder for now (see compiler.ts).
// Code execution: compile(src) → WASM binary via the playground crate's
// compile() export, then run in a sandboxed WASI iframe (wasi-shim.ts).

// MonacoEnvironment must be set before the editor creates its workers.
;(window as any).MonacoEnvironment = {
Expand All @@ -34,7 +34,7 @@ import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/browser/cursorUndo.js'
import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution.js' // hover tooltips (diagnostics, symbols)
import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/goToCommands.js' // go-to-definition (F12 / Cmd+click)
import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.js' // Ctrl/Cmd+click inline
import { compile } from './compiler.js'
import { compile, setCompileModule } from './compiler.js'
import { run } from './wasi-shim.js'
import { FinkTokenizer, type LexToken } from './tokenizer.js'
import { TokensPanel } from './tokens-panel.js'
Expand Down Expand Up @@ -153,6 +153,7 @@ async function loadAnalysisWasm(): Promise<void> {
console.log('[fink] glue module imported, calling init...')
await mod.default(wasmBin)
ParsedDocument = mod.ParsedDocument
setCompileModule(mod)
resolveWasmReady()
console.log('[fink] analysis WASM ready')

Expand Down Expand Up @@ -496,19 +497,25 @@ runBtn.addEventListener('click', async () => {

try {
const src = editor.getValue()
const wasm = await compile(src)
let wasm: Uint8Array | null
try {
wasm = await compile(src)
} catch (err) {
outputEl.textContent = `Compile error: ${err}`
outputEl.className = 'error'
return
}
if (!wasm) {
outputEl.textContent = 'Compiler not available yet.'
outputEl.className = 'error'
return
}
const result = await run(wasm)

const text = result.stdout + result.stderr
outputEl.textContent = text || '(no output)'
outputEl.className = result.exitCode === 0 ? 'ok' : 'error'
} catch (err) {
outputEl.textContent = `Error: ${err}`
outputEl.textContent = `Runtime error: ${err}`
outputEl.className = 'error'
} finally {
runBtn.disabled = false
Expand Down
119 changes: 15 additions & 104 deletions src/wasi-shim.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,28 @@
// Minimal WASI preview1 shim that runs compiled WASM inside a sandboxed
// iframe and relays stdout/stderr back to the parent via postMessage.
// Fink WASM runner.
//
// Implements only the syscalls needed by typical Fink output:
// fd_write, proc_exit, fd_close, fd_seek,
// environ_sizes_get, environ_get, args_sizes_get, args_get
// The fink codegen produces a standalone module (no WASI imports) that exports:
// fink_main — entry point (no params, no results)
// result — global i32 holding the final value written by $__halt
//
// TODO: swap to WASI preview1 once the compiler supports IO.

export interface RunResult {
stdout: string
stderr: string
exitCode: number
}

// The runner script embedded in the iframe via srcdoc.
// The closing </script> tag is intentionally split across the template
// literal and the string concatenation so the HTML parser does not
// interpret it as the end of the <script> element prematurely.
const RUNNER_HTML =
`<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>
<script>
'use strict';
let memory;

function fd_write(fd, iovs, iovs_len, nwritten_ptr) {
const view = new DataView(memory.buffer);
let total = 0, text = '';
for (let i = 0; i < iovs_len; i++) {
const ptr = view.getUint32(iovs + i * 8, true);
const len = view.getUint32(iovs + i * 8 + 4, true);
text += new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len));
total += len;
export async function run(wasm: Uint8Array): Promise<RunResult> {
const { instance } = await WebAssembly.instantiate(wasm.buffer, {})
const exports = instance.exports as {
fink_main: () => void
result: WebAssembly.Global
}
view.setUint32(nwritten_ptr, total, true);
parent.postMessage({ type: 'output', fd, text }, '*');
return 0;
}

function noopZero() { return 0; }

function sizeGetZero(pc, pb) {
const v = new DataView(memory.buffer);
v.setUint32(pc, 0, true);
v.setUint32(pb, 0, true);
return 0;
}

window.addEventListener('message', async ({ data }) => {
if (data.type !== 'run') return;

const imports = {
wasi_snapshot_preview1: {
fd_write,
proc_exit: (code) => { throw { __wasi_exit: code }; },
fd_close: noopZero,
fd_seek: () => 70, // ERRNO_SPIPE — seeks not supported
environ_sizes_get: sizeGetZero,
environ_get: noopZero,
args_sizes_get: sizeGetZero,
args_get: noopZero,
},
};

try {
const result = await WebAssembly.instantiate(data.wasm, imports);
memory = result.instance.exports.memory;
result.instance.exports._start();
parent.postMessage({ type: 'done', exitCode: 0 }, '*');
exports.fink_main()
} catch (e) {
if (e && typeof e.__wasi_exit === 'number') {
parent.postMessage({ type: 'done', exitCode: e.__wasi_exit }, '*');
} else {
parent.postMessage({ type: 'error', message: String(e) }, '*');
}
return { stdout: '', stderr: `Runtime error: ${e}`, exitCode: 1 }
}
});

parent.postMessage({ type: 'ready' }, '*');
` + `</script></body></html>`

export function run(wasm: Uint8Array): Promise<RunResult> {
return new Promise((resolve, reject) => {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.setAttribute('sandbox', 'allow-scripts')
document.body.appendChild(iframe)

let stdout = ''
let stderr = ''

function cleanup(result?: RunResult, err?: Error) {
window.removeEventListener('message', onMessage)
document.body.removeChild(iframe)
if (err) reject(err)
else resolve(result!)
}

function onMessage(e: MessageEvent) {
if (e.source !== iframe.contentWindow) return
const { data } = e

if (data.type === 'ready') {
// Structured-clone the buffer so the original Uint8Array stays usable.
iframe.contentWindow!.postMessage({ type: 'run', wasm: wasm.buffer }, '*')
} else if (data.type === 'output') {
if (data.fd === 1) stdout += data.text
else if (data.fd === 2) stderr += data.text
} else if (data.type === 'done') {
cleanup({ stdout, stderr, exitCode: data.exitCode })
} else if (data.type === 'error') {
cleanup(undefined, new Error(data.message))
}
}

window.addEventListener('message', onMessage)
iframe.srcdoc = RUNNER_HTML
})
const value = exports.result.value as number
return { stdout: String(value), stderr: '', exitCode: 0 }
}
Loading