Composition Model
Askl’s composition model allows you to query containment relationships between symbols. This is distinct from reference relationships (function calls) and enables powerful architectural queries.
Symbol Hierarchy
Symbols are organized in a hierarchy based on their type:
directory (level 5)
└── module (level 4)
└── file (level 3)
├── function (level 2)
├── type (level 2)
├── data (level 2)
├── macro (level 2)
└── field (level 1)A higher-level symbol contains a lower-level symbol if:
- They share the same source file (object)
- The higher-level symbol’s byte range encompasses the lower-level symbol’s range
- The type levels are compatible (higher > lower)
Fields sit at the lowest level, so they can be contained by types (e.g., type("file_operations") has { field }), files, modules, and directories.
Two Relationship Types
References (refs)
The default relationship type. Shows what symbols call or reference each other.
"foo" { "bar" } /* foo calls bar */
"foo" refs { "bar" } /* Same, explicit syntax */Containment (has)
Shows what symbols contain other symbols.
mod("pkg") has { "handler" } /* handler is IN pkg */
file("/main.go") { func } /* Functions IN main.go (implicit has from file) */Container Types: Implicit Relationships
Container type selectors (dir, file, mod) automatically set both containment and reference relationships for their children. This means you don’t need explicit has when using them:
/* These are equivalent: */
mod("pkg") { func }
mod("pkg") has { func }
/* dir also sets implicit refs+has: */
dir("/src") { file } /* Files in /src (no has needed) */
dir("/") {} /* Shows directories and files */
/* func overrides back to refs-only: */
dir("/") { func("main") { "bar" } }
/* "bar" found via call graph (REFS), not containment */Each container type also sets default child types:
| Container | Default Child Types |
|---|---|
dir | directories, files |
file | functions, modules |
mod | modules, functions |
type | types |
data | data |
macro | macros, functions |
field / method | functions |
Practical Examples
Find Functions in a Module
mod("api/handlers") { func }Returns the module and all functions physically located within it.
Performance Note: Inside scopes, bare type selectors like
funcuse efficient filter mode—they derive from the parent instead of querying all symbols. To explicitly select all functions regardless of context, usefunc(filter="false").
Find What Module Contains a Function
mod { "processRequest" }Returns the function “processRequest” and any modules that contain it.
Mix Containment and References
mod("handlers") { "ServeHTTP" {{}} }This query:
- Selects the “handlers” module
- Finds “ServeHTTP” functions contained in that module (implicit refs+has from
mod) - Shows the call graph of those functions (two levels deep)
Compare Container vs Explicit Relationship
/* Containment: what functions are IN the module? */
mod("pkg") { func }
/* References only: what functions does the module CALL? */
mod("pkg") derive(type="refs") { func }These return very different results:
- The first uses the implicit refs+has from
mod— functions found via containment - The second explicitly overrides to refs-only — functions found via call references
Relationship Inheritance
has, refs, and derive all inherit by default — their relationship type propagates to all descendants until explicitly overridden.
has { /* HAS for all descendants */
"foo" { /* Still uses HAS (inherited) */
"bar" /* Still uses HAS (inherited) */
}
}
has { /* HAS for descendants */
"foo" refs { /* Override to REFS for this scope and below */
"bar" /* Uses REFS */
}
}Container type selectors participate in this inheritance:
func,type,data,macro,field/methodexplicitly set REFS, overriding any inherited refs+hasmod,file,dirset refs+has with inheritance
Nested Containment
Use nested containers for multi-level containment:
dir("/src") {
mod {
func("handler")
}
}This finds:
- The
/srcdirectory - Modules contained in that directory
- Functions named “handler” contained in those modules
No explicit has needed — each container type sets it implicitly.
Multi-Instance Symbols
Some symbols have multiple instances:
- A module has one instance per file it spans
- A directory has one instance per contained file
When clicking on such nodes in the UI, a popup shows all instances, allowing you to navigate to specific files.
Use Cases
Architectural Queries
Find all handlers in a specific package:
mod("api/v1") { func("Handler") }Code Organization
See what’s in a directory:
dir("/cmd") { mod { func("main") } }Dependency Scope
Find external dependencies used by a module:
mod("myapp") { func refs { mod("external") } }This finds functions in “myapp” that reference the “external” module. Note: refs explicitly overrides the inherited refs+has from mod, so only call references are shown.
Refactoring Support
Find functions that should be moved:
mod("utils") { func("http") }If you have HTTP-related functions in a “utils” module, they might belong elsewhere.