lang: Catch duplicate resource fields or meta entries statically

This teaches the compiler to catch entries with duplicate fields, and
duplicate meta entries, because it could be ambiguous to determine which
should take precedence. For example, if you specified `content` to a
file resource twice, this should error. This is known statically, so we
can catch it. If you specified two `Meta:noop` entries, this can also be
caught.

The interesting part happens when you specify one `Meta:noop` entry, and
one `Meta` entry which happens to contain a noop field in the struct.
For this, we actually have to wait until type unification is finished,
and catch the error there. This is because after type unification we
will know the precise type of the struct being passed to `Meta`, and so
we can look at its field names, even if their values aren't yet known
because the graph hasn't run yet.
This commit is contained in:
James Shubin
2021-05-07 22:59:11 -04:00
parent 0756133a7e
commit 37be9fda9f
18 changed files with 153 additions and 73 deletions

View File

@@ -1,43 +0,0 @@
Edge: bool(false) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # noop
Edge: bool(false) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # rewatch
Edge: bool(true) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # autoedge
Edge: bool(true) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # autogroup
Edge: bool(true) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # realize
Edge: bool(true) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # reverse
Edge: bool(true) -> var(b) # var:b
Edge: bool(true) -> var(b) # var:b
Edge: float(4.2) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # limit
Edge: int(-1) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # retry
Edge: int(0) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # delay
Edge: int(3) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # burst
Edge: int(5) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # poll
Edge: list(str("foo:1"), str("bar:3")) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # sema
Edge: str("bar:3") -> list(str("foo:1"), str("bar:3")) # 1
Edge: str("foo:1") -> list(str("foo:1"), str("bar:3")) # 0
Edge: str("hello world") -> call:fmt.printf(str("hello world")) # format
Vertex: bool(false)
Vertex: bool(false)
Vertex: bool(false)
Vertex: bool(false)
Vertex: bool(true)
Vertex: bool(true)
Vertex: bool(true)
Vertex: bool(true)
Vertex: bool(true)
Vertex: bool(true)
Vertex: bool(true)
Vertex: call:fmt.printf(str("hello world"))
Vertex: float(4.2)
Vertex: int(-1)
Vertex: int(0)
Vertex: int(3)
Vertex: int(42)
Vertex: int(5)
Vertex: list(str("foo:1"), str("bar:3"))
Vertex: str("bar:3")
Vertex: str("foo:1")
Vertex: str("greeting")
Vertex: str("hello world")
Vertex: struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true))
Vertex: var(b)
Vertex: var(b)

View File

@@ -1,27 +0,0 @@
import "fmt"
$b = true
test "greeting" {
anotherstr => fmt.printf("hello world"),
Meta => $b ?: struct{
noop => false,
retry => -1,
delay => 0,
poll => 5,
limit => 4.2,
burst => 3,
sema => ["foo:1", "bar:3",],
rewatch => false,
realize => true,
reverse => true,
autoedge => true,
autogroup => true,
},
Meta:noop => false,
Meta:noop => true, # duplicates allowed atm, but not recommended!
Meta:poll => $b ?: 42,
Meta:autoedge => true,
Meta:autogroup => false,
}

View File

@@ -0,0 +1 @@
# err: err4: resource has duplicate meta entry of: noop

View File

@@ -0,0 +1,22 @@
test "test" {
anotherstr => "test",
Meta => true ?: struct{
noop => false,
retry => -1,
delay => 0,
poll => 5,
limit => 4.2,
burst => 3,
sema => ["foo:1", "bar:3",],
rewatch => false,
realize => true,
reverse => true,
autoedge => true,
autogroup => true,
},
Meta:noop => false,
#Meta:poll => $b ?: 42,
#Meta:autoedge => true,
#Meta:autogroup => false,
}

View File

@@ -0,0 +1 @@
# err: errInit: resource has duplicate field of: anotherstr

View File

@@ -0,0 +1,4 @@
test "test" {
anotherstr => "hello",
anotherstr => "hello", # values aren't checked in dupe checks
}

View File

@@ -0,0 +1 @@
# err: errInit: resource has duplicate field of: anotherstr

View File

@@ -0,0 +1,4 @@
test "test" {
anotherstr => "hello",
anotherstr => "hello world", # values aren't checked in dupe checks
}

View File

@@ -0,0 +1 @@
# err: errInit: resource has duplicate meta entry of: noop

View File

@@ -0,0 +1,5 @@
test "test" {
anotherstr => "test",
Meta:noop => false,
Meta:noop => false,
}

View File

@@ -0,0 +1 @@
# err: errInit: resource has duplicate meta entry of: noop

View File

@@ -0,0 +1,5 @@
test "test" {
anotherstr => "test",
Meta:noop => false,
Meta:noop => true,
}

View File

@@ -0,0 +1,22 @@
# XXX: should error at graph unification, but we have a type unification bug
#test "test" {
# anotherstr => "test",
#
# Meta => true ?: struct{
# noop => false,
# retry => -1,
# delay => 0,
# poll => 5,
# limit => 4.2,
# burst => 3,
# sema => ["foo:1", "bar:3",],
# rewatch => false,
# realize => true,
# reverse => true,
# autoedge => true,
# autogroup => true,
# },
# Meta => true ?: struct{
# noop => false,
# },
#}

View File

@@ -0,0 +1,21 @@
# XXX: should work, but we have a type unification bug
#test "test" {
# anotherstr => "test",
#
# Meta => true ?: struct{
# retry => -1,
# delay => 0,
# realize => true,
# autogroup => true,
# },
# Meta => true ?: struct{
# poll => 5,
# limit => 4.2,
# burst => 3,
# sema => ["foo:1", "bar:3",],
# rewatch => false,
# reverse => true,
# },
# Meta:noop => true,
# Meta:autoedge => true,
#}

View File

@@ -222,7 +222,6 @@ func TestInterpret4(t *testing.T) {
int64 => 42,
boolptr => true,
# comment 3
stringptr => "the actual field name is: StringPtr", # comment 4
int8ptr => 99, # comment 5
comment => "☺\thello\nwo\"rld\\2", # must escape these
}
@@ -240,8 +239,6 @@ func TestInterpret4(t *testing.T) {
x.Int64 = 42
b := true
x.BoolPtr = &b
stringptr := "the actual field name is: StringPtr"
x.StringPtr = &stringptr
int8ptr := int8(99)
x.Int8Ptr = &int8ptr
x.Comment = "☺\thello\nwo\"rld\\2" // must escape the escaped chars

View File

@@ -310,7 +310,32 @@ func (obj *StmtRes) Init(data *interfaces.Data) error {
if err := obj.Name.Init(data); err != nil {
return err
}
fieldNames := make(map[string]struct{})
metaNames := make(map[string]struct{})
for _, x := range obj.Contents {
// Duplicate checking for identical field names.
if line, ok := x.(*StmtResField); ok {
// Was the field already seen in this resource?
if _, exists := fieldNames[line.Field]; exists {
return fmt.Errorf("resource has duplicate field of: %s", line.Field)
}
fieldNames[line.Field] = struct{}{}
}
// NOTE: you can have as many *StmtResEdge lines as you want =D
if line, ok := x.(*StmtResMeta); ok {
// Was the meta entry already seen in this resource?
// Ignore the generic MetaField struct field for now.
// You're allowed to have more than one Meta field, but
// they can't contain the same field twice.
if _, exists := metaNames[line.Property]; exists && line.Property != MetaField {
return fmt.Errorf("resource has duplicate meta entry of: %s", line.Property)
}
metaNames[line.Property] = struct{}{}
}
if err := x.Init(data); err != nil {
return err
}
@@ -518,7 +543,47 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) {
// to the resources created, but rather, once all the values (expressions) with
// no outgoing edges have produced at least a single value, then the resources
// know they're able to be built.
//
// This runs right after type unification. For this particular resource, we can
// do some additional static analysis, but only after unification has been done.
// Since I don't think it's worth extending the Stmt API for this, we can do the
// checks here at the beginning, and error out if something was invalid. In this
// particular case, the issue is one of catching duplicate meta fields.
func (obj *StmtRes) Graph() (*pgraph.Graph, error) {
metaNames := make(map[string]struct{})
for _, x := range obj.Contents {
line, ok := x.(*StmtResMeta)
if !ok {
continue
}
properties := []string{line.Property} // "noop" or "Meta" or...
if line.Property == MetaField {
// If this is the generic MetaField struct field, then
// we lookup the type signature to see which fields are
// defined. You're allowed to have more than one Meta
// field, but they can't contain the same field twice.
typ, err := line.MetaExpr.Type() // must be known now
if err != nil {
// programming error in type unification
return nil, errwrap.Wrapf(err, "unknown resource meta type")
}
if t := typ.Kind; t != types.KindStruct {
return nil, fmt.Errorf("unexpected resource meta kind of: %s", t)
}
properties = typ.Ord // list of field names in this struct
}
for _, property := range properties {
// Was the meta entry already seen in this resource?
if _, exists := metaNames[property]; exists {
return nil, fmt.Errorf("resource has duplicate meta entry of: %s", property)
}
metaNames[property] = struct{}{}
}
}
graph, err := pgraph.NewGraph("res")
if err != nil {
return nil, errwrap.Wrapf(err, "could not create graph")