9c5506b83e
This commit introduces support for unhygienic macro expansion using the `~ident` syntax. When an identifier is prefixed with a tilde (`~`) within a macro template, it is no longer subject to hygienic renaming. Instead, it directly refers to a binding in the call site's environment. This allows macros to interact with variables defined outside the macro's scope, such as: - Modifying call-site variables. - Capturing call-site variables as upvalues in closures. - Defining new variables that are visible in the call site. A new test case `test_macro_tilde_nonparam_assign` has been added to verify this functionality. Previously, unhygienic escapes were implicitly handled by `SyntaxKind::Identifier` if they were not macro parameters. This change makes this behavior explicit and correctly handles non-parameter identifiers by returning a bare symbol.
112 lines
3.7 KiB
Rust
112 lines
3.7 KiB
Rust
use myc::ast::environment::Environment;
|
|
|
|
// --- Tests for ~ident (non-parameter unhygienic escape) ---
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_assign_single() {
|
|
// ~x where x is not a parameter reaches the call-site variable.
|
|
let env = Environment::new();
|
|
let res = env
|
|
.run_script("(do (def x 0) (macro inc-x [] `(assign ~x (+ ~x 1))) (inc-x) x)")
|
|
.expect("should increment x");
|
|
assert_eq!(format!("{}", res), "1");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_assign_multiple_expansions() {
|
|
// Multiple expansions of the same macro do not interfere with each other.
|
|
let env = Environment::new();
|
|
let res = env
|
|
.run_script(
|
|
"(do (def x 0) (macro inc-x [] `(assign ~x (+ ~x 1))) (inc-x) (inc-x) (inc-x) x)",
|
|
)
|
|
.expect("should increment x three times");
|
|
assert_eq!(format!("{}", res), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_def_creates_visible_binding() {
|
|
// ~y as a def pattern creates a visible binding at the call site.
|
|
let env = Environment::new();
|
|
let res = env
|
|
.run_script("(do (macro def-y [] `(def ~y 42)) (def-y) y)")
|
|
.expect("should create visible y");
|
|
assert_eq!(format!("{}", res), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_upvalue_capture() {
|
|
// ~x inside a lambda captures the call-site variable as an upvalue.
|
|
let env = Environment::new();
|
|
let res = env
|
|
.run_script("(do (def x 10) (macro make-adder [] `(fn [n] (+ n ~x))) (def add-x (make-adder)) (add-x 5))")
|
|
.expect("should capture x as upvalue");
|
|
assert_eq!(format!("{}", res), "15");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_plain_def_stays_isolated() {
|
|
// A plain template `(def x ...)` (without ~) remains hygienically isolated
|
|
// even when ~x is used in the same template for references.
|
|
let env = Environment::new();
|
|
let res = env
|
|
.run_script("(do (def x 1) (macro double-x [] `(do (def x (* ~x 2)) ~x)) (double-x))")
|
|
.expect("should return call-site x, not macro-internal x");
|
|
// ~x reads the call-site x=1; the plain `def x` creates an isolated x[ctx]=2.
|
|
assert_eq!(format!("{}", res), "1");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tilde_nonparam_missing_variable_is_error() {
|
|
// ~x where x does not exist at the call site produces a compile error.
|
|
let env = Environment::new();
|
|
let res = env.run_script("(do (macro inc-x [] `(assign ~x (+ ~x 1))) (inc-x))");
|
|
assert!(res.is_err(), "should fail: x is not defined at call site");
|
|
}
|
|
|
|
#[test]
|
|
fn test_macro_inlining_identity_collision() {
|
|
let source = r#"
|
|
(do
|
|
(macro wrap [f] `(fn [x] (~f x)))
|
|
|
|
(def add1 (fn [x] (+ x 1)))
|
|
(def add2 (fn [x] (+ x 2)))
|
|
|
|
(def w1 (wrap add1))
|
|
(def w2 (wrap add2))
|
|
|
|
(w1 (w2 10)))
|
|
"#;
|
|
|
|
// 1. Verify the result is correct
|
|
let env_run = Environment::new();
|
|
let res = env_run.run_script(source).expect("Failed to run script");
|
|
assert_eq!(format!("{}", res), "13");
|
|
|
|
// 2. Verify that it was actually folded into a constant by the optimizer
|
|
let env_dump = Environment::new();
|
|
let dump = env_dump.dump_ast(source).expect("Failed to dump AST");
|
|
assert!(
|
|
dump.contains("Constant: 13") || dump.contains("Call"),
|
|
"Macro-wrapped calls should be properly handled. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_repro_placeholder_in_template_failure() {
|
|
let env = Environment::new();
|
|
let source = r#"
|
|
(do
|
|
(macro repeat [var limit body]
|
|
`((fn [~var __limit]
|
|
(if (< ~var __limit)
|
|
(do ~body (again (+ ~var 1) __limit))))
|
|
0 ~limit))
|
|
(repeat n 10 42))
|
|
"#;
|
|
let res = env.run_script(source);
|
|
assert!(res.is_ok(), "Expansion failed to strip placeholders: {:?}", res.err());
|
|
}
|