Repository navigation
Formalize the ABI template, and generate language bindings outside ts-gen - #207
Draft
guybedford wants to merge 6 commits into
Draft
guybedford wants to merge 6 commits into
guybedford wants to merge 6 commits into
Conversation
The `--target wat` template listed the ABI's imports, but a bindings generator also needs what only the model held: each import's owner and JS operation, what each reference position names, and which owners inherit from which. So `--target zena` had to live inside ts-gen, and `embed` had to re-derive the ABI from the `.d.ts`. The template now carries the whole model as `@abi.*` annotations, which WAT tools ignore, so the file still assembles. ABI.md specifies the format. The model moves to a `ts-gen-abi` crate with no dependencies, holding the reference writer and reader. * Owners are first-class: `(@abi.class "Request" (extends "Body"))`, with their kind, module, aliases, heritage and docs. Inherited members are only on the base, so a generator needs `extends` to present them. * Each import names its operation (`(@abi.call "get")`, `(@abi.get "x" (on "WebAssembly"))`, `(@abi.new "Headers")`), its owner, its TS declaration and its JSDoc. * Each reference position says what it names and which empties it admits: `(@abi.ref string null)`. Scalar heads (`number`, `boolean`, `bigint`) mark a nullable scalar that crosses as a reference. * A `JsValue.undefined` sys function gives `undefined` to a language that cannot import globals. The snapshot harness checks that every fixture's template reads back and writes the same text.
…le names `embed --abi <template>` reads the ABI from the file a generator wrote bindings from, instead of re-deriving it from the `.d.ts`, so the two cannot disagree. Two options let a language link the ABI without naming its imports. * `--bindings-module <name>`: Kotlin's `@JsFun` always imports from `js_code`, under the function's qualified name, which a generator can make the ABI name. Imports from that module that are ABI functions link to the bindings; its other imports are left to the host. * `--imported-string-constants <namespace>`: the bindings import their string constants from it, so the merged module compiles with the one `importedStringConstants` the language module already uses. `embed` also implements the new `JsValue.undefined`.
`--target zena` needed the ABI model ts-gen held in memory. With the model in the template, a generator can read the file instead, so Zena's generator becomes `ts-gen-zena`, a crate depending only on `ts-gen-abi`, which can live in Zena's own tree. * `ts-gen-zena <template.wat> -o bindings.zena` writes the bindings, and `main.zena` and an `app.zena` scaffold for a template with handlers. `--target zena` is removed. * Its tests generate from the templates ts-gen's snapshot harness commits for every fixture, so the generator is tested the way any consumer of the format runs it. Every snapshot is unchanged from `--target zena`, read from the file alone. * The worker example builds through the template: `ts-gen --target wat`, `ts-gen-zena`, `zena build`, `ts-gen embed --abi`. * The Zena mapping moves from CONVENTIONS to the crate's README.
A head of "W" inside declare module "m" was resolved to m's W when it had one, else the global W, so a module could not reference a global owner its own owner shadows, as a module's constructor var typed by a global interface does. Owner references in heads and extends now carry the module, as owner records already do, and the Zena generator looks classes up by that identity.
… and owner-less heads Kotlin/Wasm emits the legacy try/catch by default. ABI.md now says an import must carry an operation of its version, since new operations change the version, and that a head may name a type with no owner record.
The generator reads the template with its own reader and no dependency on ts-gen. Its @JSFun externals import from js_code under the ABI's names, which embed --bindings-module js_code links to the bindings while leaving Kotlin's own snippets to its import object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This makes the Wasm ABI template a formal, versioned format, so language bindings generators can live outside ts-gen. It moves the Zena generator out onto that format alone, and adds a Kotlin/Wasm worker whose generator is written in Kotlin.
--target watalready wrote the ABI as a WAT import template, but a generator could only consume the ABI through ts-gen's Rust model. That meant every language target had to live in this repository. The template now carries the whole model as(@abi.*)annotations. These are the WebAssembly annotations proposal syntax, so any WAT tool still assembles the file, and a generator in any language can read it with a small S-expression reader.ABI.mdspecifies the format. It covers:extendsand docsget,set,call,newand the sys operationsts-gen-abicrate holds the model, the writer and a reader, and has no dependencies. Every fixture template,workers-typesincluded, reads back and rewrites byte for byte. The snapshot test checks this on every run.extendsare absolute:"Path"for a global owner,"Path" (module "spec")for a module's. Previously a head insidedeclare moduleresolved to the module's owner of that path first. That made a module unable to reference a global owner its own owner shadows. This changes heads in theambient-module-merge,constructor-varsandmember-scopesfixtures.extends, since inherited members are not repeated on derived owners and a generator needs to follow them.ts-gen embed --abi <template>builds from a template without the.d.ts.--bindings-module <name>links only ABI-named imports of that module and leaves the rest to the host. Kotlin's@JsFunimports all come fromjs_code.--imported-string-constants <name>sets the string-constants module the bindings use.JsValue.undefinedsys function gives modules anundefinedvalue.generators/zenaasts-gen-zena, a CLI and crate that depends only onts-gen-abi.--target zenais removed. It reproduces all Zena snapshots from the templates alone, apart from the head fixes above.examples/kotlin-workeris a Kotlin/Wasm worker.@JsFunexternals named so their imports are the ABI's names, typed owner interfaces over them, and a@JsExportentry for each handler.run.sh servebuilds it throughembed --abi --bindings-module js_codeand serves it in workerd.check-templates.shgenerates and compiles bindings for every fixture template.Tests cover the format round trip, reader rejections, absolute owner references, embedding from a template, bindings-module linking and legacy exceptions. The Zena compile and worker tests need a Zena CLI with value imports, so they run only when
ZENAis set and were not run for this change. The Kotlin example is not in CI because it needs a JDK and Gradle.Open questions surfaced by the Kotlin generator:
Promise<T>carry only thePromisehead, so a generator cannot type what they resolve to. Type arguments on heads, such as(@abi.ref "Promise" (of string)), would fix that for Promise and other generics.undefinedan external returns intonull, and cannot passundefinedeither. A position that admitsundefinedbut notnullcould have embed map a Wasm null toundefined.newresult as the module class, while its statics now return the global interface, as in TypeScript.