LuisaCompute AST Usage Markers
SkillDev toolsLocate AST read/write usage markers and builtin CallOp usage rules in LuisaCompute. Use when investigating or modifying AST variable usage propagation, FunctionBuilder internals, or CallOp argument marking.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the LuisaCompute AST Usage Markers skill
What this skill tells your AI
The instructions your AI receives, as published by luisagroup/luisacompute in .agents/skills/lc_ast/SKILL.md and read by ahel’s review.
Quick reference for how manual FunctionBuilder AST tracks variable read/write usage and how builtin CallOp calls propagate usage to their arguments.
Usage Enum
include/luisa/ast/usage.h
enum struct Usage : uint32_t {
NONE = 0u,
READ = 0x01u,
WRITE = 0x02u,
READ_WRITE = READ | WRITE
};
Flags accumulate via OR over a variable's lifetime.
Two-Layer Marker Design
1. Per-expression cache
include/luisa/ast/expression.h
class Expression {
protected:
mutable Usage _usage{Usage::NONE};
virtual void _mark(Usage usage) const noexcept = 0;
public:
void mark(Usage usage) const noexcept;
[[nodiscard]] auto usage() const noexcept { return _usage; }
};
src/ast/expression.cpp
void Expression::mark(Usage usage) const noexcept {
if (auto a = to_underlying(_usage), u = a | to_underlying(usage); a != u) {
_usage = static_cast<Usage>(u);
_mark(usage);
}
}
Propagation is idempotent: it only forwards when new bits are added.
2. FunctionBuilder storage
include/luisa/ast/function_builder.h
luisa::vector<Usage> _variable_usages;
void mark_variable_usage(uint32_t uid, Usage usage) noexcept;
[[nodiscard]] auto variable_usage(uint uid) const noexcept { return _variable_usages[uid]; }
src/ast/function_builder.cpp
void FunctionBuilder::mark_variable_usage(uint32_t uid, Usage usage) noexcept {
auto old_usage = to_underlying(_variable_usages[uid]);
auto u = static_cast<Usage>(old_usage | to_underlying(usage));
_variable_usages[uid] = u;
}
uint32_t FunctionBuilder::_next_variable_uid() noexcept {
auto uid = static_cast<uint32_t>(_variable_usages.size());
_variable_usages.emplace_back(Usage::NONE);
return uid;
}
RefExpr Forwarding
src/ast/expression.cpp
void RefExpr::_mark(Usage usage) const noexcept {
if (auto fb = detail::FunctionBuilder::current(); fb == builder()) {
fb->mark_variable_usage(_variable.uid(), usage);
}
}
Only marks when the current builder owns the expression, preventing stale marking across function boundaries.
Manual API Example
auto &cur = *FunctionBuilder::current();
auto ref = cur.reference(Type::of<float4>());
cur.mark_variable_usage(ref->variable().uid(), Usage::READ_WRITE);
Builtin CallOp Usage Marking
Builtin detection
include/luisa/ast/op.h
[[nodiscard]] constexpr auto is_builtin_operation(CallOp op) noexcept {
return op != CallOp::CUSTOM && op != CallOp::EXTERNAL;
}
include/luisa/ast/expression.h
[[nodiscard]] auto is_builtin() const noexcept { return is_builtin_operation(_op); }
CallExpr::_mark rules
src/ast/expression.cpp
void CallExpr::_mark() const noexcept {
if (is_builtin()) {
switch (_op) {
case CallOp::BUFFER_VOLATILE_WRITE:
case CallOp::BUFFER_WRITE:
case CallOp::BINDLESS_BUFFER_WRITE:
case CallOp::BYTE_BUFFER_VOLATILE_WRITE:
case CallOp::BYTE_BUFFER_WRITE:
case CallOp::TEXTURE_WRITE:
case CallOp::RAY_TRACING_SET_INSTANCE_TRANSFORM:
case CallOp::RAY_TRACING_SET_INSTANCE_VISIBILITY:
case CallOp::RAY_TRACING_SET_INSTANCE_OPACITY:
case CallOp::RAY_TRACING_SET_INSTANCE_USER_ID:
case CallOp::RAY_TRACING_SET_INSTANCE_MOTION_MATRIX:
case CallOp::RAY_TRACING_SET_INSTANCE_MOTION_SRT:
case CallOp::RAY_QUERY_COMMIT_TRIANGLE:
case CallOp::RAY_QUERY_COMMIT_PROCEDURAL:
case CallOp::RAY_QUERY_TERMINATE:
case CallOp::RAY_QUERY_PROCEED:
case CallOp::GRADIENT_MARKER:
case CallOp::ACCUMULATE_GRADIENT:
case CallOp::ATOMIC_EXCHANGE:
case CallOp::ATOMIC_COMPARE_EXCHANGE:
case CallOp::ATOMIC_FETCH_ADD:
case CallOp::ATOMIC_FETCH_SUB:
case CallOp::ATOMIC_FETCH_AND:
case CallOp::ATOMIC_FETCH_OR:
case CallOp::ATOMIC_FETCH_XOR:
case CallOp::ATOMIC_FETCH_MIN:
case CallOp::ATOMIC_FETCH_MAX:
case CallOp::INDIRECT_SET_DISPATCH_KERNEL:
case CallOp::INDIRECT_SET_DISPATCH_COUNT:
case CallOp::COOPERATIVE_OUTER_PRODUCT_ACCUMULATE:
case CallOp::COOPERATIVE_VECTOR_ACCUMULATE:
case CallOp::COOPERATIVE_VECTOR_STORE:
case CallOp::COOPERATIVE_VECTOR_WORKGROUP_STORE:
_arguments[0]->mark(Usage::WRITE);
for (size_t i = 1; i < _arguments.size(); i++) {
_arguments[i]->mark(Usage::READ);
}
break;
default:
for (auto arg : _arguments) {
arg->mark(Usage::READ);
}
}
} else if (is_external()) {
auto f = external();
for (size_t i = 0; i < _arguments.size(); i++) {
_arguments[i]->mark(f->argument_usages()[i]);
}
} else {
// custom callable
auto args = custom().arguments();
for (size_t i = 0; i < args.size(); i++) {
auto arg = args[i];
_arguments[i]->mark(
arg.is_reference() || arg.is_resource() ?
custom().variable_usage(arg.uid()) :
Usage::READ);
}
}
}
Rule summary
- Default builtin: every argument marked
READ. - Write-style builtins (list above): argument 0 marked
WRITE; remaining arguments markedREAD. - Atomic ops mark their target reference (argument 0) as
WRITE;AtomicRefNode::operate()builds theCallExprwith the target as_arguments[0](src/ast/atomic_ref_node.cpp).
Files of Record
| Purpose | Path |
|---|---|
| Usage enum | include/luisa/ast/usage.h |
Expression base & CallExpr | include/luisa/ast/expression.h |
Expression::mark, RefExpr::_mark, CallExpr::_mark | src/ast/expression.cpp |
FunctionBuilder declaration & _variable_usages | include/luisa/ast/function_builder.h |
FunctionBuilder::mark_variable_usage, _next_variable_uid, call() | src/ast/function_builder.cpp |
CallOp enum, is_builtin_operation, is_atomic_operation | include/luisa/ast/op.h |
check_builtin_call_valid | src/ast/op.cpp |
Function::variable_usage exposure | src/ast/function.cpp |
| Atomic op construction | src/ast/atomic_ref_node.cpp |
| Manual AST skill doc | .agents/skills/ast/SKILL.md |
Common Modifications
- Add a new write-style builtin op: extend the switch in
src/ast/expression.cppCallExpr::_mark()so argument 0 isWRITE. - Query usage after building: call
Function::variable_usage(uid)orFunctionBuilder::variable_usage(uid). - Custom callable reference/resource args: explicitly mark the reference variable
READ_WRITEviamark_variable_usage()so callers propagate usage correctly.
Appendix: Full AST C++ Structure
File Inventory
Headers (include/luisa/ast/)
| File | Main Class(es) | Description |
|---|---|---|
usage.h | Usage (enum) | NONE, READ, WRITE, READ_WRITE flags |
attribute.h | Attribute | Key-value pair struct for type/variable metadata |
variable.h | Variable | Typed variable with Tag (LOCAL, SHARED, REFERENCE, BUFFER, TEXTURE, BINDLESS_ARRAY, ACCEL, and builtins like THREAD_ID, BLOCK_ID, DISPATCH_ID, etc.) |
type.h | Type | Central type system: scalar types (BOOL, INT8..FLOAT64, FLOAT8), VECTOR, MATRIX, ARRAY, STRUCTURE, BUFFER, TEXTURE, BINDLESS_ARRAY, ACCEL, COOPERATIVE_VECTOR, COOPERATIVE_VECTOR_REF, COOPERATIVE_MATRIX_REF, CUSTOM. Factory methods: of<T>(), array(), vector(), matrix(), buffer(), texture(), structure(), custom(), from(description) |
type_registry.h | TypeDesc<T>, macros LUISA_STRUCT_REFLECT | Compile-time type description generation; C++20 aggregate member counting via member_reflect.inl.h |
member_reflect.inl.h | count_member<T>(), member_reflect<T>() | Compile-time struct reflection: counts aggregate members (up to 126) and builds struct<align,member1,member2,...> description strings |
constant_data.h | ConstantData, ConstantDecoder | Constant data storage with type + raw bytes; ConstantDecoder virtual dispatch for decoding vectors, matrices, structs, arrays |
expression.h | Expression (base), UnaryExpr, BinaryExpr, MemberExpr, AccessExpr, LiteralExpr, RefExpr, ConstantExpr, CallExpr, CastExpr, TypeIDExpr, StringIDExpr, FuncRefExpr, CpuCustomOpExpr, GpuCustomOpExpr | Full expression tree with visitor pattern; _usage cache, _mark() virtual, traverse_subexpressions() helper |
op.h | UnaryOp, BinaryOp, CallOp, CallOpSet, TypePromotion | Operation enums (CallOp has 300+ entries) |
statement.h | Statement (base), BreakStmt, ContinueStmt, ReturnStmt, ScopeStmt, IfStmt, LoopStmt, ExprStmt, SwitchStmt, SwitchCaseStmt, SwitchDefaultStmt, AssignStmt, ForStmt, CommentStmt, RayQueryStmt, SuspendStmt, AutoDiffStmt, PrintStmt, DebugBreakStmt | Full statement tree with visitor pattern; traverse_expressions() template helper |
function.h | Function | Public function handle wrapping FunctionBuilder*. Provides access to variables, arguments, bindings, callables, block size, hash, usage queries |
function_builder.h | FunctionBuilder | Central AST construction API. RAII scope guards, thread-local function stack. Creates expressions/literals/variables/statements. mark_variable_usage(), hash(), duplicate(), sort_bindings(), _internalize() |
external_function.h | ExternalFunction | Named external function with typed argument list and per-argument Usage |
atomic_ref_node.h | AtomicRefNode | Helper for building atomic operations: chains access paths and emits CallExpr |
callable_library.h | CallableLibrary | Serialization/deserialization of callable function graphs to/from binary blobs |
ast2json.h | to_json() | Convert Type or Function to JSON string for debugging |
interface.h | — | Convenience include aggregating type/variable/expression/statement/function headers |
Sources (src/ast/)
| File | Key Functions |
|---|---|
type.cpp | TypeRegistry singleton, TypeImpl, _decode() recursive parser for type descriptions, Type::from(), Type::array(), Type::vector(), Type::matrix(), Type::buffer(), Type::texture(), Type::structure(), Type::custom(), type query predicates |
variable.cpp | Variable::hash() |
expression.cpp | Expression::mark(), Expression::hash(), all expression _mark()/_compute_hash() overrides, CallExpr::_mark() (builtin write-style vs read-style dispatch), CallExpr::custom()/external() |
statement.cpp | All Statement::_compute_hash() overrides, PrintStmt/DebugBreakStmt constructors, default StmtVisitor methods |
function.cpp | Function methods delegating to FunctionBuilder, binding hash functions |
function_builder.cpp | FunctionBuilder::push/pop/current, break_/continue_/return_/suspend_/ray_query_/autodiff_/if_/loop_/switch_/case_/default_/for_/assign, mark_variable_usage, _internalize, _ref, _builtin, local/shared/argument/buffer/texture/bindless_array/accel, literal/unary/binary/member/swizzle/access/cast/string_id/type_id/func_ref/call, _compute_hash, sort_bindings, _duplicate_if_necessary, duplicate, set_block_size, set_name |
op.cpp | CallOpSet::Iterator, promote_types(), check_builtin_call_valid() |
constant_data.cpp | ConstantDecoder vector/matrix/struct/array decoding, ConstantData::create() with deduplication |
external_function.cpp | ExternalFunction constructor, _compute_hash() |
atomic_ref_node.cpp | AtomicRefNode construction, access() chaining, operate() builds CallExpr |
callable_library.cpp | Full serialization/deserialization of FunctionBuilder graph |
function_duplicator.cpp | FunctionDuplicator deep-copies FunctionBuilder graph with variable remapping; _duplicate_if_necessary() for leaked variables; deduplicate_custom_callables() |
ast2json.cpp | JSON value type, AST2JSON visitor converts full AST to JSON |
lc_ast_pch.h | Precompiled header |
Class Hierarchy Overview
Expression (abstract)
├── UnaryExpr
├── BinaryExpr
├── MemberExpr
├── AccessExpr
├── LiteralExpr
├── RefExpr
├── ConstantExpr
├── CallExpr
├── CastExpr
├── TypeIDExpr
├── StringIDExpr
├── FuncRefExpr
├── CpuCustomOpExpr
└── GpuCustomOpExpr
Statement (abstract)
├── BreakStmt
├── ContinueStmt
├── ReturnStmt
├── ScopeStmt
├── IfStmt
├── LoopStmt
├── ExprStmt
├── SwitchStmt
├── SwitchCaseStmt
├── SwitchDefaultStmt
├── AssignStmt
├── ForStmt
├── CommentStmt
├── RayQueryStmt
├── SuspendStmt
├── AutoDiffStmt
├── PrintStmt
└── DebugBreakStmt
Key Design Patterns
-
Ownership:
FunctionBuilderowns allExpressionandStatementobjects viaunique_ptrvectors. All raw pointers are non-owning views. -
Builder stack: Thread-local
_function_stack()enablesExpressionconstructors to automatically capture their owning builder.FunctionStackGuardpushes/pops on definition. -
Expression internalization (
_internalize()): When a callable references a variable from an outer scope, the builder clones/captures the expression chain into the current function. Lvalue locals become reference arguments; resources become new resource arguments; builtins become new builtins; statically-evaluable expressions are recursively cloned. -
Usage propagation: Two-phase: (a)
Expression::_usagebitfield caches the aggregate usage at each expression node; (b)RefExpr::_mark()writes through toFunctionBuilder::_variable_usages[uid]for final variable-level query. -
CallOp semantics:
CallOpSet(bitset) tracks which builtins a function directly/propagatedly uses. -
Serialization:
CallableLibraryprovides a custom binary serialization format for distributing callable function graphs. -
Duplication:
FunctionDuplicatorcreates a deep copy of aFunctionBuildergraph, remapping variable UIDs and hoisting leaked references. -
AtomicRefNode: Chains buffer/array/structure access paths into a flat argument list for atomic
CallExprconstruction.
Visitor Helpers
traverse_subexpressions(expr, enter, exit)— walks all expression nodes recursively.traverse_expressions<recurse_subexpr>(stmt, visit, enter_stmt, exit_stmt)— walks all expressions nested in a statement tree.ExprVisitor— abstract visitor with virtual methods for each expression type.StmtVisitor— abstract visitor with virtual methods for each statement type.
Type System Details
Type::from(description)parses string descriptions like"array<struct<16,int,float>,10>"into internedTypeobjects.TypeRegistry(singleton) manages type pool and deduplication viaunordered_set.TypeImplextendsTypewith concrete storage for hash, tag, size, alignment, dimension, members, member_attributes.TypeDesc<T>maps C++ types to their string descriptions at compile time.struct_member_tuple<T>decomposes structs intostd::tupleof member types with offset validation.
Variable Tags
LOCAL | SHARED | REFERENCE | BUFFER | TEXTURE |
BINDLESS_ARRAY | ACCEL | THREAD_ID | BLOCK_ID |
DISPATCH_ID | DISPATCH_SIZE | KERNEL_ID |
WARP_LANE_COUNT | WARP_LANE_ID |
RASTER_OBJECT_ID | RASTER_BARYCENTRICS
How to Add a New CallOp
Overview
Adding a new CallOp requires changes across the AST layer, validation, each backend codegen, and optionally the DSL. Below is the complete checklist.
Step 1: Add the enum value
File: include/luisa/ast/op.h
Append to the CallOp enum in the appropriate category section.
enum struct CallOp : uint32_t {
// ... existing ops ...
MY_NEW_OP,
// ...
};
⚠️ DO NOT reorder existing values — enum integer values are embedded in serialized function hashes and are assumed by
call_op_count. Append your new op in the appropriate category section beforeCLOCK(the last enumerator). If you must add afterCLOCK, updatecall_op_countandLUISA_MAGIC_ENUM_RANGEaccordingly.
Also update:
-
call_op_count(line ~522):static constexpr size_t call_op_count = to_underlying(CallOp::CLOCK) + 1u;— This defines the size of theCallOpSetbitset. If your new op is added BEFORECLOCK,call_op_countalready covers it. If added AFTERCLOCK, increment this value. -
LUISA_MAGIC_ENUM_RANGE(line ~664):LUISA_MAGIC_ENUM_RANGE(luisa::compute::CallOp, CUSTOM, CLOCK)— Enablesto_string/from_stringfor the range[CUSTOM, CLOCK]. If your new op is afterCLOCK, extend the range to include it.
Step 2: Update usage propagation
File: src/ast/expression.cpp — CallExpr::_mark()
Read-only (default):
No change needed — the default case marks all args Usage::READ.
Write-style (arg[0] = WRITE, rest = READ):
Add to the existing switch:
case CallOp::MY_NEW_OP:
_arguments[0]->mark(Usage::WRITE);
for (size_t i = 1; i < _arguments.size(); i++) {
_arguments[i]->mark(Usage::READ);
}
break;
Custom usage:
Implement arbitrary logic in the switch.
Step 3: Add validation (optional but recommended)
File: src/ast/op.cpp — check_builtin_call_valid()
Add a case to validate argument types and counts at AST construction time:
case CallOp::MY_NEW_OP: {
LUISA_ASSERT(args.size() == 2 &&
args[0]->type()->is_buffer() &&
args[1]->type()->is_uint32(),
"MY_NEW_OP: expected (buffer, uint32)");
break;
}
Step 4: Add helper functions for category detection (optional)
File: include/luisa/ast/op.h
If your op belongs to a new category, add a constexpr helper:
[[nodiscard]] constexpr auto is_my_category_operation(CallOp op) noexcept {
auto v = to_underlying(op);
return v >= to_underlying(CallOp::MY_CATEGORY_START) &&
v <= to_underlying(CallOp::MY_CATEGORY_END);
}
Step 5: Update each backend codegen
Each backend has a switch on CallOp that emits native code or IR. Add your case to all of them:
| Backend | Codegen File(s) | Nature |
|---|---|---|
| CUDA | src/backends/cuda/cuda_codegen_ast.cpp | Direct AST→CUDA C++ string emission |
| Metal | src/backends/metal/metal_codegen_ast.cpp | Direct AST→Metal Shading Language string emission |
| HLSL/DX12 | src/backends/common/hlsl/codegen_utils/function_codegen.cpp (main CallOp dispatch), src/backends/common/hlsl/hlsl_codegen.cpp (AST visitor), src/backends/dx/ (DXIL compilation) | AST→HLSL string, compiled to DXIL; no own CallOp switch in dx/ |
| SPIR-V (LLVM) | src/backends/common/spirv_llvm/llvm_state_visitor.cpp | AST→LLVM IR → SPIR-V binary via spirv64 target machine |
| LLVM/CPU | src/backends/common/c_codegen/codegen_visitor.cpp | AST→LLVM IR for CPU JIT backend (src/backends/cpu/) |
| Vulkan | src/backends/vk/ (device setup) + src/backends/common/spirv_llvm/ (codegen) | Uses common SPIR-V LLVM codegen; no own CallOp switch |
| XIR (intermediate) | src/xir/translators/ast2xir.cpp | AST→XIR IR; backends using XIR (CUDA XIR, Metal XIR, HIP, Fallback) handle CallOp in their own XIR visitors |
| Fallback | src/backends/fallback/ + src/xir/translators/ast2xir.cpp | Uses XIR as input; no direct AST CallOp switch |
| Hip/AMD | src/backends/hip/ + src/xir/translators/ast2xir.cpp | Uses XIR as input; no direct AST CallOp switch |
| Toy C | src/backends/toy_c/ | Simple C output; no direct AST CallOp switch |
| Remote | src/backends/remote/ | Networked backend proxy; no direct AST CallOp switch |
| Validation | src/backends/validation/ | AST validation layer wrapping another backend; no own CallOp switch |
Example CUDA addition:
case CallOp::MY_NEW_OP: {
_scratch << "my_new_op(";
for (auto i = 0u; i < args.size(); i++) {
if (i) _scratch << ", ";
emit(args[i]); // use the backend's expression emitter
}
_scratch << ")";
break;
}
Step 6: (Optional) Add DSL helper
If the op should be exposed via the high-level DSL, add a helper in src/dsl/:
// src/dsl/something.cpp
[[nodiscard]] auto my_new_op(Expr<float> x) noexcept {
return detail::FunctionBuilder::current()->call(...);
}
Step 7: (Optional) Update XIR passes
If your op needs special handling in the XIR optimization pipeline, add it to src/xir/passes/.
Full Checklist
| # | What | File(s) |
|---|---|---|
| 1 | Add enum value | include/luisa/ast/op.h |
| 2 | Update call_op_count / LUISA_MAGIC_ENUM_RANGE if needed | include/luisa/ast/op.h |
| 3 | Add usage marking in CallExpr::_mark() | src/ast/expression.cpp |
| 4 | Add argument validation in check_builtin_call_valid() | src/ast/op.cpp |
| 5 | Add codegen for each backend | See table above |
| 6 | (Optional) Add DSL helper | src/dsl/ |
| 7 | (Optional) Add category helper | include/luisa/ast/op.h |
How to Add a New Expression
Step 1: Add the expression class
File: include/luisa/ast/expression.h
- Add a new
Tagenum value toExpression::Tag(e.g.,MY_NEW_EXPR). - Forward-declare the class (e.g.,
class MyNewExpr;). - Add a
virtual void visit(const MyNewExpr *) = 0;toExprVisitor. - Implement the class inheriting
Expression:
class LUISA_AST_API MyNewExpr final : public Expression {
friend class CallableLibrary;
private:
// your data members
MyNewExpr() noexcept = default;
protected:
void _mark(Usage) const noexcept override { /* propagate usage if needed */ }
[[nodiscard]] uint64_t _compute_hash() const noexcept override;
public:
MyNewExpr(/* params */) noexcept
: Expression{Tag::MY_NEW_EXPR, type} /*, init members */ {}
// accessors
LUISA_EXPRESSION_COMMON()
};
Step 2: Add hash computation
File: src/ast/expression.cpp
Implement _compute_hash():
uint64_t MyNewExpr::_compute_hash() const noexcept {
return hash_combine({/* member hashes */});
}
Step 3: Add to traverse_subexpressions
File: include/luisa/ast/expression.h (the free function at the bottom)
Add a case for Expression::Tag::MY_NEW_EXPR so the traversal helper works correctly.
Step 4: Add creation method to FunctionBuilder
File: include/luisa/ast/function_builder.h (declaration) and src/ast/function_builder.cpp (definition)
// in function_builder.h:
[[nodiscard]] const MyNewExpr *my_new_expr(/* params */) noexcept;
// in function_builder.cpp:
const MyNewExpr *FunctionBuilder::my_new_expr(/* params */) noexcept {
return _create_expression<MyNewExpr>(/* params */);
}
Step 5: Add serialization support (optional)
File: src/ast/callable_library.cpp
Add ser_value and deser_ptr specializations for the new expression type, plus integrate into the Expression base ser_value/deser_value dispatch.
Step 6: Add codegen in each backend
Each backend that processes AST expressions directly (CUDA, Metal, HLSL, SPIR-V LLVM, LLVM/CPU) needs a case Expression::Tag::MY_NEW_EXPR in its visitor switch.
Step 7: Add JSON export (optional)
File: src/ast/ast2json.cpp
Add a conversion method in AST2JSON and wire it into _convert_expr().
How to Add a New Statement
Step 1: Add the statement class
File: include/luisa/ast/statement.h
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 1k
- Forks
- 108
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
lc-ast- Source
- github.com/luisagroup/luisacompute