package loader
import (
"reflect"
"github.com/marzeq/qk/attributes"
"github.com/marzeq/qk/parser"
"github.com/marzeq/qk/types"
)
type traitFlowKey struct {
module string
name string
any bool
mutable bool
}
type traitRecast struct {
node *parser.CastNode
source types.TraitPointerType
target types.TraitPointerType
}
// narrowRuntimeTraitCastCandidates limits a dynamic trait-to-trait cast to
// concrete types that can actually reach its source trait in this compilation.
// Without this whole-program pass, every structural conformer is referenced by
// every recast and therefore kept alive by the linker even when no value of
// that concrete type is ever erased into the source trait.
func narrowRuntimeTraitCastCandidates(mods map[string]*ModuleInfo, order []string) {
possible := make(map[traitFlowKey][]types.Type)
var recasts []traitRecast
for _, name := range order {
walkParserNodes(mods[name].Root, func(node parser.Node) {
cast, ok := node.(*parser.CastNode)
if !ok {
return
}
targetType := cast.Type
if cast.Checked {
targetType = cast.CheckedType
}
target, targetIsTrait := types.Underlying(targetType).(types.TraitPointerType)
if cast.TraitConversion && targetIsTrait && cast.ConcreteType != nil {
addPossibleTraitType(possible, target, cast.ConcreteType)
}
if !cast.TraitRecast || !targetIsTrait || cast.Operand == nil {
return
}
source, sourceIsTrait := types.Underlying(cast.Operand.GetType()).(types.TraitPointerType)
if sourceIsTrait {
recasts = append(recasts, traitRecast{node: cast, source: source, target: target})
}
})
}
changed := true
for changed {
changed = false
for _, recast := range recasts {
sources := possible[flowKey(recast.source)]
for _, candidate := range recast.node.TraitCandidates {
if containsType(sources, candidate.ConcreteType) && addPossibleTraitType(possible, recast.target, candidate.ConcreteType) {
changed = true
}
}
}
}
for _, recast := range recasts {
sources := possible[flowKey(recast.source)]
kept := recast.node.TraitCandidates[:0]
for _, candidate := range recast.node.TraitCandidates {
if containsType(sources, candidate.ConcreteType) {
kept = append(kept, candidate)
}
}
recast.node.TraitCandidates = kept
}
}
// demandedDynamicTraitSlots records the method slots that can be reached by
// dynamic dispatch in this compilation. Vtables retain their complete layout,
// but IR generation can leave unused entries null so those entries do not keep
// otherwise dead method implementations reachable.
func demandedDynamicTraitSlots(mods map[string]*ModuleInfo, order []string) map[string]map[int]bool {
used := make(map[string]map[int]bool)
externallyOpen := false
for _, name := range order {
walkParserNodes(mods[name].Root, func(node parser.Node) {
switch node := node.(type) {
case *parser.FunctionCallNode:
if !node.TraitCall || len(node.Args) == 0 {
return
}
pointer, ok := types.Underlying(node.Args[0].GetType()).(types.TraitPointerType)
if !ok {
return
}
key := pointer.Trait.String()
if used[key] == nil {
used[key] = make(map[int]bool)
}
used[key][node.TraitSlot] = true
case *parser.FunctionDefNode:
if node.Symbol == nil || node.Symbol.Signature == nil ||
(node.Symbol.Attributes.Get(attributes.AttributeTypeExport) == nil &&
node.Symbol.Attributes.Get(attributes.AttributeTypeForeign) == nil) {
return
}
for _, parameter := range node.Symbol.Signature.Parameters {
externallyOpen = externallyOpen || types.HasTraitPointer(parameter)
}
externallyOpen = externallyOpen || types.HasTraitPointer(node.Symbol.Signature.ReturnType)
}
})
}
if externallyOpen {
// A foreign or exported QK ABI can expose a generated trait value to code
// outside this compilation, whose dynamic calls are not visible here.
return nil
}
return used
}
func flowKey(pointer types.TraitPointerType) traitFlowKey {
return traitFlowKey{
module: pointer.Trait.Module,
name: pointer.Trait.Name,
any: pointer.Trait.Any,
mutable: pointer.Mutable,
}
}
func addPossibleTraitType(possible map[traitFlowKey][]types.Type, target types.TraitPointerType, concrete types.Type) bool {
changed := addPossibleType(possible, flowKey(target), concrete)
if target.Mutable {
target.Mutable = false
changed = addPossibleType(possible, flowKey(target), concrete) || changed
}
return changed
}
func addPossibleType(possible map[traitFlowKey][]types.Type, key traitFlowKey, concrete types.Type) bool {
if containsType(possible[key], concrete) {
return false
}
possible[key] = append(possible[key], concrete)
return true
}
func containsType(haystack []types.Type, needle types.Type) bool {
for _, candidate := range haystack {
if candidate.Equals(needle) {
return true
}
}
return false
}
var parserPackagePath = reflect.TypeOf(parser.RootNode{}).PkgPath()
// walkParserNodes follows only parser-owned aggregate fields. Symbol and type
// pointers deliberately remain leaves, avoiding the semantic graph's cycles.
func walkParserNodes(root parser.Node, visit func(parser.Node)) {
var walk func(reflect.Value)
walk = func(value reflect.Value) {
if !value.IsValid() {
return
}
if value.Kind() == reflect.Interface {
if value.IsNil() {
return
}
walk(value.Elem())
return
}
if value.Kind() == reflect.Pointer {
if value.IsNil() {
return
}
if node, ok := value.Interface().(parser.Node); ok {
visit(node)
walk(value.Elem())
}
return
}
switch value.Kind() {
case reflect.Struct:
if value.Type().PkgPath() != parserPackagePath {
return
}
for i := 0; i < value.NumField(); i++ {
walk(value.Field(i))
}
case reflect.Slice, reflect.Array:
for i := 0; i < value.Len(); i++ {
walk(value.Index(i))
}
}
}
walk(reflect.ValueOf(root))
}