Skip to content

Commit 60ed3ba

Browse files
authored
Merge pull request #1409 from informalsystems/gabriela/type-quantification-fix
Fix the type quantification strategy
2 parents cb460f3 + 401ca15 commit 60ed3ba

File tree

5 files changed

+129
-106
lines changed

5 files changed

+129
-106
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131

3232
- Removed a dependency causing deprecation errors messages to be emitted.
3333
(#1380)
34+
- Fixed a type checker bug causing too general types to be inferred (#1409).
3435

3536
### Security
3637

examples/cosmwasm/zero-to-hero/vote.qnt

+1-4
Original file line numberDiff line numberDiff line change
@@ -489,10 +489,7 @@ module state {
489489
)
490490
// assert that aggregated sum in `polls[poll_id]` equals the sum from above
491491
val poll = state.polls.get(poll_id)
492-
poll.options.listForall(option =>
493-
val optionKey = option._1
494-
// FIXME(#1167): Type annotation below is a workaround, inferred type is too general
495-
val optionSum: int = option._2
492+
poll.options.listForall(((optionKey, optionSum)) =>
496493
// `ballots` only contains entries if there are > 0 votes.
497494
optionSum > 0 implies and {
498495
summed_ballots.keys().contains(optionKey),

quint/src/types/base.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ export type Constraint =
2121
*/
2222
const constraintKinds = ['empty', 'eq', 'conjunction', 'isDefined'] as const
2323

24-
export interface TypeScheme {
25-
type: QuintType
24+
export interface QuantifiedVariables {
2625
typeVariables: Set<string>
2726
rowVariables: Set<string>
2827
}
2928

29+
export type TypeScheme = { type: QuintType } & QuantifiedVariables
30+
3031
export type Signature = (_arity: number) => TypeScheme
3132

3233
// Does not bind any type variables in `type`, which we take to assume

quint/src/types/constraintGenerator.ts

+108-84
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* ----------------------------------------------------------------------------------
2-
* Copyright 2022 Informal Systems
2+
* Copyright 2022-2024 Informal Systems
33
* Licensed under the Apache License, Version 2.0.
44
* See LICENSE in the project root for license information.
55
* --------------------------------------------------------------------------------- */
@@ -18,7 +18,7 @@ import {
1818
QuintAssume,
1919
QuintBool,
2020
QuintConst,
21-
QuintDef,
21+
QuintDeclaration,
2222
QuintEx,
2323
QuintInstance,
2424
QuintInt,
@@ -35,7 +35,7 @@ import { expressionToString, rowToString, typeToString } from '../ir/IRprinting'
3535
import { Either, left, mergeInMany, right } from '@sweet-monads/either'
3636
import { Error, ErrorTree, buildErrorLeaf, buildErrorTree, errorTreeToString } from '../errorTree'
3737
import { getSignatures } from './builtinSignatures'
38-
import { Constraint, Signature, TypeScheme, toScheme } from './base'
38+
import { Constraint, QuantifiedVariables, Signature, TypeScheme, toScheme } from './base'
3939
import { Substitutions, applySubstitution, compose } from './substitutions'
4040
import { LookupTable } from '../names/base'
4141
import {
@@ -95,7 +95,7 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
9595
}
9696
}
9797

98-
protected types: Map<bigint, TypeScheme> = new Map<bigint, TypeScheme>()
98+
protected types: Map<bigint, TypeScheme> = new Map()
9999
protected errors: Map<bigint, ErrorTree> = new Map<bigint, ErrorTree>()
100100
protected freshVarGenerator: FreshVarGenerator
101101
protected table: LookupTable
@@ -104,13 +104,14 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
104104
private solvingFunction: SolvingFunctionType
105105
private builtinSignatures: Map<string, Signature> = getSignatures()
106106

107+
// A map to save which type variables were free when we started visiting an opdef or an assume
108+
protected tvs: Map<bigint, QuantifiedVariables> = new Map()
109+
// Temporary type map only for types in scope for a certain declaration
110+
protected typesInScope: Map<bigint, TypeScheme> = new Map()
111+
107112
// Track location descriptions for error tree traces
108113
private location: string = ''
109114

110-
// A stack of free type variables and row variables for lambda expressions.
111-
// Nested lambdas add new entries to the stack, and pop them when exiting.
112-
private freeNames: { typeVariables: Set<string>; rowVariables: Set<string> }[] = []
113-
114115
getResult(): [Map<bigint, ErrorTree>, Map<bigint, TypeScheme>] {
115116
return [this.errors, this.types]
116117
}
@@ -119,18 +120,6 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
119120
this.location = `Generating constraints for ${expressionToString(e)}`
120121
}
121122

122-
exitDef(def: QuintDef) {
123-
if (this.constraints.length > 0) {
124-
this.solveConstraints().map(subs => {
125-
if (isAnnotatedDef(def)) {
126-
checkAnnotationGenerality(subs, def.typeAnnotation).mapLeft(err =>
127-
this.errors.set(def.typeAnnotation?.id ?? def.id, err)
128-
)
129-
}
130-
})
131-
}
132-
}
133-
134123
exitVar(e: QuintVar) {
135124
this.addToResults(e.id, right(toScheme(e.typeAnnotation)))
136125
}
@@ -242,22 +231,14 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
242231
}
243232

244233
enterLambda(expr: QuintLambda) {
245-
const lastParamNames = this.currentFreeNames()
246-
const paramNames = {
247-
typeVariables: new Set(lastParamNames.typeVariables),
248-
rowVariables: new Set(lastParamNames.rowVariables),
249-
}
250234
expr.params.forEach(p => {
251235
const varName = p.name === '_' ? this.freshVarGenerator.freshVar('_t') : `t_${p.name}_${p.id}`
252-
paramNames.typeVariables.add(varName)
253236
const paramTypeVar: QuintVarType = { kind: 'var', name: varName }
254237
this.addToResults(p.id, right(toScheme(paramTypeVar)))
255238
if (p.typeAnnotation) {
256239
this.constraints.push({ kind: 'eq', types: [paramTypeVar, p.typeAnnotation], sourceId: p.id })
257240
}
258241
})
259-
260-
this.freeNames.push(paramNames)
261242
}
262243

263244
// Γ ∪ {p0: t0, ..., pn: tn} ⊢ e: (te, c) t0, ..., tn are fresh
@@ -281,7 +262,6 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
281262
})
282263

283264
this.addToResults(e.id, result)
284-
this.freeNames.pop()
285265
}
286266

287267
// Γ ⊢ e1: (t1, c1) s = solve(c1) s(Γ ∪ {n: t1}) ⊢ e2: (t2, c2)
@@ -292,22 +272,58 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
292272
return
293273
}
294274

295-
// TODO: Occurs check on operator body to prevent recursion, see https://github.com/informalsystems/quint/issues/171
296-
297275
this.addToResults(e.id, this.fetchResult(e.expr.id))
298276
}
299277

300-
exitOpDef(e: QuintOpDef) {
278+
exitDecl(_def: QuintDeclaration) {
279+
this.typesInScope = new Map()
280+
}
281+
282+
enterOpDef(def: QuintOpDef) {
283+
// Save which type variables were free when we started visiting this op def
284+
const tvs = this.freeNamesInScope()
285+
this.tvs.set(def.id, tvs)
286+
}
287+
288+
exitOpDef(def: QuintOpDef) {
301289
if (this.errors.size !== 0) {
302290
return
303291
}
304292

305-
this.fetchResult(e.expr.id).map(t => {
306-
this.addToResults(e.id, right(this.quantify(t.type)))
307-
if (e.typeAnnotation) {
308-
this.constraints.push({ kind: 'eq', types: [t.type, e.typeAnnotation], sourceId: e.id })
293+
this.fetchResult(def.expr.id).map(t => {
294+
if (def.typeAnnotation) {
295+
this.constraints.push({ kind: 'eq', types: [t.type, def.typeAnnotation], sourceId: def.id })
309296
}
310297
})
298+
299+
const tvs_before = this.tvs.get(def.id)!
300+
301+
if (this.constraints.length > 0) {
302+
this.solveConstraints().map(subs => {
303+
// For every free name we are binding in the substitutions, the names occuring in the value of the substitution
304+
// have to become free as well.
305+
addBindingsToFreeTypes(tvs_before, subs)
306+
307+
if (isAnnotatedDef(def)) {
308+
checkAnnotationGenerality(subs, def.typeAnnotation).mapLeft(err =>
309+
this.errors.set(def.typeAnnotation?.id ?? def.id, err)
310+
)
311+
}
312+
})
313+
}
314+
315+
const tvs = this.freeNamesInScope()
316+
// Any new free names, that were not free before, have to be quantified
317+
const toQuantify = variablesDifference(tvs, tvs_before)
318+
319+
this.fetchResult(def.expr.id).map(t => {
320+
this.addToResults(def.id, right(quantify(toQuantify, t.type)))
321+
})
322+
}
323+
324+
enterAssume(e: QuintAssume) {
325+
const tvs = this.freeNamesInScope()
326+
this.tvs.set(e.id, tvs)
311327
}
312328

313329
exitAssume(e: QuintAssume) {
@@ -316,15 +332,21 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
316332
}
317333

318334
this.fetchResult(e.assumption.id).map(t => {
319-
this.addToResults(e.id, right(this.quantify(t.type)))
335+
const tvs_before = this.tvs.get(e.id)!
336+
const tvs = this.freeNamesInScope()
337+
const toQuantify = variablesDifference(tvs, tvs_before)
338+
this.addToResults(e.id, right(quantify(toQuantify, t.type)))
320339
this.constraints.push({ kind: 'eq', types: [t.type, { kind: 'bool' }], sourceId: e.id })
321340
})
322341
}
323342

324343
private addToResults(exprId: bigint, result: Either<Error, TypeScheme>) {
325344
result
326345
.mapLeft(err => this.errors.set(exprId, buildErrorTree(this.location, err)))
327-
.map(r => this.types.set(exprId, r))
346+
.map(r => {
347+
this.typesInScope.set(exprId, r)
348+
this.types.set(exprId, r)
349+
})
328350
}
329351

330352
private fetchResult(id: bigint): Either<ErrorTree, TypeScheme> {
@@ -348,16 +370,9 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
348370
return this.solvingFunction(this.table, constraint)
349371
.mapLeft(errors => errors.forEach((err, id) => this.errors.set(id, err)))
350372
.map(subs => {
351-
// For every free name we are binding in the substitutions, the names occuring in the value of the substitution
352-
// have to become free as well.
353-
this.addBindingsToFreeNames(subs)
354-
355-
// Apply substitution to environment
356-
// FIXME: We have to figure out the scope of the constraints/substitutions
357-
// https://github.com/informalsystems/quint/issues/690
358-
this.types.forEach((oldScheme, id) => {
373+
this.typesInScope.forEach((oldScheme, id) => {
359374
const newType = applySubstitution(this.table, subs, oldScheme.type)
360-
const newScheme: TypeScheme = this.quantify(newType)
375+
const newScheme: TypeScheme = { ...oldScheme, type: newType }
361376
this.addToResults(id, right(newScheme))
362377
})
363378

@@ -406,45 +421,18 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
406421
return applySubstitution(this.table, subs, t.type)
407422
}
408423

409-
private currentFreeNames(): { typeVariables: Set<string>; rowVariables: Set<string> } {
410-
return (
411-
this.freeNames[this.freeNames.length - 1] ?? {
412-
typeVariables: new Set(),
413-
rowVariables: new Set(),
414-
}
424+
private freeNamesInScope(): QuantifiedVariables {
425+
return [...this.typesInScope.values()].reduce(
426+
(acc, t) => {
427+
const names = freeTypes(t)
428+
return {
429+
typeVariables: new Set([...names.typeVariables, ...acc.typeVariables]),
430+
rowVariables: new Set([...names.rowVariables, ...acc.rowVariables]),
431+
}
432+
},
433+
{ typeVariables: new Set(), rowVariables: new Set() }
415434
)
416435
}
417-
418-
private quantify(type: QuintType): TypeScheme {
419-
const freeNames = this.currentFreeNames()
420-
const nonFreeNames = {
421-
typeVariables: new Set([...typeNames(type).typeVariables].filter(name => !freeNames.typeVariables.has(name))),
422-
rowVariables: new Set([...typeNames(type).rowVariables].filter(name => !freeNames.rowVariables.has(name))),
423-
}
424-
return { ...nonFreeNames, type }
425-
}
426-
427-
private addBindingsToFreeNames(substitutions: Substitutions) {
428-
// Assumes substitutions are topologically sorted, i.e. [ t0 |-> (t1, t2), t1 |-> (t3, t4) ]
429-
substitutions.forEach(s => {
430-
switch (s.kind) {
431-
case 'type':
432-
this.freeNames
433-
.filter(free => free.typeVariables.has(s.name))
434-
.forEach(free => {
435-
const names = typeNames(s.value)
436-
names.typeVariables.forEach(v => free.typeVariables.add(v))
437-
names.rowVariables.forEach(v => free.rowVariables.add(v))
438-
})
439-
return
440-
case 'row':
441-
this.freeNames
442-
.filter(free => free.rowVariables.has(s.name))
443-
.forEach(free => rowNames(s.value).forEach(v => free.rowVariables.add(v)))
444-
return
445-
}
446-
})
447-
}
448436
}
449437

450438
function checkAnnotationGenerality(
@@ -477,3 +465,39 @@ function checkAnnotationGenerality(
477465
return right(subs)
478466
}
479467
}
468+
469+
function quantify(tvs: QuantifiedVariables, type: QuintType): TypeScheme {
470+
return { ...tvs, type }
471+
}
472+
473+
function freeTypes(t: TypeScheme): QuantifiedVariables {
474+
const allNames = typeNames(t.type)
475+
return variablesDifference(allNames, { typeVariables: t.typeVariables, rowVariables: t.rowVariables })
476+
}
477+
478+
function addBindingsToFreeTypes(free: QuantifiedVariables, substitutions: Substitutions): void {
479+
// Assumes substitutions are topologically sorted, i.e. [ t0 |-> (t1, t2), t1 |-> (t3, t4) ]
480+
substitutions.forEach(s => {
481+
switch (s.kind) {
482+
case 'type':
483+
if (free.typeVariables.has(s.name)) {
484+
const names = typeNames(s.value)
485+
names.typeVariables.forEach(v => free.typeVariables.add(v))
486+
names.rowVariables.forEach(v => free.rowVariables.add(v))
487+
}
488+
return
489+
case 'row':
490+
if (free.rowVariables.has(s.name)) {
491+
rowNames(s.value).forEach(v => free.rowVariables.add(v))
492+
}
493+
return
494+
}
495+
})
496+
}
497+
498+
function variablesDifference(a: QuantifiedVariables, b: QuantifiedVariables): QuantifiedVariables {
499+
return {
500+
typeVariables: new Set([...a.typeVariables].filter(tv => !b.typeVariables.has(tv))),
501+
rowVariables: new Set([...a.rowVariables].filter(tv => !b.rowVariables.has(tv))),
502+
}
503+
}

0 commit comments

Comments
 (0)