diff --git a/tools/webidl-codegen/DEVELOPMENT.md b/tools/webidl-codegen/DEVELOPMENT.md new file mode 100644 index 00000000000..b1df62dcd62 --- /dev/null +++ b/tools/webidl-codegen/DEVELOPMENT.md @@ -0,0 +1,377 @@ +# WebIDL to JSG Code Generator - Development Guide + +## Overview + +This tool generates C++ header and implementation files from WebIDL definitions for the workerd JSG (JavaScript Glue) binding system. It automates the creation of boilerplate code for exposing C++ APIs to JavaScript. + +## Project Structure + +``` +webidl-codegen/ +├── src/ +│ ├── cli.js # Command-line interface and summary reporting +│ ├── parser.js # WebIDL parsing using webidl2 library +│ ├── generator.js # C++ header generation (main logic) +│ ├── impl-generator.js # C++ implementation stub generation +│ ├── type-mapper.js # WebIDL → C++ type mapping +│ └── protected-regions.js # Code preservation during regeneration +├── examples/ # Example WebIDL files for testing +├── README.md # User-facing documentation +└── DEVELOPMENT.md # This file + +``` + +## Core Architecture + +### Type System + +The generator distinguishes between different WebIDL constructs: + +1. **Interfaces** → `class X : public jsg::Object` (wrapped in `jsg::Ref`) +2. **Interface Mixins** → Plain C++ classes (used as base classes, not wrapped) +3. **Dictionaries** → C++ structs with `JSG_STRUCT` +4. **Enums** → C++ `enum class` +5. **Typedefs** → C++ `using` aliases +6. **Callbacks** → C++ `using` with `jsg::Function<...>` +7. **Namespaces** → NOT YET IMPLEMENTED (planned: `jsg::Object` with static methods) + +### Forward Declarations + +The generator creates forward declarations for: +- All interface types (both local and external) +- External interfaces are in `skipInterfaces` set (e.g., `Event`, `EventTarget`, `AbortSignal`) +- Local interfaces defined in the same file + +Forward declarations are filtered to exclude: +- Primitive types +- External enums (configured in `externalEnums` set in type-mapper.js and generator.js) +- Local non-interfaces (dictionaries, enums, typedefs, mixins) + +### External Types + +**External Enums** (manually configured): +- Listed in `externalEnums` set in both `type-mapper.js` and `generator.js` +- Example: `ReferrerPolicy` from Referrer Policy spec +- Not wrapped in `jsg::Ref<>`, not forward declared +- Used directly as enum types + +**External Interfaces** (skipInterfaces): +- Default set includes: `Event`, `EventTarget`, `EventListener`, `AbortSignal`, `AbortController` +- Can be extended via `--skip-interface` CLI option +- Forward declared but not generated +- Wrapped in `jsg::Ref<>` when referenced + +### Extended Attributes (All Jsg-Prefixed) + +Custom WebIDL extended attributes for JSG bindings (all prefixed with `Jsg` to avoid WebIDL spec conflicts): + +- `[JsgCompatFlag=Name]` - Conditional compilation when flag is ON +- `[JsgCompatFlagOff=Name]` - Conditional compilation when flag is OFF (for mutually exclusive signatures) +- `[JsgMethodName=name]` - Custom C++ method name (for overloads, reserved keywords) +- `[JsgTsOverride="..."]` - TypeScript type override via `JSG_TS_OVERRIDE` +- `[JsgTsDefine="..."]` - TypeScript type definitions via `JSG_TS_DEFINE` or `JSG_STRUCT_TS_DEFINE` +- `[JsgTsRoot]` - Mark dictionary as TypeScript root type via `JSG_STRUCT_TS_ROOT()` +- `[JsgPropertyScope=instance|prototype]` - Control JSG_*_INSTANCE_PROPERTY vs JSG_*_PROTOTYPE_PROPERTY +- `[JsgInternal]` - Exclude dictionary field from `JSG_STRUCT` parameters +- `[JsgCode="..."]` - Custom C++ constructor/method declarations + +**Note**: All attributes were prefixed with `Jsg` in November 2025 to protect against future WebIDL spec additions. + +### Mixin Handling + +**Current Implementation** (as of November 2025): +- Mixins are plain C++ classes (no `jsg::Object` base) +- Interfaces using mixins inherit from `jsg::Object` first, then mixin classes +- Mixin methods are registered in the interface's `JSG_RESOURCE_TYPE` +- `includes` statements are processed to build inheritance chain + +**Example**: +```webidl +interface mixin Body { + readonly attribute boolean bodyUsed; +}; +interface Request {}; +Request includes Body; +``` + +Generates: +```cpp +class Body { // No jsg::Object base +public: + bool getBodyUsed(jsg::Lock& js); +}; + +class Request : public jsg::Object, public Body { +public: + JSG_RESOURCE_TYPE(Request) { + JSG_READONLY_PROTOTYPE_PROPERTY(bodyUsed, getBodyUsed); + } +}; +``` + +### Protected Regions + +The generator supports **update mode** to preserve manual code: + +```cpp +// BEGIN MANUAL SECTION: ClassName::public +// User code here is preserved +// END MANUAL SECTION: ClassName::public +``` + +Protected regions exist in: +- Class public section +- Class private section +- Implementation file method bodies + +**Incremental Mode**: Only generates stubs for new methods, preserving existing implementations. + +## Generation Flow + +### Header Generation (generator.js) + +1. Parse WebIDL with `webidl2` +2. Process mixins and `includes` statements +3. Generate forward declarations +4. Sort definitions by dependency order: + - Typedefs (may be used by other types) + - Enums (simple value types) + - Callbacks (function types) + - Dictionaries (value types) + - Interface mixins (base classes) + - Interfaces (complex types) +5. For each definition, generate C++ code +6. Track what was generated in `summary` object + +### Implementation Generation (impl-generator.js) + +1. Parse same WebIDL definitions +2. Generate implementation stubs with TODO comments +3. Detect SelfRef, Unimplemented, WontImplement patterns +4. Smart return value generation (kj::StringPtr for string literals) +5. Respect protected regions if updating existing file + +### CLI Summary (cli.js) + +After generation, prints summary: +``` +============================================================ +Generation Summary +============================================================ + +Header: + ✓ 4 interface(s): Headers, Request, Response, FetchLaterResult + ✓ 1 mixin(s): Body + ✓ 3 dictionar(y/ies): RequestInit, ResponseInit, DeferredRequestInit + ... + +Implementation: + ✓ 4 implementation(s): Headers, Request, Response, FetchLaterResult + +Skipped (--skip-interface): + ⊘ mixin: WindowOrWorkerGlobalScope + ⊘ interface: Window + +Unsupported (not implemented): + ✗ namespace: Console - Namespaces not yet implemented + ✗ partial interface: Window - Partial interfaces not yet supported + +------------------------------------------------------------ +Total: 20 generated, 2 skipped, 2 unsupported +============================================================ +``` + +## Current Limitations & Future Work + +### Not Yet Implemented + +1. **Namespaces** - Planned approach: + - Generate as `jsg::Object` class with static methods + - Use `JSG_STATIC_METHOD` and `JSG_STATIC_*_PROPERTY` macros + - No special `JSG_NAMESPACE` macro (doesn't exist) + - Future: Handle method destructuring safety + +2. **Partial Definitions** - Planned approach: + - Merge all partials before generation + - `collectPartials()` → `mergePartials()` → generate merged result + - Essential for multi-spec scenarios (Window extended by many APIs) + - Currently: Partials generate separately (duplicate names possible) + +3. **Iterables/Maplike/Setlike** - Not supported + +4. **Cross-file mixins** - Mixins must be in same file as interface + +### Known Issues + +- **External type configuration** requires manual source code changes + - `externalEnums` set in type-mapper.js and generator.js + - `skipInterfaces` set in generator.js and impl-generator.js + - Future: Config file or CLI flags + +- **Partial interfaces** treated as separate interfaces + - Can result in duplicate names in summary + - Need merging implementation before full support + +## Type Mapping Details + +See `type-mapper.js` for full mappings: + +### Primitives +- `boolean` → `bool` +- `byte` → `int8_t` +- `octet` → `uint8_t` +- `short` → `int16_t` +- `unsigned short` → `uint16_t` +- `long` → `int32_t` +- `unsigned long` → `uint32_t` +- `long long` → `int64_t` +- `unsigned long long` → `uint64_t` +- `float` → `float` +- `double` → `double` +- `DOMString`, `ByteString`, `USVString` → `jsg::DOMString` (alias for `kj::String`) +- `any` → `jsg::JsValue` +- `object` → `jsg::JsObject` +- `undefined` → `void` + +### Generic Types +- `Promise` → `jsg::Promise` +- `sequence` → `kj::Array` +- `FrozenArray` → `kj::Array` +- `record` → `jsg::Dict` + +### Union Types +- `(A or B or C)` → `kj::OneOf` + +### Modifiers +- `optional T` → `jsg::Optional` +- Nullable types (`T?`) → `jsg::Optional` + +### Buffer Types +- `ArrayBuffer` → `jsg::BufferSource` +- `BufferSource` → `jsg::BufferSource` +- `Uint8Array`, etc. → `jsg::BufferSource` + +## Testing + +### Example Files +All examples in `examples/` directory should generate successfully: +- `compat-flags.webidl` - Extended attributes showcase +- `fetch.webidl` - Real-world Fetch API subset +- `selfref.webidl` - Dictionary self-references and internal fields +- `mixin-test.webidl`, `multiple-mixins.webidl` - Mixin patterns +- `dict-inheritance.webidl` - Dictionary inheritance +- `union-types.webidl` - Union type handling +- And others... + +### Quick Test +```bash +cd examples +for f in *.webidl; do + node ../src/cli.js -o /tmp/test.h --impl /tmp/test.c++ "$f" +done +``` + +All should generate without errors. + +## Key Files to Understand + +### generator.js (Core Logic) +- `generate()` - Main entry point +- `generateInterface()` - Interface → class with JSG_RESOURCE_TYPE +- `generateMixin()` - Mixin → plain class +- `generateDictionary()` - Dictionary → struct with JSG_STRUCT +- `generateForwardDeclarations()` - Smart forward declaration logic +- `processMixinsAndIncludes()` - Build mixin inheritance chain +- Extended attribute extraction methods (getCompatFlag, getMethodName, etc.) + +### type-mapper.js +- `mapType()` - Core type mapping logic +- `externalEnums` set - Manual configuration for external enums +- `isPrimitive()`, `isBufferType()`, etc. - Type classification + +### impl-generator.js +- Similar structure to generator.js but for .c++ files +- `generateMethodStub()` - Smart stub generation with TODOs +- Auto-detection of SelfRef, Unimplemented, WontImplement + +## Development Workflow + +### Adding New Features + +1. **Update parser.js** if new WebIDL constructs need validation +2. **Update type-mapper.js** if new type mappings needed +3. **Update generator.js** for header generation +4. **Update impl-generator.js** for implementation stubs +5. **Update README.md** with user-facing documentation +6. **Add example** in `examples/` directory +7. **Test** all examples still generate + +### Adding Extended Attributes + +1. Add getter method in generator.js (e.g., `getMyAttribute()`) +2. Use attribute in appropriate generation method +3. Update impl-generator.js if needed for stubs +4. Document in README.md under "Extended Attributes" +5. Add example in `examples/compat-flags.webidl` + +### Debugging + +- Set `DEBUG=1` environment variable for stack traces +- Check generated summary for what was skipped/unsupported +- Compare generated .h file against expected JSG patterns +- Test with real workerd builds if possible + +## Code Style + +- Use clear variable names (not abbreviated) +- Add comments explaining JSG-specific decisions +- Keep methods focused and single-purpose +- Generate idiomatic C++ code (proper spacing, naming) +- Preserve manual code through protected regions + +## Dependencies + +- `webidl2` - WebIDL parsing (mature, well-maintained) +- Node.js built-ins only (fs, path) +- No other external dependencies + +## Important Constants + +### Skip Lists +```javascript +// generator.js, impl-generator.js +this.skipInterfaces = new Set([ + 'Event', + 'EventTarget', + 'EventListener', + 'AbortSignal', + 'AbortController', +]); +``` + +### External Enums +```javascript +// generator.js, type-mapper.js +this.externalEnums = new Set([ + 'ReferrerPolicy', +]); +``` + +## Next Steps / TODO + +Priority improvements: +1. Implement namespace support (jsg::Object with static methods) +2. Implement partial definition merging +3. Consider config file for external types +4. Add validation for duplicate member names +5. Support cross-file mixin references +6. Better error messages with line numbers +7. Optimize generated code (remove unnecessary includes) + +## Questions to Consider + +- Should we auto-detect more external enums? +- Better way to handle cross-file dependencies? +- Config file format for external types? +- Should partials merge automatically or require flag? +- How to handle namespace registration in workerd? diff --git a/tools/webidl-codegen/README.md b/tools/webidl-codegen/README.md new file mode 100644 index 00000000000..374f6c77c53 --- /dev/null +++ b/tools/webidl-codegen/README.md @@ -0,0 +1,1431 @@ +# WebIDL to JSG Code Generator + +A tool for generating JSG C++ bindings from WebIDL interface definitions, with support for both header declarations and implementation stubs. + +## Quick Start + +```bash +# Install dependencies +cd tools/webidl-codegen +npm install + +# Generate header and implementation stubs +node src/cli.js -o api.h --impl api.c++ example.webidl + +# Edit api.c++ with your logic, then regenerate header as needed +node src/cli.js -o api.h example.webidl +``` + +## Installation + +```bash +cd tools/webidl-codegen +npm install +``` + +## Usage + +### CLI Options + +``` +Usage: webidl-codegen [options] + +Options: + -o, --output Output header file (default: stdout) + --impl Generate implementation stub file (.c++) + --header Header file path to include in implementation + (auto-detected from -o if not specified) + --skip-interface Skip generation for specific interface or mixin + (can be used multiple times) + --update Update mode: preserve manual sections in existing files + --incremental Incremental mode: only generate stubs for new methods (requires --update) + -n, --namespace C++ namespace (default: workerd::api) + -h, --help Show help message +``` + +### Command Line Examples + +```bash +# Generate header only to stdout +node tools/webidl-codegen/src/cli.js tools/webidl-codegen/examples/text-encoder.webidl + +# Generate header to file +node tools/webidl-codegen/src/cli.js -o output.h tools/webidl-codegen/examples/text-encoder.webidl + +# Generate both header and implementation stubs +node tools/webidl-codegen/src/cli.js -o output.h --impl output.c++ tools/webidl-codegen/examples/text-encoder.webidl + +# Update existing file while preserving manual sections +node tools/webidl-codegen/src/cli.js -o output.h --update example.webidl + +# Incremental update: only add stubs for new methods +node tools/webidl-codegen/src/cli.js -o api.h --impl api.c++ --update --incremental api.webidl + +# Custom namespace +node tools/webidl-codegen/src/cli.js -n workerd::api::streams -o streams.h --impl streams.c++ tools/webidl-codegen/examples/writable-stream.webidl + +# Specify custom header include path for implementation +node tools/webidl-codegen/src/cli.js -o api.h --impl api.c++ --header "workerd/api/api.h" example.webidl + +# Skip specific interfaces (e.g., Window that's defined elsewhere) +node tools/webidl-codegen/src/cli.js -o fetch.h --impl fetch.c++ --skip-interface Window --skip-interface WindowOrWorkerGlobalScope fetch.webidl +``` + +Or from within the tool directory: + +```bash +cd tools/webidl-codegen + +# Header only +node src/cli.js examples/text-encoder.webidl +node src/cli.js -o output.h examples/text-encoder.webidl + +# Header and implementation stubs +node src/cli.js -o examples/simple.h --impl examples/simple.c++ examples/simple.webidl + +# Update existing header preserving manual code +node src/cli.js -o output.h --update examples/text-encoder.webidl + +# Skip interfaces that are defined elsewhere +node src/cli.js -o fetch.h --impl fetch.c++ --skip-interface Window examples/fetch.webidl +``` + +### Implementation Stubs + +The `--impl` flag generates C++ implementation stub files (`.c++`) with placeholder implementations: + +- **C++ constructors** - Always generated for jsg::Object instances +- **JavaScript constructors** - Static `constructor()` method (only if WebIDL declares constructor) +- **Method stubs** - Include TODO comments and type-appropriate return values +- **Attribute getters/setters** - Placeholder implementations with TODO comments +- **Dictionary custom methods** - Comment templates for `[JsgCode]` declarations + +The generated stubs are **meant to be edited** - they provide a starting point for implementing the actual logic. + +**Example:** + +```bash +node src/cli.js -o calculator.h --impl calculator.c++ examples/simple.webidl +``` + +Generates: +- `calculator.h` - Interface declarations (read-only, regenerate as needed) +- `calculator.c++` - Implementation stubs (edit with your logic) + +The implementation file includes helpful comments like: +```cpp +// C++ constructor - add parameters as needed for your implementation +Calculator::Calculator() { + // TODO: Initialize member variables +} + +// JavaScript constructor (static method for JS 'new' operator) +jsg::Ref Calculator::constructor(jsg::Lock& js) { + // TODO: Implement JavaScript constructor + // Create and return a new instance using js.alloc + // The C++ constructor will be called automatically + return js.alloc(); +} + +int32_t Calculator::add(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement add + // Return the result + return 0; +} +``` + +### Running Tests + +```bash +cd tools/webidl-codegen +node src/test.js +``` + +## Workflow + +### Recommended Development Workflow + +The generator supports flexible workflows for ongoing development: + +#### Option 1: Update Mode with Protected Regions (Recommended) + +Best for iterative development where you modify both headers and implementations: + +1. **Initial generation** with protected regions: + ```bash + node src/cli.js -o api.h --impl api.c++ api.webidl + ``` + +2. **Implement your logic** in both files: + - Edit `api.c++` method implementations (inside `BEGIN/END MANUAL SECTION` markers) + - Add custom fields/methods in `api.h` (inside protected regions) + +3. **Add new methods** to WebIDL as you develop + +4. **Regenerate with --update** to get new stubs while preserving work: + ```bash + node src/cli.js -o api.h --impl api.c++ --update api.webidl + ``` + - Existing implementations preserved via protected regions + - New methods get fresh stubs + - Custom code in headers preserved + +5. **Incremental mode** for cleaner diffs (optional): + ```bash + node src/cli.js -o api.h --impl api.c++ --update --incremental api.webidl + ``` + - Only outputs stubs for brand new methods + - Existing methods left untouched (cleaner git diffs) + +#### Option 2: Header-Only Regeneration + +Simpler workflow if you don't need to modify headers after initial generation: + +1. **Generate once** with implementation stubs: + ```bash + node src/cli.js -o api.h --impl api.c++ api.webidl + ``` + +2. **Edit implementation** file (`api.c++`) with your logic + +3. **Regenerate header only** when WebIDL changes: + ```bash + node src/cli.js -o api.h api.webidl + ``` + Your implementation file stays untouched! + +4. **Manually add new implementations** when methods are added + +### Protected Regions + +Both header and implementation files support protected regions for preserving manual code: + +**Header Protected Regions** (`api.h`): +```cpp +class MyAPI: public jsg::Object { + public: + MyAPI(); + void generatedMethod(jsg::Lock& js); + + // BEGIN MANUAL SECTION: MyAPI::public + // Add custom public methods, fields, or nested types here + kj::String customHelper(int value); + struct CustomNested { int x; }; + // END MANUAL SECTION: MyAPI::public + + private: + // BEGIN MANUAL SECTION: MyAPI::private + // Add private member variables here + kj::String state; + int counter = 0; + // END MANUAL SECTION: MyAPI::private +}; +``` + +**Implementation Protected Regions** (`api.c++`): +```cpp +// BEGIN MANUAL SECTION: MyAPI::generatedMethod +void MyAPI::generatedMethod(jsg::Lock& js) { + // Your implementation here - preserved during regeneration + counter++; + state = "active"_kj; +} +// END MANUAL SECTION: MyAPI::generatedMethod +``` + +Each method gets its own protected region: `ClassName::methodName` + +### Update Modes Comparison + +| Mode | Command | Behavior | +|------|---------|----------| +| **Full regeneration** | `--update` | Regenerates all method stubs, preserves manual code in protected regions | +| **Incremental** | `--update --incremental` | Only generates stubs for new methods, existing methods unchanged | +| **No update** | (default) | Complete regeneration, overwrites everything | + +### Development Workflow with Implementation Stubs + +1. **Write WebIDL definitions** (`api.webidl`) + ```webidl + [Exposed=*] + interface MyAPI { + constructor(); + DOMString process(DOMString input); + }; + ``` + +2. **Generate header and implementation stubs** + ```bash + node src/cli.js -o api.h --impl api.c++ api.webidl + ``` + +3. **Implement the logic** in `api.c++` + ```cpp + // Replace TODO comments with actual implementation + kj::String MyAPI::process(jsg::Lock& js, kj::String input) { + // TODO: Implement process ← Replace this + return kj::str("Processed: ", input); // ← With this + } + ``` + +4. **Regenerate only the header** when WebIDL changes + ```bash + node src/cli.js -o api.h api.webidl + ``` + Your implementation file (`api.c++`) is preserved! + +5. **Add new methods/stubs** as needed + - Update WebIDL + - Regenerate header only: `node src/cli.js -o api.h api.webidl` + - Manually add new method implementations to `api.c++` + - Or regenerate stubs to a temp file and copy new methods + +### Header-Only Regeneration + +Once you've implemented your stubs, you can safely regenerate the header without `--impl`: + +```bash +# Only regenerates api.h, leaves api.c++ untouched +node src/cli.js -o api.h api.webidl +``` + +This allows iterative development: +- WebIDL changes → regenerate header +- Implementation stays intact in separate `.c++` file + +### Update Mode with Protected Regions + +For iterative development where you need to customize generated interfaces (jsg::Object classes), use **protected regions**: + +1. **Initial generation** creates protected region markers: + ```bash + node src/cli.js -o api.h api.webidl + ``` + +2. **Add custom code** between the markers in `api.h`: + ```cpp + class MyAPI: public jsg::Object { + public: + MyAPI(); + static jsg::Ref constructor(jsg::Lock& js); + void processData(jsg::Lock& js, kj::String data); + + // BEGIN MANUAL SECTION: MyAPI::public + // Add custom public methods, fields, or nested types here + kj::String customMethod(int value); // Your custom method + struct CustomNested { int x; }; // Your nested type + // END MANUAL SECTION: MyAPI::public + + private: + // BEGIN MANUAL SECTION: MyAPI::private + // Add private member variables here + kj::String state; // Your private field + int counter = 0; + // END MANUAL SECTION: MyAPI::private + + public: + JSG_RESOURCE_TYPE(MyAPI) { ... } + }; + ``` + +3. **Regenerate with --update** when WebIDL changes: + ```bash + node src/cli.js -o api.h --update api.webidl + ``` + + The generator preserves your custom code between markers while updating the generated parts! + +**Protected Regions:** +- `ClassName::public` - Custom public methods, fields, nested types +- `ClassName::private` - Private member variables + +**When to use update mode:** +- Adding custom helper methods to interfaces +- Adding private member variables for state +- Adding nested types or enums within the class +- Need to modify generated classes but keep them regeneratable + +## Features + +### Code Generation Strategies + +The generator supports multiple strategies for combining generated and manual code: + +#### Strategy 1: Split Generation (Recommended) + +Generated code is split into two files for clean separation: + +**`foo-generated.h`** - Fully generated, never manually edited: +```cpp +// GENERATED CODE - DO NOT EDIT +// Auto-generated from foo.webidl + +namespace workerd::api { +struct FooOptions { + jsg::Optional mode; + JSG_STRUCT(mode); +}; +} +``` + +**`foo.h`** - Manual implementations: +```cpp +#include "foo-generated.h" + +namespace workerd::api { +// Add validate(), constructors, JSG_MEMORY_INFO, etc. +inline void FooOptions::validate(jsg::Lock& js) { /* ... */ } +} +``` + +#### Strategy 2: Extension Pattern with `[ManualExtensions]` + +Mark types needing manual code with `[ManualExtensions]`: + +```webidl +[ManualExtensions] +dictionary FooOptions { + DOMString mode; +}; +``` + +Generates into `generated::` namespace for extension: +```cpp +namespace workerd::api::generated { + struct FooOptionsGenerated { /* ... */ }; +} + +// In separate file: +namespace workerd::api { + struct FooOptions: public generated::FooOptionsGenerated { + // Manual additions: validate(), JSG_MEMORY_INFO, + // JSG_STRUCT_TS_OVERRIDE_DYNAMIC, etc. + }; +} +``` + +#### Strategy 3: Protected Regions (Alternative) + +Use markers to protect manual code: +```cpp +// BEGIN MANUAL SECTION +void validate(jsg::Lock& js) { /* preserved */ } +// END MANUAL SECTION +``` + +**Recommendation**: Use Strategy 1 (Split Generation) for simplicity and Strategy 2 (Extension Pattern) when you need C++ inheritance. + +### Supported WebIDL Constructs + +- ✅ Interfaces with operations and attributes +- ✅ Dictionaries (structs) +- ✅ Enums +- ✅ Callbacks (function types) +- ✅ Constructors +- ✅ Optional parameters +- ✅ Nullable types (`T?`) +- ✅ Promise types +- ✅ Sequence types +- ✅ Record types +- ✅ Union types +- ✅ Default values +- ✅ Interface inheritance +- ✅ Dictionary inheritance (flattened to avoid multiple inheritance) +- ✅ Compatibility flags (extended attributes) + +### Compatibility Flags + +Use the `[JsgCompatFlag=FlagName]` extended attribute to make methods or properties conditional on runtime feature flags: + +```webidl +[Exposed=*] +interface ExperimentalAPI { + // Always available + undefined basicMethod(); + + // Only available when WorkerdExperimental flag is enabled + [JsgCompatFlag=WorkerdExperimental] + undefined experimentalMethod(); + + // Only available when ReplicaRouting flag is enabled + [JsgCompatFlag=ReplicaRouting] + Promise enableReplicas(); +}; +``` + +Generates: + +```cpp +JSG_RESOURCE_TYPE(ExperimentalAPI, CompatibilityFlags::Reader flags) { + JSG_METHOD(basicMethod); + + if (flags.getWorkerdExperimental()) { + JSG_METHOD(experimentalMethod); + } + + if (flags.getReplicaRouting()) { + JSG_METHOD(enableReplicas); + } +} +``` + +### Custom Method Names + +Use the `[JsgMethodName=name]` extended attribute to specify a custom C++ method name. This is useful for: +- Avoiding C++ reserved keywords (e.g., `delete`) +- Following C++ naming conventions +- Differentiating method overloads + +```webidl +[Exposed=*] +interface Storage { + // Maps JS 'delete' to C++ 'delete_' to avoid keyword conflict + [JsgMethodName=delete_] + undefined delete(DOMString key); + + // Method overloads with custom names + DOMString processData(DOMString input); + + [JsgCompatFlag=WorkerdExperimental, JsgMethodName=processDataWithFormat] + DOMString processData(DOMString input, optional DOMString format); +}; +``` + +Generates: + +```cpp +class Storage: public jsg::Object { +public: + void delete_(jsg::Lock& js, kj::String key); + kj::String processData(jsg::Lock& js, kj::String input); + kj::String processDataWithFormat(jsg::Lock& js, kj::String input, jsg::Optional format); + + JSG_RESOURCE_TYPE(Storage, CompatibilityFlags::Reader flags) { + JSG_METHOD_NAMED(delete, delete_); + JSG_METHOD(processData); + + if (flags.getWorkerdExperimental()) { + JSG_METHOD_NAMED(processData, processDataWithFormat); + } + } +}; +``` + +### Mutually Exclusive Signatures + +Use `[JsgCompatFlagOff=FlagName]` to define a method signature that should be used when a flag is **disabled**. This creates an `if/else` pattern for mutually exclusive implementations: + +```webidl +[Exposed=*] +interface Storage { + // Old signature (when NewApiSignature flag is OFF) + [JsgCompatFlagOff=NewApiSignature, JsgMethodName=getOld] + Promise get(DOMString key); + + // New signature (when NewApiSignature flag is ON) + [JsgCompatFlag=NewApiSignature, JsgMethodName=getNew] + Promise get(DOMString key, optional boolean parseJson); +}; +``` + +Generates: + +```cpp +class Storage: public jsg::Object { +public: + jsg::Promise getOld(jsg::Lock& js, kj::String key); + jsg::Promise getNew(jsg::Lock& js, kj::String key, jsg::Optional parseJson); + + JSG_RESOURCE_TYPE(Storage, CompatibilityFlags::Reader flags) { + if (flags.getNewApiSignature()) { + JSG_METHOD_NAMED(get, getNew); + } else { + JSG_METHOD_NAMED(get, getOld); + } + } +}; +``` + +### TypeScript Overrides + +Use `[JsgTsOverride="..."]` to provide custom TypeScript type definitions via `JSG_TS_OVERRIDE`, and `[JsgTsDefine="..."]` for type aliases via `JSG_TS_DEFINE`: + +```webidl +[Exposed=*, + JsgTsDefine="type DataFormat = 'json' | 'text' | 'binary';", + JsgTsOverride="{ + processData(input: string): string; + processData(input: string, format: DataFormat): string; +}"] +interface DataProcessor { + DOMString processData(DOMString input); + + [JsgCompatFlag=NewFormat, JsgMethodName=processDataWithFormat] + DOMString processData(DOMString input, optional DOMString format); +}; + +[JsgTsOverride="{ + mode?: 'standard' | 'experimental'; + options?: Record; +}"] +dictionary ProcessorOptions { + DOMString mode = "standard"; + any options; +}; +``` + +Generates: + +```cpp +class DataProcessor: public jsg::Object { +public: + kj::String processData(jsg::Lock& js, kj::String input); + kj::String processDataWithFormat(jsg::Lock& js, kj::String input, jsg::Optional format); + + JSG_RESOURCE_TYPE(DataProcessor, CompatibilityFlags::Reader flags) { + JSG_METHOD(processData); + + if (flags.getNewFormat()) { + JSG_METHOD_NAMED(processData, processDataWithFormat); + } + + JSG_TS_DEFINE(type DataFormat = 'json' | 'text' | 'binary';); + + JSG_TS_OVERRIDE({ + processData(input: string): string; + processData(input: string, format: DataFormat): string; + }); + } +}; + +struct ProcessorOptions { + jsg::Optional mode; // default: "standard" + jsg::Optional options; + JSG_STRUCT(mode, options); + JSG_TS_OVERRIDE({ + mode?: 'standard' | 'experimental'; + options?: Record; + }); +}; +``` + +### Generated Code + +The tool generates: + +1. **Classes** for interfaces with method declarations +2. **JSG_RESOURCE_TYPE blocks** with proper registration macros +3. **Structs with JSG_STRUCT blocks** for dictionaries +4. **Type aliases** for callbacks +5. **Conditional registration** based on compatibility flags + +### Type Mapping + +#### Basic Types + +| WebIDL Type | JSG C++ Type | +|-------------|--------------| +| `boolean` | `bool` | +| `long` | `int32_t` | +| `DOMString` | `kj::String` | +| `any` | `jsg::JsValue` | +| `Promise` | `jsg::Promise` | +| `sequence` | `kj::Array` | +| `record` | `jsg::Dict` | +| Interface | `jsg::Ref` | +| Dictionary | `DictName` (direct) | + +#### Optional and Nullable + +| WebIDL | C++ (Parameters) | C++ (Dictionary Members) | +|--------|------------------|--------------------------| +| `T` | `T` | `T` (required) | +| `optional T` | `jsg::Optional` | `jsg::Optional` | +| `T?` | `kj::Maybe` | `kj::Maybe` | +| `optional T?` | `jsg::Optional>` | `jsg::Optional>` | + +#### Collection Types + +**Sequences:** +```webidl +sequence items; // → kj::Array +sequence numbers; // → kj::Array +sequence objects; // → kj::Array> +``` + +**Records (key-value maps):** +```webidl +record stringToNumber; // → jsg::Dict +record headers; // → jsg::Dict +record> multiMap; // → jsg::Dict> +record metadata; // → jsg::Dict +``` + +`jsg::Dict` is used for WebIDL `record` types, representing JavaScript objects used as dictionaries/maps. + +## Example + +### Complete Example with Implementation Stubs + +Input WebIDL (`calculator.webidl`): + +```webidl +[Exposed=*] +interface Calculator { + constructor(); + + long add(long a, long b); + long subtract(long a, long b); + + readonly attribute DOMString version; +}; + +dictionary Point { + long x; + long y; + + JSG_STRUCT(x, y); +}; +``` + +Generate files: +```bash +node src/cli.js -o calculator.h --impl calculator.c++ calculator.webidl +``` + +**Generated Header** (`calculator.h` - regenerate as needed): + +```cpp +#pragma once +// Generated from WebIDL - DO NOT EDIT + +#include +#include +#include + +namespace workerd { +namespace api { + +class Calculator: public jsg::Object { +public: + static jsg::Ref constructor(jsg::Lock& js); + + int32_t add(jsg::Lock& js, int32_t a, int32_t b); + int32_t subtract(jsg::Lock& js, int32_t a, int32_t b); + kj::String getVersion(jsg::Lock& js); + + JSG_RESOURCE_TYPE(Calculator) { + JSG_READONLY_PROTOTYPE_PROPERTY(version, getVersion); + JSG_METHOD(add); + JSG_METHOD(subtract); + } +}; + +struct Point { + int32_t x; + int32_t y; + + JSG_STRUCT(x, y); +}; + +} // namespace api +} // namespace workerd +``` + +**Generated Implementation Stubs** (`calculator.c++` - edit with your logic): + +```cpp +// Generated implementation stubs - EDIT THIS FILE +// This file contains placeholder implementations. +// Replace TODO comments with actual logic. + +#include "calculator.h" + +namespace workerd { +namespace api { + +// C++ constructor - add parameters as needed for your implementation +Calculator::Calculator() { + // TODO: Initialize member variables +} + +// JavaScript constructor (static method for JS 'new' operator) +jsg::Ref Calculator::constructor(jsg::Lock& js) { + // TODO: Implement JavaScript constructor + // Create and return a new instance using js.alloc + // The C++ constructor will be called automatically + return js.alloc(); +} + +int32_t Calculator::add(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement add + // Return the result + return 0; // ← Replace with: return a + b; +} + +int32_t Calculator::subtract(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement subtract + // Return the result + return 0; // ← Replace with: return a - b; +} + +kj::String Calculator::getVersion(jsg::Lock& js) { + // TODO: Implement getter for version + return kj::str("TODO"); // ← Replace with: return kj::str("1.0"); +} + +} // namespace api +} // namespace workerd +``` + +### Simple Example (Header Only) + +Input WebIDL: + +```webidl +[Exposed=*] +interface TextEncoder { + constructor(); + + readonly attribute DOMString encoding; + + Uint8Array encode(optional DOMString input = ""); +}; +``` + +Generated C++: + +```cpp +#pragma once +// Generated from WebIDL - DO NOT EDIT + +#include +#include +#include + +namespace workerd { +namespace api { + +class TextEncoder: public jsg::Object { +public: + static jsg::Ref constructor(); + + virtual jsg::BufferSource encode(jsg::Lock& js, jsg::Optional input) = 0; + virtual kj::String getEncoding() = 0; + + JSG_RESOURCE_TYPE(TextEncoder) { + JSG_READONLY_PROTOTYPE_PROPERTY(encoding, getEncoding); + JSG_METHOD(encode); + } +}; + +} // namespace api +} // namespace workerd +``` + +### Implementation Features + +The implementation generator (`--impl` flag) creates starter code with protected regions: + +#### Protected Region Markers + +Every method implementation is wrapped in protected regions to enable safe regeneration: + +```cpp +// BEGIN MANUAL SECTION: Calculator::add +int32_t Calculator::add(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement add + return a + b; // Your implementation - preserved on regeneration! +} +// END MANUAL SECTION: Calculator::add +``` + +Region names follow the pattern: `ClassName::methodName` + +Special regions: +- `ClassName::constructor` - C++ constructor +- `ClassName::constructor(js)` - JavaScript constructor +- `ClassName::getPropertyName` - Attribute getters +- `ClassName::setPropertyName` - Attribute setters + +#### Type-Specific Return Values + +| Return Type | Generated Stub | Notes | +|-------------|----------------|-------| +| `kj::String`, `jsg::DOMString` | `return "TODO"_kj;` | Uses `kj::StringPtr` for efficiency | +| `void` | No return statement | - | +| `jsg::Promise` | `return js.resolvedPromise();` | Includes helpful comment | +| `jsg::Ref` | `return js.alloc();` | Allocates new instance | +| `bool` | `return false;` | - | +| Numeric types | `return 0;` | - | +| `jsg::Optional` | `return kj::none;` | - | +| `kj::Maybe` | `return kj::none;` | - | +| Dictionary types | Returns by value | Not wrapped in `jsg::Ref<>` | + +**String Return Optimization**: Generated stubs use `"TODO"_kj` which returns `kj::StringPtr`. The stub includes a comment suggesting you can change the return type in the header to `kj::StringPtr` when returning string literals or fixed strings for better performance. + +#### Constructor Separation + +The generator creates two types of constructors: + +**C++ Constructor** (always generated): +```cpp +// BEGIN MANUAL SECTION: MyAPI::constructor +MyAPI::MyAPI() { + // Initialize member variables + counter = 0; +} +// END MANUAL SECTION: MyAPI::constructor +``` + +**JavaScript Constructor** (only if WebIDL has `constructor()`): +```cpp +// BEGIN MANUAL SECTION: MyAPI::constructor(js) +jsg::Ref MyAPI::constructor(jsg::Lock& js) { + return js.alloc(); +} +// END MANUAL SECTION: MyAPI::constructor(js) +``` + +#### Dictionary vs Interface Types + +The type mapper correctly distinguishes between: +- **Dictionaries (JSG_STRUCT)**: Referenced directly as `ConfigOptions` +- **Interfaces (jsg::Object)**: Wrapped in `jsg::Ref` + +Example: +```cpp +// Dictionary passed by value +void processConfig(jsg::Lock& js, ConfigOptions config); + +// Interface passed by reference +void processAPI(jsg::Lock& js, jsg::Ref api); +``` + +#### Dictionary Inheritance Flattening + +When a dictionary extends another dictionary in WebIDL, the generator **flattens the inheritance** into a single C++ struct containing all fields from the entire hierarchy. This avoids multiple inheritance issues and simplifies the type system. + +**WebIDL:** +```webidl +dictionary BaseOptions { + DOMString mode = "read"; + boolean excludeAll = false; +}; + +dictionary FilePickerOptions : BaseOptions { + DOMString id; + boolean multiple = false; +}; + +dictionary OpenFilePickerOptions : FilePickerOptions { + boolean allowMultipleFiles = true; +}; +``` + +**Generated C++:** +```cpp +struct BaseOptions { + jsg::Optional mode; + jsg::Optional excludeAll; + JSG_STRUCT(mode, excludeAll); +}; + +struct FilePickerOptions { + // Inherited from BaseOptions + jsg::Optional mode; + jsg::Optional excludeAll; + // Own fields + jsg::Optional id; + jsg::Optional multiple; + + JSG_STRUCT(mode, excludeAll, id, multiple); + // TypeScript sees the inheritance relationship + JSG_STRUCT_TS_OVERRIDE(FilePickerOptions extends BaseOptions); +}; + +struct OpenFilePickerOptions { + // All inherited fields from BaseOptions and FilePickerOptions + jsg::Optional mode; + jsg::Optional excludeAll; + jsg::Optional id; + jsg::Optional multiple; + // Own field + jsg::Optional allowMultipleFiles; + + JSG_STRUCT(mode, excludeAll, id, multiple, allowMultipleFiles); + JSG_STRUCT_TS_OVERRIDE(OpenFilePickerOptions extends FilePickerOptions); +}; +``` + +**Key Points:** +- All parent fields are duplicated in child structs +- No C++ inheritance (`struct Child : public Parent`) is used +- `JSG_STRUCT` lists all fields including inherited ones +- `JSG_STRUCT_TS_OVERRIDE` preserves TypeScript inheritance semantics +- Field initialization order respects inheritance hierarchy (parent fields first) + +### Helpful Comments + +Each method includes context-aware comments: + +```cpp +jsg::Promise fetch(jsg::Lock& js, kj::String url) { + // TODO: Implement fetch + // Return a promise that resolves with the result + // Example: return js.resolvedPromise(...); + return js.resolvedPromise(); +} +``` + +### Dictionary Custom Methods + +For dictionaries with `[JsgCode]`: + +```cpp +// Circle custom method implementations +// Add implementations for methods declared in [JsgCode] + +// Example: +// void Circle::validate(jsg::Lock& js) { +// // Validate the struct members +// JSG_REQUIRE(radius > 0, TypeError, "radius must be positive"); +// } +``` + +## Mixins (Interface Mixins) + +WebIDL interface mixins allow sharing attributes, operations, and constants across multiple interfaces using `includes` statements. + +### Implementation: C++ Inheritance Approach + +Following the pattern established in `workerd/api/http.h`, the generator uses **C++ inheritance** to implement mixins: + +```webidl +// WebIDL +interface mixin Body { + readonly attribute ReadableStream? body; + readonly attribute boolean bodyUsed; + Promise arrayBuffer(); + Promise blob(); + Promise formData(); + Promise json(); + Promise text(); +}; + +interface Request { + constructor(RequestInfo input, optional RequestInit init); + readonly attribute USVString url; + readonly attribute ByteString method; + readonly attribute Headers headers; +}; +Request includes Body; + +interface Response { + constructor(optional BodyInit? body = null, optional ResponseInit init = {}); + readonly attribute unsigned short status; + readonly attribute ByteString statusText; + readonly attribute Headers headers; +}; +Response includes Body; +``` + +```cpp +// Generated C++ +// Mixin: Body - No base class, no JSG_RESOURCE_TYPE +class Body { +public: + jsg::Promise arrayBuffer(jsg::Lock& js); + jsg::Promise> blob(jsg::Lock& js); + jsg::Promise> formData(jsg::Lock& js); + jsg::Promise json(jsg::Lock& js); + jsg::Promise text(jsg::Lock& js); + kj::Maybe> getBody(jsg::Lock& js); + bool getBodyUsed(jsg::Lock& js); +}; + +// Interface: Request - inherits from jsg::Object and Body mixin +class Request: public jsg::Object, public Body { +public: + jsg::ByteString getMethod(jsg::Lock& js); + jsg::USVString getUrl(jsg::Lock& js); + jsg::Ref getHeaders(jsg::Lock& js); + + JSG_RESOURCE_TYPE(Request) { + JSG_READONLY_PROTOTYPE_PROPERTY(method, getMethod); + JSG_READONLY_PROTOTYPE_PROPERTY(url, getUrl); + JSG_READONLY_PROTOTYPE_PROPERTY(headers, getHeaders); + // Body mixin members registered here + JSG_READONLY_PROTOTYPE_PROPERTY(body, getBody); + JSG_READONLY_PROTOTYPE_PROPERTY(bodyUsed, getBodyUsed); + JSG_METHOD(arrayBuffer); + JSG_METHOD(blob); + JSG_METHOD(formData); + JSG_METHOD(json); + JSG_METHOD(text); + } +}; + +// Interface: Response - also inherits from jsg::Object and Body mixin +class Response: public jsg::Object, public Body { +public: + uint16_t getStatus(jsg::Lock& js); + jsg::ByteString getStatusText(jsg::Lock& js); + jsg::Ref getHeaders(jsg::Lock& js); + + JSG_RESOURCE_TYPE(Response) { + JSG_READONLY_PROTOTYPE_PROPERTY(status, getStatus); + JSG_READONLY_PROTOTYPE_PROPERTY(statusText, getStatusText); + JSG_READONLY_PROTOTYPE_PROPERTY(headers, getHeaders); + // Body mixin members registered here + JSG_READONLY_PROTOTYPE_PROPERTY(body, getBody); + JSG_READONLY_PROTOTYPE_PROPERTY(bodyUsed, getBodyUsed); + JSG_METHOD(arrayBuffer); + JSG_METHOD(blob); + JSG_METHOD(formData); + JSG_METHOD(json); + JSG_METHOD(text); + } +}; +``` + +### Benefits + +- **No member duplication**: Body methods defined once, inherited by Request and Response +- **Type safety**: C++ inheritance enforces the relationship at compile time +- **JSG compatibility**: JSG handles C++ inheritance naturally +- **Semantic clarity**: The inheritance clearly shows Request/Response include Body +- **Easier maintenance**: Changes to Body automatically apply to all including interfaces +- **No diamond inheritance**: Mixins don't inherit from `jsg::Object`, only interfaces do + +### Key Design Decisions + +1. **Mixins are plain C++ classes** (no `jsg::Object` base, no `JSG_RESOURCE_TYPE`) +2. **Interfaces inherit from `jsg::Object` first, then mixins** (e.g., `class Request: public jsg::Object, public Body`) +3. **Mixin members are registered in the interface's `JSG_RESOURCE_TYPE`** (not in the mixin itself) +4. **C++ keyword escaping**: Methods like `delete` become `delete_` with `JSG_METHOD_NAMED(delete, delete_)` +5. **Forward declarations**: All interface types (including local ones) are forward declared to handle typedef ordering issues + +### Implementation Details + +### Implementation Details + +1. **Parse mixin definitions**: `interface mixin` declarations are stored separately from interfaces +2. **Track includes statements**: Build mapping of interface → mixins (e.g., `Request includes Body`) +3. **Generate mixin classes**: Create plain C++ classes for each `interface mixin` (no base class) +4. **Apply inheritance**: Generate interfaces with `: public jsg::Object, public MixinName` for each mixin +5. **Handle multiple mixins**: Use multiple inheritance when interface includes multiple mixins +6. **Register mixin members**: Include all mixin methods/properties in the interface's `JSG_RESOURCE_TYPE` +7. **Ordering**: `jsg::Object` is always first in the inheritance list to avoid diamond inheritance + +### Handling Multiple Mixins + +When an interface includes multiple mixins: + +```webidl +interface mixin MixinA { void methodA(); }; +interface mixin MixinB { void methodB(); }; +interface Combined {}; +Combined includes MixinA; +Combined includes MixinB; +``` + +Generate with multiple inheritance (jsg::Object first): + +```cpp +// Mixin A - no base class +class MixinA { +public: + void methodA(jsg::Lock& js); +}; + +// Mixin B - no base class +class MixinB { +public: + void methodB(jsg::Lock& js); +}; + +// Combined - jsg::Object first, then mixins +class Combined: public jsg::Object, public MixinA, public MixinB { +public: + JSG_RESOURCE_TYPE(Combined) { + // Register methods from both mixins + JSG_METHOD(methodA); + JSG_METHOD(methodB); + } +}; +``` + +### Edge Cases + +- **Partial mixins**: Merge partial mixin definitions before generating the mixin class +- **Name conflicts**: WebIDL disallows member name conflicts; generator validates +- **Cross-file mixins**: Support mixins and interfaces defined in separate WebIDL fragments (not yet implemented) +- **Mixin inheritance**: Interface mixins cannot inherit from other mixins (WebIDL constraint) + +## Type System and Forward Declarations + +### Namespaces + +WebIDL namespaces are static collections of operations and attributes (no constructors or inheritance). + +**WebIDL:** +```webidl +namespace Console { + undefined log(any... data); + undefined warn(DOMString message); + readonly attribute DOMString version; +}; +``` + +**C++ Mapping:** + +Namespaces map to regular `jsg::Object` classes with static methods: + +```cpp +class Console : public jsg::Object { +public: + static void log(jsg::Lock& js, jsg::Varargs args); + static void warn(jsg::Lock& js, kj::String message); + static kj::StringPtr getVersion(); + + JSG_RESOURCE_TYPE(Console) { + JSG_STATIC_METHOD(log); + JSG_STATIC_METHOD(warn); + JSG_STATIC_READONLY_INSTANCE_PROPERTY(version, getVersion); + } +}; +``` + +**Status**: Not yet implemented. Implementation requirements: +- Parse `type: "namespace"` from webidl2 +- Generate class extending `jsg::Object` (not a special namespace type) +- All operations → static methods +- All attributes → static getters +- Use `JSG_STATIC_METHOD` and `JSG_STATIC_*_PROPERTY` macros +- Note: Method destructuring safety will be handled separately in the future + +### Partial Definitions + +Partial interfaces/dictionaries/mixins allow splitting a definition across multiple files or specs. WebIDL uses `partial` keyword: + +**WebIDL:** +```webidl +// In base spec +interface Window { + readonly attribute DOMString location; +}; + +// In another spec (e.g., Fetch API) +partial interface Window { + Promise fetch(RequestInfo input, optional RequestInit init); +}; + +// In yet another spec +partial interface Window { + undefined alert(DOMString message); +}; +``` + +**Strategy**: Merge all partial definitions before code generation: + +1. **During parsing**: Collect all partial definitions separately +2. **Merge phase**: Combine all partials with the main definition + - Main interface + all `partial interface X` → single merged interface X + - Validate no member name conflicts + - Preserve member order (main first, then partials in source order) +3. **Generate**: Treat merged result as single definition + +**Example merge:** +```javascript +// After merging +interface Window { + readonly attribute DOMString location; + Promise fetch(RequestInfo input, optional RequestInit init); + undefined alert(DOMString message); +} +``` + +**Partial Mixins/Dictionaries**: Same approach - merge before generation. + +**Status**: Not yet implemented. Current behavior: parser rejects partials with error. + +**Implementation Plan**: +1. Add `collectPartials()` method to parser that groups definitions by name +2. Add `mergePartials()` method that combines members +3. Validate no duplicate member names (WebIDL constraint) +4. Generate merged definitions normally + +**Use Case**: Essential for multi-spec scenarios like: +- `partial interface mixin WindowOrWorkerGlobalScope` (added by Fetch, Streams, etc.) +- `partial interface Window` (extended by many Web APIs) +- Allows generator to work with isolated WebIDL fragments from different specs + +### Type Mapping + +The generator distinguishes between different WebIDL construct types and maps them appropriately to C++: + +| WebIDL Type | C++ Mapping | Wrapped in jsg::Ref? | Forward Declared? | +|-------------|-------------|----------------------|-------------------| +| `interface` | Class inheriting from `jsg::Object` | Yes | Yes | +| `interface mixin` | Plain C++ class | No | No (just a base class) | +| `dictionary` | C++ struct with `JSG_STRUCT` | No | No | +| `enum` | C++ `enum class` | No | No | +| `typedef` | C++ `using` alias | Depends on target type | No | + +### Forward Declarations + +The generator automatically creates forward declarations for: +- **External interfaces** (e.g., `AbortSignal`, `Blob`) - defined elsewhere but referenced +- **Local interfaces** (e.g., `Request`, `Response`) - defined in the same file but may be used before definition (e.g., in typedefs) + +Forward declarations are placed at the top of the file, after the namespace opening and before typedefs/enums/definitions. + +**Example:** +```cpp +namespace workerd::api { + +// Forward declarations for types defined elsewhere +class AbortSignal; +class Blob; +class Request; +class Response; + +using RequestInfo = kj::OneOf, jsg::USVString>; + +// ... actual class definitions follow +``` + +### External Type Handling + +Some types are defined in other Web Platform specs and need special handling: + +#### External Enums (Manual Configuration Required) + +External enums (like `ReferrerPolicy` from the Referrer Policy spec) must be **manually registered** in the generator: + +**In `src/type-mapper.js`:** +```javascript +this.externalEnums = new Set([ + 'ReferrerPolicy', // From Referrer Policy spec + // Add other external enums here +]); +``` + +**In `src/generator.js`:** +```javascript +const externalEnums = new Set([ + 'ReferrerPolicy', // From Referrer Policy spec + // Add other external enums here +]); +``` + +This ensures they: +1. Are **not** wrapped in `jsg::Ref<>` (used directly as `ReferrerPolicy`) +2. Are **not** forward declared as classes (they're enums, need to be included) + +#### External Interfaces (Automatic) + +External interfaces (like `AbortSignal`, `Blob`, `FormData`) are automatically: +- Forward declared at the top of the file +- Wrapped in `jsg::Ref<>` when used as types +- Skipped for code generation (defined elsewhere) + +Default external interfaces in `skipInterfaces`: +- `Event`, `EventTarget`, `EventListener` +- `AbortSignal`, `AbortController` + +**Limitation**: Currently, external enums must be manually added to the source code. Future enhancement could support a configuration file (e.g., `.webidl-codegen.json`) for project-specific external types. + +## Limitations + +Current limitations: + +- **Partial interfaces/dictionaries/mixins**: Not yet supported - parser rejects them with error + - Implementation plan documented above (merge partials before generation) +- **Namespaces**: Not yet supported - WebIDL `namespace` construct needs JSG registration strategy +- **Cross-file mixins**: Mixins and interfaces must be in the same WebIDL file +- **Iterables**: No support for `iterable` or `async iterable` +- **Maplike/setlike**: Not supported +- **Limited extended attributes**: Only a subset is handled +- **External enum configuration**: External enums (like `ReferrerPolicy`) must be manually added to source code - no config file support yet +- **No cross-reference validation**: Types referenced but not defined are forward declared but not validated + +## Next Steps + +Potential improvements: + +1. **Configuration file support** for external types (enums, interfaces, dictionaries) +2. Add Bazel build integration +3. Implement validation for WebIDL constraints +4. Support more extended attributes +5. Add documentation comment generation +6. Support partial interfaces +7. Support cross-file mixins +8. Smart signature detection for return type optimization +9. Iterator/async iterator support + +## Summary of Features + +### Supported WebIDL Constructs +* ✅ Interfaces with constructors, methods, attributes +* ✅ **Interface mixins** with C++ inheritance (mixin classes, `includes` statements) +* ✅ Dictionaries (structs) with inheritance flattening +* ✅ Enums +* ✅ Typedefs (union types with `using` aliases) +* ✅ Callbacks +* ✅ Inheritance (interface hierarchy and dictionary flattening) +* ✅ Optional and nullable types +* ✅ Union types (`(A or B)`) +* ✅ Generic types (Promise, sequence, record) +* ✅ Collection types (`sequence` → `kj::Array`, `record` → `jsg::Dict`) +* ✅ **C++ keyword escaping** (e.g., `delete` → `delete_` with `JSG_METHOD_NAMED`) +* ✅ **Forward declarations** for both external and local interface types + +### Extended Attributes +* ✅ `[JsgCompatFlag=Name]` - Conditional compilation when flag is ON +* ✅ `[JsgCompatFlagOff=Name]` - Conditional compilation when flag is OFF +* ✅ `[JsgMethodName=name]` - Custom C++ method name for overloads +* ✅ `[JsgTsOverride="..."]` - TypeScript type override +* ✅ `[JsgTsDefine="..."]` - TypeScript type definitions +* ✅ `[JsgTsRoot]` - Mark as TypeScript root type +* ✅ `[JsgPropertyScope=instance|prototype]` - Property registration scope +* ✅ `[JsgInternal]` - Exclude field from JSG_STRUCT +* ✅ `[JsgCode="..."]` - Custom C++ method/constructor declarations + +### Code Generation +* ✅ Header generation (`.h`) +* ✅ Implementation stub generation (`.c++`) +* ✅ **Protected regions** for preserving manual code during regeneration +* ✅ **Incremental mode** to only generate stubs for new methods +* ✅ Update mode (`--update`) to regenerate while keeping custom modifications +* ✅ Proper JSG_STRUCT field ordering (exposed → JSG_STRUCT → internal) +* ✅ SelfRef, Unimplemented, WontImplement auto-detection +* ✅ Smart return value generation in stubs +* ✅ Helpful TODO comments in stubs +* ✅ Separate C++ and JavaScript constructor handling +* ✅ Dictionary vs interface type differentiation +* ✅ String return type optimization (`kj::StringPtr` for literals) ### Type Mappings +* ✅ All WebIDL primitive types +* ✅ Optional/nullable variants +* ✅ JSG-specific types (SelfRef, Unimplemented, WontImplement) +* ✅ Union types via kj::OneOf +* ✅ Promises via jsg::Promise +* ✅ Sequences via kj::Array +* ✅ Records via jsg::Dict +* ✅ Interface references via jsg::Ref (dictionaries, enums, typedefs used directly) +* ✅ **External enum handling** (manual configuration for types like `ReferrerPolicy`) + +## Next Steps + +1. **Configuration file support** for external types (enums, interfaces, dictionaries) +2. Add Bazel build integration +3. Implement validation for WebIDL constraints +4. Support more extended attributes +5. Add documentation comment generation +6. Support partial interfaces +7. Support cross-file mixins +8. Smart stub updates (preserve existing implementations) +9. Incremental implementation regeneration +10. Iterator/async iterator support + diff --git a/tools/webidl-codegen/examples/compat-flags.webidl b/tools/webidl-codegen/examples/compat-flags.webidl new file mode 100644 index 00000000000..1a20188fca9 --- /dev/null +++ b/tools/webidl-codegen/examples/compat-flags.webidl @@ -0,0 +1,96 @@ +// Example demonstrating compatibility flags +// Shows how runtime feature flags can control API availability + +[Exposed=*, + JsgTsDefine="type DataFormat = 'json' | 'text' | 'binary';", + JsgTsOverride="{ + constructor(); + readonly version: string; + basicMethod(): void; + experimentalFeature?: string; + experimentalMethod?(data: string): void; + processData(input: string): string; + processData(input: string, format: DataFormat): string; + delete(id: string): void; + fetch(url: string, method?: string): Promise; +}"] +interface ExperimentalAPI { + constructor(); + + // Always available + readonly attribute DOMString version; + undefined basicMethod(); + + // Property scope controls whether property is on instance or prototype + // Instance property (own property on each object instance) + [JsgPropertyScope=instance] + readonly attribute DOMString instanceProperty; + + // Prototype property (shared on prototype, default behavior) + [JsgPropertyScope=prototype] + readonly attribute DOMString prototypeProperty; + + // Backwards compatibility: instance when flag is off, prototype when on + // Old code incorrectly used instance properties, new code uses prototype + [JsgCompatFlagOff=JsgPropertyOnPrototypeTemplate, JsgPropertyScope=instance] + readonly attribute DOMString legacyProperty; + + [JsgCompatFlag=JsgPropertyOnPrototypeTemplate, JsgPropertyScope=prototype] + readonly attribute DOMString legacyProperty; + + // Only available with WorkerdExperimental flag + [JsgCompatFlag=WorkerdExperimental] + readonly attribute DOMString experimentalFeature; + + [JsgCompatFlag=WorkerdExperimental] + undefined experimentalMethod(DOMString data); + + // Only available with NodeJsCompat flag + [JsgCompatFlag=NodeJsCompat] + undefined nodeCompatMethod(); + + // Only available with ReplicaRouting flag + [JsgCompatFlag=ReplicaRouting] + Promise enableReplicas(); + + [JsgCompatFlag=ReplicaRouting] + undefined disableReplicas(); + + // Method with different signatures based on compat flag + // Default signature + DOMString processData(DOMString input); + + // Enhanced signature when WorkerdExperimental is enabled + // Uses custom C++ method name to differentiate + [JsgCompatFlag=WorkerdExperimental, JsgMethodName=processDataWithFormat] + DOMString processData(DOMString input, optional DOMString format); + + // Method with custom C++ name (useful for reserved keywords or naming conventions) + [JsgMethodName=delete_] + undefined delete(DOMString id); + + // Mutually exclusive signatures based on compat flag + // Old signature when flag is OFF + [JsgCompatFlagOff=NewApiSignature, JsgMethodName=fetchOld] + Promise fetch(DOMString url); + + // New signature when flag is ON - returns union type + [JsgCompatFlag=NewApiSignature, JsgMethodName=fetchNew] + Promise<(DOMString or BufferSource)> fetch(DOMString url, optional DOMString method); + + // Method accepting union type + undefined processInput((DOMString or long or boolean) value); +}; + +[JsgTsOverride="{ + enabled?: boolean; + mode?: 'standard' | 'experimental'; + experimentalOption?: string; +}"] +dictionary ExperimentalOptions { + boolean enabled = true; + DOMString mode = "standard"; + + // This field would be conditionally used based on flags in the implementation + DOMString experimentalOption; +}; diff --git a/tools/webidl-codegen/examples/dict-inheritance-advanced.webidl b/tools/webidl-codegen/examples/dict-inheritance-advanced.webidl new file mode 100644 index 00000000000..c040a11d847 --- /dev/null +++ b/tools/webidl-codegen/examples/dict-inheritance-advanced.webidl @@ -0,0 +1,16 @@ +// Test advanced dictionary inheritance scenarios + +dictionary Level1 { + DOMString field1; + [JsgInternal] SelfRef selfRef; +}; + +dictionary Level2 : Level1 { + long field2; + boolean flag = true; +}; + +dictionary Level3 : Level2 { + [JsgInternal] Unimplemented internal; + sequence items; +}; diff --git a/tools/webidl-codegen/examples/dict-inheritance.webidl b/tools/webidl-codegen/examples/dict-inheritance.webidl new file mode 100644 index 00000000000..b7d8c58bd31 --- /dev/null +++ b/tools/webidl-codegen/examples/dict-inheritance.webidl @@ -0,0 +1,16 @@ +// Test dictionary inheritance flattening + +dictionary BaseOptions { + DOMString mode = "read"; + boolean excludeAll = false; +}; + +dictionary FilePickerOptions : BaseOptions { + DOMString id; + boolean multiple = false; +}; + +dictionary OpenFilePickerOptions : FilePickerOptions { + boolean allowMultipleFiles = true; + DOMString startDirectory; +}; diff --git a/tools/webidl-codegen/examples/event-inheritance.webidl b/tools/webidl-codegen/examples/event-inheritance.webidl new file mode 100644 index 00000000000..3a651f84ea4 --- /dev/null +++ b/tools/webidl-codegen/examples/event-inheritance.webidl @@ -0,0 +1,61 @@ +// Example showing inheritance with Event +// https://dom.spec.whatwg.org/#interface-event + +[Exposed=*] +interface Event { + constructor(DOMString type, optional EventInit eventInitDict = {}); + + readonly attribute DOMString type; + readonly attribute EventTarget? target; + readonly attribute EventTarget? currentTarget; + readonly attribute unsigned short eventPhase; + + undefined stopPropagation(); + undefined stopImmediatePropagation(); + + readonly attribute boolean bubbles; + readonly attribute boolean cancelable; + undefined preventDefault(); + readonly attribute boolean defaultPrevented; +}; + +dictionary EventInit { + boolean bubbles = false; + boolean cancelable = false; +}; + +[Exposed=*] +interface EventTarget { + undefined addEventListener(DOMString type, EventListener? callback); + undefined removeEventListener(DOMString type, EventListener? callback); + boolean dispatchEvent(Event event); +}; + +callback interface EventListener { + undefined handleEvent(Event event); +}; + +// Custom event that extends Event +[Exposed=*] +interface MessageEvent : Event { + constructor(DOMString type, optional MessageEventInit eventInitDict = {}); + + readonly attribute any data; + readonly attribute DOMString origin; + readonly attribute DOMString lastEventId; + readonly attribute MessagePort? source; +}; + +dictionary MessageEventInit : EventInit { + any data = null; + DOMString origin = ""; + DOMString lastEventId = ""; + MessagePort? source = null; +}; + +[Exposed=*] +interface MessagePort : EventTarget { + undefined postMessage(any message); + undefined start(); + undefined close(); +}; diff --git a/tools/webidl-codegen/examples/example-dict.webidl b/tools/webidl-codegen/examples/example-dict.webidl new file mode 100644 index 00000000000..02588d4e363 --- /dev/null +++ b/tools/webidl-codegen/examples/example-dict.webidl @@ -0,0 +1,20 @@ +// Test case for dictionaries + +dictionary QueuingStrategy { + unrestricted double highWaterMark; + QueuingStrategySize size; +}; + +callback QueuingStrategySize = unrestricted double (any chunk); + +dictionary StreamPipeOptions { + boolean preventClose = false; + boolean preventAbort = false; + boolean preventCancel = false; + AbortSignal signal; +}; + +[Exposed=*] +interface AbortSignal { + readonly attribute boolean aborted; +}; diff --git a/tools/webidl-codegen/examples/fetch.webidl b/tools/webidl-codegen/examples/fetch.webidl new file mode 100644 index 00000000000..ab87c008d22 --- /dev/null +++ b/tools/webidl-codegen/examples/fetch.webidl @@ -0,0 +1,131 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Fetch Standard (https://fetch.spec.whatwg.org/) + +typedef (sequence> or record) HeadersInit; + +[Exposed=(Window,Worker)] +interface Headers { + constructor(optional HeadersInit init); + + undefined append(ByteString name, ByteString value); + undefined delete(ByteString name); + ByteString? get(ByteString name); + sequence getSetCookie(); + boolean has(ByteString name); + undefined set(ByteString name, ByteString value); + iterable; +}; + +typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) XMLHttpRequestBodyInit; + +typedef (ReadableStream or XMLHttpRequestBodyInit) BodyInit; +interface mixin Body { + readonly attribute ReadableStream? body; + readonly attribute boolean bodyUsed; + [NewObject] Promise arrayBuffer(); + [NewObject] Promise blob(); + [NewObject] Promise bytes(); + [NewObject] Promise formData(); + [NewObject] Promise json(); + [NewObject] Promise text(); +}; +typedef (Request or USVString) RequestInfo; + +[Exposed=(Window,Worker)] +interface Request { + constructor(RequestInfo input, optional RequestInit init = {}); + + readonly attribute ByteString method; + readonly attribute USVString url; + [SameObject] readonly attribute Headers headers; + + readonly attribute RequestDestination destination; + readonly attribute USVString referrer; + readonly attribute ReferrerPolicy referrerPolicy; + readonly attribute RequestMode mode; + readonly attribute RequestCredentials credentials; + readonly attribute RequestCache cache; + readonly attribute RequestRedirect redirect; + readonly attribute DOMString integrity; + readonly attribute boolean keepalive; + readonly attribute boolean isReloadNavigation; + readonly attribute boolean isHistoryNavigation; + readonly attribute AbortSignal signal; + readonly attribute RequestDuplex duplex; + + [NewObject] Request clone(); +}; +Request includes Body; + +dictionary RequestInit { + ByteString method; + HeadersInit headers; + BodyInit? body; + USVString referrer; + ReferrerPolicy referrerPolicy; + RequestMode mode; + RequestCredentials credentials; + RequestCache cache; + RequestRedirect redirect; + DOMString integrity; + boolean keepalive; + AbortSignal? signal; + RequestDuplex duplex; + RequestPriority priority; + any window; // can only be set to null +}; + +enum RequestDestination { "", "audio", "audioworklet", "document", "embed", "font", "frame", "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script", "sharedworker", "style", "track", "video", "worker", "xslt" }; +enum RequestMode { "navigate", "same-origin", "no-cors", "cors" }; +enum RequestCredentials { "omit", "same-origin", "include" }; +enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" }; +enum RequestRedirect { "follow", "error", "manual" }; +enum RequestDuplex { "half" }; +enum RequestPriority { "high", "low", "auto" }; + +[Exposed=(Window,Worker)]interface Response { + constructor(optional BodyInit? body = null, optional ResponseInit init = {}); + + [NewObject] static Response error(); + [NewObject] static Response redirect(USVString url, optional unsigned short status = 302); + [NewObject] static Response json(any data, optional ResponseInit init = {}); + + readonly attribute ResponseType type; + + readonly attribute USVString url; + readonly attribute boolean redirected; + readonly attribute unsigned short status; + readonly attribute boolean ok; + readonly attribute ByteString statusText; + [SameObject] readonly attribute Headers headers; + + [NewObject] Response clone(); +}; +Response includes Body; + +dictionary ResponseInit { + unsigned short status = 200; + ByteString statusText = ""; + HeadersInit headers; +}; + +enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" }; + +partial interface mixin WindowOrWorkerGlobalScope { + [NewObject] Promise fetch(RequestInfo input, optional RequestInit init = {}); +}; + +dictionary DeferredRequestInit : RequestInit { + DOMHighResTimeStamp activateAfter; +}; + +[Exposed=Window] +interface FetchLaterResult { + readonly attribute boolean activated; +}; + +partial interface Window { + [NewObject, SecureContext] FetchLaterResult fetchLater(RequestInfo input, optional DeferredRequestInit init = {}); +}; diff --git a/tools/webidl-codegen/examples/mixin-test.webidl b/tools/webidl-codegen/examples/mixin-test.webidl new file mode 100644 index 00000000000..45fbeffff34 --- /dev/null +++ b/tools/webidl-codegen/examples/mixin-test.webidl @@ -0,0 +1,24 @@ +// Test for mixin support + +interface mixin Body { + readonly attribute ReadableStream? body; + readonly attribute boolean bodyUsed; + Promise arrayBuffer(); + Promise blob(); + Promise json(); + Promise text(); +}; + +interface Request { + constructor(USVString url); + readonly attribute USVString url; + readonly attribute ByteString method; +}; +Request includes Body; + +interface Response { + constructor(optional any body); + readonly attribute unsigned short status; + readonly attribute ByteString statusText; +}; +Response includes Body; diff --git a/tools/webidl-codegen/examples/multiple-mixins.webidl b/tools/webidl-codegen/examples/multiple-mixins.webidl new file mode 100644 index 00000000000..eacc5917828 --- /dev/null +++ b/tools/webidl-codegen/examples/multiple-mixins.webidl @@ -0,0 +1,27 @@ +// Test case for multiple mixin inheritance + +interface mixin Body { + readonly attribute any body; + readonly attribute boolean bodyUsed; + Promise arrayBuffer(); + Promise blob(); + Promise json(); + Promise text(); +}; + +interface mixin Headers { + void append(ByteString name, ByteString value); + void delete(ByteString name); + ByteString? get(ByteString name); + boolean has(ByteString name); +}; + +[Exposed=ServiceWorker] +interface Request { + constructor(USVString url); + readonly attribute USVString url; + readonly attribute ByteString method; +}; + +Request includes Body; +Request includes Headers; diff --git a/tools/webidl-codegen/examples/record-test.webidl b/tools/webidl-codegen/examples/record-test.webidl new file mode 100644 index 00000000000..1ad3adfc5da --- /dev/null +++ b/tools/webidl-codegen/examples/record-test.webidl @@ -0,0 +1,13 @@ +// Test record type mapping to jsg::Dict + +dictionary RecordExample { + record stringToNumber; + record headers; + record> multiMap; +}; + +[Exposed=*] +interface RecordAPI { + undefined processHeaders(record headers); + record getMetadata(); +}; diff --git a/tools/webidl-codegen/examples/selfref.webidl b/tools/webidl-codegen/examples/selfref.webidl new file mode 100644 index 00000000000..9eabb34bb08 --- /dev/null +++ b/tools/webidl-codegen/examples/selfref.webidl @@ -0,0 +1,86 @@ +// Example demonstrating SelfRef and internal fields in dictionaries +// SelfRef provides a reference back to the JavaScript object representation +// Fields marked with [JsgInternal] are excluded from JSG_STRUCT parameters + +dictionary ConfigWithSelfRef { + DOMString name; + long priority = 0; + + // SelfRef stores a reference to the JavaScript object + // Automatically excluded from JSG_STRUCT parameters + SelfRef self; + + // Custom internal field that exists in C++ but isn't exposed to JS + // Must be explicitly marked with [JsgInternal] + [JsgInternal] long internalCounter; + + // Note: WebIDL dictionaries don't support methods or constructors in the spec. + // However, we can add custom C++ code using [JsgCode] to declare + // constructors and methods that won't be exposed to the type wrapper. + // Only signatures are needed - implementations are provided separately. +}; + +// Example with custom constructor and methods +[JsgCode=" + // Custom constructor + ConfigWithMethods(kj::String n, int p); + + // Custom methods (not exposed to JS) + void incrementCounter(); + int getCounter() const; +"] +dictionary ConfigWithMethods { + DOMString name; + long priority = 0; + + [JsgInternal] long counter; +}; + +// Example with validate() method for input validation +// Note: validate() is a special method called during deserialization from JS +[JsgCode=" + // Validation method called when unwrapping from JavaScript + void validate(jsg::Lock& js); +"] +dictionary ValidatedConfig { + DOMString apiKey; + long timeout = 30; + + // Implementation would check apiKey.size() > 0, timeout > 0, etc. +}; + +dictionary NestedData { + DOMString value; + boolean enabled = true; + + // SelfRef is always internal, no attribute needed + SelfRef jsObject; + + // Another internal-only field + [JsgInternal] DOMString debugInfo; +}; + +// Example with TypeScript root marker +[JsgTsRoot, + JsgTsDefine="type ConfigFormat = 'json' | 'binary' | 'text';"] +dictionary ExportedConfig { + DOMString name; + DOMString format; +}; + +[Exposed=*] +interface ConfigProcessor { + constructor(); + + // Accept a dictionary with SelfRef + undefined processConfig(ConfigWithSelfRef config); + + // Return a dictionary (SelfRef will be populated automatically) + ConfigWithSelfRef createConfig(DOMString name, optional long priority); + + // Nested dictionary example + undefined handleNested(NestedData data); + + // Use the custom methods dictionary + undefined processWithMethods(ConfigWithMethods config); +}; diff --git a/tools/webidl-codegen/examples/simple.c++.example b/tools/webidl-codegen/examples/simple.c++.example new file mode 100644 index 00000000000..2fc0d3d45a9 --- /dev/null +++ b/tools/webidl-codegen/examples/simple.c++.example @@ -0,0 +1,54 @@ +// Generated implementation stubs - EDIT THIS FILE +// This file contains placeholder implementations. +// Replace TODO comments with actual logic. + +#include "simple.h" + +// Add additional includes as needed: +// #include +// #include + +namespace workerd { +namespace api { + +// C++ constructor - add parameters as needed for your implementation +Calculator::Calculator() { + // TODO: Initialize member variables +} + +// JavaScript constructor (static method for JS 'new' operator) +jsg::Ref Calculator::constructor(jsg::Lock& js) { + // TODO: Implement JavaScript constructor + // Create and return a new instance using js.alloc + // The C++ constructor will be called automatically + return js.alloc(); +} + +int32_t Calculator::add(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement add + // Return the result + return 0; +} + +int32_t Calculator::subtract(jsg::Lock& js, int32_t a, int32_t b) { + // TODO: Implement subtract + // Return the result + return 0; +} + +kj::String Calculator::getVersion(jsg::Lock& js) { + // TODO: Implement getter for version + return kj::str("TODO"); +} + +// Circle custom method implementations +// Add implementations for methods declared in [JsgCode] + +// Example: +// void Circle::validate(jsg::Lock& js) { +// // Validate the struct members +// JSG_REQUIRE(!someField.empty(), TypeError, "someField cannot be empty"); +// } + +} // namespace api +} // namespace workerd diff --git a/tools/webidl-codegen/examples/simple.webidl b/tools/webidl-codegen/examples/simple.webidl new file mode 100644 index 00000000000..2c4c6553fcd --- /dev/null +++ b/tools/webidl-codegen/examples/simple.webidl @@ -0,0 +1,25 @@ +// Simple example for testing implementation generation + +[Exposed=*] +interface Calculator { + constructor(); + + long add(long a, long b); + long subtract(long a, long b); + + readonly attribute DOMString version; +}; + +dictionary Point { + long x; + long y; +}; + +[JsgCode=" + double area() const; + void validate(jsg::Lock& js); +"] +dictionary Circle { + Point center; + double radius; +}; diff --git a/tools/webidl-codegen/examples/text-encoder.webidl b/tools/webidl-codegen/examples/text-encoder.webidl new file mode 100644 index 00000000000..91402af0865 --- /dev/null +++ b/tools/webidl-codegen/examples/text-encoder.webidl @@ -0,0 +1,14 @@ +// Simple test case: TextEncoder API +// https://encoding.spec.whatwg.org/#interface-textencoder + +[Exposed=*] +interface TextEncoder { + constructor(); + + [SameObject] readonly attribute DOMString encoding; + + Uint8Array encode(optional DOMString input = ""); + + // Union type example - accepts string or BufferSource + Uint8Array encodeInto((DOMString or BufferSource) source); +}; diff --git a/tools/webidl-codegen/examples/union-types.webidl b/tools/webidl-codegen/examples/union-types.webidl new file mode 100644 index 00000000000..c65664efa31 --- /dev/null +++ b/tools/webidl-codegen/examples/union-types.webidl @@ -0,0 +1,41 @@ +// Example demonstrating union types (kj::OneOf) + +[Exposed=*] +interface UnionExample { + constructor(); + + // Simple union: string or number + (DOMString or long) getValue(); + + // Union with nullable: string, number, or null + (DOMString or long)? getNullableValue(); + + // Union parameter + undefined setValue((DOMString or long) value); + + // Union with BufferSource + undefined processData((DOMString or BufferSource) data); + + // Complex union: multiple types + (DOMString or long or boolean) getMultiValue(); + + // Union with optional + undefined setOptionalValue(optional (DOMString or long) value); + + // Union return with Promise + Promise<(DOMString or BufferSource)> fetchData(); +}; + +dictionary UnionOptions { + // Union in dictionary + (DOMString or long) identifier; + + // Optional union + (boolean or DOMString) mode = true; + + // Nullable union + (DOMString or long)? optionalValue; +}; + +// Callback with union +callback UnionCallback = undefined ((DOMString or long) result); diff --git a/tools/webidl-codegen/examples/writable-stream.webidl b/tools/webidl-codegen/examples/writable-stream.webidl new file mode 100644 index 00000000000..b16a6edddc9 --- /dev/null +++ b/tools/webidl-codegen/examples/writable-stream.webidl @@ -0,0 +1,18 @@ +// More complex test case: WritableStreamDefaultWriter +// https://streams.spec.whatwg.org/#default-writer-class + +[Exposed=*] +interface Foo {}; + +interface WritableStreamDefaultWriter { + constructor(WritableStream stream); + + readonly attribute Promise closed; + readonly attribute unrestricted double? desiredSize; + readonly attribute Promise ready; + + Promise abort(optional any reason); + Promise close(); + undefined releaseLock(); + Promise write(optional any chunk); +}; diff --git a/tools/webidl-codegen/package.json b/tools/webidl-codegen/package.json new file mode 100644 index 00000000000..2f13980ce76 --- /dev/null +++ b/tools/webidl-codegen/package.json @@ -0,0 +1,24 @@ +{ + "name": "webidl-codegen", + "version": "0.1.0", + "description": "WebIDL to JSG C++ code generator", + "type": "module", + "main": "src/index.js", + "bin": { + "webidl-codegen": "./src/cli.js" + }, + "scripts": { + "test": "npm run test:examples && npm run test:generation && npm run test:types && npm run test:attributes", + "test:examples": "node tests/test-examples.js", + "test:generation": "node tests/test-generation.js", + "test:types": "node tests/test-types.js", + "test:attributes": "node tests/test-attributes.js" + }, + "dependencies": { + "webidl2": "^24.2.0" + }, + "devDependencies": {}, + "engines": { + "node": ">=18" + } +} diff --git a/tools/webidl-codegen/src/cli.js b/tools/webidl-codegen/src/cli.js new file mode 100644 index 00000000000..c3151f006a0 --- /dev/null +++ b/tools/webidl-codegen/src/cli.js @@ -0,0 +1,255 @@ +#!/usr/bin/env node + +/** + * CLI tool for WebIDL to JSG code generation + */ + +import fs from 'fs'; +import path from 'path'; +import { parseWebIDL } from './parser.js'; +import { CppGenerator } from './generator.js'; +import { ImplGenerator } from './impl-generator.js'; +import { ProtectedRegions } from './protected-regions.js'; + +function printSummary(headerSummary, implSummary) { + console.log('\n' + '='.repeat(60)); + console.log('Generation Summary'); + console.log('='.repeat(60)); + + // Header generation summary + console.log('\nHeader:'); + if (headerSummary.interfaces.length > 0) { + console.log(` ✓ ${headerSummary.interfaces.length} interface(s): ${headerSummary.interfaces.join(', ')}`); + } + if (headerSummary.mixins.length > 0) { + console.log(` ✓ ${headerSummary.mixins.length} mixin(s): ${headerSummary.mixins.join(', ')}`); + } + if (headerSummary.dictionaries.length > 0) { + console.log(` ✓ ${headerSummary.dictionaries.length} dictionar(y/ies): ${headerSummary.dictionaries.join(', ')}`); + } + if (headerSummary.enums.length > 0) { + console.log(` ✓ ${headerSummary.enums.length} enum(s): ${headerSummary.enums.join(', ')}`); + } + if (headerSummary.typedefs.length > 0) { + console.log(` ✓ ${headerSummary.typedefs.length} typedef(s): ${headerSummary.typedefs.join(', ')}`); + } + if (headerSummary.callbacks.length > 0) { + console.log(` ✓ ${headerSummary.callbacks.length} callback(s): ${headerSummary.callbacks.join(', ')}`); + } + + // Implementation generation summary + if (implSummary) { + console.log('\nImplementation:'); + if (implSummary.implementations.length > 0) { + console.log(` ✓ ${implSummary.implementations.length} implementation(s): ${implSummary.implementations.join(', ')}`); + } + if (implSummary.skipped.length > 0) { + console.log(` ⊘ ${implSummary.skipped.length} skipped: ${implSummary.skipped.map(s => s.name).join(', ')}`); + } + } + + // Skipped items + if (headerSummary.skipped.length > 0) { + console.log('\nSkipped (--skip-interface):'); + for (const item of headerSummary.skipped) { + console.log(` ⊘ ${item.type}: ${item.name}`); + } + } + + // Unsupported items + if (headerSummary.unsupported.length > 0) { + console.log('\nUnsupported (not implemented):'); + for (const item of headerSummary.unsupported) { + console.log(` ✗ ${item.type}: ${item.name} - ${item.reason}`); + } + } + + const totalGenerated = headerSummary.interfaces.length + + headerSummary.mixins.length + + headerSummary.dictionaries.length + + headerSummary.enums.length + + headerSummary.typedefs.length + + headerSummary.callbacks.length; + const totalSkipped = headerSummary.skipped.length; + const totalUnsupported = headerSummary.unsupported.length; + + console.log('\n' + '-'.repeat(60)); + console.log(`Total: ${totalGenerated} generated, ${totalSkipped} skipped, ${totalUnsupported} unsupported`); + console.log('='.repeat(60) + '\n'); +} + +function printUsage() { + console.log(` +Usage: webidl-codegen [options] + +Options: + -o, --output Output file (default: stdout) + -n, --namespace C++ namespace (default: workerd::api) + --impl Generate implementation stub file + --header Header file to include in implementation (auto-detected if not specified) + --skip-interface Skip generation for specific interface (can be used multiple times) + --update Update mode: preserve manual sections in existing files + --incremental Incremental mode: only generate stubs for new methods (requires --update) + -h, --help Show this help message + +Examples: + # Generate header only + webidl-codegen -o generated.h example.webidl + + # Generate header and implementation stubs + webidl-codegen -o generated.h --impl generated.c++ example.webidl + + # Update existing files while preserving manual sections + webidl-codegen -o generated.h --update example.webidl + + # Incremental update: only add stubs for new methods + webidl-codegen -o api.h --impl api.c++ --update --incremental api.webidl + + # Specify custom header include + webidl-codegen -o api.h --impl api.c++ --header "workerd/api/api.h" example.webidl + + # Custom namespace + webidl-codegen -n workerd::api::streams -o streams.h --impl streams.c++ streams.webidl + + # Skip specific interfaces (e.g., Window) + webidl-codegen -o fetch.h --impl fetch.c++ --skip-interface Window fetch.webidl +`); +} + +function main() { + const args = process.argv.slice(2); + + if (args.length === 0 || args.includes('-h') || args.includes('--help')) { + printUsage(); + process.exit(0); + } + + let inputFile = null; + let outputFile = null; + let implFile = null; + let headerFile = null; + let namespace = 'workerd::api'; + let updateMode = false; + let incrementalMode = false; + let skipInterfaces = []; + + // Parse arguments + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '-o' || arg === '--output') { + outputFile = args[++i]; + } else if (arg === '-n' || arg === '--namespace') { + namespace = args[++i]; + } else if (arg === '--impl') { + implFile = args[++i]; + } else if (arg === '--header') { + headerFile = args[++i]; + } else if (arg === '--skip-interface') { + skipInterfaces.push(args[++i]); + } else if (arg === '--update') { + updateMode = true; + } else if (arg === '--incremental') { + incrementalMode = true; + } else if (arg.startsWith('-')) { + console.error(`Unknown option: ${arg}`); + process.exit(1); + } else { + inputFile = arg; + } + } + + if (!inputFile) { + console.error('Error: No input file specified'); + printUsage(); + process.exit(1); + } + + try { + // Read input file + const idlContent = fs.readFileSync(inputFile, 'utf-8'); + + // Parse WebIDL + const definitions = parseWebIDL(idlContent); + + // If update mode, parse existing file to preserve manual sections + let protectedRegions = null; + if (updateMode && outputFile) { + protectedRegions = new ProtectedRegions(); + protectedRegions.parseFile(outputFile); + } + + // Generate C++ header code + const generator = new CppGenerator({ skipInterfaces }); + if (protectedRegions) { + generator.setProtectedRegions(protectedRegions); + } + const filename = path.basename(inputFile, '.webidl'); + const code = generator.generate(definitions, { + namespace, + filename, + }); + + // Write header output + if (outputFile) { + fs.writeFileSync(outputFile, code); + if (updateMode && protectedRegions) { + console.log(`Updated ${outputFile} (preserved manual sections)`); + } else { + console.log(`Generated ${outputFile}`); + } + } else { + console.log(code); + } + + // Collect summary information + const headerSummary = generator.getSummary(); + let implSummary = null; + + // Generate implementation stubs if requested + if (implFile) { + // Parse existing implementation file in update mode + let implProtectedRegions = null; + if (updateMode && implFile) { + implProtectedRegions = new ProtectedRegions(); + implProtectedRegions.parseFile(implFile); + } + + const implGenerator = new ImplGenerator({ skipInterfaces }); + if (implProtectedRegions) { + implGenerator.setProtectedRegions(implProtectedRegions); + } + if (incrementalMode) { + implGenerator.setIncrementalMode(true); + } + + // Auto-detect header file if not specified + const implHeaderFile = headerFile || (outputFile ? path.basename(outputFile) : `${filename}.h`); + + const implCode = implGenerator.generate(definitions, { + namespace, + headerFile: implHeaderFile, + }); + + fs.writeFileSync(implFile, implCode); + if (updateMode && implProtectedRegions) { + console.log(`Updated ${implFile} (preserved manual sections)`); + } else { + console.log(`Generated ${implFile}`); + } + + implSummary = implGenerator.getSummary(); + } + + // Print generation summary + printSummary(headerSummary, implSummary); + } catch (error) { + console.error(`Error: ${error.message}`); + if (process.env.DEBUG) { + console.error(error.stack); + } + process.exit(1); + } +} + +main(); diff --git a/tools/webidl-codegen/src/generator.js b/tools/webidl-codegen/src/generator.js new file mode 100644 index 00000000000..ef2cd0c2b0d --- /dev/null +++ b/tools/webidl-codegen/src/generator.js @@ -0,0 +1,1236 @@ +/** + * C++ code generator for JSG bindings from WebIDL + */ + +import { TypeMapper } from './type-mapper.js'; +import { ProtectedRegions } from './protected-regions.js'; + +export class CppGenerator { + constructor(options = {}) { + this.typeMapper = new TypeMapper(); + this.protectedRegions = null; // Set via setProtectedRegions() if preserving content + // List of interfaces to skip (already defined in workerd) + this.skipInterfaces = new Set(options.skipInterfaces || [ + 'Event', + 'EventTarget', + 'EventListener', + 'AbortSignal', + 'AbortController', + ]); + // Mixin tracking + this.mixins = new Map(); // name -> mixin definition + this.includesMap = new Map(); // interface name -> [mixin names] + // Generation summary tracking + this.summary = { + interfaces: [], + mixins: [], + dictionaries: [], + enums: [], + typedefs: [], + callbacks: [], + skipped: [], + unsupported: [], + }; + } + + /** + * Set protected regions from existing file + */ + setProtectedRegions(protectedRegions) { + this.protectedRegions = protectedRegions; + } + + /** + * Get generation summary + */ + getSummary() { + return this.summary; + } + + /** + * Extract compat flag from WebIDL extended attribute + * Supports [JsgCompatFlag=FlagName] or [JsgCompatFlag="FlagName"] + */ + getCompatFlag(extAttrs) { + if (!extAttrs) return null; + + const compatAttr = extAttrs.find(attr => attr.name === 'JsgCompatFlag'); + if (!compatAttr) return null; + + // Handle [CompatFlag=FlagName] or [CompatFlag="FlagName"] + if (compatAttr.rhs) { + return compatAttr.rhs.value || compatAttr.rhs; + } + + return null; + } + + /** + * Check if method should be in the else block (when compat flag is OFF) + * Supports [JsgCompatFlagOff=FlagName] + */ + getCompatFlagOff(extAttrs) { + if (!extAttrs) return null; + + const compatAttr = extAttrs.find(attr => attr.name === 'JsgCompatFlagOff'); + if (!compatAttr) return null; + + // Handle [CompatFlagOff=FlagName] + if (compatAttr.rhs) { + return compatAttr.rhs.value || compatAttr.rhs; + } + + return null; + } + + /** + * Extract custom method name from WebIDL extended attribute + * Supports [JsgMethodName=customName] or [JsgMethodName="customName"] + */ + getMethodName(extAttrs) { + if (!extAttrs) return null; + + const methodNameAttr = extAttrs.find(attr => attr.name === 'JsgMethodName'); + if (!methodNameAttr) return null; + + // Handle [MethodName=name] or [MethodName="name"] + if (methodNameAttr.rhs) { + return methodNameAttr.rhs.value || methodNameAttr.rhs; + } + + return null; + } + + /** + * Extract TypeScript override from WebIDL extended attribute + * Supports [JsgTsOverride="typescript code"] for JSG_TS_OVERRIDE + */ + getTsOverride(extAttrs) { + if (!extAttrs) return null; + + const tsOverrideAttr = extAttrs.find(attr => attr.name === 'JsgTsOverride'); + if (!tsOverrideAttr) return null; + + // Handle [TsOverride="..."] + if (tsOverrideAttr.rhs) { + return tsOverrideAttr.rhs.value || tsOverrideAttr.rhs; + } + + return null; + } + + /** + * Extract TypeScript definitions from WebIDL extended attribute + * Supports [JsgTsDefine="typescript code"] for JSG_TS_DEFINE + */ + getTsDefine(extAttrs) { + if (!extAttrs) return null; + + const tsDefineAttr = extAttrs.find(attr => attr.name === 'JsgTsDefine'); + if (!tsDefineAttr) return null; + + // Handle [TsDefine="..."] + if (tsDefineAttr.rhs) { + return tsDefineAttr.rhs.value || tsDefineAttr.rhs; + } + + return null; + } + + /** + * Extract property scope from WebIDL extended attribute + * Supports [JsgPropertyScope=instance] or [JsgPropertyScope=prototype] + * Used to control JSG_*_INSTANCE_PROPERTY vs JSG_*_PROTOTYPE_PROPERTY + */ + getPropertyScope(extAttrs) { + if (!extAttrs) return null; + + const scopeAttr = extAttrs.find(attr => attr.name === 'JsgPropertyScope'); + if (!scopeAttr) return null; + + // Handle [PropertyScope=instance] or [PropertyScope=prototype] + if (scopeAttr.rhs) { + const value = scopeAttr.rhs.value || scopeAttr.rhs; + return value === 'instance' || value === 'prototype' ? value : null; + } + + return null; + } + + /** + * Check if a dictionary member should be excluded from JSG_STRUCT parameters + * Returns true for fields marked with [JsgInternal] or special types like SelfRef + * These fields exist in the C++ struct but aren't exposed to the type wrapper + */ + isJsgInternalField(member) { + // Check for [JsgInternal] extended attribute + if (member.extAttrs && member.extAttrs.some(attr => attr.name === 'JsgInternal')) { + return true; + } + + // SelfRef, Unimplemented, and WontImplement are always internal + const baseType = member.idlType.idlType || member.idlType; + return baseType === 'SelfRef' || baseType === 'Unimplemented' || baseType === 'WontImplement'; + } + + /** + * Get custom C++ code from [JsgCode] attribute + * Supports [JsgCode="..."] for adding constructors, methods, etc. + */ + getJsgCode(extAttrs) { + if (!extAttrs) return null; + + const jsgCodeAttr = extAttrs.find(attr => attr.name === 'JsgCode'); + if (!jsgCodeAttr) return null; + + // Handle [JsgCode="..."] + if (jsgCodeAttr.rhs) { + let code = jsgCodeAttr.rhs.value || jsgCodeAttr.rhs; + + // The WebIDL parser returns the string with outer quotes included + // Strip them if present + if (typeof code === 'string' && code.length >= 2) { + if ((code.startsWith('"') && code.endsWith('"')) || + (code.startsWith("'") && code.endsWith("'"))) { + code = code.slice(1, -1); + } + } + + return code; + } + + return null; + } /** + * Check if dictionary should be a TypeScript root type + * Supports [JsgTsRoot] for JSG_STRUCT_TS_ROOT + */ + getTsRoot(extAttrs) { + if (!extAttrs) return false; + return extAttrs.some(attr => attr.name === 'JsgTsRoot'); + } + + /** + * Generate complete C++ header from parsed WebIDL + * @param {Array} definitions - Parsed WebIDL definitions from webidl2 + * @param {object} options - Generation options + * @returns {string} Generated C++ header code + */ + generate(definitions, options = {}) { + // Store definitions for inheritance lookups + this.definitions = definitions; + + // Reset summary + this.summary = { + interfaces: [], + mixins: [], + dictionaries: [], + enums: [], + typedefs: [], + callbacks: [], + skipped: [], + unsupported: [], + }; + + // Update type mapper with definitions so it can distinguish dictionaries from interfaces + this.typeMapper = new TypeMapper(definitions); + + // Process mixins and includes statements + this.processMixinsAndIncludes(definitions); + + const { namespace = 'workerd::api', includeGuard } = options; + + let code = ''; + + // Header guard + if (includeGuard !== false) { + const guard = this.makeIncludeGuard(options.filename || 'generated'); + code += `#pragma once\n`; + code += `// Generated from WebIDL - DO NOT EDIT\n\n`; + } + + // Includes + code += this.generateIncludes(); + + // Namespace (use :: notation for nested namespaces) + code += `namespace ${namespace} {\n\n`; + + // Generate forward declarations for undefined types + code += this.generateForwardDeclarations(definitions); + + // Sort definitions by dependency order: + // 1. typedefs (may be used by other types) + // 2. enums (simple value types) + // 3. callbacks (function types) + // 4. dictionaries (value types) + // 5. mixins (base classes) + // 6. interfaces (complex types that may use all of the above) + const typeOrder = { + 'typedef': 1, + 'enum': 2, + 'callback': 3, + 'dictionary': 4, + 'interface mixin': 5, + 'interface': 6, + 'includes': 7 + }; + + const sortedDefinitions = [...definitions].sort((a, b) => { + const orderA = typeOrder[a.type] || 999; + const orderB = typeOrder[b.type] || 999; + return orderA - orderB; + }); + + // Generate code for each definition + for (const def of sortedDefinitions) { + // Skip interfaces/mixins that are already defined in workerd or explicitly excluded + if ((def.type === 'interface' || def.type === 'interface mixin') && this.skipInterfaces.has(def.name)) { + this.summary.skipped.push({ type: def.type === 'interface mixin' ? 'mixin' : 'interface', name: def.name }); + code += `// ${def.name} is defined in workerd (skipped)\n\n`; + continue; + } + + code += this.generateDefinition(def); + code += '\n'; + } + + // Close namespace + code += `\n} // namespace ${namespace}\n`; + + return code; + } + + /** + * Process mixin definitions and includes statements + */ + processMixinsAndIncludes(definitions) { + // First pass: collect all mixins + for (const def of definitions) { + if (def.type === 'interface mixin') { + this.mixins.set(def.name, def); + } + } + + // Second pass: collect includes statements + for (const def of definitions) { + if (def.type === 'includes') { + if (!this.includesMap.has(def.target)) { + this.includesMap.set(def.target, []); + } + this.includesMap.get(def.target).push(def.includes); + } + } + } + + generateIncludes() { + return `#include +#include +#include + +// Note: Base classes like Event, EventTarget may need additional includes: +// #include + +`; + } + + /** + * Generate forward declarations for types referenced but not defined in this file, + * plus interfaces defined in this file (to handle forward references in typedefs) + */ + generateForwardDeclarations(definitions) { + // Collect interface names that will be defined in this file + const localInterfaces = new Set(); + for (const def of definitions) { + if ((def.type === 'interface' || def.type === 'interface mixin') && def.name) { + localInterfaces.add(def.name); + } + } + + // Collect all non-interface types defined in this file (enums, typedefs, dictionaries) + // These should NOT be forward declared + const localNonInterfaces = new Set(); + for (const def of definitions) { + if (def.name && def.type !== 'interface' && def.type !== 'interface mixin') { + localNonInterfaces.add(def.name); + } + } + + // Collect all referenced types + const referencedTypes = new Set(); + + const extractTypesFromIdlType = (idlType) => { + if (!idlType) return; + + if (typeof idlType === 'string') { + referencedTypes.add(idlType); + return; + } + + if (idlType.union && Array.isArray(idlType.idlType)) { + idlType.idlType.forEach(extractTypesFromIdlType); + return; + } + + if (idlType.generic && Array.isArray(idlType.idlType)) { + idlType.idlType.forEach(extractTypesFromIdlType); + return; + } + + if (idlType.idlType) { + extractTypesFromIdlType(idlType.idlType); + } + }; + + for (const def of definitions) { + // Extract from interface members + if (def.type === 'interface' || def.type === 'interface mixin') { + // Check inheritance + if (def.inheritance) { + referencedTypes.add(def.inheritance); + } + + // Check members + for (const member of def.members || []) { + if (member.idlType) { + extractTypesFromIdlType(member.idlType); + } + // Check arguments + if (member.arguments) { + for (const arg of member.arguments) { + if (arg.idlType) { + extractTypesFromIdlType(arg.idlType); + } + } + } + } + } + + // Extract from dictionary members + if (def.type === 'dictionary') { + if (def.inheritance) { + referencedTypes.add(def.inheritance); + } + for (const member of def.members || []) { + if (member.idlType) { + extractTypesFromIdlType(member.idlType); + } + } + } + + // Extract from typedef + if (def.type === 'typedef' && def.idlType) { + extractTypesFromIdlType(def.idlType); + } + } + + // Filter out primitive types and types that are defined in this file + const primitiveTypes = new Set([ + 'boolean', 'byte', 'octet', 'short', 'unsigned short', 'long', 'unsigned long', + 'long long', 'unsigned long long', 'float', 'double', 'unrestricted float', + 'unrestricted double', 'DOMString', 'ByteString', 'USVString', 'any', 'object', + 'undefined', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', + 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', + 'BigInt64Array', 'BigUint64Array', 'SelfRef', 'void' + ]); + + // Known external enums that shouldn't be forward declared (they need to be included) + const externalEnums = new Set([ + 'ReferrerPolicy', // From Referrer Policy spec + ]); + + // Types to forward declare are: + // 1. Referenced types that aren't primitives, external enums, or local non-interfaces + // 2. This includes both external interfaces and local interfaces (for forward reference support) + const typesToForwardDeclare = [...referencedTypes].filter(type => + !primitiveTypes.has(type) && + !externalEnums.has(type) && + !localNonInterfaces.has(type) + ).sort(); + + if (typesToForwardDeclare.length === 0) { + return ''; + } + + let code = '// Forward declarations for types defined elsewhere\n'; + for (const type of typesToForwardDeclare) { + code += `class ${type};\n`; + } + code += '\n'; + + return code; + } + + makeIncludeGuard(filename) { + return filename + .toUpperCase() + .replace(/[^A-Z0-9]/g, '_') + + '_H_'; + } + + generateDefinition(def) { + switch (def.type) { + case 'interface': + if (def.partial) { + this.summary.unsupported.push({ type: 'partial interface', name: def.name, reason: 'Partial interfaces not yet supported (treating as regular interface)' }); + } + this.summary.interfaces.push(def.name); + return this.generateInterface(def); + case 'interface mixin': + if (def.partial) { + this.summary.unsupported.push({ type: 'partial mixin', name: def.name, reason: 'Partial mixins not yet supported (treating as regular mixin)' }); + } + this.summary.mixins.push(def.name); + // Mixins never inherit from jsg::Object to avoid diamond inheritance + return this.generateMixin(def, false); + case 'dictionary': + if (def.partial) { + this.summary.unsupported.push({ type: 'partial dictionary', name: def.name, reason: 'Partial dictionaries not yet supported (treating as regular dictionary)' }); + } + this.summary.dictionaries.push(def.name); + return this.generateDictionary(def); + case 'enum': + this.summary.enums.push(def.name); + return this.generateEnum(def); + case 'callback': + this.summary.callbacks.push(def.name); + return this.generateCallback(def); + case 'typedef': + this.summary.typedefs.push(def.name); + return this.generateTypedef(def); + case 'includes': + // Includes statements are processed separately, no code generation needed + return ''; + case 'namespace': + this.summary.unsupported.push({ type: 'namespace', name: def.name, reason: 'Namespaces not yet implemented' }); + return `// Namespace ${def.name} not yet supported\n`; + default: + this.summary.unsupported.push({ type: def.type, name: def.name || 'unknown', reason: `Unsupported definition type: ${def.type}` }); + return `// Unsupported definition type: ${def.type}\n`; + } + } + + generateInterface(iface) { + let code = ''; + + // Class declaration with inheritance + // Check if this interface includes any mixins + const mixinNames = this.includesMap.get(iface.name) || []; + const baseClasses = []; + + // Add interface inheritance or jsg::Object first + if (iface.inheritance) { + baseClasses.push(iface.inheritance); + } else { + // Always add jsg::Object if there's no explicit inheritance + // (mixins don't inherit from jsg::Object to avoid diamond inheritance) + baseClasses.push('jsg::Object'); + } + + // Add mixins after jsg::Object (in order they were included) + for (const mixinName of mixinNames) { + baseClasses.push(mixinName); + } + + const baseClassList = baseClasses.join(', public '); + code += `class ${iface.name}: public ${baseClassList} {\n`; + code += ` public:\n`; + + // C++ constructor (always needed for jsg::Object instances) + code += ` ${iface.name}();\n\n`; + + // Generate method declarations + const operations = iface.members.filter(m => m.type === 'operation'); + const attributes = iface.members.filter(m => m.type === 'attribute'); + const constructor = iface.members.find(m => m.type === 'constructor'); + + // JavaScript constructor (static method, only if WebIDL has constructor) + if (constructor) { + code += this.generateConstructor(iface.name, constructor); + } + + // Group operations by name to handle overloads + const operationsByName = new Map(); + for (const op of operations) { + if (!op.name) continue; + if (!operationsByName.has(op.name)) { + operationsByName.set(op.name, []); + } + operationsByName.get(op.name).push(op); + } + + // Generate operations - for overloads, generate all variants with suffix + for (const [name, overloads] of operationsByName) { + if (overloads.length === 1) { + // Single signature - check for custom method name + const op = overloads[0]; + const customName = this.getMethodName(op.extAttrs); + const methodName = customName ? customName : this.escapeCppKeyword(op.name); + code += this.generateOperation(op, '', methodName); + } else { + // Multiple overloads - add suffix based on compat flag or param count + for (let i = 0; i < overloads.length; i++) { + const op = overloads[i]; + const customName = this.getMethodName(op.extAttrs); + const compatFlag = this.getCompatFlag(op.extAttrs); + let suffix = ''; + + if (customName) { + // Use custom method name directly (no suffix) + code += this.generateOperation(op, '', customName); + } else if (compatFlag) { + // Use compat flag as suffix + suffix = `_${compatFlag}`; + code += this.generateOperation(op, suffix); + } else if (i > 0) { + // Fallback: use numeric suffix for subsequent overloads + suffix = `_overload${i}`; + code += this.generateOperation(op, suffix); + } else { + // First overload without custom name or flag + code += this.generateOperation(op); + } + } + } + } + + // Attribute getters/setters + for (const attr of attributes) { + code += this.generateAttribute(attr); + } + + // Protected region for custom public members + code += '\n'; + if (this.protectedRegions) { + const regionName = `${iface.name}::public`; + const defaultContent = ` // Add custom public methods, fields, or nested types here\n`; + code += this.protectedRegions.generateRegion(regionName, defaultContent); + } else { + code += ` // BEGIN MANUAL SECTION: ${iface.name}::public\n`; + code += ` // Add custom public methods, fields, or nested types here\n`; + code += ` // END MANUAL SECTION: ${iface.name}::public\n`; + } + + // JSG_RESOURCE_TYPE block + code += '\n'; + code += this.generateJsgResourceType(iface); + + // Private section (optional) + code += '\n private:\n'; + if (this.protectedRegions) { + const regionName = `${iface.name}::private`; + const defaultContent = ` // Add private member variables here\n`; + code += this.protectedRegions.generateRegion(regionName, defaultContent); + } else { + code += ` // BEGIN MANUAL SECTION: ${iface.name}::private\n`; + code += ` // Add private member variables here\n`; + code += ` // END MANUAL SECTION: ${iface.name}::private\n`; + } + + code += `};\n`; + return code; + } + + /** + * Generate a mixin class + * @param {object} mixin - The mixin definition + * @param {boolean} needsJsgObject - Whether this mixin should inherit from jsg::Object + * Should be false to avoid diamond inheritance when interfaces include multiple mixins + */ + generateMixin(mixin, needsJsgObject = false) { + let code = ''; + + // Comment indicating this is a mixin + code += `// Mixin: ${mixin.name}\n`; + code += `// Used by interfaces via C++ inheritance\n`; + + // Class declaration - only inherit from jsg::Object if needed + if (needsJsgObject) { + code += `class ${mixin.name}: public jsg::Object {\n`; + } else { + code += `class ${mixin.name} {\n`; + } + code += ` public:\n`; + + // Generate method declarations + const operations = mixin.members.filter(m => m.type === 'operation'); + const attributes = mixin.members.filter(m => m.type === 'attribute'); + + // Mixin operations + for (const op of operations) { + if (!op.name) continue; + code += this.generateOperation(op); + } + + // Mixin attribute getters/setters + for (const attr of attributes) { + code += this.generateAttribute(attr); + } + + // Protected region for custom public members + code += '\n'; + if (this.protectedRegions) { + const regionName = `${mixin.name}::public`; + const defaultContent = ` // Add custom public methods or fields here\n`; + code += this.protectedRegions.generateRegion(regionName, defaultContent); + } else { + code += ` // BEGIN MANUAL SECTION: ${mixin.name}::public\n`; + code += ` // Add custom public methods or fields here\n`; + code += ` // END MANUAL SECTION: ${mixin.name}::public\n`; + } + + // Note: Mixins don't have JSG_RESOURCE_TYPE - their members are registered + // in the JSG_RESOURCE_TYPE of interfaces that include them + + // Private section + code += '\n private:\n'; + if (this.protectedRegions) { + const regionName = `${mixin.name}::private`; + const defaultContent = ` // Add private member variables here\n`; + code += this.protectedRegions.generateRegion(regionName, defaultContent); + } else { + code += ` // BEGIN MANUAL SECTION: ${mixin.name}::private\n`; + code += ` // Add private member variables here\n`; + code += ` // END MANUAL SECTION: ${mixin.name}::private\n`; + } + + code += `};\n`; + return code; + } + + generateConstructor(className, ctor) { + const params = ['jsg::Lock& js']; + for (const arg of ctor.arguments) { + const type = this.typeMapper.mapParameter(arg); + params.push(`${type} ${arg.name}`); + } + + return ` static jsg::Ref<${className}> constructor(${params.join(', ')});\n\n`; + } + + generateOperation(op, suffix = '', customMethodName = null) { + // Handle special operations (getters, setters, etc.) + if (op.special) { + return ` // Special operation: ${op.special}\n`; + } + + const name = op.name; + const returnType = this.typeMapper.mapType(op.idlType); + + const params = ['jsg::Lock& js']; + for (const arg of op.arguments) { + const type = this.typeMapper.mapParameter(arg); + params.push(`${type} ${arg.name}`); + } + + // Use custom method name if provided, otherwise escape and add suffix to original name + const methodName = customMethodName || this.escapeCppKeyword(name) + suffix; + + return ` ${returnType} ${methodName}(${params.join(', ')});\n`; + } + + generateAttribute(attr) { + let code = ''; + const type = this.typeMapper.mapType(attr.idlType); + const getterName = this.makeGetterName(attr.name); + + // Getter + code += ` ${type} ${getterName}(jsg::Lock& js);\n`; + + // Setter (if not readonly) + if (!attr.readonly) { + const setterName = this.makeSetterName(attr.name); + code += ` void ${setterName}(jsg::Lock& js, ${type} value);\n`; + } + + return code; + } + + generateJsgResourceType(iface) { + let code = ''; + + // Collect members from this interface and any included mixins + const allMembers = [...iface.members]; + const mixinNames = this.includesMap.get(iface.name) || []; + for (const mixinName of mixinNames) { + const mixin = this.mixins.get(mixinName); + if (mixin) { + allMembers.push(...mixin.members); + } + } + + // Check if any members have compat flags - if so, add flags parameter + const hasCompatFlags = allMembers.some(m => this.getCompatFlag(m.extAttrs)); + + if (hasCompatFlags) { + code += ` JSG_RESOURCE_TYPE(${iface.name}, CompatibilityFlags::Reader flags) {\n`; + } else { + code += ` JSG_RESOURCE_TYPE(${iface.name}) {\n`; + } + + // Add JSG_INHERIT if this interface has a base class (other than the skip list) + if (iface.inheritance && !this.skipInterfaces.has(iface.inheritance)) { + code += ` JSG_INHERIT(${iface.inheritance});\n`; + } + + const operations = allMembers.filter(m => m.type === 'operation' && m.name); + const attributes = allMembers.filter(m => m.type === 'attribute'); + + // Group operations by name to detect overloads + const operationsByName = new Map(); + for (const op of operations) { + if (!operationsByName.has(op.name)) { + operationsByName.set(op.name, []); + } + operationsByName.get(op.name).push(op); + } + + // Collect all members (attributes + operations) with their flags + const membersByFlag = new Map(); + membersByFlag.set(null, []); // Members without flags + + // Track members for else blocks (CompatFlagOff) + const membersByFlagOff = new Map(); + + // Add attributes + for (const attr of attributes) { + const flag = this.getCompatFlag(attr.extAttrs); + const flagOff = this.getCompatFlagOff(attr.extAttrs); + const propertyScope = this.getPropertyScope(attr.extAttrs); + + if (flagOff) { + if (!membersByFlagOff.has(flagOff)) { + membersByFlagOff.set(flagOff, []); + } + membersByFlagOff.get(flagOff).push({ + type: 'attribute', + member: attr, + propertyScope: propertyScope + }); + } else { + if (!membersByFlag.has(flag)) { + membersByFlag.set(flag, []); + } + membersByFlag.get(flag).push({ + type: 'attribute', + member: attr, + propertyScope: propertyScope + }); + } + } // Add operations (handling overloads) + for (const [opName, overloads] of operationsByName) { + if (overloads.length === 1) { + // Single signature - register normally + const op = overloads[0]; + const flag = this.getCompatFlag(op.extAttrs); + const flagOff = this.getCompatFlagOff(op.extAttrs); + const customName = this.getMethodName(op.extAttrs); + const escapedName = this.escapeCppKeyword(op.name); + const cppMethodName = customName || escapedName; + const needsNaming = customName || (escapedName !== op.name); + + if (flagOff) { + if (!membersByFlagOff.has(flagOff)) { + membersByFlagOff.set(flagOff, []); + } + membersByFlagOff.get(flagOff).push({ + type: 'operation', + member: op, + methodName: cppMethodName, + jsName: needsNaming ? op.name : null + }); + } else { + if (!membersByFlag.has(flag)) { + membersByFlag.set(flag, []); + } + membersByFlag.get(flag).push({ + type: 'operation', + member: op, + methodName: cppMethodName, + jsName: needsNaming ? op.name : null + }); + } + } else { + // Multiple overloads - register each with its own flag + for (let i = 0; i < overloads.length; i++) { + const op = overloads[i]; + const compatFlag = this.getCompatFlag(op.extAttrs); + const compatFlagOff = this.getCompatFlagOff(op.extAttrs); + const customName = this.getMethodName(op.extAttrs); + const escapedName = this.escapeCppKeyword(op.name); + + let cppMethodName; + if (customName) { + // Use custom method name directly + cppMethodName = customName; + } else if (compatFlag || compatFlagOff) { + // Use compat flag as suffix + const flagName = compatFlag || compatFlagOff; + cppMethodName = escapedName + `_${flagName}`; + } else if (i > 0) { + // Fallback: use numeric suffix + cppMethodName = escapedName + `_overload${i}`; + } else { + // First overload without custom name or flag + cppMethodName = escapedName; + } + + // Add to the appropriate flag group + if (compatFlagOff) { + if (!membersByFlagOff.has(compatFlagOff)) { + membersByFlagOff.set(compatFlagOff, []); + } + membersByFlagOff.get(compatFlagOff).push({ + type: 'operation', + member: op, + methodName: cppMethodName, + jsName: op.name + }); + } else { + const flag = compatFlag; + if (!membersByFlag.has(flag)) { + membersByFlag.set(flag, []); + } + membersByFlag.get(flag).push({ + type: 'operation', + member: op, + methodName: cppMethodName, + jsName: op.name // The JS name stays the same for overloads + }); + } + } + } + } // Generate unconditional members first + const unconditionalMembers = membersByFlag.get(null) || []; + for (const item of unconditionalMembers) { + if (item.type === 'attribute') { + const attr = item.member; + const propName = attr.name; + const getterName = this.makeGetterName(attr.name); + const scope = item.propertyScope || 'prototype'; // Default to prototype + + if (attr.readonly) { + if (scope === 'instance') { + code += ` JSG_READONLY_INSTANCE_PROPERTY(${propName}, ${getterName});\n`; + } else { + code += ` JSG_READONLY_PROTOTYPE_PROPERTY(${propName}, ${getterName});\n`; + } + } else { + const setterName = this.makeSetterName(attr.name); + if (scope === 'instance') { + code += ` JSG_INSTANCE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } else { + code += ` JSG_PROTOTYPE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } + } + } else if (item.type === 'operation') { + const jsName = item.jsName || item.member.name; + const cppName = item.methodName; + + if (jsName === cppName) { + code += ` JSG_METHOD(${cppName});\n`; + } else { + code += ` JSG_METHOD_NAMED(${jsName}, ${cppName});\n`; + } + } + } + + // Generate conditional members grouped by flag + for (const [flag, members] of membersByFlag) { + if (flag === null || members.length === 0) continue; + + // Check if there are corresponding else block members + const elseMembers = membersByFlagOff.get(flag) || []; + + code += `\n if (flags.get${flag}()) {\n`; + + for (const item of members) { + if (item.type === 'attribute') { + const attr = item.member; + const propName = attr.name; + const getterName = this.makeGetterName(attr.name); + const scope = item.propertyScope || 'prototype'; // Default to prototype + + if (attr.readonly) { + if (scope === 'instance') { + code += ` JSG_READONLY_INSTANCE_PROPERTY(${propName}, ${getterName});\n`; + } else { + code += ` JSG_READONLY_PROTOTYPE_PROPERTY(${propName}, ${getterName});\n`; + } + } else { + const setterName = this.makeSetterName(attr.name); + if (scope === 'instance') { + code += ` JSG_INSTANCE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } else { + code += ` JSG_PROTOTYPE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } + } + } else if (item.type === 'operation') { + const jsName = item.jsName || item.member.name; + const cppName = item.methodName; + + if (jsName === cppName) { + code += ` JSG_METHOD(${cppName});\n`; + } else { + code += ` JSG_METHOD_NAMED(${jsName}, ${cppName});\n`; + } + } + } + + // Generate else block if there are members for when flag is OFF + if (elseMembers.length > 0) { + code += ` } else {\n`; + + for (const item of elseMembers) { + if (item.type === 'attribute') { + const attr = item.member; + const propName = attr.name; + const getterName = this.makeGetterName(attr.name); + const scope = item.propertyScope || 'prototype'; // Default to prototype + + if (attr.readonly) { + if (scope === 'instance') { + code += ` JSG_READONLY_INSTANCE_PROPERTY(${propName}, ${getterName});\n`; + } else { + code += ` JSG_READONLY_PROTOTYPE_PROPERTY(${propName}, ${getterName});\n`; + } + } else { + const setterName = this.makeSetterName(attr.name); + if (scope === 'instance') { + code += ` JSG_INSTANCE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } else { + code += ` JSG_PROTOTYPE_PROPERTY(${propName}, ${getterName}, ${setterName});\n`; + } + } + } else if (item.type === 'operation') { + const jsName = item.jsName || item.member.name; + const cppName = item.methodName; + + if (jsName === cppName) { + code += ` JSG_METHOD(${cppName});\n`; + } else { + code += ` JSG_METHOD_NAMED(${jsName}, ${cppName});\n`; + } + } + } + } + + code += ` }\n`; + } + + // Add TypeScript overrides and definitions if present + const tsDefine = this.getTsDefine(iface.extAttrs); + const tsOverride = this.getTsOverride(iface.extAttrs); + + if (tsDefine) { + code += `\n JSG_TS_DEFINE(${tsDefine});\n`; + } + + if (tsOverride) { + code += `\n JSG_TS_OVERRIDE(${tsOverride});\n`; + } + + code += ` }\n`; + return code; + } /** + * Recursively collect all members from a dictionary and its parents + * @param {object} dict - Dictionary definition + * @returns {Array} All members including inherited ones + */ + collectDictionaryMembers(dict) { + const members = [...dict.members]; + + // If this dictionary inherits from another, recursively collect parent members + if (dict.inheritance) { + const parentDict = this.definitions.find( + def => def.type === 'dictionary' && def.name === dict.inheritance + ); + + if (parentDict) { + // Parent members come first (for proper initialization order) + const parentMembers = this.collectDictionaryMembers(parentDict); + return [...parentMembers, ...members]; + } + } + + return members; + } + + generateDictionary(dict) { + let code = ''; + + // Struct declaration - NO inheritance in C++ (we flatten instead) + code += `struct ${dict.name} {\n`; + + // Collect all members including inherited ones + const allMembers = this.collectDictionaryMembers(dict); + + // Separate exposed and internal fields + const exposedFields = []; + const internalFields = []; + + for (const member of allMembers) { + if (this.isJsgInternalField(member)) { + internalFields.push(member); + } else { + exposedFields.push(member); + } + } + + // Generate exposed fields first + for (const member of exposedFields) { + const type = this.typeMapper.mapDictionaryMemberType(member); + code += ` ${type} ${member.name}`; + + // Add comment for default value if present (but don't generate it) + if (member.default) { + code += `; // default: ${this.formatDefaultValue(member.default)}`; + } else { + code += `;`; + } + + code += `\n`; + } + + // Add blank line before JSG_STRUCT if there are fields + if (exposedFields.length > 0) { + code += `\n`; + } + + // JSG_STRUCT with exposed field names only + const fieldNames = exposedFields.map(m => m.name).join(', '); + code += ` JSG_STRUCT(${fieldNames});\n`; + + // Always add JSG_STRUCT_TS_OVERRIDE if there's inheritance + // This ensures TypeScript sees the inheritance relationship even though C++ is flattened + if (dict.inheritance) { + code += ` JSG_STRUCT_TS_OVERRIDE(${dict.name} extends ${dict.inheritance}); +`; + } + + // Add JSG_STRUCT_TS_ROOT if requested + const tsRoot = this.getTsRoot(dict.extAttrs); + if (tsRoot) { + code += ` JSG_STRUCT_TS_ROOT();\n`; + } + + // Add TypeScript definitions if present + const tsDefine = this.getTsDefine(dict.extAttrs); + if (tsDefine) { + code += ` JSG_STRUCT_TS_DEFINE(${tsDefine});\n`; + } + + // Add TypeScript override if present + const tsOverride = this.getTsOverride(dict.extAttrs); + if (tsOverride) { + code += ` JSG_TS_OVERRIDE(${tsOverride});\n`; + } + + // Generate internal fields after JSG_STRUCT + if (internalFields.length > 0) { + code += `\n`; + for (const member of internalFields) { + const type = this.typeMapper.mapDictionaryMemberType(member); + code += ` ${type} ${member.name}`; + + // Add comment for SelfRef explaining its purpose + const baseType = member.idlType.idlType || member.idlType; + if (baseType === 'SelfRef') { + code += `; // Reference to the JavaScript object`; + } else { + code += `; // Internal field, not exposed to JS`; + } + + code += `\n`; + } + } + + // Add custom C++ code (constructors, methods) if specified with [JsgCode] + const jsgCode = this.getJsgCode(dict.extAttrs); + if (jsgCode) { + code += `\n${jsgCode}\n`; + } + + code += `};\n`; + + return code; + } generateEnum(enumDef) { + let code = ''; + code += `enum class ${enumDef.name} {\n`; + + for (const value of enumDef.values) { + // Convert "kebab-case" to SCREAMING_SNAKE_CASE for enum values + const enumValue = value.value + .replace(/-/g, '_') + .toUpperCase(); + code += ` ${enumValue},\n`; + } + + code += `};\n`; + return code; + } + + generateTypedef(typedef) { + // Generate a C++ using alias for the typedef + const mappedType = this.typeMapper.mapType(typedef.idlType); + return `using ${typedef.name} = ${mappedType};\n`; + } + + generateCallback(callback) { + const returnType = this.typeMapper.mapType(callback.idlType); + const params = callback.arguments.map(arg => { + const type = this.typeMapper.mapParameter(arg.idlType); + return type; + }).join(', '); + + return `using ${callback.name} = kj::Function<${returnType}(${params})>;\n`; + } + + /** + * Escape C++ keywords by adding a trailing underscore + */ + escapeCppKeyword(name) { + const cppKeywords = new Set([ + 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', + 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', + 'class', 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', + 'const_cast', 'continue', 'co_await', 'co_return', 'co_yield', 'decltype', + 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum', + 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', + 'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new', 'noexcept', + 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private', 'protected', + 'public', 'register', 'reinterpret_cast', 'requires', 'return', 'short', + 'signed', 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', + 'switch', 'template', 'this', 'thread_local', 'throw', 'true', 'try', + 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual', + 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq' + ]); + + return cppKeywords.has(name) ? name + '_' : name; + } + + makeGetterName(propName) { + // Convert camelCase to getterName format + return `get${propName.charAt(0).toUpperCase()}${propName.slice(1)}`; + } + + makeSetterName(propName) { + return `set${propName.charAt(0).toUpperCase()}${propName.slice(1)}`; + } + + formatDefaultValue(defaultValue) { + switch (defaultValue.type) { + case 'boolean': + return defaultValue.value ? 'true' : 'false'; + case 'number': + return String(defaultValue.value); + case 'string': + return `"${defaultValue.value}"_kj`; + case 'null': + return 'nullptr'; + case 'sequence': + case 'dictionary': + return '{}'; + default: + return '/* default */'; + } + } +} diff --git a/tools/webidl-codegen/src/impl-generator.js b/tools/webidl-codegen/src/impl-generator.js new file mode 100644 index 00000000000..d83d66a33a8 --- /dev/null +++ b/tools/webidl-codegen/src/impl-generator.js @@ -0,0 +1,517 @@ +/** + * C++ implementation stub generator for JSG bindings from WebIDL + * Generates .c++ files with placeholder implementations for user override + */ + +import { TypeMapper } from './type-mapper.js'; +import { ProtectedRegions } from './protected-regions.js'; + +export class ImplGenerator { + constructor(options = {}) { + this.typeMapper = new TypeMapper(); + this.protectedRegions = null; // Set via setProtectedRegions() if preserving content + this.incrementalMode = false; // Set to true to only generate stubs for new methods + this.skipInterfaces = new Set(options.skipInterfaces || [ + 'Event', + 'EventTarget', + 'EventListener', + 'AbortSignal', + 'AbortController', + ]); + // Mixin tracking + this.mixins = new Map(); // name -> mixin definition + // Generation summary tracking + this.summary = { + implementations: [], + skipped: [], + }; + } + + /** + * Set protected regions from existing file + */ + setProtectedRegions(protectedRegions) { + this.protectedRegions = protectedRegions; + } + + /** + * Enable incremental mode - only generate stubs for new methods + */ + setIncrementalMode(enabled) { + this.incrementalMode = enabled; + } + + /** + * Get generation summary + */ + getSummary() { + return this.summary; + } + + /** + * Generate implementation stub file + */ + generate(definitions, options = {}) { + // Reset summary + this.summary = { + implementations: [], + skipped: [], + }; + + // Update type mapper with definitions so it can distinguish dictionaries from interfaces + this.typeMapper = new TypeMapper(definitions); + + // Process mixins + this.processMixins(definitions); + + const { namespace = 'workerd::api', headerFile } = options; + + let code = ''; + + // Header comment + code += `// Generated implementation stubs - EDIT THIS FILE\n`; + code += `// This file contains placeholder implementations.\n`; + code += `// Replace TODO comments with actual logic.\n\n`; + + // Include the generated header + if (headerFile) { + code += `#include "${headerFile}"\n`; + } else { + code += `// #include "generated.h" // Include your generated header\n`; + } + code += `\n`; + + // Additional includes that might be needed + code += `// Add additional includes as needed:\n`; + code += `// #include \n`; + code += `// #include \n`; + code += `\n`; + + // Namespace (use :: notation for nested namespaces) + code += `namespace ${namespace} {\n\n`; + + // Generate implementations for each definition + for (const def of definitions) { + // Skip interfaces/mixins that are already defined in workerd or explicitly excluded + if ((def.type === 'interface' || def.type === 'interface mixin') && this.skipInterfaces.has(def.name)) { + this.summary.skipped.push({ type: def.type === 'interface mixin' ? 'mixin' : 'interface', name: def.name }); + continue; + } + + if (def.type === 'interface' && !def.mixin) { + this.summary.implementations.push(def.name); + } + + code += this.generateDefinitionImpl(def); + code += '\n'; + } + + // Close namespace + code += `} // namespace ${namespace}\n`; + + return code; + } + + /** + * Process mixin definitions + */ + processMixins(definitions) { + for (const def of definitions) { + if (def.type === 'interface mixin') { + this.mixins.set(def.name, def); + } + } + } + + generateDefinitionImpl(def) { + switch (def.type) { + case 'interface': + return this.generateInterfaceImpl(def); + case 'interface mixin': + return this.generateMixinImpl(def); + case 'dictionary': + return this.generateDictionaryImpl(def); + case 'includes': + // Includes statements don't need implementation + return ''; + default: + return ``; // No implementation needed for enums, callbacks + } + } + + generateInterfaceImpl(iface) { + let code = ''; + + const operations = iface.members.filter(m => m.type === 'operation'); + const attributes = iface.members.filter(m => m.type === 'attribute'); + const constructor = iface.members.find(m => m.type === 'constructor'); + + // C++ constructor (always needed for jsg::Object instances) + code += this.generateCppConstructorImpl(iface.name); + code += '\n'; + + // JavaScript constructor (static method, only if WebIDL has constructor) + if (constructor) { + code += this.generateJsConstructorImpl(iface.name, constructor); + code += '\n'; + } + + // Group operations by name to handle overloads + const operationsByName = new Map(); + for (const op of operations) { + if (!op.name) continue; + if (!operationsByName.has(op.name)) { + operationsByName.set(op.name, []); + } + operationsByName.get(op.name).push(op); + } + + // Operation implementations + for (const [opName, overloads] of operationsByName) { + for (let i = 0; i < overloads.length; i++) { + const op = overloads[i]; + const compatFlag = this.getCompatFlag(op.extAttrs); + const customName = this.getMethodName(op.extAttrs); + + let suffix = ''; + if (customName) { + code += this.generateOperationImpl(iface.name, op, '', customName); + } else if (compatFlag) { + suffix = `_${compatFlag}`; + code += this.generateOperationImpl(iface.name, op, suffix); + } else if (i > 0) { + suffix = `_overload${i}`; + code += this.generateOperationImpl(iface.name, op, suffix); + } else { + code += this.generateOperationImpl(iface.name, op); + } + code += '\n'; + } + } + + // Attribute implementations + for (const attr of attributes) { + code += this.generateAttributeImpl(iface.name, attr); + code += '\n'; + } + + return code; + } + + generateCppConstructorImpl(className) { + const regionName = `${className}::constructor`; + + // In incremental mode, skip if already implemented + if (this.incrementalMode && this.protectedRegions && this.protectedRegions.hasRegion(regionName)) { + return this.protectedRegions.generateInlineRegion(regionName, ''); + } + + // Default content if no preserved version exists + const defaultContent = `${className}::${className}() { + // TODO: Initialize member variables +} +`; if (this.protectedRegions && this.protectedRegions.hasRegion(regionName)) { + // Use preserved implementation + return this.protectedRegions.generateInlineRegion(regionName, defaultContent); + } + + // Generate default with protected region markers + let code = ''; + code += `// BEGIN MANUAL SECTION: ${regionName} +`; + code += `// C++ constructor - add parameters as needed for your implementation +`; + code += defaultContent; + code += `// END MANUAL SECTION: ${regionName} +`; + return code; + } + + generateJsConstructorImpl(className, ctor) { + const regionName = `${className}::constructor(js)`; + + const params = ['jsg::Lock& js']; + const paramNames = ['js']; + + for (const arg of ctor.arguments) { + const type = this.typeMapper.mapParameter(arg); + params.push(`${type} ${arg.name}`); + paramNames.push(arg.name); + } + + // In incremental mode, skip if already implemented + if (this.incrementalMode && this.protectedRegions && this.protectedRegions.hasRegion(regionName)) { + return this.protectedRegions.generateInlineRegion(regionName, ''); + } + + // Default content if no preserved version exists + const defaultContent = `jsg::Ref<${className}> ${className}::constructor(${params.join(', ')}) { + // TODO: Implement JavaScript constructor + // Create and return a new instance using js.alloc + // The C++ constructor will be called automatically + return js.alloc<${className}>(); +} +`; + + if (this.protectedRegions && this.protectedRegions.hasRegion(regionName)) { + return this.protectedRegions.generateInlineRegion(regionName, defaultContent); + } + + let code = ''; + code += `// BEGIN MANUAL SECTION: ${regionName}\n`; + code += `// JavaScript constructor (static method for JS 'new' operator)\n`; + code += defaultContent; + code += `// END MANUAL SECTION: ${regionName}\n`; + return code; + } + + generateOperationImpl(className, op, suffix = '', customMethodName = null) { + if (op.special) { + return ''; // Skip special operations + } + + const name = op.name; + const returnType = this.typeMapper.mapType(op.idlType); + const methodName = customMethodName || (name + suffix); + const regionName = `${className}::${methodName}`; + + const params = ['jsg::Lock& js']; + const paramNames = ['js']; + + for (const arg of op.arguments) { + const type = this.typeMapper.mapParameter(arg); + params.push(`${type} ${arg.name}`); + paramNames.push(arg.name); + } + + // Build default implementation + let defaultContent = `${returnType} ${className}::${methodName}(${params.join(', ')}) {\n`; + defaultContent += ` // TODO: Implement ${methodName}\n`; + + // Add helpful comments based on return type + const returnTypeStr = this.typeMapper.mapTypeToString(op.idlType); + if (returnTypeStr.includes('Promise')) { + defaultContent += ` // Return a promise that resolves with the result\n`; + defaultContent += ` // Example: return js.resolvedPromise(...);\n`; + } else if (returnTypeStr === 'void' || returnTypeStr === 'undefined') { + defaultContent += ` // Perform the operation\n`; + } else if (returnTypeStr.includes('jsg::Ref')) { + defaultContent += ` // Create and return a new object reference\n`; + defaultContent += ` // Example: return js.alloc(...);\n`; + } else { + defaultContent += ` // Return the result\n`; + } + + // Generate a basic return statement + if (returnTypeStr === 'void' || returnTypeStr === 'undefined') { + // No return needed + } else if (returnTypeStr.includes('Promise')) { + defaultContent += ` return js.resolvedPromise();\n`; + } else if (returnTypeStr.includes('jsg::Ref')) { + // Extract the type from jsg::Ref + const match = returnTypeStr.match(/jsg::Ref<(.+)>/); + if (match) { + defaultContent += ` return js.alloc<${match[1]}>();\n`; + } else { + defaultContent += ` // return ...\n`; + } + } else if (returnTypeStr.includes('kj::String') || + returnTypeStr.includes('jsg::DOMString') || + returnTypeStr.includes('jsg::USVString') || + returnTypeStr.includes('jsg::ByteString')) { + defaultContent += ` return "TODO"_kj; // kj::StringPtr avoids allocation for string literals\n`; + defaultContent += ` // Note: Consider changing return type to kj::StringPtr in header if returning literals/fixed strings\n`; + } else if (returnTypeStr.includes('bool')) { + defaultContent += ` return false;\n`; + } else if (returnTypeStr.includes('int') || returnTypeStr.includes('double') || returnTypeStr.includes('float')) { + defaultContent += ` return 0;\n`; + } else if (returnTypeStr.includes('jsg::Optional')) { + defaultContent += ` return kj::none;\n`; + } else if (returnTypeStr.includes('kj::Maybe')) { + defaultContent += ` return kj::none;\n`; + } else if (returnTypeStr.includes('kj::Array')) { + defaultContent += ` return kj::Array<...>(); // TODO: Fill in array type\n`; + } else { + defaultContent += ` // return ...\n`; + } + + defaultContent += `}\n`; + + // Check if we have a preserved implementation + if (this.protectedRegions && this.protectedRegions.hasRegion(regionName)) { + return this.protectedRegions.generateInlineRegion(regionName, defaultContent); + } + + // Generate with protected region markers + let code = ''; + code += `// BEGIN MANUAL SECTION: ${regionName}\n`; + code += defaultContent; + code += `// END MANUAL SECTION: ${regionName}\n`; + + return code; + } + + generateAttributeImpl(className, attr) { + const type = this.typeMapper.mapType(attr.idlType); + const typeStr = this.typeMapper.mapTypeToString(attr.idlType); + const getterName = this.makeGetterName(attr.name); + const getterRegionName = `${className}::${getterName}`; + + // Build getter default content + let getterContent = `${type} ${className}::${getterName}(jsg::Lock& js) {\n`; + getterContent += ` // TODO: Implement getter for ${attr.name}\n`; + + // Generate a basic return statement based on type + if (typeStr.includes('kj::String') || + typeStr.includes('jsg::DOMString') || + typeStr.includes('jsg::USVString') || + typeStr.includes('jsg::ByteString')) { + getterContent += ` return "TODO"_kj; // kj::StringPtr avoids allocation for string literals\n`; + getterContent += ` // Note: Consider changing return type to kj::StringPtr in header if returning literals/fixed strings\n`; + } else if (typeStr.includes('bool')) { + getterContent += ` return false;\n`; + } else if (typeStr.includes('int') || typeStr.includes('double') || typeStr.includes('float')) { + getterContent += ` return 0;\n`; + } else if (typeStr.includes('jsg::Optional')) { + getterContent += ` return kj::none;\n`; + } else if (typeStr.includes('kj::Maybe')) { + getterContent += ` return kj::none;\n`; + } else if (typeStr.includes('jsg::Ref')) { + const match = typeStr.match(/jsg::Ref<(.+)>/); + if (match) { + getterContent += ` return js.alloc<${match[1]}>();\n`; + } else { + getterContent += ` // return ...\n`; + } + } else { + getterContent += ` // return ...\n`; + } + + getterContent += `}\n`; + + // Generate getter with protected region + let code = ''; + // In incremental mode, skip if already implemented + if (this.incrementalMode && this.protectedRegions && this.protectedRegions.hasRegion(getterRegionName)) { + code += this.protectedRegions.generateInlineRegion(getterRegionName, ''); + } else if (this.protectedRegions && this.protectedRegions.hasRegion(getterRegionName)) { + code += this.protectedRegions.generateInlineRegion(getterRegionName, getterContent); + } else { + code += `// BEGIN MANUAL SECTION: ${getterRegionName}\n`; + code += getterContent; + code += `// END MANUAL SECTION: ${getterRegionName}\n`; + } + + // Setter implementation (if not readonly) + if (!attr.readonly) { + const setterName = this.makeSetterName(attr.name); + const setterRegionName = `${className}::${setterName}`; + + let setterContent = `void ${className}::${setterName}(jsg::Lock& js, ${type} value) {\n`; + setterContent += ` // TODO: Implement setter for ${attr.name}\n`; + setterContent += ` // Store the value in your class member\n`; + setterContent += `}\n`; + + code += `\n`; + // In incremental mode, skip if already implemented + if (this.incrementalMode && this.protectedRegions && this.protectedRegions.hasRegion(setterRegionName)) { + code += this.protectedRegions.generateInlineRegion(setterRegionName, ''); + } else if (this.protectedRegions && this.protectedRegions.hasRegion(setterRegionName)) { + code += this.protectedRegions.generateInlineRegion(setterRegionName, setterContent); + } else { + code += `// BEGIN MANUAL SECTION: ${setterRegionName}\n`; + code += setterContent; + code += `// END MANUAL SECTION: ${setterRegionName}\n`; + } + } + + return code; + } + + generateMixinImpl(mixin) { + let code = ''; + + code += `// Mixin implementation: ${mixin.name}\n`; + code += `// These methods will be inherited by interfaces that include this mixin\n\n`; + + const operations = mixin.members.filter(m => m.type === 'operation'); + const attributes = mixin.members.filter(m => m.type === 'attribute'); + + // Generate operations + for (const op of operations) { + if (!op.name) continue; + code += this.generateOperationImpl(mixin.name, op); + code += '\n'; + } + + // Generate attribute getters/setters + for (const attr of attributes) { + code += this.generateAttributeImpl(mixin.name, attr); + code += '\n'; + } + + return code; + } + + generateDictionaryImpl(dict) { + let code = ''; + + // Check if there's custom code that might need implementations + const customCode = this.getJsgCode(dict.extAttrs); + if (!customCode) { + return ''; // No custom methods to implement + } + + code += `// ${dict.name} custom method implementations\n`; + code += `// Add implementations for methods declared in [JsgCode]\n`; + code += `\n`; + + // We can't easily parse the custom code, so just add a comment + code += `// Example:\n`; + code += `// void ${dict.name}::validate(jsg::Lock& js) {\n`; + code += `// // Validate the struct members\n`; + code += `// JSG_REQUIRE(!someField.empty(), TypeError, "someField cannot be empty");\n`; + code += `// }\n`; + code += `\n`; + + return code; + } + + makeGetterName(attrName) { + return 'get' + attrName.charAt(0).toUpperCase() + attrName.slice(1); + } + + makeSetterName(attrName) { + return 'set' + attrName.charAt(0).toUpperCase() + attrName.slice(1); + } + + getCompatFlag(extAttrs) { + if (!extAttrs) return null; + const compatAttr = extAttrs.find(attr => attr.name === 'JsgCompatFlag'); + if (!compatAttr) return null; + if (compatAttr.rhs) { + return compatAttr.rhs.value || compatAttr.rhs; + } + return null; + } + + getMethodName(extAttrs) { + if (!extAttrs) return null; + const methodAttr = extAttrs.find(attr => attr.name === 'JsgMethodName'); + if (!methodAttr) return null; + if (methodAttr.rhs) { + return methodAttr.rhs.value || methodAttr.rhs; + } + return null; + } + + getJsgCode(extAttrs) { + if (!extAttrs) return null; + const jsgAttr = extAttrs.find(attr => attr.name === 'JsgCode'); + if (!jsgAttr) return null; + if (jsgAttr.rhs) { + return jsgAttr.rhs.value || jsgAttr.rhs; + } + return null; + } +} diff --git a/tools/webidl-codegen/src/index.js b/tools/webidl-codegen/src/index.js new file mode 100644 index 00000000000..73dcf5d8c04 --- /dev/null +++ b/tools/webidl-codegen/src/index.js @@ -0,0 +1,7 @@ +/** + * Main entry point for WebIDL to JSG code generator + */ + +export { parseWebIDL, validateForJSG } from './parser.js'; +export { CppGenerator } from './generator.js'; +export { TypeMapper } from './type-mapper.js'; diff --git a/tools/webidl-codegen/src/parser.js b/tools/webidl-codegen/src/parser.js new file mode 100644 index 00000000000..6967359288e --- /dev/null +++ b/tools/webidl-codegen/src/parser.js @@ -0,0 +1,66 @@ +/** + * WebIDL parser using webidl2 library + */ + +import * as WebIDL2 from 'webidl2'; + +/** + * Parse WebIDL text into an AST + * @param {string} idlText - WebIDL source text + * @returns {Array} Parsed WebIDL definitions + */ +export function parseWebIDL(idlText) { + try { + return WebIDL2.parse(idlText); + } catch (error) { + // Enhance error message + if (error.line !== undefined) { + throw new Error( + `WebIDL parse error at line ${error.line}:${error.column}: ${error.message}` + ); + } + throw error; + } +} + +/** + * Validate WebIDL definitions for JSG compatibility + * @param {Array} definitions - Parsed WebIDL definitions + */ +export function validateForJSG(definitions) { + const errors = []; + + for (const def of definitions) { + if (def.type === 'interface') { + validateInterface(def, errors); + } + } + + if (errors.length > 0) { + throw new Error( + 'WebIDL validation failed:\n' + errors.map(e => ` - ${e}`).join('\n') + ); + } +} + +function validateInterface(iface, errors) { + // Check for unsupported features + if (iface.partial) { + errors.push(`Partial interfaces not yet supported: ${iface.name}`); + } + + if (iface.includes && iface.includes.length > 0) { + errors.push(`Mixins not yet supported in interface: ${iface.name}`); + } + + // Validate members + for (const member of iface.members) { + if (member.type === 'iterable') { + errors.push(`Iterables not yet supported in interface: ${iface.name}`); + } + + if (member.type === 'maplike' || member.type === 'setlike') { + errors.push(`Maplike/setlike not yet supported in interface: ${iface.name}`); + } + } +} diff --git a/tools/webidl-codegen/src/protected-regions.js b/tools/webidl-codegen/src/protected-regions.js new file mode 100644 index 00000000000..1c01fa758da --- /dev/null +++ b/tools/webidl-codegen/src/protected-regions.js @@ -0,0 +1,127 @@ +/** + * Protected regions parser - preserves manual code sections during regeneration + */ + +import fs from 'fs'; + +export class ProtectedRegions { + constructor() { + this.regions = new Map(); + } + + /** + * Parse an existing file and extract protected regions + */ + parseFile(filePath) { + if (!fs.existsSync(filePath)) { + return; // File doesn't exist yet, nothing to preserve + } + + const content = fs.readFileSync(filePath, 'utf-8'); + this.parseContent(content); + } + + /** + * Parse content and extract protected regions + */ + parseContent(content) { + const lines = content.split('\n'); + let currentRegion = null; + let regionContent = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check for BEGIN marker + const beginMatch = line.match(/\/\/\s*BEGIN\s+MANUAL\s+SECTION:\s*(.+)/); + if (beginMatch) { + currentRegion = beginMatch[1].trim(); + regionContent = []; + continue; + } + + // Check for END marker + const endMatch = line.match(/\/\/\s*END\s+MANUAL\s+SECTION:\s*(.+)/); + if (endMatch && currentRegion) { + const regionName = endMatch[1].trim(); + if (regionName === currentRegion) { + this.regions.set(currentRegion, regionContent.join('\n')); + currentRegion = null; + regionContent = []; + } + continue; + } + + // Collect content within region + if (currentRegion) { + regionContent.push(line); + } + } + } + + /** + * Check if a region exists (has preserved content) + */ + hasRegion(name) { + return this.regions.has(name); + } + + /** + * Get preserved content for a region + */ + getRegion(name) { + return this.regions.get(name) || ''; + } + + /** + * Generate a protected region block + */ + generateRegion(name, defaultContent = '') { + let code = ''; + code += ` // BEGIN MANUAL SECTION: ${name}\n`; + + if (this.hasRegion(name)) { + // Use preserved content + code += this.getRegion(name); + if (!code.endsWith('\n')) { + code += '\n'; + } + } else { + // Use default content + if (defaultContent) { + code += defaultContent; + if (!code.endsWith('\n')) { + code += '\n'; + } + } + } + + code += ` // END MANUAL SECTION: ${name}\n`; + return code; + } + + /** + * Generate inline protected region (no indentation) + */ + generateInlineRegion(name, defaultContent = '') { + let code = ''; + code += `// BEGIN MANUAL SECTION: ${name}\n`; + + if (this.hasRegion(name)) { + code += this.getRegion(name); + if (!code.endsWith('\n')) { + code += '\n'; + } + } else { + if (defaultContent) { + code += defaultContent; + if (!code.endsWith('\n')) { + code += '\n'; + } + } + } + + code += `// END MANUAL SECTION: ${name}\n`; + return code; + } +} diff --git a/tools/webidl-codegen/src/test.js b/tools/webidl-codegen/src/test.js new file mode 100644 index 00000000000..661e1062d59 --- /dev/null +++ b/tools/webidl-codegen/src/test.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +/** + * Test runner for WebIDL code generator + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { parseWebIDL } from './parser.js'; +import { CppGenerator } from './generator.js'; +import { ImplGenerator } from './impl-generator.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const examplesDir = path.join(__dirname, '..', 'examples'); + +function runTest(name, idlFile, testImpl = false) { + console.log(`\n${'='.repeat(60)}`); + console.log(`Testing: ${name}`); + console.log('='.repeat(60)); + + try { + const idlPath = path.join(examplesDir, idlFile); + const idlContent = fs.readFileSync(idlPath, 'utf-8'); + + console.log('\nInput WebIDL:'); + console.log('-'.repeat(60)); + console.log(idlContent); + + const definitions = parseWebIDL(idlContent); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { + namespace: 'workerd::api', + filename: idlFile.replace('.webidl', ''), + }); + + console.log('\nGenerated C++ Header:'); + console.log('-'.repeat(60)); + console.log(code); + + if (testImpl) { + const implGenerator = new ImplGenerator(); + const implCode = implGenerator.generate(definitions, { + namespace: 'workerd::api', + headerFile: idlFile.replace('.webidl', '.h'), + }); + + console.log('\nGenerated C++ Implementation Stubs:'); + console.log('-'.repeat(60)); + console.log(implCode); + } + + console.log(`\n✓ ${name} passed`); + return true; + } catch (error) { + console.error(`\n✗ ${name} failed:`); + console.error(` ${error.message}`); + if (process.env.DEBUG) { + console.error(error.stack); + } + return false; + } +} + +function main() { + console.log('WebIDL to JSG Code Generator - Test Suite'); + + const tests = [ + ['TextEncoder (Simple Interface)', 'text-encoder.webidl', false], + ['WritableStreamDefaultWriter (Complex Interface)', 'writable-stream.webidl', false], + ['Dictionaries and Callbacks', 'example-dict.webidl', false], + ['Simple Calculator (with Implementation Stubs)', 'simple.webidl', true], + ]; + + let passed = 0; + let failed = 0; + + for (const [name, file, testImpl] of tests) { + if (runTest(name, file, testImpl)) { + passed++; + } else { + failed++; + } + } + + console.log('\n' + '='.repeat(60)); + console.log('Test Summary'); + console.log('='.repeat(60)); + console.log(`Passed: ${passed}`); + console.log(`Failed: ${failed}`); + console.log(`Total: ${passed + failed}`); + + process.exit(failed > 0 ? 1 : 0); +} + +main(); diff --git a/tools/webidl-codegen/src/type-mapper.js b/tools/webidl-codegen/src/type-mapper.js new file mode 100644 index 00000000000..664d001be65 --- /dev/null +++ b/tools/webidl-codegen/src/type-mapper.js @@ -0,0 +1,256 @@ +/** + * Maps WebIDL types to JSG C++ types + */ + +export class TypeMapper { + constructor(definitions = []) { + // Build a set of dictionary names so we know which types are structs vs objects + this.dictionaries = new Set( + definitions.filter(def => def.type === 'dictionary').map(def => def.name) + ); + + // Build a set of typedef names so we don't wrap them in jsg::Ref + this.typedefs = new Set( + definitions.filter(def => def.type === 'typedef').map(def => def.name) + ); + + // Build a set of enum names so we don't wrap them in jsg::Ref + this.enums = new Set( + definitions.filter(def => def.type === 'enum').map(def => def.name) + ); + + // Known external enum types from other specs that shouldn't be wrapped + this.externalEnums = new Set([ + 'ReferrerPolicy', // From Referrer Policy spec + ]); + } + + /** + * Convert a WebIDL type to JSG C++ type + * @param {object} idlType - WebIDL type object from webidl2 + * @returns {string} C++ type string + */ + mapType(idlType) { + if (!idlType) { + return 'void'; + } + + // If idlType is a string, map it directly + if (typeof idlType === 'string') { + return this.mapSimpleType(idlType); + } + + // Handle nullable types (T?) + if (idlType.nullable) { + // Create a copy without nullable flag for recursive mapping + // Note: Can't use spread operator because idlType.idlType might be non-enumerable + const innerType = { + type: idlType.type, + extAttrs: idlType.extAttrs, + generic: idlType.generic, + nullable: false, + union: idlType.union, + idlType: idlType.idlType + }; + const mappedInner = this.mapType(innerType); + return `kj::Maybe<${mappedInner}>`; + } + + // Handle union types + if (idlType.union) { + const types = idlType.idlType.map(t => this.mapType(t)); + return `kj::OneOf<${types.join(', ')}>`; + } + + // Handle generic types (Promise, sequence, FrozenArray, record) + if (idlType.generic) { + return this.mapGenericType(idlType); + } + + // Handle primitive and interface types + return this.mapSimpleType(idlType.idlType); + } + + /** + * Map generic types like Promise, sequence, etc. + */ + mapGenericType(idlType) { + const generic = idlType.generic; + const args = idlType.idlType; + + switch (generic) { + case 'Promise': + return `jsg::Promise<${this.mapType(args[0])}>`; + + case 'sequence': + return `kj::Array<${this.mapType(args[0])}>`; + + case 'FrozenArray': + return `kj::Array`; + + case 'record': { + const keyType = this.mapType(args[0]); + const valueType = this.mapType(args[1]); + return `jsg::Dict<${keyType}, ${valueType}>`; + } + + default: + throw new Error(`Unknown generic type: ${generic}`); + } + } + + /** + * Map simple (non-generic) types + */ + mapSimpleType(typeName) { + const typeMap = { + // Primitive types + 'undefined': 'void', + 'boolean': 'bool', + 'byte': 'int8_t', + 'octet': 'uint8_t', + 'short': 'int16_t', + 'unsigned short': 'uint16_t', + 'long': 'int32_t', + 'unsigned long': 'uint32_t', + 'long long': 'int64_t', + 'unsigned long long': 'uint64_t', + 'float': 'float', + 'double': 'double', + 'unrestricted double': 'double', + 'unrestricted float': 'float', + + // String types + 'DOMString': 'jsg::DOMString', + 'ByteString': 'jsg::ByteString', + 'USVString': 'jsg::USVString', + + // Special types + 'any': 'jsg::JsValue', + 'object': 'jsg::JsObject', + 'SelfRef': 'jsg::SelfRef', + 'BufferSource': 'jsg::BufferSource', + 'ArrayBuffer': 'jsg::BufferSource', + 'DataView': 'jsg::BufferSource', + 'Int8Array': 'jsg::BufferSource', + 'Uint8Array': 'jsg::BufferSource', + 'Uint8ClampedArray': 'jsg::BufferSource', + 'Int16Array': 'jsg::BufferSource', + 'Uint16Array': 'jsg::BufferSource', + 'Int32Array': 'jsg::BufferSource', + 'Uint32Array': 'jsg::BufferSource', + 'Float32Array': 'jsg::BufferSource', + 'Float64Array': 'jsg::BufferSource', + 'BigInt64Array': 'jsg::BufferSource', + 'BigUint64Array': 'jsg::BufferSource', + }; + + if (typeMap[typeName]) { + return typeMap[typeName]; + } + + // Check if this is a dictionary (struct) - if so, use directly + if (this.dictionaries.has(typeName)) { + return typeName; + } + + // Check if this is a typedef - if so, use directly + if (this.typedefs.has(typeName)) { + return typeName; + } + + // Check if this is an enum - if so, use directly + if (this.enums.has(typeName)) { + return typeName; + } + + // Check if this is a known external enum - if so, use directly + if (this.externalEnums.has(typeName)) { + return typeName; + } + + // Otherwise assume it's an interface type - wrap in jsg::Ref + return `jsg::Ref<${typeName}>`; + } + + /** + * Map function parameter, handling optional parameters + */ + mapParameter(param) { + const baseType = this.mapType(param.idlType); + + if (param.optional) { + return `jsg::Optional<${baseType}>`; + } + + return baseType; + } + + /** + * Map a dictionary member type, handling optional and nullable + * Rules: + * - optional (can be undefined or T) -> jsg::Optional + * - nullable (can be null or T) -> kj::Maybe + * - optional + nullable (can be undefined, null, or T) -> jsg::Optional> + * @param {object} member - Dictionary member object from webidl2 + * @returns {string} C++ type string + */ + mapDictionaryMemberType(member) { + const isOptional = member.required === false; // WebIDL members are optional by default + const isNullable = member.idlType.nullable; + + // Get base type without nullable wrapper (we'll add it back correctly) + // Note: Can't use spread operator because idlType.idlType is a non-enumerable property + const baseIdlType = isNullable + ? { ...member.idlType, nullable: false, idlType: member.idlType.idlType } + : member.idlType; + const baseType = this.mapType(baseIdlType); + + // Special case: SelfRef is never wrapped in Optional or Maybe + // It's always jsg::SelfRef directly, as seen in workerd's api/basics.h and api/global-scope.h + const rawType = baseIdlType.idlType || baseIdlType; + if (rawType === 'SelfRef') { + return baseType; + } + + // Both optional and nullable: jsg::Optional> + if (isOptional && isNullable) { + return `jsg::Optional>`; + } + + // Just nullable: kj::Maybe + if (isNullable) { + return `kj::Maybe<${baseType}>`; + } + + // Just optional: jsg::Optional + if (isOptional) { + return `jsg::Optional<${baseType}>`; + } + + // Required (neither optional nor nullable): T + return baseType; + } + + /** + * Check if a type is a primitive value type (not passed by reference) + */ + isPrimitiveType(cppType) { + const primitives = [ + 'void', 'bool', + 'int8_t', 'uint8_t', 'int16_t', 'uint16_t', + 'int32_t', 'uint32_t', 'int64_t', 'uint64_t', + 'float', 'double' + ]; + + return primitives.some(p => cppType.startsWith(p)); + } + + /** + * Convert a WebIDL type to a string representation (for stub generation) + * This is similar to mapType but returns the string version + */ + mapTypeToString(idlType) { + return this.mapType(idlType); + } +} diff --git a/tools/webidl-codegen/test-incremental.webidl b/tools/webidl-codegen/test-incremental.webidl new file mode 100644 index 00000000000..9c39dfd72b8 --- /dev/null +++ b/tools/webidl-codegen/test-incremental.webidl @@ -0,0 +1,6 @@ +interface MyAPI { + constructor(); + DOMString getName(); + DOMString getVersion(); + long getCount(); +}; diff --git a/tools/webidl-codegen/test-string.webidl b/tools/webidl-codegen/test-string.webidl new file mode 100644 index 00000000000..ceced013ac5 --- /dev/null +++ b/tools/webidl-codegen/test-string.webidl @@ -0,0 +1,5 @@ +interface StringTest { + constructor(); + DOMString getName(); + attribute DOMString label; +}; diff --git a/tools/webidl-codegen/tests/test-attributes.js b/tools/webidl-codegen/tests/test-attributes.js new file mode 100644 index 00000000000..8964bfca6d5 --- /dev/null +++ b/tools/webidl-codegen/tests/test-attributes.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +/** + * Test all extended attributes (Jsg-prefixed) + */ + +import { parseWebIDL } from '../src/parser.js'; +import { CppGenerator } from '../src/generator.js'; +import { ImplGenerator } from '../src/impl-generator.js'; +import { TestRunner, assertIncludes, assertNotIncludes } from './test-helpers.js'; + +const runner = new TestRunner('Extended Attributes Tests'); + +// JsgCompatFlag +runner.test('[JsgCompatFlag] generates runtime compat flag check', () => { + const webidl = ` + [Exposed=*] + interface API { + [JsgCompatFlag=experimental_feature] undefined newMethod(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'void newMethod(jsg::Lock& js);', 'Should have method'); + assertIncludes(code, 'JSG_RESOURCE_TYPE(API, CompatibilityFlags::Reader flags)', 'Should have flags parameter'); + assertIncludes(code, 'if (flags.getexperimental_feature())', 'Should have runtime check'); + assertIncludes(code, 'JSG_METHOD(newMethod);', 'Should register method'); +}); + +runner.test('[JsgCompatFlag] on attribute generates runtime check', () => { + const webidl = ` + [Exposed=*] + interface API { + [JsgCompatFlag=experimental_feature] readonly attribute DOMString value; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::DOMString getValue(jsg::Lock& js);'); + assertIncludes(code, 'if (flags.getexperimental_feature())'); + assertIncludes(code, 'JSG_READONLY_PROTOTYPE_PROPERTY(value, getValue);'); +}); + +// JsgMethodName +runner.test('[JsgMethodName] changes C++ method name', () => { + const webidl = ` + [Exposed=*] + interface API { + [JsgMethodName=customMethod] undefined jsMethod(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'void customMethod(jsg::Lock& js);', 'Should use custom method name'); + assertIncludes(code, 'JSG_METHOD_NAMED(jsMethod, customMethod);', 'Should register with JSG_METHOD_NAMED'); + assertNotIncludes(code, 'void jsMethod(jsg::Lock& js);', 'Should not use original name'); +}); + +// JsgPropertyScope +runner.test('[JsgPropertyScope=instance] generates JSG_INSTANCE_PROPERTY', () => { + const webidl = ` + [Exposed=*] + interface API { + [JsgPropertyScope=instance] readonly attribute DOMString instanceProp; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'JSG_READONLY_INSTANCE_PROPERTY(instanceProp, getInstanceProp);'); + assertNotIncludes(code, 'JSG_READONLY_PROTOTYPE_PROPERTY'); +}); + +runner.test('No [JsgPropertyScope] defaults to prototype', () => { + const webidl = ` + [Exposed=*] + interface API { + readonly attribute DOMString defaultProp; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'JSG_READONLY_PROTOTYPE_PROPERTY(defaultProp, getDefaultProp);'); +}); + +// JsgInternal +runner.test('[JsgInternal] excludes field from JSG_STRUCT', () => { + const webidl = ` + dictionary Options { + DOMString publicField; + [JsgInternal] DOMString internalField; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'options' }); + + assertIncludes(code, 'jsg::Optional publicField;'); + assertIncludes(code, 'jsg::Optional internalField;'); + assertIncludes(code, 'JSG_STRUCT(publicField);', 'Should only include publicField in JSG_STRUCT'); + assertNotIncludes(code, 'JSG_STRUCT(publicField, internalField);', 'Should not include internalField'); +}); + +const results = await runner.run(); +const success = runner.summary(); + +process.exit(success ? 0 : 1); diff --git a/tools/webidl-codegen/tests/test-examples.js b/tools/webidl-codegen/tests/test-examples.js new file mode 100644 index 00000000000..744dadc103b --- /dev/null +++ b/tools/webidl-codegen/tests/test-examples.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +/** + * Test all example WebIDL files + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { parseWebIDL } from '../src/parser.js'; +import { CppGenerator } from '../src/generator.js'; +import { ImplGenerator } from '../src/impl-generator.js'; +import { TestRunner, assert, assertIncludes } from './test-helpers.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const examplesDir = path.join(__dirname, '..', 'examples'); + +const runner = new TestRunner('Example Files Generation Tests'); + +// Get all example files +const exampleFiles = fs.readdirSync(examplesDir) + .filter(f => f.endsWith('.webidl')) + .sort(); + +console.log(`Found ${exampleFiles.length} example files to test`); + +for (const file of exampleFiles) { + runner.test(`${file} - Header generation`, () => { + const idlPath = path.join(examplesDir, file); + const idlContent = fs.readFileSync(idlPath, 'utf-8'); + const definitions = parseWebIDL(idlContent); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { + namespace: 'workerd::api', + filename: file.replace('.webidl', ''), + }); + + assert(code.length > 0, 'Generated code should not be empty'); + assertIncludes(code, '#pragma once', 'Should have pragma once'); + assertIncludes(code, 'namespace workerd::api', 'Should have namespace'); + assertIncludes(code, '} // namespace workerd::api', 'Should close namespace'); + }); + + runner.test(`${file} - Implementation generation`, () => { + const idlPath = path.join(examplesDir, file); + const idlContent = fs.readFileSync(idlPath, 'utf-8'); + const definitions = parseWebIDL(idlContent); + const implGenerator = new ImplGenerator(); + const code = implGenerator.generate(definitions, { + namespace: 'workerd::api', + headerFile: file.replace('.webidl', '.h'), + }); + + assert(code.length > 0, 'Generated implementation should not be empty'); + assertIncludes(code, 'namespace workerd::api', 'Should have namespace'); + assertIncludes(code, '} // namespace workerd::api', 'Should close namespace'); + }); + + runner.test(`${file} - Summary generation`, () => { + const idlPath = path.join(examplesDir, file); + const idlContent = fs.readFileSync(idlPath, 'utf-8'); + const definitions = parseWebIDL(idlContent); + const generator = new CppGenerator(); + generator.generate(definitions, { + namespace: 'workerd::api', + filename: file.replace('.webidl', ''), + }); + + const summary = generator.getSummary(); + assert(summary, 'Should have summary'); + assert(Array.isArray(summary.interfaces), 'Summary should have interfaces array'); + assert(Array.isArray(summary.mixins), 'Summary should have mixins array'); + assert(Array.isArray(summary.dictionaries), 'Summary should have dictionaries array'); + assert(Array.isArray(summary.enums), 'Summary should have enums array'); + assert(Array.isArray(summary.typedefs), 'Summary should have typedefs array'); + assert(Array.isArray(summary.callbacks), 'Summary should have callbacks array'); + assert(Array.isArray(summary.skipped), 'Summary should have skipped array'); + assert(Array.isArray(summary.unsupported), 'Summary should have unsupported array'); + }); +} + +const results = await runner.run(); +const success = runner.summary(); + +process.exit(success ? 0 : 1); diff --git a/tools/webidl-codegen/tests/test-generation.js b/tools/webidl-codegen/tests/test-generation.js new file mode 100644 index 00000000000..f1dc122c7df --- /dev/null +++ b/tools/webidl-codegen/tests/test-generation.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +/** + * Test code generation correctness + */ + +import { parseWebIDL } from '../src/parser.js'; +import { CppGenerator } from '../src/generator.js'; +import { ImplGenerator } from '../src/impl-generator.js'; +import { TestRunner, assert, assertIncludes, assertNotIncludes, assertMatches } from './test-helpers.js'; + +const runner = new TestRunner('Code Generation Tests'); + +runner.test('Simple interface generates correct class', () => { + const webidl = ` + [Exposed=*] + interface Calculator { + long add(long a, long b); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'calc' }); + + assertIncludes(code, 'class Calculator: public jsg::Object', 'Should generate class'); + assertIncludes(code, 'int32_t add(jsg::Lock& js, int32_t a, int32_t b);', 'Should generate method'); + assertIncludes(code, 'JSG_RESOURCE_TYPE(Calculator)', 'Should have JSG_RESOURCE_TYPE'); + assertIncludes(code, 'JSG_METHOD(add);', 'Should register method'); +}); + +runner.test('Interface with constructor generates static constructor', () => { + const webidl = ` + [Exposed=*] + interface Point { + constructor(long x, long y); + readonly attribute long x; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'point' }); + + assertIncludes(code, 'static jsg::Ref constructor(jsg::Lock& js, int32_t x, int32_t y);', 'Should generate static constructor'); + assertIncludes(code, 'JSG_RESOURCE_TYPE(Point)', 'Should have JSG_RESOURCE_TYPE'); +});runner.test('Dictionary generates struct with JSG_STRUCT', () => { + const webidl = ` + dictionary Point { + long x; + long y; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'point' }); + + assertIncludes(code, 'struct Point {', 'Should generate struct'); + assertIncludes(code, 'jsg::Optional x;', 'Should have x field'); + assertIncludes(code, 'jsg::Optional y;', 'Should have y field'); + assertIncludes(code, 'JSG_STRUCT(x, y);', 'Should have JSG_STRUCT macro'); +}); + +runner.test('Enum generates enum class', () => { + const webidl = ` + enum Color { "red", "green", "blue" }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'color' }); + + assertIncludes(code, 'enum class Color {', 'Should generate enum class'); + assertIncludes(code, 'RED,', 'Should have RED value'); + assertIncludes(code, 'GREEN,', 'Should have GREEN value'); + assertIncludes(code, 'BLUE,', 'Should have BLUE value'); +}); + +runner.test('Typedef generates using declaration', () => { + const webidl = ` + typedef (DOMString or long) StringOrNumber; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'types' }); + + assertIncludes(code, 'using StringOrNumber = kj::OneOf;', 'Should generate using declaration'); +}); + +runner.test('Interface mixin generates plain class', () => { + const webidl = ` + interface mixin Body { + readonly attribute boolean bodyUsed; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'body' }); + + assertIncludes(code, 'class Body {', 'Should generate class'); + assertNotIncludes(code, ': public jsg::Object', 'Mixin should not inherit from jsg::Object'); + assertIncludes(code, 'bool getBodyUsed(jsg::Lock& js);', 'Should have getter'); +}); + +runner.test('Interface with includes inherits from mixin', () => { + const webidl = ` + interface mixin Body { + readonly attribute boolean bodyUsed; + }; + + [Exposed=*] + interface Request {}; + Request includes Body; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'request' }); + + assertIncludes(code, 'class Request: public jsg::Object, public Body {', 'Should inherit from jsg::Object and Body'); + assertIncludes(code, 'JSG_READONLY_PROTOTYPE_PROPERTY(bodyUsed, getBodyUsed);', 'Should register mixin property'); +}); + +runner.test('Optional parameters generate jsg::Optional', () => { + const webidl = ` + [Exposed=*] + interface API { + undefined doSomething(DOMString required, optional DOMString opt); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'void doSomething(jsg::Lock& js, jsg::DOMString required, jsg::Optional opt);'); +}); + +runner.test('Nullable types generate kj::Maybe', () => { + const webidl = ` + [Exposed=*] + interface API { + attribute DOMString? nullableString; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::Maybe getNullableString(jsg::Lock& js);'); + assertIncludes(code, 'void setNullableString(jsg::Lock& js, kj::Maybe value);'); +}); + +runner.test('Promise return type generates jsg::Promise', () => { + const webidl = ` + [Exposed=*] + interface API { + Promise fetchData(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Promise fetchData(jsg::Lock& js);'); +}); + +runner.test('Sequence type generates kj::Array', () => { + const webidl = ` + [Exposed=*] + interface API { + sequence getItems(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::Array getItems(jsg::Lock& js);'); +}); + +runner.test('Record type generates jsg::Dict', () => { + const webidl = ` + [Exposed=*] + interface API { + record getMap(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Dict getMap(jsg::Lock& js);'); +}); + +runner.test('Forward declarations generated for local interfaces', () => { + const webidl = ` + typedef (Request or DOMString) RequestInfo; + + [Exposed=*] + interface Request { + constructor(RequestInfo input); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'request' }); + + // Forward declaration should come before typedef + const forwardDeclPos = code.indexOf('class Request;'); + const typedefPos = code.indexOf('using RequestInfo'); + + assert(forwardDeclPos > 0, 'Should have forward declaration'); + assert(typedefPos > 0, 'Should have typedef'); + assert(forwardDeclPos < typedefPos, 'Forward declaration should come before typedef'); +}); + +runner.test('Implementation stubs include TODO comments', () => { + const webidl = ` + [Exposed=*] + interface Calculator { + long add(long a, long b); + }; + `; + + const definitions = parseWebIDL(webidl); + const implGenerator = new ImplGenerator(); + const code = implGenerator.generate(definitions, { + namespace: 'test', + headerFile: 'calc.h', + }); + + assertIncludes(code, '// TODO: Implement add', 'Should have TODO comment'); + assertIncludes(code, 'return 0;', 'Should have placeholder return'); +}); + +runner.test('String return type generates kj::StringPtr suggestion', () => { + const webidl = ` + [Exposed=*] + interface API { + DOMString getMessage(); + }; + `; + + const definitions = parseWebIDL(webidl); + const implGenerator = new ImplGenerator(); + const code = implGenerator.generate(definitions, { + namespace: 'test', + headerFile: 'api.h', + }); + + assertIncludes(code, 'return "TODO"_kj;', 'Should suggest kj::StringPtr'); + assertIncludes(code, 'kj::StringPtr', 'Should mention kj::StringPtr optimization'); +}); + +const results = await runner.run(); +const success = runner.summary(); + +process.exit(success ? 0 : 1); diff --git a/tools/webidl-codegen/tests/test-helpers.js b/tools/webidl-codegen/tests/test-helpers.js new file mode 100644 index 00000000000..c26b04fc771 --- /dev/null +++ b/tools/webidl-codegen/tests/test-helpers.js @@ -0,0 +1,96 @@ +/** + * Test helpers and utilities + */ + +export class TestRunner { + constructor(name) { + this.name = name; + this.passed = 0; + this.failed = 0; + this.tests = []; + } + + test(description, fn) { + this.tests.push({ description, fn }); + } + + async run() { + console.log(`\n${'='.repeat(70)}`); + console.log(`${this.name}`); + console.log('='.repeat(70)); + + for (const { description, fn } of this.tests) { + try { + await fn(); + console.log(` ✓ ${description}`); + this.passed++; + } catch (error) { + console.log(` ✗ ${description}`); + console.log(` Error: ${error.message}`); + if (process.env.DEBUG) { + console.log(error.stack); + } + this.failed++; + } + } + + return { passed: this.passed, failed: this.failed }; + } + + summary() { + console.log(`\n${'-'.repeat(70)}`); + console.log(`Results: ${this.passed} passed, ${this.failed} failed`); + return this.failed === 0; + } +} + +export function assert(condition, message) { + if (!condition) { + throw new Error(message || 'Assertion failed'); + } +} + +export function assertEqual(actual, expected, message) { + if (actual !== expected) { + throw new Error( + message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` + ); + } +} + +export function assertIncludes(text, substring, message) { + if (!text.includes(substring)) { + throw new Error( + message || `Expected text to include "${substring}"\nGot: ${text.substring(0, 200)}...` + ); + } +} + +export function assertNotIncludes(text, substring, message) { + if (text.includes(substring)) { + throw new Error( + message || `Expected text to NOT include "${substring}"` + ); + } +} + +export function assertMatches(text, pattern, message) { + if (!pattern.test(text)) { + throw new Error( + message || `Expected text to match pattern ${pattern}\nGot: ${text.substring(0, 200)}...` + ); + } +} + +export function assertThrows(fn, expectedMessage, message) { + try { + fn(); + throw new Error(message || 'Expected function to throw'); + } catch (error) { + if (expectedMessage && !error.message.includes(expectedMessage)) { + throw new Error( + `Expected error message to include "${expectedMessage}", got "${error.message}"` + ); + } + } +} diff --git a/tools/webidl-codegen/tests/test-types.js b/tools/webidl-codegen/tests/test-types.js new file mode 100644 index 00000000000..b35b8232026 --- /dev/null +++ b/tools/webidl-codegen/tests/test-types.js @@ -0,0 +1,526 @@ +#!/usr/bin/env node + +/** + * Test type mapping for all WebIDL types + */ + +import { parseWebIDL } from '../src/parser.js'; +import { CppGenerator } from '../src/generator.js'; +import { TestRunner, assertIncludes } from './test-helpers.js'; + +const runner = new TestRunner('Type Mapping Tests'); + +// Primitive types +runner.test('boolean maps to bool', () => { + const webidl = ` + [Exposed=*] + interface API { + boolean getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'bool getValue(jsg::Lock& js);'); +}); + +runner.test('byte maps to int8_t', () => { + const webidl = ` + [Exposed=*] + interface API { + byte getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'int8_t getValue(jsg::Lock& js);'); +}); + +runner.test('octet maps to uint8_t', () => { + const webidl = ` + [Exposed=*] + interface API { + octet getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'uint8_t getValue(jsg::Lock& js);'); +}); + +runner.test('short maps to int16_t', () => { + const webidl = ` + [Exposed=*] + interface API { + short getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'int16_t getValue(jsg::Lock& js);'); +}); + +runner.test('unsigned short maps to uint16_t', () => { + const webidl = ` + [Exposed=*] + interface API { + unsigned short getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'uint16_t getValue(jsg::Lock& js);'); +}); + +runner.test('long maps to int32_t', () => { + const webidl = ` + [Exposed=*] + interface API { + long getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'int32_t getValue(jsg::Lock& js);'); +}); + +runner.test('unsigned long maps to uint32_t', () => { + const webidl = ` + [Exposed=*] + interface API { + unsigned long getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'uint32_t getValue(jsg::Lock& js);'); +}); + +runner.test('long long maps to int64_t', () => { + const webidl = ` + [Exposed=*] + interface API { + long long getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'int64_t getValue(jsg::Lock& js);'); +}); + +runner.test('unsigned long long maps to uint64_t', () => { + const webidl = ` + [Exposed=*] + interface API { + unsigned long long getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'uint64_t getValue(jsg::Lock& js);'); +}); + +runner.test('float maps to float', () => { + const webidl = ` + [Exposed=*] + interface API { + float getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'float getValue(jsg::Lock& js);'); +}); + +runner.test('unrestricted float maps to float', () => { + const webidl = ` + [Exposed=*] + interface API { + unrestricted float getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'float getValue(jsg::Lock& js);'); +}); + +runner.test('double maps to double', () => { + const webidl = ` + [Exposed=*] + interface API { + double getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'double getValue(jsg::Lock& js);'); +}); + +runner.test('unrestricted double maps to double', () => { + const webidl = ` + [Exposed=*] + interface API { + unrestricted double getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'double getValue(jsg::Lock& js);'); +}); + +runner.test('DOMString maps to jsg::DOMString', () => { + const webidl = ` + [Exposed=*] + interface API { + DOMString getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::DOMString getValue(jsg::Lock& js);'); +}); + +runner.test('ByteString maps to jsg::ByteString', () => { + const webidl = ` + [Exposed=*] + interface API { + ByteString getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::ByteString getValue(jsg::Lock& js);'); +}); + +runner.test('USVString maps to jsg::USVString', () => { + const webidl = ` + [Exposed=*] + interface API { + USVString getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::USVString getValue(jsg::Lock& js);'); +}); + +runner.test('undefined maps to void', () => { + const webidl = ` + [Exposed=*] + interface API { + undefined doSomething(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'void doSomething(jsg::Lock& js);'); +}); + +runner.test('any maps to jsg::JsValue', () => { + const webidl = ` + [Exposed=*] + interface API { + any getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::JsValue getValue(jsg::Lock& js);'); +}); + +runner.test('object maps to jsg::JsObject', () => { + const webidl = ` + [Exposed=*] + interface API { + object getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::JsObject getValue(jsg::Lock& js);'); +}); + +// Generic types +runner.test('Promise maps to jsg::Promise', () => { + const webidl = ` + [Exposed=*] + interface API { + Promise getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Promise getValue(jsg::Lock& js);'); +}); + +runner.test('sequence maps to kj::Array', () => { + const webidl = ` + [Exposed=*] + interface API { + sequence getValues(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::Array getValues(jsg::Lock& js);'); +}); + +runner.test('record maps to jsg::Dict', () => { + const webidl = ` + [Exposed=*] + interface API { + record getMap(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Dict getMap(jsg::Lock& js);'); +}); + +runner.test('FrozenArray maps to kj::Array', () => { + const webidl = ` + [Exposed=*] + interface API { + readonly attribute FrozenArray items; + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::Array getItems(jsg::Lock& js);'); +}); + +// Optional and nullable +runner.test('optional parameter maps to jsg::Optional', () => { + const webidl = ` + [Exposed=*] + interface API { + undefined doSomething(optional DOMString value); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'void doSomething(jsg::Lock& js, jsg::Optional value);'); +}); + +runner.test('nullable return type maps to kj::Maybe', () => { + const webidl = ` + [Exposed=*] + interface API { + DOMString? getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::Maybe getValue(jsg::Lock& js);'); +}); + +// Union types +runner.test('union type maps to kj::OneOf', () => { + const webidl = ` + [Exposed=*] + interface API { + (DOMString or long) getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::OneOf getValue(jsg::Lock& js);'); +}); + +runner.test('three-way union maps to kj::OneOf with three types', () => { + const webidl = ` + [Exposed=*] + interface API { + (DOMString or long or boolean) getValue(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'kj::OneOf getValue(jsg::Lock& js);'); +}); + +// Buffer types +runner.test('ArrayBuffer maps to jsg::BufferSource', () => { + const webidl = ` + [Exposed=*] + interface API { + ArrayBuffer getData(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::BufferSource getData(jsg::Lock& js);'); +}); + +runner.test('BufferSource maps to jsg::BufferSource', () => { + const webidl = ` + [Exposed=*] + interface API { + BufferSource getData(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::BufferSource getData(jsg::Lock& js);'); +}); + +// Interface types +runner.test('local interface maps to jsg::Ref', () => { + const webidl = ` + [Exposed=*] + interface Data {}; + + [Exposed=*] + interface API { + Data getData(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Ref getData(jsg::Lock& js);'); +}); + +runner.test('external interface maps to jsg::Ref', () => { + const webidl = ` + [Exposed=*] + interface API { + Request getRequest(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator({ externalInterfaces: ['Request'] }); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'jsg::Ref getRequest(jsg::Lock& js);'); +}); + +runner.test('enum type not wrapped in jsg::Ref', () => { + const webidl = ` + enum Color { "red", "green" }; + + [Exposed=*] + interface API { + Color getColor(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'Color getColor(jsg::Lock& js);'); +}); + +runner.test('dictionary type not wrapped in jsg::Ref', () => { + const webidl = ` + dictionary Options { + DOMString value; + }; + + [Exposed=*] + interface API { + Options getOptions(); + }; + `; + + const definitions = parseWebIDL(webidl); + const generator = new CppGenerator(); + const code = generator.generate(definitions, { namespace: 'test', filename: 'api' }); + + assertIncludes(code, 'Options getOptions(jsg::Lock& js);'); +}); + +const results = await runner.run(); +const success = runner.summary(); + +process.exit(success ? 0 : 1);