Skip to content

Commit 899360a

Browse files
committed
Fix the type quantification strategy and corresponding tests
1 parent 393ec51 commit 899360a

File tree

4 files changed

+128
-106
lines changed

4 files changed

+128
-106
lines changed

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
}
@@ -244,22 +233,14 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
244233
}
245234

246235
enterLambda(expr: QuintLambda) {
247-
const lastParamNames = this.currentFreeNames()
248-
const paramNames = {
249-
typeVariables: new Set(lastParamNames.typeVariables),
250-
rowVariables: new Set(lastParamNames.rowVariables),
251-
}
252236
expr.params.forEach(p => {
253237
const varName = p.name === '_' ? this.freshVarGenerator.freshVar('_t') : `t_${p.name}_${p.id}`
254-
paramNames.typeVariables.add(varName)
255238
const paramTypeVar: QuintVarType = { kind: 'var', name: varName }
256239
this.addToResults(p.id, right(toScheme(paramTypeVar)))
257240
if (p.typeAnnotation) {
258241
this.constraints.push({ kind: 'eq', types: [paramTypeVar, p.typeAnnotation], sourceId: p.id })
259242
}
260243
})
261-
262-
this.freeNames.push(paramNames)
263244
}
264245

265246
// Γ ∪ {p0: t0, ..., pn: tn} ⊢ e: (te, c) t0, ..., tn are fresh
@@ -283,7 +264,6 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
283264
})
284265

285266
this.addToResults(e.id, result)
286-
this.freeNames.pop()
287267
}
288268

289269
// Γ ⊢ e1: (t1, c1) s = solve(c1) s(Γ ∪ {n: t1}) ⊢ e2: (t2, c2)
@@ -294,22 +274,58 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
294274
return
295275
}
296276

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

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

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

315331
exitAssume(e: QuintAssume) {
@@ -318,15 +334,21 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
318334
}
319335

320336
this.fetchResult(e.assumption.id).map(t => {
321-
this.addToResults(e.id, right(this.quantify(t.type)))
337+
const tvs_before = this.tvs.get(e.id)!
338+
const tvs = this.freeNamesInScope()
339+
const toQuantify = variablesDifference(tvs, tvs_before)
340+
this.addToResults(e.id, right(quantify(toQuantify, t.type)))
322341
this.constraints.push({ kind: 'eq', types: [t.type, { kind: 'bool' }], sourceId: e.id })
323342
})
324343
}
325344

326345
private addToResults(exprId: bigint, result: Either<Error, TypeScheme>) {
327346
result
328347
.mapLeft(err => this.errors.set(exprId, buildErrorTree(this.location, err)))
329-
.map(r => this.types.set(exprId, r))
348+
.map(r => {
349+
this.typesInScope.set(exprId, r)
350+
this.types.set(exprId, r)
351+
})
330352
}
331353

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

@@ -408,45 +423,18 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
408423
return applySubstitution(this.table, subs, t.type)
409424
}
410425

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

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

quint/test/types/inferrer.test.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ describe('inferTypes', () => {
9797
[14n, 'int'],
9898
[15n, '((bool) => int, bool) => int'],
9999
[16n, '((bool) => int, bool) => int'],
100-
[1n, '∀ t0, t1 . (t0) => t1'],
101-
[2n, '∀ t0 . t0'],
102-
[3n, '∀ t0 . t0'],
103-
[4n, '∀ t0 . t0'],
104-
[5n, '∀ t0, t1 . ((t0) => t1, t0) => t1'],
100+
[1n, '(t_p_2) => _t4'],
101+
[2n, 't_p_2'],
102+
[3n, 't_p_2'],
103+
[4n, '_t4'],
104+
[5n, '((t_p_2) => _t4, t_p_2) => _t4'],
105105
[6n, '∀ t0, t1 . ((t0) => t1, t0) => t1'],
106106
])
107107
})
@@ -129,14 +129,14 @@ describe('inferTypes', () => {
129129
[10n, '{ f1: int, f2: bool }'],
130130
[11n, 'Set[{ f1: int, f2: bool }]'],
131131
[12n, 'Set[{ f1: int, f2: bool }]'],
132-
[13n, '∀ r0 . { f1: int | r0 }'],
132+
[13n, '{ f1: int | tail__t3 }'],
133133
[14n, '{ f1: int, f2: bool }'],
134134
[15n, 'str'],
135-
[16n, '∀ r0 . { f1: int | r0 }'],
135+
[16n, '{ f1: int | tail__t3 }'],
136136
[17n, 'str'],
137137
[18n, 'int'],
138138
[19n, '{ f1: int, f2: bool }'],
139-
[20n, '∀ r0 . ({ f1: int | r0 }) => { f1: int, f2: bool }'],
139+
[20n, '({ f1: int | tail__t3 }) => { f1: int, f2: bool }'],
140140
[21n, '∀ r0 . ({ f1: int | r0 }) => { f1: int, f2: bool }'],
141141
[23n, 'str'],
142142
[22n, 'int'],
@@ -156,16 +156,16 @@ describe('inferTypes', () => {
156156
const stringTypes = Array.from(types.entries()).map(([id, type]) => [id, typeSchemeToString(type)])
157157
// _printUpdatedStringTypes(stringTypes)
158158
assert.sameDeepMembers(stringTypes, [
159-
[1n, '∀ t0, r0 . (t0 | r0)'],
160-
[2n, '∀ t0, t1, r0 . (t0, t1 | r0)'],
161-
[3n, '∀ t0, r0 . (t0 | r0)'],
159+
[1n, '(_t0 | tail__t0)'],
160+
[2n, '(tup__t1_0, _t1 | tail__t1)'],
161+
[3n, '(_t0 | tail__t0)'],
162162
[4n, 'int'],
163-
[5n, '∀ t0 . t0'],
164-
[6n, '∀ t0, t1, r0 . (t0, t1 | r0)'],
163+
[5n, '_t0'],
164+
[6n, '(tup__t1_0, _t1 | tail__t1)'],
165165
[7n, 'int'],
166-
[8n, '∀ t0 . t0'],
167-
[9n, '∀ t0, t1 . (t0, t1)'],
168-
[10n, '∀ t0, t1, t2, r0, r1 . ((t0 | r0), (t1, t2 | r1)) => (t0, t2)'],
166+
[8n, '_t1'],
167+
[9n, '(_t0, _t1)'],
168+
[10n, '((_t0 | tail__t0), (tup__t1_0, _t1 | tail__t1)) => (_t0, _t1)'],
169169
[11n, '∀ t0, t1, t2, r0, r1 . ((t0 | r0), (t1, t2 | r1)) => (t0, t2)'],
170170
])
171171
})

0 commit comments

Comments
 (0)