lang: types: Plumb in a unification variable into our type

This is used for representing a unification variable in our type during
type unification. For example, this allows us to have a [?1] or a
map{?1:[?2]} and so on...
This commit is contained in:
James Shubin
2024-07-01 14:11:03 -04:00
parent 6066cbf075
commit 653299a88f
2 changed files with 517 additions and 21 deletions

View File

@@ -33,15 +33,20 @@ import (
"fmt"
"net"
"reflect"
"strconv"
"strings"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/disjoint"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// StructTag is the key we use in struct field names for key mapping.
StructTag = "lang"
// MaxInt8 is 127. It's max uint8: ^uint8(0), then we >> 1 for max int8.
MaxInt8 = int((^uint8(0)) >> 1)
)
// Basic types defined here as a convenience for use with Type.Cmp(X).
@@ -71,6 +76,8 @@ const (
KindStruct
KindFunc
KindVariant
KindUnification = Kind(MaxInt8) // keep this last
)
// Type is the datastructure representing any type. It can be recursive for
@@ -85,6 +92,20 @@ type Type struct {
Ord []string
Out *Type // if Kind == Func, use Map and Ord for Input, Out for Output
Var *Type // if Kind == Variant, use Var only
// unification variable (question mark, eg ?1, ?2)
Uni *Elem // if Kind == Unification (optional) use Uni only
}
// Elem is the type used for the unification variable in the Uni field of Type.
// We create this alias here to avoid needing to write *disjoint.Elem[*Type] all
// over. This is a golang type alias. These should be created with NewElem.
type Elem = disjoint.Elem[*Type]
// NewElem creates a new set with one element and returns the sole element (the
// representative element) of that set.
func NewElem() *Elem {
return disjoint.NewElem[*Type]()
}
// TypeOf takes a reflect.Type and returns an equivalent *Type. It removes any
@@ -340,6 +361,14 @@ func ConfigurableTypeOf(t reflect.Type, opts ...TypeOfOption) (*Type, error) {
// NewType creates the Type from the string representation.
func NewType(s string) *Type {
table := make(map[uint]*Elem)
return newType(s, table)
}
// newType creates the Type from the string representation. This private version
// takes a table so that we can collect unification variables as we see them and
// return a type with correctly unified unification variables.
func newType(s string, table map[uint]*Elem) *Type {
switch s {
case "bool":
return &Type{
@@ -361,7 +390,7 @@ func NewType(s string) *Type {
// KindList
if strings.HasPrefix(s, "[]") {
val := NewType(s[len("[]"):])
val := newType(s[len("[]"):], table)
if val == nil {
return nil
}
@@ -395,11 +424,11 @@ func NewType(s string) *Type {
return nil
}
key := NewType(strings.Trim(s[:found], " "))
key := newType(strings.Trim(s[:found], " "), table)
if key == nil {
return nil
}
val := NewType(strings.Trim(s[found+1:], " "))
val := newType(strings.Trim(s[found+1:], " "), table)
if val == nil {
return nil
}
@@ -457,7 +486,7 @@ func NewType(s string) *Type {
trim = 1
}
typ := NewType(strings.Trim(s[:found+1-trim], " "))
typ := newType(strings.Trim(s[:found+1-trim], " "), table)
if typ == nil {
return nil
}
@@ -557,7 +586,7 @@ func NewType(s string) *Type {
trim = 1
}
typ := NewType(strings.Trim(s[:found+1-trim], " "))
typ := newType(strings.Trim(s[:found+1-trim], " "), table)
if typ == nil {
return nil
}
@@ -568,7 +597,7 @@ func NewType(s string) *Type {
// return type
var tail *Type
if out != "" { // allow functions with no return type (in parser)
tail = NewType(out)
tail = newType(out, table)
if tail == nil {
return nil
}
@@ -589,6 +618,47 @@ func NewType(s string) *Type {
}
}
// KindUnification
if strings.HasPrefix(s, "?") {
// find end of number...
var length = 0 // number of digits
for i := len("?"); i < len(s); i++ {
c := s[i]
if length == 0 && c == '0' {
return nil // can't start with a zero
}
// Check manually because strconv.ParseUint accepts ^0x.
if '0' <= c && c <= '9' {
length++
continue
}
return nil // invalid char
}
v := s[len("?") : len("?")+length]
n, err := strconv.ParseUint(v, 10, 32) // base 10, 32 bits
if err != nil {
return nil // programming error or overflow
}
num := uint(n)
// XXX: Should we instead always return new unification
// variables, but call .Union() on all of the ones that have the
// same integer? Sam says they are equivalent.
uni, exists := table[num]
if !exists {
uni = NewElem() // unification variable, eg: ?1
table[num] = uni // store
}
// return a new type, may have an existing unification variable
return &Type{
Kind: KindUnification,
Uni: uni, // unification variable, eg: ?1
}
}
return nil // error (this also matches the empty string as input)
}
@@ -617,12 +687,21 @@ func (obj *Type) New() Value {
return NewFunc(obj)
case KindVariant:
return NewVariant(obj)
case KindUnification:
panic("can't make new value from unification variable kind")
}
panic("malformed type")
}
// String returns the textual representation for this type.
func (obj *Type) String() string {
table := make(map[*Elem]uint)
return obj.string(table)
}
// string returns the textual representation for this type. This is a private
// helper function that is used by the real String function.
func (obj *Type) string(table map[*Elem]uint) string {
switch obj.Kind {
case KindBool:
return "bool"
@@ -637,13 +716,13 @@ func (obj *Type) String() string {
if obj.Val == nil {
panic("malformed list type")
}
return "[]" + obj.Val.String()
return "[]" + obj.Val.string(table)
case KindMap:
if obj.Key == nil || obj.Val == nil {
panic("malformed map type")
}
return fmt.Sprintf("map{%s: %s}", obj.Key.String(), obj.Val.String())
return fmt.Sprintf("map{%s: %s}", obj.Key.string(table), obj.Val.string(table))
case KindStruct: // {a bool; b int}
if obj.Map == nil {
@@ -661,7 +740,7 @@ func (obj *Type) String() string {
if t == nil {
panic("malformed struct field")
}
s[i] = fmt.Sprintf("%s %s", k, t.String())
s[i] = fmt.Sprintf("%s %s", k, t.string(table))
}
return fmt.Sprintf("struct{%s}", strings.Join(s, "; "))
@@ -684,17 +763,37 @@ func (obj *Type) String() string {
// We need to print function arg names for Copy() to use
// the String() hack here and avoid erasing them here!
//s[i] = t.String()
s[i] = fmt.Sprintf("%s %s", k, t.String()) // strict
//s[i] = t.string(table)
s[i] = fmt.Sprintf("%s %s", k, t.string(table)) // strict
}
var out string
if obj.Out != nil {
out = fmt.Sprintf(" %s", obj.Out.String())
out = fmt.Sprintf(" %s", obj.Out.string(table))
}
return fmt.Sprintf("func(%s)%s", strings.Join(s, ", "), out)
case KindVariant:
return "variant"
case KindUnification:
if obj.Uni == nil {
panic("malformed unification variable")
}
// XXX: Should we instead run .IsConnected() on the two Elem
// unification variables to determine if they should have the
// same integer representation when printing them?
num, exists := table[obj.Uni]
if !exists {
for _, n := range table {
num = max(num, n)
}
num++ // add 1
table[obj.Uni] = num // store
}
//fmt.Printf("?%d: %p\n", int(num), obj.Uni.Find()) // debug
return "?" + strconv.Itoa(int(num))
}
panic("malformed type")
@@ -702,6 +801,14 @@ func (obj *Type) String() string {
// Cmp compares this type to another.
func (obj *Type) Cmp(typ *Type) error {
table1 := make(map[*Elem]uint) // for obj
table2 := make(map[*Elem]uint) // for typ
return obj.cmp(typ, table1, table2)
}
// cmp compares this type to another. This is a private helper function that is
// used by the real Cmp function.
func (obj *Type) cmp(typ *Type, table1, table2 map[*Elem]uint) error {
if obj == nil || typ == nil {
return fmt.Errorf("cannot compare to nil")
}
@@ -709,10 +816,10 @@ func (obj *Type) Cmp(typ *Type) error {
// TODO: is this correct?
// recurse into variants if we want base type comparisons
//if obj.Kind == KindVariant {
// return obj.Var.Cmp(t)
// return obj.Var.cmp(t, table1, table2)
//}
//if t.Kind == KindVariant {
// return obj.Cmp(t.Var)
// return obj.cmp(t.Var, table1, table2)
//}
if obj.Kind != typ.Kind {
@@ -732,14 +839,14 @@ func (obj *Type) Cmp(typ *Type) error {
if obj.Val == nil || typ.Val == nil {
panic("malformed list type")
}
return obj.Val.Cmp(typ.Val)
return obj.Val.cmp(typ.Val, table1, table2)
case KindMap:
if obj.Key == nil || obj.Val == nil || typ.Key == nil || typ.Val == nil {
panic("malformed map type")
}
kerr := obj.Key.Cmp(typ.Key)
verr := obj.Val.Cmp(typ.Val)
kerr := obj.Key.cmp(typ.Key, table1, table2)
verr := obj.Val.cmp(typ.Val, table1, table2)
if kerr != nil && verr != nil {
return errwrap.Append(kerr, verr) // two errors
}
@@ -775,7 +882,7 @@ func (obj *Type) Cmp(typ *Type) error {
if t1 == nil || t2 == nil {
panic("malformed struct field")
}
if err := t1.Cmp(t2); err != nil {
if err := t1.cmp(t2, table1, table2); err != nil {
return err
}
}
@@ -806,7 +913,7 @@ func (obj *Type) Cmp(typ *Type) error {
// if t1 == nil || t2 == nil {
// panic("malformed func arg")
// }
// if err := t1.Cmp(t2); err != nil {
// if err := t1.cmp(t2, table1, table2); err != nil {
// return err
// }
//}
@@ -829,13 +936,13 @@ func (obj *Type) Cmp(typ *Type) error {
panic("malformed func arg")
}
if err := t1.Cmp(t2); err != nil {
if err := t1.cmp(t2, table1, table2); err != nil {
return err
}
}
if obj.Out != nil || typ.Out != nil {
if err := obj.Out.Cmp(typ.Out); err != nil {
if err := obj.Out.cmp(typ.Out, table1, table2); err != nil {
return err
}
}
@@ -848,6 +955,39 @@ func (obj *Type) Cmp(typ *Type) error {
}
// TODO: should we Cmp obj.Var with typ.Var ? -- not necessarily
return nil
// used for testing
case KindUnification:
if obj.Uni == nil || typ.Uni == nil {
panic("malformed unification variable")
}
// If both types store and lookup variables symmetrically and in
// the same order, then the count's should also match.
// XXX: Should we instead run .IsConnected() on the two Elem
// unification variables to determine if they should have the
// same integer representation when printing them?
num1, exists := table1[obj.Uni]
if !exists {
for _, n := range table1 {
num1 = max(num1, n)
}
num1++ // add 1
table1[obj.Uni] = num1 // store
}
num2, exists := table2[typ.Uni]
if !exists {
for _, n := range table2 {
num2 = max(num2, n)
}
num2++ // add 1
table2[typ.Uni] = num2 // store
}
if num1 != num2 {
return fmt.Errorf("unbalanced unification variables")
}
return nil
}
return fmt.Errorf("unknown kind")
}
@@ -1040,6 +1180,97 @@ func (obj *Type) HasVariant() bool {
case KindVariant:
return true // found it!
case KindUnification:
return false // TODO: Do we want to panic here instead?
}
panic("malformed type")
}
// HasUni tells us if the type contains any unification variables.
func (obj *Type) HasUni() bool {
if obj == nil {
return false
}
if obj.Uni != nil {
return true // found it (by this method)
}
switch obj.Kind {
case KindBool:
return false
case KindStr:
return false
case KindInt:
return false
case KindFloat:
return false
case KindList:
if obj.Val == nil {
panic("malformed list type")
}
return obj.Val.HasUni()
case KindMap:
if obj.Key == nil || obj.Val == nil {
panic("malformed map type")
}
return obj.Key.HasUni() || obj.Val.HasUni()
case KindStruct: // {a bool; b int}
if obj.Map == nil {
panic("malformed struct type")
}
if len(obj.Map) != len(obj.Ord) {
panic("malformed struct length")
}
for _, k := range obj.Ord {
t, ok := obj.Map[k]
if !ok {
panic("malformed struct order")
}
if t == nil {
panic("malformed struct field")
}
if t.HasUni() {
return true
}
}
return false
case KindFunc:
if obj.Map == nil {
panic("malformed func type")
}
if len(obj.Map) != len(obj.Ord) {
panic("malformed func length")
}
for _, k := range obj.Ord {
t, ok := obj.Map[k]
if !ok {
panic("malformed func order")
}
if t == nil {
panic("malformed func field")
}
if t.HasUni() {
return true
}
}
if obj.Out != nil {
if obj.Out.HasUni() {
return true
}
}
return false
case KindVariant:
return obj.Var.HasUni()
case KindUnification:
return true // found it!
}
panic("malformed type")
@@ -1053,6 +1284,7 @@ func (obj *Type) HasVariant() bool {
// string, and if it is compatible with the variant type it will be "variant"...
// Comparing to a partial can only match "impossible" (error) or possible (nil).
// This now also supports comparing a partial type to a variant type as well...
// TODO: Should we support KindUnification somehow?
func (obj *Type) ComplexCmp(typ *Type) (string, error) {
// match simple "placeholder" variants... skip variants w/ sub types
isVariant := func(t *Type) bool { return t != nil && t.Kind == KindVariant && t.Var == nil }