@shd101wyy/yo 0.1.17 → 0.1.19
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.
- package/.github/skills/yo-async-effects/SKILL.md +48 -0
- package/.github/skills/yo-async-effects/async-effects-recipes.md +262 -0
- package/.github/skills/yo-core-patterns/SKILL.md +48 -0
- package/.github/skills/yo-core-patterns/core-patterns-cheatsheet.md +318 -0
- package/.github/skills/yo-project-workflow/SKILL.md +47 -0
- package/.github/skills/yo-project-workflow/workflow-cheatsheet.md +255 -0
- package/.github/skills/yo-syntax/SKILL.md +57 -0
- package/.github/skills/yo-syntax/syntax-cheatsheet.md +398 -0
- package/.github/skills/yo-wasm-integration/SKILL.md +46 -0
- package/.github/skills/yo-wasm-integration/wasm-integration-cheatsheet.md +250 -0
- package/README.md +10 -1
- package/out/cjs/index.cjs +355 -355
- package/out/cjs/yo-cli.cjs +547 -545
- package/out/cjs/yo-lsp.cjs +358 -358
- package/out/esm/index.mjs +379 -379
- package/out/types/src/codegen/functions/context.d.ts +2 -0
- package/out/types/src/skills-command.d.ts +4 -0
- package/out/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/scripts/build-site.ts +23 -14
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Yo Syntax Cheatsheet
|
|
2
|
+
|
|
3
|
+
These are baseline syntax rules for portable Yo code.
|
|
4
|
+
|
|
5
|
+
## Mental model
|
|
6
|
+
|
|
7
|
+
- Everything is an expression.
|
|
8
|
+
- Separators change meaning:
|
|
9
|
+
- commas build tuples, arrays, or struct literals
|
|
10
|
+
- semicolons create sequencing or type shapes
|
|
11
|
+
- Prefer explicit syntax over relying on parser guesswork.
|
|
12
|
+
|
|
13
|
+
## Common declaration forms
|
|
14
|
+
|
|
15
|
+
```rust
|
|
16
|
+
{ println } :: import "std/fmt";
|
|
17
|
+
|
|
18
|
+
app_name :: "yo-demo";
|
|
19
|
+
|
|
20
|
+
main :: (fn() -> unit)({
|
|
21
|
+
value := i32(1);
|
|
22
|
+
(message : str) = "hello";
|
|
23
|
+
println(message);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export main;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Top-level binding: `name :: expr;`
|
|
30
|
+
- Local binding: `name := expr;`
|
|
31
|
+
- Typed binding: `(name : Type) = expr;`
|
|
32
|
+
- Function definition: `name :: (fn(args...) -> ReturnType)(body);`
|
|
33
|
+
|
|
34
|
+
## Blocks and expressions
|
|
35
|
+
|
|
36
|
+
| Goal | Write | Avoid |
|
|
37
|
+
| ----------------- | -------------------------- | -------------------------- |
|
|
38
|
+
| Single expression | `cond(...)` | `{ cond(...) }` |
|
|
39
|
+
| Begin block | `{ x := i32(1); x }` | `{ x := i32(1), x }` |
|
|
40
|
+
| Struct literal | `{ name: "yo", ok: true }` | `{ name: "yo"; ok: true }` |
|
|
41
|
+
|
|
42
|
+
```rust
|
|
43
|
+
result := cond(
|
|
44
|
+
ready => .Ok(()),
|
|
45
|
+
true => .Err(`not ready`)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
total := {
|
|
49
|
+
base := i32(40);
|
|
50
|
+
(base + i32(2))
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Remember: `{ expr }` without semicolons is a struct literal, not a block.
|
|
55
|
+
|
|
56
|
+
## Control flow
|
|
57
|
+
|
|
58
|
+
```rust
|
|
59
|
+
value := cond(
|
|
60
|
+
(x < i32(0)) => i32(-1),
|
|
61
|
+
(x == i32(0)) => i32(0),
|
|
62
|
+
true => i32(1)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
label := match(token,
|
|
66
|
+
.Identifier(name) => name,
|
|
67
|
+
.Number(_) => "number",
|
|
68
|
+
.Eof => "eof"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if(done, println("done"), println("pending"));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Always write `cond(...)`, never bare `cond ...`
|
|
75
|
+
- Always write `match(...)`, never bare `match ...`
|
|
76
|
+
- `if(a, b)` and `if(a, b, c)` are macro forms over `cond`
|
|
77
|
+
|
|
78
|
+
## String types
|
|
79
|
+
|
|
80
|
+
| Syntax | Type | Context |
|
|
81
|
+
| ------------------ | ----------------- | -------------------------------- |
|
|
82
|
+
| `"hello"` | `str` | Runtime contexts (most code) |
|
|
83
|
+
| `"hello"` | `comptime_string` | Inside `comptime` functions |
|
|
84
|
+
| `` `hello ${x}` `` | `String` | Always (template string) |
|
|
85
|
+
| `` `hello` `` | `String` | Always (template without interp) |
|
|
86
|
+
| `*(u8)("hello")` | `*(u8)` | Pointer cast for C interop |
|
|
87
|
+
|
|
88
|
+
Key rules:
|
|
89
|
+
|
|
90
|
+
- In **runtime** code, `"hello"` is `str`. Mixing literals and variables in `cond`/`match` branches is fine.
|
|
91
|
+
- In **comptime** functions (return type `comptime(...)`), `"hello"` is `comptime_string` — it does NOT auto-convert to `str`.
|
|
92
|
+
- For `String` constants, prefer `` `hello` `` over `String.from("hello")`.
|
|
93
|
+
|
|
94
|
+
## Calls, operators, and whitespace
|
|
95
|
+
|
|
96
|
+
```rust
|
|
97
|
+
sum := add(i32(1), i32(2));
|
|
98
|
+
flag := ((a > b) && (b > c));
|
|
99
|
+
masked := ((A | B) | C);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- Prefer parenthesized calls: `func(arg1, arg2)`
|
|
103
|
+
- `func (a, b)` is a different parse shape than `func(a, b)`
|
|
104
|
+
- Yo has no operator precedence; fully parenthesize binary expressions
|
|
105
|
+
- Parenthesize unary operands: `!(ready)`, `-(value)`
|
|
106
|
+
|
|
107
|
+
## Functions and methods
|
|
108
|
+
|
|
109
|
+
```rust
|
|
110
|
+
double :: (fn(x : i32) -> i32)(
|
|
111
|
+
(x * i32(2))
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
Counter :: struct(current : i32);
|
|
115
|
+
|
|
116
|
+
impl(Counter,
|
|
117
|
+
next : (fn(self : Self) -> i32)({
|
|
118
|
+
self.current = (self.current + i32(1));
|
|
119
|
+
self.current
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- No space between a function type and its body: `(fn(...) -> T)(...)`
|
|
125
|
+
- Use `Self` in method signatures
|
|
126
|
+
- Wrap `fn` types in parentheses when they appear after `:`
|
|
127
|
+
|
|
128
|
+
### Named arguments and default values
|
|
129
|
+
|
|
130
|
+
```rust
|
|
131
|
+
create_user :: (fn(
|
|
132
|
+
name : String,
|
|
133
|
+
(age : i32) ?= 18
|
|
134
|
+
) -> User)(
|
|
135
|
+
User(name: name, age: age)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
create_user(name: `Alice`);
|
|
139
|
+
create_user(name: `Bob`, age: 30);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- Named arguments must keep the same order as the definition
|
|
143
|
+
- Default values use `?=` and must be compile-time known
|
|
144
|
+
|
|
145
|
+
### Implicit parameters (`using` / `given`)
|
|
146
|
+
|
|
147
|
+
```rust
|
|
148
|
+
Raise :: (fn(msg : String) -> i32);
|
|
149
|
+
|
|
150
|
+
safe_divide :: (fn(x : i32, y : i32, using(raise : Raise)) -> i32)(
|
|
151
|
+
cond(
|
|
152
|
+
(y == i32(0)) => raise(`divide by zero`),
|
|
153
|
+
true => (x / y)
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
caller :: (fn() -> i32)({
|
|
158
|
+
(given(raise) : Raise) = (fn(msg : String) -> i32)({
|
|
159
|
+
return i32(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
safe_divide(i32(10), i32(0))
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `using(name : Type)` declares an implicit parameter (effect)
|
|
167
|
+
- `given(name) := Type(fields...)` installs a handler in the caller's scope
|
|
168
|
+
- Effects are matched by **type**, not by name
|
|
169
|
+
- The handler is auto-resolved at call sites; pass explicitly with `using(name)`
|
|
170
|
+
|
|
171
|
+
### Closures and anonymous functions
|
|
172
|
+
|
|
173
|
+
```rust
|
|
174
|
+
(closure : Impl(Fn(x : i32) -> i32)) = ((x) => (x + i32(1)));
|
|
175
|
+
|
|
176
|
+
result := closure(i32(5));
|
|
177
|
+
|
|
178
|
+
transform :: (fn(list : ArrayList(i32), f : Impl(Fn(x : i32) -> i32)) -> unit)({
|
|
179
|
+
for list.iter(), (ptr) => {
|
|
180
|
+
ptr.* = f(ptr.*);
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- `(params) => expr` — lambda / closure syntax
|
|
186
|
+
- `Impl(Fn(params) -> ReturnType)` — closure type
|
|
187
|
+
- Value types are captured by copy; object types by reference
|
|
188
|
+
- Each closure has a unique type; you cannot assign different closures to the same variable
|
|
189
|
+
|
|
190
|
+
## Imports and modules
|
|
191
|
+
|
|
192
|
+
```rust
|
|
193
|
+
{ Parser } :: import "./parser.yo";
|
|
194
|
+
parser_module :: import "./parser.yo";
|
|
195
|
+
|
|
196
|
+
open import "std/string";
|
|
197
|
+
{ ArrayList } :: import "std/collections/array_list";
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- Use relative imports for nearby `.yo` files
|
|
201
|
+
- Use `open import "std/module"` for standard-library modules you want fully in scope
|
|
202
|
+
- Do not write `import "./file.yo" as name`
|
|
203
|
+
- Do not import `std/prelude`
|
|
204
|
+
|
|
205
|
+
## Enums and pattern matching
|
|
206
|
+
|
|
207
|
+
```rust
|
|
208
|
+
Option :: (fn(comptime(T) : Type) -> comptime(Type))(
|
|
209
|
+
enum(None, Some(value : T))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
(value : Option(i32)) = .Some(i32(42));
|
|
213
|
+
|
|
214
|
+
text := match(value,
|
|
215
|
+
.Some(inner) => "present",
|
|
216
|
+
.None => "missing"
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- Enum definitions omit the leading `.`
|
|
221
|
+
- Construction and match branches use the leading `.`
|
|
222
|
+
- Nested destructuring is not supported; match one layer at a time
|
|
223
|
+
|
|
224
|
+
## Generics and compile-time
|
|
225
|
+
|
|
226
|
+
```rust
|
|
227
|
+
identity :: (fn(forall(T : Type), value : T) -> T)(value);
|
|
228
|
+
|
|
229
|
+
max :: (fn(comptime(a) : i32, comptime(b) : i32) -> comptime(i32))(
|
|
230
|
+
cond((a > b) => a, true => b)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
show :: (fn(forall(T : Type), value : T, where(T <: ToString)) -> unit)(
|
|
234
|
+
println(value)
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- `forall(T : Type)` introduces a generic type parameter
|
|
239
|
+
- `comptime(x) : T` makes a parameter compile-time only
|
|
240
|
+
- `where(T <: Trait)` constrains a type parameter
|
|
241
|
+
- Functions returning `comptime(...)` are evaluated at compile time
|
|
242
|
+
|
|
243
|
+
## Exports
|
|
244
|
+
|
|
245
|
+
```rust
|
|
246
|
+
main :: (fn() -> unit)(());
|
|
247
|
+
export main;
|
|
248
|
+
|
|
249
|
+
export
|
|
250
|
+
helper,
|
|
251
|
+
Config
|
|
252
|
+
;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- `export name;` exports a single binding
|
|
256
|
+
- Block form exports multiple bindings separated by commas
|
|
257
|
+
- Every executable needs `export main;`
|
|
258
|
+
|
|
259
|
+
## Static and dynamic dispatch types
|
|
260
|
+
|
|
261
|
+
```rust
|
|
262
|
+
show :: (fn(value : Impl(ToString)) -> unit)(
|
|
263
|
+
println(value)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
(erased : Dyn(ToString)) = dyn(i32(42));
|
|
267
|
+
println(erased);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- `Impl(Trait)` — static dispatch; concrete type chosen at compile time
|
|
271
|
+
- `Dyn(Trait)` — dynamic dispatch via trait object
|
|
272
|
+
- `dyn(expr)` wraps a concrete value into its `Dyn(Trait)` form
|
|
273
|
+
|
|
274
|
+
## Naming conventions
|
|
275
|
+
|
|
276
|
+
| Kind | Style | Example |
|
|
277
|
+
| --------------------------- | ------------------ | ------------------ |
|
|
278
|
+
| File / directory / module | `snake_case` | `array_list` |
|
|
279
|
+
| Function / variable | `snake_case` | `safe_divide` |
|
|
280
|
+
| Trait / type / enum variant | `PascalCase` | `ToString`, `Some` |
|
|
281
|
+
| Constant | `UPPER_SNAKE_CASE` | `MAX_SIZE` |
|
|
282
|
+
|
|
283
|
+
Use 2-space indentation.
|
|
284
|
+
|
|
285
|
+
## Recursion and loops
|
|
286
|
+
|
|
287
|
+
```rust
|
|
288
|
+
factorial :: (fn(n : i32) -> i32)(
|
|
289
|
+
cond(
|
|
290
|
+
(n <= i32(1)) => i32(1),
|
|
291
|
+
true => (n * recur((n - i32(1))))
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
while runtime(true), {
|
|
296
|
+
work();
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- Use `recur(...)` for self-recursion
|
|
301
|
+
- `while true` runs at compile time
|
|
302
|
+
- Use `while runtime(true), { ... }` for open-ended runtime loops
|
|
303
|
+
|
|
304
|
+
## Return and branch safety
|
|
305
|
+
|
|
306
|
+
```rust
|
|
307
|
+
// WRONG — return consumes the comma, capturing the next match branch:
|
|
308
|
+
match(opt,
|
|
309
|
+
.Some(v) => return v, // parsed as return(v, .None => ...)
|
|
310
|
+
.None => default_value()
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// CORRECT — begin blocks isolate return from the comma:
|
|
314
|
+
match(opt,
|
|
315
|
+
.Some(v) => {
|
|
316
|
+
return v;
|
|
317
|
+
},
|
|
318
|
+
.None => {
|
|
319
|
+
return default_value();
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// BEST — expression-bodied function, no return needed:
|
|
324
|
+
get_value :: (fn(opt : Option(i32)) -> i32)(
|
|
325
|
+
match(opt,
|
|
326
|
+
.Some(v) => v,
|
|
327
|
+
.None => i32(0)
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- `return expr1, expr2` parses as a single function call: `return(expr1, expr2)`
|
|
333
|
+
- In `cond` or `match` branches, **always use begin blocks** when you need `return`
|
|
334
|
+
- If the whole function is one expression, prefer expression-bodied style and skip `return` entirely
|
|
335
|
+
- The same trap applies to any function call without parens in match branches
|
|
336
|
+
|
|
337
|
+
## Iterator and for loop
|
|
338
|
+
|
|
339
|
+
```rust
|
|
340
|
+
{ ArrayList } :: import "std/collections/array_list";
|
|
341
|
+
|
|
342
|
+
list := ArrayList(i32).new();
|
|
343
|
+
list.push(i32(10));
|
|
344
|
+
list.push(i32(20));
|
|
345
|
+
|
|
346
|
+
for list.iter(), (ptr) => {
|
|
347
|
+
println(ptr.*);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
for list.into_iter(), (value) => {
|
|
351
|
+
println(value);
|
|
352
|
+
};
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- `for collection, (variable) => { body }` iterates via the `Iterator` trait
|
|
356
|
+
- `.iter()` borrows the collection and yields pointers
|
|
357
|
+
- `.into_iter()` takes ownership and yields values
|
|
358
|
+
|
|
359
|
+
## Testing
|
|
360
|
+
|
|
361
|
+
```rust
|
|
362
|
+
test "Addition works", {
|
|
363
|
+
assert(((i32(1) + i32(1)) == i32(2)), "1+1 should be 2");
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
test "Compile-time check", {
|
|
367
|
+
comptime_assert((2 + 2) == 4);
|
|
368
|
+
comptime_expect_error({ x :: (1 / 0); });
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
test "Async test", {
|
|
372
|
+
io.await(yield());
|
|
373
|
+
};
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- `test "description", { body }` defines a test — `io : IO` is automatically available
|
|
377
|
+
- All tests can use `io.async(...)`, `io.await(...)`, etc. without a `using` clause
|
|
378
|
+
- `assert(condition, "message")` — runtime assertion (always include a message)
|
|
379
|
+
- `comptime_assert(condition)` — compile-time assertion
|
|
380
|
+
- `comptime_expect_error(expr)` — verify code produces a compile error
|
|
381
|
+
|
|
382
|
+
## Advanced features (reference)
|
|
383
|
+
|
|
384
|
+
These features are powerful but less commonly used. Consult the linked docs for full details.
|
|
385
|
+
|
|
386
|
+
| Feature | Syntax hint | Documentation |
|
|
387
|
+
| ---------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
388
|
+
| Higher-Kinded Types | `forall(F : (fn(comptime(T) : Type) -> comptime(Type)))` | [DESIGN.md § HKT](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/DESIGN.md#higher-kinded-types-hkt) |
|
|
389
|
+
| GADTs | `enum(IntVal(i : i32) -> recur(i32))` | [GADTS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/GADTS.md) |
|
|
390
|
+
| Derive traits | `derive(MyType, Eq, Hash, Clone, Ord, ToString)` | [DERIVE_TRAITS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/DERIVE_TRAITS.md) |
|
|
391
|
+
| Type reflection | `Type.get_info(T)` returns `TypeInfo` | [TYPE_REFLECTION.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/TYPE_REFLECTION.md) |
|
|
392
|
+
| Inline assembly | `asm("mov {0}, #42", out(reg, i32))` | [INLINE_ASSEMBLY.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/INLINE_ASSEMBLY.md) |
|
|
393
|
+
| Metaprogramming | `quote(...)`, `unquote(...)`, `unquote_splicing(...)` | [DESIGN.md § Meta](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/DESIGN.md#meta-programming) |
|
|
394
|
+
| Effect row variables | `forall(...(E))` with `using(...(E))` | [ALGEBRAIC_EFFECTS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ALGEBRAIC_EFFECTS.md) |
|
|
395
|
+
| Custom derive rules | `derive_rule(MyTrait, (fn(...) -> unquote(Expr)){...})` | [DERIVE_TRAITS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/DERIVE_TRAITS.md#user-defined-derive-rules) |
|
|
396
|
+
| Isolated types | `Iso(T)` for data-race-free parallelism | [ISOLATED.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ISOLATED.md) |
|
|
397
|
+
| Arc (atomic ref count) | `arc(value)`, `shared.(*)` for cross-thread sharing | [ARC.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ARC.md) |
|
|
398
|
+
| Parallelism | Thread pool, `io.spawn` for parallel work | [PARALLELISM.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/PARALLELISM.md) |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yo-wasm-integration
|
|
3
|
+
description: Build Yo libraries for WebAssembly and publish as npm packages. Use this when working with WASM targets, Emscripten, WASI, npm packaging, JavaScript/TypeScript wrappers, or browser/Node.js integration.
|
|
4
|
+
argument-hint: "[WASM task, target, or integration question]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Yo WASM Integration
|
|
8
|
+
|
|
9
|
+
Use this skill for compiling Yo projects to WebAssembly and packaging them for JavaScript/TypeScript consumption via npm.
|
|
10
|
+
|
|
11
|
+
## When to use this skill
|
|
12
|
+
|
|
13
|
+
Use this skill when you need to:
|
|
14
|
+
|
|
15
|
+
- compile a Yo project to WebAssembly (Emscripten or WASI)
|
|
16
|
+
- set up an `npm/` package directory with JavaScript/TypeScript wrappers
|
|
17
|
+
- expose Yo functions to JavaScript via WASM memory and C ABI
|
|
18
|
+
- integrate a Yo WASM module into a Node.js or browser project
|
|
19
|
+
- debug WASM-specific compilation or runtime issues
|
|
20
|
+
- publish a Yo-backed npm package
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. Decide on target: Emscripten (browser + Node.js) or WASI (standalone/server).
|
|
25
|
+
2. Define WASM build steps in `build.yo` using `build.executable` with `target: build.CompilationTarget.Wasm32_Emscripten` and `add_c_flags(...)` for Emscripten settings.
|
|
26
|
+
3. Create a JavaScript wrapper in `npm/` that loads the WASM module and exposes a clean API.
|
|
27
|
+
4. Add TypeScript declarations (`index.d.ts`) for type safety.
|
|
28
|
+
5. Test the WASM module from both Node.js and browser contexts.
|
|
29
|
+
6. Consult the [WASM integration cheatsheet](./wasm-integration-cheatsheet.md) for patterns.
|
|
30
|
+
|
|
31
|
+
## High-signal rules
|
|
32
|
+
|
|
33
|
+
- In `build.yo`, use `target: build.CompilationTarget.Wasm32_Emscripten` for Emscripten WASM builds. Use `step.add_c_flags(...)` for Emscripten `-s` options.
|
|
34
|
+
- For single-file compilation, use `yo compile --cc emcc` or `yo compile --target wasm-wasi`.
|
|
35
|
+
- WASM functions communicate through linear memory — pass pointers and lengths, not Yo types.
|
|
36
|
+
- JavaScript wrappers handle the string encoding/decoding boundary (`TextEncoder`/`TextDecoder`).
|
|
37
|
+
- Use bitmask flags (powers of 2) to pass option sets through a single `i32` parameter.
|
|
38
|
+
- Yo's `str` and `String` types are internal — export raw `*(u8)` + `usize` pairs for WASM APIs.
|
|
39
|
+
- Always test with `--sanitize address` on native first, then verify WASM behavior matches.
|
|
40
|
+
- Emscripten exports are controlled by `-sEXPORTED_FUNCTIONS` and `-sEXPORTED_RUNTIME_METHODS`.
|
|
41
|
+
- WASM modules can be loaded in Node.js using `WebAssembly.instantiate` or Emscripten's generated JS glue.
|
|
42
|
+
- Errno values differ on WASM — use constants from `std/libc/errno`, never hardcode numbers.
|
|
43
|
+
|
|
44
|
+
## Resource
|
|
45
|
+
|
|
46
|
+
- [WASM integration cheatsheet](./wasm-integration-cheatsheet.md)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Yo WASM Integration Cheatsheet
|
|
2
|
+
|
|
3
|
+
Patterns for building Yo libraries as WebAssembly modules and consuming them from JavaScript/TypeScript.
|
|
4
|
+
|
|
5
|
+
## Compilation targets
|
|
6
|
+
|
|
7
|
+
| Target | Command | Output |
|
|
8
|
+
| -------------------- | -------------------------------------------------- | ----------------------------- |
|
|
9
|
+
| Emscripten (browser) | `yo compile src/api.yo --cc emcc --release -o api` | `.js` + `.wasm` |
|
|
10
|
+
| WASI (standalone) | `yo compile src/api.yo --target wasm-wasi -o api` | `.wasm` (runs via `wasmtime`) |
|
|
11
|
+
| Native (testing) | `yo compile src/api.yo --release -o api` | Native binary |
|
|
12
|
+
|
|
13
|
+
## WASM API design pattern
|
|
14
|
+
|
|
15
|
+
Export C-compatible functions that operate on linear memory:
|
|
16
|
+
|
|
17
|
+
```rust
|
|
18
|
+
// src/wasm_api.yo
|
|
19
|
+
open import "std/string";
|
|
20
|
+
|
|
21
|
+
// Allocate WASM memory for the caller
|
|
22
|
+
wasm_alloc :: (fn(size : usize) -> *(u8))(
|
|
23
|
+
malloc(size)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Free WASM memory
|
|
27
|
+
wasm_free :: (fn(ptr : *(u8)) -> unit)(
|
|
28
|
+
free(ptr)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Process input and return result pointer + length
|
|
32
|
+
render :: (fn(input_ptr : *(u8), input_len : usize, flags : i32) -> *(u8))({
|
|
33
|
+
(input : str) = str.from_raw_parts(input_ptr, input_len);
|
|
34
|
+
// ... process input ...
|
|
35
|
+
result := do_work(input);
|
|
36
|
+
// Write length to a known location, return pointer
|
|
37
|
+
result_ptr
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export wasm_alloc;
|
|
41
|
+
export wasm_free;
|
|
42
|
+
export render;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Key rules:
|
|
46
|
+
|
|
47
|
+
- Export only `fn` functions with primitive or pointer types
|
|
48
|
+
- Use `*(u8)` + `usize` for strings across the boundary
|
|
49
|
+
- Use `i32` bitmask flags for option sets (powers of 2)
|
|
50
|
+
- Always provide `wasm_alloc` / `wasm_free` for the JavaScript side
|
|
51
|
+
|
|
52
|
+
## Bitmask flags pattern
|
|
53
|
+
|
|
54
|
+
Pass multiple boolean options as a single `i32` using bit flags:
|
|
55
|
+
|
|
56
|
+
```rust
|
|
57
|
+
// Each option is a power of 2
|
|
58
|
+
// bit 0 = 1 : feature_a
|
|
59
|
+
// bit 1 = 2 : feature_b
|
|
60
|
+
// bit 2 = 4 : feature_c
|
|
61
|
+
// bit 3 = 8 : feature_d
|
|
62
|
+
|
|
63
|
+
parse_flags :: (fn(flags : i32) -> Options)({
|
|
64
|
+
(opts : Options) = Options.default();
|
|
65
|
+
opts.feature_a = ((flags & i32(1)) != i32(0));
|
|
66
|
+
opts.feature_b = ((flags & i32(2)) != i32(0));
|
|
67
|
+
opts.feature_c = ((flags & i32(4)) != i32(0));
|
|
68
|
+
opts.feature_d = ((flags & i32(8)) != i32(0));
|
|
69
|
+
opts
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
JavaScript side:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
function buildFlags(options) {
|
|
77
|
+
let flags = 0;
|
|
78
|
+
if (options.featureA) flags |= 1;
|
|
79
|
+
if (options.featureB) flags |= 2;
|
|
80
|
+
if (options.featureC) flags |= 4;
|
|
81
|
+
if (options.featureD) flags |= 8;
|
|
82
|
+
return flags;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## build.yo WASM target
|
|
87
|
+
|
|
88
|
+
The `Executable` struct accepts: `name`, `root`, `target`, `optimize`, `allocator`, `sanitize`.
|
|
89
|
+
Emscripten-specific flags go in `add_c_flags(...)` after creating the step.
|
|
90
|
+
|
|
91
|
+
```rust
|
|
92
|
+
build :: import "std/build";
|
|
93
|
+
|
|
94
|
+
wasm_api :: build.executable({
|
|
95
|
+
name: "my_lib_wasm_api",
|
|
96
|
+
root: "./src/wasm_api.yo",
|
|
97
|
+
target: build.CompilationTarget.Wasm32_Emscripten,
|
|
98
|
+
optimize: build.Optimize.ReleaseSmall,
|
|
99
|
+
allocator: build.Allocator.Libc
|
|
100
|
+
});
|
|
101
|
+
wasm_api.add_c_flags("-O3 -flto -mbulk-memory -sALLOW_MEMORY_GROWTH -sENVIRONMENT=web,node -sMODULARIZE=1 -sEXPORT_NAME=createModule -sEXPORTED_FUNCTIONS=_wasm_alloc,_wasm_free,_render -sEXPORTED_RUNTIME_METHODS=HEAPU8");
|
|
102
|
+
|
|
103
|
+
install :: build.step("install", "Build WASM module");
|
|
104
|
+
install.depend_on(wasm_api);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Available targets: `Wasm32_Emscripten`, `Wasm32_Wasi`, `X86_64_Linux_Gnu`, `Aarch64_Macos`, etc.
|
|
108
|
+
Available optimizations: `Debug`, `ReleaseSafe`, `ReleaseFast`, `ReleaseSmall`.
|
|
109
|
+
Available allocators: `Mimalloc` (default), `Libc`.
|
|
110
|
+
|
|
111
|
+
## npm package structure
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
npm/
|
|
115
|
+
├── package.json # npm package metadata
|
|
116
|
+
├── index.js # JavaScript wrapper (loads WASM, exposes clean API)
|
|
117
|
+
├── index.d.ts # TypeScript declarations
|
|
118
|
+
├── my_lib_wasm_api.js # Emscripten-generated JS glue (build artifact)
|
|
119
|
+
└── my_lib_wasm_api.wasm # WASM binary (build artifact)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## JavaScript wrapper pattern
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// npm/index.js
|
|
126
|
+
const createModule = require("./my_lib_wasm_api.js");
|
|
127
|
+
|
|
128
|
+
let moduleInstance = null;
|
|
129
|
+
|
|
130
|
+
async function initModule() {
|
|
131
|
+
if (!moduleInstance) {
|
|
132
|
+
moduleInstance = await createModule();
|
|
133
|
+
}
|
|
134
|
+
return moduleInstance;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createRenderer() {
|
|
138
|
+
let mod = null;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
async render(input, options = {}) {
|
|
142
|
+
if (!mod) mod = await initModule();
|
|
143
|
+
|
|
144
|
+
const flags = buildFlags(options);
|
|
145
|
+
const encoder = new TextEncoder();
|
|
146
|
+
const inputBytes = encoder.encode(input);
|
|
147
|
+
const inputLen = inputBytes.length;
|
|
148
|
+
|
|
149
|
+
// Allocate WASM memory and copy input
|
|
150
|
+
const inputPtr = mod._wasm_alloc(inputLen);
|
|
151
|
+
mod.HEAPU8.set(inputBytes, inputPtr);
|
|
152
|
+
|
|
153
|
+
// Call the WASM function
|
|
154
|
+
const resultPtr = mod._render(inputPtr, inputLen, flags);
|
|
155
|
+
|
|
156
|
+
// Read result (assuming null-terminated or length-prefixed)
|
|
157
|
+
const result = readString(mod, resultPtr);
|
|
158
|
+
|
|
159
|
+
// Free memory
|
|
160
|
+
mod._wasm_free(inputPtr);
|
|
161
|
+
mod._wasm_free(resultPtr);
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildFlags(options) {
|
|
169
|
+
let flags = 0;
|
|
170
|
+
if (options.featureA) flags |= 1;
|
|
171
|
+
if (options.featureB) flags |= 2;
|
|
172
|
+
return flags;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { createRenderer };
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## TypeScript declarations
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// npm/index.d.ts
|
|
182
|
+
export interface RenderOptions {
|
|
183
|
+
/** Enable feature A */
|
|
184
|
+
featureA?: boolean;
|
|
185
|
+
/** Enable feature B */
|
|
186
|
+
featureB?: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface Renderer {
|
|
190
|
+
render(input: string, options?: RenderOptions): Promise<string>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createRenderer(): Renderer;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## String passing across WASM boundary
|
|
197
|
+
|
|
198
|
+
JavaScript → WASM:
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
const bytes = encoder.encode(str);
|
|
203
|
+
const ptr = mod._wasm_alloc(bytes.length);
|
|
204
|
+
mod.HEAPU8.set(bytes, ptr);
|
|
205
|
+
// Pass ptr and bytes.length to WASM function
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
WASM → JavaScript:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
// If result is pointer + length
|
|
212
|
+
function readString(mod, ptr, len) {
|
|
213
|
+
const bytes = mod.HEAPU8.subarray(ptr, ptr + len);
|
|
214
|
+
return new TextDecoder().decode(bytes);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If result stores length at a known offset
|
|
218
|
+
function readStringWithStoredLength(mod, ptr, lenPtr) {
|
|
219
|
+
const len = mod.HEAPU32[lenPtr >> 2];
|
|
220
|
+
return readString(mod, ptr, len);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Testing WASM modules
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Test native first (fast iteration, AddressSanitizer)
|
|
228
|
+
yo compile src/wasm_api.yo --release --sanitize address -o test && ./test
|
|
229
|
+
|
|
230
|
+
# Test Emscripten WASM
|
|
231
|
+
yo compile src/wasm_api.yo --cc emcc --release -o npm/my_lib_wasm_api
|
|
232
|
+
|
|
233
|
+
# Test WASI
|
|
234
|
+
yo compile src/wasm_api.yo --target wasm-wasi -o test.wasm && wasmtime test.wasm
|
|
235
|
+
|
|
236
|
+
# Run npm package tests
|
|
237
|
+
cd npm && node -e "const m = require('.'); m.createRenderer().render('hello').then(console.log)"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Common WASM pitfalls
|
|
241
|
+
|
|
242
|
+
- **Memory leaks**: Always `_wasm_free` every `_wasm_alloc` on the JavaScript side.
|
|
243
|
+
- **String encoding**: WASM only sees bytes — use `TextEncoder`/`TextDecoder` for UTF-8.
|
|
244
|
+
- **Errno differences**: WASI errno values differ from POSIX. Use `std/libc/errno` constants.
|
|
245
|
+
- **No `main` needed**: Library WASM modules don't need `main` or `export main;` — just export the API functions.
|
|
246
|
+
- **Emscripten environment**: Set `-sENVIRONMENT='web,node'` to support both contexts, or `'node'` for Node.js only.
|
|
247
|
+
- **Module initialization is async**: Emscripten's `createModule()` returns a Promise. Initialize once and reuse.
|
|
248
|
+
- **WASM memory growth**: Always use `-sALLOW_MEMORY_GROWTH=1` for dynamic allocations.
|
|
249
|
+
- **Trailing null bytes**: If your Yo code writes null-terminated strings, the JavaScript reader must know to stop at `\0` or use explicit length.
|
|
250
|
+
- **Native testing first**: Always debug with `--sanitize address` on native before switching to WASM — error messages are much clearer.
|