stryke/aop.rs
1//! Aspect-oriented programming primitives for stryke.
2//!
3//! Mirrors the design of [zshrs's `intercept`](../../zshrs/src/exec.rs) (call-time advice
4//! on user subs, glob pointcuts, `before`/`after`/`around` kinds, `proceed()` for around).
5//! See `Interpreter::run_intercepts_for_call` and `Op::RegisterAdvice`.
6//!
7//! Surface (parsed by `parser::parse_advice_decl`):
8//! ```text
9//! before "<glob>" { ... } # run before the matched sub; sees $INTERCEPT_NAME, @INTERCEPT_ARGS
10//! after "<glob>" { ... } # run after; sees $INTERCEPT_MS, $INTERCEPT_US, $? (retval)
11//! around "<glob>" { ... } # wrap; must call proceed() to invoke the original
12//! ```
13
14use crate::ast::{AdviceKind, Block};
15
16/// One AOP advice record. Stored in `Interpreter::intercepts`.
17#[derive(Debug, Clone)]
18pub struct Intercept {
19 /// Auto-incremented removal id (1-based).
20 pub id: u32,
21 /// `kind` field.
22 pub kind: AdviceKind,
23 /// Glob pointcut matched against the called sub's bare name.
24 pub pattern: String,
25 /// Advice body (parsed AST; kept for `intercept_list` introspection and
26 /// as a last-resort fallback when bytecode lowering was not possible).
27 pub body: Block,
28 /// Index into `Chunk::blocks` for the body's compiled bytecode. The VM
29 /// dispatches the body via `run_block_region(start, end, …)` using
30 /// `Chunk::block_bytecode_ranges[body_block_idx]`.
31 pub body_block_idx: u16,
32}
33
34/// Per-call advice context — pushed when entering an `around` block, popped on exit.
35/// Read by the `proceed` builtin to invoke the original sub with saved args.
36#[derive(Debug, Clone)]
37pub struct InterceptCtx {
38 /// `name` field.
39 pub name: String,
40 /// `args` field.
41 pub args: Vec<crate::value::StrykeValue>,
42 /// Set true when `proceed()` runs the original.
43 pub proceeded: bool,
44 /// Captured return value of the original after `proceed()`.
45 pub retval: crate::value::StrykeValue,
46}
47
48/// Glob match: `*` (any sequence), `?` (any one char), other chars literal.
49/// Mirrors zshrs's `intercept_matches` (exec.rs:3723-3739) — minimal POSIX-glob subset.
50pub fn glob_match(pattern: &str, name: &str) -> bool {
51 if pattern == "*" || pattern == name {
52 return true;
53 }
54 glob_match_inner(pattern.as_bytes(), name.as_bytes())
55}
56
57fn glob_match_inner(pat: &[u8], s: &[u8]) -> bool {
58 // Iterative backtracking matcher (no regex dep, no recursion blowup).
59 let (mut pi, mut si) = (0usize, 0usize);
60 let (mut star_pi, mut star_si): (Option<usize>, usize) = (None, 0);
61 while si < s.len() {
62 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == s[si]) {
63 pi += 1;
64 si += 1;
65 } else if pi < pat.len() && pat[pi] == b'*' {
66 star_pi = Some(pi);
67 star_si = si;
68 pi += 1;
69 } else if let Some(sp) = star_pi {
70 pi = sp + 1;
71 star_si += 1;
72 si = star_si;
73 } else {
74 return false;
75 }
76 }
77 while pi < pat.len() && pat[pi] == b'*' {
78 pi += 1;
79 }
80 pi == pat.len()
81}
82
83#[cfg(test)]
84mod tests {
85 use super::glob_match;
86
87 #[test]
88 fn exact() {
89 assert!(glob_match("foo", "foo"));
90 assert!(!glob_match("foo", "bar"));
91 }
92
93 #[test]
94 fn star() {
95 assert!(glob_match("*", "anything"));
96 assert!(glob_match("foo*", "foobar"));
97 assert!(glob_match("*bar", "foobar"));
98 assert!(glob_match("f*r", "foobar"));
99 assert!(!glob_match("foo*", "barfoo"));
100 }
101
102 #[test]
103 fn question() {
104 assert!(glob_match("f?o", "foo"));
105 assert!(!glob_match("f?o", "fxxo"));
106 }
107
108 #[test]
109 fn empty() {
110 assert!(glob_match("", ""));
111 assert!(glob_match("*", ""));
112 assert!(!glob_match("", "x"));
113 }
114}