Portable structural tree queries with a Tree-sitter-style syntax and a BPMN adapter
query := rule+
rule := captured | '(' captured predicate+ ')'
captured := node capture*
node := '(' selector part* ')' | '[' captured+ ']'
part := captured | field ':' captured | '!' field
selector := qualified-kind | 'category.' category | '_'
capture := '@' capture-name
predicate := '(' predicate-name capture string* ')'| Construct | Meaning |
|---|---|
| (prefix.kind) | A node of that exact kind |
| (category.name) | A node of that adapter category |
| (_) | Any node |
| (a (b)) | b is a direct containment child of a |
| (a (b) (c)) | Distinct children b then c, in increasing positions; other siblings can be between them |
| (a f: (b)) | A direct child b in field f |
| (a !f) | a has no child in field f |
| [(a) (b)] | Alternatives, tried in source order |
| @name | Capture the node (for an alternation, the node that the chosen branch matched) |
| Predicate | Operands | True when |
|---|---|---|
| #has-attr? | capture, attribute | The attribute is present |
| #missing-attr? | capture, attribute | The attribute is absent |
| #attr-eq? | capture, attribute, value | The attribute is present and its decoded value equals value |
| #attr-not-eq? | capture, attribute, value | The attribute is present and its value differs; absent is false |
| #attr-in? | capture, attribute, value+ | The attribute is present and its value is one of the values |
| #text-eq? | capture, value | The node has adapter scalar text equal to value; no descendant text is joined |
| Feature | Code |
|---|---|
| Quantifiers *, +, ? | query/unsupported-quantifier |
| Anchors . | query/unsupported-anchor |
| Directives such as #set! | query/unsupported-directive |
| Regular expressions such as #match? | query/unsupported-regex |
| Anonymous string nodes such as "+" | query/unsupported-anonymous-node |
| Sibling groups such as ((a) (b)) | query/unsupported-grouping |
| Other predicates such as #eq? | query/unknown-predicate |
///|
test "unsupported features fail with stable codes" {
let schema = @query.Schema::new(
id="example/tree",
version=1,
kinds=[@query.Name::new("", "box")],
categories=["thing"],
fields=[],
)
for case in [
("(box)*", "query/unsupported-quantifier"),
("(box . (box))", "query/unsupported-anchor"),
("((box) @b (#set! @b \"k\"))", "query/unsupported-directive"),
("((box) @b (#match? @b \"x\"))", "query/unsupported-regex"),
("(box \"+\")", "query/unsupported-anonymous-node"),
("((box) (box))", "query/unsupported-grouping"),
("((box) @b (#eq? @b \"x\"))", "query/unknown-predicate"),
] {
let diagnostics = @query.compile(case.0, schema).unwrap_err()
assert_eq(diagnostics[0].code, case.1)
assert_true(diagnostics[0].span is Some(_))
}
}///|
struct Shelf {
kinds : Array[String]
children : Array[Array[Int]]
names : Array[String?]
}
///|
let shelf_schema : @query.Schema = @query.Schema::new(
id="example/shelf",
version=1,
kinds=[@query.Name::new("", "shelf"), @query.Name::new("", "book")],
categories=["item"],
fields=[@query.Name::new("", "holds")],
reserved_namespaces=[""],
)
///|
impl @query.QueryTree for Shelf with schema(_self) {
shelf_schema
}
///|
impl @query.QueryTree for Shelf with root(_self) {
0
}
///|
impl @query.QueryTree for Shelf with node_count(self) {
self.kinds.length()
}
///|
impl @query.QueryTree for Shelf with kind(self, node) {
Some(@query.Name::new("", self.kinds[node]))
}
///|
impl @query.QueryTree for Shelf with category(_self, _node) {
"item"
}
///|
impl @query.QueryTree for Shelf with children(self, node) {
self.children[node][:]
}
///|
impl @query.QueryTree for Shelf with field(_self, node) {
if node == 0 {
None
} else {
Some(@query.Name::new("", "holds"))
}
}
///|
impl @query.QueryTree for Shelf with attribute(self, node, name) {
if name == @query.Name::new("", "name") {
self.names[node].map(text => @query.ScalarValue::{ tag: "text", text })
} else {
None
}
}
///|
impl @query.QueryTree for Shelf with text(_self, _node) {
None
}
///|
impl @query.QueryTree for Shelf with extra(_self, _node, _name) {
None
}
///|
impl @query.QueryTree for Shelf with location(_self, _node) {
None
}
///|
impl @query.QueryTree for Shelf with path(_self, _node) {
[]
}
///|
impl @query.QueryTree for Shelf with depth(_self, node) {
if node == 0 {
0
} else {
1
}
}
///|
test "text and typed queries give the same plan and matches" {
let shelf = Shelf::{
kinds: ["shelf", "book", "book"],
children: [[1, 2], [], []],
names: [None, Some("Dune"), Some("")],
}
let text = "((shelf holds: (book) @b) @s (#attr-eq? @b \"name\" \"Dune\"))"
let form = @query.QueryForm::new([
@query.Rule::new(
@query.Pattern::node(
Kind(@query.QName::new("shelf")),
parts=[
Field(
@query.QName::new("holds"),
@query.Pattern::node(Kind(@query.QName::new("book")), captures=["b"]),
),
],
captures=["s"],
),
predicates=[@query.Predicate::attr_eq("b", "name", "Dune")],
),
])
let a = @query.compile(text, shelf_schema).unwrap()
let b = @query.compile_form(form, shelf_schema).unwrap()
assert_eq(a.normalized(), b.normalized())
let result = a.collect(shelf)
assert_eq(result, b.collect(shelf))
assert_eq(result.matches.length(), 1)
assert_eq(result.matches[0].captures.map(c => (c.name, c.node)).iter().to_array(), [
("b", 1),
("s", 0),
])
// A missing name and an empty name are different.
let empty = @query.compile("((book) @b (#attr-eq? @b \"name\" \"\"))", shelf_schema)
.unwrap()
.collect(shelf)
assert_eq(empty.matches.map(m => m.captures[0].node).iter().to_array(), [2])
}| Bound | Default | Failure |
|---|---|---|
| CompileLimits.source_units | 65,536 | query/source-limit |
| CompileLimits.patterns, pattern_nodes, predicates | 256, 4,096, 1,024 | query/pattern-limit |
| CompileLimits.nesting (at most 1,000) | 32 | query/nesting-limit |
| CompileLimits.captures | 256 | query/capture-limit |
| ExecutionLimits.work | 10,000,000 | Failed(Limit(Work)) |
| ExecutionLimits.pending | 100,000 | Failed(Limit(Pending)) |
| ExecutionLimits.results | 1,000,000 | Failed(Limit(Results)) |
| ExecutionLimits.output_units | 100,000,000 | Failed(Limit(Output)) |
///|
test "batches, cumulative limits and truthful terminal states" {
let shelf = Shelf::{
kinds: ["shelf", "book", "book", "book"],
children: [[1, 2, 3], [], [], []],
names: [None, Some("a"), None, Some("b")],
}
let plan = @query.compile("(book) @b", shelf_schema).unwrap()
let projection = @query.Projection::new(shelf_schema, [Attribute("name")]).unwrap()
let cursor = plan.execute(shelf, projection~)
guard cursor.next_batch(2) is Rows(first) else { fail("no rows") }
assert_eq(first.length(), 2)
assert_eq(first[1].captures[0].values[0], Absent)
guard cursor.next_batch(2) is Rows(second) else { fail("no rows") }
assert_eq(second.length(), 1)
assert_true(cursor.next_batch(2) is Complete)
// A work bound reached after a batch is a failure, not completion.
let bounded = plan.execute(shelf, limits=@query.ExecutionLimits::new(work=5))
guard bounded.next_batch(1) is Rows(_) else { fail("no rows") }
assert_true(bounded.next_batch(10) is Failed(Limit(Work)))
assert_true(bounded.next_batch(10) is Failed(Limit(Work)))
}pub(open) trait QueryTree {
fn schema(Self) -> Schema
fn root(Self) -> Int
fn node_count(Self) -> Int
fn kind(Self, Int) -> Name?
fn category(Self, Int) -> String
fn children(Self, Int) -> ArrayView[Int]
fn field(Self, Int) -> Name?
fn attribute(Self, Int, Name) -> ScalarValue?
fn text(Self, Int) -> String?
fn extra(Self, Int, String) -> ScalarValue?
fn location(Self, Int) -> QueryLocation?
fn path(Self, Int) -> ReadOnlyArray[QueryPathStep]
fn depth(Self, Int) -> Int
}fn CompileLimits::new(source_units? : Int, patterns? : Int, pattern_nodes? : Int, nesting? : Int, captures? : Int, predicates? : Int) -> CompileLimitsfn ExecutionLimits::new(work? : Int, pending? : Int, results? : Int, output_units? : Int) -> ExecutionLimitspub(all) enum Projected {
Absent
Text(String)
Scalar(ScalarValue)
Name(Name)
Int(Int)
Location(QueryLocation)
Path(ReadOnlyArray[QueryPathStep])
} derive(Eq, Debug)pub struct Projection {
schema_id : String
schema_version : Int
fields : ReadOnlyArray[FieldRequest]
// private fields
}impl Debug for Projectionfn Projection::new(schema : Schema, fields : ArrayView[FieldRequest], namespaces? : ArrayView[(String, String)]) -> Result[Projection, ReadOnlyArray[QueryDiagnostic]]pub(all) enum QueryBatch {
Rows(ReadOnlyArray[QueryMatch])
Complete
Stopped
Failed(QueryFailure)
} derive(Eq, Debug)pub struct QueryCursor[T] {
// private fields
}impl Debug for QueryCursor[T]pub(all) enum QueryFailure {
Limit(QueryLimit)
IncompatibleSchema(expected~ : String, actual~ : String)
InvalidRoot
InvalidLimits
} derive(Eq, Debug)pub(all) struct QueryLocation {
source_id : String?
start : QueryPosition
end : QueryPosition
} derive(Eq, Debug)pub struct QueryPlan {
language_version : Int
schema_id : String
schema_version : Int
pattern_count : Int
capture_names : ReadOnlyArray[String]
// private fields
}fn[T : QueryTree] QueryPlan::collect(self : QueryPlan, tree : T, root? : Int, projection? : Projection, limits? : ExecutionLimits) -> QueryResultfn[T : QueryTree] QueryPlan::execute(self : QueryPlan, tree : T, root? : Int, projection? : Projection, limits? : ExecutionLimits) -> QueryCursor[T]pub struct QueryResult {
matches : ReadOnlyArray[QueryMatch]
outcome : CursorState
} derive(Eq, Debug)fn compile(source : String, schema : Schema, namespaces? : ArrayView[(String, String)], limits? : CompileLimits) -> Result[QueryPlan, ReadOnlyArray[QueryDiagnostic]]fn compile_form(form : QueryForm, schema : Schema, namespaces? : ArrayView[(String, String)], limits? : CompileLimits) -> Result[QueryPlan, ReadOnlyArray[QueryDiagnostic]]fn is_capture_name(text : String) -> Boollet language_version : Intlet max_projection_fields : Intfn parse(source : String, limits? : CompileLimits) -> Result[QueryForm, ReadOnlyArray[QueryDiagnostic]]let query_diagnostic_codes : ReadOnlyArray[String]Install
Download zipPortable structural tree queries with a Tree-sitter-style syntax and a BPMN adapter