# How DynamicValue Lazy Evaluation Works for Body Arguments in Quarkdown

> Discover how Quarkdown uses DynamicValue lazy evaluation for body arguments. Learn how raw text is stored and Markdown AST is materialized only when needed, boosting performance.

- Repository: [Giorgio Garofalo/quarkdown](https://github.com/iamgio/quarkdown)
- Tags: internals
- Published: 2026-04-29

---

**Quarkdown defers parsing of body arguments by wrapping them in a `DynamicValue` that stores the raw text and caller's evaluation context, only materialising the Markdown AST when a function explicitly consumes the content.**

In the Quarkdown markup language ([iamgio/quarkdown](https://github.com/iamgio/quarkdown)), body arguments—the indented blocks following function calls—use **DynamicValue lazy evaluation** to balance performance and lexical scoping. Rather than parsing these blocks immediately, the compiler stores the raw text alongside its original evaluation context. This ensures that variables defined outside a lambda remain accessible when the body finally executes, while avoiding the cost of parsing unused content.

## Stage 1: Extraction in FunctionCallRefiner

The lazy evaluation process begins when the parser detects a trailing indented block. In [`quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/FunctionCallRefiner.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/FunctionCallRefiner.kt) (lines 59-68), the `extractArguments()` method creates a plain-text `DynamicValue` without triggering the Markdown parser.

```kotlin
// Inside FunctionCallRefiner.extractArguments()
call.bodyArgument?.takeUnless { it.value.isBlank() }?.value?.let { body ->
    // Body is plain, indented text – store it lazily
    val value = DynamicValue(body)          // <- no parsing yet
    arguments += FunctionCallArgument(value, isBody = true)
}

```

At this stage, the `DynamicValue` holds only the raw string and carries **no context**. The argument is marked with `isBody = true` to distinguish it from standard arguments.

## Stage 2: Propagation via Lambda.invokeDynamic

When a lambda receives a body argument, it must preserve the caller's variable scope. Inside [`quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/data/Lambda.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/data/Lambda.kt) (lines 74-78), the `invokeDynamic` method forks a new context that inherits the caller's libraries and attaches this context to the `DynamicValue`.

```kotlin
// Inside Lambda.invokeDynamic(...)
val context = parentContext.fork()               // Fresh fork
context.libraries += createLambdaParametersLibrary(actualArguments) // Lambda params
if (callingContext is MutableContext) {
    context.libraries += callingContext.libraries   // Bring caller's symbols
}
return action(actualArguments, context)          // Body will now see the right scope

```

Specifically, the `createLambdaParametersLibrary` step initializes the `DynamicValue` with:

```kotlin
DynamicValue(argument.unwrappedValue, evaluationContext = call.context)

```

This **forked evaluation context** ensures that when the body eventually parses, it can resolve references to variables defined outside the lambda.

## Stage 3: Materialisation in NodeOutputValueVisitor

The final materialisation occurs when a function actually consumes the body content. In [`quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/output/node/NodeOutputValueVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/output/node/NodeOutputValueVisitor.kt) (lines 81-92), the visitor detects a `DynamicValue` and lazily parses the raw string using the stored `evaluationContext`.

```kotlin
override fun visit(value: DynamicValue): Node =
    when (value.unwrappedValue) {
        is OutputValue<*> -> value.unwrappedValue.accept(this)
        is Iterable<*>   -> GeneralCollectionValue(value.unwrappedValue as Iterable<OutputValue<*>>).accept(this)
        is Node          -> value.unwrappedValue
        else -> this.visit(parseRaw(value.unwrappedValue.toString(),
                                  value.evaluationContext)) // <- Lazy parse happens here
    }

```

The `parseRaw` method delegates to `ValueFactory.blockMarkdown`, which tokenises and parses the content using the preserved context. If `evaluationContext` is null, the visitor falls back to its own context, but body arguments typically retain the caller's context, guaranteeing correct variable resolution.

## Practical Examples

### Example 1: Basic Body Argument Resolution

```markdown
.doctype {plain}
.title {Demo}
.container
    .function {arg}
        .bold {arg}

```

Here, the `.function` call creates a `Lambda` with a body containing `.bold {arg}`. The body is stored as a `DynamicValue` during extraction. When invoked, the lambda's forked context contains the parameter `arg`, allowing the body to parse and resolve the reference correctly.

### Example 2: Lexical Scoping with Outer Variables

```markdown
.doctype {plain}
.title {Scope test}
.var x: 42               // Variable defined in outer scope
.myfunc
    .text {x}            // Body refers to `x`

```

The `.myfunc` lambda receives the indented block as a `DynamicValue`. During propagation, `Lambda.invokeDynamic` copies the caller's libraries (including variable `x`) into the forked context. When materialisation occurs, `x` resolves to **42** despite the body being written inside the lambda.

### Example 3: Nested Function Calls and Lazy Expansion

```markdown
.doctype {plain}
.title {Lazy}
.list
    .item { .bold {Hello} }   // Nested call inside body

```

The `.list` body contains a nested `.bold` call. Because the body is a `DynamicValue`, the inner call is not executed during initial extraction. Only when a consumer (such as `.list` itself) processes the body does `NodeOutputValueVisitor` trigger parsing, expand `.bold`, and produce the final AST.

## Key Implementation Files

- **[`FunctionCallRefiner.kt`](https://github.com/iamgio/quarkdown/blob/main/FunctionCallRefiner.kt)** – Detects body arguments and wraps them in `DynamicValue` without parsing ([source](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/FunctionCallRefiner.kt)).
- **[`Lambda.kt`](https://github.com/iamgio/quarkdown/blob/main/Lambda.kt)** – Forks execution contexts and attaches the caller's libraries to body arguments ([source](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/data/Lambda.kt)).
- **[`NodeOutputValueVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/NodeOutputValueVisitor.kt)** – Consumes `DynamicValue` outputs and triggers lazy parsing with stored contexts ([source](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/output/node/NodeOutputValueVisitor.kt)).
- **[`DynamicValue.kt`](https://github.com/iamgio/quarkdown/blob/main/DynamicValue.kt)** – Defines the generic wrapper that carries optional `evaluationContext` ([source](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/DynamicValue.kt)).
- **[`ValueFactory.kt`](https://github.com/iamgio/quarkdown/blob/main/ValueFactory.kt)** – Provides `blockMarkdown` and `inlineMarkdown` helpers for materialising raw strings into AST nodes.

## Summary

- **DynamicValue lazy evaluation** defers parsing of body arguments until consumption, improving compiler performance by avoiding work on unused blocks.
- The **extraction stage** wraps raw body text in a `DynamicValue` without context or parsing, occurring in `FunctionCallRefiner`.
- The **propagation stage** forks the caller's context and attaches it to the `DynamicValue` inside `Lambda.invokeDynamic`, ensuring lexical scoping.
- The **materialisation stage** parses the raw content using the stored context when `NodeOutputValueVisitor` processes the value.
- This architecture allows body arguments to reference variables from their definition scope while maintaining efficient compilation.

## Frequently Asked Questions

### When does a body argument get parsed in Quarkdown?

A body argument is parsed only when a function or visitor explicitly consumes the `DynamicValue` as Markdown content. This typically happens in `NodeOutputValueVisitor` when it detects a raw string inside a `DynamicValue` and calls `parseRaw` with the stored `evaluationContext`. Until this materialisation step, the content remains a plain string.

### Why does DynamicValue need to carry an evaluationContext?

The `evaluationContext` preserves the lexical scope from where the body argument was defined. Without it, variables or functions referenced inside the body would resolve against the wrong scope when parsing finally occurs. By forking the caller's context during `Lambda.invokeDynamic`, the `DynamicValue` ensures that outer variables remain accessible even when the body executes inside a different lambda.

### Can DynamicValue store non-string content?

Yes. While body arguments typically store raw Markdown strings, `DynamicValue` is a generic wrapper that can hold any `OutputValue`. When `NodeOutputValueVisitor` encounters a `DynamicValue`, it checks the unwrapped type—if it is already an `OutputValue`, `Iterable`, or `Node`, it processes it directly. Only raw strings trigger the lazy Markdown parsing via `ValueFactory.blockMarkdown`.

### What happens if evaluationContext is null during materialisation?

If the `evaluationContext` property is null when `NodeOutputValueVisitor` processes the `DynamicValue`, the visitor falls back to its own current context. However, in standard body argument flows, the context is populated during the propagation stage in `Lambda.invokeDynamic`, ensuring proper variable resolution. Null contexts typically occur only when `DynamicValue` is created manually outside the standard function call refinement pipeline.