haxe.PosInfos rework#12950
Conversation
…rs (#6095) Introduce a single notion of an "implicit" trailing argument (one the compiler auto-supplies when omitted; today only an optional haxe.PosInfos) in Typecore: is_implicit_arg, split_implicit_trailing_args, strip_implicit_trailing_args, has_only_implicit_args (replacing the former is_empty_or_pos_infos). Route the previously open-coded "tolerate a trailing optional PosInfos" sites through it: @:from, @:to (validity + forwarding), @:op binop and op-apply. Fix #6095: property accessors may now declare ?pos:haxe.PosInfos. The candidate accessor's implicit trailing args are stripped before unifying against the property signature, so get_a(?pos)/set_b(v, ?pos) type-check while a real extra argument still errors. is_implicit_arg is the single extension point for future user-defined auto-filled arguments.
`(...rest, ?pos:haxe.PosInfos)` is now allowed. The implicit trailing PosInfos is canonicalized to sit just before the rest (canonicalize_rest_order), keeping the rest physically last so native varargs codegen is unchanged on every target (no array-wrapping, no generator changes). check_rest is relaxed accordingly. In call unification, an implicit argument immediately before a rest is always auto-filled and never consumes a positional, so a Dynamic rest cannot eat the first positional as the PosInfos value. Rest detection in the canonicalization is shallow (no follow) and short-circuits when no rest is present, so for_type does not force lazy types during class building. Test: TestRest.testPosInfos.
Extend Issue10955 with a trailing PosInfos past the rest, exercising optional-before-rest skipping and overload resolution with auto-filled pos. Strengthen TestRest.testPosInfos to also assert the auto-filled pos identity on a Dynamic rest (which must not consume a positional as PosInfos). Manual-PosInfos semantics (overriding an auto-filled arg) are intentionally not asserted yet; that behavior is an open design question.
…gument
A call argument marked `@:posInfos e` designates `e` as the value for the
function's implicit haxe.PosInfos parameter, overriding the auto-filled
position. The marked argument is pulled out of the positional flow, so it
targets the pos slot regardless of a greedy rest:
f(...rest:String, ?pos) f("a","b", @:posInfos c) -> rest=["a","b"], pos=c
Previously such a value was either swallowed into the rest (Dynamic) or a type
error (typed rest), with no way to set pos. For non-rest functions it is now an
explicit override rather than relying on skip-by-type-mismatch.
Errors: multiple @:posInfos rejected; a mismatched override reports
"should be haxe.PosInfos"; an override with no matching PosInfos parameter is
flagged. Meta registered in src-json/meta.json.
Test: TestRest.testPosInfos.
…arguments An abstract marked `@:implicitArgResolver(name)` can auto-fill an omitted optional argument of that type; the named static function resolves the value at each call site. This generalizes the PosInfos auto-fill to user types (#4583). - Detection: cheap `Meta.has a.a_meta` on the abstract (no for_type involvement). - The former `is_pos_infos`/`mk_pos_infos` fill guards in unify_call_args become `is_implicit_value`/`mk_implicit_value`; PosInfos remains the built-in fallback. - Dispatch on the resolver field kind: a macro runs at the call site (Context API available, currentPos resolves to the call site); a plain function becomes a runtime call. A resolver's own optional/implicit args are filled normally, so e.g. a ?pos:PosInfos forwards the original call site. - Cycle guard (module-level rec_stack) wraps both paths: self- and mutual- referential resolvers error with "Cyclic @:implicitArgResolver" instead of recursing forever. Prototype scope: PosInfos is not yet migrated onto this mechanism; resolver types after a rest argument are not canonicalized; validation is minimal. Meta registered in src-json/meta.json.
…est, pos forwarding) TestImplicitArgResolver: a plain resolver fills an omitted optional arg; a resolver's own ?pos forwards the original call site; a macro resolver runs at the call site (captures the enclosing method); multiple implicit args (resolver + PosInfos, and two resolvers) all fill independently; a resolver-typed optional may precede a rest argument. Green on macro + js.
Property accessors may now declare a trailing implicit argument (?pos:haxe.PosInfos). The C++ reflective accessors in __Field/__GetStatic/__SetField/__SetStatic emit the get_X/set_X calls directly, bypassing the typer's implicit-arg injection, and the generated methods carry no C++ default value — so get_a() / set_b(v) failed to compile. Supply null() for each trailing implicit arg the accessor declares (one helper per get_/set_ shape, looked up from the class fields/statics).
Report clean errors at declaration time when an @:implicitArgResolver abstract is malformed, instead of letting misuse surface as confusing failures at call sites: - resolver function name missing / not found - resolver is not static (abstract instance methods carry CfImpl) - resolver has a required argument (it is invoked with no explicit args) - plain resolver does not return the abstract (or its underlying) type Macro resolvers skip the return-type check (their returned expression is type-checked against the target at each call site). Signature-dependent checks run in a PForce delay.
Misc compile-fail projects for the error paths of @:implicitArgResolver: - AfterRest: a resolver-typed optional after a rest argument (only a trailing PosInfos is reordered before the rest). - SelfCycle / MutualCycle: a resolver whose implicit argument resolves back to a type already being resolved, caught by the cycle guard.
The unary @:op declaration check compared the whole member type against `targ -> mono` with a strict type_eq, so a trailing implicit argument (?pos:haxe.PosInfos) made it fail. Strip implicit trailing args before the comparison, mirroring the binary @:op path. Auto-fill at the operator call site was already handled by the centralized implicit-arg machinery, so both prefix (@:op(!A)) and postfix (@:op(A!)) overloads now accept ?pos and forward the original call site.
A trailing optional haxe.PosInfos is auto-filled and forwards the call site on every abstract cast/operator-overload form, exercised here: @:from (static), @:to (member and static), @:arrayAccess (read and write), @:op(a.b) resolve (read and write), and @:op(a()) callable.
A macro function may now declare a trailing ?pos:haxe.PosInfos; it is filled with the *call-site* position (methodName/className/fileName/lineNumber), not the macro's own position (which is what Context.currentPos() returns). - typeloadFields: stop banning haxe.PosInfos on the caller-side macro signature. - macroContext.type_macro: strip the trailing implicit pos before the Expr-argument machinery (handling the canonicalized "?pos before a trailing rest" placement too), build the call-site PosInfos with the caller's typer context via mk_infos_t, evaluate it, and insert it at the matching argument position.
…rding (#11712) A macro's ?pos is compiler-filled (macro arguments resolve at compile time), so an explicit value for it cannot be honored. Detect an extra trailing argument or an @:posInfos override on a macro call and report a clear message instead of the opaque "Too many arguments". Tests: Issue11712 now also covers forwarding a macro's auto-filled pos to a regular helper run in macro context; MacroPosInfosExplicit (misc) covers the explicit-pass error.
A @:genericBuild macro may take a trailing ?pos:haxe.PosInfos, filled with the use-site position where the built type is referenced. Asserted via typedAs; the @:genericBuild class is fenced under #if !macro since the surrounding module also declares expression macros (which drag it into macro context, where @:genericBuild is disallowed).
An init macro (--macro) may take a trailing ?pos:haxe.PosInfos without breaking compilation; it is filled with the synthetic command-line position. Success project with a stdout check documenting the resolved values.
Reuse the before_rest flag already carried by the stripped implicit-pos tuple instead of re-deriving "ends with a rest" (which re-followed the last argument type), rename pos_infos -> implicit_pos (it holds the stripped parameter, not a value), and name the arity test fills_pos_slot with a note on why it is `= +1` (more than one extra positional is a genuine "too many arguments"). No behavior change.
|
@back2dos do you think the Implicit argument providers section covers your needs for #4583? (see also the added tests) |
A macro resolver now runs with the awaited argument type pushed onto the with_type stack, so Context.getExpectedType() resolves to it. This lets a generic abstract resolve a value depending on its type parameter.
Ports TestFromNothing onto @:implicitArgResolver: a generic abstract Dep<T> whose macro resolver picks a value per type parameter via the expected type.
| in | ||
| loop false e | ||
|
|
||
| let is_rest_shallow = function |
There was a problem hiding this comment.
This function already exists in tOther.ml as is_rest.
|
I agree with the sentiment but this looks like it's trying to do too much at once and seems unnecessarily messy. It's also hard to review because of that and I'm not sure where to start. I think the biggest hack is The I'll stop there for now so we can iterate on it in a sane fashion, but I'll have more complaints down the line. |
Yes, perfect ;)
The thing is that there are two different intentions intermingling here:
I concede that it's awkward trying to bring more predictability and control to a rather questionable design from two decades ago, but it's really not a user problem that these two concerns are so entangled. The more radical approach to fixing this would to be separate arguments into two different classes: an argument is either explicit (and only then can be optional) and or implicit (and then cannot be passed by the user). Then there's no ambiguity in any of these:
For inspiration, this is how Scala implements this: https://docs.scala-lang.org/tour/implicit-parameters.html On one side there's how Scala derives those implicits at the call site, which is kinda besides the point here: we move that problem into the argument type and then allow the user to take full control via a macro (arguably more flexible - definitely sufficient for implementing a solution equivalent to Scala's). But what I want to draw attention to is that there's a clear separation there - even stricter than the one I proposed above (implicits are actually passed as a separate argument list). This separation allows the compiler to clearly know whether an argument is to be found at the call site or resolved via a separate mechanism (the context in Scala, the type in Haxe). |
Implicit
?pos:haxe.PosInfosargument reworkhaxe.PosInfosused to be special-cased in a dozen isolated spots, each re-deriving "tolerate a trailing optionalPosInfos" with its own ad-hoc check, and with gaps between them. This PR centralizes that into a single notion of an implicit trailing argument (is_implicit_arg/split_implicit_trailing_args/strip_implicit_trailing_args), then wires every signature-comparison site (accessors, casts, operators, rest, macros) and every call site through it.On top of the cleanup it closes several long-standing issues and generalizes the mechanism so user types — not just
haxe.PosInfos— can auto-fill an omitted argument.Fixes
?pos:haxe.PosInfos.get_x(?pos:haxe.PosInfos)/set_x(v, ?pos:haxe.PosInfos)now type-check — the accessor's implicit trailing args are stripped before it is unified against the property's type. Closes PosInfos in accessors? #6095.PosInfosafter a rest argument.function f(...rest:Int, ?pos:haxe.PosInfos)is now accepted; the rest stays physically last (native varargs codegen is unchanged) andposis auto-filled. Closes haxe.Rest and haxe.PosInfos as last arguments #10808.PosInfoson operator overloads. A trailing?pos:haxe.PosInfosis allowed on@:opunary/binary operator functions (@:op(A!) function unwrap(?pos)), and on@:from/@:tocasts. Closes Operator overloading together with PosInfos #7362.PosInfoson macro functions. A macro function may declare a trailing?pos:haxe.PosInfos, auto-filled with the call-site position as a structuredhaxe.PosInfos— see Macros andhaxe.PosInfossection below. Closes haxe.PosInfos and macro #11712.New:
@:posInfos— explicit override of the implicit positionBy default the implicit
haxe.PosInfosargument is auto-filled with the call-site position.@:posInfoslets a call explicitly designate an argument as the value for that slot, overriding the auto-fill, and pulls it out of the positional flow.This matters when an earlier parameter's type is compatible with the position value, so a positional would otherwise capture it before it reaches
pos. The classic case is aDynamicrest, which accepts anything:Same story for a preceding optional whose type also accepts the value:
Passing multiple
@:posInfosarguments, or one with no matchinghaxe.PosInfosparameter, is a clear error.New: implicit argument providers —
@:implicitArgResolverThis generalizes the
PosInfosauto-fill to user-defined types. Marking an abstract@:implicitArgResolver(name)means: when an optional argument of that abstract type is omitted at a call site, it is auto-filled by calling the namedstaticfunction.haxe.PosInfosbecomes just the built-in instance of this mechanism. Closes #4583.The resolver may be a plain function (becomes a runtime call) or a macro function (runs at the call site, with the
ContextAPI available):Rules (validated at declaration time with clear errors):
static;?pos:haxe.PosInfosthat forwards the original call site;Cyclic @:implicitArgResolver) instead of looping.Multiple implicit arguments (a resolver plus a
PosInfos, say) fill independently, and a resolver-typed optional argument may sit before a rest argument.New: macros and
haxe.PosInfosA macro function may now declare a trailing
?pos:haxe.PosInfos, auto-filled with the call site — as a structuredhaxe.PosInfosrecord (fileName,lineNumber,className,methodName), the same shape a regular function receives.This complements
Context.currentPos()rather than duplicating it. Both refer to the call site, but they differ in format:currentPos()returns an opaquePosition(a file + byte range;Context.getPosInfosturns it into{file, min, max}), whereas?poshands you the resolvedPosInfosdirectly — including the enclosingclassName/methodName, which a rawPositiondoes not carry.It composes with the rest of the macro signature — extra
Exprarguments and a trailinghaxe.Restboth work — and the value can be forwarded: handingposto a plain helper invoked inside the macro carries the original call site through.The build-macro flavors resolve to the natural site:
@:genericBuild→ the use site (where the generic type is referenced);@:build/@:autoBuild→ the class-build site (methodNameisnull);--macro) → a synthetic command-line position.Because the value is compiler-filled at compile time, passing it explicitly (an extra positional, or an
@:posInfosoverride) is rejected with a clear error.Workaround unlocked:
Reflect.makeVarArgs+PosInfos(#6696)A
trace-like "any number of arguments + call-site position" function no longer needsReflect.makeVarArgs(whoseDynamicresult can't carry a per-call position). Thanks to rest + trailingPosInfos(above), just write:Each call gets its own position, fully typed and native. This doesn't change
makeVarArgsitself, so #6696 stays open, but the use case it was about now has a first-class solution.Notes
#10955(optional-before-rest skipping) is handled by the parent [typer] fix might_skip vs rest arguments #12936, not here.PosInfosbehavior is unchanged; the new behavior only adds previously-rejected forms.