Files
aego/script/reload/plan.go
T

223 lines
4.8 KiB
Go

package reload
import (
"fmt"
"slices"
"sort"
"aego/script/abi"
"aego/script/bytecode"
"aego/script/diag"
"aego/script/source"
)
type Conversion uint8
const (
Same Conversion = iota
Default
EnumByName
Struct
Slice
Reresolve
)
type FieldMap struct {
Name string
OldSlot int
NewSlot int
OldSlots int
NewSlots int
Old abi.TypeRef
New abi.TypeRef
Conv Conversion
}
type TypeChange struct {
Name string
Old *bytecode.ScriptDef
New *bytecode.ScriptDef
Fields []FieldMap
Reset []string
Removed []string
Changed bool
}
type Plan struct {
From Program
To Program
Types []TypeChange
Diags []diag.Diagnostic
from *layout
to *layout
byName map[string]*TypeChange
}
func (p *Plan) warn(format string, args ...any) {
p.Diags = append(p.Diags, diag.Warnf("", source.Span{}, format, args...))
}
func (p *Plan) changed(name string) bool {
tc := p.byName[name]
return tc == nil || tc.Changed
}
func sameLayout(a, b abi.TypeRef, from, to *layout) bool {
if !a.Equal(b) {
return false
}
switch a.Kind {
case abi.Struct:
os, ns := from.structs[a.Name], to.structs[b.Name]
if os == nil || ns == nil {
return os == nil && ns == nil
}
if os.Slots != ns.Slots || len(os.Fields) != len(ns.Fields) {
return false
}
for i := range os.Fields {
of, nf := os.Fields[i], ns.Fields[i]
if of.Name != nf.Name || of.Slot != nf.Slot || of.Slots != nf.Slots {
return false
}
ot, ok1 := abi.Parse(of.Type)
nt, ok2 := abi.Parse(nf.Type)
if !ok1 || !ok2 || !sameLayout(ot, nt, from, to) {
return false
}
}
case abi.Slice:
return a.Elem != nil && b.Elem != nil && sameLayout(*a.Elem, *b.Elem, from, to)
case abi.Enum:
return slices.Equal(from.enums[a.Name], to.enums[b.Name])
}
return true
}
func classify(a, b abi.TypeRef, from, to *layout) Conversion {
switch {
case sameLayout(a, b, from, to):
if a.Kind == abi.ComponentRef {
return Reresolve
}
return Same
case a.Kind != b.Kind || a.Name != b.Name:
return Default
}
switch a.Kind {
case abi.Struct:
if from.structs[a.Name] != nil && to.structs[b.Name] != nil {
return Struct
}
case abi.Enum:
if from.enums[a.Name] != nil && to.enums[b.Name] != nil {
return EnumByName
}
case abi.Slice:
if a.Elem != nil && b.Elem != nil && classify(*a.Elem, *b.Elem, from, to) != Default {
return Slice
}
}
return Default
}
func findField(od *bytecode.ScriptDef, name string, former []string) *bytecode.FieldDef {
for _, n := range append([]string{name}, former...) {
for i := range od.Fields {
if od.Fields[i].Name == n {
return &od.Fields[i]
}
}
}
return nil
}
func Build(from, to Program) (*Plan, error) {
p := &Plan{From: from, To: to, from: newLayout(from), to: newLayout(to), byName: map[string]*TypeChange{}}
olds := map[string]*bytecode.ScriptDef{}
oldPkg := map[string]string{}
for pkg, m := range from.Modules {
for i := range m.Scripts {
d := &m.Scripts[i]
olds[d.FullName] = d
oldPkg[d.FullName] = pkg
}
}
matched := map[string]bool{}
for _, nm := range to.Ordered() {
pkg := nm.Package
for i := range nm.Scripts {
nd := &nm.Scripts[i]
od := olds[nd.FullName]
if od == nil {
for _, f := range nd.Former {
if od = olds[f]; od != nil {
break
}
}
}
tc := TypeChange{Name: nd.FullName, New: nd, Old: od, Changed: true}
if od != nil {
matched[od.FullName] = true
tc.Changed = od.FullName != nd.FullName || from.Hashes[oldPkg[od.FullName]] != to.Hashes[pkg]
if err := p.fields(&tc); err != nil {
return nil, err
}
}
p.Types = append(p.Types, tc)
}
}
for i := range p.Types {
p.byName[p.Types[i].Name] = &p.Types[i]
}
names := make([]string, 0, len(olds))
for n := range olds {
if !matched[n] {
names = append(names, n)
}
}
sort.Strings(names)
for _, n := range names {
p.warn("script %s was removed, its instances stop", n)
}
return p, nil
}
func (p *Plan) fields(tc *TypeChange) error {
od, nd := tc.Old, tc.New
used := map[string]bool{}
for _, nf := range nd.Fields {
of := findField(od, nf.Name, nf.Former)
if of == nil {
continue
}
used[of.Name] = true
ot, ok1 := abi.Parse(of.Type)
nt, ok2 := abi.Parse(nf.Type)
if !ok1 || !ok2 {
return fmt.Errorf("script %s field %s has a malformed type", nd.FullName, nf.Name)
}
conv := classify(ot, nt, p.from, p.to)
if conv == Default {
tc.Reset = append(tc.Reset, nf.Name)
p.warn("field %s.%s changed from %s to %s, reset to its default", nd.FullName, nf.Name, of.Type, nf.Type)
}
tc.Fields = append(tc.Fields, FieldMap{
Name: nf.Name,
OldSlot: int(of.Slot),
NewSlot: int(nf.Slot),
OldSlots: int(of.Slots),
NewSlots: int(nf.Slots),
Old: ot,
New: nt,
Conv: conv,
})
}
for _, of := range od.Fields {
if !used[of.Name] {
tc.Removed = append(tc.Removed, of.Name)
}
}
return nil
}