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/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) : [];
};
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index a6c650e03..78631d3ec 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/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;
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 6e294129e..aa16ed5b2 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
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/packages/core/signal.mjs b/packages/core/signal.mjs
index 66ac8a5a6..5816403d3 100644
--- a/packages/core/signal.mjs
+++ b/packages/core/signal.mjs
@@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
* A sine signal between 0 and 1.
- *
* @return {Pattern}
* @example
* n(sine.segment(16).range(0,15))
@@ -100,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")
@@ -279,26 +277,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);
@@ -398,6 +396,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
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/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 });
diff --git a/packages/mondo/README.md b/packages/mondo/README.md
new file mode 100644
index 000000000..954f425a9
--- /dev/null
+++ b/packages/mondo/README.md
@@ -0,0 +1,39 @@
+# mondo
+
+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)
+
+## Example Usage
+
+```js
+import { MondoRunner } from 'mondolang'
+// 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`);
+ }
+ 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
new file mode 100644
index 000000000..73dbf1c7a
--- /dev/null
+++ b/packages/mondo/mondo.mjs
@@ -0,0 +1,471 @@
+/*
+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 MondoParser {
+ // these are the tokens we expect
+ token_types = {
+ comment: /^\/\/(.*?)(?=\n|$)/,
+ quotes_double: /^"(.*?)"/,
+ quotes_single: /^'(.*?)'/,
+ open_list: /^\(/,
+ close_list: /^\)/,
+ open_angle: /^,
+ close_angle: /^>/,
+ open_square: /^\[/,
+ close_square: /^\]/,
+ open_curly: /^\{/,
+ close_curly: /^\}/,
+ number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
+ op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
+ // dollar: /^\$/,
+ pipe: /^#/,
+ stack: /^[,$]/,
+ or: /^[|]/,
+ plain: /^[a-zA-Z0-9-~_^#]+/,
+ };
+ // matches next token
+ next_token(code, offset = 0) {
+ for (let type in this.token_types) {
+ const match = code.match(this.token_types[type]);
+ if (match) {
+ 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, 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 = 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, offset) {
+ this.code = code;
+ this.offset = offset;
+ this.tokens = this.tokenize(code, offset);
+ 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(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`);
+ // TODO: could we allow that? like (((((((( s bd
+ // return { type: 'list', children: [] };
+ }
+ let next = this.tokens[0]?.type;
+ if (next === 'open_list') {
+ return this.parse_list();
+ }
+ if (next === 'open_angle') {
+ return this.parse_angle();
+ }
+ if (next === 'open_square') {
+ return this.parse_square();
+ }
+ if (next === 'open_curly') {
+ return this.parse_curly();
+ }
+ return this.consume(next);
+ }
+ // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']]
+ split_children(children, split_type) {
+ const chunks = [];
+ while (true) {
+ let splitIndex = children.findIndex((child) => child.type === split_type);
+ if (splitIndex === -1) break;
+ const chunk = children.slice(0, splitIndex);
+ chunks.push(chunk);
+ 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 === 1) {
+ return next(children);
+ }
+ // collect args of stack function
+ 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)
+ unwrap_children(children) {
+ if (children.length === 1) {
+ return children[0].children;
+ }
+ return children;
+ }
+ desugar_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.`);
+ 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)
+ children[opIndex] = op;
+ continue;
+ }
+ // convert infix to prefix notation
+ const left = children[opIndex - 1];
+ const right = children[opIndex + 1];
+ if (left.type === 'pipe') {
+ // "x !* 2" => (* 2 x)
+ children[opIndex] = op;
+ continue;
+ }
+ 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);
+ }
+ return children;
+ }
+ get_lambda(args, children) {
+ // (.fast 2) = (fn (_) (fast _ 2))
+ children = this.desugar(children);
+ const body = children.length === 1 ? children[0] : { type: 'list', children };
+ 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]) {
+ 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) {
+ const arg = { type: 'plain', value: '_' };
+ return this.get_lambda([arg], [arg, ...children]);
+ }
+ // 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];
+ }
+ 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);
+ const node = { type: 'list', children };
+ if (begin !== undefined) {
+ node.loc = [begin, end];
+ node.raw = this.code.slice(begin, end);
+ }
+ return node;
+ }
+ desugar(children, 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];
+ }
+ children = this.desugar_ops(children);
+ // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
+ children = this.desugar_pipes(children);
+ return children;
+ }),
+ );
+ return children;
+ }
+ parse_list() {
+ let node = this.parse_pair('open_list', 'close_list');
+ node.children = this.desugar(node.children);
+ return node;
+ }
+ parse_angle() {
+ 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 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 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
+ const token = this.tokens.shift();
+ if (token.type !== type) {
+ throw new Error(`expected token type ${type}, got ${token.type}`);
+ }
+ return token;
+ }
+ get_locations(code, offset = 0) {
+ let walk = (ast, locations = []) => {
+ if (ast.type === 'list') {
+ return ast.children.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) {
+ 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 MondoRunner {
+ constructor({ evaluator } = {}) {
+ this.parser = new MondoParser();
+ 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) {
+ if (!condition) {
+ throw new Error(error);
+ }
+ }
+ run(code, scope, offset = 0) {
+ const ast = this.parser.parse(code, offset);
+ //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') {
+ // (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}`);
+ }
+ const name = ast.children[1].value;
+ const body = this.evaluate(ast.children[2], scope);
+ 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
+ const [_, formalArgs, ...body] = ast.children;
+ return (...args) => {
+ const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]]));
+ const closure = {
+ ...scope,
+ ...params,
+ };
+ // 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) {
+ // evaluate all children before evaluating list (dont mutate!!!)
+ 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);
+ }
+ 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);
+ ast.type = 'string';
+ }
+ return this.evaluator(ast, scope);
+ }
+ evaluate(ast, scope = {}) {
+ if (ast.type !== 'list') {
+ return this.evaluate_leaf(ast, scope);
+ }
+ const name = ast.children[0]?.value;
+ if (name === 'fn') {
+ return this.evaluate_lambda(ast, scope);
+ }
+ if (name === 'match') {
+ return this.evaluate_match(ast, scope);
+ }
+ 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);
+ }
+ return this.evaluate_list(ast, scope);
+ }
+}
diff --git a/packages/mondo/package.json b/packages/mondo/package.json
new file mode 100644
index 000000000..277bddb1c
--- /dev/null
+++ b/packages/mondo/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "mondolang",
+ "version": "1.1.0",
+ "description": "a language for functional composition that translates to js",
+ "main": "mondo.mjs",
+ "type": "module",
+ "publishConfig": {
+ "main": "dist/mondo.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/mondo/README.md",
+ "devDependencies": {
+ "vite": "^6.0.11",
+ "vitest": "^3.0.4"
+ }
+}
diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs
new file mode 100644
index 000000000..6393b10f9
--- /dev/null
+++ b/packages/mondo/test/mondo.test.mjs
@@ -0,0 +1,978 @@
+/*
+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 { MondoParser, printAst, MondoRunner } from '../mondo.mjs';
+
+const parser = new MondoParser();
+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 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],
+ ]));
+});
+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' }] }));
+ 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' },
+ ],
+ }));
+ it('should parse comments', () =>
+ expect(p('a // hello')).toEqual({
+ type: 'list',
+ children: [
+ { type: 'plain', value: 'a' },
+ { type: 'comment', value: '// hello' },
+ ],
+ }));
+});
+
+let desguar = (a) => {
+ return printAst(parser.parse(a), true);
+};
+
+describe('mondo sugar', () => {
+ 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 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))'));
+
+ 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', () =>
+ 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('(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))'));
+ 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(
+ '(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 lambda with pipe', () =>
+ 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', () =>
+ expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */
+});
+
+describe('mondo arithmetic', () => {
+ let multi =
+ (op) =>
+ (init, ...rest) =>
+ rest.reduce((acc, arg) => op(acc, arg), init);
+
+ 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),
+ mod: 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,
+ sin: Math.sin,
+ cos: Math.cos,
+ PI: Math.PI,
+ cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])],
+ car: (pair) => pair[0],
+ 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(' '));
+ },
+ };
+ function evaluator(node, scope) {
+ if (node.type !== 'list') {
+ // is leaf
+ return scope[node.value] ?? lib[node.value] ?? node.value;
+ }
+ // is list
+ const [fn, ...args] = node.children;
+ if (typeof fn !== 'function') {
+ throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`);
+ }
+ return fn(...args);
+ }
+ const runner = new MondoRunner({ evaluator });
+ 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 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 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 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 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 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 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 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 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 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 guess x) (average guess (/ x guess)))`, scope)).toEqual(0));
+ it('sicp 31.1', () =>
+ expect(
+ evaluate(
+ `(def (sqrtiter guess x) (if (goodenuf guess x)
+ guess
+ (sqrtiter (improve guess x) 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));
+ // 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));
+
+ // lexical scoping
+ it('sicp 39.1', () =>
+ expect(
+ evaluate(
+ `
+(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));
+
+ // recursive fac
+ 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
+ it('sicp 41.3', () =>
+ expect(
+ evaluate(
+ `
+(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));
+
+ // 46.1
+ /* (def (+ a b)
+(if (= a 0) b (inc (+ (dec a) b))))
+(def (+ a b)
+(if (= a 0) b (+ (dec a) (inc b)))) */
+
+ // Exercise 1.10
+ // Ackermann’s function
+ it('sicp 47.1', () =>
+ expect(
+ evaluate(
+ `
+(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))))))
+`,
+ 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 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)
+ `,
+ scope,
+ ),
+ ).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
+ it('sicp 79.1', () =>
+ expect(
+ evaluate(
+ `
+(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));
+
+ // 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));
+
+ // 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));
+
+ // 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));
+ // 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'));
+ // 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]));
+});
diff --git a/packages/mondo/vite.config.js b/packages/mondo/vite.config.js
new file mode 100644
index 000000000..19e53d7f3
--- /dev/null
+++ b/packages/mondo/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, 'mondo.mjs'),
+ formats: ['es'],
+ fileName: (ext) => ({ es: 'mondo.mjs' })[ext],
+ },
+ rollupOptions: {
+ // external: [...Object.keys(dependencies)],
+ },
+ target: 'esnext',
+ },
+});
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..3731e8cee
--- /dev/null
+++ b/packages/mondough/mondough.mjs
@@ -0,0 +1,129 @@
+import {
+ strudelScope,
+ reify,
+ fast,
+ slow,
+ seq,
+ stepcat,
+ extend,
+ expand,
+ pace,
+ chooseIn,
+ degradeBy,
+ silence,
+} from '@strudel/core';
+import { registerLanguage } from '@strudel/transpiler';
+import { MondoRunner } from 'mondolang';
+
+const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
+
+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 = (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;
+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['?'] = 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
+
+function evaluator(node, scope) {
+ const { type } = node;
+ // node is list
+ 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;
+ }
+ // name is expected to be a pattern of functions!
+ const first = name.firstCycle(true)[0];
+ const type = typeof first?.value;
+ if (type !== 'function') {
+ throw new Error(`[mondough] expected function, got "${first?.value}"`);
+ }
+ return name
+ .fmap((fn) => {
+ if (typeof fn !== 'function') {
+ throw new Error(`[mondough] "${fn}" is not a function b`);
+ }
+ return fn(...args);
+ })
+ .innerJoin();
+ }
+ // 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];
+ // 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') {
+ // 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);
+ }
+ if (node.loc) {
+ pat = pat.withLoc(node.loc[0], node.loc[1]);
+ }
+ return pat;
+}
+
+let runner = new MondoRunner({ evaluator });
+
+export function mondo(code, offset = 0) {
+ if (Array.isArray(code)) {
+ code = code.join('');
+ }
+ const pat = runner.run(code, undefined, offset);
+ return pat.markcss('color: var(--caret,--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,
+});
+
+// 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',
+ getLocations,
+}); */
diff --git a/packages/mondough/package.json b/packages/mondough/package.json
new file mode 100644
index 000000000..a034424c0
--- /dev/null
+++ b/packages/mondough/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@strudel/mondo",
+ "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:*",
+ "@strudel/transpiler": "workspace:*",
+ "mondolang": "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/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index a1dc30b7c..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()];
}
@@ -552,6 +556,9 @@ export const superdough = async (value, t, hapDuration) => {
let audioNodes = [];
+ if (['-', '~'].includes(s)) {
+ return;
+ }
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs
index 8bb47bb7d..f43390dfb 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,
@@ -31,6 +31,12 @@ function destroyAudioWorkletNode(node) {
}
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() {
@@ -247,6 +253,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) {
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/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs
index 2e566305f..17f5ae30b 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;
@@ -19,14 +29,33 @@ 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 = [];
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) {
@@ -120,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 },
@@ -219,12 +255,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 +302,18 @@ function tidalWithLocation(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,
+ };
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a23c80924..7aecd35bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -347,6 +347,37 @@ 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/mondough:
+ dependencies:
+ '@strudel/core':
+ specifier: workspace:*
+ version: link:../core
+ '@strudel/transpiler':
+ specifier: workspace:*
+ version: link:../transpiler
+ mondolang:
+ specifier: workspace:*
+ version: link:../mondo
+ 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':
@@ -683,6 +714,9 @@ importers:
'@strudel/mini':
specifier: workspace:*
version: link:../packages/mini
+ '@strudel/mondo':
+ specifier: workspace:*
+ version: link:../packages/mondough
'@strudel/motion':
specifier: workspace:*
version: link:../packages/motion
@@ -2975,6 +3009,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==}
@@ -3103,6 +3141,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==}
@@ -3337,6 +3378,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'}
@@ -4378,6 +4422,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'}
@@ -4439,6 +4486,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'}
@@ -5586,6 +5638,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'}
@@ -5827,6 +5883,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'}
@@ -6725,6 +6784,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'}
@@ -7155,6 +7218,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'}
@@ -7173,6 +7241,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==}
@@ -7555,6 +7626,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==}
@@ -10438,6 +10513,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
@@ -10647,6 +10725,9 @@ snapshots:
async-function@1.0.0: {}
+ async@0.2.10:
+ optional: true
+
async@3.2.6: {}
asynckit@0.4.0: {}
@@ -10904,6 +10985,8 @@ snapshots:
claviature@0.1.0: {}
+ cldr-plurals@1.0.0: {}
+
clean-stack@2.2.0: {}
cli-boxes@3.0.0: {}
@@ -12046,6 +12129,8 @@ snapshots:
minipass: 4.2.8
path-scurry: 1.11.1
+ globalize@0.1.1: {}
+
globals@11.12.0: {}
globals@14.0.0: {}
@@ -12112,6 +12197,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
@@ -13695,6 +13786,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: {}
@@ -13987,6 +14085,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
@@ -15094,6 +15196,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: {}
@@ -15561,6 +15668,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
@@ -15577,6 +15691,8 @@ snapshots:
underscore@1.13.7: {}
+ underscore@1.5.2: {}
+
undici-types@6.20.0: {}
unenv@1.10.0:
@@ -15957,6 +16073,8 @@ snapshots:
word-wrap@1.2.5: {}
+ wordwrap@0.0.3: {}
+
wordwrap@1.0.0: {}
workbox-background-sync@7.0.0:
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index 2d88d8355..9217ff3ba 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -7821,9 +7821,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 ]",
@@ -7844,9 +7842,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 ]",
@@ -8301,9 +8297,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 ]",
@@ -8324,9 +8318,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/package.json b/website/package.json
index 8e980f1ca..499758147 100644
--- a/website/package.json
+++ b/website/package.json
@@ -42,6 +42,7 @@
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
+ "@strudel/mondo": "workspace:*",
"@strudel/xen": "workspace:*",
"@supabase/supabase-js": "^2.48.1",
"@tailwindcss/forms": "^0.5.10",
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);
diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx
new file mode 100644
index 000000000..ccf5b72b6
--- /dev/null
+++ b/website/src/pages/learn/mondo-notation.mdx
@@ -0,0 +1,178 @@
+---
+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:
+
+ <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
+
+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:
+
+
+
+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:
+
+### 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)
+- ! => [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)
+- , => [stack](/learn/factories/#stack)
+- | => [chooseIn](/learn/random-modifiers/#choose)
+
+### Example
+
+`}
+/>
+
+## Chaining Functions
+
+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`}
+/>
+
+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 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 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.
+
+### 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.
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})`;
diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs
index f49dd568f..451c1d0bb 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/mondo'),
];
if (isTauri()) {
modules = modules.concat([