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:

  1. They share the same source file (object)
  2. The higher-level symbol’s byte range encompasses the lower-level symbol’s range
  3. 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:

ContainerDefault Child Types
dirdirectories, files
filefunctions, modules
modmodules, functions
typetypes
datadata
macromacros, functions
field / methodfunctions

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 func use efficient filter mode—they derive from the parent instead of querying all symbols. To explicitly select all functions regardless of context, use func(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:

  1. Selects the “handlers” module
  2. Finds “ServeHTTP” functions contained in that module (implicit refs+has from mod)
  3. 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/method explicitly set REFS, overriding any inherited refs+has
  • mod, file, dir set refs+has with inheritance

Nested Containment

Use nested containers for multi-level containment:

dir("/src") {
    mod {
        func("handler")
    }
}

This finds:

  1. The /src directory
  2. Modules contained in that directory
  3. 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.