1use std::collections::HashMap;
60
61use harn_hostlib::ast::{api, Language};
62use tree_sitter::Node;
63
64pub const ROOT_CAPTURE: &str = "__match";
68
69const PLACEHOLDER_STEM: &str = "__harn_hole_";
73
74#[derive(Debug, Clone)]
76pub struct CompiledPattern {
77 pub query: String,
80 pub metavars: Vec<String>,
82}
83
84pub fn compile_pattern(snippet: &str, language: Language) -> Result<CompiledPattern, String> {
92 let sub = substitute(snippet)?;
93
94 let mut metavar_node_patterns: HashMap<String, String> = HashMap::new();
98 for (metavar, constraint) in &sub.metavar_constraints {
99 metavar_node_patterns.insert(metavar.clone(), resolve_constraint(constraint, language)?);
100 }
101
102 let mut last_err: Option<String> = None;
103
104 for (prefix, suffix) in contexts(language) {
105 let wrapped = format!("{prefix}{}{suffix}", sub.text);
106 let tree = api::parse_tree(&wrapped, language).map_err(|err| err.to_string())?;
107 let root = tree.root_node();
108 if root.has_error() {
109 last_err = Some(format!(
110 "snippet did not parse cleanly in `{}`: `{snippet}`",
111 language.name()
112 ));
113 continue;
114 }
115
116 let start = prefix.len();
121 let end = start + sub.text.len();
122 let Some(pattern_root) = root.descendant_for_byte_range(start, end.saturating_sub(1))
123 else {
124 last_err = Some(format!(
125 "could not locate snippet subtree in `{}`",
126 language.name()
127 ));
128 continue;
129 };
130
131 let bytes = wrapped.as_bytes();
132 let mut builder =
133 QueryBuilder::new(bytes, &sub.placeholder_to_metavar, &metavar_node_patterns);
134 let body = builder.build(pattern_root);
135 let predicates = builder.predicates();
136 let query = if predicates.is_empty() {
137 format!("({body} @{ROOT_CAPTURE})")
138 } else {
139 format!("({body} @{ROOT_CAPTURE} {predicates})")
140 };
141 return Ok(CompiledPattern {
142 query,
143 metavars: sub.metavar_order,
144 });
145 }
146
147 Err(last_err.unwrap_or_else(|| format!("snippet did not parse in `{}`", language.name())))
148}
149
150fn contexts(language: Language) -> Vec<(&'static str, &'static str)> {
156 let mut v = vec![("", "")];
157 let wrapper = match language {
158 Language::Rust => Some(("fn __harn_probe() { ", " }")),
159 Language::Go => Some(("package p\nfunc __harn_probe() { ", " }")),
160 Language::Java | Language::CSharp => {
161 Some(("class __HarnProbe { void __harn_probe() { ", " } }"))
162 }
163 Language::C | Language::Cpp => Some(("void __harn_probe() { ", " }")),
164 Language::Kotlin => Some(("fun __harn_probe() { ", " }")),
165 Language::Swift => Some(("func __harn_probe() { ", " }")),
166 Language::Scala => Some(("def __harn_probe() = { ", " }")),
167 _ => None,
168 };
169 v.extend(wrapper);
170 v
171}
172
173struct Substituted {
178 text: String,
180 placeholder_to_metavar: HashMap<String, String>,
182 metavar_order: Vec<String>,
184 metavar_constraints: HashMap<String, String>,
187}
188
189#[expect(
190 clippy::string_slice,
191 reason = "cursor offsets advance by len_utf8 or across ASCII bytes, so every offset \
192 is a char boundary"
193)]
194fn substitute(snippet: &str) -> Result<Substituted, String> {
195 let mut text = String::with_capacity(snippet.len());
196 let mut placeholder_to_metavar = HashMap::new();
197 let mut metavar_to_placeholder: HashMap<String, String> = HashMap::new();
198 let mut metavar_order: Vec<String> = Vec::new();
199 let mut metavar_constraints: HashMap<String, String> = HashMap::new();
200
201 let bytes = snippet.as_bytes();
202 let mut i = 0;
203 while i < bytes.len() {
204 if bytes[i] != b'$' {
205 let ch = snippet[i..].chars().next().unwrap();
208 text.push(ch);
209 i += ch.len_utf8();
210 continue;
211 }
212 if snippet[i..].starts_with("$$$") {
213 return Err(
214 "variadic `$$$` metavariables are not yet supported (tracked in #2833)".into(),
215 );
216 }
217 let name_start = i + 1;
219 let mut j = name_start;
220 if j < bytes.len() && is_ident_start(bytes[j]) {
221 j += 1;
222 while j < bytes.len() && is_ident_continue(bytes[j]) {
223 j += 1;
224 }
225 }
226 if j == name_start {
227 text.push('$');
229 i += 1;
230 continue;
231 }
232 let name = &snippet[name_start..j];
233 let mut consumed_end = j;
238 if j < bytes.len() && bytes[j] == b':' {
239 let kind_start = j + 1;
240 if kind_start < bytes.len() && is_ident_start(bytes[kind_start]) {
241 let mut k = kind_start + 1;
242 while k < bytes.len() && is_ident_continue(bytes[k]) {
243 k += 1;
244 }
245 let constraint = &snippet[kind_start..k];
246 match metavar_constraints.get(name) {
247 Some(existing) if existing != constraint => {
248 return Err(format!(
249 "metavariable `${name}` has conflicting type constraints \
250 `:{existing}` and `:{constraint}`"
251 ));
252 }
253 _ => {
254 metavar_constraints.insert(name.to_string(), constraint.to_string());
255 }
256 }
257 consumed_end = k;
258 }
259 }
260 let placeholder = metavar_to_placeholder
261 .entry(name.to_string())
262 .or_insert_with(|| {
263 let placeholder = format!("{PLACEHOLDER_STEM}{}", metavar_order.len());
264 metavar_order.push(name.to_string());
265 placeholder
266 })
267 .clone();
268 placeholder_to_metavar.insert(placeholder.clone(), name.to_string());
269 text.push_str(&placeholder);
270 i = consumed_end;
271 }
272
273 Ok(Substituted {
277 text,
278 placeholder_to_metavar,
279 metavar_order,
280 metavar_constraints,
281 })
282}
283
284fn resolve_constraint(constraint: &str, language: Language) -> Result<String, String> {
289 let ts = language
290 .ts_language()
291 .ok_or_else(|| format!("no grammar for `{}`", language.name()))?;
292 let candidates: Vec<&str> = match constraint {
295 "expr" | "expression" => vec!["expression"],
296 "stmt" | "statement" => vec!["statement"],
297 "ty" | "type" => vec!["type"],
298 "ident" | "identifier" => vec!["identifier"],
299 other => vec![other],
300 };
301 let valid: Vec<String> = candidates
302 .iter()
303 .filter(|kind| ts.id_for_node_kind(kind, true) != 0)
304 .map(|kind| format!("({kind})"))
305 .collect();
306 if valid.is_empty() {
307 return Err(format!(
308 "typed placeholder `:{constraint}` is not a node kind in `{}` \
309 (use an exact tree-sitter kind)",
310 language.name()
311 ));
312 }
313 Ok(if valid.len() == 1 {
314 valid.into_iter().next().unwrap()
315 } else {
316 format!("[{}]", valid.join(" "))
317 })
318}
319
320fn is_ident_start(b: u8) -> bool {
321 b.is_ascii_alphabetic() || b == b'_'
322}
323
324fn is_ident_continue(b: u8) -> bool {
325 b.is_ascii_alphanumeric() || b == b'_'
326}
327
328struct QueryBuilder<'a> {
333 src: &'a [u8],
334 placeholder_to_metavar: &'a HashMap<String, String>,
335 metavar_node_patterns: &'a HashMap<String, String>,
338 occurrences: HashMap<String, usize>,
340 eq_predicates: Vec<String>,
342 literal_count: usize,
344}
345
346impl<'a> QueryBuilder<'a> {
347 fn new(
348 src: &'a [u8],
349 placeholder_to_metavar: &'a HashMap<String, String>,
350 metavar_node_patterns: &'a HashMap<String, String>,
351 ) -> Self {
352 QueryBuilder {
353 src,
354 placeholder_to_metavar,
355 metavar_node_patterns,
356 occurrences: HashMap::new(),
357 eq_predicates: Vec::new(),
358 literal_count: 0,
359 }
360 }
361
362 fn build(&mut self, node: Node<'_>) -> String {
363 if node.child_count() == 0 {
365 let text = self.node_text(node);
366 if let Some(metavar) = self.placeholder_to_metavar.get(text) {
367 let node_pattern = self
368 .metavar_node_patterns
369 .get(metavar)
370 .map(String::as_str)
371 .unwrap_or("(_)");
372 return format!("{node_pattern} @{}", self.capture_for(metavar));
373 }
374 if node.is_named() {
375 let cap = format!("__lit_{}", self.literal_count);
379 self.literal_count += 1;
380 self.eq_predicates
381 .push(format!("(#eq? @{cap} {})", quote_literal(text)));
382 return format!("({}) @{cap}", node.kind());
383 }
384 return quote_literal(text);
385 }
386
387 let mut parts: Vec<String> = Vec::new();
388 let mut cursor = node.walk();
389 for (i, child) in node.children(&mut cursor).enumerate() {
390 let sub = self.build(child);
391 match node.field_name_for_child(i as u32) {
395 Some(field) if child.is_named() => parts.push(format!("{field}: {sub}")),
396 _ => parts.push(sub),
397 }
398 }
399 format!("({} {})", node.kind(), parts.join(" "))
400 }
401
402 fn capture_for(&mut self, metavar: &str) -> String {
406 let count = self.occurrences.entry(metavar.to_string()).or_insert(0);
407 *count += 1;
408 if *count == 1 {
409 metavar.to_string()
410 } else {
411 let helper = format!("{metavar}.{count}");
412 self.eq_predicates
413 .push(format!("(#eq? @{metavar} @{helper})"));
414 helper
415 }
416 }
417
418 fn predicates(&self) -> String {
419 self.eq_predicates.join(" ")
420 }
421
422 fn node_text(&self, node: Node<'_>) -> &'a str {
423 std::str::from_utf8(&self.src[node.start_byte()..node.end_byte()]).unwrap_or_default()
424 }
425}
426
427fn quote_literal(text: &str) -> String {
430 let mut out = String::with_capacity(text.len() + 2);
431 out.push('"');
432 for ch in text.chars() {
433 if ch == '"' || ch == '\\' {
434 out.push('\\');
435 }
436 out.push(ch);
437 }
438 out.push('"');
439 out
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use streaming_iterator::StreamingIterator;
446 use tree_sitter::{Query, QueryCursor};
447
448 #[expect(
451 clippy::string_slice,
452 reason = "tree-sitter capture ranges are char-aligned byte offsets into code"
453 )]
454 fn run(snippet: &str, language: Language, code: &str) -> Vec<(String, Vec<String>)> {
455 let compiled = compile_pattern(snippet, language).expect("compiles");
456 let ts_language = language.ts_language().expect("grammar");
457 let query = Query::new(&ts_language, &compiled.query)
458 .unwrap_or_else(|e| panic!("query rejected: {e}\nquery: {}", compiled.query));
459 let tree = api::parse_tree(code, language).expect("parse code");
460 let names: Vec<&str> = query.capture_names().to_vec();
461 let mut cursor = QueryCursor::new();
462 let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
463 let mut out = Vec::new();
464 while let Some(m) = matches.next() {
465 let mut per_capture: HashMap<String, Vec<String>> = HashMap::new();
466 for cap in m.captures {
467 let name = names[cap.index as usize].to_string();
468 let text = code[cap.node.start_byte()..cap.node.end_byte()].to_string();
469 per_capture.entry(name).or_default().push(text);
470 }
471 for (name, texts) in per_capture {
472 out.push((name, texts));
473 }
474 }
475 out
476 }
477
478 fn capture<'a>(binds: &'a [(String, Vec<String>)], name: &str) -> &'a [String] {
479 binds
480 .iter()
481 .find(|(n, _)| n == name)
482 .map(|(_, v)| v.as_slice())
483 .unwrap_or(&[])
484 }
485
486 #[test]
487 fn compiles_destructuring_default_in_typescript() {
488 let snippet = "$SRC?.$KEY ?? $DEFAULT";
490 let compiled = compile_pattern(snippet, Language::TypeScript).expect("compiles");
491 assert_eq!(compiled.metavars, vec!["SRC", "KEY", "DEFAULT"]);
492 let binds = run(
494 snippet,
495 Language::TypeScript,
496 "const a = cfg?.timeout ?? 30;",
497 );
498 assert_eq!(capture(&binds, "SRC"), ["cfg".to_string()]);
499 assert_eq!(capture(&binds, "KEY"), ["timeout".to_string()]);
500 assert_eq!(capture(&binds, "DEFAULT"), ["30".to_string()]);
501 }
502
503 #[test]
504 fn compiles_optional_chain_nil_coalescing_in_harn() {
505 let snippet = "$SRC?.$KEY ?? $DEFAULT";
506 let compiled = compile_pattern(snippet, Language::Harn).expect("compiles");
507 assert_eq!(compiled.metavars, vec!["SRC", "KEY", "DEFAULT"]);
508 let binds = run(
509 snippet,
510 Language::Harn,
511 "fn main() {\n let timeout = cfg?.timeout ?? 30\n}\n",
512 );
513 assert_eq!(capture(&binds, "SRC"), ["cfg".to_string()]);
514 assert_eq!(capture(&binds, "KEY"), ["timeout".to_string()]);
515 assert_eq!(capture(&binds, "DEFAULT"), ["30".to_string()]);
516 }
517
518 #[test]
519 fn operator_is_constrained_not_just_structure() {
520 let snippet = "$SRC?.$KEY ?? $DEFAULT";
523 let binds = run(
524 snippet,
525 Language::TypeScript,
526 "const a = cfg?.timeout || 30;",
527 );
528 assert!(
529 capture(&binds, "SRC").is_empty(),
530 "|| must not match the ?? pattern"
531 );
532 }
533
534 #[test]
535 fn round_trips_the_assignment_form() {
536 let snippet = "$NAME = $SRC?.$KEY ?? $DEFAULT";
538 let compiled = compile_pattern(snippet, Language::TypeScript).expect("compiles");
539 assert_eq!(compiled.metavars, vec!["NAME", "SRC", "KEY", "DEFAULT"]);
540 let binds = run(
541 snippet,
542 Language::TypeScript,
543 "x = src?.userId ?? fallback;",
544 );
545 assert_eq!(capture(&binds, "NAME"), ["x".to_string()]);
546 assert_eq!(capture(&binds, "SRC"), ["src".to_string()]);
547 assert_eq!(capture(&binds, "KEY"), ["userId".to_string()]);
548 assert_eq!(capture(&binds, "DEFAULT"), ["fallback".to_string()]);
549 }
550
551 #[test]
552 fn lifts_metavars_in_rust() {
553 let snippet = "let $NAME = $VALUE;";
554 let binds = run(snippet, Language::Rust, "fn f() { let total = compute(); }");
555 assert_eq!(capture(&binds, "NAME"), ["total".to_string()]);
556 assert_eq!(capture(&binds, "VALUE"), ["compute()".to_string()]);
557 }
558
559 #[test]
560 fn lifts_metavars_in_python() {
561 let snippet = "$FN($ARG)";
562 let binds = run(snippet, Language::Python, "print(value)");
563 assert_eq!(capture(&binds, "FN"), ["print".to_string()]);
564 assert_eq!(capture(&binds, "ARG"), ["value".to_string()]);
565 }
566
567 #[test]
568 fn lifts_metavars_in_go() {
569 let snippet = "$FN($ARG)";
570 let binds = run(snippet, Language::Go, "package main\nfunc m() { log(err) }");
571 assert_eq!(capture(&binds, "FN"), ["log".to_string()]);
572 assert_eq!(capture(&binds, "ARG"), ["err".to_string()]);
573 }
574
575 #[test]
576 fn repeated_metavar_unifies() {
577 let snippet = "$X + $X";
579 let same = run(snippet, Language::Rust, "fn f() { let _ = a + a; }");
580 assert_eq!(capture(&same, "X"), ["a".to_string()]);
581 let different = run(snippet, Language::Rust, "fn f() { let _ = a + b; }");
582 assert!(
583 capture(&different, "X").is_empty(),
584 "unification must reject `a + b`"
585 );
586 }
587
588 #[test]
589 fn rejects_unparseable_snippet() {
590 let err = compile_pattern("$A ?? ?? $B", Language::TypeScript).unwrap_err();
591 assert!(err.contains("did not parse"), "got: {err}");
592 }
593
594 #[test]
595 fn rejects_variadic_for_now() {
596 let err = compile_pattern("foo($$$ARGS)", Language::TypeScript).unwrap_err();
597 assert!(err.contains("variadic"), "got: {err}");
598 }
599
600 #[test]
601 fn typed_placeholder_narrows_to_kind() {
602 let snippet = "$FN($ARG:identifier)";
604 let compiled = compile_pattern(snippet, Language::TypeScript).expect("compiles");
605 assert_eq!(compiled.metavars, vec!["FN", "ARG"]);
607 let hit = run(snippet, Language::TypeScript, "f(x);");
609 assert_eq!(capture(&hit, "ARG"), ["x".to_string()]);
610 let miss = run(snippet, Language::TypeScript, "f(g());");
612 assert!(
613 capture(&miss, "ARG").is_empty(),
614 "a call argument must not match `:identifier`: {miss:?}"
615 );
616 }
617
618 #[test]
619 fn typed_placeholder_expression_alias_matches_any_expression() {
620 let snippet = "$FN($ARG:expression)";
624 let ident = run(snippet, Language::TypeScript, "f(x);");
625 assert_eq!(capture(&ident, "ARG"), ["x".to_string()]);
626 let call = run(snippet, Language::TypeScript, "f(g());");
627 assert_eq!(capture(&call, "ARG"), ["g()".to_string()]);
628 }
629
630 #[test]
631 fn typed_placeholder_unknown_kind_is_an_error() {
632 let err = compile_pattern("$X:not_a_real_kind", Language::TypeScript).unwrap_err();
633 assert!(err.contains("not a node kind"), "got: {err}");
634 }
635
636 #[test]
637 fn typed_placeholder_alias_unavailable_in_grammar_errors() {
638 let err = compile_pattern("let $X = $V:expression;", Language::Rust).unwrap_err();
641 assert!(err.contains("not a node kind"), "got: {err}");
642 }
643
644 #[test]
645 fn typed_placeholder_unifies_and_constrains() {
646 let snippet = "$X:identifier + $X";
648 let same = run(snippet, Language::Rust, "fn f() { let _ = a + a; }");
649 assert_eq!(capture(&same, "X"), ["a".to_string()]);
650 let different = run(snippet, Language::Rust, "fn f() { let _ = a + b; }");
651 assert!(
652 capture(&different, "X").is_empty(),
653 "unification still holds"
654 );
655 }
656
657 #[test]
658 fn colon_without_constraint_is_left_literal() {
659 let snippet = "{$KEY: $VAL}";
662 let compiled = compile_pattern(snippet, Language::TypeScript).expect("compiles");
663 assert_eq!(compiled.metavars, vec!["KEY", "VAL"]);
664 let binds = run(snippet, Language::TypeScript, "let o = {a: 1};");
665 assert_eq!(capture(&binds, "KEY"), ["a".to_string()]);
666 assert_eq!(capture(&binds, "VAL"), ["1".to_string()]);
667 }
668
669 #[test]
670 fn literal_pattern_matches_exact_text() {
671 let snippet = "foo()";
674 let compiled = compile_pattern(snippet, Language::TypeScript).expect("compiles");
675 assert!(compiled.metavars.is_empty());
676 let hit = run(snippet, Language::TypeScript, "foo();");
678 assert!(!hit.is_empty());
679 let miss = run(snippet, Language::TypeScript, "bar();");
681 assert!(
682 miss.is_empty(),
683 "bar() must not match foo()'s literal pattern: {miss:?}"
684 );
685 }
686}