# How to Debug Function Call Resolution Failures in Quarkdown's Compiler

> Debug Quarkdown compiler function call resolution failures. Trace the four-stage pipeline from FunctionCallRefiner to UncheckedFunctionCall. Ensure function names match @Name and libraries are registered.

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

---

**To debug function call resolution failures in Quarkdown, trace the four-stage pipeline from `FunctionCallRefiner` through `BaseContext.resolve` to `UncheckedFunctionCall`, verifying that the function name matches the `@Name` annotation and that the containing library is registered in the `Context`.**

When the Quarkdown compiler throws an `UnresolvedReferenceException`, the failure occurs within a specific resolution pipeline that transforms parsed syntax into executable function calls. Understanding how to debug function call resolution failures in Quarkdown requires familiarity with the internal stages that resolve a `FunctionCallNode` into a concrete `FunctionCall`. This guide walks through the pipeline architecture and provides actionable debugging steps using the actual source code from the `iamgio/quarkdown` repository.

## The Four Stages of Resolution

The compiler processes function calls through a strict pipeline. A failure at any stage produces the "unresolved reference" error. The stages are:

- **Parsing → Refinement**: Converts a syntactic `WalkedFunctionCall` into a context-aware `FunctionCallNode` in [`FunctionCallRefiner.kt`](https://github.com/iamgio/quarkdown/blob/main/FunctionCallRefiner.kt) (lines 30–90).
- **Resolution**: Looks up the function name in the current `Context`’s libraries via `BaseContext.resolve` (lines 89–100).
- **Unchecked Delegate**: Wraps the optional result in an `UncheckedFunctionCall` that throws `UnresolvedReferenceException` if the lookup returns `null`, implemented in `BaseContext.resolveUnchecked` (lines 102–103) and [`UncheckedFunctionCall.kt`](https://github.com/iamgio/quarkdown/blob/main/UncheckedFunctionCall.kt) (lines 16–34).
- **Execution & Mapping**: Expands the call to an AST node in `FunctionCallNodeExpander.expand` (lines 31–48).

If any step cannot locate the function, the compiler throws `UnresolvedReferenceException` as defined in [`UnresolvedReferenceException.kt`](https://github.com/iamgio/quarkdown/blob/main/UnresolvedReferenceException.kt).

## Debugging Checklist

### Verify the Function Name Against `@Name` Annotations

The name the parser sees is the literal identifier used for lookup. In `FunctionCallRefiner.toNode()`, the node is created with:

```kotlin
val node = FunctionCallNode(context, call.name, …)

```

Ensure your Quarkdown source uses the exact name defined in the function's annotation. The `@Name` annotation determines the Quarkdown-visible identifier; if omitted, the Kotlin function name is used.

```kotlin
@Name("myfunction")
fun myFunction(...): StringValue { … }

```

### Confirm Library Registration in the Context

Libraries must be registered in the `Context` before parsing begins. The lookup sequence in `BaseContext` iterates through `Context.libraries`:

```kotlin
override fun getFunctionByName(name: String): Function<*>? =
    libraries.asSequence()
            .flatMap { it.functions }
            .find { it.name == name }

```

Check your `PipelineOptions` configuration to ensure the library containing your function is included in the `libraries` set. Custom libraries must be added to the pipeline before execution starts.

### Inspect the Resolution Call with Logging

To pinpoint where resolution fails, insert temporary logging or breakpoints in `FunctionCallNodeExpander.expand`:

```kotlin
val call = node.context.resolveUnchecked(node)   // ← this line
println("Resolving '${node.name}' → ${call.javaClass.simpleName}")

```

If the `UnresolvedReferenceException` is thrown, this log will not appear, confirming the failure occurs during the `resolveUnchecked` call in [`BaseContext.kt`](https://github.com/iamgio/quarkdown/blob/main/BaseContext.kt).

### Validate Chained Call Logic

Chained calls such as `.a {x}::b {y}` resolve the inner call first and pass it as an argument to the outer function. In `FunctionCallRefiner.toNode()`:

```kotlin
val call: UncheckedFunctionCall<*> = context.resolveUnchecked(node) // A
val initialArguments = listOf(FunctionCallArgument(call))      // A as argument for B

```

If the inner call `a` fails resolution, the outer call `b` never executes. Test inner calls in isolation to identify which link in the chain is broken.

### Handle Body Argument Reparsing

Functions accepting body arguments receive `DynamicValue` that is **not** automatically re-lexed. If nested calls inside body content remain unevaluated, explicitly re-parse the content:

```kotlin
@Name("wrap")
fun wrap(@LikelyBody content: DynamicValue, @Injected context: Context): OutputValue<*> {
    val parsed = ValueFactory.blockMarkdown(content.value, context)
    return Paragraph(parsed).wrappedAsValue()
}

```

Without this explicit re-lexing using `ValueFactory.blockMarkdown`, inner function calls like `.bold {Important}` inside the body will not expand.

## Common Pitfalls

| Symptom | Likely Cause | Fix |
|---------|--------------|-----|
| "Unresolved function `foo`" though `foo` is defined in a custom library | Library not added to `Context.libraries` | Register the library when building the pipeline: `pipelineOptions.libraries += myLib` |
| Function works in inline form but fails in body form | Body arguments are `DynamicValue` not automatically re-lexed | Use `ValueFactory.blockMarkdown(body, context)` inside the function implementation |
| Chained call `.a::b` fails with "Unresolved function `a`" | The inner call `a` is missing or incorrectly named | Test `.a` alone to verify its registration |
| No error message, but content renders as plain text | Function resolved but returned an empty `DynamicValue` | Ensure the function returns a proper `OutputValue` (e.g., `Paragraph(...).wrappedAsValue()`) |

## Practical Debugging Examples

### Reproducing an Unresolved Reference

Create a file `example.qd`:

```markdown
.doNotExist {42}

```

Running the compiler yields:

```

Error: Unresolved function: doNotExist

```

This occurs because `doNotExist` is not present in any loaded library, causing `BaseContext.resolve` to return `null`, which triggers the `UnresolvedReferenceException` in `UncheckedFunctionCall`.

### Registering a Custom Function

Define a library in [`MyLib.kt`](https://github.com/iamgio/quarkdown/blob/main/MyLib.kt):

```kotlin
package com.example

import com.quarkdown.core.function.Function
import com.quarkdown.core.function.output.StringValue
import com.quarkdown.core.function.annotation.Name

val MyLib = Library("my-lib") {
    function(::hello)
}

@Name("hello")
fun hello(@Name("name") name: String): StringValue =
    StringValue("Hello, $name!")

```

Register it in the pipeline:

```kotlin
val pipeline = Pipeline.builder()
    .libraries(setOf(Stdlib, MyLib))
    .build()

```

Now `.hello {World}` renders correctly. If `MyLib` is omitted from the `libraries` set, the compiler throws `UnresolvedReferenceException` for "hello".

### Debugging Chained Calls

For the chain `.foo {123}::bar {456}`, add logging inside `FunctionCallRefiner.toNode()`:

```kotlin
println("Refining '${call.name}' – inner call resolved? ${context.resolveUnchecked(node)}")

```

When the inner call cannot resolve, the log shows `null`, indicating that `foo` is not registered in [`Stdlib.kt`](https://github.com/iamgio/quarkdown/blob/main/Stdlib.kt) or your custom library.

### Handling Body Content

To ensure nested calls inside body arguments expand:

```kotlin
@Name("wrap")
fun wrap(@LikelyBody content: DynamicValue, @Injected context: Context): OutputValue<*> {
    val parsed = ValueFactory.blockMarkdown(content.value, context)
    return Paragraph(parsed).wrappedAsValue()
}

```

This allows `.wrap` followed by indented content containing `.bold {Text}` to properly evaluate the inner bold function.

## Summary

- **Function resolution** in Quarkdown follows a four-stage pipeline: refinement, resolution, unchecked delegation, and expansion.
- **`BaseContext.resolve`** searches all registered `Library` instances for a function matching the `@Name` annotation or Kotlin method name.
- **`UnresolvedReferenceException`** is thrown by `UncheckedFunctionCall` when `BaseContext.resolve` returns `null`.
- **Chained calls** resolve inner-to-outer; failure in the inner call prevents the outer call from executing.
- **Body arguments** require explicit re-parsing via `ValueFactory.blockMarkdown` to evaluate nested function calls.

## Frequently Asked Questions

### What causes an UnresolvedReferenceException in Quarkdown?

An `UnresolvedReferenceException` occurs when `BaseContext.resolve` cannot find a function matching the requested name in any loaded library. This happens if the function name is misspelled, the `@Name` annotation differs from the call site, or the library containing the function was not added to the pipeline's `Context.libraries` set before parsing began.

### How do chained function calls resolve in Quarkdown?

Chained calls like `.a {x}::b {y}` resolve from left to right. The compiler first resolves `a` using `FunctionCallRefiner.toNode()`, then passes the result as the first argument to `b` via `context.resolveUnchecked`. If `a` is not found, the exception is thrown before `b` is attempted, making it necessary to verify the innermost call first when debugging chains.

### Why is my custom library function not found despite being defined?

Custom library functions require explicit registration in the pipeline. Even if the Kotlin code compiles, the `Library` instance must be added to the `libraries` set in `PipelineOptions` before the pipeline executes. The lookup logic in `BaseContext` only searches libraries present in this set at pipeline initialization.

### How do I debug body arguments that contain unevaluated function calls?

Body content arrives as `DynamicValue` and is not automatically re-parsed. To debug, ensure your function implementation uses `ValueFactory.blockMarkdown(content.value, context)` to re-lex the body content, allowing the `FunctionCallNodeExpander` to process nested calls. Without this step, function calls inside body arguments remain as literal text.