[BREAKING] Feat: quotation is now using ()

This commit is contained in:
2026-02-28 20:25:59 +01:00
parent f88691c09c
commit 1ba946ebe6
30 changed files with 218 additions and 169 deletions

View File

@@ -1,6 +1,9 @@
# Uncomment to use local doux for development # Uncomment to use local doux for development
paths = ["/Users/bubo/doux"] paths = ["/Users/bubo/doux"]
[env]
MACOSX_DEPLOYMENT_TARGET = "12.0"
[alias] [alias]
xtask = "run --package xtask --release --" xtask = "run --package xtask --release --"

View File

@@ -7,6 +7,7 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -55,7 +55,7 @@ doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundf
rusty_link = "0.4" rusty_link = "0.4"
ratatui = "0.30" ratatui = "0.30"
crossterm = "0.29" crossterm = "0.29"
cpal = { version = "0.17", features = ["jack"], optional = true } cpal = { version = "0.17", optional = true }
clap = { version = "4", features = ["derive"], optional = true } clap = { version = "4", features = ["derive"], optional = true }
rand = "0.8" rand = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -83,6 +83,9 @@ rustc-hash = { version = "2", optional = true }
image = { version = "0.25", default-features = false, features = ["png"], optional = true } image = { version = "0.25", default-features = false, features = ["png"], optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
cpal = { version = "0.17", optional = true, features = ["jack"] }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"
@@ -109,3 +112,4 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
copyright = "Copyright (c) 2025 Raphaël Forment" copyright = "Copyright (c) 2025 Raphaël Forment"
category = "Music" category = "Music"
short_description = "Forth-based music sequencer" short_description = "Forth-based music sequencer"
minimum_system_version = "12.0"

View File

@@ -31,7 +31,7 @@ fn tokenize(input: &str) -> Vec<Token> {
continue; continue;
} }
if c == '(' || c == ')' { if c == '{' || c == '}' {
chars.next(); chars.next();
continue; continue;
} }
@@ -133,7 +133,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))), Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
Token::Word(w, span) => { Token::Word(w, span) => {
let word = w.as_str(); let word = w.as_str();
if word == "{" { if word == "(" {
let (quote_ops, consumed, end_span) = let (quote_ops, consumed, end_span) =
compile_quotation(&tokens[i + 1..], dict)?; compile_quotation(&tokens[i + 1..], dict)?;
i += consumed; i += consumed;
@@ -142,8 +142,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
end: end_span.end, end: end_span.end,
}; };
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span))); ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
} else if word == "}" { } else if word == ")" {
return Err("unexpected }".into()); return Err("unexpected )".into());
} else if word == "[" { } else if word == "[" {
let (bracket_ops, consumed, end_span) = let (bracket_ops, consumed, end_span) =
compile_bracket(&tokens[i + 1..], dict)?; compile_bracket(&tokens[i + 1..], dict)?;
@@ -203,8 +203,8 @@ fn compile_quotation(
for (i, tok) in tokens.iter().enumerate() { for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok { if let Token::Word(w, _) = tok {
match w.as_str() { match w.as_str() {
"{" => depth += 1, "(" => depth += 1,
"}" => { ")" => {
depth -= 1; depth -= 1;
if depth == 0 { if depth == 0 {
end_idx = Some(i); end_idx = Some(i);
@@ -216,7 +216,7 @@ fn compile_quotation(
} }
} }
let end_idx = end_idx.ok_or("missing }")?; let end_idx = end_idx.ok_or("missing )")?;
let end_span = match &tokens[end_idx] { let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span, Token::Word(_, span) => *span,
_ => unreachable!(), _ => unreachable!(),

View File

@@ -502,7 +502,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic", category: "Logic",
stack: "(true-quot false-quot bool --)", stack: "(true-quot false-quot bool --)",
desc: "Execute true-quot if true, else false-quot", desc: "Execute true-quot if true, else false-quot",
example: "{ 1 } { 2 } coin ifelse", example: "( 1 ) ( 2 ) coin ifelse",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -512,7 +512,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic", category: "Logic",
stack: "(..quots n --)", stack: "(..quots n --)",
desc: "Execute nth quotation (0-indexed)", desc: "Execute nth quotation (0-indexed)",
example: "{ 1 } { 2 } { 3 } 2 select => 3", example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
compile: Simple, compile: Simple,
varargs: true, varargs: true,
}, },
@@ -522,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic", category: "Logic",
stack: "(quot bool --)", stack: "(quot bool --)",
desc: "Execute quotation if true", desc: "Execute quotation if true",
example: "{ 2 distort } 0.5 chance ?", example: "( 2 distort ) 0.5 chance ?",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -532,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic", category: "Logic",
stack: "(quot bool --)", stack: "(quot bool --)",
desc: "Execute quotation if false", desc: "Execute quotation if false",
example: "{ 1 distort } 0.5 chance !?", example: "( 1 distort ) 0.5 chance !?",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -542,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic", category: "Logic",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation unconditionally", desc: "Execute quotation unconditionally",
example: "{ 2 * } apply", example: "( 2 * ) apply",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -553,7 +553,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Control", category: "Control",
stack: "(n quot --)", stack: "(n quot --)",
desc: "Execute quotation n times, @i holds current index", desc: "Execute quotation n times, @i holds current index",
example: "4 { @i . } times => 0 1 2 3", example: "4 ( @i . ) times => 0 1 2 3",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },

View File

@@ -60,7 +60,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot prob --)", stack: "(quot prob --)",
desc: "Execute quotation with probability (0.0-1.0)", desc: "Execute quotation with probability (0.0-1.0)",
example: "{ 2 distort } 0.75 chance", example: "( 2 distort ) 0.75 chance",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -70,7 +70,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot pct --)", stack: "(quot pct --)",
desc: "Execute quotation with probability (0-100)", desc: "Execute quotation with probability (0-100)",
example: "{ 2 distort } 75 prob", example: "( 2 distort ) 75 prob",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -150,7 +150,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Always execute quotation", desc: "Always execute quotation",
example: "{ 2 distort } always", example: "( 2 distort ) always",
compile: Probability(1.0), compile: Probability(1.0),
varargs: false, varargs: false,
}, },
@@ -160,7 +160,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Never execute quotation", desc: "Never execute quotation",
example: "{ 2 distort } never", example: "( 2 distort ) never",
compile: Probability(0.0), compile: Probability(0.0),
varargs: false, varargs: false,
}, },
@@ -170,7 +170,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation 75% of the time", desc: "Execute quotation 75% of the time",
example: "{ 2 distort } often", example: "( 2 distort ) often",
compile: Probability(0.75), compile: Probability(0.75),
varargs: false, varargs: false,
}, },
@@ -180,7 +180,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation 50% of the time", desc: "Execute quotation 50% of the time",
example: "{ 2 distort } sometimes", example: "( 2 distort ) sometimes",
compile: Probability(0.5), compile: Probability(0.5),
varargs: false, varargs: false,
}, },
@@ -190,7 +190,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation 25% of the time", desc: "Execute quotation 25% of the time",
example: "{ 2 distort } rarely", example: "( 2 distort ) rarely",
compile: Probability(0.25), compile: Probability(0.25),
varargs: false, varargs: false,
}, },
@@ -200,7 +200,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation 10% of the time", desc: "Execute quotation 10% of the time",
example: "{ 2 distort } almostNever", example: "( 2 distort ) almostNever",
compile: Probability(0.1), compile: Probability(0.1),
varargs: false, varargs: false,
}, },
@@ -210,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability", category: "Probability",
stack: "(quot --)", stack: "(quot --)",
desc: "Execute quotation 90% of the time", desc: "Execute quotation 90% of the time",
example: "{ 2 distort } almostAlways", example: "( 2 distort ) almostAlways",
compile: Probability(0.9), compile: Probability(0.9),
varargs: false, varargs: false,
}, },
@@ -221,7 +221,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot n --)", stack: "(quot n --)",
desc: "Execute quotation every nth iteration", desc: "Execute quotation every nth iteration",
example: "{ 2 distort } 4 every", example: "( 2 distort ) 4 every",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -231,7 +231,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot n --)", stack: "(quot n --)",
desc: "Execute quotation on all iterations except every nth", desc: "Execute quotation on all iterations except every nth",
example: "{ 2 distort } 4 except", example: "( 2 distort ) 4 except",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -241,7 +241,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot n offset --)", stack: "(quot n offset --)",
desc: "Execute quotation every nth iteration with phase offset", desc: "Execute quotation every nth iteration with phase offset",
example: "{ snare } 4 2 every+ => fires at iter 2, 6, 10...", example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -251,7 +251,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot n offset --)", stack: "(quot n offset --)",
desc: "Skip quotation every nth iteration with phase offset", desc: "Skip quotation every nth iteration with phase offset",
example: "{ snare } 4 2 except+ => skips at iter 2, 6, 10...", example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -261,7 +261,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot k n --)", stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over step runs", desc: "Execute quotation using Euclidean distribution over step runs",
example: "{ 2 distort } 3 8 bjork", example: "( 2 distort ) 3 8 bjork",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -271,7 +271,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time", category: "Time",
stack: "(quot k n --)", stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over pattern iterations", desc: "Execute quotation using Euclidean distribution over pattern iterations",
example: "{ 2 distort } 3 8 pbjork", example: "( 2 distort ) 3 8 pbjork",
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
@@ -456,7 +456,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Desktop", category: "Desktop",
stack: "(-- bool)", stack: "(-- bool)",
desc: "1 when mouse button held, 0 otherwise", desc: "1 when mouse button held, 0 otherwise",
example: "mdown { \"crash\" s . } ?", example: "mdown ( \"crash\" s . ) ?",
compile: Context("mdown"), compile: Context("mdown"),
varargs: false, varargs: false,
}, },
@@ -487,7 +487,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Generator", category: "Generator",
stack: "(quot n -- results...)", stack: "(quot n -- results...)",
desc: "Execute quotation n times, push all results", desc: "Execute quotation n times, push all results",
example: "{ 1 6 rand } 4 gen => 4 random values", example: "( 1 6 rand ) 4 gen => 4 random values",
compile: Simple, compile: Simple,
varargs: true, varargs: true,
}, },

View File

@@ -7,11 +7,11 @@
"steps": [ "steps": [
{ {
"i": 0, "i": 0,
"script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n." "script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n."
}, },
{ {
"i": 4, "i": 4,
"script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n{ . } 2 every" "script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n( . ) 2 every"
} }
], ],
"length": 16, "length": 16,

View File

@@ -54,7 +54,7 @@ Four basic types of values can live on the stack:
- **Integers**: `42`, `-7`, `0` - **Integers**: `42`, `-7`, `0`
- **Floats**: `0.5`, `3.14`, `-1.0` - **Floats**: `0.5`, `3.14`, `-1.0`
- **Strings**: `"kick"`, `"hello"` - **Strings**: `"kick"`, `"hello"`
- **Quotations**: `{ dup + }` (code as data) - **Quotations**: `( dup + )` (code as data)
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`. Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.

View File

@@ -29,22 +29,22 @@ These are compiled directly into branch instructions. For that reason, these wor
When you already have a quotation, `?` executes it if the condition is truthy: When you already have a quotation, `?` executes it if the condition is truthy:
```forth ```forth
{ 0.4 verb } coin ? ( 0.4 verb ) coin ?
saw s c4 note 0.5 gain . ;; reverb on half the hits saw s c4 note 0.5 gain . ;; reverb on half the hits
``` ```
`!?` is the opposite — executes when falsy: `!?` is the opposite — executes when falsy:
```forth ```forth
{ 0.2 gain } coin !? ( 0.2 gain ) coin !?
saw s c4 note . ;; quiet on half the hits saw s c4 note . ;; quiet on half the hits
``` ```
These pair well with `chance`, `prob`, and the other probability words: These pair well with `chance`, `prob`, and the other probability words:
```forth ```forth
{ 0.5 verb } 0.3 chance ? ;; occasional reverb wash ( 0.5 verb ) 0.3 chance ? ;; occasional reverb wash
{ 12 + } fill ? ;; octave up during fills ( 12 + ) fill ? ;; octave up during fills
``` ```
## ifelse ## ifelse
@@ -52,14 +52,14 @@ These pair well with `chance`, `prob`, and the other probability words:
Two quotations, one condition. The true branch comes first: Two quotations, one condition. The true branch comes first:
```forth ```forth
{ c3 note } { c4 note } coin ifelse ( c3 note ) ( c4 note ) coin ifelse
saw s 0.6 gain . ;; bass or lead, coin flip saw s 0.6 gain . ;; bass or lead, coin flip
``` ```
Reads naturally: "c3 or c4, depending on the coin." Reads naturally: "c3 or c4, depending on the coin."
```forth ```forth
{ 0.8 gain } { 0.3 gain } fill ifelse ( 0.8 gain ) ( 0.3 gain ) fill ifelse
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
``` ```
@@ -68,7 +68,7 @@ tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
Choose the nth option from a list of quotations: Choose the nth option from a list of quotations:
```forth ```forth
{ c4 } { e4 } { g4 } { b4 } iter 4 mod select ( c4 ) ( e4 ) ( g4 ) ( b4 ) iter 4 mod select
note sine s 0.5 decay . note sine s 0.5 decay .
``` ```
@@ -79,7 +79,7 @@ Four notes cycling through a major seventh chord, one per pattern iteration. The
When you have a quotation and want to execute it unconditionally, use `apply`: When you have a quotation and want to execute it unconditionally, use `apply`:
```forth ```forth
{ dup + } apply ;; doubles the top value ( dup + ) apply ;; doubles the top value
``` ```
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it. This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
@@ -115,14 +115,14 @@ saw s c4 note .
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0): Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
```forth ```forth
3 { c4 @i 4 * + note } times 3 ( c4 @i 4 * + note ) times
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 a chord sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 a chord
``` ```
Subdivide with `at`: Subdivide with `at`:
```forth ```forth
4 { @i 4 / at sine s c4 note 0.3 gain . } times 4 ( @i 4 / at sine s c4 note 0.3 gain . ) times
``` ```
Four evenly spaced notes within the step. Four evenly spaced notes within the step.
@@ -130,11 +130,11 @@ Four evenly spaced notes within the step.
Vary intensity per iteration: Vary intensity per iteration:
```forth ```forth
8 { 8 (
@i 8 / at @i 8 / at
@i 4 mod 0 = if 0.7 else 0.2 then gain @i 4 mod 0 = if 0.7 else 0.2 then gain
tri s c5 note 0.1 decay . tri s c5 note 0.1 decay .
} times ) times
``` ```
Eight notes per step. Every fourth one louder. Eight notes per step. Every fourth one louder.

View File

@@ -22,10 +22,10 @@ Everything after `;;` until the end of the line is ignored.
Classic Forth has no quotations. Code is not a value you can pass around. Classic Forth has no quotations. Code is not a value you can pass around.
Cagire has first-class quotations using curly braces: Cagire has first-class quotations using parentheses:
```forth ```forth
{ dup + } ( dup + )
``` ```
This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling. This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling.
@@ -41,14 +41,14 @@ x 0 > IF 1 ELSE -1 THEN
Cagire supports this syntax but also provides quotation-based conditionals: Cagire supports this syntax but also provides quotation-based conditionals:
```forth ```forth
{ 1 } { -1 } x 0 > ifelse ( 1 ) ( -1 ) x 0 > ifelse
``` ```
The words `?` and `!?` execute a quotation based on a condition: The words `?` and `!?` execute a quotation based on a condition:
```forth ```forth
{ "kick" s . } coin ? ;; execute if coin is 1 ( "kick" s . ) coin ? ;; execute if coin is 1
{ "snare" s . } coin !? ;; execute if coin is 0 ( "snare" s . ) coin !? ;; execute if coin is 0
``` ```
## Strings ## Strings
@@ -116,21 +116,21 @@ Classic Forth has `DO ... LOOP`:
Cagire uses a quotation-based loop with `times`: Cagire uses a quotation-based loop with `times`:
```forth ```forth
4 { @i . } times ;; prints 0 1 2 3 4 ( @i . ) times ;; prints 0 1 2 3
``` ```
The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations. The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations.
```forth ```forth
4 { @i 4 / at hat s . } times ;; hat at 0, 0.25, 0.5, 0.75 4 ( @i 4 / at hat s . ) times ;; hat at 0, 0.25, 0.5, 0.75
4 { c4 @i + note sine s . } times ;; ascending notes 4 ( c4 @i + note sine s . ) times ;; ascending notes
``` ```
For generating sequences without side effects, use `..` or `gen`: For generating sequences without side effects, use `..` or `gen`:
```forth ```forth
1 5 .. ;; pushes 1 2 3 4 5 1 5 .. ;; pushes 1 2 3 4 5
{ dup * } 4 gen ;; pushes 0 1 4 9 (squares) ( dup * ) 4 gen ;; pushes 0 1 4 9 (squares)
``` ```
## The Command Register ## The Command Register
@@ -167,11 +167,11 @@ These have no equivalent in classic Forth. They connect your script to the seque
Classic Forth is deterministic. Cagire has built-in randomness: Classic Forth is deterministic. Cagire has built-in randomness:
```forth ```forth
{ "snare" s . } 50 prob ;; 50% chance ( "snare" s . ) 50 prob ;; 50% chance
{ "clap" s . } 0.25 chance ;; 25% chance ( "clap" s . ) 0.25 chance ;; 25% chance
{ "hat" s . } often ;; 75% chance ( "hat" s . ) often ;; 75% chance
{ "rim" s . } sometimes ;; 50% chance ( "rim" s . ) sometimes ;; 50% chance
{ "tom" s . } rarely ;; 25% chance ( "tom" s . ) rarely ;; 25% chance
``` ```
These words take a quotation and execute it probabilistically. These words take a quotation and execute it probabilistically.
@@ -181,9 +181,9 @@ These words take a quotation and execute it probabilistically.
Execute a quotation on specific iterations: Execute a quotation on specific iterations:
```forth ```forth
{ "snare" s . } 4 every ;; every 4th pattern iteration ( "snare" s . ) 4 every ;; every 4th pattern iteration
{ "hat" s . } 3 8 bjork ;; Euclidean: 3 hits across 8 step runs ( "hat" s . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
{ "hat" s . } 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations ( "hat" s . ) 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
``` ```
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped. `every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
@@ -203,7 +203,7 @@ Each time the step runs, a different note is selected. The `3` tells `cycle` how
You can also use quotations if you need to execute code: You can also use quotations if you need to execute code:
```forth ```forth
{ c4 note } { e4 note } { g4 note } 3 cycle ( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
``` ```
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack. When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.

View File

@@ -75,7 +75,7 @@ Euclidean distribution via `euclid`:
Random timing via `gen`: Random timing via `gen`:
```forth ```forth
{ 0.0 1.0 rand } 4 gen at hat s . ;; 4 hats at random positions ( 0.0 1.0 rand ) 4 gen at hat s . ;; 4 hats at random positions
``` ```
Geometric spacing via `geom..`: Geometric spacing via `geom..`:
@@ -89,10 +89,10 @@ Geometric spacing via `geom..`:
Wrap `at` expressions in quotations for conditional timing: Wrap `at` expressions in quotations for conditional timing:
```forth ```forth
{ 0 0.25 0.5 0.75 at } 2 every ;; 16th-note hats every other bar ( 0 0.25 0.5 0.75 at ) 2 every ;; 16th-note hats every other bar
hat s . hat s .
{ 0 0.5 at } 0.5 chance ;; 50% chance of double-hit ( 0 0.5 at ) 0.5 chance ;; 50% chance of double-hit
kick s . kick s .
``` ```

View File

@@ -40,15 +40,15 @@ That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `freq`.
`gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call: `gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call:
```forth ```forth
{ 1 6 rand } 4 gen ;; 4 random values between 1 and 6 ( 1 6 rand ) 4 gen ;; 4 random values between 1 and 6
{ coin } 8 gen ;; 8 random 0s and 1s ( coin ) 8 gen ;; 8 random 0s and 1s
``` ```
Contrast with `times`, which executes for side effects and does not collect. `times` sets `@i` to the current index: Contrast with `times`, which executes for side effects and does not collect. `times` sets `@i` to the current index:
```forth ```forth
4 { @i } times ;; 0 1 2 3 (pushes @i each iteration) 4 ( @i ) times ;; 0 1 2 3 (pushes @i each iteration)
4 { @i 60 + note sine s . } times ;; plays 4 notes, collects nothing 4 ( @i 60 + note sine s . ) times ;; plays 4 notes, collects nothing
``` ```
The distinction: `gen` is for building data. `times` is for doing things. The distinction: `gen` is for building data. `times` is for doing things.
@@ -109,7 +109,7 @@ c4 e4 g4 b4 4 shuffle ;; random permutation each time
Useful for computing averages or accumulating values: Useful for computing averages or accumulating values:
```forth ```forth
{ 1 6 rand } 4 gen 4 sum ;; sum of 4 dice rolls ( 1 6 rand ) 4 gen 4 sum ;; sum of 4 dice rolls
``` ```
## Replication ## Replication

View File

@@ -263,7 +263,7 @@ c4 mtof freq sine s .
A chord progression cycling every pattern iteration: A chord progression cycling every pattern iteration:
```forth ```forth
{ c3 maj7 } { f3 maj7 } { g3 dom7 } { c3 maj7 } 4 pcycle ( c3 maj7 ) ( f3 maj7 ) ( g3 dom7 ) ( c3 maj7 ) 4 pcycle
note sine s . note sine s .
``` ```
@@ -290,7 +290,7 @@ Chord voicings with random inversion:
```forth ```forth
e3 min9 e3 min9
{ } { 1 oct } 2 choose ( ) ( 1 oct ) 2 choose
note modal s . note modal s .
``` ```

View File

@@ -31,8 +31,8 @@ These are useful for parameters where perception is logarithmic, like frequency
The probability words take a quotation and execute it with some chance. `chance` takes a float from 0.0 to 1.0, `prob` takes a percentage from 0 to 100: The probability words take a quotation and execute it with some chance. `chance` takes a float from 0.0 to 1.0, `prob` takes a percentage from 0 to 100:
```forth ```forth
{ hat s . } 0.25 chance ;; 25% chance ( hat s . ) 0.25 chance ;; 25% chance
{ hat s . } 75 prob ;; 75% chance ( hat s . ) 75 prob ;; 75% chance
``` ```
Named probability words save you from remembering numbers: Named probability words save you from remembering numbers:
@@ -48,9 +48,9 @@ Named probability words save you from remembering numbers:
| `never` | 0% | | `never` | 0% |
```forth ```forth
{ hat s . } often ;; 75% ( hat s . ) often ;; 75%
{ snare s . } sometimes ;; 50% ( snare s . ) sometimes ;; 50%
{ clap s . } rarely ;; 25% ( clap s . ) rarely ;; 25%
``` ```
`always` and `never` are useful when you want to temporarily mute or unmute a voice without deleting code. Change `sometimes` to `never` to silence it, `always` to bring it back. `always` and `never` are useful when you want to temporarily mute or unmute a voice without deleting code. Change `sometimes` to `never` to silence it, `always` to bring it back.
@@ -58,8 +58,8 @@ Named probability words save you from remembering numbers:
Use `?` and `!?` with `coin` for quick coin-flip decisions: Use `?` and `!?` with `coin` for quick coin-flip decisions:
```forth ```forth
{ hat s . } coin ? ;; execute if coin is 1 ( hat s . ) coin ? ;; execute if coin is 1
{ rim s . } coin !? ;; execute if coin is 0 ( rim s . ) coin !? ;; execute if coin is 0
``` ```
## Selection ## Selection
@@ -74,7 +74,7 @@ kick snare hat 3 choose s . ;; random drum hit
When a chosen item is a quotation, it gets executed: When a chosen item is a quotation, it gets executed:
```forth ```forth
{ 0.1 decay } { 0.5 decay } { 0.9 decay } 3 choose ( 0.1 decay ) ( 0.5 decay ) ( 0.9 decay ) 3 choose
sine s . sine s .
``` ```
@@ -115,7 +115,7 @@ The difference matters when patterns have different lengths. `cycle` counts per-
Quotations work here too: Quotations work here too:
```forth ```forth
{ c4 note } { e4 note } { g4 note } 3 cycle ( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
sine s . sine s .
``` ```
@@ -130,20 +130,20 @@ sine s .
`every` runs a quotation once every n pattern iterations: `every` runs a quotation once every n pattern iterations:
```forth ```forth
{ crash s . } 4 every ;; crash cymbal every 4th iteration ( crash s . ) 4 every ;; crash cymbal every 4th iteration
``` ```
`except` is the inverse -- it runs a quotation on all iterations *except* every nth: `except` is the inverse -- it runs a quotation on all iterations *except* every nth:
```forth ```forth
{ 2 distort } 4 except ;; distort on all iterations except every 4th ( 2 distort ) 4 except ;; distort on all iterations except every 4th
``` ```
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms: `bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
```forth ```forth
{ hat s . } 3 8 bjork ;; tresillo: x..x..x. (by step runs) ( hat s . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
{ hat s . } 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations) ( hat s . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
``` ```
`bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns: `bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns:
@@ -172,7 +172,7 @@ The real power comes from mixing techniques. A hi-hat pattern with ghost notes:
```forth ```forth
hat s hat s
{ 0.3 0.6 rand gain } { 0.8 gain } 2 cycle ( 0.3 0.6 rand gain ) ( 0.8 gain ) 2 cycle
. .
``` ```
@@ -181,18 +181,18 @@ Full volume on even runs, random quiet on odd runs.
A bass line that changes every 4 bars: A bass line that changes every 4 bars:
```forth ```forth
{ c2 note } { e2 note } { g2 note } { a2 note } 4 pcycle ( c2 note ) ( e2 note ) ( g2 note ) ( a2 note ) 4 pcycle
{ 0.5 decay } often ( 0.5 decay ) often
sine s . sine s .
``` ```
Layered percussion with different densities: Layered percussion with different densities:
```forth ```forth
{ kick s . } always ( kick s . ) always
{ snare s . } 2 every ( snare s . ) 2 every
{ hat s . } 5 8 bjork ( hat s . ) 5 8 bjork
{ rim s . } rarely ( rim s . ) rarely
``` ```
A melodic step with weighted note selection and random timbre: A melodic step with weighted note selection and random timbre:

View File

@@ -53,7 +53,7 @@ Reset on some condition:
```forth ```forth
@n 1 + !n @n 1 + !n
{ 0 !n } @n 16 > ? ;; reset after 16 ( 0 !n ) @n 16 > ? ;; reset after 16
``` ```
## When Changes Take Effect ## When Changes Take Effect

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>Cagire</string>
<key>CFBundleExecutable</key>
<string>cagire-desktop</string>
<key>CFBundleIconFile</key>
<string>Cagire.icns</string>
<key>CFBundleIdentifier</key>
<string>com.sova.cagire</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Cagire</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.9</string>
<key>CFBundleVersion</key>
<string>20260228.123720</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.music</string>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) 2025 Raphaël Forment</string>
</dict>
</plist>

Binary file not shown.

BIN
releases/Cagire-aarch64.dmg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export MACOSX_DEPLOYMENT_TARGET="12.0"
cd "$(git rev-parse --show-toplevel)" cd "$(git rev-parse --show-toplevel)"
PLUGIN_NAME="cagire-plugins" PLUGIN_NAME="cagire-plugins"
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
OUT="target/releases" OUT="releases"
PLATFORMS=( PLATFORMS=(
"aarch64-apple-darwin" "aarch64-apple-darwin"
@@ -312,13 +314,16 @@ copy_artifacts() {
# macOS .app bundle # macOS .app bundle
if [[ "$os" == "macos" ]]; then if [[ "$os" == "macos" ]]; then
local app_src="$rd/bundle/osx/Cagire.app" local app_src="$rd/bundle/osx/Cagire.app"
if [[ -d "$app_src" ]]; then if [[ ! -d "$app_src" ]]; then
local app_dst="$OUT/Cagire-${arch}.app" echo " ERROR: .app bundle not found at $app_src"
rm -rf "$app_dst" echo " Did 'cargo bundle' succeed?"
cp -R "$app_src" "$app_dst" return 1
echo " Cagire.app -> $app_dst"
scripts/make-dmg.sh "$app_dst" "$OUT"
fi fi
local app_dst="$OUT/Cagire-${arch}.app"
rm -rf "$app_dst"
cp -R "$app_src" "$app_dst"
echo " Cagire.app -> $app_dst"
scripts/make-dmg.sh "$app_dst" "$OUT"
fi fi
fi fi

View File

@@ -187,30 +187,30 @@ fn nor_ff() {
#[test] #[test]
fn ifelse_true() { fn ifelse_true() {
expect_int("{ 42 } { 99 } 1 ifelse", 42); expect_int("( 42 ) ( 99 ) 1 ifelse", 42);
} }
#[test] #[test]
fn ifelse_false() { fn ifelse_false() {
expect_int("{ 42 } { 99 } 0 ifelse", 99); expect_int("( 42 ) ( 99 ) 0 ifelse", 99);
} }
#[test] #[test]
fn select_first() { fn select_first() {
expect_int("{ 10 } { 20 } { 30 } 0 select", 10); expect_int("( 10 ) ( 20 ) ( 30 ) 0 select", 10);
} }
#[test] #[test]
fn select_second() { fn select_second() {
expect_int("{ 10 } { 20 } { 30 } 1 select", 20); expect_int("( 10 ) ( 20 ) ( 30 ) 1 select", 20);
} }
#[test] #[test]
fn select_third() { fn select_third() {
expect_int("{ 10 } { 20 } { 30 } 2 select", 30); expect_int("( 10 ) ( 20 ) ( 30 ) 2 select", 30);
} }
#[test] #[test]
fn select_preserves_stack() { fn select_preserves_stack() {
expect_int("5 { 10 } { 20 } 0 select +", 15); expect_int("5 ( 10 ) ( 20 ) 0 select +", 15);
} }

View File

@@ -59,14 +59,14 @@ fn iter() {
#[test] #[test]
fn every_true_on_zero() { fn every_true_on_zero() {
let ctx = ctx_with(|c| c.iter = 0); let ctx = ctx_with(|c| c.iter = 0);
let f = run_ctx("{ 100 } 4 every", &ctx); let f = run_ctx("( 100 ) 4 every", &ctx);
assert_eq!(stack_int(&f), 100); assert_eq!(stack_int(&f), 100);
} }
#[test] #[test]
fn every_true_on_multiple() { fn every_true_on_multiple() {
let ctx = ctx_with(|c| c.iter = 8); let ctx = ctx_with(|c| c.iter = 8);
let f = run_ctx("{ 100 } 4 every", &ctx); let f = run_ctx("( 100 ) 4 every", &ctx);
assert_eq!(stack_int(&f), 100); assert_eq!(stack_int(&f), 100);
} }
@@ -74,14 +74,14 @@ fn every_true_on_multiple() {
fn every_false_between() { fn every_false_between() {
for i in 1..4 { for i in 1..4 {
let ctx = ctx_with(|c| c.iter = i); let ctx = ctx_with(|c| c.iter = i);
let f = run_ctx("{ 100 } 4 every", &ctx); let f = run_ctx("( 100 ) 4 every", &ctx);
assert!(f.stack().is_empty(), "iter={} should not execute quotation", i); assert!(f.stack().is_empty(), "iter={} should not execute quotation", i);
} }
} }
#[test] #[test]
fn every_zero_count() { fn every_zero_count() {
expect_error("{ 1 } 0 every", "every count must be > 0"); expect_error("( 1 ) 0 every", "every count must be > 0");
} }
#[test] #[test]
@@ -105,7 +105,7 @@ fn bjork_tresillo() {
// Bresenham(3,8) hits at positions 0, 2, 5 // Bresenham(3,8) hits at positions 0, 2, 5
for runs in 0..8 { for runs in 0..8 {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = run_ctx("{ 100 } 3 8 bjork", &ctx); let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8; let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8;
if hit { if hit {
assert_eq!(stack_int(&f), 100, "runs={} should hit", runs); assert_eq!(stack_int(&f), 100, "runs={} should hit", runs);
@@ -121,7 +121,7 @@ fn bjork_hit_count() {
let mut hit_count = 0; let mut hit_count = 0;
for runs in 0..8 { for runs in 0..8 {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = run_ctx("{ 100 } 3 8 bjork", &ctx); let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
if !f.stack().is_empty() { if !f.stack().is_empty() {
hit_count += 1; hit_count += 1;
} }
@@ -132,20 +132,20 @@ fn bjork_hit_count() {
#[test] #[test]
fn bjork_all_hits() { fn bjork_all_hits() {
let ctx = ctx_with(|c| c.runs = 0); let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("{ 100 } 8 8 bjork", &ctx); let f = run_ctx("( 100 ) 8 8 bjork", &ctx);
assert_eq!(stack_int(&f), 100); assert_eq!(stack_int(&f), 100);
} }
#[test] #[test]
fn bjork_zero_hits() { fn bjork_zero_hits() {
let ctx = ctx_with(|c| c.runs = 0); let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("{ 100 } 0 8 bjork", &ctx); let f = run_ctx("( 100 ) 0 8 bjork", &ctx);
assert!(f.stack().is_empty()); assert!(f.stack().is_empty());
} }
#[test] #[test]
fn bjork_invalid() { fn bjork_invalid() {
expect_error("{ 1 } 3 0 bjork", "bjork"); expect_error("( 1 ) 3 0 bjork", "bjork");
} }
// pbjork (iter-based) // pbjork (iter-based)
@@ -155,7 +155,7 @@ fn pbjork_cinquillo() {
let mut hit_count = 0; let mut hit_count = 0;
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = run_ctx("{ 100 } 5 8 pbjork", &ctx); let f = run_ctx("( 100 ) 5 8 pbjork", &ctx);
if !f.stack().is_empty() { if !f.stack().is_empty() {
hit_count += 1; hit_count += 1;
} }
@@ -167,7 +167,7 @@ fn pbjork_cinquillo() {
fn pbjork_wraps() { fn pbjork_wraps() {
let ctx0 = ctx_with(|c| c.iter = 0); let ctx0 = ctx_with(|c| c.iter = 0);
let ctx8 = ctx_with(|c| c.iter = 8); let ctx8 = ctx_with(|c| c.iter = 8);
let f0 = run_ctx("{ 100 } 3 8 pbjork", &ctx0); let f0 = run_ctx("( 100 ) 3 8 pbjork", &ctx0);
let f8 = run_ctx("{ 100 } 3 8 pbjork", &ctx8); let f8 = run_ctx("( 100 ) 3 8 pbjork", &ctx8);
assert_eq!(f0.stack().is_empty(), f8.stack().is_empty()); assert_eq!(f0.stack().is_empty(), f8.stack().is_empty());
} }

View File

@@ -68,12 +68,12 @@ fn unexpected_semicolon_errors() {
#[test] #[test]
fn apply_executes_quotation() { fn apply_executes_quotation() {
expect_int("5 { 2 * } apply", 10); expect_int("5 ( 2 * ) apply", 10);
} }
#[test] #[test]
fn apply_with_stack_ops() { fn apply_with_stack_ops() {
expect_int("3 4 { + } apply", 7); expect_int("3 4 ( + ) apply", 7);
} }
#[test] #[test]
@@ -88,12 +88,12 @@ fn apply_non_quotation_errors() {
#[test] #[test]
fn apply_nested() { fn apply_nested() {
expect_int("2 { { 3 * } apply } apply", 6); expect_int("2 ( ( 3 * ) apply ) apply", 6);
} }
#[test] #[test]
fn define_word_containing_quotation() { fn define_word_containing_quotation() {
expect_int(": dbl { 2 * } apply ; 7 dbl", 14); expect_int(": dbl ( 2 * ) apply ; 7 dbl", 14);
} }
#[test] #[test]

View File

@@ -33,7 +33,7 @@ fn range_underflow() {
#[test] #[test]
fn gen_basic() { fn gen_basic() {
expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]); expect_stack("( 42 ) 3 gen", &[int(42), int(42), int(42)]);
} }
#[test] #[test]
@@ -42,7 +42,7 @@ fn gen_with_computation() {
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0] // 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0] // 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s // So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]); expect_stack("0 ( dup 1 + ) 3 gen", &[int(0), int(1), int(1), int(1)]);
} }
#[test] #[test]
@@ -50,12 +50,12 @@ fn gen_chained() {
// Start with 1, each iteration: dup, multiply by 2 // Start with 1, each iteration: dup, multiply by 2
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1] // 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1] // 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]); expect_stack("1 ( dup 2 * ) 3 gen", &[int(1), int(2), int(2), int(2)]);
} }
#[test] #[test]
fn gen_zero() { fn gen_zero() {
expect_stack("{ 1 } 0 gen", &[]); expect_stack("( 1 ) 0 gen", &[]);
} }
#[test] #[test]
@@ -65,17 +65,17 @@ fn gen_underflow() {
#[test] #[test]
fn gen_not_a_number() { fn gen_not_a_number() {
expect_error("{ 1 } gen", "expected number"); expect_error("( 1 ) gen", "expected number");
} }
#[test] #[test]
fn gen_negative() { fn gen_negative() {
expect_error("{ 1 } -1 gen", "gen count must be >= 0"); expect_error("( 1 ) -1 gen", "gen count must be >= 0");
} }
#[test] #[test]
fn gen_empty_quot_error() { fn gen_empty_quot_error() {
expect_error("{ } 3 gen", "quotation must produce"); expect_error("( ) 3 gen", "quotation must produce");
} }
#[test] #[test]

View File

@@ -46,20 +46,20 @@ fn pcycle_by_iter() {
#[test] #[test]
fn cycle_with_quotations() { fn cycle_with_quotations() {
let ctx = ctx_with(|c| c.runs = 0); let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx); let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
let stack = f.stack(); let stack = f.stack();
assert_eq!(stack.len(), 2); assert_eq!(stack.len(), 2);
assert_eq!(stack_int(&f), 5); assert_eq!(stack_int(&f), 5);
let ctx = ctx_with(|c| c.runs = 1); let ctx = ctx_with(|c| c.runs = 1);
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx); let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
assert_eq!(stack_int(&f), 10); assert_eq!(stack_int(&f), 10);
} }
#[test] #[test]
fn cycle_executes_quotation() { fn cycle_executes_quotation() {
let ctx = ctx_with(|c| c.runs = 0); let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("10 { 3 + } { 5 + } 2 cycle", &ctx); let f = run_ctx("10 ( 3 + ) ( 5 + ) 2 cycle", &ctx);
assert_eq!(stack_int(&f), 13); assert_eq!(stack_int(&f), 13);
} }
@@ -108,11 +108,11 @@ fn bracket_cycle() {
#[test] #[test]
fn bracket_with_quotations() { fn bracket_with_quotations() {
let ctx = ctx_with(|c| c.runs = 0); let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx); let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
assert_eq!(stack_int(&f), 8); assert_eq!(stack_int(&f), 8);
let ctx = ctx_with(|c| c.runs = 1); let ctx = ctx_with(|c| c.runs = 1);
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx); let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
assert_eq!(stack_int(&f), 10); assert_eq!(stack_int(&f), 10);
} }
@@ -172,7 +172,7 @@ fn index_negative_wraps() {
#[test] #[test]
fn index_with_quotation() { fn index_with_quotation() {
expect_int("5 [ { 3 + } { 5 + } ] 0 index", 8); expect_int("5 [ ( 3 + ) ( 5 + ) ] 0 index", 8);
} }
#[test] #[test]

View File

@@ -4,26 +4,26 @@ use super::harness::*;
fn quotation_on_stack() { fn quotation_on_stack() {
// Quotation should be pushable to stack // Quotation should be pushable to stack
let f = forth(); let f = forth();
let result = f.evaluate("{ 1 2 + }", &default_ctx()); let result = f.evaluate("( 1 2 + )", &default_ctx());
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test] #[test]
fn when_true_executes() { fn when_true_executes() {
let f = run("{ 42 } 1 ?"); let f = run("( 42 ) 1 ?");
assert_eq!(stack_int(&f), 42); assert_eq!(stack_int(&f), 42);
} }
#[test] #[test]
fn when_false_skips() { fn when_false_skips() {
let f = run("99 { 42 } 0 ?"); let f = run("99 ( 42 ) 0 ?");
// Stack should still have 99, quotation not executed // Stack should still have 99, quotation not executed
assert_eq!(stack_int(&f), 99); assert_eq!(stack_int(&f), 99);
} }
#[test] #[test]
fn when_with_arithmetic() { fn when_with_arithmetic() {
let f = run("10 { 5 + } 1 ?"); let f = run("10 ( 5 + ) 1 ?");
assert_eq!(stack_int(&f), 15); assert_eq!(stack_int(&f), 15);
} }
@@ -31,48 +31,48 @@ fn when_with_arithmetic() {
fn when_with_every() { fn when_with_every() {
// iter=0, every 2 executes quotation // iter=0, every 2 executes quotation
let ctx = ctx_with(|c| c.iter = 0); let ctx = ctx_with(|c| c.iter = 0);
let f = run_ctx("{ 100 } 2 every", &ctx); let f = run_ctx("( 100 ) 2 every", &ctx);
assert_eq!(stack_int(&f), 100); assert_eq!(stack_int(&f), 100);
// iter=1, every 2 skips quotation // iter=1, every 2 skips quotation
let ctx = ctx_with(|c| c.iter = 1); let ctx = ctx_with(|c| c.iter = 1);
let f = run_ctx("50 { 100 } 2 every", &ctx); let f = run_ctx("50 ( 100 ) 2 every", &ctx);
assert_eq!(stack_int(&f), 50); // quotation not executed assert_eq!(stack_int(&f), 50); // quotation not executed
} }
#[test] #[test]
fn when_with_chance_deterministic() { fn when_with_chance_deterministic() {
// 1.0 chance always executes quotation // 1.0 chance always executes quotation
let f = run("{ 42 } 1.0 chance"); let f = run("( 42 ) 1.0 chance");
assert_eq!(stack_int(&f), 42); assert_eq!(stack_int(&f), 42);
// 0.0 chance never executes quotation // 0.0 chance never executes quotation
let f = run("99 { 42 } 0.0 chance"); let f = run("99 ( 42 ) 0.0 chance");
assert_eq!(stack_int(&f), 99); assert_eq!(stack_int(&f), 99);
} }
#[test] #[test]
fn nested_quotations() { fn nested_quotations() {
let f = run("{ { 42 } 1 ? } 1 ?"); let f = run("( ( 42 ) 1 ? ) 1 ?");
assert_eq!(stack_int(&f), 42); assert_eq!(stack_int(&f), 42);
} }
#[test] #[test]
fn quotation_with_param() { fn quotation_with_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? ."#, 1); let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1);
assert!(outputs[0].contains("distort/2")); assert!(outputs[0].contains("distort/2"));
} }
#[test] #[test]
fn quotation_skips_param() { fn quotation_skips_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? ."#, 1); let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1);
assert!(!outputs[0].contains("distort")); assert!(!outputs[0].contains("distort"));
} }
#[test] #[test]
fn quotation_with_emit() { fn quotation_with_emit() {
// When true, . should fire // When true, . should fire
let outputs = expect_outputs(r#""kick" s { . } 1 ?"#, 1); let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1);
assert!(outputs[0].contains("kick")); assert!(outputs[0].contains("kick"));
} }
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
// When false, . should not fire // When false, . should not fire
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s { . } 0 ?"#, &default_ctx()) .evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx())
.unwrap(); .unwrap();
// No output since . was skipped and no implicit emit // No output since . was skipped and no implicit emit
assert_eq!(outputs.len(), 0); assert_eq!(outputs.len(), 0);
@@ -94,22 +94,22 @@ fn missing_quotation_error() {
#[test] #[test]
fn unclosed_quotation_error() { fn unclosed_quotation_error() {
expect_error("{ 1 2", "missing }"); expect_error("( 1 2", "missing )");
} }
#[test] #[test]
fn unexpected_close_error() { fn unexpected_close_error() {
expect_error("1 2 }", "unexpected }"); expect_error("1 2 )", "unexpected )");
} }
#[test] #[test]
fn every_with_quotation_integration() { fn every_with_quotation_integration() {
// { 2 distort } 2 every — on even iterations, distort is applied // ( 2 distort ) 2 every — on even iterations, distort is applied
for iter in 0..4 { for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s { 2 distort } 2 every ."#, &ctx) .evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx)
.unwrap(); .unwrap();
if iter % 2 == 0 { if iter % 2 == 0 {
assert!( assert!(
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8) let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s { 2 distort } 3 8 bjork ."#, &ctx) .evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx)
.unwrap(); .unwrap();
assert!(outputs[0].contains("distort/2")); assert!(outputs[0].contains("distort/2"));
} }
@@ -143,13 +143,13 @@ fn bjork_with_sound() {
#[test] #[test]
fn unless_false_executes() { fn unless_false_executes() {
let f = run("{ 42 } 0 !?"); let f = run("( 42 ) 0 !?");
assert_eq!(stack_int(&f), 42); assert_eq!(stack_int(&f), 42);
} }
#[test] #[test]
fn unless_true_skips() { fn unless_true_skips() {
let f = run("99 { 42 } 1 !?"); let f = run("99 ( 42 ) 1 !?");
assert_eq!(stack_int(&f), 99); assert_eq!(stack_int(&f), 99);
} }
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate( .evaluate(
r#""kick" s { 2 distort } iter 2 mod 0 = ? { 4 distort } iter 2 mod 0 = !? ."#, r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
&ctx, &ctx,
) )
.unwrap(); .unwrap();

View File

@@ -38,14 +38,14 @@ fn coin_binary() {
#[test] #[test]
fn chance_zero() { fn chance_zero() {
// 0.0 probability should never execute the quotation // 0.0 probability should never execute the quotation
let f = run("99 { 42 } 0.0 chance"); let f = run("99 ( 42 ) 0.0 chance");
assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack
} }
#[test] #[test]
fn chance_one() { fn chance_one() {
// 1.0 probability should always execute the quotation // 1.0 probability should always execute the quotation
let f = run("{ 42 } 1.0 chance"); let f = run("( 42 ) 1.0 chance");
assert_eq!(stack_int(&f), 42); assert_eq!(stack_int(&f), 42);
} }
@@ -281,7 +281,7 @@ fn wchoose_negative_weight() {
#[test] #[test]
fn wchoose_quotation() { fn wchoose_quotation() {
let f = forth_seeded(42); let f = forth_seeded(42);
f.evaluate("{ 10 } 0.0 { 20 } 1.0 2 wchoose", &default_ctx()) f.evaluate("( 10 ) 0.0 ( 20 ) 1.0 2 wchoose", &default_ctx())
.unwrap(); .unwrap();
assert_eq!(stack_int(&f), 20); assert_eq!(stack_int(&f), 20);
} }

View File

@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
for runs in 0..4 { for runs in 0..4 {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
if runs % 2 == 0 { if runs % 2 == 0 {
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs); assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
} else { } else {
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
for iter in 0..4 { for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
if iter % 2 == 0 { if iter % 2 == 0 {
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter); assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
} else { } else {
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = forth(); let f = forth();
let outputs = f.evaluate( let outputs = f.evaluate(
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#, r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
&ctx &ctx
).unwrap(); ).unwrap();
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs); assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 2 every+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
if iter % 4 == 2 { if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else { } else {
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 6 every+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
if iter % 4 == 2 { if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
} else { } else {
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 2 except+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
if iter % 4 != 2 { if iter % 4 != 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else { } else {
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let a = f.evaluate(r#""kick" s { . } 3 every"#, &ctx).unwrap(); let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
let b = f.evaluate(r#""kick" s { . } 3 0 every+"#, &ctx).unwrap(); let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter); assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
} }
} }