Compare commits
265 Commits
7a2ee5a4cc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 62612bd8db | |||
|
4ed78d3d7e
|
|||
|
ab7012c538
|
|||
|
06f71f2945
|
|||
|
1b5691dca7
|
|||
| 2118c260ab | |||
|
403238da09
|
|||
|
c00dfe9910
|
|||
|
597ae6bb4c
|
|||
|
1162716a25
|
|||
|
c2ebf885ac
|
|||
|
7cddc62aaa
|
|||
|
49e00d9fbc
|
|||
|
1d2f98419e
|
|||
|
2a73dc3fef
|
|||
|
ede7396f9b
|
|||
| 6ca778dbfa | |||
|
672c9c0fa1
|
|||
|
dd2f3d6f6a
|
|||
|
ef9dd95844
|
|||
|
725e030374
|
|||
| db986d5242 | |||
|
3c97e75db6
|
|||
|
e0a468a2c2
|
|||
|
6740344eba
|
|||
|
4f9099a4c4
|
|||
|
5c66c4b645
|
|||
| c0896d2b9b | |||
|
607ff53987
|
|||
|
3268783cbe
|
|||
|
a48182a4e3
|
|||
|
205d19fb72
|
|||
|
10c6ea7dda
|
|||
|
1acf33f376
|
|||
|
aae481776f
|
|||
| e855a09a6b | |||
|
094554cb72
|
|||
|
40bda81c32
|
|||
|
25c11c3a53
|
|||
|
f3dec414cc
|
|||
|
48be2d454c
|
|||
|
5958e3612b
|
|||
|
83eecd612e
|
|||
|
5311307a6f
|
|||
|
fe34c77e4c
|
|||
|
c2f41eb392
|
|||
|
ed07b01563
|
|||
|
c6b8c0a145
|
|||
|
f48ebd49d1
|
|||
|
5051e155c0
|
|||
|
1098e33d07
|
|||
|
9277bd2cd0
|
|||
|
5e87ba2cd1
|
|||
|
4eb133ce17
|
|||
|
56987f6cff
|
|||
|
af6056a83f
|
|||
|
aab4641584
|
|||
|
164648e8df
|
|||
|
a640b8b3dd
|
|||
|
8bc35f7754
|
|||
|
16c19cca75
|
|||
|
3de36e5bd5
|
|||
| 7075d011b8 | |||
|
d97a9c0209
|
|||
| a418fe3eaf | |||
|
aae3073744
|
|||
|
b11a9bb8c6
|
|||
|
bac0e334d5
|
|||
|
30aef99c08
|
|||
|
9764484fd9
|
|||
|
5b9e322c91
|
|||
|
c18d9c18de
|
|||
|
9229f00375
|
|||
|
6b7a682dc5
|
|||
|
35b97fd17b
|
|||
| 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
|
22
.gitea/workflows/tests.yaml
Normal file
22
.gitea/workflows/tests.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
- run: python3 -m tests
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ venv
|
|||||||
uv.lock
|
uv.lock
|
||||||
.python-version
|
.python-version
|
||||||
/out
|
/out
|
||||||
|
/examples/**/build/
|
||||||
|
/examples/**/*.pyi
|
||||||
107
README.md
107
README.md
@@ -1,4 +1,4 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
@@ -6,6 +6,25 @@
|
|||||||
|
|
||||||
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
|
## Requirements
|
||||||
|
|
||||||
- Python 3.11+
|
- Python 3.11+
|
||||||
@@ -32,10 +51,17 @@ This framework is being developed as part of a Bachelor's Thesis by Louis Herede
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Compiling
|
Hereafter is a description of the commands you can use with Midas. For a full description, refer to [the manual](./docs/manual.pdf) or `midas <subcommand> --help`.
|
||||||
|
|
||||||
> [!NOTE]
|
### Type Checking
|
||||||
> In the current state of the project, the `compile` command doesn't generate any runnable code, it only runs the parsers and type checker on the provided files
|
|
||||||
|
```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
|
```shell
|
||||||
midas compile -t types.midas source.py
|
midas compile -t types.midas source.py
|
||||||
@@ -43,14 +69,26 @@ 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.
|
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.
|
||||||
|
|
||||||
The optional `-l FILE` option lets you produce a highlighted version of the source code showing diagnostics from the type checker (see [Highlighting](#highlighting))
|
> [!WARNING]
|
||||||
|
> By default, any type checking error aborts the compilation and the generator is not run. You can bypass this behaviour with the `--ignore-errors` flag.
|
||||||
|
> Only use this flag if you know what you are doing as it will produce a possibly unsafe program and goes against the whole purpose of Midas
|
||||||
|
|
||||||
|
### 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
|
### Highlighting
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
midas utils highlight source.py
|
midas highlight source.py
|
||||||
# or
|
midas highlight source.py -o highlighted.html
|
||||||
midas utils highlight types.midas
|
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 `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.)
|
||||||
@@ -60,26 +98,65 @@ The optional `-o FILE` option can be used to specify an output path. By default,
|
|||||||
### Dumping the AST
|
### Dumping the AST
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
midas utils dump-ast source.py
|
midas parse source.py
|
||||||
# or
|
midas parse types.midas
|
||||||
midas utils dump-ast types.midas
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For debugging purposes, you can output the AST parsed from a Python or Midas file. For Python files, the `-p` flags lets you toggle the custom AST parsing. Without `-p`, the raw AST is returned, as produced by the builtin `ast` module. This flag has no effect on Midas files.
|
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.
|
||||||
|
|
||||||
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 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 generates 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
|
## Tests
|
||||||
|
|
||||||
Several snapshot tests are available to assert the good behaviour of the parsers and type checker. They can be run as follows:
|
Several snapshot tests are available to assert the good behaviour of the parser, type checker and generator. They can be run as follows:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
uv run -m tests.midas run -a
|
uv run -m tests.midas run -a
|
||||||
uv run -m tests.python run -a
|
uv run -m tests.python run -a
|
||||||
uv run -m tests.checker run -a
|
uv run -m tests.checker run -a
|
||||||
|
uv run -m tests.generator run -a
|
||||||
```
|
```
|
||||||
|
|
||||||
**Available subcommands:**
|
Alternatively, you can run all tests by executing the `tests` module directly:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
uv run -m tests
|
||||||
|
```
|
||||||
|
|
||||||
|
When running only one test group, you may use one of the following subcommands.\
|
||||||
|
Not specifying any subcommand is equivalent to running `run -a`
|
||||||
|
|
||||||
|
**Available subcommands**:
|
||||||
- Run all tests: `run -a`
|
- Run all tests: `run -a`
|
||||||
- Run specific tests: `run tests/cases/test1.py tests/cases/test2.py ...`
|
- Run specific tests: `run tests/cases/test1.py tests/cases/test2.py ...`
|
||||||
- Update all tests: `update -a`
|
- Update all tests: `update -a`
|
||||||
|
|||||||
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 |
2
docs/fonts.yaml
Normal file
2
docs/fonts.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Source Sans Pro:
|
||||||
|
Source Sans 3:
|
||||||
12698
docs/manual.pdf
Normal file
12698
docs/manual.pdf
Normal file
File diff suppressed because it is too large
Load Diff
1015
docs/manual.typ
Normal file
1015
docs/manual.typ
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||||
6
docs/requirements.json
Normal file
6
docs/requirements.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"preview": {},
|
||||||
|
"local": {
|
||||||
|
"codly": "https://github.com/LordBaryhobal/codly-fix-v15/archive/refs/tags/v1.3.1-fix.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
147
docs/template.typ
Normal file
147
docs/template.typ
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#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: none,
|
||||||
|
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() = {
|
||||||
|
let version-name = [v#version]
|
||||||
|
if hash != none {
|
||||||
|
version-name = [#version-name - #hash]
|
||||||
|
}
|
||||||
|
align(center)[
|
||||||
|
#{
|
||||||
|
set text(size: 1.5em)
|
||||||
|
std.title()
|
||||||
|
}
|
||||||
|
|
||||||
|
#version-name
|
||||||
|
|
||||||
|
#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()
|
||||||
|
}
|
||||||
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): ...
|
||||||
65
examples/02_demonstration/weather/custom_types.midas
Normal file
65
examples/02_demonstration/weather/custom_types.midas
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
predicate in_range(min: float, max: float)(v: float) = min <= v & v <= max
|
||||||
|
predicate is_percentage = in_range(0.0, 100.0)
|
||||||
|
|
||||||
|
type Celsius = float
|
||||||
|
type Kelvin = float where _ >= 0
|
||||||
|
type Hectopascal = float
|
||||||
|
|
||||||
|
type Temperature = Celsius where in_range(-30.0, 100.0)(_)
|
||||||
|
type Pressure = Hectopascal where in_range(800.0, 1100.0)(_)
|
||||||
|
type Humidity = float where is_percentage(_)
|
||||||
|
type HeatIndex = float
|
||||||
|
|
||||||
|
type StationID = str where len(_) == 3 & _.isupper()
|
||||||
|
|
||||||
|
type Mean[T <: float] = float
|
||||||
|
|
||||||
|
extend Celsius {
|
||||||
|
def __add__: fn(Celsius, /) -> Celsius
|
||||||
|
def __sub__: fn(Celsius, /) -> Celsius
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Kelvin {
|
||||||
|
def __add__: fn(Kelvin, /) -> Kelvin
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Hectopascal {
|
||||||
|
def __add__: fn(Hectopascal, /) -> Hectopascal
|
||||||
|
def __sub__: fn(Hectopascal, /) -> Hectopascal
|
||||||
|
}
|
||||||
|
|
||||||
|
alias RawData = Frame[
|
||||||
|
station_id: str,
|
||||||
|
timestamp: str,
|
||||||
|
temperature: float,
|
||||||
|
pressure: float,
|
||||||
|
humidity: float,
|
||||||
|
]
|
||||||
|
|
||||||
|
alias Data = Frame[
|
||||||
|
station_id: StationID,
|
||||||
|
timestamp: Any,
|
||||||
|
temperature: Temperature,
|
||||||
|
pressure: Pressure,
|
||||||
|
humidity: Humidity,
|
||||||
|
]
|
||||||
|
|
||||||
|
alias DataWithHI = Frame[
|
||||||
|
station_id: StationID,
|
||||||
|
timestamp: Any,
|
||||||
|
temperature: Temperature,
|
||||||
|
pressure: Pressure,
|
||||||
|
humidity: Humidity,
|
||||||
|
heat_index: HeatIndex,
|
||||||
|
]
|
||||||
|
|
||||||
|
alias DailyAverages = Frame[
|
||||||
|
timestamp: Any,
|
||||||
|
temperature: Mean[Temperature],
|
||||||
|
pressure: Mean[Pressure],
|
||||||
|
humidity: Mean[Humidity],
|
||||||
|
heat_index: Mean[HeatIndex],
|
||||||
|
]
|
||||||
|
|
||||||
|
// predicate limit_amplitude(max_amp: float)(ls: list[float]) = max(ls) - min(ls) <= max_amp
|
||||||
|
// type LowAmplitudeWave = list[float where _ >= 1] where limit_amplitude(10)(_)
|
||||||
43
examples/02_demonstration/weather/gen_data.py
Normal file
43
examples/02_demonstration/weather/gen_data.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
stations = ["SIO", "AIG", "ZER"]
|
||||||
|
|
||||||
|
start_ts = datetime.datetime(2026, 1, 1)
|
||||||
|
end_ts = datetime.datetime(2027, 1, 1)
|
||||||
|
delta = end_ts - start_ts
|
||||||
|
|
||||||
|
min_temp, max_temp = -30.0, 100.0
|
||||||
|
min_pres, max_pres = 800.0, 1100.0
|
||||||
|
min_hum, max_hum = 0.0, 100.0
|
||||||
|
|
||||||
|
N = 3000
|
||||||
|
|
||||||
|
rows: list[tuple[str, datetime.datetime, float, float, float]] = []
|
||||||
|
|
||||||
|
for _ in range(N):
|
||||||
|
ts = random.random() * delta + start_ts
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
random.choice(stations),
|
||||||
|
ts,
|
||||||
|
random.random() * (max_temp - min_temp) + min_temp,
|
||||||
|
random.random() * (max_pres - min_pres) + min_pres,
|
||||||
|
random.random() * (max_hum - min_hum) + min_hum,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
df = pd.DataFrame(
|
||||||
|
rows,
|
||||||
|
columns=[
|
||||||
|
"station_id",
|
||||||
|
"timestamp",
|
||||||
|
"temperature",
|
||||||
|
"pressure",
|
||||||
|
"humidity",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
df = df.sort_values(by=["timestamp", "station_id"])
|
||||||
|
df.to_csv("data.csv", index=False)
|
||||||
81
examples/02_demonstration/weather/pipeline.py
Normal file
81
examples/02_demonstration/weather/pipeline.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import pandas as pd
|
||||||
|
from custom_types import DailyAverages, Data, DataWithHI, HeatIndex, RawData
|
||||||
|
|
||||||
|
from midas.typing import Column, cast, unsafe_cast
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(path: Path) -> RawData:
|
||||||
|
# Check base types and dataframe structure
|
||||||
|
return cast(RawData, pd.read_csv(path))
|
||||||
|
|
||||||
|
|
||||||
|
def convert_data(raw_df: RawData) -> Data:
|
||||||
|
new_df = raw_df.copy()
|
||||||
|
new_df["timestamp"] = cast(
|
||||||
|
Column[object],
|
||||||
|
pd.to_datetime(new_df["timestamp"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check types and constraints at runtime, catches out-of-range values and
|
||||||
|
# invalid types / malformed data
|
||||||
|
return cast(Data, new_df)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_heat_index(df: Data):
|
||||||
|
# The computation's result can only be typed as `Column[float]`
|
||||||
|
# Casting is necessary to bring back semantic
|
||||||
|
df["heat_index"] = cast(
|
||||||
|
Column[HeatIndex],
|
||||||
|
(
|
||||||
|
df["temperature"] * 2.0
|
||||||
|
+ df["humidity"] * 10.0
|
||||||
|
- df["temperature"] * df["humidity"] * 0.2
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def daily_avg(df: DataWithHI):
|
||||||
|
# Group-by and aggregation methods keep the structure of the dataframe but
|
||||||
|
# may erase the exact types
|
||||||
|
# The type checker is still very conservative and often the result of most
|
||||||
|
# aggregation methods as `Column[Any]`
|
||||||
|
return cast(
|
||||||
|
DailyAverages,
|
||||||
|
df.groupby(
|
||||||
|
by=[
|
||||||
|
"station_id",
|
||||||
|
df["timestamp"].dt.day.rename("day"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.mean()
|
||||||
|
.sort_values(by="timestamp"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plot(df: DailyAverages):
|
||||||
|
# Some operations are not implemented in Midas but the user can still use
|
||||||
|
# them, they will just not be fully type-checked
|
||||||
|
# `unsafe_cast` can also be used to avoid trivial, redundant or costly checks
|
||||||
|
stations = unsafe_cast(list[str], list(df.index.get_level_values(0).unique()))
|
||||||
|
for station in stations:
|
||||||
|
sub_df = unsafe_cast(DailyAverages, df.loc[station])
|
||||||
|
plt.plot(sub_df["timestamp"], sub_df["heat_index"])
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Assigning to annotated variables help catch errors
|
||||||
|
raw_df: RawData = load_data(Path("data.csv"))
|
||||||
|
df: Data = convert_data(raw_df)
|
||||||
|
|
||||||
|
with_hi = compute_heat_index(df)
|
||||||
|
dailies = daily_avg(with_hi)
|
||||||
|
plot(dailies)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Helper script to generate AST nodes for Midas and Python.
|
||||||
|
|
||||||
|
Takes in simple templates and generates full dataclasses and a visitor interface
|
||||||
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|||||||
49
gen/midas.py
49
gen/midas.py
@@ -26,6 +26,14 @@ class MemberKind(Enum):
|
|||||||
METHOD = auto()
|
METHOD = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
l_paren: Token
|
||||||
|
pos: list[FunctionType.Parameter]
|
||||||
|
mixed: list[FunctionType.Parameter]
|
||||||
|
kw: list[FunctionType.Parameter]
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +44,11 @@ class TypeStmt:
|
|||||||
type: Type
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
|
class AliasStmt:
|
||||||
|
name: Token
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
class MemberStmt:
|
class MemberStmt:
|
||||||
name: Token
|
name: Token
|
||||||
type: Type
|
type: Type
|
||||||
@@ -50,9 +63,8 @@ class ExtendStmt:
|
|||||||
|
|
||||||
class PredicateStmt:
|
class PredicateStmt:
|
||||||
name: Token
|
name: Token
|
||||||
subject: Token
|
params: list[ParamSpec]
|
||||||
type: Type
|
body: Expr
|
||||||
condition: Expr
|
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
@@ -78,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
|
||||||
@@ -118,27 +136,26 @@ class ConstraintType:
|
|||||||
constraint: Expr
|
constraint: Expr
|
||||||
|
|
||||||
|
|
||||||
class ComplexType:
|
|
||||||
members: list[MemberStmt]
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionType:
|
|
||||||
base: Type
|
|
||||||
extension: ComplexType
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionType:
|
class FunctionType:
|
||||||
pos_args: list[Argument]
|
params: ParamSpec
|
||||||
args: list[Argument]
|
|
||||||
kw_args: list[Argument]
|
|
||||||
returns: Type
|
returns: Type
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Argument:
|
class Parameter:
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
name: Optional[Token]
|
name: Optional[Token]
|
||||||
type: Type
|
type: Type
|
||||||
required: bool
|
required: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FrameType:
|
||||||
|
columns: list[Column]
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Column:
|
||||||
|
location: Optional[Location] = None
|
||||||
|
name: Token
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
###<
|
###<
|
||||||
|
|||||||
@@ -12,10 +12,36 @@ from midas.ast.location import Location
|
|||||||
###<
|
###<
|
||||||
|
|
||||||
|
|
||||||
|
###> Preamble
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
pos: list[Function.Parameter]
|
||||||
|
mixed: list[Function.Parameter]
|
||||||
|
kw: list[Function.Parameter]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all(self) -> list[Function.Parameter]:
|
||||||
|
return self.pos + self.mixed + self.kw
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ImportAlias:
|
||||||
|
location: Location
|
||||||
|
name: str
|
||||||
|
alias: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def imported_name(self) -> str:
|
||||||
|
return self.alias if self.alias is not None else self.name
|
||||||
|
|
||||||
|
|
||||||
|
###<
|
||||||
|
|
||||||
|
|
||||||
###> 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:
|
||||||
@@ -42,25 +68,17 @@ class ExpressionStmt:
|
|||||||
|
|
||||||
class Function:
|
class Function:
|
||||||
name: str
|
name: str
|
||||||
posonlyargs: list[Argument]
|
params: ParamSpec
|
||||||
args: list[Argument]
|
|
||||||
sink: Optional[Argument]
|
|
||||||
kwonlyargs: list[Argument]
|
|
||||||
kw_sink: Optional[Argument]
|
|
||||||
returns: Optional[MidasType]
|
returns: Optional[MidasType]
|
||||||
body: list[Stmt]
|
body: list[Stmt]
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Argument:
|
class Parameter:
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
name: str
|
name: str
|
||||||
type: Optional[MidasType]
|
type: Optional[MidasType]
|
||||||
default: Optional[Expr]
|
default: Optional[Expr]
|
||||||
|
|
||||||
@property
|
|
||||||
def all_args(self) -> list[Argument]:
|
|
||||||
return self.posonlyargs + self.args + self.kwonlyargs
|
|
||||||
|
|
||||||
|
|
||||||
class TypeAssign:
|
class TypeAssign:
|
||||||
name: str
|
name: str
|
||||||
@@ -92,6 +110,16 @@ class ForStmt:
|
|||||||
body: list[Stmt]
|
body: list[Stmt]
|
||||||
|
|
||||||
|
|
||||||
|
class ImportStmt:
|
||||||
|
imports: list[ImportAlias]
|
||||||
|
|
||||||
|
|
||||||
|
class FromImportStmt:
|
||||||
|
module: Optional[str]
|
||||||
|
imports: list[ImportAlias]
|
||||||
|
level: int
|
||||||
|
|
||||||
|
|
||||||
class RawStmt:
|
class RawStmt:
|
||||||
stmt: ast.stmt
|
stmt: ast.stmt
|
||||||
|
|
||||||
@@ -145,6 +173,7 @@ class LogicalExpr:
|
|||||||
class CastExpr:
|
class CastExpr:
|
||||||
type: MidasType
|
type: MidasType
|
||||||
expr: Expr
|
expr: Expr
|
||||||
|
unsafe: bool
|
||||||
|
|
||||||
|
|
||||||
class TernaryExpr:
|
class TernaryExpr:
|
||||||
@@ -173,6 +202,10 @@ class SliceExpr:
|
|||||||
step: Optional[Expr]
|
step: Optional[Expr]
|
||||||
|
|
||||||
|
|
||||||
|
class TupleExpr:
|
||||||
|
items: tuple[Expr, ...]
|
||||||
|
|
||||||
|
|
||||||
class RawExpr:
|
class RawExpr:
|
||||||
expr: ast.expr
|
expr: ast.expr
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class HasLocation(Protocol):
|
|||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Location:
|
class Location:
|
||||||
|
"""Information about the location of an AST node"""
|
||||||
|
|
||||||
lineno: int
|
lineno: int
|
||||||
col_offset: int
|
col_offset: int
|
||||||
end_lineno: Optional[int]
|
end_lineno: Optional[int]
|
||||||
@@ -29,6 +31,16 @@ class Location:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def span(start: Location, end: Location) -> Location:
|
def span(start: Location, end: Location) -> Location:
|
||||||
|
"""Create a new location spanning from one location to another
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start (Location): the starting location
|
||||||
|
end (Location): the end location
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Location: a new location spanning from the start of `start`
|
||||||
|
to the end of `end`
|
||||||
|
"""
|
||||||
return Location(
|
return Location(
|
||||||
lineno=start.lineno,
|
lineno=start.lineno,
|
||||||
col_offset=start.col_offset,
|
col_offset=start.col_offset,
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ class MemberKind(Enum):
|
|||||||
METHOD = auto()
|
METHOD = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
l_paren: Token
|
||||||
|
pos: list[FunctionType.Parameter]
|
||||||
|
mixed: list[FunctionType.Parameter]
|
||||||
|
kw: list[FunctionType.Parameter]
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Statements #
|
# Statements #
|
||||||
##############
|
##############
|
||||||
@@ -43,6 +51,9 @@ class Stmt(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_type_stmt(self, stmt: TypeStmt) -> T: ...
|
def visit_type_stmt(self, stmt: TypeStmt) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_alias_stmt(self, stmt: AliasStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_member_stmt(self, stmt: MemberStmt) -> T: ...
|
def visit_member_stmt(self, stmt: MemberStmt) -> T: ...
|
||||||
|
|
||||||
@@ -63,6 +74,15 @@ class TypeStmt(Stmt):
|
|||||||
return visitor.visit_type_stmt(self)
|
return visitor.visit_type_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AliasStmt(Stmt):
|
||||||
|
name: Token
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_alias_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class MemberStmt(Stmt):
|
class MemberStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
@@ -86,9 +106,8 @@ class ExtendStmt(Stmt):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PredicateStmt(Stmt):
|
class PredicateStmt(Stmt):
|
||||||
name: Token
|
name: Token
|
||||||
subject: Token
|
params: list[ParamSpec]
|
||||||
type: Type
|
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)
|
||||||
@@ -116,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: ...
|
||||||
|
|
||||||
@@ -161,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
|
||||||
@@ -224,15 +256,12 @@ class Type(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_constraint_type(self, type: ConstraintType) -> T: ...
|
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
|
@abstractmethod
|
||||||
def visit_function_type(self, type: FunctionType) -> T: ...
|
def visit_function_type(self, type: FunctionType) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_frame_type(self, type: FrameType) -> T: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class NamedType(Type):
|
class NamedType(Type):
|
||||||
@@ -260,32 +289,13 @@ class ConstraintType(Type):
|
|||||||
return visitor.visit_constraint_type(self)
|
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)
|
@dataclass(frozen=True)
|
||||||
class FunctionType(Type):
|
class FunctionType(Type):
|
||||||
pos_args: list[Argument]
|
params: ParamSpec
|
||||||
args: list[Argument]
|
|
||||||
kw_args: list[Argument]
|
|
||||||
returns: Type
|
returns: Type
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Argument:
|
class Parameter:
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
name: Optional[Token]
|
name: Optional[Token]
|
||||||
type: Type
|
type: Type
|
||||||
@@ -293,3 +303,17 @@ class FunctionType(Type):
|
|||||||
|
|
||||||
def accept(self, visitor: Type.Visitor[T]) -> T:
|
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||||
return visitor.visit_function_type(self)
|
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)
|
||||||
|
|||||||
@@ -1,789 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import io
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from enum import Enum, auto
|
|
||||||
from typing import Generator, Generic, Optional, Protocol, TypeVar
|
|
||||||
|
|
||||||
import midas.ast.midas as m
|
|
||||||
import midas.ast.python as p
|
|
||||||
|
|
||||||
|
|
||||||
class _Level(Enum):
|
|
||||||
EMPTY = auto()
|
|
||||||
ACTIVE = auto()
|
|
||||||
LAST = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class Expr(Protocol):
|
|
||||||
def accept(self, printer: AstPrinter) -> None: ...
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=Expr)
|
|
||||||
|
|
||||||
|
|
||||||
class AstPrinter(Generic[T]):
|
|
||||||
LAST_CHILD = "└── "
|
|
||||||
CHILD = "├── "
|
|
||||||
VERTICAL = "│ "
|
|
||||||
EMPTY = " "
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._levels: list[_Level] = []
|
|
||||||
self._idx: Optional[int] = None
|
|
||||||
self._buf: io.StringIO = io.StringIO()
|
|
||||||
|
|
||||||
def print(self, expr: T):
|
|
||||||
self._buf = io.StringIO()
|
|
||||||
expr.accept(self)
|
|
||||||
return self._buf.getvalue()
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _child_level(self, single: bool = False) -> Generator[None, None, None]:
|
|
||||||
self._levels.append(_Level.LAST if single else _Level.ACTIVE)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
self._levels.pop()
|
|
||||||
|
|
||||||
def _mark_last(self):
|
|
||||||
if self._levels:
|
|
||||||
self._levels[-1] = _Level.LAST
|
|
||||||
|
|
||||||
def _write_line(self, text: str, *, last: bool = False):
|
|
||||||
if last:
|
|
||||||
self._mark_last()
|
|
||||||
indent: str = self._build_indent()
|
|
||||||
if self._idx is not None:
|
|
||||||
text = f"[{self._idx}] {text}"
|
|
||||||
self._idx = None
|
|
||||||
self._buf.write(indent + text + "\n")
|
|
||||||
|
|
||||||
def _build_indent(self) -> str:
|
|
||||||
parts: list[str] = []
|
|
||||||
for level in self._levels[:-1]:
|
|
||||||
parts.append(self.EMPTY if level == _Level.EMPTY else self.VERTICAL)
|
|
||||||
if self._levels:
|
|
||||||
if self._levels[-1] == _Level.LAST:
|
|
||||||
parts.append(self.LAST_CHILD)
|
|
||||||
self._levels[-1] = _Level.EMPTY
|
|
||||||
else:
|
|
||||||
parts.append(self.CHILD)
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
def _write_optional_child(
|
|
||||||
self, label: str, child: Optional[T], *, last: bool = False
|
|
||||||
):
|
|
||||||
if last:
|
|
||||||
self._mark_last()
|
|
||||||
if child is None:
|
|
||||||
self._write_line(f"{label}: None")
|
|
||||||
else:
|
|
||||||
self._write_line(label)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
child.accept(self)
|
|
||||||
|
|
||||||
|
|
||||||
class MidasAstPrinter(
|
|
||||||
AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None], m.Type.Visitor[None]
|
|
||||||
):
|
|
||||||
# Statements
|
|
||||||
|
|
||||||
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
|
||||||
self._write_line("TypeStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
|
||||||
self._write_line("params")
|
|
||||||
with self._child_level():
|
|
||||||
for i, param in enumerate(stmt.params):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.params) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_type_param(param)
|
|
||||||
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):
|
|
||||||
stmt.type.accept(self)
|
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
|
||||||
self._write_line("ExtendStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("params")
|
|
||||||
with self._child_level():
|
|
||||||
for i, param in enumerate(stmt.params):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.params) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_type_param(param)
|
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
|
||||||
self._write_line("params")
|
|
||||||
with self._child_level():
|
|
||||||
for i, param in enumerate(stmt.params):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.params) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_type_param(param)
|
|
||||||
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):
|
|
||||||
self._write_line("PredicateStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{stmt.name.lexeme}"')
|
|
||||||
self._write_line(f'subject: "{stmt.subject.lexeme}"')
|
|
||||||
self._write_line("type")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.type.accept(self)
|
|
||||||
self._write_line("condition", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.condition.accept(self)
|
|
||||||
|
|
||||||
# Expressions
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr):
|
|
||||||
self._write_line("LogicalExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("left")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.left.accept(self)
|
|
||||||
|
|
||||||
self._write_line(f"operator: {expr.operator.lexeme}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: m.BinaryExpr):
|
|
||||||
self._write_line("BinaryExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("left")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.left.accept(self)
|
|
||||||
|
|
||||||
self._write_line(f"operator: {expr.operator.lexeme}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: m.UnaryExpr):
|
|
||||||
self._write_line("UnaryExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"operator: {expr.operator.lexeme}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr):
|
|
||||||
self._write_line("GetExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("expr")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.expr.accept(self)
|
|
||||||
self._write_line(f'name: "{expr.name.lexeme}"', last=True)
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: m.VariableExpr):
|
|
||||||
self._write_line("VariableExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f'name: "{expr.name.lexeme}"', last=True)
|
|
||||||
|
|
||||||
def visit_grouping_expr(self, expr: m.GroupingExpr):
|
|
||||||
self._write_line("GroupingExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("expr", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.expr.accept(self)
|
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: m.LiteralExpr) -> None:
|
|
||||||
self._write_line("LiteralExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"value: {expr.value}", last=True)
|
|
||||||
|
|
||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None:
|
|
||||||
self._write_line("WildcardExpr")
|
|
||||||
|
|
||||||
def visit_named_type(self, type: m.NamedType) -> None:
|
|
||||||
self._write_line("NamedType")
|
|
||||||
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")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
type.type.accept(self)
|
|
||||||
self._write_line("constraint", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
type.constraint.accept(self)
|
|
||||||
|
|
||||||
def visit_complex_type(self, type: m.ComplexType) -> None:
|
|
||||||
self._write_line("ComplexType")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("members", last=True)
|
|
||||||
with self._child_level():
|
|
||||||
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("pos_args")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(type.pos_args):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(type.pos_args) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_function_arg(arg)
|
|
||||||
|
|
||||||
self._write_line("args")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(type.args):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(type.args) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_function_arg(arg)
|
|
||||||
|
|
||||||
self._write_line("kw_args")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(type.kw_args):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(type.kw_args) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_function_arg(arg)
|
|
||||||
|
|
||||||
self._write_line("returns", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
type.returns.accept(self)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]):
|
|
||||||
def __init__(self, indent: int = 4):
|
|
||||||
self.indent: int = indent
|
|
||||||
self.level: int = 0
|
|
||||||
|
|
||||||
def indented(self, text: str) -> str:
|
|
||||||
return " " * (self.level * self.indent) + text
|
|
||||||
|
|
||||||
def print(self, expr: m.Expr | m.Stmt | m.Type) -> str:
|
|
||||||
self.level = 0
|
|
||||||
return expr.accept(self)
|
|
||||||
|
|
||||||
def visit_type_stmt(self, stmt: m.TypeStmt) -> str:
|
|
||||||
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 = f"type {stmt.name.lexeme}{template} = {stmt.type.accept(self)}"
|
|
||||||
return self.indented(res)
|
|
||||||
|
|
||||||
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"
|
|
||||||
self.level += 1
|
|
||||||
for member in stmt.members:
|
|
||||||
res += member.accept(self) + "\n"
|
|
||||||
self.level -= 1
|
|
||||||
res += self.indented("}")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
|
||||||
name: str = stmt.name.lexeme
|
|
||||||
subject: str = stmt.subject.lexeme
|
|
||||||
type: str = stmt.type.accept(self)
|
|
||||||
condition: str = stmt.condition.accept(self)
|
|
||||||
return self.indented(f"predicate {name}({subject}: {type}) = {condition}")
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr):
|
|
||||||
left: str = expr.left.accept(self)
|
|
||||||
operator: str = expr.operator.lexeme
|
|
||||||
right: str = expr.right.accept(self)
|
|
||||||
return f"{left} {operator} {right}"
|
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: m.BinaryExpr):
|
|
||||||
left: str = expr.left.accept(self)
|
|
||||||
operator: str = expr.operator.lexeme
|
|
||||||
right: str = expr.right.accept(self)
|
|
||||||
return f"{left} {operator} {right}"
|
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: m.UnaryExpr):
|
|
||||||
operator: str = expr.operator.lexeme
|
|
||||||
right: str = expr.right.accept(self)
|
|
||||||
return f"{operator}{right}"
|
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr):
|
|
||||||
expr_: str = expr.expr.accept(self)
|
|
||||||
name: str = expr.name.lexeme
|
|
||||||
return f"{expr_}.{name}"
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: m.VariableExpr):
|
|
||||||
return expr.name.lexeme
|
|
||||||
|
|
||||||
def visit_grouping_expr(self, expr: m.GroupingExpr):
|
|
||||||
expr_: str = expr.expr.accept(self)
|
|
||||||
return f"({expr_})"
|
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: m.LiteralExpr):
|
|
||||||
return str(expr.value)
|
|
||||||
|
|
||||||
def visit_wildcard_expr(self, expr: m.WildcardExpr):
|
|
||||||
return "_"
|
|
||||||
|
|
||||||
def visit_named_type(self, type: m.NamedType) -> str:
|
|
||||||
return type.name.lexeme
|
|
||||||
|
|
||||||
def visit_generic_type(self, type: m.GenericType) -> str:
|
|
||||||
res: str = type.type.accept(self)
|
|
||||||
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:
|
|
||||||
pos_args: list[str] = [self._print_arg(arg) for arg in type.pos_args]
|
|
||||||
mixed_args: list[str] = [self._print_arg(arg) for arg in type.args]
|
|
||||||
kw_args: list[str] = [self._print_arg(arg) for arg in type.kw_args]
|
|
||||||
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"fn ({', '.join(args)}) -> {type.returns.accept(self)}"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class PythonAstPrinter(
|
|
||||||
AstPrinter,
|
|
||||||
p.MidasType.Visitor[None],
|
|
||||||
p.Stmt.Visitor[None],
|
|
||||||
p.Expr.Visitor[None],
|
|
||||||
):
|
|
||||||
def visit_base_type(self, node: p.BaseType) -> None:
|
|
||||||
self._write_line("BaseType")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"base: {node.base}")
|
|
||||||
self._write_optional_child("param", node.param, last=True)
|
|
||||||
|
|
||||||
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
|
||||||
self._write_line("ConstraintType")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("type")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
node.type.accept(self)
|
|
||||||
self._write_line(f"constraint: {ast.unparse(node.constraint)}", last=True)
|
|
||||||
|
|
||||||
def visit_frame_column(self, node: p.FrameColumn) -> None:
|
|
||||||
self._write_line("FrameColumn")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"name: {node.name}")
|
|
||||||
self._write_optional_child("type", node.type, last=True)
|
|
||||||
|
|
||||||
def visit_frame_type(self, node: p.FrameType) -> None:
|
|
||||||
self._write_line("FrameType")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("columns", last=True)
|
|
||||||
with self._child_level():
|
|
||||||
for i, col in enumerate(node.columns):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(node.columns) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
col.accept(self)
|
|
||||||
|
|
||||||
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
|
|
||||||
stmt.expr.accept(self)
|
|
||||||
|
|
||||||
def visit_function(self, stmt: p.Function) -> None:
|
|
||||||
self._write_line("Function")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"name: {stmt.name}")
|
|
||||||
|
|
||||||
self._write_line("posonlyargs")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(stmt.posonlyargs):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.posonlyargs) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_argument(arg)
|
|
||||||
|
|
||||||
self._write_line("args")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(stmt.args):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.args) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_argument(arg)
|
|
||||||
|
|
||||||
self._write_line("kwonlyargs")
|
|
||||||
with self._child_level():
|
|
||||||
for i, arg in enumerate(stmt.kwonlyargs):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.kwonlyargs) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
self._print_argument(arg)
|
|
||||||
|
|
||||||
self._write_optional_child("returns", stmt.returns)
|
|
||||||
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 _print_argument(self, arg: p.Function.Argument) -> None:
|
|
||||||
self._write_line("FunctionArgument")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"name: {arg.name}")
|
|
||||||
self._write_optional_child("type", arg.type, last=True)
|
|
||||||
|
|
||||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
|
||||||
self._write_line("TypeAssign")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"name: {stmt.name}")
|
|
||||||
self._write_line("type", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.type.accept(self)
|
|
||||||
|
|
||||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
|
||||||
self._write_line("AssignStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("targets")
|
|
||||||
with self._child_level():
|
|
||||||
for i, target in enumerate(stmt.targets):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.targets) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
target.accept(self)
|
|
||||||
self._write_line("value", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.value.accept(self)
|
|
||||||
|
|
||||||
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
|
||||||
self._write_line("ReturnStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_optional_child("value", stmt.value, last=True)
|
|
||||||
|
|
||||||
def visit_if_stmt(self, stmt: p.IfStmt) -> None:
|
|
||||||
self._write_line("IfStmt")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("test")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
stmt.test.accept(self)
|
|
||||||
self._write_line("body")
|
|
||||||
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)
|
|
||||||
self._write_line("orelse", last=True)
|
|
||||||
with self._child_level():
|
|
||||||
for i, else_stmt in enumerate(stmt.orelse):
|
|
||||||
self._idx = i
|
|
||||||
if i == len(stmt.orelse) - 1:
|
|
||||||
self._mark_last()
|
|
||||||
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:
|
|
||||||
self._write_line("BinaryExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("left")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.left.accept(self)
|
|
||||||
|
|
||||||
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_compare_expr(self, expr: p.CompareExpr) -> None:
|
|
||||||
self._write_line("CompareExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("left")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.left.accept(self)
|
|
||||||
|
|
||||||
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: p.UnaryExpr) -> None:
|
|
||||||
self._write_line("UnaryExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_call_expr(self, expr: p.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: p.GetExpr) -> None:
|
|
||||||
self._write_line("GetExpr")
|
|
||||||
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}", last=True)
|
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: p.LiteralExpr) -> None:
|
|
||||||
self._write_line("LiteralExpr")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
self._write_line(f"value: {expr.value!r}")
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
|
||||||
self._write_line("VariableExpr")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
self._write_line(f"name: {expr.name}")
|
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: p.LogicalExpr) -> None:
|
|
||||||
self._write_line("LogicalExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("left")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.left.accept(self)
|
|
||||||
|
|
||||||
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
|
||||||
|
|
||||||
self._write_line("right", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.right.accept(self)
|
|
||||||
|
|
||||||
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
|
||||||
self._write_line("CastExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("type")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.type.accept(self)
|
|
||||||
self._write_line("expr", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.expr.accept(self)
|
|
||||||
|
|
||||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None:
|
|
||||||
self._write_line("TernaryExpr")
|
|
||||||
with self._child_level():
|
|
||||||
self._write_line("test")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.test.accept(self)
|
|
||||||
|
|
||||||
self._write_line("if_true")
|
|
||||||
with self._child_level(single=True):
|
|
||||||
expr.if_true.accept(self)
|
|
||||||
|
|
||||||
self._write_line("if_false", last=True)
|
|
||||||
with self._child_level(single=True):
|
|
||||||
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_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)}")
|
|
||||||
3
midas/ast/printer/__init__.py
Normal file
3
midas/ast/printer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .midas import MidasPrinter as MidasPrinter
|
||||||
|
from .midas_ast import MidasAstPrinter as MidasAstPrinter
|
||||||
|
from .python_ast import PythonAstPrinter as PythonAstPrinter
|
||||||
103
midas/ast/printer/base.py
Normal file
103
midas/ast/printer/base.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Callable, Generator, Generic, Optional, Protocol, Sequence, TypeVar
|
||||||
|
|
||||||
|
|
||||||
|
class _Level(Enum):
|
||||||
|
EMPTY = auto()
|
||||||
|
ACTIVE = auto()
|
||||||
|
LAST = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Expr(Protocol):
|
||||||
|
def accept(self, printer: AstPrinter) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=Expr)
|
||||||
|
|
||||||
|
|
||||||
|
class AstPrinter(Generic[T]):
|
||||||
|
LAST_CHILD = "└── "
|
||||||
|
CHILD = "├── "
|
||||||
|
VERTICAL = "│ "
|
||||||
|
EMPTY = " "
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._levels: list[_Level] = []
|
||||||
|
self._idx: Optional[int] = None
|
||||||
|
self._buf: io.StringIO = io.StringIO()
|
||||||
|
|
||||||
|
def print(self, expr: T):
|
||||||
|
self._buf = io.StringIO()
|
||||||
|
expr.accept(self)
|
||||||
|
return self._buf.getvalue()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _child_level(self, single: bool = False) -> Generator[None, None, None]:
|
||||||
|
self._levels.append(_Level.LAST if single else _Level.ACTIVE)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._levels.pop()
|
||||||
|
|
||||||
|
def _mark_last(self):
|
||||||
|
if self._levels:
|
||||||
|
self._levels[-1] = _Level.LAST
|
||||||
|
|
||||||
|
def _write_line(self, text: str, *, last: bool = False):
|
||||||
|
if last:
|
||||||
|
self._mark_last()
|
||||||
|
indent: str = self._build_indent()
|
||||||
|
if self._idx is not None:
|
||||||
|
text = f"[{self._idx}] {text}"
|
||||||
|
self._idx = None
|
||||||
|
self._buf.write(indent + text + "\n")
|
||||||
|
|
||||||
|
def _build_indent(self) -> str:
|
||||||
|
parts: list[str] = []
|
||||||
|
for level in self._levels[:-1]:
|
||||||
|
parts.append(self.EMPTY if level == _Level.EMPTY else self.VERTICAL)
|
||||||
|
if self._levels:
|
||||||
|
if self._levels[-1] == _Level.LAST:
|
||||||
|
parts.append(self.LAST_CHILD)
|
||||||
|
self._levels[-1] = _Level.EMPTY
|
||||||
|
else:
|
||||||
|
parts.append(self.CHILD)
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
def _write_optional_child(
|
||||||
|
self, label: str, child: Optional[T], *, last: bool = False
|
||||||
|
):
|
||||||
|
if last:
|
||||||
|
self._mark_last()
|
||||||
|
if child is None:
|
||||||
|
self._write_line(f"{label}: None")
|
||||||
|
else:
|
||||||
|
self._write_line(label)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
child.accept(self)
|
||||||
|
|
||||||
|
def _write_sequence(
|
||||||
|
self,
|
||||||
|
label: str,
|
||||||
|
list_: Sequence[T],
|
||||||
|
*,
|
||||||
|
last: bool = False,
|
||||||
|
print_func: Optional[Callable[[T], None]] = None,
|
||||||
|
):
|
||||||
|
if last:
|
||||||
|
self._mark_last()
|
||||||
|
|
||||||
|
self._write_line(label)
|
||||||
|
with self._child_level():
|
||||||
|
for i, item in enumerate(list_):
|
||||||
|
self._idx = i
|
||||||
|
if i == len(list_) - 1:
|
||||||
|
self._mark_last()
|
||||||
|
if print_func is not None:
|
||||||
|
print_func(item)
|
||||||
|
else:
|
||||||
|
item.accept(self)
|
||||||
170
midas/ast/printer/midas.py
Normal file
170
midas/ast/printer/midas.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import midas.ast.midas as m
|
||||||
|
|
||||||
|
|
||||||
|
class MidasPrinter(
|
||||||
|
m.Expr.Visitor[str],
|
||||||
|
m.Stmt.Visitor[str],
|
||||||
|
m.Type.Visitor[str],
|
||||||
|
):
|
||||||
|
def __init__(self, indent: int = 4):
|
||||||
|
self.indent: int = indent
|
||||||
|
self.level: int = 0
|
||||||
|
|
||||||
|
def indented(self, text: str) -> str:
|
||||||
|
return " " * (self.level * self.indent) + text
|
||||||
|
|
||||||
|
def print(self, expr: m.Expr | m.Stmt | m.Type) -> str:
|
||||||
|
self.level = 0
|
||||||
|
return expr.accept(self)
|
||||||
|
|
||||||
|
# Statements
|
||||||
|
|
||||||
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> str:
|
||||||
|
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 = f"type {stmt.name.lexeme}{template} = {stmt.type.accept(self)}"
|
||||||
|
return self.indented(res)
|
||||||
|
|
||||||
|
def visit_alias_stmt(self, stmt: m.AliasStmt) -> str:
|
||||||
|
return self.indented(f"alias {stmt.name.lexeme} = {stmt.type.accept(self)}")
|
||||||
|
|
||||||
|
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"
|
||||||
|
self.level += 1
|
||||||
|
for member in stmt.members:
|
||||||
|
res += member.accept(self) + "\n"
|
||||||
|
self.level -= 1
|
||||||
|
res += self.indented("}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
||||||
|
name: str = stmt.name.lexeme
|
||||||
|
sig: str = "".join(self._visit_param_spec(spec) for spec in stmt.params)
|
||||||
|
body: str = stmt.body.accept(self)
|
||||||
|
return self.indented(f"predicate {name}{sig} = {body}")
|
||||||
|
|
||||||
|
# Expressions
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: m.LogicalExpr):
|
||||||
|
left: str = expr.left.accept(self)
|
||||||
|
operator: str = expr.operator.lexeme
|
||||||
|
right: str = expr.right.accept(self)
|
||||||
|
return f"{left} {operator} {right}"
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: m.BinaryExpr):
|
||||||
|
left: str = expr.left.accept(self)
|
||||||
|
operator: str = expr.operator.lexeme
|
||||||
|
right: str = expr.right.accept(self)
|
||||||
|
return f"{left} {operator} {right}"
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: m.UnaryExpr):
|
||||||
|
operator: str = expr.operator.lexeme
|
||||||
|
right: str = expr.right.accept(self)
|
||||||
|
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):
|
||||||
|
expr_: str = expr.expr.accept(self)
|
||||||
|
name: str = expr.name.lexeme
|
||||||
|
return f"{expr_}.{name}"
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: m.VariableExpr):
|
||||||
|
return expr.name.lexeme
|
||||||
|
|
||||||
|
def visit_grouping_expr(self, expr: m.GroupingExpr):
|
||||||
|
expr_: str = expr.expr.accept(self)
|
||||||
|
return f"({expr_})"
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: m.LiteralExpr):
|
||||||
|
return str(expr.value)
|
||||||
|
|
||||||
|
def visit_wildcard_expr(self, expr: m.WildcardExpr):
|
||||||
|
return "_"
|
||||||
|
|
||||||
|
# Types
|
||||||
|
|
||||||
|
def visit_named_type(self, type: m.NamedType) -> str:
|
||||||
|
return type.name.lexeme
|
||||||
|
|
||||||
|
def visit_generic_type(self, type: m.GenericType) -> str:
|
||||||
|
res: str = type.type.accept(self)
|
||||||
|
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_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: list[str] = [self._print_param(param) for param in spec.pos]
|
||||||
|
mixed: list[str] = [self._print_param(param) for param in spec.mixed]
|
||||||
|
kw: list[str] = [self._print_param(param) for param in spec.kw]
|
||||||
|
params: list[str] = pos
|
||||||
|
|
||||||
|
if len(pos) != 0:
|
||||||
|
params.append("/")
|
||||||
|
params += mixed
|
||||||
|
if len(kw) != 0:
|
||||||
|
params.append("*")
|
||||||
|
params += kw
|
||||||
|
return f"({', '.join(params)})"
|
||||||
|
|
||||||
|
def _print_param(self, param: m.FunctionType.Parameter) -> str:
|
||||||
|
res: str = ""
|
||||||
|
if param.name is not None:
|
||||||
|
res += param.name.lexeme
|
||||||
|
res += ": "
|
||||||
|
res += param.type.accept(self)
|
||||||
|
if not param.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)}"
|
||||||
238
midas/ast/printer/midas_ast.py
Normal file
238
midas/ast/printer/midas_ast.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.printer.base import AstPrinter
|
||||||
|
|
||||||
|
|
||||||
|
class MidasAstPrinter(
|
||||||
|
AstPrinter,
|
||||||
|
m.Expr.Visitor[None],
|
||||||
|
m.Stmt.Visitor[None],
|
||||||
|
m.Type.Visitor[None],
|
||||||
|
):
|
||||||
|
# Statements
|
||||||
|
|
||||||
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||||
|
self._write_line("TypeStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_sequence(
|
||||||
|
"params",
|
||||||
|
stmt.params,
|
||||||
|
print_func=self._print_type_param,
|
||||||
|
)
|
||||||
|
self._write_line("type", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.type.accept(self)
|
||||||
|
|
||||||
|
def visit_alias_stmt(self, stmt: m.AliasStmt) -> None:
|
||||||
|
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):
|
||||||
|
stmt.type.accept(self)
|
||||||
|
|
||||||
|
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||||
|
self._write_line("ExtendStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_sequence(
|
||||||
|
"params",
|
||||||
|
stmt.params,
|
||||||
|
print_func=self._print_type_param,
|
||||||
|
)
|
||||||
|
self._write_sequence("members", stmt.members, last=True)
|
||||||
|
|
||||||
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt):
|
||||||
|
self._write_line("PredicateStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{stmt.name.lexeme}"')
|
||||||
|
self._write_sequence(
|
||||||
|
"params",
|
||||||
|
stmt.params,
|
||||||
|
print_func=self._visit_param_spec,
|
||||||
|
)
|
||||||
|
self._write_line("body", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.body.accept(self)
|
||||||
|
|
||||||
|
# Expressions
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: m.LogicalExpr):
|
||||||
|
self._write_line("LogicalExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("left")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.left.accept(self)
|
||||||
|
|
||||||
|
self._write_line(f"operator: {expr.operator.lexeme}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: m.BinaryExpr):
|
||||||
|
self._write_line("BinaryExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("left")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.left.accept(self)
|
||||||
|
|
||||||
|
self._write_line(f"operator: {expr.operator.lexeme}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: m.UnaryExpr):
|
||||||
|
self._write_line("UnaryExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"operator: {expr.operator.lexeme}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
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_sequence("arguments", expr.arguments)
|
||||||
|
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):
|
||||||
|
self._write_line("GetExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("expr")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.expr.accept(self)
|
||||||
|
self._write_line(f'name: "{expr.name.lexeme}"', last=True)
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: m.VariableExpr):
|
||||||
|
self._write_line("VariableExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f'name: "{expr.name.lexeme}"', last=True)
|
||||||
|
|
||||||
|
def visit_grouping_expr(self, expr: m.GroupingExpr):
|
||||||
|
self._write_line("GroupingExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("expr", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.expr.accept(self)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: m.LiteralExpr) -> None:
|
||||||
|
self._write_line("LiteralExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"value: {expr.value}", last=True)
|
||||||
|
|
||||||
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None:
|
||||||
|
self._write_line("WildcardExpr")
|
||||||
|
|
||||||
|
# Types
|
||||||
|
|
||||||
|
def visit_named_type(self, type: m.NamedType) -> None:
|
||||||
|
self._write_line("NamedType")
|
||||||
|
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_sequence("args", type.args, last=True)
|
||||||
|
|
||||||
|
def visit_constraint_type(self, type: m.ConstraintType) -> None:
|
||||||
|
self._write_line("ConstraintType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.type.accept(self)
|
||||||
|
self._write_line("constraint", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
type.constraint.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_sequence(
|
||||||
|
"pos",
|
||||||
|
spec.pos,
|
||||||
|
print_func=self._print_param,
|
||||||
|
)
|
||||||
|
self._write_sequence(
|
||||||
|
"mixed",
|
||||||
|
spec.mixed,
|
||||||
|
print_func=self._print_param,
|
||||||
|
)
|
||||||
|
self._write_sequence(
|
||||||
|
"kw",
|
||||||
|
spec.kw,
|
||||||
|
print_func=self._print_param,
|
||||||
|
last=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_param(self, param: m.FunctionType.Parameter) -> None:
|
||||||
|
self._write_line("Parameter")
|
||||||
|
with self._child_level():
|
||||||
|
name: str = "None"
|
||||||
|
if param.name is not None:
|
||||||
|
name = f'"{param.name.lexeme}"'
|
||||||
|
self._write_line(f"name: {name}")
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
param.type.accept(self)
|
||||||
|
self._write_line(f"required: {param.required}", last=True)
|
||||||
|
|
||||||
|
def visit_frame_type(self, type: m.FrameType) -> None:
|
||||||
|
self._write_line("FrameType")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_sequence(
|
||||||
|
"columns",
|
||||||
|
type.columns,
|
||||||
|
print_func=self._print_frame_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)
|
||||||
285
midas/ast/printer/python_ast.py
Normal file
285
midas/ast/printer/python_ast.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import ast
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
from midas.ast.printer.base import AstPrinter
|
||||||
|
|
||||||
|
|
||||||
|
class PythonAstPrinter(
|
||||||
|
AstPrinter,
|
||||||
|
p.MidasType.Visitor[None],
|
||||||
|
p.Stmt.Visitor[None],
|
||||||
|
p.Expr.Visitor[None],
|
||||||
|
):
|
||||||
|
# Types
|
||||||
|
|
||||||
|
def visit_base_type(self, node: p.BaseType) -> None:
|
||||||
|
self._write_line("BaseType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"base: {node.base}")
|
||||||
|
self._write_sequence("args", node.args, last=True)
|
||||||
|
|
||||||
|
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
||||||
|
self._write_line("ConstraintType")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
node.type.accept(self)
|
||||||
|
self._write_line(f"constraint: {ast.unparse(node.constraint)}", last=True)
|
||||||
|
|
||||||
|
def visit_frame_column(self, node: p.FrameColumn) -> None:
|
||||||
|
self._write_line("FrameColumn")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"name: {node.name}")
|
||||||
|
self._write_optional_child("type", node.type, last=True)
|
||||||
|
|
||||||
|
def visit_frame_type(self, node: p.FrameType) -> None:
|
||||||
|
self._write_line("FrameType")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_sequence("columns", node.columns)
|
||||||
|
|
||||||
|
# Statements
|
||||||
|
|
||||||
|
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
|
||||||
|
stmt.expr.accept(self)
|
||||||
|
|
||||||
|
def visit_function(self, stmt: p.Function) -> None:
|
||||||
|
self._write_line("Function")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"name: {stmt.name}")
|
||||||
|
self._write_line("params")
|
||||||
|
with self._child_level():
|
||||||
|
self._print_param_spec(stmt.params)
|
||||||
|
|
||||||
|
self._write_optional_child("returns", stmt.returns)
|
||||||
|
self._write_sequence("body", stmt.body, last=True)
|
||||||
|
|
||||||
|
def _print_param_spec(self, spec: p.ParamSpec) -> None:
|
||||||
|
self._write_line("ParamSpec")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_sequence(
|
||||||
|
"pos",
|
||||||
|
spec.pos,
|
||||||
|
print_func=self._print_param,
|
||||||
|
)
|
||||||
|
self._write_sequence(
|
||||||
|
"mixed",
|
||||||
|
spec.mixed,
|
||||||
|
print_func=self._print_param,
|
||||||
|
)
|
||||||
|
self._write_sequence(
|
||||||
|
"kw",
|
||||||
|
spec.kw,
|
||||||
|
print_func=self._print_param,
|
||||||
|
last=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_param(self, param: p.Function.Parameter) -> None:
|
||||||
|
self._write_line("Parameter")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"name: {param.name}")
|
||||||
|
self._write_optional_child("type", param.type, last=True)
|
||||||
|
|
||||||
|
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||||
|
self._write_line("TypeAssign")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"name: {stmt.name}")
|
||||||
|
self._write_line("type", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.type.accept(self)
|
||||||
|
|
||||||
|
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
||||||
|
self._write_line("AssignStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_sequence("targets", stmt.targets)
|
||||||
|
self._write_line("value", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.value.accept(self)
|
||||||
|
|
||||||
|
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
||||||
|
self._write_line("ReturnStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_optional_child("value", stmt.value, last=True)
|
||||||
|
|
||||||
|
def visit_if_stmt(self, stmt: p.IfStmt) -> None:
|
||||||
|
self._write_line("IfStmt")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("test")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
stmt.test.accept(self)
|
||||||
|
self._write_sequence("body", stmt.body)
|
||||||
|
self._write_sequence("orelse", stmt.orelse, last=True)
|
||||||
|
|
||||||
|
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_sequence("body", stmt.body, last=True)
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
# Expressions
|
||||||
|
|
||||||
|
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
|
||||||
|
self._write_line("BinaryExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("left")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.left.accept(self)
|
||||||
|
|
||||||
|
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_compare_expr(self, expr: p.CompareExpr) -> None:
|
||||||
|
self._write_line("CompareExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("left")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.left.accept(self)
|
||||||
|
|
||||||
|
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_unary_expr(self, expr: p.UnaryExpr) -> None:
|
||||||
|
self._write_line("UnaryExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_call_expr(self, expr: p.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_sequence("arguments", expr.arguments)
|
||||||
|
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: p.GetExpr) -> None:
|
||||||
|
self._write_line("GetExpr")
|
||||||
|
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}", last=True)
|
||||||
|
|
||||||
|
def visit_literal_expr(self, expr: p.LiteralExpr) -> None:
|
||||||
|
self._write_line("LiteralExpr")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_line(f"value: {expr.value!r}")
|
||||||
|
|
||||||
|
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
||||||
|
self._write_line("VariableExpr")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
self._write_line(f"name: {expr.name}")
|
||||||
|
|
||||||
|
def visit_logical_expr(self, expr: p.LogicalExpr) -> None:
|
||||||
|
self._write_line("LogicalExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("left")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.left.accept(self)
|
||||||
|
|
||||||
|
self._write_line(f"operator: {expr.operator.__class__.__name__}")
|
||||||
|
|
||||||
|
self._write_line("right", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.right.accept(self)
|
||||||
|
|
||||||
|
def visit_cast_expr(self, expr: p.CastExpr) -> None:
|
||||||
|
self._write_line("CastExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("type")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.type.accept(self)
|
||||||
|
self._write_line("expr")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.expr.accept(self)
|
||||||
|
self._write_line(f"unsafe: {expr.unsafe}", last=True)
|
||||||
|
|
||||||
|
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None:
|
||||||
|
self._write_line("TernaryExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_line("test")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.test.accept(self)
|
||||||
|
|
||||||
|
self._write_line("if_true")
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.if_true.accept(self)
|
||||||
|
|
||||||
|
self._write_line("if_false", last=True)
|
||||||
|
with self._child_level(single=True):
|
||||||
|
expr.if_false.accept(self)
|
||||||
|
|
||||||
|
def visit_list_expr(self, expr: p.ListExpr) -> None:
|
||||||
|
self._write_line("ListExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_sequence("items", expr.items, last=True)
|
||||||
|
|
||||||
|
def visit_dict_expr(self, expr: p.DictExpr) -> None:
|
||||||
|
self._write_line("DictExpr")
|
||||||
|
with self._child_level():
|
||||||
|
self._write_sequence(
|
||||||
|
"keys",
|
||||||
|
expr.keys,
|
||||||
|
print_func=lambda k: (
|
||||||
|
self._write_line("None") if k is None else k.accept(self)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._write_sequence("values", expr.values, last=True)
|
||||||
|
|
||||||
|
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_sequence("items", expr.items, last=True)
|
||||||
|
|
||||||
|
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,27 @@ from midas.ast.location import Location
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
pos: list[Function.Parameter]
|
||||||
|
mixed: list[Function.Parameter]
|
||||||
|
kw: list[Function.Parameter]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all(self) -> list[Function.Parameter]:
|
||||||
|
return self.pos + self.mixed + self.kw
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ImportAlias:
|
||||||
|
location: Location
|
||||||
|
name: str
|
||||||
|
alias: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def imported_name(self) -> str:
|
||||||
|
return self.alias if self.alias is not None else self.name
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Type annotations #
|
# Type annotations #
|
||||||
@@ -44,7 +65,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)
|
||||||
@@ -113,6 +134,12 @@ class Stmt(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_for_stmt(self, stmt: ForStmt) -> T: ...
|
def visit_for_stmt(self, stmt: ForStmt) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_import_stmt(self, stmt: ImportStmt) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_from_import_stmt(self, stmt: FromImportStmt) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_raw_stmt(self, stmt: RawStmt) -> T: ...
|
def visit_raw_stmt(self, stmt: RawStmt) -> T: ...
|
||||||
|
|
||||||
@@ -128,25 +155,17 @@ class ExpressionStmt(Stmt):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Function(Stmt):
|
class Function(Stmt):
|
||||||
name: str
|
name: str
|
||||||
posonlyargs: list[Argument]
|
params: ParamSpec
|
||||||
args: list[Argument]
|
|
||||||
sink: Optional[Argument]
|
|
||||||
kwonlyargs: list[Argument]
|
|
||||||
kw_sink: Optional[Argument]
|
|
||||||
returns: Optional[MidasType]
|
returns: Optional[MidasType]
|
||||||
body: list[Stmt]
|
body: list[Stmt]
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Argument:
|
class Parameter:
|
||||||
location: Optional[Location] = None
|
location: Optional[Location] = None
|
||||||
name: str
|
name: str
|
||||||
type: Optional[MidasType]
|
type: Optional[MidasType]
|
||||||
default: Optional[Expr]
|
default: Optional[Expr]
|
||||||
|
|
||||||
@property
|
|
||||||
def all_args(self) -> list[Argument]:
|
|
||||||
return self.posonlyargs + self.args + self.kwonlyargs
|
|
||||||
|
|
||||||
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
return visitor.visit_function(self)
|
return visitor.visit_function(self)
|
||||||
|
|
||||||
@@ -205,6 +224,24 @@ class ForStmt(Stmt):
|
|||||||
return visitor.visit_for_stmt(self)
|
return visitor.visit_for_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ImportStmt(Stmt):
|
||||||
|
imports: list[ImportAlias]
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_import_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FromImportStmt(Stmt):
|
||||||
|
module: Optional[str]
|
||||||
|
imports: list[ImportAlias]
|
||||||
|
level: int
|
||||||
|
|
||||||
|
def accept(self, visitor: Stmt.Visitor[T]) -> T:
|
||||||
|
return visitor.visit_from_import_stmt(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RawStmt(Stmt):
|
class RawStmt(Stmt):
|
||||||
stmt: ast.stmt
|
stmt: ast.stmt
|
||||||
@@ -268,6 +305,9 @@ class Expr(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_slice_expr(self, expr: SliceExpr) -> T: ...
|
def visit_slice_expr(self, expr: SliceExpr) -> T: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def visit_tuple_expr(self, expr: TupleExpr) -> T: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def visit_raw_expr(self, expr: RawExpr) -> T: ...
|
def visit_raw_expr(self, expr: RawExpr) -> T: ...
|
||||||
|
|
||||||
@@ -350,6 +390,7 @@ class LogicalExpr(Expr):
|
|||||||
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)
|
||||||
@@ -401,6 +442,14 @@ class SliceExpr(Expr):
|
|||||||
return visitor.visit_slice_expr(self)
|
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)
|
@dataclass(frozen=True)
|
||||||
class RawExpr(Expr):
|
class RawExpr(Expr):
|
||||||
expr: ast.expr
|
expr: ast.expr
|
||||||
|
|||||||
@@ -179,3 +179,99 @@ extend dict[K, V] {
|
|||||||
// def __ior__: fn(value: Iterable[tuple[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
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
BUILTIN_SUBTYPES: dict[str, set[str]] = {
|
BUILTIN_SUBTYPES: dict[str, set[str]] = {
|
||||||
|
"object": {"float", "list", "dict", "str", "bytes", "tuple"},
|
||||||
"float": {"int"},
|
"float": {"int"},
|
||||||
"int": {"bool"},
|
|
||||||
}
|
}
|
||||||
|
"""
|
||||||
|
Hard-coded subtype relationships between builtin types
|
||||||
|
|
||||||
|
Circular dependencies and diamond inheritance MUST be avoided
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def define_builtins(reg: TypesRegistry):
|
def define_builtins(reg: TypesRegistry):
|
||||||
@@ -25,12 +30,15 @@ def define_builtins(reg: TypesRegistry):
|
|||||||
any = reg.define_type("Any", TopType())
|
any = reg.define_type("Any", TopType())
|
||||||
unit = reg.define_type("None", UnitType())
|
unit = reg.define_type("None", UnitType())
|
||||||
object = reg.define_type("object", BaseType(name="object"))
|
object = reg.define_type("object", BaseType(name="object"))
|
||||||
|
bytes = reg.define_type("bytes", BaseType(name="bytes"))
|
||||||
bool = reg.define_type("bool", BaseType(name="bool"))
|
bool = reg.define_type("bool", BaseType(name="bool"))
|
||||||
int = reg.define_type("int", BaseType(name="int"))
|
int = reg.define_type("int", BaseType(name="int"))
|
||||||
float = reg.define_type("float", BaseType(name="float"))
|
float = reg.define_type("float", BaseType(name="float"))
|
||||||
str = reg.define_type("str", BaseType(name="str"))
|
str = reg.define_type("str", BaseType(name="str"))
|
||||||
slice = reg.define_type("slice", BaseType(name="slice"))
|
slice = reg.define_type("slice", BaseType(name="slice"))
|
||||||
|
|
||||||
|
tuple = reg.define_type("tuple", BaseType(name="tuple"))
|
||||||
|
|
||||||
list = reg.define_type(
|
list = reg.define_type(
|
||||||
"list",
|
"list",
|
||||||
GenericType(
|
GenericType(
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ from midas.utils import TypedAST
|
|||||||
|
|
||||||
|
|
||||||
class TypeChecker:
|
class TypeChecker:
|
||||||
|
"""Type checking dispatcher
|
||||||
|
|
||||||
|
Contains a typer for Midas and one for Python, as well as the types registry
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.types: TypesRegistry = TypesRegistry()
|
self.types: TypesRegistry = TypesRegistry()
|
||||||
self.reporter: Reporter = Reporter()
|
self.reporter: Reporter = Reporter()
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ 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:
|
||||||
|
"""Information about a diagnostic (warning, errors, etc.)
|
||||||
|
|
||||||
|
Holds a location, a diagnostic type and a message.
|
||||||
|
Optionally bound to a file path
|
||||||
|
"""
|
||||||
|
|
||||||
file_path: Optional[str]
|
file_path: Optional[str]
|
||||||
location: Location
|
location: Location
|
||||||
type: DiagnosticType
|
type: DiagnosticType
|
||||||
@@ -20,6 +27,18 @@ class Diagnostic:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def location_str(self) -> str:
|
def location_str(self) -> str:
|
||||||
|
"""Get diagnostic type and location as a human readable string
|
||||||
|
|
||||||
|
The location is formatted as "<Type> in <file> from L<start_line>:<start_col> to <end_line>:<end_col>",
|
||||||
|
for example: "Error in /home/user/Desktop/script.py from L12:5 to L12:8"
|
||||||
|
|
||||||
|
If the file is `None`, the "in ..." section is excluded from the result.<br>
|
||||||
|
If the location's end is not specified, the formulation "at L<start_line>:<start_col>" is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the formatted type and location string
|
||||||
|
"""
|
||||||
|
|
||||||
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 (
|
||||||
|
|||||||
532
midas/checker/dispatcher.py
Normal file
532
midas/checker/dispatcher.py
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
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]
|
||||||
|
"""An expression and its type"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class MappedArgument(Generic[E]):
|
||||||
|
"""An argument passed in a call and the corresponding parameter"""
|
||||||
|
|
||||||
|
arg_expr: E
|
||||||
|
arg_type: Type
|
||||||
|
parameter: Function.Parameter
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class OverloadCandidate:
|
||||||
|
"""An overloaded function call candidate with its mapped arguments"""
|
||||||
|
|
||||||
|
function: Function
|
||||||
|
mapped: list[MappedArgument]
|
||||||
|
|
||||||
|
|
||||||
|
class CallError(StrEnum):
|
||||||
|
"""Reason of a call error"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""The result of a function call
|
||||||
|
|
||||||
|
Holds a return type, an optional error reason and message
|
||||||
|
"""
|
||||||
|
|
||||||
|
error: Optional[CallError] = None
|
||||||
|
"""The reason of the error, if there is one"""
|
||||||
|
|
||||||
|
result: Type = UnknownType()
|
||||||
|
"""The result type. `UnknownType()` if the call is invalid"""
|
||||||
|
|
||||||
|
message: Optional[str] = None
|
||||||
|
"""An optional error message"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
"""Whether the call is valid (i.e. no error)"""
|
||||||
|
return self.error is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_message(self) -> str:
|
||||||
|
"""A descriptive message for the error, if there is one"""
|
||||||
|
if self.message is not None:
|
||||||
|
return self.message
|
||||||
|
if self.error is not None:
|
||||||
|
return str(self.error)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class CallDispatcher(Generic[E]):
|
||||||
|
"""Helper class to handle dispatching calls and mapping arguments
|
||||||
|
|
||||||
|
This class is responsible for mapping call-site arguments to function
|
||||||
|
parameters, verifying the validity of calls and computing their
|
||||||
|
return types
|
||||||
|
|
||||||
|
:class:`CallDispatcher` is generic to handle AST nodes from both Midas and Python
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 callee has overloads, this function will try to resolve the
|
||||||
|
appropriate signature.
|
||||||
|
Argument types are matched with the defined parameters.
|
||||||
|
This 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 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:
|
||||||
|
CallResult: the call result, either a type or an error
|
||||||
|
"""
|
||||||
|
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]]:
|
||||||
|
"""Unwrap a type to get a callable `Function`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callee (Type): the called type
|
||||||
|
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr[E]]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Union[tuple[Function, None], tuple[None, CallError]]: a tuple
|
||||||
|
containing the callable `Function` type, or `None` if it could
|
||||||
|
not be unwrapped, and an error, or `None` if there was none.
|
||||||
|
"""
|
||||||
|
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 arg.parameter.unsupported:
|
||||||
|
# Always report error
|
||||||
|
self.reporter.error(
|
||||||
|
arg.arg_expr.location, f"Unsupported argument {arg.parameter.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.types.is_subtype(arg.arg_type, arg.parameter.type):
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
arg.arg_expr.location,
|
||||||
|
f"Wrong type for argument '{arg.parameter.name}', expected {arg.parameter.type}, got {arg.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:
|
||||||
|
Union[tuple[Function, None], tuple[None, str]]: a tuple containing
|
||||||
|
the resolved function signature if it can be determined
|
||||||
|
unambiguously, or `None`, and an error message, 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_params: set[str] = set()
|
||||||
|
|
||||||
|
required_positional: list[str] = [
|
||||||
|
param.name
|
||||||
|
for param in function.params.pos + function.params.mixed
|
||||||
|
if param.required
|
||||||
|
]
|
||||||
|
required_keyword: list[str] = [
|
||||||
|
param.name for param in function.params.kw if param.required
|
||||||
|
]
|
||||||
|
|
||||||
|
mapped: list[MappedArgument[E]] = []
|
||||||
|
|
||||||
|
pos_params: list[Function.Parameter] = list(function.params.pos)
|
||||||
|
mixed_params: list[Function.Parameter] = list(function.params.mixed)
|
||||||
|
kw_params: dict[str, Function.Parameter] = {
|
||||||
|
param.name: param for param in function.params.kw
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_call: bool = True
|
||||||
|
|
||||||
|
# TODO: handle *args and **kwargs sinks
|
||||||
|
for arg in positional:
|
||||||
|
param: Function.Parameter
|
||||||
|
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_params.add(name)
|
||||||
|
mapped.append(
|
||||||
|
MappedArgument(
|
||||||
|
arg_expr=arg[0],
|
||||||
|
arg_type=arg[1],
|
||||||
|
parameter=param,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
kw_params.update({param.name: param for param in mixed_params})
|
||||||
|
for name, arg in keywords.items():
|
||||||
|
param: Function.Parameter
|
||||||
|
if name not in kw_params:
|
||||||
|
if report_errors:
|
||||||
|
if name in set_params:
|
||||||
|
self.reporter.error(
|
||||||
|
arg[0].location, f"Multiple values for parameter '{name}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.reporter.error(
|
||||||
|
arg[0].location, f"Unknown keyword parameter '{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_params.add(name)
|
||||||
|
mapped.append(
|
||||||
|
MappedArgument(
|
||||||
|
arg_expr=arg[0],
|
||||||
|
arg_type=arg[1],
|
||||||
|
parameter=param,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def join_params(params: list[str]) -> str:
|
||||||
|
params = list(map(lambda p: f"'{p}'", params))
|
||||||
|
if len(params) == 0:
|
||||||
|
return ""
|
||||||
|
if len(params) == 1:
|
||||||
|
return params[0]
|
||||||
|
return ", ".join(params[:-1]) + " and " + params[-1]
|
||||||
|
|
||||||
|
if len(required_positional) != 0:
|
||||||
|
plural: str = "" if len(required_positional) == 1 else "s"
|
||||||
|
params: str = join_params(required_positional)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Missing required positional argument{plural}: {params}",
|
||||||
|
)
|
||||||
|
valid_call = False
|
||||||
|
|
||||||
|
if len(required_keyword) != 0:
|
||||||
|
plural: str = "" if len(required_keyword) == 1 else "s"
|
||||||
|
params: str = join_params(required_keyword)
|
||||||
|
if report_errors:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Missing required keyword argument{plural}: {params}",
|
||||||
|
)
|
||||||
|
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.arg_expr] = arg.parameter.type
|
||||||
|
|
||||||
|
for arg in mapped2:
|
||||||
|
type2: Type = arg.parameter.type
|
||||||
|
type1: Type = by_expr[arg.arg_expr]
|
||||||
|
if not self.types.is_subtype(type1, type2):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
265
midas/checker/evaluator.py
Normal file
265
midas/checker/evaluator.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.location import Location
|
||||||
|
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):
|
||||||
|
"""A partially applied predicate"""
|
||||||
|
|
||||||
|
scope: dict[str, Any]
|
||||||
|
"""A dictionary of already applied parameters"""
|
||||||
|
|
||||||
|
|
||||||
|
class Evaluator(m.Expr.Visitor[Any]):
|
||||||
|
"""Helper class to evaluate an expression
|
||||||
|
|
||||||
|
This class is used to evaluate constraint types on literals at compile-time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Evaluate the given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to evaluate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: the value of the expression
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Get the value of a variable in the current scope
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: if the variable is not defined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: the value of the variable
|
||||||
|
"""
|
||||||
|
scope: dict[str, Any] = self.scopes[-1]
|
||||||
|
return scope[name]
|
||||||
|
|
||||||
|
def set_value(self, name: str, value: Any, force_declare: bool = False):
|
||||||
|
"""Set the value of a variable
|
||||||
|
|
||||||
|
If `force_declare` is `False`, this function first tries to find the
|
||||||
|
closest scope in which the variable is defined and assign the value in
|
||||||
|
that scope, if it can find one.
|
||||||
|
|
||||||
|
If `force_declare` is `True` or if the variable is not defined in any
|
||||||
|
scope, it is declare and assigned in the current scope
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
value (Any): the value of the variable
|
||||||
|
force_declare (bool, optional): if `False` and the variable is
|
||||||
|
defined in a scope, the value is assigned in that scope (the
|
||||||
|
closest if there are multiple declarations). Defaults to 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.PLUS:
|
||||||
|
return left + right
|
||||||
|
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.PLUS:
|
||||||
|
return +right
|
||||||
|
case TokenType.MINUS:
|
||||||
|
return -right
|
||||||
|
case TokenType.BANG:
|
||||||
|
return not 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(expr.location, 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,
|
||||||
|
location: Location,
|
||||||
|
predicate: Predicate,
|
||||||
|
args: list[Any],
|
||||||
|
kwargs: dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
"""Evaluate a predicate function call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
predicate (Predicate): the predicate to evaluate
|
||||||
|
args (list[Any]): a list of positional arguments
|
||||||
|
kwargs (dict[str, Any]): a map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: the value returned by the predicate call
|
||||||
|
"""
|
||||||
|
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(location, predicate.type, args, kwargs)
|
||||||
|
res = PartialPredicate(
|
||||||
|
type=inner,
|
||||||
|
body=predicate.body,
|
||||||
|
alias=False,
|
||||||
|
scope=self.scopes[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
case Function():
|
||||||
|
self._map_args(location, predicate.type, args, kwargs)
|
||||||
|
res = self.evaluate(predicate.body)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
self.scopes.pop()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _map_args(
|
||||||
|
self,
|
||||||
|
location: Location,
|
||||||
|
function: Function,
|
||||||
|
args: list[Any],
|
||||||
|
kwargs: dict[str, Any],
|
||||||
|
):
|
||||||
|
"""Map call arguments to a function's parameters and set their values in context
|
||||||
|
|
||||||
|
Each argument is mapped to a parameter of the function, then its value
|
||||||
|
is set in the context using :func:`set_value` with the parameter's name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function (Function): the called function
|
||||||
|
args (list[Any]): a list of positional arguments
|
||||||
|
kwargs (dict[str, Any]): a map of keyword arguments
|
||||||
|
"""
|
||||||
|
positional: list[Function.Parameter] = (
|
||||||
|
function.params.pos + function.params.mixed
|
||||||
|
)
|
||||||
|
keywords: dict[str, Function.Parameter] = {
|
||||||
|
param.name: param for param in function.params.mixed + function.params.kw
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg in enumerate(args):
|
||||||
|
if i >= len(positional):
|
||||||
|
if self.reporter is not None:
|
||||||
|
self.reporter.error(
|
||||||
|
location,
|
||||||
|
f"Too many positional arguments, expected at most {len(positional)}, got {len(args)}",
|
||||||
|
)
|
||||||
|
break
|
||||||
|
param: Function.Parameter = positional[i]
|
||||||
|
self.set_value(param.name, arg)
|
||||||
|
|
||||||
|
for name, arg in kwargs.items():
|
||||||
|
if name not in keywords:
|
||||||
|
if self.reporter is not None:
|
||||||
|
self.reporter.error(location, f"Unknown keyword argument '{name}'")
|
||||||
|
break
|
||||||
|
param: Function.Parameter = keywords[name]
|
||||||
|
self.set_value(param.name, arg)
|
||||||
240
midas/checker/frames/column_groupby_methods.py
Normal file
240
midas/checker/frames/column_groupby_methods.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
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,
|
||||||
|
ParamSpec,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
"""A column group-by method call, implements :class:`utils.MethodCall`"""
|
||||||
|
|
||||||
|
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]):
|
||||||
|
"""The method registry for column group-by types"""
|
||||||
|
|
||||||
|
NAMED_ARGS: dict[str, str] = {
|
||||||
|
"numeric_only": "bool",
|
||||||
|
"skipna": "bool",
|
||||||
|
"engine": "str",
|
||||||
|
"engine_kwargs": "dict",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _aggregate(
|
||||||
|
self,
|
||||||
|
call: Call,
|
||||||
|
method: str,
|
||||||
|
params: list[str | tuple[str, str, bool]] = [],
|
||||||
|
) -> Type:
|
||||||
|
"""Compute the result type of an aggregate method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
method (str): the method name to delegate on :class:`Column`
|
||||||
|
params (list[str | tuple[str, str, bool], optional): a list of extra
|
||||||
|
mixed parameters. The list can contain strings to include
|
||||||
|
parameters predefined in `NAMED_ARGS`, or tuples containing the
|
||||||
|
parameter's name, type and required flag. Defaults to [].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
real_params: list[Function.Parameter] = []
|
||||||
|
for i, param in enumerate(params):
|
||||||
|
match param:
|
||||||
|
case str() as name:
|
||||||
|
param = Function.Parameter(
|
||||||
|
pos=i,
|
||||||
|
name=name,
|
||||||
|
type=self.types.get_type(self.NAMED_ARGS[name]),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
case (name, type, required):
|
||||||
|
param = Function.Parameter(
|
||||||
|
pos=i,
|
||||||
|
name=name,
|
||||||
|
type=self.types.get_type(type),
|
||||||
|
required=required,
|
||||||
|
)
|
||||||
|
real_params.append(param)
|
||||||
|
|
||||||
|
# TODO: maybe better to filter arguments and pass some, in case the
|
||||||
|
# return type depends on them
|
||||||
|
returns: Type = self.typer.call_method(
|
||||||
|
location=call.location,
|
||||||
|
call_expr=call.call_expr,
|
||||||
|
obj=(call.groupby_expr, call.groupby.column),
|
||||||
|
method_name=method,
|
||||||
|
positional=[],
|
||||||
|
keywords={},
|
||||||
|
)
|
||||||
|
if not isinstance(returns, ColumnType):
|
||||||
|
returns = ColumnType(type=UnknownType())
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(mixed=real_params),
|
||||||
|
returns=returns,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"kurt",
|
||||||
|
["skipna", "numeric_only"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def max(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"max",
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"mean",
|
||||||
|
["numeric_only", "skipna", "engine", "engine_kwargs"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"median",
|
||||||
|
["numeric_only", "skipna"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"min",
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def prod(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"prod",
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"std",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"ddof",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
"numeric_only",
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"sum",
|
||||||
|
[
|
||||||
|
"numeric_only",
|
||||||
|
(
|
||||||
|
"min_count",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"skipna",
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
"var",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"var",
|
||||||
|
"int",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
"engine",
|
||||||
|
"engine_kwargs",
|
||||||
|
"numeric_only",
|
||||||
|
"skipna",
|
||||||
|
],
|
||||||
|
)
|
||||||
182
midas/checker/frames/column_manager.py
Normal file
182
midas/checker/frames/column_manager.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
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.dispatcher import CallResult
|
||||||
|
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.reporter import FileReporter
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnGroupBy,
|
||||||
|
ColumnType,
|
||||||
|
Function,
|
||||||
|
OverloadedFunction,
|
||||||
|
ParamSpec,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnManager:
|
||||||
|
"""Helper class to handle methods and subscripts on column types"""
|
||||||
|
|
||||||
|
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 get(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
column: ColumnType,
|
||||||
|
index: TypedExpr,
|
||||||
|
) -> Type:
|
||||||
|
"""Compute the type of a subscript access
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
|
location (Location): the subscript's location
|
||||||
|
column (DataFrameType): the column type
|
||||||
|
index (TypedExpr): the index
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting type
|
||||||
|
"""
|
||||||
|
|
||||||
|
single = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
pos=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="index",
|
||||||
|
type=self.typer.types.get_type("int"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=column.type,
|
||||||
|
)
|
||||||
|
slice = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
pos=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="slice",
|
||||||
|
type=self.typer.types.get_type("slice"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=column,
|
||||||
|
)
|
||||||
|
|
||||||
|
overload = OverloadedFunction(overloads=[single, slice])
|
||||||
|
|
||||||
|
result: CallResult = self.typer.dispatcher.get_result(
|
||||||
|
location=location,
|
||||||
|
callee=overload,
|
||||||
|
positional=[index],
|
||||||
|
keywords={},
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Compute the result type of a column's method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): the method name
|
||||||
|
location (Location): the call's location
|
||||||
|
call_expr (p.Expr): the call expression
|
||||||
|
column (ColumnType): the column type
|
||||||
|
column_expr (p.Expr): the column expression
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result 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:
|
||||||
|
"""Compute the result type of a column group-by's method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): the method name
|
||||||
|
location (Location): the call's location
|
||||||
|
call_expr (p.Expr): the call expression
|
||||||
|
groupby (ColumnGroupBy): the column group-by object
|
||||||
|
groupby_expr (p.Expr): the column group-by expression
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result 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]:
|
||||||
|
"""Get the type of a column's attribute
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column (ColumnType): the column type
|
||||||
|
name (str): the attribute's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the attribute's type, or `None` if it doesn't exist
|
||||||
|
"""
|
||||||
|
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
|
||||||
683
midas/checker/frames/column_methods.py
Normal file
683
midas/checker/frames/column_methods.py
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Callable, Optional, TypeAlias, Union
|
||||||
|
|
||||||
|
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,
|
||||||
|
OverloadedFunction,
|
||||||
|
ParamSpec,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
UnitType,
|
||||||
|
UnknownType,
|
||||||
|
unfold_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
FormulaOperand: TypeAlias = Union["Formula", str, Type]
|
||||||
|
"""
|
||||||
|
A operand type in a :data:`Formula`
|
||||||
|
|
||||||
|
Must be one of the following:
|
||||||
|
- a nested formula
|
||||||
|
- a type name (a string)
|
||||||
|
- a type instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
Formula: TypeAlias = Union[Type, tuple[FormulaOperand, str, FormulaOperand]]
|
||||||
|
"""
|
||||||
|
A formula to compute the output type of a function
|
||||||
|
|
||||||
|
Must be either a type, or a tuple containing:
|
||||||
|
- a left operand
|
||||||
|
- an operation / method name (e.g. `"__add__"`)
|
||||||
|
- a right operand
|
||||||
|
|
||||||
|
For example, to compute the result of a `mean` function, given the input type `T`:
|
||||||
|
```python
|
||||||
|
mean_formula = ((T, "__add__", T), "__truediv__", "int")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
"""A column method call, implements :class:`utils.MethodCall`"""
|
||||||
|
|
||||||
|
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]):
|
||||||
|
"""The method registry for column types"""
|
||||||
|
|
||||||
|
def _resolve_formula_operand(self, call: Call, operand: FormulaOperand) -> Type:
|
||||||
|
"""Resolve the type of a formula operand
|
||||||
|
|
||||||
|
See :data:`FormulaOperand` for more information on the accepted format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
operand (FormulaOperand): the formula operand
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type of the operand
|
||||||
|
"""
|
||||||
|
match operand:
|
||||||
|
case str():
|
||||||
|
return self.types.get_type(operand)
|
||||||
|
case (_, _, _):
|
||||||
|
return self._resolve_formula_type(call, operand)
|
||||||
|
case _:
|
||||||
|
return operand
|
||||||
|
|
||||||
|
def _resolve_formula_type(self, call: Call, formula: Formula) -> Type:
|
||||||
|
"""Resolve the return type of a formula
|
||||||
|
|
||||||
|
See :data:`Formula` for more information on the accepted format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
formula (Formula): the formula to evaluate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the return type of the formula
|
||||||
|
"""
|
||||||
|
if not isinstance(formula, tuple):
|
||||||
|
return formula
|
||||||
|
|
||||||
|
op1, operator, op2 = formula
|
||||||
|
op1_type: Type = self._resolve_formula_operand(call, op1)
|
||||||
|
op2_type: Type = self._resolve_formula_operand(call, op2)
|
||||||
|
return self.typer.result_of_binary_op(
|
||||||
|
location=call.location,
|
||||||
|
expr=call.call_expr,
|
||||||
|
left=(call.column_expr, op1_type),
|
||||||
|
right=(call.column_expr, op2_type),
|
||||||
|
method=operator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _simple_call(self, call: Call, function: Type) -> Type:
|
||||||
|
"""Get the result of calling a simple method
|
||||||
|
|
||||||
|
This function is a simple wrapper around :func:`dispatcher.CallDispatcher.get_result`
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
function (Type): the function type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the return type
|
||||||
|
"""
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=function,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def _element_binary_op(self, call: Call, method: str) -> tuple[Type, bool]:
|
||||||
|
"""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:
|
||||||
|
tuple[Type, bool]: the resulting type and a boolean indicating
|
||||||
|
whether the operand is a column
|
||||||
|
"""
|
||||||
|
if len(call.positional) == 0:
|
||||||
|
return UnknownType(), False
|
||||||
|
|
||||||
|
col_type1: Type = call.column.type
|
||||||
|
operand: TypedExpr = call.positional[0]
|
||||||
|
unfolded_operand: Type = unfold_type(operand[1])
|
||||||
|
col_type2: Type
|
||||||
|
|
||||||
|
column_operand: bool = isinstance(unfolded_operand, ColumnType)
|
||||||
|
|
||||||
|
# Operand is a column -> get the inner type
|
||||||
|
if column_operand:
|
||||||
|
col_type2 = unfolded_operand.type
|
||||||
|
# Otherwise use the operand type itself
|
||||||
|
else:
|
||||||
|
col_type2 = operand[1]
|
||||||
|
|
||||||
|
new_inner_type = self.typer.result_of_binary_op(
|
||||||
|
location=call.location,
|
||||||
|
expr=call.call_expr,
|
||||||
|
left=(call.column_expr, col_type1),
|
||||||
|
right=(operand[0], col_type2),
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
return ColumnType(type=new_inner_type), column_operand
|
||||||
|
|
||||||
|
def _element_wise(self, call: Call, method: str) -> Type:
|
||||||
|
"""Compute the result of an element-wise method call
|
||||||
|
|
||||||
|
If the call is valid, this method also generates an assertion to check
|
||||||
|
that both operands have the same length at runtime
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
method (str): the method's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Build signature with new column type and generic operand
|
||||||
|
returns, column_operand = self._element_binary_op(call, method)
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="other",
|
||||||
|
type=TopType(),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=returns,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 and column_operand:
|
||||||
|
self._assert_same_length(
|
||||||
|
call.call_expr, call.column_expr, call.positional[0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def copy(self, call: Call) -> Type:
|
||||||
|
return self._simple_call(
|
||||||
|
call,
|
||||||
|
Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="deep",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=call.column,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def info(self, call: Call) -> Type:
|
||||||
|
def make_overload(memory_usage: Type, required: bool = False) -> Type:
|
||||||
|
return Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="verbose",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="buf",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=2,
|
||||||
|
name="max_cols",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=3,
|
||||||
|
name="memory_usage",
|
||||||
|
type=memory_usage,
|
||||||
|
required=required,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=4,
|
||||||
|
name="show_counts",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=UnitType(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._simple_call(
|
||||||
|
call,
|
||||||
|
OverloadedFunction(
|
||||||
|
overloads=[
|
||||||
|
make_overload(self.types.get_type("bool"), False),
|
||||||
|
make_overload(self.types.get_type("str"), True),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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.Parameter] = [],
|
||||||
|
*,
|
||||||
|
formula: Optional[Callable[[Type], Formula]] = None,
|
||||||
|
) -> Type:
|
||||||
|
"""Compute the result type of an aggregate method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
kwargs (list[Function.Parameter], optional): a list of extra
|
||||||
|
keyword-only parameters. Defaults to [].
|
||||||
|
formula (Callable[[Type], Formula], optional): optional formula
|
||||||
|
builder function to compute the return type. If set, the function
|
||||||
|
should accept the inner column type and return a formula.
|
||||||
|
If `None`, the result is typed as `Column[Any]`. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
|
||||||
|
returns: Type = ColumnType(type=TopType())
|
||||||
|
if formula:
|
||||||
|
returns = ColumnType(
|
||||||
|
type=self._resolve_formula_type(
|
||||||
|
call,
|
||||||
|
formula(call.column.type),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
*kwargs,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=returns,
|
||||||
|
)
|
||||||
|
|
||||||
|
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, formula=lambda t: t)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mean(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call, formula=lambda t: ((t, "__add__", t), "__truediv__", "int")
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def median(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, formula=lambda t: t)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def min(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, formula=lambda t: t)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def mode(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, formula=lambda t: t)
|
||||||
|
|
||||||
|
@method("product", "prod")
|
||||||
|
def product(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, formula=lambda t: (t, "__mul__", t))
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def std(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="ddof",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def sum(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(call, formula=lambda t: (t, "__add__", t))
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def var(self, call: Call) -> Type:
|
||||||
|
return self._aggregate(
|
||||||
|
call,
|
||||||
|
[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="var",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def head(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
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(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
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 sort_values(self, call: Call) -> Type:
|
||||||
|
str_ = self.types.get_type("str")
|
||||||
|
bool_ = self.types.get_type("bool")
|
||||||
|
|
||||||
|
def make_overload(ascending: Type) -> Function:
|
||||||
|
return Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="ascending",
|
||||||
|
type=ascending,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=2,
|
||||||
|
name="inplace",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
unsupported=True,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=3,
|
||||||
|
name="kind",
|
||||||
|
type=str_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=4,
|
||||||
|
name="na_position",
|
||||||
|
type=str_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=5,
|
||||||
|
name="ignore_index",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=6,
|
||||||
|
name="key",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=call.column,
|
||||||
|
)
|
||||||
|
|
||||||
|
list_of = self.types.list_of
|
||||||
|
overloads: list[Type] = [
|
||||||
|
make_overload(bool_),
|
||||||
|
make_overload(bool_),
|
||||||
|
make_overload(list_of(bool_)),
|
||||||
|
make_overload(list_of(bool_)),
|
||||||
|
]
|
||||||
|
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=OverloadedFunction(overloads=overloads),
|
||||||
|
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(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="by",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="level",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=i + 2,
|
||||||
|
name=name,
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
for i, name in enumerate(
|
||||||
|
["as_index", "sort", "group_keys", "observed", "dropna"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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):
|
||||||
|
"""Generate an assertion to check that two columns have the same length
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call_expr (p.Expr): the call expression, to insert the assertion
|
||||||
|
at the right place
|
||||||
|
column1 (p.Expr): the first column expression
|
||||||
|
column2 (p.Expr): the second column expression
|
||||||
|
"""
|
||||||
|
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",
|
||||||
|
)
|
||||||
110
midas/checker/frames/frame_groupby_methods.py
Normal file
110
midas/checker/frames/frame_groupby_methods.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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:
|
||||||
|
"""A frame group-by method call, implements :class:`utils.MethodCall`"""
|
||||||
|
|
||||||
|
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]):
|
||||||
|
"""The method registry for frame group-by types"""
|
||||||
|
|
||||||
|
def _aggregate(self, call: Call, method: str) -> Type:
|
||||||
|
"""Compute the result type of an aggregate method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
method (str): the method's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
|
||||||
|
for column in call.groupby.frame.columns:
|
||||||
|
with self.reporter.with_context(f"in column '{column.name}'"):
|
||||||
|
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")
|
||||||
386
midas/checker/frames/frame_manager.py
Normal file
386
midas/checker/frames/frame_manager.py
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
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]]:
|
||||||
|
"""Check whether the given list only contains literal expressions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exprs (list[p.Expr]): the list to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TypeGuard[list[p.LiteralExpr]]: whether `exprs` only contains literal expressions
|
||||||
|
"""
|
||||||
|
return all(isinstance(expr, p.LiteralExpr) for expr in exprs)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameManager:
|
||||||
|
"""Helper class to handle methods and subscripts on frame types"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Compute the new frame type after assigning a value to an index
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
|
location (Location): the assignment's location
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
index (p.Expr): the index expression
|
||||||
|
value_type (Type): the assigned value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting frame 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:
|
||||||
|
"""Compute the new frame type after assigning a single value to a column
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
|
location (Location): the assignment's location
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
name (str): the column name
|
||||||
|
type (Type): the assigned value type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting frame type
|
||||||
|
"""
|
||||||
|
if not isinstance(type, ColumnType):
|
||||||
|
reporter.error(
|
||||||
|
location,
|
||||||
|
f"Cannot assign {type} to dataframe column. Must be a ColumnType",
|
||||||
|
)
|
||||||
|
return self._set_column(frame, name, ColumnType(type=UnknownType()))
|
||||||
|
return self._set_column(frame, name, type)
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
reporter: FileReporter,
|
||||||
|
location: Location,
|
||||||
|
frame: DataFrameType,
|
||||||
|
index: p.Expr,
|
||||||
|
) -> Type:
|
||||||
|
"""Compute the type of a subscript access
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
|
location (Location): the subscript's location
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
index (p.Expr): the index expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting 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:
|
||||||
|
"""Compute the type of a subscript access on a frame group-by object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
|
location (Location): the subscript's location
|
||||||
|
groupby (FrameGroupBy): the group-by object
|
||||||
|
index (p.Expr): the index expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting 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:
|
||||||
|
"""Set a frame's column to the given type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
name (str): the column's name
|
||||||
|
column (ColumnType): the new column's type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrameType: the new frame type
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Set multiple columns of a frame to the given types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
names (list[str]): the column names
|
||||||
|
columns (list[ColumnType]): the new column types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrameType: the new frame type
|
||||||
|
"""
|
||||||
|
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]:
|
||||||
|
"""Get a column's type by name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
name (str): the column's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[ColumnType]: the column's type, or `None` if it doesn't exist
|
||||||
|
"""
|
||||||
|
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]]:
|
||||||
|
"""Get multiple column types by name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
names (list[str]): the column names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Optional[ColumnType]]: the column types (see :func:`_get_column`)
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Compute the result type of a frame's method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): the method name
|
||||||
|
location (Location): the call's location
|
||||||
|
call_expr (p.Expr): the call expression
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
frame_expr (p.Expr): the frame expression
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result 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:
|
||||||
|
"""Compute the result type of a frame group-by's method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): the method name
|
||||||
|
location (Location): the call's location
|
||||||
|
call_expr (p.Expr): the call expression
|
||||||
|
groupby (FrameGroupBy): the frame group-by object
|
||||||
|
groupby_expr (p.Expr): the frame group-by expression
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result 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]:
|
||||||
|
"""Get the type of a frame's attribute
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the frame type
|
||||||
|
name (str): the attribute's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the attribute's type, or `None` if it doesn't exist
|
||||||
|
"""
|
||||||
|
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
|
||||||
757
midas/checker/frames/frame_methods.py
Normal file
757
midas/checker/frames/frame_methods.py
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
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,
|
||||||
|
ParamSpec,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
UnitType,
|
||||||
|
UnknownType,
|
||||||
|
unfold_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
"""A frame method call, implements :class:`utils.MethodCall`"""
|
||||||
|
|
||||||
|
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]):
|
||||||
|
"""The method registry for frame types"""
|
||||||
|
|
||||||
|
def _simple_call(self, call: Call, function: Type) -> Type:
|
||||||
|
"""Get the result of calling a simple method
|
||||||
|
|
||||||
|
This function is a simple wrapper around :func:`dispatcher.CallDispatcher.get_result`
|
||||||
|
Args:
|
||||||
|
call (Call): the call that triggered this resolution
|
||||||
|
function (Type): the function type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the return type
|
||||||
|
"""
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=function,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
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) -> tuple[Type, bool]:
|
||||||
|
"""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:
|
||||||
|
tuple[Type, bool]: the resulting type and a boolean indicating
|
||||||
|
whether the operand is a frame
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(call.positional) == 0:
|
||||||
|
return UnknownType(), False
|
||||||
|
|
||||||
|
operand: TypedExpr = call.positional[0]
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
|
||||||
|
by_name: dict[str, DataFrameType.Column] = {}
|
||||||
|
frame2: Optional[DataFrameType] = None
|
||||||
|
# Get map of operand's columns by name, if the operand is a dataframe
|
||||||
|
unfolded_other: Type = unfold_type(operand[1])
|
||||||
|
frame_operand: bool = isinstance(unfolded_other, DataFrameType)
|
||||||
|
if frame_operand:
|
||||||
|
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())
|
||||||
|
|
||||||
|
col_type2: Optional[ColumnType] = None
|
||||||
|
|
||||||
|
# Operand is a frame -> lookup column with the same name
|
||||||
|
if frame2 is not None:
|
||||||
|
if column.name in by_name:
|
||||||
|
column2 = by_name[column.name]
|
||||||
|
col_type2 = column2.type
|
||||||
|
|
||||||
|
# Operand is not a frame -> scalar operation -> ad-hoc column
|
||||||
|
else:
|
||||||
|
col_type2 = ColumnType(type=operand[1])
|
||||||
|
|
||||||
|
if col_type2 is not None:
|
||||||
|
with self.reporter.with_context(f"in column '{column.name}'"):
|
||||||
|
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), frame_operand
|
||||||
|
|
||||||
|
def _element_wise(self, call: Call, method: str) -> Type:
|
||||||
|
"""Compute the result of an element-wise method call
|
||||||
|
|
||||||
|
If the call is valid, this method also generates an assertion to check
|
||||||
|
that both operands have the same length at runtime
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
method (str): the method's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
# TODO: support sequence, Series, dict operand
|
||||||
|
returns, frame_operand = self._element_binary_op(call, method)
|
||||||
|
|
||||||
|
# Build signature with new schema and generic operand
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="other",
|
||||||
|
type=TopType(),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=returns,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 and frame_operand:
|
||||||
|
self._assert_same_length(
|
||||||
|
call.call_expr, call.frame_expr, call.positional[0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def copy(self, call: Call) -> Type:
|
||||||
|
return self._simple_call(
|
||||||
|
call,
|
||||||
|
Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="deep",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=call.frame,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def info(self, call: Call) -> Type:
|
||||||
|
def make_overload(memory_usage: Type, required: bool = False) -> Type:
|
||||||
|
return Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="verbose",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="buf",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=2,
|
||||||
|
name="max_cols",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=3,
|
||||||
|
name="memory_usage",
|
||||||
|
type=memory_usage,
|
||||||
|
required=required,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=4,
|
||||||
|
name="show_counts",
|
||||||
|
type=self.types.get_type("bool"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
returns=UnitType(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._simple_call(
|
||||||
|
call,
|
||||||
|
OverloadedFunction(
|
||||||
|
overloads=[
|
||||||
|
make_overload(self.types.get_type("bool"), False),
|
||||||
|
make_overload(self.types.get_type("str"), True),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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.Parameter] = []) -> Type:
|
||||||
|
"""Compute the result type of an aggregate method call
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (Call): the call object
|
||||||
|
kwargs (list[Function.Parameter], optional): a list of extra
|
||||||
|
keyword-only parameters. Defaults to [].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
|
with_axis = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="axis",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
*kwargs,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=ColumnType(type=TopType()),
|
||||||
|
)
|
||||||
|
without_axis = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
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.Parameter(
|
||||||
|
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.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="var",
|
||||||
|
type=self.types.get_type("int"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def head(self, call: Call) -> Type:
|
||||||
|
signature = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
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(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
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 sort_values(self, call: Call) -> Type:
|
||||||
|
str_ = self.types.get_type("str")
|
||||||
|
bool_ = self.types.get_type("bool")
|
||||||
|
|
||||||
|
def make_overload(by: Type, ascending: Type) -> Function:
|
||||||
|
return Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="by",
|
||||||
|
type=by,
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="axis",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=2,
|
||||||
|
name="ascending",
|
||||||
|
type=ascending,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=3,
|
||||||
|
name="inplace",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
unsupported=True,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=4,
|
||||||
|
name="kind",
|
||||||
|
type=str_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=5,
|
||||||
|
name="na_position",
|
||||||
|
type=str_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=6,
|
||||||
|
name="ignore_index",
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=7,
|
||||||
|
name="key",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=call.frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
list_of = self.types.list_of
|
||||||
|
overloads: list[Type] = [
|
||||||
|
make_overload(by=str_, ascending=bool_),
|
||||||
|
make_overload(by=list_of(str_), ascending=bool_),
|
||||||
|
make_overload(by=str_, ascending=list_of(bool_)),
|
||||||
|
make_overload(by=list_of(str_), ascending=list_of(bool_)),
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO: check that literal strings in `by` are valid columns
|
||||||
|
result: CallResult = self.dispatcher.get_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=OverloadedFunction(overloads=overloads),
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
return result.result
|
||||||
|
|
||||||
|
def _filter_groupby_columns(
|
||||||
|
self, frame: DataFrameType, by: TypedExpr
|
||||||
|
) -> DataFrameType:
|
||||||
|
"""Remove columns passed as string literals in groupby's `by` argument
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame (DataFrameType): the original dataframe
|
||||||
|
by (TypedExpr): the by argument
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrameType: the filtered dataframe
|
||||||
|
"""
|
||||||
|
by_columns: list[str] = []
|
||||||
|
|
||||||
|
by_expr, _ = by
|
||||||
|
|
||||||
|
match by_expr:
|
||||||
|
case p.ListExpr(items=items):
|
||||||
|
for item in items:
|
||||||
|
match item:
|
||||||
|
case p.LiteralExpr(value=str() as name):
|
||||||
|
by_columns.append(name)
|
||||||
|
|
||||||
|
case p.LiteralExpr(value=str() as name):
|
||||||
|
by_columns.append(name)
|
||||||
|
|
||||||
|
if len(by_columns) == 0:
|
||||||
|
return frame
|
||||||
|
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
for column in frame.columns:
|
||||||
|
if column.name in by_columns:
|
||||||
|
continue
|
||||||
|
new_columns.append(
|
||||||
|
DataFrameType.Column(
|
||||||
|
index=len(new_columns),
|
||||||
|
name=column.name,
|
||||||
|
type=column.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return DataFrameType(columns=new_columns)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def groupby(self, call: Call) -> Type:
|
||||||
|
new_frame: DataFrameType = call.frame
|
||||||
|
|
||||||
|
by: Optional[TypedExpr] = None
|
||||||
|
if len(call.positional) != 0:
|
||||||
|
by = call.positional[0]
|
||||||
|
elif "by" in call.keywords:
|
||||||
|
by = call.keywords["by"]
|
||||||
|
|
||||||
|
if by is not None:
|
||||||
|
new_frame = self._filter_groupby_columns(call.frame, by)
|
||||||
|
|
||||||
|
bool_: Type = self.types.get_type("bool")
|
||||||
|
function: Function = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="by",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
Function.Parameter(
|
||||||
|
pos=1,
|
||||||
|
name="level",
|
||||||
|
type=TopType(),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=i + 2,
|
||||||
|
name=name,
|
||||||
|
type=bool_,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
for i, name in enumerate(
|
||||||
|
["as_index", "sort", "group_keys", "observed", "dropna"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returns=FrameGroupBy(frame=new_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):
|
||||||
|
"""Generate an assertion to check that two frames have the same length
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call_expr (p.Expr): the call expression, to insert the assertion
|
||||||
|
at the right place
|
||||||
|
frame1 (p.Expr): the first frame expression
|
||||||
|
frame2 (p.Expr): the second frame expression
|
||||||
|
"""
|
||||||
|
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",
|
||||||
|
)
|
||||||
129
midas/checker/frames/utils.py
Normal file
129
midas/checker/frames/utils.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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):
|
||||||
|
"""Meta-class for :class:`MethodRegistry`
|
||||||
|
|
||||||
|
Collects methods marked with the :func:`method` decorator into a dictionary
|
||||||
|
named `_methods` on the class itself
|
||||||
|
"""
|
||||||
|
|
||||||
|
_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):
|
||||||
|
"""A method call object
|
||||||
|
|
||||||
|
Must have at least `location`, `call_expr` and `subject` properties
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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):
|
||||||
|
"""A registry of methods"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Compute the result type of a call to the given method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): the method's name
|
||||||
|
call (T): the call
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result 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]]:
|
||||||
|
"""Simple decorator to mark a method as part of the registry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
names (str): names by which the method can be called. If left empty, the
|
||||||
|
Python method's name will be used
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
@@ -3,42 +3,77 @@ from pathlib import Path
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import midas.ast.midas as m
|
import midas.ast.midas as m
|
||||||
|
from midas.ast.location import Location
|
||||||
from midas.checker.builtins import define_builtins
|
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.registry import TypesRegistry
|
||||||
from midas.checker.reporter import FileReporter, Reporter
|
from midas.checker.reporter import FileReporter, Reporter
|
||||||
from midas.checker.types import (
|
from midas.checker.types import (
|
||||||
AliasType,
|
ColumnType,
|
||||||
ComplexType,
|
ConstraintType,
|
||||||
ExtensionType,
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
Function,
|
Function,
|
||||||
GenericType,
|
GenericType,
|
||||||
|
ParamSpec,
|
||||||
|
Predicate,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
UnknownType,
|
UnknownType,
|
||||||
)
|
)
|
||||||
|
from midas.checker.variance import VarianceInferrer
|
||||||
from midas.lexer.midas import MidasLexer
|
from midas.lexer.midas import MidasLexer
|
||||||
from midas.lexer.token import Token
|
from midas.lexer.token import Token, TokenType
|
||||||
from midas.parser.midas import MidasParser
|
from midas.parser.midas import MidasParser
|
||||||
|
|
||||||
|
|
||||||
class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type]):
|
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"""
|
"""A resolver which evaluates Midas type definitions and build a registry"""
|
||||||
|
|
||||||
def __init__(self, types: TypesRegistry, reporter: Reporter) -> None:
|
def __init__(self, types: TypesRegistry, reporter: Reporter) -> None:
|
||||||
self.logger: logging.Logger = logging.getLogger("MidasTyper")
|
self.logger: logging.Logger = logging.getLogger("MidasTyper")
|
||||||
self.reporter: FileReporter = reporter.for_file(None)
|
self.reporter: FileReporter = reporter.for_file(None)
|
||||||
|
|
||||||
self.types: TypesRegistry = types
|
self.types: TypesRegistry = types
|
||||||
|
self.dispatcher: CallDispatcher[m.Expr] = CallDispatcher[m.Expr](
|
||||||
|
self.types, self.reporter
|
||||||
|
)
|
||||||
|
|
||||||
self._local_variables: dict[str, TypeVar] = {}
|
self._local_variables: dict[str, TypeVar] = {}
|
||||||
|
|
||||||
|
self._predicate_params: dict[str, Type] = {}
|
||||||
|
|
||||||
self._current_name: Optional[str] = None
|
self._current_name: Optional[str] = None
|
||||||
|
|
||||||
define_builtins(self.types)
|
define_builtins(self.types)
|
||||||
builtins_path: Path = (Path(__file__).parent / "builtins.midas").resolve()
|
builtins_path: Path = (Path(__file__).parent / "builtins.midas").resolve()
|
||||||
self.process(builtins_path.read_text(), str(builtins_path))
|
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):
|
||||||
|
"""Set the file reporter to use for diagnostics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter
|
||||||
|
"""
|
||||||
|
self.reporter = reporter
|
||||||
|
self.dispatcher.set_reporter(reporter)
|
||||||
|
|
||||||
def process(self, source: str, path: Optional[str]):
|
def process(self, source: str, path: Optional[str]):
|
||||||
self.reporter = self.reporter.for_file(path)
|
"""Process some Midas source code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source (str): the Midas source code
|
||||||
|
path (Optional[str]): the path of the source file, if known
|
||||||
|
"""
|
||||||
|
reporter: FileReporter = self.reporter.for_file(path)
|
||||||
|
self.set_reporter(reporter)
|
||||||
|
|
||||||
lexer: MidasLexer = MidasLexer(source)
|
lexer: MidasLexer = MidasLexer(source)
|
||||||
tokens: list[Token] = lexer.process()
|
tokens: list[Token] = lexer.process()
|
||||||
parser: MidasParser = MidasParser(tokens)
|
parser: MidasParser = MidasParser(tokens)
|
||||||
@@ -47,6 +82,18 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
self.reporter.error(error.token.get_location(), error.message)
|
self.reporter.error(error.token.get_location(), error.message)
|
||||||
self.resolve(stmts)
|
self.resolve(stmts)
|
||||||
|
|
||||||
|
def type_of(self, expr: m.Expr) -> Type:
|
||||||
|
"""Compute the type of the given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type of the expression
|
||||||
|
"""
|
||||||
|
type: Type = expr.accept(self)
|
||||||
|
return type
|
||||||
|
|
||||||
def get_type(self, name: str) -> Type:
|
def get_type(self, name: str) -> Type:
|
||||||
"""Get a type from its name
|
"""Get a type from its name
|
||||||
|
|
||||||
@@ -63,6 +110,34 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
return self._local_variables[name]
|
return self._local_variables[name]
|
||||||
return self.types.get_type(name)
|
return self.types.get_type(name)
|
||||||
|
|
||||||
|
def get_variable(self, name: str) -> Type:
|
||||||
|
"""Get the type of a variable
|
||||||
|
|
||||||
|
This function will first look into the current predicate's parameters if
|
||||||
|
we are in a predicate definition.
|
||||||
|
The the variable is looked up in the preamble (i.e. global environment)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NameError: if the variable cannot be found
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type of the variable
|
||||||
|
"""
|
||||||
|
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]):
|
def resolve(self, stmts: list[m.Stmt]):
|
||||||
"""Process a sequence of statements
|
"""Process a sequence of statements
|
||||||
|
|
||||||
@@ -72,6 +147,21 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
for stmt in stmts:
|
for stmt in stmts:
|
||||||
stmt.accept(self)
|
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):
|
||||||
|
"""Check that the given expression is a subtype of `bool` or report an error
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to check
|
||||||
|
"""
|
||||||
|
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:
|
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||||
name: str = stmt.name.lexeme
|
name: str = stmt.name.lexeme
|
||||||
self._current_name = name
|
self._current_name = name
|
||||||
@@ -81,11 +171,18 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
if len(params) != 0:
|
if len(params) != 0:
|
||||||
type = GenericType(name=name, params=params, body=type)
|
type = GenericType(name=name, params=params, body=type)
|
||||||
else:
|
else:
|
||||||
type = AliasType(name=name, type=type)
|
type = DerivedType(name=name, type=type)
|
||||||
self.types.define_type(name, type)
|
self.types.define_type(name, type)
|
||||||
self._local_variables.clear()
|
self._local_variables.clear()
|
||||||
self._current_name = None
|
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_member_stmt(self, stmt: m.MemberStmt) -> None: ...
|
||||||
|
|
||||||
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None:
|
||||||
@@ -102,35 +199,180 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
base_name,
|
base_name,
|
||||||
member.name.lexeme,
|
member.name.lexeme,
|
||||||
member_type,
|
member_type,
|
||||||
member.kind == m.MemberKind.METHOD,
|
member.kind,
|
||||||
)
|
)
|
||||||
|
|
||||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||||
self.reporter.warning(stmt.location, "PredicateStmt not yet supported")
|
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)
|
||||||
|
|
||||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
|
type: Type = self.type_of(stmt.body)
|
||||||
self.reporter.warning(expr.location, "LogicalExpr not yet supported")
|
params: list[ParamSpec] = [self._visit_param_spec(spec) for spec in stmt.params]
|
||||||
|
|
||||||
def visit_binary_expr(self, expr: m.BinaryExpr) -> None:
|
if not self._is_valid_predicate(type):
|
||||||
self.reporter.warning(expr.location, "BinaryExpr not yet supported")
|
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(
|
||||||
|
params=spec,
|
||||||
|
returns=type,
|
||||||
|
)
|
||||||
|
self._predicate_params = {}
|
||||||
|
self.types.define_predicate(
|
||||||
|
stmt.name.lexeme,
|
||||||
|
Predicate(
|
||||||
|
type=type,
|
||||||
|
body=stmt.body,
|
||||||
|
alias=len(params) == 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def visit_unary_expr(self, expr: m.UnaryExpr) -> None:
|
def _is_valid_predicate(self, body: Type) -> bool:
|
||||||
self.reporter.warning(expr.location, "UnaryExpr not yet supported")
|
"""Check whether the given type is valid as a predicate's body
|
||||||
|
|
||||||
def visit_get_expr(self, expr: m.GetExpr) -> None:
|
Accepted types are either subtypes of `bool` or valid predicates
|
||||||
self.reporter.warning(expr.location, "GetExpr not yet supported")
|
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: m.VariableExpr) -> None:
|
Args:
|
||||||
self.reporter.warning(expr.location, "VariableExpr not yet supported")
|
body (Type): the potential predicate body
|
||||||
|
|
||||||
def visit_grouping_expr(self, expr: m.GroupingExpr) -> None:
|
Returns:
|
||||||
|
bool: `True` if `body` can be a predicate body, `False` otherwise
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
# Special case because there is no __not__ dunder method
|
||||||
|
match expr.operator:
|
||||||
|
case Token(type=TokenType.BANG):
|
||||||
|
return self.types.get_type("bool")
|
||||||
|
|
||||||
|
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)
|
return expr.expr.accept(self)
|
||||||
|
|
||||||
def visit_literal_expr(self, expr: m.LiteralExpr) -> None:
|
def visit_literal_expr(self, expr: m.LiteralExpr) -> Type:
|
||||||
self.reporter.warning(expr.location, "LiteralExpr not yet supported")
|
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) -> None:
|
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Type:
|
||||||
self.reporter.warning(expr.location, "WildcardExpr not yet supported")
|
return self.get_variable("_")
|
||||||
|
|
||||||
def visit_named_type(self, type: m.NamedType) -> Type:
|
def visit_named_type(self, type: m.NamedType) -> Type:
|
||||||
name: str = type.name.lexeme
|
name: str = type.name.lexeme
|
||||||
@@ -144,6 +386,16 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
return UnknownType()
|
return UnknownType()
|
||||||
|
|
||||||
def visit_generic_type(self, type: m.GenericType) -> Type:
|
def visit_generic_type(self, type: m.GenericType) -> Type:
|
||||||
|
match type.type:
|
||||||
|
case m.NamedType(name=Token(lexeme="Column")):
|
||||||
|
if len(type.args) != 1:
|
||||||
|
self.reporter.error(
|
||||||
|
type.location,
|
||||||
|
f"Column requires 1 type argument, {len(type.args)} provided",
|
||||||
|
)
|
||||||
|
return ColumnType(type=UnknownType())
|
||||||
|
return ColumnType(type=type.args[0].accept(self))
|
||||||
|
|
||||||
type_: Type = type.type.accept(self)
|
type_: Type = type.type.accept(self)
|
||||||
args: list[Type] = [arg.accept(self) for arg in type.args]
|
args: list[Type] = [arg.accept(self) for arg in type.args]
|
||||||
try:
|
try:
|
||||||
@@ -153,44 +405,62 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
|
|||||||
return UnknownType()
|
return UnknownType()
|
||||||
|
|
||||||
def visit_constraint_type(self, type: m.ConstraintType) -> Type:
|
def visit_constraint_type(self, type: m.ConstraintType) -> Type:
|
||||||
type_: Type = type.type.accept(self)
|
base_type: Type = type.type.accept(self)
|
||||||
type.constraint.accept(self)
|
self._predicate_params["_"] = base_type
|
||||||
# TODO
|
constraint_type: Type = self.type_of(type.constraint)
|
||||||
return UnknownType()
|
self._predicate_params = {}
|
||||||
|
if not self.types.is_subtype(constraint_type, self._bool):
|
||||||
|
self.reporter.error(
|
||||||
|
type.location,
|
||||||
|
f"Constraint must evaluate to a boolean, got {constraint_type}",
|
||||||
|
)
|
||||||
|
|
||||||
def visit_complex_type(self, type: m.ComplexType) -> ComplexType:
|
return ConstraintType(
|
||||||
return ComplexType(
|
type=base_type,
|
||||||
members={
|
constraint=type.constraint,
|
||||||
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:
|
def visit_function_type(self, type: m.FunctionType) -> Type:
|
||||||
n_pos_args: int = len(type.pos_args)
|
return Function(
|
||||||
n_args: int = len(type.args)
|
params=self._visit_param_spec(type.params),
|
||||||
|
returns=type.returns.accept(self),
|
||||||
|
)
|
||||||
|
|
||||||
def process_arg(arg: m.FunctionType.Argument, i: int) -> Function.Argument:
|
def _visit_param_spec(self, spec: m.ParamSpec) -> ParamSpec:
|
||||||
return Function.Argument(
|
n_pos: int = len(spec.pos)
|
||||||
|
n_mixed: int = len(spec.mixed)
|
||||||
|
|
||||||
|
def process_param(
|
||||||
|
param: m.FunctionType.Parameter, i: int
|
||||||
|
) -> Function.Parameter:
|
||||||
|
return Function.Parameter(
|
||||||
pos=i,
|
pos=i,
|
||||||
name=arg.name.lexeme if arg.name is not None else str(i),
|
name=param.name.lexeme if param.name is not None else str(i),
|
||||||
type=arg.type.accept(self),
|
type=param.type.accept(self),
|
||||||
required=arg.required,
|
required=param.required,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Function(
|
return ParamSpec(
|
||||||
pos_args=[process_arg(arg, i) for i, arg in enumerate(type.pos_args)],
|
pos=[process_param(param, i) for i, param in enumerate(spec.pos)],
|
||||||
args=[process_arg(arg, i + n_pos_args) for i, arg in enumerate(type.args)],
|
mixed=[
|
||||||
kw_args=[
|
process_param(param, i + n_pos) for i, param in enumerate(spec.mixed)
|
||||||
process_arg(arg, i + n_pos_args + n_args)
|
|
||||||
for i, arg in enumerate(type.kw_args)
|
|
||||||
],
|
],
|
||||||
returns=type.returns.accept(self),
|
kw=[
|
||||||
|
process_param(param, i + n_pos + n_mixed)
|
||||||
|
for i, param 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]):
|
def _resolve_type_params(self, params: list[m.TypeParam]):
|
||||||
|
|||||||
@@ -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__",
|
||||||
@@ -30,9 +32,40 @@ COMPARATOR_METHODS: dict[Type[ast.cmpop], str] = {
|
|||||||
# ast.NotIn: "__notin__",
|
# ast.NotIn: "__notin__",
|
||||||
}
|
}
|
||||||
|
|
||||||
UNARY_METHODS: dict[Type[ast.unaryop], str] = {
|
PY_UNARY_METHODS: dict[Type[ast.unaryop], str] = {
|
||||||
ast.Invert: "__invert__",
|
ast.Invert: "__invert__",
|
||||||
# ast.Not: "",
|
# ast.Not: "",
|
||||||
ast.UAdd: "__pos__",
|
ast.UAdd: "__pos__",
|
||||||
ast.USub: "__neg__",
|
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__",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from midas.checker.environment import Environment
|
from midas.checker.environment import Environment
|
||||||
from midas.checker.registry import TypesRegistry
|
from midas.checker.registry import TypesRegistry
|
||||||
from midas.checker.types import Function, GenericType, TopType, Type, TypeVar, UnitType
|
from midas.checker.types import (
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
ParamSpec,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -13,26 +23,32 @@ class Param:
|
|||||||
|
|
||||||
|
|
||||||
class Preamble(Environment):
|
class Preamble(Environment):
|
||||||
|
"""The initial environment containing some of Python's builtin functions"""
|
||||||
|
|
||||||
def __init__(self, types: TypesRegistry) -> None:
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._types: TypesRegistry = types
|
self._types: TypesRegistry = types
|
||||||
|
self._python_funcs: dict[str, Callable[..., Any]] = {}
|
||||||
|
|
||||||
self._def_type_constructor("object")
|
self.define("__name__", self._types.get_type("str"))
|
||||||
self._def_type_constructor("float")
|
self._def_type_constructor("object", object)
|
||||||
self._def_type_constructor("int")
|
self._def_type_constructor("float", float)
|
||||||
self._def_type_constructor("bool")
|
self._def_type_constructor("int", int)
|
||||||
self._def_type_constructor("str")
|
self._def_type_constructor("bool", bool)
|
||||||
|
self._def_type_constructor("str", str)
|
||||||
self._def_function(
|
self._def_function(
|
||||||
name="list",
|
name="list",
|
||||||
pos=[Param("object", TopType())],
|
pos=[Param("object", TopType())],
|
||||||
returns=self._list_of(TopType()),
|
returns=self._list_of(TopType()),
|
||||||
|
py_function=list,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: use sink
|
# TODO: use sink
|
||||||
self._def_function(
|
self._def_function(
|
||||||
name="print",
|
name="print",
|
||||||
pos=[Param("object", TopType())],
|
pos=[Param("object", TopType(), required=False)],
|
||||||
returns=UnitType(),
|
returns=UnitType(),
|
||||||
|
py_function=print,
|
||||||
)
|
)
|
||||||
|
|
||||||
map_in = TypeVar(name="T", bound=None)
|
map_in = TypeVar(name="T", bound=None)
|
||||||
@@ -52,17 +68,62 @@ class Preamble(Environment):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
returns=self._list_of(map_out), # TODO: replace with Iterable[U]
|
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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _list_of(self, item_type: Type) -> Type:
|
T = TypeVar(name="T", bound=None)
|
||||||
return self._types.apply_generic(self._types.get_type("list"), [item_type])
|
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 _def_type_constructor(self, name: str):
|
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
|
# TODO: more specific arg types
|
||||||
self._def_function(
|
self._def_function(
|
||||||
name=name,
|
name=name,
|
||||||
pos=[Param("object", TopType(), required=False)],
|
pos=[Param("object", TopType(), required=False)],
|
||||||
returns=self._types.get_type(name),
|
returns=self._types.get_type(name),
|
||||||
|
py_function=py_function,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_function(
|
def _make_function(
|
||||||
@@ -75,9 +136,9 @@ class Preamble(Environment):
|
|||||||
returns: Type = UnitType(),
|
returns: Type = UnitType(),
|
||||||
type_vars: list[TypeVar] = [],
|
type_vars: list[TypeVar] = [],
|
||||||
) -> Type:
|
) -> Type:
|
||||||
def map_args(params: list[Param], offset: int) -> list[Function.Argument]:
|
def map_params(params: list[Param], offset: int) -> list[Function.Parameter]:
|
||||||
return [
|
return [
|
||||||
Function.Argument(
|
Function.Parameter(
|
||||||
pos=i + offset,
|
pos=i + offset,
|
||||||
name=param.name,
|
name=param.name,
|
||||||
type=param.type,
|
type=param.type,
|
||||||
@@ -87,9 +148,11 @@ class Preamble(Environment):
|
|||||||
]
|
]
|
||||||
|
|
||||||
function = Function(
|
function = Function(
|
||||||
pos_args=map_args(pos, 0),
|
params=ParamSpec(
|
||||||
args=map_args(mixed, len(pos)),
|
pos=map_params(pos, 0),
|
||||||
kw_args=map_args(kw, len(pos) + len(mixed)),
|
mixed=map_params(mixed, len(pos)),
|
||||||
|
kw=map_params(kw, len(pos) + len(mixed)),
|
||||||
|
),
|
||||||
returns=returns,
|
returns=returns,
|
||||||
)
|
)
|
||||||
if len(type_vars) != 0:
|
if len(type_vars) != 0:
|
||||||
@@ -109,6 +172,7 @@ class Preamble(Environment):
|
|||||||
kw: list[Param] = [],
|
kw: list[Param] = [],
|
||||||
returns: Type = UnitType(),
|
returns: Type = UnitType(),
|
||||||
type_vars: list[TypeVar] = [],
|
type_vars: list[TypeVar] = [],
|
||||||
|
py_function: Optional[Callable[..., Any]] = None,
|
||||||
):
|
):
|
||||||
function: Type = self._make_function(
|
function: Type = self._make_function(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -119,3 +183,34 @@ class Preamble(Environment):
|
|||||||
type_vars=type_vars,
|
type_vars=type_vars,
|
||||||
)
|
)
|
||||||
self.define(name, function)
|
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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,46 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.ast.midas import MemberKind
|
||||||
from midas.checker.builtins import BUILTIN_SUBTYPES
|
from midas.checker.builtins import BUILTIN_SUBTYPES
|
||||||
from midas.checker.types import (
|
from midas.checker.types import (
|
||||||
AliasType,
|
|
||||||
AppliedType,
|
AppliedType,
|
||||||
BaseType,
|
BaseType,
|
||||||
ComplexType,
|
ColumnType,
|
||||||
ExtensionType,
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
Function,
|
Function,
|
||||||
GenericType,
|
GenericType,
|
||||||
OverloadedFunction,
|
OverloadedFunction,
|
||||||
|
Predicate,
|
||||||
TopType,
|
TopType,
|
||||||
|
TupleType,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
UnknownType,
|
UnknownType,
|
||||||
|
Variance,
|
||||||
substitute_typevars,
|
substitute_typevars,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Member:
|
||||||
|
"""A member of a type (property or method)"""
|
||||||
|
|
||||||
|
kind: MemberKind
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
|
||||||
class TypesRegistry:
|
class TypesRegistry:
|
||||||
|
"""A registry of types, type members and predicates"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.logger: logging.Logger = logging.getLogger("TypesRegistry")
|
self.logger: logging.Logger = logging.getLogger("TypesRegistry")
|
||||||
self._types: dict[str, Type] = {}
|
self._types: dict[str, Type] = {}
|
||||||
self._members: dict[str, dict[str, Type]] = {}
|
self._members: dict[str, dict[str, Member]] = {}
|
||||||
|
self._predicates: dict[str, Predicate] = {}
|
||||||
|
|
||||||
def get_type(self, name: str) -> Type:
|
def get_type(self, name: str) -> Type:
|
||||||
"""Get a type from its name
|
"""Get a type from its name
|
||||||
@@ -60,26 +77,89 @@ class TypesRegistry:
|
|||||||
return type
|
return type
|
||||||
|
|
||||||
def define_member(
|
def define_member(
|
||||||
self, type_name: str, member_name: str, member_type: Type, is_method: bool
|
self,
|
||||||
|
type_name: str,
|
||||||
|
member_name: str,
|
||||||
|
member_type: Type,
|
||||||
|
kind: MemberKind,
|
||||||
):
|
):
|
||||||
members: dict[str, Type] = self._members.setdefault(type_name, {})
|
"""Define a member on a type
|
||||||
|
|
||||||
|
If the member is a method and a member with the same name is already
|
||||||
|
defined on the given type, the two are combined into an :class:`OverloadedFunction`.
|
||||||
|
|
||||||
|
If the member is a property and a member with the same name is already
|
||||||
|
defined on the given type, the new definition is dropped and an error
|
||||||
|
is reported.
|
||||||
|
|
||||||
|
In any case, if a member with the same name but a different kind is
|
||||||
|
already defined on the given type, the new definition is dropped and
|
||||||
|
an error is reported.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name (str): the name of the type on which the member is defined
|
||||||
|
member_name (str): the name of the new member
|
||||||
|
member_type (Type): the type of the new member
|
||||||
|
kind (MemberKind): the kind of member to define (property or method)
|
||||||
|
"""
|
||||||
|
members: dict[str, Member] = self._members.setdefault(type_name, {})
|
||||||
if member_name in members:
|
if member_name in members:
|
||||||
if not is_method:
|
current: Member = members[member_name]
|
||||||
|
if current.kind != kind:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Member '{member_name}' already defined for type {type_name}"
|
f"Member '{member_name}' is already defined as a {current.kind},"
|
||||||
|
+ f" cannot define a {kind} with the same name"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
current: Type = members[member_name]
|
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
|
combined: Type
|
||||||
match current:
|
match current.type:
|
||||||
case OverloadedFunction(overloads=overloads):
|
case OverloadedFunction(overloads=overloads):
|
||||||
combined = OverloadedFunction(overloads=overloads + [member_type])
|
combined = OverloadedFunction(overloads=overloads + [member_type])
|
||||||
case _:
|
case _:
|
||||||
combined = OverloadedFunction(overloads=[current, member_type])
|
combined = OverloadedFunction(overloads=[current.type, member_type])
|
||||||
members[member_name] = combined
|
members[member_name] = Member(kind=current.kind, type=combined)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
members[member_name] = member_type
|
members[member_name] = Member(kind=kind, type=member_type)
|
||||||
|
|
||||||
|
def define_predicate(self, name: str, predicate: Predicate):
|
||||||
|
"""Define a predicate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the new predicate
|
||||||
|
predicate (Predicate): the predicate to define
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if a predicate with the same name is already defined
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Check whether a type is a subtype of another base on builtin subtype rules
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name1 (str): the name of the potential subtype
|
||||||
|
name2 (str): the name of the potential supertype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: _description_
|
||||||
|
"""
|
||||||
|
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:
|
def is_subtype(self, type1: Type, type2: Type) -> bool:
|
||||||
"""Check whether `type1` is a subtype of `type2`
|
"""Check whether `type1` is a subtype of `type2`
|
||||||
@@ -101,30 +181,85 @@ class TypesRegistry:
|
|||||||
case (_, TopType()):
|
case (_, TopType()):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
case (AliasType(type=base1), _):
|
case (_, UnknownType()):
|
||||||
return self.is_subtype(base1, type2)
|
|
||||||
|
|
||||||
case (BaseType(name=name1), BaseType(name=name2)):
|
|
||||||
return name1 in BUILTIN_SUBTYPES.get(name2, set())
|
|
||||||
|
|
||||||
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
|
return True
|
||||||
|
|
||||||
case (Function(), Function()):
|
|
||||||
return self.is_func_subtype(type1, type2)
|
|
||||||
|
|
||||||
case (TypeVar(bound=bound), _):
|
case (TypeVar(bound=bound), _):
|
||||||
if bound is None:
|
if bound is None:
|
||||||
return False
|
return False
|
||||||
return self.is_subtype(bound, type2)
|
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 (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
|
return False
|
||||||
|
|
||||||
|
def are_equivalent(self, type1: Type, type2: Type) -> bool:
|
||||||
|
"""Check whether two types are equivalent (T <: S and S <: T)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type1 (Type): the first type
|
||||||
|
type2 (Type): the second type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether `type1` is a subtype and a supertype of `type2`
|
||||||
|
"""
|
||||||
|
return self.is_subtype(type1, type2) and self.is_subtype(type2, type1)
|
||||||
|
|
||||||
# TODO: verify the logic in here
|
# TODO: verify the logic in here
|
||||||
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
||||||
"""Check whether a function is a subtype of another
|
"""Check whether a function is a subtype of another
|
||||||
@@ -139,101 +274,121 @@ class TypesRegistry:
|
|||||||
if not self.is_subtype(func1.returns, func2.returns):
|
if not self.is_subtype(func1.returns, func2.returns):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pos1: list[Function.Argument] = func1.pos_args
|
pos1: list[Function.Parameter] = func1.params.pos
|
||||||
mixed1: list[Function.Argument] = func1.args
|
mixed1: list[Function.Parameter] = func1.params.mixed
|
||||||
kw1: dict[str, Function.Argument] = {a.name: a for a in func1.kw_args}
|
kw1: dict[str, Function.Parameter] = {
|
||||||
pos2: list[Function.Argument] = func2.pos_args
|
param.name: param for param in func1.params.kw
|
||||||
mixed2: list[Function.Argument] = func2.args
|
}
|
||||||
kw2: dict[str, Function.Argument] = {a.name: a for a in func2.kw_args}
|
pos2: list[Function.Parameter] = func2.params.pos
|
||||||
|
mixed2: list[Function.Parameter] = func2.params.mixed
|
||||||
|
kw2: dict[str, Function.Parameter] = {
|
||||||
|
param.name: param for param in func2.params.kw
|
||||||
|
}
|
||||||
|
|
||||||
mixed_by_pos: dict[int, Function.Argument] = {arg.pos: arg for arg in mixed2}
|
mixed_by_pos: dict[int, Function.Parameter] = {
|
||||||
mixed_by_name: dict[str, Function.Argument] = {arg.name: arg for arg in mixed2}
|
param.pos: param for param in mixed2
|
||||||
|
}
|
||||||
|
mixed_by_name: dict[str, Function.Parameter] = {
|
||||||
|
param.name: param for param in mixed2
|
||||||
|
}
|
||||||
|
|
||||||
def is_arg_subtype(sub: Function.Argument, sup: Function.Argument) -> bool:
|
def is_arg_subtype(sub: Function.Parameter, sup: Function.Parameter) -> bool:
|
||||||
if not self.is_subtype(sub.type, sup.type):
|
if not self.is_subtype(sub.type, sup.type):
|
||||||
return False
|
return False
|
||||||
if not sup.required and sub.required:
|
if not sup.required and sub.required:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for arg1 in pos1:
|
for param1 in pos1:
|
||||||
arg2: Function.Argument
|
param2: Function.Parameter
|
||||||
if arg1.pos < len(pos2):
|
if param1.pos < len(pos2):
|
||||||
arg2 = pos2[arg1.pos]
|
param2 = pos2[param1.pos]
|
||||||
elif arg1.pos in mixed_by_pos:
|
elif param1.pos in mixed_by_pos:
|
||||||
arg2 = mixed_by_pos[arg1.pos]
|
param2 = mixed_by_pos[param1.pos]
|
||||||
elif not arg1.required:
|
elif not param1.required:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
if not is_arg_subtype(arg2, arg1):
|
if not is_arg_subtype(param2, param1):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for name, arg1 in kw1.items():
|
for name, param1 in kw1.items():
|
||||||
arg2: Function.Argument
|
param2: Function.Parameter
|
||||||
if name in kw2:
|
if name in kw2:
|
||||||
arg2 = kw2[name]
|
param2 = kw2[name]
|
||||||
elif name in mixed_by_name:
|
elif name in mixed_by_name:
|
||||||
arg2 = mixed_by_name[name]
|
param2 = mixed_by_name[name]
|
||||||
elif not arg1.required:
|
elif not param1.required:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
if not is_arg_subtype(arg2, arg1):
|
if not is_arg_subtype(param2, param1):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for arg1 in mixed1:
|
for param1 in mixed1:
|
||||||
pos_arg2: Optional[Function.Argument] = None
|
pos_param2: Optional[Function.Parameter] = None
|
||||||
kw_arg2: Optional[Function.Argument] = None
|
kw_param2: Optional[Function.Parameter] = None
|
||||||
if arg1.name in kw2:
|
if param1.name in kw2:
|
||||||
kw_arg2 = kw2[arg1.name]
|
kw_param2 = kw2[param1.name]
|
||||||
elif arg1.name in mixed_by_name:
|
elif param1.name in mixed_by_name:
|
||||||
kw_arg2 = mixed_by_name[arg1.name]
|
kw_param2 = mixed_by_name[param1.name]
|
||||||
if arg1.pos < len(pos2):
|
if param1.pos < len(pos2):
|
||||||
pos_arg2 = pos2[arg1.pos]
|
pos_param2 = pos2[param1.pos]
|
||||||
elif arg1.pos in mixed_by_pos:
|
elif param1.pos in mixed_by_pos:
|
||||||
pos_arg2 = mixed_by_pos[arg1.pos]
|
pos_param2 = mixed_by_pos[param1.pos]
|
||||||
|
|
||||||
# No match in func2 and arg is required
|
# No match in func2 and arg is required
|
||||||
if pos_arg2 is None and kw_arg2 is None and arg1.required:
|
if pos_param2 is None and kw_param2 is None and param1.required:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Matching keyword argument
|
# Matching keyword argument
|
||||||
if kw_arg2 is not None and not is_arg_subtype(kw_arg2, arg1):
|
if kw_param2 is not None and not is_arg_subtype(kw_param2, param1):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Matching positional argument
|
# Matching positional argument
|
||||||
if pos_arg2 is not None and not is_arg_subtype(pos_arg2, arg1):
|
if pos_param2 is not None and not is_arg_subtype(pos_param2, param1):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
mixed_positions: set[int] = {a.pos for a in mixed1}
|
mixed_positions: set[int] = {param.pos for param in mixed1}
|
||||||
mixed_names: set[str] = {a.name for a in mixed1}
|
mixed_names: set[str] = {param.name for param in mixed1}
|
||||||
for arg2 in pos2:
|
for param2 in pos2:
|
||||||
if not arg2.required:
|
if not param2.required:
|
||||||
continue
|
continue
|
||||||
if arg2.pos >= len(pos1) and arg2.pos not in mixed_positions:
|
if param2.pos >= len(pos1) and param2.pos not in mixed_positions:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for name, arg2 in kw2.items():
|
for name, param2 in kw2.items():
|
||||||
if not arg2.required:
|
if not param2.required:
|
||||||
continue
|
continue
|
||||||
if name not in kw1 and name not in mixed_names:
|
if name not in kw1 and name not in mixed_names:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for arg2 in mixed2:
|
for param2 in mixed2:
|
||||||
if arg2.required:
|
if param2.required:
|
||||||
continue
|
continue
|
||||||
pos_match: bool = arg2.pos < len(pos1) or arg2.pos in mixed_positions
|
pos_match: bool = param2.pos < len(pos1) or param2.pos in mixed_positions
|
||||||
kw_match: bool = arg2.name in kw1 or arg2.name in mixed_names
|
kw_match: bool = param2.name in kw1 or param2.name in mixed_names
|
||||||
if not pos_match or not kw_match:
|
if not pos_match or not kw_match:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def apply_generic(self, type: Type, args: list[Type]) -> Type:
|
def apply_generic(self, type: Type, args: list[Type]) -> Type:
|
||||||
|
"""Instantiate a generic type with the given type arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the generic
|
||||||
|
args (list[Type]): the type arguments
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the arguments are invalid (wrong number, bound violation)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the applied generic type
|
||||||
|
"""
|
||||||
match type:
|
match type:
|
||||||
case AliasType(name=name, type=base):
|
case DerivedType(name=name, type=base):
|
||||||
return AliasType(name=name, type=self.apply_generic(base, args))
|
return DerivedType(name=name, type=self.apply_generic(base, args))
|
||||||
|
|
||||||
case GenericType(name=name, params=type_vars, body=body):
|
case GenericType(name=name, params=type_vars, body=body):
|
||||||
n_args: int = len(args)
|
n_args: int = len(args)
|
||||||
@@ -261,6 +416,9 @@ class TypesRegistry:
|
|||||||
body=substitute_typevars(body, substitutions),
|
body=substitute_typevars(body, substitutions),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case BaseType(name="tuple"):
|
||||||
|
return TupleType(items=tuple(args))
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"{type} is not a generic type")
|
raise ValueError(f"{type} is not a generic type")
|
||||||
|
|
||||||
@@ -293,17 +451,30 @@ class TypesRegistry:
|
|||||||
return [types[i] for i in keep]
|
return [types[i] for i in keep]
|
||||||
|
|
||||||
def lookup_member(self, type: Type, member_name: str) -> Optional[Type]:
|
def lookup_member(self, type: Type, member_name: str) -> Optional[Type]:
|
||||||
|
"""Lookup a member by name on a given type
|
||||||
|
|
||||||
|
This function first looks up directly on the specified type, then
|
||||||
|
recurse through supertypes until it finds the member or reaches
|
||||||
|
the root type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type on which to lookup the member
|
||||||
|
member_name (str): the member's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the member's type, or `None` if it is not defined
|
||||||
|
"""
|
||||||
match type:
|
match type:
|
||||||
case BaseType(name=name):
|
case BaseType(name=name):
|
||||||
if name in self._members:
|
if name in self._members:
|
||||||
if member_name in self._members[name]:
|
if member_name in self._members[name]:
|
||||||
return self._members[name][member_name]
|
return self._members[name][member_name].type
|
||||||
return None
|
return None
|
||||||
|
|
||||||
case AliasType(name=name, type=base):
|
case DerivedType(name=name, type=base):
|
||||||
if name in self._members:
|
if name in self._members:
|
||||||
if member_name in self._members[name]:
|
if member_name in self._members[name]:
|
||||||
return self._members[name][member_name]
|
return self._members[name][member_name].type
|
||||||
return self.lookup_member(base, member_name)
|
return self.lookup_member(base, member_name)
|
||||||
|
|
||||||
case AppliedType(name=name, body=body, args=args):
|
case AppliedType(name=name, body=body, args=args):
|
||||||
@@ -317,7 +488,7 @@ class TypesRegistry:
|
|||||||
}
|
}
|
||||||
if name in self._members:
|
if name in self._members:
|
||||||
if member_name in self._members[name]:
|
if member_name in self._members[name]:
|
||||||
member_type: Type = self._members[name][member_name]
|
member_type: Type = self._members[name][member_name].type
|
||||||
return substitute_typevars(member_type, substitutions)
|
return substitute_typevars(member_type, substitutions)
|
||||||
|
|
||||||
member_type2: Optional[Type] = self.lookup_member(body, member_name)
|
member_type2: Optional[Type] = self.lookup_member(body, member_name)
|
||||||
@@ -325,23 +496,89 @@ class TypesRegistry:
|
|||||||
member_type2 = substitute_typevars(member_type2, substitutions)
|
member_type2 = substitute_typevars(member_type2, substitutions)
|
||||||
return member_type2
|
return member_type2
|
||||||
|
|
||||||
case ComplexType(members=members):
|
case ConstraintType(type=base):
|
||||||
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)
|
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():
|
case UnknownType():
|
||||||
return UnknownType()
|
return UnknownType()
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
self.logger.debug(f"Can't get member on {type}")
|
self.logger.debug(f"Can't get member on {type}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def lookup_predicate(self, name: str) -> Optional[Predicate]:
|
||||||
|
"""Lookup a predicate by name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the predicate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Predicate]: the predicate, or `None` if is not defined
|
||||||
|
"""
|
||||||
|
return self._predicates.get(name)
|
||||||
|
|
||||||
|
def _by_name_or_type(self, name_or_type: str | Type) -> Type:
|
||||||
|
"""Get a type by name or return it as is
|
||||||
|
|
||||||
|
If `name_or_type` is a string, the associated type is looked up and returned.
|
||||||
|
Otherwise, the type is returned as is.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name_or_type (str | Type): the type or type's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the 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:
|
||||||
|
"""Helper method to type a list of a given item type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_type (str | Type): the item type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the list 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:
|
||||||
|
"""Helper method to type a tuple of given item types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_type (str | Type): the item types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the tuple 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:
|
||||||
|
"""Helper method to type a dict of given key and value types
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key_type (str | Type): the key type
|
||||||
|
value_type (str | Type): the value type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the dict 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),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
@@ -7,6 +8,8 @@ from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
|||||||
|
|
||||||
|
|
||||||
class Reporter:
|
class Reporter:
|
||||||
|
"""Helper class to store diagnostics"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.diagnostics: list[Diagnostic] = []
|
self.diagnostics: list[Diagnostic] = []
|
||||||
|
|
||||||
@@ -17,6 +20,14 @@ class Reporter:
|
|||||||
location: Location,
|
location: Location,
|
||||||
message: str,
|
message: str,
|
||||||
):
|
):
|
||||||
|
"""Create and record a diagnostic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Optional[str]): the path linked to this diagnostic
|
||||||
|
type (DiagnosticType): the type of diagnostic
|
||||||
|
location (Location): the location if the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
self.diagnostics.append(
|
self.diagnostics.append(
|
||||||
Diagnostic(
|
Diagnostic(
|
||||||
file_path=path,
|
file_path=path,
|
||||||
@@ -27,21 +38,63 @@ class Reporter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def for_file(self, path: Optional[str]) -> FileReporter:
|
def for_file(self, path: Optional[str]) -> FileReporter:
|
||||||
|
"""Create a new file reporter for the given path using this reporter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Optional[str]): the path for the new file reporter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FileReporter: the new file reporter, linked to this reporter
|
||||||
|
"""
|
||||||
return FileReporter(self, path)
|
return FileReporter(self, path)
|
||||||
|
|
||||||
|
|
||||||
class FileReporter:
|
class FileReporter:
|
||||||
|
"""Helper class to manage diagnostics for a file"""
|
||||||
|
|
||||||
def __init__(self, base_reporter: Reporter, path: Optional[str]) -> None:
|
def __init__(self, base_reporter: Reporter, path: Optional[str]) -> None:
|
||||||
self.base_reporter: Reporter = base_reporter
|
self.base_reporter: Reporter = base_reporter
|
||||||
self.path: Optional[str] = path
|
self.path: Optional[str] = path
|
||||||
|
self._context: list[str] = []
|
||||||
|
|
||||||
def for_file(self, path: Optional[str]) -> FileReporter:
|
def for_file(self, path: Optional[str]) -> FileReporter:
|
||||||
|
"""Create a new file reporter for the given path with the same base reporter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Optional[str]): the path for the new file reporter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FileReporter: the file reporter
|
||||||
|
"""
|
||||||
return FileReporter(self.base_reporter, path)
|
return FileReporter(self.base_reporter, path)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def with_context(self, ctx: str):
|
||||||
|
self._context.append(ctx)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._context.pop()
|
||||||
|
|
||||||
def report(self, type: DiagnosticType, location: Location, message: str):
|
def report(self, type: DiagnosticType, location: Location, message: str):
|
||||||
|
"""Report a diagnostic to the base reporter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (DiagnosticType): the type of diagnostic
|
||||||
|
location (Location): the location of the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
|
for ctx in self._context:
|
||||||
|
message = message + ", " + ctx
|
||||||
self.base_reporter.report(self.path, type, location, message)
|
self.base_reporter.report(self.path, type, location, message)
|
||||||
|
|
||||||
def error(self, location: Location, message: str):
|
def error(self, location: Location, message: str):
|
||||||
|
"""Report an error diagnostic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
self.report(
|
self.report(
|
||||||
type=DiagnosticType.ERROR,
|
type=DiagnosticType.ERROR,
|
||||||
location=location,
|
location=location,
|
||||||
@@ -49,6 +102,12 @@ class FileReporter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def warning(self, location: Location, message: str):
|
def warning(self, location: Location, message: str):
|
||||||
|
"""Report a warning diagnostic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
self.report(
|
self.report(
|
||||||
type=DiagnosticType.WARNING,
|
type=DiagnosticType.WARNING,
|
||||||
location=location,
|
location=location,
|
||||||
@@ -56,8 +115,27 @@ class FileReporter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def info(self, location: Location, message: str):
|
def info(self, location: Location, message: str):
|
||||||
|
"""Report an info diagnostic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
self.report(
|
self.report(
|
||||||
type=DiagnosticType.INFO,
|
type=DiagnosticType.INFO,
|
||||||
location=location,
|
location=location,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def debug(self, location: Location, message: str):
|
||||||
|
"""Report a debug diagnostic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the diagnostic in the file
|
||||||
|
message (str): the diagnostic's message
|
||||||
|
"""
|
||||||
|
self.report(
|
||||||
|
type=DiagnosticType.DEBUG,
|
||||||
|
location=location,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import midas.ast.python as p
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
|
||||||
|
|
||||||
class ResolverError(Exception): ...
|
class ResolverError(Exception): ...
|
||||||
@@ -11,9 +13,10 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
scope is referred to when a variable is referenced
|
scope is referred to when a variable is referenced
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, reporter: FileReporter):
|
||||||
self.locals: dict[p.Expr, int] = {}
|
self.locals: dict[p.Expr, int] = {}
|
||||||
self.scopes: list[dict[str, bool]] = [{}]
|
self.scopes: list[dict[str, bool]] = [{}]
|
||||||
|
self.reporter: FileReporter = reporter
|
||||||
|
|
||||||
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"""
|
||||||
@@ -25,29 +28,29 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
"""Begin a new scope inside the current one"""
|
"""Begin a new scope inside the current one"""
|
||||||
self.scopes.append({})
|
self.scopes.append({})
|
||||||
|
|
||||||
def end_scope(self):
|
def end_scope(self) -> dict[str, bool]:
|
||||||
"""Close the current scope"""
|
"""Close and return the current scope"""
|
||||||
self.scopes.pop()
|
return self.scopes.pop()
|
||||||
|
|
||||||
def declare(self, name: str) -> None:
|
def declare(self, location: Location, name: str) -> None:
|
||||||
"""Declare a variable in the current scope
|
"""Declare a variable in the current scope
|
||||||
|
|
||||||
This method must be called *before* evaluating the variable initializer
|
This method must be called *before* evaluating the variable initializer
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
location (Location): the location where the name is declared
|
||||||
name (str): the name of the variable
|
name (str): the name of the variable
|
||||||
|
|
||||||
Raises:
|
|
||||||
ResolverError: if the variable has already been declared in the current scope
|
|
||||||
"""
|
"""
|
||||||
if len(self.scopes) == 0:
|
if len(self.scopes) == 0:
|
||||||
return
|
return
|
||||||
scope: dict[str, bool] = self.scopes[-1]
|
scope: dict[str, bool] = self.scopes[-1]
|
||||||
if name in scope:
|
if name in scope:
|
||||||
raise ResolverError(
|
self.reporter.error(
|
||||||
f"A variable with the name {name} is already declared in this scope"
|
location,
|
||||||
|
f"A variable with the name '{name}' is already declared in this scope",
|
||||||
)
|
)
|
||||||
scope[name] = False
|
else:
|
||||||
|
scope[name] = False
|
||||||
|
|
||||||
def define(self, name: str) -> None:
|
def define(self, name: str) -> None:
|
||||||
"""Define a variable in the current scope
|
"""Define a variable in the current scope
|
||||||
@@ -77,7 +80,15 @@ 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:
|
def is_declared(self, name: str) -> bool:
|
||||||
|
"""Check whether the given variable is defined in any scope
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: `True` if the variable is defined in a scope, `False` otherwise
|
||||||
|
"""
|
||||||
for scope in self.scopes:
|
for scope in self.scopes:
|
||||||
if name in scope:
|
if name in scope:
|
||||||
return True
|
return True
|
||||||
@@ -93,8 +104,12 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
function (p.Function): the function to resolve
|
function (p.Function): the function to resolve
|
||||||
"""
|
"""
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
for param in function.all_args:
|
for param in function.params.all:
|
||||||
self.declare(param.name)
|
if param.default is not None:
|
||||||
|
self.resolve(param.default)
|
||||||
|
|
||||||
|
for param in function.params.all:
|
||||||
|
self.declare(function.location, param.name)
|
||||||
self.define(param.name)
|
self.define(param.name)
|
||||||
self.resolve(*function.body)
|
self.resolve(*function.body)
|
||||||
self.end_scope()
|
self.end_scope()
|
||||||
@@ -104,14 +119,12 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def visit_function(self, stmt: p.Function) -> None:
|
def visit_function(self, stmt: p.Function) -> None:
|
||||||
# Declare before resolving body to allow recursion
|
# Declare before resolving body to allow recursion
|
||||||
self.declare(stmt.name)
|
self.declare(stmt.location, stmt.name)
|
||||||
self.define(stmt.name)
|
self.define(stmt.name)
|
||||||
self.resolve_function(stmt)
|
self.resolve_function(stmt)
|
||||||
|
|
||||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||||
self.declare(stmt.name)
|
self.declare(stmt.location, stmt.name)
|
||||||
# NOTE: resolve type here?
|
|
||||||
self.define(stmt.name)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -121,15 +134,21 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
def _visit_assign(self, target: p.Expr):
|
def _visit_assign(self, target: p.Expr):
|
||||||
match target:
|
match target:
|
||||||
case p.VariableExpr(name=name):
|
case p.VariableExpr(name=name):
|
||||||
if not self.is_defined(name):
|
if not self.is_declared(name):
|
||||||
self.declare(name)
|
self.declare(target.location, name)
|
||||||
self.define(name)
|
self.define(name)
|
||||||
target.accept(self)
|
target.accept(self)
|
||||||
|
|
||||||
case p.GetExpr():
|
case p.GetExpr():
|
||||||
target.accept(self)
|
target.accept(self)
|
||||||
|
|
||||||
|
case p.SubscriptExpr():
|
||||||
|
target.accept(self)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise Exception(f"Unsupported assignment to {target}")
|
self.reporter.error(
|
||||||
|
target.location, f"Unsupported assignment to {target}"
|
||||||
|
)
|
||||||
|
|
||||||
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
||||||
if stmt.value is not None:
|
if stmt.value is not None:
|
||||||
@@ -146,23 +165,40 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
# Body
|
# Body
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
self.resolve(*stmt.body)
|
self.resolve(*stmt.body)
|
||||||
self.end_scope()
|
body: dict[str, bool] = self.end_scope()
|
||||||
|
|
||||||
# Else
|
# Else
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
self.resolve(*stmt.orelse)
|
self.resolve(*stmt.orelse)
|
||||||
self.end_scope()
|
else_: dict[str, bool] = self.end_scope()
|
||||||
|
|
||||||
|
# Define variables in this scope if it was defined in both body and else blocks
|
||||||
|
for name, is_defined in body.items():
|
||||||
|
if is_defined and else_.get(name, False):
|
||||||
|
self.define(name)
|
||||||
|
|
||||||
def visit_pass(self, stmt: p.Pass) -> None:
|
def visit_pass(self, stmt: p.Pass) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||||
|
self.begin_scope()
|
||||||
self.resolve(stmt.iterator)
|
self.resolve(stmt.iterator)
|
||||||
self._visit_assign(stmt.target)
|
self._visit_assign(stmt.target)
|
||||||
self.begin_scope()
|
|
||||||
self.resolve(*stmt.body)
|
self.resolve(*stmt.body)
|
||||||
self.end_scope()
|
self.end_scope()
|
||||||
|
|
||||||
|
def visit_import_stmt(self, stmt: p.ImportStmt) -> None:
|
||||||
|
self._resolve_imports(stmt.imports)
|
||||||
|
|
||||||
|
def visit_from_import_stmt(self, stmt: p.FromImportStmt) -> None:
|
||||||
|
self._resolve_imports(stmt.imports)
|
||||||
|
|
||||||
|
def _resolve_imports(self, imports: list[p.ImportAlias]) -> None:
|
||||||
|
for import_ in imports:
|
||||||
|
name: str = import_.imported_name
|
||||||
|
self.declare(import_.location, name)
|
||||||
|
self.define(name)
|
||||||
|
|
||||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -192,8 +228,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
||||||
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
|
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
|
||||||
raise ResolverError(
|
self.reporter.error(
|
||||||
f"Cannot use local variable '{expr.name}' in its own initializer"
|
expr.location,
|
||||||
|
f"Variable '{expr.name}' is declared but may not be defined",
|
||||||
) # aka. UnboundLocalError
|
) # aka. UnboundLocalError
|
||||||
self.resolve_local(expr, expr.name)
|
self.resolve_local(expr, expr.name)
|
||||||
|
|
||||||
@@ -232,5 +269,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
if expr.step is not None:
|
if expr.step is not None:
|
||||||
self.resolve(expr.step)
|
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:
|
def visit_raw_expr(self, expr: p.RawExpr) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class TopType:
|
class TopType:
|
||||||
|
"""The top type (`Any`)"""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "Any"
|
return "Any"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class BaseType:
|
class BaseType:
|
||||||
|
"""A base / builtin type"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -19,7 +27,9 @@ class BaseType:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AliasType:
|
class DerivedType:
|
||||||
|
"""A derived type, i.e. a named subtype of another type"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
type: Type
|
type: Type
|
||||||
|
|
||||||
@@ -29,89 +39,111 @@ class AliasType:
|
|||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class UnknownType:
|
class UnknownType:
|
||||||
|
"""An unknown type"""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "<Unknown>"
|
return "<Unknown>"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class UnitType:
|
class UnitType:
|
||||||
|
"""The unit type (`None`)"""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class Function:
|
class Function:
|
||||||
pos_args: list[Argument] = field(default_factory=list)
|
"""A function type"""
|
||||||
args: list[Argument] = field(default_factory=list)
|
|
||||||
kw_args: list[Argument] = field(default_factory=list)
|
params: ParamSpec
|
||||||
returns: Type
|
returns: Type
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
args: list[str] = []
|
return f"{self.params} -> {self.returns}"
|
||||||
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 Parameter:
|
||||||
pos: int
|
pos: int
|
||||||
name: str
|
name: str
|
||||||
type: Type
|
type: Type
|
||||||
required: bool
|
required: bool
|
||||||
|
unsupported: bool = False
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
opt: str = "" if self.required else "?"
|
opt: str = "" if self.required else "?"
|
||||||
return f"{self.name}: {self.type}{opt}"
|
param: str = f"{self.name}: {self.type}{opt}"
|
||||||
|
if self.unsupported:
|
||||||
|
param = f"({param})"
|
||||||
|
return param
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ParamSpec:
|
||||||
|
"""A function's parameter spec"""
|
||||||
|
|
||||||
|
pos: list[Function.Parameter] = field(default_factory=list)
|
||||||
|
mixed: list[Function.Parameter] = field(default_factory=list)
|
||||||
|
kw: list[Function.Parameter] = field(default_factory=list)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
params: list[str] = []
|
||||||
|
if len(self.pos) != 0:
|
||||||
|
params += list(map(str, self.pos))
|
||||||
|
params.append("/")
|
||||||
|
|
||||||
|
if len(self.mixed) != 0:
|
||||||
|
params += list(map(str, self.mixed))
|
||||||
|
|
||||||
|
if len(self.kw) != 0:
|
||||||
|
params.append("*")
|
||||||
|
params += list(map(str, self.kw))
|
||||||
|
|
||||||
|
return f"({', '.join(params)})"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class OverloadedFunction:
|
class OverloadedFunction:
|
||||||
|
"""A list of method overloads"""
|
||||||
|
|
||||||
overloads: list[Type]
|
overloads: list[Type]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "<overloaded function>"
|
return "<overloaded function>"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
class Variance(StrEnum):
|
||||||
class ComplexType:
|
"""The variance of a :class:`TypeVar`"""
|
||||||
members: dict[str, Type]
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
INVARIANT = "INVARIANT"
|
||||||
props: list[str] = [f"{name}: {type}" for name, type in self.members.items()]
|
COVARIANT = "COVARIANT"
|
||||||
return f"{{{', '.join(props)}}}"
|
CONTRAVARIANT = "CONTRAVARIANT"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class ExtensionType:
|
|
||||||
base: Type
|
|
||||||
extension: ComplexType
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.base} & {self.extension}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class TypeVar:
|
class TypeVar:
|
||||||
|
"""A type variable, often used as type parameters for a generic type"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
bound: Optional[Type]
|
bound: Optional[Type]
|
||||||
|
variance: Variance = Variance.INVARIANT
|
||||||
|
|
||||||
def __str__(self) -> str:
|
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:
|
if self.bound is not None:
|
||||||
return f"{self.name} <: {self.bound}"
|
res = f"{res} <: {self.bound}"
|
||||||
return self.name
|
return res
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class GenericType:
|
class GenericType:
|
||||||
|
"""A generic type, with type parameters and a generic body type"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
params: list[TypeVar]
|
params: list[TypeVar]
|
||||||
body: Type
|
body: Type
|
||||||
@@ -122,6 +154,8 @@ class GenericType:
|
|||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AppliedType:
|
class AppliedType:
|
||||||
|
"""An instance of a :class:`GenericType`, with concrete type arguments substituted in its body"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
args: list[Type]
|
args: list[Type]
|
||||||
body: Type
|
body: Type
|
||||||
@@ -130,35 +164,132 @@ class AppliedType:
|
|||||||
return f"{self.name}[{', '.join(map(str, self.args))}]"
|
return f"{self.name}[{', '.join(map(str, self.args))}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ConstraintType:
|
||||||
|
"""A type with a constraint expression"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""A tuple type, containing any number of ordered item types"""
|
||||||
|
|
||||||
|
items: tuple[Type, ...]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"({', '.join(map(str, self.items))})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ColumnType:
|
||||||
|
"""A column type containing items of a given unique type"""
|
||||||
|
|
||||||
|
type: Type
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Column[{self.type}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class DataFrameType:
|
||||||
|
"""A data-frame type, containing named columns of specific :class:`ColumnType`"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""A frame group-by object"""
|
||||||
|
|
||||||
|
frame: DataFrameType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"FrameGroupBy[{self.frame}]"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ColumnGroupBy:
|
||||||
|
"""A column group-by object"""
|
||||||
|
|
||||||
|
column: ColumnType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"ColumnGroupBy[{self.column}]"
|
||||||
|
|
||||||
|
|
||||||
def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
||||||
def sub_argument(arg: Function.Argument):
|
"""Substitute type variables in the given type
|
||||||
return Function.Argument(
|
|
||||||
pos=arg.pos,
|
This function is called recursively on inner type structures
|
||||||
name=arg.name,
|
|
||||||
type=substitute_typevars(arg.type, substitutions),
|
Args:
|
||||||
required=arg.required,
|
type (Type): the type in which to substitute type variables
|
||||||
|
substitutions (dict[str, Type]): a mapping of type variable names to
|
||||||
|
concrete types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resulting type with substitutions applied
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sub_parameter(param: Function.Parameter):
|
||||||
|
return Function.Parameter(
|
||||||
|
pos=param.pos,
|
||||||
|
name=param.name,
|
||||||
|
type=substitute_typevars(param.type, substitutions),
|
||||||
|
required=param.required,
|
||||||
|
)
|
||||||
|
|
||||||
|
def sub_param_spec(spec: ParamSpec):
|
||||||
|
return ParamSpec(
|
||||||
|
pos=list(map(sub_parameter, spec.pos)),
|
||||||
|
mixed=list(map(sub_parameter, spec.mixed)),
|
||||||
|
kw=list(map(sub_parameter, spec.kw)),
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
match type:
|
||||||
|
case TopType():
|
||||||
|
return type
|
||||||
|
|
||||||
case BaseType(name=name) if name in substitutions:
|
case BaseType(name=name) if name in substitutions:
|
||||||
return substitutions[name]
|
return substitutions[name]
|
||||||
|
|
||||||
case BaseType():
|
case BaseType():
|
||||||
return type
|
return type
|
||||||
|
|
||||||
case AliasType(name=name, type=type2):
|
case DerivedType(name=name, type=type2):
|
||||||
return AliasType(name=name, type=substitute_typevars(type2, substitutions))
|
return DerivedType(
|
||||||
|
name=name, type=substitute_typevars(type2, substitutions)
|
||||||
|
)
|
||||||
|
|
||||||
case Function(
|
case Function(
|
||||||
pos_args=pos_args,
|
params=params,
|
||||||
args=args,
|
|
||||||
kw_args=kw_args,
|
|
||||||
returns=returns,
|
returns=returns,
|
||||||
):
|
):
|
||||||
return Function(
|
return Function(
|
||||||
pos_args=list(map(sub_argument, pos_args)),
|
params=sub_param_spec(params),
|
||||||
args=list(map(sub_argument, args)),
|
|
||||||
kw_args=list(map(sub_argument, kw_args)),
|
|
||||||
returns=substitute_typevars(returns, substitutions),
|
returns=substitute_typevars(returns, substitutions),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,24 +301,6 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
case AppliedType(name=name, args=args, body=body):
|
||||||
return AppliedType(
|
return AppliedType(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -195,37 +308,181 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
|||||||
body=substitute_typevars(body, substitutions),
|
body=substitute_typevars(body, substitutions),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case ConstraintType():
|
||||||
|
return ConstraintType(
|
||||||
|
type=substitute_typevars(type.type, substitutions),
|
||||||
|
constraint=type.constraint,
|
||||||
|
)
|
||||||
|
|
||||||
case TypeVar(name=name):
|
case TypeVar(name=name):
|
||||||
if name in substitutions:
|
if name in substitutions:
|
||||||
return substitutions[name]
|
return substitutions[name]
|
||||||
raise ValueError(f"Missing TypeVar substitution for {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():
|
case UnknownType() | UnitType():
|
||||||
return type
|
return type
|
||||||
|
|
||||||
case _:
|
case TopType() | GenericType():
|
||||||
raise NotImplementedError(f"Unsupported type {type}")
|
raise NotImplementedError(f"Unsupported type {type}")
|
||||||
|
|
||||||
|
# Ensure exhaustiveness
|
||||||
|
case _:
|
||||||
|
assert_never(type)
|
||||||
|
|
||||||
|
|
||||||
def unfold_type(type: Type) -> Type:
|
def unfold_type(type: Type) -> Type:
|
||||||
|
"""Unfold a chain of :class:`DerivedType` to get the root supertype
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type to unfold
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the root supertype
|
||||||
|
"""
|
||||||
match type:
|
match type:
|
||||||
case AliasType(type=ref_type):
|
case DerivedType(type=ref_type):
|
||||||
return unfold_type(ref_type)
|
return unfold_type(ref_type)
|
||||||
case _:
|
case _:
|
||||||
return type
|
return type
|
||||||
|
|
||||||
|
|
||||||
|
def to_annotation(type: Type) -> str:
|
||||||
|
"""Convert the given type to a Python annotation string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the annotation string
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _params_annotation(spec: ParamSpec) -> str:
|
||||||
|
if len(spec.kw) != 0:
|
||||||
|
return "..."
|
||||||
|
|
||||||
|
params: str = ", ".join(
|
||||||
|
to_annotation(param.type) for param in spec.pos + spec.mixed
|
||||||
|
)
|
||||||
|
return f"[{params}]"
|
||||||
|
|
||||||
|
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(params=params, returns=returns):
|
||||||
|
params_annot: str = _params_annotation(params)
|
||||||
|
return f"Callable[{params_annot}, {to_annotation(returns)}]"
|
||||||
|
|
||||||
|
case OverloadedFunction():
|
||||||
|
return "Callable"
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""A predicate"""
|
||||||
|
|
||||||
|
type: Type
|
||||||
|
body: m.Expr
|
||||||
|
alias: bool
|
||||||
|
|
||||||
|
|
||||||
Type = (
|
Type = (
|
||||||
TopType
|
TopType
|
||||||
| BaseType
|
| BaseType
|
||||||
| AliasType
|
| DerivedType
|
||||||
| UnknownType
|
| UnknownType
|
||||||
| UnitType
|
| UnitType
|
||||||
| Function
|
| Function
|
||||||
| OverloadedFunction
|
| OverloadedFunction
|
||||||
| ComplexType
|
|
||||||
| ExtensionType
|
|
||||||
| TypeVar
|
| TypeVar
|
||||||
| GenericType
|
| GenericType
|
||||||
| AppliedType
|
| AppliedType
|
||||||
|
| ConstraintType
|
||||||
|
| TupleType
|
||||||
|
| ColumnType
|
||||||
|
| DataFrameType
|
||||||
|
| FrameGroupBy
|
||||||
|
| ColumnGroupBy
|
||||||
)
|
)
|
||||||
|
|||||||
268
midas/checker/unifier.py
Normal file
268
midas/checker/unifier.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
AppliedType,
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
ParamSpec,
|
||||||
|
TopType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UnificationError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
|
class Unifier:
|
||||||
|
"""
|
||||||
|
Helper class to unify generic types in concrete usages
|
||||||
|
|
||||||
|
This can be used for example when a generic function is called with concrete
|
||||||
|
arguments, at which point the type parameters of the function signature
|
||||||
|
should be resolvable
|
||||||
|
"""
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""Try and unify a generic function call given concrete arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (GenericType): the generic function type
|
||||||
|
positional (list[Type]): the list of positional arguments
|
||||||
|
keywords (dict[str, Type]): the map of keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the concrete function type if unifiable, or `None`
|
||||||
|
"""
|
||||||
|
concrete_func: Function = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
pos=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=i,
|
||||||
|
name=str(i),
|
||||||
|
type=arg,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
for i, arg in enumerate(positional)
|
||||||
|
],
|
||||||
|
kw=[
|
||||||
|
Function.Parameter(
|
||||||
|
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]:
|
||||||
|
"""Unify a generic type's parameters given a concrete usage
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template (GenericType): the generic type
|
||||||
|
concrete (Type): a concrete usage
|
||||||
|
match_return (bool, optional): if `template` is a function type,
|
||||||
|
whether its return type must be matched (see :func:`match`).
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the concrete type if unifiable, or `None`
|
||||||
|
"""
|
||||||
|
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]:
|
||||||
|
"""Match a generic type with a concrete usage, recording parameter substitutions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template (Type): the generic type
|
||||||
|
concrete (Type): a concrete usage
|
||||||
|
match_return (bool, optional): if `template` and `concrete` are both
|
||||||
|
:class:`Function`, whether their return types are also matched.
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnificationError: if there is a conflict in parameter substitutions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Type]: the parameter substitutions which,
|
||||||
|
applied to `template`, yield `concrete`
|
||||||
|
"""
|
||||||
|
# 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.Parameter, Function.Parameter]] = (
|
||||||
|
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]:
|
||||||
|
"""Merge two maps of substitutions and raise an error if incompatible
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subs1 (dict[str, Type]): the first substitutions
|
||||||
|
subs2 (dict[str, Type]): the second substitutions
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnificationError: if there is a conflict between the two maps
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Type]: the merged map of substitutions
|
||||||
|
"""
|
||||||
|
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.Parameter, Function.Parameter]]:
|
||||||
|
"""Map parameters of two functions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func1 (Function): the first function
|
||||||
|
func2 (Function): the second function
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[tuple[Function.Parameter, Function.Parameter]]: the list of parameter pairs
|
||||||
|
"""
|
||||||
|
pos1: list[Function.Parameter] = func1.params.pos
|
||||||
|
mixed1: list[Function.Parameter] = func1.params.mixed
|
||||||
|
kw1: list[Function.Parameter] = func1.params.kw
|
||||||
|
|
||||||
|
pos2: list[Function.Parameter] = func2.params.pos
|
||||||
|
mixed2: list[Function.Parameter] = func2.params.mixed
|
||||||
|
kw2: list[Function.Parameter] = func2.params.kw
|
||||||
|
|
||||||
|
mapped: list[tuple[Function.Parameter, Function.Parameter]] = []
|
||||||
|
|
||||||
|
by_pos2: dict[int, Function.Parameter] = {
|
||||||
|
param.pos: param for param in pos2 + mixed2
|
||||||
|
}
|
||||||
|
by_name2: dict[str, Function.Parameter] = {
|
||||||
|
param.name: param for param 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
|
||||||
181
midas/checker/variance.py
Normal file
181
midas/checker/variance.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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:
|
||||||
|
"""Helper class to track the polarity of type parameter references and computer their variance"""
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Record a polarity of the given type parameter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
var (TypeVar): the type parameter
|
||||||
|
polarity (Polarity): the polarity
|
||||||
|
"""
|
||||||
|
self.refs[var.name].add(polarity)
|
||||||
|
|
||||||
|
def get_updated_vars(self) -> list[TypeVar]:
|
||||||
|
"""Get a list of the tracked type variables with their recorded variance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TypeVar]: the list of update type parameters
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Get the variance of a type parameter
|
||||||
|
|
||||||
|
If the type parameter is only referenced in positive positions, it is
|
||||||
|
covariant. If it is only referenced in negative positions, it is
|
||||||
|
contravariant. Otherwise, it is invariant
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type parameter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Variance: the variance of the type parameter
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Helper class to compute type parameter variance"""
|
||||||
|
|
||||||
|
def __init__(self, types: TypesRegistry) -> None:
|
||||||
|
self.types: TypesRegistry = types
|
||||||
|
self.tracker: Tracker = Tracker([])
|
||||||
|
|
||||||
|
def infer(self, type: GenericType) -> GenericType:
|
||||||
|
"""Infer the variance of a generic type's parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (GenericType): the generic type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GenericType: a new generic type with its parameters updated with
|
||||||
|
their inferred variance
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
"""Walk the type nodes and record variance
|
||||||
|
|
||||||
|
This function recurses into type substructures (e.g. function parameters,
|
||||||
|
overloads, constraint type bases, etc.)
|
||||||
|
|
||||||
|
When recursing, the polarity is flipped for consumer positions (e.g. function
|
||||||
|
parameters) or kept the same for producer positions (e.g. return type)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type to visit
|
||||||
|
polarity (Polarity): the current polarity
|
||||||
|
base_name (str): the root generic type name (used to detect and
|
||||||
|
handle cyclic references)
|
||||||
|
path (Optional[list[str]], optional): the path to reach the current
|
||||||
|
type from the root generic type (used for debugging). Defaults to None.
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
path = []
|
||||||
|
|
||||||
|
match type:
|
||||||
|
# Arguments are negative positions -> flip polarity
|
||||||
|
# Return is positive position -> keep polarity
|
||||||
|
case Function(params=spec):
|
||||||
|
all_params: list[Function.Parameter] = spec.pos + spec.mixed + spec.kw
|
||||||
|
for param in all_params:
|
||||||
|
self.walk(
|
||||||
|
param.type,
|
||||||
|
-polarity,
|
||||||
|
base_name,
|
||||||
|
path + [f"param:'{param.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)
|
||||||
@@ -4,5 +4,6 @@ from .format import format as format
|
|||||||
from .highlight import highlight as highlight
|
from .highlight import highlight as highlight
|
||||||
from .parse import parse as parse
|
from .parse import parse as parse
|
||||||
from .registry import dump_registry as dump_registry
|
from .registry import dump_registry as dump_registry
|
||||||
|
from .stubs import stubs as stubs
|
||||||
from .types import types as types
|
from .types import types as types
|
||||||
from .validate import validate as validate
|
from .validate import validate as validate
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TextIO
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@@ -19,24 +19,33 @@ from midas.utils import TypedAST
|
|||||||
@click.command(help="Compile source")
|
@click.command(help="Compile source")
|
||||||
@click.argument("file", type=click.File("r"))
|
@click.argument("file", type=click.File("r"))
|
||||||
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
@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(
|
def compile(
|
||||||
file: TextIO,
|
file: TextIO,
|
||||||
types: tuple[TextIO],
|
types: tuple[TextIO],
|
||||||
|
stubs: tuple[str],
|
||||||
|
ignore_errors: bool,
|
||||||
):
|
):
|
||||||
source: str = file.read()
|
source: str = file.read()
|
||||||
source_path: Path = Path(file.name).resolve()
|
source_path: Path = Path(file.name).resolve()
|
||||||
|
|
||||||
checker = TypeChecker()
|
checker = TypeChecker()
|
||||||
for types_file in types:
|
type_files: list[tuple[Path, Optional[str]]] = []
|
||||||
checker.import_midas(Path(types_file.name).resolve())
|
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))
|
typed_ast: TypedAST = checker.type_check_source(source, str(source_path))
|
||||||
diagnostics: list[Diagnostic] = checker.diagnostics.copy()
|
diagnostics: list[Diagnostic] = checker.diagnostics.copy()
|
||||||
printer = DiagnosticPrinter()
|
printer = DiagnosticPrinter()
|
||||||
printer.print_all(diagnostics)
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
if any(map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)):
|
if not ignore_errors and any(
|
||||||
|
map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)
|
||||||
|
):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
generator = Generator(workdir=source_path.parent)
|
generator = Generator(workdir=source_path.parent, types=checker.types)
|
||||||
generator.generate(typed_ast, source_path)
|
generator.generate(typed_ast, source_path, type_files=type_files)
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ from typing import TextIO
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from midas.ast.printer import MidasPrinter
|
||||||
from midas.checker.checker import TypeChecker
|
from midas.checker.checker import TypeChecker
|
||||||
from midas.checker.types import AliasType, AppliedType, BaseType, GenericType, Type
|
from midas.checker.registry import Member
|
||||||
|
from midas.checker.types import AppliedType, BaseType, DerivedType, GenericType, Type
|
||||||
|
|
||||||
|
|
||||||
def base_type(type: Type) -> Type:
|
def base_type(type: Type) -> Type:
|
||||||
match type:
|
match type:
|
||||||
case BaseType():
|
case BaseType():
|
||||||
return type
|
return type
|
||||||
case AliasType(type=base):
|
case DerivedType(type=base):
|
||||||
return base
|
return base
|
||||||
case AppliedType(body=body):
|
case AppliedType(body=body):
|
||||||
return body
|
return body
|
||||||
@@ -35,10 +37,30 @@ def dump_registry(
|
|||||||
for types_file in types:
|
for types_file in types:
|
||||||
checker.import_midas(Path(types_file.name).resolve())
|
checker.import_midas(Path(types_file.name).resolve())
|
||||||
|
|
||||||
|
print("##### Types #####")
|
||||||
for name, type in checker.types._types.items():
|
for name, type in checker.types._types.items():
|
||||||
members: dict[str, Type] = checker.types._members.get(name, {})
|
members: dict[str, Member] = checker.types._members.get(name, {})
|
||||||
print(f"{name} = {base_type(type)}")
|
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:
|
if len(members) != 0:
|
||||||
print(" " * 4 + "Members:")
|
print(" " * 4 + "Members:")
|
||||||
for member_name, member_type in members.items():
|
for member_name, member in members.items():
|
||||||
print(" " * 8 + f"{member_name}: {member_type}")
|
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()
|
||||||
@@ -41,6 +41,7 @@ def types(
|
|||||||
message=f"Type: {type}",
|
message=f"Type: {type}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
diagnostics.extend(checker.diagnostics)
|
||||||
printer = DiagnosticPrinter()
|
printer = DiagnosticPrinter()
|
||||||
printer.print_all(diagnostics)
|
printer.print_all(diagnostics)
|
||||||
|
|
||||||
|
|||||||
@@ -134,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")
|
||||||
@@ -157,15 +157,18 @@ class PythonHighlighter(
|
|||||||
|
|
||||||
def visit_function(self, stmt: p.Function) -> None:
|
def visit_function(self, stmt: p.Function) -> None:
|
||||||
self.wrap(stmt, "function")
|
self.wrap(stmt, "function")
|
||||||
for arg in stmt.posonlyargs + stmt.args + stmt.kwonlyargs:
|
self._highlight_param_spec(stmt.params)
|
||||||
self._highlight_function_argument(arg)
|
|
||||||
for body_stmt in stmt.body:
|
for body_stmt in stmt.body:
|
||||||
body_stmt.accept(self)
|
body_stmt.accept(self)
|
||||||
|
|
||||||
def _highlight_function_argument(self, arg: p.Function.Argument) -> None:
|
def _highlight_param_spec(self, spec: p.ParamSpec) -> None:
|
||||||
self.wrap(arg, "argument")
|
for param in spec.all:
|
||||||
if arg.type is not None:
|
self._highlight_function_param(param)
|
||||||
arg.type.accept(self)
|
|
||||||
|
def _highlight_function_param(self, param: p.Function.Parameter) -> None:
|
||||||
|
self.wrap(param, "parameter")
|
||||||
|
if param.type is not None:
|
||||||
|
param.type.accept(self)
|
||||||
|
|
||||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||||
stmt.type.accept(self)
|
stmt.type.accept(self)
|
||||||
@@ -228,6 +231,13 @@ class PythonHighlighter(
|
|||||||
for item in expr.items:
|
for item in expr.items:
|
||||||
item.accept(self)
|
item.accept(self)
|
||||||
|
|
||||||
|
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:
|
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> None:
|
||||||
expr.object.accept(self)
|
expr.object.accept(self)
|
||||||
expr.index.accept(self)
|
expr.index.accept(self)
|
||||||
@@ -240,6 +250,14 @@ class PythonHighlighter(
|
|||||||
if expr.step is not None:
|
if expr.step is not None:
|
||||||
expr.step.accept(self)
|
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(
|
class MidasHighlighter(
|
||||||
Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None]
|
Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None]
|
||||||
@@ -266,8 +284,9 @@ class MidasHighlighter(
|
|||||||
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")
|
||||||
self.wrap(LocatableToken(stmt.name), "predicate-name")
|
self.wrap(LocatableToken(stmt.name), "predicate-name")
|
||||||
stmt.type.accept(self)
|
for spec in stmt.params:
|
||||||
stmt.condition.accept(self)
|
self._visit_param_spec(spec)
|
||||||
|
stmt.body.accept(self)
|
||||||
|
|
||||||
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")
|
||||||
@@ -283,6 +302,14 @@ class MidasHighlighter(
|
|||||||
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)
|
||||||
@@ -311,21 +338,22 @@ class MidasHighlighter(
|
|||||||
type.type.accept(self)
|
type.type.accept(self)
|
||||||
type.constraint.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:
|
def visit_function_type(self, type: m.FunctionType) -> None:
|
||||||
self.wrap(type, "function")
|
self.wrap(type, "function")
|
||||||
for arg in type.pos_args + type.args + type.kw_args:
|
self._visit_param_spec(type.params)
|
||||||
arg.type.accept(self)
|
|
||||||
type.returns.accept(self)
|
type.returns.accept(self)
|
||||||
|
|
||||||
def visit_extension_type(self, type: m.ExtensionType) -> None:
|
def _visit_param_spec(self, spec: m.ParamSpec) -> None:
|
||||||
self.wrap(type, "extension")
|
for param in spec.pos + spec.mixed + spec.kw:
|
||||||
type.base.accept(self)
|
param.type.accept(self)
|
||||||
type.extension.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):
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ span {
|
|||||||
&.error {
|
&.error {
|
||||||
--col: 255, 0, 0;
|
--col: 255, 0, 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
--col: 250, 160, 0;
|
--col: 250, 160, 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.info {
|
&.info {
|
||||||
--col: 150, 190, 250;
|
--col: 150, 190, 250;
|
||||||
}
|
}
|
||||||
@@ -19,12 +21,12 @@ span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:has(.with-msg:hover)) {
|
&:hover:not(:has(.with-msg:hover)) {
|
||||||
.message {
|
&>.message {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
&>.message {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.2em);
|
top: calc(100% + 0.2em);
|
||||||
left: -.2em;
|
left: -.2em;
|
||||||
@@ -33,7 +35,8 @@ span {
|
|||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 300%;
|
width: max-content;
|
||||||
|
max-width: 60vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,7 @@ span {
|
|||||||
|
|
||||||
&.named-type,
|
&.named-type,
|
||||||
&.generic-type,
|
&.generic-type,
|
||||||
&.constraint-type,
|
&.constraint-type {
|
||||||
&.complex-type {
|
|
||||||
--col: 150, 150, 150;
|
--col: 150, 150, 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ span {
|
|||||||
--col: 108, 233, 108;
|
--col: 108, 233, 108;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.param {
|
&.arg {
|
||||||
--col: 103, 192, 224;
|
--col: 103, 192, 224;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ span {
|
|||||||
--col: 215, 103, 224;
|
--col: 215, 103, 224;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.argument {
|
&.parameter {
|
||||||
--col: 103, 192, 224;
|
--col: 103, 192, 224;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ midas.add_command(commands.highlight)
|
|||||||
midas.add_command(commands.parse)
|
midas.add_command(commands.parse)
|
||||||
midas.add_command(commands.dump_registry)
|
midas.add_command(commands.dump_registry)
|
||||||
midas.add_command(commands.types)
|
midas.add_command(commands.types)
|
||||||
|
midas.add_command(commands.stubs)
|
||||||
midas.add_command(commands.validate)
|
midas.add_command(commands.validate)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -7,6 +8,13 @@ from midas.cli.ansi import Ansi
|
|||||||
|
|
||||||
|
|
||||||
class DiagnosticPrinter:
|
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:
|
def __init__(self) -> None:
|
||||||
self.files: dict[Optional[str], list[str]] = {}
|
self.files: dict[Optional[str], list[str]] = {}
|
||||||
|
|
||||||
@@ -22,10 +30,25 @@ class DiagnosticPrinter:
|
|||||||
return self.files[filename]
|
return self.files[filename]
|
||||||
|
|
||||||
def print_all(self, diagnostics: list[Diagnostic], indent: int = 4):
|
def print_all(self, diagnostics: list[Diagnostic], indent: int = 4):
|
||||||
|
by_type: dict[DiagnosticType, int] = defaultdict(int)
|
||||||
for diagnostic in diagnostics:
|
for diagnostic in diagnostics:
|
||||||
filename: Optional[str] = diagnostic.file_path
|
filename: Optional[str] = diagnostic.file_path
|
||||||
lines = self.get_lines(filename)
|
lines = self.get_lines(filename)
|
||||||
self.print(lines, diagnostic, indent=indent)
|
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):
|
def print(self, lines: list[str], diagnostic: Diagnostic, indent: int = 4):
|
||||||
"""Pretty-print a diagnostic, showing some context if possible
|
"""Pretty-print a diagnostic, showing some context if possible
|
||||||
@@ -45,7 +68,7 @@ class DiagnosticPrinter:
|
|||||||
|
|
||||||
loc: Location = diagnostic.location
|
loc: Location = diagnostic.location
|
||||||
if loc.lineno != loc.end_lineno:
|
if loc.lineno != loc.end_lineno:
|
||||||
print(diagnostic)
|
self.print_multiline(lines, diagnostic, indent)
|
||||||
return
|
return
|
||||||
|
|
||||||
start_offset: int = loc.col_offset
|
start_offset: int = loc.col_offset
|
||||||
@@ -55,11 +78,7 @@ class DiagnosticPrinter:
|
|||||||
before: str = line[:start_offset]
|
before: str = line[:start_offset]
|
||||||
after: str = line[end_offset:]
|
after: str = line[end_offset:]
|
||||||
|
|
||||||
color: int = {
|
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||||
DiagnosticType.ERROR: Ansi.RED,
|
|
||||||
DiagnosticType.WARNING: Ansi.YELLOW,
|
|
||||||
DiagnosticType.INFO: Ansi.CYAN,
|
|
||||||
}.get(diagnostic.type, Ansi.WHITE)
|
|
||||||
|
|
||||||
subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET
|
subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET
|
||||||
cursor: str = (
|
cursor: str = (
|
||||||
@@ -76,3 +95,27 @@ class DiagnosticPrinter:
|
|||||||
print(indent_str + before + subject + after)
|
print(indent_str + before + subject + after)
|
||||||
print(indent_str + cursor)
|
print(indent_str + cursor)
|
||||||
print()
|
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()
|
||||||
|
|||||||
124
midas/generator/collector.py
Normal file
124
midas/generator/collector.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import ast
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
|
|
||||||
|
AssertionBuilder = Callable[..., ast.expr]
|
||||||
|
"""A callback function which builds an assertion test given some input expressions"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Assertion:
|
||||||
|
"""Runtime assertion to generate, bound to an expression"""
|
||||||
|
|
||||||
|
bound_expr: p.Expr
|
||||||
|
"""The expression the assertion is bound to"""
|
||||||
|
|
||||||
|
inputs: list[p.Expr]
|
||||||
|
"""
|
||||||
|
Expressions needed for the assertion
|
||||||
|
|
||||||
|
Each expression will be converted by the generator and passed as individual
|
||||||
|
arguments to `builder`
|
||||||
|
"""
|
||||||
|
|
||||||
|
builder: AssertionBuilder
|
||||||
|
"""The callback to build the assertion test given converted expression from `inputs`"""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
"""The assertion message"""
|
||||||
|
|
||||||
|
def is_bound_to(self, expr: p.Expr) -> bool:
|
||||||
|
"""Check whether this assertion is bound to the given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether this assertion is bound to `expr`
|
||||||
|
"""
|
||||||
|
return expr == self.bound_expr
|
||||||
|
|
||||||
|
|
||||||
|
class AssertionCollector:
|
||||||
|
"""Helper class to collect assertions from outside the generator"""
|
||||||
|
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
"""Add an assertion bound to the given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bound_expr (p.Expr): the expression before which the assertion
|
||||||
|
must be generated
|
||||||
|
inputs (list[p.Expr]): the list of input expressions (see :class:`Assertion`)
|
||||||
|
builder (AssertionBuilder): the builder callback (see :class:`Assertion`)
|
||||||
|
message (str): the assertion message
|
||||||
|
"""
|
||||||
|
self.assertions.append(
|
||||||
|
Assertion(
|
||||||
|
bound_expr=bound_expr,
|
||||||
|
inputs=inputs,
|
||||||
|
builder=builder,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove(self, assertion: Assertion):
|
||||||
|
"""Remove the given assertion from the collection
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assertion (Assertion): the assertion to remove
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.assertions.remove(assertion)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def define(self, name: str, stmt: ast.stmt):
|
||||||
|
"""Register a statement definition
|
||||||
|
|
||||||
|
This method will only register the first definition of any given name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the definition
|
||||||
|
stmt (ast.stmt): the definition statement, like a function def
|
||||||
|
"""
|
||||||
|
if name not in self.definitions:
|
||||||
|
self.definitions[name] = stmt
|
||||||
|
|
||||||
|
def get_definitions(self) -> list[ast.stmt]:
|
||||||
|
"""Get the list of definitions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ast.stmt]: the list of definitions
|
||||||
|
"""
|
||||||
|
return list(self.definitions.values())
|
||||||
|
|
||||||
|
def get_assertions(self) -> list[Assertion]:
|
||||||
|
"""Get the list of assertions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Assertion]: the list of assertions
|
||||||
|
"""
|
||||||
|
return self.assertions
|
||||||
|
|
||||||
|
def get_assertions_for(self, expr: p.Expr) -> list[Assertion]:
|
||||||
|
"""Get the list of assertions bound to a given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Assertion]: the list of assertions bound to `expr`
|
||||||
|
"""
|
||||||
|
return list(filter(lambda a: a.is_bound_to(expr), self.assertions))
|
||||||
304
midas/generator/constraints.py
Normal file
304
midas/generator/constraints.py
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import ast
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import midas.ast.midas as m
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.types import (
|
||||||
|
Function,
|
||||||
|
ParamSpec,
|
||||||
|
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]):
|
||||||
|
"""Class to generate Python code for constraint expressions"""
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""Get the list of definitions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ast.stmt]: the list of definitions
|
||||||
|
"""
|
||||||
|
return self._definitions
|
||||||
|
|
||||||
|
def generate(self, expr: m.Expr) -> ast.expr:
|
||||||
|
"""Translate the given Midas expression to a Python expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to translate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.expr: the equivalent Python expression
|
||||||
|
"""
|
||||||
|
match expr:
|
||||||
|
case m.VariableExpr():
|
||||||
|
return expr.accept(self)
|
||||||
|
case _:
|
||||||
|
func = Function(
|
||||||
|
params=ParamSpec(
|
||||||
|
mixed=[
|
||||||
|
Function.Parameter(
|
||||||
|
pos=0,
|
||||||
|
name="_",
|
||||||
|
type=self.types.get_type("Any"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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:
|
||||||
|
"""Get a unique alias for a predicate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (Optional[str]): the name of the predicate as defined by the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: a unique name
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Translate the given predicate to a Python definition (or assignment)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the predicate
|
||||||
|
predicate (Predicate): the predicate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.stmt: the equivalent Python statement
|
||||||
|
"""
|
||||||
|
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, params: ParamSpec) -> ast.arguments:
|
||||||
|
"""Translate the given parameter spec into an `ast.arguments` node
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params (ParamSpec): the parameter spec to translate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.arguments: the equivalent `ast.arguments`
|
||||||
|
"""
|
||||||
|
return ast.arguments(
|
||||||
|
posonlyargs=[
|
||||||
|
ast.arg(
|
||||||
|
arg=param.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(param.type)),
|
||||||
|
)
|
||||||
|
for param in params.pos
|
||||||
|
],
|
||||||
|
args=[
|
||||||
|
ast.arg(
|
||||||
|
arg=param.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(param.type)),
|
||||||
|
)
|
||||||
|
for param in params.mixed
|
||||||
|
],
|
||||||
|
kwonlyargs=[
|
||||||
|
ast.arg(
|
||||||
|
arg=param.name,
|
||||||
|
annotation=ast.Constant(value=to_annotation(param.type)),
|
||||||
|
)
|
||||||
|
for param in params.kw
|
||||||
|
],
|
||||||
|
defaults=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_func(
|
||||||
|
self, name: str, inner_body: list[ast.stmt], type: Type, level: int = 0
|
||||||
|
) -> ast.stmt:
|
||||||
|
"""Generate a Python function def with the given name, body and signature
|
||||||
|
|
||||||
|
If `type` returns a function, the curried arguments are separated into
|
||||||
|
inner methods.
|
||||||
|
For example, if `type` is `(a: int) -> (b: int) -> (c: int) -> int`, the
|
||||||
|
following function would be generated:
|
||||||
|
```python
|
||||||
|
def predicate(a: int):
|
||||||
|
def inner0(b: int):
|
||||||
|
def inner1(c: int):
|
||||||
|
return ...
|
||||||
|
return inner1
|
||||||
|
return inner0
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the outer function
|
||||||
|
inner_body (list[ast.stmt]): the body of the innermost function
|
||||||
|
type (Type): the function type / signature
|
||||||
|
level (int, optional): the current nesting level. Defaults to 0.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if `type` is not a function
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.stmt: the equivalent Python function definition
|
||||||
|
"""
|
||||||
|
match type:
|
||||||
|
case Function(params=params, returns=Function()):
|
||||||
|
inner_name: str = f"inner{level}"
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.make_args(params),
|
||||||
|
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(params=params):
|
||||||
|
return ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.make_args(params),
|
||||||
|
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]:
|
||||||
|
"""Get a predicate's alias, and generate its definition if first reference
|
||||||
|
|
||||||
|
When calling this function for the first time for a given predicate,
|
||||||
|
a Python definition and an alias are generated. Subsequent calls only
|
||||||
|
return the alias, without re-generating the predicate's definition
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the predicate's name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[ast.expr]: the predicate's alias, or `None` if it is not defined
|
||||||
|
"""
|
||||||
|
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="_")
|
||||||
File diff suppressed because it is too large
Load Diff
579
midas/generator/stubs.py
Normal file
579
midas/generator/stubs.py
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
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,
|
||||||
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
|
DerivedType,
|
||||||
|
FrameGroupBy,
|
||||||
|
Function,
|
||||||
|
GenericType,
|
||||||
|
OverloadedFunction,
|
||||||
|
ParamSpec,
|
||||||
|
TopType,
|
||||||
|
TupleType,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
UnitType,
|
||||||
|
UnknownType,
|
||||||
|
Variance,
|
||||||
|
substitute_typevars,
|
||||||
|
)
|
||||||
|
|
||||||
|
Empty = ast.Constant(value=...)
|
||||||
|
|
||||||
|
|
||||||
|
class StubsGenerator:
|
||||||
|
"""A class to generate Python stubs for user-defined Midas types"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Generate a Python module of stubs for all user-defined types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.Module: the generated 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):
|
||||||
|
"""Generate a stub for the given type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
type (Type): the 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]]:
|
||||||
|
"""Get the list of class bases and type parameter substitutions for a type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type whose bases to get
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[list[ast.expr], dict[str, Type]]: a tuple containing the list
|
||||||
|
of class bases (already translated to Python AST nodes), and a
|
||||||
|
mapping of type parameter substitutions (to replace them with
|
||||||
|
their generated aliases)
|
||||||
|
"""
|
||||||
|
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]:
|
||||||
|
"""Generate a class body given its members
|
||||||
|
|
||||||
|
Args:
|
||||||
|
members (dict[str, Member]): the class members
|
||||||
|
substitutions (dict[str, Type]): a mapping of type parameter
|
||||||
|
substitutions (to replace them with their generated aliases)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ast.stmt]: the generated class body statements
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Translate a type to a Python expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (Type): the type to translate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.expr: the generated Python expression
|
||||||
|
"""
|
||||||
|
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 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():
|
||||||
|
self.import_pandas = True
|
||||||
|
return ast.Attribute(
|
||||||
|
value=ast.Name(id="pd"),
|
||||||
|
attr="Series",
|
||||||
|
)
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""Generate definitions for a method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the method's name
|
||||||
|
method (Type): the method's type
|
||||||
|
overloaded (bool, optional): whether this method is part of an
|
||||||
|
overloaded method (used when called recursively). Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ast.stmt]: the generated function definitions
|
||||||
|
"""
|
||||||
|
match method:
|
||||||
|
case Function():
|
||||||
|
if overloaded:
|
||||||
|
self.add_typing_import("overload")
|
||||||
|
return [
|
||||||
|
ast.FunctionDef(
|
||||||
|
name=name,
|
||||||
|
args=self.dump_params(method.params, 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_params(self, params: ParamSpec, with_self: bool = False) -> ast.arguments:
|
||||||
|
"""Generate an `ast.arguments` node for the given parameter spec
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params (ParamSpec): the parameter spec to translate
|
||||||
|
with_self (bool, optional): whether to include a `self` parameter.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ast.arguments: the generate Python AST node
|
||||||
|
"""
|
||||||
|
pos: list[ast.arg] = [
|
||||||
|
ast.arg(
|
||||||
|
arg=f"_{param.pos}",
|
||||||
|
annotation=self.dump_type(param.type),
|
||||||
|
)
|
||||||
|
for param in params.pos
|
||||||
|
]
|
||||||
|
mixed: list[ast.arg] = [
|
||||||
|
ast.arg(
|
||||||
|
arg=param.name,
|
||||||
|
annotation=self.dump_type(param.type),
|
||||||
|
)
|
||||||
|
for param in params.mixed
|
||||||
|
]
|
||||||
|
kw: list[ast.arg] = [
|
||||||
|
ast.arg(
|
||||||
|
arg=param.name,
|
||||||
|
annotation=self.dump_type(param.type),
|
||||||
|
)
|
||||||
|
for param in params.kw
|
||||||
|
]
|
||||||
|
defaults: list[ast.expr] = [
|
||||||
|
Empty for param in params.pos + params.mixed if not param.required
|
||||||
|
]
|
||||||
|
kw_defaults: list[Optional[ast.expr]] = [
|
||||||
|
None if param.required else Empty for param in params.kw
|
||||||
|
]
|
||||||
|
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:
|
||||||
|
"""Generate a :class:`Protocol` to use in a function stub
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (Function): the function signature to define
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the name of the generated protocol
|
||||||
|
"""
|
||||||
|
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_params(func.params, 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:
|
||||||
|
"""Get a unique protocol name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the unique protocol name
|
||||||
|
"""
|
||||||
|
name: str = f"_Protocol{self.protocol_idx}"
|
||||||
|
self.protocol_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def new_stub_name(self) -> str:
|
||||||
|
"""Get a unique stub name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the unique stub name
|
||||||
|
"""
|
||||||
|
name: str = f"_Stub_{self.stub_idx}"
|
||||||
|
self.stub_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def new_type_var_name(self) -> str:
|
||||||
|
"""Get a unique type variable name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the unique type variable name
|
||||||
|
"""
|
||||||
|
name: str = f"_T{self.type_var_idx}"
|
||||||
|
self.type_var_idx += 1
|
||||||
|
return name
|
||||||
|
|
||||||
|
def add_stub(self, stub: ast.stmt):
|
||||||
|
"""Append the given statement to the output
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stub (ast.stmt): the statement to append
|
||||||
|
"""
|
||||||
|
self.stubs.append(stub)
|
||||||
|
|
||||||
|
def add_typing_import(self, name: str):
|
||||||
|
"""Add the given name to the list of names to import from `typing`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name to import
|
||||||
|
"""
|
||||||
|
self.typing_imports.add(name)
|
||||||
|
|
||||||
|
def define_type_vars(self, vars: list[TypeVar]) -> list[TypeVar]:
|
||||||
|
"""Define aliases for the given type variables
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vars (list[TypeVar]): the variables to define
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[TypeVar]: new type variables named with the generated aliases
|
||||||
|
"""
|
||||||
|
vars2: list[TypeVar] = []
|
||||||
|
for var in vars:
|
||||||
|
vars2.append(self.define_type_var(var))
|
||||||
|
return vars2
|
||||||
|
|
||||||
|
def define_type_var(self, var: TypeVar) -> TypeVar:
|
||||||
|
"""Define a type variable alias
|
||||||
|
|
||||||
|
Args:
|
||||||
|
var (TypeVar): the type variable to define
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TypeVar: a new type variable named with a uniquely generated alias
|
||||||
|
"""
|
||||||
|
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)
|
||||||
@@ -16,9 +16,10 @@ class Lexer(ABC):
|
|||||||
"""An abstract lexer which provides methods to easily extend it into a concrete one
|
"""An abstract lexer which provides methods to easily extend it into a concrete one
|
||||||
|
|
||||||
This implementation is based on the [_Crafting Interpreters_][1] book by Robert Nystrom,
|
This implementation is based on the [_Crafting Interpreters_][1] book by Robert Nystrom,
|
||||||
more specifically on my [previous Python implementation](https://git.kb28.ch/HEL/pebble)
|
more specifically on my [previous Python implementation][2]
|
||||||
|
|
||||||
[1]: https://craftinginterpreters.com/
|
[1]: https://craftinginterpreters.com/
|
||||||
|
[2]: https://git.kb28.ch/HEL/pebble
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source: str, file: Optional[str] = None) -> None:
|
def __init__(self, source: str, file: Optional[str] = None) -> None:
|
||||||
@@ -168,6 +169,6 @@ class Lexer(ABC):
|
|||||||
def scan_token(self) -> None:
|
def scan_token(self) -> None:
|
||||||
"""Scan a token
|
"""Scan a token
|
||||||
|
|
||||||
This function should (at least) consume the current character and produce the appropriate token(s), using `add_token`
|
This function should (at least) consume the current character and produce the appropriate token(s), using :func:`add_token`
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class MidasLexer(Lexer):
|
|||||||
)
|
)
|
||||||
case "!" if self.match("="):
|
case "!" if self.match("="):
|
||||||
self.add_token(TokenType.BANG_EQUAL)
|
self.add_token(TokenType.BANG_EQUAL)
|
||||||
|
case "!":
|
||||||
|
self.add_token(TokenType.BANG)
|
||||||
case ":":
|
case ":":
|
||||||
self.add_token(TokenType.COLON)
|
self.add_token(TokenType.COLON)
|
||||||
case ".":
|
case ".":
|
||||||
@@ -46,8 +48,8 @@ class MidasLexer(Lexer):
|
|||||||
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 "*":
|
||||||
@@ -69,6 +71,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()
|
||||||
@@ -78,6 +82,23 @@ class MidasLexer(Lexer):
|
|||||||
self.error("Unexpected character")
|
self.error("Unexpected character")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def scan_string(self, opening: str):
|
||||||
|
"""Scan the rest of a string and add it as a token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
opening (str): the opening quote or double quote, to be matched
|
||||||
|
at the end of the string
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
@@ -87,12 +108,15 @@ class MidasLexer(Lexer):
|
|||||||
while self.peek().isdigit():
|
while self.peek().isdigit():
|
||||||
self.advance()
|
self.advance()
|
||||||
|
|
||||||
|
is_float: bool = False
|
||||||
if self.peek() == "." and self.peek_next().isdigit():
|
if self.peek() == "." and self.peek_next().isdigit():
|
||||||
|
is_float = True
|
||||||
self.advance()
|
self.advance()
|
||||||
while self.peek().isdigit():
|
while self.peek().isdigit():
|
||||||
self.advance()
|
self.advance()
|
||||||
|
|
||||||
value: float = float(self.source[self.start : self.idx])
|
value_str: str = self.source[self.start : self.idx]
|
||||||
|
value: int | float = float(value_str) if is_float else int(value_str)
|
||||||
self.add_token(TokenType.NUMBER, value)
|
self.add_token(TokenType.NUMBER, value)
|
||||||
|
|
||||||
def scan_identifier(self):
|
def scan_identifier(self):
|
||||||
@@ -134,6 +158,18 @@ class MidasLexer(Lexer):
|
|||||||
self.add_token(TokenType.COMMENT)
|
self.add_token(TokenType.COMMENT)
|
||||||
|
|
||||||
def is_identifier_char(self, char: str, *, start: bool) -> bool:
|
def is_identifier_char(self, char: str, *, start: bool) -> bool:
|
||||||
|
"""Check whether a character is a valid as part of an identifier
|
||||||
|
|
||||||
|
Identifiers can contain any alphanumerical character or underscore.
|
||||||
|
They cannot start with a digit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
char (str): the character to check
|
||||||
|
start (bool): whether this is the first character of the identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: `True` if the character is valid, `False` otherwise
|
||||||
|
"""
|
||||||
if char == "_":
|
if char == "_":
|
||||||
return True
|
return True
|
||||||
if char.isalpha():
|
if char.isalpha():
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ 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()
|
||||||
@@ -36,6 +36,7 @@ class TokenType(Enum):
|
|||||||
EQUAL = auto()
|
EQUAL = auto()
|
||||||
EQUAL_EQUAL = auto()
|
EQUAL_EQUAL = auto()
|
||||||
BANG_EQUAL = auto()
|
BANG_EQUAL = auto()
|
||||||
|
BANG = auto()
|
||||||
|
|
||||||
# Literals
|
# Literals
|
||||||
IDENTIFIER = auto()
|
IDENTIFIER = auto()
|
||||||
@@ -43,9 +44,11 @@ class TokenType(Enum):
|
|||||||
TRUE = auto()
|
TRUE = auto()
|
||||||
FALSE = auto()
|
FALSE = auto()
|
||||||
NONE = auto()
|
NONE = auto()
|
||||||
|
STRING = auto()
|
||||||
|
|
||||||
# Keywords
|
# Keywords
|
||||||
TYPE = auto()
|
TYPE = auto()
|
||||||
|
ALIAS = auto()
|
||||||
PREDICATE = auto()
|
PREDICATE = auto()
|
||||||
EXTEND = auto()
|
EXTEND = auto()
|
||||||
WHERE = auto()
|
WHERE = auto()
|
||||||
@@ -62,6 +65,7 @@ class TokenType(Enum):
|
|||||||
|
|
||||||
KEYWORDS: dict[str, TokenType] = {
|
KEYWORDS: dict[str, TokenType] = {
|
||||||
"type": TokenType.TYPE,
|
"type": TokenType.TYPE,
|
||||||
|
"alias": TokenType.ALIAS,
|
||||||
"predicate": TokenType.PREDICATE,
|
"predicate": TokenType.PREDICATE,
|
||||||
"extend": TokenType.EXTEND,
|
"extend": TokenType.EXTEND,
|
||||||
"where": TokenType.WHERE,
|
"where": TokenType.WHERE,
|
||||||
@@ -101,6 +105,15 @@ class Token:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def location_to(self, to: Token) -> Location:
|
def location_to(self, to: Token) -> Location:
|
||||||
|
"""Create a new :class:`Location` spanning from this token to another
|
||||||
|
|
||||||
|
Args:
|
||||||
|
to (Token): the end token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Location: a new :class:`Location` starting at this token and ending
|
||||||
|
at `to`, both included
|
||||||
|
"""
|
||||||
return Location.span(self.get_location(), to.get_location())
|
return Location.span(self.get_location(), to.get_location())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class TokenError:
|
|||||||
def get_report(self) -> str:
|
def get_report(self) -> str:
|
||||||
"""Get a detailed error message
|
"""Get a detailed error message
|
||||||
|
|
||||||
|
The error message is formatted as "(<position>) Error at <token>: <message>".
|
||||||
|
For example: "(L2:5) Error at '3': Expected ')' after arguments."
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: the complete error message
|
str: the complete error message
|
||||||
"""
|
"""
|
||||||
@@ -32,9 +35,10 @@ class Parser(ABC, Generic[T]):
|
|||||||
"""An abstract parser which provides methods to easily extend it into a concrete one
|
"""An abstract parser which provides methods to easily extend it into a concrete one
|
||||||
|
|
||||||
This implementation is based on the [_Crafting Interpreters_][1] book by Robert Nystrom,
|
This implementation is based on the [_Crafting Interpreters_][1] book by Robert Nystrom,
|
||||||
more specifically on my [previous Python implementation](https://git.kb28.ch/HEL/pebble)
|
more specifically on my [previous Python implementation][2]
|
||||||
|
|
||||||
[1]: https://craftinginterpreters.com/
|
[1]: https://craftinginterpreters.com/
|
||||||
|
[2]: https://git.kb28.ch/HEL/pebble
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IGNORE: set[TokenType] = {
|
IGNORE: set[TokenType] = {
|
||||||
@@ -173,7 +177,7 @@ class Parser(ABC, Generic[T]):
|
|||||||
error_msg (str): the error message if the token doesn't match
|
error_msg (str): the error message if the token doesn't match
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
SyntaxError: if the current token doesn't match the given type
|
ParsingError: if the current token doesn't match the given type
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Token: the current token which matched the given type
|
Token: the current token which matched the given type
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ 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,
|
||||||
ComplexType,
|
CallExpr,
|
||||||
ConstraintType,
|
ConstraintType,
|
||||||
Expr,
|
Expr,
|
||||||
ExtendStmt,
|
ExtendStmt,
|
||||||
ExtensionType,
|
FrameType,
|
||||||
FunctionType,
|
FunctionType,
|
||||||
GenericType,
|
GenericType,
|
||||||
GetExpr,
|
GetExpr,
|
||||||
@@ -17,6 +18,7 @@ from midas.ast.midas import (
|
|||||||
MemberKind,
|
MemberKind,
|
||||||
MemberStmt,
|
MemberStmt,
|
||||||
NamedType,
|
NamedType,
|
||||||
|
ParamSpec,
|
||||||
PredicateStmt,
|
PredicateStmt,
|
||||||
Stmt,
|
Stmt,
|
||||||
Type,
|
Type,
|
||||||
@@ -31,10 +33,11 @@ from midas.parser.base import Parser
|
|||||||
from midas.parser.errors import ParsingError
|
from midas.parser.errors import ParsingError
|
||||||
|
|
||||||
|
|
||||||
class MidasParser(Parser):
|
class MidasParser(Parser[list[Stmt]]):
|
||||||
"""A simple parser for midas type definitions"""
|
"""A simple parser for midas type definitions"""
|
||||||
|
|
||||||
SYNC_BOUNDARY: set[TokenType] = {
|
SYNC_BOUNDARY: set[TokenType] = {
|
||||||
|
TokenType.ALIAS,
|
||||||
TokenType.TYPE,
|
TokenType.TYPE,
|
||||||
TokenType.EXTEND,
|
TokenType.EXTEND,
|
||||||
TokenType.PREDICATE,
|
TokenType.PREDICATE,
|
||||||
@@ -69,14 +72,16 @@ class MidasParser(Parser):
|
|||||||
def declaration(self) -> Optional[Stmt]:
|
def declaration(self) -> Optional[Stmt]:
|
||||||
"""Try and parse a declaration
|
"""Try and parse a declaration
|
||||||
|
|
||||||
Any parsing error is caught and None is returned
|
Any parsing error is caught and `None` is returned
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[Stmt]: the parsed Midas statement, or None if a ParsingError was raised
|
Optional[Stmt]: the parsed Midas statement, or `None` if a ParsingError was raised
|
||||||
"""
|
"""
|
||||||
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):
|
||||||
@@ -89,23 +94,14 @@ class MidasParser(Parser):
|
|||||||
def type_declaration(self) -> TypeStmt:
|
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 creates a named subtype of a type expression.
|
||||||
In either case, it can have an optional template expression after its name, wrapped in brackets.
|
It can have an optional template expression after its name, wrapped in brackets, to handle type parameters.
|
||||||
A simple type alias is derived from a base type expression, and can have a optional constraint expression preceded by the `where` keyword.
|
|
||||||
A full simple type alias is thus written:
|
|
||||||
```
|
|
||||||
type Name[Template](TypeExpr) where Condition
|
|
||||||
```
|
|
||||||
|
|
||||||
A new complex type has a set of properties which are named, have a type and an optional constraint expression (also preceded by the `where` keyword).
|
A type statement consists of:
|
||||||
A full complex type definition is thus written:
|
- the `type` keyword
|
||||||
```
|
- a name (identifier)
|
||||||
type Name[Template] {
|
- (optional) type parameters
|
||||||
prop1: TypeExpr1 where Condition1
|
- a body, a type expression (see :func:`type_expr`)
|
||||||
prop2: TypeExpr2 where Condition2
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TypeStmt: the parsed type declaration statement
|
TypeStmt: the parsed type declaration statement
|
||||||
@@ -156,30 +152,53 @@ class MidasParser(Parser):
|
|||||||
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after type parameters")
|
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after type parameters")
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def alias_declaration(self) -> AliasStmt:
|
||||||
|
"""Parse an alias declaration
|
||||||
|
|
||||||
|
An alias statement consists of:
|
||||||
|
- the `alias` keyword
|
||||||
|
- a name (identifier)
|
||||||
|
- a body, a type expression (see :func:`type_expr`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AliasStmt: the parsed alias declaration statement
|
||||||
|
"""
|
||||||
|
keyword: Token = self.previous()
|
||||||
|
name: Token = self.consume_identifier("Expected alias 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:
|
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 expression can either be a function type (see :func:`function`)
|
||||||
It can also optionally be followed by a '?' to indicate a nullable type
|
or a constraint type (see :func:`constraint_type`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TypeExpr: the parsed type expression
|
TypeExpr: the parsed type expression
|
||||||
"""
|
"""
|
||||||
base: Type
|
|
||||||
if self.match(TokenType.FUNC):
|
if self.match(TokenType.FUNC):
|
||||||
base = self.function()
|
return self.function()
|
||||||
else:
|
return self.constraint_type()
|
||||||
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:
|
def constraint_type(self) -> Type:
|
||||||
|
"""Parse a constraint type expression
|
||||||
|
|
||||||
|
A constraint type consists of a base type (see :func:`base_type`),
|
||||||
|
optionally followed by the `where` keyword and a constraint
|
||||||
|
expression (see :func:`constraint`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the parsed constraint type expression
|
||||||
|
"""
|
||||||
type: Type = self.base_type()
|
type: Type = self.base_type()
|
||||||
if self.match(TokenType.WHERE):
|
if self.match(TokenType.WHERE):
|
||||||
constraint: Expr = self.constraint()
|
constraint: Expr = self.constraint()
|
||||||
@@ -191,19 +210,37 @@ class MidasParser(Parser):
|
|||||||
return type
|
return type
|
||||||
|
|
||||||
def base_type(self) -> Type:
|
def base_type(self) -> Type:
|
||||||
|
"""Parse a base type expression
|
||||||
|
|
||||||
|
A base type is either a parenthesized type expression (see :func:`type_expr`)
|
||||||
|
or a generic type (see :func:`generic_type`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the parsed base type expression
|
||||||
|
"""
|
||||||
if self.match(TokenType.LEFT_PAREN):
|
if self.match(TokenType.LEFT_PAREN):
|
||||||
type: Type = self.type_expr()
|
type: Type = self.type_expr()
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
||||||
return type
|
return type
|
||||||
|
|
||||||
if self.check(TokenType.LEFT_BRACE):
|
|
||||||
return self.complex_type()
|
|
||||||
|
|
||||||
return self.generic_type()
|
return self.generic_type()
|
||||||
|
|
||||||
def generic_type(self) -> Type:
|
def generic_type(self) -> Type:
|
||||||
type: Type = self.named_type()
|
"""Parse a generic type expression
|
||||||
|
|
||||||
|
A generic type consists of a named type (see :func:`named_type`),
|
||||||
|
optionally followed by type arguments in brackets.
|
||||||
|
|
||||||
|
The special `Frame` type accepts a frame schema instead of type
|
||||||
|
arguments (see :func:`frame_type`).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the parsed generic type
|
||||||
|
"""
|
||||||
|
type: NamedType = self.named_type()
|
||||||
if self.check(TokenType.LEFT_BRACKET):
|
if self.check(TokenType.LEFT_BRACKET):
|
||||||
|
if type.name.lexeme == "Frame":
|
||||||
|
return self.frame_type()
|
||||||
args: list[Type] = self.type_args()
|
args: list[Type] = self.type_args()
|
||||||
return GenericType(
|
return GenericType(
|
||||||
location=Location.span(type.location, self.previous().get_location()),
|
location=Location.span(type.location, self.previous().get_location()),
|
||||||
@@ -213,6 +250,13 @@ class MidasParser(Parser):
|
|||||||
return type
|
return type
|
||||||
|
|
||||||
def type_args(self) -> list[Type]:
|
def type_args(self) -> list[Type]:
|
||||||
|
"""Parse a list of type arguments
|
||||||
|
|
||||||
|
Type arguments are a comma-separated list of type expression wrapped in brackets.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Type]: the list of type arguments, if any, or an empty list
|
||||||
|
"""
|
||||||
args: list[Type] = []
|
args: list[Type] = []
|
||||||
self.consume(TokenType.LEFT_BRACKET, "Missing '[' before generic arguments")
|
self.consume(TokenType.LEFT_BRACKET, "Missing '[' before generic arguments")
|
||||||
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET):
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET):
|
||||||
@@ -222,53 +266,85 @@ class MidasParser(Parser):
|
|||||||
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after generic arguments")
|
self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after generic arguments")
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def named_type(self) -> Type:
|
def named_type(self) -> NamedType:
|
||||||
|
"""Parse a named type expression
|
||||||
|
|
||||||
|
A named type is an identifier token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
NamedType: the parsed named type expression
|
||||||
|
"""
|
||||||
name: Token = self.consume_identifier("Expected type name")
|
name: Token = self.consume_identifier("Expected type name")
|
||||||
return NamedType(
|
return NamedType(
|
||||||
location=name.get_location(),
|
location=name.get_location(),
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def complex_type(self) -> ComplexType:
|
def frame_type(self) -> FrameType:
|
||||||
"""Parse a type definition body
|
"""Parse a frame type expression
|
||||||
|
|
||||||
A type definition body is a set of whitespace-separated
|
A frame type consists of:
|
||||||
property statements enclosed in curly braces
|
- the `Frame` identifier
|
||||||
|
- an opening bracket `[`
|
||||||
|
- a list of comma-separated column expression consisting of:
|
||||||
|
- a name (token)
|
||||||
|
- a colon `:`
|
||||||
|
- a type expression (see :func:`type_expr`)
|
||||||
|
- a closing bracket `]`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ComplexType: the parsed complex type
|
FrameType: the parsed frame type
|
||||||
"""
|
"""
|
||||||
left: Token = self.consume(
|
keyword: Token = self.previous()
|
||||||
TokenType.LEFT_BRACE, "Expected '{' to start type body"
|
self.consume(TokenType.LEFT_BRACKET, "Expected '[' to start frame schema")
|
||||||
)
|
|
||||||
members: list[MemberStmt] = []
|
columns: list[FrameType.Column] = []
|
||||||
# TODO: add keyword to differentiate properties and methods,
|
while not self.check(TokenType.RIGHT_BRACKET) and not self.is_at_end():
|
||||||
# and allow multiple methods with the same name but not properties
|
name: Token = self.advance()
|
||||||
names: set[str] = set()
|
self.consume(TokenType.COLON, "Expected ':' between column name and type")
|
||||||
while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end():
|
type: Type = self.type_expr()
|
||||||
member: MemberStmt = self.member_stmt()
|
columns.append(
|
||||||
# if member.name.lexeme in names:
|
FrameType.Column(
|
||||||
# raise self.error(member.name, "Duplicate property")
|
location=name.location_to(self.previous()),
|
||||||
# names.add(member.name.lexeme)
|
name=name,
|
||||||
members.append(member)
|
type=type,
|
||||||
right: Token = self.consume(TokenType.RIGHT_BRACE, "Unclosed type body")
|
)
|
||||||
return ComplexType(
|
)
|
||||||
location=left.location_to(right),
|
if not self.match(TokenType.COMMA):
|
||||||
members=members,
|
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:
|
||||||
"""Parse a constraint
|
"""Parse a constraint expression
|
||||||
|
|
||||||
A constraint is basically a logical predicate
|
A constraint is an expression (see :func:`expression`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed constraint expression
|
Expr: the parsed constraint expression
|
||||||
"""
|
"""
|
||||||
|
return self.expression()
|
||||||
|
|
||||||
|
def expression(self) -> Expr:
|
||||||
|
"""Parse an expression
|
||||||
|
|
||||||
|
An expression consists of a logical AND expression (see :func:`and_`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expr: the parsed expression
|
||||||
|
"""
|
||||||
return self.and_()
|
return self.and_()
|
||||||
|
|
||||||
def and_(self) -> Expr:
|
def and_(self) -> Expr:
|
||||||
"""Parse a logical AND expression or a simpler expression
|
"""Parse a logical AND expression
|
||||||
|
|
||||||
|
An AND consists of one or more equality expressions (see :func:`equality`)
|
||||||
|
separated by logical AND operators (`&`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
@@ -284,7 +360,10 @@ class MidasParser(Parser):
|
|||||||
return expr
|
return expr
|
||||||
|
|
||||||
def equality(self) -> Expr:
|
def equality(self) -> Expr:
|
||||||
"""Parse a logical equality expression or a simpler expression
|
"""Parse an equality expression
|
||||||
|
|
||||||
|
An equality consists of one or more comparison expressions (see :func:`comparison`)
|
||||||
|
separated by equality operators (`==`, `!=`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
@@ -300,18 +379,59 @@ class MidasParser(Parser):
|
|||||||
return expr
|
return expr
|
||||||
|
|
||||||
def comparison(self) -> Expr:
|
def comparison(self) -> Expr:
|
||||||
"""Parse a logical comparison expression or a simpler expression
|
"""Parse a comparison expression
|
||||||
|
|
||||||
|
A comparison consists of one or more term expressions (see :func:`term`)
|
||||||
|
separated by comparison operators (`<`, `<=`, `>`, `>=`)
|
||||||
|
|
||||||
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:
|
||||||
|
"""Parse a term expression
|
||||||
|
|
||||||
|
A term consists of one or more factor expressions (see :func:`factor`)
|
||||||
|
separated by weak arithmetic operators (`+`, `-`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expr: the parsed expression
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Parse a factor expression
|
||||||
|
|
||||||
|
A factor consists of one or more unary expressions (see :func:`unary`)
|
||||||
|
separated by strong arithmetic operators (`*`, `/`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expr: the parsed expression
|
||||||
|
"""
|
||||||
|
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)
|
||||||
@@ -321,20 +441,109 @@ class MidasParser(Parser):
|
|||||||
return expr
|
return expr
|
||||||
|
|
||||||
def unary(self) -> Expr:
|
def unary(self) -> Expr:
|
||||||
"""Parse a unary expression or a simpler expression
|
"""Parse a unary expression
|
||||||
|
|
||||||
|
A unary consists of a call expression (see :func:`call`) optionally
|
||||||
|
preceded by zero or more unary operators (`+`, `-`, `!`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
"""
|
"""
|
||||||
if self.match(TokenType.MINUS):
|
if self.match(TokenType.PLUS, TokenType.MINUS, TokenType.BANG):
|
||||||
operator: Token = self.previous()
|
operator: Token = self.previous()
|
||||||
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:
|
||||||
|
"""Parse a call expression
|
||||||
|
|
||||||
|
A call consists of a reference expression (see :func:`reference`)
|
||||||
|
optionally followed by zero or more argument groups.
|
||||||
|
|
||||||
|
Argument groups are parenthesize, comma-separated list of arguments (see :func:`finish_call`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expr: the parsed expression
|
||||||
|
"""
|
||||||
|
expr: Expr = self.reference()
|
||||||
|
while self.match(TokenType.LEFT_PAREN):
|
||||||
|
expr = self.finish_call(expr)
|
||||||
|
return expr
|
||||||
|
|
||||||
|
def finish_call(self, callee: Expr) -> Expr:
|
||||||
|
"""Parse an argument group, i.e. the arguments of a call
|
||||||
|
|
||||||
|
Arguments are either passed positionally or by name (keyword argument).
|
||||||
|
All positional arguments must come before any keyword argument and
|
||||||
|
vice-versa. Arguments are separated by commas.
|
||||||
|
|
||||||
|
A positional argument simply consists of an expression (see :func:`expression`)
|
||||||
|
|
||||||
|
A keyword argument consists of and identifier, followed by the equal `=`
|
||||||
|
token and an expression (see :func:`expression`).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callee (Expr): the callee expression
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ParsingError: if a positional argument is passed after a keyword
|
||||||
|
argument or if a keyword argument's name is invalid (i.e. not
|
||||||
|
an identifier)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expr: the parsed call expression
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
error_msg: str
|
||||||
|
if keywords:
|
||||||
|
error_msg = "Invalid keyword argument name"
|
||||||
|
else:
|
||||||
|
error_msg = (
|
||||||
|
"Cannot pass positional arguments after a keyword argument"
|
||||||
|
)
|
||||||
|
raise self.error(self.peek(), error_msg)
|
||||||
|
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 a reference expression
|
||||||
|
|
||||||
|
A reference consists of a primary expression (see :func:`primary`)
|
||||||
|
optionally followed by zero or more attribute accesses.
|
||||||
|
|
||||||
|
An attribute access consists of a dot `.` token followed by an identifier
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
@@ -349,7 +558,12 @@ class MidasParser(Parser):
|
|||||||
def primary(self) -> Expr:
|
def primary(self) -> Expr:
|
||||||
"""Parse a primary expression
|
"""Parse a primary expression
|
||||||
|
|
||||||
This includes literals (booleans, numbers, etc.), wildcards, identifiers and grouped expressions
|
This includes literals (booleans, numbers, etc.), wildcards, identifiers
|
||||||
|
and grouped expressions
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ParsingError: if a primary expressions cannot be parsed from the
|
||||||
|
following tokens
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Expr: the parsed expression
|
Expr: the parsed expression
|
||||||
@@ -365,6 +579,9 @@ 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.STRING):
|
||||||
|
return LiteralExpr(location=token.get_location(), value=token.value)
|
||||||
|
|
||||||
if self.match_identifier():
|
if self.match_identifier():
|
||||||
return VariableExpr(location=token.get_location(), name=token)
|
return VariableExpr(location=token.get_location(), name=token)
|
||||||
|
|
||||||
@@ -372,21 +589,48 @@ class MidasParser(Parser):
|
|||||||
return WildcardExpr(location=token.get_location(), token=token)
|
return WildcardExpr(location=token.get_location(), token=token)
|
||||||
|
|
||||||
if self.match(TokenType.LEFT_PAREN):
|
if self.match(TokenType.LEFT_PAREN):
|
||||||
expr: Expr = self.constraint()
|
expr: Expr = self.expression()
|
||||||
right: Token = self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
right: Token = self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
||||||
return GroupingExpr(location=token.location_to(right), expr=expr)
|
return GroupingExpr(location=token.location_to(right), expr=expr)
|
||||||
|
|
||||||
raise self.error(self.peek(), "Expected expression")
|
raise self.error(self.peek(), "Expected expression")
|
||||||
|
|
||||||
def consume_identifier(self, message: str = "Expected identifier") -> Token:
|
def consume_identifier(self, message: str = "Expected identifier") -> Token:
|
||||||
|
"""Consume the current token if it is a valid identifier or raise an error (see :func:`check_identifier`)
|
||||||
|
|
||||||
|
If the current token is not a valid identifier, an error is raised
|
||||||
|
with the provided message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str, optional): the error message. Defaults to "Expected identifier".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ParsingError: if the current token is not a valid identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Token: the current token which is a valid identifier
|
||||||
|
"""
|
||||||
if not self.match_identifier():
|
if not self.match_identifier():
|
||||||
raise self.error(self.peek(), message)
|
raise self.error(self.peek(), message)
|
||||||
return self.previous()
|
return self.previous()
|
||||||
|
|
||||||
def match_identifier(self) -> bool:
|
def match_identifier(self) -> bool:
|
||||||
|
"""Consume the next token if it is a valid identifier (see :func:`check_identifier`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether a token was matched and consumed
|
||||||
|
"""
|
||||||
return self.match(TokenType.IDENTIFIER, *KEYWORDS.values())
|
return self.match(TokenType.IDENTIFIER, *KEYWORDS.values())
|
||||||
|
|
||||||
def check_identifier(self) -> bool:
|
def check_identifier(self) -> bool:
|
||||||
|
"""Check whether the current token is a valid identifier
|
||||||
|
|
||||||
|
A valid identifier is either an identifier token or a keyword token.
|
||||||
|
This function always returns False if the parser is at the EOF token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the current token is a valid identifier and not EOF
|
||||||
|
"""
|
||||||
for tt in [TokenType.IDENTIFIER, *KEYWORDS.values()]:
|
for tt in [TokenType.IDENTIFIER, *KEYWORDS.values()]:
|
||||||
if self.check(tt):
|
if self.check(tt):
|
||||||
return True
|
return True
|
||||||
@@ -395,7 +639,14 @@ class MidasParser(Parser):
|
|||||||
def member_stmt(self) -> MemberStmt:
|
def member_stmt(self) -> MemberStmt:
|
||||||
"""Parse a member statement
|
"""Parse a member statement
|
||||||
|
|
||||||
A type member statement is written `prop name: Type` or `def name: Type`
|
A member statement is written consists of:
|
||||||
|
- the `prop` (for a property) or `def` (for a method) keyword
|
||||||
|
- an name (identifier)
|
||||||
|
- a colon `:`
|
||||||
|
- a type expression (see :func:`type_expr`)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ParsingError: if the first token is neither `prop` nor `def`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MemberStmt: the parsed member statement
|
MemberStmt: the parsed member statement
|
||||||
@@ -422,7 +673,13 @@ class MidasParser(Parser):
|
|||||||
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 }` or `extend[S <: T, U] Type { operations }`
|
An extension statement consists of:
|
||||||
|
- the `extend` keyword
|
||||||
|
- a type name (identifier)
|
||||||
|
- (optional) type parameters (see :func:`type_params`)
|
||||||
|
- an opening brace `{`
|
||||||
|
- zero or more member statements (see :func:`member_stmt`)
|
||||||
|
- a closing brace `}`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ExtendStmt: the parsed extension statement
|
ExtendStmt: the parsed extension statement
|
||||||
@@ -447,58 +704,103 @@ class MidasParser(Parser):
|
|||||||
def predicate_declaration(self) -> PredicateStmt:
|
def predicate_declaration(self) -> PredicateStmt:
|
||||||
"""Parse a predicate declaration
|
"""Parse a predicate declaration
|
||||||
|
|
||||||
A predicate is written `predicate Name(subject: Type) = constraint_expression`
|
A predicate statement consists of:
|
||||||
|
- the `predicate` keyword
|
||||||
|
- a name (identifier)
|
||||||
|
- (optional) zero or more parameter specs (see :func:`function_params`)
|
||||||
|
- an equal sign `=`
|
||||||
|
- a body, a constraint expression (see :func:`constraint`)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PredicateStmt: the parsed predicate declaration statement
|
PredicateStmt: the parsed predicate declaration statement
|
||||||
"""
|
"""
|
||||||
keyword: Token = self.previous()
|
keyword: Token = self.previous()
|
||||||
|
|
||||||
name: Token = self.consume_identifier("Expected predicate name")
|
name: Token = self.consume_identifier("Expected predicate name")
|
||||||
self.consume(TokenType.LEFT_PAREN, "Expected '(' before predicate subject")
|
|
||||||
subject: Token = self.consume_identifier("Expected subject name")
|
params: list[ParamSpec] = []
|
||||||
self.consume(TokenType.COLON, "Expected ':' after subject name")
|
while self.check(TokenType.LEFT_PAREN):
|
||||||
type: Type = self.type_expr()
|
params.append(self.function_params())
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after predicate subject")
|
|
||||||
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:
|
def function(self) -> FunctionType:
|
||||||
|
"""Parse a function type expression
|
||||||
|
|
||||||
|
A function consists of:
|
||||||
|
- the `fn` keyword
|
||||||
|
- a parameter spec (see :func:`function_params`)
|
||||||
|
- the arrow keyword `->`
|
||||||
|
- a result type expression (see :func:`type_expr`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FunctionType: the parsed function type expression
|
||||||
|
"""
|
||||||
|
params: ParamSpec = self.function_params()
|
||||||
|
|
||||||
|
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_params(self) -> ParamSpec:
|
||||||
|
"""Parse a parameter spec
|
||||||
|
|
||||||
|
A parameter spec consists of zero or more comma-separated parameters,
|
||||||
|
wrapped in parentheses.
|
||||||
|
|
||||||
|
Like in Python, it can contain positional-only, mixed and keyword-only
|
||||||
|
parameters (separated by `/` and `*`).
|
||||||
|
|
||||||
|
Each parameter has a type (see :func:`type_expr`),
|
||||||
|
preceded by a name (identifier) and a colon `:` (not required for
|
||||||
|
positional-only parameters).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParamSpec: the parsed parameter spec
|
||||||
|
"""
|
||||||
l_paren: Token = self.consume(
|
l_paren: Token = self.consume(
|
||||||
TokenType.LEFT_PAREN, "Expected '(' before function parameters"
|
TokenType.LEFT_PAREN, "Expected '(' before function parameters"
|
||||||
)
|
)
|
||||||
pos_args: list[FunctionType.Argument] = []
|
pos: list[FunctionType.Parameter] = []
|
||||||
args: list[FunctionType.Argument] = []
|
mixed: list[FunctionType.Parameter] = []
|
||||||
kw_args: list[FunctionType.Argument] = []
|
kw: list[FunctionType.Parameter] = []
|
||||||
|
|
||||||
args_first_tokens: list[Token] = []
|
mixed_first_tokens: list[Token] = []
|
||||||
|
|
||||||
section: int = 0
|
section: int = 0
|
||||||
while not self.is_at_end() and not self.check(TokenType.RIGHT_PAREN):
|
while not self.is_at_end() and not self.check(TokenType.RIGHT_PAREN):
|
||||||
match section:
|
match section:
|
||||||
case 0 if self.match(TokenType.SLASH):
|
case 0 if self.match(TokenType.SLASH):
|
||||||
pos_args = args
|
pos = mixed
|
||||||
args = []
|
mixed = []
|
||||||
args_first_tokens = []
|
mixed_first_tokens = []
|
||||||
section = 1
|
section = 1
|
||||||
case 0 | 1 if self.match(TokenType.STAR):
|
case 0 | 1 if self.match(TokenType.STAR):
|
||||||
section = 2
|
section = 2
|
||||||
case _:
|
case _:
|
||||||
# Record first token of mixed argument for errors if unnamed
|
# Record first token of mixed parameters for errors if unnamed
|
||||||
if section != 2:
|
if section != 2:
|
||||||
args_first_tokens.append(self.peek())
|
mixed_first_tokens.append(self.peek())
|
||||||
|
|
||||||
name: Optional[Token] = None
|
name: Optional[Token] = None
|
||||||
if section == 2:
|
if section == 2:
|
||||||
name = self.consume_identifier("Expected keyword argument name")
|
name = self.consume_identifier(
|
||||||
|
"Expected keyword parameter name"
|
||||||
|
)
|
||||||
self.consume(
|
self.consume(
|
||||||
TokenType.COLON, "Expected ':' after argument name"
|
TokenType.COLON, "Expected ':' after parameter name"
|
||||||
)
|
)
|
||||||
elif self.check_identifier() and self.check_next(TokenType.COLON):
|
elif self.check_identifier() and self.check_next(TokenType.COLON):
|
||||||
name = self.advance()
|
name = self.advance()
|
||||||
@@ -506,34 +808,24 @@ class MidasParser(Parser):
|
|||||||
|
|
||||||
type: Type = self.type_expr()
|
type: Type = self.type_expr()
|
||||||
optional: bool = self.match(TokenType.QMARK)
|
optional: bool = self.match(TokenType.QMARK)
|
||||||
arg = FunctionType.Argument(
|
param = FunctionType.Parameter(
|
||||||
location=None,
|
location=None,
|
||||||
name=name,
|
name=name,
|
||||||
type=type,
|
type=type,
|
||||||
required=not optional,
|
required=not optional,
|
||||||
)
|
)
|
||||||
if section == 2:
|
if section == 2:
|
||||||
kw_args.append(arg)
|
kw.append(param)
|
||||||
else:
|
else:
|
||||||
args.append(arg)
|
mixed.append(param)
|
||||||
|
|
||||||
if not self.match(TokenType.COMMA):
|
if not self.match(TokenType.COMMA):
|
||||||
break
|
break
|
||||||
|
|
||||||
for arg, token in zip(args, args_first_tokens):
|
for param, token in zip(mixed, mixed_first_tokens):
|
||||||
if arg.name is None:
|
if param.name is None:
|
||||||
# Not raised because we can keep parsing
|
# Not raised because we can keep parsing
|
||||||
self.error(token, "Unnamed mixed argument")
|
self.error(token, "Unnamed mixed parameter")
|
||||||
|
|
||||||
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after function parameters")
|
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after function parameters")
|
||||||
|
return ParamSpec(l_paren=l_paren, pos=pos, mixed=mixed, kw=kw)
|
||||||
self.consume(TokenType.ARROW, "Expected '->' before result type")
|
|
||||||
result: Type = self.type_expr()
|
|
||||||
|
|
||||||
return FunctionType(
|
|
||||||
location=l_paren.location_to(self.previous()),
|
|
||||||
pos_args=pos_args,
|
|
||||||
args=args,
|
|
||||||
kw_args=kw_args,
|
|
||||||
returns=result,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ from midas.ast.python import (
|
|||||||
ForStmt,
|
ForStmt,
|
||||||
FrameColumn,
|
FrameColumn,
|
||||||
FrameType,
|
FrameType,
|
||||||
|
FromImportStmt,
|
||||||
Function,
|
Function,
|
||||||
GetExpr,
|
GetExpr,
|
||||||
IfStmt,
|
IfStmt,
|
||||||
|
ImportAlias,
|
||||||
|
ImportStmt,
|
||||||
ListExpr,
|
ListExpr,
|
||||||
LiteralExpr,
|
LiteralExpr,
|
||||||
LogicalExpr,
|
LogicalExpr,
|
||||||
MidasType,
|
MidasType,
|
||||||
|
ParamSpec,
|
||||||
RawExpr,
|
RawExpr,
|
||||||
RawStmt,
|
RawStmt,
|
||||||
ReturnStmt,
|
ReturnStmt,
|
||||||
@@ -30,6 +34,7 @@ from midas.ast.python import (
|
|||||||
Stmt,
|
Stmt,
|
||||||
SubscriptExpr,
|
SubscriptExpr,
|
||||||
TernaryExpr,
|
TernaryExpr,
|
||||||
|
TupleExpr,
|
||||||
TypeAssign,
|
TypeAssign,
|
||||||
UnaryExpr,
|
UnaryExpr,
|
||||||
VariableExpr,
|
VariableExpr,
|
||||||
@@ -48,7 +53,10 @@ class UnsupportedSyntaxError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class PythonParser:
|
class PythonParser:
|
||||||
|
"""A parser to convert raw Python `ast` nodes in custom IR nodes"""
|
||||||
|
|
||||||
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] = []
|
||||||
@@ -100,10 +108,34 @@ class PythonParser:
|
|||||||
case ast.For(orelse=[]):
|
case ast.For(orelse=[]):
|
||||||
return self.parse_for(node)
|
return self.parse_for(node)
|
||||||
|
|
||||||
|
case ast.Import(names=imports):
|
||||||
|
return ImportStmt(
|
||||||
|
location=location,
|
||||||
|
imports=self._parse_imports(imports),
|
||||||
|
)
|
||||||
|
|
||||||
|
case ast.ImportFrom(module=module, names=imports, level=level):
|
||||||
|
return FromImportStmt(
|
||||||
|
location=location,
|
||||||
|
module=module,
|
||||||
|
imports=self._parse_imports(imports),
|
||||||
|
level=level,
|
||||||
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
print(f"Unsupported statement: {ast.unparse(node)}")
|
print(f"Unsupported statement: {ast.unparse(node)}")
|
||||||
return RawStmt(location=location, stmt=node)
|
return RawStmt(location=location, stmt=node)
|
||||||
|
|
||||||
|
def _parse_imports(self, imports: list[ast.alias]) -> list[ImportAlias]:
|
||||||
|
return [
|
||||||
|
ImportAlias(
|
||||||
|
location=Location.from_ast(import_),
|
||||||
|
name=import_.name,
|
||||||
|
alias=import_.asname,
|
||||||
|
)
|
||||||
|
for import_ in imports
|
||||||
|
]
|
||||||
|
|
||||||
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] = []
|
||||||
loc: Location = Location.from_ast(node)
|
loc: Location = Location.from_ast(node)
|
||||||
@@ -210,27 +242,10 @@ class PythonParser:
|
|||||||
match node:
|
match node:
|
||||||
case ast.FunctionDef(
|
case ast.FunctionDef(
|
||||||
name=name,
|
name=name,
|
||||||
args=ast.arguments(
|
args=args,
|
||||||
posonlyargs=posonlyargs,
|
|
||||||
args=args,
|
|
||||||
vararg=sink,
|
|
||||||
kwonlyargs=kwonlyargs,
|
|
||||||
kwarg=kw_sink,
|
|
||||||
defaults=defaults,
|
|
||||||
kw_defaults=kw_defaults,
|
|
||||||
),
|
|
||||||
returns=returns,
|
returns=returns,
|
||||||
body=raw_body,
|
body=raw_body,
|
||||||
):
|
):
|
||||||
|
|
||||||
def parse_args(
|
|
||||||
args_list: list[ast.arg], defaults: list[Optional[Expr]]
|
|
||||||
) -> list[Function.Argument]:
|
|
||||||
return [
|
|
||||||
self._parse_function_argument(arg, default)
|
|
||||||
for arg, default in zip(args_list, defaults)
|
|
||||||
]
|
|
||||||
|
|
||||||
body: list[Stmt] = []
|
body: list[Stmt] = []
|
||||||
for stmt in raw_body:
|
for stmt in raw_body:
|
||||||
stmts = self.parse_stmt(stmt)
|
stmts = self.parse_stmt(stmt)
|
||||||
@@ -239,54 +254,58 @@ class PythonParser:
|
|||||||
elif stmts is not None:
|
elif stmts is not None:
|
||||||
body.extend(stmts)
|
body.extend(stmts)
|
||||||
|
|
||||||
parsed_defaults: list[Optional[Expr]] = [
|
|
||||||
self.parse_expr(default) for default in defaults
|
|
||||||
]
|
|
||||||
n_posargs: int = len(posonlyargs)
|
|
||||||
n_args: int = len(args)
|
|
||||||
n_all_posargs = n_posargs + n_args
|
|
||||||
parsed_defaults = [
|
|
||||||
None,
|
|
||||||
] * (n_all_posargs - len(defaults)) + parsed_defaults
|
|
||||||
|
|
||||||
posargs_defaults: list[Optional[Expr]] = parsed_defaults[:n_posargs]
|
|
||||||
args_defaults: list[Optional[Expr]] = parsed_defaults[n_posargs:]
|
|
||||||
kwargs_defaults: list[Optional[Expr]] = [
|
|
||||||
self.parse_expr(default) if default is not None else None
|
|
||||||
for default in kw_defaults
|
|
||||||
]
|
|
||||||
|
|
||||||
return Function(
|
return Function(
|
||||||
location=loc,
|
location=loc,
|
||||||
name=name,
|
name=name,
|
||||||
posonlyargs=parse_args(posonlyargs, posargs_defaults),
|
params=self._parse_param_spec(args),
|
||||||
args=parse_args(args, args_defaults),
|
|
||||||
sink=(
|
|
||||||
self._parse_function_argument(sink, None)
|
|
||||||
if sink is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
kwonlyargs=parse_args(kwonlyargs, kwargs_defaults),
|
|
||||||
kw_sink=(
|
|
||||||
self._parse_function_argument(kw_sink, None)
|
|
||||||
if kw_sink is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
returns=self._parse_type(returns) if returns is not None else None,
|
returns=self._parse_type(returns) if returns is not None else None,
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
print(f"Unsupported function definition: {ast.unparse(node)}")
|
print(f"Unsupported function definition: {ast.unparse(node)}")
|
||||||
|
|
||||||
def _parse_function_argument(
|
def _parse_param_spec(self, args: ast.arguments) -> ParamSpec:
|
||||||
|
def parse_params(
|
||||||
|
args_list: list[ast.arg], defaults: list[Optional[Expr]]
|
||||||
|
) -> list[Function.Parameter]:
|
||||||
|
return [
|
||||||
|
self._parse_function_parameter(arg, default)
|
||||||
|
for arg, default in zip(args_list, defaults)
|
||||||
|
]
|
||||||
|
|
||||||
|
defaults: list[ast.expr] = args.defaults
|
||||||
|
parsed_defaults: list[Optional[Expr]] = [
|
||||||
|
self.parse_expr(default) for default in defaults
|
||||||
|
]
|
||||||
|
n_pos: int = len(args.posonlyargs)
|
||||||
|
n_mixed: int = len(args.args)
|
||||||
|
n_all_pos = n_pos + n_mixed
|
||||||
|
parsed_defaults = [
|
||||||
|
None,
|
||||||
|
] * (n_all_pos - len(defaults)) + parsed_defaults
|
||||||
|
|
||||||
|
pos_defaults: list[Optional[Expr]] = parsed_defaults[:n_pos]
|
||||||
|
mixed_defaults: list[Optional[Expr]] = parsed_defaults[n_pos:]
|
||||||
|
kw_defaults: list[Optional[Expr]] = [
|
||||||
|
self.parse_expr(default) if default is not None else None
|
||||||
|
for default in args.kw_defaults
|
||||||
|
]
|
||||||
|
|
||||||
|
return ParamSpec(
|
||||||
|
pos=parse_params(args.posonlyargs, pos_defaults),
|
||||||
|
mixed=parse_params(args.args, mixed_defaults),
|
||||||
|
kw=parse_params(args.kwonlyargs, kw_defaults),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_function_parameter(
|
||||||
self, arg: ast.arg, default: Optional[Expr]
|
self, arg: ast.arg, default: Optional[Expr]
|
||||||
) -> Function.Argument:
|
) -> Function.Parameter:
|
||||||
loc: Location = Location.from_ast(arg)
|
loc: Location = Location.from_ast(arg)
|
||||||
name: str = arg.arg
|
name: str = arg.arg
|
||||||
type: Optional[MidasType] = None
|
type: Optional[MidasType] = None
|
||||||
if arg.annotation is not None:
|
if arg.annotation is not None:
|
||||||
type = self._parse_type(arg.annotation)
|
type = self._parse_type(arg.annotation)
|
||||||
return Function.Argument(
|
return Function.Parameter(
|
||||||
location=loc,
|
location=loc,
|
||||||
name=name,
|
name=name,
|
||||||
type=type,
|
type=type,
|
||||||
@@ -299,26 +318,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(
|
||||||
@@ -344,7 +365,7 @@ class PythonParser:
|
|||||||
return BaseType(
|
return BaseType(
|
||||||
location=loc,
|
location=loc,
|
||||||
base="None",
|
base="None",
|
||||||
param=None,
|
args=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
@@ -359,7 +380,7 @@ class PythonParser:
|
|||||||
for col in cols:
|
for col in cols:
|
||||||
columns.append(self._parse_frame_column(col))
|
columns.append(self._parse_frame_column(col))
|
||||||
|
|
||||||
case ast.Slice() | ast.Name():
|
case ast.Slice() | ast.Name() | ast.Subscript():
|
||||||
columns.append(self._parse_frame_column(schema))
|
columns.append(self._parse_frame_column(schema))
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
@@ -370,7 +391,7 @@ class PythonParser:
|
|||||||
def _parse_frame_column(self, column: ast.expr) -> FrameColumn:
|
def _parse_frame_column(self, column: ast.expr) -> FrameColumn:
|
||||||
loc: Location = Location.from_ast(column)
|
loc: Location = Location.from_ast(column)
|
||||||
match column:
|
match column:
|
||||||
case ast.Name():
|
case ast.Name() | ast.Subscript():
|
||||||
return FrameColumn(
|
return FrameColumn(
|
||||||
location=loc,
|
location=loc,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -423,6 +444,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)
|
||||||
|
|
||||||
@@ -473,6 +497,12 @@ class PythonParser:
|
|||||||
step=self.parse_expr(step) if step 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 _:
|
||||||
print(f"Unsupported expression: {ast.unparse(node)}")
|
print(f"Unsupported expression: {ast.unparse(node)}")
|
||||||
return RawExpr(location=location, expr=node)
|
return RawExpr(location=location, expr=node)
|
||||||
@@ -527,16 +557,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:
|
||||||
|
|||||||
54
midas/typing.py
Normal file
54
midas/typing.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import Generic, TypeVar
|
||||||
|
from typing import cast as typing_cast
|
||||||
|
|
||||||
|
from pandas import DataFrame, Series
|
||||||
|
|
||||||
|
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(DataFrame, Generic[T]):
|
||||||
|
"""A `Frame` is the abstract type implemented by `DataFrame`
|
||||||
|
|
||||||
|
A frame contains any number of named columns (see :class:`Column`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Column(Series, Generic[T]):
|
||||||
|
"""A `Column` is the abstract type implemented by `Series`
|
||||||
|
|
||||||
|
A column contains a any number of values of the same type
|
||||||
|
"""
|
||||||
@@ -3,6 +3,7 @@ from typing import Any, Callable, Optional
|
|||||||
|
|
||||||
import midas.ast.python as p
|
import midas.ast.python as p
|
||||||
from midas.checker.types import Type
|
from midas.checker.types import Type
|
||||||
|
from midas.generator.collector import AssertionCollector
|
||||||
|
|
||||||
AllowRepeat = Callable[[object], bool]
|
AllowRepeat = Callable[[object], bool]
|
||||||
|
|
||||||
@@ -62,3 +63,5 @@ class UniversalJSONDumper:
|
|||||||
class TypedAST:
|
class TypedAST:
|
||||||
stmts: list[p.Stmt]
|
stmts: list[p.Stmt]
|
||||||
judgements: list[tuple[p.Expr, Type]]
|
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"
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
identifier ::= '[a-zA-Z][a-zA-Z_]*'
|
Identifier ::= '[a-zA-Z][a-zA-Z_]*'
|
||||||
|
|
||||||
integer ::= '\d+'
|
TypeArgs ::= "[" (Type ("," Type)*)? "]"
|
||||||
number ::= integer ["." integer]
|
|
||||||
boolean ::= "False" | "True"
|
|
||||||
none ::= "None"
|
|
||||||
|
|
||||||
value ::= number | boolean | none
|
FrameColumn ::= ((Identifier | "_") ":")? Type
|
||||||
lambda-value ::= "_" | value
|
FrameSchema ::= "[" (FrameColumn ("," FrameColumn)*)? "]"
|
||||||
lambda-operator ::= ">" | "<" | ">=" | "<=" | "==" | "!="
|
|
||||||
lambda ::= lambda-value lambda-operator lambda-value
|
|
||||||
|
|
||||||
constraint ::= identifier | "(" lambda ")"
|
Type ::= "Frame" FrameSchema | Identifier TypeArgs?
|
||||||
base-type ::= identifier
|
|
||||||
type ::= base-type { "+" constraint }
|
|
||||||
|
|
||||||
column-type ::= type | "_"
|
|
||||||
column-def ::= [ identifier ":" ] column-type
|
|
||||||
|
|
||||||
frame-def ::= column-def { "," column-def }
|
|
||||||
|
|||||||
@@ -1,64 +1,72 @@
|
|||||||
#import "@preview/fervojo:0.1.1": render
|
#import "@preview/fervojo:0.1.1": default-css, render
|
||||||
|
|
||||||
#let value = ```
|
#let extra-css = ```css
|
||||||
{[`value` <
|
svg.railroad .terminal rect {
|
||||||
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
|
fill: #F7DCD4;
|
||||||
[`boolean` <"False", "True">],
|
}
|
||||||
[`none` "None"]
|
```
|
||||||
|
#let css = default-css() + bytes(extra-css.text)
|
||||||
|
|
||||||
|
#let type-args = ```
|
||||||
|
{[`type-args` "[" <!, 'type'*","> "]"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let frame-schema = ```
|
||||||
|
{[`frame-schema` "[" <!, [[<'identifier', "_"> ":"]? 'type']*","> "]"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let type = ```
|
||||||
|
{[`type` <
|
||||||
|
["Frame" 'frame-schema'],
|
||||||
|
['identifier' <!, 'type-args'>]
|
||||||
>]}
|
>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let constraint = ```
|
|
||||||
{[`constraint` <"_", 'value'> <">", "<", ">=", "<=", "==", "!="> <"_", 'value'>]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let type-with-constraints = ```
|
|
||||||
{[`type-with-constraints` 'identifier' <!, ["+" "(" 'constraint' ")"] * !>]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let column-def = ```
|
|
||||||
{[`column-def` <!, ['identifier' ":"]> <"_", 'type-with-constraints'>]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let frame-def = ```
|
|
||||||
{[`frame-def` 'column-def' * ","]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let annotation = ```
|
|
||||||
{[`annotation` 'identifier' <!, ["[" 'frame-def' "]"]>]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let rules = (
|
#let rules = (
|
||||||
value,
|
type-args: type-args,
|
||||||
constraint,
|
frame-schema: frame-schema,
|
||||||
type-with-constraints,
|
type: type,
|
||||||
column-def,
|
)
|
||||||
frame-def,
|
|
||||||
annotation,
|
#let inline = (
|
||||||
|
"type-args",
|
||||||
|
"frame-schema",
|
||||||
)
|
)
|
||||||
|
|
||||||
#set text(font: "Source Sans 3")
|
#set text(font: "Source Sans 3")
|
||||||
|
|
||||||
= Type annotation syntax
|
#title[Supported Python annotation syntax]
|
||||||
|
|
||||||
#for rule in rules {
|
= Outline
|
||||||
render(rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
#box(
|
||||||
#let by-name = (
|
columns(
|
||||||
annotation: annotation,
|
2,
|
||||||
frame-def: frame-def,
|
outline(title: none),
|
||||||
column-def: column-def,
|
),
|
||||||
type-with-constraints: type-with-constraints,
|
height: 9cm,
|
||||||
constraint: constraint,
|
stroke: 1pt,
|
||||||
value: value,
|
inset: 1em,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
= Statements and expressions
|
||||||
|
|
||||||
|
#for (name, rule) in rules.pairs().rev() {
|
||||||
|
[== #name]
|
||||||
|
render(rule, css: css)
|
||||||
|
}
|
||||||
|
|
||||||
#let substitute(base-rule) = {
|
#let substitute(base-rule) = {
|
||||||
let new-rule = base-rule
|
let new-rule = base-rule
|
||||||
for (key, rule) in by-name.pairs() {
|
for name in inline {
|
||||||
new-rule = new-rule.replace("'" + key + "'", rule.text.slice(1, -1))
|
let rule = rules.at(name)
|
||||||
|
let replacement = rule.text.slice(1, -1).replace(regex("\[`.*?`"), "[")
|
||||||
|
replacement = "[" + replacement + "#`" + name + "`]"
|
||||||
|
new-rule = new-rule.replace(
|
||||||
|
"'" + name + "'",
|
||||||
|
replacement,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if new-rule != base-rule {
|
if new-rule != base-rule {
|
||||||
new-rule = substitute(new-rule)
|
new-rule = substitute(new-rule)
|
||||||
@@ -66,9 +74,16 @@
|
|||||||
return new-rule
|
return new-rule
|
||||||
}
|
}
|
||||||
|
|
||||||
#let combined = raw(substitute(annotation.text))
|
|
||||||
|
|
||||||
|
|
||||||
#set page(flipped: true)
|
#set page(flipped: true)
|
||||||
#render(combined)
|
|
||||||
*/
|
|
||||||
|
= Combined rules
|
||||||
|
|
||||||
|
#for (name, rule) in rules.pairs() {
|
||||||
|
if not name in inline {
|
||||||
|
[== #name]
|
||||||
|
let combined = substitute(rule.text)
|
||||||
|
render(raw(combined), css: css)
|
||||||
|
//raw(block: true, combined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,40 +4,85 @@ Identifier ::= [a-zA-Z_] [a-zA-Z_0-9]*
|
|||||||
Integer ::= '\d+'
|
Integer ::= '\d+'
|
||||||
Number ::= "-"? Integer ("." Integer)?
|
Number ::= "-"? Integer ("." Integer)?
|
||||||
Boolean ::= "False" | "True"
|
Boolean ::= "False" | "True"
|
||||||
|
String ::= '(".*?")|(\'.*?\')'
|
||||||
None ::= "None"
|
None ::= "None"
|
||||||
|
|
||||||
Value ::= Number | Boolean | None
|
Literal ::= Number | Boolean | String | None
|
||||||
|
|
||||||
|
UnaryOp ::= "+" | "-" | "!"
|
||||||
|
FactorOp ::= "*" | "/"
|
||||||
|
TermOp ::= "+" | "-"
|
||||||
ComparisonOp ::= ">" | "<" | ">=" | "<="
|
ComparisonOp ::= ">" | "<" | ">=" | "<="
|
||||||
EqualityOp ::= "==" | "!="
|
EqualityOp ::= "==" | "!="
|
||||||
|
|
||||||
Grouping ::= "(" Constraint ")"
|
PosArg ::= Expression
|
||||||
Primary ::= "_" | Value | Identifier | Grouping
|
KwArg ::= Identifier "=" Expression
|
||||||
|
|
||||||
|
PosArgs ::= PosArg ("," PosArg)*
|
||||||
|
KwArgs ::= KwArg ("," KwArg)*
|
||||||
|
Args ::= (
|
||||||
|
PosArgs
|
||||||
|
| KwArgs
|
||||||
|
| PosArgs "," KwArgs
|
||||||
|
)
|
||||||
|
|
||||||
|
Grouping ::= "(" Expression ")"
|
||||||
|
Primary ::= "_" | Literal | Identifier | Grouping
|
||||||
Reference ::= Primary ("." Identifier)*
|
Reference ::= Primary ("." Identifier)*
|
||||||
Unary ::= "-"? Unary | Reference
|
CallArgs ::= "(" Args ")"
|
||||||
Comparison ::= Unary (ComparisonOp Unary)*
|
Call ::= Reference CallArgs*
|
||||||
|
Unary ::= UnaryOp Unary | Call
|
||||||
|
Factor ::= Unary (FactorOp Unary)*
|
||||||
|
Term ::= Factor (TermOp Factor)*
|
||||||
|
Comparison ::= Term (ComparisonOp Term)*
|
||||||
Equality ::= Comparison (EqualityOp Comparison)*
|
Equality ::= Comparison (EqualityOp Comparison)*
|
||||||
Constraint ::= Equality ("&" Equality)*
|
Expression ::= Equality ("&" Equality)*
|
||||||
|
Constraint ::= Expression
|
||||||
|
|
||||||
TemplateParam ::= Identifier ("<:" Type)?
|
TemplateParam ::= Identifier ("<:" Type)?
|
||||||
Template ::= "[" (TemplateParam ("," TemplateParam)*)? "]"
|
Template ::= "[" (TemplateParam ("," TemplateParam)*)? "]"
|
||||||
|
|
||||||
|
ParamType ::= Type "?"?
|
||||||
|
PosParam ::= (Identifier ":")? ParamType
|
||||||
|
KwParam ::= Identifier ":" ParamType
|
||||||
|
|
||||||
|
PosParams ::= (
|
||||||
|
(PosParam ("," PosParam)* ("," "/")?)
|
||||||
|
| "/"
|
||||||
|
)
|
||||||
|
MixedParams ::= KwParam ("," KwParam)
|
||||||
|
KwParams ::= (
|
||||||
|
(("*", ",")? KwParam ("," KwParam)*)
|
||||||
|
| "*"
|
||||||
|
)
|
||||||
|
Params ::= (
|
||||||
|
PosParams
|
||||||
|
| MixedParams
|
||||||
|
| KwParams
|
||||||
|
| (PosParams "," MixedParams)
|
||||||
|
| (PosParams "," KwParams)
|
||||||
|
| (MixedParams "," KwParams)
|
||||||
|
| (PosParams "," MixedParams "," KwParams)
|
||||||
|
)
|
||||||
|
ParamSpec ::= "(" Params? ")"
|
||||||
|
|
||||||
TypeProperty ::= Identifier ":" Type
|
|
||||||
ComplexType ::= "{" TypeProperty* "}"
|
|
||||||
NamedType ::= Identifier
|
NamedType ::= Identifier
|
||||||
TypeParams ::= "[" (Type ("," Type)*)? "]"
|
TypeArgs ::= "[" (Type ("," Type)*)? "]"
|
||||||
GenericType ::= NamedType TypeParams?
|
FrameColumn ::= TOKEN ":" Type
|
||||||
|
FrameSchema ::= "[" (FrameColumn ("," FrameColumn)*)? "]"
|
||||||
|
GenericType ::= "Frame" FrameSchema | NamedType TypeArgs?
|
||||||
GroupedType ::= "(" Type ")"
|
GroupedType ::= "(" Type ")"
|
||||||
BaseType ::= GroupedType | ComplexType | GenericType
|
BaseType ::= GroupedType | GenericType
|
||||||
ConstraintType ::= BaseType ("where" Constraint)?
|
ConstraintType ::= BaseType ("where" Constraint)?
|
||||||
|
FuncType ::= "fn" ParamSpec "->" Type
|
||||||
Type ::= ConstraintType
|
Type ::= ConstraintType
|
||||||
|
|
||||||
OpDefinition ::= "op" Identifier "(" Type ")" "->" Type
|
MemberStatement ::= ("prop" | "def") Identifier ":" Type
|
||||||
ExtendBody ::= "{" OpDefinition* "}"
|
ExtendBody ::= "{" MemberStatement* "}"
|
||||||
|
|
||||||
|
AliasStatement ::= "alias" Identifier "=" Type
|
||||||
TypeStatement ::= "type" Identifier Template? "=" Type
|
TypeStatement ::= "type" Identifier Template? "=" Type
|
||||||
ExtendStatement ::= "extend" Type ExtendBody
|
ExtendStatement ::= "extend" Type ExtendBody
|
||||||
PredicateStatement ::= "predicate" Identifier "(" Identifier ":" Type ")" "=" Constraint
|
PredicateStatement ::= "predicate" Identifier ParamSpec* "=" Constraint
|
||||||
|
|
||||||
Statement ::= TypeStatement | ExtendStatement | PredicateStatement
|
Statement ::= AliasStatement | TypeStatement | ExtendStatement | PredicateStatement
|
||||||
|
|||||||
118
syntax/midas.typ
118
syntax/midas.typ
@@ -7,40 +7,61 @@ svg.railroad .terminal rect {
|
|||||||
```
|
```
|
||||||
#let css = default-css() + bytes(extra-css.text)
|
#let css = default-css() + bytes(extra-css.text)
|
||||||
|
|
||||||
#let value = ```
|
#let literal = ```
|
||||||
{[`value` <
|
{[`literal` <
|
||||||
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
|
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
|
||||||
[`boolean` <"False", "True">],
|
[`boolean` <"False", "True">],
|
||||||
|
[`string` <["\"" 'char'*! "\""], ["'" 'char'*! "'"]>],
|
||||||
[`none` "None"]
|
[`none` "None"]
|
||||||
>]}
|
>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let grouping = ```
|
#let grouping = ```
|
||||||
{[`grouping` "(" 'constraint' ")"]}
|
{[`grouping` "(" 'expression' ")"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let primary = ```
|
#let primary = ```
|
||||||
{[`primary` <"_", 'value', 'identifier', 'grouping'>]}
|
{[`primary` <"_", 'literal', 'identifier', 'grouping'>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let reference = ```
|
#let reference = ```
|
||||||
{[`reference` 'primary' <!, ["." 'identifier']*!>]}
|
{[`reference` 'primary' <!, ["." 'identifier']*!>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#let call-args = ```
|
||||||
|
{[`call-args` "(" <!, <'expression', ['identifier' "=" 'expression']>*","#`Same rules as Python`> ")"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let call = ```
|
||||||
|
{[`call` 'reference' <!, 'call-args'*!>]}
|
||||||
|
```
|
||||||
|
|
||||||
#let unary = ```
|
#let unary = ```
|
||||||
{[`unary` <[<!, "-"> 'unary'], 'reference'>]}
|
{[`unary` <[<"+", "-", "!"> 'unary'], 'call'>]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let factor = ```
|
||||||
|
{[`factor` 'unary'*<"*", "/">]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let term = ```
|
||||||
|
{[`term` 'factor'*<"+", "-">]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let comparison = ```
|
#let comparison = ```
|
||||||
{[`comparison` 'unary'*<">", "<", ">=", "<=">]}
|
{[`comparison` 'term'*<">", "<", ">=", "<=">]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let equality = ```
|
#let equality = ```
|
||||||
{[`equality` 'comparison'*<"==", "!=">]}
|
{[`equality` 'comparison'*<"==", "!=">]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#let expression = ```
|
||||||
|
{[`expression` 'equality'*"&"]}
|
||||||
|
```
|
||||||
|
|
||||||
#let constraint = ```
|
#let constraint = ```
|
||||||
{[`constraint` 'equality'*"&"]}
|
{[`constraint` 'expression']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let template-param = ```
|
#let template-param = ```
|
||||||
@@ -51,24 +72,20 @@ svg.railroad .terminal rect {
|
|||||||
{[`template` "[" <!, 'template-param'*","> "]"]}
|
{[`template` "[" <!, 'template-param'*","> "]"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-property = ```
|
|
||||||
{[`type-property` 'identifier' ":" 'type']}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let complex-type = ```
|
|
||||||
{[`complex-type` "{" <!, 'type-property'*!> "}"]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#let named-type = ```
|
#let named-type = ```
|
||||||
{[`named-type` 'identifier']}
|
{[`named-type` 'identifier']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-params = ```
|
#let type-args = ```
|
||||||
{[`type-params` "[" <!, 'type'*","> "]"]}
|
{[`type-args` "[" <!, 'type'*","> "]"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let frame-schema = ```
|
||||||
|
{[`frame-schema` "[" <!, ['TOKEN' ":" 'type']*","> "]"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let generic-type = ```
|
#let generic-type = ```
|
||||||
{[`generic-type` 'named-type' <!, 'type-params'>]}
|
{[`generic-type` <["Frame" 'frame-schema'], ['named-type' <!, 'type-args'>]>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let grouped-type = ```
|
#let grouped-type = ```
|
||||||
@@ -76,59 +93,88 @@ svg.railroad .terminal rect {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#let base-type = ```
|
#let base-type = ```
|
||||||
{[`base-type` <'grouped-type', 'complex-type', 'generic-type'>]}
|
{[`base-type` <'grouped-type', 'generic-type'>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let constraint-type = ```
|
#let constraint-type = ```
|
||||||
{[`constraint-type` 'base-type' <!, ["where" 'constraint']>]}
|
{[`constraint-type` 'base-type' <!, ["where" 'constraint']>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#let pos-param = ```
|
||||||
|
{[`pos-param` <!, ['identifier' ":"]> 'type' <!, "?">]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let kw-param = ```
|
||||||
|
{[`kw-param` 'identifier' ":" 'type' <!, "?">]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let param-spec = ```
|
||||||
|
{[`param-spec` "(" <!, <'pos-param', "/", "*", 'kw-param'>*",">#`Same rules as Python` ")"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let func-type = ```
|
||||||
|
{[`func-type` "fn" 'param-spec' "->" 'type']}
|
||||||
|
```
|
||||||
|
|
||||||
#let type = ```
|
#let type = ```
|
||||||
{[`type` 'constraint-type']}
|
{[`type` <'func-type', 'constraint-type'>]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#let alias-statement = ```
|
||||||
|
{[`alias-statement` "alias" 'identifier' "=" 'type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let type-statement = ```
|
#let type-statement = ```
|
||||||
{[`type-statement` "type" 'identifier' <!, 'template'> "=" 'type']}
|
{[`type-statement` "type" 'identifier' <!, 'template'> "=" 'type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let op-definition = ```
|
#let member-stmt = ```
|
||||||
{[`op-definition` "op" 'identifier' "(" 'type' ")" "->" 'type']}
|
{[`member-stmt` <"prop", "def"> 'identifier' ":" 'type']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let extend-statement = ```
|
#let extend-statement = ```
|
||||||
{[`extend-statement` "extend" 'type' "{" <!, 'op-definition'*!> "}"]}
|
{[`extend-statement` "extend" 'type' "{" <!, 'member-stmt'*!> "}"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let predicate-statement = ```
|
#let predicate-statement = ```
|
||||||
{[`predicate-statement` "predicate" 'identifier' "(" 'identifier' ":" 'type' ")" "=" 'constraint']}
|
{[`predicate-statement` "predicate" 'identifier' <!, 'param-spec'*!> "=" 'constraint']}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let statement = ```
|
#let statement = ```
|
||||||
{[`statement` <'type-statement', 'extend-statement', 'predicate-statement'>]}
|
{[`statement` <'alias-statement', 'type-statement', 'extend-statement', 'predicate-statement'>]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#let rules = (
|
#let rules = (
|
||||||
value: value,
|
literal: literal,
|
||||||
grouping: grouping,
|
grouping: grouping,
|
||||||
primary: primary,
|
primary: primary,
|
||||||
reference: reference,
|
reference: reference,
|
||||||
|
call-args: call-args,
|
||||||
|
call: call,
|
||||||
unary: unary,
|
unary: unary,
|
||||||
|
factor: factor,
|
||||||
|
term: term,
|
||||||
comparison: comparison,
|
comparison: comparison,
|
||||||
equality: equality,
|
equality: equality,
|
||||||
|
expression: expression,
|
||||||
constraint: constraint,
|
constraint: constraint,
|
||||||
template-param: template-param,
|
template-param: template-param,
|
||||||
template: template,
|
template: template,
|
||||||
type-property: type-property,
|
type-property: type-property,
|
||||||
complex-type: complex-type,
|
|
||||||
named-type: named-type,
|
named-type: named-type,
|
||||||
type-params: type-params,
|
type-args: type-args,
|
||||||
generic-type: generic-type,
|
generic-type: generic-type,
|
||||||
grouped-type: grouped-type,
|
grouped-type: grouped-type,
|
||||||
base-type: base-type,
|
base-type: base-type,
|
||||||
constraint-type: constraint-type,
|
constraint-type: constraint-type,
|
||||||
|
pos-param: pos-param,
|
||||||
|
kw-param: kw-param,
|
||||||
|
param-spec: param-spec,
|
||||||
|
func-type: func-type,
|
||||||
type: type,
|
type: type,
|
||||||
|
alias-statement: alias-statement,
|
||||||
type-statement: type-statement,
|
type-statement: type-statement,
|
||||||
op-definition: op-definition,
|
member-stmt: member-stmt,
|
||||||
extend-statement: extend-statement,
|
extend-statement: extend-statement,
|
||||||
predicate-statement: predicate-statement,
|
predicate-statement: predicate-statement,
|
||||||
statement: statement,
|
statement: statement,
|
||||||
@@ -136,18 +182,22 @@ svg.railroad .terminal rect {
|
|||||||
|
|
||||||
#let inline = (
|
#let inline = (
|
||||||
"grouping",
|
"grouping",
|
||||||
"value",
|
"literal",
|
||||||
"template-param",
|
"template-param",
|
||||||
"template",
|
"template",
|
||||||
"type-property",
|
"type-property",
|
||||||
"complex-type",
|
"call-args",
|
||||||
"type-params",
|
"type-args",
|
||||||
"named-type",
|
"named-type",
|
||||||
"grouped-type",
|
"grouped-type",
|
||||||
"generic-type",
|
"generic-type",
|
||||||
"base-type",
|
"base-type",
|
||||||
"constraint-type",
|
"constraint-type",
|
||||||
"op-definition",
|
"pos-param",
|
||||||
|
"kw-param",
|
||||||
|
"func-type",
|
||||||
|
"member-stmt",
|
||||||
|
"alias-statement",
|
||||||
"type-statement",
|
"type-statement",
|
||||||
"extend-statement",
|
"extend-statement",
|
||||||
"predicate-statement",
|
"predicate-statement",
|
||||||
@@ -164,7 +214,7 @@ svg.railroad .terminal rect {
|
|||||||
2,
|
2,
|
||||||
outline(title: none),
|
outline(title: none),
|
||||||
),
|
),
|
||||||
height: 9cm,
|
height: 15cm,
|
||||||
stroke: 1pt,
|
stroke: 1pt,
|
||||||
inset: 1em,
|
inset: 1em,
|
||||||
)
|
)
|
||||||
|
|||||||
49
tests/__main__.py
Normal file
49
tests/__main__.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from midas.cli.ansi import Ansi
|
||||||
|
from tests.base import Tester, TestsSummary
|
||||||
|
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]) -> TestsSummary:
|
||||||
|
print_banner(tester_cls.__name__)
|
||||||
|
tester: Tester = tester_cls()
|
||||||
|
summary: TestsSummary = tester.run_all_tests()
|
||||||
|
print()
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
testers: list[Type[Tester]] = [
|
||||||
|
PythonTester,
|
||||||
|
MidasTester,
|
||||||
|
CheckerTester,
|
||||||
|
GeneratorTester,
|
||||||
|
]
|
||||||
|
|
||||||
|
summaries: list[TestsSummary] = list(
|
||||||
|
map(run_tests, testers)
|
||||||
|
) # list to avoid early stop
|
||||||
|
summary: TestsSummary = TestsSummary.concat(*summaries)
|
||||||
|
|
||||||
|
if summary.success:
|
||||||
|
print(Ansi.FG(Ansi.BRIGHT_GREEN) + "All tests passed!" + Ansi.RESET)
|
||||||
|
else:
|
||||||
|
print(Ansi.FG(Ansi.BRIGHT_RED) + "Some tests failed!" + Ansi.RESET)
|
||||||
|
summary.print()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -4,14 +4,54 @@ import argparse
|
|||||||
import difflib
|
import difflib
|
||||||
import sys
|
import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
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: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestsSummary:
|
||||||
|
tests: list[tuple[str, bool]] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return all(map(lambda t: t[1], self.tests))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successes(self) -> int:
|
||||||
|
return sum(map(lambda t: int(t[1]), self.tests))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failures(self) -> int:
|
||||||
|
return len(self.tests) - self.successes
|
||||||
|
|
||||||
|
def add(self, name: str, success: bool):
|
||||||
|
self.tests.append((name, success))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def concat(*summaries: TestsSummary) -> TestsSummary:
|
||||||
|
return TestsSummary(
|
||||||
|
tests=sum(
|
||||||
|
map(lambda s: s.tests, summaries),
|
||||||
|
start=[],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def print(self):
|
||||||
|
print("Tests summary:")
|
||||||
|
tests: list[tuple[str, bool]] = sorted(self.tests, key=lambda t: t[0])
|
||||||
|
for test, success in tests:
|
||||||
|
print(f" - [{'.' if success else 'X'}] {test}")
|
||||||
|
print("-" * 20)
|
||||||
|
print(f"passed: {self.successes}, failed: {self.failures}")
|
||||||
|
|
||||||
|
|
||||||
class Tester(ABC):
|
class Tester(ABC):
|
||||||
"""A test runner to check for regressions in the lexer and parser"""
|
"""A test runner to check for regressions in the lexer and parser"""
|
||||||
|
|
||||||
@@ -32,37 +72,37 @@ class Tester(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _list_tests(self) -> list[Path]: ...
|
def _list_tests(self) -> list[Path]: ...
|
||||||
|
|
||||||
def run_all_tests(self) -> bool:
|
def run_all_tests(self) -> TestsSummary:
|
||||||
paths: list[Path] = sorted(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]) -> TestsSummary:
|
||||||
rule: str = "-" * 80
|
rule: str = "-" * 80
|
||||||
n: int = len(tests)
|
n: int = len(tests)
|
||||||
successes: int = 0
|
|
||||||
failures: int = 0
|
summary: TestsSummary = TestsSummary()
|
||||||
|
|
||||||
print(rule)
|
print(rule)
|
||||||
for i, test in enumerate(tests):
|
for i, test in enumerate(tests):
|
||||||
print(f"Case {i+1}/{n}: {test.resolve().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}")
|
||||||
success: bool = self._run_test(test)
|
success: bool = self._run_test(test)
|
||||||
if success:
|
summary.add(str(path), success)
|
||||||
successes += 1
|
|
||||||
else:
|
|
||||||
failures += 1
|
|
||||||
|
|
||||||
print(rule)
|
print(rule)
|
||||||
print(f"Success: {successes}/{n}")
|
print(f"Success: {summary.successes}/{n}")
|
||||||
print(f"Failed: {failures}/{n}")
|
print(f"Failed: {summary.failures}/{n}")
|
||||||
print(rule)
|
print(rule)
|
||||||
return failures == 0
|
return summary
|
||||||
|
|
||||||
def _run_test(self, path: Path) -> bool:
|
def _run_test(self, path: Path) -> bool:
|
||||||
result_path: Path = self._result_path(path)
|
result_path: Path = self._result_path(path)
|
||||||
if not result_path.exists():
|
if not result_path.exists():
|
||||||
print("Missing snapshot. Please run the update command first")
|
print("Missing snapshot. Please run the update command first")
|
||||||
return False
|
return False
|
||||||
|
print(Ansi.DIM, end="")
|
||||||
result: CaseResult = self._exec_case(path)
|
result: CaseResult = self._exec_case(path)
|
||||||
|
print(Ansi.RESET, end="")
|
||||||
expected: str = result_path.read_text()
|
expected: str = result_path.read_text()
|
||||||
actual: str = result.dumps()
|
actual: str = result.dumps()
|
||||||
|
|
||||||
@@ -138,16 +178,19 @@ class Tester(ABC):
|
|||||||
else:
|
else:
|
||||||
tester.update_tests(args.FILE)
|
tester.update_tests(args.FILE)
|
||||||
case "run":
|
case "run":
|
||||||
success: bool
|
summary: TestsSummary
|
||||||
if args.all:
|
if args.all:
|
||||||
success = tester.run_all_tests()
|
summary = tester.run_all_tests()
|
||||||
else:
|
else:
|
||||||
success = tester.run_tests(args.FILE)
|
summary = tester.run_tests(args.FILE)
|
||||||
if not success:
|
if not summary.success:
|
||||||
|
summary.print()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
case None:
|
case None:
|
||||||
print("No subcommand provided. Available subcommands: run, update")
|
summary: TestsSummary = tester.run_all_tests()
|
||||||
sys.exit(1)
|
if not summary.success:
|
||||||
|
summary.print()
|
||||||
|
sys.exit(1)
|
||||||
case _:
|
case _:
|
||||||
print(f"Unknown subcommand '{args.subcommand}'")
|
print(f"Unknown subcommand '{args.subcommand}'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -4,7 +4,35 @@
|
|||||||
"type": "Warning",
|
"type": "Warning",
|
||||||
"location": {
|
"location": {
|
||||||
"start": [
|
"start": [
|
||||||
6,
|
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
|
4
|
||||||
],
|
],
|
||||||
"end": [
|
"end": [
|
||||||
@@ -12,7 +40,7 @@
|
|||||||
5
|
5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"message": "FrameType not yet supported"
|
"message": "Unknown type '_'"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"judgments": []
|
"judgments": []
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,19 @@
|
|||||||
{
|
{
|
||||||
"diagnostics": [],
|
"diagnostics": [],
|
||||||
"judgments": [
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L4:30",
|
||||||
|
"to": "L4:36"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 123.45
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"location": {
|
"location": {
|
||||||
"from": "L4:18",
|
"from": "L4:18",
|
||||||
@@ -11,12 +24,13 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"_type": "BaseType",
|
"_type": "BaseType",
|
||||||
"base": "Meter",
|
"base": "Meter",
|
||||||
"param": null
|
"args": []
|
||||||
},
|
},
|
||||||
"expr": {
|
"expr": {
|
||||||
"_type": "LiteralExpr",
|
"_type": "LiteralExpr",
|
||||||
"value": 123.45
|
"value": 123.45
|
||||||
}
|
},
|
||||||
|
"unsafe": false
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"name": "Meter",
|
"name": "Meter",
|
||||||
@@ -25,6 +39,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L5:28",
|
||||||
|
"to": "L5:31"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 6.7
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"location": {
|
"location": {
|
||||||
"from": "L5:15",
|
"from": "L5:15",
|
||||||
@@ -35,12 +62,13 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"_type": "BaseType",
|
"_type": "BaseType",
|
||||||
"base": "Second",
|
"base": "Second",
|
||||||
"param": null
|
"args": []
|
||||||
},
|
},
|
||||||
"expr": {
|
"expr": {
|
||||||
"_type": "LiteralExpr",
|
"_type": "LiteralExpr",
|
||||||
"value": 6.7
|
"value": 6.7
|
||||||
}
|
},
|
||||||
|
"unsafe": false
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"name": "Second",
|
"name": "Second",
|
||||||
|
|||||||
@@ -100,41 +100,6 @@
|
|||||||
"name": "float"
|
"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": {
|
"location": {
|
||||||
"from": "L11:13",
|
"from": "L11:13",
|
||||||
@@ -161,6 +126,45 @@
|
|||||||
"name": "float"
|
"name": "float"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L11:5",
|
||||||
|
"to": "L11:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "maximum"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"params": {
|
||||||
|
"pos": [],
|
||||||
|
"mixed": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "a",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "b",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"location": {
|
"location": {
|
||||||
"from": "L11:5",
|
"from": "L11:5",
|
||||||
|
|||||||
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 = object()
|
||||||
|
covariant: Covariant = object()
|
||||||
|
contravariant: Contravariant = object()
|
||||||
|
invariant: Invariant = object()
|
||||||
|
coco: Coco = object()
|
||||||
|
cocontra: Cocontra = object()
|
||||||
|
contraco: Contraco = object()
|
||||||
|
contracontra: Contracontra = object()
|
||||||
|
t1: T1 = object()
|
||||||
|
t2: T2 = object()
|
||||||
|
|
||||||
|
# Dummy print to prudce judgements for the expressions
|
||||||
|
print(
|
||||||
|
unused,
|
||||||
|
covariant,
|
||||||
|
contravariant,
|
||||||
|
invariant,
|
||||||
|
coco,
|
||||||
|
cocontra,
|
||||||
|
contraco,
|
||||||
|
contracontra,
|
||||||
|
t1,
|
||||||
|
t2,
|
||||||
|
)
|
||||||
|
|
||||||
|
cov1: Covariant[float] = object()
|
||||||
|
cov2: Covariant[int] = object()
|
||||||
|
cov1 = cov2 # Ok because int <: float => Covariant[int] <: Covariant[float]
|
||||||
|
cov2 = cov1 # Invalid
|
||||||
|
|
||||||
|
contra1: Contravariant[float] = object()
|
||||||
|
contra2: Contravariant[int] = object()
|
||||||
|
contra1 = contra2 # Invalid
|
||||||
|
contra2 = contra1 # Ok because int <: float => Covariant[float] <: Covariant[int]
|
||||||
|
|
||||||
|
inv1: Invariant[float] = object()
|
||||||
|
inv2: Invariant[int] = object()
|
||||||
|
inv1 = inv2 # Invalid
|
||||||
|
inv2 = inv1 # Invalid
|
||||||
1475
tests/cases/checker/07_variance.py.ref.json
Normal file
1475
tests/cases/checker/07_variance.py.ref.json
Normal file
File diff suppressed because it is too large
Load Diff
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)
|
||||||
907
tests/cases/checker/08_unification.py.ref.json
Normal file
907
tests/cases/checker/08_unification.py.ref.json
Normal file
@@ -0,0 +1,907 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "object",
|
||||||
|
"type": {},
|
||||||
|
"required": false,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [],
|
||||||
|
"mixed": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [],
|
||||||
|
"mixed": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "float"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [],
|
||||||
|
"mixed": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "value",
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "transform",
|
||||||
|
"type": {
|
||||||
|
"params": {
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "v",
|
||||||
|
"type": {
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "U",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pos": 1,
|
||||||
|
"name": "iterable",
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "T",
|
||||||
|
"bound": null,
|
||||||
|
"variance": "INVARIANT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixed": [],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
118
tests/cases/checker/09_frame_ops.py
Normal file
118
tests/cases/checker/09_frame_ops.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# type: ignore
|
||||||
|
# ruff: disable [F821]
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
df1 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||||
|
df2 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||||
|
|
||||||
|
_: 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="i")
|
||||||
|
|
||||||
|
_ = 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()
|
||||||
5733
tests/cases/checker/09_frame_ops.py.ref.json
Normal file
5733
tests/cases/checker/09_frame_ops.py.ref.json
Normal file
File diff suppressed because it is too large
Load Diff
45
tests/cases/checker/10_variable_defined.py
Normal file
45
tests/cases/checker/10_variable_defined.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# type: ignore
|
||||||
|
# ruff: disable[F821]
|
||||||
|
|
||||||
|
_: Any
|
||||||
|
|
||||||
|
_ = undeclared
|
||||||
|
|
||||||
|
declared: int
|
||||||
|
_ = declared
|
||||||
|
|
||||||
|
half_defined1: int
|
||||||
|
half_defined2: int
|
||||||
|
if False:
|
||||||
|
half_defined1 = 0
|
||||||
|
else:
|
||||||
|
half_defined2 = 1
|
||||||
|
_ = half_defined1
|
||||||
|
_ = half_defined2
|
||||||
|
|
||||||
|
fully_defined: int
|
||||||
|
if False:
|
||||||
|
fully_defined = 0
|
||||||
|
else:
|
||||||
|
fully_defined = 1
|
||||||
|
_ = fully_defined
|
||||||
|
|
||||||
|
defined: int = 0
|
||||||
|
_ = defined
|
||||||
|
|
||||||
|
no_annotation = 0
|
||||||
|
_ = no_annotation
|
||||||
|
|
||||||
|
self_ref1 = self_ref1
|
||||||
|
self_ref2: int = self_ref2
|
||||||
|
|
||||||
|
|
||||||
|
def fact(n: int) -> int:
|
||||||
|
if n <= 1:
|
||||||
|
return 1
|
||||||
|
return n * fact(n - 1)
|
||||||
|
|
||||||
|
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
_ = i
|
||||||
|
_ = i
|
||||||
651
tests/cases/checker/10_variable_defined.py.ref.json
Normal file
651
tests/cases/checker/10_variable_defined.py.ref.json
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
{
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
9,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
9,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Variable 'declared' is declared but may not be defined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
17,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
17,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Variable 'half_defined1' is declared but may not be defined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
18,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
18,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Variable 'half_defined2' is declared but may not be defined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Error",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
34,
|
||||||
|
17
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
34,
|
||||||
|
26
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Variable 'self_ref2' is declared but may not be defined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
6,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
6,
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Unknown variable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
33,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
33,
|
||||||
|
21
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Unknown variable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Warning",
|
||||||
|
"location": {
|
||||||
|
"start": [
|
||||||
|
45,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
45,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "Unknown variable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"judgments": [
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L6:4",
|
||||||
|
"to": "L6:14"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "undeclared"
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L9:4",
|
||||||
|
"to": "L9:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "declared"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L13:3",
|
||||||
|
"to": "L13:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L14:20",
|
||||||
|
"to": "L14:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L16:20",
|
||||||
|
"to": "L16:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L17:4",
|
||||||
|
"to": "L17:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "half_defined1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L18:4",
|
||||||
|
"to": "L18:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "half_defined2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L21:3",
|
||||||
|
"to": "L21:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L22:20",
|
||||||
|
"to": "L22:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L24:20",
|
||||||
|
"to": "L24:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L25:4",
|
||||||
|
"to": "L25:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "fully_defined"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L27:15",
|
||||||
|
"to": "L27:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L28:4",
|
||||||
|
"to": "L28:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "defined"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L30:16",
|
||||||
|
"to": "L30:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L31:4",
|
||||||
|
"to": "L31:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "no_annotation"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L33:12",
|
||||||
|
"to": "L33:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "self_ref1"
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L34:17",
|
||||||
|
"to": "L34:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "self_ref2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L38:7",
|
||||||
|
"to": "L38:8"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L38:12",
|
||||||
|
"to": "L38:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L38:7",
|
||||||
|
"to": "L38:13"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CompareExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"operator": "<=",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "bool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L39:15",
|
||||||
|
"to": "L39:16"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:11",
|
||||||
|
"to": "L40:12"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:20",
|
||||||
|
"to": "L40:21"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:24",
|
||||||
|
"to": "L40:25"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:20",
|
||||||
|
"to": "L40:25"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:15",
|
||||||
|
"to": "L40:19"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "fact"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"params": {
|
||||||
|
"pos": [],
|
||||||
|
"mixed": [
|
||||||
|
{
|
||||||
|
"pos": 0,
|
||||||
|
"name": "n",
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"unsupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kw": []
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:15",
|
||||||
|
"to": "L40:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "fact"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L40:11",
|
||||||
|
"to": "L40:26"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"operator": "*",
|
||||||
|
"right": {
|
||||||
|
"_type": "CallExpr",
|
||||||
|
"callee": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "fact"
|
||||||
|
},
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"_type": "BinaryExpr",
|
||||||
|
"left": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "n"
|
||||||
|
},
|
||||||
|
"operator": "-",
|
||||||
|
"right": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keywords": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L43:10",
|
||||||
|
"to": "L43:11"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L43:13",
|
||||||
|
"to": "L43:14"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L43:16",
|
||||||
|
"to": "L43:17"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L43:9",
|
||||||
|
"to": "L43:18"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "ListExpr",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_type": "LiteralExpr",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "list",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"name": "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L43:4",
|
||||||
|
"to": "L43:5"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "i"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L44:8",
|
||||||
|
"to": "L44:9"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "i"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"from": "L45:4",
|
||||||
|
"to": "L45:5"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"_type": "VariableExpr",
|
||||||
|
"name": "i"
|
||||||
|
},
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,68 +7,14 @@ Module(
|
|||||||
alias(name='Meter'),
|
alias(name='Meter'),
|
||||||
alias(name='Second')],
|
alias(name='Second')],
|
||||||
level=0),
|
level=0),
|
||||||
Assign(
|
|
||||||
targets=[
|
|
||||||
Name(id='__midas_alias_0__')],
|
|
||||||
value=Constant(value=123.45)),
|
|
||||||
Assert(
|
|
||||||
test=Call(
|
|
||||||
func=Name(id='isinstance'),
|
|
||||||
args=[
|
|
||||||
Name(id='__midas_alias_0__'),
|
|
||||||
Name(id='float')],
|
|
||||||
keywords=[]),
|
|
||||||
msg=JoinedStr(
|
|
||||||
values=[
|
|
||||||
Constant(value='01_simple_types.py:L3:19: CastError: Cannot cast '),
|
|
||||||
FormattedValue(
|
|
||||||
value=Attribute(
|
|
||||||
value=Call(
|
|
||||||
func=Name(id='type'),
|
|
||||||
args=[
|
|
||||||
Name(id='__midas_alias_0__')],
|
|
||||||
keywords=[]),
|
|
||||||
attr='__name__'),
|
|
||||||
conversion=-1),
|
|
||||||
Constant(value=' to float')])),
|
|
||||||
Assign(
|
Assign(
|
||||||
targets=[
|
targets=[
|
||||||
Name(id='distance')],
|
Name(id='distance')],
|
||||||
value=Name(id='__midas_alias_0__')),
|
value=Constant(value=123.45)),
|
||||||
Delete(
|
|
||||||
targets=[
|
|
||||||
Name(id='__midas_alias_0__')]),
|
|
||||||
Assign(
|
|
||||||
targets=[
|
|
||||||
Name(id='__midas_alias_1__')],
|
|
||||||
value=Constant(value=6.7)),
|
|
||||||
Assert(
|
|
||||||
test=Call(
|
|
||||||
func=Name(id='isinstance'),
|
|
||||||
args=[
|
|
||||||
Name(id='__midas_alias_1__'),
|
|
||||||
Name(id='float')],
|
|
||||||
keywords=[]),
|
|
||||||
msg=JoinedStr(
|
|
||||||
values=[
|
|
||||||
Constant(value='01_simple_types.py:L4:16: CastError: Cannot cast '),
|
|
||||||
FormattedValue(
|
|
||||||
value=Attribute(
|
|
||||||
value=Call(
|
|
||||||
func=Name(id='type'),
|
|
||||||
args=[
|
|
||||||
Name(id='__midas_alias_1__')],
|
|
||||||
keywords=[]),
|
|
||||||
attr='__name__'),
|
|
||||||
conversion=-1),
|
|
||||||
Constant(value=' to float')])),
|
|
||||||
Assign(
|
Assign(
|
||||||
targets=[
|
targets=[
|
||||||
Name(id='time')],
|
Name(id='time')],
|
||||||
value=Name(id='__midas_alias_1__')),
|
value=Constant(value=6.7)),
|
||||||
Delete(
|
|
||||||
targets=[
|
|
||||||
Name(id='__midas_alias_1__')]),
|
|
||||||
Assign(
|
Assign(
|
||||||
targets=[
|
targets=[
|
||||||
Name(id='speed')],
|
Name(id='speed')],
|
||||||
|
|||||||
14
tests/cases/generator/02_constraints.midas
Normal file
14
tests/cases/generator/02_constraints.midas
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Inline
|
||||||
|
type T1 = float where _ > 0
|
||||||
|
|
||||||
|
// Named
|
||||||
|
predicate is_positive(v: float) = v > 0
|
||||||
|
type T2 = float where is_positive(_)
|
||||||
|
|
||||||
|
// Curried
|
||||||
|
predicate in_range(mn: float, mx: float)(v: float) = v >= mn & v < mx
|
||||||
|
type T3 = float where in_range(100, 200)(_)
|
||||||
|
|
||||||
|
// Alias
|
||||||
|
predicate minor = in_range(0, 18)
|
||||||
|
type T4 = float where minor(_)
|
||||||
8
tests/cases/generator/02_constraints.py
Normal file
8
tests/cases/generator/02_constraints.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from midas import T1, T2, T3, T4, cast
|
||||||
|
|
||||||
|
t: float = 12.5
|
||||||
|
|
||||||
|
t1: T1 = cast(T1, t)
|
||||||
|
t2: T2 = cast(T2, t)
|
||||||
|
t3: T3 = cast(T3, t)
|
||||||
|
t4: T4 = cast(T4, t)
|
||||||
333
tests/cases/generator/02_constraints.py.ref.txt
Normal file
333
tests/cases/generator/02_constraints.py.ref.txt
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
Module(
|
||||||
|
body=[
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_p0__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='_',
|
||||||
|
annotation=Constant(value='Any'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=Compare(
|
||||||
|
left=Name(id='_'),
|
||||||
|
ops=[
|
||||||
|
Gt()],
|
||||||
|
comparators=[
|
||||||
|
Constant(value=0)]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_is_positive__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='v',
|
||||||
|
annotation=Constant(value='float'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=Compare(
|
||||||
|
left=Name(id='v'),
|
||||||
|
ops=[
|
||||||
|
Gt()],
|
||||||
|
comparators=[
|
||||||
|
Constant(value=0)]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_p1__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='_',
|
||||||
|
annotation=Constant(value='Any'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='__midas_is_positive__'),
|
||||||
|
args=[
|
||||||
|
Name(id='_')],
|
||||||
|
keywords=[]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_in_range__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='mn',
|
||||||
|
annotation=Constant(value='float')),
|
||||||
|
arg(
|
||||||
|
arg='mx',
|
||||||
|
annotation=Constant(value='float'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
FunctionDef(
|
||||||
|
name='inner0',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='v',
|
||||||
|
annotation=Constant(value='float'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=BoolOp(
|
||||||
|
op=And(),
|
||||||
|
values=[
|
||||||
|
Compare(
|
||||||
|
left=Name(id='v'),
|
||||||
|
ops=[
|
||||||
|
GtE()],
|
||||||
|
comparators=[
|
||||||
|
Name(id='mn')]),
|
||||||
|
Compare(
|
||||||
|
left=Name(id='v'),
|
||||||
|
ops=[
|
||||||
|
Lt()],
|
||||||
|
comparators=[
|
||||||
|
Name(id='mx')])]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
Return(
|
||||||
|
value=Name(id='inner0'))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='Callable[[float], bool]')),
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_p2__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='_',
|
||||||
|
annotation=Constant(value='Any'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=Call(
|
||||||
|
func=Call(
|
||||||
|
func=Name(id='__midas_in_range__'),
|
||||||
|
args=[
|
||||||
|
Constant(value=100),
|
||||||
|
Constant(value=200)],
|
||||||
|
keywords=[]),
|
||||||
|
args=[
|
||||||
|
Name(id='_')],
|
||||||
|
keywords=[]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_minor__')],
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='__midas_in_range__'),
|
||||||
|
args=[
|
||||||
|
Constant(value=0),
|
||||||
|
Constant(value=18)],
|
||||||
|
keywords=[])),
|
||||||
|
FunctionDef(
|
||||||
|
name='__midas_p3__',
|
||||||
|
args=arguments(
|
||||||
|
posonlyargs=[],
|
||||||
|
args=[
|
||||||
|
arg(
|
||||||
|
arg='_',
|
||||||
|
annotation=Constant(value='Any'))],
|
||||||
|
kwonlyargs=[],
|
||||||
|
kw_defaults=[],
|
||||||
|
defaults=[]),
|
||||||
|
body=[
|
||||||
|
Return(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='__midas_minor__'),
|
||||||
|
args=[
|
||||||
|
Name(id='_')],
|
||||||
|
keywords=[]))],
|
||||||
|
decorator_list=[],
|
||||||
|
returns=Constant(value='bool')),
|
||||||
|
ImportFrom(
|
||||||
|
module='midas',
|
||||||
|
names=[
|
||||||
|
alias(name='T1'),
|
||||||
|
alias(name='T2'),
|
||||||
|
alias(name='T3'),
|
||||||
|
alias(name='T4'),
|
||||||
|
alias(name='cast')],
|
||||||
|
level=0),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='t')],
|
||||||
|
value=Constant(value=12.5)),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a0__')],
|
||||||
|
value=Name(id='t')),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='isinstance'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a0__'),
|
||||||
|
Name(id='float')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=JoinedStr(
|
||||||
|
values=[
|
||||||
|
Constant(value='02_constraints.py:L5:10: CastError: Cannot cast '),
|
||||||
|
FormattedValue(
|
||||||
|
value=Attribute(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='type'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a0__')],
|
||||||
|
keywords=[]),
|
||||||
|
attr='__name__'),
|
||||||
|
conversion=-1),
|
||||||
|
Constant(value=' to float')])),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='__midas_p0__'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a0__')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=Constant(value="02_constraints.py:L5:10: ConstraintError: Value does not fit constraint '_ > 0'")),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='t1')],
|
||||||
|
value=Name(id='__midas_a0__')),
|
||||||
|
Delete(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a0__')]),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a1__')],
|
||||||
|
value=Name(id='t')),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='isinstance'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a1__'),
|
||||||
|
Name(id='float')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=JoinedStr(
|
||||||
|
values=[
|
||||||
|
Constant(value='02_constraints.py:L6:10: CastError: Cannot cast '),
|
||||||
|
FormattedValue(
|
||||||
|
value=Attribute(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='type'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a1__')],
|
||||||
|
keywords=[]),
|
||||||
|
attr='__name__'),
|
||||||
|
conversion=-1),
|
||||||
|
Constant(value=' to float')])),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='__midas_p1__'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a1__')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=Constant(value="02_constraints.py:L6:10: ConstraintError: Value does not fit constraint 'is_positive(_)'")),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='t2')],
|
||||||
|
value=Name(id='__midas_a1__')),
|
||||||
|
Delete(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a1__')]),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a2__')],
|
||||||
|
value=Name(id='t')),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='isinstance'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a2__'),
|
||||||
|
Name(id='float')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=JoinedStr(
|
||||||
|
values=[
|
||||||
|
Constant(value='02_constraints.py:L7:10: CastError: Cannot cast '),
|
||||||
|
FormattedValue(
|
||||||
|
value=Attribute(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='type'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a2__')],
|
||||||
|
keywords=[]),
|
||||||
|
attr='__name__'),
|
||||||
|
conversion=-1),
|
||||||
|
Constant(value=' to float')])),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='__midas_p2__'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a2__')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=Constant(value="02_constraints.py:L7:10: ConstraintError: Value does not fit constraint 'in_range(100, 200)(_)'")),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='t3')],
|
||||||
|
value=Name(id='__midas_a2__')),
|
||||||
|
Delete(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a2__')]),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a3__')],
|
||||||
|
value=Name(id='t')),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='isinstance'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a3__'),
|
||||||
|
Name(id='float')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=JoinedStr(
|
||||||
|
values=[
|
||||||
|
Constant(value='02_constraints.py:L8:10: CastError: Cannot cast '),
|
||||||
|
FormattedValue(
|
||||||
|
value=Attribute(
|
||||||
|
value=Call(
|
||||||
|
func=Name(id='type'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a3__')],
|
||||||
|
keywords=[]),
|
||||||
|
attr='__name__'),
|
||||||
|
conversion=-1),
|
||||||
|
Constant(value=' to float')])),
|
||||||
|
Assert(
|
||||||
|
test=Call(
|
||||||
|
func=Name(id='__midas_p3__'),
|
||||||
|
args=[
|
||||||
|
Name(id='__midas_a3__')],
|
||||||
|
keywords=[]),
|
||||||
|
msg=Constant(value="02_constraints.py:L8:10: ConstraintError: Value does not fit constraint 'minor(_)'")),
|
||||||
|
Assign(
|
||||||
|
targets=[
|
||||||
|
Name(id='t4')],
|
||||||
|
value=Name(id='__midas_a3__')),
|
||||||
|
Delete(
|
||||||
|
targets=[
|
||||||
|
Name(id='__midas_a3__')])],
|
||||||
|
type_ignores=[])
|
||||||
@@ -9,26 +9,22 @@ type Longitude = float where (-180 <= _ <= 180)
|
|||||||
type Difference[T] = T
|
type Difference[T] = T
|
||||||
|
|
||||||
// Complex custom type, containing two values accessible through properties
|
// Complex custom type, containing two values accessible through properties
|
||||||
type GeoLocation = {
|
type GeoLocation = object
|
||||||
|
|
||||||
|
extend GeoLocation {
|
||||||
prop lat: Latitude
|
prop lat: Latitude
|
||||||
prop lon: Longitude
|
prop lon: Longitude
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define operations on our custom type
|
type GeoLocationDifference = object
|
||||||
extend GeoLocation {
|
|
||||||
// This type is compatible with the `-` operation with another GeoLocation
|
|
||||||
// i.e. you can subtract a GeoLocation from another GeoLocation, resulting
|
|
||||||
// in a Difference of GeoLocations
|
|
||||||
def __sub__: fn(GeoLocation, /) -> Difference[GeoLocation]
|
|
||||||
}
|
|
||||||
|
|
||||||
// For complex generics, you need to specify how the genericity the properties
|
extend GeoLocationDifference {
|
||||||
// are handled
|
|
||||||
type Difference[GeoLocation] = {
|
|
||||||
prop lat: Difference[Latitude]
|
prop lat: Difference[Latitude]
|
||||||
prop lon: Difference[Longitude]
|
prop lon: Difference[Longitude]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define operations on our custom type
|
||||||
|
|
||||||
// Simple operation defined on our custom types
|
// Simple operation defined on our custom types
|
||||||
extend Latitude {
|
extend Latitude {
|
||||||
def __sub__: fn(Latitude, /) -> Difference[Latitude]
|
def __sub__: fn(Latitude, /) -> Difference[Latitude]
|
||||||
@@ -38,13 +34,23 @@ extend Longitude {
|
|||||||
def __sub__: fn(Longitude, /) -> Difference[Longitude]
|
def __sub__: fn(Longitude, /) -> Difference[Longitude]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend GeoLocation {
|
||||||
|
// This type is compatible with the `-` operation with another GeoLocation
|
||||||
|
// i.e. you can subtract a GeoLocation from another GeoLocation, resulting
|
||||||
|
// in a GeoLocationDifference
|
||||||
|
def __sub__: fn(GeoLocation, /) -> GeoLocationDifference
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Predefined custom predicates that can be referenced in other definitions
|
// Predefined custom predicates that can be referenced in other definitions
|
||||||
predicate Positive(v: float) = v >= 0
|
predicate Positive(v: float) = v >= 0
|
||||||
predicate StrictlyPositive(v: float) = v > 0
|
predicate StrictlyPositive(v: float) = v > 0
|
||||||
predicate Equatorial(loc: GeoLocation) = (-10 <= loc.lat <= 10)
|
predicate Equatorial(loc: GeoLocation) = (-10 <= loc.lat <= 10)
|
||||||
predicate Arctic(loc: GeoLocation) = (loc.lat >= 66)
|
predicate Arctic(loc: GeoLocation) = (loc.lat >= 66)
|
||||||
|
|
||||||
type Person = {
|
type Person = object
|
||||||
|
|
||||||
|
extend Person {
|
||||||
prop name: str
|
prop name: str
|
||||||
|
|
||||||
// Property with an inline constraint
|
// Property with an inline constraint
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user