Skip to content

Conversation

@harehare
Copy link
Owner

This change refactors the macro system to properly support quote/unquote and introduces a new MacroEvaluator trait for evaluating macro bodies during expansion.

Key changes:

  • Add RuntimeValue::Ast variant to represent quoted expressions
  • Introduce MacroEvaluator trait to decouple macro expansion from evaluation
  • Change quote expressions to evaluate to AST values instead of erroring
  • Add InvalidMacroResultAst and InvalidMacroResult error types
  • Refactor macro collection to evaluate macro bodies at definition time
  • Update macro expansion to properly handle AST values
  • Fix tests to use proper quote blocks in macro definitions

This change refactors the macro system to properly support quote/unquote
and introduces a new MacroEvaluator trait for evaluating macro bodies
during expansion.

Key changes:
- Add RuntimeValue::Ast variant to represent quoted expressions
- Introduce MacroEvaluator trait to decouple macro expansion from evaluation
- Change quote expressions to evaluate to AST values instead of erroring
- Add InvalidMacroResultAst and InvalidMacroResult error types
- Refactor macro collection to evaluate macro bodies at definition time
- Update macro expansion to properly handle AST values
- Fix tests to use proper quote blocks in macro definitions
Copilot AI review requested due to automatic review settings December 27, 2025 14:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the macro system to properly support quote/unquote operations by introducing a new RuntimeValue::Ast variant and a MacroEvaluator trait. The changes enable macros to be evaluated at definition time and allow quoted expressions to return AST values instead of erroring.

Key changes:

  • Added RuntimeValue::Ast variant to represent quoted expressions as runtime values
  • Introduced MacroEvaluator trait to decouple macro expansion from evaluation logic
  • Modified quote expressions to return AST values instead of producing errors
  • Updated macro collection to evaluate macro bodies at definition time using the evaluator

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/mq-python/src/value.rs Added handling for RuntimeValue::Ast in Python value conversion
crates/mq-lang/tests/integration_tests.rs Commented out tests for quote expressions at runtime
crates/mq-lang/src/macro_expand.rs Introduced MacroEvaluator trait and refactored macro expansion to use it; updated all expansion methods to accept evaluator parameter
crates/mq-lang/src/eval/runtime_value.rs Added Ast variant to RuntimeValue enum with implementations for equality, ordering, display, and utility methods
crates/mq-lang/src/eval/env.rs Added variable representation for AST runtime values
crates/mq-lang/src/eval.rs Implemented MacroEvaluator trait for Evaluator; changed quote/macro evaluation behavior; added temporary macro_expander swapping to avoid borrow conflicts
crates/mq-lang/src/error/runtime.rs Added InvalidMacroResultAst and InvalidMacroResult error variants
crates/mq-lang/src/error.rs Added diagnostic messages for new macro-related error types

},
);
} else {
unreachable!("Macro body did not evaluate to AST");
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unreachable! will panic if the macro body doesn't evaluate to an AST. Consider returning a proper error instead of using unreachable!, as the MacroEvaluator implementation might have bugs or return unexpected values. Use RuntimeError::InvalidMacroResult or similar.

Suggested change
unreachable!("Macro body did not evaluate to AST");
return Err(RuntimeError::InvalidMacroResult(
"Macro body did not evaluate to AST".to_string(),
));

Copilot uses AI. Check for mistakes.
Comment on lines 174 to 177
// Temporarily take the macro_expander to avoid borrow conflicts
let mut macro_expander = std::mem::take(&mut self.macro_expander);
let expanded_result = macro_expander.expand(&program, self);
self.macro_expander = macro_expander;
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern of temporarily moving macro_expander to avoid borrow conflicts appears three times in this file (lines 175-177, 266-270, 438-440). Consider refactoring this into a helper method to reduce duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +6120 to +6124
Ok(_) | Err(_) => {
// Return the body as AST, not the evaluated result
// This allows macros without quote to work
Ok(RuntimeValue::Ast(Shared::clone(body)))
}
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently converting errors to AST values may hide issues during macro body evaluation. The error case should at least be logged or handled differently from the Ok(_) case to aid debugging.

Copilot uses AI. Check for mistakes.
@codspeed-hq
Copy link

codspeed-hq bot commented Dec 27, 2025

CodSpeed Performance Report

Merging #1027 will degrade performance by 12.86%

Comparing refactor/macro-evaluation-and-ast-support (75bf430) with main (6a5c5a0)

Summary

❌ 1 regression
✅ 28 untouched

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Efficiency
eval_variable_assignment_chain 830.9 µs 953.5 µs -12.86%

Copy link
Contributor

Copilot AI commented Dec 27, 2025

@harehare I've opened a new pull request, #1028, to work on those changes. Once the pull request is ready, I'll request review from you.

harehare and others added 3 commits December 27, 2025 23:30
…method

Co-authored-by: harehare <533078+harehare@users.noreply.github.com>
Refactor duplicated macro_expander take/restore pattern into helper method
Copilot AI review requested due to automatic review settings December 27, 2025 14:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings December 28, 2025 02:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

},
);
} else {
unreachable!("Macro body did not evaluate to AST");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unreachable message is misleading. Based on the code logic at lines 101-116, if eval_macro_body returns a non-Ast value, this line is reached. Consider changing the message to explain that this represents an internal error where the evaluator failed to return an AST value, or convert this to a proper error return instead of unreachable.

Suggested change
unreachable!("Macro body did not evaluate to AST");
unreachable!(
"internal error: macro body evaluation did not return an AST RuntimeValue"
);

Copilot uses AI. Check for mistakes.
Self::NativeFunction(_) => Cow::Borrowed("native_function"),
Self::Dict(_) => self.string(),
Self::Module(module_name) => Cow::Owned(format!(r#"module "{}""#, module_name.name)),
Self::Ast(_) => unreachable!(),
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unreachable is incorrect. The Ast case is already handled in the outer match at lines 247-252, but if that pattern matching logic is ever refactored, this could be reached. Additionally, this makes the code harder to understand. Consider removing the outer match arm for Ast and letting it fall through to the inner match, or add a clear comment explaining why this is truly unreachable.

Copilot uses AI. Check for mistakes.
impl<T: ModuleResolver> MacroEvaluator for Evaluator<T> {
fn eval_macro_body(&mut self, body: &Shared<ast::Node>, _token_id: TokenId) -> Result<RuntimeValue, RuntimeError> {
// Try to evaluate the macro body
let value = self.eval_macro(body);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable value is defined but the error case in the match at line 6139 doesn't use it. The error path returns Ok(RuntimeValue::Ast(Shared::clone(body))) regardless of whether eval_macro returned an error or a non-Ast value. This could hide evaluation errors. Consider handling errors explicitly or documenting why they should be ignored.

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +157
/// Helper method to temporarily take the macro_expander to avoid borrow conflicts.
/// This pattern is needed when the macro_expander needs to call methods that require
/// mutable access to the evaluator (self).
///
/// # Arguments
/// * `f` - A closure that operates on the taken macro_expander and returns a result
///
/// # Example
/// ```ignore
/// self.with_macro_expander(|expander, evaluator| {
/// expander.expand(&program, evaluator)
/// })?;
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation example shows a usage pattern that includes ?; at the end, but the actual function signature returns Result<R, RuntimeError>, so the caller should handle the Result. The example should either show proper error handling or clarify that this is示意代码. Additionally, document why this pattern is necessary (to work around Rust's borrow checker when MacroEvaluator needs &mut self).

Suggested change
/// Helper method to temporarily take the macro_expander to avoid borrow conflicts.
/// This pattern is needed when the macro_expander needs to call methods that require
/// mutable access to the evaluator (self).
///
/// # Arguments
/// * `f` - A closure that operates on the taken macro_expander and returns a result
///
/// # Example
/// ```ignore
/// self.with_macro_expander(|expander, evaluator| {
/// expander.expand(&program, evaluator)
/// })?;
/// Helper method to temporarily take the `macro_expander` to avoid borrow conflicts.
///
/// Rust's borrow checker does not allow holding `&mut self` and, at the same time,
/// a `&mut` borrow of one of its fields (here, `self.macro_expander`) across a call.
/// By `take`‑ing the `macro_expander` out of `self`, this helper lets the closure
/// borrow both `&mut Macro` and `&mut Self` safely for the duration of the call.
///
/// # Arguments
/// * `f` - A closure that operates on the taken `macro_expander` and returns a result.
///
/// # Example
/// In a method that itself returns `Result<R, RuntimeError>` you might write:
///
/// ```ignore
/// fn eval_with_macros(&mut self, program: &Program) -> Result<R, RuntimeError> {
/// self.with_macro_expander(|expander, evaluator| {
/// expander.expand(program, evaluator)
/// })
/// }

Copilot uses AI. Check for mistakes.
Comment on lines +1035 to +1041
/// Mock MacroEvaluator for testing.
/// Returns the program as-is wrapped in RuntimeValue::Ast.
struct MockMacroEvaluator;

impl MacroEvaluator for MockMacroEvaluator {
fn eval_macro_body(&mut self, body: &Shared<Node>, _token_id: TokenId) -> Result<RuntimeValue, RuntimeError> {
// Return the body as-is wrapped in AST
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says 'Returns the program as-is' but the implementation at line 1042-1045 wraps the body's expr in a new Node with a hardcoded token_id. This is not 'as-is'. Update the documentation to accurately reflect what the mock evaluator does.

Suggested change
/// Mock MacroEvaluator for testing.
/// Returns the program as-is wrapped in RuntimeValue::Ast.
struct MockMacroEvaluator;
impl MacroEvaluator for MockMacroEvaluator {
fn eval_macro_body(&mut self, body: &Shared<Node>, _token_id: TokenId) -> Result<RuntimeValue, RuntimeError> {
// Return the body as-is wrapped in AST
/// Mock `MacroEvaluator` for testing.
/// Wraps the macro body's expression in a new `Node` with a fixed `TokenId`
/// and returns it as `RuntimeValue::Ast`.
struct MockMacroEvaluator;
impl MacroEvaluator for MockMacroEvaluator {
fn eval_macro_body(&mut self, body: &Shared<Node>, _token_id: TokenId) -> Result<RuntimeValue, RuntimeError> {
// Wrap the body's expression in a new Node with a fixed TokenId and return it as AST

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings December 28, 2025 13:43
@harehare harehare force-pushed the refactor/macro-evaluation-and-ast-support branch from 6994516 to 8b494b7 Compare December 28, 2025 13:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Self::NativeFunction(_) => Cow::Borrowed("native_function"),
Self::Dict(_) => self.string(),
Self::Module(module_name) => Cow::Owned(format!(r#"module "{}""#, module_name.name)),
Self::Ast(_) => unreachable!(),
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unreachable!() here is inconsistent with the actual handling above at line 248-252 where Ast is explicitly handled. Since Ast formatting is already implemented, this unreachable should never be hit. Consider adding a comment explaining why this branch cannot be reached, or documenting that this is a defensive check.

Suggested change
Self::Ast(_) => unreachable!(),
// SAFETY: This branch is unreachable because `RuntimeValue::Ast`
// is already handled by the outer `match self { Self::Ast(node) => ... }` arm above.
// If this ever triggers, it indicates a bug in the `Display` implementation.
Self::Ast(_) => unreachable!("`RuntimeValue::Ast` should be handled in the outer Display match arm"),

Copilot uses AI. Check for mistakes.
.expand(&program, &mut EmptyBlockMacroEvaluator)
.expect("Failed to expand program");

assert_eq!(macro_expander.macros.len(), 1, "Expected no macros to be registered");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion message says 'Expected no macros to be registered' but the assertion checks that exactly 1 macro is registered (len() == 1). The message should be 'Expected 1 macro to be registered' to match the assertion.

Copilot uses AI. Check for mistakes.
.expand(&program, &mut EmptyBlockMacroEvaluator)
.expect("Failed to expand program");

assert_eq!(macro_expander.macros.len(), 2, "Expected no macros to be registered");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion message says 'Expected no macros to be registered' but the assertion checks that exactly 2 macros are registered (len() == 2). The message should be 'Expected 2 macros to be registered' to match the assertion.

Copilot uses AI. Check for mistakes.
Comment on lines +2543 to +2544
assert_eq!(macro_expander.macros.len(), 2, "Expected no macros to be registered");
assert_eq!(expanded.len(), 0, "Expected two macro definition nodes to remain");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first assertion message says 'Expected no macros to be registered' but checks for 2 macros. The second assertion message says 'Expected two macro definition nodes to remain' but checks for 0 expanded nodes. Both messages are inconsistent with their assertions. They should be 'Expected 2 macros to be registered' and 'Expected no nodes in expanded program' respectively.

Copilot uses AI. Check for mistakes.
Comment on lines +6197 to +6208
// If the result is already an AST (from quote), return it as-is
// If the result is None (e.g., from if(false) without else), return None to indicate removal
// Otherwise, wrap the body itself as an AST (for macros without quote)
match value {
Ok(RuntimeValue::Ast(ast)) => Ok(RuntimeValue::Ast(ast)),
Ok(RuntimeValue::None) => {
// Return None instead of empty block - this will cause the macro to be skipped
Ok(RuntimeValue::None)
}
Ok(_) | Err(_) => {
// Return the body as AST, not the evaluated result
// This allows macros without quote to work
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states that returning None will cause the macro to be skipped, but looking at the macro collection code in macro_expand.rs at lines 101-116, it uses an if-let pattern that would panic with 'unreachable' if None is returned. This suggests the comment may be inaccurate or the implementation is incomplete. Consider clarifying the intended behavior or handling None explicitly in the collection logic.

Suggested change
// If the result is already an AST (from quote), return it as-is
// If the result is None (e.g., from if(false) without else), return None to indicate removal
// Otherwise, wrap the body itself as an AST (for macros without quote)
match value {
Ok(RuntimeValue::Ast(ast)) => Ok(RuntimeValue::Ast(ast)),
Ok(RuntimeValue::None) => {
// Return None instead of empty block - this will cause the macro to be skipped
Ok(RuntimeValue::None)
}
Ok(_) | Err(_) => {
// Return the body as AST, not the evaluated result
// This allows macros without quote to work
// If the result is already an AST (from quote), return it as-is.
// Otherwise, ignore the evaluated value and wrap the body itself as an AST
// so that macros without quote still operate on their original syntax.
match value {
Ok(RuntimeValue::Ast(ast)) => Ok(RuntimeValue::Ast(ast)),
Ok(_) | Err(_) => {
// Return the body as AST, not the evaluated result.
// This allows macros without quote to work and avoids returning None,
// which the macro collector does not expect.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings December 28, 2025 14:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

},
);
} else {
unreachable!("Macro body did not evaluate to AST");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unreachable! macro should include more context about what was received instead of just stating it 'did not evaluate to AST'. Consider including the actual type or value received to aid debugging.

Suggested change
unreachable!("Macro body did not evaluate to AST");
unreachable!(
"Macro body did not evaluate to AST. Got value: {ast:?}"
);

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +153
/// * `f` - A closure that operates on the taken macro_expander and returns a result
///
/// # Example
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation example shows a trailing ? operator but the actual return type from with_macro_expander already handles the Result. The example should either remove the ? or clarify that the closure returns a Result that gets propagated by the method.

Suggested change
/// * `f` - A closure that operates on the taken macro_expander and returns a result
///
/// # Example
/// * `f` - A closure that operates on the taken macro_expander and returns a
/// [`Result`] that is propagated by this method.
///
/// # Example
/// The closure's [`Result`] is propagated, so callers can use `?` at the call site:

Copilot uses AI. Check for mistakes.
match value {
Ok(RuntimeValue::Ast(ast)) => Ok(RuntimeValue::Ast(ast)),
Ok(RuntimeValue::None) => {
// Return None instead of empty block - this will cause the macro to be skipped
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment states that returning None 'will cause the macro to be skipped', but it's unclear what 'skipped' means in this context - does it mean the macro definition is not registered, or that macro calls produce no output? The comment should clarify the specific behavior.

Suggested change
// Return None instead of empty block - this will cause the macro to be skipped
// Return None instead of an empty block: the macro expander will
// treat this as "remove this macro invocation", so the call
// contributes no AST nodes to the expanded output (the macro
// definition itself remains registered as usual).

Copilot uses AI. Check for mistakes.
Comment on lines +2543 to +2544
assert_eq!(macro_expander.macros.len(), 2, "Expected no macros to be registered");
assert_eq!(expanded.len(), 0, "Expected two macro definition nodes to remain");
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 2543's assertion message says 'Expected no macros to be registered' but checks for 2 macros. Line 2544's message says 'Expected two macro definition nodes to remain' but checks for 0 nodes. Both messages contradict their assertions and should be corrected.

Copilot uses AI. Check for mistakes.
@harehare harehare merged commit 4f5010b into main Dec 28, 2025
6 of 7 checks passed
@harehare harehare deleted the refactor/macro-evaluation-and-ast-support branch December 28, 2025 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants