From e518c54641304648daa660266e70b03f8c92f26d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:35:41 +0100 Subject: [PATCH 1/9] tsbindgen: TsClangImporter library skeleton and binding model Co-Authored-By: Claude Opus 5.5 --- tslang/include/TsClangImporter/BindingModel.h | 99 +++++++++++++++++++ tslang/lib/CMakeLists.txt | 1 + tslang/lib/TsClangImporter/BindingModel.cpp | 96 ++++++++++++++++++ tslang/lib/TsClangImporter/CMakeLists.txt | 9 ++ tslang/unittests/CMakeLists.txt | 1 + .../TsClangImporter/BindingModelTest.cpp | 52 ++++++++++ .../unittests/TsClangImporter/CMakeLists.txt | 8 ++ 7 files changed, 266 insertions(+) create mode 100644 tslang/include/TsClangImporter/BindingModel.h create mode 100644 tslang/lib/TsClangImporter/BindingModel.cpp create mode 100644 tslang/lib/TsClangImporter/CMakeLists.txt create mode 100644 tslang/unittests/TsClangImporter/BindingModelTest.cpp create mode 100644 tslang/unittests/TsClangImporter/CMakeLists.txt diff --git a/tslang/include/TsClangImporter/BindingModel.h b/tslang/include/TsClangImporter/BindingModel.h new file mode 100644 index 000000000..5c792a171 --- /dev/null +++ b/tslang/include/TsClangImporter/BindingModel.h @@ -0,0 +1,99 @@ +#ifndef TSCLANGIMPORTER_BINDINGMODEL_H +#define TSCLANGIMPORTER_BINDINGMODEL_H + +#include +#include +#include + +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 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 &nameOf); + +// The declaration keys `type` refers to, in order, each once. +void collectKeys(const TsType &type, std::vector &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); + +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: ":" + std::string name; // the C name + bool inMainFile = false; + bool inSystemHeader = false; + std::string skipReason; // non-empty: left out, with a `// skipped:` line in its place + + std::vector fields; // Struct: fields; Function: parameters + TsType type; // Function: result; Typedef: aliased type; FixedEnum: underlying + bool varargs = false; // Function + std::vector enumerators; // Enum, FixedEnum + bool scoped = false; // FixedEnum from an `enum class` + std::string value; // Macro: a TS literal +}; + +struct HeaderModel +{ + std::vector decls; // in the order the header declares them +}; + +} // namespace tsbindgen + +#endif // TSCLANGIMPORTER_BINDINGMODEL_H diff --git a/tslang/lib/CMakeLists.txt b/tslang/lib/CMakeLists.txt index d4a39e7db..f041030ac 100644 --- a/tslang/lib/CMakeLists.txt +++ b/tslang/lib/CMakeLists.txt @@ -7,3 +7,4 @@ add_subdirectory(TypeScriptRuntime) add_subdirectory(TypeScriptAsyncRuntime) add_subdirectory(TypeScriptAliasPass) add_subdirectory(TypeScriptMemAllocPass) +add_subdirectory(TsClangImporter) diff --git a/tslang/lib/TsClangImporter/BindingModel.cpp b/tslang/lib/TsClangImporter/BindingModel.cpp new file mode 100644 index 000000000..8098382b8 --- /dev/null +++ b/tslang/lib/TsClangImporter/BindingModel.cpp @@ -0,0 +1,96 @@ +#include "TsClangImporter/BindingModel.h" + +#include + +namespace tsbindgen +{ + +TsType TsType::builtin(std::string name, std::string comment) +{ + TsType type; + type.kind = Kind::Builtin; + type.name = std::move(name); + type.comment = std::move(comment); + return type; +} + +TsType TsType::named(std::string key) +{ + TsType type; + type.kind = Kind::Named; + type.name = std::move(key); + return type; +} + +TsType TsType::reference(TsType pointee) +{ + TsType type; + type.kind = Kind::Reference; + type.pointee.push_back(std::move(pointee)); + return type; +} + +TsType TsType::skip(std::string reason) +{ + TsType type; + type.skipReason = std::move(reason); + return type; +} + +std::string renderType(const TsType &type, const std::function &nameOf) +{ + switch (type.kind) + { + case TsType::Kind::Builtin: + return type.name; + case TsType::Kind::Named: + return nameOf(type.name); + case TsType::Kind::Reference: + return "Reference<" + renderType(type.pointee.front(), nameOf) + ">"; + } + + return type.name; +} + +void collectKeys(const TsType &type, std::vector &keys) +{ + if (type.kind == TsType::Kind::Named && std::find(keys.begin(), keys.end(), type.name) == keys.end()) + { + keys.push_back(type.name); + } + + for (auto &pointee : type.pointee) + { + collectKeys(pointee, keys); + } +} + +std::string nameFromKey(const std::string &key) +{ + auto colon = key.find(':'); + return colon == std::string::npos ? key : key.substr(colon + 1); +} + +std::string escapeReserved(std::string name) +{ + // ECMAScript reserved words, including the strict-mode ones + static const char *const reserved[] = { + "await", "break", "case", "catch", "class", "const", "continue", "debugger", + "default", "delete", "do", "else", "enum", "export", "extends", "false", + "finally", "for", "function", "if", "implements", "import", "in", "instanceof", + "interface", "let", "new", "null", "package", "private", "protected", "public", + "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield"}; + + for (auto *word : reserved) + { + if (name == word) + { + return name + "_"; + } + } + + return name; +} + +} // namespace tsbindgen diff --git a/tslang/lib/TsClangImporter/CMakeLists.txt b/tslang/lib/TsClangImporter/CMakeLists.txt new file mode 100644 index 000000000..2794ed48d --- /dev/null +++ b/tslang/lib/TsClangImporter/CMakeLists.txt @@ -0,0 +1,9 @@ +# C header -> tslang bindings, as a library: tsbindgen is a thin command line over it, and v2's C++ +# wrapper generation is meant to reuse it. It links clang's frontend; tslang itself does not. +set(LLVM_LINK_COMPONENTS + Support + ) + +add_llvm_library(TsClangImporter + BindingModel.cpp + ) diff --git a/tslang/unittests/CMakeLists.txt b/tslang/unittests/CMakeLists.txt index de60fb9f3..6053bd81b 100644 --- a/tslang/unittests/CMakeLists.txt +++ b/tslang/unittests/CMakeLists.txt @@ -21,3 +21,4 @@ set_Options() add_subdirectory(Example) add_subdirectory(MLIRGen) +add_subdirectory(TsClangImporter) diff --git a/tslang/unittests/TsClangImporter/BindingModelTest.cpp b/tslang/unittests/TsClangImporter/BindingModelTest.cpp new file mode 100644 index 000000000..440ccf18c --- /dev/null +++ b/tslang/unittests/TsClangImporter/BindingModelTest.cpp @@ -0,0 +1,52 @@ +// The model's helpers: how a mapped type is spelled, which declarations it refers to, and the +// names that must not reach TS unchanged. + +#include "TsClangImporter/BindingModel.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen; + +TEST(BindingModel, RendersBuiltinNamedAndReference) +{ + auto nameOf = [](const std::string &key) { return "TS_" + nameFromKey(key); }; + + EXPECT_EQ(renderType(TsType::builtin("s32"), nameOf), "s32"); + EXPECT_EQ(renderType(TsType::named("struct:Point"), nameOf), "TS_Point"); + EXPECT_EQ(renderType(TsType::reference(TsType::reference(TsType::named("struct:Point"))), nameOf), + "Reference>"); +} + +TEST(BindingModel, CollectsEachKeyOnce) +{ + std::vector keys; + collectKeys(TsType::reference(TsType::named("struct:Point")), keys); + collectKeys(TsType::named("struct:Point"), keys); + collectKeys(TsType::named("enum:Color"), keys); + collectKeys(TsType::builtin("string"), keys); + + EXPECT_THAT(keys, testing::ElementsAre("struct:Point", "enum:Color")); +} + +TEST(BindingModel, KeysNameTheirDeclaration) +{ + EXPECT_EQ(nameFromKey("typedef:callback_t"), "callback_t"); + EXPECT_EQ(nameFromKey("plain"), "plain"); +} + +TEST(BindingModel, ReservedWordsGetASuffix) +{ + EXPECT_EQ(escapeReserved("delete"), "delete_"); + EXPECT_EQ(escapeReserved("function"), "function_"); + EXPECT_EQ(escapeReserved("yield"), "yield_"); + EXPECT_EQ(escapeReserved("value"), "value"); + EXPECT_EQ(escapeReserved("len"), "len"); +} + +TEST(BindingModel, SkippedTypeKeepsItsReason) +{ + auto type = TsType::skip("long double"); + EXPECT_TRUE(type.skipped()); + EXPECT_EQ(type.skipReason, "long double"); + EXPECT_FALSE(TsType::builtin("f64").skipped()); +} diff --git a/tslang/unittests/TsClangImporter/CMakeLists.txt b/tslang/unittests/TsClangImporter/CMakeLists.txt new file mode 100644 index 000000000..1a9eb4e89 --- /dev/null +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -0,0 +1,8 @@ +add_mlir_unittest(TsClangImporterTests + BindingModelTest.cpp +) + +target_link_libraries(TsClangImporterTests + PRIVATE + TsClangImporter +) From b6e5d4c4df546c597a891b55104c5671cb5aaaca Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:36:39 +0100 Subject: [PATCH 2/9] tsbindgen: parse C headers with clang and print tslang bindings Signed C integers map to sN (tslang's iN are signless), and a struct pointer that leads back to its own struct is Opaque: tslang cannot declare a self-referencing type. Co-Authored-By: Claude Opus 5.5 --- .../include/TsClangImporter/BindingPrinter.h | 35 ++ tslang/include/TsClangImporter/HeaderParser.h | 30 + tslang/include/TsClangImporter/TypeMapper.h | 82 +++ tslang/lib/TsClangImporter/BindingPrinter.cpp | 288 +++++++++ tslang/lib/TsClangImporter/CMakeLists.txt | 10 + tslang/lib/TsClangImporter/HeaderParser.cpp | 565 ++++++++++++++++++ tslang/lib/TsClangImporter/TypeMapper.cpp | 474 +++++++++++++++ .../unittests/TsClangImporter/CMakeLists.txt | 6 + .../TsClangImporter/ImporterTestHelper.h | 65 ++ .../TsClangImporter/ParseErrorTest.cpp | 48 ++ .../TsClangImporter/TypeMappingTest.cpp | 169 ++++++ 11 files changed, 1772 insertions(+) create mode 100644 tslang/include/TsClangImporter/BindingPrinter.h create mode 100644 tslang/include/TsClangImporter/HeaderParser.h create mode 100644 tslang/include/TsClangImporter/TypeMapper.h create mode 100644 tslang/lib/TsClangImporter/BindingPrinter.cpp create mode 100644 tslang/lib/TsClangImporter/HeaderParser.cpp create mode 100644 tslang/lib/TsClangImporter/TypeMapper.cpp create mode 100644 tslang/unittests/TsClangImporter/ImporterTestHelper.h create mode 100644 tslang/unittests/TsClangImporter/ParseErrorTest.cpp create mode 100644 tslang/unittests/TsClangImporter/TypeMappingTest.cpp diff --git a/tslang/include/TsClangImporter/BindingPrinter.h b/tslang/include/TsClangImporter/BindingPrinter.h new file mode 100644 index 000000000..ff5a53e8b --- /dev/null +++ b/tslang/include/TsClangImporter/BindingPrinter.h @@ -0,0 +1,35 @@ +#ifndef TSCLANGIMPORTER_BINDINGPRINTER_H +#define TSCLANGIMPORTER_BINDINGPRINTER_H + +#include "TsClangImporter/BindingModel.h" + +#include "llvm/Support/Error.h" + +#include +#include + +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 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 headerLines; // printed first, each as a `//` comment +}; + +struct PrintResult +{ + std::string text; + std::vector warnings; // one per skipped declaration, and per name kept unstripped +}; + +// Fails only for a filter that is not a valid glob. +llvm::Expected printBindings(const HeaderModel &model, const PrintOptions &options); + +} // namespace tsbindgen + +#endif // TSCLANGIMPORTER_BINDINGPRINTER_H diff --git a/tslang/include/TsClangImporter/HeaderParser.h b/tslang/include/TsClangImporter/HeaderParser.h new file mode 100644 index 000000000..213e3c715 --- /dev/null +++ b/tslang/include/TsClangImporter/HeaderParser.h @@ -0,0 +1,30 @@ +#ifndef TSCLANGIMPORTER_HEADERPARSER_H +#define TSCLANGIMPORTER_HEADERPARSER_H + +#include "TsClangImporter/BindingModel.h" + +#include "llvm/Support/Error.h" + +#include +#include +#include + +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 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> 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 parseHeader(const ParseInput &input); + +} // namespace tsbindgen + +#endif // TSCLANGIMPORTER_HEADERPARSER_H diff --git a/tslang/include/TsClangImporter/TypeMapper.h b/tslang/include/TsClangImporter/TypeMapper.h new file mode 100644 index 000000000..d99ae4ae9 --- /dev/null +++ b/tslang/include/TsClangImporter/TypeMapper.h @@ -0,0 +1,82 @@ +#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 +#include + +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 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); + + // 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> &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 recordReasons; + std::vector> referenced; + llvm::StringSet<> referencedKeys; + std::vector owners; // structs whose fields are being mapped +}; + +} // namespace tsbindgen + +#endif // TSCLANGIMPORTER_TYPEMAPPER_H diff --git a/tslang/lib/TsClangImporter/BindingPrinter.cpp b/tslang/lib/TsClangImporter/BindingPrinter.cpp new file mode 100644 index 000000000..5ee699eb7 --- /dev/null +++ b/tslang/lib/TsClangImporter/BindingPrinter.cpp @@ -0,0 +1,288 @@ +#include "TsClangImporter/BindingPrinter.h" + +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/GlobPattern.h" + +#include + +namespace tsbindgen +{ + +namespace +{ + +// The selected declarations: every seed, then whatever a non-skipped selected declaration uses. +std::vector selectDecls(const HeaderModel &model, const std::vector &globs) +{ + llvm::StringMap byKey; + for (size_t index = 0; index < model.decls.size(); ++index) + { + byKey[model.decls[index].key] = index; + } + + std::vector selected(model.decls.size(), false); + std::vector work; + auto mark = [&](size_t index) { + if (!selected[index]) + { + selected[index] = true; + work.push_back(index); + } + }; + + for (size_t index = 0; index < model.decls.size(); ++index) + { + auto &decl = model.decls[index]; + auto seed = globs.empty() ? decl.inMainFile + : llvm::any_of(globs, [&](const llvm::GlobPattern &glob) { return glob.match(decl.name); }); + if (seed) + { + mark(index); + } + } + + while (!work.empty()) + { + auto &decl = model.decls[work.back()]; + work.pop_back(); + if (!decl.skipReason.empty()) + { + continue; // printed as Opaque or not at all: it uses nothing + } + + std::vector keys; + for (auto &field : decl.fields) + { + collectKeys(field.type, keys); + } + + collectKeys(decl.type, keys); + for (auto &key : keys) + { + auto found = byKey.find(key); + if (found != byKey.end()) + { + mark(found->second); + } + } + } + + return selected; +} + +int group(DeclKind kind) +{ + switch (kind) + { + case DeclKind::Macro: + return 0; + case DeclKind::Function: + return 2; + default: + return 1; // types, in declaration order, before any function uses them + } +} + +} // namespace + +llvm::Expected printBindings(const HeaderModel &model, const PrintOptions &options) +{ + std::vector globs; + for (auto &filter : options.filters) + { + auto glob = llvm::GlobPattern::create(filter); + if (!glob) + { + return glob.takeError(); + } + + globs.push_back(std::move(*glob)); + } + + auto selected = selectDecls(model, globs); + PrintResult result; + if (llvm::none_of(selected, [](bool isSelected) { return isSelected; })) + { + result.warnings.push_back(options.filters.empty() + ? "nothing to emit: the input file itself declares nothing; pick " + "declarations from its includes with --filter" + : "nothing to emit: no declaration matches --filter"); + } + + // TS names: the C name, minus --strip-prefix unless that leaves nothing, a leading digit, or a + // name another declaration already has + llvm::StringMap tsNames; + llvm::StringMap taken; + for (size_t index = 0; index < model.decls.size(); ++index) + { + if (!selected[index]) + { + continue; + } + + auto &decl = model.decls[index]; + auto name = decl.name; + llvm::StringRef stripped(name); + if (!options.stripPrefix.empty() && stripped.consume_front(options.stripPrefix) && !stripped.empty() && + !std::isdigit(static_cast(stripped.front()))) + { + auto candidate = escapeReserved(stripped.str()); + if (taken.count(candidate)) + { + result.warnings.push_back("kept '" + name + "' unstripped: '" + candidate + "' is taken by '" + + taken[candidate] + "'"); + } + else + { + name = candidate; + } + } + + taken[name] = decl.name; + tsNames[decl.key] = name; + } + + auto nameOf = [&](const std::string &key) { + auto found = tsNames.find(key); + return found != tsNames.end() ? found->second : nameFromKey(key); + }; + + auto inNamespace = !options.namespaceName.empty(); + auto indent = std::string(inNamespace ? " " : ""); + auto exported = std::string(inNamespace ? "export " : ""); + std::string body; + auto line = [&](const std::string &text) { body += indent + text + "\n"; }; + auto hasFunctionPointer = false; + auto withComment = [&](const Field &field) { + auto text = field.name + ": " + renderType(field.type, nameOf); + if (!field.type.comment.empty()) + { + hasFunctionPointer |= field.type.functionPointer; + text += " /* " + field.type.comment + " */"; + } + + return text; + }; + + for (int pass = 0; pass < 3; ++pass) + { + for (size_t index = 0; index < model.decls.size(); ++index) + { + auto &decl = model.decls[index]; + if (!selected[index] || group(decl.kind) != pass) + { + continue; + } + + auto tsName = nameOf(decl.key); + if (!decl.skipReason.empty()) + { + line("// skipped: " + decl.name + " \xE2\x80\x94 " + decl.skipReason); + result.warnings.push_back("skipped " + decl.name + ": " + decl.skipReason); + // a skipped struct keeps its users: a pointer to it is Opaque + if (decl.kind == DeclKind::Struct) + { + line(exported + "type " + tsName + " = Opaque;"); + } + + continue; + } + + switch (decl.kind) + { + case DeclKind::Macro: + line(exported + "const " + tsName + " = " + decl.value + ";"); + break; + case DeclKind::OpaqueStruct: + line(exported + "type " + tsName + " = Opaque;"); + break; + case DeclKind::Struct: { + std::string fields; + for (auto &field : decl.fields) + { + fields += (fields.empty() ? "" : ", ") + withComment(field); + } + + line(exported + "type " + tsName + " = [" + fields + "];"); + break; + } + case DeclKind::Enum: { + std::string members; + for (auto &enumerator : decl.enumerators) + { + members += (members.empty() ? "" : ", ") + enumerator.name + " = " + enumerator.value; + } + + line(exported + "enum " + tsName + " { " + members + " }"); + break; + } + case DeclKind::FixedEnum: + line(exported + "type " + tsName + " = " + renderType(decl.type, nameOf) + ";"); + for (auto &enumerator : decl.enumerators) + { + auto constName = decl.scoped ? tsName + "_" + enumerator.name : enumerator.name; + line(exported + "const " + constName + " = " + enumerator.value + ";"); + } + + break; + case DeclKind::Typedef: { + auto text = exported + "type " + tsName + " = " + renderType(decl.type, nameOf) + ";"; + if (!decl.type.comment.empty()) + { + hasFunctionPointer |= decl.type.functionPointer; + text += " // " + decl.type.comment; + } + + line(text); + break; + } + case DeclKind::Function: { + std::string parameters; + for (auto ¶meter : decl.fields) + { + parameters += (parameters.empty() ? "" : ", ") + withComment(parameter); + } + + std::string decorators = decl.varargs ? "@varargs " : ""; + if (inNamespace) + { + decorators += "@linkname(\"" + decl.name + "\") "; + } + + line(decorators + exported + "declare function " + tsName + "(" + parameters + + "): " + renderType(decl.type, nameOf) + ";"); + break; + } + } + } + } + + for (auto &headerLine : options.headerLines) + { + result.text += "// " + headerLine + "\n"; + } + + if (hasFunctionPointer) + { + result.text += "// A function pointer is Opaque: pass a function without captures, as `fn as Opaque`.\n"; + } + + if (!result.text.empty()) + { + result.text += "\n"; + } + + if (inNamespace) + { + result.text += "namespace " + options.namespaceName + " {\n" + body + "}\n"; + } + else + { + result.text += body; + } + + return result; +} + +} // namespace tsbindgen diff --git a/tslang/lib/TsClangImporter/CMakeLists.txt b/tslang/lib/TsClangImporter/CMakeLists.txt index 2794ed48d..22990ad86 100644 --- a/tslang/lib/TsClangImporter/CMakeLists.txt +++ b/tslang/lib/TsClangImporter/CMakeLists.txt @@ -6,4 +6,14 @@ set(LLVM_LINK_COMPONENTS add_llvm_library(TsClangImporter BindingModel.cpp + TypeMapper.cpp + HeaderParser.cpp + BindingPrinter.cpp + + LINK_LIBS + clangTooling + clangFrontend + clangAST + clangLex + clangBasic ) diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp new file mode 100644 index 000000000..fce9cb078 --- /dev/null +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -0,0 +1,565 @@ +#include "TsClangImporter/HeaderParser.h" +#include "TsClangImporter/TypeMapper.h" + +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Basic/FileManager.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/FrontendAction.h" +#include "clang/Frontend/TextDiagnosticPrinter.h" +#include "clang/Lex/LiteralSupport.h" +#include "clang/Lex/MacroInfo.h" +#include "clang/Lex/PPCallbacks.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Tooling/Tooling.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/VirtualFileSystem.h" + +namespace tsbindgen +{ + +namespace +{ + +std::string escapeString(llvm::StringRef text) +{ + std::string escaped = "\""; + for (unsigned char c : text) + { + switch (c) + { + case '\\': + escaped += "\\\\"; + break; + case '"': + escaped += "\\\""; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + if (c < 0x20 || c == 0x7f) + { + static const char hex[] = "0123456789abcdef"; + escaped += "\\x"; + escaped += hex[c >> 4]; + escaped += hex[c & 0xf]; + } + else + { + escaped += static_cast(c); + } + } + } + + return escaped + "\""; +} + +// An object-like macro's value as a TS literal: an integer, a float or a string, optionally +// negated and parenthesized. "" for anything else. +std::string literalValue(clang::Preprocessor &preprocessor, llvm::ArrayRef tokens) +{ + while (tokens.size() >= 2 && tokens.front().is(clang::tok::l_paren) && tokens.back().is(clang::tok::r_paren)) + { + tokens = tokens.drop_front().drop_back(); + } + + if (!tokens.empty() && llvm::all_of(tokens, [](const clang::Token &token) { return token.is(clang::tok::string_literal); })) + { + clang::StringLiteralParser literal(tokens, preprocessor); + if (literal.hadError || !literal.isOrdinary()) + { + return ""; + } + + return escapeString(literal.GetString()); + } + + auto negative = tokens.size() == 2 && tokens.front().is(clang::tok::minus); + if (negative) + { + tokens = tokens.drop_front(); + } + + if (tokens.size() != 1 || !tokens.front().is(clang::tok::numeric_constant)) + { + return ""; + } + + llvm::SmallString<32> buffer; + auto invalid = false; + auto spelling = preprocessor.getSpelling(tokens.front(), buffer, &invalid); + if (invalid) + { + return ""; + } + + clang::NumericLiteralParser literal(spelling, tokens.front().getLocation(), preprocessor.getSourceManager(), + preprocessor.getLangOpts(), preprocessor.getTargetInfo(), + preprocessor.getDiagnostics()); + if (literal.hadError) + { + return ""; + } + + std::string sign = negative ? "-" : ""; + if (literal.isIntegerLiteral()) + { + llvm::APInt value(64, 0); + if (literal.GetIntegerValue(value)) + { + return ""; // does not fit in 64 bits + } + + llvm::SmallString<32> text; + value.toString(text, 10, /*Signed=*/false); + return sign + text.str().str(); + } + + if (literal.isFloatingLiteral()) + { + llvm::APFloat value(llvm::APFloat::IEEEdouble()); + literal.GetFloatValue(value, llvm::RoundingMode::NearestTiesToEven); + llvm::SmallString<32> text; + value.toString(text); + // "2" would be an integer literal in TS + if (text.find_first_of(".eE") == llvm::StringRef::npos) + { + text += ".0"; + } + + return sign + text.str().str(); + } + + return ""; +} + +class MacroCollector : public clang::PPCallbacks +{ + public: + MacroCollector(clang::Preprocessor &preprocessor, std::vector ¯os) + : preprocessor(preprocessor), macros(macros) + { + } + + void MacroDefined(const clang::Token &nameToken, const clang::MacroDirective *directive) override + { + auto *info = directive->getMacroInfo(); + auto location = info->getDefinitionLoc(); + auto &sourceManager = preprocessor.getSourceManager(); + if (info->isBuiltinMacro() || location.isInvalid() || sourceManager.isWrittenInBuiltinFile(location) || + sourceManager.isWrittenInCommandLineFile(location)) + { + return; + } + + auto name = nameToken.getIdentifierInfo()->getName().str(); + + Decl macro; + macro.kind = DeclKind::Macro; + macro.key = "macro:" + name; + macro.name = name; + macro.inMainFile = sourceManager.isInMainFile(location); + macro.inSystemHeader = sourceManager.isInSystemHeader(location); + if (info->isFunctionLike()) + { + macro.skipReason = "function-like macro"; + } + else if (info->getNumTokens() == 0) + { + forget(name); // an include guard or a feature switch: nothing to bind + return; + } + else + { + macro.value = literalValue(preprocessor, info->tokens()); + if (macro.value.empty()) + { + macro.skipReason = "macro is not a literal"; + } + } + + forget(name); + macros.push_back(std::move(macro)); + } + + void MacroUndefined(const clang::Token &nameToken, const clang::MacroDefinition &, + const clang::MacroDirective *) override + { + forget(nameToken.getIdentifierInfo()->getName().str()); + } + + private: + void forget(const std::string &name) + { + llvm::erase_if(macros, [&](const Decl ¯o) { return macro.name == name; }); + } + + clang::Preprocessor &preprocessor; + std::vector ¯os; +}; + +class Collector : public clang::RecursiveASTVisitor +{ + public: + Collector(clang::ASTContext &context, HeaderModel &model) : context(context), mapper(context), model(model) + { + } + + bool VisitRecordDecl(clang::RecordDecl *record) + { + if (record->isImplicit() || !atFileScope(record)) + { + return true; + } + + if (auto *cxxRecord = llvm::dyn_cast(record); + cxxRecord && (cxxRecord->getDescribedClassTemplate() || + llvm::isa(cxxRecord))) + { + return true; + } + + if (TypeMapper::tagName(record).empty()) + { + return true; // only reachable as a member's type, which skips that member's struct + } + + auto key = TypeMapper::recordKey(record); + if (!keys.insert(key).second) + { + return true; + } + + // completed below, once the whole file is seen: its definition may come later + pendingRecords.push_back({model.decls.size(), record}); + add(DeclKind::OpaqueStruct, key, TypeMapper::tagName(record), record); + return true; + } + + bool VisitEnumDecl(clang::EnumDecl *enumDecl) + { + auto *definition = enumDecl->getDefinition(); + if (definition != enumDecl || !atFileScope(enumDecl)) + { + return true; + } + + auto name = TypeMapper::tagName(enumDecl); + if (name.empty()) + { + std::string listed; + for (auto *enumerator : enumDecl->enumerators()) + { + listed += (listed.empty() ? "" : ", ") + enumerator->getName().str(); + } + + auto &decl = add(DeclKind::Enum, "enum:{" + listed + "}", "enum { " + listed + " }", enumDecl); + decl.skipReason = "anonymous enum"; + return true; + } + + auto key = TypeMapper::enumKey(enumDecl); + if (!keys.insert(key).second) + { + return true; + } + + auto plain = mapper.isPlainEnum(enumDecl); + auto &decl = add(plain ? DeclKind::Enum : DeclKind::FixedEnum, key, name, enumDecl); + decl.scoped = enumDecl->isScoped(); + if (!plain) + { + decl.type = mapper.map(enumDecl->getIntegerType(), TypeMapper::Use::Field); + decl.skipReason = decl.type.skipReason; + } + + for (auto *enumerator : enumDecl->enumerators()) + { + llvm::SmallString<32> value; + enumerator->getInitVal().toString(value, 10); + decl.enumerators.push_back({enumerator->getName().str(), value.str().str()}); + } + + return true; + } + + bool VisitTypedefNameDecl(clang::TypedefNameDecl *typedefDecl) + { + if (isSystem(typedefDecl) || !atFileScope(typedefDecl) || TypeMapper::namesItsTag(typedefDecl)) + { + return true; + } + + auto name = typedefDecl->getName(); + if (name == "size_t" || name == "ssize_t" || name == "ptrdiff_t" || name == "intptr_t" || + name == "uintptr_t") + { + return true; + } + + auto key = TypeMapper::typedefKey(typedefDecl); + if (!keys.insert(key).second) + { + return true; + } + + auto &decl = add(DeclKind::Typedef, key, name.str(), typedefDecl); + decl.type = mapper.map(typedefDecl->getUnderlyingType(), TypeMapper::Use::Alias); + decl.skipReason = decl.type.skipReason; + return true; + } + + bool VisitFunctionDecl(clang::FunctionDecl *function) + { + if (llvm::isa(function) || function->isImplicit() || + function->getTemplatedKind() != clang::FunctionDecl::TK_NonTemplate || !atFileScope(function)) + { + return true; + } + + // C++ proper is v2: only what has C linkage has a C symbol + if (context.getLangOpts().CPlusPlus && !function->isExternC()) + { + return true; + } + + auto name = function->getName().str(); + auto key = "fn:" + name; + if (!keys.insert(key).second) + { + return true; + } + + auto &decl = add(DeclKind::Function, key, name, function); + if (function->getStorageClass() == clang::SC_Static) + { + decl.skipReason = function->isInlineSpecified() ? "static inline function: no symbol to link against" + : "static function: no symbol to link against"; + return true; + } + + auto *prototype = function->getType()->getAs(); + if (!prototype) + { + decl.skipReason = "function without a prototype"; + return true; + } + + decl.varargs = prototype->isVariadic(); + for (unsigned index = 0; index < function->getNumParams(); ++index) + { + auto *parameter = function->getParamDecl(index); + auto parameterName = parameter->getName().empty() ? "p" + std::to_string(index) + : escapeReserved(parameter->getName().str()); + auto type = mapper.map(parameter->getType(), TypeMapper::Use::Parameter); + if (type.skipped()) + { + decl.skipReason = "parameter '" + parameterName + "': " + type.skipReason; + return true; + } + + decl.fields.push_back({parameterName, type}); + } + + decl.type = mapper.map(function->getReturnType(), TypeMapper::Use::Result); + if (decl.type.skipped()) + { + decl.skipReason = "result: " + decl.type.skipReason; + } + + return true; + } + + // After the traversal: complete the structs (a definition may follow the first mention), and + // add every struct or enum a mapped type names that the traversal did not reach, until both + // stop changing - completing a struct maps its fields, which can name more. + void finish() + { + size_t completed = 0; + while (true) + { + std::vector missing; + for (auto &[key, tag] : mapper.referencedTags()) + { + if (!keys.count(key)) + { + missing.push_back(tag); + } + } + + for (auto *tag : missing) + { + if (auto *record = llvm::dyn_cast(tag)) + { + VisitRecordDecl(const_cast(record)); + } + else if (auto *enumDecl = llvm::cast(tag)->getDefinition()) + { + VisitEnumDecl(enumDecl); + } + } + + if (completed == pendingRecords.size()) + { + break; + } + + for (; completed < pendingRecords.size(); ++completed) + { + completeRecord(pendingRecords[completed].first, pendingRecords[completed].second); + } + } + } + + private: + void completeRecord(size_t index, const clang::RecordDecl *record) + { + auto *definition = record->getDefinition(); + if (!definition) + { + return; // stays OpaqueStruct + } + + auto &decl = model.decls[index]; + locate(decl, definition); + decl.kind = DeclKind::Struct; + decl.skipReason = mapper.recordSkipReason(definition); + if (!decl.skipReason.empty()) + { + return; + } + + decl.fields = mapper.mapFields(definition); + } + + bool atFileScope(const clang::Decl *decl) + { + // in C, a struct declared inside another is still file-scoped + return !context.getLangOpts().CPlusPlus || decl->getDeclContext()->getRedeclContext()->isTranslationUnit(); + } + + bool isSystem(const clang::Decl *decl) + { + auto &sourceManager = context.getSourceManager(); + return sourceManager.isInSystemHeader(sourceManager.getExpansionLoc(decl->getLocation())); + } + + void locate(Decl &decl, const clang::Decl *at) + { + auto &sourceManager = context.getSourceManager(); + auto location = sourceManager.getExpansionLoc(at->getLocation()); + decl.inMainFile = sourceManager.isInMainFile(location); + decl.inSystemHeader = sourceManager.isInSystemHeader(location); + } + + Decl &add(DeclKind kind, std::string key, std::string name, const clang::Decl *at) + { + Decl decl; + decl.kind = kind; + decl.key = std::move(key); + decl.name = std::move(name); + locate(decl, at); + model.decls.push_back(std::move(decl)); + return model.decls.back(); + } + + clang::ASTContext &context; + TypeMapper mapper; + HeaderModel &model; + llvm::StringSet<> keys; + std::vector> pendingRecords; +}; + +class ImportConsumer : public clang::ASTConsumer +{ + public: + explicit ImportConsumer(HeaderModel &model) : model(model) + { + } + + void HandleTranslationUnit(clang::ASTContext &context) override + { + Collector collector(context, model); + collector.TraverseDecl(context.getTranslationUnitDecl()); + collector.finish(); + } + + private: + HeaderModel &model; +}; + +class ImportAction : public clang::ASTFrontendAction +{ + public: + ImportAction(HeaderModel &model, std::vector ¯os) : model(model), macros(macros) + { + } + + bool BeginSourceFileAction(clang::CompilerInstance &compiler) override + { + auto &preprocessor = compiler.getPreprocessor(); + preprocessor.addPPCallbacks(std::make_unique(preprocessor, macros)); + return true; + } + + std::unique_ptr CreateASTConsumer(clang::CompilerInstance &, llvm::StringRef) override + { + return std::make_unique(model); + } + + private: + HeaderModel &model; + std::vector ¯os; +}; + +} // namespace + +llvm::Expected parseHeader(const ParseInput &input) +{ + // What clang::tooling::runToolOnCodeWithArgs does, but with our own diagnostic consumer: an + // error in the arguments (`-- --bad-flag`) goes to a diagnostics engine whose errors that + // function never checks, and the run would "succeed" on a command line clang rejected. + auto overlay = llvm::makeIntrusiveRefCnt(llvm::vfs::getRealFileSystem()); + auto memory = llvm::makeIntrusiveRefCnt(); + overlay->pushOverlay(memory); + memory->addFile(input.fileName, 0, llvm::MemoryBuffer::getMemBufferCopy(input.code, input.fileName)); + for (auto &[path, content] : input.virtualFiles) + { + memory->addFile(path, 0, llvm::MemoryBuffer::getMemBufferCopy(content, path)); + } + + auto files = llvm::makeIntrusiveRefCnt(clang::FileSystemOptions(), overlay); + + std::vector commandLine = {"tsbindgen", "-fsyntax-only"}; + commandLine.insert(commandLine.end(), input.args.begin(), input.args.end()); + commandLine.push_back(input.fileName); + + HeaderModel model; + std::vector macros; + clang::tooling::ToolInvocation invocation(commandLine, std::make_unique(model, macros), files.get()); + + clang::DiagnosticOptions diagnosticOptions; + clang::TextDiagnosticPrinter diagnostics(llvm::errs(), diagnosticOptions); + invocation.setDiagnosticConsumer(&diagnostics); + + if (!invocation.run() || diagnostics.getNumErrors() > 0) + { + return llvm::createStringError("clang could not parse '" + input.fileName + "'"); + } + + model.decls.insert(model.decls.begin(), macros.begin(), macros.end()); + return model; +} + +} // namespace tsbindgen diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp new file mode 100644 index 000000000..bc2f49e63 --- /dev/null +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -0,0 +1,474 @@ +#include "TsClangImporter/TypeMapper.h" + +#include "clang/AST/DeclCXX.h" +#include "clang/AST/RecordLayout.h" +#include "clang/Basic/SourceManager.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/Support/MathExtras.h" + +namespace tsbindgen +{ + +namespace +{ + +bool isIndexTypedef(llvm::StringRef name) +{ + return name == "size_t" || name == "ssize_t" || name == "ptrdiff_t" || name == "intptr_t" || + name == "uintptr_t"; +} + +bool isPlainChar(clang::QualType canonical) +{ + return canonical->isSpecificBuiltinType(clang::BuiltinType::Char_S) || + canonical->isSpecificBuiltinType(clang::BuiltinType::Char_U); +} + +} // namespace + +std::string TypeMapper::tagName(const clang::TagDecl *tag) +{ + if (!tag->getName().empty()) + { + return tag->getName().str(); + } + + if (auto *typedefDecl = tag->getTypedefNameForAnonDecl()) + { + return typedefDecl->getName().str(); + } + + return ""; +} + +std::string TypeMapper::recordKey(const clang::RecordDecl *record) +{ + return "struct:" + tagName(record); +} + +std::string TypeMapper::enumKey(const clang::EnumDecl *enumDecl) +{ + return "enum:" + tagName(enumDecl); +} + +std::string TypeMapper::typedefKey(const clang::TypedefNameDecl *typedefDecl) +{ + return "typedef:" + typedefDecl->getName().str(); +} + +bool TypeMapper::namesItsTag(const clang::TypedefNameDecl *typedefDecl) +{ + auto *tag = typedefDecl->getUnderlyingType().getCanonicalType()->getAsTagDecl(); + if (!tag) + { + return false; + } + + if (auto *anonName = tag->getTypedefNameForAnonDecl()) + { + return anonName->getCanonicalDecl() == typedefDecl->getCanonicalDecl(); + } + + return tag->getName() == typedefDecl->getName(); +} + +bool TypeMapper::isPlainEnum(const clang::EnumDecl *enumDecl) +{ + auto integer = enumDecl->getIntegerType(); + if (integer.isNull()) + { + return true; + } + + auto canonical = integer.getCanonicalType(); + return context.getIntWidth(canonical) == 32 && + (!enumDecl->isFixed() || canonical->isSpecificBuiltinType(clang::BuiltinType::Int)); +} + +std::string TypeMapper::referTo(const clang::TagDecl *tag) +{ + auto key = llvm::isa(tag) ? enumKey(llvm::cast(tag)) + : recordKey(llvm::cast(tag)); + if (referencedKeys.insert(key).second) + { + referenced.push_back({key, tag}); + } + + return key; +} + +bool TypeMapper::isSystem(const clang::Decl *decl) +{ + auto &sourceManager = context.getSourceManager(); + return sourceManager.isInSystemHeader(sourceManager.getExpansionLoc(decl->getLocation())); +} + +TsType TypeMapper::map(clang::QualType type, Use use) +{ + type = type.getUnqualifiedType(); + + // an array or function parameter, adjusted to a pointer + if (auto *decayed = type->getAs()) + { + return map(decayed->getDecayedType(), use); + } + + if (auto *typedefType = type->getAs()) + { + auto *typedefDecl = typedefType->getDecl(); + if (isIndexTypedef(typedefDecl->getName())) + { + return TsType::builtin("index"); + } + + // a system header's typedefs (int32_t, uint8_t, ...) are looked through: their widths are + // what matter, and the header is not emitted + if (isSystem(typedefDecl)) + { + return map(typedefDecl->getUnderlyingType(), use); + } + + auto aliased = map(typedefDecl->getUnderlyingType(), use); + if (aliased.skipped() || namesItsTag(typedefDecl) || leadsToOwner(typedefDecl->getUnderlyingType())) + { + return aliased; + } + + return TsType::named(typedefKey(typedefDecl)); + } + + auto canonical = type.getCanonicalType().getUnqualifiedType(); + if (canonical->isVoidType()) + { + return use == Use::Result ? TsType::builtin("void") : TsType::skip("void"); + } + + if (auto *pointer = type->getAs()) + { + return mapPointer(pointer, use); + } + + if (canonical->isBuiltinType()) + { + return mapBuiltin(canonical, use); + } + + if (auto *enumType = canonical->getAs()) + { + auto *enumDecl = enumType->getDecl()->getDefinitionOrSelf(); + if (tagName(enumDecl).empty()) + { + // an anonymous enum's values are plain integers of its underlying type + return mapBuiltin(enumDecl->getIntegerType().getCanonicalType(), use); + } + + return TsType::named(referTo(enumDecl)); + } + + if (auto *record = canonical->getAsRecordDecl()) + { + if (use == Use::Parameter || use == Use::Result) + { + return TsType::skip(record->isUnion() ? "union passed by value" : "struct passed by value"); + } + + auto name = tagName(record); + if (name.empty()) + { + return TsType::skip("anonymous struct"); + } + + if (use == Use::Alias) + { + return TsType::named(referTo(record)); + } + + auto *definition = record->getDefinition(); + if (!definition) + { + return TsType::skip("incomplete struct '" + name + "'"); + } + + auto reason = recordSkipReason(definition); + if (!reason.empty()) + { + return TsType::skip("'" + name + "' is skipped (" + reason + ")"); + } + + return TsType::named(referTo(record)); + } + + if (canonical->isArrayType()) + { + return TsType::skip("array"); + } + + if (canonical->isFunctionType()) + { + return TsType::skip("function type"); + } + + return TsType::skip("unsupported type '" + type.getAsString() + "'"); +} + +TsType TypeMapper::mapBuiltin(clang::QualType canonical, Use use) +{ + auto *builtin = canonical->castAs(); + switch (builtin->getKind()) + { + case clang::BuiltinType::Bool: + return TsType::builtin("boolean"); + case clang::BuiltinType::Float: + return TsType::builtin("f32"); + case clang::BuiltinType::Double: + return TsType::builtin("f64"); + case clang::BuiltinType::LongDouble: + return TsType::skip("long double"); + default: + break; + } + + if (canonical->isIntegerType()) + { + auto width = context.getIntWidth(canonical); + if (width == 8 || width == 16 || width == 32 || width == 64) + { + return TsType::builtin((canonical->isSignedIntegerType() ? "s" : "u") + std::to_string(width)); + } + + return TsType::skip(std::to_string(width) + "-bit integer"); + } + + return TsType::skip("unsupported type '" + canonical.getAsString() + "'"); +} + +TsType TypeMapper::mapPointer(const clang::PointerType *pointer, Use use) +{ + auto pointee = pointer->getPointeeType(); + auto canonicalPointee = pointee.getCanonicalType().getUnqualifiedType(); + + if (isPlainChar(canonicalPointee)) + { + return TsType::builtin("string"); + } + + if (canonicalPointee->isVoidType()) + { + return TsType::builtin("Opaque"); + } + + if (canonicalPointee->isFunctionType()) + { + auto *function = pointee->getAs(); + auto type = TsType::builtin("Opaque", function ? signatureComment(function) : "(...) => ?"); + type.functionPointer = true; + return type; + } + + if (auto *record = canonicalPointee->getAsRecordDecl()) + { + if (tagName(record).empty()) + { + return TsType::skip("pointer to an anonymous struct"); + } + + if (leadsToOwner(pointee)) + { + return TsType::builtin("Opaque", "Reference<" + tagName(record) + ">: a type cannot refer to itself"); + } + + // incomplete, a C++ class, or a struct that is itself skipped: the struct is emitted as + // `type S = Opaque;`, and a pointer to it is that S + auto *definition = record->getDefinition(); + if (!definition || !recordSkipReason(definition).empty()) + { + return TsType::named(referTo(record)); + } + + return TsType::reference(TsType::named(referTo(record))); + } + + auto inner = map(pointee, Use::Field); + if (inner.skipped()) + { + return TsType::skip("pointer to " + inner.skipReason); + } + + return TsType::reference(inner); +} + +std::string TypeMapper::signatureComment(const clang::FunctionProtoType *function) +{ + std::string text = "("; + for (unsigned index = 0; index < function->getNumParams(); ++index) + { + auto type = map(function->getParamType(index), Use::Parameter); + text += (index ? ", p" : "p") + std::to_string(index) + ": " + + (type.skipped() ? "?" : renderType(type, nameFromKey)); + } + + if (function->isVariadic()) + { + text += function->getNumParams() ? ", ..." : "..."; + } + + auto result = map(function->getReturnType(), Use::Result); + return text + ") => " + (result.skipped() ? "?" : renderType(result, nameFromKey)); +} + +std::string TypeMapper::recordSkipReason(const clang::RecordDecl *record) +{ + auto found = recordReasons.find(record); + if (found != recordReasons.end()) + { + return found->second; + } + + // a struct that points to itself is mappable while this runs + recordReasons[record] = ""; + owners.push_back(record); + + auto reason = [&]() -> std::string { + if (record->isUnion()) + { + return "union"; + } + + if (auto *cxxRecord = llvm::dyn_cast(record); cxxRecord && !cxxRecord->isCLike()) + { + return "C++ class"; + } + + if (record->field_empty()) + { + return "empty struct"; + } + + // a named tuple is laid out with natural alignment, field after field; anything else would + // put the fields where C does not + const auto &layout = context.getASTRecordLayout(record); + uint64_t offset = 0; + uint64_t maxAlign = 8; + unsigned index = 0; + for (auto *field : record->fields()) + { + auto fieldName = field->getName().str(); + if (field->isBitField()) + { + return "bitfield '" + fieldName + "'"; + } + + if (fieldName.empty()) + { + return "anonymous member"; + } + + auto type = map(field->getType(), Use::Field); + if (type.skipped()) + { + return "field '" + fieldName + "': " + type.skipReason; + } + + auto align = context.getTypeAlign(field->getType()); + offset = llvm::alignTo(offset, align); + if (layout.getFieldOffset(index) != offset) + { + return "packed or over-aligned layout"; + } + + offset += context.getTypeSize(field->getType()); + maxAlign = std::max(maxAlign, align); + ++index; + } + + if (llvm::alignTo(offset, maxAlign) != static_cast(context.toBits(layout.getSize()))) + { + return "packed or over-aligned layout"; + } + + return ""; + }(); + + owners.pop_back(); + recordReasons[record] = reason; + return reason; +} + +std::vector TypeMapper::mapFields(const clang::RecordDecl *definition) +{ + owners.push_back(definition); + std::vector fields; + for (auto *field : definition->fields()) + { + fields.push_back({escapeReserved(field->getName().str()), map(field->getType(), Use::Field)}); + } + + owners.pop_back(); + return fields; +} + +const clang::RecordDecl *TypeMapper::recordBehind(clang::QualType type) +{ + auto canonical = type.getCanonicalType(); + while (true) + { + if (auto *pointer = canonical->getAs()) + { + canonical = pointer->getPointeeType().getCanonicalType(); + } + else if (auto *array = context.getAsArrayType(canonical)) + { + canonical = array->getElementType().getCanonicalType(); + } + else + { + break; + } + } + + auto *record = canonical->getAsRecordDecl(); + return record ? record->getDefinition() : nullptr; +} + +// Whether `type` reaches the struct whose fields are being mapped, through any chain of fields. +bool TypeMapper::leadsToOwner(clang::QualType type) +{ + if (owners.empty()) + { + return false; + } + + llvm::SmallPtrSet visited; + std::vector work; + if (auto *record = recordBehind(type)) + { + work.push_back(record); + } + + while (!work.empty()) + { + auto *record = work.back(); + work.pop_back(); + if (record == owners.back()) + { + return true; + } + + if (!visited.insert(record).second) + { + continue; + } + + for (auto *field : record->fields()) + { + if (auto *next = recordBehind(field->getType())) + { + work.push_back(next); + } + } + } + + return false; +} + +} // namespace tsbindgen diff --git a/tslang/unittests/TsClangImporter/CMakeLists.txt b/tslang/unittests/TsClangImporter/CMakeLists.txt index 1a9eb4e89..210945da9 100644 --- a/tslang/unittests/TsClangImporter/CMakeLists.txt +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -1,8 +1,14 @@ add_mlir_unittest(TsClangImporterTests BindingModelTest.cpp + TypeMappingTest.cpp + ParseErrorTest.cpp ) target_link_libraries(TsClangImporterTests PRIVATE TsClangImporter ) + +# clang's own headers (stdint.h, stddef.h) for the hermetic -ffreestanding -nostdlibinc parses +target_compile_definitions(TsClangImporterTests PRIVATE + TSBINDGEN_TEST_RESOURCE_DIR="${LLVM_LIBRARY_DIR}/clang/${LLVM_VERSION_MAJOR}") diff --git a/tslang/unittests/TsClangImporter/ImporterTestHelper.h b/tslang/unittests/TsClangImporter/ImporterTestHelper.h new file mode 100644 index 000000000..dc6c86dd0 --- /dev/null +++ b/tslang/unittests/TsClangImporter/ImporterTestHelper.h @@ -0,0 +1,65 @@ +#ifndef TSCLANGIMPORTER_UNITTESTS_IMPORTERTESTHELPER_H +#define TSCLANGIMPORTER_UNITTESTS_IMPORTERTESTHELPER_H + +#include "TsClangImporter/BindingPrinter.h" +#include "TsClangImporter/HeaderParser.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +namespace tsbindgen_test +{ + +inline const char *const windowsTarget = "x86_64-pc-windows-msvc"; +inline const char *const linuxTarget = "x86_64-linux-gnu"; + +struct Generated +{ + std::string text; + std::vector warnings; +}; + +// Parses `header` as the file input.h and prints its bindings. Hermetic: -ffreestanding and +// -nostdlibinc leave only clang's own headers (stdint.h, stddef.h, ...), from the resource directory +// of the LLVM the tests were built with, so no system SDK is needed and one host checks any target. +// `files` are extra headers input.h can include; a path under "sys/" is a system header. +inline Generated generate(const std::string &header, tsbindgen::PrintOptions options = {}, + const std::string &target = windowsTarget, + std::vector> files = {}, + const std::string &language = "c") +{ + tsbindgen::ParseInput input; + input.code = header; + input.fileName = "input.h"; + input.args = {"-x", language, "--target=" + target, "-ffreestanding", "-nostdlibinc", "-isystem", "sys", + "-resource-dir", TSBINDGEN_TEST_RESOURCE_DIR}; + input.virtualFiles = std::move(files); + + auto model = tsbindgen::parseHeader(input); + if (!model) + { + ADD_FAILURE() << llvm::toString(model.takeError()); + return {}; + } + + auto printed = tsbindgen::printBindings(*model, options); + if (!printed) + { + ADD_FAILURE() << llvm::toString(printed.takeError()); + return {}; + } + + return {printed->text, printed->warnings}; +} + +inline std::string text(const std::string &header, const std::string &target = windowsTarget) +{ + return generate(header, {}, target).text; +} + +} // namespace tsbindgen_test + +#endif // TSCLANGIMPORTER_UNITTESTS_IMPORTERTESTHELPER_H diff --git a/tslang/unittests/TsClangImporter/ParseErrorTest.cpp b/tslang/unittests/TsClangImporter/ParseErrorTest.cpp new file mode 100644 index 000000000..0ef5c1f98 --- /dev/null +++ b/tslang/unittests/TsClangImporter/ParseErrorTest.cpp @@ -0,0 +1,48 @@ +// A header clang rejects, or a command line clang rejects, is an error - never an empty success. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +namespace +{ + +llvm::Expected parse(const std::string &code, std::vector extraArgs = {}) +{ + tsbindgen::ParseInput input; + input.code = code; + input.fileName = "input.h"; + input.args = {"-x", "c", "--target=x86_64-pc-windows-msvc", "-ffreestanding", "-nostdlibinc", "-resource-dir", + TSBINDGEN_TEST_RESOURCE_DIR}; + input.args.insert(input.args.end(), extraArgs.begin(), extraArgs.end()); + return tsbindgen::parseHeader(input); +} + +} // namespace + +TEST(ParseError, SyntaxError) +{ + auto model = parse("int broken(\n"); + ASSERT_FALSE(static_cast(model)); + EXPECT_THAT(llvm::toString(model.takeError()), testing::HasSubstr("clang could not parse 'input.h'")); +} + +TEST(ParseError, MissingInclude) +{ + auto model = parse("#include \"nowhere.h\"\nint f(void);\n"); + ASSERT_FALSE(static_cast(model)); + llvm::consumeError(model.takeError()); +} + +TEST(ParseError, ArgumentClangRejects) +{ + auto model = parse("int f(void);\n", {"--no-such-clang-flag"}); + ASSERT_FALSE(static_cast(model)); + llvm::consumeError(model.takeError()); +} + +TEST(ParseError, WarningsAreNotErrors) +{ + auto model = parse("int f(struct Late *l);\n"); + ASSERT_TRUE(static_cast(model)) << llvm::toString(model.takeError()); +} diff --git a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp new file mode 100644 index 000000000..a9c5845a7 --- /dev/null +++ b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp @@ -0,0 +1,169 @@ +// One test per row of the spec's type-mapping table. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::HasSubstr; +using testing::Not; + +TEST(TypeMapping, IntegersHaveTheTargetsExactWidth) +{ + auto header = "#include \n" + "int f(signed char a, unsigned short b, long c, unsigned long long d, int8_t e, uint32_t g);\n"; + + EXPECT_THAT(text(header, windowsTarget), + HasSubstr("declare function f(a: s8, b: u16, c: s32, d: u64, e: s8, g: u32): s32;")); + // C long is 32-bit on Windows and 64-bit on Linux: never `long` in the output + EXPECT_THAT(text(header, linuxTarget), + HasSubstr("declare function f(a: s8, b: u16, c: s64, d: u64, e: s8, g: u32): s32;")); +} + +TEST(TypeMapping, StructFieldsHaveTheTargetsWidths) +{ + auto header = "struct Sized { long a; char b; };\n"; + EXPECT_THAT(text(header, windowsTarget), HasSubstr("type Sized = [a: s32, b: s8];")); + EXPECT_THAT(text(header, linuxTarget), HasSubstr("type Sized = [a: s64, b: s8];")); +} + +TEST(TypeMapping, SizeTypesAreIndex) +{ + EXPECT_THAT(text("#include \n#include \n" + "size_t f(ptrdiff_t a, intptr_t b, uintptr_t c);\n"), + HasSubstr("declare function f(a: index, b: index, c: index): index;")); +} + +TEST(TypeMapping, FloatsAndBool) +{ + EXPECT_THAT(text("#include \nbool f(float a, double b, _Bool c);\n"), + HasSubstr("declare function f(a: f32, b: f64, c: boolean): boolean;")); +} + +TEST(TypeMapping, CharPointersAreStrings) +{ + EXPECT_THAT(text("const char *f(char *a, const char *b, char **c);\n"), + HasSubstr("declare function f(a: string, b: string, c: Reference): string;")); +} + +TEST(TypeMapping, PointerToScalarOrStructIsReference) +{ + auto out = text("struct Point { int x; int y; };\nvoid f(int *a, struct Point *p, double *d);\n"); + EXPECT_THAT(out, HasSubstr("type Point = [x: s32, y: s32];")); + EXPECT_THAT(out, HasSubstr("declare function f(a: Reference, p: Reference, d: Reference): void;")); +} + +TEST(TypeMapping, VoidPointerAndIncompleteStructAreOpaque) +{ + auto out = text("typedef struct Counter Counter;\nvoid *f(void *a, Counter *c, struct Other *o);\n"); + EXPECT_THAT(out, HasSubstr("type Counter = Opaque;")); + EXPECT_THAT(out, HasSubstr("type Other = Opaque;")); + EXPECT_THAT(out, HasSubstr("declare function f(a: Opaque, c: Counter, o: Other): Opaque;")); +} + +TEST(TypeMapping, PointerToCxxClassIsOpaque) +{ + auto out = generate("class Engine { public: virtual ~Engine(); int power; };\n" + "extern \"C\" int engine_power(Engine *e);\n", + {}, windowsTarget, {}, "c++") + .text; + EXPECT_THAT(out, HasSubstr("type Engine = Opaque;")); + EXPECT_THAT(out, HasSubstr("declare function engine_power(e: Engine): s32;")); +} + +TEST(TypeMapping, FunctionPointerIsOpaqueWithItsSignature) +{ + auto out = text("int apply(int (*fn)(int, const char *), int v);\ntypedef void (*callback_t)(void *);\n" + "void on(callback_t cb);\n"); + EXPECT_THAT(out, HasSubstr("declare function apply(fn: Opaque /* (p0: s32, p1: string) => s32 */, v: s32): s32;")); + EXPECT_THAT(out, HasSubstr("type callback_t = Opaque; // (p0: Opaque) => void")); + EXPECT_THAT(out, HasSubstr("declare function on(cb: callback_t): void;")); + EXPECT_THAT(out, HasSubstr("pass a function without captures, as `fn as Opaque`")); +} + +TEST(TypeMapping, CompleteStructIsANamedTuple) +{ + EXPECT_THAT(text("#include \ntypedef struct { int8_t a; int32_t b; int64_t c; } Mixed;\n" + "struct Node { int value; struct Node *next; };\n"), + testing::AllOf(HasSubstr("type Mixed = [a: s8, b: s32, c: s64];"), + HasSubstr("type Node = [value: s32, next: Opaque /* Reference: a type cannot refer to itself */];"))); +} + +// tslang cannot declare a type that refers to itself (the compiler dies on one), so every pointer +// field that leads back to its own struct - directly, through another struct, or through a typedef +// - is Opaque +TEST(TypeMapping, TypeCyclesBecomeOpaque) +{ + auto out = text("struct A { struct B *b; int x; };\nstruct B { struct A *a; };\n" + "typedef struct List *ListPtr;\nstruct List { ListPtr next; int v; };\n" + "void walk(struct A *a, ListPtr l);\n"); + EXPECT_THAT(out, HasSubstr("type A = [b: Opaque /* Reference: a type cannot refer to itself */, x: s32];")); + EXPECT_THAT(out, HasSubstr("type B = [a: Opaque /* Reference: a type cannot refer to itself */];")); + EXPECT_THAT(out, HasSubstr("type List = [next: Opaque /* Reference: a type cannot refer to itself */, v: s32];")); + EXPECT_THAT(out, HasSubstr("type ListPtr = Reference;")); + EXPECT_THAT(out, HasSubstr("declare function walk(a: Reference, l: ListPtr): void;")); + EXPECT_THAT(out, Not(HasSubstr("fn as Opaque"))); +} + +TEST(TypeMapping, CEnumIsATsEnum) +{ + auto out = text("enum Color { RED, GREEN = 5, BLUE };\nint f(enum Color c);\n"); + EXPECT_THAT(out, HasSubstr("enum Color { RED = 0, GREEN = 5, BLUE = 6 }")); + EXPECT_THAT(out, HasSubstr("declare function f(c: Color): s32;")); +} + +TEST(TypeMapping, FixedNonIntEnumIsATypeAndConstants) +{ + auto out = generate("#include \n" + "enum Mode : uint8_t { Off = 0, On = 1 };\n" + "enum class Level : int16_t { Low = -1, High = 2 };\n" + "extern \"C\" void set(Mode m, Level l);\n", + {}, windowsTarget, {}, "c++") + .text; + EXPECT_THAT(out, HasSubstr("type Mode = u8;\nconst Off = 0;\nconst On = 1;\n")); + EXPECT_THAT(out, HasSubstr("type Level = s16;\nconst Level_Low = -1;\nconst Level_High = 2;\n")); + EXPECT_THAT(out, HasSubstr("declare function set(m: Mode, l: Level): void;")); +} + +TEST(TypeMapping, VarargsKeepTheFixedParameters) +{ + EXPECT_THAT(text("int print_all(const char *format, ...);\n"), + HasSubstr("@varargs declare function print_all(format: string): s32;")); +} + +TEST(TypeMapping, LiteralMacrosAreConstants) +{ + auto out = text("#define ANSWER 42\n#define NEGATIVE (-7)\n#define MASK 0xFFu\n#define RATIO 1.5\n" + "#define WHOLE 2.0\n#define NAME \"fix\\\"ture\"\n"); + EXPECT_THAT(out, HasSubstr("const ANSWER = 42;")); + EXPECT_THAT(out, HasSubstr("const NEGATIVE = -7;")); + EXPECT_THAT(out, HasSubstr("const MASK = 255;")); + EXPECT_THAT(out, HasSubstr("const RATIO = 1.5;")); + EXPECT_THAT(out, HasSubstr("const WHOLE = 2.0;")); + EXPECT_THAT(out, HasSubstr("const NAME = \"fix\\\"ture\";")); +} + +TEST(TypeMapping, MacrosFollowUndefAndRedefinition) +{ + auto out = text("#define GONE 1\n#undef GONE\n#define LEVEL 1\n#undef LEVEL\n#define LEVEL 2\n"); + EXPECT_THAT(out, Not(HasSubstr("GONE"))); + EXPECT_THAT(out, HasSubstr("const LEVEL = 2;")); + EXPECT_THAT(out, Not(HasSubstr("const LEVEL = 1;"))); +} + +TEST(TypeMapping, TypedefIsATypeAliasUnlessItNamesTheStruct) +{ + auto out = text("typedef struct Point { int x; } Point;\ntypedef struct Point Pt;\ntypedef unsigned int handle_t;\n" + "void f(Pt *p, handle_t h);\n"); + EXPECT_THAT(out, HasSubstr("type Point = [x: s32];")); + EXPECT_THAT(out, Not(HasSubstr("type Point = Point"))); + EXPECT_THAT(out, HasSubstr("type Pt = Point;")); + EXPECT_THAT(out, HasSubstr("type handle_t = u32;")); + EXPECT_THAT(out, HasSubstr("declare function f(p: Reference, h: handle_t): void;")); +} + +TEST(TypeMapping, ParameterNames) +{ + EXPECT_THAT(text("void f(int, int delete, int function, int value);\n"), + HasSubstr("declare function f(p0: s32, delete_: s32, function_: s32, value: s32): void;")); +} From 7c53299061713014f7248784cfc8b2f5c1d48cb7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:37:21 +0100 Subject: [PATCH 3/9] tsbindgen: tests for skips, selection and naming Co-Authored-By: Claude Opus 5.5 --- .../unittests/TsClangImporter/CMakeLists.txt | 3 + .../unittests/TsClangImporter/NamingTest.cpp | 68 +++++++++++++++ .../TsClangImporter/SelectionTest.cpp | 78 +++++++++++++++++ tslang/unittests/TsClangImporter/SkipTest.cpp | 83 +++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 tslang/unittests/TsClangImporter/NamingTest.cpp create mode 100644 tslang/unittests/TsClangImporter/SelectionTest.cpp create mode 100644 tslang/unittests/TsClangImporter/SkipTest.cpp diff --git a/tslang/unittests/TsClangImporter/CMakeLists.txt b/tslang/unittests/TsClangImporter/CMakeLists.txt index 210945da9..9c9e76d7f 100644 --- a/tslang/unittests/TsClangImporter/CMakeLists.txt +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -1,6 +1,9 @@ add_mlir_unittest(TsClangImporterTests BindingModelTest.cpp TypeMappingTest.cpp + SkipTest.cpp + SelectionTest.cpp + NamingTest.cpp ParseErrorTest.cpp ) diff --git a/tslang/unittests/TsClangImporter/NamingTest.cpp b/tslang/unittests/TsClangImporter/NamingTest.cpp new file mode 100644 index 000000000..82f24c4d5 --- /dev/null +++ b/tslang/unittests/TsClangImporter/NamingTest.cpp @@ -0,0 +1,68 @@ +// --namespace wraps the output and binds each function by its C name with @linkname; +// --strip-prefix shortens the TS names but never the symbol. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::Contains; +using testing::HasSubstr; + +TEST(Naming, CNamesAreKept) +{ + auto out = text("#define FX_ANSWER 42\nint fx_add(int a, int b);\n"); + EXPECT_EQ(out, "const FX_ANSWER = 42;\ndeclare function fx_add(a: s32, b: s32): s32;\n"); +} + +TEST(Naming, NamespaceExportsEverythingAndLinksByCName) +{ + tsbindgen::PrintOptions options; + options.namespaceName = "Fx"; + auto out = generate("#define FX_ANSWER 42\nstruct fx_point { int x; };\nint fx_add(int a, int b);\n" + "int fx_sum(int n, ...);\n", + options) + .text; + EXPECT_EQ(out, "namespace Fx {\n" + " export const FX_ANSWER = 42;\n" + " export type fx_point = [x: s32];\n" + " @linkname(\"fx_add\") export declare function fx_add(a: s32, b: s32): s32;\n" + " @varargs @linkname(\"fx_sum\") export declare function fx_sum(n: s32): s32;\n" + "}\n"); +} + +TEST(Naming, StripPrefixRenamesDeclarationsAndTheirUses) +{ + tsbindgen::PrintOptions options; + options.namespaceName = "Fx"; + options.stripPrefix = "fx_"; + auto out = generate("struct fx_point { int x; };\nint fx_len(struct fx_point *p);\nint other(void);\n", options) + .text; + EXPECT_THAT(out, HasSubstr(" export type point = [x: s32];\n")); + EXPECT_THAT(out, HasSubstr(" @linkname(\"fx_len\") export declare function len(p: Reference): s32;\n")); + EXPECT_THAT(out, HasSubstr(" @linkname(\"other\") export declare function other(): s32;\n")); +} + +TEST(Naming, StripPrefixKeepsTheCNameWhenStrippingWouldBreakIt) +{ + tsbindgen::PrintOptions options; + options.namespaceName = "Fx"; + options.stripPrefix = "fx_"; + auto out = generate("int len(void);\nint fx_len(void);\nint fx_2d(void);\nint fx_delete(void);\n", options); + EXPECT_THAT(out.text, HasSubstr("@linkname(\"len\") export declare function len(): s32;")); + EXPECT_THAT(out.text, HasSubstr("@linkname(\"fx_len\") export declare function fx_len(): s32;")); + EXPECT_THAT(out.text, HasSubstr("@linkname(\"fx_2d\") export declare function fx_2d(): s32;")); + EXPECT_THAT(out.text, HasSubstr("@linkname(\"fx_delete\") export declare function delete_(): s32;")); + EXPECT_THAT(out.warnings, Contains("kept 'fx_len' unstripped: 'len' is taken by 'len'")); +} + +TEST(Naming, HeaderLinesComeFirst) +{ + tsbindgen::PrintOptions options; + options.headerLines = {"Generated by tsbindgen dev (clang 22.1.8); do not edit.", "tsbindgen input.h"}; + EXPECT_EQ(generate("int f(void);\n", options).text, + "// Generated by tsbindgen dev (clang 22.1.8); do not edit.\n" + "// tsbindgen input.h\n" + "\n" + "declare function f(): s32;\n"); +} diff --git a/tslang/unittests/TsClangImporter/SelectionTest.cpp b/tslang/unittests/TsClangImporter/SelectionTest.cpp new file mode 100644 index 000000000..e380a6723 --- /dev/null +++ b/tslang/unittests/TsClangImporter/SelectionTest.cpp @@ -0,0 +1,78 @@ +// What gets emitted: without --filter, what the input file declares; with it, what matches; and in +// both cases the types those use, wherever they are declared. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::HasSubstr; +using testing::Not; + +namespace +{ + +const std::vector> files = { + {"other.h", "typedef struct Shared { int id; } Shared;\nint other_unused(void);\n#define OTHER_VALUE 1\n"}, + {"sys/system.h", "struct sys_stat { int size; };\nint sys_call(struct sys_stat *s);\n#define SYS_VALUE 2\n"}, +}; + +} // namespace + +TEST(Selection, ByDefaultOnlyTheInputFilesDeclarations) +{ + auto out = generate("#include \"other.h\"\n#include \nint mine(void);\n", {}, windowsTarget, files).text; + EXPECT_THAT(out, HasSubstr("declare function mine(): s32;")); + EXPECT_THAT(out, Not(HasSubstr("other_unused"))); + EXPECT_THAT(out, Not(HasSubstr("OTHER_VALUE"))); + EXPECT_THAT(out, Not(HasSubstr("sys_call"))); + EXPECT_THAT(out, Not(HasSubstr("SYS_VALUE"))); + EXPECT_THAT(out, Not(HasSubstr("Shared"))); +} + +TEST(Selection, UsedTypesArePulledInFromAnyHeader) +{ + auto out = generate("#include \"other.h\"\n#include \nint use(Shared *s, struct sys_stat *st);\n", {}, + windowsTarget, files) + .text; + EXPECT_THAT(out, HasSubstr("type Shared = [id: s32];")); + EXPECT_THAT(out, HasSubstr("type sys_stat = [size: s32];")); + EXPECT_THAT(out, HasSubstr("declare function use(s: Reference, st: Reference): s32;")); + EXPECT_THAT(out, Not(HasSubstr("other_unused"))); +} + +TEST(Selection, FilterSelectsByCNameAnywhere) +{ + tsbindgen::PrintOptions options; + options.filters = {"sys_*", "mine"}; + auto out = generate("#include \"other.h\"\n#include \nint mine(void);\nint yours(void);\n", options, + windowsTarget, files) + .text; + EXPECT_THAT(out, HasSubstr("declare function mine(): s32;")); + EXPECT_THAT(out, HasSubstr("declare function sys_call(s: Reference): s32;")); + EXPECT_THAT(out, HasSubstr("type sys_stat = [size: s32];")); + EXPECT_THAT(out, Not(HasSubstr("yours"))); + EXPECT_THAT(out, Not(HasSubstr("SYS_VALUE"))); +} + +TEST(Selection, NothingSelectedIsAWarning) +{ + auto out = generate("#include \"other.h\"\n", {}, windowsTarget, files); + EXPECT_EQ(out.text, ""); + ASSERT_EQ(out.warnings.size(), 1u); + EXPECT_THAT(out.warnings.front(), HasSubstr("nothing to emit")); + + tsbindgen::PrintOptions options; + options.filters = {"no_such_*"}; + EXPECT_THAT(generate("int f(void);\n", options).warnings, testing::Contains(HasSubstr("no declaration matches"))); +} + +TEST(Selection, TypesComeBeforeTheFunctionsThatUseThem) +{ + auto out = text("int first(struct Late *l);\nstruct Late { int x; };\n"); + auto type = out.find("type Late"); + auto function = out.find("declare function first"); + ASSERT_NE(type, std::string::npos); + ASSERT_NE(function, std::string::npos); + EXPECT_LT(type, function); +} diff --git a/tslang/unittests/TsClangImporter/SkipTest.cpp b/tslang/unittests/TsClangImporter/SkipTest.cpp new file mode 100644 index 000000000..111419637 --- /dev/null +++ b/tslang/unittests/TsClangImporter/SkipTest.cpp @@ -0,0 +1,83 @@ +// Every skip reason in the spec: the declaration is left out with a `// skipped:` line and a +// warning, generation carries on, and a skipped struct is still Opaque for its pointer users. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::Contains; +using testing::HasSubstr; +using testing::Not; + +TEST(Skip, UnionIsSkippedButItsPointerUsersStay) +{ + auto out = generate("union Value { int i; float f; };\nvoid set(union Value *v);\nvoid pass(union Value v);\n"); + EXPECT_THAT(out.text, HasSubstr("// skipped: Value \xE2\x80\x94 union\ntype Value = Opaque;")); + EXPECT_THAT(out.text, HasSubstr("declare function set(v: Value): void;")); + EXPECT_THAT(out.text, HasSubstr("// skipped: pass \xE2\x80\x94 parameter 'v': union passed by value")); + EXPECT_THAT(out.warnings, Contains("skipped Value: union")); +} + +TEST(Skip, StructWithABitfieldOrAnUnmappableField) +{ + auto out = text("struct Flags { unsigned a : 1; };\nstruct Wide { long double x; };\n" + "struct Grid { int cells[4]; };\n"); + EXPECT_THAT(out, HasSubstr("// skipped: Flags \xE2\x80\x94 bitfield 'a'")); + EXPECT_THAT(out, HasSubstr("// skipped: Wide \xE2\x80\x94 field 'x': long double")); + EXPECT_THAT(out, HasSubstr("// skipped: Grid \xE2\x80\x94 field 'cells': array")); +} + +TEST(Skip, PackedStruct) +{ + EXPECT_THAT(text("struct __attribute__((packed)) Packed { char c; int i; };\n"), + HasSubstr("// skipped: Packed \xE2\x80\x94 packed or over-aligned layout")); + // how real headers usually pack + EXPECT_THAT(text("#pragma pack(push, 1)\nstruct Wire { char c; int i; };\n#pragma pack(pop)\n" + "struct Natural { char c; int i; };\n"), + testing::AllOf(HasSubstr("// skipped: Wire \xE2\x80\x94 packed or over-aligned layout"), + HasSubstr("type Natural = [c: s8, i: s32];"))); +} + +TEST(Skip, LongDouble) +{ + EXPECT_THAT(text("long double f(long double x);\n"), + HasSubstr("// skipped: f \xE2\x80\x94 parameter 'x': long double")); +} + +TEST(Skip, StructByValue) +{ + auto out = text("struct Point { int x; int y; };\nstruct Point make(int x);\nint sum(struct Point p);\n"); + EXPECT_THAT(out, HasSubstr("type Point = [x: s32, y: s32];")); + EXPECT_THAT(out, HasSubstr("// skipped: make \xE2\x80\x94 result: struct passed by value")); + EXPECT_THAT(out, HasSubstr("// skipped: sum \xE2\x80\x94 parameter 'p': struct passed by value")); +} + +TEST(Skip, FunctionLikeAndNonLiteralMacros) +{ + auto out = text("#define MAX(a, b) ((a) > (b) ? (a) : (b))\n#define FLAGS (1 << 3)\n#define GUARD_H\n"); + EXPECT_THAT(out, HasSubstr("// skipped: MAX \xE2\x80\x94 function-like macro")); + EXPECT_THAT(out, HasSubstr("// skipped: FLAGS \xE2\x80\x94 macro is not a literal")); + EXPECT_THAT(out, Not(HasSubstr("GUARD_H"))); +} + +TEST(Skip, StaticInlineFunction) +{ + auto out = text("static inline int twice(int x) { return x * 2; }\nstatic int hidden(void);\n"); + EXPECT_THAT(out, HasSubstr("// skipped: twice \xE2\x80\x94 static inline function: no symbol to link against")); + EXPECT_THAT(out, HasSubstr("// skipped: hidden \xE2\x80\x94 static function: no symbol to link against")); +} + +TEST(Skip, OtherTypes) +{ + auto out = text("_Complex double f(void);\nint g();\n"); + EXPECT_THAT(out, HasSubstr("// skipped: f \xE2\x80\x94 result: unsupported type '_Complex double'")); + EXPECT_THAT(out, HasSubstr("// skipped: g \xE2\x80\x94 function without a prototype")); +} + +TEST(Skip, ASkipDoesNotStopGeneration) +{ + auto out = text("union U { int i; };\nint before(void);\nvoid broken(union U u);\nint after(void);\n"); + EXPECT_THAT(out, HasSubstr("declare function before(): s32;")); + EXPECT_THAT(out, HasSubstr("declare function after(): s32;")); +} From bbf63dd464e71a7d2e9be778be53b736753d6f44 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:38:06 +0100 Subject: [PATCH 4/9] tsbindgen: command line, with exit codes pinned by test-bindgen-cli Co-Authored-By: Claude Opus 5.5 --- tslang/CMakeLists.txt | 1 + tslang/test/bindgen/fixture.h | 98 +++++++++ tslang/test/bindgen/run-cli-test.cmake | 67 +++++++ tslang/test/tester/CMakeLists.txt | 7 + tslang/tsbindgen/CMakeLists.txt | 37 ++++ tslang/tsbindgen/tsbindgen.cpp | 262 +++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 tslang/test/bindgen/fixture.h create mode 100644 tslang/test/bindgen/run-cli-test.cmake create mode 100644 tslang/tsbindgen/CMakeLists.txt create mode 100644 tslang/tsbindgen/tsbindgen.cpp diff --git a/tslang/CMakeLists.txt b/tslang/CMakeLists.txt index e9ab0f080..bd1243234 100644 --- a/tslang/CMakeLists.txt +++ b/tslang/CMakeLists.txt @@ -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) diff --git a/tslang/test/bindgen/fixture.h b/tslang/test/bindgen/fixture.h new file mode 100644 index 000000000..d8846e745 --- /dev/null +++ b/tslang/test/bindgen/fixture.h @@ -0,0 +1,98 @@ +// One of each thing tsbindgen v1 binds, for test/bindgen/run-bindgen-test.cmake. bindgen_test.ts +// calls every function through the generated bindings; expected.txt is what it must print. + +#ifndef FIXTURE_H +#define FIXTURE_H + +#include +#include +#include + +#if defined(_WIN32) +#define FIXTURE_API __declspec(dllexport) +#else +#define FIXTURE_API __attribute__((visibility("default"))) +#endif + +#define FIXTURE_ANSWER 42 +#define FIXTURE_RATIO 1.5 +#define FIXTURE_NAME "fixture" + +typedef struct Counter Counter; // opaque handle + +typedef struct Point +{ + int32_t x; + int32_t y; +} Point; + +typedef struct Box +{ + int8_t tag; + Point corner; // a struct inside a struct + double scale; + bool visible; +} Box; + +typedef struct Node // points to itself: `next` becomes Opaque +{ + int32_t value; + struct Node *next; +} Node; + +enum Color +{ + RED, + GREEN = 5, + BLUE +}; + +union Value // skipped, but a pointer to it is still Opaque +{ + int32_t i; + float f; +}; + +// scalars +FIXTURE_API int32_t fx_add(int32_t a, int32_t b); +FIXTURE_API double fx_scale(double v, float f); +FIXTURE_API int32_t fx_widen_s8(int8_t v); +FIXTURE_API int32_t fx_widen_u16(uint16_t v); +FIXTURE_API int8_t fx_negate_s8(int8_t v); +FIXTURE_API bool fx_not(bool b); + +// strings in and out +FIXTURE_API size_t fx_len(const char *s); +FIXTURE_API const char *fx_greet(void); + +// out-parameters +FIXTURE_API void fx_divmod(int32_t a, int32_t b, int32_t *quotient, int32_t *remainder); + +// an opaque handle +FIXTURE_API Counter *fx_counter_new(void); +FIXTURE_API void fx_counter_inc(Counter *c); +FIXTURE_API int32_t fx_counter_get(Counter *c); +FIXTURE_API void fx_counter_free(Counter *c); + +// structs by pointer, read and written by C +FIXTURE_API int32_t fx_point_sum(const Point *p); +FIXTURE_API void fx_box_fill(Box *b); +FIXTURE_API int32_t fx_list_sum(Node *head); + +// an enum +FIXTURE_API int32_t fx_color_value(enum Color c); + +// a callback +FIXTURE_API int32_t fx_apply(int32_t (*fn)(int32_t), int32_t v); + +// varargs +FIXTURE_API int32_t fx_sum(int32_t count, ...); + +// skipped declarations +FIXTURE_API void fx_value_clear(union Value *v); +static inline int32_t fx_twice_inline(int32_t x) +{ + return x * 2; +} + +#endif // FIXTURE_H diff --git a/tslang/test/bindgen/run-cli-test.cmake b/tslang/test/bindgen/run-cli-test.cmake new file mode 100644 index 000000000..0905ea28f --- /dev/null +++ b/tslang/test/bindgen/run-cli-test.cmake @@ -0,0 +1,67 @@ +# tsbindgen's command line: 0 when the file was written, 1 for a parse error or I/O failure, 2 for +# bad arguments - and never 0 for a command line clang rejected. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSBINDGEN SOURCE_DIR WORK_DIR) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +file(WRITE "${WORK_DIR}/broken.h" "int broken(\n") + +# expect( ) - leaves stdout + stderr in cli_output +function(expect code what) + execute_process(COMMAND "${TSBINDGEN}" ${ARGN} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(NOT status EQUAL code) + message(FATAL_ERROR "${what}: exit ${status}, expected ${code}\n${out}\n${err}") + endif() + set(cli_output "${out}${err}" PARENT_SCOPE) + message(STATUS "${what}: exit ${code}") +endfunction() + +set(header "${SOURCE_DIR}/fixture.h") + +expect(0 "a header, written to a file" "${header}" -o out.ts) +if(NOT EXISTS "${WORK_DIR}/out.ts") + message(FATAL_ERROR "exit 0, but out.ts was not written") +endif() + +# `import` would load a same-named library instead, so the file says how to include it +file(READ "${WORK_DIR}/out.ts" written) +if(NOT written MATCHES "// Include with: /// ") + message(FATAL_ERROR "out.ts lacks the include hint:\n${written}") +endif() + +expect(0 "a header, to stdout" "${header}" --filter fx_add) +if(NOT cli_output MATCHES "declare function fx_add\\(a: s32, b: s32\\): s32;") + message(FATAL_ERROR "stdout lacks fx_add:\n${cli_output}") +endif() + +expect(0 "--version" --version) +if(NOT cli_output MATCHES "tsbindgen .* \\(clang [0-9]+") + message(FATAL_ERROR "--version printed:\n${cli_output}") +endif() + +expect(0 "a .d.ts output warns" "${header}" -o out.d.ts) +if(NOT cli_output MATCHES "name it \\.ts") + message(FATAL_ERROR "no .d.ts warning:\n${cli_output}") +endif() + +expect(1 "a missing input" missing.h) +expect(1 "a header clang rejects" broken.h) +expect(1 "an argument clang rejects" "${header}" -- --no-such-clang-flag) +expect(1 "an unwritable output" "${header}" -o "${WORK_DIR}/no/such/dir/out.ts") + +expect(2 "no input" --filter x) +expect(2 "an unknown option" "${header}" --no-such-option) +expect(2 "--strip-prefix without --namespace" "${header}" --strip-prefix fx_) +expect(2 "an invalid glob" "${header}" --filter "[") +expect(2 "a --resource-dir without clang's headers" "${header}" --resource-dir "${WORK_DIR}") diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 99f649514..910ecdef7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2226,6 +2226,13 @@ if (TSLANG_TEST_X86) LABELS x86) endif() +add_test(NAME test-bindgen-cli + COMMAND ${CMAKE_COMMAND} + "-DTSBINDGEN=$" + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}/test/bindgen" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/bindgen-cli" + -P "${PROJECT_SOURCE_DIR}/test/bindgen/run-cli-test.cmake") + set(TSLANG_OWNERSHIP_SHARDS 8) math(EXPR ownership_last "${TSLANG_OWNERSHIP_SHARDS} - 1") foreach(ownership_shard RANGE ${ownership_last}) diff --git a/tslang/tsbindgen/CMakeLists.txt b/tslang/tsbindgen/CMakeLists.txt new file mode 100644 index 000000000..f8e8c258d --- /dev/null +++ b/tslang/tsbindgen/CMakeLists.txt @@ -0,0 +1,37 @@ +set_Options() + +set(LLVM_LINK_COMPONENTS + Support + TargetParser + ) + +add_llvm_executable(tsbindgen + tsbindgen.cpp + ) + +llvm_update_compile_flags(tsbindgen) +target_link_libraries(tsbindgen PRIVATE TsClangImporter) + +# The last place tsbindgen looks for clang's resource directory (stddef.h, stdint.h, ...): the +# LLVM it was built with. A release package puts one next to tsbindgen instead. +target_compile_definitions(tsbindgen PRIVATE + TSBINDGEN_CONFIGURED_RESOURCE_DIR="${LLVM_LIBRARY_DIR}/clang/${LLVM_VERSION_MAJOR}") + +# the same version tslang reports: the release tag, or the commit a dev build was made from +if(TSLANG_PACKAGE_VERSION) + set(TSBINDGEN_VERSION "${TSLANG_PACKAGE_VERSION}") +else() + find_package(Git QUIET) + if(GIT_EXECUTABLE) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE TSBINDGEN_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + endif() +endif() + +if(TSBINDGEN_VERSION) + target_compile_definitions(tsbindgen PRIVATE TSBINDGEN_VERSION="${TSBINDGEN_VERSION}") +endif() diff --git a/tslang/tsbindgen/tsbindgen.cpp b/tslang/tsbindgen/tsbindgen.cpp new file mode 100644 index 000000000..b4e66399a --- /dev/null +++ b/tslang/tsbindgen/tsbindgen.cpp @@ -0,0 +1,262 @@ +// tsbindgen: C header -> tslang bindings. See docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md. + +#include "TsClangImporter/BindingPrinter.h" +#include "TsClangImporter/HeaderParser.h" + +#include "clang/Basic/Version.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/WithColor.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/TargetParser/Host.h" + +#include + +namespace cl = llvm::cl; + +namespace +{ + +cl::OptionCategory category("tsbindgen options"); + +cl::opt inputFile(cl::Positional, cl::desc(""), cl::Required, cl::cat(category)); +cl::list includeDirs("I", cl::Prefix, cl::desc("Add an include directory"), cl::value_desc("dir"), + cl::cat(category)); +cl::list defines("D", cl::Prefix, cl::desc("Define a macro"), cl::value_desc("name[=value]"), + cl::cat(category)); +cl::opt target("target", cl::desc("Target triple (default: the host)"), cl::value_desc("triple"), + cl::cat(category)); +cl::list filters("filter", cl::desc("Emit the declarations whose C name matches this glob"), + cl::value_desc("glob"), cl::cat(category)); +cl::opt namespaceName("namespace", cl::desc("Wrap the output in `namespace `"), + cl::value_desc("N"), cl::cat(category)); +cl::opt stripPrefix("strip-prefix", cl::desc("Remove

from the TS names (needs --namespace)"), + cl::value_desc("P"), cl::cat(category)); +cl::opt resourceDir("resource-dir", cl::desc("clang's resource directory"), cl::value_desc("dir"), + cl::cat(category)); +cl::opt outputFile("o", cl::desc("Output file (default: stdout)"), cl::value_desc("out.ts"), + cl::init("-"), cl::cat(category)); + +enum ExitCode +{ + Written = 0, + ParseOrIOError = 1, + BadArguments = 2 +}; + +std::string version() +{ +#ifdef TSBINDGEN_VERSION + return TSBINDGEN_VERSION; +#else + return "dev"; +#endif +} + +bool hasStddef(llvm::StringRef dir) +{ + llvm::SmallString<256> path(dir); + llvm::sys::path::append(path, "include", "stddef.h"); + return llvm::sys::fs::exists(path); +} + +// clang's own stddef.h/stdint.h live here; without them those headers do not resolve. +std::optional findResourceDir(llvm::StringRef exePath, std::vector &tried) +{ + auto versioned = [](llvm::StringRef base) { + llvm::SmallString<256> path(base); + llvm::sys::path::append(path, "lib", "clang", std::to_string(CLANG_VERSION_MAJOR)); + return std::string(path); + }; + + std::vector candidates; + if (!resourceDir.empty()) + { + candidates.push_back(resourceDir); + } + else + { + auto exeDir = llvm::sys::path::parent_path(exePath); + candidates.push_back(versioned(exeDir)); // the release package + candidates.push_back(versioned(llvm::sys::path::parent_path(exeDir))); // an LLVM-style install +#ifdef TSBINDGEN_CONFIGURED_RESOURCE_DIR + candidates.push_back(TSBINDGEN_CONFIGURED_RESOURCE_DIR); // the LLVM this was built with +#endif + } + + for (auto &candidate : candidates) + { + tried.push_back(candidate); + if (hasStddef(candidate)) + { + return candidate; + } + } + + return std::nullopt; +} + +bool isCxx(llvm::StringRef path) +{ + auto extension = llvm::sys::path::extension(path).lower(); + return extension == ".cpp" || extension == ".cc" || extension == ".cxx" || extension == ".hpp" || + extension == ".hh" || extension == ".hxx"; +} + +} // namespace + +int main(int argc, char **argv) +{ + // everything after `--` goes to clang untouched + std::vector ownArgs; + std::vector extraArgs; + auto afterDashes = false; + for (int index = 0; index < argc; ++index) + { + if (afterDashes) + { + extraArgs.push_back(argv[index]); + } + else if (index > 0 && llvm::StringRef(argv[index]) == "--") + { + afterDashes = true; + } + else + { + ownArgs.push_back(argv[index]); + } + } + + cl::HideUnrelatedOptions(category); + cl::SetVersionPrinter([](llvm::raw_ostream &os) { + os << "tsbindgen " << version() << " (clang " << CLANG_VERSION_STRING << ")\n"; + }); + + if (!cl::ParseCommandLineOptions(static_cast(ownArgs.size()), ownArgs.data(), + "C header -> tslang bindings\n", &llvm::errs())) + { + return BadArguments; + } + + if (!stripPrefix.empty() && namespaceName.empty()) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") << "--strip-prefix needs --namespace\n"; + return BadArguments; + } + + std::vector tried; + auto exePath = llvm::sys::fs::getMainExecutable(argv[0], reinterpret_cast(&main)); + auto foundResourceDir = findResourceDir(exePath, tried); + if (!foundResourceDir) + { + auto &error = llvm::WithColor::error(llvm::errs(), "tsbindgen"); + error << "clang's resource directory not found; tried:"; + for (auto &path : tried) + { + error << "\n " << path; + } + + error << "\n(pass --resource-dir

, the directory with include/stddef.h)\n"; + // a wrong --resource-dir is the caller's argument; a failed lookup is a broken install + return resourceDir.empty() ? ParseOrIOError : BadArguments; + } + + auto buffer = llvm::MemoryBuffer::getFile(inputFile); + if (!buffer) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") + << "cannot read '" << inputFile << "': " << buffer.getError().message() << "\n"; + return ParseOrIOError; + } + + llvm::SmallString<256> absoluteInput(inputFile); + llvm::sys::fs::make_absolute(absoluteInput); + + auto triple = target.empty() ? llvm::sys::getDefaultTargetTriple() : target.getValue(); + + tsbindgen::ParseInput input; + input.code = (*buffer)->getBuffer().str(); + input.fileName = std::string(absoluteInput); + input.args = {"-x", isCxx(inputFile) ? "c++" : "c", "--target=" + triple, "-resource-dir", *foundResourceDir}; + for (auto &dir : includeDirs) + { + input.args.push_back("-I" + dir); + } + + for (auto &define : defines) + { + input.args.push_back("-D" + define); + } + + input.args.insert(input.args.end(), extraArgs.begin(), extraArgs.end()); + + auto model = tsbindgen::parseHeader(input); + if (!model) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") << llvm::toString(model.takeError()) << "\n"; + return ParseOrIOError; + } + + std::string commandLine = "tsbindgen"; + for (int index = 1; index < argc; ++index) + { + commandLine += " "; + commandLine += argv[index]; + } + + tsbindgen::PrintOptions options; + options.filters = filters; + options.namespaceName = namespaceName; + options.stripPrefix = stripPrefix; + options.headerLines = {"Generated by tsbindgen " + version() + " (clang " CLANG_VERSION_STRING "); do not edit.", + commandLine}; + // `import` would not do: it loads a same-named .dll/.so as a tslang library if there is one, + // and includes a source file as declarations only, so its constants would have no value + if (outputFile != "-") + { + options.headerLines.push_back("Include with: /// "); + } + + auto printed = tsbindgen::printBindings(*model, options); + if (!printed) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") << llvm::toString(printed.takeError()) << "\n"; + return BadArguments; + } + + for (auto &warning : printed->warnings) + { + llvm::WithColor::warning(llvm::errs(), "tsbindgen") << warning << "\n"; + } + + // A .d.ts is included as declarations only: a `const` in it has no value and fails to link. + if (llvm::StringRef(outputFile.getValue()).ends_with(".d.ts") && + llvm::StringRef(printed->text).contains("const ")) + { + llvm::WithColor::warning(llvm::errs(), "tsbindgen") + << "'" << outputFile << "' declares constants, which a .d.ts file cannot define; name it .ts\n"; + } + + std::error_code error; + llvm::raw_fd_ostream out(outputFile, error, llvm::sys::fs::OF_Text); + if (error) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") + << "cannot write '" << outputFile << "': " << error.message() << "\n"; + return ParseOrIOError; + } + + out << printed->text; + out.close(); + if (out.has_error()) + { + llvm::WithColor::error(llvm::errs(), "tsbindgen") << "cannot write '" << outputFile << "'\n"; + out.clear_error(); + return ParseOrIOError; + } + + return Written; +} From dbdc1f77f88abdbdcf48e2edc1b1de07139ef3cc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:39:50 +0100 Subject: [PATCH 5/9] tsbindgen: end-to-end test against a C library, linked and under the JIT Co-Authored-By: Claude Opus 5.5 --- tslang/test/bindgen/bindgen_ns_test.ts | 44 +++++++ tslang/test/bindgen/bindgen_test.ts | 44 +++++++ tslang/test/bindgen/expected.txt | 14 +++ tslang/test/bindgen/fixture.c | 137 +++++++++++++++++++++ tslang/test/bindgen/run-bindgen-test.cmake | 86 +++++++++++++ tslang/test/tester/CMakeLists.txt | 19 +++ 6 files changed, 344 insertions(+) create mode 100644 tslang/test/bindgen/bindgen_ns_test.ts create mode 100644 tslang/test/bindgen/bindgen_test.ts create mode 100644 tslang/test/bindgen/expected.txt create mode 100644 tslang/test/bindgen/fixture.c create mode 100644 tslang/test/bindgen/run-bindgen-test.cmake diff --git a/tslang/test/bindgen/bindgen_ns_test.ts b/tslang/test/bindgen/bindgen_ns_test.ts new file mode 100644 index 000000000..e238c614d --- /dev/null +++ b/tslang/test/bindgen/bindgen_ns_test.ts @@ -0,0 +1,44 @@ +// @strict-null false +/// + +// bindgen_test.ts again, against `tsbindgen fixture.h --namespace Fx --strip-prefix fx_`: every +// function is reached through a TS name that is not its C symbol, which @linkname binds. + +function twice(x: s32): s32 { + return x * 2; +} + +function main() { + print("macros", Fx.FIXTURE_ANSWER, Fx.FIXTURE_RATIO, Fx.FIXTURE_NAME); + print("add", Fx.add(2, 3)); + print("scale", Fx.scale(2.0, 1.5)); + print("narrow", Fx.widen_s8(-5), Fx.widen_u16(65535), Fx.negate_s8(5), Fx.not(true), Fx.not(false)); + print("len", Fx.len("hello")); + print("greet", Fx.greet()); + + let quotient: s32 = 0; + let remainder: s32 = 0; + Fx.divmod(17, 5, ReferenceOf(quotient), ReferenceOf(remainder)); + print("divmod", quotient, remainder); + + const counter = Fx.counter_new(); + Fx.counter_inc(counter); + Fx.counter_inc(counter); + print("counter", Fx.counter_get(counter)); + Fx.counter_free(counter); + + let point: Fx.Point = [3, 4]; + print("point", Fx.point_sum(ReferenceOf(point))); + + let box: Fx.Box = [0, [0, 0], 0.0, false]; + Fx.box_fill(ReferenceOf(box)); + print("box", box[0], box[1][0], box[1][1], box[2], box[3]); + + let second: Fx.Node = [5, null]; + let first: Fx.Node = [3, ReferenceOf(second) as Opaque]; + print("list", Fx.list_sum(ReferenceOf(first))); + + print("color", Fx.color_value(Fx.Color.BLUE)); + print("apply", Fx.apply(twice as Opaque, 21)); + print("sum", Fx.sum(3, 1, 2, 3)); +} diff --git a/tslang/test/bindgen/bindgen_test.ts b/tslang/test/bindgen/bindgen_test.ts new file mode 100644 index 000000000..30eb150f9 --- /dev/null +++ b/tslang/test/bindgen/bindgen_test.ts @@ -0,0 +1,44 @@ +// @strict-null false +/// + +// Calls everything in fixture.h through the bindings tsbindgen generated from it (fixture.ts). +// run-bindgen-test.cmake compares what this prints with expected.txt. + +function twice(x: s32): s32 { + return x * 2; +} + +function main() { + print("macros", FIXTURE_ANSWER, FIXTURE_RATIO, FIXTURE_NAME); + print("add", fx_add(2, 3)); + print("scale", fx_scale(2.0, 1.5)); + print("narrow", fx_widen_s8(-5), fx_widen_u16(65535), fx_negate_s8(5), fx_not(true), fx_not(false)); + print("len", fx_len("hello")); + print("greet", fx_greet()); + + let quotient: s32 = 0; + let remainder: s32 = 0; + fx_divmod(17, 5, ReferenceOf(quotient), ReferenceOf(remainder)); + print("divmod", quotient, remainder); + + const counter = fx_counter_new(); + fx_counter_inc(counter); + fx_counter_inc(counter); + print("counter", fx_counter_get(counter)); + fx_counter_free(counter); + + let point: Point = [3, 4]; + print("point", fx_point_sum(ReferenceOf(point))); + + let box: Box = [0, [0, 0], 0.0, false]; + fx_box_fill(ReferenceOf(box)); + print("box", box[0], box[1][0], box[1][1], box[2], box[3]); + + let second: Node = [5, null]; + let first: Node = [3, ReferenceOf(second) as Opaque]; + print("list", fx_list_sum(ReferenceOf(first))); + + print("color", fx_color_value(Color.BLUE)); + print("apply", fx_apply(twice as Opaque, 21)); + print("sum", fx_sum(3, 1, 2, 3)); +} diff --git a/tslang/test/bindgen/expected.txt b/tslang/test/bindgen/expected.txt new file mode 100644 index 000000000..0d803ad24 --- /dev/null +++ b/tslang/test/bindgen/expected.txt @@ -0,0 +1,14 @@ +macros 42 1.5 fixture +add 5 +scale 3 +narrow -5 65535 -5 false true +len 5 +greet hello from C +divmod 3 2 +counter 2 +point 7 +box -7 30 -40 2.5 true +list 8 +color 6 +apply 42 +sum 6 diff --git a/tslang/test/bindgen/fixture.c b/tslang/test/bindgen/fixture.c new file mode 100644 index 000000000..6e2753312 --- /dev/null +++ b/tslang/test/bindgen/fixture.c @@ -0,0 +1,137 @@ +#include "fixture.h" + +#include +#include + +struct Counter +{ + int32_t n; +}; + +int32_t fx_add(int32_t a, int32_t b) +{ + return a + b; +} + +double fx_scale(double v, float f) +{ + return v * f; +} + +int32_t fx_widen_s8(int8_t v) +{ + return v; +} + +int32_t fx_widen_u16(uint16_t v) +{ + return v; +} + +int8_t fx_negate_s8(int8_t v) +{ + return (int8_t)-v; +} + +bool fx_not(bool b) +{ + return !b; +} + +size_t fx_len(const char *s) +{ + size_t n = 0; + while (s[n]) + { + n++; + } + + return n; +} + +const char *fx_greet(void) +{ + return "hello from C"; +} + +void fx_divmod(int32_t a, int32_t b, int32_t *quotient, int32_t *remainder) +{ + *quotient = a / b; + *remainder = a % b; +} + +Counter *fx_counter_new(void) +{ + Counter *c = malloc(sizeof *c); + c->n = 0; + return c; +} + +void fx_counter_inc(Counter *c) +{ + c->n++; +} + +int32_t fx_counter_get(Counter *c) +{ + return c->n; +} + +void fx_counter_free(Counter *c) +{ + free(c); +} + +int32_t fx_point_sum(const Point *p) +{ + return p->x + p->y; +} + +void fx_box_fill(Box *b) +{ + b->tag = -7; + b->corner.x = 30; + b->corner.y = -40; + b->scale = 2.5; + b->visible = true; +} + +int32_t fx_list_sum(Node *head) +{ + int32_t sum = 0; + for (; head; head = head->next) + { + sum += head->value; + } + + return sum; +} + +int32_t fx_color_value(enum Color c) +{ + return (int32_t)c; +} + +int32_t fx_apply(int32_t (*fn)(int32_t), int32_t v) +{ + return fn(v); +} + +int32_t fx_sum(int32_t count, ...) +{ + va_list args; + va_start(args, count); + int32_t sum = 0; + for (int32_t i = 0; i < count; i++) + { + sum += va_arg(args, int32_t); + } + + va_end(args); + return sum; +} + +void fx_value_clear(union Value *v) +{ + v->i = 0; +} diff --git a/tslang/test/bindgen/run-bindgen-test.cmake b/tslang/test/bindgen/run-bindgen-test.cmake new file mode 100644 index 000000000..f504a41da --- /dev/null +++ b/tslang/test/bindgen/run-bindgen-test.cmake @@ -0,0 +1,86 @@ +# tsbindgen end to end: generate bindings for a real C library and call it through them. +# +# fixture.c is compiled with the in-tree clang, to an object and to a shared library; tsbindgen +# turns fixture.h into fixture.ts; bindgen_test.ts is compiled against it and linked with the +# object (--emit=exe), then run under the JIT against the shared library (--emit=jit). Both must +# print expected.txt. MODE=namespace does the same through `--namespace Fx --strip-prefix fx_`, +# which only links because @linkname binds each renamed function to its C symbol. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var MODE TSLANG TSBINDGEN CLANG SOURCE_DIR WORK_DIR GC_LIB LLVM_LIB TSLANG_LIB TSLANG_BIN OPT) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +if(WIN32) + set(exe_suffix ".exe") + set(obj_suffix ".obj") + set(fixture_library "${WORK_DIR}/fixture_c.dll") + set(runtime "${TSLANG_BIN}/TypeScriptRuntime.dll") + set(pic "") + set(clang_pic "") + set(run_env "PATH=${WORK_DIR};$ENV{PATH}") +else() + set(exe_suffix "") + set(obj_suffix ".o") + set(fixture_library "${WORK_DIR}/libfixture_c.so") + set(runtime "${TSLANG_LIB}/libTypeScriptRuntime.so") + set(pic "-relocation-model=pic") + set(clang_pic "-fPIC") + set(run_env "LD_LIBRARY_PATH=${WORK_DIR}:$ENV{LD_LIBRARY_PATH}") +endif() + +# the C library's stem differs from the bindings' on purpose: `import "./fixture"` next to a +# fixture.dll would load that as a tslang library +if(MODE STREQUAL "namespace") + set(bindings "fixture_ns.ts") + set(program "bindgen_ns_test.ts") + set(bindgen_args --namespace Fx --strip-prefix fx_) +else() + set(bindings "fixture.ts") + set(program "bindgen_test.ts") + set(bindgen_args "") +endif() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +file(COPY "${SOURCE_DIR}/fixture.h" "${SOURCE_DIR}/fixture.c" "${SOURCE_DIR}/${program}" DESTINATION "${WORK_DIR}") +file(READ "${SOURCE_DIR}/expected.txt" expected) +string(REPLACE "\r\n" "\n" expected "${expected}") + +set(common --no-default-lib ${OPT} ${pic} "--gc-lib-path=${GC_LIB}" "--llvm-lib-path=${LLVM_LIB}" + "--tslang-lib-path=${TSLANG_LIB}") + +# run( ) - fails unless it exits 0; leaves stdout in run_output +function(run what) + execute_process(COMMAND ${CMAKE_COMMAND} -E env "${run_env}" ${ARGN} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(NOT status EQUAL 0) + message(FATAL_ERROR "${what}: exit ${status}\n${out}\n${err}") + endif() + string(REPLACE "\r\n" "\n" out "${out}") + set(run_output "${out}" PARENT_SCOPE) +endfunction() + +function(expect_output what) + if(NOT run_output STREQUAL expected) + message(FATAL_ERROR "${what} printed:\n${run_output}\nexpected:\n${expected}") + endif() + message(STATUS "${what}: as expected") +endfunction() + +run("clang -c fixture.c" "${CLANG}" -c ${clang_pic} fixture.c -o "fixture${obj_suffix}") +run("clang -shared fixture.c" "${CLANG}" -shared ${clang_pic} fixture.c -o "${fixture_library}") +run("tsbindgen" "${TSBINDGEN}" fixture.h ${bindgen_args} -o "${bindings}") + +run("--emit=exe" "${TSLANG}" --emit=exe ${common} "${program}" "--obj=fixture${obj_suffix}" -o "bindgen_test${exe_suffix}") +run("the program" "${WORK_DIR}/bindgen_test${exe_suffix}") +expect_output("--emit=exe") + +run("--emit=jit" "${TSLANG}" --emit=jit ${common} "--shared-libs=${runtime}" "--shared-libs=${fixture_library}" "${program}") +expect_output("--emit=jit") diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 910ecdef7..925dd7917 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2226,6 +2226,25 @@ if (TSLANG_TEST_X86) LABELS x86) endif() +# tsbindgen: bindings generated from test/bindgen/fixture.h call the C library compiled from +# fixture.c, linked (--emit=exe) and under the JIT; `namespace` does it again through +# --namespace/--strip-prefix, which @linkname binds. And the command line's exit codes. +foreach(bindgen_mode plain namespace) + add_test(NAME test-bindgen-${bindgen_mode} + COMMAND ${CMAKE_COMMAND} + "-DMODE=${bindgen_mode}" + "-DTSLANG=$" + "-DTSBINDGEN=$" + "-DCLANG=${LLVM_TOOLS_BINARY_DIR}/clang${CMAKE_EXECUTABLE_SUFFIX}" + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}/test/bindgen" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/bindgen-${bindgen_mode}" + "-DGC_LIB=${TEST_GC_LIBDIR}" + "-DLLVM_LIB=${defaultlib_collector_llvm_lib}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + "-DTSLANG_BIN=${CMAKE_BINARY_DIR}/bin" + "-DOPT=${defaultlib_collector_opt}" + -P "${PROJECT_SOURCE_DIR}/test/bindgen/run-bindgen-test.cmake") +endforeach() add_test(NAME test-bindgen-cli COMMAND ${CMAKE_COMMAND} "-DTSBINDGEN=$" From b96a9a5328015247fb98b293766e71ebb1240f2c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:42:45 +0100 Subject: [PATCH 6/9] tsbindgen: a pointer to a namespaced or template C++ class is plain Opaque Found by running tsbindgen over the default library's regex wrappers: their functions take a std::cmatch *, which was emitted as a type name (match_results) nothing declares, so the bindings did not compile. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TsClangImporter/TypeMapper.h | 4 +++ tslang/lib/TsClangImporter/HeaderParser.cpp | 10 ++----- tslang/lib/TsClangImporter/TypeMapper.cpp | 29 +++++++++++++++++++ .../TsClangImporter/TypeMappingTest.cpp | 17 +++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tslang/include/TsClangImporter/TypeMapper.h b/tslang/include/TsClangImporter/TypeMapper.h index d99ae4ae9..68f37bdf9 100644 --- a/tslang/include/TsClangImporter/TypeMapper.h +++ b/tslang/include/TsClangImporter/TypeMapper.h @@ -54,6 +54,10 @@ class TypeMapper // 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> &referencedTags() const diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index fce9cb078..abaea8b92 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -218,14 +218,8 @@ class Collector : public clang::RecursiveASTVisitor bool VisitRecordDecl(clang::RecordDecl *record) { - if (record->isImplicit() || !atFileScope(record)) - { - return true; - } - - if (auto *cxxRecord = llvm::dyn_cast(record); - cxxRecord && (cxxRecord->getDescribedClassTemplate() || - llvm::isa(cxxRecord))) + // the mapper spells a pointer to anything else as plain Opaque, so nothing names it + if (record->isImplicit() || !mapper.isDeclarable(record)) { return true; } diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp index bc2f49e63..b99cf861e 100644 --- a/tslang/lib/TsClangImporter/TypeMapper.cpp +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -1,6 +1,7 @@ #include "TsClangImporter/TypeMapper.h" #include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclTemplate.h" #include "clang/AST/RecordLayout.h" #include "clang/Basic/SourceManager.h" #include "llvm/ADT/SmallPtrSet.h" @@ -97,6 +98,23 @@ std::string TypeMapper::referTo(const clang::TagDecl *tag) return key; } +bool TypeMapper::isDeclarable(const clang::RecordDecl *record) +{ + if (!context.getLangOpts().CPlusPlus) + { + return true; // in C, a struct declared inside another is still file-scoped + } + + if (!record->getDeclContext()->getRedeclContext()->isTranslationUnit()) + { + return false; + } + + auto *cxxRecord = llvm::dyn_cast(record); + return !cxxRecord || + (!cxxRecord->getDescribedClassTemplate() && !llvm::isa(cxxRecord)); +} + bool TypeMapper::isSystem(const clang::Decl *decl) { auto &sourceManager = context.getSourceManager(); @@ -172,6 +190,11 @@ TsType TypeMapper::map(clang::QualType type, Use use) return TsType::skip(record->isUnion() ? "union passed by value" : "struct passed by value"); } + if (!isDeclarable(record)) + { + return TsType::skip("C++ class '" + canonical.getAsString() + "'"); + } + auto name = tagName(record); if (name.empty()) { @@ -267,6 +290,12 @@ TsType TypeMapper::mapPointer(const clang::PointerType *pointer, Use use) if (auto *record = canonicalPointee->getAsRecordDecl()) { + // there is no declaration a name could refer to, so no name: the C++ type is a comment + if (!isDeclarable(record)) + { + return TsType::builtin("Opaque", pointee.getUnqualifiedType().getAsString()); + } + if (tagName(record).empty()) { return TsType::skip("pointer to an anonymous struct"); diff --git a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp index a9c5845a7..7136130e3 100644 --- a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp +++ b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp @@ -71,6 +71,23 @@ TEST(TypeMapping, PointerToCxxClassIsOpaque) EXPECT_THAT(out, HasSubstr("declare function engine_power(e: Engine): s32;")); } +// A class in a namespace or a template specialization has no file-scope TS declaration to name +// (found in the default library: regexp_* take a std::cmatch *), so a pointer to one is plain +// Opaque, with the C++ type in a comment - never a name the output does not declare. +TEST(TypeMapping, PointerToANamespacedOrTemplateClassIsPlainOpaque) +{ + auto out = generate("namespace ns { class Engine { public: virtual ~Engine(); }; }\n" + "template struct Box { T value; };\n" + "typedef Box CharBox;\n" + "extern \"C\" int use(ns::Engine *e, Box *b, CharBox *c);\n", + {}, windowsTarget, {}, "c++") + .text; + EXPECT_THAT(out, HasSubstr("declare function use(e: Opaque /* ns::Engine */, b: Opaque /* Box */, " + "c: Opaque /* CharBox */): s32;")); + EXPECT_THAT(out, Not(HasSubstr("type Engine"))); + EXPECT_THAT(out, Not(HasSubstr("type Box"))); +} + TEST(TypeMapping, FunctionPointerIsOpaqueWithItsSignature) { auto out = text("int apply(int (*fn)(int, const char *), int v);\ntypedef void (*callback_t)(void *);\n" From cfdb826625eaf8f9f0f0fa84103c591534d3549f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 13:47:48 +0100 Subject: [PATCH 7/9] Spec: tsbindgen as built - .ts output, signed sN, type cycles Co-Authored-By: Claude Opus 5.5 --- .../2026-09-25-tsbindgen-linkname-design.md | 35 ++++++++++++++----- tslang/docs/reference-counting-evaluation.md | 9 +++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md b/docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md index 798426145..fa3ef6765 100644 --- a/docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md +++ b/docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md @@ -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. @@ -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`, @@ -153,7 +153,7 @@ The `-shared` cross-module tests import re-parsed declarations that carry ```text tsbindgen [-I]... [-D[=v]]... [--target ] [--filter ]... [--namespace [--strip-prefix

]] - [--resource-dir

] [-o ] [-- ...] + [--resource-dir ] [-o ] [-- ...] ``` - `--target` defaults to the host triple. Type widths come from clang's @@ -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` | | `T*`, T a scalar or complete struct | `Reference` | | `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) | @@ -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: — ` in its place and a warning on stderr: - a union, or a struct with a bitfield or an unmappable field; @@ -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 `) | Output is a `.ts` file, included with `/// `; `-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 `/// ` 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` | A pointer field that leads back to its own struct — directly, through another struct, or through a typedef — is `Opaque`, with `/* Reference: a type cannot refer to itself */` | tslang dies on a self-referencing type alias: `type Node = [value: i32, next: Reference]; 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 @@ -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. diff --git a/tslang/docs/reference-counting-evaluation.md b/tslang/docs/reference-counting-evaluation.md index 519372580..7ca8b6922 100644 --- a/tslang/docs/reference-counting-evaluation.md +++ b/tslang/docs/reference-counting-evaluation.md @@ -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. From 1bca3a9a4f30dfb4786055ef36fd3b0d39c5b9a0 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 14:28:52 +0100 Subject: [PATCH 8/9] tsbindgen: fix names and symbols a real header can break From the branch review: - a C type named like a tslang type (typedef int boolean) is looked through, a struct of such a name renamed: tslang ignores such an alias and bound an int result as boolean's one bit - a C name TS reserves (int new(void)) gets a `_` and @linkname - an __asm__ label is the symbol (glibc's __REDIRECT) - a malformed number in an unexpanded macro skips that macro instead of failing the header - a non-C calling convention (__stdcall on i686) is skipped, not bound as cdecl - an enum declared but never defined is its integer type - the e2e fixture is built at -O2, where a callee relies on its caller extending narrow arguments Co-Authored-By: Claude Opus 5.5 --- tslang/include/TsClangImporter/BindingModel.h | 5 +++ tslang/lib/TsClangImporter/BindingModel.cpp | 22 +++++++++++++ tslang/lib/TsClangImporter/BindingPrinter.cpp | 15 +++++++-- tslang/lib/TsClangImporter/HeaderParser.cpp | 33 ++++++++++++++++--- tslang/lib/TsClangImporter/TypeMapper.cpp | 17 ++++++++-- tslang/test/bindgen/run-bindgen-test.cmake | 6 ++-- .../unittests/TsClangImporter/NamingTest.cpp | 16 +++++++++ tslang/unittests/TsClangImporter/SkipTest.cpp | 19 +++++++++++ .../TsClangImporter/TypeMappingTest.cpp | 21 ++++++++++++ 9 files changed, 143 insertions(+), 11 deletions(-) diff --git a/tslang/include/TsClangImporter/BindingModel.h b/tslang/include/TsClangImporter/BindingModel.h index 5c792a171..44999f2a3 100644 --- a/tslang/include/TsClangImporter/BindingModel.h +++ b/tslang/include/TsClangImporter/BindingModel.h @@ -49,6 +49,10 @@ 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; @@ -77,6 +81,7 @@ struct Decl DeclKind kind = DeclKind::Function; std::string key; // unique within the model: ":" 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 diff --git a/tslang/lib/TsClangImporter/BindingModel.cpp b/tslang/lib/TsClangImporter/BindingModel.cpp index 8098382b8..a301ce144 100644 --- a/tslang/lib/TsClangImporter/BindingModel.cpp +++ b/tslang/lib/TsClangImporter/BindingModel.cpp @@ -93,4 +93,26 @@ std::string escapeReserved(std::string name) return name; } +bool isTsBuiltinTypeName(const std::string &name) +{ + // MLIRGenTypes.cpp's native type table, and the TS types tslang knows by name + static const char *const builtins[] = { + "byte", "short", "ushort", "int", "uint", "index", "long", "ulong", "char", + "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "s8", + "s16", "s32", "s64", "f16", "f32", "f64", "f128", "half", "float", + "double", "boolean", "number", "string", "bigint", "any", "void", "never", "unknown", + "object", "symbol", "undefined", "null", "Opaque", "Reference", "TypeOf", "Array", "String", + "Number", "Boolean", "Object", "Function"}; + + for (auto *builtin : builtins) + { + if (name == builtin) + { + return true; + } + } + + return false; +} + } // namespace tsbindgen diff --git a/tslang/lib/TsClangImporter/BindingPrinter.cpp b/tslang/lib/TsClangImporter/BindingPrinter.cpp index 5ee699eb7..f6d624089 100644 --- a/tslang/lib/TsClangImporter/BindingPrinter.cpp +++ b/tslang/lib/TsClangImporter/BindingPrinter.cpp @@ -139,6 +139,14 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO } } + // a legal C name TS reserves (`int new(void)`), and a type named like one of tslang's own, + // which tslang would ignore in favour of its own type (`struct string`), get a `_` + name = escapeReserved(name); + if (decl.kind != DeclKind::Function && decl.kind != DeclKind::Macro && isTsBuiltinTypeName(name)) + { + name += "_"; + } + taken[name] = decl.name; tsNames[decl.key] = name; } @@ -245,9 +253,12 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO } std::string decorators = decl.varargs ? "@varargs " : ""; - if (inNamespace) + // a namespace qualifies the symbol, and a TS name can differ from the C one + // (stripped, escaped) or the C name from the symbol (an asm label) + auto symbol = decl.symbol.empty() ? decl.name : decl.symbol; + if (inNamespace || tsName != symbol) { - decorators += "@linkname(\"" + decl.name + "\") "; + decorators += "@linkname(\"" + symbol + "\") "; } line(decorators + exported + "declare function " + tsName + "(" + parameters + diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index abaea8b92..2a9a972a1 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -76,7 +76,9 @@ std::string literalValue(clang::Preprocessor &preprocessor, llvm::ArrayRef bool VisitTypedefNameDecl(clang::TypedefNameDecl *typedefDecl) { - if (isSystem(typedefDecl) || !atFileScope(typedefDecl) || TypeMapper::namesItsTag(typedefDecl)) + if (isSystem(typedefDecl) || !atFileScope(typedefDecl) || TypeMapper::namesItsTag(typedefDecl) || + isTsBuiltinTypeName(typedefDecl->getName().str())) { - return true; + return true; // the mapper looks through all of these } auto name = typedefDecl->getName(); @@ -350,6 +357,22 @@ class Collector : public clang::RecursiveASTVisitor return true; } + // tslang calls a declaration with the C convention; a callee that cleans its own stack + // (__stdcall on 32-bit Windows) or takes arguments elsewhere would be called wrongly + if (prototype->getCallConv() != clang::CC_C) + { + decl.skipReason = "calling convention " + clang::FunctionType::getNameForCallConv(prototype->getCallConv()).str(); + return true; + } + + // `int f(int) __asm__("real_f")` (glibc's __REDIRECT): the symbol is the label + if (auto *asmLabel = function->getAttr()) + { + llvm::StringRef label = asmLabel->getLabel(); + label.consume_front("\x01"); // the "use this name verbatim" marker + decl.symbol = label.str(); + } + decl.varargs = prototype->isVariadic(); for (unsigned index = 0; index < function->getNumParams(); ++index) { diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp index b99cf861e..70ccd0061 100644 --- a/tslang/lib/TsClangImporter/TypeMapper.cpp +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -140,8 +140,9 @@ TsType TypeMapper::map(clang::QualType type, Use use) } // a system header's typedefs (int32_t, uint8_t, ...) are looked through: their widths are - // what matter, and the header is not emitted - if (isSystem(typedefDecl)) + // what matter, and the header is not emitted. So is one named like a tslang type + // (`typedef int boolean;`): tslang would ignore the alias and use its own type. + if (isSystem(typedefDecl) || isTsBuiltinTypeName(typedefDecl->getName().str())) { return map(typedefDecl->getUnderlyingType(), use); } @@ -174,6 +175,18 @@ TsType TypeMapper::map(clang::QualType type, Use use) if (auto *enumType = canonical->getAs()) { auto *enumDecl = enumType->getDecl()->getDefinitionOrSelf(); + // declared, never defined (`enum E : short;`): no enumerators to emit, only its integer + if (!enumDecl->getDefinition()) + { + auto integer = enumDecl->getIntegerType(); + if (integer.isNull()) + { + return TsType::skip("incomplete enum '" + tagName(enumDecl) + "'"); + } + + return mapBuiltin(integer.getCanonicalType(), use); + } + if (tagName(enumDecl).empty()) { // an anonymous enum's values are plain integers of its underlying type diff --git a/tslang/test/bindgen/run-bindgen-test.cmake b/tslang/test/bindgen/run-bindgen-test.cmake index f504a41da..c00532f14 100644 --- a/tslang/test/bindgen/run-bindgen-test.cmake +++ b/tslang/test/bindgen/run-bindgen-test.cmake @@ -74,8 +74,10 @@ function(expect_output what) message(STATUS "${what}: as expected") endfunction() -run("clang -c fixture.c" "${CLANG}" -c ${clang_pic} fixture.c -o "fixture${obj_suffix}") -run("clang -shared fixture.c" "${CLANG}" -shared ${clang_pic} fixture.c -o "${fixture_library}") +# -O2 as a real library would be built: at -O0 clang re-extends a narrow parameter inside the callee, +# which would hide a caller that does not extend it (the `narrow` line, on the SysV ABI) +run("clang -c fixture.c" "${CLANG}" -O2 -c ${clang_pic} fixture.c -o "fixture${obj_suffix}") +run("clang -shared fixture.c" "${CLANG}" -O2 -shared ${clang_pic} fixture.c -o "${fixture_library}") run("tsbindgen" "${TSBINDGEN}" fixture.h ${bindgen_args} -o "${bindings}") run("--emit=exe" "${TSLANG}" --emit=exe ${common} "${program}" "--obj=fixture${obj_suffix}" -o "bindgen_test${exe_suffix}") diff --git a/tslang/unittests/TsClangImporter/NamingTest.cpp b/tslang/unittests/TsClangImporter/NamingTest.cpp index 82f24c4d5..1d59ec1fd 100644 --- a/tslang/unittests/TsClangImporter/NamingTest.cpp +++ b/tslang/unittests/TsClangImporter/NamingTest.cpp @@ -66,3 +66,19 @@ TEST(Naming, HeaderLinesComeFirst) "\n" "declare function f(): s32;\n"); } + +// legal C names that TS reserves: the TS name gets a `_`, and @linkname keeps the symbol +TEST(Naming, ReservedWordDeclarationNamesAreEscaped) +{ + auto out = text("int new(void);\nint class(int in);\nstruct export { int x; };\n"); + EXPECT_THAT(out, HasSubstr("@linkname(\"new\") declare function new_(): s32;")); + EXPECT_THAT(out, HasSubstr("@linkname(\"class\") declare function class_(in_: s32): s32;")); + EXPECT_THAT(out, HasSubstr("type export_ = [x: s32];")); +} + +// an asm label is the symbol the function links as (glibc's __REDIRECT: stat -> stat64) +TEST(Naming, AsmLabelIsTheSymbol) +{ + EXPECT_THAT(text("int f(int x) __asm__(\"real_f\");\n"), + HasSubstr("@linkname(\"real_f\") declare function f(x: s32): s32;")); +} diff --git a/tslang/unittests/TsClangImporter/SkipTest.cpp b/tslang/unittests/TsClangImporter/SkipTest.cpp index 111419637..06c448e23 100644 --- a/tslang/unittests/TsClangImporter/SkipTest.cpp +++ b/tslang/unittests/TsClangImporter/SkipTest.cpp @@ -81,3 +81,22 @@ TEST(Skip, ASkipDoesNotStopGeneration) EXPECT_THAT(out, HasSubstr("declare function before(): s32;")); EXPECT_THAT(out, HasSubstr("declare function after(): s32;")); } + +// a macro body is never checked by clang unless used, so a malformed number skips the macro, +// never the whole header +TEST(Skip, MalformedNumericMacroIsSkippedNotAnError) +{ + auto out = text("#define VERSION 1.2.3\n#define OCT 08\nint f(void);\n"); + EXPECT_THAT(out, HasSubstr("// skipped: VERSION \xE2\x80\x94 macro is not a literal")); + EXPECT_THAT(out, HasSubstr("// skipped: OCT \xE2\x80\x94 macro is not a literal")); + EXPECT_THAT(out, HasSubstr("declare function f(): s32;")); +} + +// tslang calls a declared function with the C convention; a callee that cleans its own stack +// (__stdcall on 32-bit Windows) would corrupt it +TEST(Skip, NonDefaultCallingConventionIsSkipped) +{ + auto out = generate("int __stdcall f(int x);\nint __cdecl g(int x);\n", {}, "i686-pc-windows-msvc").text; + EXPECT_THAT(out, HasSubstr("// skipped: f \xE2\x80\x94 calling convention stdcall")); + EXPECT_THAT(out, HasSubstr("declare function g(x: s32): s32;")); +} diff --git a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp index 7136130e3..64881812a 100644 --- a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp +++ b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp @@ -184,3 +184,24 @@ TEST(TypeMapping, ParameterNames) EXPECT_THAT(text("void f(int, int delete, int function, int value);\n"), HasSubstr("declare function f(p0: s32, delete_: s32, function_: s32, value: s32): void;")); } + +// tslang ignores a `type` alias whose name is one of its own types - `typedef int boolean` (libjpeg) +// would bind an int result as boolean's one bit - so such a typedef is looked through and never +// emitted, and a struct of such a name is renamed +TEST(TypeMapping, BuiltinTypeNamesAreNeverRedeclared) +{ + auto out = text("typedef int boolean;\nboolean is_ok(double n);\nstruct string { int len; };\n" + "void use(struct string *s);\n"); + EXPECT_THAT(out, Not(HasSubstr("type boolean"))); + EXPECT_THAT(out, HasSubstr("declare function is_ok(n: f64): s32;")); + EXPECT_THAT(out, HasSubstr("type string_ = [len: s32];")); + EXPECT_THAT(out, HasSubstr("declare function use(s: Reference): void;")); +} + +// an enum that is declared but never defined has no enumerators to emit: its values are integers +TEST(TypeMapping, EnumWithoutDefinitionIsItsInteger) +{ + auto out = generate("enum Mode : short;\nextern \"C\" void set_mode(Mode m);\n", {}, windowsTarget, {}, "c++").text; + EXPECT_THAT(out, HasSubstr("declare function set_mode(m: s16): void;")); + EXPECT_THAT(out, Not(HasSubstr("Mode"))); +} From 08a239ad437cf5f42bfae78fa4ae66ff1c887c9f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 26 Sep 2026 14:51:05 +0100 Subject: [PATCH 9/9] tsbindgen: find its own path without taking main's address gcc -Wpedantic -Werror (Linux CI) rejects &main. Co-Authored-By: Claude Opus 5.5 --- tslang/tsbindgen/tsbindgen.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tslang/tsbindgen/tsbindgen.cpp b/tslang/tsbindgen/tsbindgen.cpp index b4e66399a..6487b0aa9 100644 --- a/tslang/tsbindgen/tsbindgen.cpp +++ b/tslang/tsbindgen/tsbindgen.cpp @@ -105,6 +105,12 @@ bool isCxx(llvm::StringRef path) extension == ".hh" || extension == ".hxx"; } +std::string executablePath(const char *argv0) +{ + // any function in this binary will do; ISO C++ forbids taking main's address (-Wpedantic) + return llvm::sys::fs::getMainExecutable(argv0, reinterpret_cast(&executablePath)); +} + } // namespace int main(int argc, char **argv) @@ -147,7 +153,7 @@ int main(int argc, char **argv) } std::vector tried; - auto exePath = llvm::sys::fs::getMainExecutable(argv[0], reinterpret_cast(&main)); + auto exePath = executablePath(argv[0]); auto foundResourceDir = findResourceDir(exePath, tried); if (!foundResourceDir) {