Skip to main content

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    pub kind: AdviceKind,
22    /// Glob pointcut matched against the called sub's bare name.
23    pub pattern: String,
24    /// Advice body (parsed AST; kept for `intercept_list` introspection and
25    /// as a last-resort fallback when bytecode lowering was not possible).
26    pub body: Block,
27    /// Index into `Chunk::blocks` for the body's compiled bytecode. The VM
28    /// dispatches the body via `run_block_region(start, end, …)` using
29    /// `Chunk::block_bytecode_ranges[body_block_idx]`.
30    pub body_block_idx: u16,
31}
32
33/// Per-call advice context — pushed when entering an `around` block, popped on exit.
34/// Read by the `proceed` builtin to invoke the original sub with saved args.
35#[derive(Debug, Clone)]
36pub struct InterceptCtx {
37    pub name: String,
38    pub args: Vec<crate::value::PerlValue>,
39    /// Set true when `proceed()` runs the original.
40    pub proceeded: bool,
41    /// Captured return value of the original after `proceed()`.
42    pub retval: crate::value::PerlValue,
43}
44
45/// Glob match: `*` (any sequence), `?` (any one char), other chars literal.
46/// Mirrors zshrs's `intercept_matches` (exec.rs:3723-3739) — minimal POSIX-glob subset.
47pub fn glob_match(pattern: &str, name: &str) -> bool {
48    if pattern == "*" || pattern == name {
49        return true;
50    }
51    glob_match_inner(pattern.as_bytes(), name.as_bytes())
52}
53
54fn glob_match_inner(pat: &[u8], s: &[u8]) -> bool {
55    // Iterative backtracking matcher (no regex dep, no recursion blowup).
56    let (mut pi, mut si) = (0usize, 0usize);
57    let (mut star_pi, mut star_si): (Option<usize>, usize) = (None, 0);
58    while si < s.len() {
59        if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == s[si]) {
60            pi += 1;
61            si += 1;
62        } else if pi < pat.len() && pat[pi] == b'*' {
63            star_pi = Some(pi);
64            star_si = si;
65            pi += 1;
66        } else if let Some(sp) = star_pi {
67            pi = sp + 1;
68            star_si += 1;
69            si = star_si;
70        } else {
71            return false;
72        }
73    }
74    while pi < pat.len() && pat[pi] == b'*' {
75        pi += 1;
76    }
77    pi == pat.len()
78}
79
80#[cfg(test)]
81mod tests {
82    use super::glob_match;
83
84    #[test]
85    fn exact() {
86        assert!(glob_match("foo", "foo"));
87        assert!(!glob_match("foo", "bar"));
88    }
89
90    #[test]
91    fn star() {
92        assert!(glob_match("*", "anything"));
93        assert!(glob_match("foo*", "foobar"));
94        assert!(glob_match("*bar", "foobar"));
95        assert!(glob_match("f*r", "foobar"));
96        assert!(!glob_match("foo*", "barfoo"));
97    }
98
99    #[test]
100    fn question() {
101        assert!(glob_match("f?o", "foo"));
102        assert!(!glob_match("f?o", "fxxo"));
103    }
104
105    #[test]
106    fn empty() {
107        assert!(glob_match("", ""));
108        assert!(glob_match("*", ""));
109        assert!(!glob_match("", "x"));
110    }
111}