From 7cd0ef69c41de61cb1771ece5d034732f775278e Mon Sep 17 00:00:00 2001 From: jersmi Date: Mon, 27 Apr 2026 01:26:40 -0700 Subject: [PATCH 1/2] Document require clauses for slice helpers --- docs/lyte-dsp-quickstart.md | 67 ++++++++++++++++++++++++++++++------- docs/tutorial.md | 36 ++++++++++++++++++++ 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/docs/lyte-dsp-quickstart.md b/docs/lyte-dsp-quickstart.md index eb7784e9..87e96b38 100644 --- a/docs/lyte-dsp-quickstart.md +++ b/docs/lyte-dsp-quickstart.md @@ -11,6 +11,7 @@ When you create a new Lyte DSP node in Audulus, it starts with this template: ```lyte // Inputs, outputs, `frames`, `sampleRate`, and `storage` are globals. // Inputs, outputs, and `storage` are slices bound to the engine buffers. +// `storage` is persistent host storage, not the normal place for large DSP buffers. // MAX_FRAMES is the max processing frames (used to declare temporary arrays). init {} @@ -31,19 +32,19 @@ These are the main words you will see right away: - `input`, `output`: the audio coming in and going out for the current block. `input[i]` means “sample number `i`” - `frames`: how many samples are in this block - `sampleRate`: how many samples happen each second, usually `44100.0` or `48000.0` -- `storage`: extra memory for things like delay lines +- `storage`: persistent host storage provided by Audulus. Use it for node-owned values that should live outside the audio block, not as the default location for large delay lines or scratch buffers - `globals`: variables declared outside `process`. They keep their value from one block to the next - `slice`: a buffer you read with `[...]` - `MAX_FRAMES`: the biggest block size Audulus may send to the node. Use it when you need a temporary array -You do not create `frames`, `sampleRate`, or `storage` yourself. Audulus gives them to the node automatically. +You do not create `frames`, `sampleRate`, or `storage` yourself. Audulus gives them to the node automatically. For large DSP buffers, declare a dedicated global array with the size you need. | Global | Type | Description | |---|---|---| | `input`, `output` | `[f32]` | Default port slices. Add ports as needed above the code viewer in the Inspector. | | `frames` | `i32` | Number of samples in this processing block. | | `sampleRate` | `f32` | Current sample rate, e.g. `44100.0`. | -| `storage` | `[f32]` | Pre-allocated buffer for delay lines etc., set the size in the Inspector. | +| `storage` | `[f32]` | Persistent host storage supplied by Audulus. Use dedicated global arrays for large DSP buffers such as delay lines. | | `MAX_FRAMES` | `i32` | Maximum possible block size — use to declare stack arrays. | **`init {}`** runs once when the node loads. Use it for setup values. @@ -85,7 +86,8 @@ These are good default habits for writing Lyte in Audulus. - Start from the fresh-node block template and change one thing at a time when debugging. - Use `sin`, `cos`, `tan`, and other unsuffixed math builtins. - Read a port sample into a scratch `f32` first if the compiler gets ambiguous about expressions like `input[i] * inv_sr`. -- Keep slice index proofs inline near `storage[...]` accesses when the safety checker complains. +- Keep slice index proofs inline near buffer accesses when the safety checker complains. +- Use `require` on slice helpers when the caller can prove the index is valid. - Use `freq[i]`, `cutoff[i]`, and similar forms for normal per-sample processing. Use `freq[0]` only when you intentionally want one control value for the whole block. - Move expensive math out of the sample loop when true audio-rate updates are not needed. - Try block-rate control first when it sounds good enough. It is usually simpler and cheaper. @@ -93,13 +95,39 @@ These are good default habits for writing Lyte in Audulus. **Don't:** - Don't assume every Expr-node feature maps over exactly. Lyte does have built-in trig/math functions and stdlib helpers like `clamp(x, lo, hi)` and `mix(a, b, t)`, but constants like `pi` are still not built in. - Don't use `sinf`, `cosf`, or other suffixed math names in Audulus Lyte. Use unsuffixed names like `sin`, `cos`, `tan`, `atan2`, `sqrt`, `clamp`, and `mix`. -- Don't rely on helper functions to prove slice indices are safe; the checker usually wants the bounds checks inline. +- Don't rely on helper functions to prove slice indices by context alone. Use inline guards, or add explicit `require` clauses to the helper. - Don't use `assume` in node code — it is only allowed in the standard library or prelude. - Don't assume examples from the standalone Lyte repo will drop into Audulus unchanged. - Don't rely on block-form inline `if` expressions in assignments such as `let x = if ...` or `output[i] = if ...`. In Lyte those can trigger confusing parser errors. Use a normal assignment first, then a plain `if` block. --- +## Slice Helper Preconditions + +Audulus beta 983 and newer Lyte builds support `require` clauses on helper functions. Use them when a helper indexes into a slice and every caller must prove the index is valid. + +```lyte +set(arr: [i32], idx: i32, value: i32) require idx >= 0 require idx < arr.len { + arr[idx] = value +} +``` + +The `require` clauses do two jobs. Inside the helper, they let the safety checker treat `idx` as a valid index for `arr`. At each call site, the checker makes sure the caller has already proved those conditions. + +You can also combine the bounds into one clause: + +```lyte +set(arr: [i32], idx: i32, value: i32) require idx >= 0 && idx < arr.len { + arr[idx] = value +} +``` + +If the caller cannot prove a clause, Lyte reports that it `couldn't prove require clause` for that call. Add a nearby guard such as `if idx >= 0 && idx < arr.len { ... }`, or restructure the loop so the range proves the bound. + +The release-note form places `require` immediately after the parameter list. Until a return-valued helper form is confirmed for the Audulus build you are targeting, keep `require` examples to setter-style helpers like `set` and `write_delay`, and keep reads guarded inline. + +--- + ## Debug Printing `println` is useful for simple debugging. For numbers, write into a @@ -626,8 +654,12 @@ process { --- ## 7. Delay Line -A delay line stores past samples in `storage` and reads them back later. This is the core -of echo, comb filters, chorus, flanging, and many reverb designs. +A delay line stores past samples in a dedicated memory block and reads them back later. +This is the core of echo, comb filters, chorus, flanging, and many reverb designs. + +`storage` is persistent Audulus node storage, but it is not the preferred place for a +large delay buffer. For delay lines, declare a global array with the length you need. +This example dedicates 65536 samples of memory to the delay line. This is different from unit delay. Unit delay is always one sample. A delay line is a larger buffer measured in samples, milliseconds, or seconds. @@ -635,18 +667,23 @@ larger buffer measured in samples, milliseconds, or seconds. *Ports: `input`, `secs` in, `output` out.* ```lyte +var delay_mem: [f32; 65536] var write: i32 lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t } +write_delay(buffer: [f32], idx: i32, value: f32) require idx >= 0 require idx < buffer.len { + buffer[idx] = value +} + init {} process { for i in 0 .. frames { let x = input[i] - let len = storage.len + let len = delay_mem.len var delay = secs[i] * sampleRate if delay < 0.0 { @@ -664,7 +701,7 @@ process { } if write >= 0 && write < len { - storage[write] = x + write_delay(delay_mem, write, x) } let read_pos_0 = write as f32 - delay @@ -683,8 +720,8 @@ process { if i0 >= 0 && i0 < len { if i1 >= 0 && i1 < len { - let a = storage[i0] - let b = storage[i1] + let a = delay_mem[i0] + let b = delay_mem[i1] output[i] = lerp(a, b, frac) } else { output[i] = 0.0 @@ -705,8 +742,12 @@ This is a ring buffer. The write head moves forward one sample at a time and wra start. The read position trails behind by the amount set by `secs`, and the output is linearly interpolated for fractional delays. -This assumes `storage` is longer than the maximum delay you want. Set the Inspector -Samples value high enough to cover the longest delay time in your patch. +The `write_delay` helper uses `require` clauses so the checker knows its index is valid +inside the helper body. The call site still keeps a nearby guard, which proves the +`require` clauses before the helper call. The reads stay inline under explicit guards. + +This assumes `delay_mem` is longer than the maximum delay you want. At 48 kHz, 65536 +samples is about 1.36 seconds. --- ## 8. ADSR Envelope diff --git a/docs/tutorial.md b/docs/tutorial.md index dbe78fd3..031af84c 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -635,6 +635,34 @@ One of Lyte's safety features is that it checks array access *before* your code For example, accessing `array[1]` on a `[i32; 1]` array (which only has index `0`) is caught before your program ever runs. No mysterious crashes at runtime. +### Function preconditions with `require` + +When a helper function indexes into a slice, Lyte may need an explicit promise about the index. Audulus beta 983 and newer Lyte builds support `require` clauses for this. + +Write `require` after the parameter list and before the body, matching the Audulus release-note form: + +```lyte +set(arr: [i32], idx: i32, value: i32) require idx >= 0 require idx < arr.len { + arr[idx] = value +} +``` + +This means "`set` may only be called when `idx` is inside `arr`." The safety checker uses those clauses inside the function, so `arr[idx]` is accepted. It also checks every call to `set`; if the caller cannot prove the clauses, Lyte reports an error such as: + +``` +❌ couldn't prove require clause `idx < arr.len` for call to `set` +``` + +You can write multiple `require` clauses, or combine them with `&&`: + +```lyte +set(arr: [i32], idx: i32, value: i32) require idx >= 0 && idx < arr.len { + arr[idx] = value +} +``` + +For Audulus DSP code, this is useful for small setter-style buffer helpers. If the checker complains at the call site, put the call inside a guard like `if idx >= 0 && idx < buffer.len { ... }`, or use a loop range that proves the index is in bounds. Do not document a return-valued helper form until that exact syntax is confirmed in the target Audulus build. + ### Arrays are copied by value When you assign an array to a new variable, you get an independent copy. Changing one doesn't affect the other: @@ -764,6 +792,14 @@ The key difference: - `[i32; 5]` — a fixed array of exactly 5 integers. The size is known at *compile time* — meaning Lyte knows it before your program ever runs. - `[i32]` — a slice, a view into any array of integers. The size is only known at *runtime* — meaning when your program is actually running and the data exists. +When a function takes a slice and writes through an index, add `require` clauses if the helper expects the caller to provide a valid index: + +```lyte +write_sample(buffer: [f32], idx: i32, value: f32) require idx >= 0 require idx < buffer.len { + buffer[idx] = value +} +``` + ### Default slices have length zero A slice variable declared without a value is safe to use — its `.len` is `0`, not garbage: From 4b6188a34b7f0269643aaf6e3914824aa2c0c4ac Mon Sep 17 00:00:00 2001 From: jersmi Date: Sat, 29 Aug 2026 15:01:05 -0700 Subject: [PATCH 2/2] Update Lyte docs for current syntax --- README.md | 5 + docs/LLVM_CALL_DEPTH_REGRESSION.md | 2 +- docs/{tutorial.md => language-guide.md} | 104 ++++++++++++------ .../{lyte-dsp-quickstart.md => quickstart.md} | 31 +++++- 4 files changed, 103 insertions(+), 39 deletions(-) rename docs/{tutorial.md => language-guide.md} (94%) rename docs/{lyte-dsp-quickstart.md => quickstart.md} (90%) diff --git a/README.md b/README.md index 619a7773..11506d86 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,11 @@ cargo run --bin lyte hello.lyte -r cargo test --workspace ``` +## Documentation + +- [Lyte DSP Quick Start](docs/quickstart.md) - practical Audulus DSP node examples and current DSP authoring notes. +- [Learning Lyte](docs/language-guide.md) - beginner-friendly language guide for current Lyte syntax and behavior. + ### LLVM JIT Backend (Optional) Lyte includes an optional LLVM JIT backend alongside the default Cranelift backend. To use it, you need LLVM 18 installed: diff --git a/docs/LLVM_CALL_DEPTH_REGRESSION.md b/docs/LLVM_CALL_DEPTH_REGRESSION.md index 867d189d..b1cef27f 100644 --- a/docs/LLVM_CALL_DEPTH_REGRESSION.md +++ b/docs/LLVM_CALL_DEPTH_REGRESSION.md @@ -200,7 +200,7 @@ directive if the flag ever gets tested in CI. 3. Update `build-xcframework.sh` to pass `--no-recursion`, recovering the reverb regression without touching any user code or tests. 4. Optionally: expose the flag in the Audulus host's compile call and - document it in `docs/lyte-dsp-quickstart.md` for DSP authors. + document it in `docs/quickstart.md` for DSP authors. **Naming note:** `--no-recursion` is precise but narrow. A broader name like `--realtime` could eventually gate other realtime-hostile features diff --git a/docs/tutorial.md b/docs/language-guide.md similarity index 94% rename from docs/tutorial.md rename to docs/language-guide.md index a9312c1f..0716458a 100644 --- a/docs/tutorial.md +++ b/docs/language-guide.md @@ -72,12 +72,12 @@ Note for Lua users: Lua lets one variable hold different kinds of values over ti | Type | What it is | Example values | |------|------------|----------------| -| `i32` | A whole number (integer) | `42`, `-7`, `0`, `42i32` | -| `f32` | A decimal number (float) | `3.14`, `-0.5`, `1.0`, `1.0f32` | +| `i32` | A whole number (integer) | `42`, `-7`, `0` | +| `f32` | A decimal number (float) | `3.14`, `-0.5`, `1.0` | | `bool` | True or false | `true`, `false` | | `str` | Text (a string in quotes) | `"hello"` | | `i8` | A very small whole number, used for individual characters | `65` (the letter `A`) | -| `u32` | A whole number that cannot be negative (unsigned) | `42u32`, `42u`, `0u32` | +| `u32` | A whole number that cannot be negative (unsigned) | `42`, `0` | Single characters can also be written with single quotes: `'a'`, `'Z'`, `'\n'` (newline). This is called a character literal. @@ -103,24 +103,6 @@ In audio and DSP work, you will use `f32` constantly. Audio signals, frequencies **A note on `f64`:** The language grammar also defines an `f64` type, which is a higher-precision decimal number. In Audulus DSP work, `f32` is the type you'll usually use. -By default, whole-number literals are `i32`, and decimal literals are `f32`. - -### Numeric literal suffixes - -Numeric literals can include a type suffix when you need the literal itself to have a specific type: - -```lyte -main { - var count = 1i32 - var mask = 1u32 - var also_unsigned = 1u - var gain = 1.0f32 - var precise = 1.0f64 -} -``` - -Use `as` when converting an existing value or expression from one type to another, such as `x as i32` or `(len - 2) as f32`. - ### Why does static typing matter? In audio DSP, you are doing a lot of math very quickly. If the language has to keep figuring out what kind of value something is while it runs, that adds overhead. By knowing the types up front, Lyte can build faster code. Here, **compile** just means “turn your code into something the computer can run.” @@ -138,6 +120,19 @@ f { The phrase "no solution for X == Y" is Lyte's general way of saying "I couldn't make these two types agree." You'll see this same phrasing for other type mismatches too — mismatched array sizes, wrong argument types, and so on. If you ever see it, it means something on the left side of an operation doesn't match the type on the right. +### Numeric literal suffixes + +A numeric literal can carry an explicit type suffix instead of relying on inference: `1i32`, `1u32` (or `1u` as a shorthand), `1.0f32`, `1.0f64`. This is mainly useful when mixing `f32` and `f64` in the same expression, or when you want to pin down a literal's type without introducing an extra variable: + +```lyte +main { + assert(1.0f64 + 2.0f64 == 3.0f64) + assert(5i32 == 5) +} +``` + +An invalid suffix (like `1.0i32`, a float literal with an integer suffix) is a lexer error. In everyday Audulus DSP code, which is almost always plain `f32`, you won't need these often. + --- ## 3. Your First Program — Hello World @@ -332,6 +327,19 @@ main { `while` keeps looping as long as the condition is true. Unlike `for`, you manage the loop variable yourself. +### Semicolons as statement separators + +Lyte normally expects one statement per line, but you can also separate statements with `;` on the same line, including a trailing one right before a closing brace: + +```lyte +main { + var x = 1; var y = 2; var z = x + y + assert(z == 3) +} +``` + +This is rarely necessary — one statement per line is the more readable default for DSP code — but it's valid Lyte if you write or encounter it. + --- ## 6. Functions @@ -1293,7 +1301,7 @@ So this section is best read as the language idea behind a biquad. For practical > *Aside — why this runs efficiently: the multiply-add chains in the biquad formula (like `b0*x + b1*x1`) map directly to a hardware instruction called FMA, or fused multiply-add, which performs a multiplication and an addition in a single step rather than two. Lyte's compiler targets these instructions automatically when generating code for this kind of expression, which is one reason DSP code in Lyte can run efficiently.* -This example brings together structs, `f32` math, tuples, and functions — all of the major concepts covered in this tutorial. +This example brings together structs, `f32` math, tuples, and functions — all of the major concepts covered in this guide. > *Performance note: Lyte's repository includes a benchmark that runs this exact filter — 10 million samples of a 440 Hz sine wave through a 1 kHz lowpass biquad — and compares the results against C, Lua 5.5, and LuaJIT. It's designed to measure how close Lyte's performance gets to C for a representative DSP workload.* @@ -1308,6 +1316,7 @@ This example brings together structs, `f32` math, tuples, and functions — all | `var x = 42` | Mutable variable, type inferred | | `let y = 3.14` | Immutable (fixed) variable, type inferred | | `var x: f32` | Mutable variable, explicit type, no value yet | +| `1i32`, `1u32`, `1.0f32`, `1.0f64` | Numeric literal with an explicit type suffix | ### Basic types @@ -1365,8 +1374,6 @@ This example brings together structs, `f32` math, tuples, and functions — all **Modulo (`%`)** gives you the remainder left over after a division. For example, `10 % 3` is `1`, because 10 divided by 3 is 3 with 1 left over. In DSP this is useful for things like wrapping a value around a range — keeping a phase counter cycling between 0 and some maximum, for instance. -`%` works on floats too, and there it behaves like C's `fmod`: the result takes the sign of the left operand, so `-0.25 % 1.0` is `-0.25`. If you want the result to take the sign of the right operand instead (so that `mod(-0.25, 1.0)` is `0.75`), use the stdlib's `mod(x, y)`. - **Power (`^`)** raises a number to an exponent. For example, `2 ^ 8` is 256 (2 multiplied by itself 8 times). ### Operator precedence @@ -1538,7 +1545,7 @@ Here are some common math helpers from `stdlib.lyte`: | Function | What it does | |----------|--------------| | `fract(x)` | Fractional part of `x` | -| `mod(x, y)` | Floating-point modulo, result takes the sign of `y` (`%` takes the sign of `x`) | +| `mod(x, y)` | Floating-point modulo | | `clamp(x, lo, hi)` | Clamp `x` into a range | | `step(edge, x)` | `0` below the edge, `1` at or above it | | `smoothstep(edge0, edge1, x)` | Smooth curve between two edges | @@ -1770,9 +1777,9 @@ Any type mismatch on assignment — not just `i32`/`f32` — produces the same e The left side of `==` in the message is always the *expected* type (the variable's declared type); the right side is the *actual* type of the value you tried to assign. -### Inline `if` expressions can cause misleading parser errors +### Inline `if` as a plain assignment can cause misleading parser errors -In Lyte, block-form inline `if` expressions are not reliable in every context. Patterns like these can cause a long chain of parser errors: +An `if`-`else` used as a value works fine as the initializer in a fresh `let` or `var` declaration: ```lyte let x = if cond { @@ -1782,6 +1789,24 @@ let x = if cond { } ``` +The checker requires both branches to produce the same type when the value is actually used, and reports a clear error if they don't: + +``` +❌ if-else branches must have the same type: i32 vs f32 + let y = if c { 1 } else { 2.0 } + ^ +``` + +A branch that ends in a `var`/`let` declaration produces no value at all (declarations are statements, not expressions), so using it as a branch's value is also a clear checker error rather than a silent wrong answer: + +``` +❌ if-else branches must have the same type: i32 vs void + let y = if c { 1 } else { var z = 2 } + ^ +``` + +What still doesn't work is an `if`-`else` used as a *plain assignment* to something that already exists — an existing variable or a port index — rather than as a declaration's initializer: + ```lyte output[i] = if cond { a @@ -1790,16 +1815,25 @@ output[i] = if cond { } ``` -You may see messages like: +```lyte +x = if cond { + a +} else { + b +} +``` + +Both of these still produce a long chain of parser errors, since the parser doesn't accept `if` as the right-hand side of a plain (non-declaring) assignment: ``` Expected expression -Expected declaration, got Let -Expected declaration, got Assign -Expected declaration, got Lbracket +Expected declaration, got Lbrace +Expected declaration, got Integer(1, None) +Expected declaration, got Rbrace +Expected declaration, got Else ``` -The safer pattern is: +The safer pattern for those cases is still to assign a default first, then override with a plain `if` block: ```lyte var x = b @@ -1817,7 +1851,7 @@ if cond { } ``` -This is a little more verbose, but it is much more dependable in current Lyte. +This is a little more verbose, but it works in every position, including inside the per-sample loop, where it's the more common case anyway. ### Calling an undefined macro @@ -1883,7 +1917,7 @@ If you want a generic struct, declare the type parameter in the struct name: `st ## 20. Features in the Grammar Not Yet Covered -The official Lyte grammar file reveals a few features that exist in the language but aren't documented in the README and aren't yet covered in this tutorial. They're listed here so you're aware they exist, without going into detail that could become outdated as Lyte develops. +The official Lyte grammar file reveals a few features that exist in the language but aren't documented in the README and aren't yet covered in this guide. They're listed here so you're aware they exist, without going into detail that could become outdated as Lyte develops. **`arena`** — reserved for future use in the current Lyte repo. If you try to use it today, the compiler reports `arena is reserved for future use`. @@ -1898,5 +1932,5 @@ These are cosmetic alternatives — the standard ASCII versions work just as wel --- -*This tutorial was written as a companion to the official Lyte README and grammar.* +*This guide was written as a companion to the official Lyte README and grammar.* *Source: https://github.com/audulus/lyte* diff --git a/docs/lyte-dsp-quickstart.md b/docs/quickstart.md similarity index 90% rename from docs/lyte-dsp-quickstart.md rename to docs/quickstart.md index 87e96b38..2a70ee6d 100644 --- a/docs/lyte-dsp-quickstart.md +++ b/docs/quickstart.md @@ -47,7 +47,7 @@ You do not create `frames`, `sampleRate`, or `storage` yourself. Audulus gives t | `storage` | `[f32]` | Persistent host storage supplied by Audulus. Use dedicated global arrays for large DSP buffers such as delay lines. | | `MAX_FRAMES` | `i32` | Maximum possible block size — use to declare stack arrays. | -**`init {}`** runs once when the node loads. Use it for setup values. +**`init {}`** runs once when the node loads. Use it for setup values. A script needs `init {}` present even if it's empty — leaving it out entirely produces a dead script with no error, not a script that just skips setup. This is an easy thing for an AI tool to drop by accident if nothing in the script seems to need initialization, so always double-check `init {}` is there. **`process { ... }`** runs once per block. Most audio work happens here inside `for i in 0 .. frames`. @@ -76,11 +76,36 @@ This also avoids doing a division on every sample. --- +## A Few Newer Syntax Notes + +**Semicolons are optional statement separators.** You can put more than one statement on a line with `;`, including a trailing one before a closing brace: + +```lyte +var t = 0; t = t + a; t = t + b +``` + +This is rarely needed in normal DSP code — one statement per line stays the more readable default — but it's valid if you see it or want to write a short helper compactly. + +**Numeric literal suffixes.** A literal can carry an explicit type suffix: `1i32`, `1u32` (or `1u`), `1.0f32`, `1.0f64`. This mostly matters when you're mixing `f32` and `f64` in the same expression and need to pin down which one a literal is, or when a plain literal would otherwise infer the wrong type. In ordinary Audulus DSP code, which is almost always `f32`, you won't need these often. + +**Float `%` (modulo) is reliable.** `%` on `f32`/`f64` values is a truncated remainder — same convention as C `fmod` or Rust `%`, so the result takes the sign of the left operand (`-5.5 % 2.0` is `-1.5`, not `0.5`). This is useful for wrapping a phase or index into a range: + +```lyte +wrap01(x_in: f32) -> f32 { + var x = x_in % 1.0 + if x < 0.0 { x = x + 1.0 } + x +} +``` + +--- + ## Do's and Don'ts These are good default habits for writing Lyte in Audulus. **Do:** +- Always include `init {}`, even empty. A script missing `init {}` is dead and silent — no error, no output, no obvious clue why. - Prefer `f32` almost everywhere in DSP code. - Treat `sampleRate` like a normal float in math. - Start from the fresh-node block template and change one thing at a time when debugging. @@ -98,7 +123,7 @@ These are good default habits for writing Lyte in Audulus. - Don't rely on helper functions to prove slice indices by context alone. Use inline guards, or add explicit `require` clauses to the helper. - Don't use `assume` in node code — it is only allowed in the standard library or prelude. - Don't assume examples from the standalone Lyte repo will drop into Audulus unchanged. -- Don't rely on block-form inline `if` expressions in assignments such as `let x = if ...` or `output[i] = if ...`. In Lyte those can trigger confusing parser errors. Use a normal assignment first, then a plain `if` block. +- Don't rely on block-form inline `if` expressions in a *plain assignment*, such as `x = if ... else ...` or `output[i] = if ... else ...` — these still trigger a confusing parser error cascade ("Expected expression", "Expected declaration, got ..."). This is narrower than it used to be: `let x = if cond { a } else { b }` and `var x = if cond { a } else { b }` (an `if`-`else` used as the initializer in a declaration) now compile and run correctly, and the checker properly rejects mismatched branch types with a clear error instead of silently producing a wrong answer. If you need to write into an existing variable or a port index conditionally, use a normal assignment first, then a plain `if` block — that pattern still works everywhere and is the safer default to reach for. --- @@ -976,4 +1001,4 @@ The last four `if` lines are the mux part. *These examples are intended as starting points. DSP is iterative — adjust, listen, compare.* -*For language reference, see the companion tutorial: Learning Lyte — A Beginner's Guide.* +*For language reference, see the companion guide: `language-guide.md`.*