From c9cafa37fdb521d87d4a690d68e8092b841bf209 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 21:33:51 +0100 Subject: [PATCH] mondo: improve mondo package api + update readme --- packages/mondo/README.md | 45 +++++++++++++++---------- packages/mondo/mondo.mjs | 24 +++++--------- packages/mondo/test/mondo.test.mjs | 27 ++++++++++++++- packages/mondough/mondough.mjs | 53 ++++++++++++++++-------------- 4 files changed, 89 insertions(+), 60 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 55169046..47812e83 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -5,23 +5,32 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) +## Example Usage + ```js -import { MondoRunner } from 'uzu' - -const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') -``` - -the above code will create the following call structure: - -```lisp -(speed - (s - (seq bd - (* hh 2) - (crush cp 4) - (cat mt ht lt) - ) - ) .8 -) +import { MondoRunner } from 'mondo' +// define our library of functions and variables +let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, +}; +// this function will evaluate nodes in the syntax tree +function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); +} +const runner = new MondoRunner(evaluator); +const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 ``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d031af4e..673841cd 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { + constructor(evaluator) { this.parser = new MondoParser(); - this.lib = lib; - this.assert(!!this.lib.leaf, `no handler for leaft nodes! add "leaf" to your lib`); - this.assert(!!this.lib.call, `no handler for call nodes! add "call" to your lib`); + this.evaluator = evaluator; + this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -331,15 +330,10 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); } - return this.lib.leaf(ast, scope); + return this.evaluator(ast, scope); } - // is list - if (!ast.children.length) { - throw new Error(`empty list`); - } - - if (ast.children[0].value === 'lambda') { + if (ast.children[0]?.value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -349,10 +343,8 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - - const args = ast.children.map((arg) => this.evaluate(arg, scope)); - // we could short circuit arg[0] if its plain... - // evaluate args - return this.lib.call(args[0], args.slice(1), scope); + // evaluate all children before evaluating list + ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); + return this.evaluator(ast, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f987cbc2..338a1fec 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, expect, it } from 'vitest'; -import { MondoParser, printAst } from '../mondo.mjs'; +import { MondoParser, printAst, MondoRunner } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); @@ -128,3 +128,28 @@ describe('mondo sugar', () => { it('should desugar_lambda', () => expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); + +describe('mondo arithmetic', () => { + let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, + }; + function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); + } + const runner = new MondoRunner(evaluator); + it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); +}); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 01247e78..d84ceb0c 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,8 +43,12 @@ lib['..'] = range; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" //lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct -let runner = new MondoRunner({ - call(name, args, scope) { +function evaluator(node, scope) { + const { type } = node; + // node is list + if (type === 'list') { + const { children } = node; + const [name, ...args] = children; // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; if (typeof first?.value !== 'function') { @@ -58,30 +62,29 @@ let runner = new MondoRunner({ return fn(...args); }) .innerJoin(); - }, - leaf(token, scope) { - let { value, type } = token; - // local scope - if (type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const variable = lib[value] ?? strudelScope[value]; - let pat; - if (type === 'plain' && typeof variable !== 'undefined') { - // problem: collisions when we want a string that happens to also be a variable name - // example: "s sine" -> sine is also a variable - pat = reify(variable); - } else { - pat = reify(value); - } + } + // node is leaf + let { value } = node; + if (type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const variable = lib[value] ?? strudelScope[value]; + let pat; + if (type === 'plain' && typeof variable !== 'undefined') { + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable + pat = reify(variable); + } else { + pat = reify(value); + } + if (node.loc) { + pat = pat.withLoc(node.loc[0], node.loc[1]); + } + pat.foo = true; + return pat; +} - if (token.loc) { - pat = pat.withLoc(token.loc[0], token.loc[1]); - } - pat.foo = true; - return pat; - }, -}); +let runner = new MondoRunner(evaluator); export function mondo(code, offset = 0) { if (Array.isArray(code)) {