@shd101wyy/yo 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,17 @@ pause_then_answer :: (fn(using(io : IO)) -> Impl(Future(i32, IO)))(
26
26
 
27
27
  - `io.async(...)` is lazy
28
28
  - If a function uses `using(io : IO)` and returns a future, include `IO` in the `Future(...)` type
29
+ - **Type annotations can be omitted** in the lambda's `using` params — the compiler infers types from the `Future(T, IO, Raise, ...)` return type. You can even rename the params:
30
+
31
+ ```rust
32
+ // Equivalent — types inferred from Future(i32, IO, Raise) in the return type
33
+ work :: (fn(using(io : IO, raise : Raise)) -> Impl(Future(i32, IO, Raise)))(
34
+ io.async((using(my_io, my_raise)) => { // ← any names, no type annotations needed
35
+ my_io.await(yield());
36
+ my_raise("error")
37
+ })
38
+ );
39
+ ```
29
40
 
30
41
  ## Sequential await
31
42
 
@@ -128,12 +139,50 @@ work :: (fn(using(io : IO, raise : Raise)) -> Impl(Future(i32, IO, Raise)))(
128
139
  - Every effect used by the future should appear in the `Future(...)` type
129
140
  - Effects propagate through `using(...)` just like other contextual parameters
130
141
 
142
+ ## Async recursion — use an iterative worklist instead
143
+
144
+ `recur` does **not** work inside an `io.async` lambda — it refers to the lambda's own signature, not the outer function, so the argument types will not match. Calling the outer function by name is also forbidden in Yo. Attempting either will produce a compile-time error.
145
+
146
+ **Solution**: replace async recursion with an iterative worklist using `ArrayList` as a stack:
147
+
148
+ ```rust
149
+ { read_dir, DirEntry } :: import "std/fs/dir";
150
+
151
+ process_dir :: (fn(root: Path, using(io: IO, exn: Exception)) -> Impl(Future(unit, IO, Exception)))(
152
+ io.async((using(io, exn)) => {
153
+ stack := ArrayList(Path).new();
154
+ { stack.push(root); };
155
+
156
+ while runtime((stack.len() > usize(0))), {
157
+ cur := match(stack.pop(), .Some(p) => p, .None => return ());
158
+ entries := io.await(read_dir(cur));
159
+ // process `entries`, push subdirectories to `stack`
160
+ n := entries.len();
161
+ i := usize(0);
162
+ while runtime((i < n)), {
163
+ match(entries.get(i),
164
+ .None => (),
165
+ .Some(e) => {
166
+ match(e.file_type,
167
+ .Directory => { stack.push(cur.join(Path.new(e.name))); },
168
+ _ => () // handle files here
169
+ );
170
+ }
171
+ );
172
+ i = (i + usize(1));
173
+ };
174
+ };
175
+ })
176
+ );
177
+ ```
178
+
131
179
  ## Common pitfalls
132
180
 
133
181
  - `io.async(...)` does not run immediately
134
182
  - `escape` inside async aborts the future instead of completing it normally
135
183
  - `io.await(...)` on an aborted future can panic; `JoinHandle.await(...)` converts abort into `.None`
136
184
  - Handler functions cannot capture outer variables like closures; pass required state explicitly
185
+ - **`recur` inside `io.async` calls the lambda, not the outer function** — use an iterative worklist for async recursion
137
186
 
138
187
  ## Exception (non-resumable)
139
188
 
@@ -176,7 +225,31 @@ export main;
176
225
  - Handler uses `escape` to discard the continuation and exit the enclosing function
177
226
  - Code after the escaped call is never reached
178
227
 
179
- ## ResumableException
228
+ ### Swallowing exceptions with a fallback value (return in Exception handler)
229
+
230
+ When an exception is thrown inside an async operation (e.g., `cmd.status()` or `cmd.output()`), you can **swallow the error and resume with a fallback value** by using `return` in the handler (not `escape`). The `ResumeType` is the return type of the operation that would have thrown.
231
+
232
+ ```rust
233
+ { Command, ExitStatus, Output } :: import "std/process/command";
234
+
235
+ // Check if a tool is available — returns false if it throws (e.g., not found)
236
+ given(try_exn) := Exception(throw: ((err) -> {
237
+ return ExitStatus(raw: i32(1)); // resume with "failed" exit status
238
+ }));
239
+ status := io.await(cmd.status(using(io, try_exn)));
240
+ available := status.success(); // false if exception was swallowed
241
+
242
+ // For cmd.output(), resume with a failed Output:
243
+ given(out_exn) := Exception(throw: ((err) -> {
244
+ return Output(status: ExitStatus(raw: i32(1)), stdout: ArrayList(u8).new(), stderr: ArrayList(u8).new());
245
+ }));
246
+ out := io.await(cmd.output(using(io, out_exn)));
247
+ if((!(out.status.success())), { return (); }); // handle failure
248
+ ```
249
+
250
+ Key: the `return` inside the handler resumes the _effect invocation site_ with the provided value. The calling code then sees the fallback as if the operation returned normally. Use `escape` only when the enclosing function returns `unit` (e.g., test bodies).
251
+
252
+ **`escape T_value` constraint**: `escape T_value` inside an `Exception` handler requires that the enclosing `io.async` closure's return type matches `T_value`. Due to forward type inference, the evaluator may not know the closure's return type at the point where `given` is declared. This causes a "Expected: unit" error when `escape non_unit` is used in a handler declared before the final return expression. Prefer `return fallback_value` (resume) when possible.
180
253
 
181
254
  `ResumableException(ResumeType)` is a module effect for resumable error handling. The handler uses `return` to resume with a recovery value:
182
255
 
@@ -33,6 +33,7 @@ Key rules:
33
33
  - In **runtime** code, `"hello"` is always `str`. Mixing literal and variable branches in `cond`/`match` works fine.
34
34
  - In **comptime** functions (return type `comptime(...)`), `"hello"` is `comptime_string`. It does NOT auto-convert to `str`. Use `str.from_raw_parts(*(u8)("..."), usize(N))` if a comptime function needs to return `str`.
35
35
  - For `String` constants, prefer `` `hello` `` over `String.from("hello")`.
36
+ - **PITFALL:** Never write `String.from(`hello`)` — backtick strings are already `String`, not `str`. `String.from` takes `str`, so wrapping a backtick in `String.from` causes a type error ("Cannot unify String and str"). Only use `String.from(str_expr)` for actual `str` values.
36
37
 
37
38
  ## Import patterns
38
39
 
@@ -88,6 +89,22 @@ numbers := ArrayList(i32).new();
88
89
  numbers.push(i32(1));
89
90
  numbers.push(i32(2));
90
91
 
92
+ // Index via call syntax (Index trait) — returns the value directly:
93
+ first := numbers(usize(0)); // → i32 (value)
94
+
95
+ // Mutate in place — direct assignment syntax:
96
+ numbers(usize(0)) = i32(99);
97
+
98
+ // When you need the pointer explicitly:
99
+ ptr := &(numbers(usize(0))); // → *(i32)
100
+ ptr.* = i32(100);
101
+
102
+ // Safe access:
103
+ match(numbers.get(usize(0)),
104
+ .Some(v) => println(`${v}`),
105
+ .None => ()
106
+ );
107
+
91
108
  counts := HashMap(String, i32).new();
92
109
  counts.set(`yo`, i32(1));
93
110
  ```
@@ -126,6 +143,20 @@ counter.* = (counter.* + i32(1));
126
143
  - Use `Box(T)` or `box(value)` for owned heap allocation
127
144
  - Use `*(T)` for raw pointers
128
145
  - Model nullable pointers as `Option(*(T))` or `?*(T)`, not sentinel integers
146
+ - Constructor syntax: `Box(T)(value)` — NOT `Box(T).new(value)`
147
+ - For self-referential `object` types, use `Box(Self)` to break the recursive cycle:
148
+
149
+ ```rust
150
+ Node :: object(
151
+ value : i32,
152
+ next : Option(Box(Self)) // Box(Self) breaks the recursive type cycle
153
+ );
154
+
155
+ n := Node(value: i32(1), next: Option(Box(Node)).None);
156
+ // Constructing a Box:
157
+ child := Box(Node)(Node(value: i32(2), next: Option(Box(Node)).None));
158
+ parent := Node(value: i32(1), next: Option(Box(Node)).Some(child));
159
+ ```
129
160
 
130
161
  ## Unicode and platform checks
131
162
 
@@ -225,6 +256,60 @@ println(p1.to_string());
225
256
  - Works for both structs and enums
226
257
  - Custom derives can be registered with `derive_rule`; see [DERIVE_TRAITS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/DERIVE_TRAITS.md)
227
258
 
259
+ ### ⚠️ Circular derive trap: recursive enum with `ArrayList`
260
+
261
+ `derive(T, Eq)` and `derive(T, Clone)` fail when any field's type requires the derived trait to be already registered:
262
+
263
+ ```rust
264
+ // PROBLEM: derive expansion generates `fields_l == fields_r` (ArrayList(Node) needs
265
+ // Eq(Node)), but Eq(Node) isn't registered yet — compile error.
266
+ Node :: enum(Leaf, Branch(children : ArrayList(Self)));
267
+ derive(Node, Eq); // ← ERROR: "No matching call found for __lhs_children == __rhs_children"
268
+ ```
269
+
270
+ **Fix**: skip `derive`, write a manual recursive equality function with `recur`:
271
+
272
+ ```rust
273
+ node_eq :: (fn(a : Node, b : Node) -> bool)(
274
+ match(a,
275
+ .Leaf => match(b, .Leaf => true, _ => false),
276
+ .Branch(acs) =>
277
+ match(b,
278
+ .Branch(bcs) => {
279
+ cond(
280
+ (acs.len() != bcs.len()) => false,
281
+ true => {
282
+ (i : usize) = usize(0);
283
+ (ok : bool) = true;
284
+ while runtime(((i < acs.len()) && ok)), {
285
+ match(acs.get(i),
286
+ .Some(ac) => match(bcs.get(i),
287
+ .Some(bc) => { ok = recur(ac, bc); },
288
+ .None => { ok = false; }
289
+ ),
290
+ .None => { ok = false; }
291
+ );
292
+ i = (i + usize(1));
293
+ };
294
+ ok
295
+ }
296
+ )
297
+ },
298
+ _ => false
299
+ )
300
+ )
301
+ );
302
+
303
+ impl(Node, Eq(Node)(
304
+ (==) : (fn(a : Self, b : Self) -> bool)(node_eq(a, b))
305
+ ));
306
+ ```
307
+
308
+ Same issue applies to `Clone` when fields contain `ArrayList(Self)`.
309
+ Yo's reference counting handles shallow copies automatically (no `Clone` trait call needed);
310
+ the `Clone` trait is only for deep cloning and has the same circularity problem.
311
+ In practice, passing `EvalValue`-like types by value works fine without a `Clone` impl.
312
+
228
313
  ## Error handling
229
314
 
230
315
  ```rust
@@ -51,7 +51,7 @@ total := {
51
51
  };
52
52
  ```
53
53
 
54
- Remember: `{ expr }` without semicolons is a struct literal, not a block.
54
+ Remember: `{ expr }` without semicolons is a struct literal, not a block. The parser now detects this mistake and emits a clear error if the single expression is not a valid struct field.
55
55
 
56
56
  ## Control flow
57
57
 
@@ -90,6 +90,7 @@ Key rules:
90
90
  - In **runtime** code, `"hello"` is `str`. Mixing literals and variables in `cond`/`match` branches is fine.
91
91
  - In **comptime** functions (return type `comptime(...)`), `"hello"` is `comptime_string` — it does NOT auto-convert to `str`.
92
92
  - For `String` constants, prefer `` `hello` `` over `String.from("hello")`.
93
+ - **`assert` takes `str`, not `String`**: `assert(cond, "message")` — always use `""`. Passing a template string `` `...` `` causes a type mismatch. Use a custom `check_str` helper when you need `String` diagnostics.
93
94
 
94
95
  ## Calls, operators, and whitespace
95
96
 
@@ -102,7 +103,9 @@ masked := ((A | B) | C);
102
103
  - Prefer parenthesized calls: `func(arg1, arg2)`
103
104
  - `func (a, b)` is a different parse shape than `func(a, b)`
104
105
  - Yo has no operator precedence; fully parenthesize binary expressions
105
- - Parenthesize unary operands: `!(ready)`, `-(value)`
106
+ - **All unary operators (`!`, `&`, `-`, `~`) greedily consume everything that follows, including comma-separated args.** `func(&s, a, b)` is parsed as `func(&(s, a, b))` — ONE tuple argument! Always wrap: `p := &s; func(p, a, b)` or use `func((&s), a, b)`.
107
+ - Parenthesize other unary operands too: `!(ready)`, `-(value)`
108
+ - **`!x && y` is parsed as `!(x && y)`**, not `(!x) && y`. Prefix `!` greedily consumes the full right-hand expression. To get `(!x) && y`, write `((!x) && y)` with explicit inner parens.
106
109
 
107
110
  ## Functions and methods
108
111
 
@@ -126,6 +129,7 @@ impl(Counter,
126
129
  - `Self` also works inside generic type constructors — it refers to the current instantiation (e.g., `Tree(T)` inside `Tree`). Use `recur(args)` only when type arguments differ from the current instantiation.
127
130
  - Wrap `fn` types in parentheses when they appear after `:`
128
131
  - **Forward references between methods in the same `impl` block are supported.** A method defined later in the block can be called by a method defined earlier. Both `self.method()` and `Self.method(...)` dispatch work. Only the canonical `name : (fn(...) -> R)(body)` method shape participates; bare lambdas do not get forward-ref shells.
132
+ - **Module-level `::` function definitions are processed in order.** A function body that calls another function declared later in the same file will fail with "Variable not found". Always define leaf helpers first (bottom-up order): `eval_identifier` → `eval_atom` → `evaluate`.
129
133
 
130
134
  ### Named arguments and default values
131
135
 
@@ -223,6 +227,20 @@ text := match(value,
223
227
  - Construction and match branches use the leading `.`
224
228
  - Nested destructuring is not supported; match one layer at a time
225
229
 
230
+ Three destructuring shapes for arms (mix freely across arms):
231
+
232
+ ```rust
233
+ Shape :: enum(Circle(radius : i32), Rectangle(width : i32, height : i32));
234
+
235
+ match(s,
236
+ .Circle(r) => (r * r), // positional
237
+ .Rectangle(width: w, height: h) => (w * h), // labeled
238
+ .Rectangle({width, height: h}) => (width * h) // curly shorthand
239
+ )
240
+ ```
241
+
242
+ Curly `{a, b: c}` is sugar for `(a: a, b: c)` — order-free, supports partial matches (omit fields). Use `{label: _}` to ignore a specific field. Bare `{_}` and empty `{}` are rejected.
243
+
226
244
  ## Generics and compile-time
227
245
 
228
246
  ```rust
@@ -294,14 +312,21 @@ factorial :: (fn(n : i32) -> i32)(
294
312
  )
295
313
  );
296
314
 
297
- while runtime(true), {
315
+ // Runtime infinite loop — `while cond` is ALWAYS runtime
316
+ while true, {
298
317
  work();
299
318
  };
319
+
320
+ // Compile-time loop unrolling — requires comptime() modifier
321
+ while comptime(i < 10), {
322
+ // body evaluated/unrolled at compile time
323
+ };
300
324
  ```
301
325
 
302
326
  - Use `recur(...)` for self-recursion
303
- - `while true` runs at compile time
304
- - Use `while runtime(true), { ... }` for open-ended runtime loops
327
+ - `while cond` is **always a runtime loop** — use this for open-ended loops (e.g., server accept loops, event loops)
328
+ - `while comptime(cond)` explicitly unrolls at compile time — `cond` must be a compile-time-known value
329
+ - Using a comptime-only (`::`) variable in a bare `while` condition without `comptime()` is a **compile error** (would be an infinite loop at runtime)
305
330
 
306
331
  ## Return and branch safety
307
332
 
@@ -333,9 +358,27 @@ get_value :: (fn(opt : Option(i32)) -> i32)(
333
358
 
334
359
  - `return expr1, expr2` parses as a single function call: `return(expr1, expr2)`
335
360
  - In `cond` or `match` branches, **always use begin blocks** when you need `return`
361
+ - `return` must be the **last expression** in a begin block — dead code after `return` is rejected. Do NOT write `{ return x; fallback_val }`. Write `{ return x; }` only.
336
362
  - If the whole function is one expression, prefer expression-bodied style and skip `return` entirely
337
363
  - The same trap applies to any function call without parens in match branches
338
364
 
365
+ ## String concatenation pitfall
366
+
367
+ ```rust
368
+ // WRONG — str + str causes "comptime_string vs str" type unification error:
369
+ content := String.from("line1\n" + "line2\n");
370
+
371
+ // CORRECT — use .concat() on String objects:
372
+ content := String.from("line1\n").concat(String.from("line2\n"));
373
+
374
+ // Also CORRECT — single long string literal:
375
+ content := String.from("line1\nline2\n");
376
+ ```
377
+
378
+ - `"hello" + "world"` at runtime uses `+` on `str` values, which can cause type mismatches
379
+ - The `str + str` operator can produce a `comptime_string` in some contexts, which is not always compatible with `str`
380
+ - Prefer `.concat()` method on `String` objects when building multi-part strings at runtime
381
+
339
382
  ## Iterator and for loop
340
383
 
341
384
  ```rust
@@ -381,7 +424,150 @@ test "Async test", {
381
424
  - `comptime_assert(condition)` — compile-time assertion
382
425
  - `comptime_expect_error(expr)` — verify code produces a compile error
383
426
 
384
- ## Advanced features (reference)
427
+ ## Common pitfalls
428
+
429
+ ### `impl(...)` requires a trailing semicolon
430
+
431
+ ```rust
432
+ // WRONG — "Invalid function call on type" at runtime:
433
+ impl(MyType,
434
+ get : (fn(self : Self) -> i32)(self.x)
435
+ )
436
+
437
+ // CORRECT:
438
+ impl(MyType,
439
+ get : (fn(self : Self) -> i32)(self.x)
440
+ );
441
+ ```
442
+
443
+ ### `___` discard variable cannot appear twice in the same scope
444
+
445
+ ```rust
446
+ // WRONG — shadowing of ___ is not allowed:
447
+ ___ := foo();
448
+ ___ := bar();
449
+
450
+ // CORRECT — use unique names or bare calls:
451
+ _a := foo();
452
+ _b := bar();
453
+ // or simply:
454
+ foo();
455
+ bar();
456
+ ```
457
+
458
+ ### `type` is a reserved keyword — avoid as field/param name
459
+
460
+ ```rust
461
+ // WRONG:
462
+ Variable :: object(name : String, type : TypeValue);
463
+
464
+ // CORRECT:
465
+ Variable :: object(name : String, ty : TypeValue);
466
+ ```
467
+
468
+ ### ArrayList indexing uses call syntax
469
+
470
+ ```rust
471
+ list := ArrayList(i32).new();
472
+ list.push(i32(42));
473
+
474
+ val := list(usize(0)); // → i32 (value copy via Index trait)
475
+ list(usize(0)) = i32(99); // mutate in place directly
476
+
477
+ // When you need the pointer explicitly:
478
+ ptr := &(list(usize(0))); // → *(i32)
479
+ ptr.* = i32(99); // also works
480
+
481
+ // Safe access (returns Option(T)):
482
+ match(list.get(usize(0)),
483
+ .Some(v) => println(`${v}`),
484
+ .None => ()
485
+ );
486
+ ```
487
+
488
+ - `list(i)` returns the value `T` (not a pointer)
489
+ - `list(i) = val` mutates in place directly (preferred)
490
+ - `&(list(i))` returns `*(T)` if you need the pointer explicitly
491
+ - `list.get(i)` returns `Option(T)` for safe bounds-checked access
492
+
493
+ ### Named fields required for `struct`/`object` constructors
494
+
495
+ ```rust
496
+ Point :: struct(x : i32, y : i32);
497
+
498
+ // CORRECT:
499
+ p := Point(x: i32(1), y: i32(2));
500
+
501
+ // WRONG — positional not supported for struct/object:
502
+ p := Point(i32(1), i32(2));
503
+ ```
504
+
505
+ Enum variant construction is positional (no field names needed).
506
+
507
+ ### Object types (RC) are passed by value
508
+
509
+ `HashMap`, `ArrayList`, and other `object(...)` types are reference-counted. Passing them by value shares the underlying data — mutations are visible to all holders.
510
+
511
+ ```rust
512
+ // DO NOT use pointer params for RC objects:
513
+ // WRONG: fn(m : *(HashMap(String, V))) — will cause greedy & issues at call site
514
+ // CORRECT: fn(m : HashMap(String, V)) — pass by value, mutations propagate via RC
515
+
516
+ process_map :: (fn(m : HashMap(String, i32)) -> unit)({
517
+ m.set(String.from("key"), i32(42)); // mutation visible to caller
518
+ });
519
+
520
+ counts := HashMap(String, i32).new();
521
+ process_map(counts);
522
+ // counts now has "key" => 42
523
+ ```
524
+
525
+ ### Forward references are NOT allowed
526
+
527
+ Top-level bindings are evaluated strictly in order. A function must be defined BEFORE it is called (even inside closures that are called later).
528
+
529
+ ```rust
530
+ // WRONG — forward reference:
531
+ caller :: (fn() -> unit)({ helper(); });
532
+ helper :: (fn() -> unit)({ println("hi"); });
533
+
534
+ // CORRECT — helper before caller:
535
+ helper :: (fn() -> unit)({ println("hi"); });
536
+ caller :: (fn() -> unit)({ helper(); });
537
+ ```
538
+
539
+ This applies to ALL callee-before-caller relationships:
540
+
541
+ - `_walk_dag` before `build_dag`
542
+ - `compile_artifact`, `run_executable`, `run_test_suite` before `execute_node`
543
+ - `execute_node` before `execute_dag`
544
+ - `_print_summary_node` before `print_build_summary`
545
+ - `print_build_summary` before `execute_step`
546
+ - Exports section must come AFTER all definitions
547
+
548
+ ### `if(!cond, block)` — use parentheses
549
+
550
+ The `!` operator is greedy and consumes all following args including the block. Always parenthesize:
551
+
552
+ ```rust
553
+ // WRONG — ! consumes "cond, block" as one arg:
554
+ if(!cond, { do_thing(); });
555
+
556
+ // CORRECT — ! only consumes (cond):
557
+ if((!cond), { do_thing(); });
558
+ ```
559
+
560
+ ### Template strings produce `String`, literals are `str`
561
+
562
+ ```rust
563
+ // Template string `` `...` `` → String
564
+ // String literal "..." → str
565
+
566
+ // If a function takes `str`, call .as_str() on a template string:
567
+ fn_taking_str((`prefix_${value}`).as_str());
568
+
569
+ // Or change the function to take String
570
+ ```
385
571
 
386
572
  These features are powerful but less commonly used. Consult the linked docs for full details.
387
573