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::StrykeValue>,
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::StrykeValue,
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}