Skip to content

Commit c2e80f4

Browse files
committed
add query
1 parent 51e451c commit c2e80f4

9 files changed

Lines changed: 945 additions & 2 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
///|
2+
test "query can emulate cstyle forward simple forloop patterns-not by iterating captured body" {
3+
let source =
4+
#|fn sample {
5+
#| for i = 0; i < n; i = i + 1 {
6+
#| println("tick")
7+
#| };
8+
#| for j = 0; j < n; j = j + 1 {
9+
#| println(j)
10+
#| };
11+
#| for k = 0; k < n; k = k + 1 {
12+
#| let k = local()
13+
#| println(k)
14+
#| }
15+
#|}
16+
#|
17+
let pattern =
18+
#|for $(counter:id) = $(start:exp); $(counter:id) < $(limit:exp); $(counter:id) = $(counter:id) + 1 {
19+
#| $(body:exp)
20+
#|}
21+
let matches = ExprQuery::ExprQuery(pattern).matches_all(
22+
source_name="sample.mbt",
23+
source,
24+
)
25+
assert_eq(matches.length(), 3)
26+
let filtered : Array[Map[String, @untyped_ast.Node]] = []
27+
for captures in matches {
28+
if captures.get("counter") is Some(counter_node) &&
29+
counter_node.kind is Leaf(PString(counter_name)) &&
30+
captures.get("body") is Some(body) {
31+
// unused variable in for loop body
32+
if !body.contains_unshadowed(id=counter_name) {
33+
filtered.push(captures)
34+
}
35+
}
36+
}
37+
assert_eq(filtered.length(), 2)
38+
debug_inspect(
39+
filtered[0].get("counter").unwrap(),
40+
content=(
41+
#|{
42+
#| kind: Leaf(PString("i")),
43+
#| loc: {
44+
#| start: { fname: "sample.mbt", lnum: 2, bol: 12, cnum: 18 },
45+
#| end: { fname: "sample.mbt", lnum: 2, bol: 12, cnum: 19 },
46+
#| },
47+
#| children: <FixedArray: []>,
48+
#|}
49+
),
50+
)
51+
debug_inspect(
52+
filtered[1].get("counter").unwrap(),
53+
content=(
54+
#|{
55+
#| kind: Leaf(PString("k")),
56+
#| loc: {
57+
#| start: { fname: "sample.mbt", lnum: 8, bol: 121, cnum: 127 },
58+
#| end: { fname: "sample.mbt", lnum: 8, bol: 121, cnum: 128 },
59+
#| },
60+
#| children: <FixedArray: []>,
61+
#|}
62+
),
63+
)
64+
}

query/moon.pkg

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {
2+
"moonbit-community/moongrep/matching",
3+
"moonbit-community/moongrep/rule/compile" @rule_compile,
4+
"moonbit-community/moongrep/rule/model" @rule_model,
5+
"moonbit-community/moongrep/rule/prefilter" @rule_prefilter,
6+
"moonbit-community/moongrep/rule/traverse" @rule_traverse,
7+
"moonbit-community/moongrep/untyped_ast",
8+
"moonbitlang/core/hashmap",
9+
"moonbitlang/lexer/basic",
10+
"moonbitlang/parser",
11+
"moonbitlang/parser/syntax",
12+
}

query/pkg.generated.mbti

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "moonbit-community/moongrep/query"
3+
4+
import {
5+
"moonbit-community/moongrep/untyped_ast",
6+
}
7+
8+
// Values
9+
10+
// Errors
11+
12+
// Types and methods
13+
type ExprQuery
14+
pub fn ExprQuery::ExprQuery(String) -> Self raise
15+
pub fn ExprQuery::matches_all(Self, source_name~ : String, String) -> ArrayView[Map[String, @untyped_ast.Node]] raise
16+
17+
// Type aliases
18+
19+
// Traits
20+

query/query.mbt

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
///|
2+
using @rule_model {
3+
type CompiledRule,
4+
type CompiledRulePattern,
5+
type RawRuleSpec,
6+
}
7+
8+
///|
9+
using @rule_traverse {type ScopedExpr}
10+
11+
///|
12+
priv suberror QueryError {
13+
ParseError(source_name~ : String, info~ : String)
14+
} derive(Debug)
15+
16+
///|
17+
struct ExprQuery {
18+
rule : CompiledRule
19+
pattern : CompiledRulePattern
20+
}
21+
22+
///|
23+
pub fn ExprQuery::ExprQuery(pattern : String) -> ExprQuery raise {
24+
let rules = @rule_compile.compile_rules([anonymous_pattern_rule(pattern)])
25+
guard rules.length() == 1 else {
26+
fail("expected one compiled anonymous pattern rule")
27+
}
28+
let rule = rules[0]
29+
match rule.definition {
30+
Structural(structural) => {
31+
guard structural.patterns.length() == 1 else {
32+
fail("expected one compiled structural pattern")
33+
}
34+
{ rule, pattern: structural.patterns[0] }
35+
}
36+
Taint(_) => fail("expected anonymous pattern to compile as structural rule")
37+
}
38+
}
39+
40+
///|
41+
pub fn ExprQuery::matches_all(
42+
self : ExprQuery,
43+
source_name~ : String,
44+
source : String,
45+
) -> ArrayView[Map[String, @untyped_ast.Node]] raise {
46+
let matches : Array[Map[String, @untyped_ast.Node]] = []
47+
if !@rule_prefilter.rule_is_relevant_to_source(self.rule, source) {
48+
return matches
49+
}
50+
let (impls, reports) = @parser.parse_string(source, name=source_name)
51+
if reports.length() > 0 {
52+
raise QueryError::ParseError(
53+
source_name~,
54+
info="parse failed due to \{compact_reports(reports)}",
55+
)
56+
}
57+
for node in impls {
58+
let impl_source = source_text_for_impl(source, node)
59+
if @rule_prefilter.rule_is_relevant_to_source(self.rule, impl_source) {
60+
collect_impl_matches(self.pattern, node, matches)
61+
}
62+
}
63+
matches
64+
}
65+
66+
///|
67+
fn anonymous_pattern_rule(pattern : String) -> RawRuleSpec {
68+
{
69+
path: pattern,
70+
rule_id: pattern,
71+
description: "Anonymous query pattern.",
72+
definition: Structural({
73+
inside_expr: None,
74+
patterns: [{ shape: pattern, guards: {} }],
75+
patterns_not: [],
76+
patterns_not_mode: @rule_model.StructuralPatternsNotMode::PruneOnNegative,
77+
}),
78+
}
79+
}
80+
81+
///|
82+
fn collect_impl_matches(
83+
pattern : CompiledRulePattern,
84+
node : @syntax.Impl,
85+
matches : Array[Map[String, @untyped_ast.Node]],
86+
) -> Unit {
87+
@rule_traverse.visit_impl_scoped_expr_roots(node, fn(scoped_expr) {
88+
collect_scoped_expr_matches(pattern, scoped_expr, matches)
89+
})
90+
}
91+
92+
///|
93+
fn collect_scoped_expr_matches(
94+
pattern : CompiledRulePattern,
95+
scoped_expr : ScopedExpr,
96+
matches : Array[Map[String, @untyped_ast.Node]],
97+
) -> Unit {
98+
let candidate = @untyped_ast.from_expr(scoped_expr.expr)
99+
if @matching.match_expr_pattern(pattern.compiled, candidate) is Some(result) {
100+
matches.push(capture_map_from_bindings(result.bindings))
101+
} else {
102+
@rule_traverse.visit_scoped_expr_children(scoped_expr, fn(child) {
103+
collect_scoped_expr_matches(pattern, child, matches)
104+
})
105+
}
106+
}
107+
108+
///|
109+
fn capture_map_from_bindings(
110+
bindings : @hashmap.HashMap[String, @matching.BoundValue],
111+
) -> Map[String, @untyped_ast.Node] {
112+
let captures : Map[String, @untyped_ast.Node] = {}
113+
bindings.each(fn(name, value) { captures[name] = value })
114+
captures
115+
}
116+
117+
///|
118+
fn source_text_for_impl(source : String, node : @syntax.Impl) -> String {
119+
source_text_for_location(source, node.loc())
120+
}
121+
122+
///|
123+
fn source_text_for_location(source : String, loc : @basic.Location) -> String {
124+
let lines = source.split("\n").to_array()
125+
let start_line = loc.start.lnum
126+
let end_line = loc.end.lnum
127+
if start_line <= 0 || end_line <= 0 || end_line < start_line {
128+
return source
129+
}
130+
let start_index = start_line - 1
131+
let end_index = end_line
132+
if start_index >= lines.length() || end_index > lines.length() {
133+
return source
134+
}
135+
let block_lines = []
136+
for line in lines[start_index:end_index] {
137+
block_lines.push(line)
138+
}
139+
let block = block_lines.join("\n")
140+
if block.length() == 0 {
141+
source
142+
} else {
143+
block
144+
}
145+
}
146+
147+
///|
148+
fn compact_reports(reports : Array[@basic.Report]) -> String {
149+
let messages : Array[String] = []
150+
for report in reports {
151+
messages.push(report.msg.replace_all(old="\n", new=" "))
152+
}
153+
messages.join(" ")
154+
}

0 commit comments

Comments
 (0)