本文へ移動
cccskills
無料GitHub で公開

Compiler Plugin API (CBI)

Comprehensive guide to the Chemical Compiler Binding Interface (CBI) — how compiler plugins are built, registered, and integrated into the compilation pipeline.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemicalスター 31(GitHub 31 + cccskills 0)

実行スコア 未検証

インストール方法を見る

含まれるファイル(1)

  • SKILL.md18.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Compiler Plugin API (CBI)

The Chemical Compiler Binding Interface (CBI) allows external Chemical code to hook into the compilation process. Plugins are compiled via TinyCC JIT at build time and can intercept lexing, parsing, symbol resolution, and codegen.

Architecture

How Plugins Work

  1. Plugin declaration: A build.lab script implements build(ctx : *mut BuildContext, user_job : *mut LabJob) : *mut Module and creates a CBI job with ctx.build_cbi(name) (LabJobType::CBI)
  2. Plugin compilation: The plugin's Chemical source is compiled to C, then JIT-compiled via TinyCC like any other module
  3. Function indexing: The build script registers each plugin entrypoint with ctx.index_cbi_fn(...) (or ctx.index_def_cbi_fn(...)), recording its name and its CBIFunctionType in the binder
  4. Hook execution: During compilation the binder looks up the function by (key, CBIFunctionType) and calls it — e.g. ParseMacroNode, SymResNode, ReplacementNode

Key Files

FilePurpose
lang/libs/compiler/Compiler API bindings — the API that plugins call (interfaces declared in chemical.mod)
lang/libs/compiler/src/ASTBuilder.chAST node construction — create any Chemical AST node
lang/libs/compiler/src/Lexer.chLexer bindings — tokenize Chemical source
lang/libs/compiler/src/Parser.chParser bindings — parse Chemical source
lang/libs/compiler/src/SymbolResolver.chSymbol resolver bindings
lang/libs/compiler/src/SymbolTable.chSymbol table bindings
lang/libs/compiler/src/ASTDiagnoser.chDiagnostics bindings for plugins
lang/libs/compiler/src/BatchAllocator.chArena allocator for AST nodes
lang/libs/compiler/src/SourceProvider.chSource file provider
lang/libs/lab/src/lab.chBuildContext, Module, LabJob interfaces used by build.lab
compiler/cbi/model/CompilerBinder.hC++ side — binds CBI functions to plugin symbols
compiler/cbi/model/CBIFunctionType.hC++ side — function type definitions for CBI
compiler/cbi/model/ASTBuilder.hC++ side — base ASTBuilder class (arena allocation)
compiler/cbi/model/Model.hC++ side — CBI function typedefs / data model
compiler/cbi/bindings/CBI.cppCBI symbol maps (*SymMap) and prepare_cbi_maps

Plugin Structure

Basic Plugin Template

Plugin entrypoints are @no_mangle public functions with the exact C signatures declared in compiler/cbi/model/Model.h. The parser/symbol-resolver/codegen call them when the corresponding index was registered.

// lang/libs/my_plugin/src/main.ch

// Parse hook: called when the parser encounters `#my_macro`
@no_mangle
public func my_parseMacroNode(parser : *mut Parser, builder : *mut ASTBuilder) : *mut ASTNode {
    const tok = parser.getToken()
    const loc = parser.getEncodedLocation(tok)
    // consume tokens, parse the macro body, and build an EmbeddedNode
    // (see html_cbi's html_parseMacroNode for a full example)
    return builder.make_embedded_node(
        AccessSpecifier.Internal, std::string_view("my_macro"), data_ptr,
        node_known_type_func, node_child_res_func,
        std::span<*mut ASTNode>(...), std::span<*mut Value>(...), parent, loc)
}

// Symbol resolution hook: link identifiers found inside the macro
@no_mangle
public func my_symResNode(visitor : *mut SymResLinkBody, node : *mut EmbeddedNode) {
    visitor.visitEmbeddedNode(node)
    const resolver = visitor.getSymbolResolver()
    // ... resolve the macro's embedded nodes ...
}

// Replacement hook: return the AST node that finally generates code
@no_mangle
public func my_replacementNode(builder : *mut ASTBuilder, diagnoser : *mut ASTDiagnoser, value : *mut EmbeddedNode) : *ASTNode {
    const root = value.getDataPtr() as *mut MyRoot
    // ... build and return replacement AST ...
}

Plugin Registration in build.lab

// build.lab
public func build(ctx : *mut BuildContext, user_job : *mut LabJob) : *mut Module {

    const cbi_name = std::string_view("my_macro");
    const empty_module = ctx.empty_module(std::string_view(""), std::string_view("my_plugin"));
    if(ctx.contains_cbi(&cbi_name)) {
        return empty_module;
    }
    ctx.set_contains_cbi(&cbi_name)

    var cbi = ctx.build_cbi(&cbi_name);
    ctx.put_job_before(cbi, user_job)

    // build dependencies (std, cstd, compiler bindings, ...)
    var std_module = stdMod.build(ctx, cbi);
    var cstd_module = cstdMod.build(ctx, cbi);
    var compiler_module = compilerMod.build(ctx, cbi);
    const dependencies = [ cstd_module, std_module, compiler_module ]
    var src_path = lab::rel_path_to("src")
    const module = ctx.chemical_dir_module(std::string_view(""), std::string_view("my_plugin"), src_path.to_view(), dependencies);
    ctx.add_module(cbi, module)

    // index plugin functions with their CBIFunctionType
    ctx.index_def_cbi_fn(cbi, std::string_view("my_parseMacroNode"), CBIFunctionType.ParseMacroNode);
    ctx.index_def_cbi_fn(cbi, std::string_view("my_symResNode"), CBIFunctionType.SymResNode);
    ctx.index_def_cbi_fn(cbi, std::string_view("my_replacementNode"), CBIFunctionType.ReplacementNode);

    return empty_module;
}

Compiler API Bindings

The compiler API is in lang/libs/compiler/src/. These are Chemical files that expose compiler functionality to plugins.

Key Classes

ClassFilePurpose
ASTBuilderASTBuilder.chCreate AST nodes (functions, structs, statements, values, types)
LexerLexer.chTokenize Chemical source code
ParserParser.chParse Chemical source into AST
SymbolResolverSymbolResolver.chResolve symbols in AST
SymbolTableSymbolTable.chDeclare and look up symbols
BatchAllocatorBatchAllocator.chArena allocator for AST nodes
SourceProviderSourceProvider.chSource file access
ASTDiagnoserASTDiagnoser.chReport errors and warnings
TokenToken.chToken structure
PositionPosition.chSource position
OperationOperation.chOperation types

ASTBuilder API

The ASTBuilder is the most important class for plugin development:

// Create types (loc is a ubigint encoded location, e.g. parser.getEncodedLocation(tok)):
var builder = ASTBuilder()
var intType = builder.get_int_type()                       // *mut IntType
var ptrType = builder.make_ptr_type(intType, false, loc)   // *int
var mutPtrType = builder.make_ptr_type(intType, true, loc) // *mut int

// Create values:
var intVal = builder.make_int_value(42, loc)
var stringVal = builder.make_string_value(&view, loc)
var structVal = builder.make_struct_value(structDecl, loc)

// Create statements:
var varDecl = builder.make_varinit_stmt(false, false, "x", intType, intVal, AccessSpecifier.Internal, parent, loc)
var funcCall = builder.make_function_call_value(parentVal, loc)

// Create functions:
var funcDecl = builder.make_function("my_func", returnType, false, parent, loc)

Built-in Plugins

These are the plugins shipped with the compiler. Each demonstrates different CBI patterns:

PluginLocationPurposeKey Features
html_cbilang/libs/html_cbi/#html macroParses HTML/JSX (shared html_comp AST) and emits page.append_html* calls; resolves #styled components (MountStrategy.Styled) in #html
css_cbilang/libs/css_cbi/#css, #globalcss, #styled macros#css parses CSS properties into style strings and is always page-local; #globalcss emits app-wide global CSS to the shared sink (page fallback); #styled declares scoped, CSS-injecting components usable in #html
js_cbilang/libs/js_cbi/#js, #globaljs macrosParses JavaScript with the shared js_syntax AST / js_parser and inlines it into the JS bundle; #js is page-local, #globaljs goes to the shared sink (page fallback, once per source location)
universal_cbilang/libs/universal_cbi/#universal componentSSR + hydration — generates server function + JS hydration; uses the shared js_syntax AST
json_cbilang/libs/json_cbi/#json(Struct) macroAuto-generates std::Serializer/std::Deserializer impls; uses a marker annotation and top-level node hooks
md_cbilang/libs/md_cbi/Markdown processingConverts markdown to HTML

Supporting (non-registered) libraries these CBIs build on: html_comp/, html_parser/, html_ide/, js_syntax/ (shared token/AST definitions), js_cbi_lexer/ (compiler-side JS lexer), js_parser/, js_ide/, css_parser/, css_ide/, md_parser/, md_ide/, universal_parser/, universal_ide/.

Example: html_cbi Structure

lang/libs/html_cbi/
├── build.lab               # CBI build script — creates the job and indexes hooks
└── src/
    ├── main.ch             # Plugin entry — @no_mangle parse/symres/replacement + lexer init
    ├── converter/language/ # HtmlRoot -> Chemical AST conversion
    ├── sym_res/
    │   └── sym_res_root.ch # Symbol resolution helpers
    └── utils/
        └── comptime_utils.ch

CBI Function Types

The CBIFunctionType enum (compiler/cbi/model/CBIFunctionType.h, mirrored in lang/libs/lab/src/lab.ch) defines all hook kinds used in the CBI:

enum class CBIFunctionType : int {
    InitializeLexer,
    ParseMacroValue,
    ParseMacroNode,
    ParseMacroTopLevelNode,
    ParseMacroMemberNode,
    SymResDeclareTopLevelNode,
    SymResLinkSignatureNode,
    SymResLinkSignatureValue,
    SymResNode,
    SymResValue,
    ReplacementNodeDeclare,
    ReplacementNode,
    ReplacementValue,
    SemanticTokensPut,
    FoldingRangesPut,
    TransformerMain
};

Each function type has a corresponding C typedef (declared in compiler/cbi/model/Model.h):

// InitializeLexer:
typedef void(*EmbeddedLexerInitializeFn)(Lexer* lexer);

// ParseMacroValue:
typedef Value*(*EmbeddedParseMacroValueFn)(Parser* parser, ASTBuilder* builder);

// ParseMacroNode:
typedef ASTNode*(*EmbeddedParseMacroNodeFn)(Parser* parser, ASTBuilder* builder);

// SymResNode:
typedef void(*EmbeddedNodeSymbolResolveFunc)(SymResLinkBody* visitor, EmbeddedNode* value);

// SymResValue:
typedef bool(*EmbeddedValueSymbolResolveFunc)(SymResLinkBody* visitor, EmbeddedValue* value);

// ReplacementNode:
typedef ASTNode*(*EmbeddedNodeReplacementFunc)(ASTBuilder* builder, ASTDiagnoser* diagnoser, EmbeddedNode* value);

// ReplacementValue:
typedef Value*(*EmbeddedValueReplacementFunc)(ASTBuilder* builder, ASTDiagnoser* diagnoser, EmbeddedValue* value);

The CompilerBinder

CompilerBinder is the C++ class that manages CBI function registration:

struct CBIFunctionKey {
    chem::string_view key;
    CBIFunctionType type;
};

class CompilerBinder {
    // name -> TCC module state (private; accessible via get_cbi_map())
    util::unordered_string_map<CBIData> data;

public:
    // all indexed functions, keyed by (name, CBIFunctionType)
    std::unordered_map<CBIFunctionKey, void*, CBIFunctionHash> hooks_;

    // interface name -> exported symbol table (SourceProvider, Lexer, ASTBuilder, ...)
    std::unordered_map<chem::string_view, std::span<const std::pair<chem::string_view, void*>>> interface_maps;

    // store a compiled CBI module, guarding against overriding an existing one
    bool store_cbi(std::string name, TCCState* state);

    bool contains_cbi(const std::string_view& name);

    // import a compiler interface's functions into a TCC state
    static void import_compiler_interface(const std::span<const std::pair<chem::string_view, void*>>& interface, TCCState* state);

    // index a plugin function for a given (key, type)
    void registerHook(CBIFunctionType type, const chem::string_view& key, void* function);

    // look up an indexed plugin function
    void* findHook(const chem::string_view& key, CBIFunctionType type) const noexcept;

    // resolve a function by name in a compiled TCC state
    const char* index_function(CBIFunctionIndex& index, TCCState* state);
};

Creating a New Plugin

Step-by-Step

  1. Create the directory structure (a CBI package is driven by build.lab; a chemical.mod is optional):

    lang/libs/my_plugin/
    ├── build.lab
    └── src/
        └── main.ch
    
  2. Write main.ch — one @no_mangle public func per hook you want to expose:

    @no_mangle
    public func my_parseMacroNode(parser : *mut Parser, builder : *mut ASTBuilder) : *mut ASTNode {
        // ... parse the macro body and build an EmbeddedNode ...
    }
    
  3. Write build.lab — create the CBI job, add the module, and index the functions:

    public func build(ctx : *mut BuildContext, user_job : *mut LabJob) : *mut Module {
        const cbi_name = std::string_view("my_macro");
        const empty_module = ctx.empty_module(std::string_view(""), std::string_view("my_plugin"));
        if(ctx.contains_cbi(&cbi_name)) {
            return empty_module;
        }
        ctx.set_contains_cbi(&cbi_name)
    
        var cbi = ctx.build_cbi(&cbi_name);
        ctx.put_job_before(cbi, user_job);
        // ... build deps + ctx.add_module(cbi, module) ...
    
        ctx.index_def_cbi_fn(cbi, std::string_view("my_parseMacroNode"), CBIFunctionType.ParseMacroNode);
        return empty_module;
    }
    
  4. Make it available to consumers — a module adds import my_plugin to its chemical.mod; the compiler resolves lang/libs/my_plugin/, finds build.lab, and runs its build function:

    module my_app
    source "src"
    import std
    import my_plugin
    
  5. Test the plugin:

    cmake-build-debug/TCCCompiler "my_app/chemical.mod" -o my_app.exe -v -frecompile-plugins
    

Debugging Plugins

Plugin Debug Mode

--plugin-mode debug_complete  # Full debug info for plugins

Common Plugin Issues

IssueCauseDebug
Plugin not foundModule directory has no entry pointEnsure it contains build.lab or chemical.mod
Macro handler not calledHook not indexedVerify ctx.index_cbi_fn / index_def_cbi_fn call
CBI function not foundMissing TinyCC symbolCheck tcc_add_symbol calls
AST crashAST node created incorrectlyUse BatchAllocator for allocation
Symbol resolution errorWrong symbol table contextUse the correct SymbolTable scope
TinyCC compile errorChemical code has syntax errorsRun plugin through compiler independently

Testing Plugins

# All plugin tests (`test_plugin_exe(..., "all")`):
./scripts/test.sh --tcc --plugins

# A single plugin's tests are selected via a build.lab arg passed to the compiler:
cmake-build-debug/TCCCompiler lang/tests/build.lab --arg-test-html --mode debug_complete
cmake-build-debug/TCCCompiler lang/tests/build.lab --arg-test-css  --mode debug_complete

Related Skills

  • Intrinsics & Compiler Reflection (.agents/skills/intrinsics_compiler_reflection/SKILL.md) — Compiler intrinsics that plugins can use for compile-time evaluation, reflection, and metadata access
  • Build System (.agents/skills/build_system/SKILL.md) — How plugins are compiled, loaded, and integrated into the build pipeline
  • Macro Codegen (.agents/skills/macro_code_gen/SKILL.md) — Examples of how existing plugins (html_cbi, universal_cbi) use the CBI API
  • Compiler API (.agents/skills/compiler_api/SKILL.md) — Compiler API bindings for AST construction, lexing, parsing

CRITICAL: Enum Sync Rule

When adding a new enum value to the C++ compiler, you MUST also add the same value to the corresponding Chemical binding file. Failing to do so causes a SIGSEGV crash in all CBI plugins because the enum values are off by 1 between the C++ side and the TCC-compiled plugin side.

Affected Enums (must stay in sync)

C++ Enum (must match)Chemical Binding File
ASTNodeKind (in ast/base/ASTNodeKind.h)lang/libs/compiler/src/ast/base/ASTNodeKind.ch
TokenType (in lexer/TokenType.h)lang/libs/compiler/src/ChemicalTokenType.ch

How It Works

CBI plugins are compiled by TCC from Chemical source. They import the compiler library, which contains .ch files defining enum values that mirror the C++ enums. The TCC-compiled plugin code uses these Chemical enum values in switch statements and comparisons. If the Chemical enum is missing a value that was added to the C++ enum, every value after the insertion point is shifted by 1, causing wrong branches and crashes.

Real Bug Example

Adding InlineAsmStmt to ASTNodeKind in C++ (between PlacementNewNode and EnumDecl) without adding it to ASTNodeKind.ch caused EnumDecl to be 31 in C++ but 32 in the plugin. Every switch(node.getKind()) statement took wrong branches, dereferencing garbage pointers, and SIGSEGV at ASTNodegetKind.

Adding AsmKw to TokenType in C++ without adding it to ChemicalTokenType.ch caused RBrace to be off by 1, breaking #html macro parsing entirely.

The Fix

When adding a new enum value to a C++ enum that is exposed to CBI:

  1. Add the value in the C++ enum at the correct position
  2. Add the same value in the corresponding .ch file at the same position
  3. Verify both files have identical ordering for all values

Which Direction Is Safe?

Values at the end of an enum can be added without breaking existing plugins (they won't reference the new value). Values inserted in the middle break all subsequent values and MUST be synced immediately.

Performance Considerations

  1. TinyCC JIT: Fast compilation but slower generated code — acceptable for build-time plugins
  2. AST allocation: Always use BatchAllocator from the compiler — arena allocation is fast
  3. Minimize CBI calls: Each call crosses the Chemical→C++ boundary, which has overhead
  4. Plugin caching: --cached-plugins skips recompilation of unchanged plugins
  5. Compile once: Plugins are compiled once and reused across all jobs in a build

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Building

無料

All documentation related to building and running Project or Tests

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

chemicallang のスキルをすべて見る

このスキルの問題を報告する