Compare commits
82 Commits
9fdc003a71
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 12c5309cff | |||
|
720a82c221
|
|||
|
90e9d167bf
|
|||
|
4866ab2090
|
|||
|
3ede752cae
|
|||
|
244e5f351a
|
|||
|
e61c4070da
|
|||
|
00fdda5034
|
|||
| e302ed2377 | |||
|
1d927e532f
|
|||
|
bfe4f854ea
|
|||
| 3e06a236b5 | |||
|
ab53ac67d5
|
|||
| b3ff5a7b96 | |||
|
dce35391b4
|
|||
|
28be45eb8f
|
|||
|
b64110bb06
|
|||
|
edc2a65492
|
|||
|
dade8eb0f4
|
|||
|
9f3dfd686b
|
|||
|
5ef21917ae
|
|||
| 34072de270 | |||
|
2b09a29165
|
|||
|
309e1d4a5b
|
|||
|
74472c974b
|
|||
|
a9a3164c24
|
|||
|
88ab9dc14d
|
|||
|
f41dbb528c
|
|||
|
44963db425
|
|||
|
b67c940112
|
|||
|
ad40db98d0
|
|||
|
7a021b2450
|
|||
|
a7b62e752b
|
|||
|
44984af8a3
|
|||
| 0c851cbc9b | |||
|
54b2058b86
|
|||
|
60ff529226
|
|||
|
c3b243288d
|
|||
|
cd9b80d22b
|
|||
|
21b648e18f
|
|||
|
742693fa38
|
|||
|
4d343a61ff
|
|||
|
f314a95e87
|
|||
|
99350a9505
|
|||
|
aaa6d945d1
|
|||
|
653612ee87
|
|||
|
2df6bca948
|
|||
| 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
|
@@ -1,48 +0,0 @@
|
||||
name: Compile manual
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
container: catthehacker/ubuntu:act-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: "3.2"
|
||||
|
||||
- name: Setup Fontist
|
||||
uses: fontist/setup-fontist@v2
|
||||
|
||||
- name: Install Fonts
|
||||
run: fontist manifest install docs/fonts.yaml
|
||||
|
||||
- uses: typst-community/setup-typst@v5
|
||||
with:
|
||||
typst-version: "0.15.0"
|
||||
zip-packages: docs/requirements.json
|
||||
cache-local-packages: true
|
||||
token: ""
|
||||
|
||||
- run: |
|
||||
typst compile \
|
||||
--root . \
|
||||
--font-path ~/.fontist/fonts \
|
||||
--input hash=${{ gitea.sha }} \
|
||||
docs/manual.typ \
|
||||
docs/manual.pdf
|
||||
|
||||
- name: Upload artifact
|
||||
uses: christopherhx/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: manual
|
||||
path: docs/manual.pdf
|
||||
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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,4 +6,6 @@ venv
|
||||
*.pyc
|
||||
uv.lock
|
||||
.python-version
|
||||
/out
|
||||
/out
|
||||
/examples/**/build/
|
||||
/examples/**/*.pyi
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Louis Heredero
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
25
README.md
25
README.md
@@ -1,5 +1,11 @@
|
||||
<h1>Midas</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/icon.svg" width="64">
|
||||
</p>
|
||||
|
||||
<img src="https://git.kb28.ch/HEL/midas/actions/workflows/tests.yaml/badge.svg">
|
||||
|
||||
*Midas* is a type system to _Maintain Integrity of Data with Annotated Structures_. In Greek mythology, [Midas](https://en.wikipedia.org/wiki/Midas) was a Phrygian king who was blessed with the gift of turning everything he touched into gold.
|
||||
|
||||
*Midas* aims at providing Python developers with a simple annotation system to enable compile-time integrity and data type checks, as well as generating runtime assertions.
|
||||
@@ -22,6 +28,8 @@ This framework is being developed as part of a Bachelor's Thesis by Louis Herede
|
||||
- [Showing Type Judgements](#showing-type-judgements)
|
||||
- [Validating Definitions](#validating-definitions)
|
||||
- [Tests](#tests)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
</details>
|
||||
|
||||
|
||||
@@ -51,7 +59,7 @@ This framework is being developed as part of a Bachelor's Thesis by Louis Herede
|
||||
|
||||
## Commands
|
||||
|
||||
Hereafter is a description of the commands you can use with Midas. For a full description, refer to [the manual](./docs/manual.pdf).
|
||||
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`.
|
||||
|
||||
### Type Checking
|
||||
|
||||
@@ -118,7 +126,7 @@ This command processes the given Midas definitions and dumps the contents of the
|
||||
midas stubs types.midas -o stubs.pyi
|
||||
```
|
||||
|
||||
This command generate Python stubs from a Midas definition file
|
||||
This command generates Python stubs from a Midas definition file
|
||||
|
||||
### Showing Type Judgements
|
||||
|
||||
@@ -161,3 +169,16 @@ Not specifying any subcommand is equivalent to running `run -a`
|
||||
- Run specific tests: `run tests/cases/test1.py tests/cases/test2.py ...`
|
||||
- Update all tests: `update -a`
|
||||
- Update specific tests: `update tests/cases/test1.py tests/cases/test2.py ...`
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to [open an issue](https://git.kb28.ch/HEL/midas/issues) to report a bug or if you experience any issues with Midas.
|
||||
|
||||
Contributions are also welcome so feel free to [open a pull request](https://git.kb28.ch/HEL/midas/pulls).
|
||||
|
||||
> [!NOTE]
|
||||
> As this is part of my bachelor project, I will not accept any contributions before my defense
|
||||
|
||||
## License
|
||||
|
||||
Midas is distributed under the terms of the Apache-2.0 license. See [LICENSE](LICENSE) for details.
|
||||
@@ -1,150 +0,0 @@
|
||||
#import "@preview/cetz:0.5.2": canvas, draw
|
||||
|
||||
#let diagram-only = false
|
||||
|
||||
#set document(
|
||||
title: [Midas Architecture],
|
||||
//author: "Louis Heredero",
|
||||
)
|
||||
|
||||
#set text(
|
||||
font: "Source Sans 3",
|
||||
)
|
||||
|
||||
#let diagram = canvas({
|
||||
let framed = draw.content.with(
|
||||
padding: (x: .8em, y: 1em),
|
||||
frame: "rect",
|
||||
stroke: black,
|
||||
)
|
||||
let arrow = draw.line.with(mark: (end: ">", fill: black))
|
||||
framed(
|
||||
(0, 0),
|
||||
name: "python-parser",
|
||||
)[Python parser]
|
||||
|
||||
draw.content(
|
||||
(rel: (0, 1), to: "python-parser.north"),
|
||||
padding: 5pt,
|
||||
anchor: "south",
|
||||
name: "source-py",
|
||||
)[_`source.py`_]
|
||||
arrow("source-py", "python-parser")
|
||||
|
||||
framed(
|
||||
(rel: (3, 0), to: "python-parser.east"),
|
||||
anchor: "west",
|
||||
name: "custom-parser",
|
||||
align(center)[Custom python\ parser],
|
||||
)
|
||||
|
||||
arrow("python-parser", "custom-parser", name: "arrow-python-ast")
|
||||
draw.content(
|
||||
"arrow-python-ast",
|
||||
anchor: "south",
|
||||
padding: 5pt,
|
||||
)[`ast.Module`]
|
||||
|
||||
framed(
|
||||
(rel: (-3, -2), to: "custom-parser.south"),
|
||||
anchor: "east",
|
||||
name: "python-resolver",
|
||||
)[Python Resolver]
|
||||
arrow(
|
||||
"custom-parser",
|
||||
((), "|-", "python-resolver.east"),
|
||||
"python-resolver",
|
||||
name: "arrow-python-custom-ast",
|
||||
)
|
||||
draw.content(
|
||||
(rel: (1.5, 0), to: "arrow-python-custom-ast.end"),
|
||||
padding: 5pt,
|
||||
anchor: "south",
|
||||
)[P-AST#footnote[#strong[P]ython *AST*]<fn-past>]
|
||||
draw.content(
|
||||
"python-resolver.west",
|
||||
padding: 5pt,
|
||||
anchor: "south-east",
|
||||
)[Resolved P-AST@fn-past]
|
||||
|
||||
draw.circle(
|
||||
(rel: (1, -2), to: "custom-parser.south-east"),
|
||||
radius: .4,
|
||||
name: "midas-loader",
|
||||
)
|
||||
arrow(
|
||||
"custom-parser",
|
||||
"midas-loader",
|
||||
name: "arrow-load-midas",
|
||||
mark: (end: (symbol: ">", fill: black), start: "o"),
|
||||
)
|
||||
draw.content(
|
||||
"arrow-load-midas",
|
||||
anchor: "west",
|
||||
padding: 5pt,
|
||||
)[```python midas.using("types.midas")```]
|
||||
|
||||
framed(
|
||||
(rel: (0, -2), to: "midas-loader.south"),
|
||||
name: "midas-parser",
|
||||
)[Midas lexer/parser]
|
||||
arrow("midas-loader", "midas-parser", name: "arrow-midas-source")
|
||||
draw.content(
|
||||
"arrow-midas-source",
|
||||
anchor: "west",
|
||||
padding: 5pt,
|
||||
)[_`types.midas`_]
|
||||
|
||||
|
||||
framed(
|
||||
(rel: (-2, 0), to: "midas-parser.west"),
|
||||
anchor: "east",
|
||||
name: "midas-resolver",
|
||||
)[Midas Resolver]
|
||||
arrow("midas-parser", "midas-resolver", name: "arrow-midas-ast")
|
||||
draw.content(
|
||||
"arrow-midas-ast",
|
||||
anchor: "south",
|
||||
padding: 5pt,
|
||||
)[M-AST#footnote[#strong[M]idas *AST*]<fn-mast>]
|
||||
|
||||
framed(
|
||||
(rel: (-3, 0), to: "midas-resolver.west"),
|
||||
anchor: "east",
|
||||
name: "checker",
|
||||
)[Checker]
|
||||
arrow("midas-resolver", "checker", name: "arrow-type-ctx")
|
||||
arrow(
|
||||
"python-resolver",
|
||||
((), "-|", "checker.north"),
|
||||
"checker",
|
||||
)
|
||||
draw.content(
|
||||
"arrow-type-ctx",
|
||||
anchor: "south",
|
||||
padding: 5pt,
|
||||
)[Types context]
|
||||
})
|
||||
|
||||
#show: doc => if diagram-only {
|
||||
set page(width: auto, height: auto, margin: .5cm)
|
||||
diagram
|
||||
} else { doc }
|
||||
|
||||
#align(center, title())
|
||||
|
||||
#v(1cm)
|
||||
|
||||
#figure(
|
||||
diagram,
|
||||
caption: [Midas type-checker architecture],
|
||||
)
|
||||
|
||||
== Components
|
||||
|
||||
- *Python parser*: builtin Python AST parser, extracts abstract syntax from the raw Python source (```python ast.parse(...)```)
|
||||
- *Custom python parser*: converts the raw Python AST into custom, more suitable constructs, especially for type annotations
|
||||
- *Python resolver*: resolves bindings and references, tracks binding scopes
|
||||
- *Midas lexer/parser*: parses a Midas type definition file and extracts its AST
|
||||
- *Midas resolver*: walks the AST and fills the environment with the defined types and operations
|
||||
- *Checker*: evaluates expressions and checks type coherence
|
||||
97
docs/function_subtyping.typ
Normal file
97
docs/function_subtyping.typ
Normal file
@@ -0,0 +1,97 @@
|
||||
#import "@preview/curryst:0.6.0": prooftree, rule, rule-set
|
||||
#import "@preview/gentle-clues:1.3.1" as gc
|
||||
#import "@preview/lovelace:0.3.1": pseudocode-list
|
||||
|
||||
#set text(font: "Source Sans 3")
|
||||
#show link: set text(fill: blue)
|
||||
#set document(
|
||||
title: [Function subtyping rule],
|
||||
)
|
||||
|
||||
#let req = math.op("req")
|
||||
|
||||
#align(center, title())
|
||||
|
||||
|
||||
This document formalizes the logic used when checking whether a function is a subtype of another.
|
||||
|
||||
= Definitions
|
||||
|
||||
A *Parameter specification* has a list of positional-only parameters $P$, mixed parameters $M$ and keyword-only parameters $K$:
|
||||
$ S = (P, M, K) $
|
||||
|
||||
A *Parameter* has an index $i$, a name $n$ and a required flag $r$:
|
||||
$ p = (i, n, r) $
|
||||
|
||||
A *Function* has a param spec $S$ and a return type $R$:
|
||||
$ F = (S, R) $
|
||||
|
||||
= Main rules
|
||||
|
||||
We want to define a rule for checking structural subtyping of functions, i.e. to check when $F_1 <: F_2$
|
||||
|
||||
There are two conditions to check:
|
||||
#align(center, prooftree(
|
||||
rule(
|
||||
$Gamma tack S_2 <: S_1$,
|
||||
$Gamma tack R_1 <: R_2$,
|
||||
$Gamma tack F_1 <: F_2$,
|
||||
),
|
||||
))
|
||||
|
||||
The second condition is trivial to check.
|
||||
The first condition is a bit more tricky.
|
||||
|
||||
For a parameter specification $S_2$ to be a subtype of another $S_1$, the latter needs to be fully compatible with any usages of the former. What this means is that #link(<cond-1>)[(1)] any argument that can be passed to $S_2$ must be accepted by $S_1$, and #link(<cond-2>)[(2)] $S_1$ must not have additional required arguments that are not in $S_2$. However, $S_1$ can have additional optional parameters not present in $S_2$.
|
||||
|
||||
After mapping parameters of $S_1$ and $S_2$, types must be checked such that if a parameter $p_i: T in S_1$ is mapped to a parameter $q_j: U in S_2$, $U <: T$.
|
||||
|
||||
= Detailed rules for *ParamSpec* subtyping
|
||||
|
||||
#gc.info(title: [Notation])[
|
||||
In the following equations:
|
||||
- the notation $S in.rev a^i: T$ will be used to denote that a parameter spec $S$ accepts an argument named $a$ at index $i$ of type $T$
|
||||
- the special name $alpha$ will be used to denote any argument without a specific name
|
||||
- the special name $phi$ will be used to denote any parameter without a specific name
|
||||
- $AA$ denotes the group of all arguments, i.e. $alpha in AA$
|
||||
- $PP$ denotes the group of all parameters, i.e. $p in PP$
|
||||
- $req(S, alpha)$ is a predicate checking whether $alpha$ is required in $S$
|
||||
]
|
||||
|
||||
== Arguments of $S_2$ are compatible with $S_1$ <cond-1>
|
||||
|
||||
Formally, the condition is:
|
||||
$
|
||||
forall alpha in AA, S_2 in.rev alpha => S_1 in.rev alpha
|
||||
$
|
||||
|
||||
Let $S_1 = (P_1, M_1, K_1)$ and $S_2 = (P_2, M_2, K_2)$.
|
||||
|
||||
For each positional-only parameter $phi_i in P_2$, $phi_i in P_1 or phi_i in M_1$. A positional-only parameter of $S_2$ can either be positional-only or mixed in $S_1$. Additionally, $not req(S_2, phi_i) => not req(S_1, phi_i)$. If $phi_i$ is optional in $S_2$, it must also be optional in $S_1$ so that a call omitting it is valid.
|
||||
|
||||
Similarly, for each keyword-only parameter $p in K_2$, $p in K_1 or p in M_1$. A keyword-only parameter of $S_2$ can either be keyword-only or mixed in $S_1$. Additionally, $not req(S_2, p) => not req(S_1, p)$. If $p$ is optional in $S_2$, it must also be optional in $S_1$ so that a call omitting it is valid.
|
||||
|
||||
Finally, for mixed parameters, the rule is slightly more complex. Either there is a corresponding mixed parameter in $S_1$, or the parameter is covered by both a positional/mixed and a keyword/mixed parameter. In the second case, we must keep in mind that only one of the match will made at runtime, or maybe even none if the parameter is optional in $S_2$, meaning the parameters in $S_1$ _must_ be optional.
|
||||
|
||||
We can thus split the rule in two. $forall p_i in M_2$:
|
||||
$
|
||||
cases(
|
||||
p_i in M_1 and not req(S_2, p_i) => not req(S_1, p_i),
|
||||
"or",
|
||||
underbrace((phi_i in P_1 or phi_i in M_1), "Positional") and
|
||||
underbrace((p in K_1 or p in M_1), "Keyword") and
|
||||
underbrace((not req(S_1, phi_i) and not req(S_1, p)), "Optional")
|
||||
)
|
||||
$
|
||||
|
||||
== No additional required arguments in $S_1$ <cond-2>
|
||||
|
||||
Formally, the condition is:
|
||||
$
|
||||
forall alpha in AA, S_1 in.rev alpha and req(S_1, alpha) => S_2 in.rev alpha and req(S_2, alpha)
|
||||
$
|
||||
|
||||
Each parameter in $S_1$ that is not matched by a parameter in $S_2$ must be optional:
|
||||
$
|
||||
forall p_i in S_1, p_i in.not S_2 => not req(S_1, p_i)
|
||||
$
|
||||
13127
docs/manual.pdf
13127
docs/manual.pdf
File diff suppressed because it is too large
Load Diff
203
docs/manual.typ
203
docs/manual.typ
@@ -6,6 +6,7 @@
|
||||
#import "@preview/codly-languages:0.1.10": codly-languages
|
||||
#import "template.typ": TODO, project
|
||||
#import "@preview/gentle-clues:1.3.1" as gc
|
||||
#import "@preview/treet:1.0.0": tree-list
|
||||
|
||||
#let midas-version = toml("../pyproject.toml").project.version
|
||||
|
||||
@@ -380,6 +381,7 @@ Notice that you don't need to specify `Column` types.
|
||||
height: float where _ >= 0
|
||||
]
|
||||
```,
|
||||
caption: [Simple frame type definition],
|
||||
) <simple-frame>
|
||||
|
||||
#pagebreak()
|
||||
@@ -622,7 +624,7 @@ For example:
|
||||
|
||||
== Control flow
|
||||
|
||||
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and typ checked by Midas.
|
||||
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and type checked by Midas.
|
||||
|
||||
=== `if` / `elif` / `else` <if-else>
|
||||
|
||||
@@ -755,7 +757,7 @@ If the value passed to `cast` or `unsafe_cast` is a literal (e.g. an integer, a
|
||||
|
||||
Vanilla Python already lets you use type hints to specify the type of variables and function parameters.
|
||||
|
||||
Midas use them to type check your code. Additionally, it allows you to use a special syntax to define a `Frame` types directly in these annotations.
|
||||
Midas use them to type check your code. Additionally, it allows you to use a special syntax to define a `Frame` type directly in these annotations.
|
||||
|
||||
Because these annotations are not interpretable by Python, your integrated type checker might complain loudly about them being invalid.
|
||||
A workaround is to silence it by adding a type comment at the end of the line, as shown in @silence-errors.
|
||||
@@ -782,18 +784,213 @@ The example of @python-frame-type shows how you can annotate a dataframe with so
|
||||
|
||||
= Commands <commands>
|
||||
|
||||
#TODO
|
||||
Midas offers several commands to parse, check and compile your code.
|
||||
|
||||
All commands presented in this chapter are subcommands of the `midas` command.
|
||||
For example:
|
||||
#figure(
|
||||
```sh
|
||||
midas compile script.py -t types.midas
|
||||
```,
|
||||
caption: [Example midas command],
|
||||
)
|
||||
|
||||
== Type Checking (`check`) <cmd-check>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas check -t types.midas source.py
|
||||
```,
|
||||
caption: [Example usage of `midas check`],
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Python file to type check
|
||||
|
||||
=== Options
|
||||
|
||||
- `-t` / `--types` `FILENAME`: Midas files defining type definitions. This option can be used multiple times. Files should end with `.midas`
|
||||
- `-l` / `--highlight` `FILENAME`: if set, a highlighted version of the Python file showing inline diagnostics will be written to the given file
|
||||
|
||||
== Compiling (`compile`) <cmd-compile>
|
||||
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas compile -t types.midas source.py
|
||||
```,
|
||||
caption: [Example usage of `midas compile`],
|
||||
)
|
||||
|
||||
With the `compile` command, you can process a source Python file, with any number of custom type definition files, and the type checker will verify the coherence of your program and generate the runnable code with valid syntax and runtime assertions.
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Python file to compile
|
||||
|
||||
=== Options
|
||||
|
||||
#let output = {
|
||||
set text(size: .8em)
|
||||
tree-list[
|
||||
- build/
|
||||
- midas/
|
||||
- stubs1.py
|
||||
- misc.py
|
||||
- source.py
|
||||
]
|
||||
}
|
||||
|
||||
- `-t` / `--types` `FILENAME`: Midas files defining type definitions. This option can be used multiple times. Files should end with `.midas`
|
||||
- `-s` / `--stubs` `TEXT`: the file name of generated stub files. Each instance of this option maps to an instance of `-t`.
|
||||
For example:
|
||||
```sh
|
||||
midas -t types1.midas -s stubs1 -t types2.midas -s misc source.py
|
||||
```
|
||||
will compile to the following files:
|
||||
|
||||
#output
|
||||
- `--ignore-errors`: if set, error diagnostics will not prevent the generator from running
|
||||
|
||||
== Formatting (`format`) <cmd-format>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas format types.midas
|
||||
midas format types.midas -o formatted.midas
|
||||
```,
|
||||
caption: [Example usage of `midas format`],
|
||||
)
|
||||
|
||||
This command parses the given Midas file and outputs a pretty printed file from the AST.
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Midas file to format
|
||||
|
||||
=== Options
|
||||
|
||||
- `-o` / `--output` `FILENAME`: if set, the output is written to the given file instead of STDOUT
|
||||
|
||||
== Highlighting (`highlight`) <cmd-highlight>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas highlight source.py
|
||||
midas highlight source.py -o highlighted.html
|
||||
midas highlight types.midas
|
||||
midas highlight types.midas -o highlighted.html
|
||||
```,
|
||||
caption: [Example usage of `midas highlight`],
|
||||
)
|
||||
|
||||
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.)
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Python or Midas file to highlight
|
||||
|
||||
=== Options
|
||||
|
||||
- `-o` / `--output` `FILENAME`: if set, the output is written to the given file instead of STDOUT
|
||||
|
||||
== Dumping the AST (`parse`) <cmd-parse>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas parse source.py
|
||||
midas parse types.midas
|
||||
```,
|
||||
caption: [Example usage of `midas parse`],
|
||||
)
|
||||
|
||||
For debugging purposes, you can use this command to output the AST parsed from a Python or Midas file
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Python or Midas file to parse
|
||||
|
||||
=== Options
|
||||
|
||||
- `--raw`: if `FILE` is a Python file, the raw AST is returned, as produced by the builtin `ast` module, instead of the custom AST nodes used by Midas internally. This flag has no effect on Midas files
|
||||
|
||||
== Dumping the Registry (`dump-registry`) <cmd-registry>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas dump-registry -t types.midas
|
||||
```,
|
||||
caption: [Example usage of `midas dump-registry`],
|
||||
)
|
||||
|
||||
This command processes the given Midas definitions and dumps the contents of the types registry.
|
||||
|
||||
=== Options
|
||||
|
||||
- `-t` / `--types` `FILENAME`: Midas files defining type definitions. This option can be used multiple times. Files should end with `.midas`
|
||||
|
||||
== Generating Stubs (`stubs`) <cmd-stubs>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas stubs types.midas
|
||||
```,
|
||||
caption: [Example usage of `midas stubs`],
|
||||
)
|
||||
|
||||
This command generates Python stubs from a Midas definition file
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the source Midas file
|
||||
|
||||
=== Options
|
||||
|
||||
- `-o` / `--output` `FILENAME`: if set, the stubs are written to the given file instead of the default `FILE.pyi`
|
||||
- `-w` / `--watch`: if set, the input file will be watched and any changes to it will regenerate the stubs, otherwise the command exits after generating the stubs once
|
||||
|
||||
== Showing Type Judgements (`types`) <cmd-types>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas types -t types.midas source.py
|
||||
```,
|
||||
caption: [Example usage of `midas types`],
|
||||
)
|
||||
|
||||
This command type checks the given Python source file and logs all typing judgements made by the type checker.
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Python file to type check
|
||||
|
||||
=== Options
|
||||
|
||||
- `-t` / `--types` `FILENAME`: Midas files defining type definitions. This option can be used multiple times. Files should end with `.midas`
|
||||
- `-l` / `--highlight` `FILENAME`: if set, a highlighted version of the Python file showing inline diagnostics will be written to the given file
|
||||
|
||||
== Validating Definitions (`validate`) <cmd-validate>
|
||||
|
||||
#figure(
|
||||
```sh
|
||||
midas validate types.midas
|
||||
```,
|
||||
caption: [Example usage of `midas validate`],
|
||||
)
|
||||
|
||||
This command lets you validate a Midas definition file by running the parser and type checker, verifying syntax and references.
|
||||
|
||||
=== Parameters
|
||||
|
||||
- `FILE`: the Midas file to validate
|
||||
|
||||
=== Options
|
||||
|
||||
- `-l` / `--highlight` `FILENAME`: if set, a highlighted version of the Midas file showing inline diagnostics will be written to the given file
|
||||
|
||||
= Known limitations <limitations>
|
||||
|
||||
== Eager evaluation in runtime assertions <eager-eval>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
set document(
|
||||
title: title,
|
||||
author: author,
|
||||
date: none,
|
||||
)
|
||||
set text(
|
||||
font: "Source Sans 3",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# type: ignore
|
||||
# ruff: disable[F821]
|
||||
from __future__ import annotations
|
||||
|
||||
# A simple data-frame with different column of various simple types
|
||||
# Columns can be named and/or typed
|
||||
df: Frame[
|
||||
verified: bool,
|
||||
birth_year: int,
|
||||
height: float + ( _ > 0 ) + ( _ < 250 ),
|
||||
name: str,
|
||||
date: datetime,
|
||||
float, # unnamed
|
||||
unknown: _, # untyped
|
||||
_ # unnamed and untyped
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
// Simple custom type derived from floats
|
||||
type Latitude<float>
|
||||
type Longitude<float>
|
||||
|
||||
// Complex custom type, containing two values accessible through properties
|
||||
type GeoLocation<Latitude, Longitude> {
|
||||
lat: Latitude
|
||||
lon: Longitude
|
||||
}
|
||||
|
||||
type LatitudeDiff<float>
|
||||
type LongitudeDiff<float>
|
||||
|
||||
// Simple operation defined on our custom types
|
||||
op <Latitude> - <Latitude> = <LatitudeDiff>
|
||||
op <Longitude> - <Longitude> = <LongitudeDiff>
|
||||
|
||||
// Simple custom type with a constraint
|
||||
type Age<int + (0 <= _) + (_ < 150)>
|
||||
|
||||
// Predefined custom constraints that can be referenced in other definitions
|
||||
constraint Positive = _ >= 0
|
||||
constraint StrictlyPositive = _ > 0
|
||||
//constraint Even = _ % 2 == 0
|
||||
@@ -1,30 +0,0 @@
|
||||
# type: ignore
|
||||
# ruff: disable[F821]
|
||||
from __future__ import annotations
|
||||
|
||||
# A data-frame using a custom type
|
||||
df: Frame[
|
||||
location: GeoLocation
|
||||
]
|
||||
|
||||
# Properties of a type can be used on a column of that type
|
||||
lat: Column[GeoLocation] = df["location"].lat
|
||||
lon: Column[GeoLocation] = df["location"].lon
|
||||
|
||||
# Unregistered operations between types are not permitted
|
||||
lat + lon # Invalid operation
|
||||
|
||||
# Registered operations are permitted
|
||||
lat1: Latitude = lat[0]
|
||||
lat2: Latitude = lat[1]
|
||||
lat_diff: Difference[Latitude] = lat2 - lat1 # Valid operation
|
||||
|
||||
# In addition to the type, a column can have one or more constraints, either defined inline or in a separate file
|
||||
df2: Frame[
|
||||
age: int + (_ >= 0),
|
||||
height: float + (_ >= 0),
|
||||
]
|
||||
df2_bis: Frame[
|
||||
age: int + Positive,
|
||||
height: float + Positive,
|
||||
]
|
||||
@@ -1,73 +0,0 @@
|
||||
// Simple custom type derived from float
|
||||
type Custom(float)
|
||||
|
||||
// Simple custom types with constraints
|
||||
type Latitude(float) where (-90 <= _ <= 90)
|
||||
type Longitude(float) where (-180 <= _ <= 180)
|
||||
|
||||
// Generic custom type (a Difference of T is derived from T, e.g. a difference of floats is a float
|
||||
type Difference[T](T)
|
||||
|
||||
// Complex custom type, containing two values accessible through properties
|
||||
type GeoLocation {
|
||||
lat: Latitude
|
||||
lon: Longitude
|
||||
}
|
||||
|
||||
// Define operations on our custom type
|
||||
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
|
||||
op __sub__(GeoLocation) -> Difference[GeoLocation]
|
||||
}
|
||||
|
||||
// For complex generics, you need to specify how the genericity the properties
|
||||
// are handled
|
||||
type Difference[GeoLocation] {
|
||||
lat: Difference[Latitude]
|
||||
lon: Difference[Longitude]
|
||||
}
|
||||
|
||||
// Simple operation defined on our custom types
|
||||
extend Latitude {
|
||||
op __sub__(Latitude) -> Difference[Latitude]
|
||||
}
|
||||
|
||||
extend Longitude {
|
||||
op __sub__(Longitude) -> Difference[Longitude]
|
||||
}
|
||||
|
||||
// Predefined custom predicates that can be referenced in other definitions
|
||||
predicate Positive(v: float) = v >= 0
|
||||
predicate StrictlyPositive(v: float) = v > 0
|
||||
predicate Equatorial(loc: GeoLocation) = (-10 <= loc.lat <= 10)
|
||||
predicate Arctic(loc: GeoLocation) = (loc.lat >= 66)
|
||||
|
||||
type Person {
|
||||
name: str
|
||||
|
||||
// Property with an inline constraint
|
||||
age: int? where (0 <= _ < 150)
|
||||
|
||||
// Property referencing a predicate
|
||||
height: float where StrictlyPositive
|
||||
|
||||
home: GeoLocation
|
||||
}
|
||||
|
||||
// Custom complex type derived from another complex type, with a constraint
|
||||
// on a property
|
||||
// Multiple proposed syntaxes, not yet defined
|
||||
|
||||
// Explicit, but new keyword
|
||||
type EquatorialPerson refines Person where Equatorial(_.home)
|
||||
|
||||
// Explicit with existing keyword, might be confusing if expectations regarding 'is'
|
||||
type EquatorialPerson is Person where Equatorial(_.home)
|
||||
|
||||
// Consistent and Python-friendly but can be confused with structural extension
|
||||
type EquatorialPerson(Person) where Equatorial(_.home)
|
||||
|
||||
// Allow new properties, probably not useful
|
||||
type EquatorialPerson extends Person where Equatorial(_.home)
|
||||
@@ -1,15 +0,0 @@
|
||||
# type: ignore
|
||||
# ruff: disable[F821]
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def func(
|
||||
col1: Column[float + (0 <= _ <= 1)],
|
||||
col2: Column[float + (0 <= _ <= 1)],
|
||||
) -> Column[float + (0 <= _ <= 2)]:
|
||||
result: Column[float + (0 <= _ <= 2)] = col1 + col2
|
||||
return result
|
||||
|
||||
|
||||
def func2(a: int, /, b: float, *, c: str):
|
||||
pass
|
||||
@@ -1,33 +0,0 @@
|
||||
type Foo1 = float
|
||||
type Foo2 = float where (_ > 3)
|
||||
type Foo3 = int | float
|
||||
type Foo4 = int where (_ > 3) | float where (_ > 3)
|
||||
type Foo5 = (int | float) where (_ > 3)
|
||||
type Foo6 = {
|
||||
foo: float
|
||||
bar: float where (_ > 3)
|
||||
}
|
||||
|
||||
type Foo7[T] = T where (_ > 3)
|
||||
type Foo8[A, B<:int] = {
|
||||
a: A
|
||||
b: B
|
||||
}
|
||||
|
||||
type Complex = {
|
||||
a: int
|
||||
b: int
|
||||
}
|
||||
type Complex2 = Complex where (_.a > 3 & _.b < 5)
|
||||
|
||||
predicate Positive(n: int) = n >= 0
|
||||
|
||||
extend Foo1 {
|
||||
op __add__(Foo1) -> Foo1
|
||||
}
|
||||
|
||||
extend Foo7[T] {
|
||||
op __add__(Foo7[T]) -> Foo7[T]
|
||||
}
|
||||
|
||||
type Optional[T] = None | T
|
||||
@@ -6,7 +6,7 @@ c = a + b # -> int
|
||||
c = "invalid" # -> can't assign str to int variable
|
||||
|
||||
d = True
|
||||
e = d + d
|
||||
e = d + d # -> addition not defined between booleans
|
||||
|
||||
f: float = a
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# type: ignore
|
||||
# ruff: disable [F821]
|
||||
|
||||
distance: Meter = cast(Meter, 123.45)
|
||||
time: Second = cast(Second, 6.7)
|
||||
distance = cast(Meter, 123.45)
|
||||
time = cast(Second, 6.7)
|
||||
speed = distance / time
|
||||
print(speed)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Return types must have a LUB
|
||||
# Valid
|
||||
def minimum(x: int, y: int):
|
||||
if x < y:
|
||||
return x
|
||||
@@ -5,18 +7,28 @@ def minimum(x: int, y: int):
|
||||
return y
|
||||
|
||||
|
||||
# Invalid
|
||||
def func(a: int):
|
||||
if a < 5:
|
||||
return "Oops"
|
||||
return True
|
||||
|
||||
|
||||
a = 15
|
||||
b = 72
|
||||
c = minimum(a, b)
|
||||
|
||||
|
||||
# Recursive but typable thanks to return hint
|
||||
def factorial(n: int) -> int:
|
||||
if n <= 1:
|
||||
return 1
|
||||
return n * factorial(n - 1)
|
||||
|
||||
|
||||
category = "Category 1" if a < 10 else "Category 2"
|
||||
# Branches must be of the same type
|
||||
category = "Category 1" if a < 10 else "Category 2" # Valid
|
||||
category = "Category 1" if a < 10 else 3 # Invalid
|
||||
|
||||
|
||||
def foo() -> None:
|
||||
|
||||
@@ -15,7 +15,9 @@ extend Coordinate {
|
||||
type Difference[T <: float] = T
|
||||
type MeterDifference = Difference[Meter]
|
||||
|
||||
type CompDiff[T <: float] = {
|
||||
type CompDiff[T <: float] = object
|
||||
|
||||
extend CompDiff[T <: float] {
|
||||
prop d1: Difference[T]
|
||||
prop d2: Difference[T]
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
# type: ignore
|
||||
# ruff: disable [F821]
|
||||
|
||||
p1: Coordinate
|
||||
p2: Coordinate
|
||||
p1 = cast(Coordinate, object())
|
||||
p2 = cast(Coordinate, object())
|
||||
|
||||
diff_x = p2.x - p1.x
|
||||
diff_y = p2.y - p1.y
|
||||
|
||||
dist = diff_x + diff_y
|
||||
|
||||
p2.x += cast(Meter, 1)
|
||||
p2.x += cast(Meter, 1.0)
|
||||
p2.y = True # invalid, wrong type
|
||||
p2.z = 3 # invalid, no property 'z' on Coordinate
|
||||
p2.x.a = 3 # invalid, no properties on Meter
|
||||
|
||||
foo: list[float] = []
|
||||
foo = cast(list[float], [])
|
||||
|
||||
append = foo.append
|
||||
|
||||
@@ -23,7 +23,7 @@ foo.append(2)
|
||||
append(True) # invalid, must be float
|
||||
append(2)
|
||||
|
||||
bar: list[list[Meter]]
|
||||
bar = cast(list[list[Meter]], [])
|
||||
|
||||
bar.append([p2.x])
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# type: ignore
|
||||
# ruff: disable [F821]
|
||||
|
||||
foo: Foo
|
||||
t1: T1
|
||||
t2: T2
|
||||
foo = cast(Foo, object())
|
||||
t1 = cast(T1, object())
|
||||
t2 = cast(T2, object())
|
||||
|
||||
a = foo.bar(t1)
|
||||
b = foo.bar(t2)
|
||||
@@ -13,6 +13,6 @@ func = foo.bar
|
||||
c = func(t1)
|
||||
d = func(t2)
|
||||
|
||||
t2b: T2b
|
||||
t2b = cast(T2b, object())
|
||||
|
||||
e = foo.bar(t2b)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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): ...
|
||||
@@ -5,7 +5,7 @@ type Celsius = float
|
||||
type Kelvin = float where _ >= 0
|
||||
type Hectopascal = float
|
||||
|
||||
type Temperature = Celsius where in_range(-30.0, 100.0)
|
||||
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
|
||||
@@ -38,7 +38,7 @@ alias RawData = Frame[
|
||||
|
||||
alias Data = Frame[
|
||||
station_id: StationID,
|
||||
timestamp: object,
|
||||
timestamp: Any,
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
humidity: Humidity,
|
||||
@@ -46,7 +46,7 @@ alias Data = Frame[
|
||||
|
||||
alias DataWithHI = Frame[
|
||||
station_id: StationID,
|
||||
timestamp: object,
|
||||
timestamp: Any,
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
humidity: Humidity,
|
||||
@@ -54,12 +54,12 @@ alias DataWithHI = Frame[
|
||||
]
|
||||
|
||||
alias DailyAverages = Frame[
|
||||
timestamp: object,
|
||||
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)(_)
|
||||
// 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)(_)
|
||||
|
||||
@@ -11,7 +11,7 @@ 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, 1.0
|
||||
min_hum, max_hum = 0.0, 100.0
|
||||
|
||||
N = 3000
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def daily_avg(df: DataWithHI):
|
||||
DailyAverages,
|
||||
df.groupby(
|
||||
by=[
|
||||
df["station_id"],
|
||||
"station_id",
|
||||
df["timestamp"].dt.day.rename("day"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -136,15 +136,6 @@ class ConstraintType:
|
||||
constraint: Expr
|
||||
|
||||
|
||||
class ComplexType:
|
||||
members: list[MemberStmt]
|
||||
|
||||
|
||||
class ExtensionType:
|
||||
base: Type
|
||||
extension: ComplexType
|
||||
|
||||
|
||||
class FunctionType:
|
||||
params: ParamSpec
|
||||
returns: Type
|
||||
|
||||
@@ -44,11 +44,6 @@ class BaseType:
|
||||
args: tuple[MidasType, ...]
|
||||
|
||||
|
||||
class ConstraintType:
|
||||
type: MidasType
|
||||
constraint: ast.expr
|
||||
|
||||
|
||||
class FrameColumn:
|
||||
name: Optional[str]
|
||||
type: Optional[MidasType]
|
||||
|
||||
14
justfile
Normal file
14
justfile
Normal file
@@ -0,0 +1,14 @@
|
||||
# Local Variables:
|
||||
# mode: makefile
|
||||
# End:
|
||||
set shell := ["bash", "-uc"]
|
||||
|
||||
build-docs:
|
||||
typst c --root . docs/manual.typ
|
||||
typst c --root . docs/function_subtyping.typ
|
||||
|
||||
tests:
|
||||
uv run -m tests
|
||||
|
||||
check-docstrings:
|
||||
uv run scripts/docstring_checker.py
|
||||
@@ -256,12 +256,6 @@ class Type(ABC):
|
||||
@abstractmethod
|
||||
def visit_constraint_type(self, type: ConstraintType) -> T: ...
|
||||
|
||||
@abstractmethod
|
||||
def visit_complex_type(self, type: ComplexType) -> T: ...
|
||||
|
||||
@abstractmethod
|
||||
def visit_extension_type(self, type: ExtensionType) -> T: ...
|
||||
|
||||
@abstractmethod
|
||||
def visit_function_type(self, type: FunctionType) -> T: ...
|
||||
|
||||
@@ -295,23 +289,6 @@ class ConstraintType(Type):
|
||||
return visitor.visit_constraint_type(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ComplexType(Type):
|
||||
members: list[MemberStmt]
|
||||
|
||||
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||
return visitor.visit_complex_type(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExtensionType(Type):
|
||||
base: Type
|
||||
extension: ComplexType
|
||||
|
||||
def accept(self, visitor: Type.Visitor[T]) -> T:
|
||||
return visitor.visit_extension_type(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FunctionType(Type):
|
||||
params: ParamSpec
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from typing import final
|
||||
|
||||
import midas.ast.midas as m
|
||||
|
||||
|
||||
@final
|
||||
class MidasPrinter(
|
||||
m.Expr.Visitor[str],
|
||||
m.Stmt.Visitor[str],
|
||||
@@ -124,19 +127,6 @@ class MidasPrinter(
|
||||
res += " where " + type.constraint.accept(self)
|
||||
return res
|
||||
|
||||
def visit_complex_type(self, type: m.ComplexType) -> str:
|
||||
res: str = "{\n"
|
||||
self.level += 1
|
||||
for member in type.members:
|
||||
res += member.accept(self)
|
||||
res += "\n"
|
||||
self.level -= 1
|
||||
res += self.indented("}")
|
||||
return res
|
||||
|
||||
def visit_extension_type(self, type: m.ExtensionType) -> str:
|
||||
return f"{type.base.accept(self)} & {type.extension.accept(self)}"
|
||||
|
||||
def visit_function_type(self, type: m.FunctionType) -> str:
|
||||
spec: str = self._visit_param_spec(type.params)
|
||||
return f"fn {spec} -> {type.returns.accept(self)}"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from typing import final
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.ast.printer.base import AstPrinter
|
||||
|
||||
|
||||
@final
|
||||
class MidasAstPrinter(
|
||||
AstPrinter,
|
||||
m.Expr.Visitor[None],
|
||||
@@ -177,21 +180,6 @@ class MidasAstPrinter(
|
||||
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_sequence("members", type.members, last=True)
|
||||
|
||||
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():
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ast
|
||||
from typing import final
|
||||
|
||||
import midas.ast.python as p
|
||||
from midas.ast.printer.base import AstPrinter
|
||||
|
||||
|
||||
@final
|
||||
class PythonAstPrinter(
|
||||
AstPrinter,
|
||||
p.MidasType.Visitor[None],
|
||||
@@ -18,14 +20,6 @@ class PythonAstPrinter(
|
||||
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():
|
||||
@@ -123,6 +117,24 @@ class PythonAstPrinter(
|
||||
stmt.iterator.accept(self)
|
||||
self._write_sequence("body", stmt.body, last=True)
|
||||
|
||||
def visit_import_stmt(self, stmt: p.ImportStmt) -> None:
|
||||
self._write_line("ImportStmt")
|
||||
with self._child_level(single=True):
|
||||
self._write_sequence("imports", stmt.imports, print_func=self._print_import)
|
||||
|
||||
def visit_from_import_stmt(self, stmt: p.FromImportStmt) -> None:
|
||||
self._write_line("FromImportStmt")
|
||||
with self._child_level():
|
||||
self._write_line(f'module: "{stmt.module}"')
|
||||
self._write_sequence("imports", stmt.imports, print_func=self._print_import)
|
||||
self._write_line(f"level: {stmt.level}", last=True)
|
||||
|
||||
def _print_import(self, import_: p.ImportAlias) -> None:
|
||||
self._write_line("ImportAlias")
|
||||
with self._child_level():
|
||||
self._write_line(f'name: "{import_.name}"')
|
||||
self._write_line(f'alias: "{import_.alias}"')
|
||||
|
||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||
self._write_line("RawStmt")
|
||||
with self._child_level(single=True):
|
||||
|
||||
@@ -52,9 +52,6 @@ class MidasType(ABC):
|
||||
@abstractmethod
|
||||
def visit_base_type(self, node: BaseType) -> T: ...
|
||||
|
||||
@abstractmethod
|
||||
def visit_constraint_type(self, node: ConstraintType) -> T: ...
|
||||
|
||||
@abstractmethod
|
||||
def visit_frame_column(self, node: FrameColumn) -> T: ...
|
||||
|
||||
@@ -71,15 +68,6 @@ class BaseType(MidasType):
|
||||
return visitor.visit_base_type(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConstraintType(MidasType):
|
||||
type: MidasType
|
||||
constraint: ast.expr
|
||||
|
||||
def accept(self, visitor: MidasType.Visitor[T]) -> T:
|
||||
return visitor.visit_constraint_type(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameColumn(MidasType):
|
||||
name: Optional[str]
|
||||
|
||||
@@ -26,7 +26,11 @@ Circular dependencies and diamond inheritance MUST be avoided
|
||||
|
||||
|
||||
def define_builtins(reg: TypesRegistry):
|
||||
"""Define builtin types and operations"""
|
||||
"""Define builtin types and operations
|
||||
|
||||
Args:
|
||||
reg (TypesRegistry): the types registry
|
||||
"""
|
||||
any = reg.define_type("Any", TopType())
|
||||
unit = reg.define_type("None", UnitType())
|
||||
object = reg.define_type("object", BaseType(name="object"))
|
||||
|
||||
@@ -102,6 +102,11 @@ class CallDispatcher(Generic[E]):
|
||||
self.logger: logging.Logger = logging.getLogger("CallDispatcher")
|
||||
|
||||
def set_reporter(self, reporter: FileReporter):
|
||||
"""Set the current reporter
|
||||
|
||||
Args:
|
||||
reporter (FileReporter): the new file reporter
|
||||
"""
|
||||
self.reporter = reporter
|
||||
|
||||
def get_result(
|
||||
@@ -123,8 +128,8 @@ class CallDispatcher(Generic[E]):
|
||||
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
|
||||
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||
keywords (dict[str, TypedExpr[E]]): the map of keyword arguments
|
||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -250,7 +255,7 @@ class CallDispatcher(Generic[E]):
|
||||
"""Check whether the passed argument types correspond to their matched parameter definitions
|
||||
|
||||
Args:
|
||||
arguments (list[MappedArgument]): the list of argument/parameter pairs
|
||||
arguments (list[MappedArgument[E]]): the list of argument/parameter pairs
|
||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -286,8 +291,8 @@ class CallDispatcher(Generic[E]):
|
||||
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
|
||||
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||
keywords (dict[str, TypedExpr[E]]): the map of keywords arguments
|
||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -385,8 +390,8 @@ class CallDispatcher(Generic[E]):
|
||||
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
|
||||
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||
keywords (dict[str, TypedExpr[E]]): the map of keyword arguments
|
||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -514,8 +519,8 @@ class CallDispatcher(Generic[E]):
|
||||
function / a subtype of another.
|
||||
|
||||
Args:
|
||||
mapped1 (list[MappedArgument]): the first argument mappings (subtype)
|
||||
mapped2 (list[MappedArgument]): the second argument mappings (supertype)
|
||||
mapped1 (list[MappedArgument[E]]): the first argument mappings (subtype)
|
||||
mapped2 (list[MappedArgument[E]]): the second argument mappings (supertype)
|
||||
|
||||
Returns:
|
||||
bool: `True` if `mapped1` is a subtype of `mapped2`, `False` otherwise
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any, Callable, Optional, final
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.ast.location import Location
|
||||
@@ -18,6 +18,7 @@ class PartialPredicate(Predicate):
|
||||
"""A dictionary of already applied parameters"""
|
||||
|
||||
|
||||
@final
|
||||
class Evaluator(m.Expr.Visitor[Any]):
|
||||
"""Helper class to evaluate an expression
|
||||
|
||||
@@ -190,6 +191,7 @@ class Evaluator(m.Expr.Visitor[Any]):
|
||||
"""Evaluate a predicate function call
|
||||
|
||||
Args:
|
||||
location (Location): the location of the call expression
|
||||
predicate (Predicate): the predicate to evaluate
|
||||
args (list[Any]): a list of positional arguments
|
||||
kwargs (dict[str, Any]): a map of keyword arguments
|
||||
@@ -234,6 +236,7 @@ class Evaluator(m.Expr.Visitor[Any]):
|
||||
is set in the context using :func:`set_value` with the parameter's name
|
||||
|
||||
Args:
|
||||
location (Location): the location of the call expression
|
||||
function (Function): the called function
|
||||
args (list[Any]): a list of positional arguments
|
||||
kwargs (dict[str, Any]): a map of keyword arguments
|
||||
|
||||
@@ -86,6 +86,9 @@ class ColumnGroupByMethodRegistry(MethodRegistry[Call]):
|
||||
|
||||
# TODO: maybe better to filter arguments and pass some, in case the
|
||||
# return type depends on them
|
||||
|
||||
# Don't catch UndefinedMethodException because all aggregation
|
||||
# methods should be defined on columns too
|
||||
returns: Type = self.typer.call_method(
|
||||
location=call.location,
|
||||
call_expr=call.call_expr,
|
||||
|
||||
@@ -45,7 +45,7 @@ class ColumnManager:
|
||||
Args:
|
||||
reporter (FileReporter): the file reporter to use for diagnostics
|
||||
location (Location): the subscript's location
|
||||
column (DataFrameType): the column type
|
||||
column (ColumnType): the column type
|
||||
index (TypedExpr): the index
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -353,10 +353,12 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
|
||||
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.
|
||||
formula (Optional[Callable[[Type], Formula]], optional):
|
||||
optional formula builder function to compute the return type.<br>
|
||||
If set, the function should accept the inner column type and
|
||||
return a formula.<br>
|
||||
If `None`, the result is typed as `Column[Any]`.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
Type: the result type
|
||||
|
||||
@@ -51,24 +51,27 @@ class FrameGroupByMethodRegistry(MethodRegistry[Call]):
|
||||
new_columns: list[DataFrameType.Column] = []
|
||||
|
||||
for column in call.groupby.frame.columns:
|
||||
column_groupby: ColumnGroupBy = ColumnGroupBy(column=column.type)
|
||||
result_type: Type = self.typer.call_method(
|
||||
location=call.location,
|
||||
call_expr=call.call_expr,
|
||||
obj=(call.groupby_expr, column_groupby),
|
||||
method_name=method,
|
||||
positional=call.positional,
|
||||
keywords=call.keywords,
|
||||
)
|
||||
if not isinstance(result_type, ColumnType):
|
||||
result_type = ColumnType(type=UnknownType())
|
||||
new_columns.append(
|
||||
DataFrameType.Column(
|
||||
index=column.index,
|
||||
name=column.name,
|
||||
type=result_type,
|
||||
with self.reporter.with_context(f"in column '{column.name}'"):
|
||||
column_groupby: ColumnGroupBy = ColumnGroupBy(column=column.type)
|
||||
# Don't catch UndefinedMethodException because all aggregation
|
||||
# methods should be defined on columns too
|
||||
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)
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ class FrameManager:
|
||||
if col.name == name:
|
||||
index = i
|
||||
replace = True
|
||||
# TODO: check column type here to prevent changing it
|
||||
# TODO: might want to check column type here to disallow changing it
|
||||
new_columns.append(col)
|
||||
|
||||
new_col: DataFrameType.Column = DataFrameType.Column(
|
||||
|
||||
@@ -159,7 +159,10 @@ class FrameMethodRegistry(MethodRegistry[Call]):
|
||||
col_type2 = ColumnType(type=operand[1])
|
||||
|
||||
if col_type2 is not None:
|
||||
col_type = self._get_method_result(call, col_type1, col_type2, method)
|
||||
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,
|
||||
@@ -595,8 +598,62 @@ class FrameMethodRegistry(MethodRegistry[Call]):
|
||||
)
|
||||
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(
|
||||
@@ -626,7 +683,7 @@ class FrameMethodRegistry(MethodRegistry[Call]):
|
||||
)
|
||||
],
|
||||
),
|
||||
returns=FrameGroupBy(frame=call.frame),
|
||||
returns=FrameGroupBy(frame=new_frame),
|
||||
)
|
||||
|
||||
result: CallResult = self.dispatcher.get_result(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, final
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.ast.location import Location
|
||||
@@ -13,11 +13,9 @@ from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.reporter import FileReporter, Reporter
|
||||
from midas.checker.types import (
|
||||
ColumnType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
DataFrameType,
|
||||
DerivedType,
|
||||
ExtensionType,
|
||||
Function,
|
||||
GenericType,
|
||||
ParamSpec,
|
||||
@@ -26,12 +24,13 @@ from midas.checker.types import (
|
||||
TypeVar,
|
||||
UnknownType,
|
||||
)
|
||||
from midas.checker.variance import VarianceInferrer
|
||||
from midas.checker.variance import VarianceManager
|
||||
from midas.lexer.midas import MidasLexer
|
||||
from midas.lexer.token import Token, TokenType
|
||||
from midas.parser.midas import MidasParser
|
||||
|
||||
|
||||
@final
|
||||
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"""
|
||||
|
||||
@@ -112,7 +111,7 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
return self._local_variables[name]
|
||||
return self.types.get_type(name)
|
||||
|
||||
def get_variable(self, name: str) -> Type:
|
||||
def get_variable(self, location: Location, name: str) -> Type:
|
||||
"""Get the type of a variable
|
||||
|
||||
This function will first look into the current predicate's parameters if
|
||||
@@ -120,11 +119,9 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
The the variable is looked up in the preamble (i.e. global environment)
|
||||
|
||||
Args:
|
||||
location (Location): the location of the variable reference
|
||||
name (str): the name of the variable
|
||||
|
||||
Raises:
|
||||
NameError: if the variable cannot be found
|
||||
|
||||
Returns:
|
||||
Type: the type of the variable
|
||||
"""
|
||||
@@ -138,7 +135,8 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
if global_ is not None:
|
||||
return global_
|
||||
|
||||
raise NameError(f"Unknown variable '{name}'")
|
||||
self.reporter.error(location, f"Unknown variable '{name}'")
|
||||
return UnknownType()
|
||||
|
||||
def resolve(self, stmts: list[m.Stmt]):
|
||||
"""Process a sequence of statements
|
||||
@@ -149,10 +147,8 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
for stmt in stmts:
|
||||
stmt.accept(self)
|
||||
|
||||
for name, type in self.types._types.items():
|
||||
if isinstance(type, GenericType):
|
||||
inferrer = VarianceInferrer(self.types)
|
||||
self.types._types[name] = inferrer.infer(type)
|
||||
manager: VarianceManager = VarianceManager(self.types)
|
||||
manager.infer_all()
|
||||
|
||||
def assert_bool(self, expr: m.Expr):
|
||||
"""Check that the given expression is a subtype of `bool` or report an error
|
||||
@@ -174,7 +170,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
type = GenericType(name=name, params=params, body=type)
|
||||
else:
|
||||
type = DerivedType(name=name, type=type)
|
||||
self.types.define_type(name, type)
|
||||
try:
|
||||
self.types.define_type(name, type)
|
||||
except ValueError:
|
||||
self.reporter.error(stmt.location, f"Type {name} already defined")
|
||||
self._local_variables.clear()
|
||||
self._current_name = None
|
||||
|
||||
@@ -182,7 +181,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
name: str = stmt.name.lexeme
|
||||
self._current_name = name
|
||||
type: Type = stmt.type.accept(self)
|
||||
self.types.define_type(name, type)
|
||||
try:
|
||||
self.types.define_type(name, type)
|
||||
except ValueError:
|
||||
self.reporter.error(stmt.location, f"Type {name} already defined")
|
||||
self._current_name = None
|
||||
|
||||
def visit_member_stmt(self, stmt: m.MemberStmt) -> None: ...
|
||||
@@ -205,6 +207,7 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
)
|
||||
|
||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||
name: str = stmt.name.lexeme
|
||||
for spec in stmt.params:
|
||||
for param in spec.mixed:
|
||||
assert param.name is not None
|
||||
@@ -226,14 +229,17 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
returns=type,
|
||||
)
|
||||
self._predicate_params = {}
|
||||
self.types.define_predicate(
|
||||
stmt.name.lexeme,
|
||||
Predicate(
|
||||
type=type,
|
||||
body=stmt.body,
|
||||
alias=len(params) == 0,
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.types.define_predicate(
|
||||
name,
|
||||
Predicate(
|
||||
type=type,
|
||||
body=stmt.body,
|
||||
alias=len(params) == 0,
|
||||
),
|
||||
)
|
||||
except ValueError:
|
||||
self.reporter.error(stmt.location, f"Predicate {name} already defined")
|
||||
|
||||
def _is_valid_predicate(self, body: Type) -> bool:
|
||||
"""Check whether the given type is valid as a predicate's body
|
||||
@@ -297,6 +303,9 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
return result.result
|
||||
|
||||
def visit_unary_expr(self, expr: m.UnaryExpr) -> Type:
|
||||
# First evaluate operand to surface all errors
|
||||
operand: Type = self.type_of(expr.right)
|
||||
|
||||
# Special case because there is no __not__ dunder method
|
||||
match expr.operator:
|
||||
case Token(type=TokenType.BANG):
|
||||
@@ -310,7 +319,6 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
)
|
||||
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(
|
||||
@@ -354,7 +362,7 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
return member
|
||||
|
||||
def visit_variable_expr(self, expr: m.VariableExpr) -> Type:
|
||||
return self.get_variable(expr.name.lexeme)
|
||||
return self.get_variable(expr.location, expr.name.lexeme)
|
||||
|
||||
def visit_grouping_expr(self, expr: m.GroupingExpr) -> Type:
|
||||
return expr.expr.accept(self)
|
||||
@@ -369,12 +377,14 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
return self.types.get_type("float")
|
||||
case str():
|
||||
return self.types.get_type("str")
|
||||
case None:
|
||||
return self.types.get_type("None")
|
||||
case _:
|
||||
self.reporter.warning(expr.location, f"Unknown literal {expr}")
|
||||
return UnknownType()
|
||||
|
||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Type:
|
||||
return self.get_variable("_")
|
||||
return self.get_variable(expr.location, "_")
|
||||
|
||||
def visit_named_type(self, type: m.NamedType) -> Type:
|
||||
name: str = type.name.lexeme
|
||||
@@ -407,24 +417,21 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
return UnknownType()
|
||||
|
||||
def visit_constraint_type(self, type: m.ConstraintType) -> Type:
|
||||
base_type: Type = type.type.accept(self)
|
||||
self._predicate_params["_"] = base_type
|
||||
constraint_type: Type = self.type_of(type.constraint)
|
||||
self._predicate_params = {}
|
||||
if not self.types.is_subtype(constraint_type, self._bool):
|
||||
self.reporter.error(
|
||||
type.constraint.location,
|
||||
f"Constraint must evaluate to a boolean, got {constraint_type}",
|
||||
)
|
||||
|
||||
return ConstraintType(
|
||||
type=type.type.accept(self),
|
||||
type=base_type,
|
||||
constraint=type.constraint,
|
||||
)
|
||||
|
||||
def visit_complex_type(self, type: m.ComplexType) -> ComplexType:
|
||||
return ComplexType(
|
||||
members={
|
||||
member.name.lexeme: member.type.accept(self) for member in type.members
|
||||
}
|
||||
)
|
||||
|
||||
def visit_extension_type(self, type: m.ExtensionType) -> Type:
|
||||
return ExtensionType(
|
||||
base=type.base.accept(self),
|
||||
extension=self.visit_complex_type(type.extension),
|
||||
)
|
||||
|
||||
def visit_function_type(self, type: m.FunctionType) -> Type:
|
||||
return Function(
|
||||
params=self._visit_param_spec(type.params),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ast
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, final
|
||||
|
||||
import midas.ast.python as p
|
||||
from midas.ast.location import Location
|
||||
@@ -48,13 +48,16 @@ TypedExpr = tuple[p.Expr, Type]
|
||||
|
||||
|
||||
class ReturnException(Exception):
|
||||
pass
|
||||
def __init__(self, stmt: p.Stmt):
|
||||
super().__init__()
|
||||
self.stmt: p.Stmt = stmt
|
||||
|
||||
|
||||
class UndefinedMethodException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@final
|
||||
class PythonTyper(
|
||||
p.Stmt.Visitor[None],
|
||||
p.Expr.Visitor[Type],
|
||||
@@ -107,7 +110,7 @@ class PythonTyper(
|
||||
tree: ast.Module = ast.parse(source, filename=path or "<unknown>")
|
||||
parser = PythonParser()
|
||||
stmts: list[p.Stmt] = parser.parse_module(tree)
|
||||
resolver = Resolver()
|
||||
resolver = Resolver(reporter)
|
||||
resolver.resolve(*stmts)
|
||||
|
||||
self.env = self.global_env
|
||||
@@ -115,7 +118,10 @@ class PythonTyper(
|
||||
self.judgements = []
|
||||
self.evaluated_casts = []
|
||||
|
||||
self.check(stmts)
|
||||
try:
|
||||
self.check(stmts)
|
||||
except ReturnException as e:
|
||||
self.reporter.error(e.stmt.location, "Return statement outside of function")
|
||||
|
||||
return TypedAST(
|
||||
stmts=stmts,
|
||||
@@ -257,6 +263,9 @@ class PythonTyper(
|
||||
"""
|
||||
unfolded: Type = unfold_type(obj[1])
|
||||
match unfolded:
|
||||
case TopType() | UnknownType():
|
||||
return UnknownType()
|
||||
|
||||
case DataFrameType():
|
||||
return self.frame_mgr.call(
|
||||
method=method_name,
|
||||
@@ -451,7 +460,6 @@ class PythonTyper(
|
||||
self.env.define(stmt.name, function)
|
||||
|
||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||
# TODO check not yet defined locally
|
||||
type: Type = self.resolve_type_expr(stmt.type)
|
||||
self.env.define(stmt.name, type)
|
||||
|
||||
@@ -484,11 +492,10 @@ class PythonTyper(
|
||||
self._assign_sub(location, var, index, value_type)
|
||||
|
||||
case _:
|
||||
if not isinstance(target, p.VariableExpr):
|
||||
self.logger.warning(f"Unsupported assignment to {target}")
|
||||
self.reporter.warning(
|
||||
target.location, f"Unsupported assignment to {target}"
|
||||
)
|
||||
self.logger.warning(f"Unsupported assignment to {target}")
|
||||
self.reporter.warning(
|
||||
target.location, f"Unsupported assignment to {target}"
|
||||
)
|
||||
|
||||
def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type):
|
||||
"""Type check assignment to the given target
|
||||
@@ -516,11 +523,12 @@ class PythonTyper(
|
||||
def _assign_attr(
|
||||
self, location: Location, object: p.Expr, name: str, value_type: Type
|
||||
):
|
||||
"""Type check assignment to the given target
|
||||
"""Type check assignment to the given attribute target
|
||||
|
||||
Args:
|
||||
location (Location): the location of the assignment
|
||||
target (p.VariableExpr): the assignment's target
|
||||
object (p.Expr): the target attribute's owner object
|
||||
name (str): the target attribute's name
|
||||
value_type (Type): the value to be assigned
|
||||
"""
|
||||
object_type: Type = self.type_of(object)
|
||||
@@ -542,11 +550,15 @@ class PythonTyper(
|
||||
index: p.Expr,
|
||||
value_type: Type,
|
||||
):
|
||||
"""Type check assignment to the given target
|
||||
"""Type check assignment to the given subscript target
|
||||
|
||||
Args:
|
||||
location (Location): the location of the assignment
|
||||
target (p.VariableExpr): the assignment's target
|
||||
var (p.VariableExpr): the target subscript's owner. We only allow
|
||||
a variable expression here because we might modify its type (for
|
||||
example when assigning a column to a dataframe) and reference
|
||||
types are not implemented
|
||||
index (p.Expr): the target subscript's index expression
|
||||
value_type (Type): the value to be assigned
|
||||
"""
|
||||
var_type: Type = self.type_of(var)
|
||||
@@ -569,7 +581,7 @@ class PythonTyper(
|
||||
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
||||
type: Type = self.type_of(stmt.value) if stmt.value is not None else UnitType()
|
||||
self.env.return_types.append(type)
|
||||
raise ReturnException()
|
||||
raise ReturnException(stmt)
|
||||
|
||||
def visit_if_stmt(self, stmt: p.IfStmt) -> None:
|
||||
# Not evaluated in sub-environment because assignments in the test leak out of the if
|
||||
@@ -592,12 +604,16 @@ class PythonTyper(
|
||||
else_returned: bool = self.process_block(stmt.orelse, env)
|
||||
self.env.return_types.extend(env.return_types)
|
||||
if body_returned and else_returned:
|
||||
raise ReturnException()
|
||||
raise ReturnException(stmt)
|
||||
|
||||
def visit_pass(self, stmt: p.Pass) -> None:
|
||||
pass
|
||||
|
||||
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||
outer_env: Environment = self.env
|
||||
inner_env: Environment = Environment(self.env)
|
||||
self.env = inner_env
|
||||
|
||||
item_type: Type = UnknownType()
|
||||
iterator_type: Type = self.type_of(stmt.iterator)
|
||||
if iterator_type != UnknownType():
|
||||
@@ -611,10 +627,12 @@ class PythonTyper(
|
||||
|
||||
self._assign(stmt.location, stmt.target, item_type)
|
||||
self.judge(stmt.target, item_type)
|
||||
env: Environment = Environment(self.env)
|
||||
body_returned: bool = self.process_block(stmt.body, env)
|
||||
body_returned: bool = self.process_block(stmt.body, inner_env)
|
||||
|
||||
self.env = outer_env
|
||||
|
||||
if body_returned:
|
||||
raise ReturnException()
|
||||
raise ReturnException(stmt)
|
||||
|
||||
def visit_import_stmt(self, stmt: p.ImportStmt) -> None:
|
||||
self._visit_imports(stmt.location, stmt.imports)
|
||||
@@ -682,6 +700,20 @@ class PythonTyper(
|
||||
right: TypedExpr,
|
||||
method: str,
|
||||
) -> Type:
|
||||
"""Compute the result type of a binary operation method call
|
||||
|
||||
This method is called for dunder methods called by binary operators
|
||||
|
||||
Args:
|
||||
location (Location): the location of the operation
|
||||
expr (p.Expr): the expression which triggered this resolution
|
||||
left (TypedExpr): the left operand
|
||||
right (TypedExpr): the right operand
|
||||
method (str): the method name
|
||||
|
||||
Returns:
|
||||
Type: the result type
|
||||
"""
|
||||
try:
|
||||
return self.call_method(
|
||||
location=location,
|
||||
@@ -745,14 +777,21 @@ class PythonTyper(
|
||||
match expr.callee:
|
||||
case p.GetExpr(object=obj, name=method):
|
||||
obj_type: Type = self.type_of(obj)
|
||||
return self.call_method(
|
||||
location=expr.location,
|
||||
call_expr=expr,
|
||||
obj=(obj, obj_type),
|
||||
method_name=method,
|
||||
positional=positional,
|
||||
keywords=keywords,
|
||||
)
|
||||
try:
|
||||
return self.call_method(
|
||||
location=expr.location,
|
||||
call_expr=expr,
|
||||
obj=(obj, obj_type),
|
||||
method_name=method,
|
||||
positional=positional,
|
||||
keywords=keywords,
|
||||
)
|
||||
except UndefinedMethodException:
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Unknown method {method} on type {obj_type}",
|
||||
)
|
||||
return UnknownType()
|
||||
|
||||
callee: Type = self.type_of(expr.callee)
|
||||
result: CallResult = self.dispatcher.get_result(
|
||||
@@ -835,7 +874,7 @@ class PythonTyper(
|
||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> Type:
|
||||
test_type: Type = self.type_of(expr.test)
|
||||
|
||||
# TODO Allow subtypes or any type
|
||||
# Strict: test must be a subtype of bool, or UnknownType
|
||||
if (
|
||||
not self.is_subtype(test_type, self.types.get_type("bool"))
|
||||
and test_type != UnknownType()
|
||||
@@ -974,13 +1013,13 @@ class PythonTyper(
|
||||
|
||||
if len(node.args) != 0:
|
||||
args: list[Type] = [self.resolve_type_expr(arg) for arg in node.args]
|
||||
return self.types.apply_generic(base, args)
|
||||
try:
|
||||
return self.types.apply_generic(base, args)
|
||||
except Exception as e:
|
||||
self.reporter.error(node.location, f"Cannot apply generic type: {e}")
|
||||
return UnknownType()
|
||||
return base
|
||||
|
||||
def visit_constraint_type(self, node: p.ConstraintType) -> Type:
|
||||
self.reporter.warning(node.location, "ConstraintType not yet supported")
|
||||
return UnknownType()
|
||||
|
||||
def visit_frame_column(self, node: p.FrameColumn) -> ColumnType:
|
||||
return ColumnType(
|
||||
type=(
|
||||
@@ -1055,10 +1094,13 @@ class PythonTyper(
|
||||
bound: Optional[Type] = None
|
||||
variance: Variance = Variance.INVARIANT
|
||||
if "bound" in call.keywords:
|
||||
bound_type: p.MidasType = self._parse_type_from_expr(
|
||||
call.keywords["bound"]
|
||||
)
|
||||
bound = self.resolve_type_expr(bound_type)
|
||||
try:
|
||||
bound_type: p.MidasType = self._parse_type_from_expr(
|
||||
call.keywords["bound"]
|
||||
)
|
||||
bound = self.resolve_type_expr(bound_type)
|
||||
except NotImplementedError:
|
||||
bound = UnknownType()
|
||||
|
||||
if is_kw_true("covariant"):
|
||||
variance = Variance.COVARIANT
|
||||
@@ -1073,7 +1115,13 @@ class PythonTyper(
|
||||
else:
|
||||
variance = Variance.CONTRAVARIANT
|
||||
var: TypeVar = TypeVar(name=name, bound=bound, variance=variance)
|
||||
self.types.define_type(name, var)
|
||||
try:
|
||||
self.types.define_type(name, var)
|
||||
except ValueError:
|
||||
self.reporter.error(
|
||||
call.location,
|
||||
f"A type or type variable with the name {name} is already defined",
|
||||
)
|
||||
return var
|
||||
|
||||
case _:
|
||||
@@ -1104,6 +1152,14 @@ class PythonTyper(
|
||||
return parser._parse_type(node.body)
|
||||
case p.VariableExpr(name=name):
|
||||
return p.BaseType(location=location, base=name, args=())
|
||||
case p.SubscriptExpr(object=p.VariableExpr(name=name), index=arg):
|
||||
args: tuple[p.MidasType, ...] = (
|
||||
tuple(self._parse_type_from_expr(a) for a in arg.items)
|
||||
if isinstance(arg, p.TupleExpr)
|
||||
else (self._parse_type_from_expr(arg),)
|
||||
)
|
||||
return p.BaseType(location=location, base=name, args=args)
|
||||
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1145,8 +1201,9 @@ class PythonTyper(
|
||||
return False, None
|
||||
|
||||
if key is None:
|
||||
# TODO: check that value is always a dict
|
||||
assert isinstance(value_val, dict)
|
||||
# If literal value is not a dict, invalid Python -> abort
|
||||
if not isinstance(value_val, dict):
|
||||
return False, None
|
||||
pairs.extend(value_val.items())
|
||||
else:
|
||||
pairs.append((key_val, value_val))
|
||||
@@ -1272,9 +1329,7 @@ class PythonTyper(
|
||||
|
||||
case BaseType():
|
||||
# TODO: do we want to allow cast(float, int)? would require runtime conversion
|
||||
if not self.types.is_subtype(
|
||||
subject_type, target_type
|
||||
) or not self.types.is_subtype(target_type, subject_type):
|
||||
if not self.types.are_equivalent(subject_type, target_type):
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Value {lit_value!r} of type {subject_type} cannot be cast as {target_type}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Optional, TypeAlias
|
||||
|
||||
from midas.ast.midas import MemberKind
|
||||
from midas.checker.builtins import BUILTIN_SUBTYPES
|
||||
@@ -8,11 +8,9 @@ from midas.checker.types import (
|
||||
AppliedType,
|
||||
BaseType,
|
||||
ColumnType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
DataFrameType,
|
||||
DerivedType,
|
||||
ExtensionType,
|
||||
Function,
|
||||
GenericType,
|
||||
OverloadedFunction,
|
||||
@@ -26,6 +24,8 @@ from midas.checker.types import (
|
||||
substitute_typevars,
|
||||
)
|
||||
|
||||
Match: TypeAlias = tuple[Function.Parameter, Function.Parameter]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Member:
|
||||
@@ -202,14 +202,6 @@ class TypesRegistry:
|
||||
case (BaseType(name=name1), BaseType(name=name2)):
|
||||
return self.is_builtin_subtype(name1, name2)
|
||||
|
||||
case (ComplexType(properties=props1), ComplexType(properties=props2)):
|
||||
for k, t in props2.items():
|
||||
if k not in props1:
|
||||
return False
|
||||
if not self.is_subtype(props1[k], t):
|
||||
return False
|
||||
return True
|
||||
|
||||
case (DataFrameType(columns=columns1), DataFrameType(columns=columns2)):
|
||||
# TODO: check order?
|
||||
by_name1: dict[str, DataFrameType.Column] = {
|
||||
@@ -223,7 +215,6 @@ class TypesRegistry:
|
||||
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
|
||||
@@ -270,7 +261,6 @@ class TypesRegistry:
|
||||
"""
|
||||
return self.is_subtype(type1, type2) and self.is_subtype(type2, type1)
|
||||
|
||||
# TODO: verify the logic in here
|
||||
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
||||
"""Check whether a function is a subtype of another
|
||||
|
||||
@@ -281,14 +271,24 @@ class TypesRegistry:
|
||||
Returns:
|
||||
bool: whether `func1` is a subtype of `func2`
|
||||
"""
|
||||
|
||||
# Let func1 = (S1, R1) where S1 = (P1, M1, K1)
|
||||
# Let func2 = (S2, R2) where S2 = (P2, M2, K2)
|
||||
# We want to check that func1 <: func2
|
||||
# i.e. R1 <: R2 and S2 <: S1
|
||||
|
||||
# R1 <: R2
|
||||
if not self.is_subtype(func1.returns, func2.returns):
|
||||
return False
|
||||
|
||||
# Extract P1, M1, K1
|
||||
pos1: list[Function.Parameter] = func1.params.pos
|
||||
mixed1: list[Function.Parameter] = func1.params.mixed
|
||||
kw1: dict[str, Function.Parameter] = {
|
||||
param.name: param for param in func1.params.kw
|
||||
}
|
||||
|
||||
# Extract P2, M2, K2
|
||||
pos2: list[Function.Parameter] = func2.params.pos
|
||||
mixed2: list[Function.Parameter] = func2.params.mixed
|
||||
kw2: dict[str, Function.Parameter] = {
|
||||
@@ -296,89 +296,118 @@ class TypesRegistry:
|
||||
}
|
||||
|
||||
mixed_by_pos: dict[int, Function.Parameter] = {
|
||||
param.pos: param for param in mixed2
|
||||
param.pos: param for param in mixed1
|
||||
}
|
||||
mixed_by_name: dict[str, Function.Parameter] = {
|
||||
param.name: param for param in mixed2
|
||||
param.name: param for param in mixed1
|
||||
}
|
||||
|
||||
def is_arg_subtype(sub: Function.Parameter, sup: Function.Parameter) -> bool:
|
||||
if not self.is_subtype(sub.type, sup.type):
|
||||
return False
|
||||
if not sup.required and sub.required:
|
||||
return False
|
||||
return True
|
||||
matches: list[Match] = []
|
||||
|
||||
for param1 in pos1:
|
||||
param2: Function.Parameter
|
||||
if param1.pos < len(pos2):
|
||||
param2 = pos2[param1.pos]
|
||||
elif param1.pos in mixed_by_pos:
|
||||
param2 = mixed_by_pos[param1.pos]
|
||||
elif not param1.required:
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
if not is_arg_subtype(param2, param1):
|
||||
return False
|
||||
|
||||
for name, param1 in kw1.items():
|
||||
param2: Function.Parameter
|
||||
if name in kw2:
|
||||
param2 = kw2[name]
|
||||
elif name in mixed_by_name:
|
||||
param2 = mixed_by_name[name]
|
||||
elif not param1.required:
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
if not is_arg_subtype(param2, param1):
|
||||
return False
|
||||
|
||||
for param1 in mixed1:
|
||||
pos_param2: Optional[Function.Parameter] = None
|
||||
kw_param2: Optional[Function.Parameter] = None
|
||||
if param1.name in kw2:
|
||||
kw_param2 = kw2[param1.name]
|
||||
elif param1.name in mixed_by_name:
|
||||
kw_param2 = mixed_by_name[param1.name]
|
||||
if param1.pos < len(pos2):
|
||||
pos_param2 = pos2[param1.pos]
|
||||
elif param1.pos in mixed_by_pos:
|
||||
pos_param2 = mixed_by_pos[param1.pos]
|
||||
|
||||
# No match in func2 and arg is required
|
||||
if pos_param2 is None and kw_param2 is None and param1.required:
|
||||
return False
|
||||
|
||||
# Matching keyword argument
|
||||
if kw_param2 is not None and not is_arg_subtype(kw_param2, param1):
|
||||
return False
|
||||
|
||||
# Matching positional argument
|
||||
if pos_param2 is not None and not is_arg_subtype(pos_param2, param1):
|
||||
return False
|
||||
|
||||
mixed_positions: set[int] = {param.pos for param in mixed1}
|
||||
mixed_names: set[str] = {param.name for param in mixed1}
|
||||
# Each parameter at position i in P2 must be valid at position i in S1
|
||||
# either as a positional-only parameter in P1
|
||||
# or a mixed parameter in M1
|
||||
for param2 in pos2:
|
||||
if not param2.required:
|
||||
continue
|
||||
if param2.pos >= len(pos1) and param2.pos not in mixed_positions:
|
||||
param1: Function.Parameter
|
||||
|
||||
# In P1
|
||||
if param2.pos < len(pos1):
|
||||
param1 = pos1[param2.pos]
|
||||
|
||||
# In M1
|
||||
elif param2.pos in mixed_by_pos:
|
||||
param1 = mixed_by_pos[param2.pos]
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
# not req(p2) => not req(p1)
|
||||
if not param2.required and param1.required:
|
||||
return False
|
||||
matches.append((param1, param2))
|
||||
|
||||
# Each parameter named p in K2 must be valid with name p in S1
|
||||
# either as a keyword-only parameter in K1
|
||||
# or a mixed parameter in M1
|
||||
for name, param2 in kw2.items():
|
||||
if not param2.required:
|
||||
continue
|
||||
if name not in kw1 and name not in mixed_names:
|
||||
param1: Function.Parameter
|
||||
|
||||
# In K1
|
||||
if name in kw1:
|
||||
param1 = kw1[name]
|
||||
|
||||
# in M1
|
||||
elif name in mixed_by_name:
|
||||
param1 = mixed_by_name[name]
|
||||
else:
|
||||
return False
|
||||
|
||||
# not req(p2) => not req(p1)
|
||||
if not param2.required and param1.required:
|
||||
return False
|
||||
matches.append((param1, param2))
|
||||
|
||||
# Each parameter named p at position i in M2 must be valid with name p
|
||||
# in S1 *and* at position i in S1
|
||||
# either as a single mixed parameter in M1
|
||||
# or split into a positional parameter in P1/M1
|
||||
# and a keyword parameter in K1/M1
|
||||
for param2 in mixed2:
|
||||
if param2.required:
|
||||
continue
|
||||
pos_match: bool = param2.pos < len(pos1) or param2.pos in mixed_positions
|
||||
kw_match: bool = param2.name in kw1 or param2.name in mixed_names
|
||||
if not pos_match or not kw_match:
|
||||
pos_param1: Optional[Function.Parameter] = None
|
||||
kw_param1: Optional[Function.Parameter] = None
|
||||
|
||||
# By name in K1
|
||||
if param2.name in kw1:
|
||||
kw_param1 = kw1[param2.name]
|
||||
|
||||
# By name in M1
|
||||
elif param2.name in mixed_by_name:
|
||||
kw_param1 = mixed_by_name[param2.name]
|
||||
|
||||
# By pos in P1
|
||||
if param2.pos < len(pos1):
|
||||
pos_param1 = pos1[param2.pos]
|
||||
|
||||
# By pos in M1
|
||||
elif param2.pos in mixed_by_pos:
|
||||
pos_param1 = mixed_by_pos[param2.pos]
|
||||
|
||||
# Not fully covered
|
||||
if pos_param1 is None or kw_param1 is None:
|
||||
return False
|
||||
|
||||
# Covered by unique mixed parameter in M1
|
||||
if pos_param1 == kw_param1:
|
||||
param1: Function.Parameter = pos_param1
|
||||
# not req(p2) => not req(p1)
|
||||
if not param2.required and param1.required:
|
||||
return False
|
||||
|
||||
matches.append((param1, param2))
|
||||
|
||||
else:
|
||||
# not req(p1)
|
||||
if pos_param1.required or kw_param1.required:
|
||||
return False
|
||||
|
||||
matches.append((pos_param1, param2))
|
||||
matches.append((kw_param1, param2))
|
||||
|
||||
def is_matched(param: Function.Parameter) -> bool:
|
||||
for p1, _ in matches:
|
||||
if p1 == param:
|
||||
return True
|
||||
return False
|
||||
|
||||
all_params1: list[Function.Parameter] = pos1 + mixed1 + list(kw1.values())
|
||||
|
||||
for param1 in all_params1:
|
||||
# No new required parameters
|
||||
if not is_matched(param1) and param1.required:
|
||||
return False
|
||||
|
||||
for param1, param2 in matches:
|
||||
if not self.is_subtype(param2.type, param1.type):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -506,20 +535,6 @@ class TypesRegistry:
|
||||
member_type2 = substitute_typevars(member_type2, substitutions)
|
||||
return member_type2
|
||||
|
||||
case ComplexType(members=members):
|
||||
if member_name in members:
|
||||
return members[member_name]
|
||||
self.logger.debug(f"No member '{member_name}' in {type}")
|
||||
return None
|
||||
|
||||
case ExtensionType(base=base, extension=ComplexType(members=members)):
|
||||
if member_name in members:
|
||||
return members[member_name]
|
||||
self.logger.debug(
|
||||
f"No member '{member_name}' on {type}, looking up in base"
|
||||
)
|
||||
return self.lookup_member(base, member_name)
|
||||
|
||||
case ConstraintType(type=base):
|
||||
return self.lookup_member(base, member_name)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional
|
||||
|
||||
from midas.ast.location import Location
|
||||
@@ -54,6 +55,7 @@ class FileReporter:
|
||||
def __init__(self, base_reporter: Reporter, path: Optional[str]) -> None:
|
||||
self.base_reporter: Reporter = base_reporter
|
||||
self.path: Optional[str] = path
|
||||
self._context: list[str] = []
|
||||
|
||||
def for_file(self, path: Optional[str]) -> FileReporter:
|
||||
"""Create a new file reporter for the given path with the same base reporter
|
||||
@@ -66,6 +68,19 @@ class FileReporter:
|
||||
"""
|
||||
return FileReporter(self.base_reporter, path)
|
||||
|
||||
@contextmanager
|
||||
def with_context(self, ctx: str):
|
||||
"""Push given context for reports inside this manager and pop it on exit
|
||||
|
||||
Args:
|
||||
ctx (str): the context to temporarily push on the stack
|
||||
"""
|
||||
self._context.append(ctx)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._context.pop()
|
||||
|
||||
def report(self, type: DiagnosticType, location: Location, message: str):
|
||||
"""Report a diagnostic to the base reporter
|
||||
|
||||
@@ -74,6 +89,8 @@ class FileReporter:
|
||||
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)
|
||||
|
||||
def error(self, location: Location, message: str):
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from typing import final
|
||||
|
||||
import midas.ast.python as p
|
||||
from midas.ast.location import Location
|
||||
from midas.checker.reporter import FileReporter
|
||||
|
||||
|
||||
class ResolverError(Exception): ...
|
||||
|
||||
|
||||
@final
|
||||
class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
"""A variable assignment and reference resolver
|
||||
|
||||
@@ -11,9 +16,10 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
scope is referred to when a variable is referenced
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, reporter: FileReporter):
|
||||
self.locals: dict[p.Expr, int] = {}
|
||||
self.scopes: list[dict[str, bool]] = [{}]
|
||||
self.reporter: FileReporter = reporter
|
||||
|
||||
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
||||
"""Resolve the given statements or expressions"""
|
||||
@@ -25,29 +31,29 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
"""Begin a new scope inside the current one"""
|
||||
self.scopes.append({})
|
||||
|
||||
def end_scope(self):
|
||||
"""Close the current scope"""
|
||||
self.scopes.pop()
|
||||
def end_scope(self) -> dict[str, bool]:
|
||||
"""Close and return the current scope"""
|
||||
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
|
||||
|
||||
This method must be called *before* evaluating the variable initializer
|
||||
|
||||
Args:
|
||||
location (Location): the location where the name is declared
|
||||
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:
|
||||
return
|
||||
scope: dict[str, bool] = self.scopes[-1]
|
||||
if name in scope:
|
||||
raise ResolverError(
|
||||
f"A variable with the name {name} is already declared in this scope"
|
||||
self.reporter.error(
|
||||
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:
|
||||
"""Define a variable in the current scope
|
||||
@@ -77,7 +83,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
self.locals[expr] = i
|
||||
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:
|
||||
@@ -106,7 +112,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
self.resolve(param.default)
|
||||
|
||||
for param in function.params.all:
|
||||
self.declare(param.name)
|
||||
self.declare(function.location, param.name)
|
||||
self.define(param.name)
|
||||
self.resolve(*function.body)
|
||||
self.end_scope()
|
||||
@@ -116,14 +122,12 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
|
||||
def visit_function(self, stmt: p.Function) -> None:
|
||||
# Declare before resolving body to allow recursion
|
||||
self.declare(stmt.name)
|
||||
self.declare(stmt.location, stmt.name)
|
||||
self.define(stmt.name)
|
||||
self.resolve_function(stmt)
|
||||
|
||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||
self.declare(stmt.name)
|
||||
# NOTE: resolve type here?
|
||||
self.define(stmt.name)
|
||||
self.declare(stmt.location, stmt.name)
|
||||
|
||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
||||
self.resolve(stmt.value)
|
||||
@@ -133,9 +137,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
def _visit_assign(self, target: p.Expr):
|
||||
match target:
|
||||
case p.VariableExpr(name=name):
|
||||
if not self.is_defined(name):
|
||||
self.declare(name)
|
||||
self.define(name)
|
||||
if not self.is_declared(name):
|
||||
self.declare(target.location, name)
|
||||
self.define(name)
|
||||
target.accept(self)
|
||||
|
||||
case p.GetExpr():
|
||||
@@ -145,7 +149,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
target.accept(self)
|
||||
|
||||
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:
|
||||
if stmt.value is not None:
|
||||
@@ -162,20 +168,25 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
# Body
|
||||
self.begin_scope()
|
||||
self.resolve(*stmt.body)
|
||||
self.end_scope()
|
||||
body: dict[str, bool] = self.end_scope()
|
||||
|
||||
# Else
|
||||
self.begin_scope()
|
||||
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:
|
||||
pass
|
||||
|
||||
def visit_for_stmt(self, stmt: p.ForStmt) -> None:
|
||||
self.begin_scope()
|
||||
self.resolve(stmt.iterator)
|
||||
self._visit_assign(stmt.target)
|
||||
self.begin_scope()
|
||||
self.resolve(*stmt.body)
|
||||
self.end_scope()
|
||||
|
||||
@@ -188,7 +199,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
def _resolve_imports(self, imports: list[p.ImportAlias]) -> None:
|
||||
for import_ in imports:
|
||||
name: str = import_.imported_name
|
||||
self.declare(name)
|
||||
self.declare(import_.location, name)
|
||||
self.define(name)
|
||||
|
||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||
@@ -220,8 +231,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||
|
||||
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
||||
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
|
||||
raise ResolverError(
|
||||
f"Cannot use local variable '{expr.name}' in its own initializer"
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Variable '{expr.name}' is declared but may not be defined",
|
||||
) # aka. UnboundLocalError
|
||||
self.resolve_local(expr, expr.name)
|
||||
|
||||
|
||||
@@ -113,28 +113,6 @@ class OverloadedFunction:
|
||||
return "<overloaded function>"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ComplexType:
|
||||
"""A type with inline members"""
|
||||
|
||||
members: dict[str, Type]
|
||||
|
||||
def __str__(self) -> str:
|
||||
props: list[str] = [f"{name}: {type}" for name, type in self.members.items()]
|
||||
return f"{{{', '.join(props)}}}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ExtensionType:
|
||||
"""An extension of a type, adding members through a `ComplexType`"""
|
||||
|
||||
base: Type
|
||||
extension: ComplexType
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.base} & {self.extension}"
|
||||
|
||||
|
||||
class Variance(StrEnum):
|
||||
"""The variance of a :class:`TypeVar`"""
|
||||
|
||||
@@ -323,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):
|
||||
return AppliedType(
|
||||
name=name,
|
||||
@@ -402,9 +362,6 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
||||
case UnknownType() | UnitType():
|
||||
return type
|
||||
|
||||
case TopType() | GenericType():
|
||||
raise NotImplementedError(f"Unsupported type {type}")
|
||||
|
||||
# Ensure exhaustiveness
|
||||
case _:
|
||||
assert_never(type)
|
||||
@@ -468,9 +425,6 @@ def to_annotation(type: Type) -> str:
|
||||
case OverloadedFunction():
|
||||
return "Callable"
|
||||
|
||||
case ComplexType() | ExtensionType():
|
||||
raise NotImplementedError
|
||||
|
||||
case TypeVar(name=name):
|
||||
return name
|
||||
|
||||
@@ -480,8 +434,8 @@ def to_annotation(type: Type) -> str:
|
||||
case AppliedType(name=name, args=args):
|
||||
return f"{name}[{', '.join(map(to_annotation, args))}]"
|
||||
|
||||
case ConstraintType():
|
||||
return str(type)
|
||||
case ConstraintType(type=base):
|
||||
return to_annotation(base)
|
||||
|
||||
case TupleType(items=items):
|
||||
return f"Tuple[{', '.join(map(to_annotation, items))}]"
|
||||
@@ -519,8 +473,6 @@ Type = (
|
||||
| UnitType
|
||||
| Function
|
||||
| OverloadedFunction
|
||||
| ComplexType
|
||||
| ExtensionType
|
||||
| TypeVar
|
||||
| GenericType
|
||||
| AppliedType
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
from midas.checker.registry import Member, TypesRegistry
|
||||
@@ -73,10 +75,14 @@ class Tracker:
|
||||
class VarianceInferrer:
|
||||
"""Helper class to compute type parameter variance"""
|
||||
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
self.types: TypesRegistry = types
|
||||
def __init__(self, manager: VarianceManager) -> None:
|
||||
self.manager: VarianceManager = manager
|
||||
self.tracker: Tracker = Tracker([])
|
||||
|
||||
@property
|
||||
def types(self) -> TypesRegistry:
|
||||
return self.manager.types
|
||||
|
||||
def infer(self, type: GenericType) -> GenericType:
|
||||
"""Infer the variance of a generic type's parameters
|
||||
|
||||
@@ -150,11 +156,13 @@ class VarianceInferrer:
|
||||
# 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:
|
||||
if self.manager.is_in_queue(name):
|
||||
return
|
||||
|
||||
generic: Type = self.types.get_type(name)
|
||||
assert isinstance(generic, GenericType)
|
||||
generic = self.manager.infer(name, generic)
|
||||
|
||||
params: list[TypeVar] = generic.params
|
||||
polarities: dict[Variance, Polarity] = {
|
||||
Variance.INVARIANT: 0,
|
||||
@@ -179,3 +187,66 @@ class VarianceInferrer:
|
||||
case TypeVar():
|
||||
if type in self.tracker:
|
||||
self.tracker.record(type, polarity)
|
||||
|
||||
|
||||
class VarianceManager:
|
||||
"""Coordinator for VarianceInferrer to handle recursive types"""
|
||||
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
self.types: TypesRegistry = types
|
||||
self._queue: list[str] = []
|
||||
self._inferred: set[str] = set()
|
||||
|
||||
def infer_all(self):
|
||||
"""Infer variance on all generic types defined in the registry"""
|
||||
|
||||
for name, type in self.types._types.items():
|
||||
if isinstance(type, GenericType):
|
||||
self.infer(name, type)
|
||||
|
||||
def infer(self, name: str, type: GenericType) -> GenericType:
|
||||
"""Infer variance of parameters of the given type
|
||||
|
||||
Args:
|
||||
name (str): the type's name
|
||||
type (GenericType): the type
|
||||
|
||||
Returns:
|
||||
GenericType: a new generic type with its parameters updated with
|
||||
their inferred variance
|
||||
"""
|
||||
if self.is_inferred(name):
|
||||
return type
|
||||
|
||||
self._queue.append(name)
|
||||
|
||||
inferrer: VarianceInferrer = VarianceInferrer(self)
|
||||
inferred: GenericType = inferrer.infer(type)
|
||||
self.types._types[name] = inferred
|
||||
|
||||
self._queue.pop()
|
||||
self._inferred.add(name)
|
||||
|
||||
return inferred
|
||||
|
||||
def is_in_queue(self, name: str) -> bool:
|
||||
"""Whether the given type's variance is currently being inferred
|
||||
|
||||
Args:
|
||||
name (str): the type's name
|
||||
|
||||
Returns:
|
||||
bool: whether the type is in the queue
|
||||
"""
|
||||
return name in self._queue
|
||||
|
||||
def is_inferred(self, name: str) -> bool:
|
||||
"""Whether the given type's variance has already been inferred
|
||||
|
||||
Args:
|
||||
name (str): the type's name
|
||||
|
||||
Returns:
|
||||
bool: whether the type has been processed
|
||||
"""
|
||||
return name in self._inferred
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Generic, Optional, Protocol, TextIO, TypeVar
|
||||
from typing import Generic, Optional, Protocol, TextIO, TypeVar, final
|
||||
|
||||
import midas.ast.midas as m
|
||||
import midas.ast.python as p
|
||||
@@ -121,6 +121,7 @@ class Highlighter(ABC):
|
||||
self.openings.setdefault((l + 1, 0), []).append(opening)
|
||||
|
||||
|
||||
@final
|
||||
class PythonHighlighter(
|
||||
Highlighter,
|
||||
p.MidasType.Visitor[None],
|
||||
@@ -138,10 +139,6 @@ class PythonHighlighter(
|
||||
self.wrap(arg, "arg")
|
||||
arg.accept(self)
|
||||
|
||||
def visit_constraint_type(self, node: p.ConstraintType) -> None:
|
||||
self.wrap(node, "constraint-type")
|
||||
node.type.accept(self)
|
||||
|
||||
def visit_frame_column(self, node: p.FrameColumn) -> None:
|
||||
self.wrap(node, "frame-column")
|
||||
if node.type is not None:
|
||||
@@ -201,6 +198,10 @@ class PythonHighlighter(
|
||||
for body_stmt in stmt.body:
|
||||
body_stmt.accept(self)
|
||||
|
||||
def visit_import_stmt(self, stmt: p.ImportStmt) -> None: ...
|
||||
|
||||
def visit_from_import_stmt(self, stmt: p.FromImportStmt) -> None: ...
|
||||
|
||||
def visit_binary_expr(self, expr: p.BinaryExpr) -> None: ...
|
||||
|
||||
def visit_compare_expr(self, expr: p.CompareExpr) -> None: ...
|
||||
@@ -259,6 +260,7 @@ class PythonHighlighter(
|
||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None: ...
|
||||
|
||||
|
||||
@final
|
||||
class MidasHighlighter(
|
||||
Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None]
|
||||
):
|
||||
@@ -267,6 +269,11 @@ class MidasHighlighter(
|
||||
def highlight(self, node: Highlightable[MidasHighlighter]):
|
||||
node.accept(self)
|
||||
|
||||
def visit_alias_stmt(self, stmt: m.AliasStmt) -> None:
|
||||
self.wrap(stmt, "alias-stmt")
|
||||
self.wrap(LocatableToken(stmt.name), "type-name")
|
||||
stmt.type.accept(self)
|
||||
|
||||
def visit_type_stmt(self, stmt: m.TypeStmt) -> None:
|
||||
self.wrap(stmt, "type-stmt")
|
||||
self.wrap(LocatableToken(stmt.name), "type-name")
|
||||
@@ -338,21 +345,11 @@ class MidasHighlighter(
|
||||
type.type.accept(self)
|
||||
type.constraint.accept(self)
|
||||
|
||||
def visit_complex_type(self, type: m.ComplexType) -> None:
|
||||
self.wrap(type, "complex-type")
|
||||
for member in type.members:
|
||||
member.accept(self)
|
||||
|
||||
def visit_function_type(self, type: m.FunctionType) -> None:
|
||||
self.wrap(type, "function")
|
||||
self._visit_param_spec(type.params)
|
||||
type.returns.accept(self)
|
||||
|
||||
def visit_extension_type(self, type: m.ExtensionType) -> None:
|
||||
self.wrap(type, "extension")
|
||||
type.base.accept(self)
|
||||
type.extension.accept(self)
|
||||
|
||||
def _visit_param_spec(self, spec: m.ParamSpec) -> None:
|
||||
for param in spec.pos + spec.mixed + spec.kw:
|
||||
param.type.accept(self)
|
||||
@@ -366,6 +363,7 @@ class MidasHighlighter(
|
||||
self.wrap(column, "column")
|
||||
|
||||
|
||||
@final
|
||||
class DiagnosticsHighlighter(Highlighter):
|
||||
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_diagnostic.css"
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ span {
|
||||
|
||||
&.named-type,
|
||||
&.generic-type,
|
||||
&.constraint-type,
|
||||
&.complex-type {
|
||||
&.constraint-type {
|
||||
--col: 150, 150, 150;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,6 @@ span {
|
||||
--col: 103, 192, 224;
|
||||
}
|
||||
|
||||
&.constraint-type {
|
||||
--col: 174, 200, 195;
|
||||
}
|
||||
|
||||
&.frame-column {
|
||||
--col: 216, 231, 81;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ast
|
||||
from typing import Optional
|
||||
from typing import Optional, final
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.checker.registry import TypesRegistry
|
||||
@@ -39,6 +39,7 @@ COMPARISON_OPERATORS: dict[TokenType, type[ast.cmpop]] = {
|
||||
}
|
||||
|
||||
|
||||
@final
|
||||
class ConstraintGenerator(m.Expr.Visitor[ast.expr]):
|
||||
"""Class to generate Python code for constraint expressions"""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, assert_never
|
||||
from typing import Optional, assert_never, final
|
||||
|
||||
import midas.ast.midas as m
|
||||
import midas.ast.python as p
|
||||
@@ -16,11 +16,9 @@ from midas.checker.types import (
|
||||
BaseType,
|
||||
ColumnGroupBy,
|
||||
ColumnType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
DataFrameType,
|
||||
DerivedType,
|
||||
ExtensionType,
|
||||
FrameGroupBy,
|
||||
Function,
|
||||
GenericType,
|
||||
@@ -49,6 +47,7 @@ class Scope:
|
||||
"""A list of aliases defined in the scope, that can be discard afterwards"""
|
||||
|
||||
|
||||
@final
|
||||
class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
"""
|
||||
A class to translate the custom Python AST back into raw `ast` nodes
|
||||
@@ -271,7 +270,9 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
alias: ast.expr = self._make_alias(expr.expr, expr2)
|
||||
|
||||
type: Type = self._get_expr_type(expr)
|
||||
asserts: list[ast.stmt] = self._make_cast_asserts(expr.location, alias, type)
|
||||
asserts: list[ast.stmt] = self._make_cast_asserts(
|
||||
expr.location, alias, type, context=[]
|
||||
)
|
||||
for assert_ in asserts:
|
||||
self._add_assert(assert_)
|
||||
|
||||
@@ -354,7 +355,6 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
)
|
||||
|
||||
def visit_type_assign(self, stmt: p.TypeAssign) -> ast.stmt:
|
||||
# TODO: is that ok?
|
||||
return ast.Pass()
|
||||
|
||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> ast.stmt:
|
||||
@@ -528,7 +528,12 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
raise RuntimeError(f"Cannot get type judgement for {query}")
|
||||
|
||||
def _make_cast_asserts(
|
||||
self, src_location: Location, expr: ast.expr, type: Type
|
||||
self,
|
||||
src_location: Location,
|
||||
expr: ast.expr,
|
||||
type: Type,
|
||||
*,
|
||||
context: list[str],
|
||||
) -> list[ast.stmt]:
|
||||
"""Generate assertions for the given cast expression
|
||||
|
||||
@@ -537,6 +542,7 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
the source file
|
||||
expr (ast.expr): the expression being cast
|
||||
type (Type): the target type
|
||||
context (list[str]): the current context
|
||||
|
||||
Returns:
|
||||
list[ast.stmt]: the generated assertion statements
|
||||
@@ -553,12 +559,16 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
args=[expr, ast.Name(id=name)],
|
||||
keywords=[],
|
||||
),
|
||||
self._make_cast_assert_message(src_location, expr, type),
|
||||
self._make_cast_assert_message(
|
||||
src_location, expr, type, context=context
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
case DerivedType(type=base):
|
||||
return self._make_cast_asserts(src_location, expr, base)
|
||||
return self._make_cast_asserts(
|
||||
src_location, expr, base, context=context
|
||||
)
|
||||
|
||||
case UnitType():
|
||||
return [
|
||||
@@ -570,19 +580,25 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
ast.Constant(value=None),
|
||||
],
|
||||
),
|
||||
self._make_cast_assert_message(src_location, expr, type),
|
||||
self._make_cast_assert_message(
|
||||
src_location, expr, type, context=context
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
case AppliedType(body=body):
|
||||
return self._make_cast_asserts(src_location, expr, body)
|
||||
return self._make_cast_asserts(
|
||||
src_location, expr, body, context=context
|
||||
)
|
||||
|
||||
case ConstraintType(type=base, constraint=constraint):
|
||||
asserts: list[ast.stmt] = self._make_cast_asserts(
|
||||
src_location, expr, base
|
||||
src_location, expr, base, context=context
|
||||
)
|
||||
asserts.append(
|
||||
self._make_constraint_assert(src_location, expr, constraint)
|
||||
self._make_constraint_assert(
|
||||
src_location, expr, constraint, context=context
|
||||
)
|
||||
)
|
||||
return asserts
|
||||
|
||||
@@ -590,7 +606,9 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
# TODO: check with type from arguments / use call-site context
|
||||
if bound is None:
|
||||
return []
|
||||
return self._make_cast_asserts(src_location, expr, bound)
|
||||
return self._make_cast_asserts(
|
||||
src_location, expr, bound, context=context
|
||||
)
|
||||
|
||||
case TupleType(items=items):
|
||||
asserts: list[ast.stmt] = [
|
||||
@@ -600,13 +618,17 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
args=[expr, ast.Name(id="tuple")],
|
||||
keywords=[],
|
||||
),
|
||||
self._make_cast_assert_message(src_location, expr, type),
|
||||
self._make_cast_assert_message(
|
||||
src_location, expr, type, context=context
|
||||
),
|
||||
),
|
||||
]
|
||||
assert isinstance(expr, ast.Tuple)
|
||||
for item, item_type in zip(expr.elts, items):
|
||||
asserts.extend(
|
||||
self._make_cast_asserts(src_location, item, item_type)
|
||||
self._make_cast_asserts(
|
||||
src_location, item, item_type, context=context
|
||||
)
|
||||
)
|
||||
return asserts
|
||||
|
||||
@@ -620,7 +642,11 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
keywords=[],
|
||||
),
|
||||
self._make_cast_assert_message(
|
||||
src_location, expr, type, ": Not a dataframe"
|
||||
src_location,
|
||||
expr,
|
||||
type,
|
||||
context=context,
|
||||
extra=": Not a dataframe",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -636,7 +662,8 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
src_location,
|
||||
expr,
|
||||
type,
|
||||
f": Missing column {column.name}",
|
||||
context=context,
|
||||
extra=f": Missing column '{column.name}'",
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -647,6 +674,7 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
value=expr, slice=ast.Constant(value=column.name)
|
||||
),
|
||||
column.type,
|
||||
context=context + [f"in column '{column.name}'"],
|
||||
)
|
||||
)
|
||||
return asserts
|
||||
@@ -661,12 +689,19 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
keywords=[],
|
||||
),
|
||||
self._make_cast_assert_message(
|
||||
src_location, expr, type, ": Not a column"
|
||||
src_location,
|
||||
expr,
|
||||
type,
|
||||
context=context,
|
||||
extra=": Not a column",
|
||||
),
|
||||
),
|
||||
]
|
||||
inner_assert: Optional[ast.stmt] = self._make_column_inner_assert(
|
||||
src_location, expr, type
|
||||
src_location,
|
||||
expr,
|
||||
type,
|
||||
context,
|
||||
)
|
||||
if inner_assert is not None:
|
||||
asserts.append(inner_assert)
|
||||
@@ -675,8 +710,6 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
case (
|
||||
Function()
|
||||
| OverloadedFunction()
|
||||
| ComplexType()
|
||||
| ExtensionType()
|
||||
| GenericType()
|
||||
| FrameGroupBy()
|
||||
| ColumnGroupBy()
|
||||
@@ -693,7 +726,9 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
location: Location,
|
||||
expr: ast.expr,
|
||||
type: Type,
|
||||
extra: Optional[str] = None,
|
||||
*,
|
||||
context: list[str],
|
||||
extra: str = "",
|
||||
) -> ast.expr:
|
||||
"""Build an AST node for a cast assertion message
|
||||
|
||||
@@ -707,12 +742,14 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
source file
|
||||
expr (ast.expr): the expression being cast
|
||||
type (Type): the target type
|
||||
extra (Optional[str], optional): extra text to append at the end of
|
||||
the message. Defaults to None.
|
||||
context (list[str]): the current context
|
||||
extra (str, optional): extra text to append at the end of
|
||||
the message. Defaults to "".
|
||||
|
||||
Returns:
|
||||
ast.expr: the generated message (as an f-string)
|
||||
"""
|
||||
context_str: str = "".join(map(lambda c: f", {c}", context))
|
||||
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
|
||||
# f"file.py:L1:1: CastError: Cannot cast {type(expr).__name__} to Type"
|
||||
return ast.JoinedStr(
|
||||
@@ -729,12 +766,17 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
),
|
||||
conversion=-1,
|
||||
),
|
||||
ast.Constant(f" to {type}{extra or ''}"),
|
||||
ast.Constant(f" to {type}{context_str}{extra}"),
|
||||
]
|
||||
)
|
||||
|
||||
def _make_constraint_assert(
|
||||
self, src_location: Location, expr: ast.expr, constraint: m.Expr
|
||||
self,
|
||||
src_location: Location,
|
||||
expr: ast.expr,
|
||||
constraint: m.Expr,
|
||||
*,
|
||||
context: list[str],
|
||||
) -> ast.stmt:
|
||||
"""Build an assertion for the given constraint on the given expression
|
||||
|
||||
@@ -743,6 +785,7 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
source file
|
||||
expr (ast.expr): the expression subject to `constraint`
|
||||
constraint (m.Expr): the constraint applied on `expr`
|
||||
context (list[str]): the current context
|
||||
|
||||
Returns:
|
||||
ast.stmt: the assert statement checking the constraint
|
||||
@@ -754,11 +797,13 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
args=[expr],
|
||||
keywords=[],
|
||||
),
|
||||
self._make_constraint_assert_message(src_location, constraint),
|
||||
self._make_constraint_assert_message(
|
||||
src_location, constraint, context=context
|
||||
),
|
||||
)
|
||||
|
||||
def _make_constraint_assert_message(
|
||||
self, location: Location, constraint: m.Expr
|
||||
self, location: Location, constraint: m.Expr, *, context: list[str]
|
||||
) -> ast.expr:
|
||||
"""Build an assert message for the given constraint
|
||||
|
||||
@@ -766,16 +811,18 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
location (Location): the location of the cast expression in the
|
||||
source file
|
||||
constraint (m.Expr): the constraint
|
||||
context (list[str]): the current context
|
||||
|
||||
Returns:
|
||||
ast.expr: the assert message
|
||||
"""
|
||||
printer = MidasPrinter()
|
||||
constraint_str: str = printer.print(constraint)
|
||||
context_str: str = "".join(map(lambda c: f", {c}", context))
|
||||
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
|
||||
# f"file.py:L1:1: ConstraintError: Value does not fit constraint 'v > 0'"
|
||||
return ast.Constant(
|
||||
f"{loc_str}: ConstraintError: Value does not fit constraint '{constraint_str}'"
|
||||
f"{loc_str}: ConstraintError: Value does not fit constraint '{constraint_str}'{context_str}"
|
||||
)
|
||||
|
||||
def _get_constraint(self, expr: m.Expr) -> ast.expr:
|
||||
@@ -884,7 +931,11 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
)
|
||||
|
||||
def _make_column_inner_assert(
|
||||
self, src_location: Location, column: ast.expr, type: ColumnType
|
||||
self,
|
||||
src_location: Location,
|
||||
column: ast.expr,
|
||||
type: ColumnType,
|
||||
context: list[str],
|
||||
) -> Optional[ast.stmt]:
|
||||
"""Build a for-loop checking the type of values inside a column
|
||||
|
||||
@@ -893,18 +944,21 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
source file
|
||||
column (ast.expr): the column being cast
|
||||
type (ColumnType): the type of the column
|
||||
context (list[str]): the current context
|
||||
|
||||
Returns:
|
||||
Optional[ast.stmt]: a for-loop checking the values, or `None` if no
|
||||
assertions are necessary
|
||||
"""
|
||||
# TODO: improve message, maybe chain contexts
|
||||
col: ast.expr = ast.Name(id="col")
|
||||
body: list[ast.stmt] = self._make_cast_asserts(src_location, col, type.type)
|
||||
|
||||
value: ast.expr = ast.Name(id="value")
|
||||
body: list[ast.stmt] = self._make_cast_asserts(
|
||||
src_location, value, type.type, context=context
|
||||
)
|
||||
if len(body) == 0:
|
||||
return None
|
||||
return ast.For(
|
||||
target=col,
|
||||
target=value,
|
||||
iter=column,
|
||||
body=body,
|
||||
orelse=[],
|
||||
|
||||
@@ -8,11 +8,9 @@ from midas.checker.types import (
|
||||
BaseType,
|
||||
ColumnGroupBy,
|
||||
ColumnType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
DataFrameType,
|
||||
DerivedType,
|
||||
ExtensionType,
|
||||
FrameGroupBy,
|
||||
Function,
|
||||
GenericType,
|
||||
@@ -55,7 +53,7 @@ class StubsGenerator:
|
||||
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
|
||||
# TODO: check if added members on builtin type, or prevent it
|
||||
match type:
|
||||
case BaseType(name=name_) if name == name_:
|
||||
continue
|
||||
@@ -107,7 +105,9 @@ class StubsGenerator:
|
||||
"""
|
||||
base_type: Type = type
|
||||
|
||||
# TODO: improve
|
||||
# Generate simple assignment for type aliases
|
||||
# A type alias will have a name that is different from the type represents
|
||||
# or will neither be a `DeriveType` nor a `GenericType`
|
||||
match type:
|
||||
case DerivedType(name=name_) | GenericType(name=name_) if name_ == name:
|
||||
pass
|
||||
@@ -269,14 +269,6 @@ class StubsGenerator:
|
||||
right=self.dump_type(overloads[-1]),
|
||||
)
|
||||
|
||||
case ComplexType():
|
||||
name: str = self.new_stub_name()
|
||||
self.generate_stub(name, type)
|
||||
return ast.Name(id=name)
|
||||
|
||||
case ExtensionType():
|
||||
raise NotImplementedError
|
||||
|
||||
case TypeVar():
|
||||
return ast.Name(id=type.name)
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ from midas.ast.midas import (
|
||||
AliasStmt,
|
||||
BinaryExpr,
|
||||
CallExpr,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
Expr,
|
||||
ExtendStmt,
|
||||
ExtensionType,
|
||||
FrameType,
|
||||
FunctionType,
|
||||
GenericType,
|
||||
@@ -187,19 +185,9 @@ class MidasParser(Parser[list[Stmt]]):
|
||||
Returns:
|
||||
TypeExpr: the parsed type expression
|
||||
"""
|
||||
base: Type
|
||||
if self.match(TokenType.FUNC):
|
||||
base = self.function()
|
||||
else:
|
||||
base = self.constraint_type()
|
||||
if self.match(TokenType.AND):
|
||||
extension: ComplexType = self.complex_type()
|
||||
return ExtensionType(
|
||||
location=Location.span(base.location, extension.location),
|
||||
base=base,
|
||||
extension=extension,
|
||||
)
|
||||
return base
|
||||
return self.function()
|
||||
return self.constraint_type()
|
||||
|
||||
def constraint_type(self) -> Type:
|
||||
"""Parse a constraint type expression
|
||||
@@ -235,9 +223,6 @@ class MidasParser(Parser[list[Stmt]]):
|
||||
self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
||||
return type
|
||||
|
||||
if self.check(TokenType.LEFT_BRACE):
|
||||
return self.complex_type()
|
||||
|
||||
return self.generic_type()
|
||||
|
||||
def generic_type(self) -> Type:
|
||||
@@ -295,34 +280,6 @@ class MidasParser(Parser[list[Stmt]]):
|
||||
name=name,
|
||||
)
|
||||
|
||||
def complex_type(self) -> ComplexType:
|
||||
"""Parse a complex type expression
|
||||
|
||||
A complex type consists of zero or more member statements enclosed in
|
||||
curly braces
|
||||
|
||||
Returns:
|
||||
ComplexType: the parsed complex type expression
|
||||
"""
|
||||
left: Token = self.consume(
|
||||
TokenType.LEFT_BRACE, "Expected '{' to start type body"
|
||||
)
|
||||
members: list[MemberStmt] = []
|
||||
# TODO: add keyword to differentiate properties and methods,
|
||||
# and allow multiple methods with the same name but not properties
|
||||
names: set[str] = set()
|
||||
while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end():
|
||||
member: MemberStmt = self.member_stmt()
|
||||
# if member.name.lexeme in names:
|
||||
# raise self.error(member.name, "Duplicate property")
|
||||
# names.add(member.name.lexeme)
|
||||
members.append(member)
|
||||
right: Token = self.consume(TokenType.RIGHT_BRACE, "Unclosed type body")
|
||||
return ComplexType(
|
||||
location=left.location_to(right),
|
||||
members=members,
|
||||
)
|
||||
|
||||
def frame_type(self) -> FrameType:
|
||||
"""Parse a frame type expression
|
||||
|
||||
@@ -632,7 +589,7 @@ class MidasParser(Parser[list[Stmt]]):
|
||||
return WildcardExpr(location=token.get_location(), token=token)
|
||||
|
||||
if self.match(TokenType.LEFT_PAREN):
|
||||
expr: Expr = self.constraint()
|
||||
expr: Expr = self.expression()
|
||||
right: Token = self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis")
|
||||
return GroupingExpr(location=token.location_to(right), expr=expr)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from midas.ast.python import (
|
||||
CallExpr,
|
||||
CastExpr,
|
||||
CompareExpr,
|
||||
ConstraintType,
|
||||
DictExpr,
|
||||
Expr,
|
||||
ExpressionStmt,
|
||||
@@ -337,30 +336,6 @@ class PythonParser:
|
||||
args=(),
|
||||
)
|
||||
|
||||
case ast.BinOp(left=left_expr, op=ast.Add(), right=right_expr):
|
||||
left = self._parse_type(left_expr)
|
||||
match left:
|
||||
# If chained constraints, separate base type and rebuild constraint
|
||||
case ConstraintType(type=left_type, constraint=left_constraint):
|
||||
constraint = ast.BinOp(
|
||||
left=left_constraint,
|
||||
op=ast.Add(),
|
||||
right=right_expr,
|
||||
)
|
||||
ast.copy_location(constraint, type_expr)
|
||||
return ConstraintType(
|
||||
location=loc,
|
||||
type=left_type,
|
||||
constraint=constraint,
|
||||
)
|
||||
|
||||
case _:
|
||||
return ConstraintType(
|
||||
location=loc,
|
||||
type=left,
|
||||
constraint=right_expr,
|
||||
)
|
||||
|
||||
case ast.Constant(value=None):
|
||||
return BaseType(
|
||||
location=loc,
|
||||
@@ -380,7 +355,7 @@ class PythonParser:
|
||||
for col in cols:
|
||||
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))
|
||||
|
||||
case _:
|
||||
@@ -391,7 +366,7 @@ class PythonParser:
|
||||
def _parse_frame_column(self, column: ast.expr) -> FrameColumn:
|
||||
loc: Location = Location.from_ast(column)
|
||||
match column:
|
||||
case ast.Name():
|
||||
case ast.Name() | ast.Subscript():
|
||||
return FrameColumn(
|
||||
location=loc,
|
||||
name=None,
|
||||
|
||||
174
scripts/docstring_checker.py
Normal file
174
scripts/docstring_checker.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import ast
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArgDoc:
|
||||
name: str
|
||||
type: str
|
||||
optional: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Param:
|
||||
name: str
|
||||
annotation: Optional[str]
|
||||
optional: bool
|
||||
|
||||
|
||||
class Checker(ast.NodeVisitor):
|
||||
def _get_args(self, docstring: str) -> list[ArgDoc]:
|
||||
args: list[ArgDoc] = []
|
||||
|
||||
in_args: bool = False
|
||||
for line in docstring.splitlines():
|
||||
if not in_args:
|
||||
if line == "Args:":
|
||||
in_args = True
|
||||
continue
|
||||
|
||||
# End of args
|
||||
if not line.startswith(" "):
|
||||
break
|
||||
|
||||
# Continuation line
|
||||
if line.startswith(" "):
|
||||
continue
|
||||
|
||||
line = line.strip()
|
||||
m = re.match(r"(?P<name>\w+) \((?P<type>.*?)(?P<opt>, optional)?\):", line)
|
||||
if m is None:
|
||||
continue
|
||||
|
||||
args.append(
|
||||
ArgDoc(
|
||||
name=m.group("name"),
|
||||
type=m.group("type"),
|
||||
optional=m.group("opt") is not None,
|
||||
)
|
||||
)
|
||||
|
||||
return args
|
||||
|
||||
def log(self, node: ast.FunctionDef, msg: str):
|
||||
loc: str = f"{node.name} L{node.lineno}:{node.col_offset+1}"
|
||||
print(f" ({loc}) {msg}")
|
||||
|
||||
def _is_ignored(self, node: ast.FunctionDef) -> bool:
|
||||
name: str = node.name
|
||||
if name.startswith("visit_") or name.startswith("_visit_"):
|
||||
return True
|
||||
if name.startswith("parse_") or name.startswith("_parse_"):
|
||||
return True
|
||||
if name.startswith("_print"):
|
||||
return True
|
||||
if name.startswith("_write"):
|
||||
return True
|
||||
if name.startswith("__") and name.endswith("__"):
|
||||
return True
|
||||
if name == "accept":
|
||||
return True
|
||||
node.decorator_list
|
||||
match node:
|
||||
case ast.FunctionDef(
|
||||
decorator_list=[
|
||||
ast.Call(
|
||||
func=ast.Name(id="method"),
|
||||
),
|
||||
],
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
|
||||
docstring: Optional[str] = ast.get_docstring(node)
|
||||
func_name: str = node.name
|
||||
if docstring is None:
|
||||
if not self._is_ignored(node):
|
||||
self.log(node, f"Missing docstring for function {func_name}")
|
||||
return
|
||||
|
||||
args_doc: list[ArgDoc] = self._get_args(docstring)
|
||||
by_name: dict[str, ArgDoc] = {}
|
||||
for doc in args_doc:
|
||||
if doc.name in by_name:
|
||||
self.log(node, f"Multiple documentation lines for argument {doc.name}")
|
||||
by_name[doc.name] = doc
|
||||
|
||||
all_params: list[Param] = []
|
||||
|
||||
pos_args: list[ast.arg] = node.args.posonlyargs
|
||||
mixed_args: list[ast.arg] = node.args.args
|
||||
kw_args: list[ast.arg] = node.args.kwonlyargs
|
||||
|
||||
def add_param(arg: ast.arg, optional: bool):
|
||||
all_params.append(
|
||||
Param(
|
||||
name=arg.arg,
|
||||
annotation=(
|
||||
ast.unparse(arg.annotation)
|
||||
if arg.annotation is not None
|
||||
else None
|
||||
),
|
||||
optional=optional,
|
||||
)
|
||||
)
|
||||
|
||||
n_pos: int = len(pos_args) + len(mixed_args)
|
||||
for i, arg in enumerate(pos_args):
|
||||
j: int = n_pos - i - 1
|
||||
optional: bool = j < len(node.args.defaults)
|
||||
add_param(arg, optional)
|
||||
|
||||
for i, arg in enumerate(mixed_args):
|
||||
j: int = len(mixed_args) - i - 1
|
||||
optional: bool = j < len(node.args.defaults)
|
||||
add_param(arg, optional)
|
||||
|
||||
for arg, default in zip(kw_args, node.args.kw_defaults):
|
||||
optional: bool = default is not None
|
||||
add_param(arg, optional)
|
||||
|
||||
for param in all_params:
|
||||
doc: Optional[ArgDoc] = by_name.get(param.name, None)
|
||||
if doc is None:
|
||||
if param.name not in {"self", "cls"}:
|
||||
self.log(
|
||||
node, f"Missing documentation for parameter '{param.name}'"
|
||||
)
|
||||
continue
|
||||
|
||||
if doc.name != param.name:
|
||||
self.log(node, f"Documentation mismatch for '{param.name}': wrong name")
|
||||
|
||||
if doc.type != param.annotation:
|
||||
self.log(node, f"Documentation mismatch for '{param.name}': wrong type")
|
||||
|
||||
if doc.optional != param.optional:
|
||||
self.log(
|
||||
node,
|
||||
f"Documentation mismatch for '{param.name}': wrong optionality",
|
||||
)
|
||||
|
||||
|
||||
def check_file(path: Path):
|
||||
source: str = path.read_text()
|
||||
tree = ast.parse(source)
|
||||
checker = Checker()
|
||||
checker.visit(tree)
|
||||
|
||||
|
||||
def main():
|
||||
folder: Path = (Path(__file__).parent.parent / "midas").resolve()
|
||||
all_files = folder.rglob("*.py")
|
||||
for f in all_files:
|
||||
print(f.relative_to(folder))
|
||||
check_file(f)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,20 +1,8 @@
|
||||
identifier ::= '[a-zA-Z][a-zA-Z_]*'
|
||||
Identifier ::= '[a-zA-Z][a-zA-Z_]*'
|
||||
|
||||
integer ::= '\d+'
|
||||
number ::= integer ["." integer]
|
||||
boolean ::= "False" | "True"
|
||||
none ::= "None"
|
||||
TypeArgs ::= "[" (Type ("," Type)*)? "]"
|
||||
|
||||
value ::= number | boolean | none
|
||||
lambda-value ::= "_" | value
|
||||
lambda-operator ::= ">" | "<" | ">=" | "<=" | "==" | "!="
|
||||
lambda ::= lambda-value lambda-operator lambda-value
|
||||
FrameColumn ::= ((Identifier | "_") ":")? Type
|
||||
FrameSchema ::= "[" (FrameColumn ("," FrameColumn)*)? "]"
|
||||
|
||||
constraint ::= identifier | "(" lambda ")"
|
||||
base-type ::= identifier
|
||||
type ::= base-type { "+" constraint }
|
||||
|
||||
column-type ::= type | "_"
|
||||
column-def ::= [ identifier ":" ] column-type
|
||||
|
||||
frame-def ::= column-def { "," column-def }
|
||||
Type ::= "Frame" FrameSchema | Identifier TypeArgs?
|
||||
|
||||
@@ -1,64 +1,72 @@
|
||||
#import "@preview/fervojo:0.1.1": render
|
||||
#import "@preview/fervojo:0.1.1": default-css, render
|
||||
|
||||
#let value = ```
|
||||
{[`value` <
|
||||
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
|
||||
[`boolean` <"False", "True">],
|
||||
[`none` "None"]
|
||||
#let extra-css = ```css
|
||||
svg.railroad .terminal rect {
|
||||
fill: #F7DCD4;
|
||||
}
|
||||
```
|
||||
#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 = (
|
||||
value,
|
||||
constraint,
|
||||
type-with-constraints,
|
||||
column-def,
|
||||
frame-def,
|
||||
annotation,
|
||||
type-args: type-args,
|
||||
frame-schema: frame-schema,
|
||||
type: type,
|
||||
)
|
||||
|
||||
#let inline = (
|
||||
"type-args",
|
||||
"frame-schema",
|
||||
)
|
||||
|
||||
#set text(font: "Source Sans 3")
|
||||
|
||||
= Type annotation syntax
|
||||
#title[Supported Python annotation syntax]
|
||||
|
||||
#for rule in rules {
|
||||
render(rule)
|
||||
}
|
||||
= Outline
|
||||
|
||||
/*
|
||||
#let by-name = (
|
||||
annotation: annotation,
|
||||
frame-def: frame-def,
|
||||
column-def: column-def,
|
||||
type-with-constraints: type-with-constraints,
|
||||
constraint: constraint,
|
||||
value: value,
|
||||
#box(
|
||||
columns(
|
||||
2,
|
||||
outline(title: none),
|
||||
),
|
||||
height: 9cm,
|
||||
stroke: 1pt,
|
||||
inset: 1em,
|
||||
)
|
||||
|
||||
|
||||
= Statements and expressions
|
||||
|
||||
#for (name, rule) in rules.pairs().rev() {
|
||||
[== #name]
|
||||
render(rule, css: css)
|
||||
}
|
||||
|
||||
#let substitute(base-rule) = {
|
||||
let new-rule = base-rule
|
||||
for (key, rule) in by-name.pairs() {
|
||||
new-rule = new-rule.replace("'" + key + "'", rule.text.slice(1, -1))
|
||||
for name in inline {
|
||||
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 {
|
||||
new-rule = substitute(new-rule)
|
||||
@@ -66,9 +74,16 @@
|
||||
return new-rule
|
||||
}
|
||||
|
||||
#let combined = raw(substitute(annotation.text))
|
||||
|
||||
|
||||
#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+'
|
||||
Number ::= "-"? Integer ("." Integer)?
|
||||
Boolean ::= "False" | "True"
|
||||
String ::= '(".*?")|(\'.*?\')'
|
||||
None ::= "None"
|
||||
|
||||
Value ::= Number | Boolean | None
|
||||
Literal ::= Number | Boolean | String | None
|
||||
|
||||
UnaryOp ::= "+" | "-" | "!"
|
||||
FactorOp ::= "*" | "/"
|
||||
TermOp ::= "+" | "-"
|
||||
ComparisonOp ::= ">" | "<" | ">=" | "<="
|
||||
EqualityOp ::= "==" | "!="
|
||||
|
||||
Grouping ::= "(" Constraint ")"
|
||||
Primary ::= "_" | Value | Identifier | Grouping
|
||||
PosArg ::= Expression
|
||||
KwArg ::= Identifier "=" Expression
|
||||
|
||||
PosArgs ::= PosArg ("," PosArg)*
|
||||
KwArgs ::= KwArg ("," KwArg)*
|
||||
Args ::= (
|
||||
PosArgs
|
||||
| KwArgs
|
||||
| PosArgs "," KwArgs
|
||||
)
|
||||
|
||||
Grouping ::= "(" Expression ")"
|
||||
Primary ::= "_" | Literal | Identifier | Grouping
|
||||
Reference ::= Primary ("." Identifier)*
|
||||
Unary ::= "-"? Unary | Reference
|
||||
Comparison ::= Unary (ComparisonOp Unary)*
|
||||
CallArgs ::= "(" Args ")"
|
||||
Call ::= Reference CallArgs*
|
||||
Unary ::= UnaryOp Unary | Call
|
||||
Factor ::= Unary (FactorOp Unary)*
|
||||
Term ::= Factor (TermOp Factor)*
|
||||
Comparison ::= Term (ComparisonOp Term)*
|
||||
Equality ::= Comparison (EqualityOp Comparison)*
|
||||
Constraint ::= Equality ("&" Equality)*
|
||||
Expression ::= Equality ("&" Equality)*
|
||||
Constraint ::= Expression
|
||||
|
||||
TemplateParam ::= Identifier ("<:" Type)?
|
||||
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
|
||||
TypeParams ::= "[" (Type ("," Type)*)? "]"
|
||||
GenericType ::= NamedType TypeParams?
|
||||
TypeArgs ::= "[" (Type ("," Type)*)? "]"
|
||||
FrameColumn ::= TOKEN ":" Type
|
||||
FrameSchema ::= "[" (FrameColumn ("," FrameColumn)*)? "]"
|
||||
GenericType ::= "Frame" FrameSchema | NamedType TypeArgs?
|
||||
GroupedType ::= "(" Type ")"
|
||||
BaseType ::= GroupedType | ComplexType | GenericType
|
||||
BaseType ::= GroupedType | GenericType
|
||||
ConstraintType ::= BaseType ("where" Constraint)?
|
||||
FuncType ::= "fn" ParamSpec "->" Type
|
||||
Type ::= ConstraintType
|
||||
|
||||
OpDefinition ::= "op" Identifier "(" Type ")" "->" Type
|
||||
ExtendBody ::= "{" OpDefinition* "}"
|
||||
MemberStatement ::= ("prop" | "def") Identifier ":" Type
|
||||
ExtendBody ::= "{" MemberStatement* "}"
|
||||
|
||||
AliasStatement ::= "alias" Identifier "=" Type
|
||||
TypeStatement ::= "type" Identifier Template? "=" Type
|
||||
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 value = ```
|
||||
{[`value` <
|
||||
#let literal = ```
|
||||
{[`literal` <
|
||||
[`number` 'digit' * ! <!, ["." 'digit' * !]>],
|
||||
[`boolean` <"False", "True">],
|
||||
[`string` <["\"" 'char'*! "\""], ["'" 'char'*! "'"]>],
|
||||
[`none` "None"]
|
||||
>]}
|
||||
```
|
||||
|
||||
#let grouping = ```
|
||||
{[`grouping` "(" 'constraint' ")"]}
|
||||
{[`grouping` "(" 'expression' ")"]}
|
||||
```
|
||||
|
||||
#let primary = ```
|
||||
{[`primary` <"_", 'value', 'identifier', 'grouping'>]}
|
||||
{[`primary` <"_", 'literal', 'identifier', 'grouping'>]}
|
||||
```
|
||||
|
||||
#let reference = ```
|
||||
{[`reference` 'primary' <!, ["." 'identifier']*!>]}
|
||||
```
|
||||
|
||||
#let call-args = ```
|
||||
{[`call-args` "(" <!, <'expression', ['identifier' "=" 'expression']>*","#`Same rules as Python`> ")"]}
|
||||
```
|
||||
|
||||
#let call = ```
|
||||
{[`call` 'reference' <!, 'call-args'*!>]}
|
||||
```
|
||||
|
||||
#let unary = ```
|
||||
{[`unary` <[<!, "-"> 'unary'], 'reference'>]}
|
||||
{[`unary` <[<"+", "-", "!"> 'unary'], 'call'>]}
|
||||
```
|
||||
|
||||
#let factor = ```
|
||||
{[`factor` 'unary'*<"*", "/">]}
|
||||
```
|
||||
|
||||
#let term = ```
|
||||
{[`term` 'factor'*<"+", "-">]}
|
||||
```
|
||||
|
||||
#let comparison = ```
|
||||
{[`comparison` 'unary'*<">", "<", ">=", "<=">]}
|
||||
{[`comparison` 'term'*<">", "<", ">=", "<=">]}
|
||||
```
|
||||
|
||||
#let equality = ```
|
||||
{[`equality` 'comparison'*<"==", "!=">]}
|
||||
```
|
||||
|
||||
#let expression = ```
|
||||
{[`expression` 'equality'*"&"]}
|
||||
```
|
||||
|
||||
#let constraint = ```
|
||||
{[`constraint` 'equality'*"&"]}
|
||||
{[`constraint` 'expression']}
|
||||
```
|
||||
|
||||
#let template-param = ```
|
||||
@@ -51,24 +72,20 @@ svg.railroad .terminal rect {
|
||||
{[`template` "[" <!, 'template-param'*","> "]"]}
|
||||
```
|
||||
|
||||
#let type-property = ```
|
||||
{[`type-property` 'identifier' ":" 'type']}
|
||||
```
|
||||
|
||||
#let complex-type = ```
|
||||
{[`complex-type` "{" <!, 'type-property'*!> "}"]}
|
||||
```
|
||||
|
||||
#let named-type = ```
|
||||
{[`named-type` 'identifier']}
|
||||
```
|
||||
|
||||
#let type-params = ```
|
||||
{[`type-params` "[" <!, 'type'*","> "]"]}
|
||||
#let type-args = ```
|
||||
{[`type-args` "[" <!, 'type'*","> "]"]}
|
||||
```
|
||||
|
||||
#let frame-schema = ```
|
||||
{[`frame-schema` "[" <!, ['TOKEN' ":" 'type']*","> "]"]}
|
||||
```
|
||||
|
||||
#let generic-type = ```
|
||||
{[`generic-type` 'named-type' <!, 'type-params'>]}
|
||||
{[`generic-type` <["Frame" 'frame-schema'], ['named-type' <!, 'type-args'>]>]}
|
||||
```
|
||||
|
||||
#let grouped-type = ```
|
||||
@@ -76,59 +93,88 @@ svg.railroad .terminal rect {
|
||||
```
|
||||
|
||||
#let base-type = ```
|
||||
{[`base-type` <'grouped-type', 'complex-type', 'generic-type'>]}
|
||||
{[`base-type` <'grouped-type', 'generic-type'>]}
|
||||
```
|
||||
|
||||
#let constraint-type = ```
|
||||
{[`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 = ```
|
||||
{[`type` 'constraint-type']}
|
||||
{[`type` <'func-type', 'constraint-type'>]}
|
||||
```
|
||||
|
||||
#let alias-statement = ```
|
||||
{[`alias-statement` "alias" 'identifier' "=" 'type']}
|
||||
```
|
||||
|
||||
#let type-statement = ```
|
||||
{[`type-statement` "type" 'identifier' <!, 'template'> "=" 'type']}
|
||||
```
|
||||
|
||||
#let op-definition = ```
|
||||
{[`op-definition` "op" 'identifier' "(" 'type' ")" "->" 'type']}
|
||||
#let member-stmt = ```
|
||||
{[`member-stmt` <"prop", "def"> 'identifier' ":" 'type']}
|
||||
```
|
||||
|
||||
#let extend-statement = ```
|
||||
{[`extend-statement` "extend" 'type' "{" <!, 'op-definition'*!> "}"]}
|
||||
{[`extend-statement` "extend" 'type' "{" <!, 'member-stmt'*!> "}"]}
|
||||
```
|
||||
|
||||
#let predicate-statement = ```
|
||||
{[`predicate-statement` "predicate" 'identifier' "(" 'identifier' ":" 'type' ")" "=" 'constraint']}
|
||||
{[`predicate-statement` "predicate" 'identifier' <!, 'param-spec'*!> "=" 'constraint']}
|
||||
```
|
||||
|
||||
#let statement = ```
|
||||
{[`statement` <'type-statement', 'extend-statement', 'predicate-statement'>]}
|
||||
{[`statement` <'alias-statement', 'type-statement', 'extend-statement', 'predicate-statement'>]}
|
||||
```
|
||||
|
||||
#let rules = (
|
||||
value: value,
|
||||
literal: literal,
|
||||
grouping: grouping,
|
||||
primary: primary,
|
||||
reference: reference,
|
||||
call-args: call-args,
|
||||
call: call,
|
||||
unary: unary,
|
||||
factor: factor,
|
||||
term: term,
|
||||
comparison: comparison,
|
||||
equality: equality,
|
||||
expression: expression,
|
||||
constraint: constraint,
|
||||
template-param: template-param,
|
||||
template: template,
|
||||
type-property: type-property,
|
||||
complex-type: complex-type,
|
||||
named-type: named-type,
|
||||
type-params: type-params,
|
||||
type-args: type-args,
|
||||
generic-type: generic-type,
|
||||
grouped-type: grouped-type,
|
||||
base-type: base-type,
|
||||
constraint-type: constraint-type,
|
||||
pos-param: pos-param,
|
||||
kw-param: kw-param,
|
||||
param-spec: param-spec,
|
||||
func-type: func-type,
|
||||
type: type,
|
||||
alias-statement: alias-statement,
|
||||
type-statement: type-statement,
|
||||
op-definition: op-definition,
|
||||
member-stmt: member-stmt,
|
||||
extend-statement: extend-statement,
|
||||
predicate-statement: predicate-statement,
|
||||
statement: statement,
|
||||
@@ -136,18 +182,22 @@ svg.railroad .terminal rect {
|
||||
|
||||
#let inline = (
|
||||
"grouping",
|
||||
"value",
|
||||
"literal",
|
||||
"template-param",
|
||||
"template",
|
||||
"type-property",
|
||||
"complex-type",
|
||||
"type-params",
|
||||
"call-args",
|
||||
"type-args",
|
||||
"named-type",
|
||||
"grouped-type",
|
||||
"generic-type",
|
||||
"base-type",
|
||||
"constraint-type",
|
||||
"op-definition",
|
||||
"pos-param",
|
||||
"kw-param",
|
||||
"func-type",
|
||||
"member-stmt",
|
||||
"alias-statement",
|
||||
"type-statement",
|
||||
"extend-statement",
|
||||
"predicate-statement",
|
||||
@@ -164,7 +214,7 @@ svg.railroad .terminal rect {
|
||||
2,
|
||||
outline(title: none),
|
||||
),
|
||||
height: 9cm,
|
||||
height: 15cm,
|
||||
stroke: 1pt,
|
||||
inset: 1em,
|
||||
)
|
||||
|
||||
33
test.py
33
test.py
@@ -1,33 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from midas.ast.printer import MidasAstPrinter
|
||||
from midas.lexer.midas import MidasLexer
|
||||
from midas.lexer.token import Token
|
||||
from midas.parser.midas import MidasParser
|
||||
|
||||
|
||||
def test_midas():
|
||||
# Midas type definitions
|
||||
path: Path = Path("examples") / "00_syntax_prototype" / "03_custom_types_v2.midas"
|
||||
definitions: str = path.read_text()
|
||||
midas_lexer: MidasLexer = MidasLexer(definitions, path.name)
|
||||
tokens: list[Token] = midas_lexer.process()
|
||||
# print([f"{t.type.name}('{t.lexeme}')" for t in tokens])
|
||||
with open("tokens.json", "w") as f:
|
||||
json.dump([f"{t.type.name}('{t.lexeme}')" for t in tokens], f, indent=4)
|
||||
|
||||
parser = MidasParser(tokens)
|
||||
parsed = parser.parse()
|
||||
print(parsed)
|
||||
for err in parser.errors:
|
||||
print(err.get_report())
|
||||
printer = MidasAstPrinter()
|
||||
for stmt in parsed:
|
||||
if stmt is None:
|
||||
print("None")
|
||||
continue
|
||||
print(printer.print(stmt))
|
||||
|
||||
|
||||
test_midas()
|
||||
@@ -1,7 +1,8 @@
|
||||
import sys
|
||||
from typing import Type
|
||||
|
||||
from midas.cli.ansi import Ansi
|
||||
from tests.base import Tester
|
||||
from tests.base import Tester, TestsSummary
|
||||
from tests.checker import CheckerTester
|
||||
from tests.generator import GeneratorTester
|
||||
from tests.midas import MidasTester
|
||||
@@ -15,12 +16,12 @@ def print_banner(name: str):
|
||||
print(horizontal)
|
||||
|
||||
|
||||
def run_tests(tester_cls: Type[Tester]) -> bool:
|
||||
def run_tests(tester_cls: Type[Tester]) -> TestsSummary:
|
||||
print_banner(tester_cls.__name__)
|
||||
tester: Tester = tester_cls()
|
||||
success: bool = tester.run_all_tests()
|
||||
summary: TestsSummary = tester.run_all_tests()
|
||||
print()
|
||||
return success
|
||||
return summary
|
||||
|
||||
|
||||
def main():
|
||||
@@ -31,12 +32,17 @@ def main():
|
||||
GeneratorTester,
|
||||
]
|
||||
|
||||
success: bool = all(list(map(run_tests, testers))) # list to avoid early stop
|
||||
summaries: list[TestsSummary] = list(
|
||||
map(run_tests, testers)
|
||||
) # list to avoid early stop
|
||||
summary: TestsSummary = TestsSummary.concat(*summaries)
|
||||
|
||||
if success:
|
||||
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__":
|
||||
|
||||
@@ -4,6 +4,7 @@ import argparse
|
||||
import difflib
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Protocol
|
||||
|
||||
@@ -14,6 +15,43 @@ class CaseResult(Protocol):
|
||||
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):
|
||||
"""A test runner to check for regressions in the lexer and parser"""
|
||||
|
||||
@@ -34,40 +72,37 @@ class Tester(ABC):
|
||||
@abstractmethod
|
||||
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())
|
||||
return self.run_tests(paths)
|
||||
|
||||
def run_tests(self, tests: list[Path]) -> bool:
|
||||
def run_tests(self, tests: list[Path]) -> TestsSummary:
|
||||
rule: str = "-" * 80
|
||||
n: int = len(tests)
|
||||
successes: int = 0
|
||||
failures: int = 0
|
||||
|
||||
summary: TestsSummary = TestsSummary()
|
||||
|
||||
print(rule)
|
||||
for i, test in enumerate(tests):
|
||||
path: Path = test.resolve().relative_to(self.CASES_DIR)
|
||||
print(f"{Ansi.FG(Ansi.BRIGHT_CYAN)}Case {i+1}/{n}: {path}{Ansi.RESET}")
|
||||
print(Ansi.DIM, end="")
|
||||
success: bool = self._run_test(test)
|
||||
print(Ansi.RESET, end="")
|
||||
if success:
|
||||
successes += 1
|
||||
else:
|
||||
failures += 1
|
||||
summary.add(str(path), success)
|
||||
|
||||
print(rule)
|
||||
print(f"Success: {successes}/{n}")
|
||||
print(f"Failed: {failures}/{n}")
|
||||
print(f"Success: {summary.successes}/{n}")
|
||||
print(f"Failed: {summary.failures}/{n}")
|
||||
print(rule)
|
||||
return failures == 0
|
||||
return summary
|
||||
|
||||
def _run_test(self, path: Path) -> bool:
|
||||
result_path: Path = self._result_path(path)
|
||||
if not result_path.exists():
|
||||
print("Missing snapshot. Please run the update command first")
|
||||
return False
|
||||
print(Ansi.DIM, end="")
|
||||
result: CaseResult = self._exec_case(path)
|
||||
print(Ansi.RESET, end="")
|
||||
expected: str = result_path.read_text()
|
||||
actual: str = result.dumps()
|
||||
|
||||
@@ -143,16 +178,18 @@ class Tester(ABC):
|
||||
else:
|
||||
tester.update_tests(args.FILE)
|
||||
case "run":
|
||||
success: bool
|
||||
summary: TestsSummary
|
||||
if args.all:
|
||||
success = tester.run_all_tests()
|
||||
summary = tester.run_all_tests()
|
||||
else:
|
||||
success = tester.run_tests(args.FILE)
|
||||
if not success:
|
||||
summary = tester.run_tests(args.FILE)
|
||||
if not summary.success:
|
||||
summary.print()
|
||||
sys.exit(1)
|
||||
case None:
|
||||
success: bool = tester.run_all_tests()
|
||||
if not success:
|
||||
summary: TestsSummary = tester.run_all_tests()
|
||||
if not summary.success:
|
||||
summary.print()
|
||||
sys.exit(1)
|
||||
case _:
|
||||
print(f"Unknown subcommand '{args.subcommand}'")
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
df: Frame[
|
||||
verified: bool,
|
||||
birth_year: int,
|
||||
height: float + ( _ > 0 ) + ( _ < 250 ),
|
||||
height: float,
|
||||
name: str,
|
||||
date: datetime,
|
||||
float,
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
8,
|
||||
12
|
||||
],
|
||||
"end": [
|
||||
8,
|
||||
43
|
||||
]
|
||||
},
|
||||
"message": "ConstraintType not yet supported"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
|
||||
@@ -11,16 +11,16 @@ from _ import (
|
||||
Unused,
|
||||
)
|
||||
|
||||
unused: Unused
|
||||
covariant: Covariant
|
||||
contravariant: Contravariant
|
||||
invariant: Invariant
|
||||
coco: Coco
|
||||
cocontra: Cocontra
|
||||
contraco: Contraco
|
||||
contracontra: Contracontra
|
||||
t1: T1
|
||||
t2: T2
|
||||
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(
|
||||
@@ -36,17 +36,17 @@ print(
|
||||
t2,
|
||||
)
|
||||
|
||||
cov1: Covariant[float]
|
||||
cov2: Covariant[int]
|
||||
cov1: Covariant[float] = object()
|
||||
cov2: Covariant[int] = object()
|
||||
cov1 = cov2 # Ok because int <: float => Covariant[int] <: Covariant[float]
|
||||
cov2 = cov1 # Invalid
|
||||
|
||||
contra1: Contravariant[float]
|
||||
contra2: Contravariant[int]
|
||||
contra1: Contravariant[float] = object()
|
||||
contra2: Contravariant[int] = object()
|
||||
contra1 = contra2 # Invalid
|
||||
contra2 = contra1 # Ok because int <: float => Covariant[float] <: Covariant[int]
|
||||
|
||||
inv1: Invariant[float]
|
||||
inv2: Invariant[int]
|
||||
inv1: Invariant[float] = object()
|
||||
inv2: Invariant[int] = object()
|
||||
inv1 = inv2 # Invalid
|
||||
inv2 = inv1 # Invalid
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
# type: ignore
|
||||
# ruff: disable [F821]
|
||||
import pandas as pd
|
||||
|
||||
df1: Frame[a:int, b:float]
|
||||
df2: Frame[a:int, b:float]
|
||||
df1 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||
df2 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||
|
||||
_: Any
|
||||
|
||||
@@ -38,7 +39,7 @@ _ = df1.sum()
|
||||
_ = df1.var()
|
||||
|
||||
# Groupby
|
||||
df_gb = df1.groupby(by="a")
|
||||
df_gb = df1.groupby(by="i")
|
||||
|
||||
_ = df_gb.kurt()
|
||||
_ = df_gb.max()
|
||||
|
||||
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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
44
tests/cases/checker/11_function_subtyping.py
Normal file
44
tests/cases/checker/11_function_subtyping.py
Normal file
@@ -0,0 +1,44 @@
|
||||
def a1(param: int) -> float: ...
|
||||
def a2(param: float) -> int: ...
|
||||
|
||||
|
||||
a = a1
|
||||
a = a2
|
||||
|
||||
|
||||
def b1(a: int, /) -> float: ...
|
||||
def b2(b: float, /) -> int: ...
|
||||
|
||||
|
||||
b = b1
|
||||
b = b2
|
||||
|
||||
|
||||
def c1(a: int) -> None: ...
|
||||
def c2(p: float = 0, /, *, a: float = 0) -> None: ...
|
||||
|
||||
|
||||
c = c1
|
||||
c = c2
|
||||
|
||||
# Invalid subtypes
|
||||
|
||||
|
||||
def d1(a: int) -> float: ...
|
||||
def d2(a: str) -> float: ...
|
||||
def d3(a: int) -> str: ...
|
||||
|
||||
|
||||
d = d1
|
||||
d = d2
|
||||
d = d3
|
||||
|
||||
|
||||
def e1(*, a: int = 0) -> None: ...
|
||||
def e2(*, a: int) -> None: ...
|
||||
def e3(*, a: int = 0, b: int) -> None: ...
|
||||
|
||||
|
||||
e = e1
|
||||
e = e2
|
||||
e = e3
|
||||
881
tests/cases/checker/11_function_subtyping.py.ref.json
Normal file
881
tests/cases/checker/11_function_subtyping.py.ref.json
Normal file
@@ -0,0 +1,881 @@
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
1,
|
||||
29
|
||||
],
|
||||
"end": [
|
||||
1,
|
||||
32
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=1, col_offset=29, end_lineno=1, end_col_offset=32), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
1,
|
||||
22
|
||||
],
|
||||
"end": [
|
||||
1,
|
||||
27
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated float but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
2,
|
||||
29
|
||||
],
|
||||
"end": [
|
||||
2,
|
||||
32
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=2, col_offset=29, end_lineno=2, end_col_offset=32), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
2,
|
||||
24
|
||||
],
|
||||
"end": [
|
||||
2,
|
||||
27
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated int but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
9,
|
||||
28
|
||||
],
|
||||
"end": [
|
||||
9,
|
||||
31
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=9, col_offset=28, end_lineno=9, end_col_offset=31), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
9,
|
||||
21
|
||||
],
|
||||
"end": [
|
||||
9,
|
||||
26
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated float but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
10,
|
||||
28
|
||||
],
|
||||
"end": [
|
||||
10,
|
||||
31
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=10, col_offset=28, end_lineno=10, end_col_offset=31), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
10,
|
||||
23
|
||||
],
|
||||
"end": [
|
||||
10,
|
||||
26
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated int but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
17,
|
||||
24
|
||||
],
|
||||
"end": [
|
||||
17,
|
||||
27
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=17, col_offset=24, end_lineno=17, end_col_offset=27), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
18,
|
||||
50
|
||||
],
|
||||
"end": [
|
||||
18,
|
||||
53
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=18, col_offset=50, end_lineno=18, end_col_offset=53), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
27,
|
||||
25
|
||||
],
|
||||
"end": [
|
||||
27,
|
||||
28
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=27, col_offset=25, end_lineno=27, end_col_offset=28), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
27,
|
||||
18
|
||||
],
|
||||
"end": [
|
||||
27,
|
||||
23
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated float but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
28,
|
||||
25
|
||||
],
|
||||
"end": [
|
||||
28,
|
||||
28
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=28, col_offset=25, end_lineno=28, end_col_offset=28), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
28,
|
||||
18
|
||||
],
|
||||
"end": [
|
||||
28,
|
||||
23
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated float but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
29,
|
||||
23
|
||||
],
|
||||
"end": [
|
||||
29,
|
||||
26
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=29, col_offset=23, end_lineno=29, end_col_offset=26), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
29,
|
||||
18
|
||||
],
|
||||
"end": [
|
||||
29,
|
||||
21
|
||||
]
|
||||
},
|
||||
"message": "Return type mismatch, annotated str but returns None"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
33,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
33,
|
||||
6
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign (a: str) -> float to variable 'd' of type (a: int) -> float"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
34,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
34,
|
||||
6
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign (a: int) -> str to variable 'd' of type (a: int) -> float"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
37,
|
||||
31
|
||||
],
|
||||
"end": [
|
||||
37,
|
||||
34
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=37, col_offset=31, end_lineno=37, end_col_offset=34), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
38,
|
||||
27
|
||||
],
|
||||
"end": [
|
||||
38,
|
||||
30
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=38, col_offset=27, end_lineno=38, end_col_offset=30), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Warning",
|
||||
"location": {
|
||||
"start": [
|
||||
39,
|
||||
39
|
||||
],
|
||||
"end": [
|
||||
39,
|
||||
42
|
||||
]
|
||||
},
|
||||
"message": "Unknown literal LiteralExpr(location=Location(lineno=39, col_offset=39, end_lineno=39, end_col_offset=42), value=Ellipsis)"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
43,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
43,
|
||||
6
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign (*, a: int) -> None to variable 'e' of type (*, a: int?) -> None"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
44,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
44,
|
||||
6
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign (*, a: int?, b: int) -> None to variable 'e' of type (*, a: int?) -> None"
|
||||
}
|
||||
],
|
||||
"judgments": [
|
||||
{
|
||||
"location": {
|
||||
"from": "L1:29",
|
||||
"to": "L1:32"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L2:29",
|
||||
"to": "L2:32"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L5:4",
|
||||
"to": "L5:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a1"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "param",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:4",
|
||||
"to": "L6:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a2"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "param",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:28",
|
||||
"to": "L9:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:28",
|
||||
"to": "L10:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L13:4",
|
||||
"to": "L13:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b1"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"mixed": [],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L14:4",
|
||||
"to": "L14:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b2"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "b",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"mixed": [],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L17:24",
|
||||
"to": "L17:27"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L18:18",
|
||||
"to": "L18:19"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L18:38",
|
||||
"to": "L18:39"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L18:50",
|
||||
"to": "L18:53"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L21:4",
|
||||
"to": "L21:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "c1"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L22:4",
|
||||
"to": "L22:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "c2"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "p",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": false,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"mixed": [],
|
||||
"kw": [
|
||||
{
|
||||
"pos": 1,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": false,
|
||||
"unsupported": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L27:25",
|
||||
"to": "L27:28"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L28:25",
|
||||
"to": "L28:28"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L29:23",
|
||||
"to": "L29:26"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L32:4",
|
||||
"to": "L32:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d1"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L33:4",
|
||||
"to": "L33:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d2"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "str"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L34:4",
|
||||
"to": "L34:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d3"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
],
|
||||
"kw": []
|
||||
},
|
||||
"returns": {
|
||||
"name": "str"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L37:19",
|
||||
"to": "L37:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L37:31",
|
||||
"to": "L37:34"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L38:27",
|
||||
"to": "L38:30"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L39:19",
|
||||
"to": "L39:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L39:39",
|
||||
"to": "L39:42"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": "..."
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L42:4",
|
||||
"to": "L42:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e1"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [],
|
||||
"kw": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": false,
|
||||
"unsupported": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L43:4",
|
||||
"to": "L43:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e2"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [],
|
||||
"kw": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L44:4",
|
||||
"to": "L44:6"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e3"
|
||||
},
|
||||
"type": {
|
||||
"params": {
|
||||
"pos": [],
|
||||
"mixed": [],
|
||||
"kw": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": false,
|
||||
"unsupported": false
|
||||
},
|
||||
{
|
||||
"pos": 1,
|
||||
"name": "b",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true,
|
||||
"unsupported": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
49
tests/cases/generator/03_simple_syntax.py
Normal file
49
tests/cases/generator/03_simple_syntax.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# type: ignore
|
||||
# ruff: disable[F821]
|
||||
|
||||
import module1
|
||||
import module2 as alias2
|
||||
from module3 import submodule3
|
||||
from module4 import submodule4 as alias4
|
||||
|
||||
a: int
|
||||
b: Generic[int]
|
||||
c: Generic2[int, float]
|
||||
d: Frame[a:int, b:float]
|
||||
|
||||
e = 3
|
||||
f: int = 4
|
||||
g = []
|
||||
h = [1, 0.1, " ", None, False, True]
|
||||
i = {}
|
||||
j = {"a": 1, "b": 2}
|
||||
k = {"c": 3, **j}
|
||||
l = cast(int, a)
|
||||
m = unsafe_cast(int, a)
|
||||
|
||||
|
||||
def n(a: int, /, b: float, *, c: str) -> Any:
|
||||
return
|
||||
|
||||
|
||||
def o(a: int = 1, /, b: float = 2.0, *, c: str = "") -> Any:
|
||||
return 1
|
||||
|
||||
|
||||
for i in h:
|
||||
pass
|
||||
|
||||
if e == f:
|
||||
pass
|
||||
elif f == g:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
p = +a + -b - ~c * d / e**f
|
||||
q = not (a and b) or c
|
||||
r = a & b | c ^ d
|
||||
|
||||
s = a.b.c
|
||||
t = a[b][c, d][e:f]
|
||||
u = a(b)(c=d)
|
||||
249
tests/cases/generator/03_simple_syntax.py.ref.txt
Normal file
249
tests/cases/generator/03_simple_syntax.py.ref.txt
Normal file
@@ -0,0 +1,249 @@
|
||||
Module(
|
||||
body=[
|
||||
Import(
|
||||
names=[
|
||||
alias(name='module1')]),
|
||||
Import(
|
||||
names=[
|
||||
alias(name='module2', asname='alias2')]),
|
||||
ImportFrom(
|
||||
module='module3',
|
||||
names=[
|
||||
alias(name='submodule3')],
|
||||
level=0),
|
||||
ImportFrom(
|
||||
module='module4',
|
||||
names=[
|
||||
alias(name='submodule4', asname='alias4')],
|
||||
level=0),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='e')],
|
||||
value=Constant(value=3)),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='f')],
|
||||
value=Constant(value=4)),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='g')],
|
||||
value=List(elts=[])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='h')],
|
||||
value=List(
|
||||
elts=[
|
||||
Constant(value=1),
|
||||
Constant(value=0.1),
|
||||
Constant(value=' '),
|
||||
Constant(value=None),
|
||||
Constant(value=False),
|
||||
Constant(value=True)])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='i')],
|
||||
value=Dict(keys=[], values=[])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='j')],
|
||||
value=Dict(
|
||||
keys=[
|
||||
Constant(value='a'),
|
||||
Constant(value='b')],
|
||||
values=[
|
||||
Constant(value=1),
|
||||
Constant(value=2)])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='k')],
|
||||
value=Dict(
|
||||
keys=[
|
||||
Constant(value='c'),
|
||||
None],
|
||||
values=[
|
||||
Constant(value=3),
|
||||
Name(id='j')])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')],
|
||||
value=Name(id='a')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='__midas_a0__'),
|
||||
Name(id='int')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='03_simple_syntax.py:L21:5: 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 int')])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='l')],
|
||||
value=Name(id='__midas_a0__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='m')],
|
||||
value=Name(id='a')),
|
||||
FunctionDef(
|
||||
name='n',
|
||||
args=arguments(
|
||||
posonlyargs=[
|
||||
arg(arg='a')],
|
||||
args=[
|
||||
arg(arg='b')],
|
||||
kwonlyargs=[
|
||||
arg(arg='c')],
|
||||
kw_defaults=[
|
||||
None],
|
||||
defaults=[]),
|
||||
body=[
|
||||
Return()],
|
||||
decorator_list=[]),
|
||||
FunctionDef(
|
||||
name='o',
|
||||
args=arguments(
|
||||
posonlyargs=[
|
||||
arg(arg='a')],
|
||||
args=[
|
||||
arg(arg='b')],
|
||||
kwonlyargs=[
|
||||
arg(arg='c')],
|
||||
kw_defaults=[
|
||||
Constant(value='')],
|
||||
defaults=[
|
||||
Constant(value=1),
|
||||
Constant(value=2.0)]),
|
||||
body=[
|
||||
Return(
|
||||
value=Constant(value=1))],
|
||||
decorator_list=[]),
|
||||
For(
|
||||
target=Name(id='i'),
|
||||
iter=Name(id='h'),
|
||||
body=[
|
||||
Pass()],
|
||||
orelse=[]),
|
||||
If(
|
||||
test=Compare(
|
||||
left=Name(id='e'),
|
||||
ops=[
|
||||
Eq()],
|
||||
comparators=[
|
||||
Name(id='f')]),
|
||||
body=[
|
||||
Pass()],
|
||||
orelse=[
|
||||
If(
|
||||
test=Compare(
|
||||
left=Name(id='f'),
|
||||
ops=[
|
||||
Eq()],
|
||||
comparators=[
|
||||
Name(id='g')]),
|
||||
body=[
|
||||
Pass()],
|
||||
orelse=[])]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='p')],
|
||||
value=BinOp(
|
||||
left=BinOp(
|
||||
left=UnaryOp(
|
||||
op=UAdd(),
|
||||
operand=Name(id='a')),
|
||||
op=Add(),
|
||||
right=UnaryOp(
|
||||
op=USub(),
|
||||
operand=Name(id='b'))),
|
||||
op=Sub(),
|
||||
right=BinOp(
|
||||
left=BinOp(
|
||||
left=UnaryOp(
|
||||
op=Invert(),
|
||||
operand=Name(id='c')),
|
||||
op=Mult(),
|
||||
right=Name(id='d')),
|
||||
op=Div(),
|
||||
right=BinOp(
|
||||
left=Name(id='e'),
|
||||
op=Pow(),
|
||||
right=Name(id='f'))))),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='q')],
|
||||
value=BoolOp(
|
||||
op=Or(),
|
||||
values=[
|
||||
UnaryOp(
|
||||
op=Not(),
|
||||
operand=BoolOp(
|
||||
op=And(),
|
||||
values=[
|
||||
Name(id='a'),
|
||||
Name(id='b')])),
|
||||
Name(id='c')])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='r')],
|
||||
value=BinOp(
|
||||
left=BinOp(
|
||||
left=Name(id='a'),
|
||||
op=BitAnd(),
|
||||
right=Name(id='b')),
|
||||
op=BitOr(),
|
||||
right=BinOp(
|
||||
left=Name(id='c'),
|
||||
op=BitXor(),
|
||||
right=Name(id='d')))),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='s')],
|
||||
value=Attribute(
|
||||
value=Attribute(
|
||||
value=Name(id='a'),
|
||||
attr='b'),
|
||||
attr='c')),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='t')],
|
||||
value=Subscript(
|
||||
value=Subscript(
|
||||
value=Subscript(
|
||||
value=Name(id='a'),
|
||||
slice=Name(id='b')),
|
||||
slice=Tuple(
|
||||
elts=[
|
||||
Name(id='c'),
|
||||
Name(id='d')])),
|
||||
slice=Slice(
|
||||
lower=Name(id='e'),
|
||||
upper=Name(id='f')))),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='u')],
|
||||
value=Call(
|
||||
func=Call(
|
||||
func=Name(id='a'),
|
||||
args=[
|
||||
Name(id='b')],
|
||||
keywords=[]),
|
||||
args=[],
|
||||
keywords=[
|
||||
keyword(
|
||||
arg='c',
|
||||
value=Name(id='d'))]))],
|
||||
type_ignores=[])
|
||||
15
tests/cases/generator/04_frames.midas
Normal file
15
tests/cases/generator/04_frames.midas
Normal file
@@ -0,0 +1,15 @@
|
||||
predicate is_positive(v: float) = v >= 0
|
||||
type Positive = float where is_positive(_)
|
||||
|
||||
alias T1 = Frame[
|
||||
a: int
|
||||
]
|
||||
|
||||
alias T2 = Frame[
|
||||
a: int,
|
||||
b: str,
|
||||
c: Positive,
|
||||
d: float where is_positive(_)
|
||||
]
|
||||
|
||||
alias Positives = Column[Positive]
|
||||
13
tests/cases/generator/04_frames.py
Normal file
13
tests/cases/generator/04_frames.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Any
|
||||
|
||||
from midas import T1, T2, Column, Positive, Positives, cast
|
||||
|
||||
o: Any = object()
|
||||
|
||||
df1 = cast(T1, o)
|
||||
df2 = cast(T2, o)
|
||||
|
||||
df1 + df2
|
||||
|
||||
col1: Positives = df2["c"]
|
||||
col2 = cast(Column[Positive], col1)
|
||||
731
tests/cases/generator/04_frames.py.ref.txt
Normal file
731
tests/cases/generator/04_frames.py.ref.txt
Normal file
@@ -0,0 +1,731 @@
|
||||
Module(
|
||||
body=[
|
||||
FunctionDef(
|
||||
name='__midas_column_same_length__',
|
||||
args=arguments(
|
||||
posonlyargs=[],
|
||||
args=[
|
||||
arg(arg='column1'),
|
||||
arg(arg='column2')],
|
||||
kwonlyargs=[],
|
||||
kw_defaults=[],
|
||||
defaults=[]),
|
||||
body=[
|
||||
Return(
|
||||
value=Compare(
|
||||
left=Call(
|
||||
func=Name(id='len'),
|
||||
args=[
|
||||
Attribute(
|
||||
value=Name(id='column1'),
|
||||
attr='index')],
|
||||
keywords=[]),
|
||||
ops=[
|
||||
Eq()],
|
||||
comparators=[
|
||||
Call(
|
||||
func=Name(id='len'),
|
||||
args=[
|
||||
Attribute(
|
||||
value=Name(id='column2'),
|
||||
attr='index')],
|
||||
keywords=[])]))],
|
||||
decorator_list=[]),
|
||||
FunctionDef(
|
||||
name='__midas_frame_same_length__',
|
||||
args=arguments(
|
||||
posonlyargs=[],
|
||||
args=[
|
||||
arg(arg='frame1'),
|
||||
arg(arg='frame2')],
|
||||
kwonlyargs=[],
|
||||
kw_defaults=[],
|
||||
defaults=[]),
|
||||
body=[
|
||||
Return(
|
||||
value=Compare(
|
||||
left=Call(
|
||||
func=Name(id='len'),
|
||||
args=[
|
||||
Attribute(
|
||||
value=Name(id='frame1'),
|
||||
attr='index')],
|
||||
keywords=[]),
|
||||
ops=[
|
||||
Eq()],
|
||||
comparators=[
|
||||
Call(
|
||||
func=Name(id='len'),
|
||||
args=[
|
||||
Attribute(
|
||||
value=Name(id='frame2'),
|
||||
attr='index')],
|
||||
keywords=[])]))],
|
||||
decorator_list=[]),
|
||||
FunctionDef(
|
||||
name='__midas_is_column__',
|
||||
args=arguments(
|
||||
posonlyargs=[
|
||||
arg(arg='obj')],
|
||||
args=[],
|
||||
kwonlyargs=[],
|
||||
kw_defaults=[],
|
||||
defaults=[]),
|
||||
body=[
|
||||
Import(
|
||||
names=[
|
||||
alias(name='pandas', asname='pd')]),
|
||||
Return(
|
||||
value=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='obj'),
|
||||
Attribute(
|
||||
value=Name(id='pd'),
|
||||
attr='Series')],
|
||||
keywords=[]))],
|
||||
decorator_list=[],
|
||||
returns=Name(id='bool')),
|
||||
FunctionDef(
|
||||
name='__midas_is_dataframe__',
|
||||
args=arguments(
|
||||
posonlyargs=[
|
||||
arg(arg='obj')],
|
||||
args=[],
|
||||
kwonlyargs=[],
|
||||
kw_defaults=[],
|
||||
defaults=[]),
|
||||
body=[
|
||||
Import(
|
||||
names=[
|
||||
alias(name='pandas', asname='pd')]),
|
||||
Return(
|
||||
value=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='obj'),
|
||||
Attribute(
|
||||
value=Name(id='pd'),
|
||||
attr='DataFrame')],
|
||||
keywords=[]))],
|
||||
decorator_list=[],
|
||||
returns=Name(id='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=[
|
||||
GtE()],
|
||||
comparators=[
|
||||
Constant(value=0)]))],
|
||||
decorator_list=[],
|
||||
returns=Constant(value='bool')),
|
||||
FunctionDef(
|
||||
name='__midas_p0__',
|
||||
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_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')),
|
||||
ImportFrom(
|
||||
module='typing',
|
||||
names=[
|
||||
alias(name='Any')],
|
||||
level=0),
|
||||
ImportFrom(
|
||||
module='midas',
|
||||
names=[
|
||||
alias(name='T1'),
|
||||
alias(name='T2'),
|
||||
alias(name='Column'),
|
||||
alias(name='Positive'),
|
||||
alias(name='Positives'),
|
||||
alias(name='cast')],
|
||||
level=0),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='o')],
|
||||
value=Call(
|
||||
func=Name(id='object'),
|
||||
args=[],
|
||||
keywords=[])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')],
|
||||
value=Name(id='o')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_dataframe__'),
|
||||
args=[
|
||||
Name(id='__midas_a0__')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L7:7: 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 Frame[a: Column[int]]: Not a dataframe')])),
|
||||
Assert(
|
||||
test=Compare(
|
||||
left=Constant(value='a'),
|
||||
ops=[
|
||||
In()],
|
||||
comparators=[
|
||||
Name(id='__midas_a0__')]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L7:7: 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 Frame[a: Column[int]]: Missing column 'a'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a0__'),
|
||||
slice=Constant(value='a'))],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L7:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a0__'),
|
||||
slice=Constant(value='a'))],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to Column[int], in column 'a': Not a column")])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Subscript(
|
||||
value=Name(id='__midas_a0__'),
|
||||
slice=Constant(value='a')),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='int')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L7:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to int, in column 'a'")]))],
|
||||
orelse=[]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='df1')],
|
||||
value=Name(id='__midas_a0__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a1__')],
|
||||
value=Name(id='o')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_dataframe__'),
|
||||
args=[
|
||||
Name(id='__midas_a1__')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: 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 Frame[a: Column[int], b: Column[str], c: Column[Positive], d: Column[float where is_positive(_)]]: Not a dataframe')])),
|
||||
Assert(
|
||||
test=Compare(
|
||||
left=Constant(value='a'),
|
||||
ops=[
|
||||
In()],
|
||||
comparators=[
|
||||
Name(id='__midas_a1__')]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: 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 Frame[a: Column[int], b: Column[str], c: Column[Positive], d: Column[float where is_positive(_)]]: Missing column 'a'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='a'))],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='a'))],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to Column[int], in column 'a': Not a column")])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='a')),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='int')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to int, in column 'a'")]))],
|
||||
orelse=[]),
|
||||
Assert(
|
||||
test=Compare(
|
||||
left=Constant(value='b'),
|
||||
ops=[
|
||||
In()],
|
||||
comparators=[
|
||||
Name(id='__midas_a1__')]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: 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 Frame[a: Column[int], b: Column[str], c: Column[Positive], d: Column[float where is_positive(_)]]: Missing column 'b'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='b'))],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='b'))],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to Column[str], in column 'b': Not a column")])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='b')),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='str')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to str, in column 'b'")]))],
|
||||
orelse=[]),
|
||||
Assert(
|
||||
test=Compare(
|
||||
left=Constant(value='c'),
|
||||
ops=[
|
||||
In()],
|
||||
comparators=[
|
||||
Name(id='__midas_a1__')]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: 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 Frame[a: Column[int], b: Column[str], c: Column[Positive], d: Column[float where is_positive(_)]]: Missing column 'c'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='c'))],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='c'))],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to Column[Positive], in column 'c': Not a column")])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='c')),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='float')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to float, in column 'c'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_p0__'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
msg=Constant(value="04_frames.py:L8:7: ConstraintError: Value does not fit constraint 'is_positive(_)', in column 'c'"))],
|
||||
orelse=[]),
|
||||
Assert(
|
||||
test=Compare(
|
||||
left=Constant(value='d'),
|
||||
ops=[
|
||||
In()],
|
||||
comparators=[
|
||||
Name(id='__midas_a1__')]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: 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 Frame[a: Column[int], b: Column[str], c: Column[Positive], d: Column[float where is_positive(_)]]: Missing column 'd'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='d'))],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='d'))],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to Column[float where is_positive(_)], in column 'd': Not a column")])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Subscript(
|
||||
value=Name(id='__midas_a1__'),
|
||||
slice=Constant(value='d')),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='float')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L8:7: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=" to float, in column 'd'")])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_p1__'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
msg=Constant(value="04_frames.py:L8:7: ConstraintError: Value does not fit constraint 'is_positive(_)', in column 'd'"))],
|
||||
orelse=[]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='df2')],
|
||||
value=Name(id='__midas_a1__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a1__')]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a2__')],
|
||||
value=Name(id='df1')),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a3__')],
|
||||
value=Name(id='df2')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_column_same_length__'),
|
||||
args=[
|
||||
Name(id='__midas_a2__'),
|
||||
Name(id='__midas_a3__')],
|
||||
keywords=[]),
|
||||
msg=Constant(value='04_frames.py:L10:1: AssertionError: Columns must have the same length')),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a4__')],
|
||||
value=Name(id='__midas_a2__')),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a5__')],
|
||||
value=Name(id='__midas_a3__')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_frame_same_length__'),
|
||||
args=[
|
||||
Name(id='__midas_a4__'),
|
||||
Name(id='__midas_a5__')],
|
||||
keywords=[]),
|
||||
msg=Constant(value='04_frames.py:L10:1: AssertionError: DataFrames must have the same length')),
|
||||
Expr(
|
||||
value=BinOp(
|
||||
left=Name(id='__midas_a2__'),
|
||||
op=Add(),
|
||||
right=Name(id='__midas_a3__'))),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a2__'),
|
||||
Name(id='__midas_a3__'),
|
||||
Name(id='__midas_a4__'),
|
||||
Name(id='__midas_a5__')]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='col1')],
|
||||
value=Subscript(
|
||||
value=Name(id='df2'),
|
||||
slice=Constant(value='c'))),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a6__')],
|
||||
value=Name(id='col1')),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_is_column__'),
|
||||
args=[
|
||||
Name(id='__midas_a6__')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L13:8: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='__midas_a6__')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=' to Column[Positive]: Not a column')])),
|
||||
For(
|
||||
target=Name(id='value'),
|
||||
iter=Name(id='__midas_a6__'),
|
||||
body=[
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='value'),
|
||||
Name(id='float')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='04_frames.py:L13:8: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=' to float')])),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='__midas_p0__'),
|
||||
args=[
|
||||
Name(id='value')],
|
||||
keywords=[]),
|
||||
msg=Constant(value="04_frames.py:L13:8: ConstraintError: Value does not fit constraint 'is_positive(_)'"))],
|
||||
orelse=[]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='col2')],
|
||||
value=Name(id='__midas_a6__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a6__')])],
|
||||
type_ignores=[])
|
||||
@@ -2,33 +2,29 @@
|
||||
type Custom = float
|
||||
|
||||
// Simple custom types with constraints
|
||||
type Latitude = float where (-90 <= _ <= 90)
|
||||
type Longitude = float where (-180 <= _ <= 180)
|
||||
type Latitude = float where (-90 <= _ & _ <= 90)
|
||||
type Longitude = float where (-180 <= _ & _ <= 180)
|
||||
|
||||
// Generic custom type (a Difference of T is derived from T, e.g. a difference of floats is a float
|
||||
type Difference[T] = T
|
||||
|
||||
// Complex custom type, containing two values accessible through properties
|
||||
type GeoLocation = {
|
||||
type GeoLocation = object
|
||||
|
||||
extend GeoLocation {
|
||||
prop lat: Latitude
|
||||
prop lon: Longitude
|
||||
}
|
||||
|
||||
// Define operations on our custom type
|
||||
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]
|
||||
}
|
||||
type GeoLocationDifference = object
|
||||
|
||||
// For complex generics, you need to specify how the genericity the properties
|
||||
// are handled
|
||||
type Difference[GeoLocation] = {
|
||||
extend GeoLocationDifference {
|
||||
prop lat: Difference[Latitude]
|
||||
prop lon: Difference[Longitude]
|
||||
}
|
||||
|
||||
// Define operations on our custom type
|
||||
|
||||
// Simple operation defined on our custom types
|
||||
extend Latitude {
|
||||
def __sub__: fn(Latitude, /) -> Difference[Latitude]
|
||||
@@ -38,20 +34,30 @@ extend 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
|
||||
predicate Positive(v: float) = v >= 0
|
||||
predicate StrictlyPositive(v: float) = v > 0
|
||||
predicate Equatorial(loc: GeoLocation) = (-10 <= loc.lat <= 10)
|
||||
predicate Arctic(loc: GeoLocation) = (loc.lat >= 66)
|
||||
|
||||
type Person = {
|
||||
type Person = object
|
||||
|
||||
extend Person {
|
||||
prop name: str
|
||||
|
||||
// Property with an inline constraint
|
||||
prop age: Optional[int where (0 <= _ < 150)]
|
||||
prop age: int where (0 <= _ & _ < 150)
|
||||
|
||||
// Property referencing a predicate
|
||||
prop height: float where StrictlyPositive
|
||||
prop height: float where StrictlyPositive(_)
|
||||
|
||||
prop home: GeoLocation
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
35
tests/cases/midas-parser/02_all_syntax.midas
Normal file
35
tests/cases/midas-parser/02_all_syntax.midas
Normal file
@@ -0,0 +1,35 @@
|
||||
// Alias declaration
|
||||
alias A = object
|
||||
|
||||
// Type declaration
|
||||
type B = object
|
||||
|
||||
// Generic declaration
|
||||
type C[T] = object
|
||||
type D[T <: A] = object
|
||||
type E[T, U] = object
|
||||
|
||||
// Type expressions
|
||||
type F[T] = T
|
||||
type G = A where predicate(_)
|
||||
type H = A where _ > 0 & _.attr < 1.0 & +_ + 4.0 >= "string" & !(-_ - 4.0 <= 0 & _ == none & _ != false)
|
||||
type I = fn() -> Any
|
||||
type J = fn(a: int, /, b: float, *, c: bool) -> Any
|
||||
type K = fn(a: int, /, b: float, *, c: bool?) -> Any
|
||||
type L = fn(a: int, /, b: float?, *, c: bool?) -> Any
|
||||
type M = fn(a: int?, /, b: float?, *, c: bool?) -> Any
|
||||
|
||||
// Extend
|
||||
extend N {}
|
||||
extend O {
|
||||
prop a: int
|
||||
def b: fn(int, /) -> int
|
||||
def b: fn(float, /) -> float
|
||||
}
|
||||
|
||||
// Predicate
|
||||
predicate P = true
|
||||
predicate Q(v: float) = v > 0
|
||||
predicate R(a: float, b: float)(v: float) = a < v & v < b
|
||||
predicate S = R(0.0, 1.0)
|
||||
predicate T = R(a=0.0, b=1.0)
|
||||
3620
tests/cases/midas-parser/02_all_syntax.midas.ref.json
Normal file
3620
tests/cases/midas-parser/02_all_syntax.midas.ref.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
df: Frame[
|
||||
verified: bool,
|
||||
birth_year: int,
|
||||
height: float + ( _ > 0 ) + ( _ < 250 ),
|
||||
height: float,
|
||||
name: str,
|
||||
date: datetime,
|
||||
float,
|
||||
|
||||
@@ -40,13 +40,9 @@
|
||||
"_type": "FrameColumn",
|
||||
"name": "height",
|
||||
"type": {
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "(_ > 0) + (_ < 250)"
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,10 +16,6 @@ lat2: Latitude = lat[1]
|
||||
lat_diff: Difference[Latitude] = lat2 - lat1
|
||||
|
||||
df2: Frame[
|
||||
age: int + (_ >= 0),
|
||||
height: float + (_ >= 0),
|
||||
]
|
||||
df2_bis: Frame[
|
||||
age: int + Positive,
|
||||
height: float + Positive,
|
||||
age: int,
|
||||
height: float,
|
||||
]
|
||||
|
||||
@@ -227,61 +227,18 @@
|
||||
"_type": "FrameColumn",
|
||||
"name": "age",
|
||||
"type": {
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "_ >= 0"
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "FrameColumn",
|
||||
"name": "height",
|
||||
"type": {
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "_ >= 0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "df2_bis",
|
||||
"type": {
|
||||
"_type": "FrameType",
|
||||
"columns": [
|
||||
{
|
||||
"_type": "FrameColumn",
|
||||
"name": "age",
|
||||
"type": {
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "Positive"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "FrameColumn",
|
||||
"name": "height",
|
||||
"type": {
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "Positive"
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,11 +4,10 @@ from __future__ import annotations
|
||||
|
||||
|
||||
def func(
|
||||
col1: Column[float + (0 <= _ <= 1)],
|
||||
col2: Column[float + (0 <= _ <= 1)],
|
||||
) -> Column[float + (0 <= _ <= 2)]:
|
||||
result: Column[float + (0 <= _ <= 2)] = col1 + col2
|
||||
return result
|
||||
col1: Column[float],
|
||||
col2: Column[float],
|
||||
) -> Column[float]:
|
||||
return col1 + col2
|
||||
|
||||
|
||||
def func2(a: int, /, b: float, *, c: str):
|
||||
|
||||
@@ -26,13 +26,9 @@
|
||||
"base": "Column",
|
||||
"args": [
|
||||
{
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "0 <= _ <= 1"
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -45,13 +41,9 @@
|
||||
"base": "Column",
|
||||
"args": [
|
||||
{
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "0 <= _ <= 1"
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -65,44 +57,15 @@
|
||||
"base": "Column",
|
||||
"args": [
|
||||
{
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "0 <= _ <= 2"
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "result",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "Column",
|
||||
"args": [
|
||||
{
|
||||
"_type": "ConstraintType",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"constraint": "0 <= _ <= 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "result"
|
||||
}
|
||||
],
|
||||
"_type": "ReturnStmt",
|
||||
"value": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
@@ -115,13 +78,6 @@
|
||||
"name": "col2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "ReturnStmt",
|
||||
"value": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "result"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
49
tests/cases/python-parser/04_all_syntax.py
Normal file
49
tests/cases/python-parser/04_all_syntax.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# type: ignore
|
||||
# ruff: disable[F821]
|
||||
|
||||
import module1
|
||||
import module2 as alias2
|
||||
from module3 import submodule3
|
||||
from module4 import submodule4 as alias4
|
||||
|
||||
a: int
|
||||
b: Generic[int]
|
||||
c: Generic2[int, float]
|
||||
d: Frame[a:int, b:float]
|
||||
|
||||
e = 3
|
||||
f: int = 4
|
||||
g = []
|
||||
h = [1, 0.1, " ", None, False, True]
|
||||
i = {}
|
||||
j = {"a": 1, "b": 2}
|
||||
k = {"c": 3, **j}
|
||||
l = cast(int, a)
|
||||
m = unsafe_cast(int, a)
|
||||
|
||||
|
||||
def n(a: int, /, b: float, *, c: str) -> Any:
|
||||
return
|
||||
|
||||
|
||||
def o(a: int = 1, /, b: float = 2.0, *, c: str = "") -> Any:
|
||||
return 1
|
||||
|
||||
|
||||
for i in h:
|
||||
pass
|
||||
|
||||
if e == f:
|
||||
pass
|
||||
elif f == g:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
p = +a + -b - ~c * d / e**f
|
||||
q = not (a and b) or c
|
||||
r = a & b | c ^ d
|
||||
|
||||
s = a.b.c
|
||||
t = a[b][c, d][e:f]
|
||||
u = a(b)(c=d)
|
||||
725
tests/cases/python-parser/04_all_syntax.py.ref.json
Normal file
725
tests/cases/python-parser/04_all_syntax.py.ref.json
Normal file
@@ -0,0 +1,725 @@
|
||||
{
|
||||
"stmts": [
|
||||
{
|
||||
"_type": "ImportStmt",
|
||||
"imports": [
|
||||
{
|
||||
"_type": "ImportAlias",
|
||||
"name": "module1",
|
||||
"alias": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "ImportStmt",
|
||||
"imports": [
|
||||
{
|
||||
"_type": "ImportAlias",
|
||||
"name": "module2",
|
||||
"alias": "alias2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "FromImportStmt",
|
||||
"module": "module3",
|
||||
"imports": [
|
||||
{
|
||||
"_type": "ImportAlias",
|
||||
"name": "submodule3",
|
||||
"alias": null
|
||||
}
|
||||
],
|
||||
"level": 0
|
||||
},
|
||||
{
|
||||
"_type": "FromImportStmt",
|
||||
"module": "module4",
|
||||
"imports": [
|
||||
{
|
||||
"_type": "ImportAlias",
|
||||
"name": "submodule4",
|
||||
"alias": "alias4"
|
||||
}
|
||||
],
|
||||
"level": 0
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "a",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "b",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "Generic",
|
||||
"args": [
|
||||
{
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "c",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "Generic2",
|
||||
"args": [
|
||||
{
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "d",
|
||||
"type": {
|
||||
"_type": "FrameType",
|
||||
"columns": [
|
||||
{
|
||||
"_type": "FrameColumn",
|
||||
"name": "a",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "FrameColumn",
|
||||
"name": "b",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "e"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "TypeAssign",
|
||||
"name": "f",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "f"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "g"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "ListExpr",
|
||||
"items": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "h"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "ListExpr",
|
||||
"items": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.1
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": " "
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "i"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "DictExpr",
|
||||
"keys": [],
|
||||
"values": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "j"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "DictExpr",
|
||||
"keys": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": "a"
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": "b"
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "k"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "DictExpr",
|
||||
"keys": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": "c"
|
||||
},
|
||||
null
|
||||
],
|
||||
"values": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "j"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "l"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "CastExpr",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"unsafe": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "m"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "CastExpr",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"unsafe": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "Function",
|
||||
"name": "n",
|
||||
"params": {
|
||||
"_type": "ParamSpec",
|
||||
"pos": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"mixed": [
|
||||
{
|
||||
"name": "b",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"kw": [
|
||||
{
|
||||
"name": "c",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "str",
|
||||
"args": []
|
||||
},
|
||||
"default": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {
|
||||
"_type": "BaseType",
|
||||
"base": "Any",
|
||||
"args": []
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"_type": "ReturnStmt",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "Function",
|
||||
"name": "o",
|
||||
"params": {
|
||||
"_type": "ParamSpec",
|
||||
"pos": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "int",
|
||||
"args": []
|
||||
},
|
||||
"default": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"mixed": [
|
||||
{
|
||||
"name": "b",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "float",
|
||||
"args": []
|
||||
},
|
||||
"default": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"kw": [
|
||||
{
|
||||
"name": "c",
|
||||
"type": {
|
||||
"_type": "BaseType",
|
||||
"base": "str",
|
||||
"args": []
|
||||
},
|
||||
"default": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"returns": {
|
||||
"_type": "BaseType",
|
||||
"base": "Any",
|
||||
"args": []
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"_type": "ReturnStmt",
|
||||
"value": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "ForStmt",
|
||||
"target": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "i"
|
||||
},
|
||||
"iterator": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "h"
|
||||
},
|
||||
"body": []
|
||||
},
|
||||
{
|
||||
"_type": "IfStmt",
|
||||
"test": {
|
||||
"_type": "CompareExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e"
|
||||
},
|
||||
"operator": "==",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "f"
|
||||
}
|
||||
},
|
||||
"body": [],
|
||||
"orelse": [
|
||||
{
|
||||
"_type": "IfStmt",
|
||||
"test": {
|
||||
"_type": "CompareExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "f"
|
||||
},
|
||||
"operator": "==",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "g"
|
||||
}
|
||||
},
|
||||
"body": [],
|
||||
"orelse": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "p"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
}
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "-",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"operator": "-",
|
||||
"right": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "~",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "c"
|
||||
}
|
||||
},
|
||||
"operator": "*",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d"
|
||||
}
|
||||
},
|
||||
"operator": "/",
|
||||
"right": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e"
|
||||
},
|
||||
"operator": "**",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "f"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "q"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "LogicalExpr",
|
||||
"left": {
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "not",
|
||||
"right": {
|
||||
"_type": "LogicalExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"operator": "and",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"operator": "or",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "c"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "r"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"operator": "&",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b"
|
||||
}
|
||||
},
|
||||
"operator": "|",
|
||||
"right": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "c"
|
||||
},
|
||||
"operator": "^",
|
||||
"right": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "s"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "GetExpr",
|
||||
"object": {
|
||||
"_type": "GetExpr",
|
||||
"object": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"name": "b"
|
||||
},
|
||||
"name": "c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "t"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "SubscriptExpr",
|
||||
"object": {
|
||||
"_type": "SubscriptExpr",
|
||||
"object": {
|
||||
"_type": "SubscriptExpr",
|
||||
"object": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"index": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "b"
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"_type": "TupleExpr",
|
||||
"items": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "c"
|
||||
},
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "d"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"_type": "SliceExpr",
|
||||
"lower": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "e"
|
||||
},
|
||||
"upper": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "f"
|
||||
},
|
||||
"step": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_type": "AssignStmt",
|
||||
"targets": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "u"
|
||||
}
|
||||
],
|
||||
"value": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "a"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "b"
|
||||
}
|
||||
],
|
||||
"keywords": {}
|
||||
},
|
||||
"arguments": [],
|
||||
"keywords": {
|
||||
"c": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import ast
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -6,17 +7,30 @@ import midas.ast.python as p
|
||||
from midas.checker.checker import TypeChecker
|
||||
from midas.checker.diagnostic import Diagnostic
|
||||
from midas.checker.types import Type
|
||||
from midas.lexer.token import TokenType
|
||||
from tests.base import Tester
|
||||
from tests.serializer.python import PythonAstJsonSerializer
|
||||
|
||||
|
||||
class CustomEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, ast.AST):
|
||||
return ast.dump(o)
|
||||
if isinstance(o, TokenType):
|
||||
return o.name
|
||||
if o == ...:
|
||||
return "..."
|
||||
|
||||
return super().default(o)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaseResult:
|
||||
diagnostics: list[dict] = field(default_factory=list)
|
||||
judgments: list = field(default_factory=list)
|
||||
|
||||
def dumps(self) -> str:
|
||||
return json.dumps(asdict(self), indent=2)
|
||||
return json.dumps(asdict(self), indent=2, cls=CustomEncoder)
|
||||
|
||||
|
||||
class CheckerTester(Tester):
|
||||
|
||||
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from midas.checker.checker import TypeChecker
|
||||
from midas.checker.diagnostic import DiagnosticType
|
||||
from midas.generator.generator import Generator
|
||||
from midas.utils import TypedAST
|
||||
from tests.base import Tester
|
||||
@@ -44,10 +43,11 @@ class GeneratorTester(Tester):
|
||||
|
||||
typed_ast: TypedAST = checker.type_check(path)
|
||||
|
||||
if not any(d.type == DiagnosticType.ERROR for d in checker.diagnostics):
|
||||
generator = Generator(workdir=path.parent, types=checker.types)
|
||||
generator.set_src_path(path)
|
||||
result.compiled_ast = generator.generate_ast(typed_ast)
|
||||
# Ignore errors and generate anyway, easier here and errors should be
|
||||
# covered by checker tests
|
||||
generator = Generator(workdir=path.parent, types=checker.types)
|
||||
generator.set_src_path(path)
|
||||
result.compiled_ast = generator.generate_ast(typed_ast)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from typing import Optional, Sequence
|
||||
from typing import Optional, Sequence, final
|
||||
|
||||
from midas.ast.midas import (
|
||||
AliasStmt,
|
||||
BinaryExpr,
|
||||
CallExpr,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
Expr,
|
||||
ExtendStmt,
|
||||
ExtensionType,
|
||||
FrameType,
|
||||
FunctionType,
|
||||
GenericType,
|
||||
@@ -30,6 +28,7 @@ from midas.ast.midas import (
|
||||
)
|
||||
|
||||
|
||||
@final
|
||||
class MidasAstJsonSerializer(
|
||||
Stmt.Visitor[dict], Expr.Visitor[dict], Type.Visitor[dict]
|
||||
):
|
||||
@@ -172,12 +171,6 @@ class MidasAstJsonSerializer(
|
||||
"constraint": type.constraint.accept(self),
|
||||
}
|
||||
|
||||
def visit_complex_type(self, type: ComplexType) -> dict:
|
||||
return {
|
||||
"_type": "ComplexType",
|
||||
"members": self._serialize_list(type.members),
|
||||
}
|
||||
|
||||
def visit_function_type(self, type: FunctionType) -> dict:
|
||||
return {
|
||||
"_type": "FunctionType",
|
||||
@@ -200,13 +193,6 @@ class MidasAstJsonSerializer(
|
||||
"required": param.required,
|
||||
}
|
||||
|
||||
def visit_extension_type(self, type: ExtensionType) -> dict:
|
||||
return {
|
||||
"_type": "ExtensionType",
|
||||
"base": type.base.accept(self),
|
||||
"extension": type.extension.accept(self),
|
||||
}
|
||||
|
||||
def visit_frame_type(self, type: FrameType) -> dict:
|
||||
return {
|
||||
"_type": "FrameType",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ast
|
||||
from typing import Optional, Sequence, Type
|
||||
from typing import Optional, Sequence, Type, final
|
||||
|
||||
from midas.ast.python import (
|
||||
AssignStmt,
|
||||
@@ -8,7 +8,6 @@ from midas.ast.python import (
|
||||
CallExpr,
|
||||
CastExpr,
|
||||
CompareExpr,
|
||||
ConstraintType,
|
||||
DictExpr,
|
||||
Expr,
|
||||
ExpressionStmt,
|
||||
@@ -79,6 +78,7 @@ boolean_ops: dict[Type[ast.boolop], str] = {
|
||||
}
|
||||
|
||||
|
||||
@final
|
||||
class PythonAstJsonSerializer(
|
||||
Stmt.Visitor[dict], Expr.Visitor[dict], MidasType.Visitor[dict]
|
||||
):
|
||||
@@ -106,13 +106,6 @@ class PythonAstJsonSerializer(
|
||||
"args": self._serialize_list(node.args),
|
||||
}
|
||||
|
||||
def visit_constraint_type(self, node: ConstraintType) -> dict:
|
||||
return {
|
||||
"_type": "ConstraintType",
|
||||
"type": node.type.accept(self),
|
||||
"constraint": ast.unparse(node.constraint),
|
||||
}
|
||||
|
||||
def visit_frame_column(self, node: FrameColumn) -> dict:
|
||||
return {
|
||||
"_type": "FrameColumn",
|
||||
|
||||
83
vscode-ext/language-configuration.json
Normal file
83
vscode-ext/language-configuration.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": {
|
||||
"comment": "//",
|
||||
},
|
||||
"blockComment": [
|
||||
"/*",
|
||||
"*/"
|
||||
]
|
||||
},
|
||||
"brackets": [
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{
|
||||
"open": "{",
|
||||
"close": "}"
|
||||
},
|
||||
{
|
||||
"open": "[",
|
||||
"close": "]"
|
||||
},
|
||||
{
|
||||
"open": "(",
|
||||
"close": ")"
|
||||
},
|
||||
{
|
||||
"open": "'",
|
||||
"close": "'",
|
||||
"notIn": [
|
||||
"string",
|
||||
"comment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"open": "\"",
|
||||
"close": "\"",
|
||||
"notIn": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"open": "/**",
|
||||
"close": " */",
|
||||
"notIn": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
],
|
||||
"surroundingPairs": [
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"'",
|
||||
"'"
|
||||
],
|
||||
[
|
||||
"\"",
|
||||
"\""
|
||||
]
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user