ast: Refactor type checker and environment state

This commit does not change any functionality it just updates the
implementation so that type environment mutation only occurs within
the type checker. This makes it easier to reason about changes to the
type environment data structure(s). Specifically:

* The type environment constructor is now private. Callers do not need
  to instantiate type environments. This should only be done by the
  checker. No one appears to be using this constructor so while this
  is backwards incompatible, it should be safe.

* The schema set is now held by the checker as opposed to the type
  environment. The environment should not have to know anything about
  the schemas.

* The global input schema is now loaded by the compiler on init() on
  and provided as input to the checker along with other global options
  like the schema set. This avoids having the compiler reach into the
  type environment to perform updates.

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
This commit is contained in:
Torin Sandall
2021-04-02 10:06:15 -04:00
parent ed75f771ed
commit 8b2e9ffc3a
4 changed files with 94 additions and 84 deletions
+50 -30
View File
@@ -27,6 +27,8 @@ type typeChecker struct {
errs Errors
exprCheckers map[string]exprChecker
varRewriter rewriteVars
ss *SchemaSet
input types.Type
}
// newTypeChecker returns a new typeChecker object that has no errors.
@@ -38,23 +40,57 @@ func newTypeChecker() *typeChecker {
return tc
}
func (tc *typeChecker) newEnv(exist *TypeEnv) *TypeEnv {
if exist != nil {
return exist.wrap()
}
env := newTypeEnv(tc.copy)
if tc.input != nil {
env.tree.Put(InputRootRef, tc.input)
}
return env
}
func (tc *typeChecker) copy() *typeChecker {
return newTypeChecker().
WithVarRewriter(tc.varRewriter).
WithSchemaSet(tc.ss).
WithInputType(tc.input)
}
func (tc *typeChecker) WithSchemaSet(ss *SchemaSet) *typeChecker {
tc.ss = ss
return tc
}
func (tc *typeChecker) WithVarRewriter(f rewriteVars) *typeChecker {
tc.varRewriter = f
return tc
}
func (tc *typeChecker) WithInputType(tpe types.Type) *typeChecker {
tc.input = tpe
return tc
}
// Env returns a type environment for the specified built-ins with any other
// global types configured on the checker. In practice, this is the default
// environment that other statements will be checked against.
func (tc *typeChecker) Env(builtins map[string]*Builtin) *TypeEnv {
env := tc.newEnv(nil)
for _, bi := range builtins {
env.tree.Put(bi.Ref(), bi.Decl)
}
return env
}
// CheckBody runs type checking on the body and returns a TypeEnv if no errors
// are found. The resulting TypeEnv wraps the provided one. The resulting
// TypeEnv will be able to resolve types of vars contained in the body.
func (tc *typeChecker) CheckBody(env *TypeEnv, body Body) (*TypeEnv, Errors) {
errors := []*Error{}
if env == nil {
env = NewTypeEnv()
} else {
env = env.wrap()
}
env = tc.newEnv(env)
WalkExprs(body, func(expr *Expr) bool {
@@ -94,11 +130,7 @@ func (tc *typeChecker) CheckBody(env *TypeEnv, body Body) (*TypeEnv, Errors) {
// are found. The resulting TypeEnv wraps the provided one. The resulting
// TypeEnv will be able to resolve types of refs that refer to rules.
func (tc *typeChecker) CheckTypes(env *TypeEnv, sorted []util.T) (*TypeEnv, Errors) {
if env == nil {
env = NewTypeEnv()
} else {
env = env.wrap()
}
env = tc.newEnv(env)
for _, s := range sorted {
tc.checkRule(env, s.(*Rule))
}
@@ -111,19 +143,19 @@ func (tc *typeChecker) checkClosures(env *TypeEnv, expr *Expr) Errors {
WalkClosures(expr, func(x interface{}) bool {
switch x := x.(type) {
case *ArrayComprehension:
_, errs := newTypeChecker().WithVarRewriter(tc.varRewriter).CheckBody(env, x.Body)
_, errs := tc.copy().CheckBody(env, x.Body)
if len(errs) > 0 {
result = errs
return true
}
case *SetComprehension:
_, errs := newTypeChecker().WithVarRewriter(tc.varRewriter).CheckBody(env, x.Body)
_, errs := tc.copy().CheckBody(env, x.Body)
if len(errs) > 0 {
result = errs
return true
}
case *ObjectComprehension:
_, errs := newTypeChecker().WithVarRewriter(tc.varRewriter).CheckBody(env, x.Body)
_, errs := tc.copy().CheckBody(env, x.Body)
if len(errs) > 0 {
result = errs
return true
@@ -134,25 +166,13 @@ func (tc *typeChecker) checkClosures(env *TypeEnv, expr *Expr) Errors {
return result
}
func (tc *typeChecker) checkLanguageBuiltins(env *TypeEnv, builtins map[string]*Builtin) *TypeEnv {
if env == nil {
env = NewTypeEnv()
} else {
env = env.wrap()
}
for _, bi := range builtins {
env.tree.Put(bi.Ref(), bi.Decl)
}
return env
}
func (tc *typeChecker) checkRule(env *TypeEnv, rule *Rule) {
env = env.wrap()
if schemaAnnots := getRuleAnnotation(rule); schemaAnnots != nil {
for _, schemaAnnot := range schemaAnnots {
ref, refType, err := processAnnotation(schemaAnnot, env, rule)
ref, refType, err := processAnnotation(tc.ss, schemaAnnot, env, rule)
if err != nil {
tc.err([]*Error{err})
continue
@@ -1121,8 +1141,8 @@ func getRuleAnnotation(rule *Rule) (sannots []SchemaAnnotation) {
// NOTE: Currently, annotations must preceed the rule. In the future, this
// restriction could be relaxed with other kinds of annotation scopes.
func processAnnotation(annot SchemaAnnotation, env *TypeEnv, rule *Rule) (Ref, types.Type, *Error) {
if env.schemaSet == nil {
func processAnnotation(ss *SchemaSet, annot SchemaAnnotation, env *TypeEnv, rule *Rule) (Ref, types.Type, *Error) {
if ss == nil {
return nil, nil, NewError(TypeErr, rule.Location, "schemas need to be supplied for the annotation: %s", annot.Schema)
}
@@ -1131,7 +1151,7 @@ func processAnnotation(annot SchemaAnnotation, env *TypeEnv, rule *Rule) (Ref, t
return nil, nil, NewError(TypeErr, rule.Location, "schema is not well formed in annotation: %s", annot.Schema)
}
schema := env.schemaSet.Get(schemaRef)
schema := ss.Get(schemaRef)
if schema == nil {
return nil, nil, NewError(TypeErr, rule.Location, "schema does not exist for given path in annotation: %s", schemaRef.String())
}
+7 -7
View File
@@ -286,7 +286,7 @@ func TestCheckInference(t *testing.T) {
t.Run(tc.note, func(t *testing.T) {
body := MustParseBody(tc.query)
checker := newTypeChecker()
env := checker.checkLanguageBuiltins(nil, BuiltinMap)
env := checker.Env(BuiltinMap)
env, err := checker.CheckBody(env, body)
if len(err) != 0 {
t.Fatalf("Unexpected error: %v", err)
@@ -528,7 +528,7 @@ func TestCheckErrorSuppression(t *testing.T) {
query = `_ = [true | count(1)]`
_, errs = newTypeChecker().CheckBody(newTypeChecker().checkLanguageBuiltins(nil, BuiltinMap), MustParseBody(query))
_, errs = newTypeChecker().CheckBody(newTypeChecker().Env(BuiltinMap), MustParseBody(query))
if len(errs) != 1 {
t.Fatalf("Expected exactly one error but got: %v", errs)
}
@@ -557,7 +557,7 @@ func TestCheckBadCardinality(t *testing.T) {
for _, test := range tests {
body := MustParseBody(test.body)
tc := newTypeChecker()
env := tc.checkLanguageBuiltins(nil, BuiltinMap)
env := tc.Env(BuiltinMap)
_, err := tc.CheckBody(env, body)
if len(err) != 1 || err[0].Code != TypeErr {
t.Fatalf("Expected 1 type error from %v but got: %v", body, err)
@@ -965,7 +965,7 @@ func TestFunctionTypeInferenceUnappliedWithObjectVarKey(t *testing.T) {
f(x) = y { y = {x: 1} }
`)
env, err := newTypeChecker().CheckTypes(newTypeChecker().checkLanguageBuiltins(nil, BuiltinMap), []util.T{
env, err := newTypeChecker().CheckTypes(newTypeChecker().Env(BuiltinMap), []util.T{
module.Rules[0],
})
@@ -1208,7 +1208,7 @@ func newTestEnv(rs []string) *TypeEnv {
}
}
env, err := newTypeChecker().CheckTypes(newTypeChecker().checkLanguageBuiltins(nil, BuiltinMap), elems)
env, err := newTypeChecker().CheckTypes(newTypeChecker().Env(BuiltinMap), elems)
if len(err) > 0 {
panic(err)
}
@@ -1863,8 +1863,8 @@ whocan[user] {
}
}
oldTypeEnv := newTypeChecker().checkLanguageBuiltins(nil, BuiltinMap).WithSchemas(tc.schemaSet)
typeenv, errors := newTypeChecker().CheckTypes(oldTypeEnv, elems)
oldTypeEnv := newTypeChecker().WithSchemaSet(tc.schemaSet).Env(BuiltinMap)
typeenv, errors := newTypeChecker().WithSchemaSet(tc.schemaSet).CheckTypes(oldTypeEnv, elems)
if len(errors) > 0 {
for _, e := range errors {
if tc.err == "" || !strings.Contains(e.Error(), tc.err) {
+26 -29
View File
@@ -107,6 +107,7 @@ type Compiler struct {
initialized bool // indicates if init() has been called
debug debug.Debug // emits debug information produced during compilation
schemaSet *SchemaSet // user-supplied schemas for input and data documents
inputType types.Type // global input type retrieved from schema set
}
// CompilerStage defines the interface for stages in the compiler.
@@ -220,7 +221,6 @@ func NewCompiler() *Compiler {
c := &Compiler{
Modules: map[string]*Module{},
TypeEnv: NewTypeEnv(),
RewrittenVars: map[Var]Var{},
ruleIndices: util.NewHashMap(func(a, b util.T) bool {
r1, r2 := a.(Ref), b.(Ref)
@@ -630,7 +630,7 @@ func (c *Compiler) RuleIndex(path Ref) RuleIndex {
// PassesTypeCheck determines whether the given body passes type checking
func (c *Compiler) PassesTypeCheck(body Body) bool {
checker := newTypeChecker()
checker := newTypeChecker().WithSchemaSet(c.schemaSet).WithInputType(c.inputType)
env := c.TypeEnv
_, errs := checker.CheckBody(env, body)
return len(errs) == 0
@@ -941,7 +941,10 @@ func parseSchema(schema interface{}) (types.Type, error) {
func (c *Compiler) checkTypes() {
// Recursion is caught in earlier step, so this cannot fail.
sorted, _ := c.Graph.Sort()
checker := newTypeChecker().WithVarRewriter(rewriteVarsInRef(c.RewrittenVars))
checker := newTypeChecker().
WithSchemaSet(c.schemaSet).
WithInputType(c.inputType).
WithVarRewriter(rewriteVarsInRef(c.RewrittenVars))
env, errs := checker.CheckTypes(c.TypeEnv, sorted)
for _, err := range errs {
c.err(err)
@@ -1016,35 +1019,26 @@ func (c *Compiler) init() {
c.builtins[name] = bi
}
tc := newTypeChecker()
c.TypeEnv = tc.checkLanguageBuiltins(nil, c.builtins)
c.setSchemas()
// Load the global input schema if one was provided.
if c.schemaSet != nil {
if schema := c.schemaSet.Get(InputRootRef); schema != nil {
tpe, err := loadSchema(schema)
if err != nil {
c.err(NewError(TypeErr, nil, err.Error()))
} else {
c.inputType = tpe
}
}
}
c.TypeEnv = newTypeChecker().
WithSchemaSet(c.schemaSet).
WithInputType(c.inputType).
Env(c.builtins)
c.initialized = true
}
func (c *Compiler) setSchemas() {
if c.schemaSet != nil {
// First, set the schemaSet in the type environment
c.TypeEnv.WithSchemas(c.schemaSet)
// Second, set the schema for the input globally if it exists
schema := c.schemaSet.Get(InputRootRef)
if schema == nil {
return
}
tpe, err := loadSchema(schema)
if err != nil {
c.err(NewError(TypeErr, nil, err.Error()))
return
}
c.TypeEnv.tree.Put(InputRootRef, tpe)
}
}
func (c *Compiler) err(err *Error) {
if c.maxErrs > 0 && len(c.Errors) >= c.maxErrs {
c.Errors = append(c.Errors, errLimitReached)
@@ -1658,7 +1652,10 @@ func (qc *queryCompiler) checkSafety(_ *QueryContext, body Body) (Body, error) {
func (qc *queryCompiler) checkTypes(qctx *QueryContext, body Body) (Body, error) {
var errs Errors
checker := newTypeChecker().WithVarRewriter(rewriteVarsInRef(qc.rewritten, qc.compiler.RewrittenVars))
checker := newTypeChecker().
WithSchemaSet(qc.compiler.schemaSet).
WithInputType(qc.compiler.inputType).
WithVarRewriter(rewriteVarsInRef(qc.rewritten, qc.compiler.RewrittenVars))
qc.typeEnv, errs = checker.CheckBody(qc.compiler.TypeEnv, body)
if len(errs) > 0 {
return nil, errs
+11 -18
View File
@@ -11,24 +11,20 @@ import (
// TypeEnv contains type info for static analysis such as type checking.
type TypeEnv struct {
tree *typeTreeNode
next *TypeEnv
schemaSet *SchemaSet
tree *typeTreeNode
next *TypeEnv
newChecker func() *typeChecker
}
// NewTypeEnv returns an empty TypeEnv.
func NewTypeEnv() *TypeEnv {
// newTypeEnv returns an empty TypeEnv. The constructor is not exported because
// type environments should only be created by the type checker.
func newTypeEnv(f func() *typeChecker) *TypeEnv {
return &TypeEnv{
tree: newTypeTree(),
tree: newTypeTree(),
newChecker: f,
}
}
// WithSchemas sets the user-provided schemas
func (env *TypeEnv) WithSchemas(schemas *SchemaSet) *TypeEnv {
env.schemaSet = schemas
return env
}
// Get returns the type of x.
func (env *TypeEnv) Get(x interface{}) types.Type {
@@ -101,22 +97,19 @@ func (env *TypeEnv) Get(x interface{}) types.Type {
// Comprehensions.
case *ArrayComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
cpy, errs := env.newChecker().CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewArray(nil, cpy.Get(x.Term))
}
return nil
case *ObjectComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
cpy, errs := env.newChecker().CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewObject(nil, types.NewDynamicProperty(cpy.Get(x.Key), cpy.Get(x.Value)))
}
return nil
case *SetComprehension:
checker := newTypeChecker()
cpy, errs := checker.CheckBody(env, x.Body)
cpy, errs := env.newChecker().CheckBody(env, x.Body)
if len(errs) == 0 {
return types.NewSet(cpy.Get(x.Term))
}