5.3.15. C++ Integration: Custom Annotations
This tutorial shows how to create custom annotations in C++ that modify daslang compilation behavior. Topics covered:
FunctionAnnotation— hooks into function compilationStructureAnnotation— hooks into struct compilationapply()/finalize()— the function annotation lifecycletouch()/look()— the structure annotation lifecycleAdding fields to structs at compile time
5.3.15.1. Prerequisites
Tutorial 14 completed (tutorial_integration_cpp_serialization).
5.3.15.2. What are annotations?
Annotations are compile-time hooks defined in C++ and used from
daslang with the [annotation_name] syntax. They let the host
application validate, modify, or transform script code during
compilation.
Built-in examples: [export], [private], [deprecated].
This tutorial shows how to create your own.
5.3.15.3. Annotation class hierarchy
Annotation
├── FunctionAnnotation — [name] on functions
│ └── MarkFunctionAnnotation — convenience base
├── StructureAnnotation — [name] on structs
└── TypeAnnotation — describes C++ types
└── ManagedStructureAnnotation<T>
TypeAnnotation / ManagedStructureAnnotation describe how a
C++ type is exposed (fields, size, simulation nodes).
StructureAnnotation is a compile-time hook on daslang
struct declarations — they are unrelated concepts.
5.3.15.4. FunctionAnnotation — [log_calls]
A FunctionAnnotation is called during compilation whenever the
annotated function is parsed and type-checked:
struct LogCallsAnnotation : FunctionAnnotation {
LogCallsAnnotation() : FunctionAnnotation("log_calls") {}
// Called during parsing — can modify function flags
bool apply(const FunctionPtr & func, ModuleGroup &,
const AnnotationArgumentList &, string &) override {
printf("[log_calls] apply: %s\n", func->name.c_str());
return true; // true = success, false = error
}
// Not supported on blocks
bool apply(ExprBlock *, ModuleGroup &,
const AnnotationArgumentList &,
string & err) override {
err = "not supported for blocks";
return false;
}
// Called after type inference
bool finalize(const FunctionPtr & func, ModuleGroup &,
const AnnotationArgumentList &,
const AnnotationArgumentList &,
string &) override {
printf("[log_calls] finalize: %s (args: %d)\n",
func->name.c_str(), (int)func->arguments.size());
return true;
}
bool finalize(ExprBlock *, ModuleGroup &,
const AnnotationArgumentList &,
const AnnotationArgumentList &,
string &) override { return true; }
};
Key virtual methods:
|
Parse time — modify flags, validate |
|
After inference — validate with types |
|
Each call site — check arguments |
|
Inference — rewrite call AST |
|
Simulation — return custom SimNode |
5.3.15.5. StructureAnnotation — [add_field]
A StructureAnnotation is called during compilation of annotated
structs. The key hook is touch(), which runs before type
inference and can modify the struct:
struct AddFieldAnnotation : StructureAnnotation {
AddFieldAnnotation() : StructureAnnotation("add_field") {}
// Called BEFORE type inference — can modify struct
bool touch(const StructurePtr & st, ModuleGroup &,
const AnnotationArgumentList &, string &) override {
if (!st->findField("id")) {
st->fields.emplace_back(
"id", // name
make_smart<TypeDecl>(Type::tInt), // type
nullptr, // no init
AnnotationArgumentList(), // no ann
false, // no move
LineInfo() // loc
);
// Must invalidate the lookup cache!
st->fieldLookup.clear();
}
return true;
}
// Called AFTER type inference — read-only validation
bool look(const StructurePtr & st, ModuleGroup &,
const AnnotationArgumentList &,
string &) override {
printf("struct '%s' has %d fields\n",
st->name.c_str(), (int)st->fields.size());
return true;
}
};
Key points:
Push to
st->fieldsdirectly — there is noaddFieldmethodMust call
st->fieldLookup.clear()after adding fieldsGuard with
findField()to avoid duplicates (touchcan be called multiple times)touch→ before inference (can modify)look→ after inference (read-only)patch→ after inference (can request re-inference)
5.3.15.6. Registration
Register annotations in the module constructor:
addAnnotation(make_smart<LogCallsAnnotation>());
addAnnotation(make_smart<AddFieldAnnotation>());
5.3.15.7. Using from daslang
options gen2
require tutorial_15_cpp
[log_calls]
def attack(target : string; damage : int) {
print(" {target} takes {damage} damage!\n")
}
[add_field]
struct Monster {
name : string
hp : int
}
[export]
def test() {
attack("Goblin", 25)
// Monster now has an "id" field added by [add_field]
var m = Monster(name = "Dragon", hp = 500, id = 42)
print("{m.name}: id={m.id}\n")
5.3.15.8. Building and running
cmake --build build --config Release --target integration_cpp_15
bin\Release\integration_cpp_15.exe
Expected output:
--- Compilation (annotation hooks fire here) ---
[log_calls] apply: attack
[log_calls] apply: heal
[add_field] added 'id : int' to struct 'Monster'
[add_field] look: struct 'Monster' has 3 fields
[log_calls] finalize: attack (args: 2)
[log_calls] finalize: heal (args: 2)
--- Running script ---
=== Custom Annotations Tutorial ===
--- Annotated functions ---
Goblin takes 25 damage!
Hero heals for 10 hp
--- Normal function ---
(this function has no annotation)
--- Annotated struct ---
Dragon: hp=500, id=42
Note how the annotation hooks fire during compilation, before the script runs.
See also
Full source:
15_custom_annotations.cpp,
15_custom_annotations.das
Previous tutorial: tutorial_integration_cpp_serialization
Next tutorial: tutorial_integration_cpp_sandbox