Skip to content

Commit f51cf1a

Browse files
authored
Merge pull request #1392 from informalsystems/1391/resolve-apps-on-IR
Resolve type applications at the IR level
2 parents 62d6b55 + 862ee0a commit f51cf1a

File tree

7 files changed

+132
-48
lines changed

7 files changed

+132
-48
lines changed

examples/.scripts/run-example.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ result () {
6161
elif [[ "$file" == "classic/distributed/TwoPhaseCommit/two_phase_commit_modules.qnt" && "$cmd" =~ (test|verify) ]] ; then
6262
printf "<sup>https://github.com/informalsystems/quint/issues/1299</sup>"
6363
elif [[ "$file" == "language-features/option.qnt" && "$cmd" == "verify" ]] ; then
64-
printf "<sup>https://github.com/informalsystems/quint/issues/1391</sup>"
64+
printf "<sup>https://github.com/informalsystems/quint/issues/1393</sup>"
6565
fi
6666
fi
6767
}

examples/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ listed without any additional command line arguments.
8686
| [language-features/lists.qnt](./language-features/lists.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
8787
| [language-features/maps.qnt](./language-features/maps.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
8888
| [language-features/nondetEx.qnt](./language-features/nondetEx.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
89-
| [language-features/option.qnt](./language-features/option.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:<sup>https://github.com/informalsystems/quint/issues/1391</sup> |
89+
| [language-features/option.qnt](./language-features/option.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x:<sup>https://github.com/informalsystems/quint/issues/1393</sup> |
9090
| [language-features/records.qnt](./language-features/records.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
9191
| [language-features/sets.qnt](./language-features/sets.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
9292
| [language-features/tuples.qnt](./language-features/tuples.qnt) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |

quint/src/quintAnalyzer.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ModeChecker } from './effects/modeChecker'
2222
import { QuintError } from './quintError'
2323
import { errorTreeToString } from './errorTree'
2424
import { MultipleUpdatesChecker } from './effects/MultipleUpdatesChecker'
25+
import { TypeApplicationResolver } from './types/typeApplicationResolution'
2526

2627
/* Products from static analysis */
2728
export type AnalysisOutput = {
@@ -36,19 +37,26 @@ export type AnalysisResult = [QuintError[], AnalysisOutput]
3637
/**
3738
* Analyzes multiple Quint modules and returns the analysis result.
3839
*
40+
* NOTE: This is modifies the `lookupTable` and the `quintModules`!
41+
* See XXX for the mutation sites.
42+
*
3943
* @param lookupTable - The lookup tables for the modules.
4044
* @param quintModules - The Quint modules to be analyzed.
4145
* @returns A tuple with a list of errors and the analysis output.
4246
*/
4347
export function analyzeModules(lookupTable: LookupTable, quintModules: QuintModule[]): AnalysisResult {
4448
const analyzer = new QuintAnalyzer(lookupTable)
45-
quintModules.map(m => analyzer.analyzeDeclarations(m.declarations))
49+
// XXX: the modules are mutated here.
50+
quintModules.forEach(m => (m.declarations = analyzer.analyzeDeclarations(m.declarations)))
4651
return analyzer.getResult()
4752
}
4853

4954
/**
5055
* Analyzes declarations incrementally and returns the analysis result.
5156
*
57+
* NOTE: This is modifies the `lookupTable`!
58+
* See XXX for the mutation sites.
59+
*
5260
* @param analysisOutput - The previous analysis output to be used as a starting point.
5361
* @param lookupTable - The lookup tables for the modules.
5462
* @param declarations - The Quint declarations to be analyzed.
@@ -75,6 +83,7 @@ export function analyzeInc(
7583
* @param previousOutput - The previous analysis output to be used as a starting point.
7684
*/
7785
class QuintAnalyzer {
86+
private typeApplicationResolver: TypeApplicationResolver
7887
private effectInferrer: EffectInferrer
7988
private typeInferrer: TypeInferrer
8089
private modeChecker: ModeChecker
@@ -84,19 +93,24 @@ class QuintAnalyzer {
8493
private output: AnalysisOutput = { types: new Map(), effects: new Map(), modes: new Map() }
8594

8695
constructor(lookupTable: LookupTable, previousOutput?: AnalysisOutput) {
96+
// XXX: the lookUp table is mutated when TypeApplicationResolver is instantiated
97+
this.typeApplicationResolver = new TypeApplicationResolver(lookupTable)
8798
this.typeInferrer = new TypeInferrer(lookupTable, previousOutput?.types)
8899
this.effectInferrer = new EffectInferrer(lookupTable, previousOutput?.effects)
89100
this.multipleUpdatesChecker = new MultipleUpdatesChecker()
90101
this.modeChecker = new ModeChecker(previousOutput?.modes)
91102
}
92103

93-
analyzeDeclarations(decls: QuintDeclaration[]): void {
94-
const [typeErrMap, types] = this.typeInferrer.inferTypes(decls)
95-
const [effectErrMap, effects] = this.effectInferrer.inferEffects(decls)
104+
analyzeDeclarations(decls: QuintDeclaration[]): QuintDeclaration[] {
105+
const [typAppErrMap, resolvedDecls] = this.typeApplicationResolver.resolveTypeApplications(decls)
106+
107+
// XXX: the lookUp table is mutated during type inference
108+
const [typeErrMap, types] = this.typeInferrer.inferTypes(resolvedDecls)
109+
const [effectErrMap, effects] = this.effectInferrer.inferEffects(resolvedDecls)
96110
const updatesErrMap = this.multipleUpdatesChecker.checkEffects([...effects.values()])
97-
const [modeErrMap, modes] = this.modeChecker.checkModes(decls, effects)
111+
const [modeErrMap, modes] = this.modeChecker.checkModes(resolvedDecls, effects)
98112

99-
const errorTrees = [...typeErrMap, ...effectErrMap]
113+
const errorTrees = [...typeErrMap, ...effectErrMap, ...typAppErrMap]
100114

101115
// TODO: Type and effect checking should return QuintErrors instead of error trees
102116
this.errors.push(
@@ -114,6 +128,8 @@ class QuintAnalyzer {
114128
effects: new Map([...this.output.effects, ...effects]),
115129
modes: new Map([...this.output.modes, ...modes]),
116130
}
131+
132+
return resolvedDecls
117133
}
118134

119135
getResult(): AnalysisResult {

quint/src/types/inferrer.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,12 @@ import { TypeScheme } from './base'
2121
import { ConstraintGeneratorVisitor } from './constraintGenerator'
2222
import { solveConstraint } from './constraintSolver'
2323
import { simplify } from './simplification'
24-
import { TypeApplicationResolver } from './typeApplicationResolution'
25-
import { transformDeclaration, transformLookupDefinition } from '../ir/IRTransformer'
2624

2725
export type TypeInferenceResult = [Map<bigint, ErrorTree>, Map<bigint, TypeScheme>]
2826

2927
export class TypeInferrer extends ConstraintGeneratorVisitor {
30-
private resolver: TypeApplicationResolver
3128
constructor(table: LookupTable, types?: Map<bigint, TypeScheme>) {
3229
super(solveConstraint, table, types)
33-
this.resolver = new TypeApplicationResolver(this.table, this.freshVarGenerator)
3430
}
3531

3632
/**
@@ -43,17 +39,8 @@ export class TypeInferrer extends ConstraintGeneratorVisitor {
4339
*/
4440
inferTypes(declarations: QuintDeclaration[]): TypeInferenceResult {
4541
// Resolve all type applications used in expressions in the lookup table
46-
this.table.forEach((def, id) => {
47-
// Don't resolve type definitions, since those should keep their original type variable names
48-
// They are our source of truth for all resolutions
49-
if (def.kind !== 'typedef') {
50-
const resolvedLookupDef = transformLookupDefinition(this.resolver, def)
51-
this.table.set(id, resolvedLookupDef)
52-
}
53-
})
5442
declarations.forEach(decl => {
55-
const resolvedDecl = transformDeclaration(this.resolver, decl)
56-
walkDeclaration(this, resolvedDecl)
43+
walkDeclaration(this, decl)
5744
})
5845
const simplifiedTypes = new Map([...this.types.entries()].map(([id, t]) => [id, simplify(t)]))
5946
return [this.errors, simplifiedTypes]

quint/src/types/typeApplicationResolution.ts

+51-23
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,45 @@
1212
* @module
1313
*/
1414

15-
import { fail } from 'assert'
15+
import { ErrorTree, buildErrorLeaf } from '../errorTree'
1616
import { FreshVarGenerator } from '../FreshVarGenerator'
17+
1718
import { typeToString } from '../ir/IRprinting'
18-
import { IRTransformer, transformType } from '../ir/IRTransformer'
19-
import { QuintTypeAlias } from '../ir/quintIr'
19+
import { IRTransformer, transformDeclaration, transformLookupDefinition, transformType } from '../ir/IRTransformer'
20+
import { QuintDeclaration, QuintTypeAlias } from '../ir/quintIr'
2021
import { QuintAppType, QuintType, QuintVarType, Row } from '../ir/quintTypes'
2122
import { LookupTable } from '../names/base'
2223
import { zip } from '../util'
2324
import { Substitutions, applySubstitution } from './substitutions'
25+
import assert from 'assert'
2426

2527
/** Resolves all type applications in an IR object */
2628
export class TypeApplicationResolver implements IRTransformer {
29+
// Errors found during type application resolution
30+
private errors: Map<bigint, ErrorTree> = new Map<bigint, ErrorTree>()
2731
// Fresh variable generator, shared with the TypeInferrer
2832
private freshVarGenerator: FreshVarGenerator
2933
// Lookup table from the parser
3034
private table: LookupTable
3135

32-
constructor(table: LookupTable, freshVarGenerator: FreshVarGenerator) {
36+
constructor(table: LookupTable) {
3337
this.table = table
34-
this.freshVarGenerator = freshVarGenerator
38+
this.freshVarGenerator = new FreshVarGenerator()
39+
40+
this.table.forEach((def, id) => {
41+
const resolvedLookupDef = transformLookupDefinition(this, def)
42+
this.table.set(id, resolvedLookupDef)
43+
})
44+
}
45+
46+
resolveTypeApplications(decls: QuintDeclaration[]): [Map<bigint, ErrorTree>, QuintDeclaration[]] {
47+
const resolvedDecls = decls.map(decl => transformDeclaration(this, decl))
48+
const errors = this.errors
49+
return [errors, resolvedDecls]
3550
}
3651

3752
exitType(t: QuintType): QuintType {
38-
return this.resolveTypeApplications(t)
53+
return this.resolveTypeApplicationsForType(t)
3954
}
4055

4156
// Transforms `t` by resolving all the type applications in all its sub-terms
@@ -46,33 +61,46 @@ export class TypeApplicationResolver implements IRTransformer {
4661
// type Bar[x, y] = {i: x, j: y}
4762
//
4863
//
49-
// resolveTypeApplications(Foo[a, {f: Bar[int, str]}]) = (a, {f: {i: int, j: str}})
50-
resolveTypeApplications(t: QuintType): QuintType {
64+
// resolveTypeApplicationsForType(Foo[a, {f: Bar[int, str]}]) = (a, {f: {i: int, j: str}})
65+
private resolveTypeApplicationsForType(t: QuintType): QuintType {
5166
const f: (_: QuintType) => QuintType = x => (x.kind !== 'app' ? x : this.resolveTypeApp(x))
5267
return mapType(f, t)
5368
}
5469

5570
private resolveTypeApp(t: QuintAppType): QuintType {
56-
if (!t.ctor.id) {
57-
// This should be ensured by parsing
58-
fail(
59-
`invalid IR node: type constructor ${t.ctor.name} in type application ${typeToString(t)} id ${t.id} has no id`
60-
)
61-
}
71+
// Ensured by parsing
72+
assert(t.id, `invalid IR node: type application ${typeToString(t)} has no id`)
73+
// Ensured by parsing
74+
assert(
75+
t.ctor.id,
76+
`invalid IR node: type constructor ${t.ctor.name} in type application ${typeToString(t)} id ${t.id} has no id`
77+
)
6278

6379
const typeDef = this.table.get(t.ctor.id)
64-
if (!typeDef) {
65-
// This should be ensured by name resolution
66-
fail(`invalid IR reference: type constructor ${t.ctor.name} with id ${t.ctor.id} has no type definition`)
67-
}
68-
69-
if (typeDef.kind !== 'typedef' || !typeDef.type) {
70-
// This should be ensured by the grammar and by name resolution
71-
fail(`invalid kind looked up for constructor of type application with id ${t.ctor.id} `)
72-
}
80+
// Ensured by name resolution
81+
assert(typeDef, `invalid IR reference: type constructor ${t.ctor.name} with id ${t.ctor.id} has no type definition`)
82+
// Ensured by the grammar and by name resolution
83+
assert(
84+
typeDef.kind === 'typedef' && typeDef.type,
85+
`invalid kind looked up for constructor of type application with id ${t.ctor.id} `
86+
)
7387

7488
const { params, type } = this.freshTypeFromDef(typeDef as QuintTypeAlias)
7589

90+
// NOTE: Early exit on error
91+
// Check for arity mismatch in type application
92+
if (params.length !== t.args.length) {
93+
const ctorMsg = typeToString(t.ctor)
94+
const typeArgsMsg = t.args.map(typeToString).join(', ')
95+
const manyOrFew = params.length > t.args.length ? 'few' : 'many'
96+
const err = buildErrorLeaf(
97+
`applying type constructor ${ctorMsg} to arguments ${typeArgsMsg}`,
98+
`too ${manyOrFew} arguments supplied: ${ctorMsg} only accepts ${params.length} parameters`
99+
)
100+
this.errors.set(t.id, err)
101+
return t
102+
}
103+
76104
// Substitute the type `args` for each corresponding fresh variable
77105
const subs: Substitutions = zip(params, t.args).map(([param, arg]) => ({
78106
kind: 'type',

quint/test/flattening/fullFlattener.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,19 @@ describe('flattenModules', () => {
235235

236236
assertFlattenedModule(text)
237237
})
238+
239+
// Regression test for https://github.com/informalsystems/quint/issues/1391
240+
describe('can flatten with polymorphic types between different modules (#802)', () => {
241+
const text = `module A {
242+
type Foo[a] = F(a)
243+
def mapFoo(x: Foo[a], f: a => b): Foo[b] = match x { F(v) => F(f(v)) }
244+
}
245+
246+
module B {
247+
import A.*
248+
val b: Foo[int] = F("one").mapFoo(_ => 1)
249+
}`
250+
251+
assertFlattenedModule(text)
252+
})
238253
})

quint/test/types/inferrer.test.ts

+41-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { describe, it } from 'mocha'
22
import { assert } from 'chai'
33
import { TypeInferenceResult, TypeInferrer } from '../../src/types/inferrer'
44
import { typeSchemeToString } from '../../src/types/printing'
5-
import { errorTreeToString } from '../../src/errorTree'
5+
import { ErrorTree, errorTreeToString } from '../../src/errorTree'
66
import { parseMockedModule } from '../util'
7+
import { TypeApplicationResolver } from '../../src/types/typeApplicationResolution'
78

89
// Utility used to print update `stringType` values to make
910
// updating the expected values in the following tests less
@@ -16,10 +17,22 @@ function _printUpdatedStringTypes(stringTypes: (string | bigint)[][]) {
1617

1718
describe('inferTypes', () => {
1819
function inferTypesForModules(text: string): TypeInferenceResult {
19-
const { modules, table } = parseMockedModule(text)
20+
const { modules: parsedModules, table } = parseMockedModule(text)
2021

22+
// Type inference assumes all type applications (e.g., `Foo[int, str]`) have been resolved.
23+
const resolver = new TypeApplicationResolver(table)
2124
const inferrer = new TypeInferrer(table)
22-
return inferrer.inferTypes(modules.flatMap(m => m.declarations))
25+
26+
// Used to collect errors found during type application
27+
let typeAppErrs: Map<bigint, ErrorTree> = new Map()
28+
const modules = parsedModules.map(m => {
29+
const [errs, declarations] = resolver.resolveTypeApplications(m.declarations)
30+
typeAppErrs = new Map([...typeAppErrs, ...errs])
31+
return { ...m, declarations }
32+
})
33+
const [inferenceErrors, inferenceSchemes] = inferrer.inferTypes(modules.flatMap(m => m.declarations))
34+
const combinedErrors = new Map([...inferenceErrors, ...typeAppErrs])
35+
return [combinedErrors, inferenceSchemes]
2336
}
2437

2538
function inferTypesForDefs(defs: string[]): TypeInferenceResult {
@@ -441,6 +454,31 @@ Trying to unify ((Ok(bool) | Err(_t5))) => (Ok(bool) | Err(_t5)) and ((Ok(bool)
441454
assert.deepEqual(actualErrors, [expectedError])
442455
})
443456

457+
it('errors when polymorphic types are applied to invalid numbers of arguments', () => {
458+
const defs = [
459+
'type Result[ok, err] = Ok(ok) | Err(err)',
460+
`val too_many: Result[a, b, c] = Ok(1)`,
461+
`val too_few: Result[a] = Ok(1)`,
462+
]
463+
464+
const [errors] = inferTypesForDefs(defs)
465+
assert.isNotEmpty([...errors.entries()])
466+
467+
const actualErrors = [...errors.entries()].map(e => errorTreeToString(e[1]))
468+
const expectedErrors = [
469+
`Couldn't unify sum and app
470+
Trying to unify (Ok(int) | Err(_t3)) and Result[a, b, c]
471+
`,
472+
`too many arguments supplied: Result only accepts 2 parameters
473+
applying type constructor Result to arguments a, b, c
474+
`,
475+
`too few arguments supplied: Result only accepts 2 parameters
476+
applying type constructor Result to arguments a
477+
`,
478+
]
479+
assert.deepEqual(actualErrors, expectedErrors)
480+
})
481+
444482
it('fails when types are not unifiable', () => {
445483
const defs = ['def a = 1.map(p => p + 10)']
446484

0 commit comments

Comments
 (0)