Skip to content

haxe.PosInfos rework#12950

Open
kLabz wants to merge 22 commits into
developmentfrom
posinfos_rework
Open

haxe.PosInfos rework#12950
kLabz wants to merge 22 commits into
developmentfrom
posinfos_rework

Conversation

@kLabz

@kLabz kLabz commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Implicit ?pos:haxe.PosInfos argument rework

Based on #12936 — please review/merge that first.

haxe.PosInfos used to be special-cased in a dozen isolated spots, each re-deriving "tolerate a trailing optional PosInfos" 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

  • Property accessors may declare a trailing ?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.
  • PosInfos after a rest argument. function f(...rest:Int, ?pos:haxe.PosInfos) is now accepted; the rest stays physically last (native varargs codegen is unchanged) and pos is auto-filled. Closes haxe.Rest and haxe.PosInfos as last arguments #10808.
  • PosInfos on operator overloads. A trailing ?pos:haxe.PosInfos is allowed on @:op unary/binary operator functions (@:op(A!) function unwrap(?pos)), and on @:from / @:to casts. Closes Operator overloading together with PosInfos #7362.
  • PosInfos on macro functions. A macro function may declare a trailing ?pos:haxe.PosInfos, auto-filled with the call-site position as a structured haxe.PosInfos — see Macros and haxe.PosInfos section below. Closes haxe.PosInfos and macro #11712.

New: @:posInfos — explicit override of the implicit position

By default the implicit haxe.PosInfos argument is auto-filled with the call-site position. @:posInfos lets 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 a Dynamic rest, which accepts anything:

function log(...rest:Dynamic, ?pos:haxe.PosInfos):Void { /* ... */ }

var caller:haxe.PosInfos = getSomePos();

log(1, 2, caller);            // rest = [1, 2, caller], pos = auto-filled — caller is NOT the position!
log(1, 2, @:posInfos caller); // rest = [1, 2],         pos = caller

Same story for a preceding optional whose type also accepts the value:

function note(?value:Dynamic, ?pos:haxe.PosInfos):Void { /* ... */ }

note(caller);            // value = caller, pos = auto-filled
note(@:posInfos caller); // value = null,   pos = caller

Passing multiple @:posInfos arguments, or one with no matching haxe.PosInfos parameter, is a clear error.

New: implicit argument providers — @:implicitArgResolver

This generalizes the PosInfos auto-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 named static function. haxe.PosInfos becomes 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 Context API available):

// plain resolver: filled with a runtime value when omitted
@:implicitArgResolver(resolve)
abstract Plain(String) from String to String {
	public inline function new(s:String) this = s;
	static function resolve():Plain return new Plain("plain");
}

// macro resolver: runs at the call site (e.g. capture the enclosing method)
@:implicitArgResolver(resolve)
abstract MacroCtx(String) to String {
	public inline function new(s:String) this = s;
	macro static function resolve() {
		return macro $v{haxe.macro.Context.getLocalMethod()};
	}
}

function log(msg:String, ?ctx:Plain):String return (ctx : String) + ": " + msg;

log("hi");          // ctx auto-filled with resolve() -> "plain: hi"
log("hi", "given"); // explicit value still wins

Rules (validated at declaration time with clear errors):

  • the named field must exist and be static;
  • it must take no required arguments (it is invoked with no explicit args) — it may itself take optional/implicit args, e.g. a ?pos:haxe.PosInfos that forwards the original call site;
  • a plain (non-macro) resolver must return the abstract (or its underlying type);
  • self- and mutually-referential resolvers are rejected (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.PosInfos

A macro function may now declare a trailing ?pos:haxe.PosInfos, auto-filled with the call site — as a structured haxe.PosInfos record (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 opaque Position (a file + byte range; Context.getPosInfos turns it into {file, min, max}), whereas ?pos hands you the resolved PosInfos directly — including the enclosing className / methodName, which a raw Position does not carry.

macro static function log(e:haxe.macro.Expr, ?pos:haxe.PosInfos) {
	trace(pos.className + "." + pos.methodName); // the caller's class/method
	return macro null;
}

It composes with the rest of the macro signature — extra Expr arguments and a trailing haxe.Rest both work — and the value can be forwarded: handing pos to 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 (methodName is null);
  • an init macro (--macro) → a synthetic command-line position.

Because the value is compiler-filled at compile time, passing it explicitly (an extra positional, or an @:posInfos override) 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 needs Reflect.makeVarArgs (whose Dynamic result can't carry a per-call position). Thanks to rest + trailing PosInfos (above), just write:

function log(...rest:Dynamic, ?pos:haxe.PosInfos):Void {
	trace(pos.lineNumber, rest);
}

Each call gets its own position, fully typed and native. This doesn't change makeVarArgs itself, 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.
  • Backwards compatible: existing PosInfos behavior is unchanged; the new behavior only adds previously-rejected forms.

kLabz added 20 commits June 19, 2026 17:01
…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.
@kLabz kLabz changed the title [typer] haxe.PosInfos rework haxe.PosInfos rework Jun 20, 2026
@kLabz

kLabz commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

@back2dos do you think the Implicit argument providers section covers your needs for #4583? (see also the added tests)

kLabz added 2 commits June 21, 2026 11:54
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.
@kLabz kLabz requested a review from Simn June 23, 2026 16:30
in
loop false e

let is_rest_shallow = function

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function already exists in tOther.ml as is_rest.

@Simn

Simn commented Jun 24, 2026

Copy link
Copy Markdown
Member

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 @:posInfos which is introduced to deal with the arcane issue #10808. I still argue that no further arguments should be allowed after ...rest, which would make the ...rest:Dynamic, ?pos:haxe.PosInfos example invalid. What should be supported here instead is implicit PosInfos arguments in non-trailing positions, which avoids this whole "compiler understands it as rest but it's actually pos" nonsense.

The ?value:Dynamic, ?pos:haxe.PosInfos variant would also be covered by that. Putting a more generic optional argument type before a more specific one and then expecting it to be skipped via some hacky metadata is a user code problem.

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.

@back2dos

Copy link
Copy Markdown
Member

@back2dos do you think the Implicit argument providers section covers your needs for #4583? (see also the added tests)

Yes, perfect ;)


Putting a more generic optional argument type before a more specific one and then expecting it to be skipped via some hacky metadata is a user code problem.

The thing is that there are two different intentions intermingling here:

  1. support for different signatures (and I'm inclined to say that arg skipping is a hack that has outlived its usefulness with the advent of overloading)
  2. support for implicit arguments (which is hardcoded into the compiler for exactly one type)

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:

  • function log(value:Dynamic, @:implicit pos:haxe.PosInfos):Void;
  • function log(...values:Dynamic, @:implicit pos:haxe.PosInfos):Void;
  • function log(@:implicit pos:haxe.PosInfos, ...values:Dynamic):Void;

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants