From 62705da3deef4f9ceb7aaaa031ccc13d7bb3ad20 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:29:23 +0100 Subject: [PATCH 01/88] begin uzu package --- packages/uzu/README.md | 29 ++++ packages/uzu/package.json | 37 +++++ packages/uzu/test/uzu.test.mjs | 72 ++++++++ packages/uzu/uzu.mjs | 294 +++++++++++++++++++++++++++++++++ packages/uzu/vite.config.js | 19 +++ pnpm-lock.yaml | 14 +- 6 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 packages/uzu/README.md create mode 100644 packages/uzu/package.json create mode 100644 packages/uzu/test/uzu.test.mjs create mode 100644 packages/uzu/uzu.mjs create mode 100644 packages/uzu/vite.config.js diff --git a/packages/uzu/README.md b/packages/uzu/README.md new file mode 100644 index 000000000..53eef51f8 --- /dev/null +++ b/packages/uzu/README.md @@ -0,0 +1,29 @@ +# uzu + +an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: + +- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) +- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) + +```js +import { UzuRunner } from 'uzu' + +const runner = UzuRunner({ 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 +) +``` + +you can pass all available functions to *UzuRunner* as an object. diff --git a/packages/uzu/package.json b/packages/uzu/package.json new file mode 100644 index 000000000..9b59078d5 --- /dev/null +++ b/packages/uzu/package.json @@ -0,0 +1,37 @@ +{ + "name": "uzu", + "version": "1.1.0", + "description": "an uzu notation", + "main": "uzu.mjs", + "type": "module", + "publishConfig": { + "main": "dist/uzu.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/uzu/README.md", + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs new file mode 100644 index 000000000..c58eb1a86 --- /dev/null +++ b/packages/uzu/test/uzu.test.mjs @@ -0,0 +1,72 @@ +/* +uzu.test.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { describe, expect, it } from 'vitest'; +import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; + +const parser = new UzuParser(); +const p = (code) => parser.parse(code); + +describe('uzu s-expressions parser', () => { + it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); + it('should parse a single item', () => + expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] })); + it('should parse a list with 1 item', () => + expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse a list with 2 items', () => + expect(p('(a b)')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'plain', value: 'b' }, + ], + })); + it('should parse a list with 2 items', () => + expect(p('(a (b c))')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { + type: 'list', + children: [ + { type: 'plain', value: 'b' }, + { type: 'plain', value: 'c' }, + ], + }, + ], + })); + it('should parse numbers', () => + expect(p('(1 .2 1.2 10 22.3)')).toEqual({ + type: 'list', + children: [ + { type: 'number', value: '1' }, + { type: 'number', value: '.2' }, + { type: 'number', value: '1.2' }, + { type: 'number', value: '10' }, + { type: 'number', value: '22.3' }, + ], + })); +}); + +let desguar = (a) => { + return printAst(parser.parse(a), true); +}; + +describe('uzu sugar', () => { + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); + it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar README example', () => + expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + )); +}); diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs new file mode 100644 index 000000000..ead760be0 --- /dev/null +++ b/packages/uzu/uzu.mjs @@ -0,0 +1,294 @@ +/* +uzu.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +// evolved from https://garten.salat.dev/lisp/parser.html +export class UzuParser { + // these are the tokens we expect + token_types = { + string: /^\"(.*?)\"/, + open_list: /^\(/, + close_list: /^\)/, + open_cat: /^\/, + open_seq: /^\[/, + close_seq: /^\]/, + number: /^[0-9]*\.?[0-9]+/, // before pipe! + pipe: /^\./, + stack: /^\,/, + op: /^[\*\/]/, + plain: /^[a-zA-Z0-9\-]+/, + }; + // matches next token + next_token(code) { + for (let type in this.token_types) { + const match = code.match(this.token_types[type]); + if (match) { + return { type, value: match[0] }; + } + } + throw new Error(`zilp: could not match '${code}'`); + } + // takes code string, returns list of matched tokens (if valid) + tokenize(code) { + let tokens = []; + while (code.length > 0) { + code = code.trim(); + const token = this.next_token(code); + code = code.slice(token.value.length); + tokens.push(token); + } + return tokens; + } + // take code, return abstract syntax tree + parse(code) { + this.tokens = this.tokenize(code); + const expressions = []; + while (this.tokens.length) { + expressions.push(this.parse_expr()); + } + if (expressions.length === 0) { + // empty case + return { type: 'list', children: [] }; + } + // do we have multiple top level expressions or a single non list? + if (expressions.length > 1 || expressions[0].type !== 'list') { + return { + type: 'list', + children: this.desugar_children(expressions), + }; + } + // we have a single list + return expressions[0]; + } + // parses any valid expression + parse_expr() { + if (!this.tokens[0]) { + throw new Error(`unexpected end of file`); + } + let next = this.tokens[0]?.type; + if (next === 'open_list') { + return this.parse_list(); + } + if (next === 'open_cat') { + return this.parse_cat(); + } + if (next === 'open_seq') { + return this.parse_seq(); + } + return this.consume(next); + } + desugar_children(children) { + children = this.resolve_ops(children); + children = this.resolve_pipes(children); + return children; + } + // Token[] => Token[][] . returns empty list if type not found + split_children(children, type) { + const chunks = []; + while (true) { + let commaIndex = children.findIndex((child) => child.type === type); + if (commaIndex === -1) break; + const chunk = children.slice(0, commaIndex); + chunks.push(chunk); + children = children.slice(commaIndex + 1); + } + if (!chunks.length) { + return []; + } + chunks.push(children); + return chunks; + } + desugar_stack(children) { + let [type, ...rest] = children; + // children is expected to contain seq or cat as first item + const chunks = this.split_children(rest, 'stack'); + if (!chunks.length) { + // no stack + return children; + } + // collect args of stack function + const args = chunks.map((chunk) => { + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } else { + // chunks of multiple args are added to a subsequence of type + return { type: 'list', children: [type, ...chunk] }; + } + }); + return [{ type: 'plain', value: 'stack' }, ...args]; + } + resolve_ops(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'op'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + if (left.type === 'pipe') { + // "x !* 2" => (* 2 x) + children[opIndex] = op; + continue; + } + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } + resolve_pipes(children) { + while (true) { + let pipeIndex = children.findIndex((child) => child.type === 'pipe'); + // no pipe => we're done + if (pipeIndex === -1) break; + // pipe up front => lambda + if (pipeIndex === 0) { + // . as lambda: (.fast 2) = x=>x.fast(2) + // TODO: this doesn't work for (.fast 2 .speed 2) + // probably needs proper ast representation of lambda + children[pipeIndex] = { type: 'plain', value: '.' }; + continue; + } + const rightSide = children.slice(pipeIndex + 2); + const right = children[pipeIndex + 1]; + if (right.type === 'list') { + // apply function only to left sibling (high precedence) + // s jazz.(fast 2) => s (fast jazz 2) + const [callee, ...rest] = right.children; + const leftSide = children.slice(0, pipeIndex - 1); + const left = children[pipeIndex - 1]; + const args = [callee, left, ...rest]; + const call = { type: 'list', children: args }; + children = [...leftSide, call, ...rightSide]; + } else { + // apply function to all left siblings (low precedence) + // s jazz . fast 2 => fast (s jazz) 2 + let leftSide = children.slice(0, pipeIndex); + if (leftSide.length === 1) { + leftSide = leftSide[0]; + } else { + // wrap in (..) if multiple items on the left side + leftSide = { type: 'list', children: leftSide }; + } + children = [right, leftSide, ...rightSide]; + } + } + return children; + } + parse_pair(open_type, close_type) { + this.consume(open_type); + const children = []; + while (this.tokens[0]?.type !== close_type) { + children.push(this.parse_expr()); + } + this.consume(close_type); + return children; + } + parse_list() { + let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_children(children); + return { type: 'list', children }; + } + parse_cat() { + let children = this.parse_pair('open_cat', 'close_cat'); + children = [{ type: 'plain', value: 'cat' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'cat'); + return { type: 'list', children }; + } + parse_seq() { + let children = this.parse_pair('open_seq', 'close_seq'); + children = [{ type: 'plain', value: 'seq' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'seq'); + return { type: 'list', children }; + } + consume(type) { + // shift removes first element and returns it + const token = this.tokens.shift(); + if (token.type !== type) { + throw new Error(`expected token type ${type}, got ${token.type}`); + } + return token; + } +} + +export function printAst(ast, compact = false, lvl = 0) { + const br = compact ? '' : '\n'; + const spaces = compact ? '' : Array(lvl).fill(' ').join(''); + if (ast.type === 'list') { + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; + } + return `${ast.value}`; +} + +// lisp runner +export class UzuRunner { + constructor(lib) { + this.parser = new UzuParser(); + this.lib = lib; + } + // a helper to check conditions and throw if they are not met + assert(condition, error) { + if (!condition) { + throw new Error(error); + } + } + run(code) { + const ast = this.parser.parse(code); + return this.call(ast); + } + call(ast) { + // for a node to be callable, it needs to be a list + this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + // the first element is expected to be the function name + this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + + // process args + const args = ast.children.slice(1).map((arg) => { + if (arg.type === 'string') { + return this.lib.string(arg.value.slice(1, -1)); + } + if (arg.type === 'plain') { + return this.lib.plain(arg.value); + } + if (arg.type === 'number') { + return this.lib.number(Number(arg.value)); + } + return this.call(arg); + }); + + const name = ast.children[0].value; + if (name === '.') { + // lambda : (.fast 2) = x=>fast(2, x) + const callee = ast.children[1].value; + const innerFn = this.lib[callee]; + this.assert(innerFn, `function call: unknown function name "${callee}"`); + return (pat) => innerFn(pat, args.slice(1)); + } + + // look up function in lib + const fn = this.lib[name]; + this.assert(fn, `function call: unknown function name "${name}"`); + return fn(...args); + } +} diff --git a/packages/uzu/vite.config.js b/packages/uzu/vite.config.js new file mode 100644 index 000000000..e61a69e40 --- /dev/null +++ b/packages/uzu/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'uzu.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fd711e2..bbe64f07a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 2.2.7 '@vitest/coverage-v8': specifier: 3.0.4 - version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)) + version: 3.0.4(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -540,6 +540,15 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/uzu: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/web: dependencies: '@strudel/core': @@ -7561,7 +7570,6 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -10248,7 +10256,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 From dbe3915368e0404e56523adddaa18038ca0cac49 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:35:22 +0100 Subject: [PATCH 02/88] fix: lint errors --- packages/uzu/uzu.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index ead760be0..6a5157811 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -8,18 +8,18 @@ This program is free software: you can redistribute it and/or modify it under th export class UzuParser { // these are the tokens we expect token_types = { - string: /^\"(.*?)\"/, + string: /^"(.*?)"/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^\/, + open_cat: /^/, open_seq: /^\[/, close_seq: /^\]/, number: /^[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, - stack: /^\,/, - op: /^[\*\/]/, - plain: /^[a-zA-Z0-9\-]+/, + stack: /^,/, + op: /^[*/]/, + plain: /^[a-zA-Z0-9-]+/, }; // matches next token next_token(code) { From b71b1354c3c8dcdfe6b8c79d1a170c9d1ae3759e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 00:51:59 +0100 Subject: [PATCH 03/88] uzu: stacks now work within () + write more tests --- packages/uzu/test/uzu.test.mjs | 23 +++++++++++++++++++++-- packages/uzu/uzu.mjs | 31 ++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs index c58eb1a86..33451f0cf 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/uzu/test/uzu.test.mjs @@ -58,13 +58,32 @@ let desguar = (a) => { describe('uzu sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within , within []', () => + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + + it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); + it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); + it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); + it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index 6a5157811..e79488a59 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -86,10 +86,14 @@ export class UzuParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, type) { + split_children(children, split_type, sequence_type) { + if (sequence_type) { + // if given, the first child is ignored + children = children.slice(1); + } const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === type); + let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); chunks.push(chunk); @@ -101,13 +105,11 @@ export class UzuParser { chunks.push(children); return chunks; } - desugar_stack(children) { - let [type, ...rest] = children; + desugar_stack(children, sequence_type) { // children is expected to contain seq or cat as first item - const chunks = this.split_children(rest, 'stack'); + const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { - // no stack - return children; + return this.desugar_children(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -115,8 +117,14 @@ export class UzuParser { // chunks of one element can be added to the stack as is return chunk[0]; } else { - // chunks of multiple args are added to a subsequence of type - return { type: 'list', children: [type, ...chunk] }; + // chunks of multiple args + if (sequence_type) { + // if given, each chunk needs to be prefixed + // [a b, c d] => (stack (seq a b) (seq c d)) + chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; + } + chunk = this.desugar_children(chunk); + return { type: 'list', children: chunk }; } }); return [{ type: 'plain', value: 'stack' }, ...args]; @@ -203,21 +211,22 @@ export class UzuParser { } parse_list() { let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_stack(children); children = this.desugar_children(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'cat'); + children = this.desugar_children(children); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'seq'); + children = this.desugar_children(children); return { type: 'list', children }; } consume(type) { From e3df504423b51e29d4b7ca11199cac6641d9f7a0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 02:01:16 +0100 Subject: [PATCH 04/88] rename uzu to mondo --- packages/{uzu => mondo}/README.md | 8 ++++---- packages/{uzu/uzu.mjs => mondo/mondo.mjs} | 8 ++++---- packages/{uzu => mondo}/package.json | 10 +++++----- .../uzu.test.mjs => mondo/test/mondo.test.mjs} | 10 +++++----- packages/{uzu => mondo}/vite.config.js | 4 ++-- pnpm-lock.yaml | 18 +++++++++--------- 6 files changed, 29 insertions(+), 29 deletions(-) rename packages/{uzu => mondo}/README.md (72%) rename packages/{uzu/uzu.mjs => mondo/mondo.mjs} (98%) rename packages/{uzu => mondo}/package.json (79%) rename packages/{uzu/test/uzu.test.mjs => mondo/test/mondo.test.mjs} (95%) rename packages/{uzu => mondo}/vite.config.js (78%) diff --git a/packages/uzu/README.md b/packages/mondo/README.md similarity index 72% rename from packages/uzu/README.md rename to packages/mondo/README.md index 53eef51f8..d7ee20ac4 100644 --- a/packages/uzu/README.md +++ b/packages/mondo/README.md @@ -1,4 +1,4 @@ -# uzu +# mondo an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: @@ -6,9 +6,9 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) ```js -import { UzuRunner } from 'uzu' +import { MondoRunner } from 'uzu' -const runner = UzuRunner({ seq, cat, s, crush, speed, '*': fast }); +const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') ``` @@ -26,4 +26,4 @@ the above code will create the following call structure: ) ``` -you can pass all available functions to *UzuRunner* as an object. +you can pass all available functions to *MondoRunner* as an object. diff --git a/packages/uzu/uzu.mjs b/packages/mondo/mondo.mjs similarity index 98% rename from packages/uzu/uzu.mjs rename to packages/mondo/mondo.mjs index e79488a59..2037cce7c 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/mondo/mondo.mjs @@ -1,11 +1,11 @@ /* -uzu.mjs - +mondo.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ // evolved from https://garten.salat.dev/lisp/parser.html -export class UzuParser { +export class MondoParser { // these are the tokens we expect token_types = { string: /^"(.*?)"/, @@ -251,9 +251,9 @@ export function printAst(ast, compact = false, lvl = 0) { } // lisp runner -export class UzuRunner { +export class MondoRunner { constructor(lib) { - this.parser = new UzuParser(); + this.parser = new MondoParser(); this.lib = lib; } // a helper to check conditions and throw if they are not met diff --git a/packages/uzu/package.json b/packages/mondo/package.json similarity index 79% rename from packages/uzu/package.json rename to packages/mondo/package.json index 9b59078d5..d8a4a0bf2 100644 --- a/packages/uzu/package.json +++ b/packages/mondo/package.json @@ -1,11 +1,11 @@ { - "name": "uzu", + "name": "mondo", "version": "1.1.0", - "description": "an uzu notation", - "main": "uzu.mjs", + "description": "a language for functional composition that translates to js", + "main": "mondo.mjs", "type": "module", "publishConfig": { - "main": "dist/uzu.mjs" + "main": "dist/mondo.mjs" }, "scripts": { "test": "vitest run", @@ -29,7 +29,7 @@ "bugs": { "url": "https://github.com/tidalcycles/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/uzu/README.md", + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md", "devDependencies": { "vite": "^6.0.11", "vitest": "^3.0.4" diff --git a/packages/uzu/test/uzu.test.mjs b/packages/mondo/test/mondo.test.mjs similarity index 95% rename from packages/uzu/test/uzu.test.mjs rename to packages/mondo/test/mondo.test.mjs index 33451f0cf..6ed2df176 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -1,16 +1,16 @@ /* -uzu.test.mjs - +mondo.test.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { describe, expect, it } from 'vitest'; -import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; +import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; -const parser = new UzuParser(); +const parser = new MondoParser(); const p = (code) => parser.parse(code); -describe('uzu s-expressions parser', () => { +describe('mondo s-expressions parser', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); @@ -56,7 +56,7 @@ let desguar = (a) => { return printAst(parser.parse(a), true); }; -describe('uzu sugar', () => { +describe('mondo sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); diff --git a/packages/uzu/vite.config.js b/packages/mondo/vite.config.js similarity index 78% rename from packages/uzu/vite.config.js rename to packages/mondo/vite.config.js index e61a69e40..19e53d7f3 100644 --- a/packages/uzu/vite.config.js +++ b/packages/mondo/vite.config.js @@ -7,9 +7,9 @@ export default defineConfig({ plugins: [], build: { lib: { - entry: resolve(__dirname, 'uzu.mjs'), + entry: resolve(__dirname, 'mondo.mjs'), formats: ['es'], - fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + fileName: (ext) => ({ es: 'mondo.mjs' })[ext], }, rollupOptions: { // external: [...Object.keys(dependencies)], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbe64f07a..264b7440b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,6 +344,15 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mondo: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: dependencies: '@strudel/core': @@ -540,15 +549,6 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - packages/uzu: - devDependencies: - vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - vitest: - specifier: ^3.0.4 - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - packages/web: dependencies: '@strudel/core': From e782dc09dd3c31fb497d1f66a50bfa59924af8bb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:47:32 +0100 Subject: [PATCH 05/88] export strudelScope for mondo to use --- packages/core/evaluate.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..7d57e435e 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see . */ +export const strudelScope = {}; + export const evalScope = async (...args) => { const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); @@ -18,6 +20,7 @@ export const evalScope = async (...args) => { modules.forEach((module) => { Object.entries(module).forEach(([name, value]) => { globalThis[name] = value; + strudelScope[name] = value; }); }); return modules; From 11196fb1e6601241039f2dfd4dca6c4434ad7a1a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:48:38 +0100 Subject: [PATCH 06/88] breaking: refactor controls - multiple args wont be interpreted as sequence anymore - you can now pass value, pat to set value to pat --- packages/core/controls.mjs | 26 ++++++++++++++++------- packages/core/signal.mjs | 8 +++---- packages/core/test/pattern.test.mjs | 2 +- packages/tonal/test/tonal.test.mjs | 14 ++++++------ test/__snapshots__/examples.test.mjs.snap | 16 ++++---------- website/src/repl/tunes.mjs | 12 +++++------ 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e703..dbf1255e8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, register, sequence } from './pattern.mjs'; +import { Pattern, register, reify } from './pattern.mjs'; export function createParam(names) { let isMulti = Array.isArray(names); names = !isMulti ? [names] : names; const name = names[0]; + // todo: make this less confusing const withVal = (xs) => { let bag; // check if we have an object with an unnamed control (.value) @@ -35,25 +36,34 @@ export function createParam(names) { } }; - const func = (...pats) => sequence(...pats).withValue(withVal); - - const setter = function (...pats) { - if (!pats.length) { - return this.fmap(withVal); + // todo: make this less confusing + const func = function (value, pat) { + if (!pat) { + return reify(value).withValue(withVal); + } + if (typeof value === 'undefined') { + return pat.fmap(withVal); } - return this.set(func(...pats)); + return pat.set(reify(value).withValue(withVal)); + }; + Pattern.prototype[name] = function (value) { + return func(value, this); }; - Pattern.prototype[name] = setter; return func; } // maps control alias names to the "main" control name const controlAlias = new Map(); +export function isControlName(name) { + return controlAlias.has(name); +} + export function registerControl(names, ...aliases) { const name = Array.isArray(names) ? names[0] : names; let bag = {}; bag[name] = createParam(names); + controlAlias.set(name, name); aliases.forEach((alias) => { bag[alias] = bag[name]; controlAlias.set(alias, name); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..aa8dd9a6b 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -279,26 +279,26 @@ const _rearrangeWith = (ipat, n, pat) => { }; /** - * @name shuffle * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. + * @name shuffle * @example * note("c d e f").sound("piano").shuffle(4) * @example - * note("c d e f".shuffle(4), "g").sound("piano") + * seq("c d e f".shuffle(4), "g").note().sound("piano") */ export const shuffle = register('shuffle', (n, pat) => { return _rearrangeWith(randrun(n), n, pat); }); /** - * @name scramble * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. + * @name scramble * @example * note("c d e f").sound("piano").scramble(4) * @example - * note("c d e f".scramble(4), "g").sound("piano") + * seq("c d e f".scramble(4), "g").note().sound("piano") */ export const scramble = register('scramble', (n, pat) => { return _rearrangeWith(_irand(n)._segment(n), n, pat); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08611032c..ee6ce197f 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1001,7 +1001,7 @@ describe('Pattern', () => { }); describe('hurry', () => { it('Can speed up patterns and sounds', () => { - sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2)); + sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2)); }); }); /*describe('composable functions', () => { diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..3a5d920e5 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -25,28 +25,28 @@ describe('tonal', () => { }); it('scale with n values', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with colon', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C:major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with mininotation colon', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale(mini('C:major')) .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('transposes note numbers with interval numbers', () => { expect( - note(40, 40, 40) + note(seq(40, 40, 40)) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual([40, 41, 42]); @@ -54,7 +54,7 @@ describe('tonal', () => { }); it('transposes note numbers with interval strings', () => { expect( - note(40, 40, 40) + note(seq(40, 40, 40)) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual([40, 42, 43]); @@ -62,7 +62,7 @@ describe('tonal', () => { }); it('transposes note strings with interval numbers', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'Db', 'D']); @@ -70,7 +70,7 @@ describe('tonal', () => { }); it('transposes note strings with interval strings', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'D', 'Eb']); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 96a11d49b..93ca7ff68 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7401,9 +7401,7 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7424,9 +7422,7 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", @@ -7835,9 +7831,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 0 1`] = ` +exports[`runs examples > example "shuffle" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7858,9 +7852,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 1 1`] = ` +exports[`runs examples > example "shuffle" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index cffe0c0eb..4e2a7617f 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -69,32 +69,32 @@ stack( export const giantSteps = `// John Coltrane - Giant Steps -let melody = note( +let melody = seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 F#5]", -) +).note() stack( // melody melody.color('#F8E71C'), // chords - chord( + seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]" - ).dict('lefthand') + ).chord().dict('lefthand') .anchor(melody).mode('duck') .voicing().color('#7ED321'), // bass - note( + seq( "[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]" - ).color('#00B8D4') + ).note().color('#00B8D4') ).slow(20) .pianoroll({fold:1})`; From fae59a6bcd1624f951737b1aa2f145879069f2c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:49:18 +0100 Subject: [PATCH 07/88] feat: add mondough package to run strudel via mondo --- packages/mondo/mondo.mjs | 22 +++++-- packages/mondough/README.md | 3 + packages/mondough/mondough.mjs | 35 +++++++++++ packages/mondough/package.json | 42 +++++++++++++ packages/mondough/vite.config.js | 19 ++++++ pnpm-lock.yaml | 103 +++++++++++++++++++++++++++++++ website/package.json | 1 + website/src/repl/util.mjs | 1 + 8 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/mondough/README.md create mode 100644 packages/mondough/mondough.mjs create mode 100644 packages/mondough/package.json create mode 100644 packages/mondough/vite.config.js diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2037cce7c..907951db2 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,11 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-]+/, + plain: /^[a-zA-Z0-9-_]+/, }; + constructor(config) { + this.config = config; + } // matches next token next_token(code) { for (let type in this.token_types) { @@ -29,7 +32,7 @@ export class MondoParser { return { type, value: match[0] }; } } - throw new Error(`zilp: could not match '${code}'`); + throw new Error(`mondo: could not match '${code}'`); } // takes code string, returns list of matched tokens (if valid) tokenize(code) { @@ -182,7 +185,7 @@ export class MondoParser { const [callee, ...rest] = right.children; const leftSide = children.slice(0, pipeIndex - 1); const left = children[pipeIndex - 1]; - const args = [callee, left, ...rest]; + let args = [callee, left, ...rest]; const call = { type: 'list', children: args }; children = [...leftSide, call, ...rightSide]; } else { @@ -200,6 +203,10 @@ export class MondoParser { } return children; } + flip_call(children) { + let [name, first, ...rest] = children; + return [name, ...rest, first]; + } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -252,9 +259,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { - this.parser = new MondoParser(); + constructor(lib, config = {}) { + this.parser = new MondoParser(config); this.lib = lib; + this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -264,6 +272,7 @@ export class MondoRunner { } run(code) { const ast = this.parser.parse(code); + console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -298,6 +307,9 @@ export class MondoRunner { // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); + if (this.lib.call) { + return this.lib.call(fn, args, name); + } return fn(...args); } } diff --git a/packages/mondough/README.md b/packages/mondough/README.md new file mode 100644 index 000000000..3ae758912 --- /dev/null +++ b/packages/mondough/README.md @@ -0,0 +1,3 @@ +# @strudel/mondough + +connects mondo to strudel. diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs new file mode 100644 index 000000000..1d29749f9 --- /dev/null +++ b/packages/mondough/mondough.mjs @@ -0,0 +1,35 @@ +import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core'; +import { MondoRunner } from '../mondo/mondo.mjs'; + +let runner = new MondoRunner(strudelScope, { pipepost: true }); + +//strudelScope.plain = reify; +strudelScope.plain = (v) => { + // console.log('plain', v); + return reify(v); + // return v; +}; +// strudelScope.number = (n) => n; +strudelScope.number = reify; + +strudelScope.call = (fn, args, name) => { + const [pat, ...rest] = args; + if (!['seq', 'cat', 'stack'].includes(name)) { + args = [...rest, pat]; + } + + // console.log('call', name, ...flipped); + + return fn(...args); +}; + +strudelScope['*'] = fast; +strudelScope['/'] = slow; + +export function mondo(code, offset = 0) { + if (Array.isArray(code)) { + code = code.join(''); + } + const pat = runner.run(code, { pipepost: true }); + return pat; +} diff --git a/packages/mondough/package.json b/packages/mondough/package.json new file mode 100644 index 000000000..0754a697f --- /dev/null +++ b/packages/mondough/package.json @@ -0,0 +1,42 @@ +{ + "name": "@strudel/mondough", + "version": "1.1.0", + "description": "mondo notation for strudel", + "main": "mondough.mjs", + "type": "module", + "publishConfig": { + "main": "dist/mondough.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "mondo": "*", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js new file mode 100644 index 000000000..c46972e96 --- /dev/null +++ b/packages/mondough/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'mondough.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'mondough.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 264b7440b..f5b63c56f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,22 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mondough: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + mondo: + specifier: '*' + version: 0.4.4 + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: dependencies: '@strudel/core': @@ -674,6 +690,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/mondough': + specifier: workspace:* + version: link:../packages/mondough '@strudel/motion': specifier: workspace:* version: link:../packages/motion @@ -2963,6 +2982,10 @@ packages: resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==} engines: {node: '>= 14.0.0'} + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3091,6 +3114,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -3325,6 +3351,9 @@ packages: claviature@0.1.0: resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + cldr-plurals@1.0.0: + resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4366,6 +4395,9 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -4427,6 +4459,11 @@ packages: h3@1.14.0: resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==} + handlebars@1.1.2: + resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==} + engines: {node: '>=0.4.7'} + hasBin: true + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -5574,6 +5611,10 @@ packages: engines: {node: '>=18'} hasBin: true + mondo@0.4.4: + resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -5815,6 +5856,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + optimist@0.3.7: + resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6713,6 +6757,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -7143,6 +7191,11 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@2.3.6: + resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==} + engines: {node: '>=0.4.0'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -7161,6 +7214,9 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + underscore@1.5.2: + resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7543,6 +7599,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -10426,6 +10486,9 @@ snapshots: '@algolia/requester-fetch': 5.20.0 '@algolia/requester-node-http': 5.20.0 + amdefine@1.0.1: + optional: true + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -10635,6 +10698,9 @@ snapshots: async-function@1.0.0: {} + async@0.2.10: + optional: true + async@3.2.6: {} asynckit@0.4.0: {} @@ -10892,6 +10958,8 @@ snapshots: claviature@0.1.0: {} + cldr-plurals@1.0.0: {} + clean-stack@2.2.0: {} cli-boxes@3.0.0: {} @@ -12034,6 +12102,8 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globalize@0.1.1: {} + globals@11.12.0: {} globals@14.0.0: {} @@ -12100,6 +12170,12 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 + handlebars@1.1.2: + dependencies: + optimist: 0.3.7 + optionalDependencies: + uglify-js: 2.3.6 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -13683,6 +13759,13 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + mondo@0.4.4: + dependencies: + cldr-plurals: 1.0.0 + globalize: 0.1.1 + handlebars: 1.1.2 + underscore: 1.5.2 + mrmime@2.0.0: {} ms@2.0.0: {} @@ -13975,6 +14058,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optimist@0.3.7: + dependencies: + wordwrap: 0.0.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15082,6 +15169,11 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + optional: true + source-map@0.5.7: {} source-map@0.6.1: {} @@ -15549,6 +15641,13 @@ snapshots: ufo@1.5.4: {} + uglify-js@2.3.6: + dependencies: + async: 0.2.10 + optimist: 0.3.7 + source-map: 0.1.43 + optional: true + uglify-js@3.19.3: optional: true @@ -15565,6 +15664,8 @@ snapshots: underscore@1.13.7: {} + underscore@1.5.2: {} + undici-types@6.20.0: {} unenv@1.10.0: @@ -15945,6 +16046,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@0.0.3: {} + wordwrap@1.0.0: {} workbox-background-sync@7.0.0: diff --git a/website/package.json b/website/package.json index 257ff6430..e42493520 100644 --- a/website/package.json +++ b/website/package.json @@ -42,6 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", + "@strudel/mondough": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..cb5e6f36e 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,6 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), + import('@strudel/mondough'), ]; if (isTauri()) { modules = modules.concat([ From b0da353115fe1dcb96142e74710947ca008706e7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:05 +0100 Subject: [PATCH 08/88] mondo highlighting --- packages/mondo/mondo.mjs | 64 ++++++++++++++++++++---------- packages/mondo/test/mondo.test.mjs | 18 ++++++++- packages/mondough/mondough.mjs | 29 ++++++++------ packages/mondough/package.json | 3 +- packages/transpiler/transpiler.mjs | 62 +++++++++++++++++++++++++++-- 5 files changed, 138 insertions(+), 38 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 907951db2..be5e819fe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,35 +19,46 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_]+/, + plain: /^[a-zA-Z0-9-_\^]+/, }; - constructor(config) { - this.config = config; - } // matches next token - next_token(code) { + next_token(code, offset = 0) { for (let type in this.token_types) { const match = code.match(this.token_types[type]); 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}'`); } // takes code string, returns list of matched tokens (if valid) - tokenize(code) { + tokenize(code, offset = 0) { 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) { - code = code.trim(); - const token = this.next_token(code); + code = trim(); + const token = this.next_token(code, locEnabled ? offset : -1); code = code.slice(token.value.length); + offset += token.value.length; tokens.push(token); } return tokens; } // take code, return abstract syntax tree - parse(code) { - this.tokens = this.tokenize(code); + parse(code, offset) { + this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { expressions.push(this.parse_expr()); @@ -244,6 +255,20 @@ export class MondoParser { } 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) { @@ -259,10 +284,9 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib, config = {}) { - this.parser = new MondoParser(config); + constructor(lib) { + this.parser = new MondoParser(); this.lib = lib; - this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -270,9 +294,9 @@ export class MondoRunner { throw new Error(error); } } - run(code) { - const ast = this.parser.parse(code); - console.log(printAst(ast)); + run(code, offset = 0) { + const ast = this.parser.parse(code, offset); + // console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -284,13 +308,13 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { 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') { - return this.lib.plain(arg.value); + return this.lib.plain(arg.value, arg); } if (arg.type === 'number') { - return this.lib.number(Number(arg.value)); + return this.lib.number(Number(arg.value), arg); } return this.call(arg); }); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6ed2df176..40595004d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -8,8 +8,24 @@ import { describe, expect, it } from 'vitest'; import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; 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', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 1d29749f9..03437be64 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -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'; -let runner = new MondoRunner(strudelScope, { pipepost: true }); +let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); -//strudelScope.plain = reify; -strudelScope.plain = (v) => { - // console.log('plain', v); - return reify(v); - // return v; +let getLeaf = (value, token) => { + const [from, to] = token.loc; + return reify(value).withLoc(from, to); }; -// strudelScope.number = (n) => n; -strudelScope.number = reify; + +strudelScope.plain = getLeaf; +strudelScope.number = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { args = [...rest, pat]; } - - // console.log('call', name, ...flipped); - return fn(...args); }; @@ -30,6 +27,12 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, { pipepost: true }); + const pat = runner.run(code, offset); return pat; } + +// tell transpiler how to get locations for mondo`` calls +registerLanguage('mondo', { + getLocations: (code, offset) => runner.parser.get_locations(code, offset), +}); + diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 0754a697f..f444c5560 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -32,7 +32,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel/core": "workspace:*" + "@strudel/core": "workspace:*", + "@strudel/transpiler": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..5eeecd62b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -8,6 +8,16 @@ export function registerWidgetType(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 = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -26,7 +36,19 @@ export function transpiler(input, options = {}) { walk(ast, { 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 offset = node.quasi.start + 1; if (emitMiniLocations) { @@ -219,12 +241,16 @@ function labelToP(node) { }; } +function isLanguageLiteral(node) { + return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name); +} + // tidal highlighting // this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) // but maybe it's the only way -function isTidalTeplateLiteral(node) { - return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal'; +function isTemplateLiteral(node, value) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === value; } function collectHaskellMiniLocations(haskellCode, offset) { @@ -262,3 +288,33 @@ function tidalWithLocation(value, offset) { 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, + }; +} From 95526ac99c7db610b7246e4f48f75895f490c63b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:28 +0100 Subject: [PATCH 09/88] fix: formatting --- packages/mondough/mondough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 03437be64..2388a9353 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -35,4 +35,3 @@ export function mondo(code, offset = 0) { registerLanguage('mondo', { getLocations: (code, offset) => runner.parser.get_locations(code, offset), }); - From ea61627963e28ac15714fcba1d278fffb1010a4b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:05:04 +0100 Subject: [PATCH 10/88] fix: lint error --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be5e819fe..da8c31ecd 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_\^]+/, + plain: /^[a-zA-Z0-9-_^]+/, }; // matches next token next_token(code, offset = 0) { From 902012759ae16d9cd4ff29243751c767b63724d9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:24:22 +0100 Subject: [PATCH 11/88] fix:support negative numbers --- packages/mondo/mondo.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da8c31ecd..5ca3a0f81 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -15,7 +15,7 @@ export class MondoParser { close_cat: /^>/, open_seq: /^\[/, close_seq: /^\]/, - number: /^[0-9]*\.?[0-9]+/, // before pipe! + number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^,/, op: /^[*/]/, @@ -299,6 +299,13 @@ export class MondoRunner { // console.log(printAst(ast)); return this.call(ast); } + // todo: always use lib.call? + libcall(fn, args, name) { + if (this.lib.call) { + return this.lib.call(fn, args, name); + } + return fn(...args); + } call(ast) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); @@ -325,15 +332,12 @@ export class MondoRunner { const callee = ast.children[1].value; const innerFn = this.lib[callee]; this.assert(innerFn, `function call: unknown function name "${callee}"`); - return (pat) => innerFn(pat, args.slice(1)); + return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); + return this.libcall(fn, args, name); } } From 77ade0758e95c28383bde99b145fdf6d399688db Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:07:03 +0100 Subject: [PATCH 12/88] mondo: slightly improve error handling --- packages/mondo/mondo.mjs | 20 ++++++++++++++------ packages/mondough/mondough.mjs | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5ca3a0f81..829f34278 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,19 @@ export class MondoRunner { } return fn(...args); } + errorhead(ast) { + return `[mondo ${ast.loc?.join(':') || ''}]`; + } call(ast) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name - this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + const first = ast.children[0]; + const name = first.value; + this.assert( + first?.type === 'plain', + `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + ); // process args const args = ast.children.slice(1).map((arg) => { @@ -326,18 +334,18 @@ export class MondoRunner { return this.call(arg); }); - const name = ast.children[0].value; if (name === '.') { // lambda : (.fast 2) = x=>fast(2, x) - const callee = ast.children[1].value; + const second = ast.children[1]; + const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `function call: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); return this.libcall(fn, args, name); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 2388a9353..87089de73 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ let getLeaf = (value, token) => { strudelScope.plain = getLeaf; strudelScope.number = getLeaf; +strudelScope.string = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; From 9b8761bc454ac36767e2684542c2dce2aa922f4d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:31:40 +0100 Subject: [PATCH 13/88] fix: top-level functions arp/arpWith --- packages/core/pattern.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c1e95211d..ebd5115cf 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -904,12 +904,13 @@ Pattern.prototype.collect = function () { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) * */ -Pattern.prototype.arpWith = function (func) { - return this.collect() +export const arpWith = register('arpWith', (func, pat) => { + return pat + .collect() .fmap((v) => reify(func(v))) .innerJoin() .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value))); -}; +}); /** * Selects indices in in stacked notes. @@ -917,9 +918,11 @@ Pattern.prototype.arpWith = function (func) { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]") * */ -Pattern.prototype.arp = function (pat) { - return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); -}; +export const arp = register( + 'arp', + (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])), + false, +); /* * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are From 3c3832dbcecf3c94d81b4bb1a408c95f547741ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:08:31 +0100 Subject: [PATCH 14/88] superdough: native support for rest symbols - ~ in s --- packages/superdough/superdough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..729f4316d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -481,6 +481,9 @@ export const superdough = async (value, t, hapDuration) => { const onended = () => { toDisconnect.forEach((n) => n?.disconnect()); }; + if (['-', '~'].includes(s)) { + return; + } if (bank && s) { s = `${bank}_${s}`; value.s = s; From 2dd445c1024c22d071d49649fa7b22d0d312dbea Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:23 +0100 Subject: [PATCH 15/88] mondo: support variables --- packages/mondough/mondough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 87089de73..002a7ae5a 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -6,6 +6,9 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; + if (strudelScope[value]) { + return strudelScope[value].withLoc(from, to); + } return reify(value).withLoc(from, to); }; From 118b619b740535bb06aff9a9b0df54b57572a17c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:52 +0100 Subject: [PATCH 16/88] mondo: support - ~ in plain type + simplify errors --- packages/mondo/mondo.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 829f34278..cb088fa9f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_^]+/, + plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token next_token(code, offset = 0) { @@ -317,7 +317,7 @@ export class MondoRunner { const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); // process args @@ -339,13 +339,13 @@ export class MondoRunner { const second = ast.children[1]; const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); return this.libcall(fn, args, name); } } From 40f64891118acf33f3dabe965c336c12bfb8794e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:30:41 +0100 Subject: [PATCH 17/88] reify variables --- packages/mondough/mondough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 002a7ae5a..21250ad20 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -7,7 +7,7 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; if (strudelScope[value]) { - return strudelScope[value].withLoc(from, to); + return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; From 7db52b3dc26105e7f79db0eeabebab7926f8d318 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 18 Mar 2025 22:33:14 +0100 Subject: [PATCH 18/88] mondo improvements: - add second string type - add $ as alias for stack - simplify leaf handling --- packages/mondo/mondo.mjs | 38 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 15 ++++++------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cb088fa9f..68ce390d4 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,7 +8,8 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { - string: /^"(.*?)"/, + quotes_double: /^"(.*?)"/, + quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, open_cat: /^ 1 || expressions[0].type !== 'list') { return { type: 'list', - children: this.desugar_children(expressions), + children: this.desugar(expressions), }; } // we have a single list @@ -110,7 +111,7 @@ export class MondoParser { let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); - chunks.push(chunk); + chunk.length && chunks.push(chunk); children = children.slice(commaIndex + 1); } if (!chunks.length) { @@ -227,24 +228,26 @@ export class MondoParser { this.consume(close_type); return children; } + desugar(children, type) { + // not really needed but more readable and might be extended in the future + children = this.desugar_stack(children, type); + return children; + } parse_list() { let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar_stack(children); - children = this.desugar_children(children); + children = this.desugar(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_stack(children, 'cat'); - children = this.desugar_children(children); + children = this.desugar(children, 'cat'); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_stack(children, 'seq'); - children = this.desugar_children(children); + children = this.desugar(children, 'seq'); return { type: 'list', children }; } consume(type) { @@ -322,16 +325,19 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'string') { - return this.lib.string(arg.value.slice(1, -1), arg); + if (arg.type === 'list') { + return this.call(arg); } - if (arg.type === 'plain') { - return this.lib.plain(arg.value, arg); + if (!this.lib.leaf) { + throw new Error(`no handler for leaft nodes! add leaf to your lib`); } + if (arg.type === 'number') { - return this.lib.number(Number(arg.value), arg); + arg.value = Number(arg.value); + } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { + arg.value = arg.value.slice(1, -1); } - return this.call(arg); + return this.lib.leaf(arg); }); if (name === '.') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 40595004d..a88041821 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, MondoRunner, printAst } from '../mondo.mjs'; +import { MondoParser, printAst } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 21250ad20..52b169cae 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,21 +1,20 @@ -import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; +import { strudelScope, reify, fast, slow } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); +let runner = new MondoRunner(strudelScope); -let getLeaf = (value, token) => { +strudelScope.leaf = (token) => { + let { value } = token; const [from, to] = token.loc; - if (strudelScope[value]) { + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; -strudelScope.plain = getLeaf; -strudelScope.number = getLeaf; -strudelScope.string = getLeaf; - strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { From 129fd7d6b0b7639c7e504d2e04e6daac61803c72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:13:06 +0100 Subject: [PATCH 19/88] add notes --- packages/mondo/README.md | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d7ee20ac4..a6a59dd94 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,3 +27,138 @@ the above code will create the following call structure: ``` you can pass all available functions to *MondoRunner* as an object. + +## snippets / thoughts + +### variants of add + +```plaintext +n[0 1 2].add(<0 -4>.n).scale"C minor" +``` + +```js +n("0 1 2").add("<0 -4>".n()).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].add(n<0 -4>).scale"C minor" +``` + +```js +n("0 1 2").add(n("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].(add<0 -4>).scale"C minor" +``` + +```js +n("0 1 2".add("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].scale"C minor" +.sometimes (12.note.add) +``` + +```js +n("0 1 2").scale("C:minor") +.sometimes(add(note("12"))) +``` + +--- + +```plaintext +note g2*8.dec /2.(range .1 .4) +``` + +```js +note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +``` + +--- + +```plaintext +n <0 1 2 3 4>*4 .scale"C minor" .jux +``` + +```js +n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) +``` + +--- + +mondo` +sound [bd sd.(every 3 (.fast 4))].jux +` +// og "Alternate Timelines for TidalCycles" example: +// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] + +### things mondo cant do + +how to write lists? + +```js +arrange( + [4, "(3,8)"], + [2, "(5,8)"] +).note() +``` + +how to write objects: + +```js +samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') +``` + +how to access array indices? + +```js +note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) +``` + +s hh .struct(binaryN 55532 16) + +comments + +variables: note g2*8.dec sine + +sine.(range 0 4)/2 doesnt work +sine/2.(range 0 4) works + +n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole + +### reference + +- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] +- bank: s [bd sd [- bd] sd].bank TR909 +- beat: s sd .beat [4,12] 16 +- binary: s hh .struct (binary 5) +- binaryN: s hh .struct(binaryN 55532 16) => is wrong +- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] +- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 + +### dot is a bit ambiguous + +```plaintext +n[0 1 2].scale"C minor".ad.1 +``` + +decimal vs pipe + +### less ambiguity with [] and "" + +in js, s("hh cp") implcitily does [hh cp] +in mondo, s[hh cp] always shows the type of bracket used + +### todo + +- lists: C:minor +- spread: [0 .. 2] +- replicate: ! From 88ca54461ee69783073a363e59604206547a3cb8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:15:53 +0100 Subject: [PATCH 20/88] rename @strudel/mondough to @strudel/mondo --- packages/mondough/package.json | 2 +- pnpm-lock.yaml | 5 ++++- website/package.json | 2 +- website/src/repl/util.mjs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mondough/package.json b/packages/mondough/package.json index f444c5560..eae22519e 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,5 +1,5 @@ { - "name": "@strudel/mondough", + "name": "@strudel/mondo", "version": "1.1.0", "description": "mondo notation for strudel", "main": "mondough.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5b63c56f..7666d3fc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -358,6 +358,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler devDependencies: mondo: specifier: '*' @@ -690,7 +693,7 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini - '@strudel/mondough': + '@strudel/mondo': specifier: workspace:* version: link:../packages/mondough '@strudel/motion': diff --git a/website/package.json b/website/package.json index e42493520..069a26c73 100644 --- a/website/package.json +++ b/website/package.json @@ -42,7 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", - "@strudel/mondough": "workspace:*", + "@strudel/mondo": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index cb5e6f36e..fb9df4b90 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,7 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), - import('@strudel/mondough'), + import('@strudel/mondo'), ]; if (isTauri()) { modules = modules.concat([ From 642ddcdb59aa2df3202915936fab1f562734e9ce Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 17:03:17 +0100 Subject: [PATCH 21/88] support : in mondo --- packages/mondo/mondo.mjs | 31 ++++++++++++++++++++++++++++++ packages/mondo/test/mondo.test.mjs | 2 ++ packages/mondough/mondough.mjs | 5 ++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 68ce390d4..4402aa525 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,6 +20,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, op: /^[*/]/, + tail: /^:/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -96,6 +97,7 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { + children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -144,6 +146,35 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } + resolve_tails(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'tail'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } resolve_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a88041821..c78712e9d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -99,6 +99,8 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 52b169cae..c7fa4bcef 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -17,7 +17,7 @@ strudelScope.leaf = (token) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack'].includes(name)) { + if (!['seq', 'cat', 'stack', ':'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -26,6 +26,9 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); +strudelScope[':'] = tail; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From e04f250adbd1f4fa89b370dcdfb29af47651a8a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 18:16:37 +0100 Subject: [PATCH 22/88] mondo: proper lambda functions with local scope --- packages/mondo/mondo.mjs | 42 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 6 ++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4402aa525..fc4069434 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -214,11 +214,14 @@ export class MondoParser { if (pipeIndex === -1) break; // pipe up front => lambda if (pipeIndex === 0) { - // . as lambda: (.fast 2) = x=>x.fast(2) - // TODO: this doesn't work for (.fast 2 .speed 2) - // probably needs proper ast representation of lambda - children[pipeIndex] = { type: 'plain', value: '.' }; - continue; + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; } const rightSide = children.slice(pipeIndex + 2); const right = children[pipeIndex + 1]; @@ -343,7 +346,7 @@ export class MondoRunner { errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } - call(ast) { + call(ast, scope = []) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name @@ -354,10 +357,22 @@ export class MondoRunner { `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); + if (name === 'lambda') { + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + // console.log('lambda', argNames, body.children); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args + }; + return this.call(body, scope); + }; + } + // process args const args = ast.children.slice(1).map((arg) => { if (arg.type === 'list') { - return this.call(arg); + return this.call(arg, scope); } if (!this.lib.leaf) { throw new Error(`no handler for leaft nodes! add leaf to your lib`); @@ -368,21 +383,12 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { arg.value = arg.value.slice(1, -1); } - return this.lib.leaf(arg); + return this.lib.leaf(arg, scope); }); - if (name === '.') { - // lambda : (.fast 2) = x=>fast(2, x) - const second = ast.children[1]; - const callee = second.value; - const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); - return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); - } - // look up function in lib const fn = this.lib[name]; this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name); + return this.libcall(fn, args, name, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index c7fa4bcef..868198ca0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -4,8 +4,12 @@ import { MondoRunner } from '../mondo/mondo.mjs'; let runner = new MondoRunner(strudelScope); -strudelScope.leaf = (token) => { +strudelScope.leaf = (token, scope) => { let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } const [from, to] = token.loc; if (token.type === 'plain' && strudelScope[value]) { // what if we want a string that happens to also be a variable name? From 208706fb52436b99341c384e5a5ee5068c6c3547 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:09:29 +0100 Subject: [PATCH 23/88] simplify mondo: ":" is just another operator --- packages/mondo/mondo.mjs | 42 +++++------------------------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fc4069434..5997feb40 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^[,$]/, - op: /^[*/]/, - tail: /^:/, + op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -97,7 +96,6 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { - children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -146,32 +144,10 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } - resolve_tails(children) { - while (true) { - let opIndex = children.findIndex((child) => child.type === 'tail'); - if (opIndex === -1) break; - const op = { type: 'plain', value: children[opIndex].value }; - if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); - } - if (opIndex === 0) { - // regular function call (assuming each operator exists as function) - children[opIndex] = op; - continue; - } - const left = children[opIndex - 1]; - const right = children[opIndex + 1]; - - // convert infix to prefix notation - const call = { type: 'list', children: [op, left, right] }; - - // insert call while keeping other siblings - children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + // prevents to get a list, e.g. ((x y)) => (x y) + unwrap_children(children) { + if (children.length === 1) { + return children[0].children; } return children; } @@ -188,6 +164,7 @@ export class MondoParser { children[opIndex] = op; continue; } + // convert infix to prefix notation const left = children[opIndex - 1]; const right = children[opIndex + 1]; if (left.type === 'pipe') { @@ -195,15 +172,10 @@ export class MondoParser { children[opIndex] = op; continue; } - // convert infix to prefix notation const call = { type: 'list', children: [op, left, right] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + children = this.unwrap_children(children); } return children; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c78712e9d..e21ec75b0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -101,6 +101,7 @@ describe('mondo sugar', () => { it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 65a7b3030e674c4bbd5e588fb93c81c5e178449a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:12 +0100 Subject: [PATCH 24/88] fix: markcss can now override styles (like color) --- packages/codemirror/highlight.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index d333986cc..f9f977c6b 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -1,4 +1,4 @@ -import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state'; +import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'; import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); @@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => { setTimeout(() => { updateMiniLocations(config.editor, config.miniLocations); }, 100); - return on ? highlightExtension : []; + return on ? Prec.highest(highlightExtension) : []; }; From 3657e2fd65e11af40bbe9f2a3b66f67e8423e871 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:45 +0100 Subject: [PATCH 25/88] mondo: change default markcss --- packages/mondough/mondough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 868198ca0..33e0116df 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -38,7 +38,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat; + return pat.markcss('color: var(--foreground);text-decoration:underline'); } // tell transpiler how to get locations for mondo`` calls From 55a5f1d62a187493da3dfda9f0a22a0c4ab604b7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:54 +0100 Subject: [PATCH 26/88] transpiler cleanup --- packages/transpiler/transpiler.mjs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 5eeecd62b..0ee371e6a 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -289,21 +289,6 @@ function tidalWithLocation(value, offset) { }; } -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', From 3505732afad89870286d2c0083887375c62e6b44 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:34:47 +0100 Subject: [PATCH 27/88] mondo: add .. operator --- packages/mondo/mondo.mjs | 2 +- packages/mondo/test/mondo.test.mjs | 1 + packages/mondough/mondough.mjs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5997feb40..70c58aa6d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,9 +17,9 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! + op: /^[*\/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, - op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index e21ec75b0..aadbafbeb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -102,6 +102,7 @@ describe('mondo sugar', () => { it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 33e0116df..e69ea0994 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -30,9 +30,18 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +// : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); strudelScope[':'] = tail; +// .. operator +const arrayRange = (start, stop, step = 1) => + Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => + start < stop ? start + index * step : start - index * step, + ); +const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +strudelScope['..'] = range; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From d2e93a99f11a5ccf0475acd5888f9a84435b1421 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 10:24:22 +0100 Subject: [PATCH 28/88] fix: lint --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70c58aa6d..ff0b6741e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*\/:]|^\.{2}/, // * / : .. + op: /^[*/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e69ea0994..857a9f000 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; From 432e8dcccc7806ee42f8ed059decb485969edefd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:15:48 +0100 Subject: [PATCH 29/88] mondo: support ! and @, also express seq and cat with stepcat --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ff0b6741e..d7c90139f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:]|^\.{2}/, // * / : .. + op: /^[*/:!@]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 857a9f000..d442a0262 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,14 +21,22 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { args = [...rest, pat]; } + if (name === 'seq') { + return stepcat(...args).setSteps(1); + } + if (name === 'cat') { + return stepcat(...args).pace(1); + } return fn(...args); }; strudelScope['*'] = fast; strudelScope['/'] = slow; +strudelScope['!'] = (pat, n) => pat.extend(n); +strudelScope['@'] = (pat, n) => pat.expand(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From 9626f3cc9a58be793fd4be7cd9eacc6e0b38dadf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:24:31 +0100 Subject: [PATCH 30/88] mondo add % for pace --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d7c90139f..bcd48aafe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@]|^\.{2}/, // * / : .. + op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d442a0262..6de612324 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } if (name === 'seq') { @@ -37,6 +37,7 @@ strudelScope['*'] = fast; strudelScope['/'] = slow; strudelScope['!'] = (pat, n) => pat.extend(n); strudelScope['@'] = (pat, n) => pat.expand(n); +strudelScope['%'] = (pat, n) => pat.pace(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From e16295623d3a4861774791e2306d9398f676554f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 14:09:32 +0100 Subject: [PATCH 31/88] mondo: use stepcat for curly braces --- packages/mondo/mondo.mjs | 15 +++++++++++++-- packages/mondough/mondough.mjs | 13 +++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bcd48aafe..4ab8a793c 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,12 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, + open_seq: /^\[/, // todo: rename square close_seq: /^\]/, + open_curly: /^\{/, + close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, @@ -93,6 +95,9 @@ export class MondoParser { if (next === 'open_seq') { return this.parse_seq(); } + if (next === 'open_curly') { + return this.parse_curly(); + } return this.consume(next); } desugar_children(children) { @@ -256,6 +261,12 @@ export class MondoParser { children = this.desugar(children, 'seq'); return { type: 'list', children }; } + parse_curly() { + let children = this.parse_pair('open_curly', 'close_curly'); + children = [{ type: 'plain', value: 'curly' }, ...children]; + children = this.desugar(children, 'curly'); + return { type: 'list', children }; + } consume(type) { // shift removes first element and returns it const token = this.tokens.shift(); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 6de612324..e81cfecb7 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -18,18 +18,15 @@ strudelScope.leaf = (token, scope) => { } return reify(value).withLoc(from, to); }; +strudelScope.curly = stepcat; +strudelScope.seq = (...args) => stepcat(...args).setSteps(1); +strudelScope.cat = (...args) => stepcat(...args).pace(1); strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { + if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } - if (name === 'seq') { - return stepcat(...args).setSteps(1); - } - if (name === 'cat') { - return stepcat(...args).pace(1); - } return fn(...args); }; From c691916cbfe0f00cd0c0d99186a1d95d5eb0d2c7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 17:55:40 +0100 Subject: [PATCH 32/88] mondo: generic bracket names + simplify call handler + refactor mondough --- packages/mondo/mondo.mjs | 54 ++++++++----------- packages/mondo/test/mondo.test.mjs | 46 ++++++++-------- packages/mondough/mondough.mjs | 84 ++++++++++++++++-------------- 3 files changed, 90 insertions(+), 94 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4ab8a793c..19ed27e95 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,10 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, // todo: rename square - close_seq: /^\]/, + open_angle: /^/, + open_square: /^\[/, + close_square: /^\]/, open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! @@ -89,11 +89,11 @@ export class MondoParser { if (next === 'open_list') { return this.parse_list(); } - if (next === 'open_cat') { - return this.parse_cat(); + if (next === 'open_angle') { + return this.parse_angle(); } - if (next === 'open_seq') { - return this.parse_seq(); + if (next === 'open_square') { + return this.parse_square(); } if (next === 'open_curly') { return this.parse_curly(); @@ -126,7 +126,7 @@ export class MondoParser { return chunks; } desugar_stack(children, sequence_type) { - // children is expected to contain seq or cat as first item + // children is expected to contain square or angle as first item const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { return this.desugar_children(children); @@ -140,7 +140,7 @@ export class MondoParser { // chunks of multiple args if (sequence_type) { // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (seq a b) (seq c d)) + // [a b, c d] => (stack (square a b) (square c d)) chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; } chunk = this.desugar_children(chunk); @@ -249,16 +249,16 @@ export class MondoParser { children = this.desugar(children); return { type: 'list', children }; } - parse_cat() { - let children = this.parse_pair('open_cat', 'close_cat'); - children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar(children, 'cat'); + parse_angle() { + let children = this.parse_pair('open_angle', 'close_angle'); + children = [{ type: 'plain', value: 'angle' }, ...children]; + children = this.desugar(children, 'angle'); return { type: 'list', children }; } - parse_seq() { - let children = this.parse_pair('open_seq', 'close_seq'); - children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar(children, 'seq'); + parse_square() { + let children = this.parse_pair('open_square', 'close_square'); + children = [{ type: 'plain', value: 'square' }, ...children]; + children = this.desugar(children, 'square'); return { type: 'list', children }; } parse_curly() { @@ -307,6 +307,8 @@ export class MondoRunner { constructor(lib) { 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`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -316,16 +318,9 @@ export class MondoRunner { } run(code, offset = 0) { const ast = this.parser.parse(code, offset); - // console.log(printAst(ast)); + console.log(printAst(ast)); return this.call(ast); } - // todo: always use lib.call? - libcall(fn, args, name) { - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); - } errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } @@ -357,9 +352,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (!this.lib.leaf) { - throw new Error(`no handler for leaft nodes! add leaf to your lib`); - } if (arg.type === 'number') { arg.value = Number(arg.value); @@ -370,8 +362,6 @@ export class MondoRunner { }); // look up function in lib - const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name, scope); + return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index aadbafbeb..1b9a41e3d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,15 +12,15 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with locations', () => + it('should tokenize with loangleions', () => 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', () => + // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ [2, 4], [5, 8], @@ -73,32 +73,34 @@ let desguar = (a) => { }; describe('mondo sugar', () => { - it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); - it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); - it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); - it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)')); + it('should desugar <>', () => expect(desguar('')).toEqual('(angle a b c)')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(angle a (angle b c) d)')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); - it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); - it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); - it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); - it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); - it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); - it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); + it('should desugar , square 3', () => + expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))')); + it('should desugar , angle', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , angle 2', () => expect(desguar('')).toEqual('(stack bd (angle hh oh))')); + it('should desugar , angle 3', () => + expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); @@ -106,6 +108,6 @@ describe('mondo sugar', () => { it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e81cfecb7..13b0f602f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,52 +1,56 @@ -import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope); +const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope.leaf = (token, scope) => { - let { value } = token; - // local scope - if (token.type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? - // example: "s sine" -> sine is also a variable - return reify(strudelScope[value]).withLoc(from, to); - } - return reify(value).withLoc(from, to); -}; -strudelScope.curly = stepcat; -strudelScope.seq = (...args) => stepcat(...args).setSteps(1); -strudelScope.cat = (...args) => stepcat(...args).pace(1); - -strudelScope.call = (fn, args, name) => { - const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { - args = [...rest, pat]; - } - return fn(...args); -}; - -strudelScope['*'] = fast; -strudelScope['/'] = slow; -strudelScope['!'] = (pat, n) => pat.extend(n); -strudelScope['@'] = (pat, n) => pat.expand(n); -strudelScope['%'] = (pat, n) => pat.pace(n); - -// : operator -const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope[':'] = tail; - -// .. operator const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); -strudelScope['..'] = range; + +let lib = {}; +lib.curly = stepcat; +lib.square = (...args) => stepcat(...args).setSteps(1); +lib.angle = (...args) => stepcat(...args).pace(1); +lib['*'] = fast; +lib['/'] = slow; +lib['!'] = extend; +lib['@'] = expand; +lib['%'] = pace; +lib[':'] = tail; +lib['..'] = range; + +let runner = new MondoRunner({ + call(name, args, scope) { + console.log('call', name, args, scope); + const fn = lib[name] || strudelScope[name]; + if (!fn) { + throw new Error(`[moundough]: unknown function "${name}"`); + } + if (!['square', 'angle', 'stack', 'curly'].includes(name)) { + // flip args (pat to end) + const [pat, ...rest] = args; + args = [...rest, pat]; + } + return fn(...args); + }, + leaf(token, scope) { + let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const [from, to] = token.loc; + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable + return reify(strudelScope[value]).withLoc(from, to); + } + return reify(value).withLoc(from, to); + }, +}); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 05ae31838e2e108f29c9de3d4a90c7e8dbeddbe1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 18:01:41 +0100 Subject: [PATCH 33/88] add sin sqr cos aliases --- packages/core/signal.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index aa8dd9a6b..140b12c6c 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -71,25 +71,28 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. - * * @return {Pattern} + * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); +export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} + * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); +export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -102,11 +105,13 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} + * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); +export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From 989fdfa20bc3633389cfbb47e4a75ada603f6f11 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:18:32 +0100 Subject: [PATCH 34/88] transpiler: add mechanism to register custom mini language --- packages/transpiler/transpiler.mjs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 0ee371e6a..17f5ae30b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -29,8 +29,15 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { - const leafLocs = getLeafLocations(`"${value}"`, node.start, input); - miniLocations = miniLocations.concat(leafLocs); + const minilang = languages.get('minilang'); + if (minilang) { + const code = `[${value}]`; + const locs = minilang.getLocations(code, node.start); + miniLocations = miniLocations.concat(locs); + } else { + const leafLocs = getLeafLocations(`"${value}"`, node.start, input); + miniLocations = miniLocations.concat(leafLocs); + } }; let widgets = []; @@ -142,11 +149,18 @@ function isBackTickString(node, parent) { function miniWithLocation(value, node) { const { start: fromOffset } = node; + + const minilang = languages.get('minilang'); + let name = 'm'; + if (minilang && minilang.name) { + name = minilang.name; // name is expected to be exported from the package of the minilang + } + return { type: 'CallExpression', callee: { type: 'Identifier', - name: 'm', + name, }, arguments: [ { type: 'Literal', value }, From ac6472a43aaa9eadfd8334cec5e1cf4098680670 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:21 +0100 Subject: [PATCH 35/88] mondo: mondo as minilang (inactive) --- packages/mondough/mondough.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 13b0f602f..26c9785e0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -24,7 +24,6 @@ lib['..'] = range; let runner = new MondoRunner({ call(name, args, scope) { - console.log('call', name, args, scope); const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); @@ -60,7 +59,19 @@ export function mondo(code, offset = 0) { return pat.markcss('color: var(--foreground);text-decoration:underline'); } +let getLocations = (code, offset) => runner.parser.get_locations(code, offset); + +export const mondi = (str, offset) => { + const code = `[${str}]`; + return mondo(code, offset); +}; + // tell transpiler how to get locations for mondo`` calls registerLanguage('mondo', { - getLocations: (code, offset) => runner.parser.get_locations(code, offset), + getLocations, }); +// uncomment the following to use mondo as mini notation language +/* registerLanguage('minilang', { + name: 'mondi', + getLocations, +}); */ From c33cfc78c5c29216c6d8ad4d4603c39b097b7cc9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:47 +0100 Subject: [PATCH 36/88] waveform aliases: tri, sqr, saw, sin --- packages/superdough/synth.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..569da5560 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; import { applyFM, gainNode, @@ -27,6 +27,12 @@ const getFrequencyFromValue = (value) => { }; const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveformAliases = [ + ['tri', 'triangle'], + ['sqr', 'square'], + ['saw', 'sawtooth'], + ['sin', 'sine'], +]; const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { @@ -235,6 +241,7 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } export function waveformN(partials, type) { From 12bb82d29564b75a51c3b7360107e081bd350faa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:06 +0100 Subject: [PATCH 37/88] add chooseIn + chooseOut --- packages/core/signal.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 140b12c6c..64a177253 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -403,6 +403,10 @@ export const chooseInWith = (pat, xs) => { */ export const choose = (...xs) => chooseWith(rand, xs); +// todo: doc +export const chooseIn = (...xs) => chooseInWith(rand, xs); +export const chooseOut = choose; + /** * Chooses from the given list of values (or patterns of values), according * to the pattern that the method is called on. The pattern should be in From cf1f4ec35c065870d8fcc3e4cdaea02bdbfcff68 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:25 +0100 Subject: [PATCH 38/88] fix: patterns without structure would error on draw --- packages/draw/draw.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..6edca9882 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -150,7 +150,7 @@ export class Drawer { this.lastFrame = phase; this.visibleHaps = (this.visibleHaps || []) // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.endClipped >= phase - lookbehind - lookahead) + .filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead) // add new haps with onset (think right edge bars scrolling in) .concat(haps.filter((h) => h.hasOnset())); const time = phase - lookahead; @@ -175,7 +175,7 @@ export class Drawer { // +0.1 = workaround for weird holes in query.. const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t); this.painters = []; // will get populated by .onPaint calls attached to the pattern // query future haps const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters }); From 6fd89ddee7095f955dadc05e09a41a866b3c807b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:43 +0100 Subject: [PATCH 39/88] mondo: add | and ? --- packages/mondo/mondo.mjs | 12 +++++++----- packages/mondough/mondough.mjs | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 19ed27e95..815450263 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,9 +19,10 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. + op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. pipe: /^\./, stack: /^[,$]/, + or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -125,9 +126,9 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_stack(children, sequence_type) { + desugar_split(children, split_type, sequence_type) { // children is expected to contain square or angle as first item - const chunks = this.split_children(children, 'stack', sequence_type); + const chunks = this.split_children(children, split_type, sequence_type); if (!chunks.length) { return this.desugar_children(children); } @@ -147,7 +148,7 @@ export class MondoParser { return { type: 'list', children: chunk }; } }); - return [{ type: 'plain', value: 'stack' }, ...args]; + return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) unwrap_children(children) { @@ -241,7 +242,8 @@ export class MondoParser { } desugar(children, type) { // not really needed but more readable and might be extended in the future - children = this.desugar_stack(children, type); + children = this.desugar_split(children, 'or', type); + children = this.desugar_split(children, 'stack', type); return children; } parse_list() { diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 26c9785e0..dcfb1c3c2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,16 @@ -import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; +import { + strudelScope, + reify, + fast, + slow, + seq, + stepcat, + extend, + expand, + pace, + chooseIn, + degradeBy, +} from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -19,8 +31,11 @@ lib['/'] = slow; lib['!'] = extend; lib['@'] = expand; lib['%'] = pace; +lib['?'] = degradeBy; // todo: default 0.5 not working.. lib[':'] = tail; 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) { From bd93e441546cd4a9a28dc4b121413f149fbf9b61 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:34:53 +0100 Subject: [PATCH 40/88] mondo: fix combination of | and , --- packages/mondo/mondo.mjs | 36 +++++++++++++++--------------- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 815450263..ba1b3c568 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -107,11 +107,7 @@ export class MondoParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, split_type, sequence_type) { - if (sequence_type) { - // if given, the first child is ignored - children = children.slice(1); - } + split_children(children, split_type) { const chunks = []; while (true) { let commaIndex = children.findIndex((child) => child.type === split_type); @@ -126,11 +122,10 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_split(children, split_type, sequence_type) { - // children is expected to contain square or angle as first item - const chunks = this.split_children(children, split_type, sequence_type); + desugar_split(children, split_type, next) { + const chunks = this.split_children(children, split_type); if (!chunks.length) { - return this.desugar_children(children); + return next(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -139,12 +134,7 @@ export class MondoParser { return chunk[0]; } else { // chunks of multiple args - if (sequence_type) { - // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (square a b) (square c d)) - chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; - } - chunk = this.desugar_children(chunk); + chunk = next(chunk); return { type: 'list', children: chunk }; } }); @@ -241,9 +231,19 @@ export class MondoParser { return children; } desugar(children, type) { - // not really needed but more readable and might be extended in the future - children = this.desugar_split(children, 'or', type); - children = this.desugar_split(children, 'stack', type); + // if type is given, the first element is expected to contain it as plain value + // e.g. with (square a b, c), we want to split (a b, c) and ignore "square" + children = type ? children.slice(1) : children; + children = this.desugar_split(children, 'stack', (children) => + this.desugar_split(children, 'or', (children) => { + // chunks of multiple args + if (type) { + // the type we've removed before splitting needs to be added back + children = [{ type: 'plain', value: type }, ...children]; + } + return this.desugar_children(children); + }), + ); return children; } parse_list() { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 1b9a41e3d..c51844be0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -90,6 +90,9 @@ describe('mondo sugar', () => { it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); + it('should desugar , | of []', () => + expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))')); it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); it('should desugar , square 3', () => From 5d4ef46ac7255e9440d3303fd253cfef0ab8e4e4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:43:39 +0100 Subject: [PATCH 41/88] mondo cleanup --- packages/mondo/mondo.mjs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ba1b3c568..da969c231 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -217,10 +217,6 @@ export class MondoParser { } return children; } - flip_call(children) { - let [name, first, ...rest] = children; - return [name, ...rest, first]; - } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -340,7 +336,6 @@ export class MondoRunner { if (name === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); - // console.log('lambda', argNames, body.children); return (x) => { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args @@ -354,7 +349,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (arg.type === 'number') { arg.value = Number(arg.value); } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { @@ -363,7 +357,6 @@ export class MondoRunner { return this.lib.leaf(arg, scope); }); - // look up function in lib return this.lib.call(name, args, scope); } } From f71db4aeed3907e3a4e611100ef746fcbf5cdcf3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 23:50:14 +0100 Subject: [PATCH 42/88] mondo: flip pipe order: now pining to the end of the function... --- packages/mondo/mondo.mjs | 67 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 28 ++++++------- packages/mondough/mondough.mjs | 5 --- 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da969c231..1a211e609 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -106,25 +106,22 @@ export class MondoParser { children = this.resolve_pipes(children); return children; } - // Token[] => Token[][] . returns empty list if type not found + // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === split_type); - if (commaIndex === -1) break; - const chunk = children.slice(0, commaIndex); + let splitIndex = children.findIndex((child) => child.type === split_type); + if (splitIndex === -1) break; + const chunk = children.slice(0, splitIndex); chunk.length && chunks.push(chunk); - children = children.slice(commaIndex + 1); - } - if (!chunks.length) { - return []; + children = children.slice(splitIndex + 1); } chunks.push(children); return chunks; } desugar_split(children, split_type, next) { const chunks = this.split_children(children, split_type); - if (!chunks.length) { + if (chunks.length === 1) { return next(children); } // collect args of stack function @@ -168,7 +165,8 @@ export class MondoParser { children[opIndex] = op; continue; } - const call = { type: 'list', children: [op, left, right] }; + //const call = { type: 'list', children: [op, left, right] }; + const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; children = this.unwrap_children(children); @@ -176,46 +174,21 @@ export class MondoParser { return children; } resolve_pipes(children) { - while (true) { - let pipeIndex = children.findIndex((child) => child.type === 'pipe'); - // no pipe => we're done - if (pipeIndex === -1) break; - // pipe up front => lambda - if (pipeIndex === 0) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - const rightSide = children.slice(pipeIndex + 2); - const right = children[pipeIndex + 1]; - if (right.type === 'list') { - // apply function only to left sibling (high precedence) - // s jazz.(fast 2) => s (fast jazz 2) - const [callee, ...rest] = right.children; - const leftSide = children.slice(0, pipeIndex - 1); - const left = children[pipeIndex - 1]; - let args = [callee, left, ...rest]; - const call = { type: 'list', children: args }; - children = [...leftSide, call, ...rightSide]; + let chunks = this.split_children(children, 'pipe'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + if (right.length && right[0].type === 'list') { + // s jazz hh.(fast 2) => s jazz (fast 2 hh) + const target = left[left.length - 1]; // hh + const call = { type: 'list', children: [...right[0].children, target] }; + chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { - // apply function to all left siblings (low precedence) - // s jazz . fast 2 => fast (s jazz) 2 - let leftSide = children.slice(0, pipeIndex); - if (leftSide.length === 1) { - leftSide = leftSide[0]; - } else { - // wrap in (..) if multiple items on the left side - leftSide = { type: 'list', children: leftSide }; - } - children = [right, leftSide, ...rightSide]; + //s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } } - return children; + return chunks[0]; } parse_pair(open_type, close_type) { this.consume(open_type); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c51844be0..342a3f9fd 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -80,15 +80,15 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -102,15 +102,15 @@ describe('mondo sugar', () => { it('should desugar , angle 3', () => expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); - it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); - it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); - it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); - it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', + '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index dcfb1c3c2..0db3667c3 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,11 +43,6 @@ let runner = new MondoRunner({ if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); } - if (!['square', 'angle', 'stack', 'curly'].includes(name)) { - // flip args (pat to end) - const [pat, ...rest] = args; - args = [...rest, pat]; - } return fn(...args); }, leaf(token, scope) { From 492271d786f57921ee3bff62e30f386dd87b698d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:28:57 +0100 Subject: [PATCH 43/88] mondo: support $ tidal style --- packages/mondo/mondo.mjs | 19 +++++++++++++++---- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 1a211e609..ce9e960a1 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,8 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + dollar: /^\$/, pipe: /^\./, - stack: /^[,$]/, + stack: /^[,]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; @@ -103,7 +104,7 @@ export class MondoParser { } desugar_children(children) { children = this.resolve_ops(children); - children = this.resolve_pipes(children); + children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); return children; } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] @@ -173,7 +174,7 @@ export class MondoParser { } return children; } - resolve_pipes(children) { + resolve_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -184,10 +185,20 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; chunks = [[...right, call], ...rest]; } } + return next(chunks[0]); + } + resolve_dollars(children) { + let chunks = this.split_children(children, 'dollar'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) + const call = right.length > 1 ? { type: 'list', children: right } : right[0]; + chunks = [[...left, call], ...rest]; + } return chunks[0]; } parse_pair(open_type, close_type) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 342a3f9fd..0e859a8b2 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -108,6 +108,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); + it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From bca16cdf99614dbc8f1ef4728a13957045752075 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:51:23 +0100 Subject: [PATCH 44/88] mondo: rename resolve_ -> desugar_ --- packages/mondo/mondo.mjs | 15 ++++++--------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ce9e960a1..a14dcc5de 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -102,11 +102,6 @@ export class MondoParser { } return this.consume(next); } - desugar_children(children) { - children = this.resolve_ops(children); - children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); - return children; - } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; @@ -145,7 +140,7 @@ export class MondoParser { } return children; } - resolve_ops(children) { + desugar_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); if (opIndex === -1) break; @@ -174,7 +169,7 @@ export class MondoParser { } return children; } - resolve_pipes(children, next) { + desugar_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -191,7 +186,7 @@ export class MondoParser { } return next(chunks[0]); } - resolve_dollars(children) { + desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -221,7 +216,9 @@ export class MondoParser { // the type we've removed before splitting needs to be added back children = [{ type: 'plain', value: type }, ...children]; } - return this.desugar_children(children); + children = this.desugar_ops(children); + children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + return children; }), ); return children; diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0e859a8b2..f302174e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -104,6 +104,7 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar []*', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); From 6d214564230d77dbbb1e18df91a7a1a3884ac90f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 11:18:18 +0100 Subject: [PATCH 45/88] mondo: allow # character in plain values (for sharps) --- packages/mondo/mondo.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index a14dcc5de..cc05e131e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -109,7 +109,7 @@ export class MondoParser { let splitIndex = children.findIndex((child) => child.type === split_type); if (splitIndex === -1) break; const chunk = children.slice(0, splitIndex); - chunk.length && chunks.push(chunk); + chunks.push(chunk); children = children.slice(splitIndex + 1); } chunks.push(children); @@ -173,6 +173,16 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + if (!left.length) { + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } if (right.length && right[0].type === 'list') { // s jazz hh.(fast 2) => s jazz (fast 2 hh) const target = left[left.length - 1]; // hh From 5eabcf061875c5241ccd84add4e9b3c5b111bc17 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:47:33 +0100 Subject: [PATCH 46/88] mondo mode for StrudelMirror / repl / mini repl --- packages/codemirror/codemirror.mjs | 5 +++-- packages/core/repl.mjs | 5 +++++ website/src/config.ts | 1 + website/src/docs/MiniRepl.jsx | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index e96b533ec..193d96b55 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -62,7 +62,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -75,7 +75,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, - javascript(), + mondo ? [] : javascript(), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? @@ -209,6 +209,7 @@ export class StrudelMirror { }, onEvaluate: () => this.evaluate(), onStop: () => this.stop(), + mondo: replOptions.mondo, }); const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..147155fbe 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -21,6 +21,7 @@ export function repl({ setInterval, clearInterval, id, + mondo = false, }) { const state = { schedulerError: undefined, @@ -180,6 +181,10 @@ export function repl({ setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); shouldHush && hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = Object.values(pPatterns); diff --git a/website/src/config.ts b/website/src/config.ts index 2f499d55f..124cfb3f6 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -81,6 +81,7 @@ export const SIDEBAR: Sidebar = { { text: 'Visual Feedback', link: 'learn/visual-feedback' }, { text: 'Offline', link: 'learn/pwa' }, { text: 'Patterns', link: 'technical-manual/patterns' }, + { text: 'Mondo Notation', link: 'learn/mondo-notation' }, { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, { text: 'Hydra', link: 'learn/hydra' }, diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 0f3d3ce10..09ebb7692 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -30,6 +30,7 @@ export function MiniRepl({ maxHeight, autodraw, drawTime, + mondo = false, }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); @@ -85,6 +86,7 @@ export function MiniRepl({ }, beforeStart: () => audioReady, afterEval: ({ code }) => setVersionDefaultsFrom(code), + mondo, }); // init settings editor.setCode(code); From 51e3aa257ccbf1e1f21a4caadcd9cf7d25ce4268 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:03 +0100 Subject: [PATCH 47/88] mondolang function for mondo repl + fix rests --- packages/mondough/mondough.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 0db3667c3..f42f0ca76 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -10,6 +10,7 @@ import { pace, chooseIn, degradeBy, + silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -20,9 +21,11 @@ const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); -const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); let lib = {}; +lib['-'] = silence; +lib['~'] = silence; lib.curly = stepcat; lib.square = (...args) => stepcat(...args).setSteps(1); lib.angle = (...args) => stepcat(...args).pace(1); @@ -46,14 +49,15 @@ let runner = new MondoRunner({ return fn(...args); }, leaf(token, scope) { - let { value } = token; + let { value, type } = token; // local scope - if (token.type === 'plain' && scope[value]) { + if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? + const variable = lib[value] ?? strudelScope[value]; + 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 return reify(strudelScope[value]).withLoc(from, to); } @@ -66,7 +70,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat.markcss('color: var(--foreground);text-decoration:underline'); + return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } let getLocations = (code, offset) => runner.parser.get_locations(code, offset); @@ -80,6 +84,12 @@ export const mondi = (str, offset) => { registerLanguage('mondo', { getLocations, }); + +// this is like mondo, but with a zero offset +export const mondolang = (code) => mondo(code, 0); +registerLanguage('mondolang', { + getLocations: (code) => getLocations(code, 0), +}); // uncomment the following to use mondo as mini notation language /* registerLanguage('minilang', { name: 'mondi', From dd5743ab8c3e0ae057a0859e57badfaf98a00e1e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:23 +0100 Subject: [PATCH 48/88] mondo: bring back $ for stacking --- packages/mondo/mondo.mjs | 37 ++++++++++++++++++------------ packages/mondo/test/mondo.test.mjs | 4 ++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cc05e131e..3f0eb0385 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,9 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. - dollar: /^\$/, + // dollar: /^\$/, pipe: /^\./, - stack: /^[,]/, + stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, }; @@ -121,16 +121,20 @@ export class MondoParser { return next(children); } // collect args of stack function - const args = chunks.map((chunk) => { - if (chunk.length === 1) { - // chunks of one element can be added to the stack as is - return chunk[0]; - } else { + const args = chunks + .map((chunk) => { + if (!chunk.length) { + return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty) + } + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } // chunks of multiple args chunk = next(chunk); return { type: 'list', children: chunk }; - } - }); + }) + .filter(Boolean); // ignore empty chunks return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) @@ -169,7 +173,7 @@ export class MondoParser { } return children; } - desugar_pipes(children, next) { + desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -190,13 +194,15 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; chunks = [[...right, call], ...rest]; } } - return next(chunks[0]); + // return next(chunks[0]); + return chunks[0]; } - desugar_dollars(children) { + /* desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -205,7 +211,7 @@ export class MondoParser { chunks = [[...left, call], ...rest]; } return chunks[0]; - } + } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -227,7 +233,8 @@ export class MondoParser { children = [{ type: 'plain', value: type }, ...children]; } children = this.desugar_ops(children); - children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + children = this.desugar_pipes(children); return children; }), ); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f302174e8..a35eb8aa8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -109,9 +109,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); - it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); - it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 3a19e23473299a6664e085f9aa1620a262663f38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:56 +0100 Subject: [PATCH 49/88] mondo: doc --- website/src/pages/learn/mondo-notation.mdx | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 website/src/pages/learn/mondo-notation.mdx diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx new file mode 100644 index 000000000..caf9f67fe --- /dev/null +++ b/website/src/pages/learn/mondo-notation.mdx @@ -0,0 +1,145 @@ +--- +title: Mondo Notation +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Mondo Notation + +"Mondo Notation" is a new kind of notation that is similar to [Mini Notation](/learn/mini-notation/), but with enough abilities to make it work as a standalone pattern language. +Here's an example: + +/2 +.jux /2 +.rarely (12.note.add) .delay .4 .vib 1:.3 +.fm (sine/4.range .5 4).fmh 2.02 +.lpf (sine/2.range 200 2000).lpq 4 +.s piano .clip 2 +.add (perlin.range 0 .2 .note)`} +/> + +## Calling Functions + +Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: + + + +The first element inside the brackets is the function name. In JS, this would look like: + + + +The outermost parens are not needed, so we can drop them: + + + +## Mini Notation Features + +Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: + +- `[]` for 1-cycle sequences +- `<>` for multi-cycle sequences +- `{}` for stepped sequences (more on that later) +- \* => [fast](/learn/time-modifiers/#fast) +- / => [slow](/learn/time-modifiers/#slow) +- , => [stack](/learn/factories/#stack) +- ! => [extend](/learn/stepwise/#extend) +- @ => [expand](/learn/stepwise/#expand) +- % => [pace](/learn/stepwise/#pace) +- ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) +- : => tail (creates a list) +- .. => range (between numbers) +- | => [chooseIn](/learn/random-modifiers/#choose) + +Example: + +`} +/> + +## Chaining Functions + +Similar to how it works in JS, we can chain functions calls with the "." operator: + +*4 +.scale C4:minor +.jux rev +.dec .2 +.delay .5`} +/> + +Here's the same written in JS: + +*4") +.scale("C4:minor") +.jux(rev) +.dec(.2) +.delay(.5)`} +/> + +### Chaining Functions Locally + +A function can be applied "locally" by wrapping it in round parens: + + + +in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: + + + +here we can see much we can save when there's no boundary between mini notation and function calls! + +### Lambda Functions + +Some functions in strudel expect a function as input, for example: + +x.dec(.1))`} /> + +in mondo, the `x=>x.` can be shortened to: + + + +chaining works as expected: + + + +## Strings + +You can use "double quotes" and 'single quotes' to get a string: + + + +## Multiple Patterns + +The `$` sign can be used to separate multiple patterns: + +.voicing +.struct[x - - x - x - -].delay.5`} +/> + +The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 92b5b6538f91a300c1c2ca0aef04d3ed3830b06a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:12:52 +0100 Subject: [PATCH 50/88] mondo: improve doc --- website/src/pages/learn/mondo-notation.mdx | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index caf9f67fe..6a2f07ff9 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,14 +14,20 @@ Here's an example: /2 -.jux /2 -.rarely (12.note.add) .delay .4 .vib 1:.3 -.fm (sine/4.range .5 4).fmh 2.02 -.lpf (sine/2.range 200 2000).lpq 4 -.s piano .clip 2 -.add (perlin.range 0 .2 .note)`} + tune={`$ note c2 .(euclid <3 6 3> <8 16>) . *2 +.s "sine" .add (note [0 <12 24>]*2) +.dec(sine .range .2 2) .room .5 +.lpf(sine/3.range 120 400) +.lpenv(rand .range .5 4) +.lpq(perlin .range 5 12 . * 2) +.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> +.postgain .6 .delay .1 .clip 5 + +$ s [bd bd bd bd] .bank tr909.clip.5 +.ply<1 [1 [2 4]]> + +$ s oh*4 .press .bank tr909 .speed.8 +.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> ## Calling Functions @@ -42,21 +48,29 @@ The outermost parens are not needed, so we can drop them: Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: +### Brackets + - `[]` for 1-cycle sequences - `<>` for multi-cycle sequences - `{}` for stepped sequences (more on that later) + +### Infix Operators + - \* => [fast](/learn/time-modifiers/#fast) - / => [slow](/learn/time-modifiers/#slow) -- , => [stack](/learn/factories/#stack) - ! => [extend](/learn/stepwise/#extend) - @ => [expand](/learn/stepwise/#expand) - % => [pace](/learn/stepwise/#pace) - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) + +### Separators + +- , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) -Example: +### Example + +In this case, the \*2 will be applied to the whole pattern. + ### Lambda Functions Some functions in strudel expect a function as input, for example: From 222e479e30fede8f8cf567207b0fc691d57508d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:18:57 +0100 Subject: [PATCH 51/88] mondo: more docs --- website/src/pages/learn/mondo-notation.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 6a2f07ff9..aa7904987 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -30,6 +30,15 @@ $ s oh*4 .press .bank tr909 .speed.8 .dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> +## Mondo in the REPL + +For now, you can only use mondo in the repl like this: + + + +The rest of this site will only use the mondo notation itself. +In the future, the REPL might get a way to use mondo notation directly. + ## Calling Functions Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: @@ -64,9 +73,6 @@ Besides function calling with round parens, Mondo Notation has a lot in common w - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) - -### Separators - - , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) From 2938bfd88f9e74449a585d49c41462dda2cb9beb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 02:46:26 +0100 Subject: [PATCH 52/88] mondo: add highly impractical .(. notation for local infix application --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 79 ++++++++++++++++++++++++------ packages/mondo/test/mondo.test.mjs | 12 ++++- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index a6a59dd94..096d77d56 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan 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') +const pat = runner.run('s [bd hh*2 cp.(.crush 4) ] . speed .8') ``` the above code will create the following call structure: diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3f0eb0385..70015cc2d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -63,6 +63,8 @@ export class MondoParser { } // take code, return abstract syntax tree parse(code, offset) { + this.code = code; + this.offset = offset; this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { @@ -165,7 +167,6 @@ export class MondoParser { children[opIndex] = op; continue; } - //const call = { type: 'list', children: [op, left, right] }; const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; @@ -173,24 +174,73 @@ export class MondoParser { } return children; } + get_lambda(args, children) { + // (.fast 2) = (lambda (_) (fast _ 2)) + const body = this.desugar(children); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } + // inserts target into lambda body + desugar_lambda(lambda, target) { + // lambda looks like return value from this.get_lambda + const [_, args, body] = lambda; + if (args.length > 1) { + throw new Error('desugar_lambda with >1 arg is unsupported rn'); + } + const argNames = args.children.map((child) => child.value); + let desugar = (child) => { + if (child.type === 'plain' && child.value === argNames[0]) { + return target; + } + if (child.type === 'list') { + child.children = child.children.map(desugar); + } + return child; + }; + return desugar(body); + } + // returns location range of given ast (even if desugared) + get_range(ast, range = [Infinity, 0]) { + let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])]; + if (ast.loc) { + return union(range, ast.loc); + } + if (ast.type !== 'list') { + return range; + } + return ast.children.reduce((range, child) => { + const childrange = this.get_range(child, range); + return union(range, childrange); + }, range); + } + errorhead(ast) { + return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`; + } + // returns original user code where the given ast originates (even if desugared) + get_code_snippet(ast) { + const [min, max] = this.get_range(ast); + return this.code.slice(min - this.offset, max - this.offset); + } desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; if (!left.length) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; + const arg = { type: 'plain', value: '_' }; + return this.get_lambda([arg], [arg, ...children]); } if (right.length && right[0].type === 'list') { - // s jazz hh.(fast 2) => s jazz (fast 2 hh) + // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) const target = left[left.length - 1]; // hh - const call = { type: 'list', children: [...right[0].children, target] }; + + if (right[0].children[0].value !== 'lambda') { + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); + } + const call = this.desugar_lambda(right[0].children, target); chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) @@ -317,18 +367,15 @@ export class MondoRunner { console.log(printAst(ast)); return this.call(ast); } - errorhead(ast) { - return `[mondo ${ast.loc?.join(':') || ''}]`; - } call(ast, scope = []) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); if (name === 'lambda') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a35eb8aa8..d5e33cbce 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,7 +114,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar lambda with pipe', () => + expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + const lambda = parser.parse('(lambda (_) (fast 2 _))'); + const target = { type: 'plain', value: 'xyz' }; + it('should desugar_lambda', () => + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); }); From 7716fdb98e8ee64f70ef77472b7b38c6f7f3afd1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:22:23 +0100 Subject: [PATCH 53/88] mondo: refactor - rename MondoRunner.call => .evaluate - can now evaluate leafs - remove .( because it's confusing and half baked - support (.) as id function --- packages/mondo/README.md | 21 ++----- packages/mondo/mondo.mjs | 97 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 9 +-- 3 files changed, 37 insertions(+), 90 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 096d77d56..bd9104b9f 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan 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') +const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') ``` the above code will create the following call structure: @@ -52,16 +52,6 @@ n("0 1 2").add(n("<0 -4>")).scale("C:minor") --- -```plaintext -n[0 1 2].(add<0 -4>).scale"C minor" -``` - -```js -n("0 1 2".add("<0 -4>")).scale("C:minor") -``` - ---- - ```plaintext n[0 1 2].scale"C minor" .sometimes (12.note.add) @@ -75,11 +65,11 @@ n("0 1 2").scale("C:minor") --- ```plaintext -note g2*8.dec /2.(range .1 .4) +note g2*8.dec /2 ``` ```js -note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) ``` --- @@ -95,7 +85,7 @@ n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) --- mondo` -sound [bd sd.(every 3 (.fast 4))].jux +sound [bd (sd.every 3 (.fast 4))].jux ` // og "Alternate Timelines for TidalCycles" example: // jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] @@ -129,9 +119,6 @@ comments variables: note g2*8.dec sine -sine.(range 0 4)/2 doesnt work -sine/2.(range 0 4) works - n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole ### reference diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70015cc2d..165e597bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -176,31 +176,9 @@ export class MondoParser { } get_lambda(args, children) { // (.fast 2) = (lambda (_) (fast _ 2)) - const body = this.desugar(children); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - // inserts target into lambda body - desugar_lambda(lambda, target) { - // lambda looks like return value from this.get_lambda - const [_, args, body] = lambda; - if (args.length > 1) { - throw new Error('desugar_lambda with >1 arg is unsupported rn'); - } - const argNames = args.children.map((child) => child.value); - let desugar = (child) => { - if (child.type === 'plain' && child.value === argNames[0]) { - return target; - } - if (child.type === 'list') { - child.children = child.children.map(desugar); - } - return child; - }; - return desugar(body); + children = this.desugar(children); + const body = children.length === 1 ? children[0] : { type: 'list', children }; + return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -228,40 +206,23 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + + if (right.length && right[0].type === 'list') { + // x.(y) => not allowed anymore for now.. + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); + } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); } - if (right.length && right[0].type === 'list') { - // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) - const target = left[left.length - 1]; // hh - - if (right[0].children[0].value !== 'lambda') { - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); - } - const call = this.desugar_lambda(right[0].children, target); - chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) - } else { - //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; - chunks = [[...right, call], ...rest]; - } + // s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } // return next(chunks[0]); return chunks[0]; } - /* desugar_dollars(children) { - let chunks = this.split_children(children, 'dollar'); - while (chunks.length > 1) { - let [left, right, ...rest] = chunks; - //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) - const call = right.length > 1 ? { type: 'list', children: right } : right[0]; - chunks = [[...left, call], ...rest]; - } - return chunks[0]; - } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -365,11 +326,20 @@ export class MondoRunner { run(code, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.call(ast); + return this.evaluate(ast); } - call(ast, scope = []) { - // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); + evaluate(ast, scope = []) { + if (ast.type !== 'list') { + // is leaf + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + arg.value = arg.value.slice(1, -1); + } + return this.lib.leaf(ast, scope); + } + + // is list // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; @@ -385,23 +355,12 @@ export class MondoRunner { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args }; - return this.call(body, scope); + return this.evaluate(body, scope); }; } - // process args - const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'list') { - return this.call(arg, scope); - } - if (arg.type === 'number') { - arg.value = Number(arg.value); - } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { - arg.value = arg.value.slice(1, -1); - } - return this.lib.leaf(arg, scope); - }); - + // evaluate args + const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5e33cbce..2fd45d46d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,15 +114,16 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); - const lambda = parser.parse('(lambda (_) (fast 2 _))'); + /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => - expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); From abc7a1e48d3e8cfb301d39a711ddb59ffcf8ae38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:00 +0100 Subject: [PATCH 54/88] mondo: this is the way --- packages/mondo/mondo.mjs | 17 +++++++---------- packages/mondough/mondough.mjs | 5 +++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 165e597bb..3ff781256 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -207,11 +207,6 @@ export class MondoParser { while (chunks.length > 1) { let [left, right, ...rest] = chunks; - if (right.length && right[0].type === 'list') { - // x.(y) => not allowed anymore for now.. - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); - } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); @@ -342,11 +337,13 @@ export class MondoRunner { // is list // the first element is expected to be the function name const first = ast.children[0]; - const name = first.value; - this.assert( - first?.type === 'plain', - `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, - ); + let name; + if (first?.type !== 'list') { + name = first.value; // regular function call e.g. (fast 2 (s bd)) + } else { + // dynamic function name e.g. "( 2 (s bd))" + name = this.evaluate(first); + } if (name === 'lambda') { const [_, args, body] = ast.children; diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index f42f0ca76..081d054b9 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ import { chooseIn, degradeBy, silence, + isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -42,6 +43,10 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { + if (isPattern(name)) { + // patterned function name, e.g. "s bd . 2" + return name.fmap((fn) => fn(...args)).innerJoin(); + } const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); From 1738e4d53892d0434082b4b3af0b957530fa804b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:54 +0100 Subject: [PATCH 55/88] fix: lint --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3ff781256..5419d15b5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -329,7 +329,7 @@ export class MondoRunner { if (ast.type === 'number') { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - arg.value = arg.value.slice(1, -1); + ast.value = ast.value.slice(1, -1); } return this.lib.leaf(ast, scope); } From 64a6dacc1ed8975ff99c7022aac1495db412fb79 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:27:02 +0100 Subject: [PATCH 56/88] mondo: patternable function names --- packages/mondo/mondo.mjs | 19 ++++------- packages/mondough/mondough.mjs | 37 +++++++++++++++------- website/src/pages/learn/mondo-notation.mdx | 4 +-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5419d15b5..d031af4eb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -280,7 +280,7 @@ export class MondoParser { get_locations(code, offset = 0) { let walk = (ast, locations = []) => { if (ast.type === 'list') { - return ast.children.slice(1).forEach((child) => walk(child, locations)); + return ast.children.forEach((child) => walk(child, locations)); } if (ast.loc) { locations.push(ast.loc); @@ -335,17 +335,11 @@ export class MondoRunner { } // is list - // the first element is expected to be the function name - const first = ast.children[0]; - let name; - if (first?.type !== 'list') { - name = first.value; // regular function call e.g. (fast 2 (s bd)) - } else { - // dynamic function name e.g. "( 2 (s bd))" - name = this.evaluate(first); + if (!ast.children.length) { + throw new Error(`empty list`); } - if (name === 'lambda') { + if (ast.children[0].value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -356,8 +350,9 @@ export class MondoRunner { }; } + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + // we could short circuit arg[0] if its plain... // evaluate args - const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); - return this.lib.call(name, args, scope); + return this.lib.call(args[0], args.slice(1), scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 081d054b9..01247e78b 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,7 +11,6 @@ import { chooseIn, degradeBy, silence, - isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -24,7 +23,10 @@ const arrayRange = (start, stop, step = 1) => ); const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +let nope = (...args) => args[args.length - 1]; + let lib = {}; +lib['nope'] = nope; lib['-'] = silence; lib['~'] = silence; lib.curly = stepcat; @@ -43,15 +45,19 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { - if (isPattern(name)) { - // patterned function name, e.g. "s bd . 2" - return name.fmap((fn) => fn(...args)).innerJoin(); - } - const fn = lib[name] || strudelScope[name]; - if (!fn) { - throw new Error(`[moundough]: unknown function "${name}"`); + // name is expected to be a pattern of functions! + const first = name.firstCycle(true)[0]; + if (typeof first?.value !== 'function') { + throw new Error(`[mondough] "${first}" is not a function`); } - return fn(...args); + return name + .fmap((fn) => { + if (typeof fn !== 'function') { + throw new Error(`[mondough] "${fn}" is not a function`); + } + return fn(...args); + }) + .innerJoin(); }, leaf(token, scope) { let { value, type } = token; @@ -59,14 +65,21 @@ let runner = new MondoRunner({ if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } - const [from, to] = token.loc; 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 - return reify(strudelScope[value]).withLoc(from, to); + pat = reify(variable); + } else { + pat = reify(value); + } + + if (token.loc) { + pat = pat.withLoc(token.loc[0], token.loc[1]); } - return reify(value).withLoc(from, to); + pat.foo = true; + return pat; }, }); diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index aa7904987..2bc952341 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,7 +14,7 @@ Here's an example: <8 16>) . *2 + tune={`$ note (c2 .euclid <3 6 3> <8 16>) . *2 .s "sine" .add (note [0 <12 24>]*2) .dec(sine .range .2 2) .room .5 .lpf(sine/3.range 120 400) @@ -27,7 +27,7 @@ $ s [bd bd bd bd] .bank tr909.clip.5 .ply<1 [1 [2 4]]> $ s oh*4 .press .bank tr909 .speed.8 -.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} +.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} /> ## Mondo in the REPL From 6b2b8b5124c9eefa1d25904958a09871b1e2cb55 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:30:49 +0100 Subject: [PATCH 57/88] fix: test --- packages/mondo/test/mondo.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 2fd45d46d..f987cbc2b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,7 +12,7 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with loangleions', () => + it('should tokenize with locations', () => expect( parser .tokenize('(one two three)') @@ -22,6 +22,7 @@ describe('mondo tokenizer', () => { // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ + [0, 1], [2, 4], [5, 8], ])); From e9de45993e2c9fe9fa002ccf2af361fc8dea6214 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:54:04 +0100 Subject: [PATCH 58/88] signal: remove signal synonyms for now --- packages/core/signal.mjs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 64a177253..9152b6a32 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -72,27 +72,23 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. * @return {Pattern} - * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); -export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} - * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); -export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -105,13 +101,11 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} - * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); -export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From c9f58220a3c8bf02a35559ac9489d00becb29c2e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:55:29 +0100 Subject: [PATCH 59/88] remove minor change --- packages/core/signal.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 9152b6a32..ac8aa3e3d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -99,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); /** * A square signal between 0 and 1. - * * @return {Pattern} * @example * n(square.segment(4).range(0,7)).scale("C:minor") From 2e017e46f94962b2ec43a8db44853bcdd4d6ddc0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 20:17:09 +0100 Subject: [PATCH 60/88] trim readme --- packages/mondo/README.md | 124 --------------------------------------- 1 file changed, 124 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index bd9104b9f..551690467 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -25,127 +25,3 @@ the above code will create the following call structure: ) .8 ) ``` - -you can pass all available functions to *MondoRunner* as an object. - -## snippets / thoughts - -### variants of add - -```plaintext -n[0 1 2].add(<0 -4>.n).scale"C minor" -``` - -```js -n("0 1 2").add("<0 -4>".n()).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].add(n<0 -4>).scale"C minor" -``` - -```js -n("0 1 2").add(n("<0 -4>")).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].scale"C minor" -.sometimes (12.note.add) -``` - -```js -n("0 1 2").scale("C:minor") -.sometimes(add(note("12"))) -``` - ---- - -```plaintext -note g2*8.dec /2 -``` - -```js -note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) -``` - ---- - -```plaintext -n <0 1 2 3 4>*4 .scale"C minor" .jux -``` - -```js -n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) -``` - ---- - -mondo` -sound [bd (sd.every 3 (.fast 4))].jux -` -// og "Alternate Timelines for TidalCycles" example: -// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] - -### things mondo cant do - -how to write lists? - -```js -arrange( - [4, "(3,8)"], - [2, "(5,8)"] -).note() -``` - -how to write objects: - -```js -samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') -``` - -how to access array indices? - -```js -note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) -``` - -s hh .struct(binaryN 55532 16) - -comments - -variables: note g2*8.dec sine - -n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole - -### reference - -- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] -- bank: s [bd sd [- bd] sd].bank TR909 -- beat: s sd .beat [4,12] 16 -- binary: s hh .struct (binary 5) -- binaryN: s hh .struct(binaryN 55532 16) => is wrong -- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] -- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 - -### dot is a bit ambiguous - -```plaintext -n[0 1 2].scale"C minor".ad.1 -``` - -decimal vs pipe - -### less ambiguity with [] and "" - -in js, s("hh cp") implcitily does [hh cp] -in mondo, s[hh cp] always shows the type of bracket used - -### todo - -- lists: C:minor -- spread: [0 .. 2] -- replicate: ! From c9cafa37fdb521d87d4a690d68e8092b841bf209 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 21:33:51 +0100 Subject: [PATCH 61/88] 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 551690467..47812e83d 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) -```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') -``` +## Example Usage -the above code will create the following call structure: - -```lisp -(speed - (s - (seq bd - (* hh 2) - (crush cp 4) - (cat mt ht lt) - ) - ) .8 -) +```js +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 d031af4eb..673841cde 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 f987cbc2b..338a1fec8 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 01247e78b..d84ceb0cf 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)) { From b4027fd92ac38f9426e29982bd444fa197754f33 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:36:50 +0200 Subject: [PATCH 62/88] fix: mondo dont mutate (breaks lambdas) --- packages/mondo/mondo.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 673841cde..0e8dc8576 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -343,8 +343,9 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - // evaluate all children before evaluating list - ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); - return this.evaluator(ast, scope); + // evaluate all children before evaluating list (dont mutate!!!) + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const node = { type: 'list', children: args }; + return this.evaluator(node, scope); } } From a0fb8fb37f36644e5e344a938e828ec22ffb0cd0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:43:01 +0200 Subject: [PATCH 63/88] mondo: def node --- packages/mondo/mondo.mjs | 21 +++++++++++++++++---- packages/mondo/test/mondo.test.mjs | 6 +++--- packages/mondough/mondough.mjs | 11 +++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0e8dc8576..89236a3bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -175,10 +175,10 @@ export class MondoParser { return children; } get_lambda(args, children) { - // (.fast 2) = (lambda (_) (fast _ 2)) + // (.fast 2) = (fn (_) (fast _ 2)) children = this.desugar(children); const body = children.length === 1 ? children[0] : { type: 'list', children }; - return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; + return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -322,7 +322,7 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } - evaluate(ast, scope = []) { + evaluate(ast, scope = {}) { if (ast.type !== 'list') { // is leaf if (ast.type === 'number') { @@ -333,7 +333,20 @@ export class MondoRunner { return this.evaluator(ast, scope); } - if (ast.children[0]?.value === 'lambda') { + if (ast.children[0]?.value === 'def') { + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); + } + // (def myfn (fn (_) (ply 2 _))) + // ^name ^body + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + if (ast.children[0]?.value === 'fn') { + // (fn (_) (ply 2 _) + // ^args ^ body const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 338a1fec8..47e0e165d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -119,10 +119,10 @@ describe('mondo sugar', () => { '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d84ceb0cf..543dcfeb2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,15 +49,19 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + if (name.value === 'def') { + return silence; + } // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; - if (typeof first?.value !== 'function') { - throw new Error(`[mondough] "${first}" is not a function`); + const type = typeof first?.value; + if (type !== 'function') { + throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); } return name .fmap((fn) => { if (typeof fn !== 'function') { - throw new Error(`[mondough] "${fn}" is not a function`); + throw new Error(`[mondough] "${fn}" is not a function b`); } return fn(...args); }) @@ -80,7 +84,6 @@ function evaluator(node, scope) { if (node.loc) { pat = pat.withLoc(node.loc[0], node.loc[1]); } - pat.foo = true; return pat; } From f2372e7a415da0edce6d9460327d652a487e18ba Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 17:55:37 +0200 Subject: [PATCH 64/88] mondo: refactor evaluate function into bits --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 73 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 47812e83d..d283ccf31 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -31,6 +31,6 @@ function evaluator(node) { } return fn(...args); } -const runner = new MondoRunner(evaluator); +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 89236a3bb..e19116ae6 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,7 +306,7 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(evaluator) { + constructor({ evaluator } = {}) { this.parser = new MondoParser(); this.evaluator = evaluator; this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); @@ -322,43 +322,52 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } + evaluate_def(ast, scope) { + // (def name body) + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); + } + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + evaluate_lambda(ast, scope) { + // (fn (_) (ply 2 _) + // ^args ^ body + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args + }; + return this.evaluate(body, scope); + }; + } + evaluate_list(ast, scope) { + // evaluate all children before evaluating list (dont mutate!!!) + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const node = { type: 'list', children: args }; + return this.evaluator(node, scope); + } + evaluate_leaf(ast, scope) { + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + ast.value = ast.value.slice(1, -1); + } + return this.evaluator(ast, scope); + } evaluate(ast, scope = {}) { if (ast.type !== 'list') { - // is leaf - if (ast.type === 'number') { - ast.value = Number(ast.value); - } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - ast.value = ast.value.slice(1, -1); - } - return this.evaluator(ast, scope); + return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'def') { - if (ast.children.length !== 3) { - throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); - } - // (def myfn (fn (_) (ply 2 _))) - // ^name ^body - const name = ast.children[1].value; - const body = this.evaluate(ast.children[2], scope); - scope[name] = body; - return this.evaluator(ast, scope); + return this.evaluate_def(ast, scope); } if (ast.children[0]?.value === 'fn') { - // (fn (_) (ply 2 _) - // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args - }; - return this.evaluate(body, scope); - }; + return this.evaluate_lambda(ast, scope); } - // evaluate all children before evaluating list (dont mutate!!!) - const args = ast.children.map((arg) => this.evaluate(arg, scope)); - const node = { type: 'list', children: args }; - return this.evaluator(node, scope); + return this.evaluate_list(ast, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 47e0e165d..6f8090cc5 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -150,6 +150,6 @@ describe('mondo arithmetic', () => { } return fn(...args); } - const runner = new MondoRunner(evaluator); + 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 543dcfeb2..effaf367c 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -87,7 +87,7 @@ function evaluator(node, scope) { return pat; } -let runner = new MondoRunner(evaluator); +let runner = new MondoRunner({ evaluator }); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 94a3a60e4917e7c5592aea35b1ded71e6005ad60 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:49:28 +0200 Subject: [PATCH 65/88] mondo: remember pair locations + allow passing a scope to MondoRunner.run + make def a side effect + proper lambda with multiple args + closure --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 65 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 2 +- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d283ccf31..8bdd93143 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,7 +27,7 @@ function evaluator(node) { // 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}`); + throw new Error(`"${fn}" is not a function`); } return fn(...args); } diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index e19116ae6..2553f74fb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#]+/, + plain: /^[a-zA-Z0-9-~_^#+-]+/, }; // matches next token next_token(code, offset = 0) { @@ -88,6 +88,8 @@ export class MondoParser { parse_expr() { if (!this.tokens[0]) { throw new Error(`unexpected end of file`); + // TODO: could we allow that? like (((((((( s bd + // return { type: 'list', children: [] }; } let next = this.tokens[0]?.type; if (next === 'open_list') { @@ -219,13 +221,17 @@ export class MondoParser { return chunks[0]; } parse_pair(open_type, close_type) { + const begin = this.tokens[0].loc?.[0]; this.consume(open_type); const children = []; while (this.tokens[0]?.type !== close_type) { children.push(this.parse_expr()); } + const end = this.tokens[0].loc?.[1]; this.consume(close_type); - return children; + const node = { type: 'list', children }; + begin !== undefined && (node.loc = [begin, end]); + return node; } desugar(children, type) { // if type is given, the first element is expected to contain it as plain value @@ -247,27 +253,27 @@ export class MondoParser { return children; } parse_list() { - let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar(children); - return { type: 'list', children }; + let node = this.parse_pair('open_list', 'close_list'); + node.children = this.desugar(node.children); + return node; } parse_angle() { - let children = this.parse_pair('open_angle', 'close_angle'); - children = [{ type: 'plain', value: 'angle' }, ...children]; - children = this.desugar(children, 'angle'); - return { type: 'list', children }; + let node = this.parse_pair('open_angle', 'close_angle'); + node.children.unshift({ type: 'plain', value: 'angle' }); + node.children = this.desugar(node.children, 'angle'); + return node; } parse_square() { - let children = this.parse_pair('open_square', 'close_square'); - children = [{ type: 'plain', value: 'square' }, ...children]; - children = this.desugar(children, 'square'); - return { type: 'list', children }; + let node = this.parse_pair('open_square', 'close_square'); + node.children.unshift({ type: 'plain', value: 'square' }); + node.children = this.desugar(node.children, 'square'); + return node; } parse_curly() { - let children = this.parse_pair('open_curly', 'close_curly'); - children = [{ type: 'plain', value: 'curly' }, ...children]; - children = this.desugar(children, 'curly'); - return { type: 'list', children }; + let node = this.parse_pair('open_curly', 'close_curly'); + node.children.unshift({ type: 'plain', value: 'curly' }); + node.children = this.desugar(node.children, 'curly'); + return node; } consume(type) { // shift removes first element and returns it @@ -317,10 +323,10 @@ export class MondoRunner { throw new Error(error); } } - run(code, offset = 0) { + run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.evaluate(ast); + return this.evaluate(ast, scope); } evaluate_def(ast, scope) { // (def name body) @@ -330,18 +336,19 @@ export class MondoRunner { const name = ast.children[1].value; const body = this.evaluate(ast.children[2], scope); scope[name] = body; - return this.evaluator(ast, scope); + // def with fall through } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args + const [_, formalArgs, body] = ast.children; + return (...args) => { + const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); + const closure = { + ...scope, + ...params, }; - return this.evaluate(body, scope); + return this.evaluate(body, closure); }; } evaluate_list(ast, scope) { @@ -362,12 +369,12 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'def') { - return this.evaluate_def(ast, scope); - } if (ast.children[0]?.value === 'fn') { return this.evaluate_lambda(ast, scope); } + if (ast.children[0]?.value === 'def') { + this.evaluate_def(ast, scope); + } return this.evaluate_list(ast, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index effaf367c..e995c0e4e 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -93,7 +93,7 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, offset); + const pat = runner.run(code, undefined, offset); return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } From 077aac1888da1d9c4d307161158dfe50f875da67 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:50:03 +0200 Subject: [PATCH 66/88] mondo: add some examples from sicp book --- packages/mondo/test/mondo.test.mjs | 84 ++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6f8090cc5..c9ec8aa02 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -121,6 +121,7 @@ describe('mondo sugar', () => { it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); @@ -130,26 +131,87 @@ describe('mondo sugar', () => { }); describe('mondo arithmetic', () => { + let multi = + (op) => + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); + let lib = { - add: (a, b) => a + b, - mul: (a, b) => a * b, + '+': multi((a, b) => a + b), + '-': multi((a, b) => a - b), + '*': multi((a, b) => a * b), + '/': multi((a, b) => a / b), + run: (...args) => args[args.length - 1], + def: () => 0, PI: Math.PI, }; - function evaluator(node) { - // check if node is a leaf node (!= list) + function evaluator(node, scope) { if (node.type !== 'list') { - // check lib if we find a match in the lib, otherwise return value - return lib[node.value] ?? node.value; + // is leaf + return scope[node.value] ?? lib[node.value] ?? node.value; } - // now it can only be a list.. + // is 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}`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(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')); + let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope); + let pretty = (exp) => printAst(runner.parser.parse(exp), false); + //it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + + it('eval number', () => expect(evaluate('2')).toEqual(2)); + it('eval string', () => expect(evaluate('abc')).toEqual('abc')); + it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3)); + it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6)); + it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2)); + it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4)); + it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9)); + + // sicp + it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486)); + it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666)); + it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495)); + it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2)); + it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7)); + it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75)); + it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200)); + it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19)); + it('sicp 9.4', () => + expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+ + (* 3 + (+ + (* 2 4) + (+ 3 5) + ) + ) + (+ + (- 10 7) 6 + ) +)`)); // this is not exactly pretty printing by convention.. + + let scope = {}; + it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10)); + it('sicp 11.2', () => + expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159)); + it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); + it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); + it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); + it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); + it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); + it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); + it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); + it('sicp 17.4', () => + expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => + expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); + it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); + it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ }); From f6ffdd6ae6779698b1a84ab902d2771e9752ad15 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 22:31:42 +0200 Subject: [PATCH 67/88] mondo: move +- to ops + add "raw" to parsed pairs + implement match + if + add more sicp examples --- packages/mondo/mondo.mjs | 57 +++++++++++++++++++++++++++--- packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++---- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2553f74fb..cdbaf964a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,12 +19,12 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#+-]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -230,7 +230,10 @@ export class MondoParser { const end = this.tokens[0].loc?.[1]; this.consume(close_type); const node = { type: 'list', children }; - begin !== undefined && (node.loc = [begin, end]); + if (begin !== undefined) { + node.loc = [begin, end]; + node.raw = this.code.slice(begin, end); + } return node; } desugar(children, type) { @@ -338,6 +341,43 @@ export class MondoRunner { scope[name] = body; // def with fall through } + evaluate_match(ast, scope) { + // (match (p1 e1) (p2 e2) ... (pn en)) + // = cond in lisp + if (ast.children.length < 2) { + return; + } + const [_, ...body] = ast.children; + for (let i = 0; i < body.length; ++i) { + const [predicate, exp] = body[i].children; + if (predicate.value === 'else') { + return this.evaluate(exp, scope); + } + const outcome = this.evaluate(predicate, scope); + if (outcome) { + return this.evaluate(exp, scope); + } + } + return undefined; // nothing was matched + } + evaluate_if(ast, scope) { + // if is a special case of match + if (ast.children.length !== 4) { + return; + } + // (if predicate consequent alternative) + const [_, predicate, consequent, alternative] = ast.children; + // (match (predicate consequent) (else alternative)) + const matcher = { + type: 'list', + children: [ + { type: 'plain', value: 'match' }, + { type: 'list', children: [predicate, consequent] }, + { type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] }, + ], + }; + return this.evaluate_match(matcher, scope); + } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body @@ -369,10 +409,17 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'fn') { + const name = ast.children[0]?.value; + if (name === 'fn') { return this.evaluate_lambda(ast, scope); } - if (ast.children[0]?.value === 'def') { + if (name === 'match') { + return this.evaluate_match(ast, scope); + } + if (name === 'if') { + return this.evaluate_if(ast, scope); + } + if (name === 'def') { this.evaluate_def(ast, scope); } return this.evaluate_list(ast, scope); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c9ec8aa02..15cb42218 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,6 +141,12 @@ describe('mondo arithmetic', () => { '-': multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + eq: (a, b) => a === b, + lt: (a, b) => a < b, + gt: (a, b) => a > b, + and: (a, b) => a && b, + or: (a, b) => a || b, + not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, PI: Math.PI, @@ -204,12 +210,37 @@ describe('mondo arithmetic', () => { it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => - expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); - it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => - expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); - it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + it('sicp 22.1', () => + expect( + evaluate( + `(def abs (fn (x) + (match + ((gt x 0) x) + ((eq x 0) 0) + ((lt x 0) (- 0 x)) + )))`, // sicp was doing (- x), which doesnt work with our - + scope, + ), + ).toEqual(0)); + + it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false)); + it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true)); + it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true)); + it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false)); + + it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3)); + it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); + it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); + + it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); + it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); + it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); From aa70246afcc484d70899886965df9b1d473dd6e1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 23:24:47 +0200 Subject: [PATCH 68/88] mondo: more sicp --- packages/mondo/test/mondo.test.mjs | 114 ++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 15cb42218..d52fc004b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -138,7 +138,9 @@ describe('mondo arithmetic', () => { let lib = { '+': multi((a, b) => a + b), + add: multi((a, b) => a + b), '-': multi((a, b) => a - b), + sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), eq: (a, b) => a === b, @@ -242,7 +244,113 @@ describe('mondo arithmetic', () => { it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); - /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); - it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); - it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ + it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0)); + it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0)); + it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19)); + it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4)); + it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16)); + it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6)); + it('sicp ex1.1.7', () => + expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); + + // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... + it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); + it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); + + // 1.1.7 Example: Square Roots by Newton’s Method + it('sicp 30.1', () => + expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); + it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); + it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); + it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 31.1', () => + expect( + evaluate( + `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + guess + (sqrtiter (improve guess x) x))))`, + scope, + ), + ).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); + it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); + it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); + + // lexical scoping + // doesnt work... + /* it('sicp 39.1', () => + expect( + evaluate( + `(def sqrt (fn (x) +(def (goodenuf guess) +(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) +(average guess (/ x guess))) (def sqrtiter (fn (guess) +(if (goodenuf guess) guess + (sqrtiter (improve guess))))) + (sqrtiter 1.0))) (sqrt 7)`, + scope, + ), + ).toEqual(0)); */ + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + // uses lexical scoping -> doesnt work + /* it('sicp 41.3', () => + expect( + evaluate( + `(def fac (fn (n) (faciter 1 1 n))) +(def faciter (fn (product counter maxcount) (if (gt counter maxcount) + product + (faciter (* counter product) + (+ counter 1) + max-count))))`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + + // 46.1 + /* (define (+ a b) +(if (= a 0) b (inc (+ (dec a) b)))) +(define (+ a b) +(if (= a 0) b (+ (dec a) (inc b)))) */ + + // Exercise 1.10 + // Ackermann’s function + it('sicp 47.1', () => + expect( + evaluate( + ` +(def A (fn (x y) (match ((eq y 0) 0) +((eq x 0) (* 2 y)) +((eq y 1) 2) +(else (A (- x 1) (A x (- y 1))))))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024)); + it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536)); + it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536)); + it('sicp 47.5', () => + expect( + evaluate( + ` +(def f (fn (n) (A 0 n))) +(def g (fn (n) (A 1 n))) +(def h (fn (n) (A 2 n))) +(def k (fn (n) (* 5 n n))) + `, + scope, + ), + ).toEqual(0)); + // Tree Recursion }); From 39d27844411f0ec88c25d733d64845bb2a750811 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:09:17 +0200 Subject: [PATCH 69/88] mondo: more sicp tests + support special form for function def (might remove later) + support multiple expressions in fn body + support lexical scoping --- packages/mondo/mondo.mjs | 28 ++- packages/mondo/test/mondo.test.mjs | 284 +++++++++++++++++++++++++---- 2 files changed, 270 insertions(+), 42 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cdbaf964a..8db36bd5d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -328,10 +328,29 @@ export class MondoRunner { } run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); - console.log(printAst(ast)); + //console.log(printAst(ast)); return this.evaluate(ast, scope); } evaluate_def(ast, scope) { + // function definition special form? + if (ast.children[1].type === 'list') { + // (def (add a b) (+ a b)) + // => (def add (fn (a b) (+ a b)) ) + const args = ast.children[1].children.slice(1); + const lambda = { + // lambda + type: 'list', + children: [ + { type: 'plain', value: 'fn' }, + { type: 'list', children: args }, + ...ast.children.slice(2), // body + ], + }; + // we mutate to make sure the old ast wont make a mess later + ast.children[1] = ast.children[1].children[0]; + ast.children[2] = lambda; + ast.children = ast.children.slice(0, 3); // throw away rest + } // (def name body) if (ast.children.length !== 3) { throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); @@ -381,14 +400,17 @@ export class MondoRunner { evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, formalArgs, body] = ast.children; + const [_, formalArgs, ...body] = ast.children; return (...args) => { const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); const closure = { ...scope, ...params, }; - return this.evaluate(body, closure); + // body can have multiple expressions + const res = body.map((exp) => this.evaluate(exp, closure)); + // last expression is the return value + return res[res.length - 1]; }; } evaluate_list(ast, scope) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d52fc004b..d5b9a094f 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -143,6 +143,7 @@ describe('mondo arithmetic', () => { sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + mod: multi((a, b) => a % b), eq: (a, b) => a === b, lt: (a, b) => a < b, gt: (a, b) => a > b, @@ -207,25 +208,25 @@ describe('mondo arithmetic', () => { it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); - it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0)); it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136)); it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); it('sicp 22.1', () => expect( evaluate( - `(def abs (fn (x) + `(def (abs x) (match ((gt x 0) x) ((eq x 0) 0) ((lt x 0) (- 0 x)) - )))`, // sicp was doing (- x), which doesnt work with our - + ))`, // sicp was doing (- x), which doesnt work with our - scope, ), ).toEqual(0)); @@ -239,7 +240,7 @@ describe('mondo arithmetic', () => { it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); - it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0)); it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); @@ -254,68 +255,73 @@ describe('mondo arithmetic', () => { expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... - it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0)); it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); // 1.1.7 Example: Square Roots by Newton’s Method it('sicp 30.1', () => - expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0)); it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); - it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0)); it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); - it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0)); it('sicp 31.1', () => expect( evaluate( - `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + `(def (sqrtiter guess x) (if (goodenuf guess x) guess - (sqrtiter (improve guess x) x))))`, + (sqrtiter (improve guess x) x)))`, scope, ), ).toEqual(0)); - it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); // lexical scoping - // doesnt work... - /* it('sicp 39.1', () => + it('sicp 39.1', () => expect( evaluate( - `(def sqrt (fn (x) -(def (goodenuf guess) -(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) -(average guess (/ x guess))) (def sqrtiter (fn (guess) -(if (goodenuf guess) guess - (sqrtiter (improve guess))))) - (sqrtiter 1.0))) (sqrt 7)`, + ` +(def (sqrt x) + (def (goodenough guess) + (lt (abs (- (square guess) x)) 0.001)) +(def (improve guess) + (average guess (/ x guess))) +(def (sqrt-iter guess) + (if (goodenough guess) guess (sqrt-iter (improve guess)))) +(sqrtiter 1.0)) + + `, scope, ), - ).toEqual(0)); */ + ).toEqual(0)); // recursive fac - it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); + it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0)); it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // iterative fac - // uses lexical scoping -> doesnt work - /* it('sicp 41.3', () => + it('sicp 41.3', () => expect( evaluate( - `(def fac (fn (n) (faciter 1 1 n))) -(def faciter (fn (product counter maxcount) (if (gt counter maxcount) - product - (faciter (* counter product) - (+ counter 1) - max-count))))`, + ` +(def (factorial n) (factiter 1 1 n)) +(def (factiter product counter maxcount) + (if (gt counter maxcount) + product + (factiter (* counter product) + (+ counter 1) + maxcount))) +`, scope, ), ).toEqual(0)); - it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 /* (define (+ a b) @@ -329,10 +335,10 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def A (fn (x y) (match ((eq y 0) 0) +(def (A x y) (match ((eq y 0) 0) ((eq x 0) (* 2 y)) ((eq y 1) 2) -(else (A (- x 1) (A x (- y 1))))))) +(else (A (- x 1) (A x (- y 1)))))) `, scope, ), @@ -344,13 +350,213 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def f (fn (n) (A 0 n))) -(def g (fn (n) (A 1 n))) -(def h (fn (n) (A 2 n))) -(def k (fn (n) (* 5 n n))) +(def (f n) (A 0 n)) +(def (g n) (A 1 n))) +(def (h n) (A 2 n)) +(def (k n) (* 5 n n)) `, scope, ), ).toEqual(0)); + // Tree Recursion + // recursive process + it('sicp 48.1', () => + expect( + evaluate( + ` +(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1) +(else (+ (fib (- n 1)) (fib (- n 2)))))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // iterative process + it('sicp 48.2', () => + expect( + evaluate( + ` +(def (fib n) (fibiter 1 0 n)) +(def (fibiter a b count) (if (eq count 0) + b + (fibiter (+ a b) a (- count 1)))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // example: counting change + it('sicp 52.2', () => + expect( + evaluate( + ` +(def (countchange amount) (cc amount 5)) +(def (cc amount kindsofcoins) + (match + ((eq amount 0) 1) + ((or (lt amount 0) (eq kindsofcoins 0)) 0) + (else (+ + (cc amount (- kindsofcoins 1)) + (cc (- amount (firstdenomination kindsofcoins)) kindsofcoins))))) + +(def (firstdenomination kindsofcoins) +(match + ((eq kindsofcoins 1) 1) + ((eq kindsofcoins 2) 5) + ((eq kindsofcoins 3) 10) + ((eq kindsofcoins 4) 25) + ((eq kindsofcoins 5) 50))) + +(countchange 100) + `, + scope, + ), + ).toEqual(292)); + + // todo: pascals triangle + it('sicp 57.1', () => + expect( + evaluate( + ` +(def (cube x) (* x x x)) +(def (p x) (sub (* 3 x) (* 4 (cube x)))) +(def (sine angle) +(if (not (gt (abs angle) 0.1)) angle +(p (sine (/ angle 3.0))))) + +(sine 12.15) + `, + scope, + ), + ).toEqual(-0.39980345741334)); + + // exponentiation recursive + it('sicp 57.2', () => + expect( + evaluate( + ` +(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1))))) +(expt 2 4) +`, + scope, + ), + ).toEqual(16)); + + // exponentiation iterative + it('sicp 58.1b', () => + expect( + evaluate( + ` +(def (expt b n) (exptiter b n 1)) +(def (exptiter b counter product) (if (eq counter 0) + product + (exptiter b (- counter 1) (* b product)))) +(expt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // exponentiation fast + it('sicp 58.2', () => + expect( + evaluate( + ` +(def (fastexpt b n) (match ((eq n 0) 1) +((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1)))))) +(def (iseven n) +(eq (mod n 2) 0)) +(fastexpt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // * = repeated addition + it('sicp 60.1', () => + expect( + evaluate( + `(def (mult a b) (if (eq b 0) + 0 + (+ a (* a (- b 1))))) + (mult 3 15) + `, + ), + ).toEqual(45)); + + // gcd / euclid + it('sicp 63.1', () => + expect( + evaluate( + `(def (gcd a b) (if (eq b 0) + a + (gcd b (mod a b)))) + (gcd 20 6) + `, + ), + ).toEqual(2)); + + // 65 smallest divisor + // 67 fermat test + // .... + + // higher order procedures + + it('sicp 77.1', () => + expect( + evaluate( + ` +(def (sum term a next b) +(if (gt a b) 0 (+ (term a) +(sum term (next a) next b)))) +`, + scope, + ), + ).toEqual(0)); + + it('sicp 78.1', () => + expect( + evaluate( + ` +(def (inc n) (+ n 1)) +(def (cube a) (* a a a)) +(def (sumcubes a b) +(sum cube a inc b)) +(sumcubes 1 10) + `, + scope, + ), + ).toEqual(3025)); + + it('sicp 78.2', () => + expect( + evaluate( + ` + (def (identity x) x) + (def (sumintegers a b) + (sum identity a inc b)) + (sumintegers 1 10) + `, + scope, + ), + ).toEqual(55)); + + // pisum pulled out defs to make it work + it('sicp 79.1', () => + expect( + evaluate( + ` +(def (piterm x) +(/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) +(def (pisum a b) +(sum piterm a pinext b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); }); From b2588e8c935a6e066c449a5749fabe8e9caf83b0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:11:06 +0200 Subject: [PATCH 70/88] fix: lint --- packages/mondo/test/mondo.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5b9a094f..9f7f16be4 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -279,6 +279,7 @@ describe('mondo arithmetic', () => { it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + // eslint-disable-next-line no-loss-of-precision it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); From 60d09adb449409842734a12a5ef4a545574528d0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:33:18 +0200 Subject: [PATCH 71/88] mondo: more tests --- packages/mondo/test/mondo.test.mjs | 58 +++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9f7f16be4..0fac441e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -545,19 +545,69 @@ describe('mondo arithmetic', () => { ), ).toEqual(55)); - // pisum pulled out defs to make it work + // pisum it('sicp 79.1', () => expect( evaluate( ` -(def (piterm x) -(/ 1.0 (* x (+ x 2)))) -(def (pinext x) (+ x 4)) (def (pisum a b) + (def (piterm x) + (/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) (sum piterm a pinext b)) (* 8 (pisum 1 1000)) `, scope, ), ).toEqual(3.139592655589783)); + + // integral + it('sicp 79.2', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (def (adddx x) (+ x dx)) +(* (sum f (+ a (/ dx 2.0)) adddx b) dx)) +(integral cube 0 1 0.01) + `, + scope, + ), + ).toEqual(0.24998750000000042)); + // maximum callstack... + //it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001)); + + //lambdas + it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9)); + + it('sicp 83.2', () => + expect( + evaluate( + ` +(def (pisum a b) + (sum (fn (x) (/ 1.0 (* x (+ x 2)))) + a + (fn (x) (+ x 4)) + b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); + it('sicp 83.3', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (* (sum f + (+ a (/ dx 2.0)) + (fn (x) (+ x dx)) + b) + dx)) +(integral cube 0 1 0.01) +`, + scope, + ), + ).toEqual(0.24998750000000042)); + it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); }); From 989bd0990e638f19d694826c5509683459f1a6b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:01:36 +0200 Subject: [PATCH 72/88] mondo: let expressions --- packages/mondo/mondo.mjs | 16 +++++++++++ packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 8db36bd5d..705b9de91 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -331,6 +331,19 @@ export class MondoRunner { //console.log(printAst(ast)); return this.evaluate(ast, scope); } + evaluate_let(ast, scope) { + // (let ((x 3) (y 4)) ...body) + // = ((fn (x y) ...body) 3 4) + const defs = ast.children[1].children; + const args = defs.map((pair) => pair.children[0]); + const vals = defs.map((pair) => pair.children[1]); + const body = ast.children.slice(2); + const lambda = { + type: 'list', + children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body], + }; + return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope); + } evaluate_def(ast, scope) { // function definition special form? if (ast.children[1].type === 'list') { @@ -441,6 +454,9 @@ export class MondoRunner { if (name === 'if') { return this.evaluate_if(ast, scope); } + if (name === 'let') { + return this.evaluate_let(ast, scope); + } if (name === 'def') { this.evaluate_def(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0fac441e8..236fa2911 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -610,4 +610,47 @@ describe('mondo arithmetic', () => { ), ).toEqual(0.24998750000000042)); it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); + + // let expressions + it('sicp 87.1', () => + expect( + evaluate( + ` +(+ (let ((x 3)) +(+ x (* x 10))) x) +`, + { x: 5 }, + ), + ).toEqual(38)); + it('sicp 87.2', () => + expect( + evaluate( + ` +(let ((x 3) +(y (+ x 2))) +(* x y)) + `, + { x: 2 }, + ), + ).toEqual(12)); + it('sicp 88.1', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f square) + `, + scope, + ), + ).toEqual(4)); + it('sicp 88.2', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f (fn (z) (* z (+ z 1)))) + `, + scope, + ), + ).toEqual(6)); }); From d4733a30aba4d6ec9dfaf53046b1e3ac0193fbc7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:30:03 +0200 Subject: [PATCH 73/88] mondo: add half interval method example --- packages/mondo/test/mondo.test.mjs | 45 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 236fa2911..9c2931966 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -152,6 +152,7 @@ describe('mondo arithmetic', () => { not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, + sin: Math.sin, PI: Math.PI, }; function evaluator(node, scope) { @@ -647,10 +648,48 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def (f g) (g 2)) -(f (fn (z) (* z (+ z 1)))) - `, + (def (f g) (g 2)) + (f (fn (z) (* z (+ z 1)))) + `, scope, ), ).toEqual(6)); + + // Finding roots of equations by the half-interval method + it('sicp 89.1', () => + expect( + evaluate( + ` +(def (search f negpoint pospoint) + (let ((midpoint (average negpoint pospoint))) + (if (closeenough negpoint pospoint) + midpoint + (let ((testvalue (f midpoint))) + (match ((positive testvalue) + (search f negpoint midpoint)) + ((negative testvalue) + (search f midpoint pospoint)) + (else midpoint)))))) + +(def (closeenough x y) (lt (abs (- x y)) 0.001)) + +(def (negative x) (lt x 0)) +(def (positive x) (gt x 0)) + +(def (halfintervalmethod f a b) (let ((avalue (f a)) +(bvalue (f b))) +(match ((and (negative avalue) (positive bvalue)) +(search f a b)) +((and (negative bvalue) (positive avalue)) +(search f b a)) (else +(error "Values are not of opposite sign" a b))))) + +(halfintervalmethod sin 2.0 4.0) +`, + scope, + ), + ).toEqual(3.14111328125)); + + it('sicp 89.1', () => + expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); }); From d5ef686fe0fc3036363502e31556007412a64fa4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 00:45:00 +0200 Subject: [PATCH 74/88] mondo: sicp 100 --- packages/mondo/test/mondo.test.mjs | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9c2931966..c685bcb28 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -153,6 +153,7 @@ describe('mondo arithmetic', () => { run: (...args) => args[args.length - 1], def: () => 0, sin: Math.sin, + cos: Math.cos, PI: Math.PI, }; function evaluator(node, scope) { @@ -692,4 +693,69 @@ describe('mondo arithmetic', () => { it('sicp 89.1', () => expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); + + // Finding fixed points of functions + it('sicp 92.1', () => + expect( + evaluate( + ` +(def tolerance 0.00001) +(def (fixedpoint f first-guess) + (def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance)) + (def (try guess) + (let ((next (f guess))) + (if (closeenough guess next) next (try next)))) + (try first-guess)) + +(fixedpoint cos 1.0) +`, + scope, + ), + ).toEqual(0.7390822985224023)); + it('sicp 93.1', () => + expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173)); + // Maximum call stack size exceeded (expected) + /* it('sicp 93.2', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */ + it('sicp 93.3', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + // Procedures as Returned Values + it('sicp 97.1', () => + expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55)); + it('sicp 98.1', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + it('sicp 98.2', () => + expect( + evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope), + ).toEqual(1.912934258514886)); + it('sicp 99.1', () => + expect( + evaluate( + ` + (def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx))) + (def dx 0.00001) + (def (cube x) (* x x x)) + ((deriv cube) 5) + `, + scope, + ), + ).toEqual(75.00014999664018)); + // With the aid of deriv, we can express Newton’s method as a fixed-point process: + it('sicp 100.1', () => + expect( + evaluate( + ` +(def (newtontransform g) +(fn (x) (- x (/ (g x) ((deriv g) x))))) +(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess)) +(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); }); From 7fc21d55d7904f5047806ef50e1ddbd308fde8c5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 10:04:22 +0200 Subject: [PATCH 75/88] mondo: rational number test --- packages/mondo/mondo.mjs | 1 + packages/mondo/test/mondo.test.mjs | 113 ++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 705b9de91..bed568661 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -437,6 +437,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); + ast.type = 'plain'; // is this problematic? } return this.evaluator(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c685bcb28..7029c5772 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,6 +155,13 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, + cons: (a, b) => [a, b], + car: (pair) => pair[0], + cdr: (pair) => pair[1], + concat: (...msgs) => msgs.join(''), + error: (...msgs) => { + throw new Error(msgs.join(' ')); + }, }; function evaluator(node, scope) { if (node.type !== 'list') { @@ -164,7 +171,7 @@ describe('mondo arithmetic', () => { // is list const [fn, ...args] = node.children; if (typeof fn !== 'function') { - throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(fn)}"`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`); } return fn(...args); } @@ -327,9 +334,9 @@ describe('mondo arithmetic', () => { it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 - /* (define (+ a b) + /* (def (+ a b) (if (= a 0) b (inc (+ (dec a) b)))) -(define (+ a b) +(def (+ a b) (if (= a 0) b (+ (dec a) (inc b)))) */ // Exercise 1.10 @@ -499,6 +506,7 @@ describe('mondo arithmetic', () => { (gcd b (mod a b)))) (gcd 20 6) `, + scope, ), ).toEqual(2)); @@ -758,4 +766,103 @@ describe('mondo arithmetic', () => { scope, ), ).toEqual(2.6457513110645907)); + // whatever this is + it('sicp 101.1', () => + expect( + evaluate( + ` + (def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess)) + (def (sqrt x) (fixedpointoftransform + (fn (y) (/ x y)) averagedamp 1.0)) + (sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + it('sicp 101.2', () => + expect( + evaluate( + ` +(def (sqrt x) (fixedpointoftransform +(fn (y) (- (square y) x)) newtontransform 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + + // data abstraction + + // rational arithmetic + it('sicp 114.1', () => + expect( + evaluate( + ` +(def (addrat x y) + (makerat (+ (* + (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (subrat x y) + (makerat (- (* (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (mulrat x y) + (makerat (* (numer x) (numer y)) + (* (denom x) (denom y)))) +(def (divrat x y) + (makerat (* (numer x) (denom y)) + (* (denom x) (numer y)))) +(def (equalrat x y) + (eq (* (numer x) (denom y)) + (* (numer y) (denom x)))) + `, + scope, + ), + ).toEqual(0)); + + // markerat number denom + it('sicp 117.1', () => + expect( + evaluate( + ` +(def (makerat n d) (cons n d)) +(def (numer x) (car x)) +(def (denom x) (cdr x)) +(def (printrat x) (concat (numer x) ':' (denom x))) + `, + scope, + ), + ).toEqual(0)); + + it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2')); + it('sicp 117.2', () => + expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6')); + it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6')); + it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9')); + it('sicp 118.1', () => + expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0)); + it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3')); + + let lscope = {}; + // pairs with lambda + it('sicp 124.1', () => + expect( + evaluate( + ` +(def (cons x y) + (def (dispatch m) + (match + ((eq m 0) x) + ((eq m 1) y) + (else (error "argument not 0 or 1: CONS" m))) + ) dispatch) + (def (car z) (z 0)) + (def (cdr z) (z 1)) + `, + lscope, + ), + ).toEqual(0)); + it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); + it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); }); From 243adda3ddc2627eb51c3f93f8b2280d4da7ef12 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 1 May 2025 10:56:33 +0200 Subject: [PATCH 76/88] mondo: more sicp tests --- packages/mondo/test/mondo.test.mjs | 106 ++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 7029c5772..4ec18decb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,9 +155,12 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, - cons: (a, b) => [a, b], + cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])], car: (pair) => pair[0], - cdr: (pair) => pair[1], + cdr: (pair) => pair.slice(1), + list: (...items) => items, + nil: [], + isnull: (items) => items.length === 0, concat: (...msgs) => msgs.join(''), error: (...msgs) => { throw new Error(msgs.join(' ')); @@ -865,4 +868,103 @@ describe('mondo arithmetic', () => { ).toEqual(0)); it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); + // lists + it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4])); + it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1)); + it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4])); + it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2)); + it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4])); + // listref + it('sicp 138.1', () => + expect( + evaluate( + ` +(def (listref items n) (if (eq n 0) (car items) + (listref (cdr items) (- n 1)))) +(def squares (list 1 4 9 16 25)) +(listref squares 3)`, + scope, + ), + ).toEqual(16)); + // length recursive + it('sicp 138.2', () => + expect( + evaluate( + ` + (def (length items) + (if (isnull items) 0 + (+ 1 (length (cdr items))))) + (def odds (list 1 3 5 7)) + (length odds)`, + ), + ).toEqual(4)); + // length iterative + it('sicp 139.1', () => + expect( + evaluate( + ` + (def (length items) + (def (lengthiter a count) + (if (isnull a) count + (lengthiter (cdr a) (+ 1 count)))) + (lengthiter items 0)) + (def odds (list 1 3 5 7)) + (length odds) + `, + scope, + ), + ).toEqual(4)); + // append + it('sicp 139.1', () => + expect( + evaluate( + ` +(def (append list1 list2) +(if (isnull list1) + list2 + (cons (car list1) (append (cdr list1) list2)))) + (append squares odds) + `, + scope, + ), + ).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7])); + // (define (f x y . z) ⟨body⟩) <- tbd: variable argument count + // Mapping over lists + + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (if (isnull items) nil + (cons (* (car items) factor) + (scalelist (cdr items) factor)))) +(scalelist (list 1 2 3 4 5) 10) + `, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); + + it('sicp 143.1', () => + expect( + evaluate( + ` + (def (map proc items) (if (isnull items) nil + (cons (proc (car items)) + (map proc (cdr items))))) + (map abs (list -10 2.5 -11.6 17)) + `, + scope, + ), + ).toEqual([10, 2.5, 11.6, 17])); + it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16])); + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (map (fn (x) (* x factor)) items)) +(scalelist (list 1 2 3 4 5) 10) +`, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); }); From e3a6444f413ee01bb23aef9ac44bb0e69306296e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:30:09 +0200 Subject: [PATCH 77/88] mondo: support line comments --- packages/mondo/mondo.mjs | 5 ++++- packages/mondo/test/mondo.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bed568661..be7b2a203 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,6 +8,7 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { + comment: /^\/\/(.*?)(?=\n|$)/, quotes_double: /^"(.*?)"/, quotes_single: /^'(.*?)'/, open_list: /^\(/, @@ -428,7 +429,9 @@ export class MondoRunner { } evaluate_list(ast, scope) { // evaluate all children before evaluating list (dont mutate!!!) - const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const args = ast.children + .filter((child) => child.type !== 'comment') // ignore comments + .map((arg) => this.evaluate(arg, scope)); const node = { type: 'list', children: args }; return this.evaluator(node, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 4ec18decb..6e9260f08 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -67,6 +67,14 @@ describe('mondo s-expressions parser', () => { { type: 'number', value: '22.3' }, ], })); + it('should parse comments', () => + expect(p('a // hello')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'comment', value: '// hello' }, + ], + })); }); let desguar = (a) => { From 29e5833c8c48818289b436864af332f15b04cf39 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:32:46 +0200 Subject: [PATCH 78/88] tweak --- packages/mondo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 8bdd93143..b1f504bf9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,6 @@ # mondo -an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: +an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From 60dd8bd763d510197daafe6830dce8c4a3f075ec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:35:16 +0200 Subject: [PATCH 79/88] tweak again --- packages/mondo/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index b1f504bf9..5233c73d3 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,9 @@ # mondo -an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: +a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet. +see the `test` folder for usage examples + +more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From ffe831d24bdbd16d63e30ab8cb67c4b50c4f17d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:53:55 +0200 Subject: [PATCH 80/88] mondo: strings now get type "string" to be discernable from plain variables --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be7b2a203..626793d8e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -440,7 +440,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); - ast.type = 'plain'; // is this problematic? + ast.type = 'string'; } return this.evaluator(ast, scope); } From 7565354274bca05ec29156bf084baefdca1be4ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:54:34 +0200 Subject: [PATCH 81/88] superdough: fallback to triangle when non-string is given --- packages/superdough/superdough.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 02832b433..8c26910b5 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -101,6 +101,10 @@ export async function aliasBank(...args) { } export function getSound(s) { + if (typeof s !== 'string') { + console.warn(`getSound: expected string got "${s}". fall back to triangle`); + return soundMap.get().triangle; // is this good? + } return soundMap.get()[s.toLowerCase()]; } From 4208c79c0951ef963026b1454a0cc76ed7294555 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 15:13:04 +0200 Subject: [PATCH 82/88] fix: ! and @ operators --- packages/mondough/mondough.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e995c0e4e..d995c3d8f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,6 +49,10 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + // some functions wont be reified to make sure they work (e.g. see extend below) + if (typeof name === 'function') { + return name(...args); + } if (name.value === 'def') { return silence; } @@ -56,7 +60,7 @@ function evaluator(node, scope) { const first = name.firstCycle(true)[0]; const type = typeof first?.value; if (type !== 'function') { - throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); + throw new Error(`[mondough] expected function, got "${first?.value}"`); } return name .fmap((fn) => { @@ -73,10 +77,14 @@ function evaluator(node, scope) { return reify(scope[value]); // -> local scope has no location } const variable = lib[value] ?? strudelScope[value]; + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable 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 + // some function names are not patternable, so we skip reification here + if (['!', 'extend', '@', 'expand'].includes(value)) { + return variable; + } pat = reify(variable); } else { pat = reify(value); From 607a6121bcd8fc3ebef851b5c6f4fefb25727132 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 16:20:23 +0200 Subject: [PATCH 83/88] rename mondo package to mondolang --- packages/mondo/README.md | 2 +- packages/mondo/package.json | 2 +- packages/mondough/mondough.mjs | 2 +- packages/mondough/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 5233c73d3..954f425a9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -11,7 +11,7 @@ more info: ## Example Usage ```js -import { MondoRunner } from 'mondo' +import { MondoRunner } from 'mondolang' // define our library of functions and variables let lib = { add: (a, b) => a + b, diff --git a/packages/mondo/package.json b/packages/mondo/package.json index d8a4a0bf2..277bddb1c 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,5 +1,5 @@ { - "name": "mondo", + "name": "mondolang", "version": "1.1.0", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d995c3d8f..3731e8cee 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -13,7 +13,7 @@ import { silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; -import { MondoRunner } from '../mondo/mondo.mjs'; +import { MondoRunner } from 'mondolang'; const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); diff --git a/packages/mondough/package.json b/packages/mondough/package.json index eae22519e..a034424c0 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", - "@strudel/transpiler": "workspace:*" + "@strudel/transpiler": "workspace:*", + "mondolang": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67e5d85d1..7aecd35bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,6 +364,9 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + mondolang: + specifier: workspace:* + version: link:../mondo devDependencies: mondo: specifier: '*' From 2951a60fac778ee1b7a8b04d51282957425d7758 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 3 May 2025 11:44:02 +0200 Subject: [PATCH 84/88] fix: rests at the end --- packages/mondo/mondo.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 626793d8e..6ec63c8d0 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -155,7 +155,9 @@ export class MondoParser { if (opIndex === -1) break; const op = { type: 'plain', value: children[opIndex].value }; if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); + //throw new Error(`cannot use operator as last child.`); + children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]" + continue; } if (opIndex === 0) { // regular function call (assuming each operator exists as function) From 9b10dc85351a2ef23e71d708b0595b69132aeb10 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:37:44 +0100 Subject: [PATCH 85/88] change pipe symbol from '.' to '#' --- packages/mondo/mondo.mjs | 7 +++---- packages/mondo/test/mondo.test.mjs | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 6ec63c8d0..fe48509c5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\./, + pipe: /^\#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6e9260f08..41558aaab 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -89,13 +89,13 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); + it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # within , within []', () => + expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); @@ -123,15 +123,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp # crush 4) ] # speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); - it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); + it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); + expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), From 1eb5f6a99505d6db5c72909ab4559c4b695597b7 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:38:43 +0100 Subject: [PATCH 86/88] format --- packages/mondo/mondo.mjs | 5 +- packages/mondo/test/mondo.test.mjs | 4 +- website/src/pages/learn/mondo-notation.mdx | 71 ++++++++++++---------- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fe48509c5..142b856dc 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 41558aaab..6393b10f9 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 2bc952341..ccf5b72b6 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,20 +14,25 @@ Here's an example: <8 16>) . *2 -.s "sine" .add (note [0 <12 24>]*2) -.dec(sine .range .2 2) .room .5 -.lpf(sine/3.range 120 400) -.lpenv(rand .range .5 4) -.lpq(perlin .range 5 12 . * 2) -.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> -.postgain .6 .delay .1 .clip 5 - -$ s [bd bd bd bd] .bank tr909.clip.5 -.ply<1 [1 [2 4]]> - -$ s oh*4 .press .bank tr909 .speed.8 -.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} + tune={`$ note (c2 # euclid <3 6 3> <8 16>) # *2 + # s "sine" # add (note [0 <12 24>]*2) + # dec(sine # range .2 2) + # room .5 + # lpf (sine/3 # range 120 400) + # lpenv (rand # range .5 4) + # lpq (perlin # range 5 12 # * 2) + # dist 1 # fm 4 # fmh 5.01 # fmdecay <.1 .2> + # postgain .6 # delay .1 # clip 5 + +$ s [bd bd bd bd] # bank tr909 # clip .5 + +# ply <1 [1 [2 4]]> + +$ s oh\*4 # press # bank tr909 # speed.8 + +# dec (<.02 .05>\*2 # add (saw/8 # range 0 1)) + +`} /> ## Mondo in the REPL @@ -95,16 +100,16 @@ Besides function calling with round parens, Mondo Notation has a lot in common w ## Chaining Functions -Similar to how it works in JS, we can chain functions calls with the "." operator: +Similar to how "." works in javascript (JS), we can chain functions calls with the "#" operator: *4 -.scale C4:minor -.jux rev -.dec .2 -.delay .5`} + # scale C4:minor + # jux rev + # dec .2 + # delay .5`} /> Here's the same written in JS: @@ -112,29 +117,29 @@ Here's the same written in JS: *4") -.scale("C4:minor") -.jux(rev) -.dec(.2) -.delay(.5)`} + # scale("C4:minor") + # jux(rev) + # dec(.2) + # delay(.5)`} /> ### Chaining Functions Locally -A function can be applied "locally" by wrapping it in round parens: +A function can be applied to a single element by wrapping it in round parens: - + in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: -here we can see much we can save when there's no boundary between mini notation and function calls! +here we can see how much we can save when there's no boundary between mini notation and function calls! ### Chaining Infix Operators Infix operators exist as regular functions, so they can be chained as well: - + In this case, the \*2 will be applied to the whole pattern. @@ -146,17 +151,17 @@ Some functions in strudel expect a function as input, for example: in mondo, the `x=>x.` can be shortened to: - + chaining works as expected: - + ## Strings You can use "double quotes" and 'single quotes' to get a string: - + ## Multiple Patterns @@ -165,9 +170,9 @@ The `$` sign can be used to separate multiple patterns: .voicing -.struct[x - - x - x - -].delay.5`} + tune={`$ s [bd rim [~ bd] rim] # bank tr707 +$ chord # voicing + # struct[x ~ ~ x ~ x ~ ~] # delay .5`} /> The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 98efd72ab496231f9d67e1bfc6aa60976f2373fb Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:59:05 +0100 Subject: [PATCH 87/88] delint --- packages/mondo/mondo.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 142b856dc..0f9da0eb9 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\#/, + pipe: /^#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } From 1b3b07842ec86ac65715803d3776fe5b6dc6f300 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 21:57:01 +0100 Subject: [PATCH 88/88] format again.. --- packages/mondo/mondo.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0f9da0eb9..73dbf1c7a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; }