Compare commits

...

2 Commits

Author SHA1 Message Date
alex 85ee97c9ef minimini starting to work with some passing tests and highlights 2024-10-04 18:52:40 +01:00
alex 479041ebb6 paste in proto minimini 2024-10-04 16:00:36 +01:00
2 changed files with 174 additions and 237 deletions
+173 -223
View File
@@ -1,253 +1,203 @@
/*
mini.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
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 <https://www.gnu.org/licenses/>.
*/
import * as krill from './krill-parser.js';
import * as strudel from '@strudel/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003;
const applyOptions = (parent, enter) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
const tactus_source = pat.__tactus_source;
if (ops) {
for (const op of ops) {
switch (op.type_) {
case 'stretch': {
const legalTypes = ['fast', 'slow'];
const { type, amount } = op.arguments_;
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'replicate': {
const { amount } = op.arguments_;
pat = strudel.reify(pat);
pat = pat._repeatCycles(amount)._fast(amount);
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
} else {
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
}
break;
}
case 'degradeBy': {
pat = strudel
.reify(pat)
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
break;
}
case 'tail': {
const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
break;
}
case 'range': {
const friend = enter(op.arguments_.element);
pat = strudel.reify(pat);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
start < stop ? start + index * step : start - index * step,
);
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
pat = range(pat, friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
}
}
}
pat.__tactus_source = pat.__tactus_source || tactus_source;
return pat;
// "parser"
let token_types = {
open_polyrhythm: /^\[/,
close_polyrhythm: /^\]/,
open_polymeter: /^\{/,
close_polymeter: /^\}/,
open_slowpolymeter: /^\</,
close_slowpolymeter: /^\>/,
subpat_delimiter: /^,/,
fast: /^\*/,
slow: /^\//,
// is there a better way without back-tracking for whole numbers?
// float: /^(\d*\.)?\d+/,
plain: /^[a-zA-Z0-9\.\#]+/,
};
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
// resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source);
let pat;
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
let token_offset;
function next_token(code, global_offset) {
for (let type in token_types) {
const match = code.match(token_types[type]);
if (match) {
const leftpad = match[0].length - match[0].trimLeft().length;
const rightpad = match[0].length - match[0].trimRight().length;
const result = {
type,
value: match[0],
location: [token_offset + leftpad + global_offset, token_offset + match[0].length + global_offset - rightpad],
};
token_offset += match[0].length;
return result;
}
}
throw new Error('could not match "' + code + '"');
}
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
pat = strudel.stack(...aligned);
break;
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce(
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
strudel.Fraction(0),
);
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
}
if (with_tactus.length) {
pat.__tactus_source = true;
}
return pat;
function tokenize(code, global_offset) {
let tokens = [];
token_offset = 0;
while (code.length > 0) {
const trimmed = code.trim();
token_offset += code.length - trimmed.length;
code = trimmed;
const token = next_token(code, global_offset);
code = code.slice(token.value.length);
tokens.push(token);
}
return tokens;
}
class Parser {
parse(code, global_offset) {
this.tokens = tokenize(code, global_offset);
const subpats = this.parse_subpats();
return { type: 'polymeter', subpats };
}
consume(type) {
const token = this.tokens.shift();
this.offset += token.length;
if (token.type !== type) {
throw new Error('expected token type ' + type + ' got ' + token.type);
}
case 'element': {
1;
return enter(ast.source_);
return token;
}
parse_step() {
let next = this.tokens[0]?.type;
if (next === 'open_polyrhythm') {
return this.parse_polyrhythm();
}
case 'atom': {
if (ast.source_ === '~' || ast.source_ === '-') {
return strudel.silence;
}
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
if (offset === -1) {
// skip location handling (used when getting leaves to avoid confusion)
return strudel.pure(value);
}
const [from, to] = getLeafLocation(code, ast, offset);
return strudel.pure(value).withLoc(from, to);
if (next === 'open_polymeter') {
return this.parse_polymeter();
}
case 'stretch':
return enter(ast.source_).slow(enter(ast.arguments_.amount));
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return strudel.silence;
if (next === 'open_slowpolymeter') {
return this.parse_slowpolymeter();
}
if (next === 'plain') {
return this.consume('plain');
}
throw new Error('unexpected token ' + this.tokens[0]?.value + ' of type ' + this.tokens[0]?.type);
}
parse_expr() {
let result = this.parse_step();
let looking = true;
while (looking) {
let next = this.tokens[0]?.type;
if (!next) {
looking = false;
} else {
switch (next) {
case 'fast':
case 'slow':
this.consume(next);
const factor = this.parse_step();
result = { type: next, factor, step: result };
break;
default:
looking = false;
}
}
}
return result;
}
parse_seq(...close_types) {
const steps = [];
while (!close_types.includes(this.tokens[0]?.type)) {
steps.push(this.parse_expr());
}
return { type: 'seq', steps };
}
parse_subpats(close_type) {
const subpats = [];
while (true) {
subpats.push(this.parse_seq(close_type, 'subpat_delimiter'));
const next = this.tokens[0]?.type;
if (next) {
this.consume(next);
}
if (next === close_type) {
break;
}
}
return subpats;
}
parse_polymeter() {
this.consume('open_polymeter');
const subpats = this.parse_subpats('close_polymeter');
return { type: 'polymeter', subpats };
}
parse_polyrhythm() {
this.consume('open_polyrhythm');
const subpats = this.parse_subpats('close_polyrhythm');
return { type: 'polyrhythm', subpats };
}
parse_slowpolymeter() {
this.consume('open_slowpolymeter');
const subpats = this.parse_subpats('close_slowpolymeter');
return { type: 'slowpolymeter', subpats };
}
}
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
// value is expected without quotes!
const { start, end } = leaf.location_;
const actual = code?.split('').slice(start.offset, end.offset).join('');
// make sure whitespaces are not part of the highlight
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
};
// takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => {
try {
return krill.parse(code);
} catch (error) {
const region = [error.location.start.offset + start, error.location.end.offset + start];
const line = userCode.slice(0, region[0]).split('\n').length;
throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
function patternifyTree(tree) {
if (tree.type === 'polyrhythm') {
const args = tree.subpats.map((arg) => patternifyTree(arg));
return strudel.polyrhythm(...args);
}
if (tree.type === 'slowpolymeter') {
const args = tree.subpats.map((arg) => patternifyTree(arg));
return strudel.s_polymeterSteps(1, ...args);
}
if (tree.type === 'polymeter') {
const args = tree.subpats.map((subpat) => patternifyTree(subpat));
return strudel.s_polymeter(...args);
}
if (tree.type === 'fast') {
const step = patternifyTree(tree.step);
const factor = patternifyTree(tree.factor);
return strudel.fast(factor, step);
}
if (tree.type === 'slow') {
const step = patternifyTree(tree.step);
const factor = patternifyTree(tree.factor);
return strudel.slow(factor, step);
}
if (tree.type === 'plain') {
return strudel.pure(tree.value).withLoc(tree.location[0], tree.location[1]);
}
if (tree.type === 'seq') {
const steps = tree.steps.map((step) => patternifyTree(step));
return strudel.fastcat(...steps);
}
}
const parser = new Parser();
export const m = (code, global_offset) => {
const tree = parser.parse(code, global_offset + 1);
const pat = patternifyTree(tree);
return strudel.reify(pat);
};
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code, start, userCode) => {
const ast = mini2ast(code, start, userCode);
let leaves = [];
patternifyAST(
ast,
code,
(node) => {
if (node.type_ === 'atom') {
leaves.push(node);
}
},
-1,
);
return leaves;
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, start = 0, userCode) => {
return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start));
export const getLeafLocations = (code, start) => {
code = code.replace(/^"/, '');
code = code.replace(/"$/, '');
const leaves = tokenize(code, start).filter((x) => x.type == 'plain');
return leaves.map((x) => x.location.map((y) => y + 1));
};
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code);
});
const pats = strings.map(m);
return strudel.sequence(...pats);
};
// turns str mini string (without quotes) into pattern
// offset is the position of the mini string in the JS code
// each leaf node will get .withLoc added
// this function is used by the transpiler for double quoted strings
export const m = (str, offset) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code, null, offset);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = mini2ast(string);
return patternifyAST(ast, string);
};
export function minify(thing) {
if (typeof thing === 'string') {
return mini(thing);
+1 -14
View File
@@ -221,19 +221,6 @@ describe('mini', () => {
});
});
describe('getLeafLocation', () => {
it('gets location of leaf nodes', () => {
const code = '"bd sd"';
const ast = mini2ast(code);
const bd = ast.source_[0].source_;
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
const sd = ast.source_[1].source_;
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
});
});
describe('getLeafLocations', () => {
it('gets locations of leaf nodes', () => {
expect(getLeafLocations('"bd sd"')).toEqual([
@@ -242,9 +229,9 @@ describe('getLeafLocations', () => {
]);
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
[1, 3], // bd columns
[4, 5], // "2" columns
[7, 9], // sd columns
[10, 12], // cp columns
[4, 5], // "2" columns
]);
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
[1, 3], // bd columns