fix: clarify Call target-type propagation and dispatch resolution in RFC 0005 - #168
Merged
mwiebe merged 2 commits intoAug 12, 2026
Merged
Conversation
…RFC 0005 Pins down two ambiguities in RFC 0005's expression-evaluation pseudo-code: argument typesets come from parameter types as written (the caller's target is never unified with a signature's return type to bind type variables into the parameters), and overload resolution matches argument types alone against every arity-matching signature, with an empty target-filtered candidate set falling back to unconstrained argument evaluation instead of raising. Adds matching user-facing language to the wiki's Expression Language page and an EXPR conformance fixture making the semantics observable through the bare-args-item list[string] target context. Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
mwiebe
force-pushed
the
clarify-call-dispatch-return-types
branch
from
August 8, 2026 00:19
8f6d858 to
986532e
Compare
Ports three points from openjd-rs specs/expr/values.md that landed after the Call-dispatch clarification: - Sharpen range_expr -> list[int]: it is the only list type a range_expr implicitly coerces to. Any other element type is an error; implicit rules do not chain, so the materialized list[int] is never widened element-wise toward the target. Templates that want the widened list chain the explicit conversion list(value: range_expr) -> list[int] from RFC 0006. - State explicitly that a type-variable target (T, T1, T2, T3) has no coercion rule for concrete or unresolved values: type variables are resolved by signature matching before coercion, so reaching coercion with one unbound is always an error. - Document coercion of unresolved values: the same conversion table applies at the type level, union constraints coerce existentially (at least one member must succeed; failing possibilities are discarded), payload-dependent checks defer to resolution, and the two paths are asymmetric in exactly one direction - type-level coercion may accept what the concrete value later rejects, but must never reject what the concrete value would accept. Matching user-facing language added to the wiki's Expression Language page (Implicit Type Coercion and Static Type Checking sections). Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
mwiebe
force-pushed
the
clarify-call-dispatch-return-types
branch
from
August 11, 2026 00:34
1a5deb4 to
a3520e3
Compare
seant-aws
reviewed
Aug 11, 2026
| - SEP | ||
| - "{{ min(Param.Ints) }}" | ||
| expected: | ||
| output: |
seant-aws
approved these changes
Aug 11, 2026
seant-aws
left a comment
Contributor
There was a problem hiding this comment.
overall spec makes sense, don't have any outlying comments. Will review the implementation details now
leongdl
approved these changes
Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR resolves two ambiguities in RFC 0005's expression-evaluation
pseudo-code that an implementation tripped over. It pins
both down in the direction the RFC's own design rationale points, and adds
matching language to the wiki's Expression Language page.
Background
When a host evaluates an expression from a template, it passes a target
type — "whatever this expression computes, I need an
intat the end."RFC 0005's guiding rule for this is stated up front: target types guide
coercion, but must not constrain how sub-expressions evaluate. The target
describes the result, so a valid expression should compute the same thing
with or without one; the target only affects the type of what comes back.
The RFC's propagation table applies that rule node by node — operator
operands evaluate unconstrained, a ternary's branches inherit the target,
and so on. For function calls it says arguments get types "computed from
candidate signatures," backed by pseudo-code. That pseudo-code is where
the two ambiguities live. They were found while fixing target-type
propagation bugs in openjd-rs
(OpenJobDescription/openjd-rs#291), when code
review pushed on what the correct call-argument behavior actually is and
the pseudo-code turned out not to fully answer the question.
Ambiguity 1: do generic signatures bind the target into their arguments?
Take
sorted(list: list[T1]) -> list[T1]evaluated assorted([10, 2])with target
list[string]. Two readings:list[string]with the returntype
list[T1], concludeT1 = string, and evaluate the argumenttoward
list[string]. The list becomes["10", "2"]before sorting,and the sort is lexicographic:
["10", "2"].literal
list[T1]— any list type — so there is nothing to coercetoward. The sort is numeric, and the result coerces:
["2", "10"].The pseudo-code computes
arg_ts = {sig.param_types[i] for sig in candidates}— parameter types as written, no unification step — but neversays so explicitly, and it doesn't say what a type-variable constraint
means for the argument. The first reading is tempting (the current Python
implementation exhibits it, though via a target-propagation bug rather
than by design), but it violates the RFC's own rule: the target changes
what the expression computes (which order the list is in), not just the
type of the result.
This PR makes the second reading explicit: argument typesets come from
parameter types as written, and the caller's target is never unified with
a return type to bind type variables into the parameters. A type-variable
parameter leaves its position unconstrained; the "any list" part is still
enforced during signature resolution, where
sorted("abc")fails with orwithout a target and a
range_exprargument is still coerced tolist[int].Ambiguity 2: does the target participate in overload resolution?
resolve_and_callis declared with acandidatesparameter, and themethod-call arm passes one — but the function-call arm invokes it as
resolve_and_call(func, arg_values, ...), with no candidates argument.So the pseudo-code doesn't actually say whether final overload resolution
is restricted to the target-filtered candidate list or considers the
function's full signature set.
The related question with observable consequences: the function-call arm
raises "No matching signature" when the target filter leaves zero
candidates, even though the arguments might evaluate fine and the call
succeed — with only the final result failing to coerce. For
min([1, 2])with target
list[string], that's the difference between a generic"No matching signature for min" and the precise "Cannot coerce int to
list[string]".
This PR pins both down: overload resolution matches argument types alone
against every arity-matching signature — return types and the caller's
target play no part in selecting the implementation — and an empty
candidate set falls back to evaluating arguments unconstrained rather
than raising, so target mismatches surface as precise result-coercion
errors. The empty-candidates change is the one place this PR amends
behavior rather than only clarifying it; flagging it for reviewers who
read the old
raiseas intentional.Changes
Call (function)row states theparameter-types-as-written rule, that a type-variable parameter type
contributes no argument constraint (its concrete structure being
enforced during resolution instead), and that the target filters
candidates but never binds into parameters.
Call (method)rowcorrected to
"receiver:
None, other args:None" — the old "computed fromsignatures" wording contradicted the RFC's own pseudo-code, which
evaluates method arguments unconstrained.
Param.Count - 1example with thesortedcase, and adds a "Signature resolution is independent of thetarget type" paragraph covering the dispatch and diagnostics rules.
candidates; symbolic/empty positions produce
None(unconstrained)typesets via a new
is_symbolichelper;resolve_and_callisinvoked with an explicit all-arity-matching signature list, matching
its declared parameters.
stating the user-facing principle: the target describes the result,
never the computation, with the
sortedexample.— new execution fixture pinning the clarified
Callsemantics throughthe one template context where a
list[string]target is observable:bare
"{{expr}}"argsitems (targetstring? | list[string]per§1.3.2).
sorted(Param.Ints)on aLIST[INT]parameter must sortnumerically and coerce/flatten the result (
2,10), with thelexicographic order (
10,2) asserted as forbidden output; themethod-call form pins the receiver-unconstrained rule, and
min(Param.Ints)pins that a call whose signatures' return types maynot survive the target filter still resolves and executes normally.
Complements the existing
expr1.3.1--target-type-propagationfixture,which covers operator/ternary propagation but not calls.
No schema or template changes. openjd-rs implements
these semantics (and passes the new fixture) as of
OpenJobDescription/openjd-rs@c780846, the
merged fix for OpenJobDescription/openjd-rs#291; the
Python implementation currently diverges on both points via the same
target-propagation bug tracked there.
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.