Compare commits
324 Commits
bea3f399ad
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 03bc32400b | |||
|
4a93ee45d9
|
|||
|
8197131d8d
|
|||
|
cf91187b7a
|
|||
|
1b2bdf0b79
|
|||
| c6cc38bfeb | |||
|
4d3e3f44a1
|
|||
|
ec80b1e92e
|
|||
|
4ea15519f3
|
|||
|
7a6e01cff8
|
|||
|
733c8736b8
|
|||
|
20173a0b07
|
|||
|
a143972ef1
|
|||
|
0c70048b62
|
|||
|
1c0c917873
|
|||
|
1f6189daa4
|
|||
|
66b585c3d6
|
|||
|
819ab3c2bf
|
|||
|
d8c0b17512
|
|||
|
6e06f9078e
|
|||
|
ece2e3a6a3
|
|||
|
74c07c9afb
|
|||
|
be2fd4c837
|
|||
|
1bc4c704c3
|
|||
|
0288a05901
|
|||
|
b14f46d405
|
|||
|
8e8ed62266
|
|||
|
2fce2f4bfc
|
|||
|
640f2d1771
|
|||
|
b48dfe5301
|
|||
|
0d5840a4ce
|
|||
|
3c92f0867d
|
|||
|
b5acae4078
|
|||
|
5d20f8ec3e
|
|||
|
955c2233ed
|
|||
|
ff69b65171
|
|||
|
8df01afd8c
|
|||
|
47b2dfdd73
|
|||
|
bd4d793ce0
|
|||
|
f7a36f61b6
|
|||
|
ad2fabf471
|
|||
|
a59a58d21a
|
|||
| 3260ae4a1e | |||
|
bd1c9581c7
|
|||
|
663642ea6c
|
|||
|
e2abc04fe4
|
|||
|
a4016b55ce
|
|||
|
1ea5da7024
|
|||
|
a017a8cf1f
|
|||
|
8fc5ab623e
|
|||
|
14007db846
|
|||
|
6ad2ce4b68
|
|||
|
9a276c34c7
|
|||
|
6e717a3f9e
|
|||
|
77aadfa264
|
|||
| c81287df7f | |||
|
ffccc1bedd
|
|||
|
d14f208897
|
|||
|
293953a078
|
|||
|
bccc96e4d0
|
|||
|
9db56adf56
|
|||
|
3f99563ac8
|
|||
|
b36896cc7b
|
|||
|
cb75878ae9
|
|||
|
a5fe985eb2
|
|||
|
e324f414e6
|
|||
|
256536562f
|
|||
|
64f4314f0d
|
|||
|
6f6245d283
|
|||
|
3392bc347d
|
|||
|
7e0319906a
|
|||
|
75bd203d4a
|
|||
|
db40198357
|
|||
|
d79e1dee18
|
|||
|
4ea400265c
|
|||
|
24bffdabd4
|
|||
|
d7bb6326de
|
|||
|
dbf6f9e2db
|
|||
|
3cdc9031d3
|
|||
|
00e2ca8fe3
|
|||
|
4efb01285c
|
|||
|
f84a19159f
|
|||
|
946b2e0d2e
|
|||
|
08dd7408ec
|
|||
|
b33fadf768
|
|||
|
7219109e5d
|
|||
|
cdf1725c26
|
|||
|
7074b074bc
|
|||
|
ede7272c09
|
|||
|
87d5e286d2
|
|||
|
c91b206791
|
|||
|
a31d295eb1
|
|||
|
0d20993f02
|
|||
|
5357ca8e58
|
|||
|
556765fd35
|
|||
| d039a8e4b3 | |||
|
c4533421eb
|
|||
|
73769b42c1
|
|||
|
087f6b4669
|
|||
| d582df5927 | |||
|
6a0401833c
|
|||
|
e15607b763
|
|||
|
e28f324a85
|
|||
|
31e696c938
|
|||
|
759b416bf3
|
|||
|
4b2b0fe476
|
|||
|
4c39504750
|
|||
|
f9f3ade6c7
|
|||
|
386018b956
|
|||
|
bd47d33355
|
|||
|
93ddb28802
|
|||
| f7c43837b5 | |||
|
32ed62a6f1
|
|||
|
66f39acec0
|
|||
|
6c04e2fee4
|
|||
| 2bb2e0a684 | |||
|
5630320d21
|
|||
|
9f05ba3224
|
|||
|
5fbe965919
|
|||
| 252a5abdfd | |||
|
55fba6a088
|
|||
|
70ce263ea2
|
|||
|
e1d5eac8b8
|
|||
|
82666a4918
|
|||
|
45f84a2f23
|
|||
|
dedfcb4dbb
|
|||
|
d9ea6365ea
|
|||
| 9c7a93412c | |||
|
d6b8fbfb60
|
|||
|
b290c59ac4
|
|||
|
093f2bc477
|
|||
|
7c771c4070
|
|||
|
a50a207385
|
|||
|
7e5ea5e414
|
|||
|
0ba0266bae
|
|||
|
216c80f08c
|
|||
|
f75d7722a1
|
|||
|
2f29c47274
|
|||
|
80af2b9048
|
|||
|
577454ee7e
|
|||
|
878693383e
|
|||
|
0b91de75a8
|
|||
| 739871c101 | |||
|
4395e9339b
|
|||
|
29e601128d
|
|||
|
b591f5508f
|
|||
|
41d0c84bbe
|
|||
| cccf2f8f9f | |||
|
3f48c2138f
|
|||
|
e4ab27673d
|
|||
|
b02ecc6326
|
|||
|
9e83079910
|
|||
|
ec468dd982
|
|||
|
3edc25d778
|
|||
|
451e54b009
|
|||
|
0dc14f67aa
|
|||
|
ff79f25628
|
|||
| 12782dda1e | |||
|
48a20b4aa0
|
|||
|
9467187313
|
|||
|
cd8f14153d
|
|||
| 6eea0c02e0 | |||
|
3205e7b961
|
|||
|
0aba134290
|
|||
|
1f0bcab2ca
|
|||
|
db8d88ef35
|
|||
|
7695d50537
|
|||
|
8461d05fa6
|
|||
|
43d2118db7
|
|||
|
6a87b5396f
|
|||
|
e6a581ba6e
|
|||
|
2a7aac69ed
|
|||
|
eb5bf19c61
|
|||
|
657406ea01
|
|||
|
2974386110
|
|||
|
92ca6b6732
|
|||
|
6aacdb98b7
|
|||
|
1b100b6ceb
|
|||
|
6b4c7d27bc
|
|||
|
2523d638f7
|
|||
|
5fc7461e29
|
|||
|
c5154bde81
|
|||
|
d07e8ac0ca
|
|||
|
3380995082
|
|||
|
7efc44c496
|
|||
|
ca94443699
|
|||
|
c513a85cf2
|
|||
|
2a106c5d07
|
|||
| 9672dfd588 | |||
|
7639ccc94d
|
|||
| a4a2ed5d64 | |||
|
e5cb90aff6
|
|||
|
75f8e4af53
|
|||
|
42c2d7a098
|
|||
| 5ce3b4abed | |||
|
2a8b7d559c
|
|||
|
da38cad23d
|
|||
|
591012d059
|
|||
|
4b1087d6b9
|
|||
|
732f7b0796
|
|||
|
c4062c9595
|
|||
|
c3229b557c
|
|||
|
0a8e0fb6c2
|
|||
|
61514d036c
|
|||
| 2e5cf6f8a2 | |||
|
25fabdd6c3
|
|||
|
af1aba41e7
|
|||
|
48e13d3348
|
|||
|
faa98ce0ef
|
|||
|
274e366561
|
|||
| 119c262da4 | |||
|
81181891c4
|
|||
|
59c1a0c7b6
|
|||
|
74f51f361a
|
|||
|
f25341b1e7
|
|||
|
3281324caf
|
|||
| 5b062b46e6 | |||
|
635bf73531
|
|||
|
bd0421b5d8
|
|||
|
37a464d2bc
|
|||
|
1eedcff5aa
|
|||
|
35798e5752
|
|||
|
0a35563aaf
|
|||
|
e1da87eaa0
|
|||
|
2a579c06b1
|
|||
|
46a22797b6
|
|||
|
7598681729
|
|||
|
2df0380815
|
|||
|
178e24cd02
|
|||
|
c92b6b5c18
|
|||
|
6577241af9
|
|||
|
1c71badf24
|
|||
|
064702fe13
|
|||
|
890e2f035a
|
|||
|
0d0115534b
|
|||
|
221b5ca926
|
|||
|
9a227b6d4c
|
|||
|
df2e609c60
|
|||
|
3ee1161680
|
|||
|
eb223c6cb7
|
|||
|
6f5d971c66
|
|||
|
109c8eb35a
|
|||
|
99924ee6c2
|
|||
|
4c9cbd9faa
|
|||
|
84a5f41e62
|
|||
|
6d6bb66c54
|
|||
|
50eaafc388
|
|||
|
2935c71366
|
|||
|
52981f12f2
|
|||
|
2e898ab1e9
|
|||
|
01ff5ca8d5
|
|||
|
b5de28e291
|
|||
|
179b88bfed
|
|||
|
b3665c6462
|
|||
|
42284704de
|
|||
|
650f60e70c
|
|||
|
efea1b29e7
|
|||
|
ae0bd75f3b
|
|||
|
d9100d8300
|
|||
|
900be47d34
|
|||
|
3d5f97a0f4
|
|||
|
9fde115016
|
|||
|
f8897dd075
|
|||
|
380753ca7a
|
|||
|
4715318913
|
|||
|
a78aee1639
|
|||
|
3581b7600b
|
|||
|
32207c3d6f
|
|||
|
9474a7336a
|
|||
|
5a6a279eaf
|
|||
|
c1f95edc96
|
|||
|
098bbc35c5
|
|||
|
314d4d344b
|
|||
|
7236749bd5
|
|||
|
2ff1f27614
|
|||
|
111afe4dd4
|
|||
|
c4c142482a
|
|||
|
f9c15abaf4
|
|||
|
d51d24f865
|
|||
|
1d00875a8c
|
|||
|
f89722fad8
|
|||
| 27917496c1 | |||
|
e0179bc442
|
|||
|
e665d03533
|
|||
|
b8cb2b4273
|
|||
|
d278dc5f5b
|
|||
|
59e73f0fd9
|
|||
|
3e0dc60283
|
|||
|
c24eb5125e
|
|||
|
25bd895dde
|
|||
|
bccd75317e
|
|||
|
f0e3f7574f
|
|||
|
5d44081847
|
|||
|
2a2bb0aec7
|
|||
|
67c40a3909
|
|||
|
1c30188122
|
|||
|
82a0f13242
|
|||
| 288d15a9bc | |||
|
504703d0f7
|
|||
|
e48895d0af
|
|||
| 13d32d0d27 | |||
| 19b9fdd623 | |||
|
ddcaebb51a
|
|||
|
f182312cd2
|
|||
|
73b21789d5
|
|||
|
5d7c724bc8
|
|||
|
74b297c89c
|
|||
|
822a74acce
|
|||
|
9a934fabfd
|
|||
|
828ec9a3fa
|
|||
|
63a43d79dd
|
|||
|
029caf4526
|
|||
|
1c5c418f1c
|
|||
|
a4139d4652
|
|||
|
2fd2071d40
|
|||
|
97b1ee8ab8
|
|||
|
dee479def5
|
|||
|
c8536e20d2
|
|||
|
d70137775f
|
|||
|
35ceda99aa
|
|||
|
7f3d74ee49
|
|||
|
b9f378de6f
|
|||
|
ccb17c7290
|
|||
|
505779310a
|
154
README.md
154
README.md
@@ -1,7 +1,159 @@
|
|||||||
# Midas
|
<h1>Midas</h1>
|
||||||
|
|
||||||
*Midas* is a type system to _Maintain Integrity of Data with Annotated Structures_. In Greek mythology, [Midas](https://en.wikipedia.org/wiki/Midas) was a Phrygian king who was blessed with the gift of turning everything he touched into gold.
|
*Midas* is a type system to _Maintain Integrity of Data with Annotated Structures_. In Greek mythology, [Midas](https://en.wikipedia.org/wiki/Midas) was a Phrygian king who was blessed with the gift of turning everything he touched into gold.
|
||||||
|
|
||||||
*Midas* aims at providing Python developers with a simple annotation system to enable compile-time integrity and data type checks, as well as generating runtime assertions.
|
*Midas* aims at providing Python developers with a simple annotation system to enable compile-time integrity and data type checks, as well as generating runtime assertions.
|
||||||
|
|
||||||
This framework is being developed as part of a Bachelor's Thesis by Louis Heredero at HEI Sion.
|
This framework is being developed as part of a Bachelor's Thesis by Louis Heredero at HEI Sion.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Table of Contents</strong></summary>
|
||||||
|
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Type Checking](#type-checking)
|
||||||
|
- [Compiling](#compiling)
|
||||||
|
- [Formatting](#formatting)
|
||||||
|
- [Highlighting](#highlighting)
|
||||||
|
- [Dumping the AST](#dumping-the-ast)
|
||||||
|
- [Dumping the Registry](#dumping-the-registry)
|
||||||
|
- [Generating Stubs](#generating-stubs)
|
||||||
|
- [Showing Type Judgements](#showing-type-judgements)
|
||||||
|
- [Validating Definitions](#validating-definitions)
|
||||||
|
- [Tests](#tests)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
```shell
|
||||||
|
git clone https://git.kb28.ch/HEL/midas.git
|
||||||
|
```
|
||||||
|
2. Go in the project directory
|
||||||
|
```shell
|
||||||
|
cd midas
|
||||||
|
```
|
||||||
|
3. Install the CLI as a user-wide tool
|
||||||
|
```shell
|
||||||
|
uv tool install .
|
||||||
|
```
|
||||||
|
4. You can now run the `midas` command from anywhere
|
||||||
|
```shell
|
||||||
|
midas --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
<!--
|
||||||
|
check
|
||||||
|
compile
|
||||||
|
format
|
||||||
|
highlight
|
||||||
|
parse
|
||||||
|
dump_registry
|
||||||
|
types
|
||||||
|
validate
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas check -t types.midas source.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This command parses the given files and run the type checkers against the Midas definitions and Python program. Diagnostics are then printed showing warnings and errors.
|
||||||
|
|
||||||
|
### Compiling
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas compile -t types.midas source.py
|
||||||
|
```
|
||||||
|
|
||||||
|
With the `compile` command, you can process a source Python file, with any number of custom type definition files (`-t FILE` option), and the type checker will verify the coherence of your program and generate the runnable code with valid syntax and runtime assertions.
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas format types.midas
|
||||||
|
midas format types.midas -o formatted.midas
|
||||||
|
```
|
||||||
|
|
||||||
|
This command parses the given Midas file and outputs a pretty printed file from the AST.
|
||||||
|
|
||||||
|
### Highlighting
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas highlight source.py
|
||||||
|
midas highlight source.py -o highlighted.html
|
||||||
|
midas highlight types.midas
|
||||||
|
midas highlight types.midas -o highlighted.html
|
||||||
|
```
|
||||||
|
|
||||||
|
The `highlight` command takes in a source file (Python or Midas), runs the appropriate parser and outputs an HTML file containing the source code with added highlighting. This highlighting takes the form of hoverable annotations showing some of the parsed structures (e.g. a function definition, an assignment, a generic type, etc.)
|
||||||
|
|
||||||
|
The optional `-o FILE` option can be used to specify an output path. By default, the file is printed in stdout (equivalent to `-o -`).
|
||||||
|
|
||||||
|
### Dumping the AST
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas parse source.py
|
||||||
|
midas parse types.midas
|
||||||
|
```
|
||||||
|
|
||||||
|
For debugging purposes, you can output the AST parsed from a Python or Midas file. For Python files, the `--raw` flags lets you toggle the custom AST parsing. With `--raw`, the raw AST is returned, as produced by the builtin `ast` module. This flag has no effect on Midas files.
|
||||||
|
|
||||||
|
### Dumping the Registry
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas dump-registry -t types.midas
|
||||||
|
```
|
||||||
|
|
||||||
|
This command processes the given Midas definitions and dumps the contents of the types registry.
|
||||||
|
|
||||||
|
### Generating Stubs
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas stubs types.midas -o stubs.pyi
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generate Python stubs from a Midas definition file
|
||||||
|
|
||||||
|
### Showing Type Judgements
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas types -t types.midas source.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This command type checks the given Python source file and logs all typing judgements made by the type checker.
|
||||||
|
|
||||||
|
### Validating Definitions
|
||||||
|
|
||||||
|
```shell
|
||||||
|
midas validate types.midas
|
||||||
|
```
|
||||||
|
|
||||||
|
This command lets you validate a Midas definition file by running the parser and type checker, verifying syntax and references.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Several snapshot tests are available to assert the good behaviour of the parsers and type checker. They can be run as follows:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
uv run -m tests.midas run -a
|
||||||
|
uv run -m tests.python run -a
|
||||||
|
uv run -m tests.checker run -a
|
||||||
|
uv run -m tests.generator run -a
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available subcommands:**
|
||||||
|
- Run all tests: `run -a`
|
||||||
|
- Run specific tests: `run tests/cases/test1.py tests/cases/test2.py ...`
|
||||||
|
- Update all tests: `update -a`
|
||||||
|
- Update specific tests: `update tests/cases/test1.py tests/cases/test2.py ...`
|
||||||
|
|||||||
117
assets/icon.svg
Normal file
117
assets/icon.svg
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:export-filename="logo.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
inkscape:version="1.4.4 (1:1.4.4+202605061436+dcaf3e7d9e)"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="1.9332778"
|
||||||
|
inkscape:cx="-8.2760999"
|
||||||
|
inkscape:cy="112.2446"
|
||||||
|
inkscape:window-width="2584"
|
||||||
|
inkscape:window-height="1028"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="4"
|
||||||
|
spacingy="4"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="4"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4689"
|
||||||
|
id="linearGradient1478"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
|
||||||
|
x1="26.648937"
|
||||||
|
y1="20.603781"
|
||||||
|
x2="135.66525"
|
||||||
|
y2="114.39767" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4689">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e1be1e;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4691" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffeb82;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4693" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4671"
|
||||||
|
id="linearGradient1475"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
|
||||||
|
x1="150.96111"
|
||||||
|
y1="192.35176"
|
||||||
|
x2="112.03144"
|
||||||
|
y2="137.27299" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4671">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffdc21;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4673" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffeb82;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4675" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Calque 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g1"
|
||||||
|
transform="translate(2.911719,3.414527)">
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient1478);fill-opacity:1"
|
||||||
|
d="m 60.510156,6.3979729 c -4.583653,0.021298 -8.960939,0.4122177 -12.8125,1.09375 C 36.35144,9.4962267 34.291407,13.691825 34.291406,21.429223 v 10.21875 h 26.8125 v 3.40625 h -26.8125 -10.0625 c -7.792459,0 -14.6157592,4.683717 -16.7500002,13.59375 -2.46182,10.212966 -2.5710151,16.586023 0,27.25 1.9059283,7.937852 6.4575432,13.593748 14.2500002,13.59375 h 9.21875 v -12.25 c 0,-8.849902 7.657144,-16.656248 16.75,-16.65625 h 26.78125 c 7.454951,0 13.406253,-6.138164 13.40625,-13.625 v -25.53125 c 0,-7.266339 -6.12998,-12.7247775 -13.40625,-13.9375001 -4.605987,-0.7667253 -9.385097,-1.1150483 -13.96875,-1.09375 z m -14.5,8.2187501 c 2.769547,0 5.03125,2.298646 5.03125,5.125 -2e-6,2.816336 -2.261703,5.09375 -5.03125,5.09375 -2.779476,-1e-6 -5.03125,-2.277415 -5.03125,-5.09375 -1e-6,-2.826353 2.251774,-5.125 5.03125,-5.125 z"
|
||||||
|
id="path1948" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient1475);fill-opacity:1"
|
||||||
|
d="m 91.228906,35.054223 v 11.90625 c 0,9.230755 -7.825895,16.999999 -16.75,17 h -26.78125 c -7.335833,0 -13.406249,6.278483 -13.40625,13.625 v 25.531247 c 0,7.26634 6.318588,11.54032 13.40625,13.625 8.487331,2.49561 16.626237,2.94663 26.78125,0 6.750155,-1.95439 13.406253,-5.88761 13.40625,-13.625 V 92.897973 h -26.78125 v -3.40625 h 26.78125 13.406254 c 7.79246,0 10.69625,-5.435408 13.40624,-13.59375 2.79933,-8.398886 2.68022,-16.475776 0,-27.25 -1.92578,-7.757441 -5.60387,-13.59375 -13.40624,-13.59375 z m -15.0625,64.65625 c 2.779478,3e-6 5.03125,2.277417 5.03125,5.093747 -2e-6,2.82635 -2.251775,5.125 -5.03125,5.125 -2.76955,0 -5.03125,-2.29865 -5.03125,-5.125 2e-6,-2.81633 2.261697,-5.093747 5.03125,-5.093747 z"
|
||||||
|
id="path1950" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
809
docs/manual.typ
Normal file
809
docs/manual.typ
Normal file
@@ -0,0 +1,809 @@
|
|||||||
|
//#import "@preview/codly:1.3.0": codly, codly-init
|
||||||
|
// Fix unaligned highlights in v0.15.0 ()
|
||||||
|
// See https://github.com/Dherse/codly/pull/132
|
||||||
|
#import "@local/codly:1.3.1": codly, codly-init
|
||||||
|
|
||||||
|
#import "@preview/codly-languages:0.1.10": codly-languages
|
||||||
|
#import "template.typ": TODO, project
|
||||||
|
#import "@preview/gentle-clues:1.3.1" as gc
|
||||||
|
|
||||||
|
#let midas-version = toml("../pyproject.toml").project.version
|
||||||
|
#let head-ref = read("../.git/HEAD").split(":").at(1).trim()
|
||||||
|
#let commit-hash = read("../.git/" + head-ref).slice(0, 8)
|
||||||
|
|
||||||
|
#show: project.with(
|
||||||
|
title: [Midas User Manual],
|
||||||
|
author: "Louis Heredero",
|
||||||
|
version: midas-version,
|
||||||
|
hash: commit-hash,
|
||||||
|
icon-path: path("../assets/icon.svg"),
|
||||||
|
)
|
||||||
|
|
||||||
|
#show: codly-init
|
||||||
|
#codly(
|
||||||
|
languages: codly-languages
|
||||||
|
+ (
|
||||||
|
midas: (
|
||||||
|
name: "Midas",
|
||||||
|
color: rgb("#eedd47"),
|
||||||
|
icon: box(
|
||||||
|
image(
|
||||||
|
"../assets/icon.svg",
|
||||||
|
height: 130%,
|
||||||
|
fit: "contain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
= Introduction
|
||||||
|
|
||||||
|
Python is a very popular programming language, especially in data sciences.
|
||||||
|
However, it has been designed for simplicity, distancing itself from typed languages such as Java or C to embrace dynamic typing.
|
||||||
|
What this means is that in Python, type checks are deferred to runtime when operations are concretely executed.
|
||||||
|
|
||||||
|
For developers, it might seem like a great way of simplifying the language and making it very flexible, but it does come with a cost.
|
||||||
|
Indeed, type errors are very easy to make in Python. While passing an integer where a string is expected might not be an issue in some cases, these are the sort of thing that can cause crashes or incorrect results without a clear diagnostic to help the user fix it.
|
||||||
|
|
||||||
|
Fortunately, developers using IDEs or properly configured text editors can benefit from external type checkers such as MyPy which will perform static type analysis of their Python code. Some can also be configured to be very strict, forcing the user to make the whole code typeable statically, thus avoiding any runtime type errors.
|
||||||
|
|
||||||
|
This is not the end of the problem though. Some parts of a program, especially in data related fields, may not be available at "compile-time". For example, a dataset can be loaded from an external file, or data can be fetched from an API, with no guarantees of having the expected format when analyzing the code statically.
|
||||||
|
|
||||||
|
In turn, that can cause a range of loud and silent errors at runtime. A malformed number will probably crash the program when trying to convert it, but a NaN in a series of value might just produce wrong results without any exception. Combine this with often long-running data-processing pipelines and this is how developers can waste hours of precious computation time.
|
||||||
|
|
||||||
|
Midas is a type system which can be used on top of Python to provide better type checking capabilities and gradual typing.
|
||||||
|
It aims at providing optional but strict type annotations and casting operations which can produce runtime assertions. It also allows the user to define dependent types with value constraints that are translated into runtime checks.
|
||||||
|
|
||||||
|
= Installation
|
||||||
|
|
||||||
|
Midas comes as a very light Python package that you can install on your system in a few simple steps.
|
||||||
|
|
||||||
|
== Requirements
|
||||||
|
|
||||||
|
Here below are the requirements for installing Midas. All Python dependencies will be installed by `uv` in the installation process described in @install-steps.
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- `uv`
|
||||||
|
|
||||||
|
== Steps <install-steps>
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
```bash
|
||||||
|
git clone https://git.kb28.ch/HEL/midas.git
|
||||||
|
```
|
||||||
|
2. Navigate inside the directory
|
||||||
|
```bash
|
||||||
|
cd midas
|
||||||
|
```
|
||||||
|
3. Install Midas as a tool in your local user space
|
||||||
|
```bash
|
||||||
|
uv tool install .
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it ! You can now use Midas commands anywhere, like this:
|
||||||
|
```bash
|
||||||
|
midas --help
|
||||||
|
```
|
||||||
|
|
||||||
|
= Quick Start
|
||||||
|
|
||||||
|
This chapter will give you the keys to quickly start using Midas in your project.
|
||||||
|
|
||||||
|
== Defining custom types
|
||||||
|
|
||||||
|
To begin with, you might want to define some custom types for your project, to avoid handling anonymous float values everywhere. To do so, create a `*.midas` file in your project, and write some definitions for your types. See @midas-ref for more information on syntax and features.
|
||||||
|
|
||||||
|
@qs-midas shows a simple example of what it might look like.
|
||||||
|
|
||||||
|
#codly(header: [types.midas])
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Meter = float
|
||||||
|
extend Meter {
|
||||||
|
def __add__: fn(Meter, /) -> Meter
|
||||||
|
def __sub__: fn(Meter, /) -> Meter
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinate = object
|
||||||
|
extend Coordinate {
|
||||||
|
prop x: Meter
|
||||||
|
prop y: Meter
|
||||||
|
}
|
||||||
|
```,
|
||||||
|
caption: [Example Midas type definitions],
|
||||||
|
) <qs-midas>
|
||||||
|
|
||||||
|
You can check for any syntax error using the following command:
|
||||||
|
```bash
|
||||||
|
midas validate types.midas
|
||||||
|
```
|
||||||
|
|
||||||
|
When you are happy with your definitions, you can generate Python stubs to use in your source code. This allows other type checkers like MyPy to recognize your custom types and avoid reporting them as undefined. It can also help catch some type errors in your IDE.
|
||||||
|
```bash
|
||||||
|
midas stubs types.midas -o stubs.pyi
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will generate a file as shown in @qs-stubs, providing stub classes to represent the type lattice including methods and properties.
|
||||||
|
|
||||||
|
#codly(header: [stubs.pyi])
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```pyi
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class Meter(float):
|
||||||
|
def __add__(self, _0: Meter, /) -> Meter: ...
|
||||||
|
def __sub__(self, _0: Meter, /) -> Meter: ...
|
||||||
|
|
||||||
|
class Coordinate(object):
|
||||||
|
x: Meter
|
||||||
|
y: Meter
|
||||||
|
```,
|
||||||
|
caption: [Generated stubs from example definitions of @qs-midas],
|
||||||
|
) <qs-stubs>
|
||||||
|
|
||||||
|
== Using Midas in Python
|
||||||
|
|
||||||
|
You can now write your Python program as you would normally. You can import your custom types from the generated stubs file and use them in type annotations.
|
||||||
|
|
||||||
|
You can also import the `cast` and `unsafe_cast` functions from `midas.typing` to explicitly cast a value to a specific type (see @cast for more information).
|
||||||
|
|
||||||
|
An example Python script is shown in @qs-python, demonstrating how you can use custom types in type annotations. Notice the comments describing errors that will be caught by the type checker in @qs-type-checking.
|
||||||
|
|
||||||
|
#codly(header: [script.py])
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
from lib import load_coordinate
|
||||||
|
from midas.typing import cast
|
||||||
|
from stubs import Coordinate, Meter
|
||||||
|
|
||||||
|
p1 = cast(Coordinate, load_coordinate(0))
|
||||||
|
p2 = cast(Coordinate, load_coordinate(1))
|
||||||
|
|
||||||
|
diff_x = p2.x - p1.x
|
||||||
|
diff_y = p2.y - p1.y
|
||||||
|
|
||||||
|
dist = diff_x + diff_y
|
||||||
|
|
||||||
|
p2.x += cast(Meter, 1)
|
||||||
|
p2.y = True # invalid, wrong type
|
||||||
|
p2.z = 3 # invalid, no property 'z' on Coordinate
|
||||||
|
p2.x.a = 3 # invalid, no properties on Meter
|
||||||
|
```,
|
||||||
|
caption: [Example Python script],
|
||||||
|
) <qs-python>
|
||||||
|
|
||||||
|
== Type checking <qs-type-checking>
|
||||||
|
|
||||||
|
Now that you have defined some types and written a script, you can run the type checker with the following command. You can also skip this step and directly run the compilation command in @qs-compilation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
midas check -t types.midas script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
== Compiling <qs-compilation>
|
||||||
|
|
||||||
|
The final step is to compile your code. This step will produce a runnable Python script, including runtime assertions generated by `cast` expressions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
midas compile -t types.midas script.py
|
||||||
|
python3 build/midas/script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
= Midas Language Reference <midas-ref>
|
||||||
|
|
||||||
|
In this chapter, you will find a complete reference for the Midas definition language.
|
||||||
|
|
||||||
|
A `*.midas` file contains a number of statements, which can be:
|
||||||
|
- *`alias`* statements (see @alias-stmt): to define a new type alias
|
||||||
|
- *`type`* statements (see @type-stmt): to define a new type
|
||||||
|
- *`extend`* statements (see @extend-stmt): to define member of a type
|
||||||
|
- *`predicate`* statements (see @predicate-stmt): to define named predicates that can be used in constraint types
|
||||||
|
|
||||||
|
== Alias Statement <alias-stmt>
|
||||||
|
|
||||||
|
An *`alias`* statement lets you define a new type alias. It requires a unique name and base type.
|
||||||
|
|
||||||
|
While a `type` statement (see @type-stmt) allows generic definitions, aliases are purely a for givin an alternative name to a type.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
alias MyType = float
|
||||||
|
```,
|
||||||
|
caption: [Simple `alias` statement declaring a new type "`MyType`" equivalent to `float`],
|
||||||
|
) <midas-simple-alias>
|
||||||
|
|
||||||
|
This statement defines a new type called `MyType` which is equivalent to `float`. `MyType` and `float` can be used interchangeably.
|
||||||
|
|
||||||
|
== Type Statement <type-stmt>
|
||||||
|
|
||||||
|
A *`type`* statement lets you define a new type. It requires a unique name and base type.
|
||||||
|
|
||||||
|
The simplest form of a *`type`* statement is:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type MyType = float
|
||||||
|
```,
|
||||||
|
caption: [Simple `type` statement declaring a new type "`MyType`" as a subtype of `float`],
|
||||||
|
) <midas-simple-type>
|
||||||
|
|
||||||
|
This statement defines a new type called `MyType` which is a subtype of `float`. `MyType` is a `float` but a `float` is not necessarily `MyType`.
|
||||||
|
|
||||||
|
=== Builtin / base types
|
||||||
|
|
||||||
|
A number of base types are provided out of the box, which can be used to derive other types.
|
||||||
|
|
||||||
|
They correspond to Python's builtin types:
|
||||||
|
```py object```,
|
||||||
|
```py str```,
|
||||||
|
```py float```,
|
||||||
|
```py int```,
|
||||||
|
```py bool```,
|
||||||
|
```py list```,
|
||||||
|
```py dict```,
|
||||||
|
```py None```.
|
||||||
|
|
||||||
|
Some differences are to be noted however.
|
||||||
|
1. ```py bool``` is not a subtype of ```py int```
|
||||||
|
2. ```py list``` are homogeneous, i.e. all items must be of the same type
|
||||||
|
3. ```py dict``` keys and values are homogeneous, i.e. all keys must be of the same type and all values must be of the same type (can be different from keys).
|
||||||
|
|
||||||
|
=== Function types
|
||||||
|
|
||||||
|
A function type is written in a similar notation to Python function definitions:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Repeater = fn(text: str, count: int) -> str
|
||||||
|
```,
|
||||||
|
caption: [Simple function type definition],
|
||||||
|
)
|
||||||
|
|
||||||
|
Midas supports positional-only, keyword-only and mixed arguments (using the `/` and `*` separators). You may omit the name of positional-only arguments. The return type is required.
|
||||||
|
|
||||||
|
Optional parameters can be indicated by adding a question mark (`?`) after their type:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Repeater = fn(text: str, count: int, *, sep: str?) -> str
|
||||||
|
```,
|
||||||
|
caption: [Function type definition with an optional keyword-only parameter],
|
||||||
|
)
|
||||||
|
|
||||||
|
#gc.warning[
|
||||||
|
Sink arguments (`*args`, `**kwargs`) are not currently supported.
|
||||||
|
]
|
||||||
|
|
||||||
|
=== Constraint types
|
||||||
|
|
||||||
|
A useful feature provided by Midas is the possibility to combine types with custom value constraints. For example, you might want to define a type for positive amounts of money:
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Money = float
|
||||||
|
type Income = Money where _ >= 0
|
||||||
|
```,
|
||||||
|
caption: [Simple constraint type definition],
|
||||||
|
)
|
||||||
|
|
||||||
|
Constraints can be combined with any type using the `where` keyword, followed by a constraint expression (see @constraint-expr).
|
||||||
|
|
||||||
|
=== Generic types
|
||||||
|
|
||||||
|
For more complex types, you might want to use type parameters. For example, to define a container, we might write:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Container[T] = object
|
||||||
|
```,
|
||||||
|
caption: [Simple generic container type definition],
|
||||||
|
)
|
||||||
|
|
||||||
|
To better refine a generic type, you can also bound type parameters using the following syntax:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Container[T <: float] = object
|
||||||
|
```,
|
||||||
|
caption: [Generic container type definition with a bound],
|
||||||
|
)
|
||||||
|
|
||||||
|
This can be read as "`Container` is a generic type which takes one type parameter `T` that must be a subtype of `float`".\
|
||||||
|
You can use a generic type, i.e. instantiate it, by using a similar syntax with concrete type as arguments:
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type MyContainer = Container[MyType]
|
||||||
|
```,
|
||||||
|
caption: [Application of a generic type],
|
||||||
|
)
|
||||||
|
|
||||||
|
Generic types can also take multiple parameters, which are then separated by commas:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type ZipCodeRegistry = dict[int, str]
|
||||||
|
```,
|
||||||
|
caption: [Application of a multi-parameter generic type],
|
||||||
|
)
|
||||||
|
|
||||||
|
The _body_ of a generic type, i.e. the right-hand side of the definition, can contain or even be equal to any number of its parameters.#footnote[The latter is not something that is expressible in standard Python, yet it brings a semantic distinction on top of structurally equivalent values.] For example, the following is a valid type statement:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Price[T <: Currency] = T where _ > 0
|
||||||
|
```,
|
||||||
|
caption: [Type parameters in a generic type's body],
|
||||||
|
)
|
||||||
|
|
||||||
|
=== `Column` / `Frame` types
|
||||||
|
|
||||||
|
To provide useful type-checking for data engineers, Midas offers two special types: `Column` and `Frame`.
|
||||||
|
Their goal is to help type check Pandas' `Series` and `DataFrame` respectively.
|
||||||
|
|
||||||
|
==== `Column`
|
||||||
|
|
||||||
|
The `Column` type is a generic type used to represent a `pandas.Series` object.
|
||||||
|
You can use it like any other generic type and it will provide type checking for some common methods and attributes offered by Pandas.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Temperature = float
|
||||||
|
alias Temperatures = Column[Temperature]
|
||||||
|
```,
|
||||||
|
caption: [Simple column type definition],
|
||||||
|
)
|
||||||
|
|
||||||
|
==== `Frame` <frame-type>
|
||||||
|
|
||||||
|
The `Frame` type is a super-powered generic type used to represent a `pandas.DataFrame` object.
|
||||||
|
In place of type arguments, `Frame` accepts a schema, i.e. a series of column definitions.
|
||||||
|
@simple-frame show how you can define a simple frame type with 3 columns:
|
||||||
|
- `name`: a column of `Name` values
|
||||||
|
- `age`: a column of `int` values
|
||||||
|
- `height`: a column of `float where _ >= 0` values
|
||||||
|
|
||||||
|
Notice that you don't need to specify `Column` types.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Name = str where len(_) != 0
|
||||||
|
alias Data = Frame[
|
||||||
|
name: Name,
|
||||||
|
age: int,
|
||||||
|
height: float where _ >= 0
|
||||||
|
]
|
||||||
|
```,
|
||||||
|
) <simple-frame>
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
|
|
||||||
|
== Extend Statement <extend-stmt>
|
||||||
|
|
||||||
|
Type statements allow you to define new types, kind of like type aliases. However, a type might have properties or methods of its own. These might override those of the parent type or be brand new members.
|
||||||
|
|
||||||
|
This is where the `extend` statement comes into play. It allows defining members on a given type. Members can either be properties (`prop`) or methods (`def`). The only difference between the two is that methods must be functions and can be overloaded.
|
||||||
|
|
||||||
|
Here is a simple example showing how to define a property and a method on a custom type:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type MyType = float
|
||||||
|
extend MyType {
|
||||||
|
prop norm: float
|
||||||
|
def double: fn() -> MyType
|
||||||
|
}
|
||||||
|
```,
|
||||||
|
caption: [Simple `extend` statement defining a property and a method],
|
||||||
|
)
|
||||||
|
|
||||||
|
An `extend` statement can appear anywhere after the type it extends has been defined.
|
||||||
|
You may want to override Python's dunder methods to implement type checking for some basic operators, like `__add__` for the `+` operator.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Money = float
|
||||||
|
extend Money {
|
||||||
|
def __add__(Money, /) -> Money
|
||||||
|
def __mul__(float, /) -> Money
|
||||||
|
}
|
||||||
|
```,
|
||||||
|
caption: [Simple `extend` statement overriding some dunder methods],
|
||||||
|
)
|
||||||
|
|
||||||
|
When extending generic type, you must specify the whole type, including its parameter(s):
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Container[T <: float] = object
|
||||||
|
extend Container[T <: float] {
|
||||||
|
prop content: T
|
||||||
|
def set_content: fn(content: T) -> None
|
||||||
|
}
|
||||||
|
```,
|
||||||
|
caption: [Generic `extend` statement using type parameters in the declared members],
|
||||||
|
)
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
|
|
||||||
|
== Predicate Statement <predicate-stmt>
|
||||||
|
|
||||||
|
A *`predicate`* statement lets you define a named constraint expression, like a function, which can then be used in other constraint expressions (either in other predicate statements or in constraint types). See @constraint-expr for more information about the syntax of constraint expressions.
|
||||||
|
|
||||||
|
The left-hand side of a predicate statement is written as a function signature, without a return type. The right-hand side is a constraint expression. For example:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
predicate is_positive(v: float) = v >= 0
|
||||||
|
```,
|
||||||
|
caption: [Simple `predicate` statement defining an `is_positive` predicate],
|
||||||
|
)
|
||||||
|
|
||||||
|
The left-hand side can also be curried to allow partial application. For example:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
predicate in_range(mn: float, mx: float)(v: float) = mn <= v & v <= mx
|
||||||
|
predicate is_ratio = in_range(0.0, 1.0)
|
||||||
|
```,
|
||||||
|
caption: [Curried `predicate` statement and partial application],
|
||||||
|
) <midas-predicate-partial>
|
||||||
|
|
||||||
|
Notice that the second predicate statement doesn't take any parameters. This is simply a partial application of another predicate, kind of like an alias. You can use it in other expressions to finalize the call:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Efficiency = float where is_ratio(_)
|
||||||
|
```,
|
||||||
|
caption: [Constraint type definition using the partially applied predicate from @midas-predicate-partial],
|
||||||
|
)
|
||||||
|
|
||||||
|
Of course you can also directly call `in_range`:
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
type Efficiency = float where in_range(0.0, 1.0)(_)
|
||||||
|
```,
|
||||||
|
caption: [Full call of curried predicate from @midas-predicate-partial],
|
||||||
|
)
|
||||||
|
|
||||||
|
When compiled, named predicates are translated to Python functions which are used in runtime assertions. Only predicates that are referenced are compiled.
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
|
== Constraint Expressions <constraint-expr>
|
||||||
|
|
||||||
|
*Constraint expressions* are Python-like expressions which can appear in *`predicate`* statements or in constraint types.
|
||||||
|
|
||||||
|
They can contain comparisons, simple computations, logical operations and must evaluate to a boolean value.
|
||||||
|
|
||||||
|
Context is quite restricted inside these expressions. You can only reference some builtin functions, such as type constructors (`float(...)`, `str(...)`, etc.), parameters of predicate statements, and named predicates. In constraint type, the special variable `_` can be used to reference the value targeted by the type. For example:
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```midas
|
||||||
|
predicate not_nan(v: float) = v != float("nan")
|
||||||
|
type RealFloat = float where not_nan(_)
|
||||||
|
```,
|
||||||
|
caption: [Example constraint expressions],
|
||||||
|
) <ex-constraint-expr>
|
||||||
|
|
||||||
|
In the predicate statement (@ex-constraint-expr:1), we reference the parameter `v` and the builtin `float` function.
|
||||||
|
In the constraint type definition (@ex-constraint-expr:2), we then reference the named predicate `not_nan`, passing the value targeted by the type itself ( `_` )
|
||||||
|
|
||||||
|
= Supported Python Syntax <python-ref>
|
||||||
|
|
||||||
|
Midas integrates naturally in Python via type annotations. Through generated stubs, even other type checker can detect your custom types (see @cmd-stubs).
|
||||||
|
|
||||||
|
It has been designed to leave the user free of typing any amount of their code but be strict about the parts that are annotated. By default, any untyped Python expression is assigned `UnknownType`.
|
||||||
|
|
||||||
|
Any operation is permitted on `UnknownType` and will result in `UnknownType` values.
|
||||||
|
|
||||||
|
The moment an expression can be typed, that be thanks to an annotation or a literal value, the type checker kicks in and will validate your statements.
|
||||||
|
|
||||||
|
Because Python is very flexible language with many features, some expressions and statements might be more complex to properly type check, thus only a subset of the Python language is fully supported. This chapter lists all supported features of Python and how they affect type checking.
|
||||||
|
|
||||||
|
Some examples are presented in the following sections in the form of code blocks. Highlights in the code blocks indicate the type assigned to each expression by the type checker. Some types may be omitted for readability. For example:
|
||||||
|
|
||||||
|
#codly(
|
||||||
|
highlights: (
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 5,
|
||||||
|
fill: green,
|
||||||
|
tag: [_int_],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 2,
|
||||||
|
start: 7,
|
||||||
|
end: 7,
|
||||||
|
fill: green,
|
||||||
|
tag: [_int_],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
```python
|
||||||
|
v = 3
|
||||||
|
print(v)
|
||||||
|
```
|
||||||
|
|
||||||
|
== Literals
|
||||||
|
|
||||||
|
Literal Python values are type checked using builtin types. Lists and dictionaries of literals are also typed liked literals. This does not include comprehension lists/dicts (```py [. for . in .]```), nor formatted strings (```py f"..."```). @supported-literals shows the list of supported literal values and their type.
|
||||||
|
|
||||||
|
#let supported-literals = table(
|
||||||
|
columns: 2,
|
||||||
|
table.header[*Example value*][*Judged Type*],
|
||||||
|
```py 42```, ```py int```,
|
||||||
|
```py 3.14```, ```py float```,
|
||||||
|
```py True```, ```py bool```,
|
||||||
|
```py "Midas"```, ```py str```,
|
||||||
|
```py None```, ```py None```,
|
||||||
|
```py [1, 2, 3]```, ```py list[int]```,
|
||||||
|
```py {1: "One", 2: "Two"}```, ```py dict[int, str]```,
|
||||||
|
```py ("1", 1, True)```, ```py tuple[str, int, bool]```,
|
||||||
|
)
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
supported-literals,
|
||||||
|
caption: [Supported literal values and their judged types],
|
||||||
|
) <supported-literals>
|
||||||
|
|
||||||
|
== Assignments
|
||||||
|
|
||||||
|
Variable assignments allow assigning a new value to a variable. For the type checker, this implies two things:
|
||||||
|
1. If the variable was not already declared in the current scope, it is declared at that point with the type of the right-hand side expression
|
||||||
|
2. If the variable was already declared, the type of the right-hand side expression is checked against the declared type of the variable. Only a subtype of the variable's type can be assigned to it
|
||||||
|
|
||||||
|
Once a variable has been given a type, it cannot be changed in the same scope.
|
||||||
|
|
||||||
|
The walrus operator (```py :=```) is not currently supported.
|
||||||
|
|
||||||
|
A simple annotation declaration, without assigning a value, is enough to declare a variable. For example:
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
var: float
|
||||||
|
```,
|
||||||
|
caption: [Bare Python variable annotation without assignment],
|
||||||
|
)
|
||||||
|
|
||||||
|
Because unpacking is not supported, assigning to multiple values is also not handled by the type checker.
|
||||||
|
For more information about type annotations, see @type-annotations
|
||||||
|
|
||||||
|
== Arithmetic
|
||||||
|
|
||||||
|
- All basic binary operators are supported, through dunder methods.
|
||||||
|
- All comparison operators except ```py in``` are supported.
|
||||||
|
- All unary operators are supported (`+`, `-`, `~`).
|
||||||
|
- All logical operators are supported (```py and```, ```py or```, ```py not```).
|
||||||
|
|
||||||
|
== Ternary operator
|
||||||
|
|
||||||
|
The ternary operator ```py . if . else .``` is supported. As for `if` statements (see @if-else), the test expression must be a boolean. Additionally, both branches must be of the same type.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
#codly(
|
||||||
|
highlights: (
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 10,
|
||||||
|
end: 44,
|
||||||
|
tag: [_str_],
|
||||||
|
fill: blue,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 11,
|
||||||
|
end: 16,
|
||||||
|
tag: [_str_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 39,
|
||||||
|
end: 43,
|
||||||
|
tag: [_str_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 21,
|
||||||
|
end: 32,
|
||||||
|
tag: [_bool_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
parity = ("even" if num % 2 == 0 else "odd")
|
||||||
|
```,
|
||||||
|
caption: [Typing of ternary operator],
|
||||||
|
)
|
||||||
|
|
||||||
|
== Control flow
|
||||||
|
|
||||||
|
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and typ checked by Midas.
|
||||||
|
|
||||||
|
=== `if` / `elif` / `else` <if-else>
|
||||||
|
|
||||||
|
Conditional statements are checked relatively strictly by Midas. The test expression, i.e. what comes after the ```py if``` keyword, must be a boolean. While Python allows introducing and leaking new variables from inside an ```py if``` statement, Midas will strictly forbid leaks by restraining bindings to the scope they are defined in. For example, the following Python code will not compile with Midas:
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
age = 22
|
||||||
|
if age >= 18:
|
||||||
|
msg = "You're an adult"
|
||||||
|
else:
|
||||||
|
msg = "You're still a child"
|
||||||
|
print(msg) # -> unknown variable 'msg'
|
||||||
|
```,
|
||||||
|
caption: [`if`/`else` statement cannot leak variables],
|
||||||
|
)
|
||||||
|
|
||||||
|
=== `for` loops
|
||||||
|
|
||||||
|
Simple forms of `for` loops can be used, that is using a single variable and iterating over an object implementing the `__getitem__` method. Like above in @if-else, leaking variables from inside the loop is ignored.
|
||||||
|
|
||||||
|
`for`-`else` statements are not supported. `while` loops are also not supported.
|
||||||
|
|
||||||
|
== Functions
|
||||||
|
|
||||||
|
You can define functions as usual and the type checker will do its best to type it. Apart from argument sinks (`*args`, `**kwargs`), all forms of parameter specifications are supported (positional-only, keyword-only, mixed, optional).
|
||||||
|
|
||||||
|
As for the rest of your code, type annotations are optional, but recommended. If you omit the return type hint, the type checker will try to infer it from the function body and its return statements. If you did specify a return type, all return paths must return values that are subtypes of the type hint.
|
||||||
|
|
||||||
|
#codly(
|
||||||
|
highlights: (
|
||||||
|
(
|
||||||
|
line: 2,
|
||||||
|
start: 12,
|
||||||
|
end: 16,
|
||||||
|
tag: [_float_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 2,
|
||||||
|
start: 12,
|
||||||
|
tag: [_float_],
|
||||||
|
fill: blue,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 3,
|
||||||
|
start: 10,
|
||||||
|
end: 15,
|
||||||
|
tag: [_(value: float) -> float_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 3,
|
||||||
|
start: 17,
|
||||||
|
end: 19,
|
||||||
|
tag: [_float_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 3,
|
||||||
|
start: 10,
|
||||||
|
tag: [_float_],
|
||||||
|
fill: blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
def double(value: float) -> float:
|
||||||
|
return value * 2
|
||||||
|
result = double(4.0)
|
||||||
|
```,
|
||||||
|
caption: [Typing of function's body and call],
|
||||||
|
)
|
||||||
|
|
||||||
|
Anonymous functions (```py lambda```) are not yet supported
|
||||||
|
|
||||||
|
== Casts <cast>
|
||||||
|
|
||||||
|
#gc.info[
|
||||||
|
The functions discussed in this section are provided by the `midas.typing` submodule. You can import them in your script like so:
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
from midas.typing import cast, unsafe_cast
|
||||||
|
```,
|
||||||
|
caption: [Importing cast functions],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
Sometimes, you may want to use a value whose type is not known to the type checker in a place where it expects a particular type. In that case, if you do know that the runtime type will correspond to what is expected, you can use a `cast` expression.
|
||||||
|
|
||||||
|
Similar to the `cast` function from the `typing` package of Python's Standard Library, it allows telling the type checker that a value has a given type. While `typing`'s function doesn't have any runtime side-effect, Midas' will generate runtime assertions, ensuring that your statement is true when running the code. What cannot be checked statically is checked at runtime.
|
||||||
|
|
||||||
|
In the following example, a runtime check would be generated to ensure that the value is indeed a `float` and that it satisfies the type's constraint (i.e. `>= 0`):
|
||||||
|
|
||||||
|
#codly(
|
||||||
|
highlights: (
|
||||||
|
(
|
||||||
|
line: 1,
|
||||||
|
start: 35,
|
||||||
|
end: 47,
|
||||||
|
tag: [_UnknownType_],
|
||||||
|
fill: red,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
line: 2,
|
||||||
|
start: 7,
|
||||||
|
end: 17,
|
||||||
|
tag: [_PositiveFloat_],
|
||||||
|
fill: green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
typed_value = cast(PositiveFloat, unknown_value)
|
||||||
|
print(typed_value)
|
||||||
|
```,
|
||||||
|
caption: [Typing of `cast` expression],
|
||||||
|
)
|
||||||
|
|
||||||
|
#gc.warning[
|
||||||
|
Assertions are statements inserted just before a statement using a `cast` expression. This means that the expression is evaluated _before_ its actual intended usage location, which might cause issues if you rely on logical operator short-circuiting. See @eager-eval for more information.
|
||||||
|
]
|
||||||
|
|
||||||
|
There may be some cases where the cost of checking a value at runtime is simply not worth the safety, for example when dealing with a big dataset. If do wish so, you can use `unsafe_cast` which will only tell the type checker the type of the value, without generating a runtime assertion. This maps to the default behavior of `typing`'s own `cast` function.
|
||||||
|
|
||||||
|
If the value passed to `cast` or `unsafe_cast` is a literal (e.g. an integer, a string, a list of literals, etc.), the assertion is evaluated _at compile-time_ and no runtime assertion is generated.
|
||||||
|
|
||||||
|
== Annotations / Type Hints <type-annotations>
|
||||||
|
|
||||||
|
Vanilla Python already lets you use type hints to specify the type of variables and function parameters.
|
||||||
|
|
||||||
|
Midas use them to type check your code. Additionally, it allows you to use a special syntax to define a `Frame` types directly in these annotations.
|
||||||
|
|
||||||
|
Because these annotations are not interpretable by Python, your integrated type checker might complain loudly about them being invalid.
|
||||||
|
A workaround is to silence it by adding a type comment at the end of the line, as shown in @silence-errors.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
var: Frame[name: str, age: float] # type: ignore # noqa: F821
|
||||||
|
```,
|
||||||
|
caption: [MyPy's and Pylance's complaints about custom type annotation can be silenced with type comments],
|
||||||
|
) <silence-errors>
|
||||||
|
|
||||||
|
=== Frame type annotation
|
||||||
|
|
||||||
|
The syntax is similar to how you can define frame types in the Midas language (see @frame-type). The only difference is that types can only be name references; you cannot inline constraint types.
|
||||||
|
|
||||||
|
The example of @python-frame-type shows how you can annotate a dataframe with some columns directly in Python.
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```python
|
||||||
|
df: Frame[name: Name, age: float, height: Length[Meter]] = ...
|
||||||
|
```,
|
||||||
|
caption: [Frame type annotation in Python],
|
||||||
|
) <python-frame-type>
|
||||||
|
|
||||||
|
= Commands <commands>
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
|
||||||
|
== Type Checking (`check`) <cmd-check>
|
||||||
|
== Compiling (`compile`) <cmd-compile>
|
||||||
|
== Formatting (`format`) <cmd-format>
|
||||||
|
== Highlighting (`highlight`) <cmd-highlight>
|
||||||
|
== Dumping the AST (`parse`) <cmd-parse>
|
||||||
|
== Dumping the Registry (`dump-registry`) <cmd-registry>
|
||||||
|
== Generating Stubs (`stubs`) <cmd-stubs>
|
||||||
|
== Showing Type Judgements (`types`) <cmd-types>
|
||||||
|
== Validating Definitions (`validate`) <cmd-validate>
|
||||||
|
|
||||||
|
= Known limitations <limitations>
|
||||||
|
|
||||||
|
== Eager evaluation in runtime assertions <eager-eval>
|
||||||
|
|
||||||
|
The process of generating assertions to ensure safety at runtime, mainly for `cast` expressions, leads to the creation of aliases for the expressions being casted. These alias definitions eagerly evaluate before the assertion, and most importantly before the real usage location. This means that you should avoid using `cast` expressions inside logical expressions like `and` or `or`, because the normal "short-circuit" behavior will be irrelevant to the evaluations of the operands.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
```py
|
||||||
|
def foo():
|
||||||
|
print("Foo")
|
||||||
|
return True
|
||||||
|
def bar():
|
||||||
|
print("Bar")
|
||||||
|
return True
|
||||||
|
result = foo() or bar()
|
||||||
|
# Foo
|
||||||
|
# Bar
|
||||||
|
```,
|
||||||
|
caption: [Runtime assertions may eagerly evaluate expressions and bypass logical operator's short-circuit],
|
||||||
|
)
|
||||||
211
docs/midas.sublime-syntax
Normal file
211
docs/midas.sublime-syntax
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
%YAML 1.2
|
||||||
|
---
|
||||||
|
name: Midas
|
||||||
|
file_extensions:
|
||||||
|
- midas
|
||||||
|
scope: source.midas
|
||||||
|
|
||||||
|
variables:
|
||||||
|
identifier: "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||||
|
|
||||||
|
contexts:
|
||||||
|
prototype:
|
||||||
|
- include: comments
|
||||||
|
|
||||||
|
main:
|
||||||
|
- include: keywords
|
||||||
|
- include: types
|
||||||
|
|
||||||
|
comments:
|
||||||
|
- match: "//"
|
||||||
|
scope: punctuation.definition.comment.midas
|
||||||
|
push:
|
||||||
|
- meta_scope: comment.line.midas
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
- match: /\*
|
||||||
|
scope: punctuation.definition.comment.midas
|
||||||
|
push:
|
||||||
|
- meta_scope: comment.block.midas
|
||||||
|
- match: \*/
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
string:
|
||||||
|
- meta_include_prototype: false
|
||||||
|
- meta_scope: string.quoted.double.c
|
||||||
|
- match: '"'
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
keywords:
|
||||||
|
- match: \balias\b
|
||||||
|
scope: keyword.declaration.midas
|
||||||
|
push: alias-stmt
|
||||||
|
- match: \btype\b
|
||||||
|
scope: keyword.declaration.midas
|
||||||
|
push: type-stmt
|
||||||
|
- match: \bextend\b
|
||||||
|
scope: keyword.declaration.midas
|
||||||
|
push: extend-stmt
|
||||||
|
- match: \bpredicate\b
|
||||||
|
scope: keyword.declaration.midas
|
||||||
|
push: predicate-stmt
|
||||||
|
|
||||||
|
alias-stmt:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: entity.name.type
|
||||||
|
- match: "="
|
||||||
|
scope: keyword.operator.equal.midas
|
||||||
|
push: type-expr
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
type-stmt:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: entity.name.type
|
||||||
|
- match: \[
|
||||||
|
push: type-params
|
||||||
|
- match: "="
|
||||||
|
scope: keyword.operator.equal.midas
|
||||||
|
push: type-expr
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
type-expr:
|
||||||
|
- match: \b(fn)\s*(\()
|
||||||
|
captures:
|
||||||
|
1: keyword.other.midas
|
||||||
|
2: punctuation.section.group.begin
|
||||||
|
push: fn-params
|
||||||
|
- match: \b(where)\b
|
||||||
|
scope: keyword.other.midas
|
||||||
|
set: constraint
|
||||||
|
- match: "Frame"
|
||||||
|
scope: entity.name.type
|
||||||
|
push:
|
||||||
|
- match: \[
|
||||||
|
push: frame-schema
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: entity.name.type
|
||||||
|
- match: $
|
||||||
|
pop: 2
|
||||||
|
|
||||||
|
fn-params:
|
||||||
|
- match: "({{identifier}})(:)"
|
||||||
|
captures:
|
||||||
|
1: variable.parameter.midas
|
||||||
|
2: punctuation.separator.annotation.midas
|
||||||
|
push:
|
||||||
|
- include: type-expr
|
||||||
|
- match: \?
|
||||||
|
scope: keyword.operator.qmark.midas
|
||||||
|
- match: "(?=,)"
|
||||||
|
scope: punctuation.separator.midas
|
||||||
|
pop: true
|
||||||
|
- match: '(?=\))'
|
||||||
|
pop: true
|
||||||
|
- include: type-expr
|
||||||
|
- match: '\)'
|
||||||
|
set:
|
||||||
|
- match: "->"
|
||||||
|
scope: keyword.operator.arrow.midas
|
||||||
|
set: type-expr
|
||||||
|
|
||||||
|
constraint:
|
||||||
|
- match: $
|
||||||
|
pop: 2
|
||||||
|
|
||||||
|
- match: \d+(\.\d+)?
|
||||||
|
scope: constant.numeric.midas
|
||||||
|
|
||||||
|
- match: \b(true|false|none)\b
|
||||||
|
scope: constant.language.midas
|
||||||
|
|
||||||
|
- match: '"'
|
||||||
|
push: string
|
||||||
|
|
||||||
|
- match: (<=|>=|<|>|==|!=|&)
|
||||||
|
scope: keyword.operator
|
||||||
|
|
||||||
|
- match: _
|
||||||
|
scope: variable.language.midas
|
||||||
|
|
||||||
|
- match: '{{identifier}}(?=\s*\()'
|
||||||
|
scope: variable.function.midas
|
||||||
|
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: variable.other.readwrite.midas
|
||||||
|
|
||||||
|
type-params:
|
||||||
|
- match: "<:"
|
||||||
|
scope: keyword.operator.subtype.midas
|
||||||
|
- match: "[a-zA-Z][a-zA-Z_0-9]*"
|
||||||
|
scope: entity.name.type
|
||||||
|
- match: "]"
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
extend-stmt:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: entity.name.type
|
||||||
|
- match: \[
|
||||||
|
push: type-params
|
||||||
|
- match: \{
|
||||||
|
scope: punctuation.section.block.begin
|
||||||
|
set: extend-body
|
||||||
|
|
||||||
|
extend-body:
|
||||||
|
- include: member-stmt
|
||||||
|
- match: \}
|
||||||
|
scope: punctuation.section.block.end
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
member-stmt:
|
||||||
|
- match: \b(prop|def)\b
|
||||||
|
scope: keyword.other.midas
|
||||||
|
push:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: variable.other.member
|
||||||
|
- match: ":"
|
||||||
|
push: type-expr
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
predicate-stmt:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: entity.name.function.midas
|
||||||
|
- match: '\('
|
||||||
|
push: predicate-params
|
||||||
|
- match: "="
|
||||||
|
scope: keyword.operator.equal.midas
|
||||||
|
set: constraint
|
||||||
|
- match: $
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
predicate-params:
|
||||||
|
- match: "({{identifier}})(:)"
|
||||||
|
captures:
|
||||||
|
1: variable.parameter.midas
|
||||||
|
2: punctuation.separator.annotation.midas
|
||||||
|
push:
|
||||||
|
- include: type-expr
|
||||||
|
- match: "(?=,)"
|
||||||
|
scope: punctuation.separator.midas
|
||||||
|
pop: true
|
||||||
|
- match: '(?=\))'
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
- match: '\)'
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
frame-schema:
|
||||||
|
- include: frame-column
|
||||||
|
- match: \]
|
||||||
|
# scope: punctuation.section.block.end
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
frame-column:
|
||||||
|
- match: "{{identifier}}"
|
||||||
|
scope: variable.other.member
|
||||||
|
- match: ":"
|
||||||
|
push: type-expr
|
||||||
143
docs/template.typ
Normal file
143
docs/template.typ
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#import "@preview/modpattern:0.2.0": modpattern
|
||||||
|
|
||||||
|
#let TODO = block(
|
||||||
|
width: 6em,
|
||||||
|
height: 3em,
|
||||||
|
stroke: red,
|
||||||
|
fill: modpattern(
|
||||||
|
size: (10pt, 10pt),
|
||||||
|
line(
|
||||||
|
start: (0%, 0%),
|
||||||
|
end: (100%, 100%),
|
||||||
|
stroke: gray.transparentize(60%) + 2pt,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
align(
|
||||||
|
center + horizon,
|
||||||
|
text(fill: red, size: 1.5em)[*TODO*],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
#let _render-header(version, hash) = {
|
||||||
|
let last-heading = query(heading.where(level: 1).before(here())).last(default: none)
|
||||||
|
let next-heading = query(heading.where(level: 1).after(here())).first(default: none)
|
||||||
|
|
||||||
|
let current-heading = if next-heading != none and next-heading.location().page() == here().page() {
|
||||||
|
next-heading
|
||||||
|
} else if last-heading != none {
|
||||||
|
last-heading
|
||||||
|
} else { none }
|
||||||
|
|
||||||
|
let chapter = if current-heading != none {
|
||||||
|
let body = current-heading.body
|
||||||
|
if current-heading.numbering != none {
|
||||||
|
let num = counter(heading).display(current-heading.numbering, at: current-heading.location())
|
||||||
|
body = [#num #body]
|
||||||
|
}
|
||||||
|
body
|
||||||
|
} else []
|
||||||
|
|
||||||
|
grid(
|
||||||
|
columns: (1fr, auto, 1fr),
|
||||||
|
align: (left, center, right),
|
||||||
|
document.title, [v#version - #hash], chapter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let _unshift-prefix(prefix, content) = context {
|
||||||
|
pad(left: -measure(prefix).width, prefix + content)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let project(
|
||||||
|
title: none,
|
||||||
|
author: none,
|
||||||
|
version: "0.0.1",
|
||||||
|
hash: "abcdefgh",
|
||||||
|
icon-path: none,
|
||||||
|
doc,
|
||||||
|
) = {
|
||||||
|
assert(title != none, message: "Please provide a title")
|
||||||
|
|
||||||
|
set document(
|
||||||
|
title: title,
|
||||||
|
author: author,
|
||||||
|
)
|
||||||
|
set text(
|
||||||
|
font: "Source Sans 3",
|
||||||
|
)
|
||||||
|
|
||||||
|
set raw(syntaxes: path("midas.sublime-syntax"))
|
||||||
|
|
||||||
|
let front-page() = {
|
||||||
|
align(center)[
|
||||||
|
#{
|
||||||
|
set text(size: 1.5em)
|
||||||
|
std.title()
|
||||||
|
}
|
||||||
|
|
||||||
|
v#version - #hash
|
||||||
|
|
||||||
|
#if icon-path != none {
|
||||||
|
v(1cm)
|
||||||
|
image(icon-path)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pagebreak()
|
||||||
|
}
|
||||||
|
|
||||||
|
let outlines() = {
|
||||||
|
outline()
|
||||||
|
pagebreak()
|
||||||
|
outline(
|
||||||
|
title: [List of Listings],
|
||||||
|
target: figure.where(kind: raw),
|
||||||
|
)
|
||||||
|
|
||||||
|
outline(
|
||||||
|
title: [List of Tables],
|
||||||
|
target: figure.where(kind: table),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let main() = {
|
||||||
|
// Adapted from https://github.com/hei-templates/hei-synd-thesis/blob/7d2b941197babae0bf3afd4e5914754e09a64001/lib/template-thesis.typ#L242-L261
|
||||||
|
show heading.where(level: 1): it => {
|
||||||
|
pagebreak()
|
||||||
|
|
||||||
|
set text(size: 1.5em)
|
||||||
|
set block(above: 1.2em, below: 1.2em)
|
||||||
|
if it.numbering != none {
|
||||||
|
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||||
|
let prefix = num + h(1em)
|
||||||
|
_unshift-prefix(prefix, it.body)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show heading.where(level: 2): it => {
|
||||||
|
if it.numbering != none {
|
||||||
|
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||||
|
_unshift-prefix(num + h(0.8em), it.body)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set page(
|
||||||
|
header: context _render-header(version, hash),
|
||||||
|
footer: context if page.numbering != none {
|
||||||
|
align(center, counter(page).display(page.numbering, both: true))
|
||||||
|
},
|
||||||
|
numbering: "1 / 1",
|
||||||
|
)
|
||||||
|
|
||||||
|
show heading: set heading(numbering: "I.1.")
|
||||||
|
counter(page).update(1)
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
|
||||||
|
front-page()
|
||||||
|
outlines()
|
||||||
|
main()
|
||||||
|
}
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
# ruff: disable[F821]
|
# ruff: disable[F821]
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# Prototype of custom type import to use valid Python syntax
|
|
||||||
import midas
|
|
||||||
midas.using("02_custom_types.midas")
|
|
||||||
|
|
||||||
# A data-frame using a custom type
|
# A data-frame using a custom type
|
||||||
df: Frame[
|
df: Frame[
|
||||||
location: GeoLocation
|
location: GeoLocation
|
||||||
|
|||||||
33
examples/00_syntax_prototype/05_custom_types_v3.midas
Normal file
33
examples/00_syntax_prototype/05_custom_types_v3.midas
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
type Foo1 = float
|
||||||
|
type Foo2 = float where (_ > 3)
|
||||||
|
type Foo3 = int | float
|
||||||
|
type Foo4 = int where (_ > 3) | float where (_ > 3)
|
||||||
|
type Foo5 = (int | float) where (_ > 3)
|
||||||
|
type Foo6 = {
|
||||||
|
foo: float
|
||||||
|
bar: float where (_ > 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Foo7[T] = T where (_ > 3)
|
||||||
|
type Foo8[A, B<:int] = {
|
||||||
|
a: A
|
||||||
|
b: B
|
||||||
|
}
|
||||||
|
|
||||||
|
type Complex = {
|
||||||
|
a: int
|
||||||
|
b: int
|
||||||
|
}
|
||||||
|
type Complex2 = Complex where (_.a > 3 & _.b < 5)
|
||||||
|
|
||||||
|
predicate Positive(n: int) = n >= 0
|
||||||
|
|
||||||
|
extend Foo1 {
|
||||||
|
op __add__(Foo1) -> Foo1
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Foo7[T] {
|
||||||
|
op __add__(Foo7[T]) -> Foo7[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Optional[T] = None | T
|
||||||
@@ -9,3 +9,5 @@ d = True
|
|||||||
e = d + d
|
e = d + d
|
||||||
|
|
||||||
f: float = a
|
f: float = a
|
||||||
|
|
||||||
|
f = -f
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
type Meter(float)
|
type Meter = float
|
||||||
type Second(float)
|
type Second = float
|
||||||
type MeterPerSecond(float)
|
type MeterPerSecond = float
|
||||||
|
|
||||||
extend Meter {
|
extend Meter {
|
||||||
op __add__(Meter) -> Meter
|
def __add__: fn(Meter, /) -> Meter
|
||||||
op __sub__(Meter) -> Meter
|
def __sub__: fn(Meter, /) -> Meter
|
||||||
op __truediv__(Second) -> MeterPerSecond
|
def __truediv__: fn(Second, /) -> MeterPerSecond
|
||||||
}
|
}
|
||||||
|
|
||||||
extend Second {
|
extend Second {
|
||||||
op __add__(Second) -> Second
|
def __add__: fn(Second, /) -> Second
|
||||||
op __sub__(Second) -> Second
|
def __sub__: fn(Second, /) -> Second
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
# ruff: disable [F821]
|
# ruff: disable [F821]
|
||||||
|
|
||||||
midas.using("02_simple_types.midas")
|
|
||||||
|
|
||||||
distance: Meter = cast(Meter, 123.45)
|
distance: Meter = cast(Meter, 123.45)
|
||||||
time: Second = cast(Second, 6.7)
|
time: Second = cast(Second, 6.7)
|
||||||
speed = distance / time
|
speed = distance / time
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ def minimum(x: int, y: int):
|
|||||||
else:
|
else:
|
||||||
return y
|
return y
|
||||||
|
|
||||||
|
|
||||||
a = 15
|
a = 15
|
||||||
b = 72
|
b = 72
|
||||||
c = minimum(a, b)
|
c = minimum(a, b)
|
||||||
|
|
||||||
|
|
||||||
def factorial(n: int) -> int:
|
def factorial(n: int) -> int:
|
||||||
if n <= 1:
|
if n <= 1:
|
||||||
return 1
|
return 1
|
||||||
return n * factorial(n - 1)
|
return n * factorial(n - 1)
|
||||||
|
|
||||||
|
|
||||||
category = "Category 1" if a < 10 else "Category 2"
|
category = "Category 1" if a < 10 else "Category 2"
|
||||||
|
|
||||||
|
|
||||||
|
def foo() -> None:
|
||||||
|
pass
|
||||||
|
|||||||
21
examples/01_simple_type_checking/04_complex_types.midas
Normal file
21
examples/01_simple_type_checking/04_complex_types.midas
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
type Meter = float
|
||||||
|
|
||||||
|
extend Meter {
|
||||||
|
def __add__: fn(Meter, /) -> Meter
|
||||||
|
def __sub__: fn(Meter, /) -> Meter
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinate = object
|
||||||
|
|
||||||
|
extend Coordinate {
|
||||||
|
prop x: Meter
|
||||||
|
prop y: Meter
|
||||||
|
}
|
||||||
|
|
||||||
|
type Difference[T <: float] = T
|
||||||
|
type MeterDifference = Difference[Meter]
|
||||||
|
|
||||||
|
type CompDiff[T <: float] = {
|
||||||
|
prop d1: Difference[T]
|
||||||
|
prop d2: Difference[T]
|
||||||
|
}
|
||||||
37
examples/01_simple_type_checking/04_complex_types.py
Normal file
37
examples/01_simple_type_checking/04_complex_types.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# type: ignore
|
||||||
|
# ruff: disable [F821]
|
||||||
|
|
||||||
|
p1: Coordinate
|
||||||
|
p2: Coordinate
|
||||||
|
|
||||||
|
diff_x = p2.x - p1.x
|
||||||
|
diff_y = p2.y - p1.y
|
||||||
|
|
||||||
|
dist = diff_x + diff_y
|
||||||
|
|
||||||
|
p2.x += cast(Meter, 1)
|
||||||
|
p2.y = True # invalid, wrong type
|
||||||
|
p2.z = 3 # invalid, no property 'z' on Coordinate
|
||||||
|
p2.x.a = 3 # invalid, no properties on Meter
|
||||||
|
|
||||||
|
foo: list[float] = []
|
||||||
|
|
||||||
|
append = foo.append
|
||||||
|
|
||||||
|
foo.append("") # invalid, must be float
|
||||||
|
foo.append(2)
|
||||||
|
append(True) # invalid, must be float
|
||||||
|
append(2)
|
||||||
|
|
||||||
|
bar: list[list[Meter]]
|
||||||
|
|
||||||
|
bar.append([p2.x])
|
||||||
|
|
||||||
|
foo2 = foo + foo
|
||||||
|
|
||||||
|
a = foo[0]
|
||||||
|
b = bar[0][1]
|
||||||
|
c = bar[0][1][2] # invalid, not method __getitem__ on Meter
|
||||||
|
c = bar[""] # invalid, wrong index type
|
||||||
|
|
||||||
|
d = foo[1:2]
|
||||||
28
examples/01_simple_type_checking/05_functions.py
Normal file
28
examples/01_simple_type_checking/05_functions.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
def incr(value: int):
|
||||||
|
return value + 1
|
||||||
|
|
||||||
|
|
||||||
|
def decr(value: int):
|
||||||
|
return value - 1
|
||||||
|
|
||||||
|
|
||||||
|
def foo(a: int, /, b: float, *, c: str):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
r1 = foo() # foo() missing 2 required positional arguments: 'a' and 'b'
|
||||||
|
r2 = foo(1) # foo() missing 1 required positional argument: 'b'
|
||||||
|
r3 = foo(1, 2.0) # foo() missing 1 required keyword-only argument: 'c'
|
||||||
|
r4 = foo(1, b=2.0) # foo() missing 1 required keyword-only argument: 'c'
|
||||||
|
r5 = foo(1, 2.0, "test") # foo() takes 2 positional arguments but 3 were given
|
||||||
|
r6 = foo(1, 2.0, b=3.0) # foo() got multiple values for argument 'b'
|
||||||
|
r7 = foo(
|
||||||
|
a=1
|
||||||
|
) # foo() got some positional-only arguments passed as keyword arguments: 'a'
|
||||||
|
r8 = foo(g="test") # foo() got an unexpected keyword argument 'g'
|
||||||
|
|
||||||
|
r9a = foo(1, 2.0, c="test")
|
||||||
|
r9b = foo(1, b=2.0, c="test")
|
||||||
|
r9c = foo(1, c="test", b=2.0)
|
||||||
|
|
||||||
|
r10 = foo("a", 3, c=False) # wrong argument types
|
||||||
10
examples/01_simple_type_checking/06_overloads.midas
Normal file
10
examples/01_simple_type_checking/06_overloads.midas
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
type T1 = object
|
||||||
|
type T2 = object
|
||||||
|
type Foo = object
|
||||||
|
type T2b = T2
|
||||||
|
|
||||||
|
extend Foo {
|
||||||
|
def bar: fn(T1, /) -> int
|
||||||
|
def bar: fn(T2, /) -> float
|
||||||
|
def bar: fn(T2b, /) -> int
|
||||||
|
}
|
||||||
18
examples/01_simple_type_checking/06_overloads.py
Normal file
18
examples/01_simple_type_checking/06_overloads.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# type: ignore
|
||||||
|
# ruff: disable [F821]
|
||||||
|
|
||||||
|
foo: Foo
|
||||||
|
t1: T1
|
||||||
|
t2: T2
|
||||||
|
|
||||||
|
a = foo.bar(t1)
|
||||||
|
b = foo.bar(t2)
|
||||||
|
|
||||||
|
func = foo.bar
|
||||||
|
|
||||||
|
c = func(t1)
|
||||||
|
d = func(t2)
|
||||||
|
|
||||||
|
t2b: T2b
|
||||||
|
|
||||||
|
e = foo.bar(t2b)
|
||||||
15
examples/02_demonstration/demo.midas
Normal file
15
examples/02_demonstration/demo.midas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
predicate in_range(min: float, max: float)(v: float) = min <= v & v <= max
|
||||||
|
predicate is_ratio = in_range(0, 1)
|
||||||
|
|
||||||
|
type Currency = float
|
||||||
|
type Price[T <: Currency] = T where _ >= 0
|
||||||
|
|
||||||
|
extend Price[T <: Currency] {
|
||||||
|
def __add__: fn(Price[T], /) -> Price[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type EUR = Currency
|
||||||
|
type USD = Currency
|
||||||
|
type CHF = Currency
|
||||||
|
|
||||||
|
type Discount = float where is_ratio(_)
|
||||||
35
examples/02_demonstration/demo.py
Normal file
35
examples/02_demonstration/demo.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from demo_stubs import CHF, EUR, USD, Currency, Discount, Price
|
||||||
|
|
||||||
|
from midas.typing import cast, unsafe_cast
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=Currency)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_discount(amount: Price[T], discount: Discount) -> Price[T]:
|
||||||
|
return cast(Price[T], (1.0 - discount) * amount)
|
||||||
|
|
||||||
|
|
||||||
|
a1 = cast(Price[EUR], 3.2)
|
||||||
|
a2 = cast(Price[USD], 10.4)
|
||||||
|
r1 = cast(Discount, 0.2)
|
||||||
|
|
||||||
|
print(apply_discount(a1, r1))
|
||||||
|
print(apply_discount(a2, r1))
|
||||||
|
|
||||||
|
a3 = a1 + a1
|
||||||
|
a4 = a1 + a2 # cannot add euros and dollars
|
||||||
|
a3 = a2 # cannot change variable type
|
||||||
|
|
||||||
|
dyn_price = float(input("Price (CHF): "))
|
||||||
|
dyn_discount = float(input("Discount (0.0-1.0): "))
|
||||||
|
discounted = apply_discount(
|
||||||
|
cast(Price[CHF], dyn_price),
|
||||||
|
cast(Discount, dyn_discount),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Discounted: CHF {discounted}")
|
||||||
|
|
||||||
|
large_data = [i * 10 for i in range(100)]
|
||||||
|
prices = unsafe_cast(list[Price[EUR]], large_data)
|
||||||
14
examples/02_demonstration/demo_stubs.pyi
Normal file
14
examples/02_demonstration/demo_stubs.pyi
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
class Currency(float): ...
|
||||||
|
|
||||||
|
_T0 = TypeVar("_T0", bound=Currency, covariant=True)
|
||||||
|
|
||||||
|
class Price(Currency, Generic[_T0]):
|
||||||
|
def __add__(self, _0: Price[_T0], /) -> Price[_T0]: ...
|
||||||
|
|
||||||
|
class EUR(Currency): ...
|
||||||
|
class USD(Currency): ...
|
||||||
|
class CHF(Currency): ...
|
||||||
|
class Discount(float): ...
|
||||||
15
gen/gen.py
15
gen/gen.py
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
{preamble}
|
||||||
{sections}
|
{sections}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -57,6 +58,11 @@ IMPORTS_REGEX = re.compile(
|
|||||||
re.MULTILINE | re.DOTALL,
|
re.MULTILINE | re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PREAMBLE_REGEX = re.compile(
|
||||||
|
r"^###>\s*Preamble\s*?\n(?P<body>.*?)\n###<$",
|
||||||
|
re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def snake_case(text: str) -> str:
|
def snake_case(text: str) -> str:
|
||||||
return re.sub(r"[A-Z]", lambda c: "_" + c.group().lower(), text).lower().strip("_")
|
return re.sub(r"[A-Z]", lambda c: "_" + c.group().lower(), text).lower().strip("_")
|
||||||
@@ -88,13 +94,14 @@ def make_banner(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def make_section(full_name: str, base: str, param: str, body: str) -> str:
|
def make_section(full_name: str, base: str, param: str, body: str) -> str:
|
||||||
|
print(f" Generating {full_name}")
|
||||||
visitor_methods: list[str] = []
|
visitor_methods: list[str] = []
|
||||||
classes: list[str] = []
|
classes: list[str] = []
|
||||||
definitions: list[str] = body.strip("\n").split("\n\n\n")
|
definitions: list[str] = body.strip("\n").split("\n\n\n")
|
||||||
for cls in definitions:
|
for cls in definitions:
|
||||||
cls = cls.strip("\n")
|
cls = cls.strip("\n")
|
||||||
name: str = re.match("class (.*?):", cls).group(1) # type: ignore
|
name: str = re.match("class (.*?):", cls).group(1) # type: ignore
|
||||||
print(f"Processing {name}")
|
print(f" Processing {name}")
|
||||||
visitor_methods.append(make_visitor_method(name, param))
|
visitor_methods.append(make_visitor_method(name, param))
|
||||||
classes.append(make_class(name, cls, base))
|
classes.append(make_class(name, cls, base))
|
||||||
|
|
||||||
@@ -107,6 +114,7 @@ def make_section(full_name: str, base: str, param: str, body: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def generate(definitions_path: Path, out_path: Path):
|
def generate(definitions_path: Path, out_path: Path):
|
||||||
|
print(f"Processing generating {out_path} from {definitions_path}")
|
||||||
root_dir: Path = Path(__file__).parent.parent
|
root_dir: Path = Path(__file__).parent.parent
|
||||||
rel_path: Path = definitions_path.relative_to(root_dir)
|
rel_path: Path = definitions_path.relative_to(root_dir)
|
||||||
src: str = definitions_path.read_text()
|
src: str = definitions_path.read_text()
|
||||||
@@ -116,6 +124,10 @@ def generate(definitions_path: Path, out_path: Path):
|
|||||||
if m := IMPORTS_REGEX.search(src):
|
if m := IMPORTS_REGEX.search(src):
|
||||||
imports = m.group("body").strip("\n")
|
imports = m.group("body").strip("\n")
|
||||||
|
|
||||||
|
preamble: str = ""
|
||||||
|
if m := PREAMBLE_REGEX.search(src):
|
||||||
|
preamble = m.group("body")
|
||||||
|
|
||||||
for section_m in SECTION_REGEX.finditer(src):
|
for section_m in SECTION_REGEX.finditer(src):
|
||||||
full_name: str = section_m.group("name")
|
full_name: str = section_m.group("name")
|
||||||
base: str = section_m.group("base")
|
base: str = section_m.group("base")
|
||||||
@@ -129,6 +141,7 @@ def generate(definitions_path: Path, out_path: Path):
|
|||||||
gen_path=Path(__file__).relative_to(root_dir),
|
gen_path=Path(__file__).relative_to(root_dir),
|
||||||
),
|
),
|
||||||
imports=imports,
|
imports=imports,
|
||||||
|
preamble=preamble,
|
||||||
sections="\n\n\n".join(sections),
|
sections="\n\n\n".join(sections),
|
||||||
)
|
)
|
||||||
out_path.write_text(result)
|
out_path.write_text(result)
|
||||||
|
|||||||
116
gen/midas.py
116
gen/midas.py
@@ -4,6 +4,7 @@
|
|||||||
###> Imports
|
###> Imports
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
from typing import Any, Generic, Optional, TypeVar
|
from typing import Any, Generic, Optional, TypeVar
|
||||||
|
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
@@ -12,51 +13,64 @@ from midas.lexer.token import Token
|
|||||||
###<
|
###<
|
||||||
|
|
||||||
|
|
||||||
|
###> Preamble
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TypeParam:
|
||||||
|
location: Location
|
||||||
|
name: Token
|
||||||
|
bound: Optional[Type]
|
||||||
|
|
||||||
|
|
||||||
|
class MemberKind(Enum):
|
||||||
|
PROPERTY = auto()
|
||||||
|
METHOD = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
l_paren: Token
|
||||||
|
pos: list[FunctionType.Argument]
|
||||||
|
mixed: list[FunctionType.Argument]
|
||||||
|
kw: list[FunctionType.Argument]
|
||||||
|
|
||||||
|
|
||||||
|
###<
|
||||||
|
|
||||||
|
|
||||||
###> Stmt | Statements
|
###> Stmt | Statements
|
||||||
class SimpleTypeStmt:
|
class TypeStmt:
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
params: list[TypeParam]
|
||||||
base: TypeExpr
|
type: Type
|
||||||
constraint: Optional[Expr]
|
|
||||||
|
|
||||||
|
|
||||||
class ComplexTypeStmt:
|
class AliasStmt:
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
type: Type
|
||||||
properties: list[PropertyStmt]
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyStmt:
|
class MemberStmt:
|
||||||
name: Token
|
name: Token
|
||||||
type: TypeExpr
|
type: Type
|
||||||
constraint: Optional[Expr]
|
kind: MemberKind
|
||||||
|
|
||||||
|
|
||||||
class ExtendStmt:
|
class ExtendStmt:
|
||||||
type: TypeExpr
|
|
||||||
operations: list[OpStmt]
|
|
||||||
|
|
||||||
|
|
||||||
class OpStmt:
|
|
||||||
name: Token
|
name: Token
|
||||||
operand: TypeExpr
|
params: list[TypeParam]
|
||||||
result: TypeExpr
|
members: list[MemberStmt]
|
||||||
|
|
||||||
|
|
||||||
class PredicateStmt:
|
class PredicateStmt:
|
||||||
name: Token
|
name: Token
|
||||||
subject: Token
|
params: list[ParamSpec]
|
||||||
type: TypeExpr
|
body: Expr
|
||||||
condition: Expr
|
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|
||||||
|
|
||||||
###> Expr | Expressions
|
###> Expr | Expressions
|
||||||
class SimpleTypeExpr:
|
|
||||||
name: Token
|
|
||||||
optional: bool
|
|
||||||
|
|
||||||
|
|
||||||
class LogicalExpr:
|
class LogicalExpr:
|
||||||
@@ -76,6 +90,12 @@ class UnaryExpr:
|
|||||||
right: Expr
|
right: Expr
|
||||||
|
|
||||||
|
|
||||||
|
class CallExpr:
|
||||||
|
callee: Expr
|
||||||
|
arguments: list[Expr]
|
||||||
|
keywords: dict[str, Expr]
|
||||||
|
|
||||||
|
|
||||||
class GetExpr:
|
class GetExpr:
|
||||||
expr: Expr
|
expr: Expr
|
||||||
name: Token
|
name: Token
|
||||||
@@ -97,14 +117,54 @@ class WildcardExpr:
|
|||||||
token: Token
|
token: Token
|
||||||
|
|
||||||
|
|
||||||
class TemplateExpr:
|
###<
|
||||||
type: TypeExpr
|
|
||||||
|
###> Type | Types
|
||||||
|
|
||||||
|
|
||||||
class TypeExpr:
|
class NamedType:
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
|
||||||
optional: bool
|
|
||||||
|
class GenericType:
|
||||||
|
type: Type
|
||||||
|
args: list[Type]
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintType:
|
||||||
|
type: Type
|
||||||
|
constraint: Expr
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexType:
|
||||||
|
members: list[MemberStmt]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionType:
|
||||||
|
base: Type
|
||||||
|
extension: ComplexType
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionType:
|
||||||
|
params: ParamSpec
|
||||||
|
returns: Type
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Argument:
|
||||||
|
location: Optional[Location] = None
|
||||||
|
name: Optional[Token]
|
||||||
|
type: Type
|
||||||
|
required: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FrameType:
|
||||||
|
columns: list[Column]
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Column:
|
||||||
|
location: Optional[Location] = None
|
||||||
|
name: Token
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from midas.ast.location import Location
|
|||||||
###> MidasType | Type annotations | node
|
###> MidasType | Type annotations | node
|
||||||
class BaseType:
|
class BaseType:
|
||||||
base: str
|
base: str
|
||||||
param: Optional[MidasType]
|
args: tuple[MidasType, ...]
|
||||||
|
|
||||||
|
|
||||||
class ConstraintType:
|
class ConstraintType:
|
||||||
@@ -82,6 +82,20 @@ class IfStmt:
|
|||||||
orelse: list[Stmt]
|
orelse: list[Stmt]
|
||||||
|
|
||||||
|
|
||||||
|
class Pass:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ForStmt:
|
||||||
|
target: Expr
|
||||||
|
iterator: Expr
|
||||||
|
body: list[Stmt]
|
||||||
|
|
||||||
|
|
||||||
|
class RawStmt:
|
||||||
|
stmt: ast.stmt
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|
||||||
|
|
||||||
@@ -128,15 +142,10 @@ class LogicalExpr:
|
|||||||
right: Expr
|
right: Expr
|
||||||
|
|
||||||
|
|
||||||
class SetExpr:
|
|
||||||
object: Expr
|
|
||||||
name: str
|
|
||||||
value: Expr
|
|
||||||
|
|
||||||
|
|
||||||
class CastExpr:
|
class CastExpr:
|
||||||
type: MidasType
|
type: MidasType
|
||||||
expr: Expr
|
expr: Expr
|
||||||
|
unsafe: bool
|
||||||
|
|
||||||
|
|
||||||
class TernaryExpr:
|
class TernaryExpr:
|
||||||
@@ -145,4 +154,32 @@ class TernaryExpr:
|
|||||||
if_false: Expr
|
if_false: Expr
|
||||||
|
|
||||||
|
|
||||||
|
class ListExpr:
|
||||||
|
items: list[Expr]
|
||||||
|
|
||||||
|
|
||||||
|
class DictExpr:
|
||||||
|
keys: list[Optional[Expr]]
|
||||||
|
values: list[Expr]
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptExpr:
|
||||||
|
object: Expr
|
||||||
|
index: Expr
|
||||||
|
|
||||||
|
|
||||||
|
class SliceExpr:
|
||||||
|
lower: Optional[Expr]
|
||||||
|
upper: Optional[Expr]
|
||||||
|
step: Optional[Expr]
|
||||||
|
|
||||||
|
|
||||||
|
class TupleExpr:
|
||||||
|
items: tuple[Expr, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class RawExpr:
|
||||||
|
expr: ast.expr
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
from typing import Any, Generic, Optional, TypeVar
|
from typing import Any, Generic, Optional, TypeVar
|
||||||
|
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
@@ -14,6 +15,26 @@ from midas.lexer.token import Token
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TypeParam:
|
||||||
|
location: Location
|
||||||
|
name: Token
|
||||||
|
bound: Optional[Type]
|
||||||
|
|
||||||
|
|
||||||
|
class MemberKind(Enum):
|
||||||
|
PROPERTY = auto()
|
||||||
|
METHOD = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
l_paren: Token
|
||||||
|
pos: list[FunctionType.Argument]
|
||||||
|
mixed: list[FunctionType.Argument]
|
||||||
|
kw: list[FunctionType.Argument]
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Statements #
|
# Statements #
|
||||||
##############
|
##############
|
||||||
@@ -28,80 +49,65 @@ class Stmt(ABC):
|
|||||||
|
|
||||||
class Visitor(ABC, Generic[T]):
|
class Visitor(ABC, Generic[T]):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_simple_type_stmt(self, stmt: SimpleTypeStmt) -> T: ...
|
def visit_type_stmt(self, stmt: TypeStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_complex_type_stmt(self, stmt: ComplexTypeStmt) -> T: ...
|
def visit_alias_stmt(self, stmt: AliasStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_property_stmt(self, stmt: PropertyStmt) -> T: ...
|
def visit_member_stmt(self, stmt: MemberStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_extend_stmt(self, stmt: ExtendStmt) -> T: ...
|
def visit_extend_stmt(self, stmt: ExtendStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def visit_op_stmt(self, stmt: OpStmt) -> T: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_predicate_stmt(self, stmt: PredicateStmt) -> T: ...
|
def visit_predicate_stmt(self, stmt: PredicateStmt) -> T: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SimpleTypeStmt(Stmt):
|
class TypeStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
params: list[TypeParam]
|
||||||
base: TypeExpr
|
type: Type
|
||||||
constraint: Optional[Expr]
|
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_simple_type_stmt(self)
|
return visitor.visit_type_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ComplexTypeStmt(Stmt):
|
class AliasStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
type: Type
|
||||||
properties: list[PropertyStmt]
|
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_complex_type_stmt(self)
|
return visitor.visit_alias_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PropertyStmt(Stmt):
|
class MemberStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
type: TypeExpr
|
type: Type
|
||||||
constraint: Optional[Expr]
|
kind: MemberKind
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_property_stmt(self)
|
return visitor.visit_member_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ExtendStmt(Stmt):
|
class ExtendStmt(Stmt):
|
||||||
type: TypeExpr
|
name: Token
|
||||||
operations: list[OpStmt]
|
params: list[TypeParam]
|
||||||
|
members: list[MemberStmt]
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_extend_stmt(self)
|
return visitor.visit_extend_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class OpStmt(Stmt):
|
|
||||||
name: Token
|
|
||||||
operand: TypeExpr
|
|
||||||
result: TypeExpr
|
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
|
||||||
return visitor.visit_op_stmt(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PredicateStmt(Stmt):
|
class PredicateStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
subject: Token
|
params: list[ParamSpec]
|
||||||
type: TypeExpr
|
body: Expr
|
||||||
condition: Expr
|
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_predicate_stmt(self)
|
return visitor.visit_predicate_stmt(self)
|
||||||
@@ -120,9 +126,6 @@ class Expr(ABC):
|
|||||||
def accept(self, visitor: Visitor[T]) -> T: ...
|
def accept(self, visitor: Visitor[T]) -> T: ...
|
||||||
|
|
||||||
class Visitor(ABC, Generic[T]):
|
class Visitor(ABC, Generic[T]):
|
||||||
@abstractmethod
|
|
||||||
def visit_simple_type_expr(self, expr: SimpleTypeExpr) -> T: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_logical_expr(self, expr: LogicalExpr) -> T: ...
|
def visit_logical_expr(self, expr: LogicalExpr) -> T: ...
|
||||||
|
|
||||||
@@ -132,6 +135,9 @@ class Expr(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_unary_expr(self, expr: UnaryExpr) -> T: ...
|
def visit_unary_expr(self, expr: UnaryExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_call_expr(self, expr: CallExpr) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_get_expr(self, expr: GetExpr) -> T: ...
|
def visit_get_expr(self, expr: GetExpr) -> T: ...
|
||||||
|
|
||||||
@@ -147,21 +153,6 @@ class Expr(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_wildcard_expr(self, expr: WildcardExpr) -> T: ...
|
def visit_wildcard_expr(self, expr: WildcardExpr) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def visit_template_expr(self, expr: TemplateExpr) -> T: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def visit_type_expr(self, expr: TypeExpr) -> T: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SimpleTypeExpr(Expr):
|
|
||||||
name: Token
|
|
||||||
optional: bool
|
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
|
||||||
return visitor.visit_simple_type_expr(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class LogicalExpr(Expr):
|
class LogicalExpr(Expr):
|
||||||
@@ -192,6 +183,16 @@ class UnaryExpr(Expr):
|
|||||||
return visitor.visit_unary_expr(self)
|
return visitor.visit_unary_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CallExpr(Expr):
|
||||||
|
callee: Expr
|
||||||
|
arguments: list[Expr]
|
||||||
|
keywords: dict[str, Expr]
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_call_expr(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GetExpr(Expr):
|
class GetExpr(Expr):
|
||||||
expr: Expr
|
expr: Expr
|
||||||
@@ -233,19 +234,109 @@ class WildcardExpr(Expr):
|
|||||||
return visitor.visit_wildcard_expr(self)
|
return visitor.visit_wildcard_expr(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
#########
|
||||||
class TemplateExpr(Expr):
|
# Types #
|
||||||
type: TypeExpr
|
#########
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
|
||||||
return visitor.visit_template_expr(self)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Type(ABC):
|
||||||
|
location: Location
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def accept(self, visitor: Visitor[T]) -> T: ...
|
||||||
|
|
||||||
|
class Visitor(ABC, Generic[T]):
|
||||||
|
@abstractmethod
|
||||||
|
def visit_named_type(self, type: NamedType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_generic_type(self, type: GenericType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_constraint_type(self, type: ConstraintType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_complex_type(self, type: ComplexType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_extension_type(self, type: ExtensionType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_function_type(self, type: FunctionType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_frame_type(self, type: FrameType) -> T: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TypeExpr(Expr):
|
class NamedType(Type):
|
||||||
name: Token
|
name: Token
|
||||||
template: Optional[TemplateExpr]
|
|
||||||
optional: bool
|
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
return visitor.visit_type_expr(self)
|
return visitor.visit_named_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GenericType(Type):
|
||||||
|
type: Type
|
||||||
|
args: list[Type]
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_generic_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ConstraintType(Type):
|
||||||
|
type: Type
|
||||||
|
constraint: Expr
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_constraint_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComplexType(Type):
|
||||||
|
members: list[MemberStmt]
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_complex_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExtensionType(Type):
|
||||||
|
base: Type
|
||||||
|
extension: ComplexType
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_extension_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FunctionType(Type):
|
||||||
|
params: ParamSpec
|
||||||
|
returns: Type
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Argument:
|
||||||
|
location: Optional[Location] = None
|
||||||
|
name: Optional[Token]
|
||||||
|
type: Type
|
||||||
|
required: bool
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_function_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FrameType(Type):
|
||||||
|
columns: list[Column]
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Column:
|
||||||
|
location: Optional[Location] = None
|
||||||
|
name: Token
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_frame_type(self)
|
||||||
|
|||||||
@@ -85,88 +85,93 @@ class AstPrinter(Generic[T]):
|
|||||||
child.accept(self)
|
child.accept(self)
|
||||||
|
|
||||||
|
|
||||||
class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]):
|
class MidasAstPrinter(
|
||||||
|
AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None], m.Type.Visitor[None]
|
||||||
|
):
|
||||||
# Statements
|
# Statements
|
||||||
|
|
||||||
def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt):
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||||
self._write_line("SimpleTypeStmt")
|
self._write_line("TypeStmt")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
self._write_optional_child("template", stmt.template)
|
self._write_line("params")
|
||||||
self._write_line("base")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.base.accept(self)
|
|
||||||
self._write_optional_child("constraint", stmt.constraint, last=True)
|
|
||||||
|
|
||||||
def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt):
|
|
||||||
self._write_line("ComplexTypeStmt")
|
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
for i, param in enumerate(stmt.params):
|
||||||
self._write_optional_child("template", stmt.template)
|
|
||||||
self._write_line("properties", last=True)
|
|
||||||
with self._child_level():
|
|
||||||
for i, prop in enumerate(stmt.properties):
|
|
||||||
self._idx = i
|
self._idx = i
|
||||||
if i == len(stmt.properties) - 1:
|
if i == len(stmt.params) - 1:
|
||||||
self._mark_last()
|
self._mark_last()
|
||||||
prop.accept(self)
|
self._print_type_param(param)
|
||||||
|
self._write_line("type", last=True)
|
||||||
def visit_property_stmt(self, stmt: m.PropertyStmt):
|
with self._child_level(single=True):
|
||||||
self._write_line("PropertyStmt")
|
stmt.type.accept(self)
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
def visit_alias_stmt(self, stmt: m.AliasStmt) -> None:
|
||||||
self._write_line("type")
|
self._write_line("AliasStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_line("type", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.type.accept(self)
|
||||||
|
|
||||||
|
def _print_type_param(self, param: m.TypeParam) -> None:
|
||||||
|
self._write_line("Param")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{param.name.lexeme}"')
|
||||||
|
self._write_optional_child("bound", param.bound, last=True)
|
||||||
|
|
||||||
|
def visit_member_stmt(self, stmt: m.MemberStmt):
|
||||||
|
self._write_line("MemberStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"kind: {stmt.kind.name}")
|
||||||
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_line("type", last=True)
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
stmt.type.accept(self)
|
stmt.type.accept(self)
|
||||||
self._write_optional_child("constraint", stmt.constraint, last=True)
|
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||||
self._write_line("ExtendStmt")
|
self._write_line("ExtendStmt")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line("type")
|
self._write_line("params")
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.type.accept(self)
|
|
||||||
self._write_line("operations", last=True)
|
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
for i, op in enumerate(stmt.operations):
|
for i, param in enumerate(stmt.params):
|
||||||
self._idx = i
|
self._idx = i
|
||||||
if i == len(stmt.operations) - 1:
|
if i == len(stmt.params) - 1:
|
||||||
self._mark_last()
|
self._mark_last()
|
||||||
op.accept(self)
|
self._print_type_param(param)
|
||||||
|
|
||||||
def visit_op_stmt(self, stmt: m.OpStmt) -> None:
|
|
||||||
self._write_line("OpStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_line("params")
|
||||||
self._write_line("operand")
|
with self._child_level():
|
||||||
with self._child_level(single=True):
|
for i, param in enumerate(stmt.params):
|
||||||
stmt.operand.accept(self)
|
self._idx = i
|
||||||
|
if i == len(stmt.params) - 1:
|
||||||
self._write_line("result", last=True)
|
self._mark_last()
|
||||||
with self._child_level(single=True):
|
self._print_type_param(param)
|
||||||
stmt.result.accept(self)
|
self._write_line("members", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, member in enumerate(stmt.members):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(stmt.members) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
member.accept(self)
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
||||||
self._write_line("PredicateStmt")
|
self._write_line("PredicateStmt")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
self._write_line(f'subject: "{stmt.subject.lexeme}"')
|
self._write_line("params")
|
||||||
self._write_line("type")
|
with self._child_level():
|
||||||
|
for i, spec in enumerate(stmt.params):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(stmt.params) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._visit_param_spec(spec)
|
||||||
|
|
||||||
|
self._write_line("body", last=True)
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
stmt.type.accept(self)
|
stmt.body.accept(self)
|
||||||
self._write_line("condition", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.condition.accept(self)
|
|
||||||
|
|
||||||
# Expressions
|
# Expressions
|
||||||
|
|
||||||
def visit_simple_type_expr(self, expr: m.SimpleTypeExpr):
|
|
||||||
self._write_line("SimpleTypeExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{expr.name.lexeme}"')
|
|
||||||
self._write_line(f"optional: {expr.optional}", last=True)
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr):
|
def visit_logical_expr(self, expr: m.LogicalExpr):
|
||||||
self._write_line("LogicalExpr")
|
self._write_line("LogicalExpr")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
@@ -202,6 +207,29 @@ class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]):
|
|||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.right.accept(self)
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> None:
|
||||||
|
self._write_line("CallExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("callee")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.callee.accept(self)
|
||||||
|
self._write_line("arguments")
|
||||||
|
with self._child_level():
|
||||||
|
for i, arg in enumerate(expr.arguments):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.arguments) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
arg.accept(self)
|
||||||
|
self._write_line("keywords", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, (name, arg) in enumerate(expr.keywords.items()):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.keywords) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._write_line(name)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
arg.accept(self)
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr):
|
def visit_get_expr(self, expr: m.GetExpr):
|
||||||
self._write_line("GetExpr")
|
self._write_line("GetExpr")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
@@ -230,22 +258,127 @@ class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]):
|
|||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None:
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None:
|
||||||
self._write_line("WildcardExpr")
|
self._write_line("WildcardExpr")
|
||||||
|
|
||||||
def visit_template_expr(self, expr: m.TemplateExpr) -> None:
|
def visit_named_type(self, type: m.NamedType) -> None:
|
||||||
self._write_line("TemplateExpr")
|
self._write_line("NamedType")
|
||||||
with self._child_level(single=True):
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{type.name.lexeme}"', last=True)
|
||||||
|
|
||||||
|
def visit_generic_type(self, type: m.GenericType) -> None:
|
||||||
|
self._write_line("GenericType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level():
|
||||||
|
type.type.accept(self)
|
||||||
|
self._write_line("args", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, param in enumerate(type.args):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(type.args) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
param.accept(self)
|
||||||
|
|
||||||
|
def visit_constraint_type(self, type: m.ConstraintType) -> None:
|
||||||
|
self._write_line("ConstraintType")
|
||||||
|
with self._child_level():
|
||||||
self._write_line("type")
|
self._write_line("type")
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.type.accept(self)
|
type.type.accept(self)
|
||||||
|
self._write_line("constraint", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.constraint.accept(self)
|
||||||
|
|
||||||
def visit_type_expr(self, expr: m.TypeExpr):
|
def visit_complex_type(self, type: m.ComplexType) -> None:
|
||||||
self._write_line("TypeExpr")
|
self._write_line("ComplexType")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line(f'name: "{expr.name.lexeme}"')
|
self._write_line("members", last=True)
|
||||||
self._write_optional_child("template", expr.template)
|
with self._child_level():
|
||||||
self._write_line(f"optional: {expr.optional}", last=True)
|
for i, member in enumerate(type.members):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(type.members) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
member.accept(self)
|
||||||
|
|
||||||
|
def visit_extension_type(self, type: m.ExtensionType) -> None:
|
||||||
|
self._write_line("ExtensionType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("base")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.base.accept(self)
|
||||||
|
self._write_line("extension", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.extension.accept(self)
|
||||||
|
|
||||||
|
def visit_function_type(self, type: m.FunctionType) -> None:
|
||||||
|
self._write_line("FunctionType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("params")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._visit_param_spec(type.params)
|
||||||
|
|
||||||
|
self._write_line("returns", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.returns.accept(self)
|
||||||
|
|
||||||
|
def _visit_param_spec(self, spec: m.ParamSpec) -> None:
|
||||||
|
self._write_line("ParamSpec")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("pos")
|
||||||
|
with self._child_level():
|
||||||
|
for i, arg in enumerate(spec.pos):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(spec.pos) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._print_function_arg(arg)
|
||||||
|
|
||||||
|
self._write_line("mixed")
|
||||||
|
with self._child_level():
|
||||||
|
for i, arg in enumerate(spec.mixed):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(spec.mixed) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._print_function_arg(arg)
|
||||||
|
|
||||||
|
self._write_line("kw", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, arg in enumerate(spec.kw):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(spec.kw) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._print_function_arg(arg)
|
||||||
|
|
||||||
|
def _print_function_arg(self, arg: m.FunctionType.Argument) -> None:
|
||||||
|
self._write_line("Argument")
|
||||||
|
with self._child_level():
|
||||||
|
name: str = "None"
|
||||||
|
if arg.name is not None:
|
||||||
|
name = f'"{arg.name.lexeme}"'
|
||||||
|
self._write_line(f"name: {name}")
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
arg.type.accept(self)
|
||||||
|
self._write_line(f"required: {arg.required}", last=True)
|
||||||
|
|
||||||
|
def visit_frame_type(self, type: m.FrameType) -> None:
|
||||||
|
self._write_line("FrameType")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_line("columns")
|
||||||
|
with self._child_level():
|
||||||
|
for i, column in enumerate(type.columns):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(type.columns) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
self._print_frame_column(column)
|
||||||
|
|
||||||
|
def _print_frame_column(self, column: m.FrameType.Column) -> None:
|
||||||
|
self._write_line("Column")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{column.name.lexeme}"')
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
column.type.accept(self)
|
||||||
|
|
||||||
|
|
||||||
class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
|
class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]):
|
||||||
def __init__(self, indent: int = 4):
|
def __init__(self, indent: int = 4):
|
||||||
self.indent: int = indent
|
self.indent: int = indent
|
||||||
self.level: int = 0
|
self.level: int = 0
|
||||||
@@ -253,59 +386,54 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
|
|||||||
def indented(self, text: str) -> str:
|
def indented(self, text: str) -> str:
|
||||||
return " " * (self.level * self.indent) + text
|
return " " * (self.level * self.indent) + text
|
||||||
|
|
||||||
def print(self, expr: m.Expr | m.Stmt):
|
def print(self, expr: m.Expr | m.Stmt | m.Type) -> str:
|
||||||
self.level = 0
|
self.level = 0
|
||||||
return expr.accept(self)
|
return expr.accept(self)
|
||||||
|
|
||||||
def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt):
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> str:
|
||||||
template: str = stmt.template.accept(self) if stmt.template is not None else ""
|
template: str = ""
|
||||||
res: str = f"type {stmt.name.lexeme}{template}({stmt.base.accept(self)})"
|
if len(stmt.params) != 0:
|
||||||
if stmt.constraint is not None:
|
params: list[str] = [self._print_type_param(param) for param in stmt.params]
|
||||||
res += " where " + stmt.constraint.accept(self)
|
template = f"[{', '.join(params)}]"
|
||||||
|
res: str = f"type {stmt.name.lexeme}{template} = {stmt.type.accept(self)}"
|
||||||
return self.indented(res)
|
return self.indented(res)
|
||||||
|
|
||||||
def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt):
|
def visit_alias_stmt(self, stmt: m.AliasStmt) -> str:
|
||||||
template: str = stmt.template.accept(self) if stmt.template is not None else ""
|
return self.indented(f"alias {stmt.name.lexeme} = {stmt.type.accept(self)}")
|
||||||
res: str = self.indented(f"type {stmt.name.lexeme}{template}")
|
|
||||||
|
def _print_type_param(self, param: m.TypeParam) -> str:
|
||||||
|
res: str = param.name.lexeme
|
||||||
|
if param.bound is not None:
|
||||||
|
res += "<:" + param.bound.accept(self)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_member_stmt(self, stmt: m.MemberStmt):
|
||||||
|
keyword: str = {
|
||||||
|
m.MemberKind.PROPERTY: "prop",
|
||||||
|
m.MemberKind.METHOD: "def",
|
||||||
|
}.get(stmt.kind, "")
|
||||||
|
res: str = f"{keyword} {stmt.name.lexeme}: {stmt.type.accept(self)}"
|
||||||
|
return self.indented(res)
|
||||||
|
|
||||||
|
def visit_extend_stmt(self, stmt: m.ExtendStmt):
|
||||||
|
template: str = ""
|
||||||
|
if len(stmt.params) != 0:
|
||||||
|
params: list[str] = [self._print_type_param(param) for param in stmt.params]
|
||||||
|
template = f"[{', '.join(params)}]"
|
||||||
|
res: str = self.indented(f"extend {stmt.name.lexeme}{template}")
|
||||||
res += " {\n"
|
res += " {\n"
|
||||||
self.level += 1
|
self.level += 1
|
||||||
for prop in stmt.properties:
|
for member in stmt.members:
|
||||||
res += prop.accept(self)
|
res += member.accept(self) + "\n"
|
||||||
res += "\n"
|
|
||||||
self.level -= 1
|
self.level -= 1
|
||||||
res += self.indented("}")
|
res += self.indented("}")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def visit_property_stmt(self, stmt: m.PropertyStmt):
|
|
||||||
res: str = f"{stmt.name.lexeme}: {stmt.type.accept(self)}"
|
|
||||||
if stmt.constraint is not None:
|
|
||||||
res += " where " + stmt.constraint.accept(self)
|
|
||||||
return self.indented(res)
|
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt):
|
|
||||||
res: str = self.indented(f"extend {stmt.type.accept(self)}")
|
|
||||||
res += " {\n"
|
|
||||||
self.level += 1
|
|
||||||
for op in stmt.operations:
|
|
||||||
res += op.accept(self)
|
|
||||||
self.level -= 1
|
|
||||||
res += "\n" + self.indented("}")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def visit_op_stmt(self, stmt: m.OpStmt):
|
|
||||||
operand: str = stmt.operand.accept(self)
|
|
||||||
result: str = stmt.result.accept(self)
|
|
||||||
return self.indented(f"op {stmt.name.lexeme}({operand}) -> {result}")
|
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
||||||
name: str = stmt.name.lexeme
|
name: str = stmt.name.lexeme
|
||||||
subject: str = stmt.subject.lexeme
|
sig: str = "".join(self._visit_param_spec(spec) for spec in stmt.params)
|
||||||
type: str = stmt.type.accept(self)
|
body: str = stmt.body.accept(self)
|
||||||
condition: str = stmt.condition.accept(self)
|
return self.indented(f"predicate {name}{sig} = {body}")
|
||||||
return self.indented(f"predicate {name}({subject}: {type}) = {condition}")
|
|
||||||
|
|
||||||
def visit_simple_type_expr(self, expr: m.SimpleTypeExpr):
|
|
||||||
return f"{expr.name.lexeme}{'?' if expr.optional else ''}"
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr):
|
def visit_logical_expr(self, expr: m.LogicalExpr):
|
||||||
left: str = expr.left.accept(self)
|
left: str = expr.left.accept(self)
|
||||||
@@ -324,6 +452,12 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
|
|||||||
right: str = expr.right.accept(self)
|
right: str = expr.right.accept(self)
|
||||||
return f"{operator}{right}"
|
return f"{operator}{right}"
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> str:
|
||||||
|
args: list[str] = [arg.accept(self) for arg in expr.arguments] + [
|
||||||
|
f"{name}={arg.accept(self)}" for name, arg in expr.keywords.items()
|
||||||
|
]
|
||||||
|
return f"{expr.callee.accept(self)}({', '.join(args)})"
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr):
|
def visit_get_expr(self, expr: m.GetExpr):
|
||||||
expr_: str = expr.expr.accept(self)
|
expr_: str = expr.expr.accept(self)
|
||||||
name: str = expr.name.lexeme
|
name: str = expr.name.lexeme
|
||||||
@@ -342,12 +476,78 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
|
|||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr):
|
def visit_wildcard_expr(self, expr: m.WildcardExpr):
|
||||||
return "_"
|
return "_"
|
||||||
|
|
||||||
def visit_template_expr(self, expr: m.TemplateExpr):
|
def visit_named_type(self, type: m.NamedType) -> str:
|
||||||
return f"[{expr.type.accept(self)}]"
|
return type.name.lexeme
|
||||||
|
|
||||||
def visit_type_expr(self, expr: m.TypeExpr):
|
def visit_generic_type(self, type: m.GenericType) -> str:
|
||||||
template: str = expr.template.accept(self) if expr.template is not None else ""
|
res: str = type.type.accept(self)
|
||||||
return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}"
|
if len(type.args) != 0:
|
||||||
|
args: list[str] = [param.accept(self) for param in type.args]
|
||||||
|
res += f"[{', '.join(args)}]"
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_constraint_type(self, type: m.ConstraintType) -> str:
|
||||||
|
res: str = type.type.accept(self)
|
||||||
|
res += " where " + type.constraint.accept(self)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_complex_type(self, type: m.ComplexType) -> str:
|
||||||
|
res: str = "{\n"
|
||||||
|
self.level += 1
|
||||||
|
for member in type.members:
|
||||||
|
res += member.accept(self)
|
||||||
|
res += "\n"
|
||||||
|
self.level -= 1
|
||||||
|
res += self.indented("}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_extension_type(self, type: m.ExtensionType) -> str:
|
||||||
|
return f"{type.base.accept(self)} & {type.extension.accept(self)}"
|
||||||
|
|
||||||
|
def visit_function_type(self, type: m.FunctionType) -> str:
|
||||||
|
spec: str = self._visit_param_spec(type.params)
|
||||||
|
return f"fn {spec} -> {type.returns.accept(self)}"
|
||||||
|
|
||||||
|
def _visit_param_spec(self, spec: m.ParamSpec) -> str:
|
||||||
|
pos_args: list[str] = [self._print_arg(arg) for arg in spec.pos]
|
||||||
|
mixed_args: list[str] = [self._print_arg(arg) for arg in spec.mixed]
|
||||||
|
kw_args: list[str] = [self._print_arg(arg) for arg in spec.kw]
|
||||||
|
args: list[str] = pos_args
|
||||||
|
|
||||||
|
if len(pos_args) != 0:
|
||||||
|
args.append("/")
|
||||||
|
args += mixed_args
|
||||||
|
if len(kw_args) != 0:
|
||||||
|
args.append("*")
|
||||||
|
args += kw_args
|
||||||
|
return f"({', '.join(args)})"
|
||||||
|
|
||||||
|
def _print_arg(self, arg: m.FunctionType.Argument) -> str:
|
||||||
|
res: str = ""
|
||||||
|
if arg.name is not None:
|
||||||
|
res += arg.name.lexeme
|
||||||
|
res += ": "
|
||||||
|
res += arg.type.accept(self)
|
||||||
|
if not arg.required:
|
||||||
|
res += "?"
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_frame_type(self, type: m.FrameType) -> str:
|
||||||
|
res: str = self.indented("Frame[")
|
||||||
|
if len(type.columns) != 0:
|
||||||
|
res += "\n"
|
||||||
|
self.level += 1
|
||||||
|
columns: list[str] = []
|
||||||
|
for column in type.columns:
|
||||||
|
columns.append(self.indented(self._print_frame_column(column)))
|
||||||
|
res += ",\n".join(columns)
|
||||||
|
self.level -= 1
|
||||||
|
res += "\n"
|
||||||
|
res += "]"
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _print_frame_column(self, column: m.FrameType.Column) -> str:
|
||||||
|
return f"{column.name.lexeme}: {column.type.accept(self)}"
|
||||||
|
|
||||||
|
|
||||||
class PythonAstPrinter(
|
class PythonAstPrinter(
|
||||||
@@ -360,7 +560,13 @@ class PythonAstPrinter(
|
|||||||
self._write_line("BaseType")
|
self._write_line("BaseType")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line(f"base: {node.base}")
|
self._write_line(f"base: {node.base}")
|
||||||
self._write_optional_child("param", node.param, last=True)
|
self._write_line("args:", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, arg in enumerate(node.args):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(node.args) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
arg.accept(self)
|
||||||
|
|
||||||
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
||||||
self._write_line("ConstraintType")
|
self._write_line("ConstraintType")
|
||||||
@@ -482,6 +688,31 @@ class PythonAstPrinter(
|
|||||||
self._mark_last()
|
self._mark_last()
|
||||||
else_stmt.accept(self)
|
else_stmt.accept(self)
|
||||||
|
|
||||||
|
def visit_pass(self, stmt: p.Pass) -> None:
|
||||||
|
self._write_line("Pass")
|
||||||
|
|
||||||
|
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||||
|
self._write_line("ForStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("target")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.target.accept(self)
|
||||||
|
self._write_line("iterator")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.iterator.accept(self)
|
||||||
|
self._write_line("body", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, body_stmt in enumerate(stmt.body):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(stmt.body) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
body_stmt.accept(self)
|
||||||
|
|
||||||
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||||
|
self._write_line("RawStmt")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_line(f"stmt: {ast.unparse(stmt.stmt)}")
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
|
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
|
||||||
self._write_line("BinaryExpr")
|
self._write_line("BinaryExpr")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
@@ -553,7 +784,7 @@ class PythonAstPrinter(
|
|||||||
def visit_literal_expr(self, expr: p.LiteralExpr) -> None:
|
def visit_literal_expr(self, expr: p.LiteralExpr) -> None:
|
||||||
self._write_line("LiteralExpr")
|
self._write_line("LiteralExpr")
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
self._write_line(f"value: {expr.value}")
|
self._write_line(f"value: {expr.value!r}")
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
||||||
self._write_line("VariableExpr")
|
self._write_line("VariableExpr")
|
||||||
@@ -573,26 +804,16 @@ class PythonAstPrinter(
|
|||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.right.accept(self)
|
expr.right.accept(self)
|
||||||
|
|
||||||
def visit_set_expr(self, expr: p.SetExpr) -> None:
|
|
||||||
self._write_line("SetExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("object")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.object.accept(self)
|
|
||||||
self._write_line(f"name: {expr.name}")
|
|
||||||
self._write_line("value", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.value.accept(self)
|
|
||||||
|
|
||||||
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
||||||
self._write_line("CastExpr")
|
self._write_line("CastExpr")
|
||||||
with self._child_level():
|
with self._child_level():
|
||||||
self._write_line("type")
|
self._write_line("type")
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.type.accept(self)
|
expr.type.accept(self)
|
||||||
self._write_line("expr", last=True)
|
self._write_line("expr")
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.expr.accept(self)
|
expr.expr.accept(self)
|
||||||
|
self._write_line(f"unsafe: {expr.unsafe}", last=True)
|
||||||
|
|
||||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None:
|
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None:
|
||||||
self._write_line("TernaryExpr")
|
self._write_line("TernaryExpr")
|
||||||
@@ -608,3 +829,68 @@ class PythonAstPrinter(
|
|||||||
self._write_line("if_false", last=True)
|
self._write_line("if_false", last=True)
|
||||||
with self._child_level(single=True):
|
with self._child_level(single=True):
|
||||||
expr.if_false.accept(self)
|
expr.if_false.accept(self)
|
||||||
|
|
||||||
|
def visit_list_expr(self, expr: p.ListExpr) -> None:
|
||||||
|
self._write_line("ListExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("items", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, item in enumerate(expr.items):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.items) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
item.accept(self)
|
||||||
|
|
||||||
|
def visit_dict_expr(self, expr: p.DictExpr) -> None:
|
||||||
|
self._write_line("DictExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("keys")
|
||||||
|
with self._child_level():
|
||||||
|
for i, key in enumerate(expr.keys):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.keys) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
if key is None:
|
||||||
|
self._write_line("None")
|
||||||
|
else:
|
||||||
|
key.accept(self)
|
||||||
|
self._write_line("values", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, value in enumerate(expr.values):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.values) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
value.accept(self)
|
||||||
|
|
||||||
|
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> None:
|
||||||
|
self._write_line("SubscriptExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("object")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.object.accept(self)
|
||||||
|
self._write_line("index", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.index.accept(self)
|
||||||
|
|
||||||
|
def visit_slice_expr(self, expr: p.SliceExpr) -> None:
|
||||||
|
self._write_line("SliceExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_optional_child("lower", expr.lower)
|
||||||
|
self._write_optional_child("upper", expr.upper)
|
||||||
|
self._write_optional_child("step", expr.step, last=True)
|
||||||
|
|
||||||
|
def visit_tuple_expr(self, expr: p.TupleExpr) -> None:
|
||||||
|
self._write_line("TupleExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("items", last=True)
|
||||||
|
with self._child_level():
|
||||||
|
for i, item in enumerate(expr.items):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(expr.items) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
item.accept(self)
|
||||||
|
|
||||||
|
def visit_raw_expr(self, expr: p.RawExpr) -> None:
|
||||||
|
self._write_line("RawExpr")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_line(f"expr: {ast.unparse(expr.expr)}")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from midas.ast.location import Location
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Type annotations #
|
# Type annotations #
|
||||||
####################
|
####################
|
||||||
@@ -43,7 +44,7 @@ class MidasType(ABC):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BaseType(MidasType):
|
class BaseType(MidasType):
|
||||||
base: str
|
base: str
|
||||||
param: Optional[MidasType]
|
args: tuple[MidasType, ...]
|
||||||
|
|
||||||
def accept(self, visitor: MidasType.Visitor[T]) -> T:
|
def accept(self, visitor: MidasType.Visitor[T]) -> T:
|
||||||
return visitor.visit_base_type(self)
|
return visitor.visit_base_type(self)
|
||||||
@@ -106,6 +107,15 @@ class Stmt(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_if_stmt(self, stmt: IfStmt) -> T: ...
|
def visit_if_stmt(self, stmt: IfStmt) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_pass(self, stmt: Pass) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_for_stmt(self, stmt: ForStmt) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_raw_stmt(self, stmt: RawStmt) -> T: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ExpressionStmt(Stmt):
|
class ExpressionStmt(Stmt):
|
||||||
@@ -177,6 +187,32 @@ class IfStmt(Stmt):
|
|||||||
return visitor.visit_if_stmt(self)
|
return visitor.visit_if_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pass(Stmt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_pass(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ForStmt(Stmt):
|
||||||
|
target: Expr
|
||||||
|
iterator: Expr
|
||||||
|
body: list[Stmt]
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_for_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RawStmt(Stmt):
|
||||||
|
stmt: ast.stmt
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_raw_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# Expressions #
|
# Expressions #
|
||||||
###############
|
###############
|
||||||
@@ -214,15 +250,30 @@ class Expr(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_logical_expr(self, expr: LogicalExpr) -> T: ...
|
def visit_logical_expr(self, expr: LogicalExpr) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def visit_set_expr(self, expr: SetExpr) -> T: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_cast_expr(self, expr: CastExpr) -> T: ...
|
def visit_cast_expr(self, expr: CastExpr) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_ternary_expr(self, expr: TernaryExpr) -> T: ...
|
def visit_ternary_expr(self, expr: TernaryExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_list_expr(self, expr: ListExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_dict_expr(self, expr: DictExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_subscript_expr(self, expr: SubscriptExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_slice_expr(self, expr: SliceExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_tuple_expr(self, expr: TupleExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_raw_expr(self, expr: RawExpr) -> T: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BinaryExpr(Expr):
|
class BinaryExpr(Expr):
|
||||||
@@ -298,20 +349,11 @@ class LogicalExpr(Expr):
|
|||||||
return visitor.visit_logical_expr(self)
|
return visitor.visit_logical_expr(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SetExpr(Expr):
|
|
||||||
object: Expr
|
|
||||||
name: str
|
|
||||||
value: Expr
|
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
|
||||||
return visitor.visit_set_expr(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CastExpr(Expr):
|
class CastExpr(Expr):
|
||||||
type: MidasType
|
type: MidasType
|
||||||
expr: Expr
|
expr: Expr
|
||||||
|
unsafe: bool
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
return visitor.visit_cast_expr(self)
|
return visitor.visit_cast_expr(self)
|
||||||
@@ -325,3 +367,55 @@ class TernaryExpr(Expr):
|
|||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
return visitor.visit_ternary_expr(self)
|
return visitor.visit_ternary_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ListExpr(Expr):
|
||||||
|
items: list[Expr]
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_list_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DictExpr(Expr):
|
||||||
|
keys: list[Optional[Expr]]
|
||||||
|
values: list[Expr]
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_dict_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SubscriptExpr(Expr):
|
||||||
|
object: Expr
|
||||||
|
index: Expr
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_subscript_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SliceExpr(Expr):
|
||||||
|
lower: Optional[Expr]
|
||||||
|
upper: Optional[Expr]
|
||||||
|
step: Optional[Expr]
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_slice_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TupleExpr(Expr):
|
||||||
|
items: tuple[Expr, ...]
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_tuple_expr(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RawExpr(Expr):
|
||||||
|
expr: ast.expr
|
||||||
|
|
||||||
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_raw_expr(self)
|
||||||
|
|||||||
277
midas/checker/builtins.midas
Normal file
277
midas/checker/builtins.midas
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
extend float {
|
||||||
|
def hex: fn() -> str
|
||||||
|
def is_integer: fn() -> bool
|
||||||
|
prop real: float
|
||||||
|
prop imag: float
|
||||||
|
def conjugate: fn() -> float
|
||||||
|
def __add__: fn(value: float, /) -> float
|
||||||
|
def __sub__: fn(value: float, /) -> float
|
||||||
|
def __mul__: fn(value: float, /) -> float
|
||||||
|
def __floordiv__: fn(value: float, /) -> float
|
||||||
|
def __truediv__: fn(value: float, /) -> float
|
||||||
|
def __mod__: fn(value: float, /) -> float
|
||||||
|
// def __divmod__: fn(value: float, /) -> tuple[float, float]
|
||||||
|
|
||||||
|
def __pow__: fn(value: int, /) -> float
|
||||||
|
// positive __value -> float; negative __value -> complex
|
||||||
|
// return type must be Any as `float | complex` causes too many false-positive errors
|
||||||
|
def __pow__: fn(value: float, /) -> Any
|
||||||
|
def __radd__: fn(value: float, /) -> float
|
||||||
|
def __rsub__: fn(value: float, /) -> float
|
||||||
|
def __rmul__: fn(value: float, /) -> float
|
||||||
|
def __rfloordiv__: fn(value: float, /) -> float
|
||||||
|
def __rtruediv__: fn(value: float, /) -> float
|
||||||
|
def __rmod__: fn(value: float, /) -> float
|
||||||
|
// def __rdivmod__: fn(value: float, /) -> tuple[float, float]
|
||||||
|
// def __rpow__: fn(value: _PositiveInteger, mod: None = None, /) -> float
|
||||||
|
// def __rpow__: fn(value: _NegativeInteger, mod: None = None, /) -> complex
|
||||||
|
// Returning `complex` for the general case gives too many false-positive errors.
|
||||||
|
// def __rpow__: fn(value: float, mod: None = None, /) -> Any
|
||||||
|
// def __getnewargs__: fn() -> tuple[float]
|
||||||
|
def __trunc__: fn() -> int
|
||||||
|
def __ceil__: fn() -> int
|
||||||
|
def __floor__: fn() -> int
|
||||||
|
def __round__: fn(ndigits: None?, /) -> int
|
||||||
|
def __round__: fn(ndigits: int, /) -> float
|
||||||
|
def __eq__: fn(value: object, /) -> bool
|
||||||
|
def __ne__: fn(value: object, /) -> bool
|
||||||
|
def __lt__: fn(value: float, /) -> bool
|
||||||
|
def __le__: fn(value: float, /) -> bool
|
||||||
|
def __gt__: fn(value: float, /) -> bool
|
||||||
|
def __ge__: fn(value: float, /) -> bool
|
||||||
|
def __neg__: fn() -> float
|
||||||
|
def __pos__: fn() -> float
|
||||||
|
def __int__: fn() -> int
|
||||||
|
def __float__: fn() -> float
|
||||||
|
def __abs__: fn() -> float
|
||||||
|
def __hash__: fn() -> int
|
||||||
|
def __bool__: fn() -> bool
|
||||||
|
def __format__: fn(format_spec: str, /) -> str
|
||||||
|
}
|
||||||
|
|
||||||
|
extend int {
|
||||||
|
prop real: int
|
||||||
|
prop imag: int
|
||||||
|
prop numerator: int
|
||||||
|
prop denominator: int
|
||||||
|
def conjugate: fn() -> int
|
||||||
|
def bit_length: fn() -> int
|
||||||
|
def bit_count: fn() -> int
|
||||||
|
// def to_bytes: fn(length: int?, byteorder: str?, *, signed: bool?) -> bytes
|
||||||
|
|
||||||
|
def __add__: fn(value: int, /) -> int
|
||||||
|
def __sub__: fn(value: int, /) -> int
|
||||||
|
def __mul__: fn(value: int, /) -> int
|
||||||
|
def __floordiv__: fn(value: int, /) -> int
|
||||||
|
def __truediv__: fn(value: int, /) -> float
|
||||||
|
def __mod__: fn(value: int, /) -> int
|
||||||
|
// def __divmod__: fn(value: int, /) -> tuple[int, int]
|
||||||
|
def __radd__: fn(value: int, /) -> int
|
||||||
|
def __rsub__: fn(value: int, /) -> int
|
||||||
|
def __rmul__: fn(value: int, /) -> int
|
||||||
|
def __rfloordiv__: fn(value: int, /) -> int
|
||||||
|
def __rtruediv__: fn(value: int, /) -> float
|
||||||
|
def __rmod__: fn(value: int, /) -> int
|
||||||
|
// def __rdivmod__: fn(value: int, /) -> tuple[int, int]
|
||||||
|
def __pow__: fn(value: int, /) -> int
|
||||||
|
// def __pow__: fn(value: _PositiveInteger, mod: None = None, /) -> int
|
||||||
|
// def __pow__: fn(value: _NegativeInteger, mod: None = None, /) -> float
|
||||||
|
// positive __value -> int; negative __value -> float
|
||||||
|
// return type must be Any as `int | float` causes too many false-positive errors
|
||||||
|
// def __pow__: fn(value: int, mod: None = None, /) -> Any
|
||||||
|
// def __pow__: fn(value: int, mod: int, /) -> int
|
||||||
|
def __rpow__: fn(value: int, /) -> Any
|
||||||
|
def __and__: fn(value: int, /) -> int
|
||||||
|
def __or__: fn(value: int, /) -> int
|
||||||
|
def __xor__: fn(value: int, /) -> int
|
||||||
|
def __lshift__: fn(value: int, /) -> int
|
||||||
|
def __rshift__: fn(value: int, /) -> int
|
||||||
|
def __rand__: fn(value: int, /) -> int
|
||||||
|
def __ror__: fn(value: int, /) -> int
|
||||||
|
def __rxor__: fn(value: int, /) -> int
|
||||||
|
def __rlshift__: fn(value: int, /) -> int
|
||||||
|
def __rrshift__: fn(value: int, /) -> int
|
||||||
|
def __neg__: fn() -> int
|
||||||
|
def __pos__: fn() -> int
|
||||||
|
def __invert__: fn() -> int
|
||||||
|
def __trunc__: fn() -> int
|
||||||
|
def __ceil__: fn() -> int
|
||||||
|
def __floor__: fn() -> int
|
||||||
|
def __round__: fn(ndigits: None?, /) -> int
|
||||||
|
def __round__: fn(ndigits: int, /) -> int
|
||||||
|
|
||||||
|
// def __getnewargs__: fn() -> tuple[int]
|
||||||
|
def __eq__: fn(value: object, /) -> bool
|
||||||
|
def __ne__: fn(value: object, /) -> bool
|
||||||
|
def __lt__: fn(value: int, /) -> bool
|
||||||
|
def __le__: fn(value: int, /) -> bool
|
||||||
|
def __gt__: fn(value: int, /) -> bool
|
||||||
|
def __ge__: fn(value: int, /) -> bool
|
||||||
|
def __float__: fn() -> float
|
||||||
|
def __int__: fn() -> int
|
||||||
|
def __abs__: fn() -> int
|
||||||
|
def __hash__: fn() -> int
|
||||||
|
def __bool__: fn() -> bool
|
||||||
|
def __index__: fn() -> int
|
||||||
|
def __format__: fn(format_spec: str, /) -> str
|
||||||
|
}
|
||||||
|
|
||||||
|
extend list[T] {
|
||||||
|
def copy: fn () -> list[T]
|
||||||
|
def append: fn (object: T, /) -> None
|
||||||
|
def extend: fn (iterable: list[T], /) -> None
|
||||||
|
def pop: fn (index: int?, /) -> T
|
||||||
|
def index: fn (value: T, start: int?, stop: int?, /) -> int
|
||||||
|
def count: fn (value: T, /) -> int
|
||||||
|
def insert: fn (index: int, object: T, /) -> None
|
||||||
|
def remove: fn (value: T, /) -> None
|
||||||
|
def sort: fn (*, reverse: bool?) -> None
|
||||||
|
def __len__: fn () -> int
|
||||||
|
// def __iter__: fn () -> Iterator[T]
|
||||||
|
def __getitem__: fn (i: int, /) -> T
|
||||||
|
def __getitem__: fn (s: slice, /) -> list[T]
|
||||||
|
def __setitem__: fn (key: int, value: T, /) -> None
|
||||||
|
def __setitem__: fn (key: slice, value: list[T], /) -> None
|
||||||
|
def __delitem__: fn (key: int, /) -> None
|
||||||
|
def __delitem__: fn (key: slice, /) -> None
|
||||||
|
// def __add__: fn[S <: T] (value: list[S], /) -> list[T]
|
||||||
|
def __add__: fn (value: list[T], /) -> list[T]
|
||||||
|
def __iadd__: fn (value: list[T], /) -> list[T]
|
||||||
|
def __mul__: fn (value: int, /) -> list[T]
|
||||||
|
def __rmul__: fn (value: int, /) -> list[T]
|
||||||
|
def __imul__: fn (value: int, /) -> list[T]
|
||||||
|
def __contains__: fn (key: object, /) -> bool
|
||||||
|
// def __reversed__: fn (self) -> Iterator[_T]
|
||||||
|
def __gt__: fn (value: list[T], /) -> bool
|
||||||
|
def __ge__: fn (value: list[T], /) -> bool
|
||||||
|
def __lt__: fn (value: list[T], /) -> bool
|
||||||
|
def __le__: fn (value: list[T], /) -> bool
|
||||||
|
def __eq__: fn (value: object, /) -> bool
|
||||||
|
|
||||||
|
prop __doc__: str
|
||||||
|
}
|
||||||
|
|
||||||
|
extend dict[K, V] {
|
||||||
|
def copy: fn() -> dict[K, V]
|
||||||
|
def keys: fn() -> list[K] // TODO: use builtin types
|
||||||
|
def values: fn() -> list[V] // TODO: use builtin types
|
||||||
|
// def items: fn() -> list[tuple[K, V]] // TODO: use builtin types
|
||||||
|
|
||||||
|
// def get: fn(key: K, default: None = None, /) -> V | None
|
||||||
|
def get: fn(key: K, default: V, /) -> V
|
||||||
|
// def get: fn[T](key: K, default: T, /) -> V | T
|
||||||
|
def pop: fn(key: K, /) -> V
|
||||||
|
def pop: fn(key: K, default: V, /) -> V
|
||||||
|
// def pop: fn[T](key: K, default: T, /) -> V | T
|
||||||
|
def __len__: fn() -> int
|
||||||
|
def __getitem__: fn(key: K, /) -> V
|
||||||
|
def __setitem__: fn(key: K, value: V, /) -> None
|
||||||
|
def __delitem__: fn(key: K, /) -> None
|
||||||
|
// def __iter__: fn() -> Iterator[K]
|
||||||
|
def __eq__: fn(value: object, /) -> bool
|
||||||
|
// def __reversed__: fn() -> Iterator[K]
|
||||||
|
|
||||||
|
def __or__: fn(value: dict[K, V], /) -> dict[K, V]
|
||||||
|
// def __or__: fn[K2, V2](value: dict[K2, V2], /) -> dict[K | K2, V | V2]
|
||||||
|
def __ror__: fn(value: dict[K, V], /) -> dict[K, V]
|
||||||
|
// def __ror__: fn[K2, V2](value: dict[K2, V2], /) -> dict[K | K2, V | V2]
|
||||||
|
// def __ior__: fn(value: SupportsKeysAndGetItem[K, V], /) -> dict[K, V]
|
||||||
|
// def __ior__: fn(value: Iterable[tuple[K, V]], /) -> dict[K, V]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extend str {
|
||||||
|
def capitalize: fn() -> str
|
||||||
|
def casefold: fn() -> str
|
||||||
|
def center: fn(width: int, fillchar: str?, /) -> str
|
||||||
|
def count: fn(sub: str, start: None?, end: None?, /) -> int
|
||||||
|
def count: fn(sub: str, start: int, end: None?, /) -> int
|
||||||
|
def count: fn(sub: str, start: None, end: int, /) -> int
|
||||||
|
def count: fn(sub: str, start: int, end: int, /) -> int
|
||||||
|
def encode: fn(encoding: str?, errors: str?) -> bytes
|
||||||
|
def endswith: fn(suffix: str, start: None?, end: None?, /) -> bool
|
||||||
|
def endswith: fn(suffix: str, start: int, end: None?, /) -> bool
|
||||||
|
def endswith: fn(suffix: str, start: None, end: int, /) -> bool
|
||||||
|
def endswith: fn(suffix: str, start: int, end: int, /) -> bool
|
||||||
|
def expandtabs: fn(tabsize: int?) -> str
|
||||||
|
def find: fn(sub: str, start: None?, end: None?, /) -> int
|
||||||
|
def find: fn(sub: str, start: int, end: None?, /) -> int
|
||||||
|
def find: fn(sub: str, start: None, end: int, /) -> int
|
||||||
|
def find: fn(sub: str, start: int, end: int, /) -> int
|
||||||
|
// def format: fn(*args: object, **kwargs: object) -> str
|
||||||
|
// def format_map: fn(mapping: _FormatMapMapping, /) -> str
|
||||||
|
def index: fn(sub: str, start: None?, end: None?, /) -> int
|
||||||
|
def index: fn(sub: str, start: int, end: None?, /) -> int
|
||||||
|
def index: fn(sub: str, start: None, end: int, /) -> int
|
||||||
|
def index: fn(sub: str, start: int, end: int, /) -> int
|
||||||
|
def isalnum: fn() -> bool
|
||||||
|
def isalpha: fn() -> bool
|
||||||
|
def isascii: fn() -> bool
|
||||||
|
def isdecimal: fn() -> bool
|
||||||
|
def isdigit: fn() -> bool
|
||||||
|
def isidentifier: fn() -> bool
|
||||||
|
def islower: fn() -> bool
|
||||||
|
def isnumeric: fn() -> bool
|
||||||
|
def isprintable: fn() -> bool
|
||||||
|
def isspace: fn() -> bool
|
||||||
|
def istitle: fn() -> bool
|
||||||
|
def isupper: fn() -> bool
|
||||||
|
def join: fn(iterable: list[str], /) -> str // TODO: use Iterable
|
||||||
|
def ljust: fn(width: int, fillchar: str?, /) -> str
|
||||||
|
def lower: fn() -> str
|
||||||
|
def lstrip: fn(chars: None?, /) -> str
|
||||||
|
def lstrip: fn(chars: str, /) -> str
|
||||||
|
def partition: fn(sep: str, /) -> tuple[str, str, str]
|
||||||
|
|
||||||
|
def replace: fn(old: str, new: str, count: int?, /) -> str
|
||||||
|
|
||||||
|
def removeprefix: fn(prefix: str, /) -> str
|
||||||
|
def removesuffix: fn(suffix: str, /) -> str
|
||||||
|
def rfind: fn(sub: str, start: None?, end: None?, /) -> int
|
||||||
|
def rfind: fn(sub: str, start: int, end: None?, /) -> int
|
||||||
|
def rfind: fn(sub: str, start: None, end: int, /) -> int
|
||||||
|
def rfind: fn(sub: str, start: int, end: int, /) -> int
|
||||||
|
def rindex: fn(sub: str, start: None?, end: None?, /) -> int
|
||||||
|
def rindex: fn(sub: str, start: int, end: None?, /) -> int
|
||||||
|
def rindex: fn(sub: str, start: None, end: int, /) -> int
|
||||||
|
def rindex: fn(sub: str, start: int, end: int, /) -> int
|
||||||
|
def rjust: fn(width: int, fillchar: str?, /) -> str
|
||||||
|
def rpartition: fn(sep: str, /) -> tuple[str, str, str]
|
||||||
|
def rsplit: fn(sep: None?, maxsplit: int?) -> list[str]
|
||||||
|
def rsplit: fn(sep: str, maxsplit: int?) -> list[str]
|
||||||
|
def rstrip: fn(chars: None?, /) -> str
|
||||||
|
def rstrip: fn(chars: str, /) -> str
|
||||||
|
def split: fn(sep: None?, maxsplit: int?) -> list[str]
|
||||||
|
def split: fn(sep: str, maxsplit: int?) -> list[str]
|
||||||
|
def splitlines: fn(keepends: bool?) -> list[str]
|
||||||
|
def startswith: fn(prefix: str, start: None?, end: None?, /) -> bool
|
||||||
|
def startswith: fn(prefix: str, start: int, end: None?, /) -> bool
|
||||||
|
def startswith: fn(prefix: str, start: None, end: int, /) -> bool
|
||||||
|
def startswith: fn(prefix: str, start: int, end: int, /) -> bool
|
||||||
|
def strip: fn(chars: None?, /) -> str
|
||||||
|
def strip: fn(chars: str, /) -> str
|
||||||
|
def swapcase: fn() -> str
|
||||||
|
def title: fn() -> str
|
||||||
|
// def translate: fn(table: _TranslateTable, /) -> str
|
||||||
|
def upper: fn() -> str
|
||||||
|
def zfill: fn(width: int, /) -> str
|
||||||
|
def __add__: fn(value: str, /) -> str
|
||||||
|
// Incompatible with Sequence.__contains__
|
||||||
|
def __contains__: fn(key: str, /) -> bool
|
||||||
|
def __eq__: fn(value: object, /) -> bool
|
||||||
|
def __ge__: fn(value: str, /) -> bool
|
||||||
|
def __getitem__: fn(key: slice, /) -> str
|
||||||
|
def __getitem__: fn(key: int, /) -> str
|
||||||
|
def __gt__: fn(value: str, /) -> bool
|
||||||
|
def __hash__: fn() -> int
|
||||||
|
// def __iter__: fn() -> Iterator[str]
|
||||||
|
def __le__: fn(value: str, /) -> bool
|
||||||
|
def __len__: fn() -> int
|
||||||
|
def __lt__: fn(value: str, /) -> bool
|
||||||
|
def __mod__: fn(value: Any, /) -> str
|
||||||
|
def __mul__: fn(value: int, /) -> str
|
||||||
|
def __ne__: fn(value: object, /) -> bool
|
||||||
|
def __rmul__: fn(value: int, /) -> str
|
||||||
|
def __getnewargs__: fn() -> tuple[str]
|
||||||
|
def __format__: fn(format_spec: str, /) -> str
|
||||||
|
}
|
||||||
57
midas/checker/builtins.py
Normal file
57
midas/checker/builtins.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from midas.checker.types import (
|
||||||
|
BaseType,
|
||||||
|
GenericType,
|
||||||
|
TopType,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
|
||||||
|
|
||||||
|
# Hard-coded subtype relationships between builtin types
|
||||||
|
# Circular dependencies and diamond inheritance MUST be avoided
|
||||||
|
BUILTIN_SUBTYPES: dict[str, set[str]] = {
|
||||||
|
"object": {"float", "list", "dict", "str", "bytes", "tuple"},
|
||||||
|
"float": {"int"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def define_builtins(reg: TypesRegistry):
|
||||||
|
"""Define builtin types and operations"""
|
||||||
|
any = reg.define_type("Any", TopType())
|
||||||
|
unit = reg.define_type("None", UnitType())
|
||||||
|
object = reg.define_type("object", BaseType(name="object"))
|
||||||
|
bytes = reg.define_type("bytes", BaseType(name="bytes"))
|
||||||
|
bool = reg.define_type("bool", BaseType(name="bool"))
|
||||||
|
int = reg.define_type("int", BaseType(name="int"))
|
||||||
|
float = reg.define_type("float", BaseType(name="float"))
|
||||||
|
str = reg.define_type("str", BaseType(name="str"))
|
||||||
|
slice = reg.define_type("slice", BaseType(name="slice"))
|
||||||
|
|
||||||
|
tuple = reg.define_type("tuple", BaseType(name="tuple"))
|
||||||
|
|
||||||
|
list = reg.define_type(
|
||||||
|
"list",
|
||||||
|
GenericType(
|
||||||
|
name="list",
|
||||||
|
params=[TypeVar(name="T", bound=None)],
|
||||||
|
body=BaseType(name="list"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
dict = reg.define_type(
|
||||||
|
"dict",
|
||||||
|
GenericType(
|
||||||
|
name="dict",
|
||||||
|
params=[
|
||||||
|
TypeVar(name="K", bound=None),
|
||||||
|
TypeVar(name="V", bound=None),
|
||||||
|
],
|
||||||
|
body=BaseType(name="dict"),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -1,546 +1,36 @@
|
|||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import midas.ast.midas as m
|
from midas.checker.diagnostic import Diagnostic
|
||||||
import midas.ast.python as p
|
from midas.checker.midas import MidasTyper
|
||||||
from midas.ast.location import Location
|
from midas.checker.python import PythonTyper
|
||||||
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
from midas.checker.registry import TypesRegistry
|
||||||
from midas.checker.environment import Environment
|
from midas.checker.reporter import Reporter
|
||||||
from midas.checker.operators import COMPARATOR_METHODS, OPERATOR_METHODS
|
from midas.utils import TypedAST
|
||||||
from midas.checker.types import BaseType, Function, SimpleType, Type, UnitType, UnknownType
|
|
||||||
from midas.lexer.midas import MidasLexer
|
|
||||||
from midas.lexer.token import Token
|
|
||||||
from midas.parser.midas import MidasParser
|
|
||||||
from midas.resolver.midas import MidasResolver
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnException(Exception):
|
class TypeChecker:
|
||||||
pass
|
def __init__(self):
|
||||||
|
self.types: TypesRegistry = TypesRegistry()
|
||||||
|
self.reporter: Reporter = Reporter()
|
||||||
|
|
||||||
|
self.midas_typer = MidasTyper(self.types, self.reporter)
|
||||||
|
self.python_typer = PythonTyper(self.types, self.reporter)
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
def import_midas(self, path: Path):
|
||||||
class MappedArgument:
|
source: str = path.read_text()
|
||||||
expr: p.Expr
|
return self.import_midas_source(source, path=str(path))
|
||||||
type: Type
|
|
||||||
argument: Function.Argument
|
|
||||||
|
|
||||||
|
def import_midas_source(self, source: str, path: Optional[str] = None):
|
||||||
|
self.midas_typer.process(source, path)
|
||||||
|
|
||||||
class Checker(
|
def type_check(self, path: Path) -> TypedAST:
|
||||||
p.Stmt.Visitor[None],
|
source: str = path.read_text()
|
||||||
p.Expr.Visitor[Type],
|
return self.type_check_source(source, path=str(path))
|
||||||
p.MidasType.Visitor[Type],
|
|
||||||
):
|
|
||||||
"""A type checker which can use custom type definitions"""
|
|
||||||
|
|
||||||
def __init__(self, locals: dict[p.Expr, int], file_path: Path):
|
def type_check_source(self, source: str, path: Optional[str] = None) -> TypedAST:
|
||||||
self.logger: logging.Logger = logging.getLogger("Checker")
|
return self.python_typer.process(source, path)
|
||||||
self.file_path: Path = file_path
|
|
||||||
self.ctx: MidasResolver = MidasResolver()
|
|
||||||
self.global_env: Environment = Environment()
|
|
||||||
self.env: Environment = self.global_env
|
|
||||||
self.locals: dict[p.Expr, int] = locals
|
|
||||||
self.diagnostics: list[Diagnostic] = []
|
|
||||||
|
|
||||||
def diagnostic(self, type: DiagnosticType, location: Location, message: str):
|
@property
|
||||||
self.diagnostics.append(
|
def diagnostics(self) -> list[Diagnostic]:
|
||||||
Diagnostic(
|
return self.reporter.diagnostics
|
||||||
file_path=self.file_path,
|
|
||||||
location=location,
|
|
||||||
type=type,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def error(self, location: Location, message: str):
|
|
||||||
self.diagnostic(
|
|
||||||
type=DiagnosticType.ERROR,
|
|
||||||
location=location,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
def warning(self, location: Location, message: str):
|
|
||||||
self.diagnostic(
|
|
||||||
type=DiagnosticType.WARNING,
|
|
||||||
location=location,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
def info(self, location: Location, message: str):
|
|
||||||
self.diagnostic(
|
|
||||||
type=DiagnosticType.INFO,
|
|
||||||
location=location,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
def evaluate(self, expr: p.Expr) -> Type:
|
|
||||||
"""Evaluate the type of an expression
|
|
||||||
|
|
||||||
Args:
|
|
||||||
expr (p.Expr): the expression to evaluate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Type: the type of the given expression
|
|
||||||
"""
|
|
||||||
return expr.accept(self)
|
|
||||||
|
|
||||||
def evaluate_block(self, block: list[p.Stmt], env: Environment) -> bool:
|
|
||||||
"""Evaluate a sequence of statements
|
|
||||||
|
|
||||||
Args:
|
|
||||||
block (list[p.Stmt]): the statements to evaluate
|
|
||||||
env (Environment): the environment in which to evaluate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: whether a return statement is present in the block
|
|
||||||
"""
|
|
||||||
previous_env: Environment = self.env
|
|
||||||
self.env = env
|
|
||||||
returned: bool = False
|
|
||||||
for i, stmt in enumerate(block):
|
|
||||||
try:
|
|
||||||
stmt.accept(self)
|
|
||||||
except ReturnException:
|
|
||||||
returned = True
|
|
||||||
if i < len(block) - 1:
|
|
||||||
self.warning(block[i + 1].location, "Unreachable statement")
|
|
||||||
break
|
|
||||||
self.env = previous_env
|
|
||||||
return returned
|
|
||||||
|
|
||||||
def check(self, statements: list[p.Stmt]) -> list[Diagnostic]:
|
|
||||||
"""Type check a sequence of statements and returns diagnostics
|
|
||||||
|
|
||||||
Args:
|
|
||||||
statements (list[p.Stmt]): the statements to evaluate and check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Diagnostic]: the list of diagnostics (errors, warning, etc.)
|
|
||||||
"""
|
|
||||||
self.diagnostics = []
|
|
||||||
for stmt in statements:
|
|
||||||
stmt.accept(self)
|
|
||||||
|
|
||||||
self.logger.debug(f"Final environment: {self.env.flat_dict()}")
|
|
||||||
return self.diagnostics
|
|
||||||
|
|
||||||
def look_up_variable(self, name: str, expr: p.Expr) -> Optional[Type]:
|
|
||||||
"""Look up a variable in the environment it was declared
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): the name of the variable
|
|
||||||
expr (p.Expr): the variable expression, used to lookup the scope distance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Type]: the type of the variable, or None if it was not found
|
|
||||||
"""
|
|
||||||
distance: Optional[int] = self.locals.get(expr)
|
|
||||||
if distance is not None:
|
|
||||||
return self.env.get_at(distance, name)
|
|
||||||
return self.global_env.get(name)
|
|
||||||
|
|
||||||
def parse_midas_import(self, expr: p.CallExpr) -> Optional[Path]:
|
|
||||||
"""Parse a Midas import statement
|
|
||||||
|
|
||||||
The statement should be written as `midas.using("path/to/types.midas")`
|
|
||||||
|
|
||||||
Args:
|
|
||||||
expr (p.CallExpr): the import call expression
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Path]: the path to the imported file, or None if the expression is malformed
|
|
||||||
"""
|
|
||||||
match expr:
|
|
||||||
case p.CallExpr(
|
|
||||||
callee=p.GetExpr(
|
|
||||||
object=p.VariableExpr(name="midas"),
|
|
||||||
name="using",
|
|
||||||
),
|
|
||||||
arguments=[
|
|
||||||
p.LiteralExpr(value=path),
|
|
||||||
],
|
|
||||||
):
|
|
||||||
return Path(path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def import_midas(self, path: Path) -> None:
|
|
||||||
"""Import Midas definitions from a path
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (Path): the import path
|
|
||||||
"""
|
|
||||||
self.logger.debug(f"Importing type definitions from {path}")
|
|
||||||
path = (self.file_path.parent / path).resolve()
|
|
||||||
lexer: MidasLexer = MidasLexer(path.read_text())
|
|
||||||
tokens: list[Token] = lexer.process()
|
|
||||||
parser: MidasParser = MidasParser(tokens)
|
|
||||||
stmts: list[m.Stmt] = parser.parse()
|
|
||||||
self.ctx.resolve(stmts)
|
|
||||||
self.logger.debug(f"Midas types: {self.ctx._types}")
|
|
||||||
self.logger.debug(f"Midas operations: {self.ctx._operations}")
|
|
||||||
|
|
||||||
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
|
|
||||||
self.evaluate(stmt.expr)
|
|
||||||
|
|
||||||
def visit_function(self, stmt: p.Function) -> None:
|
|
||||||
env: Environment = Environment(self.env)
|
|
||||||
pos_args: list[Function.Argument] = []
|
|
||||||
args: list[Function.Argument] = []
|
|
||||||
kw_args: list[Function.Argument] = []
|
|
||||||
|
|
||||||
def eval_arg_type(arg: p.Function.Argument) -> Type:
|
|
||||||
if arg.type is not None:
|
|
||||||
return arg.type.accept(self)
|
|
||||||
if arg.default is not None:
|
|
||||||
return arg.default.accept(self)
|
|
||||||
return UnknownType()
|
|
||||||
|
|
||||||
for arg in stmt.posonlyargs:
|
|
||||||
pos_args.append(
|
|
||||||
Function.Argument(
|
|
||||||
name=arg.name,
|
|
||||||
type=eval_arg_type(arg),
|
|
||||||
required=arg.default is None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for arg in stmt.args:
|
|
||||||
args.append(
|
|
||||||
Function.Argument(
|
|
||||||
name=arg.name,
|
|
||||||
type=eval_arg_type(arg),
|
|
||||||
required=arg.default is None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for arg in stmt.kwonlyargs:
|
|
||||||
kw_args.append(
|
|
||||||
Function.Argument(
|
|
||||||
name=arg.name,
|
|
||||||
type=eval_arg_type(arg),
|
|
||||||
required=arg.default is None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for arg in pos_args + args + kw_args:
|
|
||||||
env.define(arg.name, arg.type)
|
|
||||||
|
|
||||||
returns_hint: Optional[Type] = None
|
|
||||||
if stmt.returns is not None:
|
|
||||||
returns_hint = stmt.returns.accept(self)
|
|
||||||
# Early define to handle simple fully-typed recursion
|
|
||||||
inside_function: Function = Function(
|
|
||||||
name=stmt.name,
|
|
||||||
pos_args=pos_args,
|
|
||||||
args=args,
|
|
||||||
kw_args=kw_args,
|
|
||||||
returns=returns_hint,
|
|
||||||
)
|
|
||||||
self.env.define(stmt.name, inside_function)
|
|
||||||
|
|
||||||
returned: bool = self.evaluate_block(stmt.body, env)
|
|
||||||
inferred_return: Type = UnknownType()
|
|
||||||
if not returned:
|
|
||||||
env.return_types.append(UnitType())
|
|
||||||
return_types: set[Type] = set(env.return_types)
|
|
||||||
if len(return_types) == 1:
|
|
||||||
inferred_return = list(return_types)[0]
|
|
||||||
elif len(return_types) > 1:
|
|
||||||
self.error(
|
|
||||||
stmt.location,
|
|
||||||
f"Mixed return types: {env.return_types}",
|
|
||||||
)
|
|
||||||
|
|
||||||
returns: Type = UnknownType()
|
|
||||||
if returns_hint is not None:
|
|
||||||
assert stmt.returns is not None
|
|
||||||
returns = returns_hint
|
|
||||||
if returns != inferred_return:
|
|
||||||
self.error(
|
|
||||||
stmt.returns.location,
|
|
||||||
f"Return type mismatch, annotated {returns} but returns {inferred_return}",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
returns = inferred_return
|
|
||||||
|
|
||||||
# TODO: handle *args and **kwargs sinks
|
|
||||||
function: Function = Function(
|
|
||||||
name=stmt.name,
|
|
||||||
pos_args=pos_args,
|
|
||||||
args=args,
|
|
||||||
kw_args=kw_args,
|
|
||||||
returns=returns,
|
|
||||||
)
|
|
||||||
self.env.define(stmt.name, function)
|
|
||||||
|
|
||||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
|
||||||
# TODO check not yet defined locally
|
|
||||||
type: Type = stmt.type.accept(self)
|
|
||||||
self.env.define(stmt.name, type)
|
|
||||||
|
|
||||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
|
||||||
value: Type = self.evaluate(stmt.value)
|
|
||||||
for target in stmt.targets:
|
|
||||||
if not isinstance(target, p.VariableExpr):
|
|
||||||
self.logger.warning(f"Unsupported assignment to {target}")
|
|
||||||
self.warning(target.location, f"Unsupported assignment to {target}")
|
|
||||||
continue
|
|
||||||
name: str = target.name
|
|
||||||
var_type: Optional[Type] = self.look_up_variable(name, target)
|
|
||||||
|
|
||||||
if var_type is None:
|
|
||||||
self.env.define(name, value)
|
|
||||||
else:
|
|
||||||
# TODO: implement real comparison method
|
|
||||||
if var_type != value:
|
|
||||||
self.error(
|
|
||||||
stmt.location,
|
|
||||||
f"Cannot assign {value} to {name} of type {var_type}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
|
||||||
type: Type = stmt.value.accept(self) if stmt.value is not None else UnitType()
|
|
||||||
self.env.return_types.append(type)
|
|
||||||
raise ReturnException()
|
|
||||||
|
|
||||||
def visit_if_stmt(self, stmt: p.IfStmt) -> None:
|
|
||||||
# Not evaluated in sub-environment because assignments in the test leak out of the if
|
|
||||||
# For example:
|
|
||||||
# if (m := 1 + 1) < 2:
|
|
||||||
# ...
|
|
||||||
# print(m) # <- m is still defined
|
|
||||||
test_type: Type = stmt.test.accept(self)
|
|
||||||
|
|
||||||
# TODO Allow subtypes or any type
|
|
||||||
if test_type != self.ctx.get_type("bool"):
|
|
||||||
self.error(
|
|
||||||
stmt.test.location, f"If test must be a boolean, got {test_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
env: Environment = Environment(self.env)
|
|
||||||
body_returned: bool = self.evaluate_block(stmt.body, env)
|
|
||||||
else_returned: bool = self.evaluate_block(stmt.orelse, env)
|
|
||||||
self.env.return_types.extend(env.return_types)
|
|
||||||
if body_returned and else_returned:
|
|
||||||
raise ReturnException()
|
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: p.BinaryExpr) -> Type:
|
|
||||||
method: Optional[str] = OPERATOR_METHODS.get(expr.operator.__class__)
|
|
||||||
if method is None:
|
|
||||||
self.logger.warning(f"Unsupported operator {expr.operator}")
|
|
||||||
self.warning(expr.location, f"Unsupported operator {expr.operator}")
|
|
||||||
return UnknownType()
|
|
||||||
left: Type = self.evaluate(expr.left)
|
|
||||||
right: Type = self.evaluate(expr.right)
|
|
||||||
|
|
||||||
result: Optional[Type] = self.ctx.get_operation_result(left, method, right)
|
|
||||||
if result is None:
|
|
||||||
self.error(
|
|
||||||
expr.location,
|
|
||||||
f"Undefined operation {method} between {left} and {right}",
|
|
||||||
)
|
|
||||||
return UnknownType()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def visit_compare_expr(self, expr: p.CompareExpr) -> Type:
|
|
||||||
method: Optional[str] = COMPARATOR_METHODS.get(expr.operator.__class__)
|
|
||||||
if method is None:
|
|
||||||
self.logger.warning(f"Unsupported operator {expr.operator}")
|
|
||||||
self.warning(expr.location, f"Unsupported operator {expr.operator}")
|
|
||||||
return UnknownType()
|
|
||||||
left: Type = self.evaluate(expr.left)
|
|
||||||
right: Type = self.evaluate(expr.right)
|
|
||||||
|
|
||||||
result: Optional[Type] = self.ctx.get_operation_result(left, method, right)
|
|
||||||
if result is None:
|
|
||||||
self.error(
|
|
||||||
expr.location,
|
|
||||||
f"Undefined operation {method} between {left} and {right}",
|
|
||||||
)
|
|
||||||
return UnknownType()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: p.UnaryExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_call_expr(self, expr: p.CallExpr) -> Type:
|
|
||||||
if path := self.parse_midas_import(expr):
|
|
||||||
self.import_midas(path)
|
|
||||||
return UnknownType()
|
|
||||||
callee: Type = self.evaluate(expr.callee)
|
|
||||||
if not isinstance(callee, Function):
|
|
||||||
self.error(expr.callee.location, "Callee is not a function")
|
|
||||||
return UnknownType()
|
|
||||||
function: Function = callee
|
|
||||||
mapped: list[MappedArgument] = self.map_call_arguments(function, expr)
|
|
||||||
for arg in mapped:
|
|
||||||
if arg.type != arg.argument.type:
|
|
||||||
self.error(
|
|
||||||
arg.expr.location,
|
|
||||||
f"Wrong type for argument '{arg.argument.name}', expected {arg.argument.type}, got {arg.type}",
|
|
||||||
)
|
|
||||||
return function.returns
|
|
||||||
|
|
||||||
def visit_get_expr(self, expr: p.GetExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: p.LiteralExpr) -> Type:
|
|
||||||
match expr.value:
|
|
||||||
case bool(): # Must be before int
|
|
||||||
return self.ctx.get_type("bool")
|
|
||||||
case int():
|
|
||||||
return self.ctx.get_type("int")
|
|
||||||
case float():
|
|
||||||
return self.ctx.get_type("float")
|
|
||||||
case str():
|
|
||||||
return self.ctx.get_type("str")
|
|
||||||
case _:
|
|
||||||
self.warning(expr.location, f"Unknown literal {expr}")
|
|
||||||
return UnknownType()
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: p.VariableExpr) -> Type:
|
|
||||||
return self.look_up_variable(expr.name, expr) or UnknownType()
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: p.LogicalExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_set_expr(self, expr: p.SetExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_cast_expr(self, expr: p.CastExpr) -> Type:
|
|
||||||
return expr.type.accept(self)
|
|
||||||
|
|
||||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> Type:
|
|
||||||
test_type: Type = expr.test.accept(self)
|
|
||||||
|
|
||||||
# TODO Allow subtypes or any type
|
|
||||||
if test_type != self.ctx.get_type("bool"):
|
|
||||||
self.error(
|
|
||||||
expr.test.location, f"If test must be a boolean, got {test_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
true_type: Type = expr.if_true.accept(self)
|
|
||||||
false_type: Type = expr.if_false.accept(self)
|
|
||||||
if true_type != false_type:
|
|
||||||
self.error(expr.location, f"Type mismatch in ternary if branches: true={true_type} != false={false_type}")
|
|
||||||
return UnknownType()
|
|
||||||
return true_type
|
|
||||||
|
|
||||||
def visit_base_type(self, node: p.BaseType) -> Type:
|
|
||||||
return self.ctx.get_type(node.base)
|
|
||||||
|
|
||||||
def visit_constraint_type(self, node: p.ConstraintType) -> Type: ...
|
|
||||||
|
|
||||||
def visit_frame_column(self, node: p.FrameColumn) -> Type: ...
|
|
||||||
|
|
||||||
def visit_frame_type(self, node: p.FrameType) -> Type: ...
|
|
||||||
|
|
||||||
def map_call_arguments(
|
|
||||||
self, function: Function, call: p.CallExpr
|
|
||||||
) -> list[MappedArgument]:
|
|
||||||
"""Map call arguments to function parameters as defined in its signature
|
|
||||||
|
|
||||||
This method maps positional-only, keyword-only and mixed parameter definitions
|
|
||||||
with the arguments passed at the call site
|
|
||||||
|
|
||||||
Any mismatched, missing or unexpected argument is reported as a diagnostic
|
|
||||||
|
|
||||||
Args:
|
|
||||||
function (Function): the function definition
|
|
||||||
call (p.CallExpr): the call expression
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[MappedArgument]: the list of mapped arguments
|
|
||||||
"""
|
|
||||||
positional: list[tuple[p.Expr, Type]] = [
|
|
||||||
(arg, self.evaluate(arg)) for arg in call.arguments
|
|
||||||
]
|
|
||||||
keywords: dict[str, tuple[p.Expr, Type]] = {
|
|
||||||
name: (arg, self.evaluate(arg)) for name, arg in call.keywords.items()
|
|
||||||
}
|
|
||||||
set_args: set[str] = set()
|
|
||||||
|
|
||||||
required_positional: list[str] = [
|
|
||||||
arg.name for arg in function.pos_args + function.args if arg.required
|
|
||||||
]
|
|
||||||
required_keyword: list[str] = [
|
|
||||||
arg.name for arg in function.kw_args if arg.required
|
|
||||||
]
|
|
||||||
|
|
||||||
mapped: list[MappedArgument] = []
|
|
||||||
|
|
||||||
pos_params: list[Function.Argument] = list(function.pos_args)
|
|
||||||
mixed_params: list[Function.Argument] = list(function.args)
|
|
||||||
kw_params: dict[str, Function.Argument] = {
|
|
||||||
arg.name: arg for arg in function.kw_args
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: handle *args and **kwargs sinks
|
|
||||||
for arg in positional:
|
|
||||||
param: Function.Argument
|
|
||||||
if len(pos_params) != 0:
|
|
||||||
param = pos_params.pop(0)
|
|
||||||
elif len(mixed_params) != 0:
|
|
||||||
param = mixed_params.pop(0)
|
|
||||||
else:
|
|
||||||
self.error(arg[0].location, "Too many positional arguments")
|
|
||||||
break
|
|
||||||
name: str = param.name
|
|
||||||
if name in required_positional:
|
|
||||||
required_positional.remove(name)
|
|
||||||
if name in required_keyword:
|
|
||||||
required_keyword.remove(name)
|
|
||||||
set_args.add(name)
|
|
||||||
mapped.append(
|
|
||||||
MappedArgument(
|
|
||||||
expr=arg[0],
|
|
||||||
type=arg[1],
|
|
||||||
argument=param,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
kw_params.update({arg.name: arg for arg in mixed_params})
|
|
||||||
for name, arg in keywords.items():
|
|
||||||
param: Function.Argument
|
|
||||||
if name not in kw_params:
|
|
||||||
if name in set_args:
|
|
||||||
self.error(
|
|
||||||
arg[0].location, f"Multiple values for argument '{name}'"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.error(arg[0].location, f"Unknown keyword argument '{name}'")
|
|
||||||
continue
|
|
||||||
param = kw_params.pop(name)
|
|
||||||
if name in required_positional:
|
|
||||||
required_positional.remove(name)
|
|
||||||
if name in required_keyword:
|
|
||||||
required_keyword.remove(name)
|
|
||||||
set_args.add(name)
|
|
||||||
mapped.append(
|
|
||||||
MappedArgument(
|
|
||||||
expr=arg[0],
|
|
||||||
type=arg[1],
|
|
||||||
argument=param,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def join_args(args: list[str]) -> str:
|
|
||||||
args = list(map(lambda a: f"'{a}'", args))
|
|
||||||
if len(args) == 0:
|
|
||||||
return ""
|
|
||||||
if len(args) == 1:
|
|
||||||
return args[0]
|
|
||||||
return ", ".join(args[:-1]) + " and " + args[-1]
|
|
||||||
|
|
||||||
if len(required_positional) != 0:
|
|
||||||
plural: str = "" if len(required_positional) == 1 else "s"
|
|
||||||
args: str = join_args(required_positional)
|
|
||||||
self.error(
|
|
||||||
call.location,
|
|
||||||
f"Missing required positional argument{plural}: {args}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(required_keyword) != 0:
|
|
||||||
plural: str = "" if len(required_keyword) == 1 else "s"
|
|
||||||
args: str = join_args(required_keyword)
|
|
||||||
self.error(
|
|
||||||
call.location,
|
|
||||||
f"Missing required keyword argument{plural}: {args}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return mapped
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
@@ -10,16 +9,18 @@ class DiagnosticType(StrEnum):
|
|||||||
ERROR = "Error"
|
ERROR = "Error"
|
||||||
WARNING = "Warning"
|
WARNING = "Warning"
|
||||||
INFO = "Info"
|
INFO = "Info"
|
||||||
|
DEBUG = "Debug"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Diagnostic:
|
class Diagnostic:
|
||||||
file_path: Path
|
file_path: Optional[str]
|
||||||
location: Location
|
location: Location
|
||||||
type: DiagnosticType
|
type: DiagnosticType
|
||||||
message: str
|
message: str
|
||||||
|
|
||||||
def __str__(self) -> str:
|
@property
|
||||||
|
def location_str(self) -> str:
|
||||||
start_loc: str = f"L{self.location.lineno}:{self.location.col_offset+1}"
|
start_loc: str = f"L{self.location.lineno}:{self.location.col_offset+1}"
|
||||||
end_loc: Optional[str] = ""
|
end_loc: Optional[str] = ""
|
||||||
if (
|
if (
|
||||||
@@ -27,7 +28,16 @@ class Diagnostic:
|
|||||||
and self.location.end_col_offset is not None
|
and self.location.end_col_offset is not None
|
||||||
):
|
):
|
||||||
end_loc = f"L{self.location.end_lineno}:{self.location.end_col_offset+1}"
|
end_loc = f"L{self.location.end_lineno}:{self.location.end_col_offset+1}"
|
||||||
loc: str = (
|
|
||||||
f"at {start_loc}" if end_loc is None else f"from {start_loc} to {end_loc}"
|
loc: str = ""
|
||||||
)
|
if self.file_path is not None:
|
||||||
return f"{self.type} in {self.file_path} {loc}: {self.message}"
|
loc += f" in {self.file_path}"
|
||||||
|
if end_loc is None:
|
||||||
|
loc += f" at {start_loc}"
|
||||||
|
else:
|
||||||
|
loc += f" from {start_loc} to {end_loc}"
|
||||||
|
|
||||||
|
return f"{self.type}{loc}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.location_str}: {self.message}"
|
||||||
|
|||||||
484
midas/checker/dispatcher.py
Normal file
484
midas/checker/dispatcher.py
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Generic, Optional, Protocol, TypeVar, Union
|
||||||
|
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
DerivedType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
from midas.checker.unifier import Unifier
|
||||||
|
|
||||||
|
|
||||||
|
class HasLocation(Protocol):
|
||||||
|
@property
|
||||||
|
def location(self) -> Location: ...
|
||||||
|
|
||||||
|
|
||||||
|
E = TypeVar("E", bound=HasLocation)
|
||||||
|
|
||||||
|
TypedExpr = tuple[E, Type]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class MappedArgument(Generic[E]):
|
||||||
|
expr: E
|
||||||
|
type: Type
|
||||||
|
argument: Function.Argument
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class OverloadCandidate:
|
||||||
|
function: Function
|
||||||
|
mapped: list[MappedArgument]
|
||||||
|
|
||||||
|
|
||||||
|
class CallError(StrEnum):
|
||||||
|
INVALID_ARGS = "Invalid arguments"
|
||||||
|
NO_MATCHING_OVERLOAD = "No matching overload"
|
||||||
|
IMPOSSIBLE_UNIFICATION = "Parameters unification failed"
|
||||||
|
NOT_CALLABLE = "Not callable"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class CallResult:
|
||||||
|
error: Optional[CallError] = None
|
||||||
|
result: Type = UnknownType()
|
||||||
|
message: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return self.error is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_message(self) -> str:
|
||||||
|
if self.message is not None:
|
||||||
|
return self.message
|
||||||
|
if self.error is not None:
|
||||||
|
return str(self.error)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class CallDispatcher(Generic[E]):
|
||||||
|
def __init__(self, types: TypesRegistry, reporter: FileReporter) -> None:
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.reporter: FileReporter = reporter
|
||||||
|
self.logger: logging.Logger = logging.getLogger("CallDispatcher")
|
||||||
|
|
||||||
|
def set_reporter(self, reporter: FileReporter):
|
||||||
|
self.reporter = reporter
|
||||||
|
|
||||||
|
def get_result(
|
||||||
|
self,
|
||||||
|
location: Location,
|
||||||
|
callee: Type,
|
||||||
|
positional: list[TypedExpr[E]],
|
||||||
|
keywords: dict[str, TypedExpr[E]],
|
||||||
|
report_errors: bool = True,
|
||||||
|
) -> CallResult:
|
||||||
|
"""Get the result type of a function call
|
||||||
|
|
||||||
|
If the function has overloads, the function will try to resolve the
|
||||||
|
appropriate signature.
|
||||||
|
Argument types are matched to the defined parameters.
|
||||||
|
The function doesn't take the raw expression as a parameter to accommodate
|
||||||
|
for desugared calls such as for operators.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the call location
|
||||||
|
callee (Type): the called function
|
||||||
|
positional (list[TypedExpr]): the list positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the return type of the call, or `None` if either
|
||||||
|
the call is invalid or no overload matched the arguments uniquely
|
||||||
|
"""
|
||||||
|
match callee:
|
||||||
|
case Function() as function:
|
||||||
|
valid: bool
|
||||||
|
mapped: list[MappedArgument[E]]
|
||||||
|
valid, mapped = self.map_call_arguments(
|
||||||
|
function, location, positional, keywords
|
||||||
|
)
|
||||||
|
valid = valid and self._are_arguments_valid(mapped, report_errors)
|
||||||
|
if not valid:
|
||||||
|
return CallResult(error=CallError.INVALID_ARGS)
|
||||||
|
return CallResult(result=function.returns)
|
||||||
|
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
res = self._match_overload(
|
||||||
|
overloads, location, positional, keywords, report_errors
|
||||||
|
)
|
||||||
|
if res[0] is None:
|
||||||
|
return CallResult(
|
||||||
|
error=CallError.NO_MATCHING_OVERLOAD,
|
||||||
|
message=res[1],
|
||||||
|
)
|
||||||
|
return CallResult(result=res[0].returns)
|
||||||
|
|
||||||
|
case AppliedType(body=body):
|
||||||
|
return self.get_result(
|
||||||
|
location, body, positional, keywords, report_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
case UnknownType():
|
||||||
|
return CallResult(result=UnknownType())
|
||||||
|
|
||||||
|
case DerivedType(type=base):
|
||||||
|
return self.get_result(
|
||||||
|
location, base, positional, keywords, report_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
case GenericType():
|
||||||
|
unifier: Unifier = Unifier(self.types)
|
||||||
|
pos: list[Type] = [a[1] for a in positional]
|
||||||
|
kw: dict[str, Type] = {k: v[1] for k, v in keywords.items()}
|
||||||
|
unified: Optional[Type] = unifier.unify_call(callee, pos, kw)
|
||||||
|
if unified is None:
|
||||||
|
pos_str: str = ", ".join(str(t) for t in pos)
|
||||||
|
kw_str: str = ", ".join(f"{k}: {v}" for k, v in kw.items())
|
||||||
|
message: str = (
|
||||||
|
f"Could not unify {callee}={callee.body} with pos=[{pos_str}] and kw={{{kw_str}}}"
|
||||||
|
)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(location, message)
|
||||||
|
return CallResult(
|
||||||
|
error=CallError.IMPOSSIBLE_UNIFICATION,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
return self.get_result(
|
||||||
|
location,
|
||||||
|
unified,
|
||||||
|
positional,
|
||||||
|
keywords,
|
||||||
|
report_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
message: str = f"{callee} ({callee.__class__.__name__}) is not callable"
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(location, message)
|
||||||
|
return CallResult(
|
||||||
|
error=CallError.NOT_CALLABLE,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _unwrap_function(
|
||||||
|
self,
|
||||||
|
callee: Type,
|
||||||
|
positional: list[TypedExpr[E]],
|
||||||
|
keywords: dict[str, TypedExpr[E]],
|
||||||
|
) -> Union[tuple[Function, None], tuple[None, CallError]]:
|
||||||
|
match callee:
|
||||||
|
case DerivedType(type=base):
|
||||||
|
return self._unwrap_function(base, positional, keywords)
|
||||||
|
|
||||||
|
case GenericType():
|
||||||
|
unifier: Unifier = Unifier(self.types)
|
||||||
|
unified: Optional[Type] = unifier.unify_call(
|
||||||
|
callee,
|
||||||
|
[a[1] for a in positional],
|
||||||
|
{k: v[1] for k, v in keywords.items()},
|
||||||
|
)
|
||||||
|
if unified is None:
|
||||||
|
return None, CallError.IMPOSSIBLE_UNIFICATION
|
||||||
|
return self._unwrap_function(unified, positional, keywords)
|
||||||
|
|
||||||
|
case Function():
|
||||||
|
return callee, None
|
||||||
|
|
||||||
|
case AppliedType(body=body):
|
||||||
|
return self._unwrap_function(body, positional, keywords)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return None, CallError.NOT_CALLABLE
|
||||||
|
|
||||||
|
def _are_arguments_valid(
|
||||||
|
self,
|
||||||
|
arguments: list[MappedArgument[E]],
|
||||||
|
report_errors: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""Check whether the passed argument types correspond to their matched parameter definitions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments (list[MappedArgument]): the list of argument/parameter pairs
|
||||||
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if all arguments fit the matching parameter definitions, False otherwise
|
||||||
|
"""
|
||||||
|
valid: bool = True
|
||||||
|
for arg in arguments:
|
||||||
|
if not self.types.is_subtype(arg.type, arg.argument.type):
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
arg.expr.location,
|
||||||
|
f"Wrong type for argument '{arg.argument.name}', expected {arg.argument.type}, got {arg.type}",
|
||||||
|
)
|
||||||
|
valid = False
|
||||||
|
return valid
|
||||||
|
|
||||||
|
def _match_overload(
|
||||||
|
self,
|
||||||
|
overloads: list[Type],
|
||||||
|
location: Location,
|
||||||
|
positional: list[TypedExpr[E]],
|
||||||
|
keywords: dict[str, TypedExpr[E]],
|
||||||
|
report_errors: bool = True,
|
||||||
|
) -> Union[tuple[Function, None], tuple[None, str]]:
|
||||||
|
"""Try and resolve the appropriate overload for the given arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
overloads (list[Type]): the list of possible overloads
|
||||||
|
location (Location): the call location
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keywords arguments
|
||||||
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Function]: the resolved function signature if it can be
|
||||||
|
determined unambiguously, or `None`.
|
||||||
|
"""
|
||||||
|
candidates: list[OverloadCandidate] = []
|
||||||
|
errors: list[CallError] = []
|
||||||
|
for overload in overloads:
|
||||||
|
function, unwrap_error = self._unwrap_function(
|
||||||
|
overload, positional, keywords
|
||||||
|
)
|
||||||
|
if function is None:
|
||||||
|
errors.append(unwrap_error) # type: ignore
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid, mapped = self.map_call_arguments(
|
||||||
|
function=function,
|
||||||
|
location=location,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
report_errors=False,
|
||||||
|
)
|
||||||
|
if valid and self._are_arguments_valid(mapped, report_errors=False):
|
||||||
|
candidates.append(
|
||||||
|
OverloadCandidate(
|
||||||
|
function=function,
|
||||||
|
mapped=mapped,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
pos_types: str = ", ".join(str(type) for _, type in positional)
|
||||||
|
kw_types: str = ", ".join(
|
||||||
|
f"{name}: {type}" for name, (_, type) in keywords.items()
|
||||||
|
)
|
||||||
|
for_args: str = f"for arguments pos=[{pos_types}] and kw={{{kw_types}}}"
|
||||||
|
|
||||||
|
n_candidates: int = len(candidates)
|
||||||
|
|
||||||
|
# Exactly 1 match -> return it
|
||||||
|
if n_candidates == 1:
|
||||||
|
return candidates[0].function, None
|
||||||
|
|
||||||
|
# No match -> invalid call
|
||||||
|
if n_candidates == 0:
|
||||||
|
overloads_str: str = ", ".join(map(str, overloads))
|
||||||
|
errors_str: str = ", ".join(errors)
|
||||||
|
message: str = (
|
||||||
|
f"No matching overload in [{overloads_str}] {for_args} (errors: {errors_str})"
|
||||||
|
)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(location, message)
|
||||||
|
return None, message
|
||||||
|
|
||||||
|
# Multiple matches -> see if one <: all others (more specific)
|
||||||
|
for i1, c1 in enumerate(candidates):
|
||||||
|
mapped1: list[MappedArgument[E]] = c1.mapped
|
||||||
|
best_match: bool = True
|
||||||
|
for i2, c2 in enumerate(candidates):
|
||||||
|
if i1 == i2:
|
||||||
|
continue
|
||||||
|
mapped2: list[MappedArgument[E]] = c2.mapped
|
||||||
|
if not self._are_mapped_subtypes(mapped1, mapped2):
|
||||||
|
best_match = False
|
||||||
|
break
|
||||||
|
self.logger.debug(f"{c1.function} is a full overload of {c2.function}")
|
||||||
|
if best_match:
|
||||||
|
return c1.function, None
|
||||||
|
|
||||||
|
candidates_str: str = ", ".join(
|
||||||
|
str(candidate.function) for candidate in candidates
|
||||||
|
)
|
||||||
|
message: str = f"Multiple matching overloads {for_args}: {candidates_str}"
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(location, message)
|
||||||
|
return None, message
|
||||||
|
|
||||||
|
def map_call_arguments(
|
||||||
|
self,
|
||||||
|
function: Function,
|
||||||
|
location: Location,
|
||||||
|
positional: list[TypedExpr[E]],
|
||||||
|
keywords: dict[str, TypedExpr[E]],
|
||||||
|
report_errors: bool = True,
|
||||||
|
) -> tuple[bool, list[MappedArgument]]:
|
||||||
|
"""Map call arguments to a function's parameters as defined in its signature
|
||||||
|
|
||||||
|
This method maps positional-only, keyword-only and mixed parameter definitions
|
||||||
|
with the arguments passed at the call site
|
||||||
|
|
||||||
|
Any mismatched, missing or unexpected argument is reported as a diagnostic,
|
||||||
|
unless `report_errors` is set to `False`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function (Function): the function definition
|
||||||
|
location (Location): the call location
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[bool, list[MappedArgument]]: a boolean reporting whether
|
||||||
|
the call is valid and the list of mapped arguments
|
||||||
|
"""
|
||||||
|
set_args: set[str] = set()
|
||||||
|
|
||||||
|
required_positional: list[str] = [
|
||||||
|
arg.name for arg in function.pos_args + function.args if arg.required
|
||||||
|
]
|
||||||
|
required_keyword: list[str] = [
|
||||||
|
arg.name for arg in function.kw_args if arg.required
|
||||||
|
]
|
||||||
|
|
||||||
|
mapped: list[MappedArgument[E]] = []
|
||||||
|
|
||||||
|
pos_params: list[Function.Argument] = list(function.pos_args)
|
||||||
|
mixed_params: list[Function.Argument] = list(function.args)
|
||||||
|
kw_params: dict[str, Function.Argument] = {
|
||||||
|
arg.name: arg for arg in function.kw_args
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_call: bool = True
|
||||||
|
|
||||||
|
# TODO: handle *args and **kwargs sinks
|
||||||
|
for arg in positional:
|
||||||
|
param: Function.Argument
|
||||||
|
if len(pos_params) != 0:
|
||||||
|
param = pos_params.pop(0)
|
||||||
|
elif len(mixed_params) != 0:
|
||||||
|
param = mixed_params.pop(0)
|
||||||
|
else:
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
arg[0].location, "Too many positional arguments"
|
||||||
|
)
|
||||||
|
valid_call = False
|
||||||
|
break
|
||||||
|
name: str = param.name
|
||||||
|
if name in required_positional:
|
||||||
|
required_positional.remove(name)
|
||||||
|
if name in required_keyword:
|
||||||
|
required_keyword.remove(name)
|
||||||
|
set_args.add(name)
|
||||||
|
mapped.append(
|
||||||
|
MappedArgument(
|
||||||
|
expr=arg[0],
|
||||||
|
type=arg[1],
|
||||||
|
argument=param,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
kw_params.update({arg.name: arg for arg in mixed_params})
|
||||||
|
for name, arg in keywords.items():
|
||||||
|
param: Function.Argument
|
||||||
|
if name not in kw_params:
|
||||||
|
if report_errors:
|
||||||
|
if name in set_args:
|
||||||
|
self.reporter.error(
|
||||||
|
arg[0].location, f"Multiple values for argument '{name}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.reporter.error(
|
||||||
|
arg[0].location, f"Unknown keyword argument '{name}'"
|
||||||
|
)
|
||||||
|
valid_call = False
|
||||||
|
continue
|
||||||
|
param = kw_params.pop(name)
|
||||||
|
if name in required_positional:
|
||||||
|
required_positional.remove(name)
|
||||||
|
if name in required_keyword:
|
||||||
|
required_keyword.remove(name)
|
||||||
|
set_args.add(name)
|
||||||
|
mapped.append(
|
||||||
|
MappedArgument(
|
||||||
|
expr=arg[0],
|
||||||
|
type=arg[1],
|
||||||
|
argument=param,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def join_args(args: list[str]) -> str:
|
||||||
|
args = list(map(lambda a: f"'{a}'", args))
|
||||||
|
if len(args) == 0:
|
||||||
|
return ""
|
||||||
|
if len(args) == 1:
|
||||||
|
return args[0]
|
||||||
|
return ", ".join(args[:-1]) + " and " + args[-1]
|
||||||
|
|
||||||
|
if len(required_positional) != 0:
|
||||||
|
plural: str = "" if len(required_positional) == 1 else "s"
|
||||||
|
args: str = join_args(required_positional)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Missing required positional argument{plural}: {args}",
|
||||||
|
)
|
||||||
|
valid_call = False
|
||||||
|
|
||||||
|
if len(required_keyword) != 0:
|
||||||
|
plural: str = "" if len(required_keyword) == 1 else "s"
|
||||||
|
args: str = join_args(required_keyword)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Missing required keyword argument{plural}: {args}",
|
||||||
|
)
|
||||||
|
valid_call = False
|
||||||
|
|
||||||
|
return valid_call, mapped
|
||||||
|
|
||||||
|
def _are_mapped_subtypes(
|
||||||
|
self, mapped1: list[MappedArgument[E]], mapped2: list[MappedArgument[E]]
|
||||||
|
) -> bool:
|
||||||
|
"""Check whether the given argument mappings are subtype/supertype of one another
|
||||||
|
|
||||||
|
This function checks whether the argument mappings `mapped1` are subtypes
|
||||||
|
of `mapped2`. If any of the parameter type in `mapped1` is not a subtype
|
||||||
|
of the corresponding parameter in `mapped2`, `False` is returned.
|
||||||
|
|
||||||
|
This is used to check whether a given overload is
|
||||||
|
a more specific function/ a subtype of another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapped1 (list[MappedArgument]): the first argument mappings (subtype)
|
||||||
|
mapped2 (list[MappedArgument]): the second argument mappings (supertype)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: `True` if `mapped1` is a subtype of `mapped2`, `False` otherwise
|
||||||
|
"""
|
||||||
|
by_expr: dict[E, Type] = {}
|
||||||
|
for arg in mapped1:
|
||||||
|
by_expr[arg.expr] = arg.argument.type
|
||||||
|
|
||||||
|
for arg in mapped2:
|
||||||
|
type2: Type = arg.argument.type
|
||||||
|
type1: Type = by_expr[arg.expr]
|
||||||
|
if not self.types.is_subtype(type1, type2):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
172
midas/checker/evaluator.py
Normal file
172
midas/checker/evaluator.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.checker.preamble import Preamble
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
from midas.checker.types import Function, Predicate
|
||||||
|
from midas.lexer.token import TokenType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class PartialPredicate(Predicate):
|
||||||
|
scope: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class Evaluator(m.Expr.Visitor[Any]):
|
||||||
|
def __init__(self, types: TypesRegistry, reporter: Optional[FileReporter] = None):
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.reporter: Optional[FileReporter] = reporter
|
||||||
|
self.preamble: Preamble = Preamble(self.types)
|
||||||
|
self.scopes: list[dict[str, Any]] = [{}]
|
||||||
|
|
||||||
|
def evaluate(self, expr: m.Expr) -> Any:
|
||||||
|
value: Any = expr.accept(self)
|
||||||
|
if self.reporter is not None:
|
||||||
|
self.reporter.debug(expr.location, f"Value: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_value(self, name: str) -> Any:
|
||||||
|
scope: dict[str, Any] = self.scopes[-1]
|
||||||
|
return scope[name]
|
||||||
|
|
||||||
|
def set_value(self, name: str, value: Any, force_declare: bool = False):
|
||||||
|
if not force_declare:
|
||||||
|
for scope in reversed(self.scopes):
|
||||||
|
if name in scope:
|
||||||
|
scope[name] = value
|
||||||
|
return
|
||||||
|
self.scopes[-1][name] = value
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: m.LogicalExpr) -> Any:
|
||||||
|
def left():
|
||||||
|
return self.evaluate(expr.left)
|
||||||
|
|
||||||
|
def right():
|
||||||
|
return self.evaluate(expr.right)
|
||||||
|
|
||||||
|
match expr.operator.type:
|
||||||
|
case TokenType.AND:
|
||||||
|
return left() and right()
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: m.BinaryExpr) -> Any:
|
||||||
|
left: Any = self.evaluate(expr.left)
|
||||||
|
right: Any = self.evaluate(expr.right)
|
||||||
|
match expr.operator.type:
|
||||||
|
case TokenType.MINUS:
|
||||||
|
return left - right
|
||||||
|
case TokenType.STAR:
|
||||||
|
return left * right
|
||||||
|
case TokenType.SLASH:
|
||||||
|
return left / right
|
||||||
|
case TokenType.GREATER:
|
||||||
|
return left > right
|
||||||
|
case TokenType.GREATER_EQUAL:
|
||||||
|
return left >= right
|
||||||
|
case TokenType.LESS:
|
||||||
|
return left < right
|
||||||
|
case TokenType.LESS_EQUAL:
|
||||||
|
return left <= right
|
||||||
|
case TokenType.EQUAL_EQUAL:
|
||||||
|
return left == right
|
||||||
|
case TokenType.BANG_EQUAL:
|
||||||
|
return left != right
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: m.UnaryExpr) -> Any:
|
||||||
|
right: Any = self.evaluate(expr.right)
|
||||||
|
match expr.operator.type:
|
||||||
|
case TokenType.MINUS:
|
||||||
|
return -right
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> Any:
|
||||||
|
callee: Any = self.evaluate(expr.callee)
|
||||||
|
args: list[Any] = [self.evaluate(arg) for arg in expr.arguments]
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
name: self.evaluate(arg) for name, arg in expr.keywords.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
match callee:
|
||||||
|
case Predicate():
|
||||||
|
return self._evaluate_predicate(callee, args, kwargs)
|
||||||
|
case _ if callable(callee):
|
||||||
|
return callee(*args, **kwargs)
|
||||||
|
case _:
|
||||||
|
return NotImplementedError
|
||||||
|
|
||||||
|
def visit_get_expr(self, expr: m.GetExpr) -> Any:
|
||||||
|
obj: Any = self.evaluate(expr.expr)
|
||||||
|
return getattr(obj, expr.name.lexeme)
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: m.VariableExpr) -> Any:
|
||||||
|
name: str = expr.name.lexeme
|
||||||
|
for scope in reversed(self.scopes):
|
||||||
|
if name in scope:
|
||||||
|
return scope[name]
|
||||||
|
|
||||||
|
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
||||||
|
if predicate is not None:
|
||||||
|
if predicate.alias:
|
||||||
|
return self.evaluate(predicate.body)
|
||||||
|
return predicate
|
||||||
|
|
||||||
|
glob: Optional[Callable] = self.preamble.get_py_func(name)
|
||||||
|
if glob is not None:
|
||||||
|
return glob
|
||||||
|
raise NameError(f"Unknown variable '{name}'")
|
||||||
|
|
||||||
|
def visit_grouping_expr(self, expr: m.GroupingExpr) -> Any:
|
||||||
|
return self.evaluate(expr.expr)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: m.LiteralExpr) -> Any:
|
||||||
|
return expr.value
|
||||||
|
|
||||||
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Any:
|
||||||
|
return self.get_value("_")
|
||||||
|
|
||||||
|
def _evaluate_predicate(
|
||||||
|
self, predicate: Predicate, args: list[Any], kwargs: dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
res: Any = None
|
||||||
|
if isinstance(predicate, PartialPredicate):
|
||||||
|
self.scopes.append(predicate.scope)
|
||||||
|
else:
|
||||||
|
self.scopes.append({})
|
||||||
|
match predicate.type:
|
||||||
|
case Function(returns=Function() as inner):
|
||||||
|
self._map_args(predicate.type, args, kwargs)
|
||||||
|
res = PartialPredicate(
|
||||||
|
type=inner,
|
||||||
|
body=predicate.body,
|
||||||
|
alias=False,
|
||||||
|
scope=self.scopes[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
case Function():
|
||||||
|
self._map_args(predicate.type, args, kwargs)
|
||||||
|
res = self.evaluate(predicate.body)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
self.scopes.pop()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _map_args(self, function: Function, args: list[Any], kwargs: dict[str, Any]):
|
||||||
|
positional: list[Function.Argument] = function.pos_args + function.args
|
||||||
|
keywords: dict[str, Function.Argument] = {
|
||||||
|
arg.name: arg for arg in function.args + function.kw_args
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg in enumerate(args):
|
||||||
|
param: Function.Argument = positional[i]
|
||||||
|
self.set_value(param.name, arg)
|
||||||
|
|
||||||
|
for name, arg in kwargs.items():
|
||||||
|
param: Function.Argument = keywords[name]
|
||||||
|
self.set_value(param.name, arg)
|
||||||
203
midas/checker/frames/column_groupby_methods.py
Normal file
203
midas/checker/frames/column_groupby_methods.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.dispatcher import CallResult
|
||||||
|
from midas.checker.frames.utils import MethodRegistry, method
|
||||||
|
from midas.checker.types import ColumnGroupBy, ColumnType, Function, TopType, Type
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
location: Location
|
||||||
|
call_expr: p.Expr
|
||||||
|
groupby: ColumnGroupBy
|
||||||
|
groupby_expr: p.Expr
|
||||||
|
positional: list[TypedExpr]
|
||||||
|
keywords: dict[str, TypedExpr]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subject(self) -> TypedExpr:
|
||||||
|
return (self.groupby_expr, self.groupby)
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnGroupByMethodRegistry(MethodRegistry[Call]):
|
||||||
|
NAMED_ARGS: dict[str, str] = {
|
||||||
|
"numeric_only": "bool",
|
||||||
|
"skipna": "bool",
|
||||||
|
"engine": "str",
|
||||||
|
"engine_kwargs": "dict",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _aggregate(
|
||||||
|
self,
|
||||||
|
call: Call,
|
||||||
|
args: list[str | tuple[str, str, bool]] = [],
|
||||||
|
*,
|
||||||
|
preserve_inner_type: bool = False,
|
||||||
|
) -> Type:
|
||||||
|
real_args: list[Function.Argument] = []
|
||||||
|
for i, arg in enumerate(args):
|
||||||
|
match arg:
|
||||||
|
case str() as name:
|
||||||
|
arg = Function.Argument(
|
||||||
|
pos=i,
|
||||||
|
name=name,
|
||||||
|
type=self.types.get_type(self.NAMED_ARGS[name]),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
case (name, type, required):
|
||||||
|
arg = Function.Argument(
|
||||||
|
pos=i,
|
||||||
|
name=name,
|
||||||
|
type=self.types.get_type(type),
|
||||||
|
required=required,
|
||||||
|
)
|
||||||
|
real_args.append(arg)
|
||||||
|
|
||||||
|
signature = Function(
|
||||||
|
args=real_args,
|
||||||
|
returns=(
|
||||||
|
call.groupby.column
|
||||||
|
if preserve_inner_type
|
||||||
|
else ColumnType(type=TopType())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def kurt(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
["skipna", "numeric_only"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def max(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
preserve_inner_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
["numeric_only", "skipna", "engine", "engine_kwargs"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
["numeric_only", "skipna"],
|
||||||
|
preserve_inner_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
preserve_inner_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def prod(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"ddof",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
"numeric_only",
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"var",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
"numeric_only",
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
78
midas/checker/frames/column_manager.py
Normal file
78
midas/checker/frames/column_manager.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.frames.column_groupby_methods import Call as GroupByCall
|
||||||
|
from midas.checker.frames.column_groupby_methods import ColumnGroupByMethodRegistry
|
||||||
|
from midas.checker.frames.column_methods import Call, ColumnMethodRegistry
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import ColumnGroupBy, ColumnType, Type
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnManager:
|
||||||
|
def __init__(self, typer: PythonTyper) -> None:
|
||||||
|
self.typer: PythonTyper = typer
|
||||||
|
self.method_resolver: ColumnMethodRegistry = ColumnMethodRegistry(self.typer)
|
||||||
|
self.groupby_method_resolver: ColumnGroupByMethodRegistry = (
|
||||||
|
ColumnGroupByMethodRegistry(self.typer)
|
||||||
|
)
|
||||||
|
|
||||||
|
def call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
location: Location,
|
||||||
|
call_expr: p.Expr,
|
||||||
|
column: ColumnType,
|
||||||
|
column_expr: p.Expr,
|
||||||
|
positional: list[TypedExpr],
|
||||||
|
keywords: dict[str, TypedExpr],
|
||||||
|
) -> Type:
|
||||||
|
call: Call = Call(
|
||||||
|
location=location,
|
||||||
|
call_expr=call_expr,
|
||||||
|
column=column,
|
||||||
|
column_expr=column_expr,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return self.method_resolver.call(method, call)
|
||||||
|
|
||||||
|
def groupby_call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
location: Location,
|
||||||
|
call_expr: p.Expr,
|
||||||
|
groupby: ColumnGroupBy,
|
||||||
|
groupby_expr: p.Expr,
|
||||||
|
positional: list[TypedExpr],
|
||||||
|
keywords: dict[str, TypedExpr],
|
||||||
|
) -> Type:
|
||||||
|
call: GroupByCall = GroupByCall(
|
||||||
|
location=location,
|
||||||
|
call_expr=call_expr,
|
||||||
|
groupby=groupby,
|
||||||
|
groupby_expr=groupby_expr,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return self.groupby_method_resolver.call(method, call)
|
||||||
|
|
||||||
|
def get_attribute(self, column: ColumnType, name: str) -> Optional[Type]:
|
||||||
|
types: TypesRegistry = self.typer.types
|
||||||
|
match name:
|
||||||
|
case "ndim" | "size":
|
||||||
|
return types.get_type("int")
|
||||||
|
|
||||||
|
case "shape":
|
||||||
|
return types.tuple_of("int")
|
||||||
|
|
||||||
|
case "T":
|
||||||
|
return column
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return None
|
||||||
410
midas/checker/frames/column_methods.py
Normal file
410
midas/checker/frames/column_methods.py
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.dispatcher import CallResult
|
||||||
|
from midas.checker.frames.utils import MethodRegistry, method
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnknownType,
|
||||||
|
unfold_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
location: Location
|
||||||
|
call_expr: p.Expr
|
||||||
|
column: ColumnType
|
||||||
|
column_expr: p.Expr
|
||||||
|
positional: list[TypedExpr]
|
||||||
|
keywords: dict[str, TypedExpr]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subject(self) -> TypedExpr:
|
||||||
|
return (self.column_expr, self.column)
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnMethodRegistry(MethodRegistry[Call]):
|
||||||
|
def _element_binary_op(self, call: Call, method: str) -> ColumnType:
|
||||||
|
"""Compute the result of an element-wise binary operation
|
||||||
|
|
||||||
|
This function delegates to the inner types for computing the resulting
|
||||||
|
type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
method (str): the method name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ColumnType: the resulting column type
|
||||||
|
"""
|
||||||
|
column2: Optional[ColumnType] = None
|
||||||
|
|
||||||
|
col_type1: Type = call.column.type
|
||||||
|
new_column: Type = ColumnType(type=UnknownType())
|
||||||
|
if len(call.positional) != 0:
|
||||||
|
other: Type = call.positional[0][1]
|
||||||
|
unfolded_other: Type = unfold_type(other)
|
||||||
|
if isinstance(unfolded_other, ColumnType):
|
||||||
|
column2 = unfolded_other
|
||||||
|
col_type2: Type = column2.type
|
||||||
|
|
||||||
|
new_inner_type = self.typer.result_of_binary_op(
|
||||||
|
location=call.location,
|
||||||
|
expr=call.call_expr,
|
||||||
|
left=(call.column_expr, col_type1),
|
||||||
|
right=(call.positional[0][0], col_type2),
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
new_column = ColumnType(type=new_inner_type)
|
||||||
|
return new_column
|
||||||
|
|
||||||
|
def _element_wise(self, call: Call, method: str) -> Type:
|
||||||
|
# TODO: support add with scalar
|
||||||
|
|
||||||
|
# Build signature with new column type and generic operand
|
||||||
|
param_type: TypeVar = TypeVar(name="T", bound=None)
|
||||||
|
signature = GenericType(
|
||||||
|
name="add",
|
||||||
|
params=[param_type],
|
||||||
|
body=Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="other",
|
||||||
|
type=ColumnType(type=param_type),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=self._element_binary_op(call, method),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map arguments and compute result type
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
if result.is_valid:
|
||||||
|
self._assert_same_length(
|
||||||
|
call.call_expr, call.column_expr, call.positional[0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method("add", "__add__")
|
||||||
|
def add(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__add__")
|
||||||
|
|
||||||
|
@method("sub", "__sub__")
|
||||||
|
def sub(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__sub__")
|
||||||
|
|
||||||
|
@method("mul", "__mul__")
|
||||||
|
def mul(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__mul__")
|
||||||
|
|
||||||
|
@method("div", "truediv", "__truediv__")
|
||||||
|
def truediv(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__truediv__")
|
||||||
|
|
||||||
|
@method("floordiv", "__floordiv__")
|
||||||
|
def floordiv(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__floordiv__")
|
||||||
|
|
||||||
|
@method("mod", "__mod__")
|
||||||
|
def mod(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__mod__")
|
||||||
|
|
||||||
|
@method("pow", "__pow__")
|
||||||
|
def pow(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__pow__")
|
||||||
|
|
||||||
|
@method("lt", "__lt__")
|
||||||
|
def lt(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__lt__")
|
||||||
|
|
||||||
|
@method("gt", "__gt__")
|
||||||
|
def gt(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__gt__")
|
||||||
|
|
||||||
|
@method("le", "__le__")
|
||||||
|
def le(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__le__")
|
||||||
|
|
||||||
|
@method("ge", "__ge__")
|
||||||
|
def ge(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__ge__")
|
||||||
|
|
||||||
|
@method("ne", "__ne__")
|
||||||
|
def ne(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__ne__")
|
||||||
|
|
||||||
|
@method("eq", "__eq__")
|
||||||
|
def eq(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__eq__")
|
||||||
|
|
||||||
|
def _aggregate(
|
||||||
|
self,
|
||||||
|
call: Call,
|
||||||
|
kwargs: list[Function.Argument] = [],
|
||||||
|
*,
|
||||||
|
preserve_inner_type: bool = False,
|
||||||
|
) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
*kwargs,
|
||||||
|
],
|
||||||
|
returns=call.column if preserve_inner_type else ColumnType(type=TopType()),
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method("kurtosis", "kurt")
|
||||||
|
def kurtosis(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def max(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, preserve_inner_type=True)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, preserve_inner_type=True)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, preserve_inner_type=True)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mode(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, preserve_inner_type=True)
|
||||||
|
|
||||||
|
@method("product", "prod")
|
||||||
|
def product(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="ddof",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="var",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def head(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="n",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=call.column,
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def tail(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="n",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=call.column,
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def groupby(self, call: Call) -> Type:
|
||||||
|
bool_: Type = self.types.get_type("bool")
|
||||||
|
function: Function = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="by",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="level",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=2,
|
||||||
|
name="as_index",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=3,
|
||||||
|
name="sort",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=4,
|
||||||
|
name="group_keys",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=5,
|
||||||
|
name="observed",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=6,
|
||||||
|
name="dropna",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=ColumnGroupBy(column=call.column),
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=function,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def _assert_same_length(self, call_expr: p.Expr, column1: p.Expr, column2: p.Expr):
|
||||||
|
func_name: str = "__midas_column_same_length__"
|
||||||
|
|
||||||
|
# Efficiently compute length
|
||||||
|
# https://stackoverflow.com/a/15943975/11109181
|
||||||
|
def len_of_col(col: ast.expr) -> ast.expr:
|
||||||
|
return ast.Call(
|
||||||
|
func=ast.Name(id="len"),
|
||||||
|
args=[
|
||||||
|
ast.Attribute(
|
||||||
|
value=col,
|
||||||
|
attr="index",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
keywords=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertions.define(
|
||||||
|
func_name,
|
||||||
|
ast.FunctionDef(
|
||||||
|
name=func_name,
|
||||||
|
args=ast.arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
ast.arg(arg="column1"),
|
||||||
|
ast.arg(arg="column2"),
|
||||||
|
],
|
||||||
|
kwonlyargs=[],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
),
|
||||||
|
body=[
|
||||||
|
ast.Return(
|
||||||
|
value=ast.Compare(
|
||||||
|
left=len_of_col(ast.Name(id="column1")),
|
||||||
|
ops=[ast.Eq()],
|
||||||
|
comparators=[
|
||||||
|
len_of_col(ast.Name(id="column2")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
decorator_list=[],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertions.add(
|
||||||
|
bound_expr=call_expr,
|
||||||
|
inputs=[column1, column2],
|
||||||
|
builder=lambda c1, c2: ast.Call(
|
||||||
|
func=ast.Name(id=func_name),
|
||||||
|
args=[c1, c2],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
message="Columns must have the same length",
|
||||||
|
)
|
||||||
103
midas/checker/frames/frame_groupby_methods.py
Normal file
103
midas/checker/frames/frame_groupby_methods.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.frames.utils import MethodRegistry, method
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
FrameGroupBy,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
location: Location
|
||||||
|
call_expr: p.Expr
|
||||||
|
groupby: FrameGroupBy
|
||||||
|
groupby_expr: p.Expr
|
||||||
|
positional: list[TypedExpr]
|
||||||
|
keywords: dict[str, TypedExpr]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subject(self) -> TypedExpr:
|
||||||
|
return (self.groupby_expr, self.groupby)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameGroupByMethodRegistry(MethodRegistry[Call]):
|
||||||
|
NAMED_ARGS: dict[str, str] = {
|
||||||
|
"numeric_only": "bool",
|
||||||
|
"skipna": "bool",
|
||||||
|
"engine": "str",
|
||||||
|
"engine_kwargs": "dict",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _aggregate(self, call: Call, method: str) -> Type:
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
|
||||||
|
for column in call.groupby.frame.columns:
|
||||||
|
column_groupby: ColumnGroupBy = ColumnGroupBy(column=column.type)
|
||||||
|
result_type: Type = self.typer.call_method(
|
||||||
|
location=call.location,
|
||||||
|
call_expr=call.call_expr,
|
||||||
|
obj=(call.groupby_expr, column_groupby),
|
||||||
|
method_name=method,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
if not isinstance(result_type, ColumnType):
|
||||||
|
result_type = ColumnType(type=UnknownType())
|
||||||
|
new_columns.append(
|
||||||
|
DataFrameType.Column(
|
||||||
|
index=column.index,
|
||||||
|
name=column.name,
|
||||||
|
type=result_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return DataFrameType(columns=new_columns)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def kurt(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "kurt")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def max(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "max")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "mean")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "median")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "min")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def prod(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "prod")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "std")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "sum")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, "var")
|
||||||
255
midas/checker/frames/frame_manager.py
Normal file
255
midas/checker/frames/frame_manager.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional, TypeGuard, cast
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.frames.frame_groupby_methods import Call as GroupByCall
|
||||||
|
from midas.checker.frames.frame_groupby_methods import FrameGroupByMethodRegistry
|
||||||
|
from midas.checker.frames.frame_methods import Call, FrameMethodRegistry
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
FrameGroupBy,
|
||||||
|
TupleType,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
||||||
|
return all(isinstance(expr, p.LiteralExpr) for expr in exprs)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameManager:
|
||||||
|
def __init__(self, typer: PythonTyper) -> None:
|
||||||
|
self.typer: PythonTyper = typer
|
||||||
|
self.method_resolver: FrameMethodRegistry = FrameMethodRegistry(self.typer)
|
||||||
|
self.groupby_method_resolver: FrameGroupByMethodRegistry = (
|
||||||
|
FrameGroupByMethodRegistry(self.typer)
|
||||||
|
)
|
||||||
|
|
||||||
|
def assign(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
frame: DataFrameType,
|
||||||
|
index: p.Expr,
|
||||||
|
value_type: Type,
|
||||||
|
) -> Type:
|
||||||
|
match index:
|
||||||
|
case p.LiteralExpr(value=str() as name):
|
||||||
|
return self.assign_column(reporter, location, frame, name, value_type)
|
||||||
|
|
||||||
|
case p.ListExpr(items=indices) if is_list_of_literals(indices) and all(
|
||||||
|
isinstance(index.value, str) for index in indices
|
||||||
|
):
|
||||||
|
names: list[str] = [cast(str, index.value) for index in indices]
|
||||||
|
|
||||||
|
if not isinstance(value_type, TupleType):
|
||||||
|
reporter.error(
|
||||||
|
location,
|
||||||
|
f"Cannot assign {type} to dataframe columns. Must be a tuple of columns",
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
if len(names) != len(value_type.items):
|
||||||
|
reporter.error(
|
||||||
|
location,
|
||||||
|
f"Wrong number of columns. Cannot assign {len(value_type.items)} to {len(names)} targets",
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
new_frame: Type = frame
|
||||||
|
for name, value in zip(names, value_type.items):
|
||||||
|
new_frame = self.assign_column(
|
||||||
|
reporter,
|
||||||
|
location,
|
||||||
|
new_frame,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
if not isinstance(new_frame, DataFrameType):
|
||||||
|
return new_frame
|
||||||
|
return new_frame
|
||||||
|
|
||||||
|
case _:
|
||||||
|
reporter.error(
|
||||||
|
location, f"Invalid index type {index} on {frame} (assignment)"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
def assign_column(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
frame: DataFrameType,
|
||||||
|
name: str,
|
||||||
|
type: Type,
|
||||||
|
) -> Type:
|
||||||
|
if not isinstance(type, ColumnType):
|
||||||
|
reporter.error(
|
||||||
|
location,
|
||||||
|
f"Cannot assign {type} to dataframe column. Must be a ColumnType",
|
||||||
|
)
|
||||||
|
return frame
|
||||||
|
return self._set_column(frame, name, type)
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
frame: DataFrameType,
|
||||||
|
index: p.Expr,
|
||||||
|
) -> Type:
|
||||||
|
match index:
|
||||||
|
case p.LiteralExpr(value=str() as name):
|
||||||
|
column: Optional[ColumnType] = FrameManager._get_column(frame, name)
|
||||||
|
if column is None:
|
||||||
|
reporter.error(location, f"Unknown column '{name}' on {frame}")
|
||||||
|
return UnknownType()
|
||||||
|
return column
|
||||||
|
|
||||||
|
case p.ListExpr(items=indices) if is_list_of_literals(indices) and all(
|
||||||
|
isinstance(index.value, str) for index in indices
|
||||||
|
):
|
||||||
|
names: list[str] = [cast(str, index.value) for index in indices]
|
||||||
|
columns: list[ColumnType] = []
|
||||||
|
for name in names:
|
||||||
|
column: Optional[ColumnType] = FrameManager._get_column(frame, name)
|
||||||
|
if column is None:
|
||||||
|
reporter.error(location, f"Unknown column '{name}' on {frame}")
|
||||||
|
return UnknownType()
|
||||||
|
columns.append(column)
|
||||||
|
return TupleType(items=tuple(columns))
|
||||||
|
|
||||||
|
case _:
|
||||||
|
reporter.error(
|
||||||
|
location, f"Invalid index type {index} on {frame} (access)"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
def groupby_get(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
groupby: FrameGroupBy,
|
||||||
|
index: p.Expr,
|
||||||
|
) -> Type:
|
||||||
|
result: Type = self.get(reporter, location, groupby.frame, index)
|
||||||
|
match result:
|
||||||
|
case ColumnType():
|
||||||
|
result = ColumnGroupBy(column=result)
|
||||||
|
case TupleType(items=columns):
|
||||||
|
result = TupleType(
|
||||||
|
items=tuple(
|
||||||
|
ColumnGroupBy(column=cast(ColumnType, column))
|
||||||
|
for column in columns
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_column(
|
||||||
|
cls, frame: DataFrameType, name: str, column: ColumnType
|
||||||
|
) -> DataFrameType:
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
index: int = len(frame.columns)
|
||||||
|
replace: bool = False
|
||||||
|
for i, col in enumerate(frame.columns):
|
||||||
|
if col.name == name:
|
||||||
|
index = i
|
||||||
|
replace = True
|
||||||
|
# TODO: check column type here to prevent changing it
|
||||||
|
new_columns.append(col)
|
||||||
|
|
||||||
|
new_col: DataFrameType.Column = DataFrameType.Column(
|
||||||
|
index=index,
|
||||||
|
name=name,
|
||||||
|
type=column,
|
||||||
|
)
|
||||||
|
if replace:
|
||||||
|
new_columns[index] = new_col
|
||||||
|
else:
|
||||||
|
new_columns.append(new_col)
|
||||||
|
|
||||||
|
return DataFrameType(columns=new_columns)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_columns(
|
||||||
|
cls, frame: DataFrameType, names: list[str], columns: list[ColumnType]
|
||||||
|
) -> DataFrameType:
|
||||||
|
for name, col in zip(names, columns):
|
||||||
|
frame = cls._set_column(frame, name, col)
|
||||||
|
return frame
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column(cls, frame: DataFrameType, name: str) -> Optional[ColumnType]:
|
||||||
|
for col in frame.columns:
|
||||||
|
if col.name == name:
|
||||||
|
return col.type
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_columns(
|
||||||
|
cls, frame: DataFrameType, names: list[str]
|
||||||
|
) -> list[Optional[ColumnType]]:
|
||||||
|
return [cls._get_column(frame, name) for name in names]
|
||||||
|
|
||||||
|
def call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
location: Location,
|
||||||
|
call_expr: p.Expr,
|
||||||
|
frame: DataFrameType,
|
||||||
|
frame_expr: p.Expr,
|
||||||
|
positional: list[TypedExpr],
|
||||||
|
keywords: dict[str, TypedExpr],
|
||||||
|
) -> Type:
|
||||||
|
call: Call = Call(
|
||||||
|
location=location,
|
||||||
|
call_expr=call_expr,
|
||||||
|
frame=frame,
|
||||||
|
frame_expr=frame_expr,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return self.method_resolver.call(method, call)
|
||||||
|
|
||||||
|
def groupby_call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
location: Location,
|
||||||
|
call_expr: p.Expr,
|
||||||
|
groupby: FrameGroupBy,
|
||||||
|
groupby_expr: p.Expr,
|
||||||
|
positional: list[TypedExpr],
|
||||||
|
keywords: dict[str, TypedExpr],
|
||||||
|
) -> Type:
|
||||||
|
call: GroupByCall = GroupByCall(
|
||||||
|
location=location,
|
||||||
|
call_expr=call_expr,
|
||||||
|
groupby=groupby,
|
||||||
|
groupby_expr=groupby_expr,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return self.groupby_method_resolver.call(method, call)
|
||||||
|
|
||||||
|
def get_attribute(self, frame: DataFrameType, name: str) -> Optional[Type]:
|
||||||
|
types: TypesRegistry = self.typer.types
|
||||||
|
match name:
|
||||||
|
case "ndim" | "size":
|
||||||
|
return types.get_type("int")
|
||||||
|
|
||||||
|
case "shape":
|
||||||
|
return types.tuple_of("int", "int")
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return None
|
||||||
487
midas/checker/frames/frame_methods.py
Normal file
487
midas/checker/frames/frame_methods.py
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.dispatcher import CallResult
|
||||||
|
from midas.checker.frames.utils import MethodRegistry, method
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
FrameGroupBy,
|
||||||
|
Function,
|
||||||
|
OverloadedFunction,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
unfold_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
location: Location
|
||||||
|
call_expr: p.Expr
|
||||||
|
frame: DataFrameType
|
||||||
|
frame_expr: p.Expr
|
||||||
|
positional: list[TypedExpr]
|
||||||
|
keywords: dict[str, TypedExpr]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subject(self) -> TypedExpr:
|
||||||
|
return (self.frame_expr, self.frame)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameMethodRegistry(MethodRegistry[Call]):
|
||||||
|
def _get_method_result(
|
||||||
|
self,
|
||||||
|
call: Call,
|
||||||
|
column1: ColumnType,
|
||||||
|
column2: ColumnType,
|
||||||
|
method: str,
|
||||||
|
) -> ColumnType:
|
||||||
|
"""Get the result of calling a method on a column, passing a second
|
||||||
|
|
||||||
|
This function delegates to the main typer the resolution of the method
|
||||||
|
member, as well as computing the result type. Because we don't have any
|
||||||
|
AST expression for the individual columns, the frame expressions are
|
||||||
|
used instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
column1 (ColumnType): the first column, i.e. left operand
|
||||||
|
column2 (ColumnType): the second column, i.e. right operand
|
||||||
|
method (str): the method name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ColumnType: the resulting column.
|
||||||
|
If the operation is invalid / doesn't exist,
|
||||||
|
`ColumnType(type=UnknownType())` is returned
|
||||||
|
"""
|
||||||
|
|
||||||
|
result: Type = self.typer.result_of_binary_op(
|
||||||
|
location=call.location,
|
||||||
|
expr=call.call_expr,
|
||||||
|
left=(call.frame_expr, column1),
|
||||||
|
right=(call.positional[0][0], column2),
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(result, ColumnType):
|
||||||
|
return ColumnType(type=UnknownType())
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _element_binary_op(self, call: Call, method: str) -> DataFrameType:
|
||||||
|
"""Compute the result of an element-wise binary operation
|
||||||
|
|
||||||
|
This function delegates to the matching columns for computing resulting
|
||||||
|
types. Any column only present in one of the frames is forwarded as a
|
||||||
|
generic `ColumnType(type=UnknownType())`. Columns only in the second
|
||||||
|
frame are append at the end of the schema.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
method (str): the method name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrameType: the resulting frame type
|
||||||
|
"""
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
|
||||||
|
by_name: dict[str, DataFrameType.Column] = {}
|
||||||
|
frame2: Optional[DataFrameType] = None
|
||||||
|
# Get map of operand's columns by name, if there is at least 1 operand, which is a dataframe
|
||||||
|
if len(call.positional) != 0:
|
||||||
|
operand: TypedExpr = call.positional[0]
|
||||||
|
unfolded_other: Type = unfold_type(operand[1])
|
||||||
|
if isinstance(unfolded_other, DataFrameType):
|
||||||
|
frame2 = unfolded_other
|
||||||
|
by_name = {
|
||||||
|
col.name: col for col in frame2.columns if col.name is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compute new schema:
|
||||||
|
# Step 1: for all columns in frame1:
|
||||||
|
# - if present in frame2 -> delegate operation to columns
|
||||||
|
# - if not -> add to schema as unknown
|
||||||
|
in_frame1: set[str] = set()
|
||||||
|
for column in call.frame.columns:
|
||||||
|
if column.name is not None:
|
||||||
|
in_frame1.add(column.name)
|
||||||
|
|
||||||
|
col_type1: ColumnType = column.type
|
||||||
|
col_type: ColumnType = ColumnType(type=UnknownType())
|
||||||
|
if column.name in by_name:
|
||||||
|
column2 = by_name[column.name]
|
||||||
|
col_type2: ColumnType = column2.type
|
||||||
|
|
||||||
|
col_type = self._get_method_result(call, col_type1, col_type2, method)
|
||||||
|
|
||||||
|
new_column = DataFrameType.Column(
|
||||||
|
index=column.index,
|
||||||
|
name=column.name,
|
||||||
|
type=col_type,
|
||||||
|
)
|
||||||
|
new_columns.append(new_column)
|
||||||
|
|
||||||
|
# Step 2: for all columns in frame2
|
||||||
|
# - if not in frame1 -> add to schema as unknown
|
||||||
|
if frame2 is not None:
|
||||||
|
for column in frame2.columns:
|
||||||
|
if column.name in in_frame1:
|
||||||
|
continue
|
||||||
|
new_columns.append(
|
||||||
|
DataFrameType.Column(
|
||||||
|
index=len(new_columns),
|
||||||
|
name=column.name,
|
||||||
|
type=ColumnType(type=UnknownType()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return DataFrameType(columns=new_columns)
|
||||||
|
|
||||||
|
def _element_wise(self, call: Call, method: str) -> Type:
|
||||||
|
# TODO: support scalar, sequence, Series, dict operand
|
||||||
|
# Build signature with new schema and generic operand
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="other",
|
||||||
|
type=DataFrameType(columns=[]),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=self._element_binary_op(call, method),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map arguments and compute result type
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
if result.is_valid:
|
||||||
|
self._assert_same_length(
|
||||||
|
call.call_expr, call.frame_expr, call.positional[0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method("add", "__add__")
|
||||||
|
def add(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__add__")
|
||||||
|
|
||||||
|
@method("sub", "__sub__")
|
||||||
|
def sub(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__sub__")
|
||||||
|
|
||||||
|
@method("mul", "__mul__")
|
||||||
|
def mul(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__mul__")
|
||||||
|
|
||||||
|
@method("div", "truediv", "__truediv__")
|
||||||
|
def truediv(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__truediv__")
|
||||||
|
|
||||||
|
@method("floordiv", "__floordiv__")
|
||||||
|
def floordiv(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__floordiv__")
|
||||||
|
|
||||||
|
@method("mod", "__mod__")
|
||||||
|
def mod(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__mod__")
|
||||||
|
|
||||||
|
@method("pow", "__pow__")
|
||||||
|
def pow(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__pow__")
|
||||||
|
|
||||||
|
@method("lt", "__lt__")
|
||||||
|
def lt(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__lt__")
|
||||||
|
|
||||||
|
@method("gt", "__gt__")
|
||||||
|
def gt(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__gt__")
|
||||||
|
|
||||||
|
@method("le", "__le__")
|
||||||
|
def le(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__le__")
|
||||||
|
|
||||||
|
@method("ge", "__ge__")
|
||||||
|
def ge(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__ge__")
|
||||||
|
|
||||||
|
@method("ne", "__ne__")
|
||||||
|
def ne(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__ne__")
|
||||||
|
|
||||||
|
@method("eq", "__eq__")
|
||||||
|
def eq(self, call: Call) -> Type:
|
||||||
|
return self._element_wise(call, "__eq__")
|
||||||
|
|
||||||
|
def _aggregate(self, call: Call, kwargs: list[Function.Argument] = []) -> Type:
|
||||||
|
with_axis = Function(
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
*kwargs,
|
||||||
|
],
|
||||||
|
returns=ColumnType(type=TopType()),
|
||||||
|
)
|
||||||
|
without_axis = Function(
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=self.types.get_type("None"),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
*kwargs,
|
||||||
|
],
|
||||||
|
returns=TopType(),
|
||||||
|
)
|
||||||
|
overload = OverloadedFunction(
|
||||||
|
overloads=[
|
||||||
|
with_axis,
|
||||||
|
without_axis,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=overload,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method("kurtosis", "kurt")
|
||||||
|
def kurtosis(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def max(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mode(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method("product", "prod")
|
||||||
|
def product(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="ddof",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="var",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def head(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="n",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=call.frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def tail(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="n",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=call.frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def groupby(self, call: Call) -> Type:
|
||||||
|
bool_: Type = self.types.get_type("bool")
|
||||||
|
function: Function = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="by",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=1,
|
||||||
|
name="level",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=2,
|
||||||
|
name="as_index",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=3,
|
||||||
|
name="sort",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=4,
|
||||||
|
name="group_keys",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=5,
|
||||||
|
name="observed",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Argument(
|
||||||
|
pos=6,
|
||||||
|
name="dropna",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=FrameGroupBy(frame=call.frame),
|
||||||
|
)
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=function,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def _assert_same_length(self, call_expr: p.Expr, frame1: p.Expr, frame2: p.Expr):
|
||||||
|
func_name: str = "__midas_frame_same_length__"
|
||||||
|
|
||||||
|
# Efficiently compute length
|
||||||
|
# https://stackoverflow.com/a/15943975/11109181
|
||||||
|
def len_of_df(df: ast.expr) -> ast.expr:
|
||||||
|
return ast.Call(
|
||||||
|
func=ast.Name(id="len"),
|
||||||
|
args=[
|
||||||
|
ast.Attribute(
|
||||||
|
value=df,
|
||||||
|
attr="index",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
keywords=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertions.define(
|
||||||
|
func_name,
|
||||||
|
ast.FunctionDef(
|
||||||
|
name=func_name,
|
||||||
|
args=ast.arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
ast.arg(arg="frame1"),
|
||||||
|
ast.arg(arg="frame2"),
|
||||||
|
],
|
||||||
|
kwonlyargs=[],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
),
|
||||||
|
body=[
|
||||||
|
ast.Return(
|
||||||
|
value=ast.Compare(
|
||||||
|
left=len_of_df(ast.Name(id="frame1")),
|
||||||
|
ops=[ast.Eq()],
|
||||||
|
comparators=[len_of_df(ast.Name(id="frame2"))],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
decorator_list=[],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertions.add(
|
||||||
|
bound_expr=call_expr,
|
||||||
|
inputs=[frame1, frame2],
|
||||||
|
builder=lambda f1, f2: ast.Call(
|
||||||
|
func=ast.Name(id=func_name),
|
||||||
|
args=[f1, f2],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
message="DataFrames must have the same length",
|
||||||
|
)
|
||||||
100
midas/checker/frames/utils.py
Normal file
100
midas/checker/frames/utils.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Generic,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
Self,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.dispatcher import CallDispatcher
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
from midas.checker.types import Type, UnknownType
|
||||||
|
from midas.generator.collector import AssertionCollector
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
class _MethodRegistryMeta(type):
|
||||||
|
_methods: dict[str, Callable[..., Type]] = {}
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
bases: tuple[type, ...],
|
||||||
|
namespace: dict[str, Any],
|
||||||
|
):
|
||||||
|
new_class = super().__new__(cls, name, bases, namespace)
|
||||||
|
new_class._methods = {}
|
||||||
|
for attr in namespace.values():
|
||||||
|
if callable(attr) and hasattr(attr, "__method_names__"):
|
||||||
|
for name in attr.__method_names__: # type: ignore
|
||||||
|
new_class._methods[name] = attr # type: ignore
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
class MethodCall(Protocol):
|
||||||
|
@property
|
||||||
|
def location(self) -> Location: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def call_expr(self) -> p.Expr: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subject(self) -> TypedExpr: ...
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=MethodCall)
|
||||||
|
|
||||||
|
|
||||||
|
class MethodRegistry(Generic[T], metaclass=_MethodRegistryMeta):
|
||||||
|
def __init__(self, typer: PythonTyper) -> None:
|
||||||
|
self.typer: PythonTyper = typer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reporter(self) -> FileReporter:
|
||||||
|
return self.typer.reporter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def types(self) -> TypesRegistry:
|
||||||
|
return self.typer.types
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dispatcher(self) -> CallDispatcher[p.Expr]:
|
||||||
|
return self.typer.dispatcher
|
||||||
|
|
||||||
|
@property
|
||||||
|
def assertions(self) -> AssertionCollector:
|
||||||
|
return self.typer.assertions
|
||||||
|
|
||||||
|
def call(self, method: str, call: T) -> Type:
|
||||||
|
func: Optional[Callable[[Self, T], Type]] = self._methods.get(method)
|
||||||
|
if func is None:
|
||||||
|
self.reporter.warning(
|
||||||
|
call.location, f"Unknown method {method} on {call.subject[1]}"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
return func(self, call)
|
||||||
|
|
||||||
|
|
||||||
|
_Self = TypeVar("_Self", bound=MethodRegistry[Any])
|
||||||
|
Method = Callable[[_Self, T], Type]
|
||||||
|
|
||||||
|
|
||||||
|
def method(*names: str) -> Callable[[Method[_Self, T]], Method[_Self, T]]:
|
||||||
|
def wrapper(func: Method[_Self, T]) -> Method[_Self, T]:
|
||||||
|
names_: tuple[str, ...] = names
|
||||||
|
if len(names_) == 0:
|
||||||
|
names_ = (func.__name__,)
|
||||||
|
setattr(func, "__method_names__", names_)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
437
midas/checker/midas.py
Normal file
437
midas/checker/midas.py
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.builtins import define_builtins
|
||||||
|
from midas.checker.dispatcher import CallDispatcher, CallResult
|
||||||
|
from midas.checker.environment import Environment
|
||||||
|
from midas.checker.operators import MIDAS_BINARY_METHODS, MIDAS_UNARY_METHODS
|
||||||
|
from midas.checker.preamble import Preamble
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter, Reporter
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnType,
|
||||||
|
ComplexType,
|
||||||
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
|
ExtensionType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
Predicate,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
from midas.checker.variance import VarianceInferrer
|
||||||
|
from midas.lexer.midas import MidasLexer
|
||||||
|
from midas.lexer.token import Token
|
||||||
|
from midas.parser.midas import MidasParser
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TypedParamSpec:
|
||||||
|
pos: list[Function.Argument]
|
||||||
|
mixed: list[Function.Argument]
|
||||||
|
kw: list[Function.Argument]
|
||||||
|
|
||||||
|
|
||||||
|
class ReturnException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class MappedArgument:
|
||||||
|
expr: m.Expr
|
||||||
|
type: Type
|
||||||
|
argument: Function.Argument
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class OverloadCandidate:
|
||||||
|
function: Function
|
||||||
|
mapped: list[MappedArgument]
|
||||||
|
|
||||||
|
|
||||||
|
class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type]):
|
||||||
|
"""A resolver which evaluates Midas type definitions and build a registry"""
|
||||||
|
|
||||||
|
def __init__(self, types: TypesRegistry, reporter: Reporter) -> None:
|
||||||
|
self.logger: logging.Logger = logging.getLogger("MidasTyper")
|
||||||
|
self.reporter: FileReporter = reporter.for_file(None)
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.dispatcher: CallDispatcher[m.Expr] = CallDispatcher[m.Expr](
|
||||||
|
self.types, self.reporter
|
||||||
|
)
|
||||||
|
|
||||||
|
self._local_variables: dict[str, TypeVar] = {}
|
||||||
|
|
||||||
|
self._predicate_params: dict[str, Type] = {}
|
||||||
|
|
||||||
|
self._current_name: Optional[str] = None
|
||||||
|
|
||||||
|
define_builtins(self.types)
|
||||||
|
builtins_path: Path = (Path(__file__).parent / "builtins.midas").resolve()
|
||||||
|
self.process(builtins_path.read_text(), str(builtins_path))
|
||||||
|
|
||||||
|
self._bool: Type = self.get_type("bool")
|
||||||
|
|
||||||
|
self._preamble: Environment = Preamble(self.types)
|
||||||
|
|
||||||
|
def set_reporter(self, reporter: FileReporter):
|
||||||
|
self.reporter = reporter
|
||||||
|
self.dispatcher.set_reporter(reporter)
|
||||||
|
|
||||||
|
def process(self, source: str, path: Optional[str]):
|
||||||
|
reporter: FileReporter = self.reporter.for_file(path)
|
||||||
|
self.set_reporter(reporter)
|
||||||
|
|
||||||
|
lexer: MidasLexer = MidasLexer(source)
|
||||||
|
tokens: list[Token] = lexer.process()
|
||||||
|
parser: MidasParser = MidasParser(tokens)
|
||||||
|
stmts: list[m.Stmt] = parser.parse()
|
||||||
|
for error in parser.errors:
|
||||||
|
self.reporter.error(error.token.get_location(), error.message)
|
||||||
|
self.resolve(stmts)
|
||||||
|
|
||||||
|
def type_of(self, expr: m.Expr) -> Type:
|
||||||
|
type: Type = expr.accept(self)
|
||||||
|
return type
|
||||||
|
|
||||||
|
def get_type(self, name: str) -> Type:
|
||||||
|
"""Get a type from its name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NameError: if the type is not defined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type
|
||||||
|
"""
|
||||||
|
if name in self._local_variables:
|
||||||
|
return self._local_variables[name]
|
||||||
|
return self.types.get_type(name)
|
||||||
|
|
||||||
|
def get_variable(self, name: str) -> Type:
|
||||||
|
if name in self._predicate_params:
|
||||||
|
return self._predicate_params[name]
|
||||||
|
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
||||||
|
if predicate is not None:
|
||||||
|
return predicate.type
|
||||||
|
|
||||||
|
global_: Optional[Type] = self._preamble.get(name)
|
||||||
|
if global_ is not None:
|
||||||
|
return global_
|
||||||
|
|
||||||
|
raise NameError(f"Unknown variable '{name}'")
|
||||||
|
|
||||||
|
def resolve(self, stmts: list[m.Stmt]):
|
||||||
|
"""Process a sequence of statements
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stmts (list[m.Stmt]): the statements
|
||||||
|
"""
|
||||||
|
for stmt in stmts:
|
||||||
|
stmt.accept(self)
|
||||||
|
|
||||||
|
for name, type in self.types._types.items():
|
||||||
|
if isinstance(type, GenericType):
|
||||||
|
inferrer = VarianceInferrer(self.types)
|
||||||
|
self.types._types[name] = inferrer.infer(type)
|
||||||
|
|
||||||
|
def assert_bool(self, expr: m.Expr):
|
||||||
|
type: Type = self.type_of(expr)
|
||||||
|
if not self.types.is_subtype(type, self._bool):
|
||||||
|
self.reporter.error(expr.location, f"Must be a boolean but is {type}")
|
||||||
|
|
||||||
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||||
|
name: str = stmt.name.lexeme
|
||||||
|
self._current_name = name
|
||||||
|
params: list[TypeVar] = self._resolve_type_params(stmt.params)
|
||||||
|
|
||||||
|
type: Type = stmt.type.accept(self)
|
||||||
|
if len(params) != 0:
|
||||||
|
type = GenericType(name=name, params=params, body=type)
|
||||||
|
else:
|
||||||
|
type = DerivedType(name=name, type=type)
|
||||||
|
self.types.define_type(name, type)
|
||||||
|
self._local_variables.clear()
|
||||||
|
self._current_name = None
|
||||||
|
|
||||||
|
def visit_alias_stmt(self, stmt: m.AliasStmt) -> None:
|
||||||
|
name: str = stmt.name.lexeme
|
||||||
|
self._current_name = name
|
||||||
|
type: Type = stmt.type.accept(self)
|
||||||
|
self.types.define_type(name, type)
|
||||||
|
self._current_name = None
|
||||||
|
|
||||||
|
def visit_member_stmt(self, stmt: m.MemberStmt) -> None: ...
|
||||||
|
|
||||||
|
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||||
|
self._resolve_type_params(stmt.params)
|
||||||
|
base_name: str = stmt.name.lexeme
|
||||||
|
try:
|
||||||
|
_ = self.get_type(base_name)
|
||||||
|
except NameError:
|
||||||
|
self.reporter.error(stmt.name.get_location(), f"Unknown type '{base_name}'")
|
||||||
|
|
||||||
|
for member in stmt.members:
|
||||||
|
member_type: Type = member.type.accept(self)
|
||||||
|
self.types.define_member(
|
||||||
|
base_name,
|
||||||
|
member.name.lexeme,
|
||||||
|
member_type,
|
||||||
|
member.kind,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||||
|
for spec in stmt.params:
|
||||||
|
for param in spec.mixed:
|
||||||
|
assert param.name is not None
|
||||||
|
self._predicate_params[param.name.lexeme] = param.type.accept(self)
|
||||||
|
|
||||||
|
type: Type = self.type_of(stmt.body)
|
||||||
|
params: list[TypedParamSpec] = [
|
||||||
|
self._visit_param_spec(spec) for spec in stmt.params
|
||||||
|
]
|
||||||
|
|
||||||
|
if not self._is_valid_predicate(type):
|
||||||
|
self.reporter.error(
|
||||||
|
stmt.body.location,
|
||||||
|
f"Predicate function body must evaluate to a boolean, got {type}",
|
||||||
|
)
|
||||||
|
if len(params) != 0:
|
||||||
|
type = self._bool
|
||||||
|
for spec in reversed(params):
|
||||||
|
type = Function(
|
||||||
|
pos_args=spec.pos,
|
||||||
|
args=spec.mixed,
|
||||||
|
kw_args=spec.kw,
|
||||||
|
returns=type,
|
||||||
|
)
|
||||||
|
self._predicate_params = {}
|
||||||
|
self.types.define_predicate(
|
||||||
|
stmt.name.lexeme,
|
||||||
|
Predicate(
|
||||||
|
type=type,
|
||||||
|
body=stmt.body,
|
||||||
|
alias=len(params) == 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_valid_predicate(self, body: Type) -> bool:
|
||||||
|
match body:
|
||||||
|
case Function(returns=returns):
|
||||||
|
return self._is_valid_predicate(returns)
|
||||||
|
case _ if self.types.is_subtype(body, self._bool):
|
||||||
|
return True
|
||||||
|
case _:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: m.LogicalExpr) -> Type:
|
||||||
|
self.assert_bool(expr.left)
|
||||||
|
self.assert_bool(expr.right)
|
||||||
|
return self._bool
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: m.BinaryExpr) -> Type:
|
||||||
|
method: Optional[str] = MIDAS_BINARY_METHODS.get(expr.operator.type)
|
||||||
|
if method is None:
|
||||||
|
self.logger.warning(f"Unsupported operator {expr.operator.lexeme}")
|
||||||
|
self.reporter.warning(
|
||||||
|
expr.location, f"Unsupported operator {expr.operator.lexeme}"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
return self._visit_binary_expr(expr.location, expr.left, expr.right, method)
|
||||||
|
|
||||||
|
def _visit_binary_expr(
|
||||||
|
self, location: Location, left_expr: m.Expr, right_expr: m.Expr, method: str
|
||||||
|
) -> Type:
|
||||||
|
left: Type = self.type_of(left_expr)
|
||||||
|
right: Type = self.type_of(right_expr)
|
||||||
|
|
||||||
|
operation: Optional[Type] = self.types.lookup_member(left, method)
|
||||||
|
if operation is None:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Undefined operation {method} between {left} and {right}",
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=location,
|
||||||
|
callee=operation,
|
||||||
|
positional=[(right_expr, right)],
|
||||||
|
keywords={},
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: m.UnaryExpr) -> Type:
|
||||||
|
method: Optional[str] = MIDAS_UNARY_METHODS.get(expr.operator.type)
|
||||||
|
if method is None:
|
||||||
|
self.logger.warning(f"Unsupported operator {expr.operator.lexeme}")
|
||||||
|
self.reporter.warning(
|
||||||
|
expr.location, f"Unsupported operator {expr.operator.lexeme}"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
operand: Type = self.type_of(expr.right)
|
||||||
|
operation: Optional[Type] = self.types.lookup_member(operand, method)
|
||||||
|
if operation is None:
|
||||||
|
self.reporter.error(
|
||||||
|
expr.location,
|
||||||
|
f"Undefined operation {method} for {operand}",
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=expr.location,
|
||||||
|
callee=operation,
|
||||||
|
positional=[],
|
||||||
|
keywords={},
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> Type:
|
||||||
|
callee: Type = expr.callee.accept(self)
|
||||||
|
positional: list[tuple[m.Expr, Type]] = [
|
||||||
|
(arg, self.type_of(arg)) for arg in expr.arguments
|
||||||
|
]
|
||||||
|
keywords: dict[str, tuple[m.Expr, Type]] = {
|
||||||
|
name: (arg, self.type_of(arg)) for name, arg in expr.keywords.items()
|
||||||
|
}
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=expr.location,
|
||||||
|
callee=callee,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def visit_get_expr(self, expr: m.GetExpr) -> Type:
|
||||||
|
object: Type = expr.expr.accept(self)
|
||||||
|
member: Optional[Type] = self.types.lookup_member(object, expr.name.lexeme)
|
||||||
|
if member is None:
|
||||||
|
self.reporter.error(
|
||||||
|
expr.location, f"Unknown member '{expr.name.lexeme}' of {object}"
|
||||||
|
)
|
||||||
|
return UnknownType()
|
||||||
|
return member
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: m.VariableExpr) -> Type:
|
||||||
|
return self.get_variable(expr.name.lexeme)
|
||||||
|
|
||||||
|
def visit_grouping_expr(self, expr: m.GroupingExpr) -> Type:
|
||||||
|
return expr.expr.accept(self)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: m.LiteralExpr) -> Type:
|
||||||
|
match expr.value:
|
||||||
|
case bool(): # Must be before int
|
||||||
|
return self.types.get_type("bool")
|
||||||
|
case int():
|
||||||
|
return self.types.get_type("int")
|
||||||
|
case float():
|
||||||
|
return self.types.get_type("float")
|
||||||
|
case str():
|
||||||
|
return self.types.get_type("str")
|
||||||
|
case _:
|
||||||
|
self.reporter.warning(expr.location, f"Unknown literal {expr}")
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Type:
|
||||||
|
return self.get_variable("_")
|
||||||
|
|
||||||
|
def visit_named_type(self, type: m.NamedType) -> Type:
|
||||||
|
name: str = type.name.lexeme
|
||||||
|
try:
|
||||||
|
return self.get_type(name)
|
||||||
|
except NameError:
|
||||||
|
msg: str = f"Undefined type {name}"
|
||||||
|
if self._current_name == name:
|
||||||
|
msg += ". Recursive types are not supported, use an extend block"
|
||||||
|
self.reporter.error(type.name.get_location(), msg)
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
def visit_generic_type(self, type: m.GenericType) -> Type:
|
||||||
|
type_: Type = type.type.accept(self)
|
||||||
|
args: list[Type] = [arg.accept(self) for arg in type.args]
|
||||||
|
try:
|
||||||
|
return self.types.apply_generic(type_, args)
|
||||||
|
except Exception as e:
|
||||||
|
self.reporter.error(type.location, f"Cannot apply generic type: {e}")
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
def visit_constraint_type(self, type: m.ConstraintType) -> Type:
|
||||||
|
return ConstraintType(
|
||||||
|
type=type.type.accept(self),
|
||||||
|
constraint=type.constraint,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_complex_type(self, type: m.ComplexType) -> ComplexType:
|
||||||
|
return ComplexType(
|
||||||
|
members={
|
||||||
|
member.name.lexeme: member.type.accept(self) for member in type.members
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_extension_type(self, type: m.ExtensionType) -> Type:
|
||||||
|
return ExtensionType(
|
||||||
|
base=type.base.accept(self),
|
||||||
|
extension=self.visit_complex_type(type.extension),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_function_type(self, type: m.FunctionType) -> Type:
|
||||||
|
params: TypedParamSpec = self._visit_param_spec(type.params)
|
||||||
|
return Function(
|
||||||
|
pos_args=params.pos,
|
||||||
|
args=params.mixed,
|
||||||
|
kw_args=params.kw,
|
||||||
|
returns=type.returns.accept(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _visit_param_spec(self, spec: m.ParamSpec) -> TypedParamSpec:
|
||||||
|
n_pos: int = len(spec.pos)
|
||||||
|
n_mixed: int = len(spec.mixed)
|
||||||
|
|
||||||
|
def process_arg(arg: m.FunctionType.Argument, i: int) -> Function.Argument:
|
||||||
|
return Function.Argument(
|
||||||
|
pos=i,
|
||||||
|
name=arg.name.lexeme if arg.name is not None else str(i),
|
||||||
|
type=arg.type.accept(self),
|
||||||
|
required=arg.required,
|
||||||
|
)
|
||||||
|
|
||||||
|
return TypedParamSpec(
|
||||||
|
pos=[process_arg(arg, i) for i, arg in enumerate(spec.pos)],
|
||||||
|
mixed=[process_arg(arg, i + n_pos) for i, arg in enumerate(spec.mixed)],
|
||||||
|
kw=[process_arg(arg, i + n_pos + n_mixed) for i, arg in enumerate(spec.kw)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_frame_type(self, type: m.FrameType) -> Type:
|
||||||
|
def process_column(i: int, col: m.FrameType.Column) -> DataFrameType.Column:
|
||||||
|
return DataFrameType.Column(
|
||||||
|
index=i,
|
||||||
|
name=col.name.lexeme,
|
||||||
|
type=ColumnType(type=col.type.accept(self)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return DataFrameType(
|
||||||
|
columns=[process_column(i, col) for i, col in enumerate(type.columns)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _resolve_type_params(self, params: list[m.TypeParam]):
|
||||||
|
vars: list[TypeVar] = []
|
||||||
|
for param in params:
|
||||||
|
name: str = param.name.lexeme
|
||||||
|
bound: Optional[Type] = None
|
||||||
|
if param.bound is not None:
|
||||||
|
bound = param.bound.accept(self)
|
||||||
|
var = TypeVar(name=name, bound=bound)
|
||||||
|
self._local_variables[name] = var
|
||||||
|
vars.append(var)
|
||||||
|
return vars
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import ast
|
import ast
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
OPERATOR_METHODS: dict[Type[ast.operator], str] = {
|
from midas.lexer.token import TokenType
|
||||||
|
|
||||||
|
PY_OPERATOR_METHODS: dict[Type[ast.operator], str] = {
|
||||||
ast.Add: "__add__",
|
ast.Add: "__add__",
|
||||||
ast.Sub: "__sub__",
|
ast.Sub: "__sub__",
|
||||||
ast.Mult: "__mul__",
|
ast.Mult: "__mul__",
|
||||||
@@ -17,9 +19,9 @@ OPERATOR_METHODS: dict[Type[ast.operator], str] = {
|
|||||||
ast.FloorDiv: "__floordiv__",
|
ast.FloorDiv: "__floordiv__",
|
||||||
}
|
}
|
||||||
|
|
||||||
COMPARATOR_METHODS: dict[Type[ast.cmpop], str] = {
|
PY_COMPARATOR_METHODS: dict[Type[ast.cmpop], str] = {
|
||||||
ast.Eq: "__eq__",
|
ast.Eq: "__eq__",
|
||||||
# ast.NotEq: "__noteq__",
|
ast.NotEq: "__eq__",
|
||||||
ast.Lt: "__lt__",
|
ast.Lt: "__lt__",
|
||||||
ast.LtE: "__le__",
|
ast.LtE: "__le__",
|
||||||
ast.Gt: "__gt__",
|
ast.Gt: "__gt__",
|
||||||
@@ -29,3 +31,41 @@ COMPARATOR_METHODS: dict[Type[ast.cmpop], str] = {
|
|||||||
# ast.In: "__in__",
|
# ast.In: "__in__",
|
||||||
# ast.NotIn: "__notin__",
|
# ast.NotIn: "__notin__",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PY_UNARY_METHODS: dict[Type[ast.unaryop], str] = {
|
||||||
|
ast.Invert: "__invert__",
|
||||||
|
# ast.Not: "",
|
||||||
|
ast.UAdd: "__pos__",
|
||||||
|
ast.USub: "__neg__",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MIDAS_BINARY_METHODS: dict[TokenType, str] = {
|
||||||
|
TokenType.PLUS: "__add__",
|
||||||
|
TokenType.MINUS: "__sub__",
|
||||||
|
TokenType.STAR: "__mul__",
|
||||||
|
TokenType.SLASH: "__truediv__",
|
||||||
|
# TokenType.MODULO: "__mod__",
|
||||||
|
# TokenType.POW: "__pow__",
|
||||||
|
# ast.BitOr: "__or__",
|
||||||
|
# ast.BitXor: "__xor__",
|
||||||
|
# ast.BitAnd: "__and__",
|
||||||
|
# ast.FloorDiv: "__floordiv__",
|
||||||
|
TokenType.EQUAL_EQUAL: "__eq__",
|
||||||
|
TokenType.BANG_EQUAL: "__eq__",
|
||||||
|
TokenType.LESS: "__lt__",
|
||||||
|
TokenType.LESS_EQUAL: "__le__",
|
||||||
|
TokenType.GREATER: "__gt__",
|
||||||
|
TokenType.GREATER_EQUAL: "__ge__",
|
||||||
|
# ast.Is: "__is__",
|
||||||
|
# ast.IsNot: "__isnot__",
|
||||||
|
# ast.In: "__in__",
|
||||||
|
# ast.NotIn: "__notin__",
|
||||||
|
}
|
||||||
|
|
||||||
|
MIDAS_UNARY_METHODS: dict[TokenType, str] = {
|
||||||
|
# ast.Invert: "__invert__",
|
||||||
|
# ast.Not: "",
|
||||||
|
# TokenType.PLUS: "__pos__",
|
||||||
|
TokenType.MINUS: "__neg__",
|
||||||
|
}
|
||||||
|
|||||||
210
midas/checker/preamble.py
Normal file
210
midas/checker/preamble.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
from midas.checker.environment import Environment
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Param:
|
||||||
|
name: str
|
||||||
|
type: Type
|
||||||
|
required: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class Preamble(Environment):
|
||||||
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._types: TypesRegistry = types
|
||||||
|
self._python_funcs: dict[str, Callable[..., Any]] = {}
|
||||||
|
|
||||||
|
self._def_type_constructor("object", object)
|
||||||
|
self._def_type_constructor("float", float)
|
||||||
|
self._def_type_constructor("int", int)
|
||||||
|
self._def_type_constructor("bool", bool)
|
||||||
|
self._def_type_constructor("str", str)
|
||||||
|
self._def_function(
|
||||||
|
name="list",
|
||||||
|
pos=[Param("object", TopType())],
|
||||||
|
returns=self._list_of(TopType()),
|
||||||
|
py_function=list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: use sink
|
||||||
|
self._def_function(
|
||||||
|
name="print",
|
||||||
|
pos=[Param("object", TopType(), required=False)],
|
||||||
|
returns=UnitType(),
|
||||||
|
py_function=print,
|
||||||
|
)
|
||||||
|
|
||||||
|
map_in = TypeVar(name="T", bound=None)
|
||||||
|
map_out = TypeVar(name="U", bound=None)
|
||||||
|
mapper = self._make_function(
|
||||||
|
name="MapTransform",
|
||||||
|
pos=[Param("v", map_in)],
|
||||||
|
returns=map_out,
|
||||||
|
)
|
||||||
|
self._def_function(
|
||||||
|
name="map",
|
||||||
|
pos=[
|
||||||
|
Param("transform", mapper),
|
||||||
|
Param(
|
||||||
|
"iterable",
|
||||||
|
self._list_of(map_in), # TODO: replace with Iterable[T]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=self._list_of(map_out), # TODO: replace with Iterable[U]
|
||||||
|
type_vars=[map_in, map_out],
|
||||||
|
py_function=map,
|
||||||
|
)
|
||||||
|
self._def_function(
|
||||||
|
name="input",
|
||||||
|
pos=[Param("prompt", TopType(), required=False)],
|
||||||
|
returns=self._types.get_type("str"),
|
||||||
|
)
|
||||||
|
self._def_function(
|
||||||
|
name="len",
|
||||||
|
pos=[Param("object", TopType())],
|
||||||
|
returns=self._types.get_type("int"),
|
||||||
|
)
|
||||||
|
|
||||||
|
T = TypeVar(name="T", bound=None)
|
||||||
|
self._def_overloads(
|
||||||
|
name="max",
|
||||||
|
py_function=max,
|
||||||
|
signatures=[
|
||||||
|
(
|
||||||
|
[Param("arg1", T), Param("arg2", T)],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
T,
|
||||||
|
[T],
|
||||||
|
),
|
||||||
|
([Param("iterable", self._list_of(T))], [], [], T, [T]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self._def_overloads(
|
||||||
|
name="min",
|
||||||
|
py_function=min,
|
||||||
|
signatures=[
|
||||||
|
(
|
||||||
|
[Param("arg1", T), Param("arg2", T)],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
T,
|
||||||
|
[T],
|
||||||
|
),
|
||||||
|
([Param("iterable", self._list_of(T))], [], [], T, [T]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _list_of(self, item_type: str | Type) -> Type:
|
||||||
|
return self._types.list_of(item_type)
|
||||||
|
|
||||||
|
def _def_type_constructor(
|
||||||
|
self, name: str, py_function: Optional[Callable[..., Any]] = None
|
||||||
|
):
|
||||||
|
# TODO: more specific arg types
|
||||||
|
self._def_function(
|
||||||
|
name=name,
|
||||||
|
pos=[Param("object", TopType(), required=False)],
|
||||||
|
returns=self._types.get_type(name),
|
||||||
|
py_function=py_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_function(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
pos: list[Param] = [],
|
||||||
|
mixed: list[Param] = [],
|
||||||
|
kw: list[Param] = [],
|
||||||
|
returns: Type = UnitType(),
|
||||||
|
type_vars: list[TypeVar] = [],
|
||||||
|
) -> Type:
|
||||||
|
def map_args(params: list[Param], offset: int) -> list[Function.Argument]:
|
||||||
|
return [
|
||||||
|
Function.Argument(
|
||||||
|
pos=i + offset,
|
||||||
|
name=param.name,
|
||||||
|
type=param.type,
|
||||||
|
required=param.required,
|
||||||
|
)
|
||||||
|
for i, param in enumerate(params)
|
||||||
|
]
|
||||||
|
|
||||||
|
function = Function(
|
||||||
|
pos_args=map_args(pos, 0),
|
||||||
|
args=map_args(mixed, len(pos)),
|
||||||
|
kw_args=map_args(kw, len(pos) + len(mixed)),
|
||||||
|
returns=returns,
|
||||||
|
)
|
||||||
|
if len(type_vars) != 0:
|
||||||
|
function = GenericType(
|
||||||
|
name=name,
|
||||||
|
params=type_vars,
|
||||||
|
body=function,
|
||||||
|
)
|
||||||
|
return function
|
||||||
|
|
||||||
|
def _def_function(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
pos: list[Param] = [],
|
||||||
|
mixed: list[Param] = [],
|
||||||
|
kw: list[Param] = [],
|
||||||
|
returns: Type = UnitType(),
|
||||||
|
type_vars: list[TypeVar] = [],
|
||||||
|
py_function: Optional[Callable[..., Any]] = None,
|
||||||
|
):
|
||||||
|
function: Type = self._make_function(
|
||||||
|
name=name,
|
||||||
|
pos=pos,
|
||||||
|
mixed=mixed,
|
||||||
|
kw=kw,
|
||||||
|
returns=returns,
|
||||||
|
type_vars=type_vars,
|
||||||
|
)
|
||||||
|
self.define(name, function)
|
||||||
|
if py_function is not None:
|
||||||
|
self._python_funcs[name] = py_function
|
||||||
|
|
||||||
|
def _def_overloads(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
signatures: list[
|
||||||
|
tuple[list[Param], list[Param], list[Param], Type, list[TypeVar]]
|
||||||
|
],
|
||||||
|
py_function: Optional[Callable[..., Any]] = None,
|
||||||
|
):
|
||||||
|
overloads: list[Type] = []
|
||||||
|
for pos, mixed, kw, returns, type_vars in signatures:
|
||||||
|
overloads.append(
|
||||||
|
self._make_function(
|
||||||
|
name=name,
|
||||||
|
pos=pos,
|
||||||
|
mixed=mixed,
|
||||||
|
kw=kw,
|
||||||
|
returns=returns,
|
||||||
|
type_vars=type_vars,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
function: Type = OverloadedFunction(overloads=overloads)
|
||||||
|
self.define(name, function)
|
||||||
|
if py_function is not None:
|
||||||
|
self._python_funcs[name] = py_function
|
||||||
|
|
||||||
|
def get_py_func(self, name: str) -> Optional[Callable[..., Any]]:
|
||||||
|
return self._python_funcs.get(name)
|
||||||
1151
midas/checker/python.py
Normal file
1151
midas/checker/python.py
Normal file
File diff suppressed because it is too large
Load Diff
480
midas/checker/registry.py
Normal file
480
midas/checker/registry.py
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.ast.midas import MemberKind
|
||||||
|
from midas.checker.builtins import BUILTIN_SUBTYPES
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
BaseType,
|
||||||
|
ColumnType,
|
||||||
|
ComplexType,
|
||||||
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
|
ExtensionType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
Predicate,
|
||||||
|
TopType,
|
||||||
|
TupleType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnknownType,
|
||||||
|
Variance,
|
||||||
|
substitute_typevars,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Member:
|
||||||
|
kind: MemberKind
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
|
class TypesRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.logger: logging.Logger = logging.getLogger("TypesRegistry")
|
||||||
|
self._types: dict[str, Type] = {}
|
||||||
|
self._members: dict[str, dict[str, Member]] = {}
|
||||||
|
self._predicates: dict[str, Predicate] = {}
|
||||||
|
|
||||||
|
def get_type(self, name: str) -> Type:
|
||||||
|
"""Get a type from its name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NameError: if the type is not defined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type
|
||||||
|
"""
|
||||||
|
if name in self._types:
|
||||||
|
return self._types[name]
|
||||||
|
raise NameError(f"Undefined type {name}")
|
||||||
|
|
||||||
|
def define_type(self, name: str, type: Type) -> Type:
|
||||||
|
"""Define a type in the registry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
type (Type): the type to define
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if a type is already defined with that name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the defined type
|
||||||
|
"""
|
||||||
|
if name in self._types:
|
||||||
|
raise ValueError(f"Type {name} already defined")
|
||||||
|
self._types[name] = type
|
||||||
|
return type
|
||||||
|
|
||||||
|
def define_member(
|
||||||
|
self,
|
||||||
|
type_name: str,
|
||||||
|
member_name: str,
|
||||||
|
member_type: Type,
|
||||||
|
kind: MemberKind,
|
||||||
|
):
|
||||||
|
members: dict[str, Member] = self._members.setdefault(type_name, {})
|
||||||
|
if member_name in members:
|
||||||
|
current: Member = members[member_name]
|
||||||
|
if current.kind != kind:
|
||||||
|
self.logger.error(
|
||||||
|
f"Member '{member_name}' is already defined as a {current.kind},"
|
||||||
|
+ f" cannot define a {kind} with the same name"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if kind != MemberKind.METHOD:
|
||||||
|
self.logger.error(
|
||||||
|
f"Member '{member_name}' already defined for type {type_name},"
|
||||||
|
+ " only methods can be overloaded"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
combined: Type
|
||||||
|
match current.type:
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
combined = OverloadedFunction(overloads=overloads + [member_type])
|
||||||
|
case _:
|
||||||
|
combined = OverloadedFunction(overloads=[current.type, member_type])
|
||||||
|
members[member_name] = Member(kind=current.kind, type=combined)
|
||||||
|
|
||||||
|
else:
|
||||||
|
members[member_name] = Member(kind=kind, type=member_type)
|
||||||
|
|
||||||
|
def define_predicate(self, name: str, predicate: Predicate):
|
||||||
|
if name in self._predicates:
|
||||||
|
raise ValueError(f"Predicate {name} already defined")
|
||||||
|
self._predicates[name] = predicate
|
||||||
|
|
||||||
|
def is_builtin_subtype(self, name1: str, name2: str) -> bool:
|
||||||
|
subtypes: set[str] = BUILTIN_SUBTYPES.get(name2, set())
|
||||||
|
if name1 in subtypes:
|
||||||
|
return True
|
||||||
|
for subtype in subtypes:
|
||||||
|
if self.is_builtin_subtype(name1, subtype):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_subtype(self, type1: Type, type2: Type) -> bool:
|
||||||
|
"""Check whether `type1` is a subtype of `type2`
|
||||||
|
|
||||||
|
For more details on the rules checked here, see TAPL Chap. 15-16-17
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type1 (Type): the potential subtype
|
||||||
|
type2 (Type): the potential supertype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether `type1` is a subtype of `type2`
|
||||||
|
"""
|
||||||
|
|
||||||
|
if type1 == type2:
|
||||||
|
return True
|
||||||
|
|
||||||
|
match (type1, type2):
|
||||||
|
case (_, TopType()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (_, UnknownType()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (TypeVar(bound=bound), _):
|
||||||
|
if bound is None:
|
||||||
|
return False
|
||||||
|
return self.is_subtype(bound, type2)
|
||||||
|
|
||||||
|
case (_, TypeVar(bound=bound)):
|
||||||
|
if bound is None:
|
||||||
|
return True
|
||||||
|
return self.is_subtype(type1, bound)
|
||||||
|
|
||||||
|
case (DerivedType(type=base1), _):
|
||||||
|
return self.is_subtype(base1, type2)
|
||||||
|
|
||||||
|
case (BaseType(name=name1), BaseType(name=name2)):
|
||||||
|
return self.is_builtin_subtype(name1, name2)
|
||||||
|
|
||||||
|
case (ComplexType(properties=props1), ComplexType(properties=props2)):
|
||||||
|
for k, t in props2.items():
|
||||||
|
if k not in props1:
|
||||||
|
return False
|
||||||
|
if not self.is_subtype(props1[k], t):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (DataFrameType(columns=columns1), DataFrameType(columns=columns2)):
|
||||||
|
# TODO: check order?
|
||||||
|
by_name1: dict[str, DataFrameType.Column] = {
|
||||||
|
col.name: col for col in columns1 if col.name is not None
|
||||||
|
}
|
||||||
|
for col2 in columns2:
|
||||||
|
if col2.name not in by_name1:
|
||||||
|
return False
|
||||||
|
if not self.is_subtype(by_name1[col2.name].type, col2.type):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (ColumnType(type=inner1), ColumnType(type=inner2)):
|
||||||
|
# TODO: invariant, replace ColumnType with simple GenericType
|
||||||
|
if not self.are_equivalent(inner1, inner2):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (Function(), Function()):
|
||||||
|
return self.is_func_subtype(type1, type2)
|
||||||
|
|
||||||
|
case (ConstraintType(type=base1), _):
|
||||||
|
return self.is_subtype(base1, type2)
|
||||||
|
|
||||||
|
case (
|
||||||
|
AppliedType(name=name1, args=args1),
|
||||||
|
AppliedType(name=name2, args=args2),
|
||||||
|
) if (
|
||||||
|
name1 == name2
|
||||||
|
):
|
||||||
|
generic: Type = self.get_type(name1)
|
||||||
|
assert isinstance(generic, GenericType)
|
||||||
|
for param, arg1, arg2 in zip(generic.params, args1, args2):
|
||||||
|
variance: Variance = param.variance
|
||||||
|
if variance in {Variance.INVARIANT, Variance.COVARIANT}:
|
||||||
|
if not self.is_subtype(arg1, arg2):
|
||||||
|
return False
|
||||||
|
if variance in {Variance.INVARIANT, Variance.CONTRAVARIANT}:
|
||||||
|
if not self.is_subtype(arg2, arg1):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# TODO: verify legitimacy
|
||||||
|
case (AppliedType(body=body), _):
|
||||||
|
return self.is_subtype(body, type2)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def are_equivalent(self, type1: Type, type2: Type) -> bool:
|
||||||
|
return self.is_subtype(type1, type2) and self.is_subtype(type2, type1)
|
||||||
|
|
||||||
|
# TODO: verify the logic in here
|
||||||
|
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
||||||
|
"""Check whether a function is a subtype of another
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func1 (Function): the potential function subtype
|
||||||
|
func2 (Function): the potential function supertype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether `func1` is a subtype of `func2`
|
||||||
|
"""
|
||||||
|
if not self.is_subtype(func1.returns, func2.returns):
|
||||||
|
return False
|
||||||
|
|
||||||
|
pos1: list[Function.Argument] = func1.pos_args
|
||||||
|
mixed1: list[Function.Argument] = func1.args
|
||||||
|
kw1: dict[str, Function.Argument] = {a.name: a for a in func1.kw_args}
|
||||||
|
pos2: list[Function.Argument] = func2.pos_args
|
||||||
|
mixed2: list[Function.Argument] = func2.args
|
||||||
|
kw2: dict[str, Function.Argument] = {a.name: a for a in func2.kw_args}
|
||||||
|
|
||||||
|
mixed_by_pos: dict[int, Function.Argument] = {arg.pos: arg for arg in mixed2}
|
||||||
|
mixed_by_name: dict[str, Function.Argument] = {arg.name: arg for arg in mixed2}
|
||||||
|
|
||||||
|
def is_arg_subtype(sub: Function.Argument, sup: Function.Argument) -> bool:
|
||||||
|
if not self.is_subtype(sub.type, sup.type):
|
||||||
|
return False
|
||||||
|
if not sup.required and sub.required:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
for arg1 in pos1:
|
||||||
|
arg2: Function.Argument
|
||||||
|
if arg1.pos < len(pos2):
|
||||||
|
arg2 = pos2[arg1.pos]
|
||||||
|
elif arg1.pos in mixed_by_pos:
|
||||||
|
arg2 = mixed_by_pos[arg1.pos]
|
||||||
|
elif not arg1.required:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if not is_arg_subtype(arg2, arg1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for name, arg1 in kw1.items():
|
||||||
|
arg2: Function.Argument
|
||||||
|
if name in kw2:
|
||||||
|
arg2 = kw2[name]
|
||||||
|
elif name in mixed_by_name:
|
||||||
|
arg2 = mixed_by_name[name]
|
||||||
|
elif not arg1.required:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if not is_arg_subtype(arg2, arg1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for arg1 in mixed1:
|
||||||
|
pos_arg2: Optional[Function.Argument] = None
|
||||||
|
kw_arg2: Optional[Function.Argument] = None
|
||||||
|
if arg1.name in kw2:
|
||||||
|
kw_arg2 = kw2[arg1.name]
|
||||||
|
elif arg1.name in mixed_by_name:
|
||||||
|
kw_arg2 = mixed_by_name[arg1.name]
|
||||||
|
if arg1.pos < len(pos2):
|
||||||
|
pos_arg2 = pos2[arg1.pos]
|
||||||
|
elif arg1.pos in mixed_by_pos:
|
||||||
|
pos_arg2 = mixed_by_pos[arg1.pos]
|
||||||
|
|
||||||
|
# No match in func2 and arg is required
|
||||||
|
if pos_arg2 is None and kw_arg2 is None and arg1.required:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Matching keyword argument
|
||||||
|
if kw_arg2 is not None and not is_arg_subtype(kw_arg2, arg1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Matching positional argument
|
||||||
|
if pos_arg2 is not None and not is_arg_subtype(pos_arg2, arg1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
mixed_positions: set[int] = {a.pos for a in mixed1}
|
||||||
|
mixed_names: set[str] = {a.name for a in mixed1}
|
||||||
|
for arg2 in pos2:
|
||||||
|
if not arg2.required:
|
||||||
|
continue
|
||||||
|
if arg2.pos >= len(pos1) and arg2.pos not in mixed_positions:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for name, arg2 in kw2.items():
|
||||||
|
if not arg2.required:
|
||||||
|
continue
|
||||||
|
if name not in kw1 and name not in mixed_names:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for arg2 in mixed2:
|
||||||
|
if arg2.required:
|
||||||
|
continue
|
||||||
|
pos_match: bool = arg2.pos < len(pos1) or arg2.pos in mixed_positions
|
||||||
|
kw_match: bool = arg2.name in kw1 or arg2.name in mixed_names
|
||||||
|
if not pos_match or not kw_match:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply_generic(self, type: Type, args: list[Type]) -> Type:
|
||||||
|
match type:
|
||||||
|
case DerivedType(name=name, type=base):
|
||||||
|
return DerivedType(name=name, type=self.apply_generic(base, args))
|
||||||
|
|
||||||
|
case GenericType(name=name, params=type_vars, body=body):
|
||||||
|
n_args: int = len(args)
|
||||||
|
n_type_vars: int = len(type_vars)
|
||||||
|
if n_args < n_type_vars:
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing type arguments, expected {n_type_vars} but only {n_args} provided"
|
||||||
|
)
|
||||||
|
if n_args > n_type_vars:
|
||||||
|
raise ValueError(
|
||||||
|
f"Too many type arguments, expected {n_type_vars} but {n_args} provided"
|
||||||
|
)
|
||||||
|
substitutions: dict[str, Type] = {}
|
||||||
|
for arg, type_var in zip(args, type_vars):
|
||||||
|
if type_var.bound is not None and not self.is_subtype(
|
||||||
|
arg, type_var.bound
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"Type argument {arg} is not a subtype of {type_var.bound}"
|
||||||
|
)
|
||||||
|
substitutions[type_var.name] = arg
|
||||||
|
return AppliedType(
|
||||||
|
name=name,
|
||||||
|
args=args,
|
||||||
|
body=substitute_typevars(body, substitutions),
|
||||||
|
)
|
||||||
|
|
||||||
|
case BaseType(name="tuple"):
|
||||||
|
return TupleType(items=tuple(args))
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"{type} is not a generic type")
|
||||||
|
|
||||||
|
def reduce_types(self, types: list[Type]) -> list[Type]:
|
||||||
|
"""Reduce a list of types to remove subtypes and only keep the highest types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
types (list[Type]): the types to reduce
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Type]: the reduced list of types
|
||||||
|
"""
|
||||||
|
|
||||||
|
reduced: bool = True
|
||||||
|
keep: list[int] = list(range(len(types)))
|
||||||
|
while reduced:
|
||||||
|
reduced = False
|
||||||
|
for i, i1 in enumerate(keep):
|
||||||
|
type1: Type = types[i1]
|
||||||
|
for i2 in keep[i + 1 :]:
|
||||||
|
type2 = types[i2]
|
||||||
|
if self.is_subtype(type1, type2):
|
||||||
|
keep.remove(i1)
|
||||||
|
elif self.is_subtype(type2, type1):
|
||||||
|
keep.remove(i2)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
reduced = True
|
||||||
|
break
|
||||||
|
return [types[i] for i in keep]
|
||||||
|
|
||||||
|
def lookup_member(self, type: Type, member_name: str) -> Optional[Type]:
|
||||||
|
match type:
|
||||||
|
case BaseType(name=name):
|
||||||
|
if name in self._members:
|
||||||
|
if member_name in self._members[name]:
|
||||||
|
return self._members[name][member_name].type
|
||||||
|
return None
|
||||||
|
|
||||||
|
case DerivedType(name=name, type=base):
|
||||||
|
if name in self._members:
|
||||||
|
if member_name in self._members[name]:
|
||||||
|
return self._members[name][member_name].type
|
||||||
|
return self.lookup_member(base, member_name)
|
||||||
|
|
||||||
|
case AppliedType(name=name, body=body, args=args):
|
||||||
|
generic: Type = self.get_type(name)
|
||||||
|
|
||||||
|
if not isinstance(generic, GenericType):
|
||||||
|
raise ValueError("AppliedType not derived from a GenericType")
|
||||||
|
|
||||||
|
substitutions = {
|
||||||
|
type_var.name: arg for arg, type_var in zip(args, generic.params)
|
||||||
|
}
|
||||||
|
if name in self._members:
|
||||||
|
if member_name in self._members[name]:
|
||||||
|
member_type: Type = self._members[name][member_name].type
|
||||||
|
return substitute_typevars(member_type, substitutions)
|
||||||
|
|
||||||
|
member_type2: Optional[Type] = self.lookup_member(body, member_name)
|
||||||
|
if member_type2 is not None:
|
||||||
|
member_type2 = substitute_typevars(member_type2, substitutions)
|
||||||
|
return member_type2
|
||||||
|
|
||||||
|
case ComplexType(members=members):
|
||||||
|
if member_name in members:
|
||||||
|
return members[member_name]
|
||||||
|
self.logger.debug(f"No member '{member_name}' in {type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
case ExtensionType(base=base, extension=ComplexType(members=members)):
|
||||||
|
if member_name in members:
|
||||||
|
return members[member_name]
|
||||||
|
self.logger.debug(
|
||||||
|
f"No member '{member_name}' on {type}, looking up in base"
|
||||||
|
)
|
||||||
|
return self.lookup_member(base, member_name)
|
||||||
|
|
||||||
|
case ConstraintType(type=base):
|
||||||
|
return self.lookup_member(base, member_name)
|
||||||
|
|
||||||
|
case TypeVar(bound=bound) if bound is not None:
|
||||||
|
return self.lookup_member(bound, member_name)
|
||||||
|
|
||||||
|
case UnknownType():
|
||||||
|
return UnknownType()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.debug(f"Can't get member on {type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def lookup_predicate(self, name: str) -> Optional[Predicate]:
|
||||||
|
return self._predicates.get(name)
|
||||||
|
|
||||||
|
def _by_name_or_type(self, name_or_type: str | Type) -> Type:
|
||||||
|
if isinstance(name_or_type, str):
|
||||||
|
return self.get_type(name_or_type)
|
||||||
|
return name_or_type
|
||||||
|
|
||||||
|
def list_of(self, item_type: str | Type) -> Type:
|
||||||
|
list_ = self.get_type("list")
|
||||||
|
return self.apply_generic(list_, [self._by_name_or_type(item_type)])
|
||||||
|
|
||||||
|
def tuple_of(self, *item_types: str | Type) -> Type:
|
||||||
|
tuple_ = self.get_type("tuple")
|
||||||
|
return self.apply_generic(
|
||||||
|
tuple_,
|
||||||
|
[self._by_name_or_type(item_type) for item_type in item_types],
|
||||||
|
)
|
||||||
|
|
||||||
|
def dict_of(self, key_type: str | Type, value_type: str | Type) -> Type:
|
||||||
|
dict_ = self.get_type("dict")
|
||||||
|
return self.apply_generic(
|
||||||
|
dict_,
|
||||||
|
[
|
||||||
|
self._by_name_or_type(key_type),
|
||||||
|
self._by_name_or_type(value_type),
|
||||||
|
],
|
||||||
|
)
|
||||||
70
midas/checker/reporter.py
Normal file
70
midas/checker/reporter.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
||||||
|
|
||||||
|
|
||||||
|
class Reporter:
|
||||||
|
def __init__(self):
|
||||||
|
self.diagnostics: list[Diagnostic] = []
|
||||||
|
|
||||||
|
def report(
|
||||||
|
self,
|
||||||
|
path: Optional[str],
|
||||||
|
type: DiagnosticType,
|
||||||
|
location: Location,
|
||||||
|
message: str,
|
||||||
|
):
|
||||||
|
self.diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
file_path=path,
|
||||||
|
location=location,
|
||||||
|
type=type,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def for_file(self, path: Optional[str]) -> FileReporter:
|
||||||
|
return FileReporter(self, path)
|
||||||
|
|
||||||
|
|
||||||
|
class FileReporter:
|
||||||
|
def __init__(self, base_reporter: Reporter, path: Optional[str]) -> None:
|
||||||
|
self.base_reporter: Reporter = base_reporter
|
||||||
|
self.path: Optional[str] = path
|
||||||
|
|
||||||
|
def for_file(self, path: Optional[str]) -> FileReporter:
|
||||||
|
return FileReporter(self.base_reporter, path)
|
||||||
|
|
||||||
|
def report(self, type: DiagnosticType, location: Location, message: str):
|
||||||
|
self.base_reporter.report(self.path, type, location, message)
|
||||||
|
|
||||||
|
def error(self, location: Location, message: str):
|
||||||
|
self.report(
|
||||||
|
type=DiagnosticType.ERROR,
|
||||||
|
location=location,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def warning(self, location: Location, message: str):
|
||||||
|
self.report(
|
||||||
|
type=DiagnosticType.WARNING,
|
||||||
|
location=location,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def info(self, location: Location, message: str):
|
||||||
|
self.report(
|
||||||
|
type=DiagnosticType.INFO,
|
||||||
|
location=location,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def debug(self, location: Location, message: str):
|
||||||
|
self.report(
|
||||||
|
type=DiagnosticType.DEBUG,
|
||||||
|
location=location,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
@@ -13,7 +13,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.locals: dict[p.Expr, int] = {}
|
self.locals: dict[p.Expr, int] = {}
|
||||||
self.scopes: list[dict[str, bool]] = []
|
self.scopes: list[dict[str, bool]] = [{}]
|
||||||
|
|
||||||
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
||||||
"""Resolve the given statements or expressions"""
|
"""Resolve the given statements or expressions"""
|
||||||
@@ -77,6 +77,12 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.locals[expr] = i
|
self.locals[expr] = i
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def is_defined(self, name: str) -> bool:
|
||||||
|
for scope in self.scopes:
|
||||||
|
if name in scope:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def resolve_function(self, function: p.Function) -> None:
|
def resolve_function(self, function: p.Function) -> None:
|
||||||
"""Resolve a function definition
|
"""Resolve a function definition
|
||||||
|
|
||||||
@@ -110,10 +116,22 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
||||||
self.resolve(stmt.value)
|
self.resolve(stmt.value)
|
||||||
for target in stmt.targets:
|
for target in stmt.targets:
|
||||||
|
self._visit_assign(target)
|
||||||
|
|
||||||
|
def _visit_assign(self, target: p.Expr):
|
||||||
match target:
|
match target:
|
||||||
case p.VariableExpr(name=name):
|
case p.VariableExpr(name=name):
|
||||||
self.resolve_local(target, name)
|
if not self.is_defined(name):
|
||||||
# TODO: declare if not found
|
self.declare(name)
|
||||||
|
self.define(name)
|
||||||
|
target.accept(self)
|
||||||
|
|
||||||
|
case p.GetExpr():
|
||||||
|
target.accept(self)
|
||||||
|
|
||||||
|
case p.SubscriptExpr():
|
||||||
|
target.accept(self)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise Exception(f"Unsupported assignment to {target}")
|
raise Exception(f"Unsupported assignment to {target}")
|
||||||
|
|
||||||
@@ -139,6 +157,19 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.resolve(*stmt.orelse)
|
self.resolve(*stmt.orelse)
|
||||||
self.end_scope()
|
self.end_scope()
|
||||||
|
|
||||||
|
def visit_pass(self, stmt: p.Pass) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||||
|
self.resolve(stmt.iterator)
|
||||||
|
self._visit_assign(stmt.target)
|
||||||
|
self.begin_scope()
|
||||||
|
self.resolve(*stmt.body)
|
||||||
|
self.end_scope()
|
||||||
|
|
||||||
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
|
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
|
||||||
self.resolve(expr.left)
|
self.resolve(expr.left)
|
||||||
self.resolve(expr.right)
|
self.resolve(expr.right)
|
||||||
@@ -174,10 +205,6 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.resolve(expr.left)
|
self.resolve(expr.left)
|
||||||
self.resolve(expr.right)
|
self.resolve(expr.right)
|
||||||
|
|
||||||
def visit_set_expr(self, expr: p.SetExpr) -> None:
|
|
||||||
self.resolve(expr.value)
|
|
||||||
self.resolve(expr.object)
|
|
||||||
|
|
||||||
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
||||||
self.resolve(expr.expr)
|
self.resolve(expr.expr)
|
||||||
|
|
||||||
@@ -185,3 +212,33 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.resolve(expr.test)
|
self.resolve(expr.test)
|
||||||
self.resolve(expr.if_true)
|
self.resolve(expr.if_true)
|
||||||
self.resolve(expr.if_false)
|
self.resolve(expr.if_false)
|
||||||
|
|
||||||
|
def visit_list_expr(self, expr: p.ListExpr) -> None:
|
||||||
|
for item in expr.items:
|
||||||
|
self.resolve(item)
|
||||||
|
|
||||||
|
def visit_dict_expr(self, expr: p.DictExpr) -> None:
|
||||||
|
for key in expr.keys:
|
||||||
|
if key is not None:
|
||||||
|
self.resolve(key)
|
||||||
|
for value in expr.values:
|
||||||
|
self.resolve(value)
|
||||||
|
|
||||||
|
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> None:
|
||||||
|
self.resolve(expr.object)
|
||||||
|
self.resolve(expr.index)
|
||||||
|
|
||||||
|
def visit_slice_expr(self, expr: p.SliceExpr) -> None:
|
||||||
|
if expr.lower is not None:
|
||||||
|
self.resolve(expr.lower)
|
||||||
|
if expr.upper is not None:
|
||||||
|
self.resolve(expr.upper)
|
||||||
|
if expr.step is not None:
|
||||||
|
self.resolve(expr.step)
|
||||||
|
|
||||||
|
def visit_tuple_expr(self, expr: p.TupleExpr) -> None:
|
||||||
|
for item in expr.items:
|
||||||
|
self.resolve(item)
|
||||||
|
|
||||||
|
def visit_raw_expr(self, expr: p.RawExpr) -> None:
|
||||||
|
pass
|
||||||
@@ -1,42 +1,446 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Optional, assert_never, cast
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.printer import MidasPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TopType:
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Any"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class BaseType:
|
class BaseType:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class SimpleType:
|
class DerivedType:
|
||||||
name: str
|
name: str
|
||||||
base: BaseType | SimpleType
|
type: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class UnknownType:
|
class UnknownType:
|
||||||
pass
|
def __str__(self) -> str:
|
||||||
|
return "<Unknown>"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class UnitType:
|
class UnitType:
|
||||||
pass
|
def __str__(self) -> str:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Function:
|
class Function:
|
||||||
name: str
|
pos_args: list[Argument] = field(default_factory=list)
|
||||||
pos_args: list[Argument]
|
args: list[Argument] = field(default_factory=list)
|
||||||
args: list[Argument]
|
kw_args: list[Argument] = field(default_factory=list)
|
||||||
kw_args: list[Argument]
|
|
||||||
returns: Type
|
returns: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
args: list[str] = []
|
||||||
|
if len(self.pos_args) != 0:
|
||||||
|
args += list(map(str, self.pos_args))
|
||||||
|
args.append("/")
|
||||||
|
|
||||||
|
if len(self.args) != 0:
|
||||||
|
args += list(map(str, self.args))
|
||||||
|
|
||||||
|
if len(self.kw_args) != 0:
|
||||||
|
args.append("*")
|
||||||
|
args += list(map(str, self.kw_args))
|
||||||
|
|
||||||
|
return f"({', '.join(args)}) -> {self.returns}"
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Argument:
|
class Argument:
|
||||||
|
pos: int
|
||||||
name: str
|
name: str
|
||||||
type: Type
|
type: Type
|
||||||
required: bool
|
required: bool
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
opt: str = "" if self.required else "?"
|
||||||
|
return f"{self.name}: {self.type}{opt}"
|
||||||
|
|
||||||
Type = BaseType | SimpleType | UnknownType | UnitType | Function
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class OverloadedFunction:
|
||||||
|
overloads: list[Type]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "<overloaded function>"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ComplexType:
|
||||||
|
members: dict[str, Type]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
props: list[str] = [f"{name}: {type}" for name, type in self.members.items()]
|
||||||
|
return f"{{{', '.join(props)}}}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ExtensionType:
|
||||||
|
base: Type
|
||||||
|
extension: ComplexType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.base} & {self.extension}"
|
||||||
|
|
||||||
|
|
||||||
|
class Variance(StrEnum):
|
||||||
|
INVARIANT = "INVARIANT"
|
||||||
|
COVARIANT = "COVARIANT"
|
||||||
|
CONTRAVARIANT = "CONTRAVARIANT"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TypeVar:
|
||||||
|
name: str
|
||||||
|
bound: Optional[Type]
|
||||||
|
variance: Variance = Variance.INVARIANT
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
variance: str = {
|
||||||
|
Variance.COVARIANT: "+",
|
||||||
|
Variance.CONTRAVARIANT: "-",
|
||||||
|
}.get(self.variance, "")
|
||||||
|
res: str = f"{variance}{self.name}"
|
||||||
|
if self.bound is not None:
|
||||||
|
res = f"{res} <: {self.bound}"
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class GenericType:
|
||||||
|
name: str
|
||||||
|
params: list[TypeVar]
|
||||||
|
body: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}[{', '.join(map(str, self.params))}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AppliedType:
|
||||||
|
name: str
|
||||||
|
args: list[Type]
|
||||||
|
body: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}[{', '.join(map(str, self.args))}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ConstraintType:
|
||||||
|
type: Type
|
||||||
|
constraint: m.Expr
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
printer = MidasPrinter()
|
||||||
|
return f"{self.type} where {printer.print(self.constraint)}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TupleType:
|
||||||
|
items: tuple[Type, ...]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"({', '.join(map(str, self.items))})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ColumnType:
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Column[{self.type}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class DataFrameType:
|
||||||
|
columns: list[Column]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
schema: list[str] = [f"{col.name}: {col.type}" for col in self.columns]
|
||||||
|
return f"Frame[{', '.join(schema)}]"
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Column:
|
||||||
|
index: int
|
||||||
|
name: Optional[str]
|
||||||
|
type: ColumnType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class FrameGroupBy:
|
||||||
|
frame: DataFrameType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"FrameGroupBy[{self.frame}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ColumnGroupBy:
|
||||||
|
column: ColumnType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"ColumnGroupBy[{self.column}]"
|
||||||
|
|
||||||
|
|
||||||
|
def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
||||||
|
def sub_argument(arg: Function.Argument):
|
||||||
|
return Function.Argument(
|
||||||
|
pos=arg.pos,
|
||||||
|
name=arg.name,
|
||||||
|
type=substitute_typevars(arg.type, substitutions),
|
||||||
|
required=arg.required,
|
||||||
|
)
|
||||||
|
|
||||||
|
def sub_column(col: DataFrameType.Column):
|
||||||
|
return DataFrameType.Column(
|
||||||
|
index=col.index,
|
||||||
|
name=col.name,
|
||||||
|
type=cast(ColumnType, substitute_typevars(col.type, substitutions)),
|
||||||
|
)
|
||||||
|
|
||||||
|
match type:
|
||||||
|
case TopType():
|
||||||
|
return type
|
||||||
|
|
||||||
|
case BaseType(name=name) if name in substitutions:
|
||||||
|
return substitutions[name]
|
||||||
|
|
||||||
|
case BaseType():
|
||||||
|
return type
|
||||||
|
|
||||||
|
case DerivedType(name=name, type=type2):
|
||||||
|
return DerivedType(
|
||||||
|
name=name, type=substitute_typevars(type2, substitutions)
|
||||||
|
)
|
||||||
|
|
||||||
|
case Function(
|
||||||
|
pos_args=pos_args,
|
||||||
|
args=args,
|
||||||
|
kw_args=kw_args,
|
||||||
|
returns=returns,
|
||||||
|
):
|
||||||
|
return Function(
|
||||||
|
pos_args=list(map(sub_argument, pos_args)),
|
||||||
|
args=list(map(sub_argument, args)),
|
||||||
|
kw_args=list(map(sub_argument, kw_args)),
|
||||||
|
returns=substitute_typevars(returns, substitutions),
|
||||||
|
)
|
||||||
|
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
return OverloadedFunction(
|
||||||
|
overloads=[
|
||||||
|
substitute_typevars(overload, substitutions)
|
||||||
|
for overload in overloads
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
case ComplexType(members=members):
|
||||||
|
members2: dict[str, Type] = {
|
||||||
|
name: substitute_typevars(prop, substitutions)
|
||||||
|
for name, prop in members.items()
|
||||||
|
}
|
||||||
|
return ComplexType(members=members2)
|
||||||
|
|
||||||
|
case ExtensionType(base=base, extension=ComplexType(members=members)):
|
||||||
|
return ExtensionType(
|
||||||
|
base=substitute_typevars(base, substitutions),
|
||||||
|
extension=ComplexType(
|
||||||
|
members={
|
||||||
|
name: substitute_typevars(prop, substitutions)
|
||||||
|
for name, prop in members.items()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case AppliedType(name=name, args=args, body=body):
|
||||||
|
return AppliedType(
|
||||||
|
name=name,
|
||||||
|
args=[substitute_typevars(arg, substitutions) for arg in args],
|
||||||
|
body=substitute_typevars(body, substitutions),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ConstraintType():
|
||||||
|
return ConstraintType(
|
||||||
|
type=substitute_typevars(type.type, substitutions),
|
||||||
|
constraint=type.constraint,
|
||||||
|
)
|
||||||
|
|
||||||
|
case TypeVar(name=name):
|
||||||
|
if name in substitutions:
|
||||||
|
return substitutions[name]
|
||||||
|
raise ValueError(f"Missing TypeVar substitution for {name}")
|
||||||
|
|
||||||
|
case GenericType(name=name, params=params, body=body):
|
||||||
|
params2: list[TypeVar] = []
|
||||||
|
for param in params:
|
||||||
|
param2: Type = substitute_typevars(param, substitutions)
|
||||||
|
if not isinstance(param2, TypeVar):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid type parameter substitution, expected TypeVar, got {param2}"
|
||||||
|
)
|
||||||
|
params2.append(param2)
|
||||||
|
return GenericType(
|
||||||
|
name=name,
|
||||||
|
params=params2,
|
||||||
|
body=substitute_typevars(body, substitutions),
|
||||||
|
)
|
||||||
|
|
||||||
|
case TupleType(items=items):
|
||||||
|
return TupleType(
|
||||||
|
items=tuple(substitute_typevars(item, substitutions) for item in items),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ColumnType(type=items_type):
|
||||||
|
return ColumnType(
|
||||||
|
type=substitute_typevars(items_type, substitutions),
|
||||||
|
)
|
||||||
|
|
||||||
|
case DataFrameType(columns=columns):
|
||||||
|
return DataFrameType(
|
||||||
|
columns=list(map(sub_column, columns)),
|
||||||
|
)
|
||||||
|
|
||||||
|
case FrameGroupBy(frame=frame):
|
||||||
|
return FrameGroupBy(
|
||||||
|
frame=cast(DataFrameType, substitute_typevars(frame, substitutions))
|
||||||
|
)
|
||||||
|
|
||||||
|
case ColumnGroupBy(column=column):
|
||||||
|
return ColumnGroupBy(
|
||||||
|
column=cast(ColumnType, substitute_typevars(column, substitutions))
|
||||||
|
)
|
||||||
|
|
||||||
|
case UnknownType() | UnitType():
|
||||||
|
return type
|
||||||
|
|
||||||
|
case TopType() | GenericType():
|
||||||
|
raise NotImplementedError(f"Unsupported type {type}")
|
||||||
|
|
||||||
|
# Ensure exhaustiveness
|
||||||
|
case _:
|
||||||
|
assert_never(type)
|
||||||
|
|
||||||
|
|
||||||
|
def unfold_type(type: Type) -> Type:
|
||||||
|
match type:
|
||||||
|
case DerivedType(type=ref_type):
|
||||||
|
return unfold_type(ref_type)
|
||||||
|
case _:
|
||||||
|
return type
|
||||||
|
|
||||||
|
|
||||||
|
def to_annotation(type: Type) -> str:
|
||||||
|
def _args_annotation(func: Function) -> str:
|
||||||
|
if len(func.kw_args) != 0:
|
||||||
|
return "..."
|
||||||
|
|
||||||
|
args: str = ", ".join(
|
||||||
|
to_annotation(arg.type) for arg in func.pos_args + func.args
|
||||||
|
)
|
||||||
|
return f"[{args}]"
|
||||||
|
|
||||||
|
match type:
|
||||||
|
case TopType():
|
||||||
|
return "Any"
|
||||||
|
|
||||||
|
case BaseType(name=name):
|
||||||
|
return name
|
||||||
|
|
||||||
|
case DerivedType(name=name):
|
||||||
|
return name
|
||||||
|
|
||||||
|
case UnknownType():
|
||||||
|
return "Any"
|
||||||
|
|
||||||
|
case UnitType():
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
case Function(returns=returns):
|
||||||
|
params_annot: str = _args_annotation(type)
|
||||||
|
return f"Callable[{params_annot}, {to_annotation(returns)}]"
|
||||||
|
|
||||||
|
case OverloadedFunction():
|
||||||
|
return "Callable"
|
||||||
|
|
||||||
|
case ComplexType() | ExtensionType():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
case TypeVar(name=name):
|
||||||
|
return name
|
||||||
|
|
||||||
|
case GenericType(name=name, params=params):
|
||||||
|
return f"{name}[{', '.join(map(to_annotation, params))}]"
|
||||||
|
|
||||||
|
case AppliedType(name=name, args=args):
|
||||||
|
return f"{name}[{', '.join(map(to_annotation, args))}]"
|
||||||
|
|
||||||
|
case ConstraintType():
|
||||||
|
return str(type)
|
||||||
|
|
||||||
|
case TupleType(items=items):
|
||||||
|
return f"Tuple[{', '.join(map(to_annotation, items))}]"
|
||||||
|
|
||||||
|
case ColumnType():
|
||||||
|
return "pd.Series"
|
||||||
|
|
||||||
|
case DataFrameType():
|
||||||
|
return "pd.DataFrame"
|
||||||
|
|
||||||
|
case FrameGroupBy():
|
||||||
|
return "pd.api.typing.DataFrameGroupBy"
|
||||||
|
|
||||||
|
case ColumnGroupBy():
|
||||||
|
return "pd.api.typing.SeriesGroupBy"
|
||||||
|
|
||||||
|
case _:
|
||||||
|
assert_never(type)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Predicate:
|
||||||
|
type: Type
|
||||||
|
body: m.Expr
|
||||||
|
alias: bool
|
||||||
|
|
||||||
|
|
||||||
|
Type = (
|
||||||
|
TopType
|
||||||
|
| BaseType
|
||||||
|
| DerivedType
|
||||||
|
| UnknownType
|
||||||
|
| UnitType
|
||||||
|
| Function
|
||||||
|
| OverloadedFunction
|
||||||
|
| ComplexType
|
||||||
|
| ExtensionType
|
||||||
|
| TypeVar
|
||||||
|
| GenericType
|
||||||
|
| AppliedType
|
||||||
|
| ConstraintType
|
||||||
|
| TupleType
|
||||||
|
| ColumnType
|
||||||
|
| DataFrameType
|
||||||
|
| FrameGroupBy
|
||||||
|
| ColumnGroupBy
|
||||||
|
)
|
||||||
|
|||||||
195
midas/checker/unifier.py
Normal file
195
midas/checker/unifier.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UnificationError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
|
class Unifier:
|
||||||
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.logger: logging.Logger = logging.getLogger("Unifier")
|
||||||
|
|
||||||
|
def unify_call(
|
||||||
|
self,
|
||||||
|
type: GenericType,
|
||||||
|
positional: list[Type],
|
||||||
|
keywords: dict[str, Type],
|
||||||
|
) -> Optional[Type]:
|
||||||
|
concrete_func: Function = Function(
|
||||||
|
pos_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=i,
|
||||||
|
name=str(i),
|
||||||
|
type=arg,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
for i, arg in enumerate(positional)
|
||||||
|
],
|
||||||
|
args=[],
|
||||||
|
kw_args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=len(positional) + i,
|
||||||
|
name=name,
|
||||||
|
type=arg,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
for i, (name, arg) in enumerate(keywords.items())
|
||||||
|
],
|
||||||
|
returns=TopType(), # TODO: use expected type
|
||||||
|
)
|
||||||
|
return self.unify_generic(type, concrete_func, match_return=False)
|
||||||
|
|
||||||
|
def unify_generic(
|
||||||
|
self,
|
||||||
|
template: GenericType,
|
||||||
|
concrete: Type,
|
||||||
|
match_return: bool = True,
|
||||||
|
) -> Optional[Type]:
|
||||||
|
substitutions: dict[str, Type]
|
||||||
|
try:
|
||||||
|
substitutions = self.match(template.body, concrete, match_return)
|
||||||
|
except UnificationError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args: list[Type] = []
|
||||||
|
for param in template.params:
|
||||||
|
if param.name not in substitutions:
|
||||||
|
return None
|
||||||
|
args.append(substitutions[param.name])
|
||||||
|
|
||||||
|
applied: Type = self.types.apply_generic(template, args)
|
||||||
|
return applied
|
||||||
|
|
||||||
|
def match(
|
||||||
|
self,
|
||||||
|
template: Type,
|
||||||
|
concrete: Type,
|
||||||
|
match_return: bool = True,
|
||||||
|
) -> dict[str, Type]:
|
||||||
|
# TODO: if concrete is Generic, record bound TypeVar. Then when merging
|
||||||
|
# substitutions, check that the constraint is respected
|
||||||
|
match (template, concrete):
|
||||||
|
case (TypeVar(name=name), _):
|
||||||
|
return {name: concrete}
|
||||||
|
|
||||||
|
case (
|
||||||
|
AppliedType(name=template_name, args=template_args),
|
||||||
|
AppliedType(name=concrete_name, args=concrete_args),
|
||||||
|
) if template_name == concrete_name and len(template_args) == len(
|
||||||
|
concrete_args
|
||||||
|
):
|
||||||
|
substitutions: dict[str, Type] = {}
|
||||||
|
for template_arg, concrete_arg in zip(template_args, concrete_args):
|
||||||
|
new_substistutions: dict[str, Type] = self.match(
|
||||||
|
template_arg, concrete_arg
|
||||||
|
)
|
||||||
|
substitutions = self.merge(substitutions, new_substistutions)
|
||||||
|
|
||||||
|
return substitutions
|
||||||
|
|
||||||
|
case (
|
||||||
|
DataFrameType(columns=template_columns),
|
||||||
|
DataFrameType(columns=concrete_columns),
|
||||||
|
) if len(template_columns) == len(concrete_columns):
|
||||||
|
substitutions: dict[str, Type] = {}
|
||||||
|
for template_column, concrete_column in zip(
|
||||||
|
template_columns, concrete_columns
|
||||||
|
):
|
||||||
|
if template_column.index != concrete_column or (
|
||||||
|
template_column.name != concrete_column.name
|
||||||
|
):
|
||||||
|
self.logger.debug(
|
||||||
|
f"Column mismatch: template={template_column}, concrete={concrete_column}"
|
||||||
|
)
|
||||||
|
raise UnificationError
|
||||||
|
new_substistutions: dict[str, Type] = self.match(
|
||||||
|
template_column.type, concrete_column.type
|
||||||
|
)
|
||||||
|
substitutions = self.merge(substitutions, new_substistutions)
|
||||||
|
return substitutions
|
||||||
|
|
||||||
|
case (ColumnType(type=template_column), ColumnType(type=concrete_column)):
|
||||||
|
return self.match(template_column, concrete_column)
|
||||||
|
|
||||||
|
case (Function(), Function()):
|
||||||
|
mapped: list[tuple[Function.Argument, Function.Argument]] = (
|
||||||
|
self.map_params(template, concrete)
|
||||||
|
)
|
||||||
|
substitutions: dict[str, Type] = {}
|
||||||
|
for template_arg, concrete_arg in mapped:
|
||||||
|
arg_subs: dict[str, Type] = self.match(
|
||||||
|
template_arg.type, concrete_arg.type
|
||||||
|
)
|
||||||
|
substitutions = self.merge(substitutions, arg_subs)
|
||||||
|
|
||||||
|
if match_return:
|
||||||
|
return_subs: dict[str, Type] = self.match(
|
||||||
|
template.returns, concrete.returns
|
||||||
|
)
|
||||||
|
substitutions = self.merge(substitutions, return_subs)
|
||||||
|
|
||||||
|
return substitutions
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.debug(f"Can't match {concrete!r} with {template!r}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def merge(self, subs1: dict[str, Type], subs2: dict[str, Type]) -> dict[str, Type]:
|
||||||
|
merged: dict[str, Type] = subs1.copy()
|
||||||
|
|
||||||
|
for k, v in subs2.items():
|
||||||
|
if k in merged and merged[k] != v:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Substitution already defined for {k} with type {merged[k]}, got {v}"
|
||||||
|
)
|
||||||
|
raise UnificationError
|
||||||
|
merged[k] = v
|
||||||
|
return merged
|
||||||
|
|
||||||
|
def map_params(
|
||||||
|
self, func1: Function, func2: Function
|
||||||
|
) -> list[tuple[Function.Argument, Function.Argument]]:
|
||||||
|
pos1: list[Function.Argument] = func1.pos_args
|
||||||
|
mixed1: list[Function.Argument] = func1.args
|
||||||
|
kw1: list[Function.Argument] = func1.kw_args
|
||||||
|
|
||||||
|
pos2: list[Function.Argument] = func2.pos_args
|
||||||
|
mixed2: list[Function.Argument] = func2.args
|
||||||
|
kw2: list[Function.Argument] = func2.kw_args
|
||||||
|
|
||||||
|
mapped: list[tuple[Function.Argument, Function.Argument]] = []
|
||||||
|
|
||||||
|
by_pos2: dict[int, Function.Argument] = {arg.pos: arg for arg in pos2 + mixed2}
|
||||||
|
by_name2: dict[str, Function.Argument] = {arg.name: arg for arg in mixed2 + kw2}
|
||||||
|
|
||||||
|
for arg1 in pos1:
|
||||||
|
if (arg2 := by_pos2.get(arg1.pos)) is not None:
|
||||||
|
mapped.append((arg1, arg2))
|
||||||
|
|
||||||
|
for arg1 in mixed1:
|
||||||
|
# Match both positionally and by name, conflicts are caught
|
||||||
|
# when merging substitutions
|
||||||
|
if (arg2 := by_pos2.get(arg1.pos)) is not None:
|
||||||
|
mapped.append((arg1, arg2))
|
||||||
|
|
||||||
|
if (arg2 := by_name2.get(arg1.name)) is not None:
|
||||||
|
mapped.append((arg1, arg2))
|
||||||
|
|
||||||
|
for arg1 in kw1:
|
||||||
|
if (arg2 := by_name2.get(arg1.name)) is not None:
|
||||||
|
mapped.append((arg1, arg2))
|
||||||
|
|
||||||
|
return mapped
|
||||||
129
midas/checker/variance.py
Normal file
129
midas/checker/variance.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
from typing import Literal, Optional, cast
|
||||||
|
|
||||||
|
from midas.checker.registry import Member, TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
ConstraintType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Variance,
|
||||||
|
)
|
||||||
|
|
||||||
|
Polarity = Literal[-1, 0, 1]
|
||||||
|
|
||||||
|
|
||||||
|
class Tracker:
|
||||||
|
def __init__(self, vars: list[TypeVar]) -> None:
|
||||||
|
self.vars: list[TypeVar] = vars
|
||||||
|
self.refs: dict[str, set[Polarity]] = {var.name: set() for var in self.vars}
|
||||||
|
|
||||||
|
def record(self, var: TypeVar, polarity: Polarity):
|
||||||
|
self.refs[var.name].add(polarity)
|
||||||
|
|
||||||
|
def get_updated_vars(self) -> list[TypeVar]:
|
||||||
|
return [
|
||||||
|
TypeVar(
|
||||||
|
name=var.name, bound=var.bound, variance=self.get_variance(var.name)
|
||||||
|
)
|
||||||
|
for var in self.vars
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_variance(self, name: str) -> Variance:
|
||||||
|
refs: set[Polarity] = self.refs[name]
|
||||||
|
if refs == {-1}:
|
||||||
|
return Variance.CONTRAVARIANT
|
||||||
|
if refs == {1}:
|
||||||
|
return Variance.COVARIANT
|
||||||
|
return Variance.INVARIANT
|
||||||
|
|
||||||
|
def __contains__(self, item: TypeVar | str):
|
||||||
|
if isinstance(item, TypeVar):
|
||||||
|
return item.name in self
|
||||||
|
return item in self.refs
|
||||||
|
|
||||||
|
|
||||||
|
class VarianceInferrer:
|
||||||
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.tracker: Tracker = Tracker([])
|
||||||
|
|
||||||
|
def infer(self, type: GenericType) -> GenericType:
|
||||||
|
self.tracker = Tracker(type.params)
|
||||||
|
|
||||||
|
self.walk(type.body, 1, type.name)
|
||||||
|
members: dict[str, Member] = self.types._members.get(type.name, {})
|
||||||
|
for name, member in members.items():
|
||||||
|
self.walk(member.type, 1, type.name, [f"member:'{name}'"])
|
||||||
|
|
||||||
|
return GenericType(
|
||||||
|
name=type.name,
|
||||||
|
params=self.tracker.get_updated_vars(),
|
||||||
|
body=type.body,
|
||||||
|
)
|
||||||
|
|
||||||
|
def walk(
|
||||||
|
self,
|
||||||
|
type: Type,
|
||||||
|
polarity: Polarity,
|
||||||
|
base_name: str,
|
||||||
|
path: Optional[list[str]] = None,
|
||||||
|
):
|
||||||
|
if path is None:
|
||||||
|
path = []
|
||||||
|
|
||||||
|
match type:
|
||||||
|
# Arguments are negative positions -> flip polarity
|
||||||
|
# Return is positive position -> keep polarity
|
||||||
|
case Function(pos_args=pos_args, args=mixed_args, kw_args=kw_args):
|
||||||
|
all_args: list[Function.Argument] = pos_args + mixed_args + kw_args
|
||||||
|
for arg in all_args:
|
||||||
|
self.walk(
|
||||||
|
arg.type,
|
||||||
|
-polarity,
|
||||||
|
base_name,
|
||||||
|
path + [f"arg:'{arg.name}'"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.walk(type.returns, polarity, base_name, path + ["return"])
|
||||||
|
|
||||||
|
# Walk all overloads
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
for overload in overloads:
|
||||||
|
self.walk(overload, polarity, base_name, path)
|
||||||
|
|
||||||
|
# If same name as root generic -> skip
|
||||||
|
# Get inferred variance of parameters and multiply with current
|
||||||
|
# polarity to recurse through arguments
|
||||||
|
case AppliedType(name=name, args=args):
|
||||||
|
# TODO: handle mutually recursive types
|
||||||
|
if name == base_name:
|
||||||
|
return
|
||||||
|
generic: Type = self.types.get_type(name)
|
||||||
|
assert isinstance(generic, GenericType)
|
||||||
|
params: list[TypeVar] = generic.params
|
||||||
|
polarities: dict[Variance, Polarity] = {
|
||||||
|
Variance.INVARIANT: 0,
|
||||||
|
Variance.COVARIANT: 1,
|
||||||
|
Variance.CONTRAVARIANT: -1,
|
||||||
|
}
|
||||||
|
for arg, param in zip(args, params):
|
||||||
|
param_polarity: Polarity = polarities[param.variance]
|
||||||
|
self.walk(
|
||||||
|
arg,
|
||||||
|
cast(Polarity, polarity * param_polarity),
|
||||||
|
base_name,
|
||||||
|
path + [f"applied:'{name}'"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Walk base type
|
||||||
|
case ConstraintType(type=base):
|
||||||
|
self.walk(base, polarity, base_name, path + ["constraint"])
|
||||||
|
|
||||||
|
# Reached end
|
||||||
|
# If tracked, record polarity
|
||||||
|
case TypeVar():
|
||||||
|
if type in self.tracker:
|
||||||
|
self.tracker.record(type, polarity)
|
||||||
41
midas/cli/ansi.py
Normal file
41
midas/cli/ansi.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
class Ansi:
|
||||||
|
CTRL = "\x1b["
|
||||||
|
RESET = CTRL + "0m"
|
||||||
|
BOLD = CTRL + "1m"
|
||||||
|
DIM = CTRL + "2m"
|
||||||
|
ITALIC = CTRL + "3m"
|
||||||
|
UNDERLINE = CTRL + "4m"
|
||||||
|
|
||||||
|
BLACK = 0
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
YELLOW = 3
|
||||||
|
BLUE = 4
|
||||||
|
MAGENTA = 5
|
||||||
|
CYAN = 6
|
||||||
|
WHITE = 7
|
||||||
|
|
||||||
|
BRIGHT_BLACK = 60
|
||||||
|
BRIGHT_RED = 61
|
||||||
|
BRIGHT_GREEN = 62
|
||||||
|
BRIGHT_YELLOW = 63
|
||||||
|
BRIGHT_BLUE = 64
|
||||||
|
BRIGHT_MAGENTA = 65
|
||||||
|
BRIGHT_CYAN = 66
|
||||||
|
BRIGHT_WHITE = 67
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def FG(cls, col: int) -> str:
|
||||||
|
return f"{cls.CTRL}{30 + col}m"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def BG(cls, col: int) -> str:
|
||||||
|
return f"{cls.CTRL}{40 + col}m"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def FG_RGB(cls, r: int, g: int, b: int) -> str:
|
||||||
|
return f"{cls.CTRL}38;2;{r};{g};{b}m"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def BG_RGB(cls, r: int, g: int, b: int) -> str:
|
||||||
|
return f"{cls.CTRL}48;2;{r};{g};{b}m"
|
||||||
9
midas/cli/commands/__init__.py
Normal file
9
midas/cli/commands/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .check import check as check
|
||||||
|
from .compile import compile as compile
|
||||||
|
from .format import format as format
|
||||||
|
from .highlight import highlight as highlight
|
||||||
|
from .parse import parse as parse
|
||||||
|
from .registry import dump_registry as dump_registry
|
||||||
|
from .stubs import stubs as stubs
|
||||||
|
from .types import types as types
|
||||||
|
from .validate import validate as validate
|
||||||
41
midas/cli/commands/check.py
Normal file
41
midas/cli/commands/check.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# **Run type checker and report diagnostics**
|
||||||
|
# ```shell
|
||||||
|
# midas check <file.py> [--types <file.midas>]
|
||||||
|
# ```
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.diagnostic import Diagnostic
|
||||||
|
from midas.cli.highlighter import DiagnosticsHighlighter
|
||||||
|
from midas.cli.utils import DiagnosticPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Run type checker and report diagnostics")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||||
|
@click.option("-l", "--highlight", type=click.File("w"))
|
||||||
|
def check(
|
||||||
|
file: TextIO,
|
||||||
|
types: tuple[TextIO],
|
||||||
|
highlight: Optional[TextIO],
|
||||||
|
):
|
||||||
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
|
||||||
|
checker = TypeChecker()
|
||||||
|
for types_file in types:
|
||||||
|
checker.import_midas(Path(types_file.name).resolve())
|
||||||
|
|
||||||
|
checker.type_check(source_path)
|
||||||
|
diagnostics: list[Diagnostic] = checker.diagnostics.copy()
|
||||||
|
printer = DiagnosticPrinter()
|
||||||
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
|
if highlight is not None:
|
||||||
|
source: str = file.read()
|
||||||
|
highlighter = DiagnosticsHighlighter(source)
|
||||||
|
highlighter.highlight(diagnostics)
|
||||||
|
highlighter.dump(highlight)
|
||||||
51
midas/cli/commands/compile.py
Normal file
51
midas/cli/commands/compile.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# **Compile source**
|
||||||
|
# ```shell
|
||||||
|
# midas compile <file.py> [--types <file.midas>] [-o <output>] [--assertions|--strict|--no-checks]
|
||||||
|
# ```
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
||||||
|
from midas.cli.utils import DiagnosticPrinter
|
||||||
|
from midas.generator.generator import Generator
|
||||||
|
from midas.utils import TypedAST
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Compile source")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||||
|
@click.option("-s", "--stubs", type=str, multiple=True)
|
||||||
|
@click.option("--ignore-errors", is_flag=True)
|
||||||
|
def compile(
|
||||||
|
file: TextIO,
|
||||||
|
types: tuple[TextIO],
|
||||||
|
stubs: tuple[str],
|
||||||
|
ignore_errors: bool,
|
||||||
|
):
|
||||||
|
source: str = file.read()
|
||||||
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
|
||||||
|
checker = TypeChecker()
|
||||||
|
type_files: list[tuple[Path, Optional[str]]] = []
|
||||||
|
for i, types_file in enumerate(types):
|
||||||
|
in_path: Path = Path(types_file.name).resolve()
|
||||||
|
checker.import_midas(in_path)
|
||||||
|
type_files.append((in_path, stubs[i] if i < len(stubs) else None))
|
||||||
|
|
||||||
|
typed_ast: TypedAST = checker.type_check_source(source, str(source_path))
|
||||||
|
diagnostics: list[Diagnostic] = checker.diagnostics.copy()
|
||||||
|
printer = DiagnosticPrinter()
|
||||||
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
|
if not ignore_errors and any(
|
||||||
|
map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)
|
||||||
|
):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
generator = Generator(workdir=source_path.parent, types=checker.types)
|
||||||
|
generator.generate(typed_ast, source_path, type_files=type_files)
|
||||||
25
midas/cli/commands/format.py
Normal file
25
midas/cli/commands/format.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.printer import MidasPrinter
|
||||||
|
from midas.lexer.midas import MidasLexer
|
||||||
|
from midas.lexer.token import Token
|
||||||
|
from midas.parser.midas import MidasParser
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Parse and pretty print a Midas file")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-o", "--output", type=click.File("w"), default="-")
|
||||||
|
def format(file: TextIO, output: TextIO):
|
||||||
|
source: str = file.read()
|
||||||
|
printer = MidasPrinter()
|
||||||
|
lexer = MidasLexer(source, file=file.name)
|
||||||
|
tokens: list[Token] = lexer.process()
|
||||||
|
parser = MidasParser(tokens)
|
||||||
|
stmts: list[m.Stmt] = parser.parse()
|
||||||
|
for err in parser.errors:
|
||||||
|
print(err.get_report())
|
||||||
|
for stmt in stmts:
|
||||||
|
output.write(printer.print(stmt) + "\n")
|
||||||
66
midas/cli/commands/highlight.py
Normal file
66
midas/cli/commands/highlight.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import ast
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.cli.highlighter import (
|
||||||
|
Highlighter,
|
||||||
|
LocatableToken,
|
||||||
|
MidasHighlighter,
|
||||||
|
PythonHighlighter,
|
||||||
|
)
|
||||||
|
from midas.lexer.midas import MidasLexer
|
||||||
|
from midas.lexer.token import Token, TokenType
|
||||||
|
from midas.parser.midas import MidasParser
|
||||||
|
from midas.parser.python import PythonParser
|
||||||
|
|
||||||
|
|
||||||
|
def highlight_python(source: str, path: str) -> Highlighter:
|
||||||
|
tree: ast.Module = ast.parse(source, filename=path)
|
||||||
|
parser = PythonParser()
|
||||||
|
stmts: list[p.Stmt] = parser.parse_module(tree)
|
||||||
|
highlighter = PythonHighlighter(source)
|
||||||
|
for stmt in stmts:
|
||||||
|
highlighter.highlight(stmt)
|
||||||
|
return highlighter
|
||||||
|
|
||||||
|
|
||||||
|
def highlight_midas(source: str, path: str) -> Highlighter:
|
||||||
|
lexer = MidasLexer(source, file=path)
|
||||||
|
tokens: list[Token] = lexer.process()
|
||||||
|
parser = MidasParser(tokens)
|
||||||
|
stmts: list[m.Stmt] = parser.parse()
|
||||||
|
highlighter = MidasHighlighter(source)
|
||||||
|
for err in parser.errors:
|
||||||
|
print(err.get_report())
|
||||||
|
|
||||||
|
for stmt in stmts:
|
||||||
|
highlighter.highlight(stmt)
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == TokenType.COMMENT:
|
||||||
|
highlighter.wrap(LocatableToken(token), "comment")
|
||||||
|
elif token.is_keyword:
|
||||||
|
highlighter.wrap(LocatableToken(token), "keyword")
|
||||||
|
return highlighter
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(
|
||||||
|
help="Parse a Python or Midas file and produce a highlighted version showing AST node types inline",
|
||||||
|
short_help="Parse and highlight a Python or Midas file",
|
||||||
|
)
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-o", "--output", type=click.File("w"), default="-")
|
||||||
|
def highlight(output: TextIO, file: TextIO):
|
||||||
|
source: str = file.read()
|
||||||
|
highlighter: Highlighter
|
||||||
|
|
||||||
|
if file.name.endswith(".py"):
|
||||||
|
highlighter = highlight_python(source, file.name)
|
||||||
|
elif file.name.endswith(".midas"):
|
||||||
|
highlighter = highlight_midas(source, file.name)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported file type")
|
||||||
|
|
||||||
|
highlighter.dump(output)
|
||||||
66
midas/cli/commands/parse.py
Normal file
66
midas/cli/commands/parse.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# **Parse and pretty-print AST**
|
||||||
|
# ```shell
|
||||||
|
# midas parse <file.midas / file.py>
|
||||||
|
# ```
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.printer import MidasAstPrinter, PythonAstPrinter
|
||||||
|
from midas.lexer.midas import MidasLexer
|
||||||
|
from midas.lexer.token import Token
|
||||||
|
from midas.parser.midas import MidasParser
|
||||||
|
from midas.parser.python import PythonParser
|
||||||
|
|
||||||
|
|
||||||
|
def dump_python_ast(tree: ast.Module) -> str:
|
||||||
|
parser = PythonParser()
|
||||||
|
stmts: list[p.Stmt] = parser.parse_module(tree)
|
||||||
|
printer = PythonAstPrinter()
|
||||||
|
dump: str = ""
|
||||||
|
for stmt in stmts:
|
||||||
|
dump += printer.print(stmt)
|
||||||
|
dump += "\n"
|
||||||
|
return dump
|
||||||
|
|
||||||
|
|
||||||
|
def dump_midas_ast(source: str, filename: str) -> str:
|
||||||
|
lexer = MidasLexer(source, file=filename)
|
||||||
|
tokens: list[Token] = lexer.process()
|
||||||
|
parser = MidasParser(tokens)
|
||||||
|
stmts: list[m.Stmt] = parser.parse()
|
||||||
|
if len(parser.errors) != 0:
|
||||||
|
for err in parser.errors:
|
||||||
|
print(err.get_report())
|
||||||
|
raise RuntimeError("A parsing error occurred")
|
||||||
|
printer = MidasAstPrinter()
|
||||||
|
dump: str = ""
|
||||||
|
for stmt in stmts:
|
||||||
|
dump += printer.print(stmt)
|
||||||
|
dump += "\n"
|
||||||
|
return dump
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Parse a Python or Midas file and pretty-print its AST")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("--raw", is_flag=True)
|
||||||
|
def parse(file: TextIO, raw: bool):
|
||||||
|
source: str = file.read()
|
||||||
|
|
||||||
|
dump: str
|
||||||
|
if file.name.endswith(".py"):
|
||||||
|
tree: ast.Module = ast.parse(source, filename=file.name)
|
||||||
|
if raw:
|
||||||
|
dump = ast.dump(tree, indent=4)
|
||||||
|
else:
|
||||||
|
dump = dump_python_ast(tree)
|
||||||
|
elif file.name.endswith(".midas"):
|
||||||
|
dump = dump_midas_ast(source, file.name)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported file type")
|
||||||
|
|
||||||
|
click.echo(dump)
|
||||||
66
midas/cli/commands/registry.py
Normal file
66
midas/cli/commands/registry.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# **Dump types registry**
|
||||||
|
# ```shell
|
||||||
|
# midas dump-registry [--types <file.midas>]
|
||||||
|
# ```
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from midas.ast.printer import MidasPrinter
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.registry import Member
|
||||||
|
from midas.checker.types import AppliedType, BaseType, DerivedType, GenericType, Type
|
||||||
|
|
||||||
|
|
||||||
|
def base_type(type: Type) -> Type:
|
||||||
|
match type:
|
||||||
|
case BaseType():
|
||||||
|
return type
|
||||||
|
case DerivedType(type=base):
|
||||||
|
return base
|
||||||
|
case AppliedType(body=body):
|
||||||
|
return body
|
||||||
|
case GenericType(body=body):
|
||||||
|
return body
|
||||||
|
case _:
|
||||||
|
return type
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Dump types registry")
|
||||||
|
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||||
|
def dump_registry(
|
||||||
|
types: tuple[TextIO],
|
||||||
|
):
|
||||||
|
checker = TypeChecker()
|
||||||
|
for types_file in types:
|
||||||
|
checker.import_midas(Path(types_file.name).resolve())
|
||||||
|
|
||||||
|
print("##### Types #####")
|
||||||
|
for name, type in checker.types._types.items():
|
||||||
|
members: dict[str, Member] = checker.types._members.get(name, {})
|
||||||
|
params: str = ""
|
||||||
|
if isinstance(type, GenericType):
|
||||||
|
params = ", ".join(map(str, type.params))
|
||||||
|
params = f"[{params}]"
|
||||||
|
print(f"{name}{params} = {base_type(type)}")
|
||||||
|
if len(members) != 0:
|
||||||
|
print(" " * 4 + "Members:")
|
||||||
|
for member_name, member in members.items():
|
||||||
|
kind: str = member.kind.name
|
||||||
|
print(" " * 8 + f"({kind:8}) {member_name}: {member.type}")
|
||||||
|
|
||||||
|
print("##### Predicates #####")
|
||||||
|
printer = MidasPrinter()
|
||||||
|
for name, predicate in checker.types._predicates.items():
|
||||||
|
body: str = printer.print(predicate.body)
|
||||||
|
if predicate.alias:
|
||||||
|
print(f"{name}: {predicate.type} = {body}")
|
||||||
|
else:
|
||||||
|
print(f"{name}{predicate.type}:")
|
||||||
|
body = "\n".join(
|
||||||
|
" " + ("return " if i == 0 else "") + line
|
||||||
|
for i, line in enumerate(body.split("\n"))
|
||||||
|
)
|
||||||
|
print(body)
|
||||||
66
midas/cli/commands/stubs.py
Normal file
66
midas/cli/commands/stubs.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import ast
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
import black
|
||||||
|
import click
|
||||||
|
from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.generator.stubs import StubsGenerator
|
||||||
|
|
||||||
|
|
||||||
|
def generate_stubs(in_path: Path, out_path: Path):
|
||||||
|
checker = TypeChecker()
|
||||||
|
checker.import_midas(in_path)
|
||||||
|
|
||||||
|
generator = StubsGenerator(checker.types)
|
||||||
|
module: ast.Module = generator.generate_stubs()
|
||||||
|
module = ast.fix_missing_locations(module)
|
||||||
|
|
||||||
|
output: str = ast.unparse(module)
|
||||||
|
output = black.format_str(output, mode=black.Mode(is_pyi=True))
|
||||||
|
|
||||||
|
out_path.write_text(output)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(FileSystemEventHandler):
|
||||||
|
def __init__(self, in_path: Path, out_path: Path) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.in_path: Path = in_path
|
||||||
|
self.out_path: Path = out_path
|
||||||
|
|
||||||
|
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
|
||||||
|
generate_stubs(self.in_path, self.out_path)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Generate stubs from Midas definitions")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-o", "--output", type=click.File("w"))
|
||||||
|
@click.option("-w", "--watch", is_flag=True)
|
||||||
|
def stubs(
|
||||||
|
file: TextIO,
|
||||||
|
output: Optional[TextIO],
|
||||||
|
watch: bool,
|
||||||
|
):
|
||||||
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
out_path: Path = source_path.with_suffix(".pyi")
|
||||||
|
if output is not None:
|
||||||
|
out_path = Path(output.name).resolve()
|
||||||
|
generate_stubs(source_path, out_path)
|
||||||
|
|
||||||
|
if watch:
|
||||||
|
print(f"Watching {source_path}...")
|
||||||
|
print("Press CTRL+C to stop")
|
||||||
|
handler = Handler(source_path, out_path)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(handler, str(source_path))
|
||||||
|
observer.start()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
52
midas/cli/commands/types.py
Normal file
52
midas/cli/commands/types.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# **Print judgements**
|
||||||
|
# ```shell
|
||||||
|
# midas types <file.py> [--types <file.midas>]
|
||||||
|
# ```
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
||||||
|
from midas.cli.highlighter import DiagnosticsHighlighter
|
||||||
|
from midas.cli.utils import DiagnosticPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Print typing judgements")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||||
|
@click.option("-l", "--highlight", type=click.File("w"))
|
||||||
|
def types(
|
||||||
|
file: TextIO,
|
||||||
|
types: tuple[TextIO],
|
||||||
|
highlight: Optional[TextIO],
|
||||||
|
):
|
||||||
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
|
||||||
|
checker = TypeChecker()
|
||||||
|
for types_file in types:
|
||||||
|
checker.import_midas(Path(types_file.name).resolve())
|
||||||
|
|
||||||
|
checker.type_check(source_path)
|
||||||
|
|
||||||
|
diagnostics: list[Diagnostic] = []
|
||||||
|
for expr, type in checker.python_typer.judgements:
|
||||||
|
diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
file_path=str(source_path),
|
||||||
|
location=expr.location,
|
||||||
|
type=DiagnosticType.INFO,
|
||||||
|
message=f"Type: {type}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
diagnostics.extend(checker.diagnostics)
|
||||||
|
printer = DiagnosticPrinter()
|
||||||
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
|
if highlight is not None:
|
||||||
|
source: str = file.read()
|
||||||
|
highlighter = DiagnosticsHighlighter(source)
|
||||||
|
highlighter.highlight(diagnostics)
|
||||||
|
highlighter.dump(highlight)
|
||||||
37
midas/cli/commands/validate.py
Normal file
37
midas/cli/commands/validate.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# **Validate midas definitions**
|
||||||
|
# ```shell
|
||||||
|
# midas validate <file.midas>
|
||||||
|
# ```
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.diagnostic import Diagnostic
|
||||||
|
from midas.cli.highlighter import DiagnosticsHighlighter
|
||||||
|
from midas.cli.utils import DiagnosticPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(help="Validate Midas definitions")
|
||||||
|
@click.argument("file", type=click.File("r"))
|
||||||
|
@click.option("-l", "--highlight", type=click.File("w"))
|
||||||
|
def validate(
|
||||||
|
file: TextIO,
|
||||||
|
highlight: Optional[TextIO],
|
||||||
|
):
|
||||||
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
|
||||||
|
checker = TypeChecker()
|
||||||
|
checker.import_midas(source_path)
|
||||||
|
|
||||||
|
diagnostics: list[Diagnostic] = checker.diagnostics.copy()
|
||||||
|
printer = DiagnosticPrinter()
|
||||||
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
|
if highlight is not None:
|
||||||
|
source: str = file.read()
|
||||||
|
highlighter = DiagnosticsHighlighter(source)
|
||||||
|
highlighter.highlight(diagnostics)
|
||||||
|
highlighter.dump(highlight)
|
||||||
@@ -53,5 +53,6 @@ span {
|
|||||||
|
|
||||||
&.keyword {
|
&.keyword {
|
||||||
color: rgb(211, 72, 9);
|
color: rgb(211, 72, 9);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generic, Optional, Protocol, TextIO, TypeVar
|
from typing import Generic, Optional, Protocol, TextIO, TypeVar
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ import midas.ast.midas as m
|
|||||||
import midas.ast.python as p
|
import midas.ast.python as p
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
from midas.checker.diagnostic import Diagnostic
|
from midas.checker.diagnostic import Diagnostic
|
||||||
|
from midas.lexer.token import Token
|
||||||
|
|
||||||
H = TypeVar("H", bound="Highlighter", contravariant=True)
|
H = TypeVar("H", bound="Highlighter", contravariant=True)
|
||||||
|
|
||||||
@@ -22,6 +24,15 @@ class Locatable(Protocol):
|
|||||||
def location(self) -> Optional[Location]: ...
|
def location(self) -> Optional[Location]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LocatableToken:
|
||||||
|
token: Token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location(self) -> Location:
|
||||||
|
return self.token.get_location()
|
||||||
|
|
||||||
|
|
||||||
class Highlighter(ABC):
|
class Highlighter(ABC):
|
||||||
BASE_CSS_PATH: Path = Path(__file__).parent / "highlight.css"
|
BASE_CSS_PATH: Path = Path(__file__).parent / "highlight.css"
|
||||||
EXTRA_CSS_PATH: Optional[Path] = None
|
EXTRA_CSS_PATH: Optional[Path] = None
|
||||||
@@ -123,9 +134,9 @@ class PythonHighlighter(
|
|||||||
|
|
||||||
def visit_base_type(self, node: p.BaseType) -> None:
|
def visit_base_type(self, node: p.BaseType) -> None:
|
||||||
self.wrap(node, "base-type")
|
self.wrap(node, "base-type")
|
||||||
if node.param is not None:
|
for arg in node.args:
|
||||||
self.wrap(node.param, "param")
|
self.wrap(arg, "arg")
|
||||||
node.param.accept(self)
|
arg.accept(self)
|
||||||
|
|
||||||
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
||||||
self.wrap(node, "constraint-type")
|
self.wrap(node, "constraint-type")
|
||||||
@@ -177,6 +188,16 @@ class PythonHighlighter(
|
|||||||
for else_stmt in stmt.orelse:
|
for else_stmt in stmt.orelse:
|
||||||
else_stmt.accept(self)
|
else_stmt.accept(self)
|
||||||
|
|
||||||
|
def visit_pass(self, stmt: p.Pass) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||||
|
self.wrap(stmt, "for")
|
||||||
|
stmt.iterator.accept(self)
|
||||||
|
stmt.target.accept(self)
|
||||||
|
for body_stmt in stmt.body:
|
||||||
|
body_stmt.accept(self)
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: p.BinaryExpr) -> None: ...
|
def visit_binary_expr(self, expr: p.BinaryExpr) -> None: ...
|
||||||
|
|
||||||
def visit_compare_expr(self, expr: p.CompareExpr) -> None: ...
|
def visit_compare_expr(self, expr: p.CompareExpr) -> None: ...
|
||||||
@@ -199,60 +220,70 @@ class PythonHighlighter(
|
|||||||
|
|
||||||
def visit_logical_expr(self, expr: p.LogicalExpr) -> None: ...
|
def visit_logical_expr(self, expr: p.LogicalExpr) -> None: ...
|
||||||
|
|
||||||
def visit_set_expr(self, expr: p.SetExpr) -> None: ...
|
|
||||||
|
|
||||||
def visit_cast_expr(self, expr: p.CastExpr) -> None: ...
|
def visit_cast_expr(self, expr: p.CastExpr) -> None: ...
|
||||||
|
|
||||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None: ...
|
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None: ...
|
||||||
|
|
||||||
|
def visit_list_expr(self, expr: p.ListExpr) -> None:
|
||||||
|
for item in expr.items:
|
||||||
|
item.accept(self)
|
||||||
|
|
||||||
class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]):
|
def visit_dict_expr(self, expr: p.DictExpr) -> None:
|
||||||
|
for key in expr.keys:
|
||||||
|
if key is not None:
|
||||||
|
key.accept(self)
|
||||||
|
for value in expr.values:
|
||||||
|
value.accept(self)
|
||||||
|
|
||||||
|
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> None:
|
||||||
|
expr.object.accept(self)
|
||||||
|
expr.index.accept(self)
|
||||||
|
|
||||||
|
def visit_slice_expr(self, expr: p.SliceExpr) -> None:
|
||||||
|
if expr.lower is not None:
|
||||||
|
expr.lower.accept(self)
|
||||||
|
if expr.upper is not None:
|
||||||
|
expr.upper.accept(self)
|
||||||
|
if expr.step is not None:
|
||||||
|
expr.step.accept(self)
|
||||||
|
|
||||||
|
def visit_tuple_expr(self, expr: p.TupleExpr) -> None:
|
||||||
|
for item in expr.items:
|
||||||
|
item.accept(self)
|
||||||
|
|
||||||
|
def visit_raw_expr(self, expr: p.RawExpr) -> None: ...
|
||||||
|
|
||||||
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class MidasHighlighter(
|
||||||
|
Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None]
|
||||||
|
):
|
||||||
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_midas.css"
|
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_midas.css"
|
||||||
|
|
||||||
def highlight(self, node: Highlightable[MidasHighlighter]):
|
def highlight(self, node: Highlightable[MidasHighlighter]):
|
||||||
node.accept(self)
|
node.accept(self)
|
||||||
|
|
||||||
def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt) -> None:
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||||
self.wrap(stmt, "simple-type")
|
self.wrap(stmt, "type-stmt")
|
||||||
if stmt.template is not None:
|
self.wrap(LocatableToken(stmt.name), "type-name")
|
||||||
stmt.template.accept(self)
|
stmt.type.accept(self)
|
||||||
stmt.base.accept(self)
|
|
||||||
if stmt.constraint is not None:
|
def visit_member_stmt(self, stmt: m.MemberStmt) -> None:
|
||||||
self.wrap(stmt.constraint, "constraint")
|
self.wrap(stmt, "member")
|
||||||
stmt.constraint.accept(self)
|
|
||||||
|
|
||||||
def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt) -> None:
|
|
||||||
self.wrap(stmt, "complex-type")
|
|
||||||
if stmt.template is not None:
|
|
||||||
stmt.template.accept(self)
|
|
||||||
for prop in stmt.properties:
|
|
||||||
prop.accept(self)
|
|
||||||
|
|
||||||
def visit_property_stmt(self, stmt: m.PropertyStmt) -> None:
|
|
||||||
self.wrap(stmt, "property")
|
|
||||||
stmt.type.accept(self)
|
stmt.type.accept(self)
|
||||||
if stmt.constraint is not None:
|
|
||||||
self.wrap(stmt.constraint, "constraint")
|
|
||||||
stmt.constraint.accept(self)
|
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||||
self.wrap(stmt, "extend")
|
self.wrap(stmt, "extend")
|
||||||
stmt.type.accept(self)
|
for member in stmt.members:
|
||||||
for op in stmt.operations:
|
member.accept(self)
|
||||||
op.accept(self)
|
|
||||||
|
|
||||||
def visit_op_stmt(self, stmt: m.OpStmt) -> None:
|
|
||||||
self.wrap(stmt, "op")
|
|
||||||
stmt.operand.accept(self)
|
|
||||||
stmt.result.accept(self)
|
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||||
self.wrap(stmt, "predicate")
|
self.wrap(stmt, "predicate")
|
||||||
stmt.type.accept(self)
|
self.wrap(LocatableToken(stmt.name), "predicate-name")
|
||||||
stmt.condition.accept(self)
|
for spec in stmt.params:
|
||||||
|
self._visit_param_spec(spec)
|
||||||
def visit_simple_type_expr(self, expr: m.SimpleTypeExpr) -> None:
|
stmt.body.accept(self)
|
||||||
self.wrap(expr, "simple-type-expr")
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
|
def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
|
||||||
self.wrap(expr, "logical-expr")
|
self.wrap(expr, "logical-expr")
|
||||||
@@ -268,6 +299,14 @@ class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]):
|
|||||||
self.wrap(expr, "unary-expr")
|
self.wrap(expr, "unary-expr")
|
||||||
expr.right.accept(self)
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> None:
|
||||||
|
self.wrap(expr, "call-expr")
|
||||||
|
expr.callee.accept(self)
|
||||||
|
for arg in expr.arguments:
|
||||||
|
arg.accept(self)
|
||||||
|
for arg in expr.keywords.values():
|
||||||
|
arg.accept(self)
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr) -> None:
|
def visit_get_expr(self, expr: m.GetExpr) -> None:
|
||||||
self.wrap(expr, "get-expr")
|
self.wrap(expr, "get-expr")
|
||||||
expr.expr.accept(self)
|
expr.expr.accept(self)
|
||||||
@@ -282,14 +321,46 @@ class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: ...
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: ...
|
||||||
|
|
||||||
def visit_template_expr(self, expr: m.TemplateExpr) -> None:
|
def visit_named_type(self, type: m.NamedType) -> None:
|
||||||
self.wrap(expr, "template")
|
self.wrap(type, "named-type")
|
||||||
expr.type.accept(self)
|
|
||||||
|
|
||||||
def visit_type_expr(self, expr: m.TypeExpr) -> None:
|
def visit_generic_type(self, type: m.GenericType) -> None:
|
||||||
self.wrap(expr, "type")
|
self.wrap(type, "generic-type")
|
||||||
if expr.template is not None:
|
type.type.accept(self)
|
||||||
expr.template.accept(self)
|
for arg in type.args:
|
||||||
|
arg.accept(self)
|
||||||
|
|
||||||
|
def visit_constraint_type(self, type: m.ConstraintType) -> None:
|
||||||
|
self.wrap(type, "constraint-type")
|
||||||
|
type.type.accept(self)
|
||||||
|
type.constraint.accept(self)
|
||||||
|
|
||||||
|
def visit_complex_type(self, type: m.ComplexType) -> None:
|
||||||
|
self.wrap(type, "complex-type")
|
||||||
|
for member in type.members:
|
||||||
|
member.accept(self)
|
||||||
|
|
||||||
|
def visit_function_type(self, type: m.FunctionType) -> None:
|
||||||
|
self.wrap(type, "function")
|
||||||
|
self._visit_param_spec(type.params)
|
||||||
|
type.returns.accept(self)
|
||||||
|
|
||||||
|
def visit_extension_type(self, type: m.ExtensionType) -> None:
|
||||||
|
self.wrap(type, "extension")
|
||||||
|
type.base.accept(self)
|
||||||
|
type.extension.accept(self)
|
||||||
|
|
||||||
|
def _visit_param_spec(self, spec: m.ParamSpec) -> None:
|
||||||
|
for param in spec.pos + spec.mixed + spec.kw:
|
||||||
|
param.type.accept(self)
|
||||||
|
|
||||||
|
def visit_frame_type(self, type: m.FrameType) -> None:
|
||||||
|
self.wrap(type, "frame")
|
||||||
|
for column in type.columns:
|
||||||
|
self._visit_frame_column(column)
|
||||||
|
|
||||||
|
def _visit_frame_column(self, column: m.FrameType.Column) -> None:
|
||||||
|
self.wrap(column, "column")
|
||||||
|
|
||||||
|
|
||||||
class DiagnosticsHighlighter(Highlighter):
|
class DiagnosticsHighlighter(Highlighter):
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ span {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.simple-type {
|
&.named-type,
|
||||||
--col: 108, 233, 108;
|
&.generic-type,
|
||||||
}
|
&.constraint-type,
|
||||||
|
|
||||||
&.complex-type {
|
&.complex-type {
|
||||||
--col: 233, 206, 108;
|
--col: 150, 150, 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.constraint {
|
&.constraint {
|
||||||
@@ -33,10 +32,6 @@ span {
|
|||||||
--col: 193, 108, 233;
|
--col: 193, 108, 233;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.simple-type-expr {
|
|
||||||
--col: 150, 150, 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.logical-expr,
|
&.logical-expr,
|
||||||
&.binary-expr,
|
&.binary-expr,
|
||||||
&.unary-expr,
|
&.unary-expr,
|
||||||
@@ -48,7 +43,9 @@ span {
|
|||||||
--col: 163, 117, 71;
|
--col: 163, 117, 71;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type {
|
&.type-name,
|
||||||
|
&.op-name,
|
||||||
|
&.predicate-name {
|
||||||
--col: 200, 200, 200;
|
--col: 200, 200, 200;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ span {
|
|||||||
--col: 108, 233, 108;
|
--col: 108, 233, 108;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.param {
|
&.arg {
|
||||||
--col: 103, 192, 224;
|
--col: 103, 192, 224;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,179 +1,25 @@
|
|||||||
import ast
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional, TextIO, get_args
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import midas.ast.midas as m
|
from midas.cli import commands
|
||||||
import midas.ast.python as p
|
|
||||||
from midas.ast.location import Location
|
|
||||||
from midas.ast.printer import MidasAstPrinter, PythonAstPrinter
|
|
||||||
from midas.checker.checker import Checker
|
|
||||||
from midas.checker.diagnostic import Diagnostic
|
|
||||||
from midas.checker.types import Type
|
|
||||||
from midas.cli.highlighter import (
|
|
||||||
DiagnosticsHighlighter,
|
|
||||||
Highlighter,
|
|
||||||
MidasHighlighter,
|
|
||||||
PythonHighlighter,
|
|
||||||
)
|
|
||||||
from midas.lexer.midas import MidasLexer
|
|
||||||
from midas.lexer.token import Token, TokenType
|
|
||||||
from midas.parser.midas import MidasParser
|
|
||||||
from midas.parser.python import PythonParser
|
|
||||||
from midas.resolver.resolver import Resolver
|
|
||||||
from midas.utils import UniversalJSONDumper
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def midas():
|
@click.option("-v", "--verbose", is_flag=True)
|
||||||
click.echo("Welcome to Midas!")
|
def midas(verbose: bool):
|
||||||
|
logging.basicConfig(level=logging.DEBUG if verbose else logging.WARN)
|
||||||
|
|
||||||
|
|
||||||
@midas.command()
|
midas.add_command(commands.check)
|
||||||
@click.option("-l", "--highlight", type=click.File("w"))
|
midas.add_command(commands.compile)
|
||||||
@click.argument("file", type=click.File("r"))
|
midas.add_command(commands.format)
|
||||||
def compile(highlight: Optional[TextIO], file: TextIO):
|
midas.add_command(commands.highlight)
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
midas.add_command(commands.parse)
|
||||||
source: str = file.read()
|
midas.add_command(commands.dump_registry)
|
||||||
tree: ast.Module = ast.parse(source, filename=file.name)
|
midas.add_command(commands.types)
|
||||||
parser = PythonParser()
|
midas.add_command(commands.stubs)
|
||||||
stmts: list[p.Stmt] = parser.parse_module(tree)
|
midas.add_command(commands.validate)
|
||||||
resolver = Resolver()
|
|
||||||
resolver.resolve(*stmts)
|
|
||||||
checker = Checker(resolver.locals, file_path=Path(file.name).resolve())
|
|
||||||
diagnostics: list[Diagnostic] = checker.check(stmts)
|
|
||||||
for diagnostic in diagnostics:
|
|
||||||
print(diagnostic)
|
|
||||||
|
|
||||||
print(
|
|
||||||
json.dumps(
|
|
||||||
UniversalJSONDumper.dump(
|
|
||||||
checker.global_env,
|
|
||||||
[("Environment", "_children")],
|
|
||||||
lambda obj: isinstance(obj, get_args(Type)),
|
|
||||||
),
|
|
||||||
indent=4,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if highlight is not None:
|
|
||||||
highlighter = DiagnosticsHighlighter(source)
|
|
||||||
highlighter.highlight(diagnostics)
|
|
||||||
highlighter.dump(highlight)
|
|
||||||
|
|
||||||
|
|
||||||
@midas.group()
|
|
||||||
def utils():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def dump_python_ast(tree: ast.Module) -> str:
|
|
||||||
parser = PythonParser()
|
|
||||||
stmts: list[p.Stmt] = parser.parse_module(tree)
|
|
||||||
printer = PythonAstPrinter()
|
|
||||||
dump: str = ""
|
|
||||||
for stmt in stmts:
|
|
||||||
dump += printer.print(stmt)
|
|
||||||
dump += "\n"
|
|
||||||
return dump
|
|
||||||
|
|
||||||
|
|
||||||
def dump_midas_ast(source: str, filename: str) -> str:
|
|
||||||
lexer = MidasLexer(source, file=filename)
|
|
||||||
tokens: list[Token] = lexer.process()
|
|
||||||
parser = MidasParser(tokens)
|
|
||||||
stmts: list[m.Stmt] = parser.parse()
|
|
||||||
if len(parser.errors) != 0:
|
|
||||||
for err in parser.errors:
|
|
||||||
print(err.get_report())
|
|
||||||
raise RuntimeError("A parsing error occurred")
|
|
||||||
printer = MidasAstPrinter()
|
|
||||||
dump: str = ""
|
|
||||||
for stmt in stmts:
|
|
||||||
dump += printer.print(stmt)
|
|
||||||
dump += "\n"
|
|
||||||
return dump
|
|
||||||
|
|
||||||
|
|
||||||
@utils.command()
|
|
||||||
@click.option("-o", "--output", type=click.File("w"))
|
|
||||||
@click.option("-p", "--parse", is_flag=True)
|
|
||||||
@click.argument("file", type=click.File("r"))
|
|
||||||
def dump_ast(output: Optional[TextIO], parse: bool, file: TextIO):
|
|
||||||
source: str = file.read()
|
|
||||||
|
|
||||||
dump: str
|
|
||||||
if file.name.endswith(".py"):
|
|
||||||
tree: ast.Module = ast.parse(source, filename=file.name)
|
|
||||||
if parse:
|
|
||||||
dump = dump_python_ast(tree)
|
|
||||||
else:
|
|
||||||
dump = ast.dump(tree, indent=4)
|
|
||||||
elif file.name.endswith(".midas"):
|
|
||||||
dump = dump_midas_ast(source, file.name)
|
|
||||||
else:
|
|
||||||
raise ValueError("Unsupported file type")
|
|
||||||
|
|
||||||
if output is None:
|
|
||||||
click.echo(dump)
|
|
||||||
else:
|
|
||||||
output.write(dump)
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_python(source: str, path: str) -> Highlighter:
|
|
||||||
tree: ast.Module = ast.parse(source, filename=path)
|
|
||||||
parser = PythonParser()
|
|
||||||
stmts: list[p.Stmt] = parser.parse_module(tree)
|
|
||||||
highlighter = PythonHighlighter(source)
|
|
||||||
for stmt in stmts:
|
|
||||||
highlighter.highlight(stmt)
|
|
||||||
return highlighter
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_midas(source: str, path: str) -> Highlighter:
|
|
||||||
lexer = MidasLexer(source, file=path)
|
|
||||||
tokens: list[Token] = lexer.process()
|
|
||||||
parser = MidasParser(tokens)
|
|
||||||
stmts: list[m.Stmt] = parser.parse()
|
|
||||||
highlighter = MidasHighlighter(source)
|
|
||||||
for err in parser.errors:
|
|
||||||
print(err.get_report())
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class LocatableToken:
|
|
||||||
token: Token
|
|
||||||
|
|
||||||
@property
|
|
||||||
def location(self) -> Location:
|
|
||||||
return self.token.get_location()
|
|
||||||
|
|
||||||
for stmt in stmts:
|
|
||||||
highlighter.highlight(stmt)
|
|
||||||
for token in tokens:
|
|
||||||
if token.type == TokenType.COMMENT:
|
|
||||||
highlighter.wrap(LocatableToken(token), "comment")
|
|
||||||
elif token.is_keyword:
|
|
||||||
highlighter.wrap(LocatableToken(token), "keyword")
|
|
||||||
return highlighter
|
|
||||||
|
|
||||||
|
|
||||||
@utils.command()
|
|
||||||
@click.option("-o", "--output", type=click.File("w"), default="-")
|
|
||||||
@click.argument("file", type=click.File("r"))
|
|
||||||
def highlight(output: TextIO, file: TextIO):
|
|
||||||
source: str = file.read()
|
|
||||||
highlighter: Highlighter
|
|
||||||
|
|
||||||
if file.name.endswith(".py"):
|
|
||||||
highlighter = highlight_python(source, file.name)
|
|
||||||
elif file.name.endswith(".midas"):
|
|
||||||
highlighter = highlight_midas(source, file.name)
|
|
||||||
else:
|
|
||||||
raise ValueError("Unsupported file type")
|
|
||||||
highlighter.dump(output)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
121
midas/cli/utils.py
Normal file
121
midas/cli/utils.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
||||||
|
from midas.cli.ansi import Ansi
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticPrinter:
|
||||||
|
COLORS: dict[DiagnosticType, int] = {
|
||||||
|
DiagnosticType.ERROR: Ansi.RED,
|
||||||
|
DiagnosticType.WARNING: Ansi.YELLOW,
|
||||||
|
DiagnosticType.INFO: Ansi.CYAN,
|
||||||
|
DiagnosticType.DEBUG: Ansi.MAGENTA,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.files: dict[Optional[str], list[str]] = {}
|
||||||
|
|
||||||
|
def get_lines(self, filename: Optional[str]) -> list[str]:
|
||||||
|
if filename is None:
|
||||||
|
return []
|
||||||
|
if filename not in self.files:
|
||||||
|
path: Path = Path(filename)
|
||||||
|
if path.exists() and path.is_file():
|
||||||
|
self.files[filename] = path.read_text().split("\n")
|
||||||
|
else:
|
||||||
|
self.files[filename] = []
|
||||||
|
return self.files[filename]
|
||||||
|
|
||||||
|
def print_all(self, diagnostics: list[Diagnostic], indent: int = 4):
|
||||||
|
by_type: dict[DiagnosticType, int] = defaultdict(int)
|
||||||
|
for diagnostic in diagnostics:
|
||||||
|
filename: Optional[str] = diagnostic.file_path
|
||||||
|
lines = self.get_lines(filename)
|
||||||
|
self.print(lines, diagnostic, indent=indent)
|
||||||
|
by_type[diagnostic.type] += 1
|
||||||
|
|
||||||
|
if len(diagnostics) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
counts: list[str] = []
|
||||||
|
for type in DiagnosticType:
|
||||||
|
if type not in by_type:
|
||||||
|
continue
|
||||||
|
count: int = by_type[type]
|
||||||
|
color: int = self.COLORS.get(type, Ansi.WHITE)
|
||||||
|
counts.append(f"{Ansi.FG(color)}{type.value}s{Ansi.RESET}: {count}")
|
||||||
|
|
||||||
|
print(" ".join(counts))
|
||||||
|
|
||||||
|
def print(self, lines: list[str], diagnostic: Diagnostic, indent: int = 4):
|
||||||
|
"""Pretty-print a diagnostic, showing some context if possible
|
||||||
|
|
||||||
|
If the diagnostic concerns a specific part of one line, the line is shown
|
||||||
|
with the affected part highlighted. The message is clearly printed under the
|
||||||
|
line with an underline further indicating the target expression.
|
||||||
|
|
||||||
|
If multiple lines are concerned, no context is shown, only the
|
||||||
|
diagnostic type, location and message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lines (list[str]): source code lines
|
||||||
|
diagnostic (Diagnostic): the diagnostic to print
|
||||||
|
indent (int, optional): the number of spaces added before the target line to indent if from the location header. Defaults to 4.
|
||||||
|
"""
|
||||||
|
|
||||||
|
loc: Location = diagnostic.location
|
||||||
|
if loc.lineno != loc.end_lineno:
|
||||||
|
self.print_multiline(lines, diagnostic, indent)
|
||||||
|
return
|
||||||
|
|
||||||
|
start_offset: int = loc.col_offset
|
||||||
|
end_offset: int = loc.end_col_offset or (start_offset + 1)
|
||||||
|
|
||||||
|
line: str = lines[loc.lineno - 1]
|
||||||
|
before: str = line[:start_offset]
|
||||||
|
after: str = line[end_offset:]
|
||||||
|
|
||||||
|
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||||
|
|
||||||
|
subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET
|
||||||
|
cursor: str = (
|
||||||
|
" " * start_offset
|
||||||
|
+ Ansi.FG(color)
|
||||||
|
+ "~" * (end_offset - start_offset)
|
||||||
|
+ "> "
|
||||||
|
+ diagnostic.message
|
||||||
|
+ Ansi.RESET
|
||||||
|
)
|
||||||
|
|
||||||
|
indent_str: str = " " * indent
|
||||||
|
print(diagnostic.location_str + ":")
|
||||||
|
print(indent_str + before + subject + after)
|
||||||
|
print(indent_str + cursor)
|
||||||
|
print()
|
||||||
|
|
||||||
|
def print_multiline(
|
||||||
|
self, all_lines: list[str], diagnostic: Diagnostic, indent: int = 4
|
||||||
|
):
|
||||||
|
loc: Location = diagnostic.location
|
||||||
|
lines: list[str] = all_lines[loc.lineno - 1 : loc.end_lineno]
|
||||||
|
|
||||||
|
start_offset: int = loc.col_offset
|
||||||
|
end_offset: int = loc.end_col_offset or (start_offset + 1)
|
||||||
|
|
||||||
|
indent_str: str = " " * indent
|
||||||
|
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||||
|
res: str = indent_str + lines[0][:start_offset]
|
||||||
|
res += Ansi.FG(color) + lines[0][start_offset:]
|
||||||
|
for line in lines[1:-1]:
|
||||||
|
res += "\n" + indent_str + line
|
||||||
|
res += "\n" + indent_str + lines[-1][:end_offset]
|
||||||
|
res += Ansi.RESET + lines[-1][end_offset:]
|
||||||
|
|
||||||
|
print(diagnostic.location_str + ":")
|
||||||
|
print(res)
|
||||||
|
print()
|
||||||
|
print(Ansi.FG(color) + diagnostic.message + Ansi.RESET)
|
||||||
|
print()
|
||||||
59
midas/generator/collector.py
Normal file
59
midas/generator/collector.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import ast
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
|
||||||
|
AssertionBuilder = Callable[..., ast.expr]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Assertion:
|
||||||
|
bound_expr: p.Expr
|
||||||
|
inputs: list[p.Expr]
|
||||||
|
builder: AssertionBuilder
|
||||||
|
message: str
|
||||||
|
|
||||||
|
def is_bound_to(self, expr: p.Expr) -> bool:
|
||||||
|
return expr == self.bound_expr
|
||||||
|
|
||||||
|
|
||||||
|
class AssertionCollector:
|
||||||
|
def __init__(self):
|
||||||
|
self.assertions: list[Assertion] = []
|
||||||
|
self.definitions: dict[str, ast.stmt] = {}
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
bound_expr: p.Expr,
|
||||||
|
inputs: list[p.Expr],
|
||||||
|
builder: AssertionBuilder,
|
||||||
|
message: str,
|
||||||
|
):
|
||||||
|
self.assertions.append(
|
||||||
|
Assertion(
|
||||||
|
bound_expr=bound_expr,
|
||||||
|
inputs=inputs,
|
||||||
|
builder=builder,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove(self, assertion: Assertion):
|
||||||
|
try:
|
||||||
|
self.assertions.remove(assertion)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def define(self, name: str, stmt: ast.stmt):
|
||||||
|
if name not in self.definitions:
|
||||||
|
self.definitions[name] = stmt
|
||||||
|
|
||||||
|
def get_definitions(self) -> list[ast.stmt]:
|
||||||
|
return list(self.definitions.values())
|
||||||
|
|
||||||
|
def get_assertions(self) -> list[Assertion]:
|
||||||
|
return self.assertions
|
||||||
|
|
||||||
|
def get_assertions_for(self, expr: p.Expr) -> list[Assertion]:
|
||||||
|
return list(filter(lambda a: a.is_bound_to(expr), self.assertions))
|
||||||
224
midas/generator/constraints.py
Normal file
224
midas/generator/constraints.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import ast
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
Function,
|
||||||
|
Predicate,
|
||||||
|
Type,
|
||||||
|
to_annotation,
|
||||||
|
)
|
||||||
|
from midas.lexer.token import TokenType
|
||||||
|
|
||||||
|
LOGICAL_OPERATORS: dict[TokenType, type[ast.boolop]] = {
|
||||||
|
TokenType.AND: ast.And,
|
||||||
|
# TokenType.OR: ast.Or,
|
||||||
|
}
|
||||||
|
|
||||||
|
BINARY_OPERATORS: dict[TokenType, type[ast.operator]] = {
|
||||||
|
# TokenType.PLUS: ast.Add,
|
||||||
|
TokenType.MINUS: ast.Sub,
|
||||||
|
TokenType.STAR: ast.Mult,
|
||||||
|
TokenType.SLASH: ast.Div,
|
||||||
|
}
|
||||||
|
|
||||||
|
UNARY_OPERATORS: dict[TokenType, type[ast.unaryop]] = {
|
||||||
|
# TokenType.PLUS: ast.UAdd,
|
||||||
|
TokenType.MINUS: ast.USub,
|
||||||
|
}
|
||||||
|
|
||||||
|
COMPARISON_OPERATORS: dict[TokenType, type[ast.cmpop]] = {
|
||||||
|
TokenType.GREATER: ast.Gt,
|
||||||
|
TokenType.GREATER_EQUAL: ast.GtE,
|
||||||
|
TokenType.LESS: ast.Lt,
|
||||||
|
TokenType.LESS_EQUAL: ast.LtE,
|
||||||
|
TokenType.EQUAL_EQUAL: ast.Eq,
|
||||||
|
TokenType.BANG_EQUAL: ast.NotEq,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintGenerator(m.Expr.Visitor[ast.expr]):
|
||||||
|
def __init__(self, types: TypesRegistry):
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self._id: int = 0
|
||||||
|
self._definitions: list[ast.stmt] = []
|
||||||
|
self._aliases: dict[str, str] = {}
|
||||||
|
|
||||||
|
def get_definitions(self) -> list[ast.stmt]:
|
||||||
|
return self._definitions
|
||||||
|
|
||||||
|
def generate(self, expr: m.Expr) -> ast.expr:
|
||||||
|
match expr:
|
||||||
|
case m.VariableExpr():
|
||||||
|
return expr.accept(self)
|
||||||
|
case _:
|
||||||
|
func = Function(
|
||||||
|
pos_args=[],
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="_",
|
||||||
|
type=self.types.get_type("Any"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
kw_args=[],
|
||||||
|
returns=self.types.get_type("bool"),
|
||||||
|
)
|
||||||
|
alias: str = self.make_alias(None)
|
||||||
|
definition: ast.stmt = self.make_definition(
|
||||||
|
alias, Predicate(type=func, body=expr, alias=False)
|
||||||
|
)
|
||||||
|
self._definitions.append(definition)
|
||||||
|
return ast.Name(id=alias)
|
||||||
|
|
||||||
|
def make_alias(self, name: Optional[str]) -> str:
|
||||||
|
suffix: str
|
||||||
|
if name is None:
|
||||||
|
suffix = f"p{self._id}"
|
||||||
|
self._id += 1
|
||||||
|
else:
|
||||||
|
suffix = name
|
||||||
|
alias: str = f"__midas_{suffix}__"
|
||||||
|
return alias
|
||||||
|
|
||||||
|
def make_definition(self, name: str, predicate: Predicate) -> ast.stmt:
|
||||||
|
body: ast.expr = predicate.body.accept(self)
|
||||||
|
if predicate.alias:
|
||||||
|
return ast.Assign(
|
||||||
|
targets=[
|
||||||
|
ast.Name(id=name),
|
||||||
|
],
|
||||||
|
value=body,
|
||||||
|
)
|
||||||
|
return self.make_func(name, [ast.Return(value=body)], predicate.type)
|
||||||
|
|
||||||
|
def make_args(self, func: Function) -> ast.arguments:
|
||||||
|
return ast.arguments(
|
||||||
|
posonlyargs=[
|
||||||
|
ast.arg(
|
||||||
|
arg=arg.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(arg.type)),
|
||||||
|
)
|
||||||
|
for arg in func.pos_args
|
||||||
|
],
|
||||||
|
args=[
|
||||||
|
ast.arg(
|
||||||
|
arg=arg.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(arg.type)),
|
||||||
|
)
|
||||||
|
for arg in func.args
|
||||||
|
],
|
||||||
|
kwonlyargs=[
|
||||||
|
ast.arg(
|
||||||
|
arg=arg.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(arg.type)),
|
||||||
|
)
|
||||||
|
for arg in func.kw_args
|
||||||
|
],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_func(
|
||||||
|
self, name: str, inner_body: list[ast.stmt], type: Type, level: int = 0
|
||||||
|
) -> ast.stmt:
|
||||||
|
match type:
|
||||||
|
case Function(returns=Function()):
|
||||||
|
inner_name: str = f"inner{level}"
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.make_args(type),
|
||||||
|
body=[
|
||||||
|
self.make_func(inner_name, inner_body, type.returns, level + 1),
|
||||||
|
ast.Return(value=ast.Name(id=inner_name)),
|
||||||
|
],
|
||||||
|
returns=ast.Constant(value=to_annotation(type.returns)),
|
||||||
|
decorator_list=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case Function():
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.make_args(type),
|
||||||
|
body=inner_body,
|
||||||
|
returns=ast.Constant(value=to_annotation(type.returns)),
|
||||||
|
decorator_list=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Expected function, got {type!r}")
|
||||||
|
|
||||||
|
def get_predicate(self, name: str) -> Optional[ast.expr]:
|
||||||
|
if name not in self._aliases:
|
||||||
|
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
||||||
|
if predicate is None:
|
||||||
|
return None
|
||||||
|
alias: str = self.make_alias(name)
|
||||||
|
self._aliases[name] = alias
|
||||||
|
self._definitions.append(self.make_definition(alias, predicate))
|
||||||
|
|
||||||
|
return ast.Name(id=self._aliases[name])
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: m.LogicalExpr) -> ast.expr:
|
||||||
|
return ast.BoolOp(
|
||||||
|
op=LOGICAL_OPERATORS[expr.operator.type](),
|
||||||
|
values=[
|
||||||
|
expr.left.accept(self),
|
||||||
|
expr.right.accept(self),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: m.BinaryExpr) -> ast.expr:
|
||||||
|
op: TokenType = expr.operator.type
|
||||||
|
if op in BINARY_OPERATORS:
|
||||||
|
return ast.BinOp(
|
||||||
|
left=expr.left.accept(self),
|
||||||
|
op=BINARY_OPERATORS[op](),
|
||||||
|
right=expr.right.accept(self),
|
||||||
|
)
|
||||||
|
if op in COMPARISON_OPERATORS:
|
||||||
|
return ast.Compare(
|
||||||
|
left=expr.left.accept(self),
|
||||||
|
ops=[COMPARISON_OPERATORS[op]()],
|
||||||
|
comparators=[expr.right.accept(self)],
|
||||||
|
)
|
||||||
|
raise ValueError(f"Unexpected binary operator {op}")
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: m.UnaryExpr) -> ast.expr:
|
||||||
|
return ast.UnaryOp(
|
||||||
|
op=UNARY_OPERATORS[expr.operator.type](),
|
||||||
|
operand=expr.right.accept(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: m.CallExpr) -> ast.expr:
|
||||||
|
return ast.Call(
|
||||||
|
func=expr.callee.accept(self),
|
||||||
|
args=[arg.accept(self) for arg in expr.arguments],
|
||||||
|
keywords=[
|
||||||
|
ast.keyword(arg=name, value=arg.accept(self))
|
||||||
|
for name, arg in expr.keywords.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_get_expr(self, expr: m.GetExpr) -> ast.expr:
|
||||||
|
return ast.Attribute(
|
||||||
|
value=expr.expr.accept(self),
|
||||||
|
attr=expr.name.lexeme,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: m.VariableExpr) -> ast.expr:
|
||||||
|
name: str = expr.name.lexeme
|
||||||
|
if (p := self.get_predicate(name)) is not None:
|
||||||
|
return p
|
||||||
|
return ast.Name(id=name)
|
||||||
|
|
||||||
|
def visit_grouping_expr(self, expr: m.GroupingExpr) -> ast.expr:
|
||||||
|
return expr.accept(self)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: m.LiteralExpr) -> ast.expr:
|
||||||
|
return ast.Constant(value=expr.value)
|
||||||
|
|
||||||
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> ast.expr:
|
||||||
|
return ast.Name(id="_")
|
||||||
689
midas/generator/generator.py
Normal file
689
midas/generator/generator.py
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
import ast
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, assert_never
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.ast.printer import MidasPrinter
|
||||||
|
from midas.checker.checker import TypeChecker
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
BaseType,
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
ComplexType,
|
||||||
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
|
ExtensionType,
|
||||||
|
FrameGroupBy,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
TopType,
|
||||||
|
TupleType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
from midas.generator.collector import Assertion, AssertionCollector
|
||||||
|
from midas.generator.constraints import ConstraintGenerator
|
||||||
|
from midas.generator.stubs import StubsGenerator
|
||||||
|
from midas.utils import TypedAST
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Scope:
|
||||||
|
pre_assertions: list[ast.stmt] = field(default_factory=list[ast.stmt])
|
||||||
|
aliases: list[str] = field(default_factory=list[str])
|
||||||
|
|
||||||
|
|
||||||
|
class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||||
|
IS_DATAFRAME_FUNC = "__midas_is_dataframe__"
|
||||||
|
IS_COLUMN_FUNC = "__midas_is_column__"
|
||||||
|
|
||||||
|
def __init__(self, workdir: Path, types: TypesRegistry) -> None:
|
||||||
|
self.workdir: Path = workdir.resolve()
|
||||||
|
self.build_dir: Path = self.workdir / "build" / "midas"
|
||||||
|
self.rel_src_path: Path = Path()
|
||||||
|
self.logger: logging.Logger = logging.getLogger("Generator")
|
||||||
|
|
||||||
|
self._typed_ast: TypedAST = TypedAST(
|
||||||
|
stmts=[],
|
||||||
|
judgements=[],
|
||||||
|
evaluated_casts=[],
|
||||||
|
assertions=AssertionCollector(),
|
||||||
|
)
|
||||||
|
self._alias_count: int = 0
|
||||||
|
self._predicate_count: int = 0
|
||||||
|
self._scopes: list[Scope] = []
|
||||||
|
self._aliases: list[tuple[p.Expr, ast.expr]] = []
|
||||||
|
|
||||||
|
self._constraint_generator: ConstraintGenerator = ConstraintGenerator(types)
|
||||||
|
self._constraints: list[tuple[m.Expr, ast.expr]] = []
|
||||||
|
|
||||||
|
self.define_is_dataframe: bool = False
|
||||||
|
self.define_is_column: bool = False
|
||||||
|
|
||||||
|
def set_src_path(self, path: Path):
|
||||||
|
self.rel_src_path = path.resolve().relative_to(self.workdir)
|
||||||
|
|
||||||
|
def generate_ast(self, typed_ast: TypedAST) -> ast.AST:
|
||||||
|
self._typed_ast = typed_ast
|
||||||
|
body: list[ast.stmt] = self._visit_body(typed_ast.stmts, can_be_empty=True)
|
||||||
|
predicates: list[ast.stmt] = self._constraint_generator.get_definitions()
|
||||||
|
|
||||||
|
body = predicates + body
|
||||||
|
|
||||||
|
if self.define_is_dataframe:
|
||||||
|
body = [self._is_dataframe_definition()] + body
|
||||||
|
|
||||||
|
if self.define_is_column:
|
||||||
|
body = [self._is_column_definition()] + body
|
||||||
|
|
||||||
|
module = ast.Module(body=body, type_ignores=[])
|
||||||
|
module = ast.fix_missing_locations(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
typed_ast: TypedAST,
|
||||||
|
src_path: Path,
|
||||||
|
out_path: Optional[Path] = None,
|
||||||
|
type_files: Optional[list[tuple[Path, Optional[str]]]] = None,
|
||||||
|
) -> Path:
|
||||||
|
self.set_src_path(src_path)
|
||||||
|
if out_path is None:
|
||||||
|
if self.build_dir.exists():
|
||||||
|
shutil.rmtree(self.build_dir)
|
||||||
|
self.build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_path = (self.build_dir / self.rel_src_path).resolve()
|
||||||
|
try:
|
||||||
|
_ = out_path.relative_to(self.build_dir)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Directory traversal, {self.rel_src_path} points outside of parent directory"
|
||||||
|
)
|
||||||
|
out_dir: Path = out_path.parent
|
||||||
|
out_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if type_files is not None:
|
||||||
|
for in_path, out_name in type_files:
|
||||||
|
if out_name is None:
|
||||||
|
out_name = in_path.stem
|
||||||
|
self.generate_stubs(in_path, out_dir / f"{out_name}.py")
|
||||||
|
|
||||||
|
module: ast.AST = self.generate_ast(typed_ast)
|
||||||
|
compiled: str = ast.unparse(module)
|
||||||
|
|
||||||
|
out_path.write_text(compiled)
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
def generate_stubs(self, in_path: Path, out_path: Path):
|
||||||
|
checker = TypeChecker()
|
||||||
|
checker.import_midas(in_path)
|
||||||
|
generator = StubsGenerator(checker.types)
|
||||||
|
module: ast.Module = generator.generate_stubs()
|
||||||
|
module = ast.fix_missing_locations(module)
|
||||||
|
output: str = ast.unparse(module)
|
||||||
|
out_path.write_text(output)
|
||||||
|
|
||||||
|
def convert(self, expr: p.Expr) -> ast.expr:
|
||||||
|
for expr2, alias in self._aliases:
|
||||||
|
if expr2 == expr:
|
||||||
|
return alias
|
||||||
|
assertions = self._typed_ast.assertions.get_assertions_for(expr)
|
||||||
|
if len(assertions) != 0:
|
||||||
|
return self._apply_assertions(expr, assertions)
|
||||||
|
return expr.accept(self)
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: p.BinaryExpr) -> ast.expr:
|
||||||
|
return ast.BinOp(
|
||||||
|
left=self.convert(expr.left),
|
||||||
|
op=expr.operator,
|
||||||
|
right=self.convert(expr.right),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_compare_expr(self, expr: p.CompareExpr) -> ast.expr:
|
||||||
|
return ast.Compare(
|
||||||
|
left=self.convert(expr.left),
|
||||||
|
ops=[expr.operator],
|
||||||
|
comparators=[self.convert(expr.right)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: p.UnaryExpr) -> ast.expr:
|
||||||
|
return ast.UnaryOp(
|
||||||
|
op=expr.operator,
|
||||||
|
operand=self.convert(expr.right),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: p.CallExpr) -> ast.expr:
|
||||||
|
return ast.Call(
|
||||||
|
func=self.convert(expr.callee),
|
||||||
|
args=[self.convert(arg) for arg in expr.arguments],
|
||||||
|
keywords=[
|
||||||
|
ast.keyword(arg=name, value=self.convert(arg))
|
||||||
|
for name, arg in expr.keywords.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_get_expr(self, expr: p.GetExpr) -> ast.expr:
|
||||||
|
return ast.Attribute(
|
||||||
|
value=self.convert(expr.object),
|
||||||
|
attr=expr.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: p.LiteralExpr) -> ast.expr:
|
||||||
|
return ast.Constant(value=expr.value)
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: p.VariableExpr) -> ast.expr:
|
||||||
|
return ast.Name(id=expr.name)
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: p.LogicalExpr) -> ast.expr:
|
||||||
|
return ast.BoolOp(
|
||||||
|
op=expr.operator,
|
||||||
|
values=[self.convert(expr.left), self.convert(expr.right)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_cast_expr(self, expr: p.CastExpr) -> ast.expr:
|
||||||
|
expr2: ast.expr = self.convert(expr.expr)
|
||||||
|
|
||||||
|
if expr in self._typed_ast.evaluated_casts or expr.unsafe:
|
||||||
|
return expr2
|
||||||
|
|
||||||
|
alias: ast.expr = self._make_alias(expr.expr, expr2)
|
||||||
|
|
||||||
|
type: Type = self._get_expr_type(expr)
|
||||||
|
asserts: list[ast.stmt] = self._make_cast_asserts(expr.location, alias, type)
|
||||||
|
for assert_ in asserts:
|
||||||
|
self._add_assert(assert_)
|
||||||
|
|
||||||
|
return alias
|
||||||
|
|
||||||
|
def visit_ternary_expr(self, expr: p.TernaryExpr) -> ast.expr:
|
||||||
|
return ast.IfExp(
|
||||||
|
test=self.convert(expr.test),
|
||||||
|
body=self.convert(expr.if_true),
|
||||||
|
orelse=self.convert(expr.if_false),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_list_expr(self, expr: p.ListExpr) -> ast.expr:
|
||||||
|
return ast.List(
|
||||||
|
elts=[self.convert(item) for item in expr.items],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_dict_expr(self, expr: p.DictExpr) -> ast.expr:
|
||||||
|
return ast.Dict(
|
||||||
|
keys=[self.convert(key) if key is not None else None for key in expr.keys],
|
||||||
|
values=[self.convert(value) for value in expr.values],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> ast.expr:
|
||||||
|
return ast.Subscript(
|
||||||
|
value=self.convert(expr.object),
|
||||||
|
slice=self.convert(expr.index),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_slice_expr(self, expr: p.SliceExpr) -> ast.expr:
|
||||||
|
return ast.Slice(
|
||||||
|
lower=self.convert(expr.lower) if expr.lower is not None else None,
|
||||||
|
upper=self.convert(expr.upper) if expr.upper is not None else None,
|
||||||
|
step=self.convert(expr.step) if expr.step is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_tuple_expr(self, expr: p.TupleExpr) -> ast.expr:
|
||||||
|
return ast.Tuple(
|
||||||
|
elts=[self.convert(item) for item in expr.items],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_raw_expr(self, expr: p.RawExpr) -> ast.expr:
|
||||||
|
return expr.expr
|
||||||
|
|
||||||
|
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> ast.stmt:
|
||||||
|
return ast.Expr(
|
||||||
|
value=self.convert(stmt.expr),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_function(self, stmt: p.Function) -> ast.stmt:
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=stmt.name,
|
||||||
|
args=ast.arguments(
|
||||||
|
posonlyargs=[ast.arg(arg=arg.name) for arg in stmt.posonlyargs],
|
||||||
|
vararg=None,
|
||||||
|
args=[ast.arg(arg=arg.name) for arg in stmt.args],
|
||||||
|
kwonlyargs=[ast.arg(arg=arg.name) for arg in stmt.kwonlyargs],
|
||||||
|
kwarg=None,
|
||||||
|
defaults=[
|
||||||
|
self.convert(arg.default)
|
||||||
|
for arg in stmt.posonlyargs + stmt.args
|
||||||
|
if arg.default is not None
|
||||||
|
],
|
||||||
|
kw_defaults=[
|
||||||
|
self.convert(arg.default) if arg.default is not None else None
|
||||||
|
for arg in stmt.kwonlyargs
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body=self._visit_body(stmt.body),
|
||||||
|
decorator_list=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_type_assign(self, stmt: p.TypeAssign) -> ast.stmt:
|
||||||
|
# TODO: is that ok?
|
||||||
|
return ast.Pass()
|
||||||
|
|
||||||
|
def visit_assign_stmt(self, stmt: p.AssignStmt) -> ast.stmt:
|
||||||
|
return ast.Assign(
|
||||||
|
targets=[self.convert(target) for target in stmt.targets],
|
||||||
|
value=self.convert(stmt.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_return_stmt(self, stmt: p.ReturnStmt) -> ast.stmt:
|
||||||
|
return ast.Return(
|
||||||
|
value=self.convert(stmt.value) if stmt.value is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_if_stmt(self, stmt: p.IfStmt) -> ast.stmt:
|
||||||
|
return ast.If(
|
||||||
|
test=self.convert(stmt.test),
|
||||||
|
body=self._visit_body(stmt.body),
|
||||||
|
orelse=self._visit_body(stmt.orelse, can_be_empty=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_pass(self, stmt: p.Pass) -> ast.stmt:
|
||||||
|
return ast.Pass()
|
||||||
|
|
||||||
|
def visit_for_stmt(self, stmt: p.ForStmt) -> ast.stmt:
|
||||||
|
return ast.For(
|
||||||
|
target=self.convert(stmt.target),
|
||||||
|
iter=self.convert(stmt.iterator),
|
||||||
|
body=self._visit_body(stmt.body),
|
||||||
|
orelse=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> ast.stmt:
|
||||||
|
return stmt.stmt
|
||||||
|
|
||||||
|
def _visit_body(
|
||||||
|
self, stmts: list[p.Stmt], can_be_empty: bool = False
|
||||||
|
) -> list[ast.stmt]:
|
||||||
|
generated: list[ast.stmt] = []
|
||||||
|
for stmt in stmts:
|
||||||
|
scope = Scope()
|
||||||
|
self._scopes.append(scope)
|
||||||
|
|
||||||
|
stmt2 = stmt.accept(self)
|
||||||
|
generated.extend(scope.pre_assertions)
|
||||||
|
generated.append(stmt2)
|
||||||
|
if len(scope.aliases) != 0:
|
||||||
|
generated.append(
|
||||||
|
ast.Delete(targets=[ast.Name(id=alias) for alias in scope.aliases])
|
||||||
|
)
|
||||||
|
self._scopes.pop()
|
||||||
|
|
||||||
|
# Remove redundant pass statements
|
||||||
|
if len(generated) > 1:
|
||||||
|
generated = [stmt for stmt in generated if not isinstance(stmt, ast.Pass)]
|
||||||
|
if len(generated) == 0 and not can_be_empty:
|
||||||
|
generated = [ast.Pass()]
|
||||||
|
return generated
|
||||||
|
|
||||||
|
def _make_alias(self, node: p.Expr, expr: ast.expr) -> ast.expr:
|
||||||
|
name: str = f"__midas_a{self._alias_count}__"
|
||||||
|
alias = ast.Name(id=name)
|
||||||
|
self._alias_count += 1
|
||||||
|
self._scopes[-1].aliases.append(name)
|
||||||
|
self._scopes[-1].pre_assertions.append(
|
||||||
|
ast.Assign(
|
||||||
|
targets=[alias],
|
||||||
|
value=expr,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._aliases.append((node, alias))
|
||||||
|
return alias
|
||||||
|
|
||||||
|
def _build_assert(self, expr: ast.expr, message: str | ast.expr) -> ast.stmt:
|
||||||
|
if isinstance(message, str):
|
||||||
|
message = ast.Constant(value=message)
|
||||||
|
return ast.Assert(
|
||||||
|
test=expr,
|
||||||
|
msg=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_assert(self, assertion: ast.stmt):
|
||||||
|
self._scopes[-1].pre_assertions.append(assertion)
|
||||||
|
|
||||||
|
def _get_expr_type(self, query: p.Expr) -> Type:
|
||||||
|
for expr, type in self._typed_ast.judgements:
|
||||||
|
if expr == query:
|
||||||
|
return type
|
||||||
|
raise RuntimeError(f"Cannot get type judgement for {query}")
|
||||||
|
|
||||||
|
def _make_cast_asserts(
|
||||||
|
self, src_location: Location, expr: ast.expr, type: Type
|
||||||
|
) -> list[ast.stmt]:
|
||||||
|
match type:
|
||||||
|
case UnknownType() | TopType():
|
||||||
|
return []
|
||||||
|
|
||||||
|
case BaseType(name=name):
|
||||||
|
return [
|
||||||
|
self._build_assert(
|
||||||
|
ast.Call(
|
||||||
|
func=ast.Name(id="isinstance"),
|
||||||
|
args=[expr, ast.Name(id=name)],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(src_location, expr, type),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
case DerivedType(type=base):
|
||||||
|
return self._make_cast_asserts(src_location, expr, base)
|
||||||
|
|
||||||
|
case UnitType():
|
||||||
|
return [
|
||||||
|
self._build_assert(
|
||||||
|
ast.Compare(
|
||||||
|
left=expr,
|
||||||
|
ops=[ast.Is()],
|
||||||
|
comparators=[
|
||||||
|
ast.Constant(value=None),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(src_location, expr, type),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
case AppliedType(body=body):
|
||||||
|
return self._make_cast_asserts(src_location, expr, body)
|
||||||
|
|
||||||
|
case ConstraintType(type=base, constraint=constraint):
|
||||||
|
asserts: list[ast.stmt] = self._make_cast_asserts(
|
||||||
|
src_location, expr, base
|
||||||
|
)
|
||||||
|
asserts.append(
|
||||||
|
self._make_constraint_assert(src_location, expr, constraint)
|
||||||
|
)
|
||||||
|
return asserts
|
||||||
|
|
||||||
|
case TypeVar(bound=bound):
|
||||||
|
# TODO: check with type from arguments / use call-site context
|
||||||
|
if bound is None:
|
||||||
|
return []
|
||||||
|
return self._make_cast_asserts(src_location, expr, bound)
|
||||||
|
|
||||||
|
case TupleType(items=items):
|
||||||
|
asserts: list[ast.stmt] = [
|
||||||
|
self._build_assert(
|
||||||
|
ast.Call(
|
||||||
|
func=ast.Name(id="isinstance"),
|
||||||
|
args=[expr, ast.Name(id="tuple")],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(src_location, expr, type),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert isinstance(expr, ast.Tuple)
|
||||||
|
for item, item_type in zip(expr.elts, items):
|
||||||
|
asserts.extend(
|
||||||
|
self._make_cast_asserts(src_location, item, item_type)
|
||||||
|
)
|
||||||
|
return asserts
|
||||||
|
|
||||||
|
case DataFrameType(columns=columns):
|
||||||
|
self.define_is_dataframe = True
|
||||||
|
asserts: list[ast.stmt] = [
|
||||||
|
self._build_assert(
|
||||||
|
ast.Call(
|
||||||
|
func=ast.Name(id=self.IS_DATAFRAME_FUNC),
|
||||||
|
args=[expr],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(
|
||||||
|
src_location, expr, type, ": Not a dataframe"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for column in columns:
|
||||||
|
asserts.append(
|
||||||
|
self._build_assert(
|
||||||
|
ast.Compare(
|
||||||
|
left=ast.Constant(value=column.name),
|
||||||
|
ops=[ast.In()],
|
||||||
|
comparators=[expr],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(
|
||||||
|
src_location,
|
||||||
|
expr,
|
||||||
|
type,
|
||||||
|
f": Missing column {column.name}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
asserts.extend(
|
||||||
|
self._make_cast_asserts(
|
||||||
|
src_location,
|
||||||
|
ast.Subscript(
|
||||||
|
value=expr, slice=ast.Constant(value=column.name)
|
||||||
|
),
|
||||||
|
column.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return asserts
|
||||||
|
|
||||||
|
case ColumnType():
|
||||||
|
self.define_is_column = True
|
||||||
|
asserts: list[ast.stmt] = [
|
||||||
|
self._build_assert(
|
||||||
|
ast.Call(
|
||||||
|
func=ast.Name(id=self.IS_COLUMN_FUNC),
|
||||||
|
args=[expr],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
self._make_cast_assert_message(
|
||||||
|
src_location, expr, type, ": Not a column"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
inner_assert: Optional[ast.stmt] = self._make_column_inner_assert(
|
||||||
|
src_location, expr, type
|
||||||
|
)
|
||||||
|
if inner_assert is not None:
|
||||||
|
asserts.append(inner_assert)
|
||||||
|
return asserts
|
||||||
|
|
||||||
|
case (
|
||||||
|
Function()
|
||||||
|
| OverloadedFunction()
|
||||||
|
| ComplexType()
|
||||||
|
| ExtensionType()
|
||||||
|
| GenericType()
|
||||||
|
| FrameGroupBy()
|
||||||
|
| ColumnGroupBy()
|
||||||
|
):
|
||||||
|
self.logger.warning(f"Can't make assertion for type {type}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Ensure exhaustiveness
|
||||||
|
case _:
|
||||||
|
assert_never(type)
|
||||||
|
|
||||||
|
def _make_cast_assert_message(
|
||||||
|
self,
|
||||||
|
location: Location,
|
||||||
|
expr: ast.expr,
|
||||||
|
type: Type,
|
||||||
|
extra: Optional[str] = None,
|
||||||
|
) -> ast.expr:
|
||||||
|
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
|
||||||
|
# f"file.py:L1:1: CastError: Cannot cast {type(expr).__name__} to Type"
|
||||||
|
return ast.JoinedStr(
|
||||||
|
values=[
|
||||||
|
ast.Constant(f"{loc_str}: CastError: Cannot cast "),
|
||||||
|
ast.FormattedValue(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Call(
|
||||||
|
func=ast.Name(id="type"),
|
||||||
|
args=[expr],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
attr="__name__",
|
||||||
|
),
|
||||||
|
conversion=-1,
|
||||||
|
),
|
||||||
|
ast.Constant(f" to {type}{extra or ''}"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_constraint_assert(
|
||||||
|
self, src_location: Location, expr: ast.expr, constraint: m.Expr
|
||||||
|
) -> ast.stmt:
|
||||||
|
test_func: ast.expr = self._get_constraint(constraint)
|
||||||
|
return self._build_assert(
|
||||||
|
ast.Call(
|
||||||
|
func=test_func,
|
||||||
|
args=[expr],
|
||||||
|
keywords=[],
|
||||||
|
),
|
||||||
|
self._make_constraint_assert_message(src_location, expr, constraint),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_constraint_assert_message(
|
||||||
|
self, location: Location, expr: ast.expr, constraint: m.Expr
|
||||||
|
) -> ast.expr:
|
||||||
|
printer = MidasPrinter()
|
||||||
|
constraint_str: str = printer.print(constraint)
|
||||||
|
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
|
||||||
|
# f"file.py:L1:1: ConstraintError: Value does not fit constraint 'v > 0'"
|
||||||
|
return ast.Constant(
|
||||||
|
f"{loc_str}: ConstraintError: Value does not fit constraint '{constraint_str}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_constraint(self, expr: m.Expr) -> ast.expr:
|
||||||
|
for expr2, constraint in self._constraints:
|
||||||
|
if expr2 == expr:
|
||||||
|
return constraint
|
||||||
|
|
||||||
|
constraint: ast.expr = self._constraint_generator.generate(expr)
|
||||||
|
self._constraints.append((expr, constraint))
|
||||||
|
return constraint
|
||||||
|
|
||||||
|
def _is_dataframe_definition(self) -> ast.stmt:
|
||||||
|
"""
|
||||||
|
def IS_DATAFRAME_FUNC(obj) -> bool:
|
||||||
|
import pandas as pd
|
||||||
|
return isinstance(obj, pd.DataFrame)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=self.IS_DATAFRAME_FUNC,
|
||||||
|
args=ast.arguments(
|
||||||
|
posonlyargs=[ast.arg(arg="obj")],
|
||||||
|
args=[],
|
||||||
|
kwonlyargs=[],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
),
|
||||||
|
body=[
|
||||||
|
ast.Import(names=[ast.alias(name="pandas", asname="pd")]),
|
||||||
|
ast.Return(
|
||||||
|
value=ast.Call(
|
||||||
|
func=ast.Name(id="isinstance"),
|
||||||
|
args=[
|
||||||
|
ast.Name(id="obj"),
|
||||||
|
ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="DataFrame",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
keywords=[],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=ast.Name(id="bool"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_column_definition(self) -> ast.stmt:
|
||||||
|
"""
|
||||||
|
def IS_COLUMN_FUNC(obj) -> bool:
|
||||||
|
import pandas as pd
|
||||||
|
return isinstance(obj, pd.Series)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=self.IS_COLUMN_FUNC,
|
||||||
|
args=ast.arguments(
|
||||||
|
posonlyargs=[ast.arg(arg="obj")],
|
||||||
|
args=[],
|
||||||
|
kwonlyargs=[],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
),
|
||||||
|
body=[
|
||||||
|
ast.Import(names=[ast.alias(name="pandas", asname="pd")]),
|
||||||
|
ast.Return(
|
||||||
|
value=ast.Call(
|
||||||
|
func=ast.Name(id="isinstance"),
|
||||||
|
args=[
|
||||||
|
ast.Name(id="obj"),
|
||||||
|
ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="Series",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
keywords=[],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=ast.Name(id="bool"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_column_inner_assert(
|
||||||
|
self, src_location: Location, column: ast.expr, type: ColumnType
|
||||||
|
) -> Optional[ast.stmt]:
|
||||||
|
# TODO: improve message, maybe chain contexts
|
||||||
|
col: ast.expr = ast.Name(id="col")
|
||||||
|
body: list[ast.stmt] = self._make_cast_asserts(src_location, col, type.type)
|
||||||
|
if len(body) == 0:
|
||||||
|
return None
|
||||||
|
return ast.For(
|
||||||
|
target=col,
|
||||||
|
iter=column,
|
||||||
|
body=body,
|
||||||
|
orelse=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _convert_assertion(self, assertion: Assertion) -> ast.stmt:
|
||||||
|
inputs: list[ast.expr] = []
|
||||||
|
|
||||||
|
for input in assertion.inputs:
|
||||||
|
converted: ast.expr = self.convert(input)
|
||||||
|
alias: ast.expr = self._make_alias(input, converted)
|
||||||
|
inputs.append(alias)
|
||||||
|
|
||||||
|
test: ast.expr = assertion.builder(*inputs)
|
||||||
|
location: Location = assertion.bound_expr.location
|
||||||
|
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
|
||||||
|
return self._build_assert(
|
||||||
|
test, f"{loc_str}: AssertionError: {assertion.message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_assertions(self, expr: p.Expr, assertions: list[Assertion]) -> ast.expr:
|
||||||
|
for assertion in assertions:
|
||||||
|
assert_stmt: ast.stmt
|
||||||
|
assert_stmt = self._convert_assertion(assertion)
|
||||||
|
self._add_assert(assert_stmt)
|
||||||
|
|
||||||
|
# Mutating list in frozen dataclass
|
||||||
|
# Not ideal but easiest way to avoid duplicate assertions
|
||||||
|
self._typed_ast.assertions.remove(assertion)
|
||||||
|
|
||||||
|
return expr.accept(self)
|
||||||
470
midas/generator/stubs.py
Normal file
470
midas/generator/stubs.py
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import ast
|
||||||
|
from typing import Optional, assert_never
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.checker.registry import Member, TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
BaseType,
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
ComplexType,
|
||||||
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
|
ExtensionType,
|
||||||
|
FrameGroupBy,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
TopType,
|
||||||
|
TupleType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
UnknownType,
|
||||||
|
Variance,
|
||||||
|
substitute_typevars,
|
||||||
|
)
|
||||||
|
|
||||||
|
Empty = ast.Constant(value=...)
|
||||||
|
|
||||||
|
|
||||||
|
class StubsGenerator:
|
||||||
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.stubs: list[ast.stmt] = []
|
||||||
|
self.typing_imports: set[str] = set()
|
||||||
|
self.import_pandas: bool = False
|
||||||
|
self.protocol_idx: int = 0
|
||||||
|
self.stub_idx: int = 0
|
||||||
|
self.type_var_idx: int = 0
|
||||||
|
self.substitutions: dict[str, dict[str, Type]] = {}
|
||||||
|
|
||||||
|
def generate_stubs(self) -> ast.Module:
|
||||||
|
self.stubs = []
|
||||||
|
self.typing_imports = set()
|
||||||
|
self.import_pandas = False
|
||||||
|
for name, type in self.types._types.items():
|
||||||
|
# Skip builtin types, not just based on name so the user can override
|
||||||
|
# TODO: check if added members on builtin type
|
||||||
|
match type:
|
||||||
|
case BaseType(name=name_) if name == name_:
|
||||||
|
continue
|
||||||
|
case GenericType(
|
||||||
|
name=name1,
|
||||||
|
body=BaseType(name=name2),
|
||||||
|
) if (
|
||||||
|
name == name1 == name2
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
self.generate_stub(name, type)
|
||||||
|
|
||||||
|
imports: list[ast.stmt] = [
|
||||||
|
ast.ImportFrom(
|
||||||
|
module="__future__",
|
||||||
|
names=[ast.alias(name="annotations")],
|
||||||
|
level=0,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if len(self.typing_imports) != 0:
|
||||||
|
imports.append(
|
||||||
|
ast.ImportFrom(
|
||||||
|
module="typing",
|
||||||
|
names=[
|
||||||
|
ast.alias(name=name) for name in sorted(self.typing_imports)
|
||||||
|
],
|
||||||
|
level=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.import_pandas:
|
||||||
|
imports.append(
|
||||||
|
ast.Import(
|
||||||
|
names=[
|
||||||
|
ast.alias(
|
||||||
|
name="pandas",
|
||||||
|
asname="pd",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ast.Module(body=imports + self.stubs, type_ignores=[])
|
||||||
|
|
||||||
|
def generate_stub(self, name: str, type: Type):
|
||||||
|
base_type: Type = type
|
||||||
|
|
||||||
|
# TODO: improve
|
||||||
|
match type:
|
||||||
|
case DerivedType(name=name_) | GenericType(name=name_) if name_ == name:
|
||||||
|
pass
|
||||||
|
case UnitType() if name == "None":
|
||||||
|
pass
|
||||||
|
case TopType() if name == "Any":
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
alias = ast.Assign(
|
||||||
|
targets=[ast.Name(id=name)], value=self.dump_type(type)
|
||||||
|
)
|
||||||
|
self.add_stub(alias)
|
||||||
|
return
|
||||||
|
|
||||||
|
members: dict[str, Member] = self.types._members.get(name, {})
|
||||||
|
if isinstance(base_type, (BaseType, TopType, UnitType)) and len(members) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
bases: list[ast.expr] = []
|
||||||
|
substitutions: dict[str, Type] = {}
|
||||||
|
bases, substitutions = self.get_bases(type)
|
||||||
|
self.substitutions[name] = substitutions
|
||||||
|
|
||||||
|
body = self.generate_body(members, substitutions)
|
||||||
|
stub = ast.ClassDef(
|
||||||
|
name=name,
|
||||||
|
bases=bases,
|
||||||
|
body=body,
|
||||||
|
keywords=[],
|
||||||
|
decorator_list=[],
|
||||||
|
)
|
||||||
|
self.add_stub(stub)
|
||||||
|
|
||||||
|
def get_bases(self, type: Type) -> tuple[list[ast.expr], dict[str, Type]]:
|
||||||
|
match type:
|
||||||
|
case DerivedType(type=base):
|
||||||
|
return [self.dump_type(base)], {}
|
||||||
|
|
||||||
|
case GenericType(params=params, body=body):
|
||||||
|
self.add_typing_import("Generic")
|
||||||
|
type_vars: ast.expr
|
||||||
|
|
||||||
|
params2: list[TypeVar] = self.define_type_vars(params)
|
||||||
|
if len(params) == 1:
|
||||||
|
type_vars = ast.Name(id=params2[0].name)
|
||||||
|
else:
|
||||||
|
type_vars = ast.Tuple(
|
||||||
|
elts=[ast.Name(id=param.name) for param in params2]
|
||||||
|
)
|
||||||
|
|
||||||
|
substitutions: dict[str, TypeVar] = {
|
||||||
|
param.name: param2 for param, param2 in zip(params, params2)
|
||||||
|
}
|
||||||
|
|
||||||
|
body_bases, body_subsitutions = self.get_bases(body)
|
||||||
|
return (
|
||||||
|
body_bases
|
||||||
|
+ [
|
||||||
|
ast.Subscript(
|
||||||
|
value=ast.Name(id="Generic"),
|
||||||
|
slice=type_vars,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
body_subsitutions | substitutions,
|
||||||
|
)
|
||||||
|
|
||||||
|
case ConstraintType(type=base):
|
||||||
|
return self.get_bases(base)
|
||||||
|
|
||||||
|
case TypeVar(bound=bound) if bound is not None:
|
||||||
|
return [self.dump_type(bound)], {}
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return [], {}
|
||||||
|
|
||||||
|
def generate_body(
|
||||||
|
self, members: dict[str, Member], substitutions: dict[str, Type]
|
||||||
|
) -> list[ast.stmt]:
|
||||||
|
if len(members) == 0:
|
||||||
|
return [ast.Expr(value=Empty)]
|
||||||
|
|
||||||
|
body: list[ast.stmt] = []
|
||||||
|
for name, member in members.items():
|
||||||
|
type: Type = member.type
|
||||||
|
type = substitute_typevars(type, substitutions)
|
||||||
|
match member.kind:
|
||||||
|
case m.MemberKind.PROPERTY:
|
||||||
|
body.append(
|
||||||
|
ast.AnnAssign(
|
||||||
|
target=ast.Name(id=name),
|
||||||
|
annotation=self.dump_type(type),
|
||||||
|
simple=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case m.MemberKind.METHOD:
|
||||||
|
body.extend(self.dump_method(name, type))
|
||||||
|
return body
|
||||||
|
|
||||||
|
def dump_type(self, type: Type) -> ast.expr:
|
||||||
|
match type:
|
||||||
|
case DerivedType(name=name) | GenericType(name=name) if (
|
||||||
|
name in self.substitutions
|
||||||
|
):
|
||||||
|
type = substitute_typevars(type, self.substitutions[name])
|
||||||
|
|
||||||
|
match type:
|
||||||
|
case TopType() | UnknownType():
|
||||||
|
self.add_typing_import("Any")
|
||||||
|
return ast.Name(id="Any")
|
||||||
|
|
||||||
|
case BaseType(name=name):
|
||||||
|
return ast.Name(id=name)
|
||||||
|
|
||||||
|
case DerivedType(name=name):
|
||||||
|
return ast.Name(id=name)
|
||||||
|
|
||||||
|
case UnitType():
|
||||||
|
return ast.Constant(value=None)
|
||||||
|
|
||||||
|
case Function():
|
||||||
|
name: str = self.define_protocol(type)
|
||||||
|
return ast.Name(id=name)
|
||||||
|
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
if len(overloads) == 1:
|
||||||
|
return self.dump_type(overloads[0])
|
||||||
|
return ast.BinOp(
|
||||||
|
left=self.dump_type(OverloadedFunction(overloads=overloads[:-1])),
|
||||||
|
op=ast.BitOr(),
|
||||||
|
right=self.dump_type(overloads[-1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ComplexType():
|
||||||
|
name: str = self.new_stub_name()
|
||||||
|
self.generate_stub(name, type)
|
||||||
|
return ast.Name(id=name)
|
||||||
|
|
||||||
|
case ExtensionType():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
case TypeVar():
|
||||||
|
return ast.Name(id=type.name)
|
||||||
|
|
||||||
|
case GenericType(name=name):
|
||||||
|
params: ast.expr
|
||||||
|
if len(type.params) == 1:
|
||||||
|
params = self.dump_type(type.params[0])
|
||||||
|
else:
|
||||||
|
params = ast.Tuple(
|
||||||
|
elts=[self.dump_type(param) for param in type.params]
|
||||||
|
)
|
||||||
|
return ast.Subscript(
|
||||||
|
value=ast.Name(id=type.name),
|
||||||
|
slice=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
case AppliedType():
|
||||||
|
args: ast.expr
|
||||||
|
if len(type.args) == 1:
|
||||||
|
args = self.dump_type(type.args[0])
|
||||||
|
else:
|
||||||
|
args = ast.Tuple(elts=[self.dump_type(arg) for arg in type.args])
|
||||||
|
return ast.Subscript(
|
||||||
|
value=ast.Name(id=type.name),
|
||||||
|
slice=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
case ConstraintType():
|
||||||
|
return self.dump_type(type.type)
|
||||||
|
|
||||||
|
case TupleType(items=items):
|
||||||
|
return ast.Subscript(
|
||||||
|
value=ast.Name(id="tuple"),
|
||||||
|
slice=ast.Tuple(
|
||||||
|
elts=[self.dump_type(item) for item in items],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ColumnType(type=inner):
|
||||||
|
self.import_pandas = True
|
||||||
|
return ast.Subscript(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="Series",
|
||||||
|
),
|
||||||
|
slice=self.dump_type(inner),
|
||||||
|
)
|
||||||
|
|
||||||
|
case DataFrameType():
|
||||||
|
self.import_pandas = True
|
||||||
|
return ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="DataFrame",
|
||||||
|
)
|
||||||
|
|
||||||
|
case FrameGroupBy():
|
||||||
|
self.import_pandas = True
|
||||||
|
return ast.Attribute(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="api",
|
||||||
|
),
|
||||||
|
attr="typing",
|
||||||
|
),
|
||||||
|
attr="DataFrameGroupBy",
|
||||||
|
)
|
||||||
|
|
||||||
|
case ColumnGroupBy():
|
||||||
|
self.import_pandas = True
|
||||||
|
return ast.Attribute(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="api",
|
||||||
|
),
|
||||||
|
attr="typing",
|
||||||
|
),
|
||||||
|
attr="SeriesGroupBy",
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
assert_never(type)
|
||||||
|
|
||||||
|
def dump_method(
|
||||||
|
self, name: str, method: Type, overloaded: bool = False
|
||||||
|
) -> list[ast.stmt]:
|
||||||
|
match method:
|
||||||
|
case Function():
|
||||||
|
if overloaded:
|
||||||
|
self.add_typing_import("overload")
|
||||||
|
return [
|
||||||
|
ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.dump_args(method, with_self=True),
|
||||||
|
returns=self.dump_type(method.returns),
|
||||||
|
body=[ast.Expr(value=Empty)],
|
||||||
|
decorator_list=[ast.Name(id="overload")] if overloaded else [],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
case OverloadedFunction(overloads=overloads):
|
||||||
|
stmts: list[ast.stmt] = []
|
||||||
|
for overload in overloads:
|
||||||
|
stmts.extend(self.dump_method(name, overload, True))
|
||||||
|
return stmts
|
||||||
|
case _:
|
||||||
|
return [
|
||||||
|
ast.AnnAssign(
|
||||||
|
target=ast.Name(id=name),
|
||||||
|
annotation=self.dump_type(method),
|
||||||
|
simple=1,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def dump_args(self, func: Function, with_self: bool = False) -> ast.arguments:
|
||||||
|
pos: list[ast.arg] = [
|
||||||
|
ast.arg(arg=f"_{arg.pos}", annotation=self.dump_type(arg.type))
|
||||||
|
for arg in func.pos_args
|
||||||
|
]
|
||||||
|
mixed: list[ast.arg] = [
|
||||||
|
ast.arg(arg=arg.name, annotation=self.dump_type(arg.type))
|
||||||
|
for arg in func.args
|
||||||
|
]
|
||||||
|
kw: list[ast.arg] = [
|
||||||
|
ast.arg(arg=arg.name, annotation=self.dump_type(arg.type))
|
||||||
|
for arg in func.kw_args
|
||||||
|
]
|
||||||
|
defaults: list[ast.expr] = [
|
||||||
|
Empty for arg in func.pos_args + func.args if not arg.required
|
||||||
|
]
|
||||||
|
kw_defaults: list[Optional[ast.expr]] = [
|
||||||
|
None if arg.required else Empty for arg in func.kw_args
|
||||||
|
]
|
||||||
|
if with_self:
|
||||||
|
arg = ast.arg(arg="self", annotation=None)
|
||||||
|
if len(pos) != 0:
|
||||||
|
pos.insert(0, arg)
|
||||||
|
else:
|
||||||
|
mixed.insert(0, arg)
|
||||||
|
return ast.arguments(
|
||||||
|
posonlyargs=pos,
|
||||||
|
args=mixed,
|
||||||
|
kwonlyargs=kw,
|
||||||
|
defaults=defaults,
|
||||||
|
kw_defaults=kw_defaults,
|
||||||
|
)
|
||||||
|
|
||||||
|
def define_protocol(self, func: Function) -> str:
|
||||||
|
self.add_typing_import("Protocol")
|
||||||
|
name: str = self.new_protocol_name()
|
||||||
|
protocol = ast.ClassDef(
|
||||||
|
name=name,
|
||||||
|
bases=[ast.Name(id="Protocol")],
|
||||||
|
keywords=[],
|
||||||
|
body=[
|
||||||
|
ast.FunctionDef(
|
||||||
|
name="__call__",
|
||||||
|
args=self.dump_args(func, with_self=True),
|
||||||
|
returns=self.dump_type(func.returns),
|
||||||
|
body=[ast.Expr(value=Empty)],
|
||||||
|
decorator_list=[],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
decorator_list=[],
|
||||||
|
)
|
||||||
|
self.add_stub(protocol)
|
||||||
|
return name
|
||||||
|
|
||||||
|
def new_protocol_name(self) -> str:
|
||||||
|
name: str = f"_Protocol{self.protocol_idx}"
|
||||||
|
self.protocol_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def new_stub_name(self) -> str:
|
||||||
|
name: str = f"_Stub_{self.stub_idx}"
|
||||||
|
self.stub_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def new_type_var_name(self) -> str:
|
||||||
|
name: str = f"_T{self.type_var_idx}"
|
||||||
|
self.type_var_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def add_stub(self, stub: ast.stmt):
|
||||||
|
self.stubs.append(stub)
|
||||||
|
|
||||||
|
def add_typing_import(self, name: str):
|
||||||
|
self.typing_imports.add(name)
|
||||||
|
|
||||||
|
def define_type_vars(self, vars: list[TypeVar]) -> list[TypeVar]:
|
||||||
|
vars2: list[TypeVar] = []
|
||||||
|
for var in vars:
|
||||||
|
vars2.append(self.define_type_var(var))
|
||||||
|
return vars2
|
||||||
|
|
||||||
|
def define_type_var(self, var: TypeVar) -> TypeVar:
|
||||||
|
name: str = self.new_type_var_name()
|
||||||
|
self.add_typing_import("TypeVar")
|
||||||
|
|
||||||
|
kwargs: list[ast.keyword] = []
|
||||||
|
if var.bound is not None:
|
||||||
|
kwargs.append(
|
||||||
|
ast.keyword(
|
||||||
|
arg="bound",
|
||||||
|
value=self.dump_type(var.bound),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if var.variance == Variance.COVARIANT:
|
||||||
|
kwargs.append(
|
||||||
|
ast.keyword(
|
||||||
|
arg="covariant",
|
||||||
|
value=ast.Constant(value=True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif var.variance == Variance.CONTRAVARIANT:
|
||||||
|
kwargs.append(
|
||||||
|
ast.keyword(
|
||||||
|
arg="contravariant",
|
||||||
|
value=ast.Constant(value=True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.add_stub(
|
||||||
|
ast.Assign(
|
||||||
|
targets=[ast.Name(id=name)],
|
||||||
|
value=ast.Call(
|
||||||
|
func=ast.Name(id="TypeVar"),
|
||||||
|
args=[
|
||||||
|
ast.Constant(value=name),
|
||||||
|
],
|
||||||
|
keywords=kwargs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return TypeVar(name=name, bound=None)
|
||||||
@@ -40,22 +40,24 @@ class MidasLexer(Lexer):
|
|||||||
self.add_token(TokenType.AND)
|
self.add_token(TokenType.AND)
|
||||||
case "?":
|
case "?":
|
||||||
self.add_token(TokenType.QMARK)
|
self.add_token(TokenType.QMARK)
|
||||||
# case ",":
|
case ",":
|
||||||
# self.add_token(TokenType.COMMA)
|
self.add_token(TokenType.COMMA)
|
||||||
case "_" if not self.is_identifier_char(self.peek_next(), start=False):
|
case "_" if not self.is_identifier_char(self.peek_next(), start=False):
|
||||||
self.add_token(TokenType.UNDERSCORE)
|
self.add_token(TokenType.UNDERSCORE)
|
||||||
case "-" if self.match(">"):
|
case "-" if self.match(">"):
|
||||||
self.add_token(TokenType.ARROW)
|
self.add_token(TokenType.ARROW)
|
||||||
# case "+":
|
case "+":
|
||||||
# self.add_token(TokenType.PLUS)
|
self.add_token(TokenType.PLUS)
|
||||||
case "-":
|
case "-":
|
||||||
self.add_token(TokenType.MINUS)
|
self.add_token(TokenType.MINUS)
|
||||||
# case "*":
|
case "*":
|
||||||
# self.add_token(TokenType.STAR)
|
self.add_token(TokenType.STAR)
|
||||||
case "/" if self.match("/"):
|
case "/" if self.match("/"):
|
||||||
self.scan_comment()
|
self.scan_comment()
|
||||||
case "/" if self.match("*"):
|
case "/" if self.match("*"):
|
||||||
self.scan_comment_multiline()
|
self.scan_comment_multiline()
|
||||||
|
case "/":
|
||||||
|
self.add_token(TokenType.SLASH)
|
||||||
case "\n":
|
case "\n":
|
||||||
self.add_token(TokenType.NEWLINE)
|
self.add_token(TokenType.NEWLINE)
|
||||||
case " " | "\r" | "\t":
|
case " " | "\r" | "\t":
|
||||||
@@ -67,6 +69,8 @@ class MidasLexer(Lexer):
|
|||||||
):
|
):
|
||||||
self.advance()
|
self.advance()
|
||||||
self.add_token(TokenType.WHITESPACE)
|
self.add_token(TokenType.WHITESPACE)
|
||||||
|
case '"' | "'":
|
||||||
|
self.scan_string(char)
|
||||||
case _:
|
case _:
|
||||||
if char.isdigit():
|
if char.isdigit():
|
||||||
self.scan_number()
|
self.scan_number()
|
||||||
@@ -76,6 +80,17 @@ class MidasLexer(Lexer):
|
|||||||
self.error("Unexpected character")
|
self.error("Unexpected character")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def scan_string(self, opening: str):
|
||||||
|
while self.peek() != opening and not self.is_at_end():
|
||||||
|
self.advance()
|
||||||
|
|
||||||
|
if self.is_at_end():
|
||||||
|
self.error("Unterminated string")
|
||||||
|
|
||||||
|
self.advance()
|
||||||
|
value: str = self.source[self.start + 1 : self.idx - 1]
|
||||||
|
self.add_token(TokenType.STRING, value)
|
||||||
|
|
||||||
def scan_number(self):
|
def scan_number(self):
|
||||||
"""Scan the rest of number and add it as a token
|
"""Scan the rest of number and add it as a token
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TokenType(Enum):
|
|||||||
LEFT_BRACE = auto()
|
LEFT_BRACE = auto()
|
||||||
RIGHT_BRACE = auto()
|
RIGHT_BRACE = auto()
|
||||||
COLON = auto()
|
COLON = auto()
|
||||||
# COMMA = auto()
|
COMMA = auto()
|
||||||
UNDERSCORE = auto()
|
UNDERSCORE = auto()
|
||||||
ARROW = auto()
|
ARROW = auto()
|
||||||
AND = auto()
|
AND = auto()
|
||||||
@@ -25,10 +25,10 @@ class TokenType(Enum):
|
|||||||
DOT = auto()
|
DOT = auto()
|
||||||
|
|
||||||
# Operators
|
# Operators
|
||||||
# PLUS = auto()
|
PLUS = auto()
|
||||||
MINUS = auto()
|
MINUS = auto()
|
||||||
# STAR = auto()
|
STAR = auto()
|
||||||
# SLASH = auto()
|
SLASH = auto()
|
||||||
GREATER = auto()
|
GREATER = auto()
|
||||||
GREATER_EQUAL = auto()
|
GREATER_EQUAL = auto()
|
||||||
LESS = auto()
|
LESS = auto()
|
||||||
@@ -43,13 +43,17 @@ class TokenType(Enum):
|
|||||||
TRUE = auto()
|
TRUE = auto()
|
||||||
FALSE = auto()
|
FALSE = auto()
|
||||||
NONE = auto()
|
NONE = auto()
|
||||||
|
STRING = auto()
|
||||||
|
|
||||||
# Keywords
|
# Keywords
|
||||||
TYPE = auto()
|
TYPE = auto()
|
||||||
OP = auto()
|
ALIAS = auto()
|
||||||
PREDICATE = auto()
|
PREDICATE = auto()
|
||||||
EXTEND = auto()
|
EXTEND = auto()
|
||||||
WHERE = auto()
|
WHERE = auto()
|
||||||
|
PROP = auto()
|
||||||
|
DEF = auto()
|
||||||
|
FUNC = auto()
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
COMMENT = auto()
|
COMMENT = auto()
|
||||||
@@ -60,13 +64,16 @@ class TokenType(Enum):
|
|||||||
|
|
||||||
KEYWORDS: dict[str, TokenType] = {
|
KEYWORDS: dict[str, TokenType] = {
|
||||||
"type": TokenType.TYPE,
|
"type": TokenType.TYPE,
|
||||||
"op": TokenType.OP,
|
"alias": TokenType.ALIAS,
|
||||||
"predicate": TokenType.PREDICATE,
|
"predicate": TokenType.PREDICATE,
|
||||||
"extend": TokenType.EXTEND,
|
"extend": TokenType.EXTEND,
|
||||||
"where": TokenType.WHERE,
|
"where": TokenType.WHERE,
|
||||||
"true": TokenType.TRUE,
|
"true": TokenType.TRUE,
|
||||||
"false": TokenType.FALSE,
|
"false": TokenType.FALSE,
|
||||||
"none": TokenType.NONE,
|
"none": TokenType.NONE,
|
||||||
|
"prop": TokenType.PROP,
|
||||||
|
"def": TokenType.DEF,
|
||||||
|
"fn": TokenType.FUNC,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,35 @@ from typing import Optional
|
|||||||
|
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
from midas.ast.midas import (
|
from midas.ast.midas import (
|
||||||
|
AliasStmt,
|
||||||
BinaryExpr,
|
BinaryExpr,
|
||||||
ComplexTypeStmt,
|
CallExpr,
|
||||||
|
ComplexType,
|
||||||
|
ConstraintType,
|
||||||
Expr,
|
Expr,
|
||||||
ExtendStmt,
|
ExtendStmt,
|
||||||
|
ExtensionType,
|
||||||
|
FrameType,
|
||||||
|
FunctionType,
|
||||||
|
GenericType,
|
||||||
GetExpr,
|
GetExpr,
|
||||||
GroupingExpr,
|
GroupingExpr,
|
||||||
LiteralExpr,
|
LiteralExpr,
|
||||||
LogicalExpr,
|
LogicalExpr,
|
||||||
OpStmt,
|
MemberKind,
|
||||||
|
MemberStmt,
|
||||||
|
NamedType,
|
||||||
|
ParamSpec,
|
||||||
PredicateStmt,
|
PredicateStmt,
|
||||||
PropertyStmt,
|
|
||||||
SimpleTypeExpr,
|
|
||||||
SimpleTypeStmt,
|
|
||||||
Stmt,
|
Stmt,
|
||||||
TemplateExpr,
|
Type,
|
||||||
TypeExpr,
|
TypeParam,
|
||||||
|
TypeStmt,
|
||||||
UnaryExpr,
|
UnaryExpr,
|
||||||
VariableExpr,
|
VariableExpr,
|
||||||
WildcardExpr,
|
WildcardExpr,
|
||||||
)
|
)
|
||||||
from midas.lexer.token import Token, TokenType
|
from midas.lexer.token import KEYWORDS, Token, TokenType
|
||||||
from midas.parser.base import Parser
|
from midas.parser.base import Parser
|
||||||
from midas.parser.errors import ParsingError
|
from midas.parser.errors import ParsingError
|
||||||
|
|
||||||
@@ -32,9 +40,10 @@ class MidasParser(Parser):
|
|||||||
|
|
||||||
SYNC_BOUNDARY: set[TokenType] = {
|
SYNC_BOUNDARY: set[TokenType] = {
|
||||||
TokenType.TYPE,
|
TokenType.TYPE,
|
||||||
TokenType.OP,
|
|
||||||
TokenType.EXTEND,
|
TokenType.EXTEND,
|
||||||
TokenType.PREDICATE,
|
TokenType.PREDICATE,
|
||||||
|
TokenType.PROP,
|
||||||
|
TokenType.FUNC,
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(self) -> list[Stmt]:
|
def parse(self) -> list[Stmt]:
|
||||||
@@ -72,6 +81,8 @@ class MidasParser(Parser):
|
|||||||
try:
|
try:
|
||||||
if self.match(TokenType.TYPE):
|
if self.match(TokenType.TYPE):
|
||||||
return self.type_declaration()
|
return self.type_declaration()
|
||||||
|
if self.match(TokenType.ALIAS):
|
||||||
|
return self.alias_declaration()
|
||||||
if self.match(TokenType.EXTEND):
|
if self.match(TokenType.EXTEND):
|
||||||
return self.extend_declaration()
|
return self.extend_declaration()
|
||||||
if self.match(TokenType.PREDICATE):
|
if self.match(TokenType.PREDICATE):
|
||||||
@@ -81,7 +92,7 @@ class MidasParser(Parser):
|
|||||||
self.synchronize()
|
self.synchronize()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def type_declaration(self) -> SimpleTypeStmt | ComplexTypeStmt:
|
def type_declaration(self) -> TypeStmt:
|
||||||
"""Parse a type declaration
|
"""Parse a type declaration
|
||||||
|
|
||||||
A type declaration can either be a simple type alias or a new complex type.
|
A type declaration can either be a simple type alias or a new complex type.
|
||||||
@@ -106,51 +117,71 @@ class MidasParser(Parser):
|
|||||||
TypeStmt: the parsed type declaration statement
|
TypeStmt: the parsed type declaration statement
|
||||||
"""
|
"""
|
||||||
keyword: Token = self.previous()
|
keyword: Token = self.previous()
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name")
|
name: Token = self.consume_identifier("Expected type name")
|
||||||
template: Optional[TemplateExpr] = None
|
params: list[TypeParam] = self.type_params()
|
||||||
if self.check(TokenType.LEFT_BRACKET):
|
|
||||||
template = self.template_expr()
|
|
||||||
|
|
||||||
if self.match(TokenType.LEFT_PAREN):
|
self.consume(TokenType.EQUAL, "Expected '=' before type definition")
|
||||||
base: TypeExpr = self.type_expr()
|
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Unclosed base type parenthesis")
|
type: Type = self.type_expr()
|
||||||
constraint: Optional[Expr] = None
|
|
||||||
if self.match(TokenType.WHERE):
|
return TypeStmt(
|
||||||
constraint = self.constraint()
|
|
||||||
return SimpleTypeStmt(
|
|
||||||
location=keyword.location_to(self.previous()),
|
location=keyword.location_to(self.previous()),
|
||||||
name=name,
|
name=name,
|
||||||
template=template,
|
params=params,
|
||||||
base=base,
|
type=type,
|
||||||
constraint=constraint,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
properties: list[PropertyStmt] = self.type_properties()
|
|
||||||
return ComplexTypeStmt(
|
|
||||||
location=keyword.location_to(self.previous()),
|
|
||||||
name=name,
|
|
||||||
template=template,
|
|
||||||
properties=properties,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def template_expr(self) -> TemplateExpr:
|
def type_params(self) -> list[TypeParam]:
|
||||||
"""Parse a generic template expression
|
"""Parse a list of type parameters
|
||||||
|
|
||||||
A template is written `[TypeExpr]`
|
Type parameters are a comma-separated list of type variables wrapped in brackets.
|
||||||
|
Each type variable is either a simple variable, or a bounded variable written `S <: T`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TemplateExpr: the parsed template expression
|
list[TypeParam]: the list of type parameters, if any, or an empty list
|
||||||
"""
|
"""
|
||||||
left: Token = self.consume(
|
if not self.match(TokenType.LEFT_BRACKET):
|
||||||
TokenType.LEFT_BRACKET, "Missing '[' before template expression"
|
return []
|
||||||
)
|
|
||||||
type: TypeExpr = self.type_expr()
|
|
||||||
right: Token = self.consume(
|
|
||||||
TokenType.RIGHT_BRACKET, "Missing ']' after template expression"
|
|
||||||
)
|
|
||||||
return TemplateExpr(location=left.location_to(right), type=type)
|
|
||||||
|
|
||||||
def type_expr(self) -> TypeExpr:
|
params: list[TypeParam] = []
|
||||||
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET):
|
||||||
|
name: Token = self.consume_identifier("Expected type variable")
|
||||||
|
bound: Optional[Type] = None
|
||||||
|
if self.match(TokenType.LESS):
|
||||||
|
self.consume(TokenType.COLON, "Expected ':' after '<'")
|
||||||
|
bound = self.type_expr()
|
||||||
|
params.append(
|
||||||
|
TypeParam(
|
||||||
|
location=name.location_to(self.previous()),
|
||||||
|
name=name,
|
||||||
|
bound=bound,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not self.match(TokenType.COMMA):
|
||||||
|
break
|
||||||
|
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after type parameters")
|
||||||
|
return params
|
||||||
|
|
||||||
|
def alias_declaration(self) -> AliasStmt:
|
||||||
|
"""Parse an alias declaration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AliasStmt: the parsed alias declaration statement
|
||||||
|
"""
|
||||||
|
keyword: Token = self.previous()
|
||||||
|
name: Token = self.consume_identifier("Expected type name")
|
||||||
|
|
||||||
|
self.consume(TokenType.EQUAL, "Expected '=' before alias definition")
|
||||||
|
|
||||||
|
type: Type = self.type_expr()
|
||||||
|
|
||||||
|
return AliasStmt(
|
||||||
|
location=keyword.location_to(self.previous()),
|
||||||
|
name=name,
|
||||||
|
type=type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def type_expr(self) -> Type:
|
||||||
"""Parse a type expression
|
"""Parse a type expression
|
||||||
|
|
||||||
A type is an identifier, optionally followed by a template expression.
|
A type is an identifier, optionally followed by a template expression.
|
||||||
@@ -159,30 +190,124 @@ class MidasParser(Parser):
|
|||||||
Returns:
|
Returns:
|
||||||
TypeExpr: the parsed type expression
|
TypeExpr: the parsed type expression
|
||||||
"""
|
"""
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name")
|
base: Type
|
||||||
template: Optional[TemplateExpr] = None
|
if self.match(TokenType.FUNC):
|
||||||
|
base = self.function()
|
||||||
|
else:
|
||||||
|
base = self.constraint_type()
|
||||||
|
if self.match(TokenType.AND):
|
||||||
|
extension: ComplexType = self.complex_type()
|
||||||
|
return ExtensionType(
|
||||||
|
location=Location.span(base.location, extension.location),
|
||||||
|
base=base,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
return base
|
||||||
|
|
||||||
|
def constraint_type(self) -> Type:
|
||||||
|
type: Type = self.base_type()
|
||||||
|
if self.match(TokenType.WHERE):
|
||||||
|
constraint: Expr = self.constraint()
|
||||||
|
return ConstraintType(
|
||||||
|
location=Location.span(type.location, constraint.location),
|
||||||
|
type=type,
|
||||||
|
constraint=constraint,
|
||||||
|
)
|
||||||
|
return type
|
||||||
|
|
||||||
|
def base_type(self) -> Type:
|
||||||
|
if self.match(TokenType.LEFT_PAREN):
|
||||||
|
type: Type = self.type_expr()
|
||||||
|
self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
||||||
|
return type
|
||||||
|
|
||||||
|
if self.check(TokenType.LEFT_BRACE):
|
||||||
|
return self.complex_type()
|
||||||
|
|
||||||
|
return self.generic_type()
|
||||||
|
|
||||||
|
def generic_type(self) -> Type:
|
||||||
|
type: NamedType = self.named_type()
|
||||||
if self.check(TokenType.LEFT_BRACKET):
|
if self.check(TokenType.LEFT_BRACKET):
|
||||||
template = self.template_expr()
|
if type.name.lexeme == "Frame":
|
||||||
optional: bool = self.match(TokenType.QMARK)
|
return self.frame_type()
|
||||||
return TypeExpr(
|
args: list[Type] = self.type_args()
|
||||||
location=name.location_to(self.previous()),
|
return GenericType(
|
||||||
|
location=Location.span(type.location, self.previous().get_location()),
|
||||||
|
type=type,
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
return type
|
||||||
|
|
||||||
|
def type_args(self) -> list[Type]:
|
||||||
|
args: list[Type] = []
|
||||||
|
self.consume(TokenType.LEFT_BRACKET, "Missing '[' before generic arguments")
|
||||||
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET):
|
||||||
|
args.append(self.type_expr())
|
||||||
|
if not self.match(TokenType.COMMA):
|
||||||
|
break
|
||||||
|
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after generic arguments")
|
||||||
|
return args
|
||||||
|
|
||||||
|
def named_type(self) -> NamedType:
|
||||||
|
name: Token = self.consume_identifier("Expected type name")
|
||||||
|
return NamedType(
|
||||||
|
location=name.get_location(),
|
||||||
name=name,
|
name=name,
|
||||||
template=template,
|
|
||||||
optional=optional,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def simple_type_expr(self) -> SimpleTypeExpr:
|
def complex_type(self) -> ComplexType:
|
||||||
"""Parse a simple type expression
|
"""Parse a type definition body
|
||||||
|
|
||||||
A simple type is just an identifier optionally followed by a '?'
|
A type definition body is a set of whitespace-separated
|
||||||
|
property statements enclosed in curly braces
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SimpleTypeExpr: the parsed simple type expression
|
ComplexType: the parsed complex type
|
||||||
"""
|
"""
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name")
|
left: Token = self.consume(
|
||||||
optional: bool = self.match(TokenType.QMARK)
|
TokenType.LEFT_BRACE, "Expected '{' to start type body"
|
||||||
return SimpleTypeExpr(
|
)
|
||||||
location=name.location_to(self.previous()), name=name, optional=optional
|
members: list[MemberStmt] = []
|
||||||
|
# TODO: add keyword to differentiate properties and methods,
|
||||||
|
# and allow multiple methods with the same name but not properties
|
||||||
|
names: set[str] = set()
|
||||||
|
while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end():
|
||||||
|
member: MemberStmt = self.member_stmt()
|
||||||
|
# if member.name.lexeme in names:
|
||||||
|
# raise self.error(member.name, "Duplicate property")
|
||||||
|
# names.add(member.name.lexeme)
|
||||||
|
members.append(member)
|
||||||
|
right: Token = self.consume(TokenType.RIGHT_BRACE, "Unclosed type body")
|
||||||
|
return ComplexType(
|
||||||
|
location=left.location_to(right),
|
||||||
|
members=members,
|
||||||
|
)
|
||||||
|
|
||||||
|
def frame_type(self) -> FrameType:
|
||||||
|
keyword: Token = self.previous()
|
||||||
|
self.consume(TokenType.LEFT_BRACKET, "Expected '[' to start frame schema")
|
||||||
|
|
||||||
|
columns: list[FrameType.Column] = []
|
||||||
|
while not self.check(TokenType.RIGHT_BRACKET) and not self.is_at_end():
|
||||||
|
name: Token = self.advance()
|
||||||
|
self.consume(TokenType.COLON, "Expected ':' between column name and type")
|
||||||
|
type: Type = self.type_expr()
|
||||||
|
columns.append(
|
||||||
|
FrameType.Column(
|
||||||
|
location=name.location_to(self.previous()),
|
||||||
|
name=name,
|
||||||
|
type=type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not self.match(TokenType.COMMA):
|
||||||
|
break
|
||||||
|
|
||||||
|
self.consume(TokenType.RIGHT_BRACKET, "Unclosed frame schema")
|
||||||
|
|
||||||
|
return FrameType(
|
||||||
|
location=keyword.location_to(self.previous()),
|
||||||
|
columns=columns,
|
||||||
)
|
)
|
||||||
|
|
||||||
def constraint(self) -> Expr:
|
def constraint(self) -> Expr:
|
||||||
@@ -193,6 +318,9 @@ class MidasParser(Parser):
|
|||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed constraint expression
|
Expr: the parsed constraint expression
|
||||||
"""
|
"""
|
||||||
|
return self.expression()
|
||||||
|
|
||||||
|
def expression(self) -> Expr:
|
||||||
return self.and_()
|
return self.and_()
|
||||||
|
|
||||||
def and_(self) -> Expr:
|
def and_(self) -> Expr:
|
||||||
@@ -233,13 +361,35 @@ class MidasParser(Parser):
|
|||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
"""
|
"""
|
||||||
expr: Expr = self.unary()
|
expr: Expr = self.term()
|
||||||
while self.match(
|
while self.match(
|
||||||
TokenType.LESS,
|
TokenType.LESS,
|
||||||
TokenType.LESS_EQUAL,
|
TokenType.LESS_EQUAL,
|
||||||
TokenType.GREATER,
|
TokenType.GREATER,
|
||||||
TokenType.GREATER_EQUAL,
|
TokenType.GREATER_EQUAL,
|
||||||
):
|
):
|
||||||
|
operator: Token = self.previous()
|
||||||
|
right: Expr = self.term()
|
||||||
|
location: Location = Location.span(expr.location, right.location)
|
||||||
|
expr = BinaryExpr(
|
||||||
|
location=location, left=expr, operator=operator, right=right
|
||||||
|
)
|
||||||
|
return expr
|
||||||
|
|
||||||
|
def term(self) -> Expr:
|
||||||
|
expr: Expr = self.factor()
|
||||||
|
while self.match(TokenType.PLUS, TokenType.MINUS):
|
||||||
|
operator: Token = self.previous()
|
||||||
|
right: Expr = self.factor()
|
||||||
|
location: Location = Location.span(expr.location, right.location)
|
||||||
|
expr = BinaryExpr(
|
||||||
|
location=location, left=expr, operator=operator, right=right
|
||||||
|
)
|
||||||
|
return expr
|
||||||
|
|
||||||
|
def factor(self) -> Expr:
|
||||||
|
expr: Expr = self.unary()
|
||||||
|
while self.match(TokenType.STAR, TokenType.SLASH):
|
||||||
operator: Token = self.previous()
|
operator: Token = self.previous()
|
||||||
right: Expr = self.unary()
|
right: Expr = self.unary()
|
||||||
location: Location = Location.span(expr.location, right.location)
|
location: Location = Location.span(expr.location, right.location)
|
||||||
@@ -259,7 +409,55 @@ class MidasParser(Parser):
|
|||||||
right: Expr = self.unary()
|
right: Expr = self.unary()
|
||||||
location: Location = Location.span(operator.get_location(), right.location)
|
location: Location = Location.span(operator.get_location(), right.location)
|
||||||
return UnaryExpr(location=location, operator=operator, right=right)
|
return UnaryExpr(location=location, operator=operator, right=right)
|
||||||
return self.reference()
|
return self.call()
|
||||||
|
|
||||||
|
def call(self) -> Expr:
|
||||||
|
expr: Expr = self.reference()
|
||||||
|
while self.match(TokenType.LEFT_PAREN):
|
||||||
|
expr = self.finish_call(expr)
|
||||||
|
return expr
|
||||||
|
|
||||||
|
def finish_call(self, callee: Expr) -> Expr:
|
||||||
|
pos_args: list[Expr] = []
|
||||||
|
kw_args: dict[str, Expr] = {}
|
||||||
|
keywords: bool = False
|
||||||
|
while not self.check(TokenType.RIGHT_PAREN):
|
||||||
|
if self.check_identifier() and self.check_next(TokenType.EQUAL):
|
||||||
|
keywords = True
|
||||||
|
keyword: Token = self.advance()
|
||||||
|
self.advance()
|
||||||
|
value: Expr = self.expression()
|
||||||
|
name: str = keyword.lexeme
|
||||||
|
if name in kw_args:
|
||||||
|
self.error(
|
||||||
|
self.peek(),
|
||||||
|
f"Multiple values passed for '{name}', only the last occurrence will be used",
|
||||||
|
)
|
||||||
|
kw_args[name] = value
|
||||||
|
else:
|
||||||
|
value = self.expression()
|
||||||
|
if self.check(TokenType.EQUAL):
|
||||||
|
if keywords:
|
||||||
|
raise self.error(self.peek(), "Invalid keyword argument name")
|
||||||
|
else:
|
||||||
|
raise self.error(
|
||||||
|
self.peek(),
|
||||||
|
"Cannot pass positional arguments after a keyword argument",
|
||||||
|
)
|
||||||
|
pos_args.append(value)
|
||||||
|
|
||||||
|
if not self.match(TokenType.COMMA):
|
||||||
|
break
|
||||||
|
|
||||||
|
r_paren: Token = self.consume(
|
||||||
|
TokenType.RIGHT_PAREN, "Expected ')' after arguments."
|
||||||
|
)
|
||||||
|
return CallExpr(
|
||||||
|
location=Location.span(callee.location, r_paren.get_location()),
|
||||||
|
callee=callee,
|
||||||
|
arguments=pos_args,
|
||||||
|
keywords=kw_args,
|
||||||
|
)
|
||||||
|
|
||||||
def reference(self) -> Expr:
|
def reference(self) -> Expr:
|
||||||
"""Parse an attribute access expression or a simpler expression
|
"""Parse an attribute access expression or a simpler expression
|
||||||
@@ -269,9 +467,7 @@ class MidasParser(Parser):
|
|||||||
"""
|
"""
|
||||||
expr: Expr = self.primary()
|
expr: Expr = self.primary()
|
||||||
while self.match(TokenType.DOT):
|
while self.match(TokenType.DOT):
|
||||||
name: Token = self.consume(
|
name: Token = self.consume_identifier("Expected property name after '.'")
|
||||||
TokenType.IDENTIFIER, "Expected property name after '.'"
|
|
||||||
)
|
|
||||||
location: Location = Location.span(expr.location, name.get_location())
|
location: Location = Location.span(expr.location, name.get_location())
|
||||||
expr = GetExpr(location=location, expr=expr, name=name)
|
expr = GetExpr(location=location, expr=expr, name=name)
|
||||||
return expr
|
return expr
|
||||||
@@ -295,7 +491,10 @@ class MidasParser(Parser):
|
|||||||
if self.match(TokenType.NUMBER):
|
if self.match(TokenType.NUMBER):
|
||||||
return LiteralExpr(location=token.get_location(), value=token.value)
|
return LiteralExpr(location=token.get_location(), value=token.value)
|
||||||
|
|
||||||
if self.match(TokenType.IDENTIFIER):
|
if self.match(TokenType.STRING):
|
||||||
|
return LiteralExpr(location=token.get_location(), value=token.value)
|
||||||
|
|
||||||
|
if self.match_identifier():
|
||||||
return VariableExpr(location=token.get_location(), name=token)
|
return VariableExpr(location=token.get_location(), name=token)
|
||||||
|
|
||||||
if self.match(TokenType.UNDERSCORE):
|
if self.match(TokenType.UNDERSCORE):
|
||||||
@@ -308,89 +507,70 @@ class MidasParser(Parser):
|
|||||||
|
|
||||||
raise self.error(self.peek(), "Expected expression")
|
raise self.error(self.peek(), "Expected expression")
|
||||||
|
|
||||||
def type_properties(self) -> list[PropertyStmt]:
|
def consume_identifier(self, message: str = "Expected identifier") -> Token:
|
||||||
"""Parse a type definition body
|
if not self.match_identifier():
|
||||||
|
raise self.error(self.peek(), message)
|
||||||
|
return self.previous()
|
||||||
|
|
||||||
A type definition body is a set of whitespace-separated
|
def match_identifier(self) -> bool:
|
||||||
property statements enclosed in curly braces
|
return self.match(TokenType.IDENTIFIER, *KEYWORDS.values())
|
||||||
|
|
||||||
|
def check_identifier(self) -> bool:
|
||||||
|
for tt in [TokenType.IDENTIFIER, *KEYWORDS.values()]:
|
||||||
|
if self.check(tt):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def member_stmt(self) -> MemberStmt:
|
||||||
|
"""Parse a member statement
|
||||||
|
|
||||||
|
A type member statement is written `prop name: Type` or `def name: Type`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[PropertyStmt]: the parsed type properties
|
MemberStmt: the parsed member statement
|
||||||
"""
|
"""
|
||||||
self.consume(TokenType.LEFT_BRACE, "Expected '{' to start type body")
|
kind: MemberKind
|
||||||
properties: list[PropertyStmt] = []
|
if self.match(TokenType.PROP):
|
||||||
names: set[str] = set()
|
kind = MemberKind.PROPERTY
|
||||||
while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end():
|
elif self.match(TokenType.DEF):
|
||||||
prop: PropertyStmt = self.property_stmt()
|
kind = MemberKind.METHOD
|
||||||
if prop.name.lexeme in names:
|
else:
|
||||||
raise self.error(prop.name, "Duplicate property")
|
raise self.error(self.peek(), "Expected 'prop' or 'def'")
|
||||||
names.add(prop.name.lexeme)
|
|
||||||
properties.append(prop)
|
|
||||||
self.consume(TokenType.RIGHT_BRACE, "Unclosed type body")
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def property_stmt(self) -> PropertyStmt:
|
name: Token = self.consume_identifier("Expected member name")
|
||||||
"""Parse a property statement
|
self.consume(TokenType.COLON, "Expected ':' after member name")
|
||||||
|
|
||||||
A type property statement is written `name: Type` or `name: Type where Condition`
|
type: Type = self.type_expr()
|
||||||
|
return MemberStmt(
|
||||||
Returns:
|
|
||||||
PropertyStmt: the parsed property statement
|
|
||||||
"""
|
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected property name")
|
|
||||||
self.consume(TokenType.COLON, "Expected ':' after property name")
|
|
||||||
type: TypeExpr = self.type_expr()
|
|
||||||
constraint: Optional[Expr] = None
|
|
||||||
if self.match(TokenType.WHERE):
|
|
||||||
constraint = self.constraint()
|
|
||||||
return PropertyStmt(
|
|
||||||
location=name.location_to(self.previous()),
|
location=name.location_to(self.previous()),
|
||||||
name=name,
|
name=name,
|
||||||
type=type,
|
type=type,
|
||||||
constraint=constraint,
|
kind=kind,
|
||||||
)
|
)
|
||||||
|
|
||||||
def extend_declaration(self) -> ExtendStmt:
|
def extend_declaration(self) -> ExtendStmt:
|
||||||
"""Parse an extension definition
|
"""Parse an extension definition
|
||||||
|
|
||||||
An extension is written `extend Type { operations }`
|
An extension is written `extend Type { operations }` or `extend[S <: T, U] Type { operations }`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ExtendStmt: the parsed extension statement
|
ExtendStmt: the parsed extension statement
|
||||||
"""
|
"""
|
||||||
keyword: Token = self.previous()
|
keyword: Token = self.previous()
|
||||||
type: TypeExpr = self.type_expr()
|
name: Token = self.consume_identifier("Expected type name")
|
||||||
|
params: list[TypeParam] = self.type_params()
|
||||||
|
|
||||||
self.consume(TokenType.LEFT_BRACE, "Expected '{' to start extend body")
|
self.consume(TokenType.LEFT_BRACE, "Expected '{' to start extend body")
|
||||||
operations: list[OpStmt] = []
|
members: list[MemberStmt] = []
|
||||||
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACE):
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACE):
|
||||||
operations.append(self.op_declaration())
|
members.append(self.member_stmt())
|
||||||
self.consume(TokenType.RIGHT_BRACE, "Unclosed extend body")
|
self.consume(TokenType.RIGHT_BRACE, "Unclosed extend body")
|
||||||
location: Location = keyword.location_to(self.previous())
|
location: Location = keyword.location_to(self.previous())
|
||||||
return ExtendStmt(location=location, type=type, operations=operations)
|
return ExtendStmt(
|
||||||
|
location=location,
|
||||||
def op_declaration(self) -> OpStmt:
|
|
||||||
"""Parse an operation definition
|
|
||||||
|
|
||||||
An operation is written `op name(Type) -> Type`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OpStmt: the parsed operation statement
|
|
||||||
"""
|
|
||||||
keyword: Token = self.consume(TokenType.OP, "Expected 'op' keyword")
|
|
||||||
|
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected operation name")
|
|
||||||
self.consume(TokenType.LEFT_PAREN, "Expected '(' before operand type")
|
|
||||||
operand: TypeExpr = self.type_expr()
|
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after operand type")
|
|
||||||
|
|
||||||
self.consume(TokenType.ARROW, "Expected '->' before result type")
|
|
||||||
result: TypeExpr = self.type_expr()
|
|
||||||
|
|
||||||
return OpStmt(
|
|
||||||
location=keyword.location_to(self.previous()),
|
|
||||||
name=name,
|
name=name,
|
||||||
operand=operand,
|
params=params,
|
||||||
result=result,
|
members=members,
|
||||||
)
|
)
|
||||||
|
|
||||||
def predicate_declaration(self) -> PredicateStmt:
|
def predicate_declaration(self) -> PredicateStmt:
|
||||||
@@ -402,18 +582,89 @@ class MidasParser(Parser):
|
|||||||
PredicateStmt: the parsed predicate declaration statement
|
PredicateStmt: the parsed predicate declaration statement
|
||||||
"""
|
"""
|
||||||
keyword: Token = self.previous()
|
keyword: Token = self.previous()
|
||||||
name: Token = self.consume(TokenType.IDENTIFIER, "Expected predicate name")
|
|
||||||
self.consume(TokenType.LEFT_PAREN, "Expected '(' before predicate subject")
|
name: Token = self.consume_identifier("Expected predicate name")
|
||||||
subject: Token = self.consume(TokenType.IDENTIFIER, "Expected subject name")
|
|
||||||
self.consume(TokenType.COLON, "Expected ':' after subject name")
|
params: list[ParamSpec] = []
|
||||||
type: TypeExpr = self.type_expr()
|
while self.check(TokenType.LEFT_PAREN):
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after predicate subject")
|
params.append(self.function_args())
|
||||||
|
|
||||||
self.consume(TokenType.EQUAL, "Expected '=' after predicate subject")
|
self.consume(TokenType.EQUAL, "Expected '=' after predicate subject")
|
||||||
condition: Expr = self.constraint()
|
body: Expr = self.constraint()
|
||||||
return PredicateStmt(
|
return PredicateStmt(
|
||||||
location=keyword.location_to(self.previous()),
|
location=keyword.location_to(self.previous()),
|
||||||
name=name,
|
name=name,
|
||||||
subject=subject,
|
params=params,
|
||||||
type=type,
|
body=body,
|
||||||
condition=condition,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def function(self) -> FunctionType:
|
||||||
|
params: ParamSpec = self.function_args()
|
||||||
|
|
||||||
|
self.consume(TokenType.ARROW, "Expected '->' before result type")
|
||||||
|
result: Type = self.type_expr()
|
||||||
|
|
||||||
|
return FunctionType(
|
||||||
|
location=params.l_paren.location_to(self.previous()),
|
||||||
|
params=params,
|
||||||
|
returns=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
def function_args(self) -> ParamSpec:
|
||||||
|
l_paren: Token = self.consume(
|
||||||
|
TokenType.LEFT_PAREN, "Expected '(' before function parameters"
|
||||||
|
)
|
||||||
|
pos_args: list[FunctionType.Argument] = []
|
||||||
|
args: list[FunctionType.Argument] = []
|
||||||
|
kw_args: list[FunctionType.Argument] = []
|
||||||
|
|
||||||
|
args_first_tokens: list[Token] = []
|
||||||
|
|
||||||
|
section: int = 0
|
||||||
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_PAREN):
|
||||||
|
match section:
|
||||||
|
case 0 if self.match(TokenType.SLASH):
|
||||||
|
pos_args = args
|
||||||
|
args = []
|
||||||
|
args_first_tokens = []
|
||||||
|
section = 1
|
||||||
|
case 0 | 1 if self.match(TokenType.STAR):
|
||||||
|
section = 2
|
||||||
|
case _:
|
||||||
|
# Record first token of mixed argument for errors if unnamed
|
||||||
|
if section != 2:
|
||||||
|
args_first_tokens.append(self.peek())
|
||||||
|
|
||||||
|
name: Optional[Token] = None
|
||||||
|
if section == 2:
|
||||||
|
name = self.consume_identifier("Expected keyword argument name")
|
||||||
|
self.consume(
|
||||||
|
TokenType.COLON, "Expected ':' after argument name"
|
||||||
|
)
|
||||||
|
elif self.check_identifier() and self.check_next(TokenType.COLON):
|
||||||
|
name = self.advance()
|
||||||
|
self.advance()
|
||||||
|
|
||||||
|
type: Type = self.type_expr()
|
||||||
|
optional: bool = self.match(TokenType.QMARK)
|
||||||
|
arg = FunctionType.Argument(
|
||||||
|
location=None,
|
||||||
|
name=name,
|
||||||
|
type=type,
|
||||||
|
required=not optional,
|
||||||
|
)
|
||||||
|
if section == 2:
|
||||||
|
kw_args.append(arg)
|
||||||
|
else:
|
||||||
|
args.append(arg)
|
||||||
|
|
||||||
|
if not self.match(TokenType.COMMA):
|
||||||
|
break
|
||||||
|
|
||||||
|
for arg, token in zip(args, args_first_tokens):
|
||||||
|
if arg.name is None:
|
||||||
|
# Not raised because we can keep parsing
|
||||||
|
self.error(token, "Unnamed mixed argument")
|
||||||
|
|
||||||
|
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after function parameters")
|
||||||
|
return ParamSpec(l_paren=l_paren, pos=pos_args, mixed=args, kw=kw_args)
|
||||||
|
|||||||
@@ -10,19 +10,27 @@ from midas.ast.python import (
|
|||||||
CastExpr,
|
CastExpr,
|
||||||
CompareExpr,
|
CompareExpr,
|
||||||
ConstraintType,
|
ConstraintType,
|
||||||
|
DictExpr,
|
||||||
Expr,
|
Expr,
|
||||||
ExpressionStmt,
|
ExpressionStmt,
|
||||||
|
ForStmt,
|
||||||
FrameColumn,
|
FrameColumn,
|
||||||
FrameType,
|
FrameType,
|
||||||
Function,
|
Function,
|
||||||
GetExpr,
|
GetExpr,
|
||||||
IfStmt,
|
IfStmt,
|
||||||
|
ListExpr,
|
||||||
LiteralExpr,
|
LiteralExpr,
|
||||||
LogicalExpr,
|
LogicalExpr,
|
||||||
MidasType,
|
MidasType,
|
||||||
|
RawExpr,
|
||||||
|
RawStmt,
|
||||||
ReturnStmt,
|
ReturnStmt,
|
||||||
|
SliceExpr,
|
||||||
Stmt,
|
Stmt,
|
||||||
|
SubscriptExpr,
|
||||||
TernaryExpr,
|
TernaryExpr,
|
||||||
|
TupleExpr,
|
||||||
TypeAssign,
|
TypeAssign,
|
||||||
UnaryExpr,
|
UnaryExpr,
|
||||||
VariableExpr,
|
VariableExpr,
|
||||||
@@ -42,6 +50,7 @@ class UnsupportedSyntaxError(Exception):
|
|||||||
|
|
||||||
class PythonParser:
|
class PythonParser:
|
||||||
CAST_FUNCTION = "cast"
|
CAST_FUNCTION = "cast"
|
||||||
|
UNSAFE_CAST_FUNCTION = "unsafe_cast"
|
||||||
|
|
||||||
def parse_module(self, node: ast.Module) -> list[Stmt]:
|
def parse_module(self, node: ast.Module) -> list[Stmt]:
|
||||||
statements: list[Stmt] = []
|
statements: list[Stmt] = []
|
||||||
@@ -87,9 +96,15 @@ class PythonParser:
|
|||||||
case ast.If():
|
case ast.If():
|
||||||
return self.parse_if(node)
|
return self.parse_if(node)
|
||||||
|
|
||||||
|
case ast.Pass():
|
||||||
|
return None
|
||||||
|
|
||||||
|
case ast.For(orelse=[]):
|
||||||
|
return self.parse_for(node)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
print(f"Unsupported statement: {ast.unparse(node)}")
|
print(f"Unsupported statement: {ast.unparse(node)}")
|
||||||
return None
|
return RawStmt(location=location, stmt=node)
|
||||||
|
|
||||||
def parse_annotation_assign(self, node: ast.AnnAssign) -> list[Stmt]:
|
def parse_annotation_assign(self, node: ast.AnnAssign) -> list[Stmt]:
|
||||||
statements: list[Stmt] = []
|
statements: list[Stmt] = []
|
||||||
@@ -176,6 +191,22 @@ class PythonParser:
|
|||||||
orelse=orelse,
|
orelse=orelse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parse_for(self, node: ast.For) -> ForStmt:
|
||||||
|
body: list[Stmt] = []
|
||||||
|
for stmt in node.body:
|
||||||
|
stmts = self.parse_stmt(stmt)
|
||||||
|
if isinstance(stmts, Stmt):
|
||||||
|
body.append(stmts)
|
||||||
|
elif stmts is not None:
|
||||||
|
body.extend(stmts)
|
||||||
|
|
||||||
|
return ForStmt(
|
||||||
|
location=Location.from_ast(node),
|
||||||
|
target=self.parse_expr(node.target),
|
||||||
|
iterator=self.parse_expr(node.iter),
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
def parse_function(self, node: ast.FunctionDef) -> Function:
|
def parse_function(self, node: ast.FunctionDef) -> Function:
|
||||||
loc: Location = Location.from_ast(node)
|
loc: Location = Location.from_ast(node)
|
||||||
match node:
|
match node:
|
||||||
@@ -270,26 +301,28 @@ class PythonParser:
|
|||||||
case ast.Subscript(value=ast.Name(id="Frame"), slice=schema):
|
case ast.Subscript(value=ast.Name(id="Frame"), slice=schema):
|
||||||
return self._parse_frame_type(schema)
|
return self._parse_frame_type(schema)
|
||||||
|
|
||||||
case ast.Subscript(value=ast.Name(id=name), slice=param):
|
case ast.Subscript(value=ast.Name(id=name), slice=arg):
|
||||||
|
args: tuple[MidasType, ...] = (
|
||||||
|
tuple(self._parse_type(a) for a in arg.elts)
|
||||||
|
if isinstance(arg, ast.Tuple)
|
||||||
|
else (self._parse_type(arg),)
|
||||||
|
)
|
||||||
return BaseType(
|
return BaseType(
|
||||||
location=loc,
|
location=loc,
|
||||||
base=name,
|
base=name,
|
||||||
param=self._parse_type(param),
|
args=args,
|
||||||
)
|
)
|
||||||
|
|
||||||
case ast.Name(id=name):
|
case ast.Name(id=name):
|
||||||
return BaseType(
|
return BaseType(
|
||||||
location=loc,
|
location=loc,
|
||||||
base=name,
|
base=name,
|
||||||
param=None,
|
args=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
case ast.BinOp(left=left_expr, op=ast.Add(), right=right_expr):
|
case ast.BinOp(left=left_expr, op=ast.Add(), right=right_expr):
|
||||||
left = self._parse_type(left_expr)
|
left = self._parse_type(left_expr)
|
||||||
match left:
|
match left:
|
||||||
case None:
|
|
||||||
raise InvalidSyntaxError()
|
|
||||||
|
|
||||||
# If chained constraints, separate base type and rebuild constraint
|
# If chained constraints, separate base type and rebuild constraint
|
||||||
case ConstraintType(type=left_type, constraint=left_constraint):
|
case ConstraintType(type=left_type, constraint=left_constraint):
|
||||||
constraint = ast.BinOp(
|
constraint = ast.BinOp(
|
||||||
@@ -311,6 +344,13 @@ class PythonParser:
|
|||||||
constraint=right_expr,
|
constraint=right_expr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case ast.Constant(value=None):
|
||||||
|
return BaseType(
|
||||||
|
location=loc,
|
||||||
|
base="None",
|
||||||
|
args=(),
|
||||||
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise UnsupportedSyntaxError(type_expr)
|
raise UnsupportedSyntaxError(type_expr)
|
||||||
|
|
||||||
@@ -387,6 +427,9 @@ class PythonParser:
|
|||||||
case ast.Call(func=ast.Name(id=self.CAST_FUNCTION)):
|
case ast.Call(func=ast.Name(id=self.CAST_FUNCTION)):
|
||||||
return self.parse_cast(node)
|
return self.parse_cast(node)
|
||||||
|
|
||||||
|
case ast.Call(func=ast.Name(id=self.UNSAFE_CAST_FUNCTION)):
|
||||||
|
return self.parse_cast(node)
|
||||||
|
|
||||||
case ast.Call():
|
case ast.Call():
|
||||||
return self.parse_call(node)
|
return self.parse_call(node)
|
||||||
|
|
||||||
@@ -406,8 +449,46 @@ class PythonParser:
|
|||||||
case ast.Name(id=name):
|
case ast.Name(id=name):
|
||||||
return VariableExpr(location=location, name=name)
|
return VariableExpr(location=location, name=name)
|
||||||
|
|
||||||
|
case ast.List(elts=items):
|
||||||
|
return ListExpr(
|
||||||
|
location=location,
|
||||||
|
items=[self.parse_expr(item) for item in items],
|
||||||
|
)
|
||||||
|
|
||||||
|
case ast.Dict(keys=keys, values=values):
|
||||||
|
return DictExpr(
|
||||||
|
location=location,
|
||||||
|
keys=[
|
||||||
|
self.parse_expr(key) if key is not None else None
|
||||||
|
for key in keys
|
||||||
|
],
|
||||||
|
values=[self.parse_expr(value) for value in values],
|
||||||
|
)
|
||||||
|
|
||||||
|
case ast.Subscript(value=value, slice=index):
|
||||||
|
return SubscriptExpr(
|
||||||
|
location=location,
|
||||||
|
object=self.parse_expr(value),
|
||||||
|
index=self.parse_expr(index),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ast.Slice(lower=lower, upper=upper, step=step):
|
||||||
|
return SliceExpr(
|
||||||
|
location=location,
|
||||||
|
lower=self.parse_expr(lower) if lower is not None else None,
|
||||||
|
upper=self.parse_expr(upper) if upper is not None else None,
|
||||||
|
step=self.parse_expr(step) if step is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
case ast.Tuple(elts=items):
|
||||||
|
return TupleExpr(
|
||||||
|
location=location,
|
||||||
|
items=tuple(self.parse_expr(item) for item in items),
|
||||||
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise UnsupportedSyntaxError(node)
|
print(f"Unsupported expression: {ast.unparse(node)}")
|
||||||
|
return RawExpr(location=location, expr=node)
|
||||||
|
|
||||||
def parse_bool_op(self, node: ast.BoolOp) -> LogicalExpr:
|
def parse_bool_op(self, node: ast.BoolOp) -> LogicalExpr:
|
||||||
op: ast.boolop = node.op
|
op: ast.boolop = node.op
|
||||||
@@ -459,16 +540,19 @@ class PythonParser:
|
|||||||
return expr
|
return expr
|
||||||
|
|
||||||
def parse_cast(self, node: ast.Call) -> CastExpr:
|
def parse_cast(self, node: ast.Call) -> CastExpr:
|
||||||
|
assert isinstance(node.func, ast.Name)
|
||||||
|
func: str = node.func.id
|
||||||
match node:
|
match node:
|
||||||
case ast.Call(args=[type, expr], keywords=[]):
|
case ast.Call(args=[type, expr], keywords=[]):
|
||||||
return CastExpr(
|
return CastExpr(
|
||||||
location=Location.from_ast(node),
|
location=Location.from_ast(node),
|
||||||
type=self._parse_type(type),
|
type=self._parse_type(type),
|
||||||
expr=self.parse_expr(expr),
|
expr=self.parse_expr(expr),
|
||||||
|
unsafe=func == self.UNSAFE_CAST_FUNCTION,
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
raise InvalidSyntaxError(
|
raise InvalidSyntaxError(
|
||||||
f"Invalid call to {self.CAST_FUNCTION}, expected type and expression"
|
f"Invalid call to {func}, expected type and expression"
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_call(self, node: ast.Call) -> CallExpr:
|
def parse_call(self, node: ast.Call) -> CallExpr:
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from midas.checker.types import BaseType, Type, UnitType
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from midas.resolver.midas import MidasResolver
|
|
||||||
|
|
||||||
|
|
||||||
def op(ctx: MidasResolver, t1: Type, operator: str, t2: Type, t3: Type):
|
|
||||||
ctx.define_operation(
|
|
||||||
left=t1,
|
|
||||||
operator=operator,
|
|
||||||
right=t2,
|
|
||||||
result=t3,
|
|
||||||
)
|
|
||||||
|
|
||||||
def basic_op(ctx: MidasResolver, type: Type, op: str):
|
|
||||||
ctx.define_operation(
|
|
||||||
left=type,
|
|
||||||
operator=op,
|
|
||||||
right=type,
|
|
||||||
result=type,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def define_builtins(ctx: MidasResolver):
|
|
||||||
"""Define builtin types and operations"""
|
|
||||||
unit = ctx.define_type("None", UnitType())
|
|
||||||
bool = ctx.define_type("bool", BaseType(name="bool"))
|
|
||||||
int = ctx.define_type("int", BaseType(name="int"))
|
|
||||||
float = ctx.define_type("float", BaseType(name="float"))
|
|
||||||
str = ctx.define_type("str", BaseType(name="str"))
|
|
||||||
|
|
||||||
basic_op(ctx, int, "__add__") # int + int = int
|
|
||||||
basic_op(ctx, int, "__sub__") # int - int = int
|
|
||||||
basic_op(ctx, int, "__mul__") # int * int = int
|
|
||||||
basic_op(ctx, int, "__pow__") # int ** int = int
|
|
||||||
basic_op(ctx, int, "__mod__") # int % int = int
|
|
||||||
basic_op(ctx, int, "__and__") # int & int = int
|
|
||||||
basic_op(ctx, int, "__or__") # int | int = int
|
|
||||||
basic_op(ctx, int, "__xor__") # int ^ int = int
|
|
||||||
op(ctx, int, "__lt__", int, bool) # int < int = bool
|
|
||||||
op(ctx, int, "__gt__", int, bool) # int > int = bool
|
|
||||||
op(ctx, int, "__le__", int, bool) # int <= int = bool
|
|
||||||
op(ctx, int, "__ge__", int, bool) # int >= int = bool
|
|
||||||
op(ctx, int, "__eq__", int, bool) # int == int = bool
|
|
||||||
basic_op(ctx, float, "__add__") # float + float = float
|
|
||||||
basic_op(ctx, float, "__sub__") # float - float = float
|
|
||||||
basic_op(ctx, float, "__mul__") # float * float = float
|
|
||||||
basic_op(ctx, float, "__truediv__") # float / float = float
|
|
||||||
op(ctx, float, "__lt__", float, bool) # float < float = bool
|
|
||||||
op(ctx, float, "__gt__", float, bool) # float > float = bool
|
|
||||||
op(ctx, float, "__le__", float, bool) # float <= float = bool
|
|
||||||
op(ctx, float, "__ge__", float, bool) # float >= float = bool
|
|
||||||
op(ctx, float, "__eq__", float, bool) # float == float = bool
|
|
||||||
basic_op(ctx, str, "__add__") # str + str = str
|
|
||||||
op(ctx, str, "__eq__", str, bool) # str == str = bool
|
|
||||||
|
|
||||||
op(ctx, int, "__lt__", float, bool) # int < float = bool
|
|
||||||
op(ctx, int, "__gt__", float, bool) # int > float = bool
|
|
||||||
op(ctx, int, "__le__", float, bool) # int <= float = bool
|
|
||||||
op(ctx, int, "__ge__", float, bool) # int >= float = bool
|
|
||||||
op(ctx, int, "__eq__", float, bool) # int == float = bool
|
|
||||||
|
|
||||||
op(ctx, float, "__lt__", int, bool) # float < int = bool
|
|
||||||
op(ctx, float, "__gt__", int, bool) # float > int = bool
|
|
||||||
op(ctx, float, "__le__", int, bool) # float <= int = bool
|
|
||||||
op(ctx, float, "__ge__", int, bool) # float >= int = bool
|
|
||||||
op(ctx, float, "__eq__", int, bool) # float == int = bool
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
import midas.ast.midas as m
|
|
||||||
from midas.checker.types import BaseType, SimpleType, Type
|
|
||||||
from midas.resolver.builtin import define_builtins
|
|
||||||
|
|
||||||
|
|
||||||
class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
|
||||||
"""A resolver which evaluates Midas type definitions and build a registry"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._types: dict[str, Type] = {}
|
|
||||||
self._operations: dict[tuple[Type, str, Type], Type] = {}
|
|
||||||
|
|
||||||
define_builtins(self)
|
|
||||||
|
|
||||||
def get_type(self, name: str) -> Type:
|
|
||||||
"""Get a type from its name
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): the name of the type
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NameError: if the type is not defined
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Type: the type
|
|
||||||
"""
|
|
||||||
type: Optional[Type] = self._types.get(name)
|
|
||||||
if type is None:
|
|
||||||
raise NameError(f"Undefined type {name}")
|
|
||||||
return type
|
|
||||||
|
|
||||||
def get_operation_result(
|
|
||||||
self, left: Type, operator: str, right: Type
|
|
||||||
) -> Optional[Type]:
|
|
||||||
"""Get the resulting type of an operation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
left (Type): the type of the left operand
|
|
||||||
operator (str): the operation name
|
|
||||||
right (Type): the type of the right operand
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Type]: the result type, or None if no matching operation was found
|
|
||||||
"""
|
|
||||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
|
||||||
result: Optional[Type] = self._operations.get(operation)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def define_type(self, name: str, type: Type) -> Type:
|
|
||||||
"""Define a type in the registry
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): the name of the type
|
|
||||||
type (Type): the type to define
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: if a type is already defined with that name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Type: the defined type
|
|
||||||
"""
|
|
||||||
if name in self._types:
|
|
||||||
raise ValueError(f"Type {name} already defined")
|
|
||||||
self._types[name] = type
|
|
||||||
return type
|
|
||||||
|
|
||||||
def define_operation(self, left: Type, operator: str, right: Type, result: Type):
|
|
||||||
"""Define an operation in the registry
|
|
||||||
|
|
||||||
Args:
|
|
||||||
left (Type): the type of the left operand
|
|
||||||
operator (str): the operation name
|
|
||||||
right (Type): the type of the right operand
|
|
||||||
result (Type): the result type
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: if an operation is already defined with these operands and name
|
|
||||||
"""
|
|
||||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
|
||||||
if operation in self._operations:
|
|
||||||
raise ValueError(
|
|
||||||
f"Operation {operator} already defined between {left} and {right}"
|
|
||||||
)
|
|
||||||
self._operations[operation] = result
|
|
||||||
|
|
||||||
def resolve(self, stmts: list[m.Stmt]):
|
|
||||||
"""Process a sequence of statements
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stmts (list[m.Stmt]): the statements
|
|
||||||
"""
|
|
||||||
for stmt in stmts:
|
|
||||||
stmt.accept(self)
|
|
||||||
|
|
||||||
def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt) -> None:
|
|
||||||
# TODO generics, optional, constraint
|
|
||||||
base: Type = self.get_type(stmt.base.name.lexeme)
|
|
||||||
match base:
|
|
||||||
case BaseType() | SimpleType():
|
|
||||||
type = SimpleType(
|
|
||||||
name=stmt.name.lexeme,
|
|
||||||
base=base,
|
|
||||||
)
|
|
||||||
self.define_type(type.name, type)
|
|
||||||
case _:
|
|
||||||
raise TypeError(f"Invalid base {base} for simple type")
|
|
||||||
|
|
||||||
def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt) -> None: ...
|
|
||||||
|
|
||||||
def visit_property_stmt(self, stmt: m.PropertyStmt) -> None: ...
|
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
|
||||||
base: Type = stmt.type.accept(self)
|
|
||||||
for op in stmt.operations:
|
|
||||||
right: Type = op.operand.accept(self)
|
|
||||||
result: Type = op.result.accept(self)
|
|
||||||
self.define_operation(
|
|
||||||
left=base,
|
|
||||||
operator=op.name.lexeme,
|
|
||||||
right=right,
|
|
||||||
result=result,
|
|
||||||
)
|
|
||||||
|
|
||||||
def visit_op_stmt(self, stmt: m.OpStmt) -> None: ...
|
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None: ...
|
|
||||||
|
|
||||||
def visit_simple_type_expr(self, expr: m.SimpleTypeExpr) -> Type:
|
|
||||||
return self.get_type(expr.name.lexeme)
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: m.BinaryExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: m.UnaryExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: m.VariableExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_grouping_expr(self, expr: m.GroupingExpr) -> Type:
|
|
||||||
return expr.expr.accept(self)
|
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: m.LiteralExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_template_expr(self, expr: m.TemplateExpr) -> Type: ...
|
|
||||||
|
|
||||||
def visit_type_expr(self, expr: m.TypeExpr) -> Type:
|
|
||||||
return self.get_type(expr.name.lexeme)
|
|
||||||
52
midas/typing.py
Normal file
52
midas/typing.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from typing import Generic, TypeVar
|
||||||
|
from typing import cast as typing_cast
|
||||||
|
|
||||||
|
cast = typing_cast
|
||||||
|
"""### Midas documentation
|
||||||
|
Cast a value to a type.
|
||||||
|
|
||||||
|
- **Compile-time**: tells the type checker that the return value has the designated type.
|
||||||
|
- **Run-time**: generates assertions to ensure the value can be interpreted as the given type.
|
||||||
|
|
||||||
|
---
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
_**Internal Python documentation**_
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
unsafe_cast = typing_cast
|
||||||
|
"""### Midas documentation
|
||||||
|
Cast a value to a type.
|
||||||
|
|
||||||
|
- **Compile-time**: tells the type checker that the return value has the designated type.
|
||||||
|
- **Run-time**: -
|
||||||
|
|
||||||
|
This operation is unsound, use at your own risk!
|
||||||
|
|
||||||
|
---
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
_**Internal Python documentation**_
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class Frame(Generic[T]):
|
||||||
|
"""A `Frame` is the abstract type implemented by `DataFrame`
|
||||||
|
|
||||||
|
A frame contains any number of named columns (see :class:`Column`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Column(Generic[T]):
|
||||||
|
"""A `Column` is the abstract type implemented by `Series`
|
||||||
|
|
||||||
|
A column contains a any number of values of the same type
|
||||||
|
"""
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.checker.types import Type
|
||||||
|
from midas.generator.collector import AssertionCollector
|
||||||
|
|
||||||
AllowRepeat = Callable[[object], bool]
|
AllowRepeat = Callable[[object], bool]
|
||||||
|
|
||||||
|
|
||||||
@@ -52,3 +57,11 @@ class UniversalJSONDumper:
|
|||||||
}
|
}
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unsupported value: {obj}")
|
raise ValueError(f"Unsupported value: {obj}")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TypedAST:
|
||||||
|
stmts: list[p.Stmt]
|
||||||
|
judgements: list[tuple[p.Expr, Type]]
|
||||||
|
evaluated_casts: list[p.CastExpr]
|
||||||
|
assertions: AssertionCollector
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ authors = [
|
|||||||
{ name = "Louis Heredero", email = "louis.heredero@students.hevs.ch" },
|
{ name = "Louis Heredero", email = "louis.heredero@students.hevs.ch" },
|
||||||
]
|
]
|
||||||
classifiers = ["Programming Language :: Python :: 3"]
|
classifiers = ["Programming Language :: Python :: 3"]
|
||||||
dependencies = ["click>=8.4.1"]
|
dependencies = [
|
||||||
|
"black>=26.5.1",
|
||||||
|
"click>=8.4.1",
|
||||||
|
"watchdog>=6.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://git.kbk28.ch/HEL/midas"
|
Homepage = "https://git.kbk28.ch/HEL/midas"
|
||||||
|
|||||||
@@ -19,16 +19,24 @@ Comparison ::= Unary (ComparisonOp Unary)*
|
|||||||
Equality ::= Comparison (EqualityOp Comparison)*
|
Equality ::= Comparison (EqualityOp Comparison)*
|
||||||
Constraint ::= Equality ("&" Equality)*
|
Constraint ::= Equality ("&" Equality)*
|
||||||
|
|
||||||
SimpleType ::= Identifier "?"?
|
TemplateParam ::= Identifier ("<:" Type)?
|
||||||
Template ::= "[" Type "]"
|
Template ::= "[" (TemplateParam ("," TemplateParam)*)? "]"
|
||||||
Type ::= Identifier Template? "?"?
|
|
||||||
|
|
||||||
|
TypeProperty ::= Identifier ":" Type
|
||||||
|
ComplexType ::= "{" TypeProperty* "}"
|
||||||
|
NamedType ::= Identifier
|
||||||
|
TypeParams ::= "[" (Type ("," Type)*)? "]"
|
||||||
|
GenericType ::= NamedType TypeParams?
|
||||||
|
GroupedType ::= "(" Type ")"
|
||||||
|
BaseType ::= GroupedType | ComplexType | GenericType
|
||||||
|
ConstraintType ::= BaseType ("where" Constraint)?
|
||||||
|
Type ::= ConstraintType
|
||||||
|
|
||||||
TypeProperty ::= Identifier ":" Type ("where" Constraints)?
|
|
||||||
ComplexTypeBody ::= "{" TypeProperty* "}"
|
|
||||||
OpDefinition ::= "op" Identifier "(" Type ")" "->" Type
|
OpDefinition ::= "op" Identifier "(" Type ")" "->" Type
|
||||||
ExtendBody ::= "{" OpDefinition* "}"
|
ExtendBody ::= "{" OpDefinition* "}"
|
||||||
|
|
||||||
TypeStatement ::= "type" Identifier Template? ("(" Type ")" ("where" Constraint)? | ComplexTypeBody)
|
TypeStatement ::= "type" Identifier Template? "=" Type
|
||||||
ExtendStatement ::= "extend" Type ExtendBody
|
ExtendStatement ::= "extend" Type ExtendBody
|
||||||
PredicateStatement ::= "predicate" Identifier "(" Identifier ":" Type ")" "=" Constraint
|
PredicateStatement ::= "predicate" Identifier "(" Identifier ":" Type ")" "=" Constraint
|
||||||
|
|
||||||
|
|||||||
@@ -43,28 +43,52 @@ svg.railroad .terminal rect {
|
|||||||
{[`constraint` 'equality'*"&"]}
|
{[`constraint` 'equality'*"&"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let simple-type = ```
|
#let template-param = ```
|
||||||
{[`simple-type` 'identifier' <!, "?">]}
|
{[`template-param` 'identifier' <!, ["<:" 'type']>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let template = ```
|
#let template = ```
|
||||||
{[`template` "[" 'type' "]"]}
|
{[`template` "[" <!, 'template-param'*","> "]"]}
|
||||||
```
|
|
||||||
|
|
||||||
#let type = ```
|
|
||||||
{[`type` 'identifier' <!, 'template'> <!, "?">]}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-property = ```
|
#let type-property = ```
|
||||||
{[`type-property` 'identifier' ":" 'type' <!, ["where" 'constraint']>]}
|
{[`type-property` 'identifier' ":" 'type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-body = ```
|
#let complex-type = ```
|
||||||
{[`type-body` "{" <!, 'type-property'*!> "}"]}
|
{[`complex-type` "{" <!, 'type-property'*!> "}"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let named-type = ```
|
||||||
|
{[`named-type` 'identifier']}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let type-params = ```
|
||||||
|
{[`type-params` "[" <!, 'type'*","> "]"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let generic-type = ```
|
||||||
|
{[`generic-type` 'named-type' <!, 'type-params'>]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let grouped-type = ```
|
||||||
|
{[`grouped-type` "(" 'type' ")"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let base-type = ```
|
||||||
|
{[`base-type` <'grouped-type', 'complex-type', 'generic-type'>]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let constraint-type = ```
|
||||||
|
{[`constraint-type` 'base-type' <!, ["where" 'constraint']>]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let type = ```
|
||||||
|
{[`type` 'constraint-type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-statement = ```
|
#let type-statement = ```
|
||||||
{[`type-statement` "type" 'identifier' <!, 'template'> <[["(" 'type' ")"] <!, ["where" 'constraint']>], 'type-body'>]}
|
{[`type-statement` "type" 'identifier' <!, 'template'> "=" 'type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let op-definition = ```
|
#let op-definition = ```
|
||||||
@@ -92,11 +116,17 @@ svg.railroad .terminal rect {
|
|||||||
comparison: comparison,
|
comparison: comparison,
|
||||||
equality: equality,
|
equality: equality,
|
||||||
constraint: constraint,
|
constraint: constraint,
|
||||||
simple-type: simple-type,
|
template-param: template-param,
|
||||||
template: template,
|
template: template,
|
||||||
type: type,
|
|
||||||
type-property: type-property,
|
type-property: type-property,
|
||||||
type-body: type-body,
|
complex-type: complex-type,
|
||||||
|
named-type: named-type,
|
||||||
|
type-params: type-params,
|
||||||
|
generic-type: generic-type,
|
||||||
|
grouped-type: grouped-type,
|
||||||
|
base-type: base-type,
|
||||||
|
constraint-type: constraint-type,
|
||||||
|
type: type,
|
||||||
type-statement: type-statement,
|
type-statement: type-statement,
|
||||||
op-definition: op-definition,
|
op-definition: op-definition,
|
||||||
extend-statement: extend-statement,
|
extend-statement: extend-statement,
|
||||||
@@ -107,10 +137,16 @@ svg.railroad .terminal rect {
|
|||||||
#let inline = (
|
#let inline = (
|
||||||
"grouping",
|
"grouping",
|
||||||
"value",
|
"value",
|
||||||
|
"template-param",
|
||||||
"template",
|
"template",
|
||||||
"simple-type",
|
|
||||||
"type-property",
|
"type-property",
|
||||||
"type-body",
|
"complex-type",
|
||||||
|
"type-params",
|
||||||
|
"named-type",
|
||||||
|
"grouped-type",
|
||||||
|
"generic-type",
|
||||||
|
"base-type",
|
||||||
|
"constraint-type",
|
||||||
"op-definition",
|
"op-definition",
|
||||||
"type-statement",
|
"type-statement",
|
||||||
"extend-statement",
|
"extend-statement",
|
||||||
|
|||||||
43
tests/__main__.py
Normal file
43
tests/__main__.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from midas.cli.ansi import Ansi
|
||||||
|
from tests.base import Tester
|
||||||
|
from tests.checker import CheckerTester
|
||||||
|
from tests.generator import GeneratorTester
|
||||||
|
from tests.midas import MidasTester
|
||||||
|
from tests.python import PythonTester
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner(name: str):
|
||||||
|
horizontal: str = "+" + "-" * (len(name) + 2) + "+"
|
||||||
|
print(horizontal)
|
||||||
|
print(f"| {name} |")
|
||||||
|
print(horizontal)
|
||||||
|
|
||||||
|
|
||||||
|
def run_tests(tester_cls: Type[Tester]) -> bool:
|
||||||
|
print_banner(tester_cls.__name__)
|
||||||
|
tester: Tester = tester_cls()
|
||||||
|
success: bool = tester.run_all_tests()
|
||||||
|
print()
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
testers: list[Type[Tester]] = [
|
||||||
|
PythonTester,
|
||||||
|
MidasTester,
|
||||||
|
CheckerTester,
|
||||||
|
GeneratorTester,
|
||||||
|
]
|
||||||
|
|
||||||
|
success: bool = all(map(run_tests, testers))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(Ansi.FG(Ansi.BRIGHT_GREEN) + "All tests passed!" + Ansi.RESET)
|
||||||
|
else:
|
||||||
|
print(Ansi.FG(Ansi.BRIGHT_RED) + "Some tests failed!" + Ansi.RESET)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,6 +7,8 @@ from abc import ABC, abstractmethod
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, Protocol
|
from typing import Iterator, Protocol
|
||||||
|
|
||||||
|
from midas.cli.ansi import Ansi
|
||||||
|
|
||||||
|
|
||||||
class CaseResult(Protocol):
|
class CaseResult(Protocol):
|
||||||
def dumps(self) -> str: ...
|
def dumps(self) -> str: ...
|
||||||
@@ -21,6 +23,10 @@ class Tester(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def namespace(self) -> str: ...
|
def namespace(self) -> str: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extension(self) -> str:
|
||||||
|
return "json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_dir(self) -> Path:
|
def base_dir(self) -> Path:
|
||||||
return self.CASES_DIR / self.namespace
|
return self.CASES_DIR / self.namespace
|
||||||
@@ -29,7 +35,7 @@ class Tester(ABC):
|
|||||||
def _list_tests(self) -> list[Path]: ...
|
def _list_tests(self) -> list[Path]: ...
|
||||||
|
|
||||||
def run_all_tests(self) -> bool:
|
def run_all_tests(self) -> bool:
|
||||||
paths: list[Path] = self._list_tests()
|
paths: list[Path] = sorted(self._list_tests())
|
||||||
return self.run_tests(paths)
|
return self.run_tests(paths)
|
||||||
|
|
||||||
def run_tests(self, tests: list[Path]) -> bool:
|
def run_tests(self, tests: list[Path]) -> bool:
|
||||||
@@ -40,8 +46,11 @@ class Tester(ABC):
|
|||||||
|
|
||||||
print(rule)
|
print(rule)
|
||||||
for i, test in enumerate(tests):
|
for i, test in enumerate(tests):
|
||||||
print(f"Case {i+1}/{n}: {test.relative_to(self.CASES_DIR)}")
|
path: Path = test.resolve().relative_to(self.CASES_DIR)
|
||||||
|
print(f"{Ansi.FG(Ansi.BRIGHT_CYAN)}Case {i+1}/{n}: {path}{Ansi.RESET}")
|
||||||
|
print(Ansi.DIM, end="")
|
||||||
success: bool = self._run_test(test)
|
success: bool = self._run_test(test)
|
||||||
|
print(Ansi.RESET, end="")
|
||||||
if success:
|
if success:
|
||||||
successes += 1
|
successes += 1
|
||||||
else:
|
else:
|
||||||
@@ -78,7 +87,7 @@ class Tester(ABC):
|
|||||||
def _exec_case(self, path: Path) -> CaseResult: ...
|
def _exec_case(self, path: Path) -> CaseResult: ...
|
||||||
|
|
||||||
def update_all_tests(self):
|
def update_all_tests(self):
|
||||||
paths: list[Path] = self._list_tests()
|
paths: list[Path] = sorted(self._list_tests())
|
||||||
return self.update_tests(paths)
|
return self.update_tests(paths)
|
||||||
|
|
||||||
def update_tests(self, tests: list[Path]):
|
def update_tests(self, tests: list[Path]):
|
||||||
@@ -99,7 +108,7 @@ class Tester(ABC):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _result_path(self, test_path: Path) -> Path:
|
def _result_path(self, test_path: Path) -> Path:
|
||||||
return test_path.parent / (test_path.name + ".ref.json")
|
return test_path.parent / (test_path.name + f".ref.{self.extension}")
|
||||||
|
|
||||||
def _print_diff(self, diff: Iterator[str]):
|
def _print_diff(self, diff: Iterator[str]):
|
||||||
for line in diff:
|
for line in diff:
|
||||||
@@ -141,3 +150,10 @@ class Tester(ABC):
|
|||||||
success = tester.run_tests(args.FILE)
|
success = tester.run_tests(args.FILE)
|
||||||
if not success:
|
if not success:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
case None:
|
||||||
|
success: bool = tester.run_all_tests()
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
case _:
|
||||||
|
print(f"Unknown subcommand '{args.subcommand}'")
|
||||||
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,3 +1,47 @@
|
|||||||
{
|
{
|
||||||
"diagnostics": []
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
8,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
8,
|
||||||
|
43
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "ConstraintType not yet supported"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
10,
|
||||||
|
18
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Unknown type 'datetime'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
13,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
13,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Unknown type '_'"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"judgments": []
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
13
|
13
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"message": "Cannot assign BaseType(name='str') to c of type BaseType(name='int')"
|
"message": "Cannot assign str to variable 'c' of type int"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Error",
|
"type": "Error",
|
||||||
@@ -26,21 +26,166 @@
|
|||||||
9
|
9
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"message": "Undefined operation __add__ between BaseType(name='bool') and BaseType(name='bool')"
|
"message": "Undefined operation __add__ between bool and bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L1:9",
|
||||||
|
"to": "L1:10"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Error",
|
|
||||||
"location": {
|
"location": {
|
||||||
"start": [
|
"from": "L2:9",
|
||||||
11,
|
"to": "L2:10"
|
||||||
0
|
|
||||||
],
|
|
||||||
"end": [
|
|
||||||
11,
|
|
||||||
12
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"message": "Cannot assign BaseType(name='int') to f of type BaseType(name='float')"
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:4",
|
||||||
|
"to": "L4:5"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:8",
|
||||||
|
"to": "L4:9"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:4",
|
||||||
|
"to": "L4:9"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:4",
|
||||||
|
"to": "L6:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": "invalid"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "str"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L8:4",
|
||||||
|
"to": "L8:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:4",
|
||||||
|
"to": "L9:5"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "d"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:8",
|
||||||
|
"to": "L9:9"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "d"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:4",
|
||||||
|
"to": "L9:9"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "d"
|
||||||
|
},
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:11",
|
||||||
|
"to": "L11:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
|||||||
type Meter(float)
|
type Meter = float
|
||||||
type Second(float)
|
type Second = float
|
||||||
type MeterPerSecond(float)
|
type MeterPerSecond = float
|
||||||
|
|
||||||
extend Meter {
|
extend Meter {
|
||||||
op __add__(Meter) -> Meter
|
def __add__: fn(Meter, /) -> Meter
|
||||||
op __sub__(Meter) -> Meter
|
def __sub__: fn(Meter, /) -> Meter
|
||||||
op __truediv__(Second) -> MeterPerSecond
|
def __truediv__: fn(Second, /) -> MeterPerSecond
|
||||||
}
|
}
|
||||||
|
|
||||||
extend Second {
|
extend Second {
|
||||||
op __add__(Second) -> Second
|
def __add__: fn(Second, /) -> Second
|
||||||
op __sub__(Second) -> Second
|
def __sub__: fn(Second, /) -> Second
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
# ruff: disable [F821]
|
# ruff: disable [F821]
|
||||||
|
|
||||||
midas.using("04_custom_types.midas")
|
|
||||||
|
|
||||||
distance: Meter = cast(Meter, 123.45)
|
distance: Meter = cast(Meter, 123.45)
|
||||||
time: Second = cast(Second, 6.7)
|
time: Second = cast(Second, 6.7)
|
||||||
speed = distance / time
|
speed = distance / time
|
||||||
|
|||||||
@@ -1,3 +1,137 @@
|
|||||||
{
|
{
|
||||||
"diagnostics": []
|
"diagnostics": [],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:30",
|
||||||
|
"to": "L4:36"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 123.45
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:18",
|
||||||
|
"to": "L4:37"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CastExpr",
|
||||||
|
"type": {
|
||||||
|
"_type": "BaseType",
|
||||||
|
"base": "Meter",
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 123.45
|
||||||
|
},
|
||||||
|
"unsafe": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Meter",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:28",
|
||||||
|
"to": "L5:31"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 6.7
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:15",
|
||||||
|
"to": "L5:32"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CastExpr",
|
||||||
|
"type": {
|
||||||
|
"_type": "BaseType",
|
||||||
|
"base": "Second",
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 6.7
|
||||||
|
},
|
||||||
|
"unsafe": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Second",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:8",
|
||||||
|
"to": "L6:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "distance"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Meter",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:19",
|
||||||
|
"to": "L6:23"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "time"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Second",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:8",
|
||||||
|
"to": "L6:23"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "distance"
|
||||||
|
},
|
||||||
|
"operator": "/",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "MeterPerSecond",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -42,5 +42,409 @@
|
|||||||
},
|
},
|
||||||
"message": "Mixed return types: [BaseType(name='int'), BaseType(name='str')]"
|
"message": "Mixed return types: [BaseType(name='int'), BaseType(name='str')]"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:11",
|
||||||
|
"to": "L2:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:15",
|
||||||
|
"to": "L2:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:11",
|
||||||
|
"to": "L2:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:7",
|
||||||
|
"to": "L5:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:11",
|
||||||
|
"to": "L5:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:7",
|
||||||
|
"to": "L5:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CompareExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "<",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:15",
|
||||||
|
"to": "L6:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:19",
|
||||||
|
"to": "L6:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:15",
|
||||||
|
"to": "L6:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L8:15",
|
||||||
|
"to": "L8:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L8:19",
|
||||||
|
"to": "L8:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L8:15",
|
||||||
|
"to": "L8:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L15:7",
|
||||||
|
"to": "L15:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L15:11",
|
||||||
|
"to": "L15:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 10
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L15:7",
|
||||||
|
"to": "L15:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CompareExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": ">",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L16:15",
|
||||||
|
"to": "L16:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L16:19",
|
||||||
|
"to": "L16:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 10
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L16:15",
|
||||||
|
"to": "L16:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L18:15",
|
||||||
|
"to": "L18:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L22:7",
|
||||||
|
"to": "L22:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L22:11",
|
||||||
|
"to": "L22:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L22:7",
|
||||||
|
"to": "L22:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CompareExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"operator": "<",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L23:15",
|
||||||
|
"to": "L23:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L23:19",
|
||||||
|
"to": "L23:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L23:15",
|
||||||
|
"to": "L23:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L25:15",
|
||||||
|
"to": "L25:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": "oops"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "str"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
12
tests/cases/checker/06_subtyping.py
Normal file
12
tests/cases/checker/06_subtyping.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
v1: int = 3
|
||||||
|
v2: float = 4
|
||||||
|
|
||||||
|
|
||||||
|
def maximum(a: float, b: float):
|
||||||
|
if b > a:
|
||||||
|
return b
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
v3 = maximum(v1, v2)
|
||||||
|
v3 = v2 + v1
|
||||||
239
tests/cases/checker/06_subtyping.py.ref.json
Normal file
239
tests/cases/checker/06_subtyping.py.ref.json
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
{
|
||||||
|
"diagnostics": [],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L1:10",
|
||||||
|
"to": "L1:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:12",
|
||||||
|
"to": "L2:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:7",
|
||||||
|
"to": "L6:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:11",
|
||||||
|
"to": "L6:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:7",
|
||||||
|
"to": "L6:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CompareExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"operator": ">",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L7:15",
|
||||||
|
"to": "L7:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "b"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L8:11",
|
||||||
|
"to": "L8:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "a"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:13",
|
||||||
|
"to": "L11:15"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:17",
|
||||||
|
"to": "L11:19"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:5",
|
||||||
|
"to": "L11:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "maximum"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "a",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "b",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:5",
|
||||||
|
"to": "L11:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "maximum"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:5",
|
||||||
|
"to": "L12:7"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:10",
|
||||||
|
"to": "L12:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:5",
|
||||||
|
"to": "L12:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v2"
|
||||||
|
},
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
tests/cases/checker/07_variance.midas
Normal file
59
tests/cases/checker/07_variance.midas
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// T is invariant (unused)
|
||||||
|
type Unused[T] = object
|
||||||
|
|
||||||
|
// T is covariant
|
||||||
|
type Covariant[T] = object
|
||||||
|
|
||||||
|
// T is contravariant
|
||||||
|
type Contravariant[T] = object
|
||||||
|
|
||||||
|
// T is invariant
|
||||||
|
type Invariant[T] = object
|
||||||
|
|
||||||
|
extend Covariant[T] {
|
||||||
|
def foo: fn() -> T
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Contravariant[T] {
|
||||||
|
def foo: fn(T, /) -> None
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Invariant[T] {
|
||||||
|
def foo: fn(T, /) -> T
|
||||||
|
}
|
||||||
|
|
||||||
|
// T is covariant
|
||||||
|
type Coco[T] = object
|
||||||
|
extend Coco[T] {
|
||||||
|
def foo: fn() -> Covariant[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// T is contravariant
|
||||||
|
type Cocontra[T] = object
|
||||||
|
extend Cocontra[T] {
|
||||||
|
def foo: fn() -> Contravariant[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// T is contravariant
|
||||||
|
type Contraco[T] = object
|
||||||
|
extend Contraco[T] {
|
||||||
|
def foo: fn(Covariant[T], /) -> None
|
||||||
|
}
|
||||||
|
|
||||||
|
// T is covariant
|
||||||
|
type Contracontra[T] = object
|
||||||
|
extend Contracontra[T] {
|
||||||
|
def foo: fn(Contravariant[T], /) -> None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type T1[T] = object
|
||||||
|
type T2[T] = object
|
||||||
|
|
||||||
|
extend T1[T] {
|
||||||
|
def foo: fn() -> T2[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
extend T2[T] {
|
||||||
|
def foo: fn() -> T1[T]
|
||||||
|
}
|
||||||
52
tests/cases/checker/07_variance.py
Normal file
52
tests/cases/checker/07_variance.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from _ import (
|
||||||
|
T1,
|
||||||
|
T2,
|
||||||
|
Coco,
|
||||||
|
Cocontra,
|
||||||
|
Contraco,
|
||||||
|
Contracontra,
|
||||||
|
Contravariant,
|
||||||
|
Covariant,
|
||||||
|
Invariant,
|
||||||
|
Unused,
|
||||||
|
)
|
||||||
|
|
||||||
|
unused: Unused
|
||||||
|
covariant: Covariant
|
||||||
|
contravariant: Contravariant
|
||||||
|
invariant: Invariant
|
||||||
|
coco: Coco
|
||||||
|
cocontra: Cocontra
|
||||||
|
contraco: Contraco
|
||||||
|
contracontra: Contracontra
|
||||||
|
t1: T1
|
||||||
|
t2: T2
|
||||||
|
|
||||||
|
# Dummy print to prudce judgements for the expressions
|
||||||
|
print(
|
||||||
|
unused,
|
||||||
|
covariant,
|
||||||
|
contravariant,
|
||||||
|
invariant,
|
||||||
|
coco,
|
||||||
|
cocontra,
|
||||||
|
contraco,
|
||||||
|
contracontra,
|
||||||
|
t1,
|
||||||
|
t2,
|
||||||
|
)
|
||||||
|
|
||||||
|
cov1: Covariant[float]
|
||||||
|
cov2: Covariant[int]
|
||||||
|
cov1 = cov2 # Ok because int <: float => Covariant[int] <: Covariant[float]
|
||||||
|
cov2 = cov1 # Invalid
|
||||||
|
|
||||||
|
contra1: Contravariant[float]
|
||||||
|
contra2: Contravariant[int]
|
||||||
|
contra1 = contra2 # Invalid
|
||||||
|
contra2 = contra1 # Ok because int <: float => Covariant[float] <: Covariant[int]
|
||||||
|
|
||||||
|
inv1: Invariant[float]
|
||||||
|
inv2: Invariant[int]
|
||||||
|
inv1 = inv2 # Invalid
|
||||||
|
inv2 = inv1 # Invalid
|
||||||
512
tests/cases/checker/07_variance.py.ref.json
Normal file
512
tests/cases/checker/07_variance.py.ref.json
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
{
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
28,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
28,
|
||||||
|
13
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Too many positional arguments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
42,
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Cannot assign Covariant[float] to variable 'cov2' of type Covariant[int]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
46,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
46,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Cannot assign Contravariant[int] to variable 'contra1' of type Contravariant[float]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
51,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
51,
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Cannot assign Invariant[int] to variable 'inv1' of type Invariant[float]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
52,
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Cannot assign Invariant[float] to variable 'inv2' of type Invariant[int]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L27:4",
|
||||||
|
"to": "L27:10"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "unused"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Unused",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L28:4",
|
||||||
|
"to": "L28:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "covariant"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Covariant",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "COVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L29:4",
|
||||||
|
"to": "L29:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contravariant"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Contravariant",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "CONTRAVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L30:4",
|
||||||
|
"to": "L30:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "invariant"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Invariant",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L31:4",
|
||||||
|
"to": "L31:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "coco"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Coco",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "COVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L32:4",
|
||||||
|
"to": "L32:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "cocontra"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Cocontra",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "CONTRAVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L33:4",
|
||||||
|
"to": "L33:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contraco"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Contraco",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "CONTRAVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L34:4",
|
||||||
|
"to": "L34:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contracontra"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Contracontra",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "COVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L35:4",
|
||||||
|
"to": "L35:6"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "t1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "T1",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L36:4",
|
||||||
|
"to": "L36:6"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "t2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "T2",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L26:0",
|
||||||
|
"to": "L26:5"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "print"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "object",
|
||||||
|
"type": {},
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L26:0",
|
||||||
|
"to": "L37:1"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "print"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "unused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "covariant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contravariant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "invariant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "coco"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "cocontra"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contraco"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contracontra"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "t1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "t2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L41:7",
|
||||||
|
"to": "L41:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "cov2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Covariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L42:7",
|
||||||
|
"to": "L42:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "cov1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Covariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L46:10",
|
||||||
|
"to": "L46:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contra2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Contravariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L47:10",
|
||||||
|
"to": "L47:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "contra1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Contravariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L51:7",
|
||||||
|
"to": "L51:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "inv2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Invariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L52:7",
|
||||||
|
"to": "L52:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "inv1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "Invariant",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
14
tests/cases/checker/08_unification.py
Normal file
14
tests/cases/checker/08_unification.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
def double(value: float) -> float:
|
||||||
|
return value * 2
|
||||||
|
|
||||||
|
|
||||||
|
def is_odd(value: int) -> bool:
|
||||||
|
return bool(value % 2)
|
||||||
|
|
||||||
|
|
||||||
|
floats: list[float] = [0.2, 0.5, 0.1, 1.2]
|
||||||
|
ints: list[int] = [1, 2, 6, -3]
|
||||||
|
|
||||||
|
doubled_floats = map(double, floats)
|
||||||
|
doubled_ints = map(double, ints)
|
||||||
|
odd_ints = map(is_odd, ints)
|
||||||
874
tests/cases/checker/08_unification.py.ref.json
Normal file
874
tests/cases/checker/08_unification.py.ref.json
Normal file
@@ -0,0 +1,874 @@
|
|||||||
|
{
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
13,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
13,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Could not unify map[T, U]=(transform: (v: T, /) -> U, iterable: list[T], /) -> list[U] with pos=[(value: float) -> float, list[int]] and kw={}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:11",
|
||||||
|
"to": "L2:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "value"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:19",
|
||||||
|
"to": "L2:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L2:11",
|
||||||
|
"to": "L2:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "value"
|
||||||
|
},
|
||||||
|
"operator": "*",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:16",
|
||||||
|
"to": "L6:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "value"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:24",
|
||||||
|
"to": "L6:25"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:16",
|
||||||
|
"to": "L6:25"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "value"
|
||||||
|
},
|
||||||
|
"operator": "%",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:11",
|
||||||
|
"to": "L6:15"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "bool"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "object",
|
||||||
|
"type": {},
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:11",
|
||||||
|
"to": "L6:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "bool"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "value"
|
||||||
|
},
|
||||||
|
"operator": "%",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:23",
|
||||||
|
"to": "L9:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:28",
|
||||||
|
"to": "L9:31"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.5
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:33",
|
||||||
|
"to": "L9:36"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:38",
|
||||||
|
"to": "L9:41"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1.2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:22",
|
||||||
|
"to": "L9:42"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "ListExpr",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:19",
|
||||||
|
"to": "L10:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:22",
|
||||||
|
"to": "L10:23"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:25",
|
||||||
|
"to": "L10:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 6
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:29",
|
||||||
|
"to": "L10:30"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:28",
|
||||||
|
"to": "L10:30"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "UnaryExpr",
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L10:18",
|
||||||
|
"to": "L10:31"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "ListExpr",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "UnaryExpr",
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:21",
|
||||||
|
"to": "L12:27"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "double"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:29",
|
||||||
|
"to": "L12:35"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "floats"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:17",
|
||||||
|
"to": "L12:20"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "map",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L12:17",
|
||||||
|
"to": "L12:36"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "double"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "floats"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L13:19",
|
||||||
|
"to": "L13:25"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "double"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L13:27",
|
||||||
|
"to": "L13:31"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "ints"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L13:15",
|
||||||
|
"to": "L13:18"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "map",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L13:15",
|
||||||
|
"to": "L13:32"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "double"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "ints"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L14:15",
|
||||||
|
"to": "L14:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "is_odd"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"pos_args": [],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L14:23",
|
||||||
|
"to": "L14:27"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "ints"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L14:11",
|
||||||
|
"to": "L14:14"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "map",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"pos_args": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [],
|
||||||
|
"kw_args": [],
|
||||||
|
"returns": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L14:11",
|
||||||
|
"to": "L14:28"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "map"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "is_odd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "ints"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
117
tests/cases/checker/09_frame_ops.py
Normal file
117
tests/cases/checker/09_frame_ops.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# type: ignore
|
||||||
|
# ruff: disable [F821]
|
||||||
|
|
||||||
|
df1: Frame[a:int, b:float]
|
||||||
|
df2: Frame[a:int, b:float]
|
||||||
|
|
||||||
|
_: Any
|
||||||
|
|
||||||
|
# Arithmetic
|
||||||
|
_ = df1 + df2
|
||||||
|
_ = df1 - df2
|
||||||
|
_ = df1 * df2
|
||||||
|
_ = df1 / df2
|
||||||
|
_ = df1 // df2
|
||||||
|
_ = df1 % df2
|
||||||
|
_ = df1**df2
|
||||||
|
|
||||||
|
# Comparisons
|
||||||
|
_ = df1 < df2
|
||||||
|
_ = df1 > df2
|
||||||
|
_ = df1 <= df2
|
||||||
|
_ = df1 >= df2
|
||||||
|
_ = df1 != df2
|
||||||
|
_ = df1 == df2
|
||||||
|
|
||||||
|
# Aggregate
|
||||||
|
_ = df1.kurt()
|
||||||
|
_ = df1.kurtosis()
|
||||||
|
_ = df1.max()
|
||||||
|
_ = df1.mean()
|
||||||
|
_ = df1.median()
|
||||||
|
_ = df1.min()
|
||||||
|
_ = df1.mode()
|
||||||
|
_ = df1.prod()
|
||||||
|
_ = df1.product()
|
||||||
|
_ = df1.std()
|
||||||
|
_ = df1.sum()
|
||||||
|
_ = df1.var()
|
||||||
|
|
||||||
|
# Groupby
|
||||||
|
df_gb = df1.groupby(by="a")
|
||||||
|
|
||||||
|
_ = df_gb.kurt()
|
||||||
|
_ = df_gb.max()
|
||||||
|
_ = df_gb.mean()
|
||||||
|
_ = df_gb.median()
|
||||||
|
_ = df_gb.min()
|
||||||
|
_ = df_gb.prod()
|
||||||
|
_ = df_gb.std()
|
||||||
|
_ = df_gb.sum()
|
||||||
|
_ = df_gb.var()
|
||||||
|
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
|
||||||
|
col1 = df1["a"]
|
||||||
|
col2 = df1["a"]
|
||||||
|
|
||||||
|
# Arithmetic
|
||||||
|
_ = col1 + col2
|
||||||
|
_ = col1 - col2
|
||||||
|
_ = col1 * col2
|
||||||
|
_ = col1 / col2
|
||||||
|
_ = col1 // col2
|
||||||
|
_ = col1 % col2
|
||||||
|
_ = col1**col2
|
||||||
|
|
||||||
|
# Comparisons
|
||||||
|
_ = col1 < col2
|
||||||
|
_ = col1 > col2
|
||||||
|
_ = col1 <= col2
|
||||||
|
_ = col1 >= col2
|
||||||
|
_ = col1 != col2
|
||||||
|
_ = col1 == col2
|
||||||
|
|
||||||
|
# Aggregate
|
||||||
|
_ = col1.kurt()
|
||||||
|
_ = col1.kurtosis()
|
||||||
|
_ = col1.max()
|
||||||
|
_ = col1.mean()
|
||||||
|
_ = col1.median()
|
||||||
|
_ = col1.min()
|
||||||
|
_ = col1.mode()
|
||||||
|
_ = col1.prod()
|
||||||
|
_ = col1.product()
|
||||||
|
_ = col1.std()
|
||||||
|
_ = col1.sum()
|
||||||
|
_ = col1.var()
|
||||||
|
|
||||||
|
# Groupby
|
||||||
|
col_gb = col1.groupby(level=0)
|
||||||
|
|
||||||
|
_ = col_gb.kurt()
|
||||||
|
_ = col_gb.max()
|
||||||
|
_ = col_gb.mean()
|
||||||
|
_ = col_gb.median()
|
||||||
|
_ = col_gb.min()
|
||||||
|
_ = col_gb.prod()
|
||||||
|
_ = col_gb.std()
|
||||||
|
_ = col_gb.sum()
|
||||||
|
_ = col_gb.var()
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
_ = df1.ndim # int
|
||||||
|
_ = df1.size # int
|
||||||
|
_ = df1.shape # (int, int)
|
||||||
|
_ = col1.ndim # int
|
||||||
|
_ = col1.size # int
|
||||||
|
_ = col1.shape # (int)
|
||||||
|
_ = col1.T # Column[int]
|
||||||
|
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
_ = df1.head()
|
||||||
|
_ = df1.tail()
|
||||||
|
_ = col1.head()
|
||||||
|
_ = col1.tail()
|
||||||
4924
tests/cases/checker/09_frame_ops.py.ref.json
Normal file
4924
tests/cases/checker/09_frame_ops.py.ref.json
Normal file
File diff suppressed because it is too large
Load Diff
14
tests/cases/generator/01_simple_types.midas
Normal file
14
tests/cases/generator/01_simple_types.midas
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
type Meter = float
|
||||||
|
type Second = float
|
||||||
|
type MeterPerSecond = float
|
||||||
|
|
||||||
|
extend Meter {
|
||||||
|
def __add__: fn(Meter, /) -> Meter
|
||||||
|
def __sub__: fn(Meter, /) -> Meter
|
||||||
|
def __truediv__: fn(Second, /) -> MeterPerSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Second {
|
||||||
|
def __add__: fn(Second, /) -> Second
|
||||||
|
def __sub__: fn(Second, /) -> Second
|
||||||
|
}
|
||||||
5
tests/cases/generator/01_simple_types.py
Normal file
5
tests/cases/generator/01_simple_types.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from midas import cast, Meter, Second
|
||||||
|
|
||||||
|
distance: Meter = cast(Meter, 123.45)
|
||||||
|
time: Second = cast(Second, 6.7)
|
||||||
|
speed = distance / time
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user