Skip to content

Commit 0824de3

Browse files
committed
docs: update CLAUDE.md with recent refactoring changes
Update FFI pattern to show to_option() helper, document callback/closure resolution and typedef chains, add guard clause syntax, update BigInt section for consolidated emit functions, note ArgMbtType method predicates.
1 parent eb40dfd commit 0824de3

1 file changed

Lines changed: 21 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ WebIDL specs (@webref/idl) → Parser (webapi_gen/parser/) → AST → Emitter (
7171
- `JsArray` - JavaScript array interop
7272
- `JsObject` - Builder for plain JS objects with arbitrary string keys (e.g., keyframe objects)
7373
- `EventListener` - Callback interface for event handlers
74+
- `JsValue::to_option[T]()` - Convert nullable JsValue to `T?` for externref types
75+
- `JsValue::to_option_prim[T : FromJsAny]()` - Convert nullable JsValue to `T?` for wasm value types (Bool, Int, Double)
7476

7577
## Type Mappings (WebIDL → MoonBit)
7678

@@ -113,11 +115,24 @@ extern "js" fn element_get_attribute_ffi(obj : JsValue, name : JsValue) -> JsVal
113115
114116
///|
115117
impl TElement for Element with get_attribute(self, name) {
116-
let result = element_get_attribute_ffi(TJsValue::to_js(self), TJsValue::to_js(name))
117-
if JsValue::is_null(result) { None } else { Some(result.unsafe_cast()) }
118+
element_get_attribute_ffi(TJsValue::to_js(self), TJsValue::to_js(name)).to_option()
118119
}
119120
```
120121

122+
### Callback and Closure Pattern
123+
124+
Callback-typed method arguments and attribute setters accept closures directly. The generator resolves callback types (including through typedef chains like `EventHandler = EventHandlerNonNull?`) and emits closure-accepting signatures:
125+
126+
```moonbit
127+
// Generated setter accepts a closure directly:
128+
element.set_onclick(fn(event) { ... })
129+
130+
// addEventListener also accepts closures:
131+
element.add_event_listener("click", fn(event) { ... })
132+
```
133+
134+
The resolution logic lives in `try_resolve_callback()` in `interface_emitter.mbt`, which checks both direct callbacks (`type_registry.get_callback_def()`) and typedef→callback chains (`type_registry.resolve_typedef_to_callback()`).
135+
121136
### FromJsAny Pattern
122137

123138
Every generated type (`#external` interfaces, dictionaries, callbacks, typedefs, namespaces) gets a `FromJsAny` impl via `ImplFromJsAny` in the `Emit` enum. Enums get a custom impl using `from_unchecked(String)`. This enables type-safe `JsPromise[T]` resolution on wasm-gc.
@@ -220,6 +235,7 @@ Quick reference for MoonBit patterns that differ from Rust/OCaml and cause frequ
220235
- **Private types**: Use `priv enum` / `priv struct` for types not in the public API; the compiler warns if you forget `priv`
221236
- **`///|` doc comments**: Required before every top-level declaration (function, type, let binding); `moon fmt` adds them automatically
222237
- **Enum constructors in expressions**: Can omit the type prefix when the expected type is known from context (e.g., `HasArg("x")` instead of `ArgMatch::HasArg("x")` when the field type is `ArgMatch`)
238+
- **Guard clauses**: `guard expr is Pattern(x) else { return None }` for early returns from pattern matching on Option/enum types. Preferred over nested `match` when extracting a single variant.
223239

224240
### Parser AST Construction
225241

@@ -242,6 +258,7 @@ Key gotchas:
242258
- **Test file naming**: `*_wbtest.mbt` = whitebox tests (access package-private functions); `*_test.mbt` = blackbox tests (public API only)
243259
- **Test helpers**: Define shared helpers (like `make_arg`, `setup_emitter`) at the top of `_wbtest.mbt` files to reduce boilerplate. The emit package has `setup_test` in `emit_wbtest.mbt`.
244260
- **Multi-target output**: Use `mbt_code_gen_multi(emits)` which returns `{ shared, js_ffi, wasm_ffi }` — always assert all three fields to catch regressions in both JS and wasm-gc output.
261+
- **Type predicates**: Use `ArgMbtType` methods (`is_string()`, `is_enum()`, `is_dictionary()`) — not standalone functions.
245262

246263
## wasm-gc Known Issues
247264

@@ -253,9 +270,9 @@ All examples compile for both js and wasm-gc targets except `fetch-async` (requi
253270

254271
**`JsValue::null()` defaults on non-nullable types (resolved)**: On wasm-gc, `JsValue::null()` returns `externref` (nullable), but string-typed aliases like `CSSOMString = String` map to `(ref extern)` (non-nullable). The MoonBit compiler generates a default-value thunk returning `(ref extern)` that internally calls the nullable `JsNull::null` import — this fails `wasm-tools validate`. The code generator in `method_args()` (`interface_emitter.mbt`) now strips `JsValue::null()` defaults for `Primitive(_)`, `Enum(_)`, **and `Other(_)`** types (the last catches type aliases). The parameter becomes a plain optional and `opt_to_js` sends `undefined` when absent. Use `make validate-wasm` after `make build-examples` to catch these issues early — it runs in <2 seconds vs 14+ seconds for Playwright.
255272

256-
**`externref` vs `(ref extern)` nullability on wasm-gc (resolved)**: On wasm-gc, `JsValue` maps to `externref` (nullable) and `JsAny`/`String` maps to `(ref extern)` (non-nullable). MoonBit's `unsafe_cast()` generates no wasm instructions — it cannot change nullability. To convert `externref``(ref extern)`, use the `jsvalue_to_jsany()` helper (defined in `base.mbt/js_value_wasm.mbt`) which uses the `String?` unwrap trick to emit `ref.as_non_null`. This is needed when passing JsValue results to `FromJsAny::from_js_any()` for primitive type conversions (Bool, Int, Double). See dictionary getter code in `emit.mbt` (`render_dictionary_getter_shared`). For `#external` types returned from wasm FFI imports, the pattern is: declare the FFI as returning `JsValue` (externref), then `.unsafe_cast()` to the target type (see `js_object_wasm.mbt` for an example).
273+
**`externref` vs `(ref extern)` nullability on wasm-gc (resolved)**: On wasm-gc, `JsValue` maps to `externref` (nullable) and `JsAny`/`String` maps to `(ref extern)` (non-nullable). MoonBit's `unsafe_cast()` generates no wasm instructions — it cannot change nullability. To convert `externref``(ref extern)`, use the `jsvalue_to_jsany()` helper (defined in `base.mbt/js_value_wasm.mbt`) which uses the `String?` unwrap trick to emit `ref.as_non_null`. This is needed when passing JsValue results to `FromJsAny::from_js_any()` for primitive type conversions (Bool, Int, Double). The `JsValue::to_option_prim()` helper in `base.mbt/js_value.mbt` encapsulates this pattern for nullable returns. For `#external` types returned from wasm FFI imports, the pattern is: declare the FFI as returning `JsValue` (externref), then `.unsafe_cast()` to the target type (see `js_object_wasm.mbt` for an example).
257274

258-
**`unsigned long long` (UInt64) properties on wasm-gc (resolved)**: The JS runtime code generator now wraps return values of `LongLong`, `UnsignedLongLong`, and `Bigint` types with `BigInt()` in generated getters and methods (e.g., `get_version: (obj) => BigInt(obj.version)`). The `is_bigint_type` helper in `emit_js_runtime.mbt` detects these types and the `is_bigint` parameter on `emit_js_getter`/`emit_js_method`/`emit_js_namespace_getter`/`emit_js_namespace_method` controls wrapping.
275+
**`unsigned long long` (UInt64) properties on wasm-gc (resolved)**: The JS runtime code generator now wraps return values of `LongLong`, `UnsignedLongLong`, and `Bigint` types with `BigInt()` in generated getters and methods (e.g., `get_version: (obj) => BigInt(obj.version)`). The `is_bigint_type` helper in `emit_js_runtime.mbt` detects these types and the `is_bigint` parameter on `emit_js_getter`/`emit_js_method` controls wrapping. Namespace methods/getters reuse these same functions with `is_static=true`.
259276

260277
## Pending: remove default_value from code generation (branch `remove-default-value`)
261278

0 commit comments

Comments
 (0)