Skip to content

FEATURE: Macros in type positions#193

Open
elias-michaias wants to merge 2 commits into
onyx-lang:masterfrom
elias-michaias:macros-in-type-positions
Open

FEATURE: Macros in type positions#193
elias-michaias wants to merge 2 commits into
onyx-lang:masterfrom
elias-michaias:macros-in-type-positions

Conversation

@elias-michaias

@elias-michaias elias-michaias commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Macros returning type_expr now usable in type positions

Previously, a macro declared with -> type_expr could not actually return their type. The best way to get a type from a macro was by immediately instantiating the type in the macro body and then using typeof on the macro call.
This change makes them work wherever a type is expected in the compiler's type-checker, including:

  • Variable type annotations (:)
  • Struct field type annotations
  • Constant type alias bindings (::)
  • Struct literals (.{})

What changed

Three fixes in compiler/src/checker.c:

1. Ast_Kind_Poly_Call_Type: macro in type annotation / inline call position

When the parser sees SomeMacro(A, B) in a type position, it creates an
Ast_Kind_Poly_Call_Type node. Previously, if the callee resolved to a macro instead
of a struct/type, the compiler would error with "Cannot instantiate a concrete type off
of a non-polymorphic type."

The fix detects a macro callee, builds a synthetic AstCall, runs it through
check_expression to trigger macro expansion, then extracts the returned type node from
the resulting AstDoBlock.

2. Ast_Kind_Alias in check_type: :: constant bindings

When OptArr :: composition(Foo, Bar) is processed, the RHS is stored as an
AstAlias wrapping an AstDoBlock (the expanded macro body). check_type previously
had no logic for this case, so subsequent uses of OptArr as a type would fail.

The fix walks the do-block body to find the return statement, extracts the returned
type node, and redirects the alias directly to it.

3. Ast_Kind_Field_Access: check_type — field-access macro callee

When a macro is accessed via field notation (F.up) and used as a type-position callee
(F.up(F.{}, i32.{})), check_field_access correctly resolved it to the macro node
but then check_type immediately errored with "This field access did not resolve to be
a type. It resolved to be a MACRO."

The fix adds a one-line early-out: if the resolved field is a macro, break out of the
error path so the enclosing Ast_Kind_Poly_Call_Type handler can expand it normally.


Examples

Macro in variable type annotation

composition :: macro ($Lhs: type_expr, $Rhs: type_expr) -> type_expr {
    return Compose(Lhs, Rhs)
}

main :: () {
    // Previously: "Cannot instantiate a concrete type off of a non-polymorphic type."
    m: composition(Optional.F, Array.F) = .{}
}

Macro in :: constant binding and struct literal

main :: () {
    // Previously: "Type used for struct literal is not a type."
    OptArr :: composition(Optional.F, Array.F)
    m := OptArr.{}         // struct literal via alias
    m2: OptArr = .{}       // type annotation via alias
}

Field-access macro as a struct field type

#inject Array {
    F :: struct {
        _ := true

        // macro that produces a type at compile time
        up :: macro (self: #Self, r: $R) -> type_expr {
            return #type [] R
        }
    }
}

// Previously: "This field access did not resolve to be a type. It resolved to be a MACRO."
Wrapper :: struct (F: type_expr) {
    value: F.up(F.{}, i32.{})   // resolves to [] i32
}

main :: () {
    w: Wrapper(Array.F) = .{ .[1, 2, 3] }
    println(w)  // Wrapper(F) { value = [ 1, 2, 3 ] }
}

All existing language tests seem to pass.

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