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 anextpointer to.bar - The second node represents
.bar {y}with a nullnextreference
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
WalkedFunctionCalllinked lists into nestedFunctionCallNodetrees through recursive refinement. - The
nextproperty inWalkedFunctionCalllinks chain elements during the lexing phase. - Each call is resolved via
context.resolveUnchecked(node)before being wrapped as aFunctionCallArgumentfor the subsequent function. - The transformation turns
.foo::barsyntax into the equivalent ofbar(foo), enabling left-to-right function composition. - Body arguments are preserved as
DynamicValueobjects 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →