Skip to content

Commit 62d6b55

Browse files
author
Shon Feder
authored
Merge pull request #1386 from informalsystems/1298/typechecking-polymorphic-types-redux
Add support for polymorphic type declarations
2 parents 053cf33 + 420f295 commit 62d6b55

File tree

10 files changed

+305
-58
lines changed

10 files changed

+305
-58
lines changed

CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## UNRELEASED
99

1010
### Added
11+
12+
- Added polymorphic type declarations, allowing abstracting commonly used data
13+
types like `Option[a]` and `Result[err, ok]`. Note that this is not yet
14+
supported by `verify`. (#1298)
15+
1116
### Changed
17+
18+
- The latest supported node version is now bounded at <= 20, which covers the
19+
latest LTS. (#1380)
20+
1221
### Deprecated
1322
### Removed
1423
### Fixed
24+
25+
- Removed a dependency causing deprecation errors messages to be emitted.
26+
(#1380)
27+
1528
### Security
1629

1730
## v0.18.3 -- 2024-02-08

doc/lang.md

+29-14
Original file line numberDiff line numberDiff line change
@@ -147,31 +147,34 @@ This is the same type system as in Apalache:
147147

148148
A type is one of the following:
149149

150-
- Basic type: `bool`, `int`, `str`.
150+
- Basic type: `bool`, `int`, `str`.
151151

152-
- Uninterpreted type or type name: `IDENTIFIER_IN_CAPS`.
152+
- Uninterpreted type or type name: `IDENTIFIER_IN_CAPS`.
153153

154-
- Type variable (parameter): `a`, ..., `z`.
154+
- Type variable (parameter): `a`, ..., `z`.
155155

156-
- Set: `Set[T]` for a type `T`.
156+
- Set: `Set[T]` for a type `T`.
157157

158-
- List: `List[T]` for a type `T`.
158+
- List: `List[T]` for a type `T`.
159159

160-
- Tuple: `(T_1, T_2, ..., T_n)` for `n >= 2` types `T_1`, ..., `T_n`.
160+
- Tuple: `(T_1, T_2, ..., T_n)` for `n >= 2` types `T_1`, ..., `T_n`.
161161

162-
- Record: `{ name_1: T_1, name_2: T_2, ..., name_n: T_n }`
163-
for `n >= 1` types `T_1`, ..., `T_n`.
162+
- Record: `{ name_1: T_1, name_2: T_2, ..., name_n: T_n }`
163+
for `n >= 1` types `T_1`, ..., `T_n`.
164164

165-
- Function: `T1 -> T2` for types `T1` and `T2`.
165+
- Function: `T1 -> T2` for types `T1` and `T2`.
166166

167-
- Operator: `(T_1, ..., T_n) => R` for `n >= 0` argument types `T_1, ..., T_n`
168-
and result type `R`.
167+
- Operator: `(T_1, ..., T_n) => R` for `n >= 0` argument types `T_1, ..., T_n`
168+
and result type `R`.
169169

170-
- Sum Types: `type T = L_1(T_1) | ... | L_n(T_n) ` for `n >= 1`, argument types
171-
`T_1`, ..., `T_n`, and a type alais `T`.
170+
- Sum Types: `type T = L_1(T_1) | ... | L_n(T_n) ` for `n >= 1`, argument types
171+
`T_1`, ..., `T_n`, and a type alais `T`.
172172

173-
- Type in parentheses: `(T)` for a type `T`.
173+
- Type in parentheses: `(T)` for a type `T`.
174174

175+
- An instance of a defined polymorphic type `T[T_1, ..., T_n]` for a defined type
176+
constructor with type parameters `T[p_1, ..., p_n]` and types `T_1, ...,
177+
T_n`.
175178

176179
It is often convenient to declare a type alias. You can use `type` to define
177180
an alias inside a module definition. For instance:
@@ -180,6 +183,18 @@ an alias inside a module definition. For instance:
180183
type Temperature = int
181184
```
182185

186+
A type alias specified with type parameters defines a polymorphic type
187+
constructor for instances of the defined type. For instance, given
188+
189+
```bluespec
190+
type Option[a] =
191+
| Some(a)
192+
| None
193+
```
194+
195+
You can then construct concrete types such as `Option[int]` or
196+
`Option[List[int]]`.
197+
183198
A type identifier can also introduce an uninterpreted type by defining a type without any constructors for values of that type:
184199

185200
```bluespec

examples/.scripts/run-example.sh

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ result () {
6060
printf "<sup>https://github.com/informalsystems/quint/issues/1284</sup>"
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>"
63+
elif [[ "$file" == "language-features/option.qnt" && "$cmd" == "verify" ]] ; then
64+
printf "<sup>https://github.com/informalsystems/quint/issues/1391</sup>"
6365
fi
6466
fi
6567
}

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: | :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> |
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: |

examples/language-features/option.qnt

+35-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
module option {
22
// A demonstration of sum types, specifying an option type.
33

4-
// An option type for int values.
5-
type VoteOption =
4+
// A polymorphic option type
5+
type Option[a] =
6+
| Some(a)
67
| None
7-
| Some(int)
88

9-
var votes: List[VoteOption]
9+
// `o.optionMap(f)` is `Some(f(v))` if `o = Some(v)` or `None` of `o = None`
10+
def optionMap(o: Option[a], f: a => b): Option[b] =
11+
match o {
12+
| Some(x) => Some(f(x))
13+
| None => None
14+
}
15+
16+
// `o.getOr(v)` is `x` or `Some(x) = o` or `v` if `o = None`
17+
def getOr(o: Option[a], default: a): a =
18+
match o {
19+
| Some(x) => x
20+
| None => default
21+
}
22+
23+
var votes: List[Option[int]]
1024
var outcome: int
1125

1226
action init = all {
@@ -18,22 +32,31 @@ module option {
1832
votes' = votes.append(Some(v))
1933
}
2034

35+
action incrVote(i, n) = {
36+
val vote0 = votes[i]
37+
val vote1 = vote0.optionMap(x => x + n)
38+
votes' = votes.replaceAt(i, vote1)
39+
}
40+
2141
action unvote(i) = all {
22-
votes[i] != None,
2342
votes' = votes.replaceAt(i, None),
2443
}
2544

2645
val sumVotes =
27-
votes.foldl(0, (sum, v) => match v {
28-
| Some(n) => sum + n
29-
| None => sum
30-
}
31-
)
46+
votes.foldl(0, (sum, v) => v.getOr(0) + sum)
3247

3348
action step = all {
3449
any {
35-
nondet v = oneOf(Int); vote(v),
36-
nondet i = oneOf(votes.indices()); unvote(i),
50+
nondet v = oneOf(0.to(5)); vote(v),
51+
all {
52+
votes.length() > 0,
53+
nondet i = oneOf(votes.indices()); unvote(i),
54+
},
55+
all {
56+
votes.length() > 0,
57+
nondet i = oneOf(votes.indices()); nondet v = oneOf(0.to(5)) incrVote(i, v),
58+
}
59+
3760
},
3861
outcome' = sumVotes
3962
}

quint/src/FreshVarGenerator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class FreshVarGenerator {
1616
private freshVarCounters: Map<string, number> = new Map<string, number>()
1717

1818
freshVar(prefix: string): string {
19-
const counter = this.freshVarCounters.get(prefix)! ?? 0
19+
const counter = this.freshVarCounters.get(prefix) ?? 0
2020
this.freshVarCounters.set(prefix, counter + 1)
2121

2222
return `${prefix}${counter}`

quint/src/types/constraintGenerator.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ function validateArity(
8282
}
8383

8484
// A visitor that collects types and constraints for a module's expressions
85+
//
86+
// NOTE: Assumes all type applications have been resolved by typeApplicationResolution first
8587
export class ConstraintGeneratorVisitor implements IRVisitor {
8688
// Inject dependency to allow manipulation in unit tests
8789
constructor(solvingFunction: SolvingFunctionType, table: LookupTable, types?: Map<bigint, TypeScheme>) {
@@ -95,13 +97,12 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
9597

9698
protected types: Map<bigint, TypeScheme> = new Map<bigint, TypeScheme>()
9799
protected errors: Map<bigint, ErrorTree> = new Map<bigint, ErrorTree>()
100+
protected freshVarGenerator: FreshVarGenerator
101+
protected table: LookupTable
98102

99-
private solvingFunction: SolvingFunctionType
100103
private constraints: Constraint[] = []
101-
104+
private solvingFunction: SolvingFunctionType
102105
private builtinSignatures: Map<string, Signature> = getSignatures()
103-
private table: LookupTable
104-
private freshVarGenerator: FreshVarGenerator
105106

106107
// Track location descriptions for error tree traces
107108
private location: string = ''
@@ -356,13 +357,11 @@ export class ConstraintGeneratorVisitor implements IRVisitor {
356357
// Apply substitution to environment
357358
// FIXME: We have to figure out the scope of the constraints/substitutions
358359
// https://github.com/informalsystems/quint/issues/690
359-
this.types = new Map<bigint, TypeScheme>(
360-
[...this.types.entries()].map(([id, te]) => {
361-
const newType = applySubstitution(this.table, subs, te.type)
362-
const scheme: TypeScheme = this.quantify(newType)
363-
return [id, scheme]
364-
})
365-
)
360+
this.types.forEach((oldScheme, id) => {
361+
const newType = applySubstitution(this.table, subs, oldScheme.type)
362+
const newScheme: TypeScheme = this.quantify(newType)
363+
this.addToResults(id, right(newScheme))
364+
})
366365

367366
return subs
368367
})

quint/src/types/inferrer.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ 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'
2426

2527
export type TypeInferenceResult = [Map<bigint, ErrorTree>, Map<bigint, TypeScheme>]
2628

2729
export class TypeInferrer extends ConstraintGeneratorVisitor {
30+
private resolver: TypeApplicationResolver
2831
constructor(table: LookupTable, types?: Map<bigint, TypeScheme>) {
2932
super(solveConstraint, table, types)
33+
this.resolver = new TypeApplicationResolver(this.table, this.freshVarGenerator)
3034
}
3135

3236
/**
@@ -38,8 +42,18 @@ export class TypeInferrer extends ConstraintGeneratorVisitor {
3842
* ids to the corresponding error for any problematic expressions.
3943
*/
4044
inferTypes(declarations: QuintDeclaration[]): TypeInferenceResult {
45+
// 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+
})
4154
declarations.forEach(decl => {
42-
walkDeclaration(this, decl)
55+
const resolvedDecl = transformDeclaration(this.resolver, decl)
56+
walkDeclaration(this, resolvedDecl)
4357
})
4458
const simplifiedTypes = new Map([...this.types.entries()].map(([id, t]) => [id, simplify(t)]))
4559
return [this.errors, simplifiedTypes]

0 commit comments

Comments
 (0)