engine, lang: Allow resources with a field of type interface

This lets us add a resource that has an implementation with a field
whose type is determined at compile time. This let's us write more
flexible resources.

What's missing is additional type checking so that we guarantee that a
specific resource doesn't change types during run-time.
This commit is contained in:
James Shubin
2023-11-12 16:16:47 -05:00
parent 9a1a81925e
commit b048b2684b
7 changed files with 62 additions and 1 deletions

View File

@@ -93,6 +93,13 @@ func (obj *Engine) SendRecv(res engine.RecvableRes) (map[string]bool, error) {
value2 := obj2.FieldByName(key2) value2 := obj2.FieldByName(key2)
kind2 := value2.Kind() kind2 := value2.Kind()
// For situations where we send a variant to the resource!
// TODO: should this be a for loop to un-nest multiple times?
if kind1 == reflect.Interface {
value1 = value1.Elem() // un-nest one interface
kind1 = value1.Kind()
}
if obj.Debug { if obj.Debug {
obj.Logf("Send(%s) has %v: %v", type1, kind1, value1) obj.Logf("Send(%s) has %v: %v", type1, kind1, value1)
obj.Logf("Recv(%s) has %v: %v", type2, kind2, value2) obj.Logf("Recv(%s) has %v: %v", type2, kind2, value2)

View File

@@ -192,6 +192,12 @@ func StructFieldCompat(st1 interface{}, key1 string, st2 interface{}, key2 strin
return fmt.Errorf("can't interface the recv") return fmt.Errorf("can't interface the recv")
} }
// If we're sending _from_ an interface...
if kind1 == reflect.Interface {
// TODO: Can we do more checks instead of only returning early?
return nil
}
if kind1 != kind2 { if kind1 != kind2 {
return fmt.Errorf("field kind mismatch between %s and %s", kind1, kind2) return fmt.Errorf("field kind mismatch between %s and %s", kind1, kind2)
} }

View File

@@ -746,7 +746,13 @@ func (obj *StmtRes) resource(table map[interfaces.Func]types.Value, resName stri
if err != nil { if err != nil {
return nil, errwrap.Wrapf(err, "resource field `%s` has no compatible type", x.Field) return nil, errwrap.Wrapf(err, "resource field `%s` has no compatible type", x.Field)
} }
if err := t.Cmp(typ); err != nil { if t == nil {
// possible programming error
return nil, fmt.Errorf("resource field `%s` of nil type cannot match type `%+v`", x.Field, typ)
}
// Let the variants pass through...
if err := t.Cmp(typ); err != nil && t.Kind != types.KindVariant {
return nil, errwrap.Wrapf(err, "resource field `%s` of type `%+v`, cannot take type `%+v`", x.Field, t, typ) return nil, errwrap.Wrapf(err, "resource field `%s` of type `%+v`, cannot take type `%+v`", x.Field, t, typ)
} }
@@ -1320,6 +1326,29 @@ func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) {
if !exists { if !exists {
return nil, fmt.Errorf("field `%s` does not exist in `%s`", obj.Field, kind) return nil, fmt.Errorf("field `%s` does not exist in `%s`", obj.Field, kind)
} }
if typ == nil {
// possible programming error
return nil, fmt.Errorf("type for field `%s` in `%s` is nil", obj.Field, kind)
}
if typ.Kind == types.KindVariant { // special path, res field has interface{}
if typ.Var == nil {
invar := &interfaces.AnyInvariant{
Expr: obj.Value,
}
invariants = append(invariants, invar)
return invariants, nil
}
// in case it is present (nil is okay too)
invar := &interfaces.EqualsInvariant{
Expr: obj.Value,
Type: typ.Var, // in case it is present (nil is okay too)
}
invariants = append(invariants, invar)
return invariants, nil
}
// regular scenario
invar := &interfaces.EqualsInvariant{ invar := &interfaces.EqualsInvariant{
Expr: obj.Value, Expr: obj.Value,
Type: typ, Type: typ,

View File

@@ -942,6 +942,11 @@ func TestAstFunc2(t *testing.T) {
t.Errorf("test #%d: unification passed, expected fail", index) t.Errorf("test #%d: unification passed, expected fail", index)
return return
} }
// XXX: Should we do a kind of SetType on resources here
// to tell the ones with variant fields what their
// concrete field types are? They should only be dynamic
// in implementation and before unification, and static
// once we've unified the specific resource.
// build the function graph // build the function graph
graph, err := iast.Graph() graph, err := iast.Graph()

View File

@@ -202,6 +202,10 @@ func (obj *Lang) Init() error {
if err := unifier.Unify(); err != nil { if err := unifier.Unify(); err != nil {
return errwrap.Wrapf(err, "could not unify types") return errwrap.Wrapf(err, "could not unify types")
} }
// XXX: Should we do a kind of SetType on resources here to tell the
// ones with variant fields what their concrete field types are? They
// should only be dynamic in implementation and before unification, and
// static once we've unified the specific resource.
obj.Logf("building function graph...") obj.Logf("building function graph...")
// we assume that for some given code, the list of funcs doesn't change // we assume that for some given code, the list of funcs doesn't change

View File

@@ -98,6 +98,7 @@ func ResTypeOf(t reflect.Type) (*Type, error) {
StructTagOpt(StructTag), StructTagOpt(StructTag),
StrictStructTagOpt(true), StrictStructTagOpt(true),
SkipBadStructFieldsOpt(true), SkipBadStructFieldsOpt(true),
AllowInterfaceTypeOpt(true),
} }
return ConfigurableTypeOf(t, opts...) return ConfigurableTypeOf(t, opts...)
} }

View File

@@ -283,6 +283,15 @@ func Into(v Value, rv reflect.Value) error {
if typ == nil { if typ == nil {
return fmt.Errorf("cannot Into() %+v of type %s into a nil type", v, v.Type()) return fmt.Errorf("cannot Into() %+v of type %s into a nil type", v, v.Type())
} }
// This is used when we are setting a resource field which has type of
// interface{} instead of a string, bool, list, etc...
if isInterface := typ.Kind() == reflect.Interface; isInterface {
//x := reflect.ValueOf(v) // no!
// use the value with type interface{}, not types.Value
x := reflect.ValueOf(v.Value())
rv.Set(x)
return nil
}
switch v := v.(type) { switch v := v.(type) {
case *BoolValue: case *BoolValue: