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    /// `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}