Askl is a pattern-matching query language designed for source code analysis. It allows you to find symbols (functions, modules, files), trace dependencies, and build custom views of your codebase.

Overview

An Askl query consists of statements that select and filter code symbols and define relationships between them through scopes.

Basic Structure

statement1
statement2 {
    nested_statement
}

Core Concepts

1. Statements

A statement is the fundamental unit of an Askl query. Each statement contains:

  • Commands: One or more verbs that define what to select or filter
  • Scope: Optional nested statements that define relationships

Statement Separation

Newlines separate consecutive statements:

"foo"           /* First statement */
"bar"           /* Second statement */

Semicolons also work as separators (useful for single-line queries):

"foo"; "bar"    /* Two statements on one line */

Without newlines or semicolons, verbs on the same line belong to the same statement:

"foo" "bar"     /* Single statement with two selector verbs */

A scope { must be on the same line as its verb to attach:

"foo" { "bar" }  /* "bar" is inside foo's scope */

"foo"
{ "bar" }        /* Two separate statements (scope does NOT attach to "foo") */

2. Symbol Types

Askl supports eight symbol types organized in a hierarchy:

TypeLevelDescription
field1Struct fields, interface methods (dispatch points)
function2Functions, methods, procedures
type2Structs, interfaces, type declarations
data2Package-level variables, constants
macro2C/C++ preprocessor macros (#define)
file3Source files
module4Packages, modules, namespaces
directory5Filesystem directories

Higher-level symbols can contain lower-level symbols (e.g., a module contains files, which contain functions). Fields are the lowest level — they can be contained by types, functions, files, and modules. Functions, data, types, and macros share the same level.

3. Relationships

Askl supports two types of relationships between symbols:

Reference Relationships (refs, default)

Shows what symbols call or reference each other. This is the default relationship type.

"foo" { "bar" }        /* Functions foo calls (references) */
"foo" refs { "bar" }   /* Explicit refs (same as above) */

Containment Relationships (has)

Shows what symbols contain other symbols based on source code location.

mod("mypackage") { func }   /* Functions contained in module */
file("/main.go") { "handler" }  /* handler function in main.go */

Note: Container types (dir, file, mod) implicitly set both containment and reference relationships for their children. You don’t need explicit has when using them — mod("pkg") { func } works directly.

4. Pattern Matching

Askl uses exact, case-sensitive matching. How a name is matched depends on whether it contains separator characters:

Simple Names (Leaf Matching)

A name with no separators matches the last component of the symbol path. This uses a fast B-tree index lookup:

  • func("handler") matches functions whose last path component is “handler”
  • "main" matches symbols named “main” (not “main_helper” or “domain”)
  • field("read") matches fields named “read” across all structs

Separator characters depend on the symbol type:

  • Code symbols (func, type, data, macro, field, mod): . / :
  • Files and directories: / : (. is NOT a separator — it’s part of filenames)

So file("main.go") is a simple name (no / or :) and matches files whose last path component is “main_go”.

Compound Names (Pattern Matching)

A name with separators uses pattern matching across the full symbol path:

  • "cli.Run" matches symbols containing both “cli” and “Run” tokens in order
  • func("http.Handler") matches functions with “http” and “Handler” in their path
  • "cli" will NOT match “click” (exact token matching)

Path-based Matching for Files and Directories

File and directory selectors support additional matching modes:

ArgumentMatchingExample
Starts with /Exact path matchfile("/src/main.go")
Simple name (no / :)Leaf match (last component)dir("kueue") matches directories named “kueue”
Compound (has / or :)Leaf-anchored patterndir("pkg/kueue") matches “kueue” dirs with “pkg” earlier in path

For compound dir/file queries, intermediate tokens match anywhere but the last is anchored: dir("pkg/kueue") matches directories named “kueue” that have “pkg” somewhere earlier in their path.

To match a token anywhere in the path (non-anchored), use match="contains":

dir("kueue", match="contains")   // matches any directory with "kueue" in its path
file("main", match="contains")   // matches any file with "main" in its path

Note: The match parameter has no effect when the argument starts with / (exact path match always takes precedence).

Verb Types

Verbs in Askl fall into three categories:

Selectors

Selectors add symbols to the result. They query the database and produce nodes in the graph.

VerbDescription
"name" / select(name="...")Select symbols matching a name pattern
func("name")Select functions matching a name
type("name")Select types matching a name
data("name")Select data symbols (variables, constants) matching a name
macro("name")Select macros matching a name
field("name")Select field symbols (function pointer dispatch points) matching a name
method("name")Alias for field — select interface methods / virtual dispatch points
mod("name")Select modules matching a name
file("name")Select files matching a name
dir("name")Select directories matching a name
use("label") / #labelSelect symbols from a labeled statement

Multiple selectors in a statement combine—all must match for a symbol to be included.

Filters

Filters constrain the selection without adding symbols. They remove symbols that don’t match criteria.

VerbDescription
ignore("pattern")Exclude symbols matching the pattern
project("name")Only include symbols from a specific project
filter("kind", "value")Generic filter (see below)
func (no name)Only include function symbols
type (no name)Only include type symbols
data (no name)Only include data symbols
macro (no name)Only include macro symbols
field (no name)Only include field symbols
method (no name)Only include field symbols (alias)
mod (no name)Only include module symbols
file (no name)Only include file symbols
dir (no name)Only include directory symbols

Note: Type selectors like func behave differently based on arguments:

  • Without a name (func) → Filter (constrains to type, inherits to all descendants)
  • With a name (func("foo")) → Selector (queries matching symbols, does NOT inherit)
  • Use any in a child scope to remove inherited type filtering
  • Explicit: func(filter="false") forces selector mode

Modifiers

Modifiers change context or behavior for the current statement or scope.

VerbDescription
hasUse containment relationships instead of references
refsUse reference relationships (default)
derive(type="...")Set relationship type with options
preambleApply subsequent verbs to the global scope
label("name") / @nameLabel this statement for reuse
unnestInclude transitive children/references and all containment levels
anyRemove inherited type filtering (match all symbol types)
! (forced)Force display of relationships
? (weak)Make statement non-constraining

Type Selectors

Type selectors target specific symbol types. As explained above, they act as selectors (with a name) or filters (without a name).

Selector vs Filter Behavior

SyntaxRoleBehavior
func("name")SelectorQueries functions matching “name”
funcFilterConstrains to functions; derives from parent
func(filter="false")SelectorQueries ALL functions
func(filter="true")FilterExplicit filter (same as bare func)

Why this default? Querying all symbols of a type is expensive. Inside scopes, filter mode is much more efficient—it derives from the parent’s contained symbols instead of querying the entire database.

func

Selects function symbols. Explicitly sets the relationship to references only — this overrides any inherited containment from container parents.

func("handler")         /* Functions matching "handler" */
func("http.Handler")    /* Functions matching both "http" and "Handler" */
func(filter="false")    /* All functions (explicit selector mode) */
file("/main.go") { func }  /* Functions in main.go (filter mode) */

type

Selects type symbols (structs, interfaces, type declarations). Like func, explicitly sets the relationship to references only.

type("Request")          /* Types matching "Request" */
type("http.Request")     /* Types matching both "http" and "Request" */
type(filter="false")     /* All types */
mod("net/http") { type }  /* Types in module (filter mode) */
type("Request") { type }  /* Types referenced by Request */

Default child types: types.

data

Selects data symbols (package-level variables and constants). Like func and type, explicitly sets the relationship to references only.

data("Debug")            /* Data symbols matching "Debug" */
data("config.Debug")     /* Data symbols matching both "config" and "Debug" */
data(filter="false")     /* All data symbols */
mod("config") { data }   /* Data symbols in module (filter mode) */

Default child types: data.

macro

Selects macro symbols (C/C++ preprocessor #define directives). Like func and type, explicitly sets the relationship to references only. Macros are indexed with their body range, so function calls inside a macro body become children of the macro by offset containment.

macro("LOG")              /* Macros matching "LOG" */
macro(filter="false")     /* All macros */
func("main") { macro }   /* Macros referenced by main */
macro("LOG") { func }    /* Functions called inside LOG's body */

Default child types: macros and functions.

field / method

Selects field symbols — struct members that act as function pointer dispatch points (C) or interface method signatures (Go). method is an alias for field. Like func, explicitly sets the relationship to references only.

Field names use compound naming: struct_name.field_name (e.g., file_operations.read). A simple name like field("read") matches any field whose last component is “read” across all structs, while field("file_operations.read") uses pattern matching for a precise match.

field("read")                      /* Fields matching "read" (broad) */
field("file_operations.read")      /* Precise match */
method("Read")                     /* Interface methods matching "Read" (Go) */
func("vfs_read") { field("read") { func } }  /* Full dispatch chain */
type("file_operations") has { field }         /* All fields of a struct */
{ field("file_operations.read") }             /* Who calls through this field? */

Default child types: functions.

mod

Selects module/package symbols. Implicitly sets refs+has for children, so contained symbols are found without explicit has.

mod("util")              /* Modules matching "util" */
mod("k8s.io/api")        /* Modules matching the pattern */
mod(filter="false")      /* All modules */
mod("pkg") { func }      /* Functions in module (no has needed) */

Default child types: modules and functions.

file

Selects file symbols. Implicitly sets refs+has for children.

file("main.go")          /* Files whose last component is "main_go" (leaf match) */
file("/src/main.go")     /* Exact path match */
file(filter="false")     /* All files */
dir("/src") { file }     /* Files in /src directory */

Default child types: functions and modules.

dir

Selects directory symbols. Implicitly sets refs+has for children.

dir("cmd")               /* Directories named "cmd" (leaf match) */
dir("/src")              /* Exact path match for /src */
dir(filter="false")      /* All directories */
dir("/") { file }        /* Files in root directory (no has needed) */
dir("/") {}              /* Shows directories and files (default child types) */

Default child types: directories and files.

Default Type Inheritance

At root level (no parent type selector), the default is all types — no filtering is applied. When a bare type selector is used, it sets default child types for its scope and inherits to all descendants:

Type SelectorDefault Child TypesInherits
(root level)all types
funcfunctionsyes
typetypesyes
datadatayes
macromacros, functionsyes
field / methodfunctionsyes
modmodules, functionsyes
filefunctions, modulesyes
dirdirectories, filesyes

Bare type selectors (without a name) inherit by default — their type filter propagates into all descendant scopes. Named type selectors like func("foo") do not inherit.

mod("mypackage") {}      /* Children include modules AND functions */
mod("mypackage") { func }  /* Children explicitly filtered to functions only */
dir("/") {}              /* Children include directories AND files */
data { { "bar" } }       /* data filter inherits — inner scope also filters to data */
data { any { "bar" } }   /* any removes inheritance — inner scope matches all types */

Container Types and Implicit Relationships

Container types (dir, file, mod) automatically set both containment and reference relationships for their children, with inheritance. This means:

/* These are equivalent: */
dir("/src") { file }
dir("/src") has { file }

/* func overrides back to refs-only: */
dir("/") { func("main") { "bar" } }
/* func explicitly sets REFS, so "bar" is found via call graph, not containment */

Relationship Modifiers

refs (References)

Explicitly use reference-based relationships. This is the default. Inherits to all descendants until overridden.

"foo" refs { "bar" }  /* foo calls bar */
"foo" { "bar" }       /* Same - refs is default */

Use refs to override an inherited has:

has { "foo" refs { "bar" } }
/* foo found via containment, bar found via references */

has (Containment)

Use containment-based relationships. Inherits to all descendants until overridden.

has { func }              /* Functions contained in parent */
has { "foo" { "bar" } }   /* HAS propagates: bar also found via containment */

Containment is determined by source code byte ranges: if symbol A’s range contains symbol B’s range, and A’s type level is higher than B’s, then A contains B.

derive (Relationship Configuration)

Advanced relationship modifier with explicit control over type and inheritance.

derive(type="has")                  /* Same as has (inherits by default) */
derive(type="refs")                 /* Same as refs (inherits by default) */
derive(type="has,refs")             /* Both containment and references */
derive(type="refs", inherit="false") /* REFS for this scope only, children reset to default */

Parameters:

  • type: Comma-separated relationship types ("has", "refs", or "has,refs")
  • inherit: Whether children inherit this setting (default: "true")

unnest (Transitive Traversal)

By default, scopes show only direct children (for has) or direct references (for refs), and upward HAS derivation returns only the innermost parent. The unnest modifier removes these restrictions, enabling full transitive traversal through all nesting levels.

func("main") has { func }           /* Only direct children of main */
func("main") unnest has { func }    /* All transitively nested functions */

Without unnest, if main contains function foo which contains function bar, only foo appears. With unnest, both foo and bar appear.

unnest also affects reference traversal:

func("main") { func }               /* Only refs directly in main's body */
func("main") unnest { func }        /* Refs from main and all nested scopes */

For upward (caller/parent) derivation, unnest returns all containment levels instead of just the innermost parent:

{ "inner_symbol" }          /* Only the innermost container */
unnest { "inner_symbol" }   /* All containers at every level */

Note: unnest does not inherit to child scopes. Each statement that needs transitive traversal must use unnest explicitly.

any (Remove Inherited Type Filtering)

The any modifier removes inherited type filters from parent scopes, allowing the current statement to match all symbol types regardless of what the parent specified.

data { any { "bar" } }     /* Inner scope matches all types, not just data */
func { any { "baz" } }     /* Inner scope matches all types, not just functions */

Without any, a bare type selector like data inherits its type filter to all descendants. Use any in a child scope to opt out.

Note: any does not inherit to child scopes. It only affects the statement it appears on. At root level (where there is no inherited type filter), any is a no-op.

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 (inherited from refs override) */
    }
}

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

Generic Verbs

select (Symbol Selection)

Selects symbols whose names match a specific pattern.

Full syntax:

select(name="cli.Run")

Shortcut syntax:

"cli.Run"

Examples:

"main"          /* Symbols whose last name component is "main" */
"http.Handler"  /* Symbols with both "http" and "Handler" in their path */
"user.Create"   /* Symbols with both "user" and "Create" in their path */

filter (Generic Filter)

A generic filter verb supporting multiple filter kinds.

filter("type", "func")              /* Filter to functions (same as bare func) */
filter("compound_name", "main")     /* Filter by compound name match */
filter("exact_name", "/src/main.go") /* Filter by exact name */

Filter kinds:

  • "type": Filter by symbol type ("func", "type", "data", "macro", "field", "method", "mod", "file", "dir")
  • "compound_name": Filter by compound name pattern (token matching)
  • "exact_name": Filter by exact symbol name

ignore (Symbol Filtering)

Excludes symbols matching a pattern from current and nested statements.

ignore("test") "main" {}        /* Ignore test functions */
ignore("builtin") ignore("fmt") "process" {}  /* Multiple ignores */

preamble (Global Configuration)

Applies verbs to the global scope, affecting all subsequent statements. Use scope syntax { } to group multiple preamble verbs across lines:

preamble {
    ignore("builtin")
    ignore("test")
}

"main"  /* This and all following queries ignore builtin and test */

Single-line syntax also works (all preamble verbs must be on the same line):

preamble ignore("builtin") ignore("test")

project (Project Filter)

Filters results to a specific project (useful in multi-project setups).

project("myproject") "main" {}    /* Only symbols from myproject */

forced (Override Relationships)

Forces display of relationships that don’t exist in the actual code.

Syntax:

"parent" {
    !"forced_child"
}

When to use:

  • Function pointer calls not detected by analysis
  • Complex runtime relationships
  • Simplified architectural views
  • Working around analysis limitations

Labels and Reuse

label (Define a Label)

Labels a statement for later reuse with use (or the # shortcut).

Long form:

label("handlers") "handler" {}

Shortcut syntax:

@handlers "handler" {}

The @ prefix is shorthand for label("..."). Use @@ for inheritable labels:

@@handlers "handler" {}
/* Equivalent to: label("handlers", inherit="true") "handler" {} */

use (Reuse a Label)

References a previously labeled statement.

Long form:

label("handlers") "handler" {}
"main" { use("handlers") }    /* Reuse the handlers selection */

Shortcut syntax:

@handlers "handler" {}
"main" { #handlers }    /* Reuse the handlers selection */

The # prefix is shorthand for use("...").

Forced usage:

label("handlers") "handler" {}
"main" { !use("handlers", forced=true) }  /* Force the relationship */

Shortcuts

Askl provides shortcut syntax for labels and reuse, using the @ and # prefixes:

ShortcutEquivalentDescription
@foolabel("foo")Labels the current statement as “foo”
@@foolabel("foo", inherit="true")Labels with propagation to child scopes
#foouse("foo")Uses the labeled statement’s results

Example:

@handlers func("Handle") {}
"main" { #handlers }

/* Equivalent long form: */
label("handlers") func("Handle") {}
"main" { use("handlers") }

Note: Since # is now used for the use-shortcut, comments use /* ... */ syntax instead.

Query Examples

Basic Call Graph

"main" {{}}  /* main and two levels of callees */

Who Calls This Function?

{ "targetFunction" }  /* All callers of targetFunction */

Module Contents

mod("mypackage") { func }  /* All functions in module */

Type Queries

type("Request")             /* Find a type by name */
type("Request") { type }   /* Types referenced by Request */
mod("net/http") { type }   /* All types in a module */

Data Queries

data("Debug")               /* Find a data symbol by name */
func("main") { data }      /* Data symbols referenced by main */
mod("config") { data }     /* All data symbols in a module */

Directory Contents

dir("/src") { file }       /* All files in /src */
dir("/src") {}             /* Directories and files in /src */

Mixed Relationships

mod("pkg") { "handler" {{}} }
/* Module pkg, functions named "handler" within it, and their call graph */

Filter by Project and Type

project("backend") mod("api") { func("Create") }
/* Create functions in api module of backend project */

Exclude Test Code

preamble ignore("test") ignore("mock")
"main" {{}}  /* Call graph excluding test/mock code */

Scope Types

Callee Scope

Shows what the parent symbol calls:

"parent" { "child" }  /* parent calls child */

Caller Scope

Shows what calls the nested symbol:

{ "target" }  /* Functions that call target */

Nested Scopes

Multi-level relationships:

"a" { "b" { "c" } }  /* a calls b, b calls c */

Query Rules and Validation

Required Elements

Every global statement must contain at least one selection verb at some nesting level.

Valid examples:

"foo" {}            /* Direct selection */
{"foo"}             /* Selection in scope */
{{{"foo"}}}         /* Deeply nested selection */

Invalid example:

{{{{}}}}            /* No selection anywhere */

Scopes Filter Results

Scopes only show results if the actual relationship exists. If parent doesn’t call child, no results are displayed:

"parent" { "child" }  /* Empty if parent doesn't call child */