diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3441651..256f93c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Added
+- Resolved value annotations: nondeterministic words (`rand`, `choose`, `cycle`, `bounce`, `wchoose`, `coin`, `chance`, `prob`, `exprand`, `logrand`) now display their resolved value inline (e.g., `choose [sine]`, `rand [7]`, `chance [yes]`) during playback in both Preview and Editor modals.
+
+## [0.0.9] - 2026-02-08
+
+### Website
+- Compressed screenshot images: resized to 1600px and converted PNG to WebP (8MB → 538KB).
+- Version number displayed in subtitle, read automatically from `Cargo.toml` at build time.
+
### Added
- Inline sample finder in the editor: press `Ctrl+B` to open a fuzzy-search popup of all sample folder names. Type to filter, `Ctrl+N`/`Ctrl+P` to navigate, `Tab`/`Enter` to insert the folder name at cursor, `Esc` to dismiss. Mutually exclusive with word completion.
- Sample browser now displays the 0-based file index next to each sample name, making it easy to reference samples by index in Forth scripts (e.g., `"drums" bank 0 n`).
@@ -12,6 +21,9 @@ All notable changes to this project will be documented in this file.
- Header bar stats block (CPU/voices/Link peers) is now centered like all other header sections.
- CPU percentage changes color when load is high: accent color at 50%+, error color at 80%+.
+### Fixed
+- Soundless emits (e.g., `1 gain .`) no longer stack infinite voices. All emitted commands now receive a default duration of one beat unless the user explicitly sets `dur`. Use `0 dur` for intentionally infinite voices.
+
## [0.0.8] - 2026-02-07
### Fixed
diff --git a/crates/forth/src/lib.rs b/crates/forth/src/lib.rs
index 4ce27f5..1f37c50 100644
--- a/crates/forth/src/lib.rs
+++ b/crates/forth/src/lib.rs
@@ -6,8 +6,8 @@ mod vm;
mod words;
pub use types::{
- CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
- VariablesMap,
+ CcAccess, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, StepContext, Value,
+ Variables, VariablesMap,
};
pub use vm::Forth;
pub use words::{lookup_word, Word, WordCompile, WORDS};
diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs
index 5eab270..121dfc6 100644
--- a/crates/forth/src/ops.rs
+++ b/crates/forth/src/ops.rs
@@ -65,18 +65,18 @@ pub enum Op {
Get,
Set,
GetContext(&'static str),
- Rand,
- ExpRand,
- LogRand,
+ Rand(Option),
+ ExpRand(Option),
+ LogRand(Option),
Seed,
- Cycle,
- PCycle,
- Choose,
- Bounce,
- WChoose,
- ChanceExec,
- ProbExec,
- Coin,
+ Cycle(Option),
+ PCycle(Option),
+ Choose(Option),
+ Bounce(Option),
+ WChoose(Option),
+ ChanceExec(Option),
+ ProbExec(Option),
+ Coin(Option),
Mtof,
Ftom,
SetTempo,
diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs
index e24d189..814bfc5 100644
--- a/crates/forth/src/types.rs
+++ b/crates/forth/src/types.rs
@@ -18,10 +18,30 @@ pub struct SourceSpan {
pub end: u32,
}
+#[derive(Clone, Debug)]
+pub enum ResolvedValue {
+ Int(i64),
+ Float(f64),
+ Bool(bool),
+ Str(Arc),
+}
+
+impl ResolvedValue {
+ pub fn display(&self) -> String {
+ match self {
+ ResolvedValue::Int(i) => i.to_string(),
+ ResolvedValue::Float(f) => format!("{f:.2}"),
+ ResolvedValue::Bool(b) => if *b { "yes" } else { "no" }.into(),
+ ResolvedValue::Str(s) => s.to_string(),
+ }
+ }
+}
+
#[derive(Clone, Debug, Default)]
pub struct ExecutionTrace {
pub executed_spans: Vec,
pub selected_spans: Vec,
+ pub resolved: Vec<(SourceSpan, ResolvedValue)>,
}
pub struct StepContext<'a> {
diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs
index ab02e81..793fb20 100644
--- a/crates/forth/src/vm.rs
+++ b/crates/forth/src/vm.rs
@@ -8,8 +8,8 @@ use std::sync::Arc;
use super::compiler::compile_script;
use super::ops::Op;
use super::types::{
- CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
- VariablesMap,
+ CmdRegister, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, Stack, StepContext,
+ Value, Variables, VariablesMap,
};
pub struct Forth {
@@ -637,7 +637,7 @@ impl Forth {
stack.push(val);
}
- Op::Rand => {
+ Op::Rand(word_span) => {
let b = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?;
match (&a, &b) {
@@ -648,6 +648,7 @@ impl Forth {
(*b_i, *a_i)
};
let val = self.rng.lock().gen_range(lo..=hi);
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Int(val));
stack.push(Value::Int(val, None));
}
_ => {
@@ -659,11 +660,12 @@ impl Forth {
} else {
self.rng.lock().gen_range(lo..hi)
};
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
}
}
- Op::ExpRand => {
+ Op::ExpRand(word_span) => {
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
if lo <= 0.0 || hi <= 0.0 {
@@ -672,9 +674,10 @@ impl Forth {
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().gen();
let val = lo * (hi / lo).powf(u);
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
- Op::LogRand => {
+ Op::LogRand(word_span) => {
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
if lo <= 0.0 || hi <= 0.0 {
@@ -683,6 +686,7 @@ impl Forth {
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().gen();
let val = hi * (lo / hi).powf(u);
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
Op::Seed => {
@@ -690,28 +694,42 @@ impl Forth {
*self.rng.lock() = StdRng::seed_from_u64(s as u64);
}
- Op::Cycle | Op::PCycle => {
+ Op::Cycle(word_span) | Op::PCycle(word_span) => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("cycle count must be > 0".into());
}
let idx = match &ops[pc] {
- Op::Cycle => ctx.runs,
+ Op::Cycle(_) => ctx.runs,
_ => ctx.iter,
} % count;
+ if let Some(span) = word_span {
+ if stack.len() >= count {
+ let start = stack.len() - count;
+ let selected = &stack[start + idx];
+ record_resolved_from_value(&trace_cell, Some(*span), selected);
+ }
+ }
drain_select_run(count, idx, stack, outputs, cmd)?;
}
- Op::Choose => {
+ Op::Choose(word_span) => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("choose count must be > 0".into());
}
let idx = self.rng.lock().gen_range(0..count);
+ if let Some(span) = word_span {
+ if stack.len() >= count {
+ let start = stack.len() - count;
+ let selected = &stack[start + idx];
+ record_resolved_from_value(&trace_cell, Some(*span), selected);
+ }
+ }
drain_select_run(count, idx, stack, outputs, cmd)?;
}
- Op::Bounce => {
+ Op::Bounce(word_span) => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("bounce count must be > 0".into());
@@ -723,10 +741,17 @@ impl Forth {
let raw = ctx.runs % period;
if raw < count { raw } else { period - raw }
};
+ if let Some(span) = word_span {
+ if stack.len() >= count {
+ let start = stack.len() - count;
+ let selected = &stack[start + idx];
+ record_resolved_from_value(&trace_cell, Some(*span), selected);
+ }
+ }
drain_select_run(count, idx, stack, outputs, cmd)?;
}
- Op::WChoose => {
+ Op::WChoose(word_span) => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("wchoose count must be > 0".into());
@@ -763,25 +788,30 @@ impl Forth {
}
}
let selected = values.swap_remove(selected_idx);
+ record_resolved_from_value(&trace_cell, *word_span, &selected);
select_and_run(selected, stack, outputs, cmd)?;
}
- Op::ChanceExec | Op::ProbExec => {
+ Op::ChanceExec(word_span) | Op::ProbExec(word_span) => {
let threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
let quot = stack.pop().ok_or("stack underflow")?;
let val: f64 = self.rng.lock().gen();
let limit = match &ops[pc] {
- Op::ChanceExec => threshold,
+ Op::ChanceExec(_) => threshold,
_ => threshold / 100.0,
};
- if val < limit {
+ let fired = val < limit;
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(fired));
+ if fired {
run_quotation(quot, stack, outputs, cmd)?;
}
}
- Op::Coin => {
+ Op::Coin(word_span) => {
let val: f64 = self.rng.lock().gen();
- stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None));
+ let result = val < 0.5;
+ record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
+ stack.push(Value::Int(if result { 1 } else { 0 }, None));
}
Op::Every => {
@@ -1241,6 +1271,36 @@ impl Forth {
}
}
+fn record_resolved(
+ trace_cell: &std::cell::RefCell