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
1 change: 1 addition & 0 deletions crate/Cargo.lock

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

1 change: 1 addition & 0 deletions crate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ path = "src/lib.rs"

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

[profile.release]
Expand Down
19 changes: 19 additions & 0 deletions crate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,25 @@ pub fn compile(src: &str) -> Result<Vec<u8>, JsValue> {
Ok(result.wasm)
}

/// Compile Fink source and return the WAT text disassembly.
/// Returns the WAT string on success, or throws a JS error on failure.
#[wasm_bindgen]
pub fn compile_wat(src: &str) -> Result<String, 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);
wasmprinter::print_bytes(&result.wasm)
.map_err(|e| JsValue::from_str(&format!("{e}")))
}

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

/// Stateful parsed document - parse once, query many times.
Expand Down
7 changes: 6 additions & 1 deletion src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// WASM crate (fink_playground_wasm). Call setCompileModule() once the crate
// is initialised (see main.ts) so compile() can delegate to it.

type WasmModule = { compile: (src: string) => Uint8Array }
type WasmModule = { compile: (src: string) => Uint8Array; compile_wat: (src: string) => string }

let wasmMod: WasmModule | null = null

Expand All @@ -14,3 +14,8 @@ export async function compile(src: string): Promise<Uint8Array | null> {
if (!wasmMod) return null
return wasmMod.compile(src)
}

export function compileWat(src: string): string | null {
if (!wasmMod) return null
return wasmMod.compile_wat(src)
}
5 changes: 4 additions & 1 deletion src/fragment.html
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@
/* --- CPS panels --- */

#fink-cps,
#fink-cps-lifted {
#fink-cps-lifted,
#fink-wat {
overflow: hidden; /* Monaco manages its own scroll */
}

Expand Down Expand Up @@ -322,6 +323,7 @@
<button class="fink-tab" data-tab="fink-ast">AST</button>
<button class="fink-tab" data-tab="fink-cps">CPS</button>
<button class="fink-tab" data-tab="fink-cps-lifted">Lifted</button>
<button class="fink-tab" data-tab="fink-wat">WAT</button>
<span class="fink-playground-tabs-spacer"></span>
<button id="fink-share-btn">Share</button>
</div>
Expand All @@ -336,6 +338,7 @@
<div id="fink-ast" class="fink-tab-panel"></div>
<div id="fink-cps" class="fink-tab-panel"></div>
<div id="fink-cps-lifted" class="fink-tab-panel"></div>
<div id="fink-wat" class="fink-tab-panel"></div>
</div>
</div>
</div>
Expand Down
15 changes: 14 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ 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, setCompileModule } from './compiler.js'
import { compile, compileWat, setCompileModule } from './compiler.js'
import { run } from './wasi-shim.js'
import { FinkTokenizer, type LexToken } from './tokenizer.js'
import { TokensPanel } from './tokens-panel.js'
import { AstPanel } from './ast-panel.js'
import { CpsPanel } from './cps-panel.js'
import { WatPanel } from './wat-panel.js'
import { defineTheme, watchColorScheme } from './theme.js'

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -129,6 +130,12 @@ function _reparse(src: string, _modelVersion: number): void {
cpsPanel?.updateSrcTokens(highlightTokens)
cpsPanelLifted?.update(cpsLiftedJson, ParsedDocument)
cpsPanelLifted?.updateSrcTokens(highlightTokens)
try {
const wat = compileWat(src)
if (wat !== null) watPanel?.update(wat)
} catch (e) {
console.warn('[fink] WAT compilation failed:', e)
}
const t3 = performance.now()
lastParseMs = t1 - t0
statusParseEl?.updateTime(lastParseMs)
Expand Down Expand Up @@ -184,6 +191,8 @@ let astPanel: AstPanel | null = null
// CPS panels — initialized after the editor is created (see below).
let cpsPanel: CpsPanel | null = null
let cpsPanelLifted: CpsPanel | null = null
// WAT panel — initialized after the editor is created (see below).
let watPanel: WatPanel | null = null

monaco.languages.setLanguageConfiguration('fink', {
comments: {
Expand Down Expand Up @@ -410,6 +419,7 @@ for (const tab of document.querySelectorAll<HTMLElement>('.fink-tab')) {
activeTab = tab.dataset.tab!
if (activeTab === 'fink-cps') cpsPanel?.layout()
if (activeTab === 'fink-cps-lifted') cpsPanelLifted?.layout()
if (activeTab === 'fink-wat') watPanel?.layout()
if (!SYNC_TABS.has(activeTab)) {
// Passive tab (e.g. Output) — clear all decorations and stop syncing.
clearAllDecorations()
Expand Down Expand Up @@ -453,6 +463,9 @@ cpsPanelLifted = new CpsPanel(
editor,
)

// WAT panel — read-only WAT disassembly, updated live on each reparse
watPanel = new WatPanel(document.getElementById('fink-wat')!)

// When a CPS panel becomes active, clear token decoration and the other CPS panel.
cpsPanel.onActivate = () => { tokensPanel.clearEditorHighlight(); cpsPanelLifted?.clearAll() }
cpsPanelLifted.onActivate = () => { tokensPanel.clearEditorHighlight(); cpsPanel?.clearAll() }
Expand Down
153 changes: 153 additions & 0 deletions src/wat-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// WAT panel — read-only Monaco editor showing the WAT disassembly of compiled Fink source.
//
// No cursor sync for now — sourcemap integration is deferred.
// Updated live on every reparse via update(wat).
//
// Syntax highlighting: hand-written Monarch tokenizer (no onigasm / TextMate deps).
// Covers the WAT subset emitted by fink codegen plus the full WAT MVP spec.

import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'

// Register language + tokenizer once.
monaco.languages.register({ id: 'wat' })

monaco.languages.setMonarchTokensProvider('wat', {
// WAT structural / type keywords
keywords: [
'module', 'func', 'type', 'import', 'export', 'param', 'result',
'local', 'global', 'memory', 'table', 'elem', 'data', 'start',
'block', 'loop', 'if', 'else', 'end', 'then',
'offset', 'align', 'declare',
'mut', 'sub', 'rec', 'final', 'field',
// value types
'i32', 'i64', 'f32', 'f64', 'v128',
'funcref', 'externref', 'anyref', 'eqref', 'i31ref', 'nullref',
'ref', 'null',
// GC types
'struct', 'array',
// instructions (common subset emitted by fink codegen)
'unreachable', 'nop', 'return', 'call', 'call_indirect', 'call_ref',
'return_call', 'return_call_indirect', 'return_call_ref',
'drop', 'select',
'local.get', 'local.set', 'local.tee',
'global.get', 'global.set',
'table.get', 'table.set', 'table.size', 'table.grow', 'table.fill', 'table.copy', 'table.init',
'memory.size', 'memory.grow', 'memory.fill', 'memory.copy', 'memory.init',
'ref.null', 'ref.is_null', 'ref.func', 'ref.as_non_null',
'ref.eq', 'ref.test', 'ref.cast',
'struct.new', 'struct.get', 'struct.set', 'struct.new_default',
'array.new', 'array.get', 'array.set', 'array.len', 'array.new_fixed', 'array.new_default',
'i31.new', 'i31.get_s', 'i31.get_u',
// numeric
'i32.const', 'i64.const', 'f32.const', 'f64.const',
'i32.clz', 'i32.ctz', 'i32.popcnt',
'i32.add', 'i32.sub', 'i32.mul', 'i32.div_s', 'i32.div_u',
'i32.rem_s', 'i32.rem_u', 'i32.and', 'i32.or', 'i32.xor',
'i32.shl', 'i32.shr_s', 'i32.shr_u', 'i32.rotl', 'i32.rotr',
'i32.eqz', 'i32.eq', 'i32.ne', 'i32.lt_s', 'i32.lt_u',
'i32.gt_s', 'i32.gt_u', 'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u',
'i32.wrap_i64', 'i32.trunc_f32_s', 'i32.trunc_f32_u', 'i32.trunc_f64_s', 'i32.trunc_f64_u',
'i32.reinterpret_f32', 'i32.extend8_s', 'i32.extend16_s',
'i64.clz', 'i64.ctz', 'i64.popcnt',
'i64.add', 'i64.sub', 'i64.mul', 'i64.div_s', 'i64.div_u',
'i64.rem_s', 'i64.rem_u', 'i64.and', 'i64.or', 'i64.xor',
'i64.shl', 'i64.shr_s', 'i64.shr_u', 'i64.rotl', 'i64.rotr',
'i64.eqz', 'i64.eq', 'i64.ne', 'i64.lt_s', 'i64.lt_u',
'i64.gt_s', 'i64.gt_u', 'i64.le_s', 'i64.le_u', 'i64.ge_s', 'i64.ge_u',
'i64.extend_i32_s', 'i64.extend_i32_u', 'i64.trunc_f32_s', 'i64.trunc_f32_u',
'i64.trunc_f64_s', 'i64.trunc_f64_u', 'i64.reinterpret_f64',
'f32.const', 'f32.abs', 'f32.neg', 'f32.ceil', 'f32.floor', 'f32.trunc', 'f32.nearest',
'f32.sqrt', 'f32.add', 'f32.sub', 'f32.mul', 'f32.div', 'f32.min', 'f32.max',
'f32.copysign', 'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
'f32.convert_i32_s', 'f32.convert_i32_u', 'f32.convert_i64_s', 'f32.convert_i64_u',
'f32.demote_f64', 'f32.reinterpret_i32',
'f64.const', 'f64.abs', 'f64.neg', 'f64.ceil', 'f64.floor', 'f64.trunc', 'f64.nearest',
'f64.sqrt', 'f64.add', 'f64.sub', 'f64.mul', 'f64.div', 'f64.min', 'f64.max',
'f64.copysign', 'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.convert_i64_s', 'f64.convert_i64_u',
'f64.promote_f32', 'f64.reinterpret_i64',
// memory load/store
'i32.load', 'i32.load8_s', 'i32.load8_u', 'i32.load16_s', 'i32.load16_u',
'i32.store', 'i32.store8', 'i32.store16',
'i64.load', 'i64.load8_s', 'i64.load8_u', 'i64.load16_s', 'i64.load16_u',
'i64.load32_s', 'i64.load32_u', 'i64.store', 'i64.store8', 'i64.store16', 'i64.store32',
'f32.load', 'f32.store', 'f64.load', 'f64.store',
],

tokenizer: {
root: [
// line comment ;; ...
[/;;.*$/, 'comment'],
// block comment (; ... ;)
[/\(;/, 'comment', '@blockComment'],
// string
[/"/, 'string', '@string'],
// hex number (0x...) or float with exponent
[/[+-]?0x[0-9a-fA-F][0-9a-fA-F_]*(?:\.[0-9a-fA-F_]*)?(?:[pP][+-]?\d+)?/, 'number.hex'],
// decimal / float
[/[+-]?(?:\d+(?:\.\d*)?(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)/, 'number'],
// special float literals
[/[+-]?(?:inf|nan(?::0x[0-9a-fA-F]+)?)/, 'number'],
// identifiers / keywords: $name or plain word
[/\$[A-Za-z0-9_.!#$%&'*+\-/:<=>?@\\^`|~]*/, 'variable'],
[/[A-Za-z][A-Za-z0-9_.!#$%&'*+\-/:<=>?@\\^`|~]*/, {
cases: {
'@keywords': 'keyword',
'@default': 'identifier',
},
}],
// parens
[/[()]/, 'delimiter'],
// index annotations (;= ... ;) — produced by wasmprinter for readability
[/\(;[^;]*;\)/, 'comment.doc'],
],

blockComment: [
[/[^(;]+/, 'comment'],
[/\(;/, 'comment', '@push'],
[/;\)/, 'comment', '@pop'],
[/[();]/, 'comment'],
],

string: [
[/[^"\\]+/, 'string'],
[/\\./, 'string.escape'],
[/"/, 'string', '@pop'],
],
},
})

monaco.languages.setLanguageConfiguration('wat', {
comments: { lineComment: ';;', blockComment: ['(;', ';)'] },
brackets: [['(', ')']],
autoClosingPairs: [{ open: '(', close: ')' }, { open: '"', close: '"' }],
})

export class WatPanel {
private editor: monaco.editor.IStandaloneCodeEditor

constructor(container: HTMLElement) {
this.editor = monaco.editor.create(container, {
value: '',
language: 'wat',
theme: 'fink',
readOnly: true,
fontSize: 14,
fontFamily: '"Hack", "Consolas", "Menlo", monospace',
minimap: { enabled: false },
scrollBeyondLastLine: false,
padding: { top: 16, bottom: 16 },
lineNumbers: 'on',
accessibilitySupport: 'off',
automaticLayout: true,
})
}

update(wat: string): void {
this.editor.getModel()?.setValue(wat)
}

layout(): void {
this.editor.layout()
}
}
Loading