mondo highlighting
This commit is contained in:
parent
fae59a6bcd
commit
b0da353115
5 changed files with 138 additions and 38 deletions
|
|
@ -19,35 +19,46 @@ export class MondoParser {
|
||||||
pipe: /^\./,
|
pipe: /^\./,
|
||||||
stack: /^,/,
|
stack: /^,/,
|
||||||
op: /^[*/]/,
|
op: /^[*/]/,
|
||||||
plain: /^[a-zA-Z0-9-_]+/,
|
plain: /^[a-zA-Z0-9-_\^]+/,
|
||||||
};
|
};
|
||||||
constructor(config) {
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
// matches next token
|
// matches next token
|
||||||
next_token(code) {
|
next_token(code, offset = 0) {
|
||||||
for (let type in this.token_types) {
|
for (let type in this.token_types) {
|
||||||
const match = code.match(this.token_types[type]);
|
const match = code.match(this.token_types[type]);
|
||||||
if (match) {
|
if (match) {
|
||||||
return { type, value: match[0] };
|
let token = { type, value: match[0] };
|
||||||
|
if (offset !== -1) {
|
||||||
|
// add location
|
||||||
|
token.loc = [offset, offset + match[0].length];
|
||||||
|
}
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`mondo: could not match '${code}'`);
|
throw new Error(`mondo: could not match '${code}'`);
|
||||||
}
|
}
|
||||||
// takes code string, returns list of matched tokens (if valid)
|
// takes code string, returns list of matched tokens (if valid)
|
||||||
tokenize(code) {
|
tokenize(code, offset = 0) {
|
||||||
let tokens = [];
|
let tokens = [];
|
||||||
|
let locEnabled = offset !== -1;
|
||||||
|
let trim = () => {
|
||||||
|
// trim whitespace at start, update offset
|
||||||
|
offset += code.length - code.trimStart().length;
|
||||||
|
// trim start and end to not confuse parser
|
||||||
|
return code.trim();
|
||||||
|
};
|
||||||
|
code = trim();
|
||||||
while (code.length > 0) {
|
while (code.length > 0) {
|
||||||
code = code.trim();
|
code = trim();
|
||||||
const token = this.next_token(code);
|
const token = this.next_token(code, locEnabled ? offset : -1);
|
||||||
code = code.slice(token.value.length);
|
code = code.slice(token.value.length);
|
||||||
|
offset += token.value.length;
|
||||||
tokens.push(token);
|
tokens.push(token);
|
||||||
}
|
}
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
// take code, return abstract syntax tree
|
// take code, return abstract syntax tree
|
||||||
parse(code) {
|
parse(code, offset) {
|
||||||
this.tokens = this.tokenize(code);
|
this.tokens = this.tokenize(code, offset);
|
||||||
const expressions = [];
|
const expressions = [];
|
||||||
while (this.tokens.length) {
|
while (this.tokens.length) {
|
||||||
expressions.push(this.parse_expr());
|
expressions.push(this.parse_expr());
|
||||||
|
|
@ -244,6 +255,20 @@ export class MondoParser {
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
get_locations(code, offset = 0) {
|
||||||
|
let walk = (ast, locations = []) => {
|
||||||
|
if (ast.type === 'list') {
|
||||||
|
return ast.children.slice(1).forEach((child) => walk(child, locations));
|
||||||
|
}
|
||||||
|
if (ast.loc) {
|
||||||
|
locations.push(ast.loc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ast = this.parse(code, offset);
|
||||||
|
let locations = [];
|
||||||
|
walk(ast, locations);
|
||||||
|
return locations;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printAst(ast, compact = false, lvl = 0) {
|
export function printAst(ast, compact = false, lvl = 0) {
|
||||||
|
|
@ -259,10 +284,9 @@ export function printAst(ast, compact = false, lvl = 0) {
|
||||||
|
|
||||||
// lisp runner
|
// lisp runner
|
||||||
export class MondoRunner {
|
export class MondoRunner {
|
||||||
constructor(lib, config = {}) {
|
constructor(lib) {
|
||||||
this.parser = new MondoParser(config);
|
this.parser = new MondoParser();
|
||||||
this.lib = lib;
|
this.lib = lib;
|
||||||
this.config = config;
|
|
||||||
}
|
}
|
||||||
// a helper to check conditions and throw if they are not met
|
// a helper to check conditions and throw if they are not met
|
||||||
assert(condition, error) {
|
assert(condition, error) {
|
||||||
|
|
@ -270,9 +294,9 @@ export class MondoRunner {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run(code) {
|
run(code, offset = 0) {
|
||||||
const ast = this.parser.parse(code);
|
const ast = this.parser.parse(code, offset);
|
||||||
console.log(printAst(ast));
|
// console.log(printAst(ast));
|
||||||
return this.call(ast);
|
return this.call(ast);
|
||||||
}
|
}
|
||||||
call(ast) {
|
call(ast) {
|
||||||
|
|
@ -284,13 +308,13 @@ export class MondoRunner {
|
||||||
// process args
|
// process args
|
||||||
const args = ast.children.slice(1).map((arg) => {
|
const args = ast.children.slice(1).map((arg) => {
|
||||||
if (arg.type === 'string') {
|
if (arg.type === 'string') {
|
||||||
return this.lib.string(arg.value.slice(1, -1));
|
return this.lib.string(arg.value.slice(1, -1), arg);
|
||||||
}
|
}
|
||||||
if (arg.type === 'plain') {
|
if (arg.type === 'plain') {
|
||||||
return this.lib.plain(arg.value);
|
return this.lib.plain(arg.value, arg);
|
||||||
}
|
}
|
||||||
if (arg.type === 'number') {
|
if (arg.type === 'number') {
|
||||||
return this.lib.number(Number(arg.value));
|
return this.lib.number(Number(arg.value), arg);
|
||||||
}
|
}
|
||||||
return this.call(arg);
|
return this.call(arg);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,24 @@ import { describe, expect, it } from 'vitest';
|
||||||
import { MondoParser, MondoRunner, printAst } from '../mondo.mjs';
|
import { MondoParser, MondoRunner, printAst } from '../mondo.mjs';
|
||||||
|
|
||||||
const parser = new MondoParser();
|
const parser = new MondoParser();
|
||||||
const p = (code) => parser.parse(code);
|
const p = (code) => parser.parse(code, -1);
|
||||||
|
|
||||||
|
describe('mondo tokenizer', () => {
|
||||||
|
const parser = new MondoParser();
|
||||||
|
it('should tokenize with locations', () =>
|
||||||
|
expect(
|
||||||
|
parser
|
||||||
|
.tokenize('(one two three)')
|
||||||
|
.map((t) => t.value + '=' + t.loc.join('-'))
|
||||||
|
.join(' '),
|
||||||
|
).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15'));
|
||||||
|
// it('should parse with locations', () => expect(parser.parse('(one two three)')).toEqual());
|
||||||
|
it('should get locations', () =>
|
||||||
|
expect(parser.get_locations('s bd rim')).toEqual([
|
||||||
|
[2, 4],
|
||||||
|
[5, 8],
|
||||||
|
]));
|
||||||
|
});
|
||||||
describe('mondo s-expressions parser', () => {
|
describe('mondo s-expressions parser', () => {
|
||||||
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
|
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
|
||||||
it('should parse a single item', () =>
|
it('should parse a single item', () =>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core';
|
import { strudelScope, reify, fast, slow } from '@strudel/core';
|
||||||
|
import { registerLanguage } from '@strudel/transpiler';
|
||||||
import { MondoRunner } from '../mondo/mondo.mjs';
|
import { MondoRunner } from '../mondo/mondo.mjs';
|
||||||
|
|
||||||
let runner = new MondoRunner(strudelScope, { pipepost: true });
|
let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true });
|
||||||
|
|
||||||
//strudelScope.plain = reify;
|
let getLeaf = (value, token) => {
|
||||||
strudelScope.plain = (v) => {
|
const [from, to] = token.loc;
|
||||||
// console.log('plain', v);
|
return reify(value).withLoc(from, to);
|
||||||
return reify(v);
|
|
||||||
// return v;
|
|
||||||
};
|
};
|
||||||
// strudelScope.number = (n) => n;
|
|
||||||
strudelScope.number = reify;
|
strudelScope.plain = getLeaf;
|
||||||
|
strudelScope.number = getLeaf;
|
||||||
|
|
||||||
strudelScope.call = (fn, args, name) => {
|
strudelScope.call = (fn, args, name) => {
|
||||||
const [pat, ...rest] = args;
|
const [pat, ...rest] = args;
|
||||||
if (!['seq', 'cat', 'stack'].includes(name)) {
|
if (!['seq', 'cat', 'stack'].includes(name)) {
|
||||||
args = [...rest, pat];
|
args = [...rest, pat];
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('call', name, ...flipped);
|
|
||||||
|
|
||||||
return fn(...args);
|
return fn(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -30,6 +27,12 @@ export function mondo(code, offset = 0) {
|
||||||
if (Array.isArray(code)) {
|
if (Array.isArray(code)) {
|
||||||
code = code.join('');
|
code = code.join('');
|
||||||
}
|
}
|
||||||
const pat = runner.run(code, { pipepost: true });
|
const pat = runner.run(code, offset);
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tell transpiler how to get locations for mondo`` calls
|
||||||
|
registerLanguage('mondo', {
|
||||||
|
getLocations: (code, offset) => runner.parser.get_locations(code, offset),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*"
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/transpiler": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mondo": "*",
|
"mondo": "*",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ export function registerWidgetType(type) {
|
||||||
widgetMethods.push(type);
|
widgetMethods.push(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let languages = new Map();
|
||||||
|
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||||
|
// see mondough.mjs for example use
|
||||||
|
// the language will kick in when the code contains a template literal of type
|
||||||
|
// example: mondo`...` will use language of type "mondo"
|
||||||
|
// TODO: refactor tidal.mjs to use this
|
||||||
|
export function registerLanguage(type, config) {
|
||||||
|
languages.set(type, config);
|
||||||
|
}
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||||
|
|
||||||
|
|
@ -26,7 +36,19 @@ export function transpiler(input, options = {}) {
|
||||||
|
|
||||||
walk(ast, {
|
walk(ast, {
|
||||||
enter(node, parent /* , prop, index */) {
|
enter(node, parent /* , prop, index */) {
|
||||||
if (isTidalTeplateLiteral(node)) {
|
if (isLanguageLiteral(node)) {
|
||||||
|
const { name } = node.tag;
|
||||||
|
const language = languages.get(name);
|
||||||
|
const code = node.quasi.quasis[0].value.raw;
|
||||||
|
const offset = node.quasi.start + 1;
|
||||||
|
if (emitMiniLocations) {
|
||||||
|
const locs = language.getLocations(code, offset);
|
||||||
|
miniLocations = miniLocations.concat(locs);
|
||||||
|
}
|
||||||
|
this.skip();
|
||||||
|
return this.replace(languageWithLocation(name, code, offset));
|
||||||
|
}
|
||||||
|
if (isTemplateLiteral(node, 'tidal')) {
|
||||||
const raw = node.quasi.quasis[0].value.raw;
|
const raw = node.quasi.quasis[0].value.raw;
|
||||||
const offset = node.quasi.start + 1;
|
const offset = node.quasi.start + 1;
|
||||||
if (emitMiniLocations) {
|
if (emitMiniLocations) {
|
||||||
|
|
@ -219,12 +241,16 @@ function labelToP(node) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLanguageLiteral(node) {
|
||||||
|
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
||||||
|
}
|
||||||
|
|
||||||
// tidal highlighting
|
// tidal highlighting
|
||||||
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
||||||
// but maybe it's the only way
|
// but maybe it's the only way
|
||||||
|
|
||||||
function isTidalTeplateLiteral(node) {
|
function isTemplateLiteral(node, value) {
|
||||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal';
|
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||||
|
|
@ -262,3 +288,33 @@ function tidalWithLocation(value, offset) {
|
||||||
optional: false,
|
optional: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mondoWithLocation(value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'mondo',
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageWithLocation(name, value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue