Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
Design for binding C (and `extern "C"` C++) code from tslang without writing
`.d.ts` declarations by hand. It covers two items: a `@linkname` decorator for
declaring a C function under a different TS name, and `tsbindgen`, a generator
that turns C headers into `.d.ts` declarations.
that turns C headers into tslang binding files (`.ts`).

Status: approved design, not implemented. Refines Option A of
Status: PR 1 and PR 2 implemented; PR 3 (packaging) not started. Refines Option A of
`docs/c-cpp-header-import.md`; C++ wrapper generation (that doc's "C++ 2"
route) is v2 and gets its own spec once v1 has merged.

Expand Down Expand Up @@ -141,7 +141,7 @@ The `-shared` cross-module tests import re-parsed declarations that carry
plus `PPCallbacks` for macros) and returns plain records: functions,
records, enums, typedefs, macros, each with its source location.
- `TypeMapper`: C type → tslang type text, or a skip reason.
- `DtsPrinter`: records → `.d.ts` text.
- `BindingPrinter`: records → `.ts` text (see "As built" below: not `.d.ts`).
- `tslang/tsbindgen/` — CLI executable over the library.
- Links against clang libraries from the in-tree LLVM install
(`clangTooling`, `clangFrontend`, `clangAST`, `clangSema`, `clangLex`,
Expand All @@ -153,7 +153,7 @@ The `-shared` cross-module tests import re-parsed declarations that carry
```text
tsbindgen <input.h|.c|.cpp> [-I<dir>]... [-D<name>[=v]]... [--target <triple>]
[--filter <glob>]... [--namespace <N> [--strip-prefix <P>]]
[--resource-dir <dir>] [-o <out.d.ts>] [-- <extra clang args>...]
[--resource-dir <dir>] [-o <out.ts>] [-- <extra clang args>...]
```

- `--target` defaults to the host triple. Type widths come from clang's
Expand All @@ -179,14 +179,14 @@ declarations, so this rule is essential.

| C | tslang |
| --- | --- |
| integer types | exact width `i8`…`i64` / `u8`…`u64` from the target. Never `int`/`long`: tslang `long` is 64-bit, but C `long` is 32-bit on Windows. |
| integer types | exact width, signed `s8`…`s64` / unsigned `u8`…`u64`, from the target (tslang's `i8`…`i64` are signless). Never `int`/`long`: tslang `long` is 64-bit, but C `long` is 32-bit on Windows. |
| `size_t`, `ssize_t`, `ptrdiff_t`, `intptr_t`, `uintptr_t` | `index` |
| `float` / `double` / `_Bool`, `bool` | `f32` / `f64` / `boolean` |
| `char*`, `const char*` | `string` |
| `char**` | `Reference<string>` |
| `T*`, T a scalar or complete struct | `Reference<T>` |
| `void*`; pointer to an incomplete struct or a C++ class | `Opaque`. A named incomplete struct `S` also gets `type S = Opaque;` and parameters use `S`. |
| function pointer | `Opaque`, with a trailing comment giving the C signature (`// (a: i32) => i32`). |
| function pointer | `Opaque`, with the C signature in a comment: `fn: Opaque /* (p0: s32) => s32 */` for a parameter or field, a trailing `// ...` for a `typedef`. |
| complete struct of mappable fields | named tuple `type S = [a: T1, b: T2];`, as `tm` in `core.os.d.ts` |
| C `enum` | TS `enum` with the same names and values |
| C++ `enum : u8` (fixed non-`int` underlying type) | `type E = u8;` plus `const A = 1;` per enumerator, using the enumerator's own name (`A`, as C code spells it for an unscoped enum; `E_A` for an `enum class`, which has no unqualified name) |
Expand All @@ -199,7 +199,7 @@ Parameter names are taken from the header; an unnamed parameter becomes

### Skipped declarations

Each of these is left out of the `.d.ts`, with
Each of these is left out of the output, with
`// skipped: <name> — <reason>` in its place and a warning on stderr:

- a union, or a struct with a bitfield or an unmappable field;
Expand All @@ -220,6 +220,25 @@ A skip never stops generation. The exit code is `0` if the file was written,
`1` for a clang parse error or I/O failure (clang's diagnostics are printed
as-is), `2` for bad arguments.

### As built (2026-09-26)

Measured while implementing PR 2 (see `docs/superpowers/plans/2026-09-26-tsbindgen.md`); where this
section and the text above disagree, this section is what was built.

Each of these was found by running generated bindings through tslang; the spec's version does not work.

| Spec says | Plan does | Why (measured) |
| --- | --- | --- |
| Output is a `.d.ts` (`-o <out.d.ts>`) | Output is a `.ts` file, included with `/// <reference path="fixture.ts" />`; `-o` defaults to stdout; a `-o x.d.ts` gets a warning | In a `.d.ts` — and in any file pulled in with `import` — a `const` is an ambient declaration with no value: `const FIXTURE_ANSWER = 42;` fails with `LNK2019: unresolved external symbol FIXTURE_ANSWER` (exe) and `Symbols not found: [ FIXTURE_ANSWER ]` (JIT). The same text in a `.ts` included by `/// <reference path>` works in both. `import "./fixture"` is also wrong for another reason: when `fixture.dll` exists it loads that as a tslang library (`MLIRGenModule.cpp`, `mlirGen(ImportDeclaration)`). |
| Signed C integers map to `i8`…`i64` | Signed → `s8`…`s64`, unsigned → `u8`…`u64` | tslang's `iN` are **signless** (`MLIRGenTypes.cpp`: `{"i32", builder.getIntegerType(32)}` vs `{"s32", builder.getIntegerType(32, true)}`): an `int8_t` -5 prints `251`, an `int16_t` -3 prints `65533`, an `int32_t` -5 prints `4294967291`. With `sN` all print correctly. |
| `struct Node { struct Node *next; }` → `next: Reference<Node>` | A pointer field that leads back to its own struct — directly, through another struct, or through a typedef — is `Opaque`, with `/* Reference<Node>: a type cannot refer to itself */` | tslang dies on a self-referencing type alias: `type Node = [value: i32, next: Reference<Node>]; function main() { let n: Node = [1, null]; }` exits 127 with no message, even at `--emit=mlir`. That is a compiler bug to file separately; tsbindgen must not emit a cycle meanwhile. |
| Function-pointer parameters get a trailing `// (a: i32) => i32` | Inline `fn: Opaque /* (p0: s32) => s32 */` for parameters and fields; trailing `//` for a `typedef` | A declaration can have several callbacks; one trailing comment cannot say which is which. Parameter names inside the signature are `p0…`: a C function type carries no names. The file header says once that a callback must have no captures and is passed as `fn as Opaque` (spec open issue "Callback lifetime"). |
| `runToolOnCodeWithArgs`-style parsing (implied) | `ToolInvocation` with our own diagnostic consumer | `runToolOnCodeWithArgs` ignores errors in the clang command line itself: `tsbindgen fixture.h -- --bogus-flag` printed `error: unknown argument` and still exited 0 with output. |
| (silent) | An object-like macro that is not a literal (`#define FLAGS (1 << 3)`, `#define API __declspec(dllexport)`) is skipped with `macro is not a literal`; one with no tokens (include guards) is ignored | The skip list names function-like macros only; a silent drop would hide API. |
| (silent) | `--strip-prefix` never produces a name that is empty, starts with a digit, or is taken: it keeps the C name and warns; a stripped reserved word gets `_` | `fx_len` stripping to `len` when `len` exists would declare two functions `len`. |
| (silent) | A struct whose clang layout is not natural field-after-field (packed, `#pragma pack`, over-aligned) is skipped: `packed or over-aligned layout` | A named tuple is laid out naturally; the spec's measured baseline covers only natural layouts. |
| (silent) | Nothing selected is a warning | `tsbindgen umbrella.h` where `umbrella.h` only `#include`s others emits an empty file; the warning says to use `--filter`. |

### Naming

C names are kept. `--namespace N` wraps the output in
Expand Down Expand Up @@ -253,7 +272,7 @@ declaration that starts with it; the `@linkname` keeps the C name.
out, an out-parameter, an opaque handle, a struct passed by pointer, an
enum, a literal macro, a callback, a varargs function.
- At test time: compile `fixture.c` with the in-tree clang to an object and
a shared library, run `tsbindgen fixture.h -o fixture.d.ts`, compile
a shared library, run `tsbindgen fixture.h -o fixture.ts`, compile
`bindgen_test.ts` against it, link with `-obj=` and run it; then run the
same program under `--emit=jit` with `-shared-libs=` pointing at the
shared library. Both runs must print the expected output.
Expand Down
1 change: 1 addition & 0 deletions tslang/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ add_subdirectory(include)
add_subdirectory(lib)
add_subdirectory(test)
add_subdirectory(tslang)
add_subdirectory(tsbindgen)
add_subdirectory(ts-new-parser)

if (NOT DEFINED TYPESCRIPT_IGNORE_UNIT_TESTS)
Expand Down
9 changes: 9 additions & 0 deletions tslang/docs/reference-counting-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5894,3 +5894,12 @@ script clears only the model directory it is writing, so they survive until dele
> **§9.7's larger case is closed by §9.77.** The default lib is now built per model and a
> program links the one matching its own `-mm=`. What remains of §9.7 is the general mixed-link
> question for *user* libraries, where the policy is unchanged: allow, warn, and leak.

### 9.78 A C string returned through generated bindings (open)

tsbindgen maps a C function's `char *` / `const char *` result to `string`, as the default library
already does by hand (`regexp_match_results_format`). A tslang string carries an 8-byte header before
its characters; a C-owned `char *` has none. Under `-mm=gc` that is harmless - nothing reads the
header. Under `-mm=rc` a release of such a value reads a count that is not there, which is undefined.
Not fixed: tsbindgen v1 targets `gc`. Revisit if rc becomes a supported model for bindings - the
likely shape is a distinct C-string type in the compiler that is never released.
104 changes: 104 additions & 0 deletions tslang/include/TsClangImporter/BindingModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#ifndef TSCLANGIMPORTER_BINDINGMODEL_H
#define TSCLANGIMPORTER_BINDINGMODEL_H

#include <functional>
#include <string>
#include <vector>

namespace tsbindgen
{

// A tslang type as the printer spells it. A named type holds the key of the declaration it refers
// to, not a TS name: --strip-prefix renames declarations after the header has been mapped.
struct TsType
{
enum class Kind
{
Builtin,
Named,
Reference
};

Kind kind = Kind::Builtin;
std::string name; // Builtin: the tslang spelling; Named: the key of a Decl
std::vector<TsType> pointee; // Reference: exactly one element
std::string comment; // shown next to the type: a function pointer's C signature, ...
bool functionPointer = false;
std::string skipReason; // non-empty: the C type has no tslang mapping

bool skipped() const
{
return !skipReason.empty();
}

static TsType builtin(std::string name, std::string comment = {});
static TsType named(std::string key);
static TsType reference(TsType pointee);
static TsType skip(std::string reason);
};

// Spells `type`, asking `nameOf` for the TS name of each named declaration it refers to.
std::string renderType(const TsType &type, const std::function<std::string(const std::string &)> &nameOf);

// The declaration keys `type` refers to, in order, each once.
void collectKeys(const TsType &type, std::vector<std::string> &keys);

// "struct:Point" -> "Point"
std::string nameFromKey(const std::string &key);

// A name that is a TS reserved word gets a `_` suffix: `delete` -> `delete_`.
std::string escapeReserved(std::string name);

// A type tslang itself defines (`boolean`, `string`, `index`, `s32`, ...). tslang ignores a `type`
// alias of such a name and keeps its own type, so a C type of that name must never be declared under it.
bool isTsBuiltinTypeName(const std::string &name);

struct Field
{
std::string name;
TsType type;
};

struct Enumerator
{
std::string name;
std::string value; // decimal
};

enum class DeclKind
{
Macro, // const N = 42;
OpaqueStruct, // type S = Opaque;
Struct, // type S = [a: T1, b: T2]; (type S = Opaque; when skipped)
Enum, // enum E { A = 0 }
FixedEnum, // type E = u8; const A = 1;
Typedef, // type T = ...;
Function // declare function f(...): R;
};

struct Decl
{
DeclKind kind = DeclKind::Function;
std::string key; // unique within the model: "<kind>:<C name>"
std::string name; // the C name
std::string symbol; // Function: the symbol it links as, when an asm label makes it differ from the name
bool inMainFile = false;
bool inSystemHeader = false;
std::string skipReason; // non-empty: left out, with a `// skipped:` line in its place

std::vector<Field> fields; // Struct: fields; Function: parameters
TsType type; // Function: result; Typedef: aliased type; FixedEnum: underlying
bool varargs = false; // Function
std::vector<Enumerator> enumerators; // Enum, FixedEnum
bool scoped = false; // FixedEnum from an `enum class`
std::string value; // Macro: a TS literal
};

struct HeaderModel
{
std::vector<Decl> decls; // in the order the header declares them
};

} // namespace tsbindgen

#endif // TSCLANGIMPORTER_BINDINGMODEL_H
35 changes: 35 additions & 0 deletions tslang/include/TsClangImporter/BindingPrinter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef TSCLANGIMPORTER_BINDINGPRINTER_H
#define TSCLANGIMPORTER_BINDINGPRINTER_H

#include "TsClangImporter/BindingModel.h"

#include "llvm/Support/Error.h"

#include <string>
#include <vector>

namespace tsbindgen
{

struct PrintOptions
{
// A declaration is selected if its C name matches one of these globs, or, with none, if the
// input file itself declares it. Types a selected declaration uses are emitted too.
std::vector<std::string> filters;
std::string namespaceName; // --namespace: wrap in `namespace N { export ... }`, with @linkname
std::string stripPrefix; // --strip-prefix: removed from the TS names (needs a namespace)
std::vector<std::string> headerLines; // printed first, each as a `//` comment
};

struct PrintResult
{
std::string text;
std::vector<std::string> warnings; // one per skipped declaration, and per name kept unstripped
};

// Fails only for a filter that is not a valid glob.
llvm::Expected<PrintResult> printBindings(const HeaderModel &model, const PrintOptions &options);

} // namespace tsbindgen

#endif // TSCLANGIMPORTER_BINDINGPRINTER_H
30 changes: 30 additions & 0 deletions tslang/include/TsClangImporter/HeaderParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef TSCLANGIMPORTER_HEADERPARSER_H
#define TSCLANGIMPORTER_HEADERPARSER_H

#include "TsClangImporter/BindingModel.h"

#include "llvm/Support/Error.h"

#include <string>
#include <utility>
#include <vector>

namespace tsbindgen
{

struct ParseInput
{
std::string code; // the input file's contents
std::string fileName; // its path; `#include "x.h"` resolves next to it
std::vector<std::string> args; // clang arguments: -x, --target=, -resource-dir, -I, -D, ...
// extra files visible to clang only (tests use them in place of real headers)
std::vector<std::pair<std::string, std::string>> virtualFiles;
};

// Runs clang over the input and maps what it declares. clang's diagnostics go to stderr as clang
// prints them; an error means clang reported an error.
llvm::Expected<HeaderModel> parseHeader(const ParseInput &input);

} // namespace tsbindgen

#endif // TSCLANGIMPORTER_HEADERPARSER_H
86 changes: 86 additions & 0 deletions tslang/include/TsClangImporter/TypeMapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#ifndef TSCLANGIMPORTER_TYPEMAPPER_H
#define TSCLANGIMPORTER_TYPEMAPPER_H

#include "TsClangImporter/BindingModel.h"

#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringSet.h"

#include <utility>
#include <vector>

namespace tsbindgen
{

// C type -> tslang type, for one translation unit. Widths come from the ASTContext, so they are the
// target's: C `long` is i32 for x86_64-pc-windows-msvc and i64 for x86_64-linux-gnu.
class TypeMapper
{
public:
enum class Use
{
Parameter,
Result,
Field,
Alias // the right-hand side of a typedef
};

explicit TypeMapper(clang::ASTContext &context) : context(context)
{
}

TsType map(clang::QualType type, Use use);

// Why a complete struct cannot be a named tuple ("" if it can). Cached; a struct that refers to
// itself through a pointer counts as mappable while it is being checked.
std::string recordSkipReason(const clang::RecordDecl *record);

// A complete struct's fields. A pointer field that leads back to this struct is Opaque:
// tslang cannot declare a type that refers to itself.
std::vector<Field> mapFields(const clang::RecordDecl *definition);

// The struct/enum's own name, or the typedef that names it when it has none ("" for neither).
static std::string tagName(const clang::TagDecl *tag);
static std::string recordKey(const clang::RecordDecl *record);
static std::string enumKey(const clang::EnumDecl *enumDecl);
static std::string typedefKey(const clang::TypedefNameDecl *typedefDecl);

// `typedef struct S S;` and `typedef struct { ... } S;` name the struct itself: no alias is emitted.
static bool namesItsTag(const clang::TypedefNameDecl *typedefDecl);

// A C enum is a TS enum only when it is int-sized and not given a fixed underlying type other
// than int; anything else is a type alias plus constants.
bool isPlainEnum(const clang::EnumDecl *enumDecl);

// Whether a struct can have a TS declaration of its own: in C, any; in C++, not one inside a
// namespace or a class, and not a template or a template specialization (std::cmatch).
bool isDeclarable(const clang::RecordDecl *record);

// Every struct and enum a mapped type has named, by key. The AST traversal does not reach a
// struct first named inside a prototype (`void f(struct S *s);`), so the parser adds these.
const std::vector<std::pair<std::string, const clang::TagDecl *>> &referencedTags() const
{
return referenced;
}

private:
std::string referTo(const clang::TagDecl *tag);
TsType mapPointer(const clang::PointerType *pointer, Use use);
TsType mapBuiltin(clang::QualType canonical, Use use);
std::string signatureComment(const clang::FunctionProtoType *function);
bool isSystem(const clang::Decl *decl);
const clang::RecordDecl *recordBehind(clang::QualType type);
bool leadsToOwner(clang::QualType type);

clang::ASTContext &context;
llvm::DenseMap<const clang::RecordDecl *, std::string> recordReasons;
std::vector<std::pair<std::string, const clang::TagDecl *>> referenced;
llvm::StringSet<> referencedKeys;
std::vector<const clang::RecordDecl *> owners; // structs whose fields are being mapped
};

} // namespace tsbindgen

#endif // TSCLANGIMPORTER_TYPEMAPPER_H
1 change: 1 addition & 0 deletions tslang/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ add_subdirectory(TypeScriptRuntime)
add_subdirectory(TypeScriptAsyncRuntime)
add_subdirectory(TypeScriptAliasPass)
add_subdirectory(TypeScriptMemAllocPass)
add_subdirectory(TsClangImporter)
Loading
Loading