diff --git a/package-lock.json b/package-lock.json index 6598248..7418c4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1071,6 +1071,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2035,11 +2044,13 @@ "@replit/codemirror-vim": "^6.3.0", "codemirror": "^6.0.2", "codemirror-lang-graphalg": "^0.1.0", + "highlight.js": "^11.11.1", "katex": "^0.16.25", "vis-network": "^10.0.2" }, "devDependencies": { "@lezer/generator": "^1.8.0", + "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.2", "@rollup/plugin-typescript": "^12.3.0", "@rollup/plugin-url": "^8.0.2", diff --git a/playground/binding.mjs b/playground/binding.mjs index 59da24e..fa26ab2 100644 --- a/playground/binding.mjs +++ b/playground/binding.mjs @@ -41,6 +41,7 @@ export function loadPlaygroundWasm() { bindings.ga_diag_col_end = instance.cwrap('ga_diag_col_end', 'number', ['number', 'number']); bindings.ga_diag_msg = instance.cwrap('ga_diag_msg', 'number', ['number', 'number']); bindings.ga_desugar = instance.cwrap('ga_desugar', 'number', ['number']) + bindings.ga_print_module = instance.cwrap('ga_print_module', 'number', ['number']) bindings.ga_add_arg = instance.cwrap('ga_add_arg', null, ['number', 'number', 'number']); bindings.ga_set_dims = instance.cwrap('ga_set_dims', 'number', ['number', 'string']); bindings.ga_set_arg_bool = instance.cwrap('ga_set_arg_bool', 'number', ['number', 'number', 'number', 'number', 'number']); diff --git a/playground/cpp/CMakeLists.txt b/playground/cpp/CMakeLists.txt index 2d19f38..8f654c4 100644 --- a/playground/cpp/CMakeLists.txt +++ b/playground/cpp/CMakeLists.txt @@ -50,6 +50,7 @@ if(ENABLE_WASM) _ga_diag_col_end _ga_diag_msg _ga_desugar + _ga_print_module _ga_add_arg _ga_set_dims _ga_set_arg_bool diff --git a/playground/cpp/graphalg-playground.cpp b/playground/cpp/graphalg-playground.cpp index b0effac..8375181 100644 --- a/playground/cpp/graphalg-playground.cpp +++ b/playground/cpp/graphalg-playground.cpp @@ -44,6 +44,7 @@ class Playground { mlir::MLIRContext _ctx; llvm::SmallVector _diagnostics; mlir::OwningOpRef _moduleOp; + std::string _moduleString; llvm::SmallVector _argDims; mlir::func::FuncOp _funcOp; llvm::SmallVector _argBuilders; @@ -64,6 +65,8 @@ class Playground { bool desugarToCore(); + const char *printModule(); + void addArgument(std::size_t rows, std::size_t cols); bool setDimensions(llvm::StringRef func); @@ -168,6 +171,13 @@ bool Playground::desugarToCore() { } } +const char *Playground::printModule() { + _moduleString.clear(); + llvm::raw_string_ostream os(_moduleString); + _moduleOp->print(os); + return _moduleString.c_str(); +} + bool Playground::setDimensions(llvm::StringRef func) { graphalg::GraphAlgSetDimensionsOptions options{ .functionName = func.str(), @@ -275,6 +285,8 @@ const char *ga_diag_msg(Playground *pg, std::size_t i) { bool ga_desugar(Playground *pg) { return pg->desugarToCore(); } +const char *ga_print_module(Playground *pg) { return pg->printModule(); } + void ga_add_arg(Playground *pg, std::size_t rows, std::size_t cols) { pg->addArgument(rows, cols); } diff --git a/playground/editor.ts b/playground/editor.ts index 7affefd..a872650 100644 --- a/playground/editor.ts +++ b/playground/editor.ts @@ -1,229 +1,16 @@ -import { EditorView, basicSetup } from "codemirror" -import { vim } from "@replit/codemirror-vim" -import { keymap } from "@codemirror/view" -import { indentWithTab } from "@codemirror/commands" import { linter, Diagnostic } from "@codemirror/lint" -import { GraphAlg } from "codemirror-lang-graphalg" import { loadPlaygroundWasm } from "./binding.mjs" -import { DataSet, Network } from "vis-network/standalone" import katex from "katex" import renderMathInElement from "katex/contrib/auto-render" +import { PlaygroundInstance } from "./src/PlaygroundInstance" +import { GraphAlgMatrix, GraphAlgMatrixEntry } from "./src/GraphAlgMatrix" +import { GraphAlgEditor, GraphAlgEditorMode } from "./src/GraphAlgEditor" +import { MatrixRenderMode, renderMatrix } from "./src/matrixRendering" +import { parseMatrix, ParseMatrixError } from "./src/matrixParsing" // Load and register all webassembly bindings let playgroundWasmBindings = loadPlaygroundWasm(); -interface GraphAlgDiagnostic { - startLine: number; - endLine: number; - startColumn: number; - endColumn: number; - message: string; -} - -interface GraphAlgMatrixEntry { - row: number; - col: number; - val: boolean | bigint | number; -} -interface GraphAlgMatrix { - ring: string; - rows: number; - cols: number; - values: GraphAlgMatrixEntry[], -} - -interface RunResults { - result?: GraphAlgMatrix; - diagnostics: GraphAlgDiagnostic[]; -} - -class PlaygroundInstance { - bindings: any; - - constructor(bindings: any) { - this.bindings = bindings; - } - - getDiagnosticsAndFree(pg: number): GraphAlgDiagnostic[] { - const ga_diag_count = this.bindings.ga_diag_count; - const ga_diag_line_start = this.bindings.ga_diag_line_start; - const ga_diag_line_end = this.bindings.ga_diag_line_end; - const ga_diag_col_start = this.bindings.ga_diag_col_start; - const ga_diag_col_end = this.bindings.ga_diag_col_end; - const ga_diag_msg = this.bindings.ga_diag_msg; - const ga_free = this.bindings.ga_free; - const UTF8ToString = this.bindings.UTF8ToString; - - const diagnostics: GraphAlgDiagnostic[] = []; - const ndiag = ga_diag_count(pg); - for (let i = 0; i < ndiag; i++) { - diagnostics.push({ - startLine: ga_diag_line_start(pg, i), - endLine: ga_diag_line_end(pg, i), - startColumn: ga_diag_col_start(pg, i), - endColumn: ga_diag_col_end(pg, i), - message: UTF8ToString(ga_diag_msg(pg, i)) - }); - } - - ga_free(pg); - return diagnostics; - } - - lint(program: string): GraphAlgDiagnostic[] { - const ga_new = this.bindings.ga_new; - const ga_free = this.bindings.ga_free; - const ga_parse = this.bindings.ga_parse; - const ga_desugar = this.bindings.ga_desugar; - - const pg = ga_new(); - if (ga_parse(pg, program)) { - // Also desugar - // TODO: Catch desugar error - ga_desugar(pg); - } - - return this.getDiagnosticsAndFree(pg); - } - - compile(program: string): GraphAlgDiagnostic[] { - const ga_new = this.bindings.ga_new; - const ga_parse = this.bindings.ga_parse; - const ga_desugar = this.bindings.ga_desugar; - - const pg = ga_new(); - - if (!ga_parse(pg, program)) { - return this.getDiagnosticsAndFree(pg); - } - - if (!ga_desugar(pg)) { - return this.getDiagnosticsAndFree(pg); - } - - return []; - } - - run(program: string, func: string, args: GraphAlgMatrix[]): RunResults { - const ga_new = this.bindings.ga_new; - const ga_parse = this.bindings.ga_parse; - const ga_desugar = this.bindings.ga_desugar; - const ga_add_arg = this.bindings.ga_add_arg; - const ga_set_dims = this.bindings.ga_set_dims; - const ga_set_arg_bool = this.bindings.ga_set_arg_bool; - const ga_set_arg_int = this.bindings.ga_set_arg_int; - const ga_set_arg_real = this.bindings.ga_set_arg_real; - const ga_evaluate = this.bindings.ga_evaluate; - const ga_get_res_ring = this.bindings.ga_get_res_ring; - const ga_get_res_rows = this.bindings.ga_get_res_rows; - const ga_get_res_cols = this.bindings.ga_get_res_cols; - const ga_get_res_bool = this.bindings.ga_get_res_bool; - const ga_get_res_int = this.bindings.ga_get_res_int; - const ga_get_res_real = this.bindings.ga_get_res_real; - const ga_get_res_inf = this.bindings.ga_get_res_inf; - const UTF8ToString = this.bindings.UTF8ToString; - - const pg = ga_new(); - - if (!ga_parse(pg, program)) { - return { - diagnostics: this.getDiagnosticsAndFree(pg), - }; - } - - if (!ga_desugar(pg)) { - return { - diagnostics: this.getDiagnosticsAndFree(pg), - }; - } - - for (let arg of args) { - ga_add_arg(pg, arg.rows, arg.cols); - } - - if (!ga_set_dims(pg, func)) { - return { - diagnostics: this.getDiagnosticsAndFree(pg), - }; - } - - args.forEach((arg, idx) => { - for (let val of arg.values) { - switch (arg.ring) { - case 'i1': - ga_set_arg_bool(pg, idx, val.row, val.col, val.val); - break; - case 'i64': - case '!graphalg.trop_i64': - case '!graphalg.trop_max_i64': - ga_set_arg_int(pg, idx, val.row, val.col, val.val); - break; - case 'f64': - case '!graphalg.trop_f64': - ga_set_arg_real(pg, idx, val.row, val.col, val.val); - break; - } - } - }); - - if (!ga_evaluate(pg)) { - return { - diagnostics: this.getDiagnosticsAndFree(pg), - }; - } - - const resultRing = UTF8ToString(ga_get_res_ring(pg)); - const resultRows = ga_get_res_rows(pg); - const resultCols = ga_get_res_cols(pg); - let resultVals: GraphAlgMatrixEntry[] = []; - for (let r = 0; r < resultRows; r++) { - for (let c = 0; c < resultCols; c++) { - let val; - switch (resultRing) { - case 'i1': - val = ga_get_res_bool(pg, r, c); - break; - case '!graphalg.trop_i64': - case '!graphalg.trop_max_i64': - if (ga_get_res_inf(pg, r, c)) { - // Skip infinity - continue; - } - case 'i64': - val = ga_get_res_int(pg, r, c); - break; - case '!graphalg.trop_f64': - if (ga_get_res_inf(pg, r, c)) { - // Skip infinity - continue; - } - case 'f64': - val = ga_get_res_real(pg, r, c); - break; - default: - throw Error(`Invalid result semiring '${resultRing}'`); - } - - resultVals.push({ - row: r, - col: c, - val: val, - }); - } - } - - return { - result: { - ring: resultRing, - rows: resultRows, - cols: resultCols, - values: resultVals, - }, - diagnostics: this.getDiagnosticsAndFree(pg), - }; - } -} - const GraphAlgLinter = linter(view => { let diagnostics: Diagnostic[] = []; if (!playgroundWasmBindings.loaded) { @@ -252,392 +39,6 @@ const GraphAlgLinter = linter(view => { return diagnostics; }); -function parseMatrix(input: string): GraphAlgMatrix { - const lines = input.split(';'); - const header = lines[0].split(','); - const rows = parseInt(header[0]); - const cols = parseInt(header[1]); - const ring = header[2].trim(); - - let values: GraphAlgMatrixEntry[] = []; - for (let line of lines.slice(1)) { - if (!line) { - // Skip empty lines - continue - } - - const parts = line.split(','); - - let val: boolean | bigint | number; - switch (ring) { - case 'i1': - val = true; - break; - case 'i64': - case '!graphalg.trop_i64': - case '!graphalg.trop_max_i64': - val = BigInt(parts[2]); - break; - case 'f64': - case '!graphalg.trop_f64': - val = parseFloat(parts[2]); - break; - default: - throw new Error(`invalid ring ${ring}`); - } - - values.push({ - row: parseInt(parts[0]), - col: parseInt(parts[1]), - val, - }) - } - - return { - ring, - rows, - cols, - values, - } -} - -function renderValue(entry: boolean | bigint | number, ring: string) { - switch (ring) { - case 'f64': - case '!graphalg.trop_f64': - return (entry as number).toFixed(3); - case 'i1': - return (entry as boolean) ? "1" : "0"; - default: - return entry.toString(); - } -} - -function renderMatrixLatex(m: GraphAlgMatrix): HTMLElement { - let defaultCellValue; - switch (m.ring) { - case "i1": - case "i64": - case "f64": - defaultCellValue = "0"; - break; - case "!graphalg.trop_i64": - case "!graphalg.trop_f64": - case "!graphalg.trop_max_i64": - defaultCellValue = "\\infty"; - break; - default: - defaultCellValue = ""; - break; - } - - let rows: string[][] = []; - for (let r = 0; r < m.rows; r++) { - let cols: string[] = []; - for (let c = 0; c < m.cols; c++) { - cols.push(defaultCellValue); - } - - rows.push(cols); - } - - for (let val of m.values) { - rows[val.row][val.col] = renderValue(val.val, m.ring); - } - - const tex = - "\\begin{bmatrix}\n" + - rows.map((row) => row.join(" & ")).join("\\\\") - + "\n\\end{bmatrix}"; - - const katexCont = document.createElement("div"); - // TODO: We could generate MathML directly, skipping katex entirely. - katex.render(tex, katexCont, { output: "mathml" }); - return katexCont; -} - -function renderMatrixTable(m: GraphAlgMatrix): HTMLTableElement { - // Create an output table. - const table = document.createElement("table"); - - // Header - const thead = document.createElement("thead"); - const tr = document.createElement("tr"); - const thRow = document.createElement("th"); - thRow.textContent = "Row"; - const thCol = document.createElement("th"); - thCol.textContent = "Column"; - const thVal = document.createElement("th"); - thVal.textContent = "Value"; - tr.append(thRow, thCol, thVal); - thead.appendChild(tr); - - // Body - const tbody = document.createElement("tbody"); - for (let val of m.values) { - const tr = document.createElement("tr"); - const tdRow = document.createElement("td"); - tdRow.textContent = val.row.toString(); - const tdCol = document.createElement("td"); - tdCol.textContent = val.col.toString(); - - const tdVal = document.createElement("td"); - tdVal.textContent = renderValue(val.val, m.ring); - tr.append(tdRow, tdCol, tdVal); - tbody.appendChild(tr); - } - - table.append(thead, tbody); - return table; -} - -function renderMatrixVisGraph(m: GraphAlgMatrix): HTMLElement { - if (m.rows != m.cols) { - throw Error("renderMatrixVisGraph called with a non-square matrix"); - } - - const container = document.createElement("div"); - container.style.width = "100%"; - container.style.height = "300px"; - container.style.border = "1px solid black"; - - interface NodeItem { - id: number; - label: string; - } - const nodes = new DataSet(); - for (let r = 0; r < m.rows; r++) { - nodes.add({ id: r, label: r.toString() }); - } - - // create an array with edges - interface EdgeItem { - id: number; - from: number; - to: number; - label: string; - } - const edges = new DataSet(); - for (let val of m.values) { - let label = renderValue(val.val, m.ring); - if (m.ring == "i1" && val.val == true) { - label = ""; - } - - edges.add({ - id: edges.length, - from: val.row, - to: val.col, - label: label - }); - } - - // create a network - const data = { - nodes: nodes, - edges: edges, - }; - var options = { - layout: { - // Deterministic layout of graphs - randomSeed: 42 - }, - edges: { - arrows: { - to: { - enabled: true - } - } - } - }; - var network = new Network(container, data, options); - - return container; -} - -function renderVectorAsNodeProperty( - vector: GraphAlgMatrix, - graph: GraphAlgMatrix): HTMLElement { - if (vector.rows != graph.rows - || graph.rows != graph.cols - || vector.cols != 1) { - console.warn("cannot render as node property due to incompatible dimensions, falling back to default output rendering"); - return renderMatrixAuto(vector); - } - - const container = document.createElement("div"); - container.style.width = "100%"; - container.style.height = "300px"; - container.style.border = "1px solid black"; - - interface NodeItem { - id: number; - label: string; - color?: string; - } - let nodes: NodeItem[] = []; - for (let r = 0; r < vector.rows; r++) { - let label = "Node " + r.toString(); - if (vector.ring == '!graphalg.trop_f64' - || vector.ring == '!graphalg.trop_i64') { - label = `Node ${r}\nvalue: ∞`; - } - - nodes.push({ id: r, label: label }); - } - - for (let val of vector.values) { - const renderVal = renderValue(val.val, vector.ring); - nodes[val.row].label = `Node ${val.row}\nvalue: ${renderVal}`; - if (vector.ring == 'i1' && val.val) { - // A shade of red to complement default blue. - nodes[val.row].color = '#FB7E81'; - } - } - - const nodeDataSet = new DataSet(nodes); - - // create an array with edges - interface EdgeItem { - id: number; - from: number; - to: number; - label: string; - } - const edges = new DataSet(); - for (let val of graph.values) { - edges.add({ - id: edges.length, - from: val.row, - to: val.col, - label: renderValue(val.val, graph.ring) - }); - } - - // create a network - const data = { - nodes: nodes, - edges: edges, - }; - var options = { - layout: { - // Deterministic layout of graphs - randomSeed: 42 - }, - edges: { - arrows: { - to: { - enabled: true - } - } - } - }; - var network = new Network(container, data, options); - - return container; -} - -function renderMatrixAuto(m: GraphAlgMatrix): HTMLElement { - if (m.rows == 1 && m.cols == 1) { - // Simple scalar - return renderMatrixLatex(m); - } else if (m.rows == m.cols && m.rows < 20) { - return renderMatrixVisGraph(m); - } else if (m.rows < 20 && m.cols < 20) { - return renderMatrixLatex(m); - } else { - return renderMatrixTable(m); - } -} - -enum MatrixRenderMode { - AUTO, - LATEX, - VIS_GRAPH, - TABLE, - VERTEX_PROPERTY, -} - -function renderMatrix(m: GraphAlgMatrix, mode: MatrixRenderMode) { - switch (mode) { - case MatrixRenderMode.LATEX: - return renderMatrixLatex(m); - case MatrixRenderMode.VIS_GRAPH: - return renderMatrixVisGraph(m); - case MatrixRenderMode.TABLE: - return renderMatrixTable(m); - default: - return renderMatrixAuto(m); - } -} - -class GraphAlgEditor { - root: Element; - toolbar: Element; - editorContainer: Element; - argumentContainer: Element; - outputContainer: Element; - - initialProgram: string; - functionName?: string; - arguments: GraphAlgMatrix[] = []; - renderMode: MatrixRenderMode = MatrixRenderMode.AUTO; - resultRenderMode: MatrixRenderMode = MatrixRenderMode.AUTO; - - editorView?: EditorView; - - constructor(rootElem: Element, program: string) { - this.root = rootElem; - this.initialProgram = program; - - // Container for toolbar buttons above the editor - this.toolbar = document.createElement("div"); - - // Container to host the editor view - this.editorContainer = document.createElement("div"); - // NOTE: pt-1 is a just-the-docs class to add padding at the top. - // This helps separate it from the toolbar. - this.editorContainer.setAttribute('class', 'pt-1'); - - this.argumentContainer = document.createElement("div"); - - // Container for output - this.outputContainer = document.createElement("div"); - - this.root.append( - this.toolbar, - this.editorContainer, - this.argumentContainer, - this.outputContainer); - } - - initializeEditorView() { - this.editorView = new EditorView({ - extensions: [ - //vim(), - keymap.of([indentWithTab]), - basicSetup, - GraphAlg(), - GraphAlgLinter, - ], - parent: this.editorContainer, - doc: this.initialProgram, - }); - } - - addArgument(arg: GraphAlgMatrix) { - this.arguments.push(arg); - - // Display in accordion below the editor. - const argDetails = document.createElement("details"); - const argSummary = document.createElement("summary"); - argSummary.textContent = `Argument ${this.arguments.length} (${arg.ring} x ${arg.rows} x ${arg.cols})`; - const table = renderMatrix(arg, this.renderMode); - argDetails.append(argSummary, table); - this.argumentContainer.appendChild(argDetails); - } -} - // Find code snippets to turn into editors. let editors: GraphAlgEditor[] = []; const codeElems = document.getElementsByClassName("language-graphalg"); @@ -651,6 +52,11 @@ for (let elem of Array.from(codeElems)) { // Have additional annotations in a pre wrapper elem = elem.parentElement; + const editorMode = elem.getAttribute('data-ga-editor'); + if (editorMode && editorMode == 'playground') { + editor.editorMode = GraphAlgEditorMode.PLAYGROUND; + } + const func = elem.getAttribute('data-ga-func'); if (func) { editor.functionName = func; @@ -669,7 +75,11 @@ for (let elem of Array.from(codeElems)) { } const parsed = parseMatrix(arg); - editor.addArgument(parsed); + if (parsed instanceof ParseMatrixError) { + console.error(parsed); + } else { + editor.addArgument(parsed); + } } const resultRender = elem.getAttribute('data-ga-result-render'); @@ -690,116 +100,14 @@ for (let elem of Array.from(codeElems)) { // Initialize editor views for (let editor of editors) { - editor.initializeEditorView(); -} - -function buildErrorNote(diagnostics: GraphAlgDiagnostic[]): HTMLQuoteElement { - const quote = document.createElement("blockquote"); - quote.setAttribute('class', 'error-title'); - const title = document.createElement("p"); - title.textContent = "Compiler error"; - quote.appendChild(title); - - for (let diag of diagnostics) { - const pelem = document.createElement("p"); - pelem.textContent = `line ${diag.startLine}: ${diag.message}`; - quote.appendChild(pelem); - } - - return quote; -} - -function buildCompileSuccessNote(): HTMLQuoteElement { - const quote = document.createElement("blockquote"); - quote.setAttribute('class', 'success-title'); - quote.innerHTML = ` -

Compiled successfully

-

- Parser: Syntax valid ✓ -
- Type checker: Types valid ✓ -

`; - - return quote; -} - -function run(editor: GraphAlgEditor, inst: PlaygroundInstance) { - const program = editor.editorView?.state.doc.toString(); - if (!program) { - throw new Error("No program to run"); - } - - const result = inst.run(program, editor.functionName!!, editor.arguments); - let resultElem; - if (result.result) { - if (editor.resultRenderMode == MatrixRenderMode.VERTEX_PROPERTY) { - resultElem = renderVectorAsNodeProperty(result.result, editor.arguments[0]); - } else { - resultElem = renderMatrix(result.result, editor.resultRenderMode); - } - } else { - resultElem = buildErrorNote(result.diagnostics); - } - - // Place output in a default-open accordion - const details = document.createElement("details"); - details.setAttribute('open', 'true'); - const summary = document.createElement("summary"); - summary.textContent = "Output"; - details.append(summary, resultElem); - editor.outputContainer.replaceChildren(details); -} - -function compile(editor: GraphAlgEditor, inst: PlaygroundInstance) { - const program = editor.editorView?.state.doc.toString(); - if (!program) { - throw new Error("No program to compile"); - } - - const diagnostics = inst.compile(program); - let resultElem; - if (diagnostics.length > 0) { - resultElem = buildErrorNote(diagnostics); - } else { - resultElem = buildCompileSuccessNote(); - } - - // Place output in a default-open accordion - const details = document.createElement("details"); - details.setAttribute('open', 'true'); - const summary = document.createElement("summary"); - summary.textContent = "Output"; - details.append(summary, resultElem); - editor.outputContainer.replaceChildren(details); + editor.initializeEditorView(GraphAlgLinter); } // Add run buttons playgroundWasmBindings.onLoaded((bindings: any) => { const instance = new PlaygroundInstance(bindings); - for (let editor of editors) { - if (editor.functionName) { - const runButton = document.createElement("button"); - runButton.setAttribute('type', 'button'); - runButton.setAttribute('name', 'run'); - runButton.setAttribute('class', 'btn'); - runButton.textContent = `Run '${editor.functionName}'`; - runButton.addEventListener('click', () => { - run(editor, instance); - }); - editor.toolbar.appendChild(runButton); - } else { - // No function to run, compile only - const compileButton = document.createElement("button"); - compileButton.setAttribute('type', 'button'); - compileButton.setAttribute('name', 'compile'); - compileButton.setAttribute('class', 'btn'); - compileButton.textContent = "Compile"; - compileButton.addEventListener('click', () => { - compile(editor, instance); - }); - editor.toolbar.appendChild(compileButton); - } + editor.bindPlayground(instance); } }); @@ -807,6 +115,10 @@ playgroundWasmBindings.onLoaded((bindings: any) => { const graphElems = document.getElementsByClassName("language-graphalg-matrix"); for (let elem of Array.from(graphElems)) { const mat = parseMatrix(elem.textContent.trim()); + if (mat instanceof ParseMatrixError) { + console.error(mat); + continue; + } let mode: string | null = null; if (elem.parentElement?.tagName == 'PRE') { @@ -816,16 +128,14 @@ for (let elem of Array.from(graphElems)) { mode = elem.getAttribute('data-ga-mode'); } - let rendered: HTMLElement; + let renderMode = MatrixRenderMode.LATEX; if (mode == "vis") { - rendered = renderMatrixVisGraph(mat); + renderMode = MatrixRenderMode.VIS_GRAPH; } else if (mode == "coo") { - rendered = renderMatrixTable(mat); - } else { - rendered = renderMatrixLatex(mat); + renderMode = MatrixRenderMode.TABLE; } - elem.replaceWith(rendered); + elem.replaceWith(renderMatrix(mat, renderMode)); } // Initialize math views diff --git a/playground/index.md b/playground/index.md index 72d0ebf..00bee56 100644 --- a/playground/index.md +++ b/playground/index.md @@ -7,430 +7,6 @@ nav_order: 4 # GraphAlg Playground Compile and execute GraphAlg programs in your browser! -{: .warning-title } -> Early Preview -> -> The examples below are highly experimental and may malfunction. - -## Breadth-First Search - -{: - data-ga-func="BFS" - data-ga-arg-0=" - 10, 10, i1; - 0, 1; - 0, 2; - 1, 2; - 1, 3; - 1, 4; - 2, 0; - 3, 5; - 3, 6; - 3, 7; - 4, 0; - 4, 1; - 5, 3; - 5, 7; - 7, 0; - 7, 1; - 7, 2; - 8, 9;" - data-ga-arg-1=" - 10, 1, i1; - 0, 0;" - data-ga-result-render="vertex-property" -} -```graphalg -func setDepth(b:bool, iter:int) -> int { - return cast(b) * (iter + int(2)); -} - -func BFS( - graph: Matrix, - source: Vector) -> Vector { - v = Vector(graph.nrows); - v[:] = int(1); - - frontier = source; - reach = source; - - for i in graph.nrows { - step = Vector(graph.nrows); - step = frontier * graph; - - v += apply(setDepth, step, i); - - frontier = step; - reach += step; - } until frontier.nvals == int(0); - - return v; -} -``` - -## Label Propagation - -{: - data-ga-func="CDLP" - data-ga-arg-0=" - 8, 8, i1; - 0, 1; - 0, 2; - 0, 6; - 1, 0; - 1, 2; - 2, 0; - 2, 1; - 3, 4; - 3, 5; - 4, 3; - 4, 5; - 4, 6; - 5, 4; - 5, 6; - 6, 4; - 6, 5; - 6, 7; - 7, 5;" - data-ga-result-render="latex" -} -```graphalg -func isMax(v: int, max: trop_max_int) -> bool { - return (cast(v) == max) - * (v != zero(int)); -} - -func CDLP(graph: Matrix) -> Matrix { - iterations = int(5); - id = Vector(graph.nrows); - id[:] = bool(true); - L = diag(id); - - for i in int(0):iterations { - step_forward = cast(graph) * cast(L); - step_backward = cast(graph.T) * cast(L); - step = step_forward (.+) step_backward; - - // Max per row - max = reduceRows(cast(step)); - - // Broadcast to all columns - b = Vector(graph.ncols); - b[:] = one(trop_max_int); - max_broadcast = max * b.T; - - // Matrix with true at every position where L has max element. - step_max = step (.isMax) max_broadcast; - - // Keep only one assigned label per vertex. - // The implementation always picks the one with the lowest id. - L = pickAny(step_max); - } - - // Map isolated nodes to their own label. - connected = reduceRows(graph) (.+) reduceRows(graph.T); - isolated = Vector(graph.nrows); - isolated[:] = bool(true); - L = diag(isolated) (.+) L; - - return L; -} -``` - -## PageRank - -{: - data-ga-func="PR" - data-ga-arg-0=" - 50, 50, i1; - 0, 18; - 0, 20; - 0, 21; - 0, 26; - 0, 30; - 0, 36; - 0, 44; - 0, 47; - 1, 2; - 1, 19; - 1, 38; - 1, 45; - 2, 5; - 2, 9; - 2, 31; - 2, 40; - 2, 44; - 3, 14; - 4, 14; - 4, 15; - 4, 17; - 4, 27; - 4, 46; - 5, 48; - 6, 5; - 6, 26; - 6, 42; - 6, 45; - 7, 4; - 7, 20; - 7, 28; - 7, 29; - 7, 31; - 7, 42; - 8, 15; - 8, 17; - 8, 20; - 8, 27; - 8, 29; - 8, 34; - 8, 39; - 9, 8; - 9, 12; - 9, 27; - 9, 28; - 9, 32; - 10, 2; - 10, 38; - 11, 46; - 11, 49; - 12, 6; - 12, 11; - 12, 16; - 12, 31; - 12, 47; - 13, 3; - 13, 19; - 13, 20; - 13, 34; - 13, 37; - 13, 39; - 14, 7; - 14, 23; - 14, 30; - 14, 34; - 14, 43; - 16, 4; - 16, 8; - 16, 10; - 16, 15; - 16, 25; - 16, 36; - 17, 0; - 17, 11; - 17, 27; - 17, 29; - 17, 43; - 17, 44; - 17, 46; - 17, 49; - 18, 9; - 18, 10; - 18, 12; - 18, 26; - 18, 37; - 19, 14; - 19, 24; - 20, 21; - 20, 26; - 20, 30; - 20, 31; - 20, 39; - 21, 18; - 21, 25; - 21, 26; - 21, 30; - 22, 21; - 22, 34; - 22, 35; - 22, 37; - 22, 39; - 22, 45; - 22, 46; - 23, 8; - 23, 12; - 23, 14; - 23, 33; - 23, 35; - 23, 49; - 24, 7; - 24, 23; - 24, 29; - 24, 33; - 24, 40; - 24, 46; - 25, 6; - 25, 30; - 25, 36; - 25, 39; - 25, 43; - 25, 46; - 26, 30; - 26, 32; - 26, 42; - 27, 7; - 27, 31; - 27, 41; - 27, 44; - 28, 0; - 28, 1; - 28, 11; - 28, 13; - 28, 15; - 28, 18; - 28, 19; - 28, 35; - 29, 8; - 29, 23; - 29, 33; - 29, 43; - 30, 10; - 30, 16; - 30, 31; - 30, 38; - 30, 45; - 30, 46; - 31, 1; - 31, 27; - 31, 28; - 31, 29; - 31, 30; - 32, 6; - 32, 7; - 32, 8; - 32, 9; - 32, 31; - 32, 33; - 32, 36; - 33, 25; - 33, 47; - 34, 2; - 34, 9; - 34, 16; - 34, 23; - 34, 25; - 34, 27; - 34, 32; - 34, 40; - 35, 19; - 35, 20; - 35, 28; - 35, 31; - 35, 45; - 36, 0; - 36, 4; - 36, 8; - 36, 12; - 36, 22; - 36, 23; - 37, 1; - 37, 21; - 37, 49; - 38, 5; - 38, 7; - 38, 19; - 38, 27; - 38, 29; - 38, 46; - 38, 47; - 39, 4; - 39, 6; - 39, 7; - 39, 10; - 39, 32; - 39, 33; - 39, 36; - 39, 48; - 40, 23; - 40, 42; - 42, 0; - 42, 1; - 42, 10; - 42, 14; - 42, 16; - 42, 28; - 42, 37; - 42, 46; - 43, 10; - 43, 12; - 43, 14; - 44, 4; - 44, 10; - 44, 11; - 44, 20; - 44, 23; - 45, 22; - 45, 23; - 45, 25; - 45, 30; - 45, 35; - 45, 40; - 46, 7; - 46, 13; - 46, 15; - 46, 27; - 46, 28; - 46, 33; - 46, 34; - 46, 39; - 46, 41; - 46, 45; - 46, 49; - 47, 7; - 47, 18; - 47, 29; - 47, 34; - 47, 37; - 47, 42; - 47, 49; - 48, 6; - 48, 7; - 48, 16; - 48, 17; - 49, 3; - 49, 27; - 49, 46;" - data-ga-result-render="vertex-property" -} -```graphalg -func withDamping(degree:int, damping:real) -> real { - return cast(degree) / damping; -} - -func PR(graph: Matrix) -> Vector { - damping = real(0.85); - iterations = int(10); - n = graph.nrows; - teleport = (real(1.0) - damping) / cast(n); - rdiff = real(1.0); - - d_out = reduceRows(cast(graph)); - - d = apply(withDamping, d_out, damping); - - connected = reduceRows(graph); - sinks = Vector(n); - sinks[:] = bool(true); - - pr = Vector(n); - pr[:] = real(1.0) / cast(n); - - for i in int(0):iterations { - sink_pr = Vector(n); - sink_pr = pr; - redist = (damping / cast(n)) * reduce(sink_pr); - - w = pr (./) d; - - pr[:] = teleport + redist; - pr += cast(graph).T * w; - } - - return pr; -} -``` - -## Single-Source Shortest Paths - {: data-ga-func="SSSP" data-ga-arg-0=" @@ -451,6 +27,7 @@ func PR(graph: Matrix) -> Vector { data-ga-arg-1=" 10, 1, i1; 0, 0;" + data-ga-editor="playground" data-ga-result-render="vertex-property" } ```graphalg @@ -465,44 +42,5 @@ func SSSP( } ``` -## Weakly Connected Components - -{: - data-ga-func="WCC" - data-ga-arg-0=" - 9, 9, i1; - 0, 1; - 0, 2; - 1, 0; - 1, 2; - 1, 3; - 3, 1; - 5, 6; - 5, 7; - 6, 5; - 8, 2;" - data-ga-result-render="latex" -} -```graphalg -func WCC(graph: Matrix) -> Matrix { - id = Vector(graph.nrows); - id[:] = bool(true); - label = diag(id); - - for i in graph.nrows { - // Keep current label - alternatives = label; - // Labels reachable with a forward step - alternatives += graph * label; - // Labels reachable with a backward step - alternatives += graph.T * label; - - // Select a new label - label = pickAny(alternatives); - } - - return label; -} -``` - + diff --git a/playground/package.json b/playground/package.json index 1fe84e0..28f3aa2 100644 --- a/playground/package.json +++ b/playground/package.json @@ -8,6 +8,7 @@ "@replit/codemirror-vim": "^6.3.0", "codemirror": "^6.0.2", "codemirror-lang-graphalg": "^0.1.0", + "highlight.js": "^11.11.1", "katex": "^0.16.25", "vis-network": "^10.0.2" }, diff --git a/playground/src/GraphAlgDiagnostic.ts b/playground/src/GraphAlgDiagnostic.ts new file mode 100644 index 0000000..c4ed7e8 --- /dev/null +++ b/playground/src/GraphAlgDiagnostic.ts @@ -0,0 +1,7 @@ +export interface GraphAlgDiagnostic { + startLine: number; + endLine: number; + startColumn: number; + endColumn: number; + message: string; +} diff --git a/playground/src/GraphAlgEditor.ts b/playground/src/GraphAlgEditor.ts new file mode 100644 index 0000000..9b4f33f --- /dev/null +++ b/playground/src/GraphAlgEditor.ts @@ -0,0 +1,394 @@ +import { GraphAlgMatrix } from "./GraphAlgMatrix" +import { GraphAlgDiagnostic } from "./GraphAlgDiagnostic" +import { PlaygroundInstance } from "./PlaygroundInstance" +import { renderVectorAsNodeProperty, renderMatrix, MatrixRenderMode } from "./matrixRendering" +import { EditorView, basicSetup } from "codemirror" +import { Extension } from "@codemirror/state" +import { keymap } from "@codemirror/view" +import { indentWithTab } from "@codemirror/commands" +import { GraphAlg } from "codemirror-lang-graphalg" +import { parseMatrix, ParseMatrixError } from "./matrixParsing" +import { highlightMLIR } from './highlightMLIR' + +export enum GraphAlgEditorMode { + TUTORIAL, + PLAYGROUND, +} + +class EditorArgument { + rootElem: HTMLDetailsElement; + value?: GraphAlgMatrix; + + constructor(root: HTMLDetailsElement) { + this.rootElem = root; + } + + destroy() { + // TODO: Cleanup network vis etc. + } +}; + +export class GraphAlgEditor { + root: Element; + toolbar: Element; + editorContainer: Element; + argumentContainer: Element; + argumentToolbar: HTMLElement; + outputContainer: Element; + + initialProgram: string; + functionName?: string; + arguments: EditorArgument[] = []; + editorMode: GraphAlgEditorMode = GraphAlgEditorMode.TUTORIAL; + renderMode: MatrixRenderMode = MatrixRenderMode.AUTO; + resultRenderMode: MatrixRenderMode = MatrixRenderMode.AUTO; + + editorView?: EditorView; + + constructor(rootElem: Element, program: string) { + this.root = rootElem; + this.initialProgram = program; + + // Container for toolbar buttons above the editor + this.toolbar = document.createElement("div"); + + // Container to host the editor view + this.editorContainer = document.createElement("div"); + // NOTE: pt-1 is a just-the-docs class to add padding at the top. + // This helps separate it from the toolbar. + this.editorContainer.setAttribute('class', 'pt-1'); + + this.argumentContainer = document.createElement("div"); + + this.argumentToolbar = document.createElement("div"); + + // Container for output + this.outputContainer = document.createElement("div"); + + this.root.append( + this.toolbar, + this.editorContainer, + this.argumentContainer, + this.argumentToolbar, + this.outputContainer); + } + + initializeEditorView(linter: Extension) { + this.editorView = new EditorView({ + extensions: [ + //vim(), + keymap.of([indentWithTab]), + basicSetup, + GraphAlg(), + linter, + ], + parent: this.editorContainer, + doc: this.initialProgram, + }); + + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND) { + this.argumentToolbar.style.marginTop = '.5em'; + this.argumentToolbar.style.marginBottom = '.5em'; + + // Add argument button + const addButton = document.createElement("button"); + addButton.setAttribute('type', 'button'); + addButton.setAttribute('class', 'btn'); + addButton.textContent = "Add Argument"; + addButton.addEventListener('click', () => { + this.addArgument(); + }); + this.argumentToolbar.appendChild(addButton); + + // Remove argument button + const removeButton = document.createElement("button"); + removeButton.setAttribute('type', 'button'); + removeButton.setAttribute('class', 'btn'); + removeButton.textContent = "Remove Argument"; + removeButton.addEventListener('click', () => { + this.dropArgument(); + }); + this.argumentToolbar.appendChild(removeButton); + } + } + + addArgument(value?: GraphAlgMatrix) { + const argElem = document.createElement("details"); + const argument = new EditorArgument(argElem); + argument.value = value; + + this.argumentContainer.appendChild(argElem); + this.arguments.push(argument); + this.renderArgument(this.arguments.length - 1); + } + + dropArgument() { + // TODO: If there are no more arguments left, disable the remove + // argument button. + const arg = this.arguments.pop(); + if (arg) { + arg.destroy(); + arg.rootElem.remove(); + } + } + + bindPlayground(instance: PlaygroundInstance) { + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND) { + // Allow changing the name of called function. + const funcNameContainer = document.createElement("div"); + funcNameContainer.style.backgroundColor = '#f7f7f7'; + funcNameContainer.style.borderRadius = '4px'; + funcNameContainer.style.padding = '0.3em 1em'; + funcNameContainer.style.borderWidth = '0'; + funcNameContainer.style.boxShadow = 'rgba(0, 0, 0, 0.12) 0px 1px 2px 0px, rgba(0, 0, 0, 0.08) 0px 3px 10px 0px'; + funcNameContainer.style.color = '#7253ed'; + funcNameContainer.style.lineHeight = '1.5'; + funcNameContainer.style.display = 'inline-block'; + funcNameContainer.style.fontWeight = '500'; + funcNameContainer.textContent = "Function:"; + + const funcNameInput = document.createElement("input"); + funcNameInput.setAttribute('type', 'text'); + funcNameInput.style.backgroundColor = '#f7f7f7'; + funcNameInput.style.color = '#7253ed'; + funcNameInput.style.borderWidth = '0'; + funcNameContainer.appendChild(funcNameInput); + + if (this.functionName) { + funcNameInput.value = this.functionName; + } + + funcNameInput.addEventListener('change', () => { + console.log(funcNameInput.value); + this.functionName = funcNameInput.value; + }); + + this.toolbar.appendChild(funcNameContainer); + } + + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND + || this.functionName) { + // Add run button + const runButton = document.createElement("button"); + runButton.setAttribute('type', 'button'); + runButton.setAttribute('class', 'btn'); + runButton.textContent = + this.editorMode == GraphAlgEditorMode.TUTORIAL + ? + `Run '${this.functionName}'` + : "Run"; + runButton.addEventListener('click', () => { + this.run(instance); + }); + this.toolbar.appendChild(runButton); + } else { + // No function to run, compile only + const compileButton = document.createElement("button"); + compileButton.setAttribute('type', 'button'); + compileButton.setAttribute('class', 'btn'); + compileButton.textContent = "Compile"; + compileButton.addEventListener('click', () => { + this.compile(instance); + }); + this.toolbar.appendChild(compileButton); + } + } + + tryRun(inst: PlaygroundInstance): HTMLElement[] { + let outputElems: HTMLElement[] = []; + const program = this.editorView?.state.doc.toString(); + if (!program) { + outputElems.push(buildErrorNote("No program to run")); + } + + const args: GraphAlgMatrix[] = []; + this.arguments.forEach((arg, idx) => { + if (arg.value) { + args.push(arg.value); + } else { + outputElems.push(buildErrorNote(`Argument ${idx} has no value set`)); + } + }); + + if (!this.functionName) { + outputElems.push(buildErrorNote("No function name set")); + } + + if (outputElems.length > 0) { + // Collected some errors. + return outputElems; + } + + const result = inst.run(program!!, this.functionName!!, args); + let resultElem; + if (result.result) { + if (this.resultRenderMode == MatrixRenderMode.VERTEX_PROPERTY + && this.arguments.length >= 1) { + const height = this.editorMode == GraphAlgEditorMode.PLAYGROUND ? + "600px" + : "300px"; + resultElem = renderVectorAsNodeProperty( + result.result, + this.arguments[0].value!!, + height); + } else { + resultElem = renderMatrix(result.result, this.resultRenderMode); + } + } else { + resultElem = buildDiagnosticsNote(result.diagnostics); + } + + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND && result.parsedIR) { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = "GraphAlg IR"; + details.append(summary, renderIR(result.parsedIR)); + outputElems.push(details); + } + + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND && result.coreIR) { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = "Core IR"; + details.append(summary, renderIR(result.coreIR)); + outputElems.push(details); + } + + // Place output in a default-open accordion + const details = document.createElement("details"); + details.setAttribute('open', 'true'); + const summary = document.createElement("summary"); + summary.textContent = "Output"; + details.append(summary, resultElem); + outputElems.push(details); + + return outputElems; + } + + run(inst: PlaygroundInstance) { + const outputElems = this.tryRun(inst); + this.outputContainer.replaceChildren(...outputElems); + } + + compile(inst: PlaygroundInstance) { + const program = this.editorView?.state.doc.toString(); + if (!program) { + throw new Error("No program to compile"); + } + + const diagnostics = inst.compile(program); + let resultElem; + if (diagnostics.length > 0) { + resultElem = buildDiagnosticsNote(diagnostics); + } else { + resultElem = buildCompileSuccessNote(); + } + + // Place output in a default-open accordion + const details = document.createElement("details"); + details.setAttribute('open', 'true'); + const summary = document.createElement("summary"); + summary.textContent = "Output"; + details.append(summary, resultElem); + this.outputContainer.replaceChildren(details); + } + + renderArgument(argIndex: number) { + const arg = this.arguments[argIndex]; + // Cleanup visualizations etc. + arg.destroy(); + + const argSummary = document.createElement("summary"); + if (arg.value) { + argSummary.textContent = `Argument ${argIndex} (${arg.value.ring} x ${arg.value.rows} x ${arg.value.cols})`; + } else { + argSummary.textContent = `Argument ${argIndex}`; + } + + // NOTE: Also cleans up previously rendered nodes. + arg.rootElem.replaceChildren(argSummary); + + if (this.editorMode == GraphAlgEditorMode.PLAYGROUND) { + // Allow uploading a replacement file. + const inputFile = document.createElement("input"); + inputFile.setAttribute("type", "file"); + arg.rootElem.appendChild(inputFile); + + inputFile.addEventListener("change", async () => { + if (inputFile.files?.length != 1) { + return; + } + + const file = inputFile.files[0]; + const content = await file.text(); + const mat = parseMatrix(content); + if (mat instanceof ParseMatrixError) { + window.alert(`Invalid input matrix: ${mat.message}`); + } else { + arg.value = mat; + this.renderArgument(argIndex); + } + }); + } + + if (arg.value) { + console.log(arg.value); + arg.rootElem.appendChild(renderMatrix(arg.value, this.renderMode)); + } + } +} + +function renderIR(ir: string): HTMLElement { + const pre = document.createElement("pre"); + const code = document.createElement("code"); + code.textContent = ir; + code.classList.add('language-mlir'); + highlightMLIR(code); + pre.appendChild(code); + return pre; +} + +function buildDiagnosticsNote(diagnostics: GraphAlgDiagnostic[]): HTMLQuoteElement { + const quote = document.createElement("blockquote"); + quote.setAttribute('class', 'error-title'); + const title = document.createElement("p"); + title.textContent = "Compiler error"; + quote.appendChild(title); + + for (let diag of diagnostics) { + const pelem = document.createElement("p"); + pelem.textContent = `line ${diag.startLine}: ${diag.message}`; + quote.appendChild(pelem); + } + + return quote; +} + +function buildErrorNote(message: string): HTMLQuoteElement { + const quote = document.createElement("blockquote"); + quote.setAttribute('class', 'error-title'); + const title = document.createElement("p"); + title.textContent = "Compiler error"; + quote.appendChild(title); + + const pelem = document.createElement("p"); + pelem.textContent = message; + quote.appendChild(pelem); + + return quote; +} + + +function buildCompileSuccessNote(): HTMLQuoteElement { + const quote = document.createElement("blockquote"); + quote.setAttribute('class', 'success-title'); + quote.innerHTML = ` +

Compiled successfully

+

+ Parser: Syntax valid ✓ +
+ Type checker: Types valid ✓ +

`; + + return quote; +} diff --git a/playground/src/GraphAlgMatrix.ts b/playground/src/GraphAlgMatrix.ts new file mode 100644 index 0000000..686c1ae --- /dev/null +++ b/playground/src/GraphAlgMatrix.ts @@ -0,0 +1,12 @@ +export interface GraphAlgMatrixEntry { + row: number; + col: number; + val: boolean | bigint | number; +} + +export interface GraphAlgMatrix { + ring: string; + rows: number; + cols: number; + values: GraphAlgMatrixEntry[], +} diff --git a/playground/src/PlaygroundInstance.ts b/playground/src/PlaygroundInstance.ts new file mode 100644 index 0000000..b3f5475 --- /dev/null +++ b/playground/src/PlaygroundInstance.ts @@ -0,0 +1,207 @@ +import { GraphAlgDiagnostic } from "./GraphAlgDiagnostic" +import { GraphAlgMatrix, GraphAlgMatrixEntry } from "./GraphAlgMatrix" + +export interface RunResults { + result?: GraphAlgMatrix; + diagnostics: GraphAlgDiagnostic[]; + parsedIR?: string; // IR immediately after parsing + coreIR?: string; // IR after desugaring to Core +} + +export class PlaygroundInstance { + bindings: any; + + constructor(bindings: any) { + this.bindings = bindings; + } + + getDiagnosticsAndFree(pg: number): GraphAlgDiagnostic[] { + const ga_diag_count = this.bindings.ga_diag_count; + const ga_diag_line_start = this.bindings.ga_diag_line_start; + const ga_diag_line_end = this.bindings.ga_diag_line_end; + const ga_diag_col_start = this.bindings.ga_diag_col_start; + const ga_diag_col_end = this.bindings.ga_diag_col_end; + const ga_diag_msg = this.bindings.ga_diag_msg; + const ga_free = this.bindings.ga_free; + const UTF8ToString = this.bindings.UTF8ToString; + + const diagnostics: GraphAlgDiagnostic[] = []; + const ndiag = ga_diag_count(pg); + for (let i = 0; i < ndiag; i++) { + diagnostics.push({ + startLine: ga_diag_line_start(pg, i), + endLine: ga_diag_line_end(pg, i), + startColumn: ga_diag_col_start(pg, i), + endColumn: ga_diag_col_end(pg, i), + message: UTF8ToString(ga_diag_msg(pg, i)) + }); + } + + ga_free(pg); + return diagnostics; + } + + lint(program: string): GraphAlgDiagnostic[] { + const ga_new = this.bindings.ga_new; + const ga_free = this.bindings.ga_free; + const ga_parse = this.bindings.ga_parse; + const ga_desugar = this.bindings.ga_desugar; + + const pg = ga_new(); + if (ga_parse(pg, program)) { + // Also desugar + // TODO: Catch desugar error + ga_desugar(pg); + } + + return this.getDiagnosticsAndFree(pg); + } + + compile(program: string): GraphAlgDiagnostic[] { + const ga_new = this.bindings.ga_new; + const ga_parse = this.bindings.ga_parse; + const ga_desugar = this.bindings.ga_desugar; + + const pg = ga_new(); + + if (!ga_parse(pg, program)) { + return this.getDiagnosticsAndFree(pg); + } + + if (!ga_desugar(pg)) { + return this.getDiagnosticsAndFree(pg); + } + + return []; + } + + run(program: string, func: string, args: GraphAlgMatrix[]): RunResults { + const ga_new = this.bindings.ga_new; + const ga_parse = this.bindings.ga_parse; + const ga_desugar = this.bindings.ga_desugar; + const ga_print_module = this.bindings.ga_print_module; + const ga_add_arg = this.bindings.ga_add_arg; + const ga_set_dims = this.bindings.ga_set_dims; + const ga_set_arg_bool = this.bindings.ga_set_arg_bool; + const ga_set_arg_int = this.bindings.ga_set_arg_int; + const ga_set_arg_real = this.bindings.ga_set_arg_real; + const ga_evaluate = this.bindings.ga_evaluate; + const ga_get_res_ring = this.bindings.ga_get_res_ring; + const ga_get_res_rows = this.bindings.ga_get_res_rows; + const ga_get_res_cols = this.bindings.ga_get_res_cols; + const ga_get_res_bool = this.bindings.ga_get_res_bool; + const ga_get_res_int = this.bindings.ga_get_res_int; + const ga_get_res_real = this.bindings.ga_get_res_real; + const ga_get_res_inf = this.bindings.ga_get_res_inf; + const UTF8ToString = this.bindings.UTF8ToString; + + const pg = ga_new(); + + if (!ga_parse(pg, program)) { + return { + diagnostics: this.getDiagnosticsAndFree(pg), + }; + } + + const parsedIR = UTF8ToString(ga_print_module(pg)); + + if (!ga_desugar(pg)) { + return { + diagnostics: this.getDiagnosticsAndFree(pg), + }; + } + + const coreIR = UTF8ToString(ga_print_module(pg)); + + for (let arg of args) { + ga_add_arg(pg, arg.rows, arg.cols); + } + + if (!ga_set_dims(pg, func)) { + return { + diagnostics: this.getDiagnosticsAndFree(pg), + parsedIR, + coreIR, + }; + } + + args.forEach((arg, idx) => { + for (let val of arg.values) { + switch (arg.ring) { + case 'i1': + ga_set_arg_bool(pg, idx, val.row, val.col, val.val); + break; + case 'i64': + case '!graphalg.trop_i64': + case '!graphalg.trop_max_i64': + ga_set_arg_int(pg, idx, val.row, val.col, val.val); + break; + case 'f64': + case '!graphalg.trop_f64': + ga_set_arg_real(pg, idx, val.row, val.col, val.val); + break; + } + } + }); + + if (!ga_evaluate(pg)) { + return { + diagnostics: this.getDiagnosticsAndFree(pg), + parsedIR, + coreIR, + }; + } + + const resultRing = UTF8ToString(ga_get_res_ring(pg)); + const resultRows = ga_get_res_rows(pg); + const resultCols = ga_get_res_cols(pg); + let resultVals: GraphAlgMatrixEntry[] = []; + for (let r = 0; r < resultRows; r++) { + for (let c = 0; c < resultCols; c++) { + let val; + switch (resultRing) { + case 'i1': + val = ga_get_res_bool(pg, r, c); + break; + case '!graphalg.trop_i64': + case '!graphalg.trop_max_i64': + if (ga_get_res_inf(pg, r, c)) { + // Skip infinity + continue; + } + case 'i64': + val = ga_get_res_int(pg, r, c); + break; + case '!graphalg.trop_f64': + if (ga_get_res_inf(pg, r, c)) { + // Skip infinity + continue; + } + case 'f64': + val = ga_get_res_real(pg, r, c); + break; + default: + throw Error(`Invalid result semiring '${resultRing}'`); + } + + resultVals.push({ + row: r, + col: c, + val: val, + }); + } + } + + return { + result: { + ring: resultRing, + rows: resultRows, + cols: resultCols, + values: resultVals, + }, + diagnostics: this.getDiagnosticsAndFree(pg), + parsedIR, + coreIR: coreIR, + }; + } +} diff --git a/playground/src/highlightMLIR.js b/playground/src/highlightMLIR.js new file mode 100644 index 0000000..d9021ae --- /dev/null +++ b/playground/src/highlightMLIR.js @@ -0,0 +1,79 @@ +import hljs from '@highlightjs/cdn-assets/es/highlight.js' + +function mlir(hljs) { + var ID = '[\\w\\d_$.]+'; + var PRIMITIVE_TYPES = { + className: 'type', + begin: '[x\\b\\s]*(i\\d+|f(16|32|64)|bf16)', + }; + + var SEMI_AFFINE_MAP = { + className: 'attr', + begin: '\\([^)>]*\\)\\s*->\\s*\\([^)>]*\\)' + }; + var LAYOUT_SPECIFICATION = { + className: 'type', + variants: [SEMI_AFFINE_MAP] + }; + + return { + name: 'MLIR', + keywords: + 'func module ' + + 'br cond_br return', + contains: [ + PRIMITIVE_TYPES, + { + className: 'type', + begin: '!' + ID, + }, + hljs.C_LINE_COMMENT_MODE, + hljs.QUOTE_STRING_MODE, + { + className: 'type', begin: '(memref|tensor|vector)<\\b', end: '>', + keywords: "memref tensor vector", + contains: [ + { + className: 'number', + variants: [ + { begin: '[*]x' }, + { begin: '((\\?|\\d+)\\s*x\\s*)+' }, + ] + }, + 'self', + PRIMITIVE_TYPES, + LAYOUT_SPECIFICATION + ] + }, + { + className: 'keyword', begin: 'affine_map<', end: '>', + keywords: 'affine_map', + contains: [ + SEMI_AFFINE_MAP + ] + }, + { + className: 'title', + variants: [ + { begin: '@' + ID }, + { begin: '@\\d+' }, + ] + }, + { + className: 'symbol', + variants: [ + { begin: '%' + ID + '([:#]\\d+)?' }, + { begin: '\\^' + ID }, + { begin: '#' + ID }, + ] + }, + hljs.C_NUMBER_MODE + ] + }; +} + +hljs.registerLanguage('mlir', mlir); + +export function highlightMLIR(elem) { + hljs.highlightElement(elem); +} diff --git a/playground/src/matrixParsing.ts b/playground/src/matrixParsing.ts new file mode 100644 index 0000000..d623c4e --- /dev/null +++ b/playground/src/matrixParsing.ts @@ -0,0 +1,118 @@ +import { GraphAlgMatrix, GraphAlgMatrixEntry } from "./GraphAlgMatrix"; + +export class ParseMatrixError extends Error { + cause?: Error; + + constructor(message: string) { + super(message); + this.name = "ParseMatrixError"; + } +} + +const VALID_RINGS = new Set([ + "i1", + "i64", + "f64", + "!graphalg.trop_i64", + "!graphalg.trop_f64", + "!graphalg.trop_max_i64", +]); + +export function parseMatrix(input: string): GraphAlgMatrix | ParseMatrixError { + const lines = input.split(';'); + if (lines.length == 0) { + return new ParseMatrixError("Empty input"); + } + + const header = lines[0].split(','); + if (header.length != 3) { + return new ParseMatrixError(`Header should have 3 values separated by commas, got ${header.length}`); + } + + const rows = parseInt(header[0]); + if (isNaN(rows)) { + return new ParseMatrixError(`Invalid number of rows '${header[0]}' in header`); + } + + const cols = parseInt(header[1]); + if (isNaN(cols)) { + return new ParseMatrixError(`Invalid number of columns '${header[1]}' in header`); + } + + const ring = header[2].trim(); + if (!VALID_RINGS.has(ring)) { + return new ParseMatrixError(`Invalid semiring '${header[2]}' in header`); + } + + let values: GraphAlgMatrixEntry[] = []; + for (let line of lines.slice(1)) { + if (!line.trim()) { + // Skip empty lines + continue + } + + const parts = line.split(','); + if (ring == 'i1' && parts.length != 2) { + return new ParseMatrixError(`Expected two values (row, col) per entry, got ${parts.length} in '${line}'`); + } else if (ring != 'i1' && parts.length != 3) { + return new ParseMatrixError(`Expected three values (row, col, val) per entry, got ${parts.length} in '${line}'`); + } + + let val: boolean | bigint | number; + switch (ring) { + case 'i1': + val = true; + break; + case 'i64': + case '!graphalg.trop_i64': + case '!graphalg.trop_max_i64': + try { + val = BigInt(parts[2]); + } catch (err) { + const parseErr = new ParseMatrixError(`Invalid value for ring ${ring} '${parts[2]}'`); + if (err instanceof Error) { + parseErr.cause = err; + } + + return parseErr; + } + break; + case 'f64': + case '!graphalg.trop_f64': + val = parseFloat(parts[2]); + if (isNaN(val)) { + return new ParseMatrixError(`Invalid floatig-point value '${parts[2]}'`); + } + break; + default: + return new ParseMatrixError(`invalid ring ${ring}`); + } + + const row = parseInt(parts[0]); + if (isNaN(row)) { + return new ParseMatrixError(`Invalid row index '${parts[0]}'`); + } else if (row >= rows) { + return new ParseMatrixError(`Row index ${row} exceeds matrix dimensions ${rows} x ${cols}`); + } + + const col = parseInt(parts[1]); + if (isNaN(col)) { + return new ParseMatrixError(`Invalid column index '${parts[1]}'`); + } else if (col >= cols) { + return new ParseMatrixError(`Column index ${col} exceeds matrix dimensions ${rows} x ${cols}`); + } + + values.push({ + row, + col, + val, + }) + } + + return { + ring, + rows, + cols, + values, + } +} diff --git a/playground/src/matrixRendering.ts b/playground/src/matrixRendering.ts new file mode 100644 index 0000000..af35c0c --- /dev/null +++ b/playground/src/matrixRendering.ts @@ -0,0 +1,274 @@ +import { DataSet, Network } from "vis-network/standalone" +import { GraphAlgMatrix } from "./GraphAlgMatrix" +import katex from "katex" + +function renderValue(entry: boolean | bigint | number, ring: string) { + switch (ring) { + case 'f64': + case '!graphalg.trop_f64': + return (entry as number).toFixed(3); + case 'i1': + return (entry as boolean) ? "1" : "0"; + default: + return entry.toString(); + } +} + +function renderMatrixLatex(m: GraphAlgMatrix): HTMLElement { + let defaultCellValue; + switch (m.ring) { + case "i1": + case "i64": + case "f64": + defaultCellValue = "0"; + break; + case "!graphalg.trop_i64": + case "!graphalg.trop_f64": + case "!graphalg.trop_max_i64": + defaultCellValue = "\\infty"; + break; + default: + defaultCellValue = ""; + break; + } + + let rows: string[][] = []; + for (let r = 0; r < m.rows; r++) { + let cols: string[] = []; + for (let c = 0; c < m.cols; c++) { + cols.push(defaultCellValue); + } + + rows.push(cols); + } + + for (let val of m.values) { + rows[val.row][val.col] = renderValue(val.val, m.ring); + } + + const tex = + "\\begin{bmatrix}\n" + + rows.map((row) => row.join(" & ")).join("\\\\") + + "\n\\end{bmatrix}"; + + const katexCont = document.createElement("div"); + // TODO: We could generate MathML directly, skipping katex entirely. + katex.render(tex, katexCont, { output: "mathml" }); + return katexCont; +} + +function renderMatrixTable(m: GraphAlgMatrix): HTMLTableElement { + // Create an output table. + const table = document.createElement("table"); + + // Header + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + const thRow = document.createElement("th"); + thRow.textContent = "Row"; + const thCol = document.createElement("th"); + thCol.textContent = "Column"; + const thVal = document.createElement("th"); + thVal.textContent = "Value"; + tr.append(thRow, thCol, thVal); + thead.appendChild(tr); + + // Body + const tbody = document.createElement("tbody"); + for (let val of m.values) { + const tr = document.createElement("tr"); + const tdRow = document.createElement("td"); + tdRow.textContent = val.row.toString(); + const tdCol = document.createElement("td"); + tdCol.textContent = val.col.toString(); + + const tdVal = document.createElement("td"); + tdVal.textContent = renderValue(val.val, m.ring); + tr.append(tdRow, tdCol, tdVal); + tbody.appendChild(tr); + } + + table.append(thead, tbody); + return table; +} + +function renderMatrixVisGraph(m: GraphAlgMatrix): HTMLElement { + if (m.rows != m.cols) { + throw Error("renderMatrixVisGraph called with a non-square matrix"); + } + + const container = document.createElement("div"); + container.style.width = "100%"; + container.style.height = "300px"; + container.style.border = "1px solid black"; + + interface NodeItem { + id: number; + label: string; + } + const nodes = new DataSet(); + for (let r = 0; r < m.rows; r++) { + nodes.add({ id: r, label: r.toString() }); + } + + // create an array with edges + interface EdgeItem { + id: number; + from: number; + to: number; + label: string; + } + const edges = new DataSet(); + for (let val of m.values) { + let label = renderValue(val.val, m.ring); + if (m.ring == "i1" && val.val == true) { + label = ""; + } + + edges.add({ + id: edges.length, + from: val.row, + to: val.col, + label: label + }); + } + + // create a network + const data = { + nodes: nodes, + edges: edges, + }; + var options = { + layout: { + // Deterministic layout of graphs + randomSeed: 42 + }, + edges: { + arrows: { + to: { + enabled: true + } + } + } + }; + var network = new Network(container, data, options); + + return container; +} + +export function renderVectorAsNodeProperty( + vector: GraphAlgMatrix, + graph: GraphAlgMatrix, + height = "300px"): HTMLElement { + if (vector.rows != graph.rows + || graph.rows != graph.cols + || vector.cols != 1) { + console.warn("cannot render as node property due to incompatible dimensions, falling back to default output rendering"); + return renderMatrixAuto(vector); + } + + const container = document.createElement("div"); + container.style.width = "100%"; + container.style.height = height; + container.style.border = "1px solid black"; + + interface NodeItem { + id: number; + label: string; + color?: string; + } + let nodes: NodeItem[] = []; + for (let r = 0; r < vector.rows; r++) { + let label = "Node " + r.toString(); + if (vector.ring == '!graphalg.trop_f64' + || vector.ring == '!graphalg.trop_i64') { + label = `Node ${r}\nvalue: ∞`; + } + + nodes.push({ id: r, label: label }); + } + + for (let val of vector.values) { + const renderVal = renderValue(val.val, vector.ring); + nodes[val.row].label = `Node ${val.row}\nvalue: ${renderVal}`; + if (vector.ring == 'i1' && val.val) { + // A shade of red to complement default blue. + nodes[val.row].color = '#FB7E81'; + } + } + + const nodeDataSet = new DataSet(nodes); + + // create an array with edges + interface EdgeItem { + id: number; + from: number; + to: number; + label: string; + } + const edges = new DataSet(); + for (let val of graph.values) { + edges.add({ + id: edges.length, + from: val.row, + to: val.col, + label: renderValue(val.val, graph.ring) + }); + } + + // create a network + const data = { + nodes: nodes, + edges: edges, + }; + var options = { + layout: { + // Deterministic layout of graphs + randomSeed: 42 + }, + edges: { + arrows: { + to: { + enabled: true + } + } + } + }; + var network = new Network(container, data, options); + + return container; +} + +function renderMatrixAuto(m: GraphAlgMatrix): HTMLElement { + if (m.rows == 1 && m.cols == 1) { + // Simple scalar + return renderMatrixLatex(m); + } else if (m.rows == m.cols && m.rows < 20) { + return renderMatrixVisGraph(m); + } else if (m.rows < 20 && m.cols < 20) { + return renderMatrixLatex(m); + } else { + return renderMatrixTable(m); + } +} + +export enum MatrixRenderMode { + AUTO, + LATEX, + VIS_GRAPH, + TABLE, + VERTEX_PROPERTY, +} + +export function renderMatrix(m: GraphAlgMatrix, mode: MatrixRenderMode) { + switch (mode) { + case MatrixRenderMode.LATEX: + return renderMatrixLatex(m); + case MatrixRenderMode.VIS_GRAPH: + return renderMatrixVisGraph(m); + case MatrixRenderMode.TABLE: + return renderMatrixTable(m); + default: + return renderMatrixAuto(m); + } +}