move haskell-tree-sitter-playground
This commit is contained in:
parent
2aac085bb8
commit
09e455e17e
9 changed files with 362 additions and 103 deletions
2
packages/haskell/.gitignore
vendored
2
packages/haskell/.gitignore
vendored
|
|
@ -25,5 +25,3 @@ dist-ssr
|
|||
|
||||
public/tree-sitter.wasm
|
||||
public/tree-sitter-haskell.wasm
|
||||
public/tree-sitter-haskell_mine.wasm
|
||||
public/tree-sitter-haskell_theirs.wasm
|
||||
|
|
@ -1,31 +1,11 @@
|
|||
# @strudel/haskell
|
||||
# @strudel/tidal
|
||||
|
||||
This is an experiment in implementing tree-sitter for parsing haskell.
|
||||
|
||||
So far, I have just set up a vite project that imports and inits `web-tree-sitter`, which works after
|
||||
|
||||
- <https://github.com/tree-sitter/tree-sitter/issues/2831>
|
||||
- <https://github.com/tree-sitter/tree-sitter/pull/2830>
|
||||
|
||||
Running:
|
||||
|
||||
```sh
|
||||
cd haskell && pnpm i
|
||||
pnpm i
|
||||
cd haskell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
will start the vite dev server, loading tree sitter on `http://localhost:5174/`.
|
||||
|
||||
The next step is be to generate `tree-sitter-haskell.wasm` file following <https://www.npmjs.com/package/web-tree-sitter#generate-wasm-language-files>.
|
||||
|
||||
I've tried to generate it using <https://www.npmjs.com/package/tree-sitter-haskell> but it failed, due to some versioning conflicts involving node / v8 / node-gyp.
|
||||
|
||||
It seems a lot of work has gone into this package in <https://github.com/tree-sitter/tree-sitter-haskell/pull/29>, without a new npm package version being released, which is why I've written this comment: <https://github.com/tree-sitter/tree-sitter-haskell/pull/29#issuecomment-1865951565>.
|
||||
|
||||
So either someone authorized releases a new version of the package or we might need to pull the changes and try to build it from the tree-sitter master branch.
|
||||
|
||||
## Update 1
|
||||
|
||||
I've managed to make it work by using [this tree-sitter-haskell.wasm](https://github.com/tree-sitter/tree-sitter-haskell/blob/master/tree-sitter-haskell.wasm), instead of using the version on npm! Make sure both `tree-sitter.wasm` and `tree-sitter-haskell.wasm` are in the public dir, then run `pnpm dev`. The console should log a tree sitter AST string.
|
||||
|
||||
Next step: understand how the ast works to then transform it into JS function calls!
|
||||
This
|
||||
43
packages/haskell/graph.mjs
Normal file
43
packages/haskell/graph.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Graphviz } from '@hpcc-js/wasm';
|
||||
import toDot from 'jgf-dot';
|
||||
|
||||
const graphvizLoaded = Graphviz.load();
|
||||
|
||||
function walk(node, branch = [0], parent) {
|
||||
let nodes = [];
|
||||
let edges = [];
|
||||
const color = 'white';
|
||||
const current = {
|
||||
id: branch.join('-'),
|
||||
color,
|
||||
fontcolor: color,
|
||||
label: node.type.replace('\\', 'lambda'), // backslash kills graphviz..
|
||||
};
|
||||
nodes.push(current);
|
||||
parent && edges.push({ source: parent.id, target: current.id, color });
|
||||
if (node.children.length) {
|
||||
node.children.forEach((child, j) => {
|
||||
const { nodes: childNodes, edges: childEdges } = walk(child, branch.concat([j]), current);
|
||||
nodes = nodes.concat(childNodes || []);
|
||||
edges = edges.concat(childEdges || []);
|
||||
});
|
||||
}
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
export async function renderGraph(tree, container) {
|
||||
const { nodes, edges } = walk(tree.rootNode);
|
||||
const graphviz = await graphvizLoaded;
|
||||
let dot = toDot({
|
||||
graph: {
|
||||
nodes,
|
||||
edges,
|
||||
},
|
||||
});
|
||||
dot = dot.split('\n');
|
||||
dot.splice(1, 0, 'bgcolor="transparent"');
|
||||
dot.splice(1, 0, 'color="white"');
|
||||
dot = dot.join('\n');
|
||||
const svg = await graphviz.layout(dot, 'svg', 'dot', {});
|
||||
container.innerHTML = svg;
|
||||
}
|
||||
131
packages/haskell/hs2js.mjs
Normal file
131
packages/haskell/hs2js.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
function runApply(node, scope, ops) {
|
||||
if (node.children.length !== 2)
|
||||
throw new Error(`expected 2 children for node type apply, got ${node.children.length}`);
|
||||
const [fn, arg] = node.children.map((child) => run(child, scope, ops));
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`${node.children[0].text} is not a function`);
|
||||
}
|
||||
// only works if fn is curried!
|
||||
return fn(arg);
|
||||
}
|
||||
|
||||
function runInfix(left, symbol, right, ops) {
|
||||
const customOp = ops[symbol];
|
||||
if (customOp) {
|
||||
return customOp(left, right);
|
||||
}
|
||||
switch (symbol) {
|
||||
case '+':
|
||||
return left + right;
|
||||
case '-':
|
||||
return left - right;
|
||||
case '*':
|
||||
return left * right;
|
||||
case '/':
|
||||
return left / right;
|
||||
case '$':
|
||||
return left(right);
|
||||
case '&':
|
||||
console.log('right', right);
|
||||
return right(left);
|
||||
case '.':
|
||||
return (x) => left(right(x));
|
||||
default:
|
||||
throw new Error('unexpected infix operator ' + symbol);
|
||||
}
|
||||
}
|
||||
|
||||
function curry(patterns, body, scope, ops) {
|
||||
const [variable, ...rest] = patterns;
|
||||
return (arg) => {
|
||||
let _scope = { ...scope, [variable.text]: arg };
|
||||
if (patterns.length === 1) {
|
||||
const result = run(body, _scope, ops);
|
||||
return result;
|
||||
}
|
||||
return curry(rest, body, _scope, ops);
|
||||
};
|
||||
}
|
||||
|
||||
export function run(node, scope, ops = {}) {
|
||||
let runInScope = (node, scp = scope) => run(node, scp, ops);
|
||||
//console.log("node", node.type, node.text);
|
||||
switch (node.type) {
|
||||
case 'ERROR':
|
||||
throw new Error(`invalid syntax: "${node.text}"`);
|
||||
case 'declarations':
|
||||
let result;
|
||||
node.children.forEach((declaration) => {
|
||||
result = runInScope(declaration);
|
||||
});
|
||||
return result;
|
||||
case 'integer':
|
||||
return Number(node.text);
|
||||
case 'float':
|
||||
return Number(node.text);
|
||||
case 'string':
|
||||
const str = node.text.slice(1, -1);
|
||||
return String(str);
|
||||
case 'lambda':
|
||||
const [_, lpatterns, __, lbody] = node.children;
|
||||
return curry(lpatterns.children, lbody, scope, ops);
|
||||
case 'function':
|
||||
const [fvariable, fpatterns, fbody] = node.children;
|
||||
scope[fvariable.text] = curry(fpatterns.children, fbody, scope, ops);
|
||||
return scope[fvariable.text];
|
||||
case 'list': {
|
||||
return node.children
|
||||
.filter((_, i) => i % 2 === 1) // elements are at odd indices
|
||||
.map((node) => runInScope(node));
|
||||
}
|
||||
case 'match':
|
||||
if (node.children[0].text !== '=' || node.children.length !== 2) {
|
||||
throw new Error('match node so far only support simple assignments');
|
||||
}
|
||||
return runInScope(node.children[1]);
|
||||
case 'bind':
|
||||
if (node.children.length !== 2) throw new Error('expected 2 children for node type bind');
|
||||
if (node.children[0].type !== 'variable') throw new Error('expected variable as first child of bind node');
|
||||
if (node.children[1].type !== 'match') throw new Error('expected match as first child of bind node');
|
||||
const [bvariable, bmatch] = node.children;
|
||||
const value = runInScope(bmatch);
|
||||
scope[bvariable.text] = value;
|
||||
return value;
|
||||
case 'variable':
|
||||
return scope[node.text];
|
||||
case 'infix': {
|
||||
const [a, op, b] = node.children;
|
||||
const symbol = op.text;
|
||||
const [left, right] = [runInScope(a), runInScope(b)];
|
||||
return runInfix(left, symbol, right, ops);
|
||||
}
|
||||
case 'apply':
|
||||
return runApply(node, scope, ops);
|
||||
case 'left_section': {
|
||||
const [_, b, op] = node.children;
|
||||
const right = runInScope(b);
|
||||
return (left) => runInfix(left, op.text, right, ops);
|
||||
}
|
||||
case 'right_section': {
|
||||
const [_, op, b] = node.children;
|
||||
const right = runInScope(b);
|
||||
return (left) => runInfix(left, op.text, right, ops);
|
||||
}
|
||||
case 'parens':
|
||||
if (node.children.length !== 3) throw new Error('expected 3 children for node type parens');
|
||||
return runInScope(node.children[1]);
|
||||
default:
|
||||
if (node.children.length === 0) {
|
||||
throw new Error('unhandled leaf type ' + node.type);
|
||||
}
|
||||
if (node.children.length > 1) {
|
||||
throw new Error('unhandled branch type ' + node.type);
|
||||
}
|
||||
return runInScope(node.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function evaluate(haskellCode, scope = globalThis, ops) {
|
||||
const ast = await parse(haskellCode);
|
||||
return run(ast.rootNode, scope, ops);
|
||||
}
|
||||
|
|
@ -2,13 +2,28 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tree sitter test</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121213;
|
||||
color: white;
|
||||
}
|
||||
textarea {
|
||||
padding: 10px;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="padding: 0; margin: 0">
|
||||
<textarea id="code" style="width: 100%; height: 100px"></textarea>
|
||||
<div id="graph" style="display: flex; width: 100%"></div>
|
||||
<body style="margin: 0; padding: 0">
|
||||
<textarea id="code" style="width: 100%; height: 200px"></textarea>
|
||||
<pre id="result"></pre>
|
||||
<div id="graph" style="display: flex; width: 100%; margin: auto; justify-content: center"></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,89 @@
|
|||
import Parser from 'web-tree-sitter';
|
||||
import toDot from 'jgf-dot';
|
||||
import { Graphviz } from '@hpcc-js/wasm';
|
||||
import { run } from './hs2js.mjs';
|
||||
import { renderGraph } from './graph.mjs';
|
||||
import { parse } from './parser.mjs';
|
||||
import { initStrudel, reify, late } from '@strudel/web';
|
||||
initStrudel({
|
||||
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||
});
|
||||
|
||||
const graphvizLoaded = Graphviz.load();
|
||||
const parserLoaded = loadParser();
|
||||
const graphContainer = document.getElementById('graph');
|
||||
|
||||
const textarea = document.getElementById('code');
|
||||
textarea.value = 'd1 $ s "hh(3,8)"';
|
||||
textarea.addEventListener('input', (e) => renderGraph(e.target.value, graphContainer));
|
||||
renderGraph(textarea.value, graphContainer);
|
||||
if (window.location.hash) {
|
||||
textarea.value = atob(window.location.hash.slice(1));
|
||||
} else {
|
||||
textarea.value = 'd1 $ s "jvbass(3,8)"';
|
||||
}
|
||||
textarea.addEventListener('input', (e) => {
|
||||
window.location.hash = btoa(e.target.value);
|
||||
update();
|
||||
});
|
||||
update();
|
||||
|
||||
function walk(node, branch = [0], parent) {
|
||||
let nodes = [];
|
||||
let edges = [];
|
||||
const current = { id: branch.join('-'), label: node.type };
|
||||
nodes.push(current);
|
||||
parent && edges.push({ source: parent.id, target: current.id });
|
||||
if (node.children.length) {
|
||||
node.children.forEach((child, j) => {
|
||||
const { nodes: childNodes, edges: childEdges } = walk(child, branch.concat([j]), current);
|
||||
nodes = nodes.concat(childNodes || []);
|
||||
edges = edges.concat(childEdges || []);
|
||||
function getInfixOperators() {
|
||||
let operators = {
|
||||
'>': 'set',
|
||||
'#': 'set',
|
||||
'+': 'add',
|
||||
'-': 'sub',
|
||||
'*': 'mul',
|
||||
'/': 'div',
|
||||
};
|
||||
let alignments = {
|
||||
in: (s) => '|' + s,
|
||||
out: (s) => s + '|',
|
||||
mix: (s) => '|' + s + '|',
|
||||
};
|
||||
let ops = {};
|
||||
Object.entries(operators).forEach(([o, name]) => {
|
||||
// operator without alignment
|
||||
ops[o] = (l, r) => reify(l)[name](reify(r));
|
||||
Object.entries(alignments).forEach(([a, getSymbol]) => {
|
||||
// get symbol with alignment
|
||||
let symbol = getSymbol(o);
|
||||
ops[symbol] = (l, r) => reify(l)[name][a](reify(r));
|
||||
});
|
||||
}
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
async function renderGraph(code, container) {
|
||||
const parser = await parserLoaded;
|
||||
const tree = parser.parse(code);
|
||||
const { nodes, edges } = walk(tree.rootNode);
|
||||
const graphviz = await graphvizLoaded;
|
||||
const dot = toDot({
|
||||
graph: {
|
||||
nodes,
|
||||
edges,
|
||||
},
|
||||
});
|
||||
const svg = await graphviz.layout(dot, 'svg', 'dot');
|
||||
container.innerHTML = svg;
|
||||
ops['~>'] = (l, r) => reify(l).late(reify(r));
|
||||
ops['<~'] = (l, r) => reify(l).early(reify(r));
|
||||
ops['<$>'] = (l, r) => reify(r).fmap(l).outerJoin(); // is this right?
|
||||
return ops;
|
||||
}
|
||||
const ops = getInfixOperators();
|
||||
|
||||
async function loadParser() {
|
||||
await Parser.init();
|
||||
const parser = new Parser();
|
||||
const Lang = await Parser.Language.load('tree-sitter-haskell.wasm');
|
||||
parser.setLanguage(Lang);
|
||||
return parser;
|
||||
async function update() {
|
||||
let result, tree;
|
||||
try {
|
||||
tree = await parse(textarea.value);
|
||||
} catch (err) {
|
||||
console.warn('parse error');
|
||||
console.error(err);
|
||||
}
|
||||
console.log('parsed tree');
|
||||
console.log(tree.rootNode.toString());
|
||||
try {
|
||||
renderGraph(tree, graphContainer);
|
||||
} catch (err) {
|
||||
console.warn('could not render graph');
|
||||
console.error(err);
|
||||
}
|
||||
try {
|
||||
let patterns = {};
|
||||
window.p = (name, pattern) => {
|
||||
patterns[name] = pattern;
|
||||
};
|
||||
window.d1 = (pat) => window.p(1, pat);
|
||||
window.d2 = (pat) => window.p(2, pat);
|
||||
window.d3 = (pat) => window.p(3, pat);
|
||||
window.rot = late;
|
||||
result = run(tree.rootNode, window, ops);
|
||||
if (Object.values(patterns).length) {
|
||||
stack(...Object.values(patterns)).play();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('eval error');
|
||||
console.error(err);
|
||||
result = 'ERROR: ' + err.message;
|
||||
}
|
||||
document.getElementById('result').innerHTML = 'Result: ' + result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,42 @@
|
|||
{
|
||||
"name": "haskell",
|
||||
"name": "@strudel/tidal",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"postinstall": "cp node_modules/web-tree-sitter/tree-sitter.wasm public",
|
||||
"parse": "tree-sitter parse -D ./test.hs"
|
||||
"postinstall": "cp node_modules/web-tree-sitter/tree-sitter.wasm public && npm run build:parser",
|
||||
"build:parser": "tree-sitter build-wasm node_modules/tree-sitter-haskell && mv tree-sitter-haskell.wasm public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tree-sitter": "^0.20.6",
|
||||
"tree-sitter-cli": "^0.20.8",
|
||||
"tree-sitter-javascript": "^0.20.1",
|
||||
"vite": "^5.0.8"
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@hpcc-js/wasm": "^2.15.3",
|
||||
"jgf-dot": "^1.1.1",
|
||||
"web-tree-sitter": "^0.20.8"
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"tree-sitter-haskell": "^0.21.0",
|
||||
"@strudel/web": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tree-sitter": "^0.21.1",
|
||||
"tree-sitter-cli": "^0.20.8",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
packages/haskell/parser.mjs
Normal file
22
packages/haskell/parser.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import Parser from 'web-tree-sitter';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
|
||||
async function loadParser() {
|
||||
await Parser.init({
|
||||
locateFile(scriptName, scriptDirectory) {
|
||||
return `${base}${scriptName}`;
|
||||
},
|
||||
});
|
||||
const parser = new Parser();
|
||||
const Lang = await Parser.Language.load(`${base}tree-sitter-haskell.wasm`);
|
||||
parser.setLanguage(Lang);
|
||||
return parser;
|
||||
}
|
||||
|
||||
let parserLoaded = loadParser();
|
||||
export async function parse(code) {
|
||||
const parser = await parserLoaded;
|
||||
// for some reason, the parser doesn't like new lines..
|
||||
return parser.parse(code.replaceAll('\n\n', '~~~~').replaceAll('\n', '').replaceAll('~~~~', '\n'));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue