Skip to content

Commit 619cbdb

Browse files
authored
Merge pull request #15 from jersmi7/update-docs-v2
Document require clauses in Lyte DSP docs
2 parents 0ce40bf + 02f9388 commit 619cbdb

2 files changed

Lines changed: 90 additions & 13 deletions

File tree

docs/lyte-dsp-quickstart.md

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ When you create a new Lyte DSP node in Audulus, it starts with this template:
1111
```lyte
1212
// Inputs, outputs, `frames`, `sampleRate`, and `storage` are globals.
1313
// Inputs, outputs, and `storage` are slices bound to the engine buffers.
14+
// `storage` is persistent host storage, not the normal place for large DSP buffers.
1415
// MAX_FRAMES is the max processing frames (used to declare temporary arrays).
1516
1617
init {}
@@ -31,19 +32,19 @@ These are the main words you will see right away:
3132
- `input`, `output`: the audio coming in and going out for the current block. `input[i]` means “sample number `i`
3233
- `frames`: how many samples are in this block
3334
- `sampleRate`: how many samples happen each second, usually `44100.0` or `48000.0`
34-
- `storage`: extra memory for things like delay lines
35+
- `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
3536
- `globals`: variables declared outside `process`. They keep their value from one block to the next
3637
- `slice`: a buffer you read with `[...]`
3738
- `MAX_FRAMES`: the biggest block size Audulus may send to the node. Use it when you need a temporary array
3839

39-
You do not create `frames`, `sampleRate`, or `storage` yourself. Audulus gives them to the node automatically.
40+
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.
4041

4142
| Global | Type | Description |
4243
|---|---|---|
4344
| `input`, `output` | `[f32]` | Default port slices. Add ports as needed above the code viewer in the Inspector. |
4445
| `frames` | `i32` | Number of samples in this processing block. |
4546
| `sampleRate` | `f32` | Current sample rate, e.g. `44100.0`. |
46-
| `storage` | `[f32]` | Pre-allocated buffer for delay lines etc., set the size in the Inspector. |
47+
| `storage` | `[f32]` | Persistent host storage supplied by Audulus. Use dedicated global arrays for large DSP buffers such as delay lines. |
4748
| `MAX_FRAMES` | `i32` | Maximum possible block size — use to declare stack arrays. |
4849

4950
**`init {}`** runs once when the node loads. Use it for setup values.
@@ -85,21 +86,48 @@ These are good default habits for writing Lyte in Audulus.
8586
- Start from the fresh-node block template and change one thing at a time when debugging.
8687
- Use `sin`, `cos`, `tan`, and other unsuffixed math builtins.
8788
- Read a port sample into a scratch `f32` first if the compiler gets ambiguous about expressions like `input[i] * inv_sr`.
88-
- Keep slice index proofs inline near `storage[...]` accesses when the safety checker complains.
89+
- Keep slice index proofs inline near buffer accesses when the safety checker complains.
90+
- Use `require` on slice helpers when the caller can prove the index is valid.
8991
- 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.
9092
- Move expensive math out of the sample loop when true audio-rate updates are not needed.
9193
- Try block-rate control first when it sounds good enough. It is usually simpler and cheaper.
9294

9395
**Don't:**
9496
- 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.
9597
- 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`.
96-
- Don't rely on helper functions to prove slice indices are safe; the checker usually wants the bounds checks inline.
98+
- Don't rely on helper functions to prove slice indices by context alone. Use inline guards, or add explicit `require` clauses to the helper.
9799
- Don't use `assume` in node code — it is only allowed in the standard library or prelude.
98100
- Don't assume examples from the standalone Lyte repo will drop into Audulus unchanged.
99101
- 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.
100102

101103
---
102104

105+
## Slice Helper Preconditions
106+
107+
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.
108+
109+
```lyte
110+
set(arr: [i32], idx: i32, value: i32) require idx >= 0 require idx < arr.len {
111+
arr[idx] = value
112+
}
113+
```
114+
115+
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.
116+
117+
You can also combine the bounds into one clause:
118+
119+
```lyte
120+
set(arr: [i32], idx: i32, value: i32) require idx >= 0 && idx < arr.len {
121+
arr[idx] = value
122+
}
123+
```
124+
125+
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.
126+
127+
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.
128+
129+
---
130+
103131
## Debug Printing
104132

105133
`println` is useful for simple debugging. For numbers, write into a
@@ -626,27 +654,36 @@ process {
626654
---
627655
## 7. Delay Line
628656

629-
A delay line stores past samples in `storage` and reads them back later. This is the core
630-
of echo, comb filters, chorus, flanging, and many reverb designs.
657+
A delay line stores past samples in a dedicated memory block and reads them back later.
658+
This is the core of echo, comb filters, chorus, flanging, and many reverb designs.
659+
660+
`storage` is persistent Audulus node storage, but it is not the preferred place for a
661+
large delay buffer. For delay lines, declare a global array with the length you need.
662+
This example dedicates 65536 samples of memory to the delay line.
631663

632664
This is different from unit delay. Unit delay is always one sample. A delay line is a
633665
larger buffer measured in samples, milliseconds, or seconds.
634666

635667
*Ports: `input`, `secs` in, `output` out.*
636668

637669
```lyte
670+
var delay_mem: [f32; 65536]
638671
var write: i32
639672
640673
lerp(a: f32, b: f32, t: f32) -> f32 {
641674
a + (b - a) * t
642675
}
643676
677+
write_delay(buffer: [f32], idx: i32, value: f32) require idx >= 0 require idx < buffer.len {
678+
buffer[idx] = value
679+
}
680+
644681
init {}
645682
646683
process {
647684
for i in 0 .. frames {
648685
let x = input[i]
649-
let len = storage.len
686+
let len = delay_mem.len
650687
651688
var delay = secs[i] * sampleRate
652689
if delay < 0.0 {
@@ -664,7 +701,7 @@ process {
664701
}
665702
666703
if write >= 0 && write < len {
667-
storage[write] = x
704+
write_delay(delay_mem, write, x)
668705
}
669706
670707
let read_pos_0 = write as f32 - delay
@@ -683,8 +720,8 @@ process {
683720
684721
if i0 >= 0 && i0 < len {
685722
if i1 >= 0 && i1 < len {
686-
let a = storage[i0]
687-
let b = storage[i1]
723+
let a = delay_mem[i0]
724+
let b = delay_mem[i1]
688725
output[i] = lerp(a, b, frac)
689726
} else {
690727
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
705742
start. The read position trails behind by the amount set by `secs`, and the output is
706743
linearly interpolated for fractional delays.
707744

708-
This assumes `storage` is longer than the maximum delay you want. Set the Inspector
709-
Samples value high enough to cover the longest delay time in your patch.
745+
The `write_delay` helper uses `require` clauses so the checker knows its index is valid
746+
inside the helper body. The call site still keeps a nearby guard, which proves the
747+
`require` clauses before the helper call. The reads stay inline under explicit guards.
748+
749+
This assumes `delay_mem` is longer than the maximum delay you want. At 48 kHz, 65536
750+
samples is about 1.36 seconds.
710751

711752
---
712753
## 8. ADSR Envelope

docs/tutorial.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,34 @@ One of Lyte's safety features is that it checks array access *before* your code
635635

636636
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.
637637

638+
### Function preconditions with `require`
639+
640+
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.
641+
642+
Write `require` after the parameter list and before the body, matching the Audulus release-note form:
643+
644+
```lyte
645+
set(arr: [i32], idx: i32, value: i32) require idx >= 0 require idx < arr.len {
646+
arr[idx] = value
647+
}
648+
```
649+
650+
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:
651+
652+
```
653+
❌ couldn't prove require clause `idx < arr.len` for call to `set`
654+
```
655+
656+
You can write multiple `require` clauses, or combine them with `&&`:
657+
658+
```lyte
659+
set(arr: [i32], idx: i32, value: i32) require idx >= 0 && idx < arr.len {
660+
arr[idx] = value
661+
}
662+
```
663+
664+
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.
665+
638666
### Arrays are copied by value
639667

640668
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:
764792
- `[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.
765793
- `[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.
766794

795+
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:
796+
797+
```lyte
798+
write_sample(buffer: [f32], idx: i32, value: f32) require idx >= 0 require idx < buffer.len {
799+
buffer[idx] = value
800+
}
801+
```
802+
767803
### Default slices have length zero
768804

769805
A slice variable declared without a value is safe to use — its `.len` is `0`, not garbage:

0 commit comments

Comments
 (0)