Skip to content
Noodle
InstallLearnPlayground
GitHub

Question parameters

Question parameters are named inputs written in a trailing ?{...} group. They complement positional parameters when call-site labels, defaults, presence information, or compiler-resolved capabilities are part of the API.

func make_range(
?{start: Int, stop: Int},
) -> (Int, Int) do
(start, stop)
end
func main() -> Unit do
range = make_range(?{stop: 10, start: 2});
Debug.trace(range);
end

Answers are matched by name and may appear in any source order. Duplicate, unknown, missing, or incorrectly typed answers are compile-time errors.

A default is evaluated by the callee only when its answer is omitted:

func scale(
value : Int,
?{factor: Int default 2},
) -> Int do
value * factor
end
func main() -> Unit do
Debug.trace(scale(6));
Debug.trace(scale(6, ?{factor: 3}));
end

Defaults are processed in declaration order. A default may refer to positional parameters and earlier question parameters, but not later ones.

A presence parameter uses name?: T. Inside the function its type is Option[T]: omission becomes None, and an explicit answer becomes Some.

func choose_label(
fallback : String,
?{label?: String},
) -> String do
switch label
case Option::Some(value) then value
case Option::None then fallback
end
end
func main() -> Unit do
Console.println(choose_label("default"));
Console.println(choose_label("default", ?{label: "custom"}));
end

Presence questions depend on the standard library’s canonical Option; see Option and Result.

Question inputs are part of a function’s type. A defaulted or presence parameter is represented as optional in that type, while the concrete default expression remains an implementation detail.

Explicit answers are evaluated after positional arguments, in the order written by the caller. Matching them by name does not reorder effects.

The special form ?{extension Name: Interface for T} asks the compiler to resolve an interface provider when the caller omits the answer:

func equivalent[T](
left : T,
right : T,
?{extension Equal: IEqual for T},
) -> Bool do
Equal.equal(left, right)
end

The uppercase binding is the selected immutable provider dictionary. Provider selection, association, activation, and ambiguity are explained in Interfaces and extensions.

An autofilled question asks the compiler to attach a source range to a direct function call. The answer type must be the opaque Meta.SourceLocation type. It is an immutable description of a file and a source range; you normally do not construct one yourself. You can pass it to another API, or turn it into a human-readable string with its to_string() method.

There are two recipes:

  • autofill $loc_of($callsite) records the complete call expression;
  • autofill $loc_of(parameter) records the expression supplied for that positional parameter.

For example, this diagnostic helper records the range of its value argument:

func describe_value(
value : String,
?{location: Meta.SourceLocation autofill $loc_of(value)},
) -> String do
_ = value;
"value at " ++ location.to_string()
end
func main() -> Unit do
Console.println(describe_value("ready"));
end

The compiler fills location at describe_value("ready"); the range points to the string expression, not to the function declaration. A call-site recipe uses the whole call instead:

func mark_call(
?{location: Meta.SourceLocation autofill $loc_of($callsite)},
) -> String do
location.to_string()
end
func main() -> Unit do
Console.println(mark_call());
end

Autofill is only available on top-level ordinary functions. The function must be called directly by name (or through a module-qualified name); an autofilled function cannot be stored in a binding, returned, captured by a closure, or passed as an argument. Its function type still has an optional question parameter, but the autofill recipe is metadata used by direct-call checking.

An explicit answer suppresses autofill. This is how a wrapper preserves the original caller’s location instead of recording the wrapper’s own call:

func show_at(
value : String,
?{location: Meta.SourceLocation autofill $loc_of(value)},
) -> String do
value ++ " at " ++ location.to_string()
end
func forward_location(
value : String,
?{location: Meta.SourceLocation},
) -> String do
show_at(value, ?{location: location})
end

The wrapper’s incoming location is intentionally not constructed in user code: a source location is opaque, so an application normally receives it from another location-aware API. The important part is show_at(value, ?{location: location}): an explicit question answer wins over show_at’s autofill. If a wrapper omits that answer, the compiler records the wrapper’s own call to show_at, not the original caller’s argument.

The standard testing helpers use these rules in normal test code:

test "reports the checked expressions" do
Testing.assert(2 + 2 == 4)!;
Testing.assert_equal(2 + 2, 4)!;
end

Testing.assert declares condition_loc with $loc_of(condition). Testing.assert_equal declares actual_loc and expected_loc with the corresponding recipes. When an assertion fails, Testing.assert_equal keeps the rendered actual and expected values together with both source locations, so a diagnostic can identify both expressions.

This is different from an ordinary default question. A default is an expression evaluated by the callee when the caller omits an answer; autofill is compiler metadata derived from the caller’s syntax, and it has the direct-call restriction described above.

Next: interfaces and extensions.