How FunctionCallRefiner Handles Chaining in Quarkdown

FunctionCallRefiner transforms linear function call chains into nested AST trees by recursively wrapping each resolved call as the initial argument of the subsequent function, converting syntax like .foo {x}::bar {y} into the equivalent of bar(foo(x), y).

The Quarkdown markup language enables powerful functional programming patterns through its unique :: chaining syntax, allowing developers to pipeline operations without nested parentheses. In the iamgio/quarkdown repository, the FunctionCallRefiner class—located in quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/FunctionCallRefiner.kt—handles the critical transformation of these linear chains into a tree structure that the compiler can evaluate.

The Two-Stage Parsing Architecture

Quarkdown processes function chains through a distinct two-phase pipeline that separates context-free lexing from semantic refinement.

Stage 1: Lexing into Linked Lists

During the initial lexing phase, the FunctionCallWalkerParser constructs a linked list of WalkedFunctionCall objects defined in quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/walker/funcall/WalkedFunctionCall.kt. Each node contains the function name, its arguments, an optional body argument, and a next reference pointing to the following call.

For the expression .foo {x}::bar {y}, the parser creates a structure where:

  • The first node represents .foo {x} with a next pointer to .bar
  • The second node represents .bar {y} with a null next reference

This linear representation preserves the textual order but lacks the nested semantic relationship required for execution.

Stage 2: Refinement into AST Trees

The FunctionCallRefiner consumes this linked list and produces a nested tree of FunctionCallNode objects (defined in quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/quarkdown/FunctionCallNode.kt). Rather than maintaining the flat structure, it recursively nests each call as an argument to the next, effectively transforming .foo {x}::bar {y} into the semantic equivalent of .bar {.foo {x}} {y}.

Inside the Refinement Process

The FunctionCallRefiner.toNode() method implements the chaining logic through recursive descent and argument wrapping.

First, it instantiates the node for the current call:

val node = FunctionCallNode(context, call.name, extractArguments(),
                             isBlock, sourceText, sourceRange)

When the current WalkedFunctionCall has a next reference, the refiner resolves the current node into an unchecked call:

val call: UncheckedFunctionCall<*> = context.resolveUnchecked(node)

It then wraps this resolved call as a FunctionCallArgument to become the first argument of the subsequent function:

val initialArguments = listOf(FunctionCallArgument(call))

Finally, it recursively instantiates a new FunctionCallRefiner for the next element in the chain, passing the wrapped arguments:

val refiner = FunctionCallRefiner(
    context, next, isBlock, sourceText, sourceRange, initialArguments)
return refiner.toNode()

This recursive pattern continues until the chain ends, at which point the constructed FunctionCallNode propagates back up the call stack, resulting in properly nested function calls.

Practical Examples of Function Chaining

Simple Two-Function Pipeline

.doSomething {value}
::uppercase

This generates an AST equivalent to uppercase(doSomething("value")). The FunctionCallRefiner wraps the .doSomething call as the first argument of .uppercase, ensuring the operations execute in the correct sequence.

Deeply Nested Chains

.format {text}
::trim
::lowercase

The refinement process recursively nests each call, producing lowercase(trim(format("text"))). Each :: operator triggers a new level of nesting in the FunctionCallNode tree, with each subsequent function receiving the previous entire expression as its initial argument.

Chaining with Body Arguments

.list
    - Item 1
    - Item 2
::ordered

Here, the body content (the bullet list) becomes a DynamicValue argument to .list. The FunctionCallRefiner then wraps the entire resolved .list call—including its body content—as the first argument to .ordered, resulting in the semantic structure ordered(list("- Item 1\n- Item 2")).

Summary

  • FunctionCallRefiner converts linear WalkedFunctionCall linked lists into nested FunctionCallNode trees through recursive refinement.
  • The next property in WalkedFunctionCall links chain elements during the lexing phase.
  • Each call is resolved via context.resolveUnchecked(node) before being wrapped as a FunctionCallArgument for the subsequent function.
  • The transformation turns .foo::bar syntax into the equivalent of bar(foo), enabling left-to-right function composition.
  • Body arguments are preserved as DynamicValue objects and participate normally in the nesting process.

Frequently Asked Questions

What is the difference between WalkedFunctionCall and FunctionCallNode?

WalkedFunctionCall is a context-free, temporary structure created during lexing that links calls via a next property in quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/walker/funcall/WalkedFunctionCall.kt. FunctionCallNode is the context-aware AST node produced after refinement in quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/quarkdown/FunctionCallNode.kt, where chains are unwrapped into nested function calls with proper argument resolution.

Where does the actual chaining logic live in the source code?

The core transformation logic resides in quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/FunctionCallRefiner.kt. The class's primary method processes the WalkedFunctionCall linked list and constructs the nested tree structure through the recursive algorithm described above.

How does the refiner handle errors in chained calls?

Each call is resolved via context.resolveUnchecked(node) before being wrapped as an argument, ensuring that invalid function names or argument mismatches are caught during the refinement phase rather than at execution time. This allows Quarkdown to provide early error detection for undefined functions in a chain.

Can chaining mix inline arguments and body arguments?

Yes, the refinement process treats body arguments as standard FunctionCallArgument instances. When a call with a body (like .list with bullet items) chains into another function via ::, the entire resolved call—including its body content—wraps as the first argument of the subsequent function, maintaining the pipeline semantics.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →