Compare commits
58 Commits
12782dda1e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d582df5927 | |||
|
6a0401833c
|
|||
|
e15607b763
|
|||
|
e28f324a85
|
|||
|
31e696c938
|
|||
|
759b416bf3
|
|||
|
4b2b0fe476
|
|||
|
4c39504750
|
|||
|
f9f3ade6c7
|
|||
|
386018b956
|
|||
|
bd47d33355
|
|||
|
93ddb28802
|
|||
| f7c43837b5 | |||
|
32ed62a6f1
|
|||
|
66f39acec0
|
|||
|
6c04e2fee4
|
|||
| 2bb2e0a684 | |||
|
5630320d21
|
|||
|
9f05ba3224
|
|||
|
5fbe965919
|
|||
| 252a5abdfd | |||
|
55fba6a088
|
|||
|
70ce263ea2
|
|||
|
e1d5eac8b8
|
|||
|
82666a4918
|
|||
|
45f84a2f23
|
|||
|
dedfcb4dbb
|
|||
|
d9ea6365ea
|
|||
| 9c7a93412c | |||
|
d6b8fbfb60
|
|||
|
b290c59ac4
|
|||
|
093f2bc477
|
|||
|
7c771c4070
|
|||
|
a50a207385
|
|||
|
7e5ea5e414
|
|||
|
0ba0266bae
|
|||
|
216c80f08c
|
|||
|
f75d7722a1
|
|||
|
2f29c47274
|
|||
|
80af2b9048
|
|||
|
577454ee7e
|
|||
|
878693383e
|
|||
|
0b91de75a8
|
|||
| 739871c101 | |||
|
4395e9339b
|
|||
|
29e601128d
|
|||
|
b591f5508f
|
|||
|
41d0c84bbe
|
|||
| cccf2f8f9f | |||
|
3f48c2138f
|
|||
|
e4ab27673d
|
|||
|
b02ecc6326
|
|||
|
9e83079910
|
|||
|
ec468dd982
|
|||
|
3edc25d778
|
|||
|
451e54b009
|
|||
|
0dc14f67aa
|
|||
|
ff79f25628
|
@@ -18,6 +18,7 @@ This framework is being developed as part of a Bachelor's Thesis by Louis Herede
|
||||
- [Highlighting](#highlighting)
|
||||
- [Dumping the AST](#dumping-the-ast)
|
||||
- [Dumping the Registry](#dumping-the-registry)
|
||||
- [Generating Stubs](#generating-stubs)
|
||||
- [Showing Type Judgements](#showing-type-judgements)
|
||||
- [Validating Definitions](#validating-definitions)
|
||||
- [Tests](#tests)
|
||||
@@ -116,6 +117,14 @@ midas dump-registry -t types.midas
|
||||
|
||||
This command processes the given Midas definitions and dumps the contents of the types registry.
|
||||
|
||||
### Generating Stubs
|
||||
|
||||
```shell
|
||||
midas stubs types.midas -o stubs.pyi
|
||||
```
|
||||
|
||||
This command generate Python stubs from a Midas definition file
|
||||
|
||||
### Showing Type Judgements
|
||||
|
||||
```shell
|
||||
|
||||
117
assets/icon.svg
Normal file
117
assets/icon.svg
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 128 128"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4.4 (1:1.4.4+202605061436+dcaf3e7d9e)"
|
||||
sodipodi:docname="logo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="true"
|
||||
inkscape:zoom="1.9332778"
|
||||
inkscape:cx="-8.2760999"
|
||||
inkscape:cy="112.2446"
|
||||
inkscape:window-width="2584"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="4"
|
||||
spacingy="4"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="4"
|
||||
enabled="true"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4689"
|
||||
id="linearGradient1478"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
|
||||
x1="26.648937"
|
||||
y1="20.603781"
|
||||
x2="135.66525"
|
||||
y2="114.39767" />
|
||||
<linearGradient
|
||||
id="linearGradient4689">
|
||||
<stop
|
||||
style="stop-color:#e1be1e;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4691" />
|
||||
<stop
|
||||
style="stop-color:#ffeb82;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4693" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4671"
|
||||
id="linearGradient1475"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
|
||||
x1="150.96111"
|
||||
y1="192.35176"
|
||||
x2="112.03144"
|
||||
y2="137.27299" />
|
||||
<linearGradient
|
||||
id="linearGradient4671">
|
||||
<stop
|
||||
style="stop-color:#ffdc21;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4673" />
|
||||
<stop
|
||||
style="stop-color:#ffeb82;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4675" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g1"
|
||||
transform="translate(2.911719,3.414527)">
|
||||
<path
|
||||
style="fill:url(#linearGradient1478);fill-opacity:1"
|
||||
d="m 60.510156,6.3979729 c -4.583653,0.021298 -8.960939,0.4122177 -12.8125,1.09375 C 36.35144,9.4962267 34.291407,13.691825 34.291406,21.429223 v 10.21875 h 26.8125 v 3.40625 h -26.8125 -10.0625 c -7.792459,0 -14.6157592,4.683717 -16.7500002,13.59375 -2.46182,10.212966 -2.5710151,16.586023 0,27.25 1.9059283,7.937852 6.4575432,13.593748 14.2500002,13.59375 h 9.21875 v -12.25 c 0,-8.849902 7.657144,-16.656248 16.75,-16.65625 h 26.78125 c 7.454951,0 13.406253,-6.138164 13.40625,-13.625 v -25.53125 c 0,-7.266339 -6.12998,-12.7247775 -13.40625,-13.9375001 -4.605987,-0.7667253 -9.385097,-1.1150483 -13.96875,-1.09375 z m -14.5,8.2187501 c 2.769547,0 5.03125,2.298646 5.03125,5.125 -2e-6,2.816336 -2.261703,5.09375 -5.03125,5.09375 -2.779476,-1e-6 -5.03125,-2.277415 -5.03125,-5.09375 -1e-6,-2.826353 2.251774,-5.125 5.03125,-5.125 z"
|
||||
id="path1948" />
|
||||
<path
|
||||
style="fill:url(#linearGradient1475);fill-opacity:1"
|
||||
d="m 91.228906,35.054223 v 11.90625 c 0,9.230755 -7.825895,16.999999 -16.75,17 h -26.78125 c -7.335833,0 -13.406249,6.278483 -13.40625,13.625 v 25.531247 c 0,7.26634 6.318588,11.54032 13.40625,13.625 8.487331,2.49561 16.626237,2.94663 26.78125,0 6.750155,-1.95439 13.406253,-5.88761 13.40625,-13.625 V 92.897973 h -26.78125 v -3.40625 h 26.78125 13.406254 c 7.79246,0 10.69625,-5.435408 13.40624,-13.59375 2.79933,-8.398886 2.68022,-16.475776 0,-27.25 -1.92578,-7.757441 -5.60387,-13.59375 -13.40624,-13.59375 z m -15.0625,64.65625 c 2.779478,3e-6 5.03125,2.277417 5.03125,5.093747 -2e-6,2.82635 -2.251775,5.125 -5.03125,5.125 -2.76955,0 -5.03125,-2.29865 -5.03125,-5.125 2e-6,-2.81633 2.261697,-5.093747 5.03125,-5.093747 z"
|
||||
id="path1950" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
697
docs/manual.typ
Normal file
697
docs/manual.typ
Normal file
@@ -0,0 +1,697 @@
|
||||
//#import "@preview/codly:1.3.0": codly, codly-init
|
||||
// Fix unaligned highlights in v0.15.0 ()
|
||||
// See https://github.com/Dherse/codly/pull/132
|
||||
#import "@local/codly:1.3.1": codly, codly-init
|
||||
|
||||
#import "@preview/codly-languages:0.1.10": codly-languages
|
||||
#import "template.typ": TODO, project
|
||||
#import "@preview/gentle-clues:1.3.1" as gc
|
||||
|
||||
#let midas-version = toml("../pyproject.toml").project.version
|
||||
#let head-ref = read("../.git/HEAD").split(":").at(1).trim()
|
||||
#let commit-hash = read("../.git/" + head-ref).slice(0, 8)
|
||||
|
||||
#show: project.with(
|
||||
title: [Midas User Manual],
|
||||
author: "Louis Heredero",
|
||||
version: midas-version,
|
||||
hash: commit-hash,
|
||||
icon-path: path("../assets/icon.svg"),
|
||||
)
|
||||
|
||||
#show: codly-init
|
||||
#codly(
|
||||
languages: codly-languages
|
||||
+ (
|
||||
midas: (
|
||||
name: "Midas",
|
||||
color: rgb("#eedd47"),
|
||||
icon: box(
|
||||
image(
|
||||
"../assets/icon.svg",
|
||||
height: 130%,
|
||||
fit: "contain",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
= Introduction
|
||||
|
||||
Python is a very popular programming language, especially in data sciences.
|
||||
However, it has been designed for simplicity, distancing itself from typed languages such as Java or C to embrace dynamic typing.
|
||||
What this means is that in Python, type checks are deferred to runtime when operations are concretely executed.
|
||||
|
||||
For developers, it might seem like a great way of simplifying the language and making it very flexible, but it does come with a cost.
|
||||
Indeed, type errors are very easy to make in Python. While passing an integer where a string is expected might not be an issue in some cases, these are the sort of thing that can cause crashes or incorrect results without a clear diagnostic to help the user fix it.
|
||||
|
||||
Fortunately, developers using IDEs or properly configured text editors can benefit from external type checkers such as MyPy which will perform static type analysis of their Python code. Some can also be configured to be very strict, forcing the user to make the whole code typeable statically, thus avoiding any runtime type errors.
|
||||
|
||||
This is not the end of the problem though. Some parts of a program, especially in data related fields, may not be available at "compile-time". For example, a dataset can be loaded from an external file, or data can be fetched from an API, with no guarantees of having the expected format when analyzing the code statically.
|
||||
|
||||
In turn, that can cause a range of loud and silent errors at runtime. A malformed number will probably crash the program when trying to convert it, but a NaN in a series of value might just produce wrong results without any exception. Combine this with often long-running data-processing pipelines and this is how developers can waste hours of precious computation time.
|
||||
|
||||
Midas is a type system which can be used on top of Python to provide better type checking capabilities and gradual typing.
|
||||
It aims at providing optional but strict type annotations and casting operations which can produce runtime assertions. It also allows the user to define dependent types with value constraints that are translated into runtime checks.
|
||||
|
||||
= Installation
|
||||
|
||||
Midas comes as a very light Python package that you can install on your system in a few simple steps.
|
||||
|
||||
== Requirements
|
||||
|
||||
Here below are the requirements for installing Midas. All Python dependencies will be installed by `uv` in the installation process described in @install-steps.
|
||||
|
||||
- Python 3.11+
|
||||
- `uv`
|
||||
|
||||
== Steps <install-steps>
|
||||
|
||||
1. Clone the repository
|
||||
```bash
|
||||
git clone https://git.kb28.ch/HEL/midas.git
|
||||
```
|
||||
2. Navigate inside the directory
|
||||
```bash
|
||||
cd midas
|
||||
```
|
||||
3. Install Midas as a tool in your local user space
|
||||
```bash
|
||||
uv tool install .
|
||||
```
|
||||
|
||||
And that's it ! You can now use Midas commands anywhere, like this:
|
||||
```bash
|
||||
midas --help
|
||||
```
|
||||
|
||||
= Quick Start
|
||||
|
||||
This chapter will give you the keys to quickly start using Midas in your project.
|
||||
|
||||
== Defining custom types
|
||||
|
||||
To begin with, you might want to define some custom types for your project, to avoid handling anonymous float values everywhere. To do so, create a `*.midas` file in your project, and write some definitions for your types. See @midas-ref for more information on syntax and features.
|
||||
|
||||
@qs-midas shows a simple example of what it might look like.
|
||||
|
||||
#codly(header: [types.midas])
|
||||
|
||||
#figure(
|
||||
```midas
|
||||
type Meter = float
|
||||
extend Meter {
|
||||
def __add__: fn(Meter, /) -> Meter
|
||||
def __sub__: fn(Meter, /) -> Meter
|
||||
}
|
||||
|
||||
type Coordinate = object
|
||||
extend Coordinate {
|
||||
prop x: Meter
|
||||
prop y: Meter
|
||||
}
|
||||
```,
|
||||
caption: [Example Midas type definitions],
|
||||
) <qs-midas>
|
||||
|
||||
You can check for any syntax error using the following command:
|
||||
```bash
|
||||
midas validate types.midas
|
||||
```
|
||||
|
||||
When you are happy with your definitions, you can generate Python stubs to use in your source code. This allows other type checkers like MyPy to recognize your custom types and avoid reporting them as undefined. It can also help catch some type errors in your IDE.
|
||||
```bash
|
||||
midas stubs types.midas -o stubs.pyi
|
||||
```
|
||||
|
||||
This command will generate a file as shown in @qs-stubs, providing stub classes to represent the type lattice including methods and properties.
|
||||
|
||||
#codly(header: [stubs.pyi])
|
||||
|
||||
#figure(
|
||||
```pyi
|
||||
from __future__ import annotations
|
||||
|
||||
class Meter(float):
|
||||
def __add__(self, _0: Meter, /) -> Meter: ...
|
||||
def __sub__(self, _0: Meter, /) -> Meter: ...
|
||||
|
||||
class Coordinate(object):
|
||||
x: Meter
|
||||
y: Meter
|
||||
```,
|
||||
caption: [Generated stubs from example definitions of @qs-midas],
|
||||
) <qs-stubs>
|
||||
|
||||
== Using Midas in Python
|
||||
|
||||
You can now write your Python program as you would normally. You can import your custom types from the generated stubs file and use them in type annotations.
|
||||
|
||||
You can also import the `cast` and `unsafe_cast` functions from `midas.typing` to explicitly cast a value to a specific type (see @cast for more information).
|
||||
|
||||
An example Python script is shown in @qs-python, demonstrating how you can use custom types in type annotations. Notice the comments describing errors that will be caught by the type checker in @qs-type-checking.
|
||||
|
||||
#codly(header: [script.py])
|
||||
|
||||
#figure(
|
||||
```python
|
||||
from lib import load_coordinate
|
||||
from midas.typing import cast
|
||||
from stubs import Coordinate, Meter
|
||||
|
||||
p1 = cast(Coordinate, load_coordinate(0))
|
||||
p2 = cast(Coordinate, load_coordinate(1))
|
||||
|
||||
diff_x = p2.x - p1.x
|
||||
diff_y = p2.y - p1.y
|
||||
|
||||
dist = diff_x + diff_y
|
||||
|
||||
p2.x += cast(Meter, 1)
|
||||
p2.y = True # invalid, wrong type
|
||||
p2.z = 3 # invalid, no property 'z' on Coordinate
|
||||
p2.x.a = 3 # invalid, no properties on Meter
|
||||
```,
|
||||
caption: [Example Python script],
|
||||
) <qs-python>
|
||||
|
||||
== Type checking <qs-type-checking>
|
||||
|
||||
Now that you have defined some types and written a script, you can run the type checker with the following command. You can also skip this step and directly run the compilation command in @qs-compilation.
|
||||
|
||||
```bash
|
||||
midas check -t types.midas script.py
|
||||
```
|
||||
|
||||
== Compiling <qs-compilation>
|
||||
|
||||
The final step is to compile your code. This step will produce a runnable Python script, including runtime assertions generated by `cast` expressions.
|
||||
|
||||
```bash
|
||||
midas compile -t types.midas script.py
|
||||
python3 build/midas/script.py
|
||||
```
|
||||
|
||||
= Midas Language Reference <midas-ref>
|
||||
|
||||
In this chapter, you will find a complete reference for the Midas definition language.
|
||||
|
||||
A `*.midas` file contains a number of statements, which can be:
|
||||
- *`type`* statements (see @type-stmt): to define a new type
|
||||
- *`extend`* statements (see @extend-stmt): to define member of a type
|
||||
- *`predicate`* statements (see @predicate-stmt): to define named predicates that can be used in constraint types
|
||||
|
||||
== Type Statement <type-stmt>
|
||||
|
||||
A *`type`* statement lets you define a new type. It requires a unique name and base type.
|
||||
|
||||
The simplest form of a *`type`* statement is:
|
||||
#figure(
|
||||
```midas
|
||||
type MyType = float
|
||||
```,
|
||||
caption: [Simple `type` statement declaring a new type "`MyType`" as a subtype of `float`],
|
||||
) <midas-simple-alias>
|
||||
|
||||
This statement defines a new type called `MyType` which is a subtype of `float`. `MyType` is a `float` but a `float` is not necessarily `MyType`.
|
||||
|
||||
=== Builtin / base types
|
||||
|
||||
A number of base types are provided out of the box, which can be used to derive other types.
|
||||
|
||||
They correspond to Python's builtin types:
|
||||
```py object```,
|
||||
```py str```,
|
||||
```py float```,
|
||||
```py int```,
|
||||
```py bool```,
|
||||
```py list```,
|
||||
```py dict```,
|
||||
```py None```.
|
||||
|
||||
Some differences are to be noted however.
|
||||
1. ```py bool``` is not a subtype of ```py int```
|
||||
2. ```py list``` are homogeneous, i.e. all items must be of the same type
|
||||
3. ```py dict``` keys and values are homogeneous, i.e. all keys must be of the same type and all values must be of the same type (can be different from keys).
|
||||
|
||||
=== Function types
|
||||
|
||||
A function type is written in a similar notation to Python function definitions:
|
||||
#figure(
|
||||
```midas
|
||||
type Repeater = fn(text: str, count: int) -> str
|
||||
```,
|
||||
caption: [Simple function type definition],
|
||||
)
|
||||
|
||||
Midas supports positional-only, keyword-only and mixed arguments (using the `/` and `*` separators). You may omit the name of positional-only arguments. The return type is required.
|
||||
|
||||
Optional parameters can be indicated by adding a question mark (`?`) after their type:
|
||||
#figure(
|
||||
```midas
|
||||
type Repeater = fn(text: str, count: int, *, sep: str?) -> str
|
||||
```,
|
||||
caption: [Function type definition with an optional keyword-only parameter],
|
||||
)
|
||||
|
||||
#gc.warning[
|
||||
Sink arguments (`*args`, `**kwargs`) are not currently supported.
|
||||
]
|
||||
|
||||
=== Constraint types
|
||||
|
||||
A useful feature provided by Midas is the possibility to combine types with custom value constraints. For example, you might want to define a type for positive amounts of money:
|
||||
|
||||
#figure(
|
||||
```midas
|
||||
type Money = float
|
||||
type Income = Money where _ >= 0
|
||||
```,
|
||||
caption: [Simple constraint type definition],
|
||||
)
|
||||
|
||||
Constraints can be combined with any type using the `where` keyword, followed by a constraint expression (see @constraint-expr).
|
||||
|
||||
=== Generic types
|
||||
|
||||
For more complex types, you might want to use type parameters. For example, to define a container, we might write:
|
||||
#figure(
|
||||
```midas
|
||||
type Container[T] = object
|
||||
```,
|
||||
caption: [Simple generic container type definition],
|
||||
)
|
||||
|
||||
To better refine a generic type, you can also bound type parameters using the following syntax:
|
||||
#figure(
|
||||
```midas
|
||||
type Container[T <: float] = object
|
||||
```,
|
||||
caption: [Generic container type definition with a bound],
|
||||
)
|
||||
|
||||
This can be read as "`Container` is a generic type which takes one type parameter `T` that must be a subtype of `float`".
|
||||
|
||||
You can use a generic type, i.e. instantiate it, by using a similar syntax with concrete type as arguments:
|
||||
|
||||
#figure(
|
||||
```midas
|
||||
type MyContainer = Container[MyType]
|
||||
```,
|
||||
caption: [Application of a generic type],
|
||||
)
|
||||
|
||||
Generic types can also take multiple parameters, which are then separated by commas:
|
||||
#figure(
|
||||
```midas
|
||||
type ZipCodeRegistry = dict[int, str]
|
||||
```,
|
||||
caption: [Application of a multi-parameter generic type],
|
||||
)
|
||||
|
||||
The _body_ of a generic type, i.e. the right-hand side of the definition, can contain or even be equal to any number of its parameters.#footnote[The latter is not something that is expressible in standard Python, yet it brings a semantic distinction on top of structurally equivalent values.] For example, the following is a valid type statement:
|
||||
#figure(
|
||||
```midas
|
||||
type Price[T <: Currency] = T where _ > 0
|
||||
```,
|
||||
caption: [Type parameters in a generic type's body],
|
||||
)
|
||||
|
||||
#pagebreak()
|
||||
|
||||
== Extend Statement <extend-stmt>
|
||||
|
||||
Type statements allow you to define new types, kind of like type aliases. However, a type might have properties or methods of its own. These might override those of the parent type or be brand new members.
|
||||
|
||||
This is where the `extend` statement comes into play. It allows defining members on a given type. Members can either be properties (`prop`) or methods (`def`). The only difference between the two is that methods must be functions and can be overloaded.
|
||||
|
||||
Here is a simple example showing how to define a property and a method on a custom type:
|
||||
#figure(
|
||||
```midas
|
||||
type MyType = float
|
||||
extend MyType {
|
||||
prop norm: float
|
||||
def double: fn() -> MyType
|
||||
}
|
||||
```,
|
||||
caption: [Simple `extend` statement defining a property and a method],
|
||||
)
|
||||
|
||||
An `extend` statement can appear anywhere after the type it extends has been defined.
|
||||
You may want to override Python's dunder methods to implement type checking for some basic operators, like `__add__` for the `+` operator.
|
||||
|
||||
#figure(
|
||||
```midas
|
||||
type Money = float
|
||||
extend Money {
|
||||
def __add__(Money, /) -> Money
|
||||
def __mul__(float, /) -> Money
|
||||
}
|
||||
```,
|
||||
caption: [Simple `extend` statement overriding some dunder methods],
|
||||
)
|
||||
|
||||
When extending generic type, you must specify the whole type, including its parameter(s):
|
||||
#figure(
|
||||
```midas
|
||||
type Container[T <: float] = object
|
||||
extend Container[T <: float] {
|
||||
prop content: T
|
||||
def set_content: fn(content: T) -> None
|
||||
}
|
||||
```,
|
||||
caption: [Generic `extend` statement using type parameters in the declared members],
|
||||
)
|
||||
|
||||
#pagebreak()
|
||||
|
||||
== Predicate Statement <predicate-stmt>
|
||||
|
||||
A *`predicate`* statement lets you define a named constraint expression, like a function, which can then be used in other constraint expressions (either in other predicate statements or in constraint types). See @constraint-expr for more information about the syntax of constraint expressions.
|
||||
|
||||
The left-hand side of a predicate statement is written as a function signature, without a return type. The right-hand side is a constraint expression. For example:
|
||||
#figure(
|
||||
```midas
|
||||
predicate is_positive(v: float) = v >= 0
|
||||
```,
|
||||
caption: [Simple `predicate` statement defining an `is_positive` predicate],
|
||||
)
|
||||
|
||||
The left-hand side can also be curried to allow partial application. For example:
|
||||
#figure(
|
||||
```midas
|
||||
predicate in_range(mn: float, mx: float)(v: float) = mn <= v & v <= mx
|
||||
predicate is_ratio = in_range(0.0, 1.0)
|
||||
```,
|
||||
caption: [Curried `predicate` statement and partial application],
|
||||
) <midas-predicate-partial>
|
||||
|
||||
Notice that the second predicate statement doesn't take any parameters. This is simply a partial application of another predicate, kind of like an alias. You can use it in other expressions to finalize the call:
|
||||
#figure(
|
||||
```midas
|
||||
type Efficiency = float where is_ratio(_)
|
||||
```,
|
||||
caption: [Constraint type definition using the partially applied predicate from @midas-predicate-partial],
|
||||
)
|
||||
|
||||
Of course you can also directly call `in_range`:
|
||||
#figure(
|
||||
```midas
|
||||
type Efficiency = float where in_range(0.0, 1.0)(_)
|
||||
```,
|
||||
caption: [Full call of curried predicate from @midas-predicate-partial],
|
||||
)
|
||||
|
||||
When compiled, named predicates are translated to Python functions which are used in runtime assertions. Only predicates that are referenced are compiled.
|
||||
|
||||
#pagebreak()
|
||||
== Constraint Expressions <constraint-expr>
|
||||
|
||||
*Constraint expressions* are Python-like expressions which can appear in *`predicate`* statements or in constraint types.
|
||||
|
||||
They can contain comparisons, simple computations, logical operations and must evaluate to a boolean value.
|
||||
|
||||
Context is quite restricted inside these expressions. You can only reference some builtin functions, such as type constructors (`float(...)`, `str(...)`, etc.), parameters of predicate statements, and named predicates. In constraint type, the special variable `_` can be used to reference the value targeted by the type. For example:
|
||||
|
||||
#figure(
|
||||
```midas
|
||||
predicate not_nan(v: float) = v != float("nan")
|
||||
type RealFloat = float where not_nan(_)
|
||||
```,
|
||||
caption: [Example constraint expressions],
|
||||
) <ex-constraint-expr>
|
||||
|
||||
In the predicate statement (@ex-constraint-expr:1), we reference the parameter `v` and the builtin `float` function.
|
||||
In the constraint type definition (@ex-constraint-expr:2), we then reference the named predicate `not_nan`, passing the value targeted by the type itself ( `_` )
|
||||
|
||||
= Supported Python Syntax <python-ref>
|
||||
|
||||
Midas integrates naturally in Python via type annotations. Through generated stubs, even other type checker can detect your custom types (see @cmd-stubs).
|
||||
|
||||
It has been designed to leave the user free of typing any amount of their code but be strict about the parts that are annotated. By default, any untyped Python expression is assigned `UnknownType`.
|
||||
|
||||
Any operation is permitted on `UnknownType` and will result in `UnknownType` values.
|
||||
|
||||
The moment an expression can be typed, that be thanks to an annotation or a literal value, the type checker kicks in and will validate your statements.
|
||||
|
||||
Because Python is very flexible language with many features, some expressions and statements might be more complex to properly type check, thus only a subset of the Python language is fully supported. This chapter lists all supported features of Python and how they affect type checking.
|
||||
|
||||
Some examples are presented in the following sections in the form of code blocks. Highlights in the code blocks indicate the type assigned to each expression by the type checker. Some types may be omitted for readability. For example:
|
||||
|
||||
#codly(
|
||||
highlights: (
|
||||
(
|
||||
line: 1,
|
||||
start: 5,
|
||||
fill: green,
|
||||
tag: [_int_],
|
||||
),
|
||||
(
|
||||
line: 2,
|
||||
start: 7,
|
||||
end: 7,
|
||||
fill: green,
|
||||
tag: [_int_],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
```python
|
||||
v = 3
|
||||
print(v)
|
||||
```
|
||||
|
||||
== Literals
|
||||
|
||||
Literal Python values are type checked using builtin types. Lists and dictionaries of literals are also typed liked literals. This does not include comprehension lists/dicts (```py [. for . in .]```), nor formatted strings (```py f"..."```). @supported-literals shows the list of supported literal values and their type.
|
||||
|
||||
#let supported-literals = table(
|
||||
columns: 2,
|
||||
table.header[*Example value*][*Judged Type*],
|
||||
```py 42```, ```py int```,
|
||||
```py 3.14```, ```py float```,
|
||||
```py True```, ```py bool```,
|
||||
```py "Midas"```, ```py str```,
|
||||
```py None```, ```py None```,
|
||||
```py [1, 2, 3]```, ```py list[int]```,
|
||||
```py {1: "One", 2: "Two"}```, ```py dict[int, str]```,
|
||||
```py ("1", 1, True)```, ```py tuple[str, int, bool]```,
|
||||
)
|
||||
|
||||
#figure(
|
||||
supported-literals,
|
||||
caption: [Supported literal values and their judged types],
|
||||
) <supported-literals>
|
||||
|
||||
== Assignments
|
||||
|
||||
Variable assignments allow assigning a new value to a variable. For the type checker, this implies two things:
|
||||
1. If the variable was not already declared in the current scope, it is declared at that point with the type of the right-hand side expression
|
||||
2. If the variable was already declared, the type of the right-hand side expression is checked against the declared type of the variable. Only a subtype of the variable's type can be assigned to it
|
||||
|
||||
Once a variable has been given a type, it cannot be changed in the same scope.
|
||||
|
||||
The walrus operator (```py :=```) is not currently supported.
|
||||
|
||||
A simple annotation declaration, without assigning a value, is enough to declare a variable. For example:
|
||||
#figure(
|
||||
```python
|
||||
var: float
|
||||
```,
|
||||
caption: [Bare Python variable annotation without assignment],
|
||||
)
|
||||
|
||||
Because unpacking is not supported, assigning to multiple values is also not handled by the type checker.
|
||||
|
||||
== Arithmetic
|
||||
|
||||
- All basic binary operators are supported, through dunder methods.
|
||||
- All comparison operators except ```py in``` are supported.
|
||||
- All unary operators are supported (`+`, `-`, `~`).
|
||||
- All logical operators are supported (```py and```, ```py or```, ```py not```).
|
||||
|
||||
== Ternary operator
|
||||
|
||||
The ternary operator ```py . if . else .``` is supported. As for `if` statements (see @if-else), the test expression must be a boolean. Additionally, both branches must be of the same type.
|
||||
|
||||
For example:
|
||||
#codly(
|
||||
highlights: (
|
||||
(
|
||||
line: 1,
|
||||
start: 10,
|
||||
end: 44,
|
||||
tag: [_str_],
|
||||
fill: blue,
|
||||
),
|
||||
(
|
||||
line: 1,
|
||||
start: 11,
|
||||
end: 16,
|
||||
tag: [_str_],
|
||||
fill: green,
|
||||
),
|
||||
(
|
||||
line: 1,
|
||||
start: 39,
|
||||
end: 43,
|
||||
tag: [_str_],
|
||||
fill: green,
|
||||
),
|
||||
(
|
||||
line: 1,
|
||||
start: 21,
|
||||
end: 32,
|
||||
tag: [_bool_],
|
||||
fill: green,
|
||||
),
|
||||
),
|
||||
)
|
||||
#figure(
|
||||
```python
|
||||
parity = ("even" if num % 2 == 0 else "odd")
|
||||
```,
|
||||
caption: [Typing of ternary operator],
|
||||
)
|
||||
|
||||
== Control flow
|
||||
|
||||
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and typ checked by Midas.
|
||||
|
||||
=== `if` / `elif` / `else` <if-else>
|
||||
|
||||
Conditional statements are checked relatively strictly by Midas. The test expression, i.e. what comes after the ```py if``` keyword, must be a boolean. While Python allows introducing and leaking new variables from inside an ```py if``` statement, Midas will strictly forbid leaks by restraining bindings to the scope they are defined in. For example, the following Python code will not compile with Midas:
|
||||
#figure(
|
||||
```python
|
||||
age = 22
|
||||
if age >= 18:
|
||||
msg = "You're an adult"
|
||||
else:
|
||||
msg = "You're still a child"
|
||||
print(msg) # -> unknown variable 'msg'
|
||||
```,
|
||||
caption: [`if`/`else` statement cannot leak variables],
|
||||
)
|
||||
|
||||
=== `for` loops
|
||||
|
||||
Simple forms of `for` loops can be used, that is using a single variable and iterating over an object implementing the `__getitem__` method. Like above in @if-else, leaking variables from inside the loop is ignored.
|
||||
|
||||
The `for`-`else` statements are not supported. `while` loops are also not not supported.
|
||||
|
||||
== Functions
|
||||
|
||||
You can define functions as usual and the type checker will do its best to type it. Apart from argument sinks (`*args`, `**kwargs`), all forms of parameter specifications are supported (positional-only, keyword-only, mixed, optional).
|
||||
|
||||
As for the rest of your code, type annotations are optional, but recommended. If you omit the return type hint, the type checker will try to infer it from the function body and its return statements. If you did specify a return type, all return paths must return values that are subtypes of the type hint.
|
||||
|
||||
#codly(
|
||||
highlights: (
|
||||
(
|
||||
line: 2,
|
||||
start: 12,
|
||||
end: 16,
|
||||
tag: [_float_],
|
||||
fill: green,
|
||||
),
|
||||
(
|
||||
line: 2,
|
||||
start: 12,
|
||||
tag: [_float_],
|
||||
fill: blue,
|
||||
),
|
||||
(
|
||||
line: 3,
|
||||
start: 10,
|
||||
end: 15,
|
||||
tag: [_(value: float) -> float_],
|
||||
fill: green,
|
||||
),
|
||||
(
|
||||
line: 3,
|
||||
start: 17,
|
||||
end: 19,
|
||||
tag: [_float_],
|
||||
fill: green,
|
||||
),
|
||||
(
|
||||
line: 3,
|
||||
start: 10,
|
||||
tag: [_float_],
|
||||
fill: blue,
|
||||
),
|
||||
),
|
||||
)
|
||||
#figure(
|
||||
```python
|
||||
def double(value: float) -> float:
|
||||
return value * 2
|
||||
result = double(4.0)
|
||||
```,
|
||||
caption: [Typing of function's body and call],
|
||||
)
|
||||
|
||||
Anonymous functions (```py lambda```) are not yet supported
|
||||
|
||||
== Casts <cast>
|
||||
|
||||
#gc.info[
|
||||
The functions discussed in this section are provided by the `midas.typing` submodule. You can import them in your script like so:
|
||||
#figure(
|
||||
```python
|
||||
from midas.typing import cast, unsafe_cast
|
||||
```,
|
||||
caption: [Importing cast functions],
|
||||
)
|
||||
]
|
||||
|
||||
Sometimes, you may want to use a value whose type is not known to the type checker in a place where it expects a particular type. In that case, if you do know that the runtime type will correspond to what is expected, you can use a `cast` expression.
|
||||
|
||||
Similar to the `cast` function from the `typing` package of Python's Standard Library, it allows telling the type checker that a value has a given type. While `typing`'s function doesn't have any runtime side-effect, Midas' will generate runtime assertions, ensuring that your statement is true when running the code. What cannot be checked statically is checked at runtime.
|
||||
|
||||
In the following example, a runtime check would be generated to ensure that the value is indeed a `float` and that it satisfies the type's constraint (i.e. `>= 0`):
|
||||
|
||||
#codly(
|
||||
highlights: (
|
||||
(
|
||||
line: 1,
|
||||
start: 35,
|
||||
end: 47,
|
||||
tag: [_UnknownType_],
|
||||
fill: red,
|
||||
),
|
||||
(
|
||||
line: 2,
|
||||
start: 7,
|
||||
end: 17,
|
||||
tag: [_PositiveFloat_],
|
||||
fill: green,
|
||||
),
|
||||
),
|
||||
)
|
||||
#figure(
|
||||
```python
|
||||
typed_value = cast(PositiveFloat, unknown_value)
|
||||
print(typed_value)
|
||||
```,
|
||||
caption: [Typing of `cast` expression],
|
||||
)
|
||||
|
||||
There may be some cases where the cost of checking a value at runtime is simply not worth the safety, for example when dealing with a big dataset. If do wish so, you can use `unsafe_cast` which will only tell the type checker the type of the value, without generating a runtime assertion. This maps to the default behavior of `typing`'s own `cast` function.
|
||||
|
||||
If the value passed to `cast` or `unsafe_cast` is a literal (e.g. an integer, a string, a list of literals, etc.), the assertion is evaluated _at compile-time_ and no runtime assertion is generated.
|
||||
|
||||
= Commands <commands>
|
||||
|
||||
#TODO
|
||||
|
||||
== Type Checking (`check`) <cmd-check>
|
||||
== Compiling (`compile`) <cmd-compile>
|
||||
== Formatting (`format`) <cmd-format>
|
||||
== Highlighting (`highlight`) <cmd-highlight>
|
||||
== Dumping the AST (`parse`) <cmd-parse>
|
||||
== Dumping the Registry (`dump-registry`) <cmd-registry>
|
||||
== Generating Stubs (`stubs`) <cmd-stubs>
|
||||
== Showing Type Judgements (`types`) <cmd-types>
|
||||
== Validating Definitions (`validate`) <cmd-validate>
|
||||
180
docs/midas.sublime-syntax
Normal file
180
docs/midas.sublime-syntax
Normal file
@@ -0,0 +1,180 @@
|
||||
%YAML 1.2
|
||||
---
|
||||
name: Midas
|
||||
file_extensions:
|
||||
- midas
|
||||
scope: source.midas
|
||||
|
||||
variables:
|
||||
identifier: "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
|
||||
contexts:
|
||||
prototype:
|
||||
- include: comments
|
||||
|
||||
main:
|
||||
- include: keywords
|
||||
- include: types
|
||||
|
||||
comments:
|
||||
- match: "//"
|
||||
scope: punctuation.definition.comment.midas
|
||||
push:
|
||||
- meta_scope: comment.line.midas
|
||||
- match: $
|
||||
pop: true
|
||||
- match: /\*
|
||||
scope: punctuation.definition.comment.midas
|
||||
push:
|
||||
- meta_scope: comment.block.midas
|
||||
- match: \*/
|
||||
pop: true
|
||||
|
||||
string:
|
||||
- meta_include_prototype: false
|
||||
- meta_scope: string.quoted.double.c
|
||||
- match: '"'
|
||||
pop: true
|
||||
|
||||
keywords:
|
||||
- match: \btype\b
|
||||
scope: keyword.declaration.midas
|
||||
push: type-stmt
|
||||
- match: \bextend\b
|
||||
scope: keyword.declaration.midas
|
||||
push: extend-stmt
|
||||
- match: \bpredicate\b
|
||||
scope: keyword.declaration.midas
|
||||
push: predicate-stmt
|
||||
|
||||
type-stmt:
|
||||
- match: "{{identifier}}"
|
||||
scope: entity.name.type
|
||||
- match: \[
|
||||
push: type-params
|
||||
- match: "="
|
||||
scope: keyword.operator.equal.midas
|
||||
push: type-expr
|
||||
- match: $
|
||||
pop: true
|
||||
|
||||
type-expr:
|
||||
- match: \b(fn)\s*(\()
|
||||
captures:
|
||||
1: keyword.other.midas
|
||||
2: punctuation.section.group.begin
|
||||
push: fn-params
|
||||
- match: \b(where)\b
|
||||
scope: keyword.other.midas
|
||||
set: constraint
|
||||
- match: "{{identifier}}"
|
||||
scope: entity.name.type
|
||||
- match: $
|
||||
pop: 2
|
||||
|
||||
fn-params:
|
||||
- match: "({{identifier}})(:)"
|
||||
captures:
|
||||
1: variable.parameter.midas
|
||||
2: punctuation.separator.annotation.midas
|
||||
push:
|
||||
- include: type-expr
|
||||
- match: \?
|
||||
scope: keyword.operator.qmark.midas
|
||||
- match: "(?=,)"
|
||||
scope: punctuation.separator.midas
|
||||
pop: true
|
||||
- match: '(?=\))'
|
||||
pop: true
|
||||
- include: type-expr
|
||||
- match: '\)'
|
||||
set:
|
||||
- match: "->"
|
||||
scope: keyword.operator.arrow.midas
|
||||
set: type-expr
|
||||
|
||||
constraint:
|
||||
- match: $
|
||||
pop: 2
|
||||
|
||||
- match: \d+(\.\d+)?
|
||||
scope: constant.numeric.midas
|
||||
|
||||
- match: \b(true|false|none)\b
|
||||
scope: constant.language.midas
|
||||
|
||||
- match: '"'
|
||||
push: string
|
||||
|
||||
- match: (<=|>=|<|>|==|!=|&)
|
||||
scope: keyword.operator
|
||||
|
||||
- match: _
|
||||
scope: variable.language.midas
|
||||
|
||||
- match: '{{identifier}}(?=\s*\()'
|
||||
scope: variable.function.midas
|
||||
|
||||
- match: "{{identifier}}"
|
||||
scope: variable.other.readwrite.midas
|
||||
|
||||
type-params:
|
||||
- match: "<:"
|
||||
scope: keyword.operator.subtype.midas
|
||||
- match: "[a-zA-Z][a-zA-Z_0-9]*"
|
||||
scope: entity.name.type
|
||||
- match: "]"
|
||||
pop: true
|
||||
|
||||
extend-stmt:
|
||||
- match: "{{identifier}}"
|
||||
scope: entity.name.type
|
||||
- match: \[
|
||||
push: type-params
|
||||
- match: \{
|
||||
scope: punctuation.section.block.begin
|
||||
set: extend-body
|
||||
|
||||
extend-body:
|
||||
- include: member-stmt
|
||||
- match: \}
|
||||
scope: punctuation.section.block.end
|
||||
pop: true
|
||||
|
||||
member-stmt:
|
||||
- match: \b(prop|def)\b
|
||||
scope: keyword.other.midas
|
||||
push:
|
||||
- match: "{{identifier}}"
|
||||
scope: variable.other.member
|
||||
- match: ":"
|
||||
push: type-expr
|
||||
- match: $
|
||||
pop: true
|
||||
|
||||
predicate-stmt:
|
||||
- match: "{{identifier}}"
|
||||
scope: entity.name.function.midas
|
||||
- match: '\('
|
||||
push: predicate-params
|
||||
- match: "="
|
||||
scope: keyword.operator.equal.midas
|
||||
set: constraint
|
||||
- match: $
|
||||
pop: true
|
||||
|
||||
predicate-params:
|
||||
- match: "({{identifier}})(:)"
|
||||
captures:
|
||||
1: variable.parameter.midas
|
||||
2: punctuation.separator.annotation.midas
|
||||
push:
|
||||
- include: type-expr
|
||||
- match: "(?=,)"
|
||||
scope: punctuation.separator.midas
|
||||
pop: true
|
||||
- match: '(?=\))'
|
||||
pop: true
|
||||
|
||||
- match: '\)'
|
||||
pop: true
|
||||
143
docs/template.typ
Normal file
143
docs/template.typ
Normal file
@@ -0,0 +1,143 @@
|
||||
#import "@preview/modpattern:0.2.0": modpattern
|
||||
|
||||
#let TODO = block(
|
||||
width: 6em,
|
||||
height: 3em,
|
||||
stroke: red,
|
||||
fill: modpattern(
|
||||
size: (10pt, 10pt),
|
||||
line(
|
||||
start: (0%, 0%),
|
||||
end: (100%, 100%),
|
||||
stroke: gray.transparentize(60%) + 2pt,
|
||||
),
|
||||
),
|
||||
align(
|
||||
center + horizon,
|
||||
text(fill: red, size: 1.5em)[*TODO*],
|
||||
),
|
||||
)
|
||||
|
||||
#let _render-header(version, hash) = {
|
||||
let last-heading = query(heading.where(level: 1).before(here())).last(default: none)
|
||||
let next-heading = query(heading.where(level: 1).after(here())).first(default: none)
|
||||
|
||||
let current-heading = if next-heading != none and next-heading.location().page() == here().page() {
|
||||
next-heading
|
||||
} else if last-heading != none {
|
||||
last-heading
|
||||
} else { none }
|
||||
|
||||
let chapter = if current-heading != none {
|
||||
let body = current-heading.body
|
||||
if current-heading.numbering != none {
|
||||
let num = counter(heading).display(current-heading.numbering, at: current-heading.location())
|
||||
body = [#num #body]
|
||||
}
|
||||
body
|
||||
} else []
|
||||
|
||||
grid(
|
||||
columns: (1fr, auto, 1fr),
|
||||
align: (left, center, right),
|
||||
document.title, [v#version - #hash], chapter,
|
||||
)
|
||||
}
|
||||
|
||||
#let _unshift-prefix(prefix, content) = context {
|
||||
pad(left: -measure(prefix).width, prefix + content)
|
||||
}
|
||||
|
||||
#let project(
|
||||
title: none,
|
||||
author: none,
|
||||
version: "0.0.1",
|
||||
hash: "abcdefgh",
|
||||
icon-path: none,
|
||||
doc,
|
||||
) = {
|
||||
assert(title != none, message: "Please provide a title")
|
||||
|
||||
set document(
|
||||
title: title,
|
||||
author: author,
|
||||
)
|
||||
set text(
|
||||
font: "Source Sans 3",
|
||||
)
|
||||
|
||||
set raw(syntaxes: path("midas.sublime-syntax"))
|
||||
|
||||
let front-page() = {
|
||||
align(center)[
|
||||
#{
|
||||
set text(size: 1.5em)
|
||||
std.title()
|
||||
}
|
||||
|
||||
v#version - #hash
|
||||
|
||||
#if icon-path != none {
|
||||
v(1cm)
|
||||
image(icon-path)
|
||||
}
|
||||
]
|
||||
pagebreak()
|
||||
}
|
||||
|
||||
let outlines() = {
|
||||
outline()
|
||||
pagebreak()
|
||||
outline(
|
||||
title: [List of Listings],
|
||||
target: figure.where(kind: raw),
|
||||
)
|
||||
|
||||
outline(
|
||||
title: [List of Tables],
|
||||
target: figure.where(kind: table),
|
||||
)
|
||||
}
|
||||
|
||||
let main() = {
|
||||
// Adapted from https://github.com/hei-templates/hei-synd-thesis/blob/7d2b941197babae0bf3afd4e5914754e09a64001/lib/template-thesis.typ#L242-L261
|
||||
show heading.where(level: 1): it => {
|
||||
pagebreak()
|
||||
|
||||
set text(size: 1.5em)
|
||||
set block(above: 1.2em, below: 1.2em)
|
||||
if it.numbering != none {
|
||||
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||
let prefix = num + h(1em)
|
||||
_unshift-prefix(prefix, it.body)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
show heading.where(level: 2): it => {
|
||||
if it.numbering != none {
|
||||
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||
_unshift-prefix(num + h(0.8em), it.body)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
set page(
|
||||
header: context _render-header(version, hash),
|
||||
footer: context if page.numbering != none {
|
||||
align(center, counter(page).display(page.numbering, both: true))
|
||||
},
|
||||
numbering: "1 / 1",
|
||||
)
|
||||
|
||||
show heading: set heading(numbering: "I.1.")
|
||||
counter(page).update(1)
|
||||
doc
|
||||
}
|
||||
|
||||
front-page()
|
||||
outlines()
|
||||
main()
|
||||
}
|
||||
15
examples/02_demonstration/demo.midas
Normal file
15
examples/02_demonstration/demo.midas
Normal file
@@ -0,0 +1,15 @@
|
||||
predicate in_range(min: float, max: float)(v: float) = min <= v & v <= max
|
||||
predicate is_ratio = in_range(0, 1)
|
||||
|
||||
type Currency = float
|
||||
type Price[T <: Currency] = T where _ >= 0
|
||||
|
||||
extend Price[T <: Currency] {
|
||||
def __add__: fn(Price[T], /) -> Price[T]
|
||||
}
|
||||
|
||||
type EUR = Currency
|
||||
type USD = Currency
|
||||
type CHF = Currency
|
||||
|
||||
type Discount = float where is_ratio(_)
|
||||
35
examples/02_demonstration/demo.py
Normal file
35
examples/02_demonstration/demo.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from demo_stubs import CHF, EUR, USD, Currency, Discount, Price
|
||||
|
||||
from midas.typing import cast, unsafe_cast
|
||||
|
||||
T = TypeVar("T", bound=Currency)
|
||||
|
||||
|
||||
def apply_discount(amount: Price[T], discount: Discount) -> Price[T]:
|
||||
return cast(Price[T], (1.0 - discount) * amount)
|
||||
|
||||
|
||||
a1 = cast(Price[EUR], 3.2)
|
||||
a2 = cast(Price[USD], 10.4)
|
||||
r1 = cast(Discount, 0.2)
|
||||
|
||||
print(apply_discount(a1, r1))
|
||||
print(apply_discount(a2, r1))
|
||||
|
||||
a3 = a1 + a1
|
||||
a4 = a1 + a2 # cannot add euros and dollars
|
||||
a3 = a2 # cannot change variable type
|
||||
|
||||
dyn_price = float(input("Price (CHF): "))
|
||||
dyn_discount = float(input("Discount (0.0-1.0): "))
|
||||
discounted = apply_discount(
|
||||
cast(Price[CHF], dyn_price),
|
||||
cast(Discount, dyn_discount),
|
||||
)
|
||||
|
||||
print(f"Discounted: CHF {discounted}")
|
||||
|
||||
large_data = [i * 10 for i in range(100)]
|
||||
prices = unsafe_cast(list[Price[EUR]], large_data)
|
||||
14
examples/02_demonstration/demo_stubs.pyi
Normal file
14
examples/02_demonstration/demo_stubs.pyi
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
class Currency(float): ...
|
||||
|
||||
_T0 = TypeVar("_T0", bound=Currency, covariant=True)
|
||||
|
||||
class Price(Currency, Generic[_T0]):
|
||||
def __add__(self, _0: Price[_T0], /) -> Price[_T0]: ...
|
||||
|
||||
class EUR(Currency): ...
|
||||
class USD(Currency): ...
|
||||
class CHF(Currency): ...
|
||||
class Discount(float): ...
|
||||
@@ -145,6 +145,7 @@ class LogicalExpr:
|
||||
class CastExpr:
|
||||
type: MidasType
|
||||
expr: Expr
|
||||
unsafe: bool
|
||||
|
||||
|
||||
class TernaryExpr:
|
||||
|
||||
@@ -757,9 +757,10 @@ class PythonAstPrinter(
|
||||
self._write_line("type")
|
||||
with self._child_level(single=True):
|
||||
expr.type.accept(self)
|
||||
self._write_line("expr", last=True)
|
||||
self._write_line("expr")
|
||||
with self._child_level(single=True):
|
||||
expr.expr.accept(self)
|
||||
self._write_line(f"unsafe: {expr.unsafe}", last=True)
|
||||
|
||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> None:
|
||||
self._write_line("TernaryExpr")
|
||||
|
||||
@@ -350,6 +350,7 @@ class LogicalExpr(Expr):
|
||||
class CastExpr(Expr):
|
||||
type: MidasType
|
||||
expr: Expr
|
||||
unsafe: bool
|
||||
|
||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||
return visitor.visit_cast_expr(self)
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
BUILTIN_SUBTYPES: dict[str, set[str]] = {
|
||||
"object": {"float", "list", "dict"},
|
||||
"object": {"float", "list", "dict", "str"},
|
||||
"float": {"int"},
|
||||
"int": {"bool"},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ class DiagnosticType(StrEnum):
|
||||
ERROR = "Error"
|
||||
WARNING = "Warning"
|
||||
INFO = "Info"
|
||||
DEBUG = "Debug"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
172
midas/checker/evaluator.py
Normal file
172
midas/checker/evaluator.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.checker.preamble import Preamble
|
||||
from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.reporter import FileReporter
|
||||
from midas.checker.types import Function, Predicate
|
||||
from midas.lexer.token import TokenType
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PartialPredicate(Predicate):
|
||||
scope: dict[str, Any]
|
||||
|
||||
|
||||
class Evaluator(m.Expr.Visitor[Any]):
|
||||
def __init__(self, types: TypesRegistry, reporter: Optional[FileReporter] = None):
|
||||
self.types: TypesRegistry = types
|
||||
self.reporter: Optional[FileReporter] = reporter
|
||||
self.preamble: Preamble = Preamble(self.types)
|
||||
self.scopes: list[dict[str, Any]] = [{}]
|
||||
|
||||
def evaluate(self, expr: m.Expr) -> Any:
|
||||
value: Any = expr.accept(self)
|
||||
if self.reporter is not None:
|
||||
self.reporter.debug(expr.location, f"Value: {value}")
|
||||
return value
|
||||
|
||||
def get_value(self, name: str) -> Any:
|
||||
scope: dict[str, Any] = self.scopes[-1]
|
||||
return scope[name]
|
||||
|
||||
def set_value(self, name: str, value: Any, force_declare: bool = False):
|
||||
if not force_declare:
|
||||
for scope in reversed(self.scopes):
|
||||
if name in scope:
|
||||
scope[name] = value
|
||||
return
|
||||
self.scopes[-1][name] = value
|
||||
|
||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> Any:
|
||||
def left():
|
||||
return self.evaluate(expr.left)
|
||||
|
||||
def right():
|
||||
return self.evaluate(expr.right)
|
||||
|
||||
match expr.operator.type:
|
||||
case TokenType.AND:
|
||||
return left() and right()
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
def visit_binary_expr(self, expr: m.BinaryExpr) -> Any:
|
||||
left: Any = self.evaluate(expr.left)
|
||||
right: Any = self.evaluate(expr.right)
|
||||
match expr.operator.type:
|
||||
case TokenType.MINUS:
|
||||
return left - right
|
||||
case TokenType.STAR:
|
||||
return left * right
|
||||
case TokenType.SLASH:
|
||||
return left / right
|
||||
case TokenType.GREATER:
|
||||
return left > right
|
||||
case TokenType.GREATER_EQUAL:
|
||||
return left >= right
|
||||
case TokenType.LESS:
|
||||
return left < right
|
||||
case TokenType.LESS_EQUAL:
|
||||
return left <= right
|
||||
case TokenType.EQUAL_EQUAL:
|
||||
return left == right
|
||||
case TokenType.BANG_EQUAL:
|
||||
return left != right
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
def visit_unary_expr(self, expr: m.UnaryExpr) -> Any:
|
||||
right: Any = self.evaluate(expr.right)
|
||||
match expr.operator.type:
|
||||
case TokenType.MINUS:
|
||||
return -right
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
def visit_call_expr(self, expr: m.CallExpr) -> Any:
|
||||
callee: Any = self.evaluate(expr.callee)
|
||||
args: list[Any] = [self.evaluate(arg) for arg in expr.arguments]
|
||||
kwargs: dict[str, Any] = {
|
||||
name: self.evaluate(arg) for name, arg in expr.keywords.items()
|
||||
}
|
||||
|
||||
match callee:
|
||||
case Predicate():
|
||||
return self._evaluate_predicate(callee, args, kwargs)
|
||||
case _ if callable(callee):
|
||||
return callee(*args, **kwargs)
|
||||
case _:
|
||||
return NotImplementedError
|
||||
|
||||
def visit_get_expr(self, expr: m.GetExpr) -> Any:
|
||||
obj: Any = self.evaluate(expr.expr)
|
||||
return getattr(obj, expr.name.lexeme)
|
||||
|
||||
def visit_variable_expr(self, expr: m.VariableExpr) -> Any:
|
||||
name: str = expr.name.lexeme
|
||||
for scope in reversed(self.scopes):
|
||||
if name in scope:
|
||||
return scope[name]
|
||||
|
||||
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
||||
if predicate is not None:
|
||||
if predicate.alias:
|
||||
return self.evaluate(predicate.body)
|
||||
return predicate
|
||||
|
||||
glob: Optional[Callable] = self.preamble.get_py_func(name)
|
||||
if glob is not None:
|
||||
return glob
|
||||
raise NameError(f"Unknown variable '{name}'")
|
||||
|
||||
def visit_grouping_expr(self, expr: m.GroupingExpr) -> Any:
|
||||
return self.evaluate(expr.expr)
|
||||
|
||||
def visit_literal_expr(self, expr: m.LiteralExpr) -> Any:
|
||||
return expr.value
|
||||
|
||||
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Any:
|
||||
return self.get_value("_")
|
||||
|
||||
def _evaluate_predicate(
|
||||
self, predicate: Predicate, args: list[Any], kwargs: dict[str, Any]
|
||||
) -> Any:
|
||||
res: Any = None
|
||||
if isinstance(predicate, PartialPredicate):
|
||||
self.scopes.append(predicate.scope)
|
||||
else:
|
||||
self.scopes.append({})
|
||||
match predicate.type:
|
||||
case Function(returns=Function() as inner):
|
||||
self._map_args(predicate.type, args, kwargs)
|
||||
res = PartialPredicate(
|
||||
type=inner,
|
||||
body=predicate.body,
|
||||
alias=False,
|
||||
scope=self.scopes[-1],
|
||||
)
|
||||
|
||||
case Function():
|
||||
self._map_args(predicate.type, args, kwargs)
|
||||
res = self.evaluate(predicate.body)
|
||||
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
self.scopes.pop()
|
||||
return res
|
||||
|
||||
def _map_args(self, function: Function, args: list[Any], kwargs: dict[str, Any]):
|
||||
positional: list[Function.Argument] = function.pos_args + function.args
|
||||
keywords: dict[str, Function.Argument] = {
|
||||
arg.name: arg for arg in function.args + function.kw_args
|
||||
}
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
param: Function.Argument = positional[i]
|
||||
self.set_value(param.name, arg)
|
||||
|
||||
for name, arg in kwargs.items():
|
||||
param: Function.Argument = keywords[name]
|
||||
self.set_value(param.name, arg)
|
||||
@@ -173,7 +173,7 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
||||
base_name,
|
||||
member.name.lexeme,
|
||||
member_type,
|
||||
member.kind == m.MemberKind.METHOD,
|
||||
member.kind,
|
||||
)
|
||||
|
||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from midas.checker.environment import Environment
|
||||
from midas.checker.registry import TypesRegistry
|
||||
@@ -16,16 +17,18 @@ class Preamble(Environment):
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
super().__init__()
|
||||
self._types: TypesRegistry = types
|
||||
self._python_funcs: dict[str, Callable] = {}
|
||||
|
||||
self._def_type_constructor("object")
|
||||
self._def_type_constructor("float")
|
||||
self._def_type_constructor("int")
|
||||
self._def_type_constructor("bool")
|
||||
self._def_type_constructor("str")
|
||||
self._def_type_constructor("object", object)
|
||||
self._def_type_constructor("float", float)
|
||||
self._def_type_constructor("int", int)
|
||||
self._def_type_constructor("bool", bool)
|
||||
self._def_type_constructor("str", str)
|
||||
self._def_function(
|
||||
name="list",
|
||||
pos=[Param("object", TopType())],
|
||||
returns=self._list_of(TopType()),
|
||||
py_function=list,
|
||||
)
|
||||
|
||||
# TODO: use sink
|
||||
@@ -33,6 +36,7 @@ class Preamble(Environment):
|
||||
name="print",
|
||||
pos=[Param("object", TopType())],
|
||||
returns=UnitType(),
|
||||
py_function=print,
|
||||
)
|
||||
|
||||
map_in = TypeVar(name="T", bound=None)
|
||||
@@ -52,17 +56,25 @@ class Preamble(Environment):
|
||||
),
|
||||
],
|
||||
returns=self._list_of(map_out), # TODO: replace with Iterable[U]
|
||||
type_vars=[map_in, map_out],
|
||||
py_function=map,
|
||||
)
|
||||
self._def_function(
|
||||
name="input",
|
||||
pos=[Param("prompt", TopType(), required=False)],
|
||||
returns=self._types.get_type("str"),
|
||||
)
|
||||
|
||||
def _list_of(self, item_type: Type) -> Type:
|
||||
return self._types.apply_generic(self._types.get_type("list"), [item_type])
|
||||
|
||||
def _def_type_constructor(self, name: str):
|
||||
def _def_type_constructor(self, name: str, py_function: Optional[Callable] = None):
|
||||
# TODO: more specific arg types
|
||||
self._def_function(
|
||||
name=name,
|
||||
pos=[Param("object", TopType(), required=False)],
|
||||
returns=self._types.get_type(name),
|
||||
py_function=py_function,
|
||||
)
|
||||
|
||||
def _make_function(
|
||||
@@ -109,6 +121,7 @@ class Preamble(Environment):
|
||||
kw: list[Param] = [],
|
||||
returns: Type = UnitType(),
|
||||
type_vars: list[TypeVar] = [],
|
||||
py_function: Optional[Callable] = None,
|
||||
):
|
||||
function: Type = self._make_function(
|
||||
name=name,
|
||||
@@ -119,3 +132,8 @@ class Preamble(Environment):
|
||||
type_vars=type_vars,
|
||||
)
|
||||
self.define(name, function)
|
||||
if py_function is not None:
|
||||
self._python_funcs[name] = py_function
|
||||
|
||||
def get_py_func(self, name: str) -> Optional[Callable]:
|
||||
return self._python_funcs.get(name)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import ast
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import midas.ast.python as p
|
||||
from midas.ast.location import Location
|
||||
from midas.ast.printer import MidasPrinter
|
||||
from midas.checker.environment import Environment
|
||||
from midas.checker.evaluator import Evaluator
|
||||
from midas.checker.operators import (
|
||||
PY_COMPARATOR_METHODS,
|
||||
PY_OPERATOR_METHODS,
|
||||
@@ -16,14 +18,21 @@ from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.reporter import FileReporter, Reporter
|
||||
from midas.checker.resolver import Resolver
|
||||
from midas.checker.types import (
|
||||
AliasType,
|
||||
AppliedType,
|
||||
BaseType,
|
||||
ConstraintType,
|
||||
Function,
|
||||
GenericType,
|
||||
OverloadedFunction,
|
||||
Type,
|
||||
TypeVar,
|
||||
UnitType,
|
||||
UnknownType,
|
||||
Variance,
|
||||
unfold_type,
|
||||
)
|
||||
from midas.checker.unifier import Unifier
|
||||
from midas.parser.python import PythonParser
|
||||
from midas.utils import TypedAST
|
||||
|
||||
@@ -66,6 +75,7 @@ class PythonTyper(
|
||||
self.env: Environment = self.global_env
|
||||
self.locals: dict[p.Expr, int] = {}
|
||||
self.judgements: list[tuple[p.Expr, Type]] = []
|
||||
self.evaluated_casts: list[p.CastExpr] = []
|
||||
|
||||
def process(self, source: str, path: Optional[str]) -> TypedAST:
|
||||
self.reporter = self.reporter.for_file(path)
|
||||
@@ -79,10 +89,15 @@ class PythonTyper(
|
||||
self.env = self.global_env
|
||||
self.locals = resolver.locals
|
||||
self.judgements = []
|
||||
self.evaluated_casts = []
|
||||
|
||||
self.check(stmts)
|
||||
|
||||
return TypedAST(stmts=stmts, judgements=self.judgements)
|
||||
return TypedAST(
|
||||
stmts=stmts,
|
||||
judgements=self.judgements,
|
||||
evaluated_casts=self.evaluated_casts,
|
||||
)
|
||||
|
||||
def judge(self, expr: p.Expr, type: Type):
|
||||
"""Record a typing judgement
|
||||
@@ -226,7 +241,8 @@ class PythonTyper(
|
||||
)
|
||||
pos += 1
|
||||
|
||||
for arg in pos_args + args + kw_args:
|
||||
all_args: list[Function.Argument] = pos_args + args + kw_args
|
||||
for arg in all_args:
|
||||
env.define(arg.name, arg.type)
|
||||
|
||||
returns_hint: Optional[Type] = None
|
||||
@@ -267,12 +283,25 @@ class PythonTyper(
|
||||
returns = inferred_return
|
||||
|
||||
# TODO: handle *args and **kwargs sinks
|
||||
function: Function = Function(
|
||||
function: Type = Function(
|
||||
pos_args=pos_args,
|
||||
args=args,
|
||||
kw_args=kw_args,
|
||||
returns=returns,
|
||||
)
|
||||
generic_params: list[TypeVar] = []
|
||||
all_types: list[Type] = [arg.type for arg in all_args] + [returns]
|
||||
for type in all_types:
|
||||
if isinstance(type, TypeVar):
|
||||
if type not in generic_params:
|
||||
generic_params.append(type)
|
||||
|
||||
if len(generic_params) != 0:
|
||||
function = GenericType(
|
||||
name=stmt.name,
|
||||
params=generic_params,
|
||||
body=function,
|
||||
)
|
||||
self.env.define(stmt.name, function)
|
||||
|
||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||
@@ -450,6 +479,10 @@ class PythonTyper(
|
||||
return result or UnknownType()
|
||||
|
||||
def visit_call_expr(self, expr: p.CallExpr) -> Type:
|
||||
match expr.callee:
|
||||
case p.VariableExpr(name="TypeVar"):
|
||||
return self.define_typevar(expr) or UnknownType()
|
||||
|
||||
callee: Type = self.type_of(expr.callee)
|
||||
positional: list[TypedExpr] = [
|
||||
(arg, self.type_of(arg)) for arg in expr.arguments
|
||||
@@ -515,7 +548,16 @@ class PythonTyper(
|
||||
return UnknownType()
|
||||
|
||||
def visit_cast_expr(self, expr: p.CastExpr) -> Type:
|
||||
return self.resolve_type_expr(expr.type)
|
||||
subject_type: Type = self.type_of(expr.expr)
|
||||
target_type: Type = self.resolve_type_expr(expr.type)
|
||||
is_lit, lit_value = self._get_literal(expr.expr)
|
||||
if is_lit:
|
||||
evaluated: bool = self._evaluate_cast_statically(
|
||||
expr, subject_type, target_type, lit_value
|
||||
)
|
||||
if evaluated:
|
||||
self.evaluated_casts.append(expr)
|
||||
return target_type
|
||||
|
||||
def visit_ternary_expr(self, expr: p.TernaryExpr) -> Type:
|
||||
test_type: Type = self.type_of(expr.test)
|
||||
@@ -698,9 +740,39 @@ class PythonTyper(
|
||||
case UnknownType():
|
||||
return UnknownType()
|
||||
|
||||
case AliasType(type=base):
|
||||
return self._get_call_result(
|
||||
location, base, positional, keywords, report_errors
|
||||
)
|
||||
|
||||
case GenericType():
|
||||
unifier: Unifier = Unifier(self.types)
|
||||
pos: list[Type] = [a[1] for a in positional]
|
||||
kw: dict[str, Type] = {k: v[1] for k, v in keywords.items()}
|
||||
unified: Optional[Type] = unifier.unify_call(callee, pos, kw)
|
||||
if unified is None:
|
||||
if report_errors:
|
||||
pos_str: str = ", ".join(str(t) for t in pos)
|
||||
kw_str: str = ", ".join(f"{k}: {v}" for k, v in kw.items())
|
||||
self.reporter.error(
|
||||
location,
|
||||
f"Could not unify {callee}={callee.body} with pos=[{pos_str}] and kw={{{kw_str}}}",
|
||||
)
|
||||
return None
|
||||
return self._get_call_result(
|
||||
location,
|
||||
unified,
|
||||
positional,
|
||||
keywords,
|
||||
report_errors,
|
||||
)
|
||||
|
||||
case _:
|
||||
if report_errors:
|
||||
self.reporter.error(location, f"{callee} is not callable")
|
||||
self.reporter.error(
|
||||
location,
|
||||
f"{callee} ({callee.__class__.__name__}) is not callable",
|
||||
)
|
||||
return None
|
||||
|
||||
def _are_arguments_valid(
|
||||
@@ -1000,3 +1072,147 @@ class PythonTyper(
|
||||
report_errors=False,
|
||||
)
|
||||
return result
|
||||
|
||||
def define_typevar(self, call: p.CallExpr) -> Optional[TypeVar]:
|
||||
def is_kw_true(name: str) -> bool:
|
||||
match call.keywords.get(name):
|
||||
case p.LiteralExpr(value=True):
|
||||
return True
|
||||
case _:
|
||||
return False
|
||||
|
||||
match call:
|
||||
case p.CallExpr(
|
||||
arguments=[p.LiteralExpr(value=str() as name)],
|
||||
):
|
||||
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)
|
||||
|
||||
if is_kw_true("covariant"):
|
||||
variance = Variance.COVARIANT
|
||||
|
||||
if is_kw_true("contravariant"):
|
||||
if variance == Variance.COVARIANT:
|
||||
self.reporter.warning(
|
||||
call.keywords["contravariant"].location,
|
||||
"TypeVar cannot be covariant and contravariant at the same time. Marked as invariant",
|
||||
)
|
||||
variance = Variance.INVARIANT
|
||||
else:
|
||||
variance = Variance.CONTRAVARIANT
|
||||
var: TypeVar = TypeVar(name=name, bound=bound, variance=variance)
|
||||
self.types.define_type(name, var)
|
||||
return var
|
||||
|
||||
case _:
|
||||
self.reporter.warning(
|
||||
call.location, "Invalid usage of 'TypeVar', skipping"
|
||||
)
|
||||
return None
|
||||
|
||||
def _parse_type_from_expr(self, expr: p.Expr) -> p.MidasType:
|
||||
location: Location = expr.location
|
||||
parser = PythonParser()
|
||||
match expr:
|
||||
case p.LiteralExpr(value=str() as value):
|
||||
node: ast.Expression = ast.parse(value, mode="eval")
|
||||
return parser._parse_type(node.body)
|
||||
case p.VariableExpr(name=name):
|
||||
return p.BaseType(location=location, base=name, param=None)
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_literal(self, expr: p.Expr) -> tuple[bool, Any]:
|
||||
match expr:
|
||||
case p.LiteralExpr(value=value):
|
||||
return True, value
|
||||
|
||||
case p.ListExpr(items=items):
|
||||
values: list[Any] = []
|
||||
for item in items:
|
||||
is_lit, value = self._get_literal(item)
|
||||
if not is_lit:
|
||||
return False, None
|
||||
values.append(value)
|
||||
return True, values
|
||||
|
||||
case p.DictExpr(keys=keys, values=values):
|
||||
pairs: list[tuple[Any, Any]] = []
|
||||
for key, value in zip(keys, values):
|
||||
key_val = None
|
||||
if key is not None:
|
||||
is_lit, key_val = self._get_literal(key)
|
||||
if not is_lit:
|
||||
return False, None
|
||||
|
||||
is_lit, value_val = self._get_literal(value)
|
||||
if not is_lit:
|
||||
return False, None
|
||||
|
||||
if key is None:
|
||||
# TODO: check that value is always a dict
|
||||
assert isinstance(value_val, dict)
|
||||
pairs.extend(value_val.items())
|
||||
else:
|
||||
pairs.append((key_val, value_val))
|
||||
return True, dict(pairs)
|
||||
|
||||
case _:
|
||||
return False, None
|
||||
|
||||
def _evaluate_cast_statically(
|
||||
self, expr: p.CastExpr, subject_type: Type, target_type: Type, lit_value: Any
|
||||
) -> bool:
|
||||
match target_type:
|
||||
case AliasType(type=base):
|
||||
return self._evaluate_cast_statically(
|
||||
expr, subject_type, base, lit_value
|
||||
)
|
||||
|
||||
case AppliedType(body=body):
|
||||
return self._evaluate_cast_statically(
|
||||
expr, subject_type, body, lit_value
|
||||
)
|
||||
|
||||
case ConstraintType(type=base, constraint=constraint):
|
||||
evaluated: bool = True
|
||||
if not self._evaluate_cast_statically(
|
||||
expr, subject_type, base, lit_value
|
||||
):
|
||||
evaluated = False
|
||||
|
||||
evaluator = Evaluator(self.types)
|
||||
evaluator.set_value("_", lit_value)
|
||||
res = evaluator.evaluate(constraint)
|
||||
if not res:
|
||||
printer = MidasPrinter()
|
||||
constraint_str: str = printer.print(constraint)
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Value {lit_value!r} does not fit constraint '{constraint_str}'",
|
||||
)
|
||||
evaluated = False
|
||||
return evaluated
|
||||
|
||||
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):
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Value {lit_value!r} of type {subject_type} cannot be cast as {target_type}",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
case _:
|
||||
self.reporter.info(
|
||||
expr.location, f"Cannot evaluate cast to {target_type} statically"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from midas.ast.midas import MemberKind
|
||||
from midas.checker.builtins import BUILTIN_SUBTYPES
|
||||
from midas.checker.types import (
|
||||
AliasType,
|
||||
@@ -22,11 +24,17 @@ from midas.checker.types import (
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Member:
|
||||
kind: MemberKind
|
||||
type: Type
|
||||
|
||||
|
||||
class TypesRegistry:
|
||||
def __init__(self) -> None:
|
||||
self.logger: logging.Logger = logging.getLogger("TypesRegistry")
|
||||
self._types: dict[str, Type] = {}
|
||||
self._members: dict[str, dict[str, Type]] = {}
|
||||
self._members: dict[str, dict[str, Member]] = {}
|
||||
self._predicates: dict[str, Predicate] = {}
|
||||
|
||||
def get_type(self, name: str) -> Type:
|
||||
@@ -64,26 +72,38 @@ class TypesRegistry:
|
||||
return type
|
||||
|
||||
def define_member(
|
||||
self, type_name: str, member_name: str, member_type: Type, is_method: bool
|
||||
self,
|
||||
type_name: str,
|
||||
member_name: str,
|
||||
member_type: Type,
|
||||
kind: MemberKind,
|
||||
):
|
||||
members: dict[str, Type] = self._members.setdefault(type_name, {})
|
||||
members: dict[str, Member] = self._members.setdefault(type_name, {})
|
||||
if member_name in members:
|
||||
if not is_method:
|
||||
current: Member = members[member_name]
|
||||
if current.kind != kind:
|
||||
self.logger.error(
|
||||
f"Member '{member_name}' already defined for type {type_name}"
|
||||
f"Member '{member_name}' is already defined as a {current.kind},"
|
||||
+ f" cannot define a {kind} with the same name"
|
||||
)
|
||||
return
|
||||
current: Type = members[member_name]
|
||||
if kind != MemberKind.METHOD:
|
||||
self.logger.error(
|
||||
f"Member '{member_name}' already defined for type {type_name},"
|
||||
+ " only methods can be overloaded"
|
||||
)
|
||||
return
|
||||
|
||||
combined: Type
|
||||
match current:
|
||||
match current.type:
|
||||
case OverloadedFunction(overloads=overloads):
|
||||
combined = OverloadedFunction(overloads=overloads + [member_type])
|
||||
case _:
|
||||
combined = OverloadedFunction(overloads=[current, member_type])
|
||||
members[member_name] = combined
|
||||
combined = OverloadedFunction(overloads=[current.type, member_type])
|
||||
members[member_name] = Member(kind=current.kind, type=combined)
|
||||
|
||||
else:
|
||||
members[member_name] = member_type
|
||||
members[member_name] = Member(kind=kind, type=member_type)
|
||||
|
||||
def define_predicate(self, name: str, predicate: Predicate):
|
||||
if name in self._predicates:
|
||||
@@ -110,6 +130,19 @@ class TypesRegistry:
|
||||
case (_, TopType()):
|
||||
return True
|
||||
|
||||
case (_, UnknownType()):
|
||||
return True
|
||||
|
||||
case (TypeVar(bound=bound), _):
|
||||
if bound is None:
|
||||
return False
|
||||
return self.is_subtype(bound, type2)
|
||||
|
||||
case (_, TypeVar(bound=bound)):
|
||||
if bound is None:
|
||||
return True
|
||||
return self.is_subtype(type1, bound)
|
||||
|
||||
case (AliasType(type=base1), _):
|
||||
return self.is_subtype(base1, type2)
|
||||
|
||||
@@ -127,11 +160,6 @@ class TypesRegistry:
|
||||
case (Function(), Function()):
|
||||
return self.is_func_subtype(type1, type2)
|
||||
|
||||
case (TypeVar(bound=bound), _):
|
||||
if bound is None:
|
||||
return False
|
||||
return self.is_subtype(bound, type2)
|
||||
|
||||
case (ConstraintType(type=base1), _):
|
||||
return self.is_subtype(base1, type2)
|
||||
|
||||
@@ -153,6 +181,10 @@ class TypesRegistry:
|
||||
return False
|
||||
return True
|
||||
|
||||
# TODO: verify legitimacy
|
||||
case (AppliedType(body=body), _):
|
||||
return self.is_subtype(body, type2)
|
||||
|
||||
return False
|
||||
|
||||
# TODO: verify the logic in here
|
||||
@@ -327,13 +359,13 @@ class TypesRegistry:
|
||||
case BaseType(name=name):
|
||||
if name in self._members:
|
||||
if member_name in self._members[name]:
|
||||
return self._members[name][member_name]
|
||||
return self._members[name][member_name].type
|
||||
return None
|
||||
|
||||
case AliasType(name=name, type=base):
|
||||
if name in self._members:
|
||||
if member_name in self._members[name]:
|
||||
return self._members[name][member_name]
|
||||
return self._members[name][member_name].type
|
||||
return self.lookup_member(base, member_name)
|
||||
|
||||
case AppliedType(name=name, body=body, args=args):
|
||||
@@ -347,7 +379,7 @@ class TypesRegistry:
|
||||
}
|
||||
if name in self._members:
|
||||
if member_name in self._members[name]:
|
||||
member_type: Type = self._members[name][member_name]
|
||||
member_type: Type = self._members[name][member_name].type
|
||||
return substitute_typevars(member_type, substitutions)
|
||||
|
||||
member_type2: Optional[Type] = self.lookup_member(body, member_name)
|
||||
@@ -369,6 +401,12 @@ class TypesRegistry:
|
||||
)
|
||||
return self.lookup_member(base, member_name)
|
||||
|
||||
case ConstraintType(type=base):
|
||||
return self.lookup_member(base, member_name)
|
||||
|
||||
case TypeVar(bound=bound) if bound is not None:
|
||||
return self.lookup_member(bound, member_name)
|
||||
|
||||
case UnknownType():
|
||||
return UnknownType()
|
||||
|
||||
|
||||
@@ -61,3 +61,10 @@ class FileReporter:
|
||||
location=location,
|
||||
message=message,
|
||||
)
|
||||
|
||||
def debug(self, location: Location, message: str):
|
||||
self.report(
|
||||
type=DiagnosticType.DEBUG,
|
||||
location=location,
|
||||
message=message,
|
||||
)
|
||||
|
||||
@@ -166,6 +166,9 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
||||
)
|
||||
|
||||
match type:
|
||||
case TopType():
|
||||
return type
|
||||
|
||||
case BaseType(name=name) if name in substitutions:
|
||||
return substitutions[name]
|
||||
|
||||
@@ -232,6 +235,21 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
|
||||
return substitutions[name]
|
||||
raise ValueError(f"Missing TypeVar substitution for {name}")
|
||||
|
||||
case GenericType(name=name, params=params, body=body):
|
||||
params2: list[TypeVar] = []
|
||||
for param in params:
|
||||
param2: Type = substitute_typevars(param, substitutions)
|
||||
if not isinstance(param2, TypeVar):
|
||||
raise ValueError(
|
||||
f"Invalid type parameter substitution, expected TypeVar, got {param2}"
|
||||
)
|
||||
params2.append(param2)
|
||||
return GenericType(
|
||||
name=name,
|
||||
params=params2,
|
||||
body=substitute_typevars(body, substitutions),
|
||||
)
|
||||
|
||||
case UnknownType() | UnitType():
|
||||
return type
|
||||
|
||||
|
||||
169
midas/checker/unifier.py
Normal file
169
midas/checker/unifier.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.types import (
|
||||
AppliedType,
|
||||
Function,
|
||||
GenericType,
|
||||
TopType,
|
||||
Type,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
|
||||
class UnificationError(Exception): ...
|
||||
|
||||
|
||||
class Unifier:
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
self.types: TypesRegistry = types
|
||||
self.logger: logging.Logger = logging.getLogger("Unifier")
|
||||
|
||||
def unify_call(
|
||||
self,
|
||||
type: GenericType,
|
||||
positional: list[Type],
|
||||
keywords: dict[str, Type],
|
||||
) -> Optional[Type]:
|
||||
concrete_func: Function = Function(
|
||||
pos_args=[
|
||||
Function.Argument(
|
||||
pos=i,
|
||||
name=str(i),
|
||||
type=arg,
|
||||
required=True,
|
||||
)
|
||||
for i, arg in enumerate(positional)
|
||||
],
|
||||
args=[],
|
||||
kw_args=[
|
||||
Function.Argument(
|
||||
pos=len(positional) + i,
|
||||
name=name,
|
||||
type=arg,
|
||||
required=True,
|
||||
)
|
||||
for i, (name, arg) in enumerate(keywords.items())
|
||||
],
|
||||
returns=TopType(), # TODO: use expected type
|
||||
)
|
||||
return self.unify_generic(type, concrete_func, match_return=False)
|
||||
|
||||
def unify_generic(
|
||||
self,
|
||||
template: GenericType,
|
||||
concrete: Type,
|
||||
match_return: bool = True,
|
||||
) -> Optional[Type]:
|
||||
substitutions: dict[str, Type]
|
||||
try:
|
||||
substitutions = self.match(template.body, concrete, match_return)
|
||||
except UnificationError:
|
||||
return None
|
||||
|
||||
args: list[Type] = []
|
||||
for param in template.params:
|
||||
if param.name not in substitutions:
|
||||
return None
|
||||
args.append(substitutions[param.name])
|
||||
|
||||
applied: Type = self.types.apply_generic(template, args)
|
||||
return applied
|
||||
|
||||
def match(
|
||||
self,
|
||||
template: Type,
|
||||
concrete: Type,
|
||||
match_return: bool = True,
|
||||
) -> dict[str, Type]:
|
||||
# TODO: if concrete is Generic, record bound TypeVar. Then when merging
|
||||
# substitutions, check that the constraint is respected
|
||||
match (template, concrete):
|
||||
case (TypeVar(name=name), _):
|
||||
return {name: concrete}
|
||||
|
||||
case (
|
||||
AppliedType(name=template_name, args=template_args),
|
||||
AppliedType(name=concrete_name, args=concrete_args),
|
||||
) if template_name == concrete_name and len(template_args) == len(
|
||||
concrete_args
|
||||
):
|
||||
substitutions: dict[str, Type] = {}
|
||||
for template_arg, concrete_arg in zip(template_args, concrete_args):
|
||||
new_substistutions: dict[str, Type] = self.match(
|
||||
template_arg, concrete_arg
|
||||
)
|
||||
substitutions = self.merge(substitutions, new_substistutions)
|
||||
|
||||
return substitutions
|
||||
|
||||
case (Function(), Function()):
|
||||
mapped: list[tuple[Function.Argument, Function.Argument]] = (
|
||||
self.map_params(template, concrete)
|
||||
)
|
||||
substitutions: dict[str, Type] = {}
|
||||
for template_arg, concrete_arg in mapped:
|
||||
arg_subs: dict[str, Type] = self.match(
|
||||
template_arg.type, concrete_arg.type
|
||||
)
|
||||
substitutions = self.merge(substitutions, arg_subs)
|
||||
|
||||
if match_return:
|
||||
return_subs: dict[str, Type] = self.match(
|
||||
template.returns, concrete.returns
|
||||
)
|
||||
substitutions = self.merge(substitutions, return_subs)
|
||||
|
||||
return substitutions
|
||||
|
||||
case _:
|
||||
self.logger.debug(f"Can't match {concrete!r} with {template!r}")
|
||||
return {}
|
||||
|
||||
def merge(self, subs1: dict[str, Type], subs2: dict[str, Type]) -> dict[str, Type]:
|
||||
merged: dict[str, Type] = subs1.copy()
|
||||
|
||||
for k, v in subs2.items():
|
||||
if k in merged and merged[k] != v:
|
||||
self.logger.debug(
|
||||
f"Substitution already defined for {k} with type {merged[k]}, got {v}"
|
||||
)
|
||||
raise UnificationError
|
||||
merged[k] = v
|
||||
return merged
|
||||
|
||||
def map_params(
|
||||
self, func1: Function, func2: Function
|
||||
) -> list[tuple[Function.Argument, Function.Argument]]:
|
||||
pos1: list[Function.Argument] = func1.pos_args
|
||||
mixed1: list[Function.Argument] = func1.args
|
||||
kw1: list[Function.Argument] = func1.kw_args
|
||||
|
||||
pos2: list[Function.Argument] = func2.pos_args
|
||||
mixed2: list[Function.Argument] = func2.args
|
||||
kw2: list[Function.Argument] = func2.kw_args
|
||||
|
||||
mapped: list[tuple[Function.Argument, Function.Argument]] = []
|
||||
|
||||
by_pos2: dict[int, Function.Argument] = {arg.pos: arg for arg in pos2 + mixed2}
|
||||
by_name2: dict[str, Function.Argument] = {arg.name: arg for arg in mixed2 + kw2}
|
||||
|
||||
for arg1 in pos1:
|
||||
if (arg2 := by_pos2.get(arg1.pos)) is not None:
|
||||
mapped.append((arg1, arg2))
|
||||
|
||||
for arg1 in mixed1:
|
||||
# Match both positionally and by name, conflicts are caught
|
||||
# when merging substitutions
|
||||
if (arg2 := by_pos2.get(arg1.pos)) is not None:
|
||||
mapped.append((arg1, arg2))
|
||||
|
||||
if (arg2 := by_name2.get(arg1.name)) is not None:
|
||||
mapped.append((arg1, arg2))
|
||||
|
||||
for arg1 in kw1:
|
||||
if (arg2 := by_name2.get(arg1.name)) is not None:
|
||||
mapped.append((arg1, arg2))
|
||||
|
||||
return mapped
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.registry import Member, TypesRegistry
|
||||
from midas.checker.types import (
|
||||
AppliedType,
|
||||
ConstraintType,
|
||||
@@ -54,9 +54,9 @@ class VarianceInferrer:
|
||||
self.tracker = Tracker(type.params)
|
||||
|
||||
self.walk(type.body, 1, type.name)
|
||||
members: dict[str, Type] = self.types._members.get(type.name, {})
|
||||
members: dict[str, Member] = self.types._members.get(type.name, {})
|
||||
for name, member in members.items():
|
||||
self.walk(member, 1, type.name, [f"member:'{name}'"])
|
||||
self.walk(member.type, 1, type.name, [f"member:'{name}'"])
|
||||
|
||||
return GenericType(
|
||||
name=type.name,
|
||||
|
||||
@@ -4,5 +4,6 @@ from .format import format as format
|
||||
from .highlight import highlight as highlight
|
||||
from .parse import parse as parse
|
||||
from .registry import dump_registry as dump_registry
|
||||
from .stubs import stubs as stubs
|
||||
from .types import types as types
|
||||
from .validate import validate as validate
|
||||
|
||||
@@ -19,9 +19,11 @@ from midas.utils import TypedAST
|
||||
@click.command(help="Compile source")
|
||||
@click.argument("file", type=click.File("r"))
|
||||
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||
@click.option("--ignore-errors", is_flag=True)
|
||||
def compile(
|
||||
file: TextIO,
|
||||
types: tuple[TextIO],
|
||||
ignore_errors: bool,
|
||||
):
|
||||
source: str = file.read()
|
||||
source_path: Path = Path(file.name).resolve()
|
||||
@@ -35,7 +37,9 @@ def compile(
|
||||
printer = DiagnosticPrinter()
|
||||
printer.print_all(diagnostics)
|
||||
|
||||
if any(map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)):
|
||||
if not ignore_errors and any(
|
||||
map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)
|
||||
):
|
||||
sys.exit(1)
|
||||
|
||||
generator = Generator(workdir=source_path.parent, types=checker.types)
|
||||
|
||||
@@ -10,6 +10,7 @@ import click
|
||||
|
||||
from midas.ast.printer import MidasPrinter
|
||||
from midas.checker.checker import TypeChecker
|
||||
from midas.checker.registry import Member
|
||||
from midas.checker.types import AliasType, AppliedType, BaseType, GenericType, Type
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ def dump_registry(
|
||||
|
||||
print("##### Types #####")
|
||||
for name, type in checker.types._types.items():
|
||||
members: dict[str, Type] = checker.types._members.get(name, {})
|
||||
members: dict[str, Member] = checker.types._members.get(name, {})
|
||||
params: str = ""
|
||||
if isinstance(type, GenericType):
|
||||
params = ", ".join(map(str, type.params))
|
||||
@@ -46,8 +47,9 @@ def dump_registry(
|
||||
print(f"{name}{params} = {base_type(type)}")
|
||||
if len(members) != 0:
|
||||
print(" " * 4 + "Members:")
|
||||
for member_name, member_type in members.items():
|
||||
print(" " * 8 + f"{member_name}: {member_type}")
|
||||
for member_name, member in members.items():
|
||||
kind: str = member.kind.name
|
||||
print(" " * 8 + f"({kind:8}) {member_name}: {member.type}")
|
||||
|
||||
print("##### Predicates #####")
|
||||
printer = MidasPrinter()
|
||||
|
||||
64
midas/cli/commands/stubs.py
Normal file
64
midas/cli/commands/stubs.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import ast
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
import black
|
||||
import click
|
||||
from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from midas.checker.checker import TypeChecker
|
||||
from midas.generator.stubs import StubsGenerator
|
||||
|
||||
|
||||
def generate_stubs(in_path: Path, out_path: Path):
|
||||
checker = TypeChecker()
|
||||
checker.import_midas(in_path)
|
||||
|
||||
generator = StubsGenerator(checker.types)
|
||||
module: ast.Module = generator.generate_stubs()
|
||||
module = ast.fix_missing_locations(module)
|
||||
|
||||
output: str = ast.unparse(module)
|
||||
output = black.format_str(output, mode=black.Mode(is_pyi=True))
|
||||
|
||||
out_path.write_text(output)
|
||||
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
def __init__(self, in_path: Path, out_path: Path) -> None:
|
||||
super().__init__()
|
||||
self.in_path: Path = in_path
|
||||
self.out_path: Path = out_path
|
||||
|
||||
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
|
||||
generate_stubs(self.in_path, self.out_path)
|
||||
|
||||
|
||||
@click.command(help="Generate stubs from Midas definitions")
|
||||
@click.argument("file", type=click.File("r"))
|
||||
@click.option("-o", "--output", type=click.File("w"), default="-")
|
||||
@click.option("-w", "--watch", is_flag=True)
|
||||
def stubs(
|
||||
file: TextIO,
|
||||
output: TextIO,
|
||||
watch: bool,
|
||||
):
|
||||
source_path: Path = Path(file.name).resolve()
|
||||
out_path: Path = Path(output.name).resolve()
|
||||
generate_stubs(source_path, out_path)
|
||||
|
||||
if watch:
|
||||
print(f"Watching {source_path}...")
|
||||
print("Press CTRL+C to stop")
|
||||
handler = Handler(source_path, out_path)
|
||||
observer = Observer()
|
||||
observer.schedule(handler, str(source_path))
|
||||
observer.start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
@@ -41,6 +41,7 @@ def types(
|
||||
message=f"Type: {type}",
|
||||
)
|
||||
)
|
||||
diagnostics.extend(checker.diagnostics)
|
||||
printer = DiagnosticPrinter()
|
||||
printer.print_all(diagnostics)
|
||||
|
||||
|
||||
@@ -228,6 +228,13 @@ class PythonHighlighter(
|
||||
for item in expr.items:
|
||||
item.accept(self)
|
||||
|
||||
def visit_dict_expr(self, expr: p.DictExpr) -> None:
|
||||
for key in expr.keys:
|
||||
if key is not None:
|
||||
key.accept(self)
|
||||
for value in expr.values:
|
||||
value.accept(self)
|
||||
|
||||
def visit_subscript_expr(self, expr: p.SubscriptExpr) -> None:
|
||||
expr.object.accept(self)
|
||||
expr.index.accept(self)
|
||||
@@ -240,6 +247,10 @@ class PythonHighlighter(
|
||||
if expr.step is not None:
|
||||
expr.step.accept(self)
|
||||
|
||||
def visit_raw_expr(self, expr: p.RawExpr) -> None: ...
|
||||
|
||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None: ...
|
||||
|
||||
|
||||
class MidasHighlighter(
|
||||
Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None]
|
||||
@@ -266,8 +277,9 @@ class MidasHighlighter(
|
||||
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
|
||||
self.wrap(stmt, "predicate")
|
||||
self.wrap(LocatableToken(stmt.name), "predicate-name")
|
||||
stmt.type.accept(self)
|
||||
stmt.condition.accept(self)
|
||||
for spec in stmt.params:
|
||||
self._visit_param_spec(spec)
|
||||
stmt.body.accept(self)
|
||||
|
||||
def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
|
||||
self.wrap(expr, "logical-expr")
|
||||
@@ -283,6 +295,14 @@ class MidasHighlighter(
|
||||
self.wrap(expr, "unary-expr")
|
||||
expr.right.accept(self)
|
||||
|
||||
def visit_call_expr(self, expr: m.CallExpr) -> None:
|
||||
self.wrap(expr, "call-expr")
|
||||
expr.callee.accept(self)
|
||||
for arg in expr.arguments:
|
||||
arg.accept(self)
|
||||
for arg in expr.keywords.values():
|
||||
arg.accept(self)
|
||||
|
||||
def visit_get_expr(self, expr: m.GetExpr) -> None:
|
||||
self.wrap(expr, "get-expr")
|
||||
expr.expr.accept(self)
|
||||
@@ -318,8 +338,7 @@ class MidasHighlighter(
|
||||
|
||||
def visit_function_type(self, type: m.FunctionType) -> None:
|
||||
self.wrap(type, "function")
|
||||
for arg in type.pos_args + type.args + type.kw_args:
|
||||
arg.type.accept(self)
|
||||
self._visit_param_spec(type.params)
|
||||
type.returns.accept(self)
|
||||
|
||||
def visit_extension_type(self, type: m.ExtensionType) -> None:
|
||||
@@ -327,6 +346,10 @@ class MidasHighlighter(
|
||||
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)
|
||||
|
||||
|
||||
class DiagnosticsHighlighter(Highlighter):
|
||||
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_diagnostic.css"
|
||||
|
||||
@@ -18,6 +18,7 @@ midas.add_command(commands.highlight)
|
||||
midas.add_command(commands.parse)
|
||||
midas.add_command(commands.dump_registry)
|
||||
midas.add_command(commands.types)
|
||||
midas.add_command(commands.stubs)
|
||||
midas.add_command(commands.validate)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -7,6 +8,13 @@ from midas.cli.ansi import Ansi
|
||||
|
||||
|
||||
class DiagnosticPrinter:
|
||||
COLORS: dict[DiagnosticType, int] = {
|
||||
DiagnosticType.ERROR: Ansi.RED,
|
||||
DiagnosticType.WARNING: Ansi.YELLOW,
|
||||
DiagnosticType.INFO: Ansi.CYAN,
|
||||
DiagnosticType.DEBUG: Ansi.MAGENTA,
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.files: dict[Optional[str], list[str]] = {}
|
||||
|
||||
@@ -22,10 +30,25 @@ class DiagnosticPrinter:
|
||||
return self.files[filename]
|
||||
|
||||
def print_all(self, diagnostics: list[Diagnostic], indent: int = 4):
|
||||
by_type: dict[DiagnosticType, int] = defaultdict(int)
|
||||
for diagnostic in diagnostics:
|
||||
filename: Optional[str] = diagnostic.file_path
|
||||
lines = self.get_lines(filename)
|
||||
self.print(lines, diagnostic, indent=indent)
|
||||
by_type[diagnostic.type] += 1
|
||||
|
||||
if len(diagnostics) == 0:
|
||||
return
|
||||
|
||||
counts: list[str] = []
|
||||
for type in DiagnosticType:
|
||||
if type not in by_type:
|
||||
continue
|
||||
count: int = by_type[type]
|
||||
color: int = self.COLORS.get(type, Ansi.WHITE)
|
||||
counts.append(f"{Ansi.FG(color)}{type.value}s{Ansi.RESET}: {count}")
|
||||
|
||||
print(" ".join(counts))
|
||||
|
||||
def print(self, lines: list[str], diagnostic: Diagnostic, indent: int = 4):
|
||||
"""Pretty-print a diagnostic, showing some context if possible
|
||||
@@ -55,11 +78,7 @@ class DiagnosticPrinter:
|
||||
before: str = line[:start_offset]
|
||||
after: str = line[end_offset:]
|
||||
|
||||
color: int = {
|
||||
DiagnosticType.ERROR: Ansi.RED,
|
||||
DiagnosticType.WARNING: Ansi.YELLOW,
|
||||
DiagnosticType.INFO: Ansi.CYAN,
|
||||
}.get(diagnostic.type, Ansi.WHITE)
|
||||
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||
|
||||
subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET
|
||||
cursor: str = (
|
||||
|
||||
@@ -44,6 +44,7 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
self._typed_ast: TypedAST = TypedAST(
|
||||
stmts=[],
|
||||
judgements=[],
|
||||
evaluated_casts=[],
|
||||
)
|
||||
self._alias_count: int = 0
|
||||
self._predicate_count: int = 0
|
||||
@@ -131,6 +132,10 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
|
||||
def visit_cast_expr(self, expr: p.CastExpr) -> ast.expr:
|
||||
expr2: ast.expr = expr.expr.accept(self)
|
||||
|
||||
if expr in self._typed_ast.evaluated_casts or expr.unsafe:
|
||||
return expr2
|
||||
|
||||
alias: ast.expr = self._make_alias(expr2)
|
||||
|
||||
type: Type = self._get_expr_type(expr)
|
||||
@@ -322,8 +327,10 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
|
||||
self._make_cast_asserts(src_location, expr, base)
|
||||
self._make_constraint_assert(src_location, expr, constraint)
|
||||
|
||||
case TypeVar():
|
||||
raise RuntimeError("Unexpected TypeVar")
|
||||
case TypeVar(bound=bound):
|
||||
# TODO: check with type from arguments / use call-site context
|
||||
if bound is not None:
|
||||
self._make_cast_asserts(src_location, expr, bound)
|
||||
|
||||
case (
|
||||
TopType()
|
||||
|
||||
386
midas/generator/stubs.py
Normal file
386
midas/generator/stubs.py
Normal file
@@ -0,0 +1,386 @@
|
||||
import ast
|
||||
from typing import Optional, assert_never
|
||||
|
||||
import midas.ast.midas as m
|
||||
from midas.checker.registry import Member, TypesRegistry
|
||||
from midas.checker.types import (
|
||||
AliasType,
|
||||
AppliedType,
|
||||
BaseType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
ExtensionType,
|
||||
Function,
|
||||
GenericType,
|
||||
OverloadedFunction,
|
||||
TopType,
|
||||
Type,
|
||||
TypeVar,
|
||||
UnitType,
|
||||
UnknownType,
|
||||
Variance,
|
||||
substitute_typevars,
|
||||
)
|
||||
|
||||
Empty = ast.Constant(value=...)
|
||||
|
||||
|
||||
class StubsGenerator:
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
self.types: TypesRegistry = types
|
||||
self.stubs: list[ast.stmt] = []
|
||||
self.typing_imports: set[str] = set()
|
||||
self.protocol_idx: int = 0
|
||||
self.stub_idx: int = 0
|
||||
self.type_var_idx: int = 0
|
||||
self.substitutions: dict[str, dict[str, Type]] = {}
|
||||
|
||||
def generate_stubs(self) -> ast.Module:
|
||||
self.stubs = []
|
||||
self.typing_imports = set()
|
||||
for name, type in self.types._types.items():
|
||||
# Skip builtin types, not just based on name so the user can override
|
||||
# TODO: check if added members on builtin type
|
||||
match type:
|
||||
case BaseType(name=name_) if name == name_:
|
||||
continue
|
||||
case GenericType(
|
||||
name=name1,
|
||||
body=BaseType(name=name2),
|
||||
) if (
|
||||
name == name1 == name2
|
||||
):
|
||||
continue
|
||||
self.generate_stub(name, type)
|
||||
|
||||
imports = [
|
||||
ast.ImportFrom(
|
||||
module="__future__",
|
||||
names=[ast.alias(name="annotations")],
|
||||
level=0,
|
||||
)
|
||||
]
|
||||
if len(self.typing_imports) != 0:
|
||||
imports.append(
|
||||
ast.ImportFrom(
|
||||
module="typing",
|
||||
names=[
|
||||
ast.alias(name=name) for name in sorted(self.typing_imports)
|
||||
],
|
||||
level=0,
|
||||
)
|
||||
)
|
||||
return ast.Module(body=imports + self.stubs, type_ignores=[])
|
||||
|
||||
def generate_stub(self, name: str, type: Type):
|
||||
base_type: Type = type
|
||||
|
||||
members: dict[str, Member] = self.types._members.get(name, {})
|
||||
if isinstance(base_type, (BaseType, TopType, UnitType)) and len(members) == 0:
|
||||
return
|
||||
|
||||
bases: list[ast.expr] = []
|
||||
substitutions: dict[str, Type] = {}
|
||||
bases, substitutions = self.get_bases(type)
|
||||
self.substitutions[name] = substitutions
|
||||
|
||||
body = self.generate_body(members, substitutions)
|
||||
stub = ast.ClassDef(
|
||||
name=name,
|
||||
bases=bases,
|
||||
body=body,
|
||||
keywords=[],
|
||||
decorator_list=[],
|
||||
)
|
||||
self.add_stub(stub)
|
||||
|
||||
def get_bases(self, type: Type) -> tuple[list[ast.expr], dict[str, Type]]:
|
||||
match type:
|
||||
case AliasType(type=base):
|
||||
return [self.dump_type(base)], {}
|
||||
|
||||
case GenericType(params=params, body=body):
|
||||
self.add_typing_import("Generic")
|
||||
type_vars: ast.expr
|
||||
|
||||
params2: list[TypeVar] = self.define_type_vars(params)
|
||||
if len(params) == 1:
|
||||
type_vars = ast.Name(id=params2[0].name)
|
||||
else:
|
||||
type_vars = ast.Tuple(
|
||||
elts=[ast.Name(id=param.name) for param in params2]
|
||||
)
|
||||
|
||||
substitutions: dict[str, TypeVar] = {
|
||||
param.name: param2 for param, param2 in zip(params, params2)
|
||||
}
|
||||
|
||||
body_bases, body_subsitutions = self.get_bases(body)
|
||||
return (
|
||||
body_bases
|
||||
+ [
|
||||
ast.Subscript(
|
||||
value=ast.Name(id="Generic"),
|
||||
slice=type_vars,
|
||||
)
|
||||
],
|
||||
body_subsitutions | substitutions,
|
||||
)
|
||||
|
||||
case ConstraintType(type=base):
|
||||
return self.get_bases(base)
|
||||
|
||||
case TypeVar(bound=bound) if bound is not None:
|
||||
return [self.dump_type(bound)], {}
|
||||
|
||||
case _:
|
||||
return [], {}
|
||||
|
||||
def generate_body(
|
||||
self, members: dict[str, Member], substitutions: dict[str, Type]
|
||||
) -> list[ast.stmt]:
|
||||
if len(members) == 0:
|
||||
return [ast.Expr(value=Empty)]
|
||||
|
||||
body: list[ast.stmt] = []
|
||||
for name, member in members.items():
|
||||
type: Type = member.type
|
||||
type = substitute_typevars(type, substitutions)
|
||||
match member.kind:
|
||||
case m.MemberKind.PROPERTY:
|
||||
body.append(
|
||||
ast.AnnAssign(
|
||||
target=ast.Name(id=name),
|
||||
annotation=self.dump_type(type),
|
||||
simple=1,
|
||||
)
|
||||
)
|
||||
case m.MemberKind.METHOD:
|
||||
body.extend(self.dump_method(name, type))
|
||||
return body
|
||||
|
||||
def dump_type(self, type: Type) -> ast.expr:
|
||||
match type:
|
||||
case AliasType(name=name) | GenericType(name=name) if (
|
||||
name in self.substitutions
|
||||
):
|
||||
type = substitute_typevars(type, self.substitutions[name])
|
||||
|
||||
match type:
|
||||
case TopType() | UnknownType():
|
||||
self.add_typing_import("Any")
|
||||
return ast.Name(id="Any")
|
||||
|
||||
case BaseType(name=name):
|
||||
return ast.Name(id=name)
|
||||
|
||||
case AliasType(name=name):
|
||||
return ast.Name(id=name)
|
||||
|
||||
case UnitType():
|
||||
return ast.Constant(value=None)
|
||||
|
||||
case Function():
|
||||
name: str = self.define_protocol(type)
|
||||
return ast.Name(id=name)
|
||||
|
||||
case OverloadedFunction(overloads=overloads):
|
||||
if len(overloads) == 1:
|
||||
return self.dump_type(overloads[0])
|
||||
return ast.BinOp(
|
||||
left=self.dump_type(OverloadedFunction(overloads=overloads[:-1])),
|
||||
op=ast.BitOr(),
|
||||
right=self.dump_type(overloads[-1]),
|
||||
)
|
||||
|
||||
case ComplexType():
|
||||
name: str = self.new_stub_name()
|
||||
self.generate_stub(name, type)
|
||||
return ast.Name(id=name)
|
||||
|
||||
case ExtensionType():
|
||||
raise NotImplementedError
|
||||
|
||||
case TypeVar():
|
||||
return ast.Name(id=type.name)
|
||||
|
||||
case GenericType(name=name):
|
||||
params: ast.expr
|
||||
if len(type.params) == 1:
|
||||
params = self.dump_type(type.params[0])
|
||||
else:
|
||||
params = ast.Tuple(
|
||||
elts=[self.dump_type(param) for param in type.params]
|
||||
)
|
||||
return ast.Subscript(
|
||||
value=ast.Name(id=type.name),
|
||||
slice=params,
|
||||
)
|
||||
|
||||
case AppliedType():
|
||||
args: ast.expr
|
||||
if len(type.args) == 1:
|
||||
args = self.dump_type(type.args[0])
|
||||
else:
|
||||
args = ast.Tuple(elts=[self.dump_type(arg) for arg in type.args])
|
||||
return ast.Subscript(
|
||||
value=ast.Name(id=type.name),
|
||||
slice=args,
|
||||
)
|
||||
|
||||
case ConstraintType():
|
||||
return self.dump_type(type.type)
|
||||
|
||||
case _:
|
||||
assert_never(type)
|
||||
|
||||
def dump_method(
|
||||
self, name: str, method: Type, overloaded: bool = False
|
||||
) -> list[ast.stmt]:
|
||||
match method:
|
||||
case Function():
|
||||
if overloaded:
|
||||
self.add_typing_import("overload")
|
||||
return [
|
||||
ast.FunctionDef(
|
||||
name=name,
|
||||
args=self.dump_args(method, with_self=True),
|
||||
returns=self.dump_type(method.returns),
|
||||
body=[ast.Expr(value=Empty)],
|
||||
decorator_list=[ast.Name(id="overload")] if overloaded else [],
|
||||
)
|
||||
]
|
||||
case OverloadedFunction(overloads=overloads):
|
||||
stmts: list[ast.stmt] = []
|
||||
for overload in overloads:
|
||||
stmts.extend(self.dump_method(name, overload, True))
|
||||
return stmts
|
||||
case _:
|
||||
return [
|
||||
ast.AnnAssign(
|
||||
target=ast.Name(id=name),
|
||||
annotation=self.dump_type(method),
|
||||
simple=1,
|
||||
)
|
||||
]
|
||||
|
||||
def dump_args(self, func: Function, with_self: bool = False) -> ast.arguments:
|
||||
pos: list[ast.arg] = [
|
||||
ast.arg(arg=f"_{arg.pos}", annotation=self.dump_type(arg.type))
|
||||
for arg in func.pos_args
|
||||
]
|
||||
mixed: list[ast.arg] = [
|
||||
ast.arg(arg=arg.name, annotation=self.dump_type(arg.type))
|
||||
for arg in func.args
|
||||
]
|
||||
kw: list[ast.arg] = [
|
||||
ast.arg(arg=arg.name, annotation=self.dump_type(arg.type))
|
||||
for arg in func.kw_args
|
||||
]
|
||||
defaults: list[ast.expr] = [
|
||||
Empty for arg in func.pos_args + func.args if not arg.required
|
||||
]
|
||||
kw_defaults: list[Optional[ast.expr]] = [
|
||||
None if arg.required else Empty for arg in func.kw_args
|
||||
]
|
||||
if with_self:
|
||||
arg = ast.arg(arg="self", annotation=None)
|
||||
if len(pos) != 0:
|
||||
pos.insert(0, arg)
|
||||
else:
|
||||
mixed.insert(0, arg)
|
||||
return ast.arguments(
|
||||
posonlyargs=pos,
|
||||
args=mixed,
|
||||
kwonlyargs=kw,
|
||||
defaults=defaults,
|
||||
kw_defaults=kw_defaults,
|
||||
)
|
||||
|
||||
def define_protocol(self, func: Function) -> str:
|
||||
self.add_typing_import("Protocol")
|
||||
name: str = self.new_protocol_name()
|
||||
protocol = ast.ClassDef(
|
||||
name=name,
|
||||
bases=[ast.Name(id="Protocol")],
|
||||
keywords=[],
|
||||
body=[
|
||||
ast.FunctionDef(
|
||||
name="__call__",
|
||||
args=self.dump_args(func, with_self=True),
|
||||
returns=self.dump_type(func.returns),
|
||||
body=[ast.Expr(value=Empty)],
|
||||
decorator_list=[],
|
||||
),
|
||||
],
|
||||
decorator_list=[],
|
||||
)
|
||||
self.add_stub(protocol)
|
||||
return name
|
||||
|
||||
def new_protocol_name(self) -> str:
|
||||
name: str = f"_Protocol{self.protocol_idx}"
|
||||
self.protocol_idx += 1
|
||||
return name
|
||||
|
||||
def new_stub_name(self) -> str:
|
||||
name: str = f"_Stub_{self.stub_idx}"
|
||||
self.stub_idx += 1
|
||||
return name
|
||||
|
||||
def new_type_var_name(self) -> str:
|
||||
name: str = f"_T{self.type_var_idx}"
|
||||
self.type_var_idx += 1
|
||||
return name
|
||||
|
||||
def add_stub(self, stub: ast.stmt):
|
||||
self.stubs.append(stub)
|
||||
|
||||
def add_typing_import(self, name: str):
|
||||
self.typing_imports.add(name)
|
||||
|
||||
def define_type_vars(self, vars: list[TypeVar]) -> list[TypeVar]:
|
||||
vars2: list[TypeVar] = []
|
||||
for var in vars:
|
||||
vars2.append(self.define_type_var(var))
|
||||
return vars2
|
||||
|
||||
def define_type_var(self, var: TypeVar) -> TypeVar:
|
||||
name: str = self.new_type_var_name()
|
||||
self.add_typing_import("TypeVar")
|
||||
|
||||
kwargs: list[ast.keyword] = []
|
||||
if var.bound is not None:
|
||||
kwargs.append(
|
||||
ast.keyword(
|
||||
arg="bound",
|
||||
value=self.dump_type(var.bound),
|
||||
)
|
||||
)
|
||||
if var.variance == Variance.COVARIANT:
|
||||
kwargs.append(
|
||||
ast.keyword(
|
||||
arg="covariant",
|
||||
value=ast.Constant(value=True),
|
||||
)
|
||||
)
|
||||
elif var.variance == Variance.CONTRAVARIANT:
|
||||
kwargs.append(
|
||||
ast.keyword(
|
||||
arg="contravariant",
|
||||
value=ast.Constant(value=True),
|
||||
)
|
||||
)
|
||||
self.add_stub(
|
||||
ast.Assign(
|
||||
targets=[ast.Name(id=name)],
|
||||
value=ast.Call(
|
||||
func=ast.Name(id="TypeVar"),
|
||||
args=[
|
||||
ast.Constant(value=name),
|
||||
],
|
||||
keywords=kwargs,
|
||||
),
|
||||
)
|
||||
)
|
||||
return TypeVar(name=name, bound=None)
|
||||
@@ -49,6 +49,7 @@ class UnsupportedSyntaxError(Exception):
|
||||
|
||||
class PythonParser:
|
||||
CAST_FUNCTION = "cast"
|
||||
UNSAFE_CAST_FUNCTION = "unsafe_cast"
|
||||
|
||||
def parse_module(self, node: ast.Module) -> list[Stmt]:
|
||||
statements: list[Stmt] = []
|
||||
@@ -423,6 +424,9 @@ class PythonParser:
|
||||
case ast.Call(func=ast.Name(id=self.CAST_FUNCTION)):
|
||||
return self.parse_cast(node)
|
||||
|
||||
case ast.Call(func=ast.Name(id=self.UNSAFE_CAST_FUNCTION)):
|
||||
return self.parse_cast(node)
|
||||
|
||||
case ast.Call():
|
||||
return self.parse_call(node)
|
||||
|
||||
@@ -527,16 +531,19 @@ class PythonParser:
|
||||
return expr
|
||||
|
||||
def parse_cast(self, node: ast.Call) -> CastExpr:
|
||||
assert isinstance(node.func, ast.Name)
|
||||
func: str = node.func.id
|
||||
match node:
|
||||
case ast.Call(args=[type, expr], keywords=[]):
|
||||
return CastExpr(
|
||||
location=Location.from_ast(node),
|
||||
type=self._parse_type(type),
|
||||
expr=self.parse_expr(expr),
|
||||
unsafe=func == self.UNSAFE_CAST_FUNCTION,
|
||||
)
|
||||
case _:
|
||||
raise InvalidSyntaxError(
|
||||
f"Invalid call to {self.CAST_FUNCTION}, expected type and expression"
|
||||
f"Invalid call to {func}, expected type and expression"
|
||||
)
|
||||
|
||||
def parse_call(self, node: ast.Call) -> CallExpr:
|
||||
|
||||
34
midas/typing.py
Normal file
34
midas/typing.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from typing import cast as typing_cast
|
||||
|
||||
cast = typing_cast
|
||||
"""### Midas documentation
|
||||
Cast a value to a type.
|
||||
|
||||
- **Compile-time**: tells the type checker that the return value has the designated type.
|
||||
- **Run-time**: generates assertions to ensure the value can be interpreted as the given type.
|
||||
|
||||
---
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
_**Internal Python documentation**_
|
||||
"""
|
||||
|
||||
|
||||
unsafe_cast = typing_cast
|
||||
"""### Midas documentation
|
||||
Cast a value to a type.
|
||||
|
||||
- **Compile-time**: tells the type checker that the return value has the designated type.
|
||||
- **Run-time**: -
|
||||
|
||||
This operation is unsound, use at your own risk!
|
||||
|
||||
---
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
_**Internal Python documentation**_
|
||||
"""
|
||||
@@ -62,3 +62,4 @@ class UniversalJSONDumper:
|
||||
class TypedAST:
|
||||
stmts: list[p.Stmt]
|
||||
judgements: list[tuple[p.Expr, Type]]
|
||||
evaluated_casts: list[p.CastExpr]
|
||||
|
||||
@@ -8,7 +8,11 @@ authors = [
|
||||
{ name = "Louis Heredero", email = "louis.heredero@students.hevs.ch" },
|
||||
]
|
||||
classifiers = ["Programming Language :: Python :: 3"]
|
||||
dependencies = ["click>=8.4.1"]
|
||||
dependencies = [
|
||||
"black>=26.5.1",
|
||||
"click>=8.4.1",
|
||||
"watchdog>=6.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.kbk28.ch/HEL/midas"
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
{
|
||||
"diagnostics": [],
|
||||
"judgments": [
|
||||
{
|
||||
"location": {
|
||||
"from": "L4:30",
|
||||
"to": "L4:36"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 123.45
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L4:18",
|
||||
@@ -16,7 +29,8 @@
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 123.45
|
||||
}
|
||||
},
|
||||
"unsafe": false
|
||||
},
|
||||
"type": {
|
||||
"name": "Meter",
|
||||
@@ -25,6 +39,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L5:28",
|
||||
"to": "L5:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 6.7
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L5:15",
|
||||
@@ -40,7 +67,8 @@
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 6.7
|
||||
}
|
||||
},
|
||||
"unsafe": false
|
||||
},
|
||||
"type": {
|
||||
"name": "Second",
|
||||
|
||||
14
tests/cases/checker/08_unification.py
Normal file
14
tests/cases/checker/08_unification.py
Normal file
@@ -0,0 +1,14 @@
|
||||
def double(value: float) -> float:
|
||||
return value * 2
|
||||
|
||||
|
||||
def is_odd(value: int) -> bool:
|
||||
return bool(value % 2)
|
||||
|
||||
|
||||
floats: list[float] = [0.2, 0.5, 0.1, 1.2]
|
||||
ints: list[int] = [1, 2, 6, -3]
|
||||
|
||||
doubled_floats = map(double, floats)
|
||||
doubled_ints = map(double, ints)
|
||||
odd_ints = map(is_odd, ints)
|
||||
874
tests/cases/checker/08_unification.py.ref.json
Normal file
874
tests/cases/checker/08_unification.py.ref.json
Normal file
@@ -0,0 +1,874 @@
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
13,
|
||||
15
|
||||
],
|
||||
"end": [
|
||||
13,
|
||||
32
|
||||
]
|
||||
},
|
||||
"message": "Could not unify map[T, U]=(transform: (v: T, /) -> U, iterable: list[T], /) -> list[U] with pos=[(value: float) -> float, list[int]] and kw={}"
|
||||
}
|
||||
],
|
||||
"judgments": [
|
||||
{
|
||||
"location": {
|
||||
"from": "L2:11",
|
||||
"to": "L2:16"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "value"
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L2:19",
|
||||
"to": "L2:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L2:11",
|
||||
"to": "L2:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "value"
|
||||
},
|
||||
"operator": "*",
|
||||
"right": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:11",
|
||||
"to": "L6:15"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "bool"
|
||||
},
|
||||
"type": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "object",
|
||||
"type": {},
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:16",
|
||||
"to": "L6:21"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "value"
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:24",
|
||||
"to": "L6:25"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:16",
|
||||
"to": "L6:25"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "value"
|
||||
},
|
||||
"operator": "%",
|
||||
"right": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L6:11",
|
||||
"to": "L6:26"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "bool"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"_type": "BinaryExpr",
|
||||
"left": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "value"
|
||||
},
|
||||
"operator": "%",
|
||||
"right": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"keywords": {}
|
||||
},
|
||||
"type": {
|
||||
"name": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:23",
|
||||
"to": "L9:26"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.2
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:28",
|
||||
"to": "L9:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.5
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:33",
|
||||
"to": "L9:36"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.1
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:38",
|
||||
"to": "L9:41"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1.2
|
||||
},
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L9:22",
|
||||
"to": "L9:42"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "ListExpr",
|
||||
"items": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.2
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.5
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 0.1
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1.2
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "float"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:19",
|
||||
"to": "L10:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:22",
|
||||
"to": "L10:23"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:25",
|
||||
"to": "L10:26"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 6
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:29",
|
||||
"to": "L10:30"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 3
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:28",
|
||||
"to": "L10:30"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "-",
|
||||
"right": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 3
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L10:18",
|
||||
"to": "L10:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "ListExpr",
|
||||
"items": [
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"_type": "LiteralExpr",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"_type": "UnaryExpr",
|
||||
"operator": "-",
|
||||
"right": {
|
||||
"_type": "LiteralExpr",
|
||||
"value": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "int"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L12:17",
|
||||
"to": "L12:20"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"type": {
|
||||
"name": "map",
|
||||
"params": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "transform",
|
||||
"type": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "v",
|
||||
"type": {
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"pos": 1,
|
||||
"name": "iterable",
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L12:21",
|
||||
"to": "L12:27"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "double"
|
||||
},
|
||||
"type": {
|
||||
"pos_args": [],
|
||||
"args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "value",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L12:29",
|
||||
"to": "L12:35"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "floats"
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "float"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L12:17",
|
||||
"to": "L12:36"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "double"
|
||||
},
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "floats"
|
||||
}
|
||||
],
|
||||
"keywords": {}
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "float"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L13:15",
|
||||
"to": "L13:18"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"type": {
|
||||
"name": "map",
|
||||
"params": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "transform",
|
||||
"type": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "v",
|
||||
"type": {
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"pos": 1,
|
||||
"name": "iterable",
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L13:19",
|
||||
"to": "L13:25"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "double"
|
||||
},
|
||||
"type": {
|
||||
"pos_args": [],
|
||||
"args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "value",
|
||||
"type": {
|
||||
"name": "float"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L13:27",
|
||||
"to": "L13:31"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "ints"
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "int"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L13:15",
|
||||
"to": "L13:32"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "double"
|
||||
},
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "ints"
|
||||
}
|
||||
],
|
||||
"keywords": {}
|
||||
},
|
||||
"type": {}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L14:11",
|
||||
"to": "L14:14"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"type": {
|
||||
"name": "map",
|
||||
"params": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "transform",
|
||||
"type": {
|
||||
"pos_args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "v",
|
||||
"type": {
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"pos": 1,
|
||||
"name": "iterable",
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "T",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"args": [],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "U",
|
||||
"bound": null,
|
||||
"variance": "INVARIANT"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L14:15",
|
||||
"to": "L14:21"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "is_odd"
|
||||
},
|
||||
"type": {
|
||||
"pos_args": [],
|
||||
"args": [
|
||||
{
|
||||
"pos": 0,
|
||||
"name": "value",
|
||||
"type": {
|
||||
"name": "int"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"kw_args": [],
|
||||
"returns": {
|
||||
"name": "bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L14:23",
|
||||
"to": "L14:27"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "ints"
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "int"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
"from": "L14:11",
|
||||
"to": "L14:28"
|
||||
},
|
||||
"expr": {
|
||||
"_type": "CallExpr",
|
||||
"callee": {
|
||||
"_type": "VariableExpr",
|
||||
"name": "map"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "is_odd"
|
||||
},
|
||||
{
|
||||
"_type": "VariableExpr",
|
||||
"name": "ints"
|
||||
}
|
||||
],
|
||||
"keywords": {}
|
||||
},
|
||||
"type": {
|
||||
"name": "list",
|
||||
"args": [
|
||||
{
|
||||
"name": "bool"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"name": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,68 +7,14 @@ Module(
|
||||
alias(name='Meter'),
|
||||
alias(name='Second')],
|
||||
level=0),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')],
|
||||
value=Constant(value=123.45)),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='__midas_a0__'),
|
||||
Name(id='float')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='01_simple_types.py:L3:19: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='__midas_a0__')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=' to float')])),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='distance')],
|
||||
value=Name(id='__midas_a0__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a0__')]),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='__midas_a1__')],
|
||||
value=Constant(value=6.7)),
|
||||
Assert(
|
||||
test=Call(
|
||||
func=Name(id='isinstance'),
|
||||
args=[
|
||||
Name(id='__midas_a1__'),
|
||||
Name(id='float')],
|
||||
keywords=[]),
|
||||
msg=JoinedStr(
|
||||
values=[
|
||||
Constant(value='01_simple_types.py:L4:16: CastError: Cannot cast '),
|
||||
FormattedValue(
|
||||
value=Attribute(
|
||||
value=Call(
|
||||
func=Name(id='type'),
|
||||
args=[
|
||||
Name(id='__midas_a1__')],
|
||||
keywords=[]),
|
||||
attr='__name__'),
|
||||
conversion=-1),
|
||||
Constant(value=' to float')])),
|
||||
value=Constant(value=123.45)),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='time')],
|
||||
value=Name(id='__midas_a1__')),
|
||||
Delete(
|
||||
targets=[
|
||||
Name(id='__midas_a1__')]),
|
||||
value=Constant(value=6.7)),
|
||||
Assign(
|
||||
targets=[
|
||||
Name(id='speed')],
|
||||
|
||||
@@ -263,6 +263,7 @@ class PythonAstJsonSerializer(
|
||||
"_type": "CastExpr",
|
||||
"type": expr.type.accept(self),
|
||||
"expr": expr.expr.accept(self),
|
||||
"unsafe": expr.unsafe,
|
||||
}
|
||||
|
||||
def visit_ternary_expr(self, expr: TernaryExpr) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user