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
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,58 @@ declare function add(a: s32, b: s32): s32;
declare function dist(p: Reference<Point>): f64;
```

Only plain structs, enums and `extern "C"` functions are emitted. Classes, namespace members (`lib::helper`) and functions with C++ linkage (`cxx_only`) are left out, without a warning: their symbol names are mangled, so tslang cannot link to them. To bind a C++ API, expose it through `extern "C"` wrapper functions (with an opaque handle for each class) and run tsbindgen on the header that declares them.
Plain structs, enums and `extern "C"` functions are emitted, and polymorphic classes (below). A class without virtual methods (`lib::Widget`), namespace functions (`lib::helper`) and functions with C++ linkage (`cxx_only`) are left out, without a warning. To bind those, expose them through `extern "C"` wrapper functions (with an opaque handle for each class) and run tsbindgen on the header that declares them.

#### C++ classes

A polymorphic class (one with virtual methods), in any namespace, becomes a [`@cpp` class](docs/cpp-classes.md): tslang calls its virtual methods through the object's vtable and its non-virtual methods by their mangled symbols, with no wrapper. clang computes the slots and the symbols for the target's ABI (`--target`), so generate the bindings once per ABI: MSVC and the Itanium ABI (Linux, Android) number the slots differently.

```cpp
namespace geo {
struct Handle { void *p; Handle(const Handle &); ~Handle(); };
class Shape {
public:
virtual ~Shape();
virtual int area() const = 0;
virtual void scale(int by) = 0;
virtual void scale(double by) = 0;
virtual Handle handle() = 0;
int id(int x);
};
class Square : public Shape { public: int area() const override; virtual bool bigger(const Shape &other) const; int side; };
}
extern "C" geo::Square *make_square(int side);
```

`tsbindgen shapes.hpp --target x86_64-pc-windows-msvc` writes:

```typescript
@cpp
class Shape { // geo::Shape
@slot(1) area(): s32;
@slot(3) scale(by: s32): void;
@slot(2) scale_2(by: f64): void;
@cppnontrivial
@slot(4) handle(): [p: Opaque | null];
@linkname("?id@Shape@geo@@QEAAHH@Z") id(x: s32): s32;
}
@cpp
class Square extends Shape { // geo::Square
side: s32;
@slot(5) bigger(other: Shape): boolean;
}
declare function make_square(side: s32): Square;
```

- **Slots** are clang's: the destructor (slot 0 under MSVC, 0 and 1 under the Itanium ABI) and methods that aren't public are slots left out. A method overriding a base's is not declared again: the `@cpp` class's override keeps the base's slot.
- **Overloads** get `_2`, `_3`, ... in declaration order, whatever order the ABI puts their slots in.
- **A pointer or a reference** to an object of a bound class is the class; a reference to anything else is a pointer.
- **A record returned by value** is a tuple with its layout, `@cppnontrivial` when it has a destructor or a copy constructor.
- **Fields** are declared when they are where a `@cpp` class puts them (after the vtable pointer and the base's storage).
- **Left out, with a `// skipped:` line:** `inline` methods (no symbol to link to), struct parameters passed by value, and a non-virtual method returning a struct. Static methods and operators are left out silently.
- **Not bound at all:** templates, classes with several bases or a virtual one, and 32-bit targets. A pointer to such a class stays `Opaque`.

Compile the bindings with `-mm=gc` or `-mm=none`: a `@cpp` class's objects are C++'s.

## Memory models

Expand Down
158 changes: 158 additions & 0 deletions TSLANG_ISSUES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# tslang issues found while working on native_ts_graphics

Collected 2026-10-12 on `tslang v0.0-pre-alpha96`, updated the same day against
`tslang v0.0-pre-alpha97` (I:\tslang\tslang.exe, Windows x64, JIT run: `tslang file.ts`).
Every repro below is a complete file that was run. Source project:
https://github.com/ASDAlexander77/native_ts_graphics

## Status

| # | Issue | pre-alpha96 | pre-alpha97 |
|---|---|---|---|
| 1 | A derived class without a constructor does not forward its arguments to the base constructor | bug | **fixed** (TypeScriptCompiler#557) |
| 2 | The error for (1) pointed at line 1, column 1 of the base class | bug | **moot**: nothing synthesized fails any more; an explicit bad `super()` is reported at the call |
| 3 | `u64` `<<` shifted by the count modulo 32 | bug | **fixed** |
| 4 | `Ref<string>` field with a `null` initializer rejected | question | **answered**: declare it `Ref<string> \| null` |
| 5 | `@cppvtable` overload naming (C++ overloads need distinct TypeScript names) | open | **addressed** on branch `cppvtable-slot`: `@cpp` + `@slot(N)` (docs/cpp-classes.md) |
| 6 | C `string` return values keep the raw `char*` (no copy) | open | open, not re-checked |
| 7 | `Opaque` compared with `Opaque \| null` crashed the compiler | - | **fixed** by #536 (re-checked with the original drawScene shape) |
| 8 | A ternary of number literals in arithmetic truncated to the other operand's integer type | - | **fixed** by #560 (still wrong on pre-alpha97) |
| 9-12 | Bool const arrays, `Math.max`, `f32` array literal, `let x = 0` | - | fixed (#537, DefaultLib#31, works, #544 warning) |

Verified downstream: native_ts_graphics dropped its 71 forwarding constructors (PR #49) and rebuilt
everything with pre-alpha97 (all objects deleted first): Windows build clean, CTest 61/61, Android
x86_64 build of 105 APKs clean, emulator smoke test 64/65 (the one failure, `oit_linked_lists`, was a
process that vanished without a crash dump after loading a 3624 x 3624 texture and passed when rerun
alone; no sign of a compiler cause).

---

## Fixed in pre-alpha97

### 1. Implicit constructor of a derived class did not forward its arguments

TypeScript gives a derived class with no constructor an implicit
`constructor(...args) { super(...args); }`. tslang generated `super()` with no arguments:

```ts
class Base {
constructor(protected a: int, protected b: int = 0) {
}
sum(): int { return this.a + this.b; }
}
class Derived extends Base {
extra = 1; // no constructor
}
function main(): void {
const d = new Derived(2, 3); // TypeScript: fine, prints 5
console.log(d.sum());
}
```

pre-alpha96: `error: Expected 1-2 arguments, but got 0.` at `class Base {`, then `failed statement`.
pre-alpha97: prints `5`.

### 2. Diagnostic location

Was reported at the base class (1:1 of the file in the project's case), not at the derived class
whose implicit constructor was wrong. With (1) fixed the case no longer occurs; a wrong explicit call
is reported correctly on pre-alpha97:

```ts
class Derived extends Base {
constructor() {
super(); // badsuper.ts:7:9: error: Expected 1-2 arguments, but got 0.
}
}
```

The general point (code synthesized by the compiler should report the location of the construct it was
synthesized for) may still be worth keeping in mind for other synthesized members.

### 3. `u64` shift count

```ts
function main(): void {
let a: u64 = 1;
let b = a << 40;
console.log(b); // pre-alpha96: 256, pre-alpha97: 1099511627776
let c: u64 = 1;
c = c << 33;
console.log(c); // pre-alpha96: 2, pre-alpha97: 8589934592
}
```

Not checked on pre-alpha97: `>>`, `>>>`, `i64`, `<<=`. Worth a regression test if the fix did not cover them.

### 4. `Ref<string>` field initialized with `null`

`protected argv: Ref<string> = null;` gives `can't cast from 'null' to 'Reference<string>' in 'strict null
mode'`, which is the intended strictness. The nullable spelling works (pre-alpha97):

```ts
class Holder {
argv: Ref<string> | null = null;
}
```

---

## Open

### 5. `@cppvtable`: overload naming

From the notes on TypeScriptCompiler PR #550 (the `@cppvtable` decorator): C++ overloads of an abstract
interface method need distinct TypeScript names, and the MSVC slot order of overloads must be checked by
hand (`cl /d1reportSingleClassLayout<Class>`). A way to name the C++ overload (or a tool that prints the
slot map) would remove that manual step. See `docs/cppvtable-classes.md`. Not re-checked.

### 6. C `string` return values are not copied

A C/C++ function declared as returning `string` hands tslang its raw `char*`, with no copy and no
lifetime tracking. A C helper that reused one `std::string` buffer for several calls returned garbage
to TypeScript later; the project now returns pointers to storage that lives as long as the owning object.
Decide whether this is by design; if so, the C interop docs should say the string must outlive its use,
otherwise copying on return (or an explicit borrowed annotation) would be safer. Not re-checked.

---

## 8. A union of literals in arithmetic (found live on pre-alpha97)

`c ? 0.5 : 1.0` has the type `0.5 | 1`, a union of literals. A binary operator cast that union to
the other operand's type, so with an integer on the other side the fraction was truncated:

```ts
function main(): void {
let c = true;
const width = 100; // s32
console.log(width * (c ? 0.5 : 1.0)); // pre-alpha97: 0, expected 50
const h = c ? 0.5 : 1.0;
console.log(width * h); // pre-alpha97: 0
const left = c ? 448.0 : 96.0;
let m = left * 2;
m = m / 3;
console.log(m); // pre-alpha97: 298, expected 298.667
}
```

MLIR: `ts.Cast (!ts.union<!ts.literal<0.5>, !ts.literal<1.0>>) -> si32`, then an s32 multiply.
Fixed by #560: `unwrapForBinaryOp` unwraps a union whose members are
all literals of one type to that type, as it already did for a single literal. Regression test
`tslang/test/tester/tests/00literal_union_arith.ts`.

---

## Older reports (history)

Seen in real code on tslang versions up to pre-alpha93/94. TypeScriptCompiler's own findings
document (removed by #558, in the history) records #536, #537, #544 and DefaultLib#31 as their fixes;
the ternary one (8 above) was the only one still live.

| # | Symptom (as first seen) | Minimal repro tried | Result |
|---|---|---|---|
| 7 | Silent compiler crash, exit 0xC00000FD (stack overflow, no message), on `opaque != nullableOpaque` inside a method (`examples/async_compute_bloom.ts` `drawScene`, 2026-10-08) | class `Foo`; `p != this.cur` with `cur: Foo \| null` | fixed by #536; the original shape (`Opaque` ternary vs an `Opaque \| null` local in a loop) compiles and runs correctly on pre-alpha97 |
| 8 | Ternary of numeric literal consts inside arithmetic evaluated wrong, silently (`width * (split ? 0.5 : 1.0)` gave 0) | consts `HALF`/`FULL`, `width * (split ? HALF : FULL) + ((held ? 1.0 : 0.0) - (split ? 1.0 : 0.0))` | correct there because the consts were folded; with a runtime condition it was still wrong: see 8 above |
| 9 | namespace-level `const X = [false, true, ...]` read `false` for every element in `if (X[i])` | same shape, 4 elements, loop | correct |
| 10 | `Math.max` of all-negative numbers returned 2.2e-308 (seeded with `Number.MIN_VALUE`) | `Math.max(-1.0, -2.0)` | `-1`: fixed |
| 11 | array literal mixing an `f32` element and number literals, passed as `number[]`, failed to cast | `[f, 0, 2]` assigned to `number[]` | works |
| 12 | `let x = 0;` later assigned a fraction silently kept an `s32` | `let x = 0; x = 2.5;` | now a clear warning ("a 'number' is stored in a 's32'..."): fixed |
5 changes: 5 additions & 0 deletions docs/cpp-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ A generic or static class can't be `@cpp` either.

## Writing a binding

`tsbindgen` writes the binding from the C++ header, with the slots, the mangled symbols and the
`@cppnontrivial` marks clang computes for the target (README, "C++ classes"):
`tsbindgen nvrhi.hpp --target x86_64-pc-windows-msvc --filter 'nvrhi::I*' -- -std=c++17`.
Writing one by hand:

- **Slot numbers.** Take them from the compiler, not by counting: MSVC prints a class's vtable
with `cl /c /Zs /d1reportSingleClassLayout<Name> file.cpp`; clang (Itanium) with
`clang++ -S -emit-llvm -o /dev/null -Xclang -fdump-vtable-layouts file.cpp`, for a class whose
Expand Down
28 changes: 25 additions & 3 deletions tslang/include/TsClangImporter/BindingModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ struct TsType
{
Builtin,
Named,
Reference
Reference,
Tuple // an inline named tuple, `[a: T1, b: T2]`: a C++ record a method returns by value
};

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::vector<TsType> pointee; // Reference: exactly one element; Tuple: the field types
std::vector<std::string> fieldNames; // Tuple
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
Expand All @@ -34,6 +36,7 @@ struct TsType
static TsType builtin(std::string name, std::string comment = {});
static TsType named(std::string key);
static TsType reference(TsType pointee);
static TsType tuple(std::vector<std::string> names, std::vector<TsType> types);
static TsType skip(std::string reason);
};

Expand Down Expand Up @@ -73,9 +76,23 @@ enum class DeclKind
Enum, // enum E { A = 0 }
FixedEnum, // type E = u8; const A = 1;
Typedef, // type T = ...;
CppClass, // @cpp class C extends B { @slot(N) f(...): R; @linkname("...") g(...): R; }
Function // declare function f(...): R;
};

// A method of a C++ class bound as a @cpp class.
struct Method
{
std::string name; // the TS name: the C++ name, `_2`, `_3`, ... for later overloads
std::string cppName; // the C++ name, for messages
int slot = -1; // a virtual method's vtable slot; -1 for a non-virtual one
std::string symbol; // a non-virtual method's mangled symbol
std::vector<Field> parameters;
TsType result;
bool nonTrivialReturn = false; // returns a record with a destructor or a copy constructor
std::string skipReason; // non-empty: left out, with a `// skipped:` line in its place
};

struct Decl
{
DeclKind kind = DeclKind::Function;
Expand All @@ -86,12 +103,17 @@ struct Decl
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
std::vector<Field> fields; // Struct, CppClass: 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

std::string qualifiedName; // CppClass: the C++ name with its namespaces (`nvrhi::IDevice`)
std::string base; // CppClass: the key of the base class, or empty
std::vector<Method> methods; // CppClass: the methods it introduces, in declaration order
std::string fieldsSkipReason; // CppClass: non-empty when its fields aren't declared
};

struct HeaderModel
Expand Down
29 changes: 28 additions & 1 deletion tslang/include/TsClangImporter/TypeMapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"

namespace clang
{
class Sema;
}
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringSet.h"

Expand Down Expand Up @@ -58,6 +63,26 @@ class TypeMapper
// namespace or a class, and not a template or a template specialization (std::cmatch).
bool isDeclarable(const clang::RecordDecl *record);

// Why a C++ class can't be a @cpp class ("" if it can): it has to be polymorphic, not a
// template, on a 64-bit target, with at most one base, itself bindable and not virtual - the
// layout a @cpp class has (the vtable pointer first, a base object the prefix of a derived one).
std::string classSkipReason(const clang::CXXRecordDecl *record);

// "class:nvrhi::IDevice"
static std::string classKey(const clang::CXXRecordDecl *record);

// A C++ record returned by value, as a method declared to return a tuple returns it: the
// record's fields, laid out as a tuple's, a pointer as `Opaque | null`. `nonTrivial` is set
// when C++ can't pass it in registers (a destructor or a copy constructor: @cppnontrivial).
TsType mapRecordResult(clang::QualType type, bool &nonTrivial);

// clang's semantic analysis, to complete a class template specialization only ever named as a
// result (`RefCountPtr<IBuffer> createBuffer(...)` doesn't instantiate it)
void setSema(clang::Sema *value)
{
sema = value;
}

// 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
Expand All @@ -67,15 +92,17 @@ class TypeMapper

private:
std::string referTo(const clang::TagDecl *tag);
TsType mapPointer(const clang::PointerType *pointer, Use use);
TsType mapPointer(clang::QualType pointee, 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;
clang::Sema *sema = nullptr;
llvm::DenseMap<const clang::RecordDecl *, std::string> recordReasons;
llvm::DenseMap<const clang::CXXRecordDecl *, std::string> classReasons;
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
Expand Down
18 changes: 18 additions & 0 deletions tslang/lib/TsClangImporter/BindingModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ TsType TsType::reference(TsType pointee)
return type;
}

TsType TsType::tuple(std::vector<std::string> names, std::vector<TsType> types)
{
TsType type;
type.kind = Kind::Tuple;
type.fieldNames = std::move(names);
type.pointee = std::move(types);
return type;
}

TsType TsType::skip(std::string reason)
{
TsType type;
Expand All @@ -47,6 +56,15 @@ std::string renderType(const TsType &type, const std::function<std::string(const
return nameOf(type.name);
case TsType::Kind::Reference:
return "Reference<" + renderType(type.pointee.front(), nameOf) + ">";
case TsType::Kind::Tuple: {
std::string fields;
for (size_t index = 0; index < type.pointee.size(); ++index)
{
fields += (index ? ", " : "") + type.fieldNames[index] + ": " + renderType(type.pointee[index], nameOf);
}

return "[" + fields + "]";
}
}

return type.name;
Expand Down
Loading
Loading