aboutsummaryrefslogtreecommitdiff
path: root/docs/dev/pages/issues/remove-r-print-macro.md
blob: 96f99ef70e3df89e0bff688ad5591539aa66ee27 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<h1 align="center">Remove r_print! and r_println! Macros</h1>

`r_print!` and `r_println!` are important macros in Mingling for use inside `#[help]` and `#[renderer]` functions, but their implementation is not clean: they implicitly introduce a `__renderer_inner_result` field. While this might look elegant at the API level, it is **incorrect** and even **objectionable**.

## Why **Objectionable**?

Because you can't define declarative macros with `macro_rules` that wrap them.

This is because `r_println!` depends on the implicit variable `__renderer_inner_result` injected by the `#[renderer]` proc macro into the function body. However, when a `macro_rules` declarative macro expands, **its internal code is placed in the caller's context**, which does not contain `__renderer_inner_result` — that variable only exists within the direct scope of the function body processed by `#[renderer]`.

Let's look at some code to see why:

```rust
// Suppose you want to write a wrapper macro:
macro_rules! my_println {
    ($($arg:tt)*) => {
        // When expanded here, the context is the call site of my_println!,
        // not the location where the renderer function's injected variables live.
        // So __renderer_inner_result is NOT visible here!
        r_println!("Custom: {}", format!($($arg)*));
    };
}
 
#[renderer]
fn render_something(_p: ResultSomething) {
    // Although this function body has __renderer_inner_result injected,
    // the code from my_println! does NOT expand "inside this function body" —
    // macro_rules expansion is essentially text replacement. The replaced code
    // lives at the line where my_println! is called, and any variables referenced
    // inside that macro must resolve to identifiers accessible at the call site.
    // __renderer_inner_result is not a public, path-accessible variable;
    // it's a hygienic local variable generated by the `#[renderer]` macro,
    // and external macros cannot directly access it by name.
    my_println!("{}", box_val); // Compile error: cannot find __renderer_inner_result
}
```
 
## Deeper Issues

I have to admit, this is an early design flaw. After re-examining the code, I found the problem goes beyond "can't be wrapped".

This isn't just a "can't wrap" issue — it reflects that `r_println!`'s design fundamentally violates Rust's macro hygiene principles:

- **Implicit dependency**: Users of the macro must know that a variable named `__renderer_inner_result` exists — but this variable is neither part of the public API nor explicitly documented anywhere.
- **Scope leakage**: Variables injected by a proc macro should be confined to the scope processed by that macro. But `r_println!` attempts to make that variable accessible across macro calls, which effectively breaks Rust's identifier hygiene.
- **Non-composable**: Any attempt to wrap `r_println!` will fail, because declarative macros cannot "pass through" access to implicit variables. Even using a proc macro to wrap it would encounter similar hygiene issues.

## Desired New Syntax

I've designed two alternative approaches and will choose based on actual needs.

### Option 1: Explicit Return

```rust
#[renderer]
fn render_something(prev: ResultSomething) -> RenderResult {
    let mut result = RenderResult::new();
    result.println(prev.to_string());
    // or
    write!(result, "{}", prev.to_string());
 
    result // return here
}
```
 
Clear boundaries — the entire rendering process is confined within the function body decorated by `#[help]` or `#[renderer]`, without introducing extra out-of-scope dependencies. The trade-off is slightly more boilerplate compared to the original approach.

### Option 2: Resource Injection

```rust
#[renderer]
fn render_something(prev: ResultSomething, result: &mut ResRenderResult) {
    result.println(prev.to_string());
    // or
    write!(result, "{}", prev.to_string());
 
    result // return here
}
```
 
More flexible, but blurs the boundary between logic functions like `#[chain]` and rendering functions like `#[help]`.

### Preferred Direction

I lean toward **Option 1 (Explicit Return)**. There's no need to turn `RenderResult` into `ResRenderResult` as a global resource.

As for rendering in logic functions like `#[chain]`, that should be handled by a separate system — not discussed here.

## 🕘 Progress

- [ ] In Progress
  - [ ] Remove `r_println!` and `r_print!` macros
  - [ ] Modify `#[renderer]` and `#[help]` macros, remove implicit injection
  - [ ] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros
  - [ ] Add new simplified syntax
  - [ ] Update documentation and test cases, ensure **all pass**
- [ ] Complete