How Quarkdown Function Declaration Works with Lambda Parameters
Quarkdown compiles .function blocks into native SimpleFunction objects that execute Lambda bodies by binding arguments as callable functions in a temporary __lambda-parameters__ library, enabling both explicit named parameters and implicit positional access.
The iamgio/quarkdown repository implements a sophisticated macro system where Markdown documents define reusable procedures through the .function declaration. Understanding how Quarkdown function declaration lambda parameters operate requires examining the compiler's three-stage transformation from syntax parsing to runtime execution, as implemented in the Kotlin source of the stdlib and core modules.
Stage 1: Declaration and Parameter Extraction
When the parser encounters a .function block, the declaration process begins in Flow.function within quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Flow.kt. Lines 99-101 extract explicit parameters from the lambda body—those defined with syntax like a b: at the start of the function block.
These parameters are read from Lambda.explicitParameters and converted into FunctionParameter instances of type DynamicValue. The @LikelyBody annotation on the body parameter signals to the compiler that the content should be parsed as a Lambda object rather than plain text.
At this stage, the system distinguishes between:
- Explicit parameters: Named arguments declared at the function head (e.g.,
a b:) - Implicit parameters: Positional arguments accessed via
.1,.2, etc., when no explicit names are provided
Stage 2: Runtime Context Creation and Argument Binding
When a custom function is invoked, the stored Lambda executes through Lambda.invokeDynamic in quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/data/Lambda.kt. Lines 42-56 in the createLambdaParametersLibrary method handle the critical step of making arguments available inside the function body.
The runtime creates a forked context—a new isolated scope that inherits from the calling environment. It then registers arguments as zero-argument functions within a temporary library named __lambda-parameters__. This architectural choice means that inside the function body, parameters are accessed as regular function calls like .a or .b rather than simple variables.
The key operations in this stage include:
- Creating the lambda-parameter library via
createLambdaParametersLibrary - Mapping explicit parameter names to their argument values
- Auto-generating positional accessors (
.1,.2) whenexplicitParameters.isEmpty() - Preparing the context for destructuring if the argument count and type allow it
Stage 3: Execution and Argument Resolution
The final stage occurs within Lambda.invokeDynamic (lines 84-95 and 100-115), where the system validates inputs and executes the lambda body. This method performs several critical validation steps:
- Argument count validation: Checks if the provided arguments match the expected parameters, filling missing optional arguments with
NoneValue - Destructuring: If enabled and applicable, splits dictionary or pair arguments into multiple explicit parameters (lines 90-94)
- Library propagation: Merges the calling context's libraries into the forked context after registering lambda parameters, ensuring outer variables remain accessible but can be shadowed by parameters
The lambda's action then runs within this prepared context, returning an OutputValue that the pipeline treats as a dynamic value for further processing or rendering.
Working with Lambda Parameter Types
Explicit Parameters
Explicit parameters provide named access to arguments. When you declare:
.function {add}
a b:
.a::plus { .b }
The Flow.function parser identifies a and b as explicit parameters. During invocation of .add {2} {3}, invokeDynamic registers .a → 2 and .b → 3 in the lambda context, allowing the body to reference them as callable functions.
Implicit Parameters
For functions without explicit parameter declarations, Quarkdown automatically creates positional accessors:
.function {double}
.1:
.1::multiply by:{2}
Here, explicitParameters.isEmpty() triggers createLambdaParametersLibrary (line 48) to generate .1, .2, etc., based on argument position. Calling .double {5} binds 5 to .1.
Optional Parameters
Parameters declared with a ? suffix are optional. The system marks the LambdaParameter with isOptional = true, and invokeDynamic (lines 107-110) automatically supplies NoneValue for missing arguments:
.function {greet}
name?:
**Hello .name::otherwise {World}**
If called without arguments, .name resolves to none, and .name::otherwise {World} yields "World".
Advanced Features: Destructuring and Scope Propagation
Argument Destructuring
Quarkdown supports destructuring single arguments into multiple parameters when the argument implements Destructurable:
.function {show}
key value:
.key → .value
When called as .show { {k: "foo", v: "bar"} }, invokeDynamic recognizes the dictionary can be destructured into two parts to satisfy the two explicit parameters (lines 90-94).
Outer Scope Access
The context forking mechanism preserves access to outer variables. Consider:
.var {prefix} {Mr.}
.function {fullname}
name:
.prefix .name
The callingContext.libraries are copied into the lambda's context after parameter registration (lines 26-32 in the initialization flow), ensuring .prefix resolves correctly while allowing the parameter .name to shadow any conflicting outer definitions.
Summary
- Declaration Phase:
Flow.functioninFlow.ktparses.functionbodies asLambdaobjects, extracting explicit parameters fromLambda.explicitParametersand creatingFunctionParameterdefinitions. - Runtime Binding:
Lambda.invokeDynamiccreates a forked execution context andcreateLambdaParametersLibraryregisters arguments as callable functions in the__lambda-parameters__library. - Access Patterns: Parameters are accessible as explicit names (
.a,.b) or implicit positions (.1,.2), with optional parameters defaulting tononewhen omitted. - Scope Management: The runtime propagates calling context libraries while ensuring parameters shadow outer variables, and supports destructuring complex arguments into multiple parameters.
Frequently Asked Questions
How do explicit and implicit lambda parameters differ in Quarkdown?
Explicit parameters are declared with names at the start of a .function block (e.g., a b:) and accessed within the body as .a and .b. Implicit parameters are used when no explicit declaration exists; the system automatically creates positional accessors .1, .2, etc., corresponding to the argument order. Both types are implemented via the createLambdaParametersLibrary method in Lambda.kt, but explicit parameters are extracted during parsing in Flow.kt (lines 99-101) while implicit ones are generated at runtime (line 48).
What happens when a Quarkdown function is called with missing optional arguments?
When a parameter is declared with the ? suffix (making it optional), the Lambda.invokeDynamic method detects the missing argument during the validation phase (lines 107-110) and automatically fills it with NoneValue. Inside the function body, this none value can be handled using functions like .name::otherwise {default} to provide fallback behavior, or it will propagate as none through subsequent operations.
Can Quarkdown functions access variables defined in outer scopes?
Yes. When a lambda executes, invokeDynamic copies the callingContext.libraries into the new forked context after registering the lambda's own parameters. This propagation occurs in the initialization logic (referenced in lines 26-32 of the context setup), ensuring that variables from enclosing scopes remain visible. However, function parameters take precedence and will shadow any outer variables with conflicting names due to the library merge order.
How does parameter destructuring work in Quarkdown function declarations?
Destructuring occurs in Lambda.invokeDynamic (lines 90-94) when a single argument implements the Destructurable interface and the function expects multiple explicit parameters. The system splits the complex argument—such as a dictionary or pair—into constituent parts, mapping each part to the corresponding explicit parameter. This allows a function declared with key value: parameters to accept a single dictionary argument and automatically extract the values into the respective parameters.
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 →