본문으로 건너뛰기
cccskills
무료GitHub에 공개

Annotations

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/chemical스타 31(GitHub 31 + cccskills 0)

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md31.6 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Annotations

Annotations are the @name / @name(args) attribute syntax in Chemical source. They attach compiler directives to the declaration that follows them: @extern, @test, @make, @deprecated, @no_mangle, @inline, @volatile, and many more.

Overview

Syntax

@extern public func printf(format : *char, _ : any...) : int

@test
public func my_test(env : &mut TestEnv) { ... }

@test.timeout(60000)
public func slow_test(env : &mut TestEnv) { ... }

@direct_init
public struct RGBA8 {
    var r : u8; var g : u8; var b : u8; var a : u8
    @make func make(r_ : u8, g_ : u8, b_ : u8, a_ : u8 = 255) : RGBA8 { ... }
}
  • An annotation starts with @ immediately followed by an identifier-start character.
  • The name may contain letters, digits, _, ., and : (lexer/Lexer.cpp:724 read_annotation_id). This is why names like @inline.always, @compiler.interface, @size:opt, and @test.timeout work — the whole dotted/colon name is one TokenType::Annotation token.
  • Arguments are optional and parenthesised: @align(8), @test.timeout(60000). The argument list is parsed with parseExpressionOrArrayOrStruct, so arbitrary constant expressions and comma-separated lists are accepted (parser/statements/AnnotationMacro.cpp:32-50).
  • An annotation applies to the next declaration node parsed; the parser buffers them in order and flushes them with Parser::annotate(node).
  • An unknown annotation name is a hard parse error: unknown annotation found '@foo' (parser/statements/AnnotationMacro.cpp:27-30).

Where annotations can appear

Parser::parseAnnotation is invoked from the top-level statement loop (parser/Parser.cpp:80), the nested-statement loop (parser/structures/Block.cpp:22), and struct body parsing (parser/structures/Struct.cpp:203,216). Buffered annotations are consumed by Parser::annotate(node) at every declaration construction site:

Declarationannotate() call site
Function (func)parser/structures/Function.cpp:464
Struct declarationparser/structures/Struct.cpp:62
Struct memberparser/structures/Struct.cpp:29
Union declarationparser/structures/Union.cpp:17,59
Variant declaration / memberparser/structures/Variant.cpp:141,19
Interface declarationparser/structures/Interface.cpp:25
Enum declarationparser/structures/Enum.cpp:26
Impl blockparser/structures/ImplDef.cpp:23
Namespaceparser/structures/Namespace.cpp:17
var / const statementparser/statements/VarInitialization.cpp:187
type aliasparser/statements/Typealias.cpp:139

There is no static "valid parent" list enforced before parsing — an annotation is applied to whatever node annotate() receives next. Each handler validates the node kind at runtime and calls parser->error(...) when the target is wrong (see the table below).

Key Files

FilePurpose
compiler/frontend/AnnotationController.hAnnotationController, AnnotationDefinition, AnnotationDefType, SingleMarkerMultiplePolicy, storage maps, creation/mark/collect/query APIs
compiler/frontend/AnnotationController.cppAll intrinsic annotation handlers (annot_handler_*), built-in definitions table in initialize(), handle_annotation, mark_single
parser/statements/AnnotationMacro.cppParser::parseAnnotation (token → buffered annotation) and Parser::annotate (apply to node)
parser/Parser.hSavedAnnotation struct (annotation def + args), annotations buffer, parseAnnotation/annotate declarations
lexer/Lexer.cppread_annotation_id — allowed annotation-name characters; emits TokenType::Annotation
lexer/TokenType.hTokenType::Annotation enum value (lexer/TokenType.h:146)
ast/base/AnnotationKind.hEmpty legacy enum (see "Legacy model")
ast/base/Annotation.h, ast/base/Annotation.cppLegacy/unused node-local annotation storage
ast/base/AnnotationParent.hLegacy/unused container for node-local annotations
ast/base/AnnotableNode.hLegacy/unused marker base class
ast/base/ASTNode.hset_no_mangle / set_deprecated / set_anonymous virtual flags (:225-237), isAnnotableNode (:601)
compiler/cbi/bindings/AnnotationController.cpp, AnnotationControllerCBI.hCBI exposure of the controller to compiler plugins
lang/libs/compiler/src/AnnotationController.chChemical CBI interface used by plugins to read/mark/collect annotations
lang/libs/lab/src/AnnotationController.chChemical CBI interface used by build.lab scripts to create annotation definitions
ast/utils/GlobalFunctions.cppIntrinsics that read the controller: get_tests, get_single_marked_decl_ptr (:1575, :1623)

The Active Model: AnnotationController

All annotation state lives in a single AnnotationController owned by LabBuildCompiler/ASTProcessor (compiler/ASTProcessor.h:86, compiler/lab/LabBuildCompiler.cpp:207,262). It is shared across all files and jobs, and because files are parsed in parallel, its mutating operations are guarded by mutexes (single_marker_mutex, marker_mutex, collector_mutex — AnnotationController.h:153-165).

AnnotationDefType — the kind of definition

AnnotationDefType (AnnotationController.h:23-47) is the real "annotation kind" enum:

AnnotationDefTypeMeaningStorageQuery API
HandlerCalls a C++ handler(parser, node, args) function that mutates the nodenonehandle_annotation
SingleMarkerExactly one node per build may carry this name; subsequent uses obey a SingleMarkerMultiplePolicysingle_marked map (name → CollectedAnnotation)get_single_marked(name)
MarkerMarks any number of nodes; presence is the payloadmarked map ({node,name} → args)is_marked(node,name), get_args(node,name)
CollectorCollects nodes into an indexed collectioncollections[id].nodesget_collection(id)
MarkerAndCollectorDoes both Marker and Collector workbothboth

AnnotationDefinition (AnnotationController.h:75-94) holds a name, a type, and a union that is read according to type:

  • handler (function pointer) — for Handler
  • collection_id (std::size_t) — for Collector / MarkerAndCollector
  • policy (SingleMarkerMultiplePolicy) — for SingleMarker

Reading the union member that does not correspond to type is undefined — each definition is initialised with only its own member (see the aggregate/designated initialisers in AnnotationController.cpp:364-419).

SingleMarkerMultiplePolicy (AnnotationController.h:49-53): Override (last wins), Ignore (first wins), Error (a second use reports an error). See AnnotationController::mark_single (AnnotationController.cpp:433-453).

Storage & query

There is no per-node annotation vector in the active system. The controller is the single source of truth:

std::unordered_map<chem::string_view, AnnotationDefinition> definitions;   // name → def
std::vector<AnnotationCollection> collections;                              // Collector buckets
std::unordered_map<MarkedAnnotatedNode, std::vector<Value*>, ...> marked;   // (node,name) → args
std::unordered_map<chem::string_view, CollectedAnnotation> single_marked;   // name → {node,args}

(AnnotationController.h:129-145.) Key-based lookups use the composite MarkedAnnotatedNode { ASTNode* ptr; chem::string_view sv; } with a custom hash/equality (AnnotationController.h:97-121).

Query/creation APIs (AnnotationController.h:206-280):

APIPurpose
get_definition(name)Look up definition by exact name (nullptr if unknown)
create_collector_annotation(name, expected)Register a Collector
create_marker_annotation(name)Register a Marker
create_marker_and_collector_annotation(name, expected)Register a MarkerAndCollector
create_single_marker_annotation(name, policy)Register a SingleMarker
is_marked(node, name)Presence check for a Marker
get_args(node, name)Annotation arguments for a marked node (nullptr if absent)
get_single_marked(name)CollectedAnnotation* for a SingleMarker
get_collection(id)Collection bucket
handle_annotation(def, parser, node, args)Dispatch by AnnotationDefType
clear_marked_or_collected()Clear marked, single_marked, and all collections between jobs
clear()Full reset: wipe definitions + marks, then initialize() again

handle_annotation (AnnotationController.cpp:455-477) switches on the definition type and either invokes the handler, marks, collects, or errors.

Legacy AnnotationKind / AnnotationParent model (do not use)

The task of tracking node-local annotations is not what the code does today. The following are dead/vestigial and must not be extended:

  • ast/base/AnnotationKind.h declares enum class AnnotationKind {}; — it is empty (AnnotationKind.h:7-9). There are no built-in enum kinds.
  • Annotation / AnnotationParent (ast/base/Annotation.h, Annotation.cpp, AnnotationParent.h) implement a vector-of-annotations-on-a-node design with add_annotation, get_annotation, has_annotation, get_all. No class inherits AnnotationParent and no node stores annotations through it.
  • AnnotableNode (ast/base/AnnotableNode.h) is an empty ASTNode subclass; nothing derives from it either. ASTNode::isAnnotableNode (ASTNode.h:601-603) still lists node categories (UsingStmt, VarInitStmt, NamespaceDecl, isBaseDefMember, isFunctionDecl, isMembersContainer) and as_annotable_node can cast, but no annotation logic reads it.
  • Note a latent bug in the dead code: Annotation::has_annotation calls get_annotation(kind) instead of get_annotation(expected) (Annotation.h:49-51).

Use AnnotationController instead. The only "target model" is the runtime node-kind check inside each handler.

Built-in Annotations

All built-ins are registered in AnnotationController::initialize() (compiler/frontend/AnnotationController.cpp:364-419). Names are matched exactly (@test.id ≠ @test).

Handler annotations (mutate node flags)

AnnotationValid target(s)SemanticsHandler (file:line)
@inlinefunctionInlineStrategy::InlineHintAnnotationController.cpp:20
@inline.alwaysfunctionInlineStrategy::AlwaysInlineAnnotationController.cpp:29
@noinline / @inline.nofunctionInlineStrategy::NoInlineAnnotationController.cpp:38
@compiler.inlinefunctionInlineStrategy::CompilerInlineAnnotationController.cpp:47
@size:optfunctionInlineStrategy::OptSizeAnnotationController.cpp:56
@size:minfunctionInlineStrategy::MinSizeAnnotationController.cpp:65
@compiler.interfaceinterfaceset_extern(true) + set_is_static(true) (node->kind()==InterfaceDecl)AnnotationController.cpp:74
@no_mangle / @exportnode with set_no_manglenode->set_no_mangle(true)AnnotationController.cpp:84
@constructor / @makefunctionfunc->set_constructor_fn(true)AnnotationController.cpp:90
@deletefunctionfunc->set_delete_fn(true) (destructor)AnnotationController.cpp:99
@unsafefunctionfunc->set_unsafe(true)AnnotationController.cpp:108
@stdcallfunctionfunc->set_std_call(true)AnnotationController.cpp:117
@dllimportfunctionfunc->set_dll_import(true)AnnotationController.cpp:126
@dllexportfunctionfunc->set_dll_export(true)AnnotationController.cpp:135
@no_initstructstruct->set_no_init(true)AnnotationController.cpp:144
@anonymousstruct/union/variantnode->set_anonymous(true)AnnotationController.cpp:153
@const—No-op (TODO, not implemented)AnnotationController.cpp:159
@retainedfunction, struct, union, variant, interfaceset_is_body_retained(true) (keeps body for reflection)AnnotationController.cpp:163
@externfunction, struct, interface, varset_no_mangle(true) then set_extern(true); other kinds silently acceptedAnnotationController.cpp:186
@implicitfunctionfunc->set_implicit(true)AnnotationController.cpp:208
@direct_initstructstruct->set_direct_init(true)AnnotationController.cpp:217
@thread_localtop-level var/constvarInit->set_thread_local(true)AnnotationController.cpp:226
@volatilevar or struct memberset_volatile(true); rejects args and other targetsAnnotationController.cpp:234
@maxalignstruct or struct memberrequired alignment 16 (64-bit) / 8 (32-bit)AnnotationController.cpp:254
@no_returnfunctionfunc->set_noReturn(true) (noreturn)AnnotationController.cpp:270
@cppfunctionset_cpp_mangle(true) + set_no_mangle(true) (C++ mangle TODO)AnnotationController.cpp:279
@staticinterfaceinterface->set_is_static(true)AnnotationController.cpp:291
@overridefunctionfunc->set_override(true); lets a top-level function coexist with a same-named function from a different module (see Top-Level @override)AnnotationController.cpp:300
@deprecatednode with set_deprecatednode->set_deprecated(true)AnnotationController.cpp:300
@align(N)struct or struct memberrequired alignment from first constant integer arg; errors if absent/zeroAnnotationController.cpp:306
@allow_zeroedstruct/union/variantcontainer->allow_zeroed = true via get_master_members_container()AnnotationController.cpp:327
@non_dyninterfaceinterface->set_non_dynamic(true)AnnotationController.cpp:336
@partial_instantiategeneric type aliasalias->generic_parent->is_partial_instantiate = trueAnnotationController.cpp:344
@never_destructedvar/constvarInit->set_never_destructed(true)AnnotationController.cpp:356

Many flags are declared on the base ASTNode as virtuals and overridden per node: set_deprecated (ASTNode.cpp:459), set_anonymous (ASTNode.cpp:517), set_no_mangle (ASTNode.cpp:545).

Top-Level @override

@override on a top-level function lets it coexist with an existing function of the same name that was declared in a different module, instead of failing with a duplicate-symbol error during symbol resolution.

// a library (module) declares a test entry point
// sqlite/tests/lib_tests.ch
public func main(argc : int, argv : **char) : int { ... }

// the application declares its OWN entry point, overriding the library's
// app/tests/test_main.ch
@override
public func main(argc : int, argv : **char) : int { ... }

Semantics

  • Allowed only across modules. The overriding function and the previous function must belong to different LabModules (get_mod_scope()->container differs). Two same-named top-level functions in the same module still produce a duplicate-symbol error, even with @override.
  • @override must be on the function that comes after (the one doing the overriding). The earlier declaration needs no annotation.
  • Without @override, a same-named function is a duplicate-symbol error exactly as before.

Why it is safe (and when it still errors)

Symbol resolution is not the whole story: what actually lands in the executable is the mangled name. A top-level function in a library is mangled with its module scope (write_file_scope → sqlite_main), while the application's own is mangled as app_main. Because those runtime names differ, the two functions can coexist in one executable — @override simply stops symbol resolution from rejecting them up front.

Two things therefore still error, by design:

SituationResult
Same-named function in the same moduleduplicate-symbol error
No @override on the (later) overriding functionduplicate-symbol error
Both functions @no_mangle / @extern (same runtime name)linker error: redefinition of main

The @no_mangle/@extern case is the important guarantee: @override only permits coexistence when the mangled runtime names differ. If either side opts out of mangling so both emit the literal symbol, the collision is caught at the linker.

Use case: application main overriding a library's test main

When a library compiles its own tests under if test (i.e. source "tests" if test) it may define a main for its test runner. An application that imports that library and also runs with --test then has two mains: the application's and the library's. Without help this is a duplicate-symbol error that blocks the application from defining its own test entry point.

Marking the application's main with @override resolves this cleanly and with no linker error, because the library's main mangles to <module>_main while the application's stays main (application main is made no_mangle by the compiler, ASTProcessor.cpp:648; a library's main is mangled). This lets both the library author and the application author write their own main under the if test pattern.

Caveat — this does not remove the library's tests. @override only permits the two main functions to coexist; it does not stop the library's test sources from being compiled in. When the application is built with --test, every imported library whose .mod contains source "tests" if test still pulls its whole test tree into the application's build (test files, test-only imports, fixtures). If you want to eliminate or not pull in a library's tests, that is a separate feature which is still in development — a mechanism to scope --test per package / select which libraries' tests are compiled. Until it ships, @override is the way to make the main-collision problem go away without touching the library.

Test-related annotations

AnnotationTypeSemanticsSource
@testCollectorCollects the function into collection 0; read by intrinsics::get_tests<TestFunction>()Registered in the inline constructor AnnotationController.h:174; consumed GlobalFunctions.cpp:1674-1675
@test.before_eachSingleMarker (Override)One function run before each testAnnotationController.cpp:406; consumed lang/libs/test/src/runner.ch:85
@test.after_eachSingleMarker (Override)One function run after each testAnnotationController.cpp:407; consumed lang/libs/test/src/runner.ch:86
@test.id(N)MarkerOverrides numeric test IDAnnotationController.cpp:410; consumed GlobalFunctions.cpp:1695
@test.name("…")MarkerOverrides display nameAnnotationController.cpp:411; consumed GlobalFunctions.cpp:1712
@test.group("…")MarkerTest group stringAnnotationController.cpp:412; consumed GlobalFunctions.cpp:1720
@test.pass_on_crashMarkerTreat non-zero exit as pass on crashAnnotationController.cpp:413; consumed GlobalFunctions.cpp:1773
@test.ignoreMarkerReserved; registered but not read by the current test runnerAnnotationController.cpp:414
@test.timeout(ms)MarkerPer-test timeout in ms (default 10000)AnnotationController.cpp:415; consumed GlobalFunctions.cpp:1741
@test.retry(N)MarkerRetry count (clamped 0…999999)AnnotationController.cpp:416; consumed GlobalFunctions.cpp:1750
@test.benchmarkMarkerRun as a benchmarkAnnotationController.cpp:417; consumed GlobalFunctions.cpp:1778

The get_tests intrinsic materialises the collected @test functions into an array of values (id, name, group, function pointer, file, timeout, retry, returns_bool, pass_on_crash, benchmark, source line/char) in ast/utils/GlobalFunctions.cpp:1623-1796.

Plugin-defined annotations

CBI plugins register their own annotations from build.lab. Examples:

  • component (marker) — lang/libs/universal_cbi/build.lab:31, lang/libs/css_cbi/build.lab:30, lang/libs/js_cbi/build.lab:30; read by universal_cbi/html_cbi/css_cbi sym_res passes.
  • serializable (marker) — lang/libs/json_cbi/build.lab:25.
// lang/libs/universal_cbi/build.lab
const controller = ctx.getAnnotationController()
controller.createMarkerAnnotation(std::string_view("component"))

Plugins then mark generated nodes at parse time and later test membership:

// lang/libs/universal_cbi/src/react/macro.ch:107-117
const controller = parser.getAnnotationController()
const definition = controller.getDefinition("component")
...
controller.mark(node, definition, std::span<*mut Value>(args))

// lang/libs/universal_cbi/src/sym_res/sym_res_root.ch:41
if(!controller.isMarked(compNode, "component")) { ... }

Parser Flow

  1. The lexer emits a TokenType::Annotation whose value includes the leading @ (lexer/Lexer.cpp:861-868).
  2. Parser::parseAnnotation (parser/statements/AnnotationMacro.cpp:19-52):
    • strips the leading @, looks up the definition with controller.get_definition(name_view),
    • errors unknown annotation found '...' if absent,
    • appends a SavedAnnotation { AnnotationDefinition&, std::vector<Value*> } (parser/Parser.h:42-48) to the parser's annotations buffer (parser/Parser.h:411),
    • if ( follows, parses comma-separated argument expressions into SavedAnnotation::arguments.
  3. When a declaration node is created, Parser::annotate(node) (parser/statements/AnnotationMacro.cpp:54-60) iterates the buffered annotations, calls controller.handle_annotation(annot.definition, this, node, annot.arguments), then clears the buffer.

Because the buffer is flushed on the next declaration, annotations accumulate if you place several before one declaration:

@inline
@no_mangle
func f() { ... }

@make + @direct_init (Struct Initialization)

@constructor and @make run the same handler annot_handler_constructor (AnnotationController.cpp:90), setting is_constructor_fn on a function (node->as_function() — applying either to a non-function errors "couldn't make the function constructor"). @make is a synonym of @constructor; in practice the constructor function is named make, so T.make(...) resolves to it (MembersContainer::default_constructor_func requires is_constructor_fn + no params, MembersContainer.cpp:617). @direct_init applies to the struct and sets is_direct_init (AnnotationController.cpp:217).

The struct-literal syntax allowed depends on whether the struct has a constructor function (any @constructor/@make function, conventionally named make) and whether the struct has @direct_init:

Constructor (@make/@constructor fn)@direct_init on structT{} works?T.make() works?T{field: val} works?
YesonlyYes (all fields required)n/a (no @make)Yes (all fields required)
YesnoNoYesNo
YesyesYes (all fields required)YesYes (all fields required)
NoanyYes (all fields required)NoYes (all fields required)

Critical rule: a struct with a constructor function but without @direct_init cannot use {} syntax at all — use T.make(...). The compiler emits struct with name '…' has a constructor, use @direct_init to allow direct initialization (compiler/symres/LinkSignature.cpp:501, compiler/symres/SymResLinkBody.cpp:3107).

@delete (AnnotationController.cpp:99) marks the destructor function called on scope exit / delete; struct->has_destructor() (StructDefinition.h:204) returns true when a @delete function exists.

CBI Plugin Access

The controller is exposed to CBI plugins through three entry points, all returning AnnotationController*:

Accessor (C++ binding)Chemical APIScope
ParsergetAnnotationController (compiler/cbi/bindings/ParserCBI.cpp:15)Parser.getAnnotationController() (lang/libs/compiler/src/Parser.ch:9)parsing plugins (define + mark)
SymbolResolvergetAnnotationController (compiler/cbi/bindings/SymbolResolverCBI.cpp:12)SymbolResolver.getAnnotationController() (lang/libs/compiler/src/SymbolResolver.ch:4)symres plugins (read marks)
BuildContextgetAnnotationController (compiler/cbi/bindings/BuildContextCBI.cpp:21)BuildContext.getAnnotationController() (lang/libs/lab/src/lab.ch:138)build.lab scripts (create definitions)

The CBI functions themselves live in compiler/cbi/bindings/AnnotationController.cpp (extern "C"), with prototypes in AnnotationControllerCBI.h and dispatch entries in compiler/cbi/bindings/CBI.cpp:99-102,140,402:

  • AnnotationControllergetDefinition
  • AnnotationControllercreateSingleMarkerAnnotation / createMarkerAnnotation / createCollectorAnnotation / createMarkerAndCollectorAnnotation
  • AnnotationControllermarkSingle / mark / collect / markAndCollect
  • AnnotationControllerhandleAnnotation
  • AnnotationControllerisMarked

Arguments cross the CBI boundary as ValueSpan* and are unpacked with take_chemical_values (AnnotationController.cpp:26-54).

Plugin-side chemical interfaces:

  • lang/libs/lab/src/AnnotationController.ch — creation API (createSingleMarkerAnnotation, createMarkerAnnotation, createCollectorAnnotation, createMarkerAndCollectorAnnotation).
  • lang/libs/compiler/src/AnnotationController.ch — read/mark API (getDefinition, markSingle, mark, collect, markAndCollect, handleAnnotation, isMarked).

Enum-sync warning: if you add or reorder an AnnotationDefType / SingleMarkerMultiplePolicy value, keep any exposed Chemical binding in sync. The general CBI rule (see AGENTS.md) is that enum values must match exactly between the C++ header and the .ch binding; mid-enum insertions shift all later values and cause crashes. The annotation controller currently exposes only these two enums by integer policy value (createSingleMarkerAnnotation(..., policy : int)), so update SingleMarkerMultiplePolicy consumers and the lab binding together.

Adding a New Annotation

Checklist:

  1. Register the definition in the definitions map inside AnnotationController::initialize() (compiler/frontend/AnnotationController.cpp:364-419).
    • Intrinsic flag: add { "my_annot", { annot_handler_my_annot, "my_annot", AnnotationDefType::Handler } }.
    • Marker / Single marker / Collector: use the appropriate aggregate initialiser, or create it from a plugin's build.lab.
  2. Write the handler (for Handler type) next to the others in AnnotationController.cpp. Validate node->kind() and call parser->error(...) on wrong targets. Arguments arrive in std::vector<Value*>& args; inspect val_kind() / get_number() / get_the_string() (see annot_handler_align at :306 and annot_handler_volatile at :234).
  3. Add/confirm the node accessor the handler calls (e.g. set_my_flag on the relevant node class — often on the base ASTNode virtuals at ASTNode.h:225-237, or on StructDefinition/FunctionDeclaration/VarInit).
  4. Consume the flag/mark where it matters: symres, type verification, interpreter, LLVM backend (compiler/backend/LLVM.cpp), or C codegen (preprocess/2c/). For markers, read via controller.is_marked(node, "name"); for single markers get_single_marked(name); for collectors get_collection(id).
  5. Ensure annotate() runs for the target node. If the annotation attaches to a new node type, add annotate(node) at that construction site (see the table above).
  6. Test with an isolated lang/compiled/temp.ch or a @test-annotated function; a bad name is a parse error and a bad target is a parser->error.
  7. No enum change needed for string-named annotations — names are strings, so adding @my_annot does not touch AnnotationKind. Only update CBI enum bindings if you add a new AnnotationDefType/policy value.

Gotchas

  • Unknown annotation is fatal at parse time. @foobar → unknown annotation found '@foobar' (AnnotationMacro.cpp:27-30). Register it first.
  • Name matching is exact and case-sensitive, including punctuation. @test (collector) is distinct from @test.id, @test.timeout, etc.
  • Annotation names may contain . and : because read_annotation_id explicitly allows them (Lexer.cpp:724-739); @inline.always and @size:opt are single tokens.
  • Buffered-until-next-node semantics. An annotation with no following declaration is not applied; consecutive annotations all attach to the next declaration.
  • Target checks are per-handler, not a central schema. @volatile errors unless applied to a VarInitStmt or StructMember; @align errors off structs/members; @maxalign rejects arguments. Always read the handler for the real rules.
  • @extern silently accepts some targets. After set_no_mangle, the switch only handles FunctionDecl, StructDecl, InterfaceDecl, VarInitStmt; other node kinds fall through with no error (AnnotationController.cpp:190-205).
  • @const is a no-op (AnnotationController.cpp:159-161) — do not rely on it.
  • The legacy Annotation/AnnotationParent/AnnotableNode classes are dead code. Do not add annotation fields to nodes through AnnotationParent; the controller is authoritative.
  • State is cleared between jobs. LabBuildCompiler calls controller.clear_marked_or_collected() after a job (LabBuildCompiler.cpp:262); marks/collections do not survive across compilation units.
  • Concurrency. Files are parsed in parallel; use the controller's thread-safe create_* / mark* / collect* APIs rather than touching the private maps.
  • Plugin definitions are per-build. component is registered in build.lab; if a plugin's build.lab did not run (e.g. missing dependency), getDefinition("component") returns null and marking is skipped (universal_cbi/src/react/macro.ch:110-113).

Worked Examples

Simple flag annotation:

@extern public func malloc(size : usize) : *mut void

@no_mangle
public func my_entry() : int { return 0 }

@volatile
var shared_flag : int = 0

Function strategies and constructor/destructor:

@inline
func fast() : int { return 1 }

@direct_init
public struct RGBA8 {
    var r : u8; var g : u8; var b : u8; var a : u8
    @make func make(r_ : u8, g_ : u8, b_ : u8, a_ : u8 = 255) : RGBA8 { ... }
}

struct Widget {
    var handle : *mut void
    @constructor func Widget() { }
    @delete func delete(&mut self) { }
}

Test metadata (all markers/interceptors read by the test runner):

@test
@test.id(1073741823)
@test.name("font_create_empty_works")
@test.group("font")
@test.timeout(60000)
public func font_create_empty_works(env : &mut TestEnv) { ... }

@test.before_each
func setup(env : &mut TestEnv) { ... }

Plugin defines and reads its own marker:

// build.lab
ctx.getAnnotationController().createMarkerAnnotation(std::string_view("component"))

// parse/transform pass
const controller = parser.getAnnotationController()
const definition = controller.getDefinition("component")
controller.mark(node, definition, std::span<*mut Value>(args))

// later symres pass
if(!controller.isMarked(compNode, "component")) { /* not a component */ }

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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일 업데이트

The compiler-side machinery of the Chemical Compiler Binding Interface (CBI) — the CompilerBinder/CBIData/CBIFunctionIndex model, the full CBIFunctionType hook enum and where each fires, the extern "C" bindings dispatch tables in CBI.cpp, ASTBuilder/ASTCBI/PtrVec/BatchAllocator/SourceProvider/ASTDiagnoser wrappers, the TinyCC JIT bridge and LabBuildCompiler plugin compilation/indexing, the enum-sync rule, and how to add a new CBI binding. Load when modifying the CBI model/bindings layer, adding compiler hooks or bindings, debugging plugin ABI/symbol issues, or trying to understand how the compiler calls into Chemical plugin code.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

chemicallang/chemical312026년 10월 26일 업데이트

chemicallang의 스킬 모두 보기

이 스킬의 문제 신고