Skip to content

Generate concrete generic entry signatures - #3911

Draft
Ian Davis (idavis) wants to merge 1 commit into
iadavis/pass-split/defunc-aggregate-environmentsfrom
iadavis/pass-split/generic-entry-signatures
Draft

Ian Davis (idavis) wants to merge 1 commit into
iadavis/pass-split/defunc-aggregate-environmentsfrom
iadavis/pass-split/generic-entry-signatures

Conversation

@idavis

@idavis Ian Davis (idavis) commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes generated entry points for parameterless generic operations.

Previously, the generated call could omit generic arguments or use the operation body’s result type as the callee type. This left the entry call ill-typed or generic during lowering and monomorphization.

The branch now:

  • defaults generated-entry type parameters to Unit;
  • validates those defaults against generic bounds;
  • gives the generated callee its concrete callable signature;
  • carries concrete generic arguments into FIR and monomorphization;
  • limits defaulting to the automatically selected entry point; and
  • preserves user diagnostics and source locations.

The branch fixes bugs where automatically generated calls to generic entry operations could:

  • omit concrete generic arguments;
  • assign the callee its result type instead of its arrow type;
  • leave generic result types unresolved;
  • reject harmless unused type parameters;
  • accept defaults that violate class or dependent bounds;
  • report the wrong failing parameter;
  • default an unselected Main;
  • override ordinary inference for explicit entry expressions;
  • lose generic arguments during FIR lowering;
  • contaminate nested or foreign generic calls; or
  • mask source-located user failures with entry typing errors.

1. Generated generic entries missing type arguments

A parameterless generic Main has no call arguments from which to infer 'T.

@EntryPoint()
operation Main<'T>() : Int {
    42
}

Previously, the generated call could reference Main with an empty generic-argument list, leaving a reachable generic callable after entry generation.

The branch generates the equivalent of Main<Unit>().

2. Generated callees receiving the wrong callable type

The generated callee was previously typed using the operation body’s result type rather than its arrow type.

@EntryPoint()
operation Main<'T>() : Int {
    42
}

The callee must have type:

Unit => Int

not merely Int.

The branch constructs the concrete callable signature and uses its output type for the call expression.

3. Generic result types remaining unresolved

The result type may itself contain a generic parameter:

@EntryPoint()
operation Main<'T>() : 'T {
    fail "unreachable"
}

The generated entry must have a concrete result type even though execution always fails.

The branch instantiates 'T as Unit, producing a concrete Unit => Unit entry signature.

4. Unused generic parameters preventing compilation

A generic parameter can be absent from both the input and output:

@EntryPoint()
operation Main<'T>() : Int {
    42
}

There is no source-level type evidence for 'T, but it also has no effect on the operation’s behavior.

The branch consistently defaults such parameters to Unit, allowing QIR generation and execution to proceed.

5. Satisfiable bounds not being applied to generated entries

A default is valid only when it satisfies the declared bounds.

@EntryPoint()
operation Main<'T : Eq + Show>() : 'T[] {
    fail "unreachable"
}

Because Unit satisfies these bounds, the generated entry can safely instantiate 'T as Unit.

The branch validates the default through the frontend constraint solver before generating the call.

6. Unsatisfied bounds producing malformed entries

Unit does not satisfy every class constraint:

@EntryPoint()
operation Main<'T : Add>() : 'T {
    fail "unreachable"
}

Previously, entry generation could leave an unresolved generic call or fail later during specialization.

The branch rejects this entry immediately with a targeted diagnostic explaining that no concrete generated-entry type satisfies 'T’s bounds.

7. Dependent bounds being accepted without validation

One type parameter’s bound may depend on another:

@EntryPoint()
operation Main<'Item, 'Container : Iterable['Item]>() : Unit {
}

Defaulting both parameters to Unit would require Unit to implement Iterable[Unit], which it does not.

The branch validates the complete default argument vector, including dependent bound payloads, and rejects this entry.

8. The wrong generic parameter being blamed

With multiple parameters, an earlier default may be valid while a later one is not:

@EntryPoint()
operation Main<'T : Eq, 'U : Add>() : Unit {
}

Unit satisfies Eq but not Add.

The branch attributes the entry-point diagnostic to 'U, the parameter whose default violates its bounds.

9. Defaulting an unselected generic Main

An explicitly attributed entry point should take precedence over an unrelated generic callable named Main.

operation Main<'T : Add>() : Unit {
}

@EntryPoint()
operation Run() : Int {
    42
}

The invalid Unit default for Main<'T : Add> is irrelevant because Run is the selected entry.

The branch applies generated-entry defaulting only to the callable actually chosen as the entry point.

10. Overriding type evidence in an explicit entry expression

The special Unit default applies only when the compiler must synthesize an entry call.

An explicit call still uses ordinary Q# type inference:

operation Main<'T : Add>() : Unit {
}

// Explicit entry expression supplied by the host:
Main()

This call provides no argument or expected-result evidence for 'T, so it must still report a type-inference error.

The branch does not apply generated-entry defaults to explicit entry expressions.

Conversely, an explicit call with evidence retains that evidence:

function Identity<'T>(value : 'T) : 'T {
    value
}

// Explicit entry expression:
Identity(42)

This remains an Int instantiation rather than inheriting the generated-entry Unit policy.

11. Generic arguments being lost during HIR-to-FIR lowering

The generated HIR entry may be correct but still fail if its concrete generic arguments disappear during lowering.

function Make<'T>(value : 'T) : Unit -> 'T {
    () -> value
}

@EntryPoint()
operation Main<'T>() : Int {
    let getValue = Make(42);
    getValue()
}

The generated call to Main<Unit> must reach FIR with its Unit argument intact so monomorphization can create and call the concrete entry specialization.

The branch verifies that the argument survives lowering and is consumed by monomorphization.

12. Entry defaults contaminating nested generic calls

Defaulting the entry’s 'T to Unit must not overwrite generic arguments inferred for calls inside the entry.

function Identity<'T>(value : 'T) : 'T {
    value
}

@EntryPoint()
operation Main<'Unused>() : Int {
    Identity(42)
}

The required instantiations are:

  • Main<Unit>; and
  • Identity<Int>.

The branch preserves each call’s independently inferred generic arguments.

13. Entry defaults contaminating foreign generic calls

The same isolation is required across package boundaries.

// Library package
namespace Library {
    function Identity<'T>(value : 'T) : 'T {
        value
    }
}
// Application package
namespace Application {
    @EntryPoint()
    operation Main<'Unused>() : Int {
        Library.Identity(42)
    }
}

The entry defaults to Main<Unit>, while the library call remains Identity<Int> and is specialized in the library package.

14. Entry typing errors masking user failures

A valid generic entry that explicitly fails should reach the user’s failure:

@EntryPoint()
operation Main<'T>() : Unit {
    fail "expected user failure"
}

Previously, malformed generated typing or incomplete specialization could fail before execution reached this statement.

The branch ensures the generated Main<Unit> call is concrete, allowing the original failure and its source span to be reported across interpreter and QIR paths.

@idavis
Ian Davis (idavis) added this pull request to stack #3892 October 7, 2026 18:39

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant