82 Commits

Author SHA1 Message Date
12c5309cff Merge pull request 'Avoid raising exceptions, report diagnostics instead' (#42) from fix/remove-raises into main
All checks were successful
Tests / tests (push) Successful in 6s
Reviewed-on: #42
2026-07-10 09:35:02 +00:00
720a82c221 fix(checker): remove redundant match case
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-10 11:30:10 +02:00
90e9d167bf fix(checker): better handle invalid generic instantiation 2026-07-10 11:23:29 +02:00
4866ab2090 fix(checker): handle already defined predicates 2026-07-10 11:20:39 +02:00
3ede752cae fix(checker): handle already defined types 2026-07-10 11:19:06 +02:00
244e5f351a fix(checker): handle subscript and error in typevar bound 2026-07-10 11:13:32 +02:00
e61c4070da fix(checker): handle returns outside functions 2026-07-10 11:04:49 +02:00
00fdda5034 fix(checker): catch UndefinedMethodException where needed 2026-07-10 11:00:23 +02:00
e302ed2377 Merge pull request 'Add Contributing and License sections to README' (#41) from feat/readme-additions into main
All checks were successful
Tests / tests (push) Successful in 6s
Reviewed-on: #41
2026-07-09 22:39:09 +00:00
1d927e532f chore: add icon to README
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-10 00:38:34 +02:00
bfe4f854ea chore: add Contributing and License to README 2026-07-10 00:38:34 +02:00
3e06a236b5 Merge pull request 'Add Apache 2.0 license' (#40) from feat/license into main
All checks were successful
Tests / tests (push) Successful in 6s
Reviewed-on: #40
2026-07-09 22:20:37 +00:00
ab53ac67d5 chore: add Apache 2.0 license
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-10 00:19:31 +02:00
b3ff5a7b96 Merge pull request 'Tidy examples and minor housekeeping' (#39) from feat/tidy-examples into main
All checks were successful
Tests / tests (push) Successful in 5s
Reviewed-on: #39
2026-07-09 21:41:48 +00:00
dce35391b4 chore: add justfile
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-09 23:41:04 +02:00
28be45eb8f chore: add CI badge 2026-07-09 23:41:03 +02:00
b64110bb06 chore: remove draft architecture diagram 2026-07-09 23:41:02 +02:00
edc2a65492 chore: remove test file 2026-07-09 23:41:01 +02:00
dade8eb0f4 chore: move currency demo in subfolder 2026-07-09 23:41:01 +02:00
9f3dfd686b chore: remove syntax prototype examples 2026-07-09 23:41:00 +02:00
5ef21917ae chore: update and clean some examples 2026-07-09 23:40:59 +02:00
34072de270 Merge pull request 'Minor fixes and more tests' (#38) from feat/add-tests into main
All checks were successful
Tests / tests (push) Successful in 5s
Reviewed-on: #38
2026-07-09 20:19:42 +00:00
2b09a29165 tests: add frame constraint generation test
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-09 22:16:28 +02:00
309e1d4a5b tests: add generator test for simple syntax 2026-07-09 22:04:39 +02:00
74472c974b tests: add test covering some function subtyping rules 2026-07-09 21:58:51 +02:00
a9a3164c24 fix: implement missing methods on printer and highlighter 2026-07-09 18:57:26 +02:00
88ab9dc14d fix: add final decorator to Visitor subclasses 2026-07-09 18:30:06 +02:00
f41dbb528c tests: add custom JSON encoder to for checker output 2026-07-09 18:27:51 +02:00
44963db425 tests: add test for all supported Python syntaxes 2026-07-09 17:59:20 +02:00
b67c940112 tests: add test for all Midas syntaxes 2026-07-09 17:47:18 +02:00
ad40db98d0 fix(checker): always type check unary operand 2026-07-09 17:46:52 +02:00
7a021b2450 fix(checker): type check none literal 2026-07-09 17:39:11 +02:00
a7b62e752b fix(checker): adjust error message location
when constraint doesn't evaluate to a boolean, report error on the constraint expression instead of the whole type
2026-07-09 17:38:53 +02:00
44984af8a3 fix(checker): avoid raising on unknown variable
when an unknown variable is referenced, avoid raising an error and only report it with a diagnostic, returning `UnknownType` instead
2026-07-09 17:37:49 +02:00
0c851cbc9b Merge pull request 'Cleanup and minor improvements' (#37) from feat/tidying into main
All checks were successful
Tests / tests (push) Successful in 5s
Reviewed-on: #37
2026-07-09 14:59:29 +00:00
54b2058b86 chore: fix name and complete language configuration
All checks were successful
Tests / tests (pull_request) Successful in 7s
2026-07-09 16:54:38 +02:00
60ff529226 chore: add midas snippets 2026-07-09 16:54:37 +02:00
c3b243288d fix(checker): remove redundant instance check 2026-07-09 15:59:55 +02:00
cd9b80d22b docs: fix some typos in manual 2026-07-09 15:59:29 +02:00
21b648e18f docs: fix some docstrings 2026-07-09 15:56:56 +02:00
742693fa38 chore: add script to check docstrings 2026-07-09 15:55:06 +02:00
4d343a61ff feat!: remove Python constraint type
this feature was only partially implemented (parsing) and the syntax was not ideal so this commit removes it entirely
2026-07-09 14:15:36 +02:00
f314a95e87 fix(checker): fix function subtyping logic 2026-07-09 12:32:51 +02:00
99350a9505 docs: add description of function subtyping 2026-07-09 12:32:27 +02:00
aaa6d945d1 fix: tidy some TODOs 2026-07-09 00:25:23 +02:00
653612ee87 feat(checker): add variance manager to improve recursive types 2026-07-09 00:02:25 +02:00
2df6bca948 fix(gen): improve cast assert message for columns 2026-07-08 23:28:27 +02:00
62612bd8db Merge pull request 'Properly check variable assignment' (#36) from feat/variable-is-defined into main
All checks were successful
Tests / tests (push) Successful in 6s
Reviewed-on: #36
2026-07-08 17:30:40 +00:00
4ed78d3d7e chore: add new parameter to docstring
All checks were successful
Tests / tests (pull_request) Successful in 5s
2026-07-08 19:30:15 +02:00
ab7012c538 fix(checker): leaking for-loop iterator target
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-08 19:21:19 +02:00
06f71f2945 tests: add test for variable assignment 2026-07-08 19:09:59 +02:00
1b5691dca7 fix(resolver): properly check if variable is defined 2026-07-08 18:42:28 +02:00
2118c260ab Merge pull request 'Complete documentation' (#32) from feat/complete-docs into main
All checks were successful
Tests / tests (push) Successful in 6s
Reviewed-on: #32
2026-07-08 15:41:00 +00:00
403238da09 docs: add commands in manual
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-08 17:39:06 +02:00
c00dfe9910 chore: pin python version for test ci
All checks were successful
Tests / tests (pull_request) Successful in 23s
2026-07-08 16:46:53 +02:00
597ae6bb4c tests: add tests summary
Some checks failed
Tests / tests (pull_request) Failing after 5s
2026-07-08 16:45:03 +02:00
1162716a25 tests: exit with error code if failure when running all tests
Some checks failed
Tests / tests (pull_request) Failing after 5s
2026-07-08 16:20:44 +02:00
c2ebf885ac chore: remove uv from test workflow
All checks were successful
Tests / tests (pull_request) Successful in 6s
2026-07-08 16:19:20 +02:00
7cddc62aaa chore: add some files in gitignore
Some checks failed
Tests / tests (pull_request) Failing after 1m32s
2026-07-08 16:11:51 +02:00
49e00d9fbc chore: add workflow to run tests 2026-07-08 16:09:53 +02:00
1d2f98419e chore: allow passing commit hash through inputs 2026-07-08 15:48:48 +02:00
2a73dc3fef chore: add ci to compile manual 2026-07-08 15:48:47 +02:00
ede7396f9b chore: update README 2026-07-08 15:48:46 +02:00
6ca778dbfa Merge pull request 'Remove complex type' (#35) from feat/remove-complex-type into main
Reviewed-on: #35
2026-07-08 13:44:58 +00:00
672c9c0fa1 docs: remove complex type from syntax definition 2026-07-08 15:44:07 +02:00
dd2f3d6f6a tests: fix frame ops with filtered groupby columns
see 205d19fb72
2026-07-08 15:37:45 +02:00
ef9dd95844 tests: rewrite test with complex types 2026-07-08 15:35:13 +02:00
725e030374 feat: remove complex and extension types 2026-07-08 15:34:40 +02:00
db986d5242 Merge pull request 'Update syntax definitions' (#34) from feat/update-syntax into main
Reviewed-on: #34
2026-07-08 12:48:14 +00:00
3c97e75db6 feat(parser): allow subscript in type annotations 2026-07-08 14:43:52 +02:00
e0a468a2c2 docs: update annotation syntax definitions 2026-07-08 14:43:10 +02:00
6740344eba docs: update Midas EBNF 2026-07-08 14:08:00 +02:00
4f9099a4c4 chore: update VSCode syntax definition
the TextMate language definition was completely rewritten by Claude from my Sublime Syntax definition
some tests against Midas files show that it seems on par with the other definition

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-08 13:41:47 +02:00
5c66c4b645 docs: update syntax railroad diagrams 2026-07-08 13:32:32 +02:00
c0896d2b9b Merge pull request 'Minor improvements' (#33) from fix/weather-pipeline into main
Reviewed-on: #33
2026-07-08 08:13:08 +00:00
607ff53987 feat(checker): handle single string literal in groupby 2026-07-08 10:12:54 +02:00
3268783cbe chore: improve weather pipeline 2026-07-08 10:04:16 +02:00
a48182a4e3 fix(checker): allow calling methods on TopType and UnknownType 2026-07-08 10:03:51 +02:00
205d19fb72 feat(checker): try to filter groupby columns 2026-07-08 10:03:14 +02:00
10c6ea7dda feat(checker): add context to reports 2026-07-08 09:39:11 +02:00
1acf33f376 chore: fix weather pipeline example 2026-07-08 09:32:39 +02:00
aae481776f fix(checker): check ConstraintType's constraint type 2026-07-08 09:32:13 +02:00
104 changed files with 21659 additions and 9191 deletions

View File

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

View 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
View File

@@ -6,4 +6,6 @@ venv
*.pyc
uv.lock
.python-version
/out
/out
/examples/**/build/
/examples/**/*.pyi

202
LICENSE Normal file
View 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.

View File

@@ -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.

View File

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

View 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)
$

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -61,6 +61,7 @@
set document(
title: title,
author: author,
date: none,
)
set text(
font: "Source Sans 3",

View File

@@ -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
]

View File

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

View File

@@ -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,
]

View File

@@ -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)

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -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]
}

View File

@@ -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])

View File

@@ -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)

View File

@@ -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): ...

View File

@@ -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)(_)

View File

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

View File

@@ -47,7 +47,7 @@ def daily_avg(df: DataWithHI):
DailyAverages,
df.groupby(
by=[
df["station_id"],
"station_id",
df["timestamp"].dt.day.rename("day"),
],
)

View File

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

View File

@@ -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
View 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

View File

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

View File

@@ -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)}"

View File

@@ -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():

View File

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

View File

@@ -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]

View File

@@ -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"))

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -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)

View File

@@ -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(

View File

@@ -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(

View File

@@ -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),

View File

@@ -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}",

View File

@@ -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)

View File

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

View File

@@ -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)

View File

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

View File

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

View File

@@ -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"

View File

@@ -7,8 +7,7 @@ span {
&.named-type,
&.generic-type,
&.constraint-type,
&.complex-type {
&.constraint-type {
--col: 150, 150, 150;
}

View File

@@ -7,10 +7,6 @@ span {
--col: 103, 192, 224;
}
&.constraint-type {
--col: 174, 200, 195;
}
&.frame-column {
--col: 216, 231, 81;
}

View File

@@ -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"""

View File

@@ -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=[],

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View 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()

View File

@@ -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?

View File

@@ -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)
}
}

View File

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

View File

@@ -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
View File

@@ -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()

View File

@@ -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__":

View File

@@ -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}'")

View File

@@ -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,

View File

@@ -1,19 +1,5 @@
{
"diagnostics": [
{
"type": "Warning",
"location": {
"start": [
8,
12
],
"end": [
8,
43
]
},
"message": "ConstraintType not yet supported"
},
{
"type": "Warning",
"location": {

View File

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

View File

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

View 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

View 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": {}
}
]
}

View 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

View 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": {}
}
}
]
}

View 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)

View 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=[])

View 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]

View 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)

View 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=[])

View File

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

View 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)

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -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": []
}
},
{

View File

@@ -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,
]

View File

@@ -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": []
}
}
]

View File

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

View File

@@ -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"
}
}
]
},

View 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)

View 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"
}
}
}
}
]
}

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",

View 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