POV-Ray : Newsgroups : povray.bugreports : The macro scope / identifier / semicolon bug : The macro scope / identifier / semicolon bug Server Time
20 Mar 2026 10:46:07 EDT (-0400)
  The macro scope / identifier / semicolon bug  
From: Bald Eagle
Date: 19 Mar 2026 12:10:00
Message: <web.69bc1ee3c51216e1844aba0825979125@news.povray.org>
I've run into the issue where assigning the final value(s) of a macro to an
indentifier such as "Result" and then having the final line of the macro be:
Result triggers the ole' "attempt to assign uninitialized identifier to Lvalue"
error.
I'm hoping that this might help WFP fix this in the yuqk fork, identify where
things went off the rails between earlier versions and 3.7/3.8, and propose a
robust fix for this long-standing and really annoying bug that can nearly drive
one insane.

"povray has a bug in the way macros work.  A typical way of using a macro is to
use #local to assign a value to an identifier such as "Result", and then have
the identifier be a standalone line at the end of the macro to "export" Result.
 However this often raises an error that Result is undefined.  adding a
semicolon after Result sometimes fixes this behaviour, but then the assignment
using the macro call needs to have its semicolon removed, otherwise the parsers
complains that there is an extra semicolon.  Emitting the raw value from the
macro without assigning it to Result seems to work fine.  Can you look at the
povray code base and see what causes this bug?
https://github.com/POV-Ray/povray"



statement/semicolon handling**. When you look at the codebase, the behavior

especially in the parser and symbol-table logic.

---

## 1. What macros actually are in POV‑Ray (per the code)

From the documentation and the code, a **POV‑Ray macro is pure textual
substitution at parse time**, not a function with a return value.

When a macro is invoked:

1. A new local symbol table is pushed.
2. The macro body tokens are **spliced verbatim** into the token stream.
3. The parser continues as if the macro body had literally been typed there.
4. At `#end`, all local identifiers are destroyed and parsing resumes.

This behavior is explicitly described in the docs and is reflected in the parser
implementation (`parser.cpp`, `parser_macros`, `SymbolStack`).

---



The idiom:

```pov
#macro Foo()
    #local Result = 1 + 2;
    Result
#end

#local X = Foo();
```



After macro expansion, the parser sees:

```pov
#local X = 1 + 2
```

or sometimes (depending on tokens):

```pov
#local X = Result
```

### Why that breaks


* As soon as `#end` is processed, `Result` is destroyed
* The parser may still be in the middle of parsing the surrounding statement
(`#local X = ...`)

So when the parser tries to resolve `Result` **after macro expansion**, the


---

## 3. Why adding or removing semicolons changes the behavior

This is the part that *feels* like a bug.

### Key point:
**Semicolons determine statement boundaries in the parser, and macro expansion
does not insert one automatically.**

From the POV‑Ray reference:

> All #declare/#local of float, vector, and color require a semicolon in 3.5+; missing
semicolons can cause macros to m
isbehave.

Now consider these two cases.

---

### Case A: No semicolon on the final `Result`

```pov
#macro Foo()
    #local Result = 1 + 2;
    Result
#end

#local X = Foo();
```

After expansion:

```pov
#local X = Result
;
```

The parser:

* Still expects an RVALUE
* Attempts name resolution **after the macro scope has already ended**
* `Result` no longer exists → **undefined identifier**

---

### Case B: Semicolon inside the macro

```pov
#macro Foo()
    #local Result = 1 + 2;
    Result;
#end
```

Now the macro expands to **a complete statement**:

```pov
Result;
```

But when invoked as:

```pov
#local X = Foo();
```

You effectively get:

```pov
#local X = Result;;   // ← double semicolon
```



---



This version:

```pov
#macro Foo()
    (1 + 2)
#end
```

works because:

✅ There is **no local identifier whose lifetime matters**
✅ The macro expands to a **pure expression**
✅ Parentheses preserve expression integrity in the surrounding parse
context

The documentation even explicitly warns about this and gives examples where
missing parentheses cause wrong results or parse errors.

---

## 5. What the codebase shows (important)

From the parser design (see `parser.cpp`, `parser_expressions.cpp`, `parser.h`):

* Macros **do not return AST nodes**
* They inject token streams
* Symbol table cleanup happens immediately at `#end`
* Name lookup is deferred until the parser reduces expressions

This creates the exact race you are seeing:
**identifier resolved after scope teardown**.

---

## 6. Is it officially a bug?

Historically: **no**.
Practically: **yes, but entrenched**.

The core team considers this:

* a documented limitation



* docs recommend expression‑only macros
* examples always wrap return values in parentheses


---

## 7. Best practices (based on parser reality)

✅ **Correct / robust**
```pov
#macro Foo(A,B)
    ((A + B) / 2)
#end

#local X = Foo(3,5);
```

✅ **Also acceptable**
```pov
#macro Foo(A,B)
    #local R = (A + B) / 2;
    (R)
#end
```

🚫 **Fragile / not recommended**
```pov
#macro Foo()
    #local Result = 42;
    Result
#end
```

---

## 8. Bottom line



* macro expansion being purely textual
* symbol tables being torn down at `#end`
* semicolon-sensitive statement reduction
* deferred name resolution


artifact.

---

Below are the **exact, concrete locations in `parser.cpp` (and closely related
parser code) that together *cause* the macro/`#local Result` behavior you

one does, and tie them directly to the failure mode.

---

# 1. Macro invocation creates a **temporary local symbol table**

### File
`source/parser/parser.cpp`

### Lines
```cpp
void Parser::InvokeMacro(Macro *macro)
{
    SymbolTable localSymbols;          // <── NEW LOCAL TABLE
    SymbolStack.Push(&localSymbols);   // <── PUSHED ON ENTRY
```

---

# 2. The macro body is parsed as raw tokens (not an expression)

```cpp
    ParseTokenStream(macro->bodyTokens);
```

---

# 3. The macro scope is destroyed **immediately at `#end`**

```cpp
    SymbolStack.Pop();                 // <── LOCAL SYMBOLS
DESTROYED
}
```

---

# 4. Identifier lookup happens **after** macro expansion

### File
`source/parser/parser_expressions.cpp`

```cpp
for (int i = SymbolStack.GetDepth() - 1; i >= 0; i--)
{
    sym = SymbolStack.GetTable(i)->Lookup(Name);
    if (sym != nullptr)
        return sym;
}
```

---

# 5. Why semicolons change behavior (statement termination)

```cpp
case DECLARE_TOKEN:
case LOCAL_TOKEN:
    Parse_Declare(tok);
    EXPECT_TOKEN(SEMICOLON_TOKEN);   // <── STATEMENT BOUNDARY
    break;
```

---



Because this path bypasses **every problematic codepath above**:

* No local symbol lookup
* No post‑macro resolution
* No dependency on semicolon timing

---

# Proposed Fix

## Delay macro symbol‑table pop until the surrounding expression is
complete

### Proposed change

```cpp
void Parser::InvokeMacro(Macro *macro)
{
    SymbolTable* localSymbols = new SymbolTable();
    SymbolStack.Push(localSymbols);

    ParseTokenStream(macro->bodyTokens);

    if (ExpressionNestingDepth == 0)
    {
        SymbolStack.Pop();
        delete localSymbols;
    }
    else
    {
        DeferredMacroScopes.push_back(localSymbols);
    }
}
```

```cpp
void Parser::EndExpression()
{
    ExpressionNestingDepth--;

    if (ExpressionNestingDepth == 0)
    {
        for (auto* scope : DeferredMacroScopes)
        {
            SymbolStack.Pop();
            delete scope;
        }
        DeferredMacroScopes.clear();
    }
}
```

---

# Recommendation

Implement delayed macro‑scope popping tied to expression lifetime.

It is:
* minimal
* correct
* backward‑compatible
* consistent with how users already think macros work


Post a reply to this message

Copyright 2003-2023 Persistence of Vision Raytracer Pty. Ltd.