From 479041ebb61d106a7c1140e7b77e08b9dfb715a0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 4 Oct 2024 16:00:36 +0100 Subject: [PATCH 1/2] paste in proto minimini --- packages/mini/mini.mjs | 584 ++++++++++++++++++++++++++--------------- 1 file changed, 371 insertions(+), 213 deletions(-) diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 86c372b87..336e05ca1 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -4,231 +4,394 @@ Copyright (C) 2022 Strudel contributors - see . */ -import * as krill from './krill-parser.js'; +// 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; +// "parser" +let token_types = { + open_polyrhythm: /^\[/, + close_polyrhythm: /^\]/, + open_polymeter: /^\{/, + close_polymeter: /^\}/, + open_slowpolymeter: /^\/, + subpat_delimiter: /^,/, + fast: /^\*/, + slow: /^\//, + // is there a better way without back-tracking for whole numbers? + // float: /^(\d*\.)?\d+/, + plain: /^[a-zA-Z0-9\.\#]+/, +}; -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`); - } +let token_offset = 0; +function next_token(code) { + for (let type in token_types) { + const match = code.match(token_types[type]); + if (match) { + return { type, value: match[0] }; } - } } - pat.__tactus_source = pat.__tactus_source || tactus_source; - return pat; -}; + throw new Error('could not match "' + code + '"'); +} -// 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)); +function tokenize(code) { + let tokens = []; + while (code.length > 0) { + code = code.trim(); + const token = next_token(code); + code = code.slice(token.value.length); + tokens.push(token); + } + return tokens; +} - 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; +class Parser { + parse(code) { + this.tokens = tokenize(code); + const subpats = this.parse_subpats(); + return { type: 'polymeter', subpats }; + return this.parse_expr(); + } + 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); + } + return token; + } + parse_step() { + let next = this.tokens[0]?.type; + if (next === 'open_polyrhythm') { + return this.parse_polyrhythm(); + } + if (next === 'open_polymeter') { + return this.parse_polymeter(); + } + 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; } - 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; + 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; } } } - if (with_tactus.length) { - pat.__tactus_source = true; - } - return pat; - } - case 'element': { - 1; - return enter(ast.source_); - } - case 'atom': { - if (ast.source_ === '~' || ast.source_ === '-') { - return strudel.silence; - } - if (!ast.location_) { - console.warn('no location for', ast); - return ast.source_; + return result; + } + parse_seq(...close_types) { + const steps = []; + while (! close_types.includes(this.tokens[0]?.type)) { + steps.push(this.parse_expr()); } - 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); + 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; + } } - const [from, to] = getLeafLocation(code, ast, offset); - return strudel.pure(value).withLoc(from, to); - } - 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; + 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 polyrhythm(...args); } -}; + if (tree.type === 'slowpolymeter') { + const args = tree.subpats.map((arg) => patternifyTree(arg)); + return s_polymeterSteps(1, ...args); + } + if (tree.type === 'polymeter') { + const args = tree.subpats.map((subpat) => patternifyTree(subpat)); + return s_polymeter(...args); + } + if (tree.type === 'fast') { + const step = patternifyTree(tree.step); + const factor = patternifyTree(tree.factor) + return fast(factor, step); + } + if (tree.type === 'slow') { + const step = patternifyTree(tree.step); + const factor = patternifyTree(tree.factor) + return slow(factor, step); + } + if (tree.type === 'plain') { + return tree.value; + } + if(tree.type === 'seq') { + const steps = tree.steps.map((step) => patternifyTree(step)); + return fastcat(...steps); + } +} +// let reify = (value) => (value instanceof Pattern ? value : pure(value)); -// 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)); -}; -// mini notation only (wraps in "") +// 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; +// }; + +// // 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)); + +// 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; +// } +// case 'element': { +// 1; +// return enter(ast.source_); +// } +// 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); +// } +// 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; +// } +// } + +// // 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}`); +// } +// }; + +// // 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 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); }; @@ -237,15 +400,10 @@ export const mini = (...strings) => { // 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); + const parser = new Parser(); + const tree = parser.parse(str); + const pat = patternifyTree(tree); + return reify(pat); }; export function minify(thing) { From 85ee97c9efcde0beb3dd983929bd6e90377108c0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 4 Oct 2024 18:52:40 +0100 Subject: [PATCH 2/2] minimini starting to work with some passing tests and highlights --- packages/mini/mini.mjs | 450 +++++++++---------------------- packages/mini/test/mini.test.mjs | 15 +- 2 files changed, 122 insertions(+), 343 deletions(-) diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 336e05ca1..7d8daa940 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -1,10 +1,9 @@ /* mini.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2024 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 * as krill from './krill-parser.js'; import * as strudel from '@strudel/core'; // "parser" @@ -23,389 +22,182 @@ let token_types = { plain: /^[a-zA-Z0-9\.\#]+/, }; -let token_offset = 0; -function next_token(code) { - for (let type in token_types) { - const match = code.match(token_types[type]); - if (match) { - return { type, value: match[0] }; - } +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 + '"'); } -function tokenize(code) { +function tokenize(code, global_offset) { let tokens = []; + token_offset = 0; while (code.length > 0) { - code = code.trim(); - const token = next_token(code); - code = code.slice(token.value.length); - tokens.push(token); + 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) { - this.tokens = tokenize(code); - const subpats = this.parse_subpats(); - return { type: 'polymeter', subpats }; - return this.parse_expr(); + 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); - } - return token; + const token = this.tokens.shift(); + this.offset += token.length; + if (token.type !== type) { + throw new Error('expected token type ' + type + ' got ' + token.type); + } + return token; } parse_step() { - let next = this.tokens[0]?.type; - if (next === 'open_polyrhythm') { - return this.parse_polyrhythm(); - } - if (next === 'open_polymeter') { - return this.parse_polymeter(); - } - 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 - ); + let next = this.tokens[0]?.type; + if (next === 'open_polyrhythm') { + return this.parse_polyrhythm(); + } + if (next === 'open_polymeter') { + return this.parse_polymeter(); + } + 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; - } + 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; + } + 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}; + 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; - } + const subpats = []; + while (true) { + subpats.push(this.parse_seq(close_type, 'subpat_delimiter')); + const next = this.tokens[0]?.type; + if (next) { + this.consume(next); } - return subpats; + if (next === close_type) { + break; + } + } + return subpats; } parse_polymeter() { - this.consume('open_polymeter'); - const subpats = this.parse_subpats('close_polymeter'); - return { type: 'polymeter', subpats }; + 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 }; + 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 }; + this.consume('open_slowpolymeter'); + const subpats = this.parse_subpats('close_slowpolymeter'); + return { type: 'slowpolymeter', subpats }; } } function patternifyTree(tree) { if (tree.type === 'polyrhythm') { - const args = tree.subpats.map((arg) => patternifyTree(arg)); - return polyrhythm(...args); + 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 s_polymeterSteps(1, ...args); + 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 s_polymeter(...args); + 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 fast(factor, step); + 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 slow(factor, step); + const step = patternifyTree(tree.step); + const factor = patternifyTree(tree.factor); + return strudel.slow(factor, step); } if (tree.type === 'plain') { - return tree.value; + 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 fastcat(...steps); + if (tree.type === 'seq') { + const steps = tree.steps.map((step) => patternifyTree(step)); + return strudel.fastcat(...steps); } } -// let reify = (value) => (value instanceof Pattern ? value : pure(value)); - - -// 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; -// }; - -// // 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)); - -// 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; -// } -// case 'element': { -// 1; -// return enter(ast.source_); -// } -// 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); -// } -// 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; -// } -// } - -// // 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}`); -// } -// }; - -// // 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; -// }; +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 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)); -// }; +// takes quoted mini string, returns all nodes that are leaves +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(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 parser = new Parser(); - const tree = parser.parse(str); - const pat = patternifyTree(tree); - return reify(pat); -}; - export function minify(thing) { if (typeof thing === 'string') { return mini(thing); diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 112edcb73..4cc45f266 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -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