frml 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
frml/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Frml: a minimal contract-based verification language."""
2
+
3
+ __version__ = "0.1.0"
frml/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
frml/ast_nodes.py ADDED
@@ -0,0 +1,347 @@
1
+ """Abstract-syntax-tree node definitions for Frml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from .types import Type
8
+
9
+
10
+ class Node:
11
+ """Base class carrying a source location."""
12
+
13
+ def __init__(self, line: int, col: int):
14
+ self.line = line
15
+ self.col = col
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Top level
20
+ # ---------------------------------------------------------------------------
21
+
22
+ @dataclass
23
+ class Function(Node):
24
+ name: str
25
+ params: list[Parameter]
26
+ return_type: Type | None # None means a void procedure
27
+ requires: list[Expr]
28
+ ensures: list[Expr]
29
+ decreases: Expr | None
30
+ body: list[Stmt]
31
+ line: int = 0
32
+ col: int = 0
33
+
34
+ def __init__(self, name, params, return_type, requires, ensures, decreases, body, line, col):
35
+ Node.__init__(self, line, col)
36
+ self.name = name
37
+ self.params = params
38
+ self.return_type = return_type
39
+ self.requires = requires
40
+ self.ensures = ensures
41
+ self.decreases = decreases
42
+ self.body = body
43
+
44
+
45
+ @dataclass
46
+ class Parameter(Node):
47
+ name: str
48
+ type: Type
49
+ line: int = 0
50
+ col: int = 0
51
+
52
+ def __init__(self, name: str, type_: Type, line: int, col: int):
53
+ Node.__init__(self, line, col)
54
+ self.name = name
55
+ self.type = type_
56
+
57
+
58
+ @dataclass
59
+ class Program:
60
+ functions: list[Function] = field(default_factory=list)
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Statements
65
+ # ---------------------------------------------------------------------------
66
+
67
+ @dataclass
68
+ class Stmt(Node):
69
+ pass
70
+
71
+
72
+ @dataclass
73
+ class StmtArrayAssign(Stmt):
74
+ array: Expr
75
+ index: Expr
76
+ value: Expr
77
+ line: int = 0
78
+ col: int = 0
79
+
80
+ def __init__(self, array, index, value, line, col):
81
+ Node.__init__(self, line, col)
82
+ self.array = array
83
+ self.index = index
84
+ self.value = value
85
+
86
+
87
+ @dataclass
88
+ class StmtAssign(Stmt):
89
+ name: str
90
+ expr: Expr
91
+ line: int = 0
92
+ col: int = 0
93
+
94
+ def __init__(self, name, expr, line, col):
95
+ Node.__init__(self, line, col)
96
+ self.name = name
97
+ self.expr = expr
98
+
99
+
100
+ @dataclass
101
+ class StmtAssert(Stmt):
102
+ expr: Expr
103
+ line: int = 0
104
+ col: int = 0
105
+
106
+ def __init__(self, expr, line, col):
107
+ Node.__init__(self, line, col)
108
+ self.expr = expr
109
+
110
+
111
+ @dataclass
112
+ class StmtCall(Stmt):
113
+ name: str
114
+ args: list[Expr]
115
+ line: int = 0
116
+ col: int = 0
117
+
118
+ def __init__(self, name, args, line, col):
119
+ Node.__init__(self, line, col)
120
+ self.name = name
121
+ self.args = args
122
+
123
+
124
+ @dataclass
125
+ class StmtIf(Stmt):
126
+ cond: Expr
127
+ then: list[Stmt]
128
+ else_: list[Stmt] | None
129
+ line: int = 0
130
+ col: int = 0
131
+
132
+ def __init__(self, cond, then, else_, line, col):
133
+ Node.__init__(self, line, col)
134
+ self.cond = cond
135
+ self.then = then
136
+ self.else_ = else_
137
+
138
+
139
+ @dataclass
140
+ class StmtLet(Stmt):
141
+ name: str
142
+ type: Type
143
+ init: Expr
144
+ line: int = 0
145
+ col: int = 0
146
+
147
+ def __init__(self, name, type_, init, line, col):
148
+ Node.__init__(self, line, col)
149
+ self.name = name
150
+ self.type = type_
151
+ self.init = init
152
+
153
+
154
+ @dataclass
155
+ class StmtReturn(Stmt):
156
+ expr: Expr
157
+ line: int = 0
158
+ col: int = 0
159
+
160
+ def __init__(self, expr, line, col):
161
+ Node.__init__(self, line, col)
162
+ self.expr = expr
163
+
164
+
165
+ @dataclass
166
+ class StmtWhile(Stmt):
167
+ cond: Expr
168
+ invariants: list[Expr]
169
+ decreases: Expr | None
170
+ body: list[Stmt]
171
+ line: int = 0
172
+ col: int = 0
173
+
174
+ def __init__(self, cond, invariants, decreases, body, line, col):
175
+ Node.__init__(self, line, col)
176
+ self.cond = cond
177
+ self.invariants = invariants
178
+ self.decreases = decreases
179
+ self.body = body
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Expressions
184
+ # ---------------------------------------------------------------------------
185
+
186
+ @dataclass
187
+ class Expr(Node):
188
+ pass
189
+
190
+
191
+ @dataclass
192
+ class ExprArrayAccess(Expr):
193
+ array: Expr
194
+ index: Expr
195
+ line: int = 0
196
+ col: int = 0
197
+
198
+ def __init__(self, array, index, line, col):
199
+ Node.__init__(self, line, col)
200
+ self.array = array
201
+ self.index = index
202
+
203
+
204
+ @dataclass
205
+ class ExprArrayLiteral(Expr):
206
+ elements: list[Expr]
207
+ line: int = 0
208
+ col: int = 0
209
+
210
+ def __init__(self, elements, line, col):
211
+ Node.__init__(self, line, col)
212
+ self.elements = elements
213
+
214
+
215
+ @dataclass
216
+ class ExprBinary(Expr):
217
+ op: str
218
+ left: Expr
219
+ right: Expr
220
+ line: int = 0
221
+ col: int = 0
222
+
223
+ def __init__(self, op, left, right, line, col):
224
+ Node.__init__(self, line, col)
225
+ self.op = op
226
+ self.left = left
227
+ self.right = right
228
+
229
+
230
+ @dataclass
231
+ class ExprCall(Expr):
232
+ name: str
233
+ args: list[Expr]
234
+ line: int = 0
235
+ col: int = 0
236
+
237
+ def __init__(self, name, args, line, col):
238
+ Node.__init__(self, line, col)
239
+ self.name = name
240
+ self.args = args
241
+
242
+
243
+ @dataclass
244
+ class ExprLength(Expr):
245
+ arg: Expr
246
+ line: int = 0
247
+ col: int = 0
248
+
249
+ def __init__(self, arg, line, col):
250
+ Node.__init__(self, line, col)
251
+ self.arg = arg
252
+
253
+
254
+ @dataclass
255
+ class ExprOld(Expr):
256
+ arg: Expr
257
+ line: int = 0
258
+ col: int = 0
259
+
260
+ def __init__(self, arg, line, col):
261
+ Node.__init__(self, line, col)
262
+ self.arg = arg
263
+
264
+
265
+ @dataclass
266
+ class ExprQuantifier(Expr):
267
+ quant: str # "forall" or "exists"
268
+ var_name: str
269
+ var_type: Type
270
+ body: Expr
271
+ line: int = 0
272
+ col: int = 0
273
+
274
+ def __init__(self, quant, var_name, var_type, body, line, col):
275
+ Node.__init__(self, line, col)
276
+ self.quant = quant
277
+ self.var_name = var_name
278
+ self.var_type = var_type
279
+ self.body = body
280
+
281
+
282
+ @dataclass
283
+ class ExprStringify(Expr):
284
+ operand: Expr
285
+ line: int = 0
286
+ col: int = 0
287
+
288
+ def __init__(self, operand, line, col):
289
+ Node.__init__(self, line, col)
290
+ self.operand = operand
291
+
292
+
293
+ @dataclass
294
+ class ExprUnary(Expr):
295
+ op: str
296
+ operand: Expr
297
+ line: int = 0
298
+ col: int = 0
299
+
300
+ def __init__(self, op, operand, line, col):
301
+ Node.__init__(self, line, col)
302
+ self.op = op
303
+ self.operand = operand
304
+
305
+
306
+ @dataclass
307
+ class ExprVar(Expr):
308
+ name: str
309
+ line: int = 0
310
+ col: int = 0
311
+
312
+ def __init__(self, name, line, col):
313
+ Node.__init__(self, line, col)
314
+ self.name = name
315
+
316
+
317
+ @dataclass
318
+ class LiteralBool(Expr):
319
+ value: bool
320
+ line: int = 0
321
+ col: int = 0
322
+
323
+ def __init__(self, value, line, col):
324
+ Node.__init__(self, line, col)
325
+ self.value = value
326
+
327
+
328
+ @dataclass
329
+ class LiteralInt(Expr):
330
+ value: int
331
+ line: int = 0
332
+ col: int = 0
333
+
334
+ def __init__(self, value, line, col):
335
+ Node.__init__(self, line, col)
336
+ self.value = value
337
+
338
+
339
+ @dataclass
340
+ class LiteralString(Expr):
341
+ value: str
342
+ line: int = 0
343
+ col: int = 0
344
+
345
+ def __init__(self, value, line, col):
346
+ Node.__init__(self, line, col)
347
+ self.value = value
frml/cli.py ADDED
@@ -0,0 +1,120 @@
1
+ """Command-line interface for Frml.
2
+
3
+ Usage:
4
+ frml check FILE.frml parse, type-check and verify
5
+ frml run FILE.frml type-check and execute main()
6
+ frml verify-run FILE.frml verify, then execute main()
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+
13
+ from . import ast_nodes as ast
14
+ from .errors import FrmlError
15
+ from .interpreter import Interpreter
16
+ from .parser import parse
17
+ from .prover import verify_program
18
+ from .typechecker import TypeChecker
19
+
20
+
21
+ def main(argv: list[str] | None = None) -> int:
22
+ argv = list(sys.argv[1:] if argv is None else argv)
23
+ if not argv:
24
+ print(__doc__.strip(), file=sys.stderr)
25
+ return 1
26
+
27
+ command = argv[0]
28
+ if command in ("-h", "--help", "help"):
29
+ print(__doc__.strip())
30
+ return 0
31
+
32
+ if command not in ("check", "run", "verify-run"):
33
+ print(f"unknown command {command!r}", file=sys.stderr)
34
+ print(__doc__.strip(), file=sys.stderr)
35
+ return 1
36
+
37
+ if len(argv) != 2:
38
+ print(f"usage: frml {command} FILE.frml", file=sys.stderr)
39
+ return 1
40
+
41
+ filename = argv[1]
42
+ if command == "check":
43
+ return do_check(filename)
44
+ if command == "run":
45
+ return do_run(filename)
46
+ return do_verify_run(filename)
47
+
48
+
49
+ def do_check(filename: str, timeout_ms: int = 10000) -> int:
50
+ try:
51
+ program = load_program(filename)
52
+ except FrmlError as e:
53
+ print(e.format(filename), file=sys.stderr)
54
+ return 1
55
+
56
+ _, outcomes = verify_program(program, timeout_ms=timeout_ms)
57
+
58
+ failed = [o for o in outcomes if o.status == "FAILED"]
59
+ unknown = [o for o in outcomes if o.status == "UNKNOWN"]
60
+
61
+ for o in failed:
62
+ print(_format_outcome(filename, o), file=sys.stderr)
63
+ for o in unknown:
64
+ print(_format_outcome(filename, o), file=sys.stderr)
65
+
66
+ if failed:
67
+ print("FAILED", file=sys.stderr)
68
+ return 1
69
+ if unknown:
70
+ print("UNKNOWN", file=sys.stderr)
71
+ return 2
72
+ print("VERIFIED")
73
+ return 0
74
+
75
+
76
+ def do_run(filename: str) -> int:
77
+ try:
78
+ program = load_program(filename)
79
+ except FrmlError as e:
80
+ print(e.format(filename), file=sys.stderr)
81
+ return 1
82
+
83
+ try:
84
+ code = Interpreter(program).run_main()
85
+ except FrmlError as e:
86
+ print(e.format(filename), file=sys.stderr)
87
+ return 1
88
+
89
+ return int(code)
90
+
91
+
92
+ def do_verify_run(filename: str, timeout_ms: int = 10000) -> int:
93
+ status = do_check(filename, timeout_ms=timeout_ms)
94
+ if status != 0:
95
+ return status
96
+ return do_run(filename)
97
+
98
+
99
+ def load_program(filename: str) -> ast.Program:
100
+ with open(filename, "r", encoding="utf-8") as f:
101
+ source = f.read()
102
+ program = parse(source)
103
+ TypeChecker(program).check()
104
+ return program
105
+
106
+
107
+ def _format_outcome(filename: str, outcome) -> str:
108
+ ob = outcome.obligation
109
+ loc = filename
110
+ if ob.line is not None:
111
+ loc += f":{ob.line}"
112
+ if ob.col is not None:
113
+ loc += f":{ob.col}"
114
+ if outcome.status == "FAILED":
115
+ return f"{loc}: FAILED\n{ob.kind} may not hold:\n {ob.description}"
116
+ return f"{loc}: UNKNOWN\n{ob.kind} could not be decided:\n {ob.description}"
117
+
118
+
119
+ if __name__ == "__main__": # pragma: no cover
120
+ sys.exit(main())
frml/errors.py ADDED
@@ -0,0 +1,61 @@
1
+ """Exception types used throughout the Frml compiler, interpreter and prover."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class FrmlError(Exception):
7
+ """Base class for all Frml errors.
8
+
9
+ Each error carries a human-readable category (SyntaxError, TypeError, ...)
10
+ and an optional source location so the command-line tool can print
11
+ `file.frml:line:col: message` diagnostics.
12
+ """
13
+
14
+ category = "Error"
15
+
16
+ def __init__(self, message: str, line: int | None = None, col: int | None = None):
17
+ self.message = message
18
+ self.line = line
19
+ self.col = col
20
+ super().__init__(message)
21
+
22
+ def format(self, filename: str | None = None) -> str:
23
+ prefix = ""
24
+ if filename:
25
+ prefix = filename
26
+ if self.line is not None:
27
+ loc = str(self.line)
28
+ if self.col is not None:
29
+ loc += f":{self.col}"
30
+ prefix = f"{prefix}:{loc}" if prefix else loc
31
+ if prefix:
32
+ return f"{prefix}: {self.category}: {self.message}"
33
+ return f"{self.category}: {self.message}"
34
+
35
+
36
+ class FrmlContractError(FrmlError):
37
+ category = "ContractError"
38
+
39
+
40
+ class FrmlNameError(FrmlError):
41
+ category = "NameError"
42
+
43
+
44
+ class FrmlRuntimeError(FrmlError):
45
+ category = "RuntimeError"
46
+
47
+
48
+ class FrmlSyntaxError(FrmlError):
49
+ category = "SyntaxError"
50
+
51
+
52
+ class FrmlTerminationError(FrmlError):
53
+ category = "TerminationError"
54
+
55
+
56
+ class FrmlTypeError(FrmlError):
57
+ category = "TypeError"
58
+
59
+
60
+ class FrmlVerificationError(FrmlError):
61
+ category = "VerificationError"