lang: unification, interfaces: Don't pass over generators

We were skipping over being fully consistent with all of the generator
invariants when running the solver. This allowed us to miss some of the
conditions that a generator might impose. Usually this caused us to be
"solved" when in fact we had an invalid program.
This commit is contained in:
James Shubin
2023-08-29 19:19:26 -04:00
parent 2214954c51
commit 318f28affd
4 changed files with 73 additions and 3 deletions

View File

@@ -671,6 +671,11 @@ type GeneratorInvariant struct {
// from the list. If we error, it's because we don't have any new // from the list. If we error, it's because we don't have any new
// information to provide at this time... // information to provide at this time...
Func func(invariants []Invariant, solved map[Expr]*types.Type) ([]Invariant, error) Func func(invariants []Invariant, solved map[Expr]*types.Type) ([]Invariant, error)
// Inactive specifies that we tried to run this, but it didn't help us
// progress forwards. It can be reset if needed. It should only be set
// or read by the solver itself.
Inactive bool
} }
// String returns a representation of this invariant. // String returns a representation of this invariant.

View File

@@ -0,0 +1,10 @@
-- main.mcl --
import "fmt"
# This should not unify, we need at least one arg.
# NOTE: We have historically needed to turn on the recursive solver to find that
# this was a bug!
test fmt.printf() {}
-- OUTPUT --
# err: errUnify: 2 unconsumed generators

View File

@@ -2,4 +2,4 @@
import "fmt" import "fmt"
test fmt.printf("%d%d", 42) {} # should not pass, missing second int test fmt.printf("%d%d", 42) {} # should not pass, missing second int
-- OUTPUT -- -- OUTPUT --
# err: errUnify: only recursive solutions left # err: errUnify: 2 unconsumed generators

View File

@@ -349,12 +349,39 @@ func SimpleInvariantSolver(invariants []interfaces.Invariant, expected []interfa
} }
} }
countGenerators := func() (int, int) {
active := 0
total := 0
for _, x := range equalities {
gen, ok := x.(*interfaces.GeneratorInvariant)
if !ok {
continue
}
total++ // total count
if gen.Inactive {
continue // skip inactive
}
active++ // active
}
return total, active
}
activeGenerators := func() int {
_, active := countGenerators()
return active
}
logf("%s: starting loop with %d equalities", Name, len(equalities)) logf("%s: starting loop with %d equalities", Name, len(equalities))
// run until we're solved, stop consuming equalities, or type clash // run until we're solved, stop consuming equalities, or type clash
Loop: Loop:
for { for {
// Once we're done solving everything else except the generators
// then we can exit, but we want to make sure the generators had
// a chance to "speak up" and make sure they were part of Unify.
// Every generator gets to run once, and if that does not change
// the result, then we mark it as inactive.
logf("%s: iterate...", Name) logf("%s: iterate...", Name)
if len(equalities) == 0 && len(exclusives) == 0 { if len(equalities) == 0 && len(exclusives) == 0 && activeGenerators() == 0 {
break // we're done, nothing left break // we're done, nothing left
} }
used := []int{} used := []int{}
@@ -988,6 +1015,13 @@ Loop:
continue continue
} }
// skip if the inactive flag has been set, as it
// won't produce any new (novel) inequalities we
// can use to progress to a result at this time.
if eq.Inactive {
continue
}
// If this returns nil, we add the invariants // If this returns nil, we add the invariants
// it returned and we remove it from the list. // it returned and we remove it from the list.
// If we error, it's because we don't have any // If we error, it's because we don't have any
@@ -995,6 +1029,8 @@ Loop:
// XXX: should we pass in `invariants` instead? // XXX: should we pass in `invariants` instead?
gi, err := eq.Func(equalities, solved) gi, err := eq.Func(equalities, solved)
if err != nil { if err != nil {
// set the inactive flag of this generator
eq.Inactive = true
continue continue
} }
@@ -1008,6 +1044,15 @@ Loop:
used = append(used, eqi) // mark equality as used up used = append(used, eqi) // mark equality as used up
logf("%s: solved `generator` equality", Name) logf("%s: solved `generator` equality", Name)
// reset all other generator equality "inactive" flags
for _, x := range equalities {
gen, ok := x.(*interfaces.GeneratorInvariant)
if !ok {
continue
}
gen.Inactive = false
}
continue continue
// wtf matching // wtf matching
@@ -1033,7 +1078,7 @@ Loop:
return nil, fmt.Errorf("unknown invariant type: %T", eqx) return nil, fmt.Errorf("unknown invariant type: %T", eqx)
} }
} // end inner for loop } // end inner for loop
if len(used) == 0 { if len(used) == 0 && activeGenerators() == 0 {
// Looks like we're now ambiguous, but if we have any // Looks like we're now ambiguous, but if we have any
// exclusives, recurse into each possibility to see if // exclusives, recurse into each possibility to see if
// one of them can help solve this! first one wins. Add // one of them can help solve this! first one wins. Add
@@ -1066,6 +1111,16 @@ Loop:
} }
} }
total, active := countGenerators()
// we still have work to do for consistency
if active > 0 {
continue Loop
}
if total > 0 {
return nil, fmt.Errorf("%d unconsumed generators", total)
}
// check for consistency against remaining invariants // check for consistency against remaining invariants
logf("%s: checking for consistency against %d exclusives...", Name, len(exclusives)) logf("%s: checking for consistency against %d exclusives...", Name, len(exclusives))
done := []int{} done := []int{}