diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..bc1e652e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +## Bug description + +**Describe the bug**: a clear and concise description of what the bug is. + +## Steps to reproduce it + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-implementation.md b/.github/ISSUE_TEMPLATE/feature-implementation.md new file mode 100644 index 00000000..c17bc64c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-implementation.md @@ -0,0 +1,15 @@ +--- +name: Feature Implementation +about: Use this template for issues related to the documentation TODOs page. +title: "[IMPL] " +labels: implementation +assignees: '' + +--- + +## Implementation description + +## Expected behavior + + +## Tests diff --git a/.github/ISSUE_TEMPLATE/roadmap-item.md b/.github/ISSUE_TEMPLATE/roadmap-item.md new file mode 100644 index 00000000..f48f7057 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/roadmap-item.md @@ -0,0 +1,14 @@ +--- +name: Roadmap item +about: Describe this issue template's purpose here. +title: "[ROADMAP]" +labels: roadmap +assignees: '' + +--- + +## Item Goals + +## Dependency + +## Description diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index decdb5d2..0f2c97c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: ci_gh-pages on: push: branches: - - dev/python_impl/minimal_lang + - main permissions: contents: write jobs: @@ -25,4 +25,4 @@ jobs: restore-keys: | mkdocs-material- - run: pip install mkdocs-material mkdocstrings markdown-exec - - run: mkdocs gh-deploy --force \ No newline at end of file + - run: mkdocs gh-deploy --force diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..90c483b1 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,21 @@ +name: pre-commit + +on: + pull_request: + branches: [main, dev/python] + push: + branches: [main, dev/python] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + python-version: '3.12' + - name: Run pre-commit + working-directory: python + run: | + pip install pre-commit + pre-commit run --all-files diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..d65f6ae6 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,30 @@ +name: pytest + +on: + push: + branches: [main, dev/python] + pull_request: + branches: [main, dev/python] + +jobs: + build: + name: Run pytest + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + working-directory: python + run: | + pip install ".[all,dev]" + - name: Run tests + working-directory: python + run: | + pytest . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a10ac111 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing to H-hat + +You can check the issues in the [H-hat issue's page](https://github.com/hhat-lang/hhat_lang/issues) and try contributing on the issues with [good first issue](https://github.com/hhat-lang/hhat_lang/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) label. Depending on the programming language you are using, some formatting styles and checks are required. For instance: +- Python: + - Pull Request (PR) code should follow formatting from [pre-commit configuration](python/.pre-commit-config.yaml) and [pyproject](python/pyproject.toml). +- Rust: + - No specific formatting style or checks other than rust formatter and analyzer. + +Additionally to code contribution, you are encourage to make part of discussions involving how H-hat can handle some language features or concepts. Reach us out at the [Discord](http://discord.unitary.foundation)'s `#h-hat` channel to learn more on how to contribute on that and also to chill and chat, if you feel like doing so. + + +## AI Contributions + +The rise of Generative AI brought many advancements for newcomers to start writing code, but also created concerning situations when incorporating AI-generated code to repositories. Regardless whether you choose to use the tool or not, the code you put in a PR is your responsibility. Make sure you checked it is doing what is intended to do, and does not introduce unnecessary complexity or bugs to the existing codebase. Simply AI prompted code will not be accepted. You are required to evaluate the intent of existing issue or PR, the existing codebase, and the code you are bringing up. Should your PR produce automated AI content (description and content) without support of your critical thinking, it will be closed. diff --git a/README.md b/README.md index 76d1404d..c16b8891 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,9 @@ MIT ## How to Contribute -!!! info "Important" - - Please read this documentation before to understand how the repository is organized and how the language structure works. - -You can check the [TODOs.md](TODOs.md) page to see what is listed to be done. There (probably) are issues in the [H-hat issue's page](https://github.com/hhat-lang/hhat_lang/issues) that you may want to check and try to solve/implement as well. - - -At last, reach us out at the [Discord](http://discord.unitary.foundation)'s `#h-hat` channel to -learn more on how to contribute and chat, if you feel like doing so. +Read [How to Contribute](CONTRIBUTING.md). ## Code of Conduct We coexist in the same world. So be nice to others as you expect others to be nice to you :) - the same world. So be nice to others as you expect others to be nice to you :) \ No newline at end of file + the same world. So be nice to others as you expect others to be nice to you :) diff --git a/definitions/IR_diagrams.drawio b/definitions/IR_diagrams.drawio new file mode 100644 index 00000000..660ff8af --- /dev/null +++ b/definitions/IR_diagrams.drawio @@ -0,0 +1,1554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/CNAME b/docs/CNAME index c3f06ca2..54b25b80 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -docs.hhat-lang.org \ No newline at end of file +docs.hhat-lang.org diff --git a/docs/TODOs.md b/docs/TODOs.md index 0497fdff..4dfa9277 100644 --- a/docs/TODOs.md +++ b/docs/TODOs.md @@ -1,6 +1,6 @@ # TODOs -Here sits an updated list of things to implement/write. Feel free to check them out and [place an issue](https://github.com/hhat-lang/hhat_lang/issues) or discuss them at the [Discord](http://discord.unitary.foundation)'s `#h-hat` channel. +Here sits an updated list of things to implement/write. Feel free to check them out and [place an issue](https://github.com/hhat-lang/hhat_lang/issues) or discuss them at the [Discord `#h-hat` channel](https://discord.gg/J8udsUNRnk). ## H-hat core modules diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml new file mode 100644 index 00000000..cfe4c30e --- /dev/null +++ b/docs/blog/.authors.yml @@ -0,0 +1,9 @@ +authors: + dooms: + name: Dooms + description: Hat master + avatar: https://github.com/Doomsk.png + qinho: + name: Inho + description: Feutre hat + avatar: https://github.com/q-inho.png diff --git a/docs/blog/author/dooms.md b/docs/blog/author/dooms.md new file mode 100644 index 00000000..94a86a53 --- /dev/null +++ b/docs/blog/author/dooms.md @@ -0,0 +1,6 @@ +# Dooms + + +Mind behind H-hat quantum programming language project. + +--- diff --git a/docs/blog/author/inho.md b/docs/blog/author/inho.md new file mode 100644 index 00000000..f75dfdab --- /dev/null +++ b/docs/blog/author/inho.md @@ -0,0 +1,3 @@ +# Inho + +_Description in progress_ diff --git a/python/src/hhat_lang/dialects/heather/code/mlir_builder/__init__.py b/docs/blog/index.md similarity index 100% rename from python/src/hhat_lang/dialects/heather/code/mlir_builder/__init__.py rename to docs/blog/index.md diff --git a/docs/blog/posts/2025/2025-09-09_roadmap_v0.3.md b/docs/blog/posts/2025/2025-09-09_roadmap_v0.3.md new file mode 100644 index 00000000..ab01cec8 --- /dev/null +++ b/docs/blog/posts/2025/2025-09-09_roadmap_v0.3.md @@ -0,0 +1,110 @@ +--- +date: + created: 2025-09-09 + +authors: + - dooms + +categories: + - Roadmap + +tags: + - v0.3 + - roadmap + - announcement + +slug: v0.3-roadmap +--- + +# Roadmap for the first working version + +It is with satisfaction that I, [Dooms](../../author/dooms.md), announce the roadmap for a first working version of H-hat. _For legacy reasons, it will be starting as the version 0.3_. + + + +## Summary + +- The H-hat development will be divided into two steps: Python based (step 1), and Rust based (step 2) +- For everyone, the way to use the language is through the **Rust**-written interpreter/compiler (aka `stable`) +- For those willing to test new features or even develop them, the **Python**-written interpreter/compiler is the way to go (aka `experimental`) +- Heather is the default H-hat's dialect shipped with either Python or Rust interpreter/compiler +- Version `0.3-E0` in Python to roll over by the beginning of next year, as well as initial development for `0.3.0` in Rust + +## Explanation + +So far, exploratory steps were taken mercilessly, scrapping, rewriting, fixing things without second thoughts. Now, things are getting more clear and solid, specifically on my proficiency at understanding what software requirements for the language development are needed, as well as improving the community interaction and exploring how to design a toolchain around it. + +### Python and Rust + +A common sense between me and [Inho](../../author/inho.md) (mostly) is that, while the language is not bootstrappeable and there is a mismatch between people interested on quantum with programming language skills and good programming languages to build programming languages, there is going to be a two-step development cycle system for H-hat: **step 1** is _test it in Python_, and **step 2** is _establish it in Rust_. + +In practice, it means that new features, new ideas, new concepts, and breaking changes will be first developed in Python. Why is that? Because most of the people with some knowledge and interest in quantum right now may be (only or mostly) familiar with Python, so we want to make sure their ideas and suggestions can be easily tested/added to the language so we can understand how things will work. Thus, for those willing to test new features or develop them, the Python-written interpreter/compiler is the way to go. + +Once those features/changes are accepted to be incorporated into H-hat, the next step is followed; _let's implement that in Rust_. It means that stable, (much more) performant and reliant version of the language will be written in Rust. For everyone, this will be the way to use the language: through the Rust-written interpreter/compiler. But, why Rust? you may ask. Simple reason I can find is: I would rather write it in Rust than in any other language that can offer some performance due to their systems programming capabilities and compiled nature. And since Rust seems to be a good language to do most of the work needed here and people are willing to learn it, it seems compelling to pursue this path. + +### Versioning scheme + +Now, regarding the versioning. Python code version will be defined as the experimental side of things, so its versioning scheme is as follows: `.-E` (example: `0.3-E1` ), while Rust code version will be defined as the stable side and thus having the versioning scheme as: `..` (example: `0.3.4`). + +The distinction between both is to make it clear what their purposes are for, so there is no confusion which one to download or build, for instance. Also, it shows that the patch version from Python's has no relation to the patch version of Rust's, because they may have independent cycles and independent bugs/fixes. + +**The only thing they share is the core concepts to be leverage from one to the other.** It means that a version `0.3-Ex` in Python code version should present the same language features as the one in version `0.3.y` in Rust (where `x` and `y` are unrelated to each other). It also means that Python code version will _always be ahead_ of Rust's. Once `0.3-Ex` is approved, the pipeline will move towards implementing the features described on it to the Rust code version. It alone will have the traditional scope, as `0.3.0-beta` for beta versions and `0.3.0` once it is officially released, for instance. Regarding release candidate (e.g. `0.3.0-rc`), it still is not clear, but let's see how it goes. At that point, Python code version must start developing at the version `0.4-Ex`. + +I also plan to have a `0.3-STABLE` tag to direct to the latest, most stable version (in Rust). I particularly am not a fan of having version `x.y.z` overall, because you never know which `z` is the latest or more stable one. This will align better with some ideas I have for the future of the language as well. But I will discuss it in a future post. + + +### Dialects + +One important aspect of H-hat is to be a rule system with core concepts to be followed, rather than a strict syntax, semantics and compilation implementation. The reason is that throughout the years presenting H-hat to people from various backgrounds, I noticed a pattern: people always want to have something familiar to look for; thus, I always had the "why not having the syntax as ?". **I realized that nobody was happy with any syntax I would ever try to present to appease their tastes**. So instead, I decided to define the rules in which H-hat is composed on and write dialects on top of it to demonstrate the features I want to present. + +In the long term, I envision some interoperability between dialects happening, so people could write their own dialect with their favorite syntax/semantics and easily integrate with existing code or be easily integrated with future code in other dialects. + +For now, Heather is the current dialect that ships with the language (Python and Rust code versions will have it). Its main point is to manifest H-hat capabilities, so we can build things and understand how to do better in the next version. + + +### Content of version `0.3` + +- Types definitions: single, struct, enum +- Heather dialect +- Classical and quantum data types and functions constructs +- Built-in data types: + - `bool` + - `u32` + - `u64` + - `i32` + - `i64` + - `f32` + - `f64` + - `str` + - `hashmap` + - `@bool` + - `@u2` + - `@u3` + - `@u4` +- Built-in functions: + - basic IO features on: keyboard input and binary file reading, terminal printing and binary file writing + - basic math operations on: logic, arithmetic, trigonometry + - basic quantum data transformation: single- and multi-index operations +- Function construction and overloading +- Basic distributed computing features +- Cast system to enable quantum and classical data interoperability +- low level quantum languages: OpenQASM v2 and NetQASM +- target backends: `qiskit` (IBM machines) and `netsquid` +- Modifier feature +- Meta module feature + +### Version `0.3` release roadmap + +- Python, experimental version (`0.3-Ex`) + - September and October: built-in functions feature, openqasmv2 and cast system + - October and November: cast system, qiskit and modifier feature + - November and December: netqasm, modifier and meta module features + - January and February: netsquid, distributed features + +Rust version will follow through shortly. + +### Final remarks + +Exciting months ahead and looking forward to knowing what you think about it! You can reach us out at the Unitary Foundation's Discord server (linked at [README.md](https://github.com/hhat-lang/hhat_lang/blob/main/README.md) file) #h-hat channel. + +See you around :)) diff --git a/docs/blog/posts/2025/2025-09-24_followup_roadmap_v0.3.md b/docs/blog/posts/2025/2025-09-24_followup_roadmap_v0.3.md new file mode 100644 index 00000000..691d5522 --- /dev/null +++ b/docs/blog/posts/2025/2025-09-24_followup_roadmap_v0.3.md @@ -0,0 +1,157 @@ +--- +date: + created: 2025-09-24 + +authors: + - dooms + +categories: + - Roadmap + +tags: + - v0.3 + - roadmap + - announcement + +slug: followup-v0.3-roadmap +--- + + +# Follow up on the roadmap for version 0.3 + + +As discussed [previously](2025-09-09_roadmap_v0.3.md), I will give a little bit more information on the actual roadmap. + + + +Considering I am focusing on studying, designing and writing code for H-hat in the coming weeks/months with part-time dedication and eventually some people will be helping here and there, the goals and timelines are: + +## Codebase + +> [!NOTE] +> +> The order of the items does not reflect their priority or implementation order. +> + +### September + +#### Finish minimal function implementation capabilities + +How to invoke built-in, in-code or external (library/package) functions. In principle, they should use the same principles behind. In the case of built-in ones, a special space of the `BaseIRInstr` class so it can also follow the same workflow, aka be called through `resolve` method to address the correct function. + +Considering Heather dialect uses function overloading, it will try to find the correct function through the combination of arguments types provided and the given function name. However, it cannot figure out the function output type, so a same function same with same number and types of arguments must not have different definitions with different outputs. + + +#### Add minimal built-in functions (dialect or core?) + +A very specific set of built-in functions will be added at this moment. Hopefully I can get enough feedback to choose the best ones for the first working version. So your feedback matters here (or anywhere where you can find me, really)! + +It is still uncertain whether it should be: (1) a core feature or a dialect-specific feature, (3) available by default or need to do some function importing. + + +#### Implement cast system + +Cast system is a core feature of H-hat, and it has been tried a few times so far, with different outcomes from each iteration. I will write more about it in a separated post, but so far it suffices to say that this feature contains the heart of all I think is important to provide the proper level of high abstraction from programmers using quantum resources. + + +#### Refactor/reimplement OpenQASMv2 code logic and structure + +This needs to be rethought... again. This part always seems easy and lazy to do, but always prove hard to be good enough to be extensible for all the optimizations and passes available by Qiskit ecosystem or some other custom-made features, and at the same time have a robust enough base API to be used by it or any other low level language (such as NetQASM). I keep postponing improving this part... maybe other things are more priority or interesting. + + +### October + +#### Finish cast system implementation (structure and logic) + +Nothing much to say here. Cast system is a delicate structure that interfaces quantum types with classical ones while performing all the execution requests, checks and casting type part for the quantum programs. It may take longer, but I hope that I have enough experience and material from previous iterations to finalize it by October (at most beginning of November?). + + +#### Finish working on OpenQASMv2 base code + +I may drag this throughout October, but be nice (or do it for me 👼 ). + + +#### Refactor/reimplement Qiskit code logic and structure for target backends (no optimizations, extra passes, etc. yet) + +This part is less traumatic than the OpenQASMv2 part (can't even start thinking of OpenQASMv3 yet), but it needs to have a good base API to handle not only Qiskit but also any other target backend infrastructure, such as NetQASM/SquidASM/AWSBraket/etc. So in the end, doing Qiskit will help shape the base API for it (and of course implement it throughout the process) and future backends. + + +#### Implement modifier feature + +Modifier is a nice (I hope) feature I have been thinking that may bring some power to the programmer and also clarity on the code. It basically enables to modify the behavior, property or add extra properties to some instance, should it be a variable or a function, for example. The syntax is `something`, where the `<>` brackets defines the scope of the modifier for a given instance (`something` in this case). A few simple examples: + +- `some-var<&>` means the reference of `some-var`, or `&some_var` in some other languages, like Rust or C +- `@some-var` means it will use `3000` shots for this quantum variable execution on the backend, regardless the default configuration is + +In most cases, the modifier argument needs a argument name and a value. In specific unequivocal cases, a symbol alone can be used (as is the case of the reference `&`). It may stack modifiers and their order must not matter, unless they are used in different moments throughout the code. In the later case, the current modifier overwrites any repeated modifier argument, but must use all previous ones. + +_Maybe having a tag in the modifier stating "use it once" should be a good idea, so that the modifier will only work at that specific moment and not after._ + + +### November + +#### More modifier + +Hopefully it will be done by November. + + +#### Meta module feature + +Something that bothers me _a lot_ in Python is the hacky way to do dynamic imports. So I thought "why not trying to do something better, like creating a meta module with a template so other modules can use it and define their expected functions and behaviors?". Seemed a good idea, so I am going to implement it. Still unsure whether it makes sense to be implemented at this point, but I see potential and need for it. + + +#### NetQASM implementation + +It is not the first implementation, but _will be_ the first successful working one. On the previous ones, I spent too much time fighting with divergent approaches and could never make up my mind on which one to use. In the end, I had to focus on other things, and it was put aside to freeze. It will basically take the same ideas from OpenQASMv2 successful implementation and, using the base API, should be no more than writing down the NetQASM-specific instructions. + +Previously, I was stuck because NetQASM uses a very thoughtful approach to have internal memory for classical and quantum data inside its routines. It was very nice for NetQASM, but very hard to access it smoothly from the outside to read or write its content. I ended up lost in their code logic and couldn't find a neat solution at that time without changing H-hat internals. Result: I _had_ to change H-hat internals to account for that. So I decided to create a memory manager for quantum programs that basically handles classical and quantum memory, qubits, scopes and other relevant resources. It is still not extensible, so if in the future some other low level language requires other kinds of features, I may need to rethink... again. + +### December + +December will be full of wrapping up previous tasks. + + +### January + +#### NetSquid/SquidASM + +Then, it comes to the time of implementing NetSquid/SquidASM (maybe Simulaqron?) backend. I just think that having at least one "standalone" and one quantum networks backends would make sense to test, try and develop relevant features that the language want the programmer to benefit from and also to learn how to create them. + + +#### Distributed features + +Besides the quantum networks backend, H-hat needs to have some support that can show such capabilities even if just on simulators. So having distributed features is very important, but maybe not detrimental for the release of version 0.3. Let's see how it works out. + +#### Release!(?) + +At this point, it should be able to be good enough to at least be tested. Hopefully, kind people will join to help feedback, guide and improve the features, language and its ecosystem, and maybe even be part of the community! + + +### February + +I expect February be a January++. + + +## Conferences + +Besides the code development side, some interesting conferences are right in the horizon and I think they cannot be missed (again). I missed a few ones this year due to work and personal reasons, for instance, the 2nd Workshop on Quantum Software 2025 in Seoul (https://pldi25.sigplan.org/home/wqs-2025) and the 7th International Workshop on Quantum Compilation in Helsinki (https://quantum-compilers.github.io/iwqc25/). So now, there are a few opportunities coming up due this year: + +- Munich Quantum Software Forum 2025 (https://www.cda.cit.tum.de/research/quantum/mqsf/); _H-hat was accepted and is attending! preparations in progress_ +- Seventh International Workshop on Quantum Software Engineering (Q-SE 2026) (https://conf.researchr.org/home/icse-2026/q-se-2026); _checking viability_ +- International Conference on Quantum Communications, Networking, and Computing (QCNC 2026) (https://www.ieee-qcnc.org/2026/); _draft in progress_ + + +## On the stable version part + +Depending on how things evolve people-wise, code-wise, time-wise, opportunities-wise, it may be possible to resume the Rust side, making the _actual JIT compiler_ for the stable version. And that is another exciting challenge and project on its own that I have been studying and testing a few things (with less frequency). Using [Pliron](https://github.com/vaivaswatha/pliron), [Cranelift](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift) or [Melior](https://github.com/mlir-rs/melior) framework? + +I have been in touch with Pliron's creator and collaborators, and it has been a very interesting experience so far to learn about it. Still unsure how practical it can be to use on its current state for H-hat current needs, but definitely I want to test it in practice. Cranelift is another one that caught my attention, especially because its JIT compiler-driven features already in place. + +If that depends only on me, I will try to use whatever uses less C++ for now. + + +## Final remarks + +I hope this post can bring some light on what is in progress and what is planned for the coming months. Hopefully more people can join the effort! Feedback, reviews, discussions, beta testing, coding, designing, emotional support. Anything helps! + +Well, that is it. Until the next post. diff --git a/docs/core/builtin_instr.md b/docs/core/builtin_instr.md new file mode 100644 index 00000000..46f31392 --- /dev/null +++ b/docs/core/builtin_instr.md @@ -0,0 +1,11 @@ + +!!! info + + Built-in instructions are continuously being added. Check this page from time to time. + + +The core instructions are divided between classical and quantum, as follows: + +- [Classical](classical_instr.md) + +- [Quantum](quantum_instr.md) diff --git a/docs/core/classical_instr.md b/docs/core/classical_instr.md new file mode 100644 index 00000000..ea673081 --- /dev/null +++ b/docs/core/classical_instr.md @@ -0,0 +1,86 @@ +# Classical Instructions + +Below you can find a list of existing (:white_check_mark:), under implementation (:construction:), or on TODO (:memo:) list functions/instructions. + + +## 1. `null` type + +### `print` + +- Status: :memo: +- Syntax: `print(msg:?T)` +- Description: print `msg` (generic) argument as text on the terminal +- Returns: nothing + + +## 2. `bool` type + +### `not` + +- Status: :memo: +- Syntax: `not(data:bool)` +- Description: negates `data` (`bool`) argument binary data +- Returns: the same type as `data` argument + + +### `eq` + +- Status: :memo: +- Syntax: `eq(a:?T b:?T)` +- Description: compares `a` (generic) argument with `b` (generic) argument; both must be of the same type +- Returns: a boolean literal (`true`, `false`) from the comparison + + +### `le` + +- Status: :memo: +- Syntax: `le(a:?T b:?T)` +- Description: checks if `a` (generic) argument is less or equal than `b` (generic) argument; both must be of the same type +- Returns: a boolean literal from the comparison + + +### `lt` + +- Status: :memo: +- Syntax: `lt(a:?T b:?T)` +- Description: checks if `a` (generic) argument is less than `b` (generic) argument; both must be of the same type +- Returns: a boolean literal from the comparison + + +### `ge` + +- Status: :memo: +- Syntax: `ge(a:?T b:?T)` +- Description: checks if `a` (generic) argument is greater or equal than `b` (generic) argument; both must be of the same type +- Returns: a boolean literal from the comparison + + +### `gt` + +- Status: :memo: +- Syntax: `gt(a:?T b:?T)` +- Description: checks if `a` (generic) argument is greater than `b` (generic) argument; both must be of the same type +- Returns: a boolean literal from the comparison + + +## 3. `u16` type + + +## 4. `u32` type + +### `add` + +- Status: :memo: +- Syntax: `add(a:u32 b:u32)` +- Description: performs an addition operation on `a` (`u32`) and `b` (`u32`) arguments +- Returns: a `u32` type from the operation + + +## 5. `u64` type + +### `add` + +- Status: :memo: +- Syntax: `add(a:u64 b:u64)` +- Description: performs an addition operation on `a` (`u64`) and `b` (`u64`) arguments +- Returns: a `u64` type from the operation diff --git a/docs/core/index.md b/docs/core/index.md index 75f02441..9f9a0948 100644 --- a/docs/core/index.md +++ b/docs/core/index.md @@ -1,3 +1,60 @@ # Core Features -In progress. This section is part of a TODO list. + +## Core implementation + +Some core features provide wide range of possibilities for the dialect implementation. + + +### 1. Call with options + +It has the structure: + +``` +id ( + option1: body + option2: body + ... +) +``` + +It can be used to define an identifier `id` to hold some transformation through the options that are functions call (and not identifiers, as usually in function calls with arguments) with values being the body that is executed for that particular option. + +Example: [`if` statement](../dialects/heather/current_syntax.md#8-conditional-statements-if). + + +### 2. Call with body + +It has the structure: + +``` +id (args) { body } +``` + + +### 3. Call with body options + +It has the structure: + +``` +id (arg) { + option1: body + option2: body + ... +} +``` + +Example: [pattern matching `match`](../dialects/heather/current_syntax.md#9-pattern-matching-match). + + +### 4. Modifier + +It has the structure: + +``` +id +id +id +``` + +Example: [] diff --git a/docs/core/quantum_instr.md b/docs/core/quantum_instr.md new file mode 100644 index 00000000..8c507ee2 --- /dev/null +++ b/docs/core/quantum_instr.md @@ -0,0 +1,79 @@ +# Quantum Instructions + +Below you can find a list of existing (:white_check_mark:), under implementation (:construction:), or on TODO (:memo:) list functions/instructions. + + +## 1. `@null` type + + +## 2. `@bool` type + + +### `@not` + +- Status: :construction: +- Syntax: `@not(@data:@bool)` +- Description: negates `@data` (`@bool`) argument quantum binary data +- Returns: a `@bool` quantum data + + +### `@redim` + +- Status: :white_check_mark: +- Syntax: `@redim(@data:@bool)` +- Description: performs a [Hadamard gate](https://www.quantum-inspire.com/kbase/hadamard/) on a `@data` (`@bool`) argument +- Returns: a `@bool` quantum data + + +## 3. `@u2` type + +### `@not` + +- Status: :construction: +- Syntax: `@not(@data:@u2)` +- Description: negates `@data` (`@u2`) argument quantum binary data +- Returns: a `@u2` quantum data + + +### `@redim` + +- Status: :white_check_mark: +- Syntax: `@redim(@data:@u2)` +- Description: performs a [Hadamard gate](https://www.quantum-inspire.com/kbase/hadamard/) on a `@data` (`@u2`) argument +- Returns: a `@u2` quantum data + + +## 4. `@u3` type + +### `@not` + +- Status: :construction: +- Syntax: `@not(@data:@u3)` +- Description: negates `@data` (`@u3`) argument quantum binary data +- Returns: a `@u3` quantum data + + +### `@redim` + +- Status: :white_check_mark: +- Syntax: `@redim(@data:@u3)` +- Description: performs a [Hadamard gate](https://www.quantum-inspire.com/kbase/hadamard/) on a `@data` (`@u3`) argument +- Returns: a `@u3` quantum data + + +## 5. `@u4` type + +### `@not` + +- Status: :construction: +- Syntax: `@not(@data:@u4)` +- Description: negates `@data` (`@u4`) argument quantum binary data +- Returns: a `@u4` quantum data + + +### `@redim` + +- Status: :white_check_mark: +- Syntax: `@redim(@data:@u4)` +- Description: performs a [Hadamard gate](https://www.quantum-inspire.com/kbase/hadamard/) on a `@data` (`@u4`) argument +- Returns: a `@u4` quantum data diff --git a/docs/dialects/creation.md b/docs/dialects/creation.md index 080c6a04..36ff350d 100644 --- a/docs/dialects/creation.md +++ b/docs/dialects/creation.md @@ -1,2 +1,2 @@ -In progress. This section is part of a TODO list. \ No newline at end of file +In progress. This section is part of a TODO list. diff --git a/docs/dialects/heather/current_syntax.md b/docs/dialects/heather/current_syntax.md index bc8accee..b56cc5a3 100644 --- a/docs/dialects/heather/current_syntax.md +++ b/docs/dialects/heather/current_syntax.md @@ -6,60 +6,193 @@ The H-hat's Heather dialect syntax works as follows: -1. There is a main file that will be used for program execution. Its name can be anything, but it must contain a `main` keyword with brackets: +## 1. Main +There is a main file that will be used for program execution. Its name can be anything, but it must contain a `main` keyword with brackets: - ``` - main {} - ``` +``` +main {} +``` -2. Code to be executed must live inside `main` body, e.g. anything inside the brackets will be executed. -3. Comments are: - - `// comment here` for oneliner - - `/- big comment here... -/` for multiple lines -4. Variable declaration: +Code to be executed must live inside `main` body, e.g. anything inside the brackets will be executed. + +## 2. Comments + +``` +// single line comment here + +/- multiple +lines comment +here +-/ +``` + +## 3. Variables + +1. Variable declaration: ``` - var:type // for classical data + var:some-type // for classical data - @var:@type // for quantum data + @var:@some-type // for quantum data ``` -5. Variable assignment: + All quantum literals, types, functions, variables (and so on) start with `@`. This is the universal identifier for quantum-related things. + +2. Variable assignment: ``` // classical - var1:type = value // declare+assign + var1:some-type = value // declare+assign var2 = value // assign // quantum - @var1:@type = @value // declare+assign + @var1:@some-type = @value // declare+assign @var2 = @value // assign ``` -6. Call: - ``` - do_smt() // empty call - print("hoi") // one-argument call - add(1 2) // multiple-anonymous argument call - range(start:0 end:10) // multiple-named argument call - ``` - - Multiple-argument call arguments can be separated by [any Heather-defined whitespaces](index.md#features) - - Calls with named argument will have the `argument-name` followed by colon `:` and its value, e.g. `arg:val` -7. Classical variable assignment: + +## 4. Calls + +``` +do_smt() // empty call +print("hoi") // one-argument call +add(1 2) // multiple-anonymous argument call +range(start:0 end:10) // multiple-named argument call +``` + +- Multiple-argument call arguments can be separated by [any Heather-defined whitespaces](index.md#features) + +- Calls with named argument will have the `argument-name` followed by colon `:` and its value, e.g. `arg:val` + +1. Classical variable assignment: ``` - var:type = data // assign value + var:some-type = data // assign value var = other-data // assign a new data ``` - Assigning data more than once to a classical variable may be possible if it is mutable. More on that at the [language core system page](../../core/index.md). If the variable is immutable, an error will happen. -8. Quantum variable assignment: + +2. Quantum variable assignment: ``` - @var:@type = @first_value // assign the first value + @var:@some-type = @first_value // assign the first value @fn(@var) // @fn will be appended to @var data @other-fn(@var params) // @other-fn will be appended next ``` - A quantum data is an _appendable data container_, that is a data container that appends instructions applied to it in order. In the case above, the content of `@var` will be an array of elements: `[first_value, @fn(%self), @other-fn(%self params)]` that will be transformed and executed in order. More on what appendable data container is at the [language core system page](../../core/index.md). -9. Casting: - ``` - // classical data to classical data casting - u32*16 // casts 16 to u32 type - - // quantum data to classical data casting - u32*@2 // casts @2 to u32 type - ``` - - Casting is a special property in the H-hat logic system. There is the usual classical to classical data casting, but also the quantum to classical data casting. The quantum to classical is special due to the nature of quantum data/variables. More on that in the [rule system page](../../rule_system.md). The syntax is `type*literal` or `type*variable`. In a similar fashion when declaring a variable one uses `variable:type`, it can be thought as the "other way around" process, that is why it was chosen to define the type first on casting (with a different syntax sugar, `*`, to connect the type with the data). + +## 5. Casting + +``` +// classical data to classical data casting +16*u32 // casts 16 to u32 type + +// quantum data to classical data casting +@2*u32 // casts @2 to u32 type +``` + - Casting is a special property in the H-hat logic system. There is the usual classical to classical data casting, but also the quantum to classical data casting. The quantum to classical is special due to the nature of quantum data/variables. More on that in the [rule system page](../../rule_system.md). The syntax is `literal*type` or `variable*type`. In a similar fashion when declaring a variable one uses `variable:type`, with a different syntax sugar, `*`, to connect the data with the type. + +## 6. Types + +There are 4 types of type definitions: `single`, `struct`, `enum` and `union`. Below you can find how to define them: + +``` +type lines:u32 +/- a single type called 'lines' that is +defined by u32. -/ + +type point { x:u32 y:u32 } +/- a struct type called 'point' with +members 'x ' of type u32 and 'y' of type u32. -/ + + +type result { + ok{ msg:str } + err +} +/- a enum type called 'result' with +members 'ok', a struct data with member 'msg', +and 'err', a simple identifier. -/ + + +type code union { + number:u64 + text:str +} +/- a union type called 'code' with +members 'number' of type u64 and 'text' of type str. -/ +``` + +Some key aspects of each type: + + - Single types can be thought as a "label" for a given type, but it has its own properties and checks + + - Structs are always defined by members with a name and type + + - Enums can be either identifiers or structs + + - Unions have the `union` keyword before the body and can hold members with name and type, structs or enums; they behave like usual unions in C, for instance + +## 7. Functions + + +``` +fn sum (a:u32 b:u32) u32 { =add(a b) } +``` + +- The `fn` keyword followed by the function name, `sum`, followed by the arguments between parenthesis, `a` and `b` of type `u32`, followed by the function type, `u32`, followed by the function body between brackets, `=add(a b)`. If the function has no return value, the `null` type can be left empty. + +- The last expression to be return must contain a `=` syntax sugar to indicate it is the "return" expression. + +## 8. Conditional statements (`if`) + + +``` +if( + eq(a b): some-result + lt(a b): { some-bracket-body-result } + true: else-result +) +``` + +An `if` statement is a call with options: each option is one condition with its result. If the result contains multiple expressions, it must be inside a bracket body. The last condition can be a `true` option, representing the `else` clause in other programming languages, or a fallback `default`. + +## 9. Pattern matching (`match`) + + +``` +match (something) { + option1: some-result + option2: { some-bracket-body-result } + default: fallback-result +} +``` + +In a similar fashion to `if`, pattern matching `match` will have options containing their respective instructions. However, `match` requires a variable, function call, literal, etc. to be matched against. This something is placed inside parenthesis after `match` and a bracket body is defined with the options. + + +### 10. Modifiers + +``` +id + +id + +id +``` + +Modifiers provide an extensive way to complement, define or modify the data it is attached to, a literal, variable, type, function call, etc. + + +### 11. Generics + + +### 12. Type trait + + +### 13. `typespace` + + +### 14. Function traits and `fnspace` + +#### 14.1 call with options + + +#### 14.2 call with body + + +#### 14.3 call with body options diff --git a/docs/dialects/heather/heather_syntax.md b/docs/dialects/heather/heather_syntax.md index 72750051..0ad2365e 100644 --- a/docs/dialects/heather/heather_syntax.md +++ b/docs/dialects/heather/heather_syntax.md @@ -81,4 +81,4 @@ where `rvc` is the label for `stats.rv-continuous` and `rvd` is the label for `s --- -**Note**: Incorporating external functions from downloaded sources or local project is still on design phase. \ No newline at end of file +**Note**: Incorporating external functions from downloaded sources or local project is still on design phase. diff --git a/docs/dialects/heather/index.md b/docs/dialects/heather/index.md index 5b9ce4c9..ae99586f 100644 --- a/docs/dialects/heather/index.md +++ b/docs/dialects/heather/index.md @@ -10,7 +10,7 @@ The name is twofold: a [plant](https://en.wikipedia.org/wiki/Calluna)[^1] and a ## Introduction -This dialect was developed to enable programmers to experience H-hat rule system and explore ideas for a new quantum computer science theory that intends to focus more on the computer science of the thing, namely manipulate quantum data rather than quantum states. +This dialect was developed to enable programmers to experience H-hat rule system and explore ideas for a new quantum computer science theory that intends to focus more on the computer science of the thing, namely manipulate quantum data rather than quantum states. ### Features diff --git a/docs/hhat_logo.ico b/docs/hhat_logo.ico new file mode 100644 index 00000000..5d99cfa5 Binary files /dev/null and b/docs/hhat_logo.ico differ diff --git a/docs/hhat_logo.svg b/docs/hhat_logo.svg new file mode 100644 index 00000000..db3c13a6 --- /dev/null +++ b/docs/hhat_logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/how_contribute.md b/docs/how_contribute.md new file mode 100644 index 00000000..3abfbf7e --- /dev/null +++ b/docs/how_contribute.md @@ -0,0 +1,26 @@ + +## Before writing code + +!!! info "Important" + + Please read this documentation before to understand how the repository is organized and how the language structure works. + + +## Setting up your environment + +Check the [Getting Started](getting_started.md) page on how to prepare your environment to contributing. + +You can check both [H-hat issues page](https://github.com/hhat-lang/hhat_lang/issues) and [TODOs.md](TODOs.md) page to see what is listed to be done. When picking something from the TODOs page, make sure to create a new issue through the issues page and selecting the "Feature Implementation" template. Fill it up accordingly (you can check the current open issues for guidance). + +Once the code is ready, you can make a pull request (PR) to the H-hat repository. Write a comprehensive description, referencing all the issues it addresses. You can assign [Doomsk](https://github.com/Doomsk) as reviewer, for instance. + + +### Python code + +The code must follow the [pre-commit](https://pre-commit.com/) settings from pre-commit yaml ([.pre-commit-config.yaml](https://github.com/hhat-lang/hhat_lang/blob/main/python/.pre-commit-config,yaml)) file at H-hat's python implementation. Check more on the pre-commit page on how to set up and run it. + + +### Community + +At last, you can reach us out at the [UnitaryFundation Discord](http://discord.unitary.foundation)'s `#h-hat` channel to +learn more on how to contribute, address some questions, or chat, if you feel like doing so. diff --git a/docs/index.md b/docs/index.md index 524e40dd..1c9e0402 100644 --- a/docs/index.md +++ b/docs/index.md @@ -98,15 +98,8 @@ MIT ## How to Contribute -!!! info "Important" +Check the [How to Contribute](how_contribute.md) page. - Please read this documentation before to understand how the repository is organized and how the language structure works. - -You can check the [TODOs.md](TODOs.md) page to see what is listed to be done. There (probably) are issues in the [H-hat issue's page](https://github.com/hhat-lang/hhat_lang/issues) that you may want to check and try to solve/implement as well. - - -At last, reach us out at the [Discord](http://discord.unitary.foundation)'s `#h-hat` channel to -learn more on how to contribute and chat, if you feel like doing so. ## Code of Conduct diff --git a/docs/python/python_guide.md b/docs/python/python_guide.md index 1b51c946..2c98490c 100644 --- a/docs/python/python_guide.md +++ b/docs/python/python_guide.md @@ -1,7 +1,9 @@ -To properly and safely install H-hat on your computer, you need to configure a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html "Python official virtual environment tutorial"). You can choose between various packages, including [venv](https://docs.python.org/3/library/venv.html#creating-virtual-environments "Create with Python's venv"), [hatch](https://hatch.pypa.io/1.12/ "Hatch: package and project manager"), [uv](https://docs.astral.sh/uv/ "uv: fast package and project manager in Rust"), [pdm](https://pdm-project.org/latest/ "PDM: modern package and project manager"), and [poetry](https://python-poetry.org/ "poetry: package manager"). :material-information-outline:{ title="Here we mentioned the most common ones, but there are plenty of other package and project managers. Choose the one that suits your needs." } +To properly and safely install H-hat on your computer, you need to configure a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html "Python official virtual environment tutorial"). You can choose between various packages, including [venv](https://docs.python.org/3/library/venv.html#creating-virtual-environments "Create with Python's venv"), [hatch](https://hatch.pypa.io/1.12/ "Hatch: package and project manager"), [uv](https://docs.astral.sh/uv/ "uv: fast package and project manager in Rust"), [pdm](https://pdm-project.org/latest/ "PDM: modern package and project manager"), and [poetry](https://python-poetry.org/ "poetry: package manager"). (1) +{ .annotate } +1. Here we mentioned the most common ones, but there are plenty of other package and project managers. Choose the one that suits your needs. -After configuring it, activate it (_each package has their own way to do it, please check it out before proceeding_), and choose one of the methods below to install `H-hat`: +After configuring it, activate it (_each package has their own way to do it, please check it out before proceeding_), and choose **one** of the methods below to install `H-hat`, [Pypi](#via-pypi) or [Source code (currently recommended)](#via-source-code): ## Via Pypi @@ -11,7 +13,7 @@ After configuring it, activate it (_each package has their own way to do it, ple This is the easiest, most straightforward and simplest way to install `H-hat`. On the terminal, with the virtual environment enabled, type:[^1] -[^1]: How to check [which shell I am using](https://askubuntu.com/questions/590899/how-do-i-check-which-shell-i-am-using#590902)? +[^1]: How to check [which shell I am using](https://askubuntu.com/questions/590899/how-do-i-check-which-shell-i-am-using#590902)? ### If you are using `bash` shell: @@ -28,7 +30,7 @@ pip install hhat-lang ".[all]" Either the options above will install all the [tools](../toolchain.md), features and a [H-hat dialect](../dialects/index.md) called [Heather](../dialects/heather/index.md) so you can start learning and writing your own code. -## Via source code recommended +## Via source code recommended {#via-source-code} Use the clone HTTPS link in [the H-hat repository page](https://github.com/hhat-lang/hhat_lang) and git clone it, using the terminal: @@ -53,5 +55,3 @@ pip install -e ".[all]" !!! note This approach is meant to be used for those who want to modify the code. - - diff --git a/docs/running_hhat.md b/docs/running_hhat.md index 07848055..604d2b48 100644 --- a/docs/running_hhat.md +++ b/docs/running_hhat.md @@ -18,7 +18,7 @@ For more information on commands available. ## With Python currently available :gear: !!! info "In progress" - This step is in progress, so you may experience some breaking or incomplete parts. + This step is in progress, so you may experience some breaking or incomplete parts. The project file organization is created as follows: @@ -54,7 +54,7 @@ A new file can be created through: ```python -new.create_new_file("new_project", "file_name") +new.create_new_fn_file("new_project", "file_name") ``` This will create a `file_name.hat` file into the `new_project` project, as well as a `file_name.hat.md` file at `hat_docs/`. For every file, there will be a documentation file. @@ -73,4 +73,3 @@ It will create a `file_type.hat` at `hat_types/`, as well as its documentation c !!! failure "Unavailable" This step is currently unavailable. May be implemented in the future. - diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index bdc3ba25..55764880 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,4 +1,8 @@ :root > * { +.md-typeset .admonition, +.md-typeset details { + font-size: 100% +} } diff --git a/docs/toolchain.md b/docs/toolchain.md index d3b47171..c2c6fb98 100644 --- a/docs/toolchain.md +++ b/docs/toolchain.md @@ -3,4 +3,3 @@ The tools available for H-hat are the following: - [Dialect creation](dialects/creation.md): the most important features that identifies a programming language as part of H-hat family are present there, and can be incorporated and extended within your own dialect. - [Commandline interface (CLI)](cli.md): the commands you can use to create a new project, update it, include code documentation, execute a project code, access notebooks for interactive code, etc. - [Notebooks](notebooks.md): write your code in an interactive way through [Jupyter](https://jupyter.org/) notebooks. - diff --git a/mkdocs.yml b/mkdocs.yml index 309d5d8a..c035ff48 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,13 +6,19 @@ repo_url: "https://github.com/hhat-lang/hhat_lang/" repo_name: hhat-lang/hhat_lang nav: - - Home: index.md + - Home: + - H-hat language: index.md + - How to Contribute: how_contribute.md - Getting Started: - Getting Started: getting_started.md - Guides: - Python: python/python_guide.md - Rust: rust/rust_guide.md - Understanding H-hat: + - Built-in Instructions: + - core/builtin_instr.md + - Classical: core/classical_instr.md + - Quantum: core/quantum_instr.md - Rule System: rule_system.md - Core Features: core/index.md - Tools: @@ -32,6 +38,8 @@ nav: - Calling a function: dialects/heather/examples/calling_fn.md - Defining a new type: dialects/heather/examples/new_type.md - Casting quantum data: dialects/heather/examples/casting_quantum.md + - Blog: + - blog/index.md - TODOs: TODOs.md markdown_extensions: @@ -74,6 +82,43 @@ markdown_extensions: - toc: permalink: true +plugins: + - privacy + - search + - blog: + blog_toc: true + post_date_format: full + authors_profiles: true + post_excerpt: required + categories_allowed: + - Roadmap + - Technical + - Demo + - Use-cases + - Deepdive + - Special + - Announcement + - News + - Thoughts + - meta + - tags +# - search +# - mkdocstrings: +# - api-autonav: +# modules: ['python/src/hhat_lang', 'rust/hhat_lang'] +# nav_section_title: "Python API Reference" +# show_full_namespace: false +# on_implicit_namespace_package: "skip" +# module_options: +# package.submodule: +# docstring_section_style: "table" +# docstring_style: google +# merge_init_into_class: true +# parameter_headings: true +# show_signature_annotations: true +# show_symbol_type_heading: true +# show_symbol_type_toc: true +# summary: true extra_javascript: @@ -87,9 +132,13 @@ theme: font: text: Lato code: JetBrains Mono + logo: hhat_logo.svg + favicon: hhat_logo.ico features: - navigation.sections - navigation.tabs + - navigation.top + - navigation.indexes - navigation.tabs.sticky - content.tooltips - content.code.annotate diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml new file mode 100644 index 00000000..5cc15ba2 --- /dev/null +++ b/python/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: check-added-large-files + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.1 + hooks: + - id: mypy + args: [ + --ignore-missing-imports, + --disable-error-code, attr-defined, + --disable-error-code, override, + --disable-error-code, misc, + ] + exclude: tests + additional_dependencies: [tokenize-rt==3.2.0] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.14.9" + hooks: + - id: ruff-check + args: [--fix, --show-fixes, --show-files] + - id: ruff-format + args: [--line-length=100] + +default_language_version: + python: python3.12 diff --git a/python/README.md b/python/README.md index 774df346..ef3ce831 100644 --- a/python/README.md +++ b/python/README.md @@ -49,6 +49,10 @@ After installing it, you should be able to use H-hat [cli](https://en.wikipedia.org/wiki/Command-line_interface) to prepare the environment for a new H-hat project. +## Documentation + +Check more on [the documentation](https://docs.hhat-lang.org). + > [!NOTE] > > Work in progress. diff --git a/python/pip.conf.tpl b/python/pip.conf.tpl deleted file mode 100644 index 3a7c39c6..00000000 --- a/python/pip.conf.tpl +++ /dev/null @@ -1,2 +0,0 @@ -[global] -extra-index-url = https://${NETSQUIDPYPI_USER}:${NETSQUIDPYPI_PWD}@pypi.netsquid.org diff --git a/python/pyproject.toml b/python/pyproject.toml index 4c8304ed..54fa2ac9 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -7,7 +7,8 @@ name = "hhat-lang" description = "H-hat quantum programming language core and rule system implemented in Python" dynamic = ["version"] authors = [ - { name = "Eduardo Maschio", email = "eduardo.maschio@hhat-lang.org" } + { name = "Eduardo Maschio", email = "eduardo.maschio@hhat-lang.org" }, + { name = "Inho Choi", email = "inho.quantum@gmail.com" } ] readme = "README" requires-python = ">=3.12" @@ -25,24 +26,22 @@ classifiers = [ ] dependencies = [ + "rich>=14.0.0", + "typer>=0.16.0", ] [project.optional-dependencies] heather = [ - "arpeggio", - "pygments", + "arpeggio==2.0.3", + "pygments==2.19.2", ] qiskit = [ "qiskit", + "qiskit-aer", "qiskit-ibm-runtime", ] -squidasm = [ - "squidasm", - "netsquid", -] - netqasm = [ "netqasm", ] @@ -52,7 +51,7 @@ openqasm2 = [ ] all = [ - "hhat-lang[heather,qiskit,squidasm,netqasm,openqasm2]" + "hhat-lang[heather,qiskit,openqasm2,netqasm]" ] dev = [ @@ -60,17 +59,28 @@ dev = [ "pytest", "pytest-xdist", "pytest-mypy", + "pytest-mock", "ipython", + "isort", + "ruff", "black", + "flaky", "isort", "pre-commit", "mkdocs", "mkdocs-material", + "mkdocs-autoapi", "mkdocstrings", "markdown", "mike", ] +[project.scripts] +hat = "hhat_lang.toolchain.cli.cli:main" + +[project.entry-points."pygments.lexers"] +hhat = "hhat_lang.dialects.heather.toolchain.pygments.lexer:HhatLexer" + [tool.setuptools_scm] root = ".." version_scheme = "no-guess-dev" @@ -99,6 +109,6 @@ required-imports = ["from __future__ import annotations"] [tool.mypy] python_version = "3.12" -disable_error_code = ["import-untyped"] +disable_error_code = ["import-untyped", "override", "misc", "attr-defined"] pretty = true -exclude = "build" \ No newline at end of file +exclude = ["build", "tests"] diff --git a/python/src/hhat_lang/core/README.md b/python/src/hhat_lang/core/README.md new file mode 100644 index 00000000..4915e8a4 --- /dev/null +++ b/python/src/hhat_lang/core/README.md @@ -0,0 +1,41 @@ +# Core + +The `core` directory holds all the essential directives, rules, building blocks and features a H-hat implementation must have in order to be considered a H-hat dialect. + + +## Organization + +The organization sits around a few concepts: + +![](core_diagram.svg) + +- code (representation): internal intermediate representation related +- data (content): internal representation of symbols, literals or identifiers (reference to types, functions, variables, language reserved keywords); variable behavior on declaration, assignment, retrieval; function definition, call, lookup +- memory (content): how to store, retrieve and represent data to be used during a program execution +- lowlevel (representation/interface): internal structure and logic for low-level quantum languages interface and target backends +- cast (process): workflow definition, transformations and specifications on casting some data into some type +- compiler (representation/process): definition of structure and logic for a compiler, as well as its content workflow +- execution (process): structure and logic for the executing the program's code, as well as where to handle its data workflow +- fns (representation): how to structure function definitions and execute them +- config (interface): define necessary information for a program to function +- imports (interface): how to handle external local or remote resources +- types (representation/content): how to define type structures and logic, as well as create the essential built-in ones +- error handlers (interface): for every possible error inside H-hat, there must be an error class to describe it + + +## Rules + +Core rules for any dialect implementation of H-hat: +- Classical and quantum must have types, functions, variables and literals clear definitions and behaviors +- Upon manipulation, quantum data must be stored instead of immediately evaluated; only when a cast operation to a classical type is evaluated should its content be lazily evaluated according to the cast protocol +- Quantum data may contain classical instructions and classical data internally; the opposite must not be true +- A (single or set of) configuration file(s) must be provided to give relevant information for the quantum part of the program to run properly; the classical part can be included if needed + + +### Cast protocol + +The cast protocol is especially relevant and unique for the case where a quantum data is to be cast into a classical type. In this case, the data follows a specific workflow: +1. Quantum data content is lazily evaluated, being translated into the low-level quantum language (LLQ); if either the LLQ or the target backend does not have a given classical instruction, it should fall back to H-hat dialect's counterpart +2. The translated code is then executed by the target backend (and by the dialect when fall back instructions are requested), producing a data sampling out of the quantum computation; any classical computation will follow the regular workflow inside the dialect (bridging the dialect and target backend whenever requested) +3. The data sampling result is then interpreted according to some criteria (how to post-process the sampling into reasonable result), and the output is cast into the classical type +4. The final result is then returned back to the code evaluation diff --git a/python/src/hhat_lang/dialects/heather/code/ssa_ir_builder/__init__.py b/python/src/hhat_lang/core/cast/__init__.py similarity index 100% rename from python/src/hhat_lang/dialects/heather/code/ssa_ir_builder/__init__.py rename to python/src/hhat_lang/core/cast/__init__.py diff --git a/python/src/hhat_lang/core/cast/base.py b/python/src/hhat_lang/core/cast/base.py new file mode 100644 index 00000000..3a04706f --- /dev/null +++ b/python/src/hhat_lang/core/cast/base.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Mapping, Protocol, runtime_checkable, Callable +from collections import Counter + +from hhat_lang.core.code.ir_graph import IRNode, IRGraph +from hhat_lang.core.data.core import Literal +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import InterpreterEvaluationError +from hhat_lang.core.execution.abstract_program import QuantumProgram +from hhat_lang.core.memory.core import MemoryManager +from hhat_lang.core.types.abstract_base import BaseTypeDef + + +CastFnType = Callable[[BaseDataContainer | Literal | Any], Literal] +"""cast function type annotation""" + + +def is_iterable(data: Any) -> bool: + return True if hasattr(data, "__iter__") else False + + +def is_dict_like(data: Any) -> bool: + return True if is_iterable(data) and hasattr(data, "__getitem__") else False + + +def is_result_obj(data: Any) -> bool: + return True if hasattr(data, "data") and hasattr(data, "metadata") else False + + +def get_max_count(sample: BaseBitString) -> str: + """Return the bitstring of the maximum count""" + + return Counter(sample.get_counts()).most_common(1)[0][0] + + +def get_min_count(sample: BaseBitString) -> str: + """Return the bistring of the minimum count""" + + return Counter(sample.get_counts()).most_common()[-1][0] + + +def get_sample(sample: BaseBitString) -> BaseDataContainer: + pass + + +@runtime_checkable +class ResultObj(Protocol): + @property + def data(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError() + + @property + def metadata(self): + raise NotImplementedError() + + +@runtime_checkable +class MappingLike(Protocol): + def __getitem__(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError() + + def __iter__(self) -> Any: + raise NotImplementedError() + + def shape(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError() + + +class BaseBitString(ABC): + """ + Abstract class to define bit string instances regardless the backend platform + so H-hat can handle the raw measurement results properly. + """ + + _sample: ResultObj | MappingLike | Mapping + + def __init__(self, data: ResultObj | MappingLike | Mapping, **config: Any): + if isinstance(data, ResultObj | MappingLike | Mapping): + self._sample = data + self._config = config + + else: + raise ValueError( + "cast operation -> bit string result -> bit string class must be a " + "result object, a mapping-like object or a dictionary object." + ) + + @property + def config(self) -> dict: + return self._config + + @abstractmethod + def get_counts(self) -> dict: + raise NotImplementedError() + + +class BaseCastOperator(ABC): + """Cast base class to handle the casting workflow""" + + _data: BaseDataContainer | Literal + _to_type: BaseTypeDef + _cast_fn: CastFnType + + def __init__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + cast_fn: CastFnType, + ): + if ( + isinstance(data, BaseDataContainer | Literal) + and isinstance(to_type, BaseTypeDef) + and isinstance(cast_fn, Callable) + ): + self._data = data + self._to_type = to_type + self._cast_fn = cast_fn + + else: + raise InterpreterEvaluationError( + error_where="cast operator instantiation", + msg=f"data {data} must be BaseDataContainer or literal, " + f"type must be BaseTypeDataStructure and cast function" + f" a callable.", + ) + + @abstractmethod + def flush(self) -> BaseCastOperator: + """Use this method to execute the cast logic.""" + + raise NotImplementedError() + + @abstractmethod + def cast(self) -> BaseCastOperator: + """Use this method to perform the cast conversion.""" + + raise NotImplementedError() + + @abstractmethod + def retrieve_cast_data(self) -> BaseDataContainer | Literal: + """ + Retrieve the cast data with the correct type. Must be used after + ``flush`` and ``cast`` methods. + """ + + raise NotImplementedError() + + +class BaseCastC2C(BaseCastOperator): + """Class to handle classical data casting to classical type""" + + _mem: MemoryManager + _node: IRNode + _ir_graph: IRGraph + + def __init__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + cast_fn: CastFnType, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + if ( + isinstance(mem, MemoryManager) + and isinstance(node, IRNode) + and isinstance(ir_graph, IRGraph) + ): + super().__init__(data=data, to_type=to_type, cast_fn=cast_fn) + self._mem = mem + self._node = node + self._ir_graph = ir_graph + + def flush(self) -> BaseCastC2C: + raise NotImplementedError() + + @abstractmethod + def cast(self) -> BaseCastC2C: + raise NotImplementedError() + + def retrieve_cast_data(self) -> BaseDataContainer | Literal: + pass + + +class BaseCastQ2C(BaseCastOperator): + """Class to handle quantum data casting to classical type""" + + _program: QuantumProgram + + def __init__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + cast_fn: CastFnType, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + super().__init__(data=data, to_type=to_type, cast_fn=cast_fn) + self._program = QuantumProgram( + qdata=self._data, + mem=mem, + node=node, + ir_graph=ir_graph, + base_llq=None, + executor=None, + ) + + def flush(self) -> BaseCastQ2C: + self._program.run() + return self + + @abstractmethod + def cast(self) -> BaseCastQ2C: + raise NotImplementedError() + + def retrieve_cast_data(self) -> BaseDataContainer | Literal: + return self._cast_fn(self._data) + + +class BaseCastC2Q(BaseCastOperator): + """Class to handle classical data casting to quantum type""" + + def flush(self) -> BaseCastC2Q: + raise NotImplementedError() + + @abstractmethod + def cast(self) -> BaseCastC2Q: + raise NotImplementedError() + + def retrieve_cast_data(self) -> BaseDataContainer | Literal: + raise NotImplementedError() + + +class BaseCastQ2Q(BaseCastOperator): + """Class to handle quantum data casting to quantum type""" + + def flush(self) -> BaseCastQ2Q: + raise NotImplementedError() + + @abstractmethod + def cast(self) -> BaseCastQ2Q: + raise NotImplementedError() + + def retrieve_cast_data(self) -> BaseDataContainer | Literal: + raise NotImplementedError() diff --git a/python/src/hhat_lang/core/cast/transform_fns.py b/python/src/hhat_lang/core/cast/transform_fns.py new file mode 100644 index 00000000..6422c8db --- /dev/null +++ b/python/src/hhat_lang/core/cast/transform_fns.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from typing import Any diff --git a/python/src/hhat_lang/core/code/abstract.py b/python/src/hhat_lang/core/code/abstract.py new file mode 100644 index 00000000..085d310a --- /dev/null +++ b/python/src/hhat_lang/core/code/abstract.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any, Iterable + +from hhat_lang.core.code.base import BaseFnCheck, BaseIRBlock +from hhat_lang.core.code.symbol_table import SymbolTable +from hhat_lang.core.data.core import CompositeSymbol, Symbol + + +class IRHash: + """ + IR key class to handle the nodes for the IRGraph. + + Use ``key`` attribute when comparing between ``IRModule``. It is also hashable. + + Use ``uid`` attribute when comparing between type or function name + and an ``IRHash``, ``IRNode`` or ``IRModule``. This is the default when applying + ``hash`` function to this class instance. + """ + + _key: Path + _uid: int + __slots__ = ("_key", "_uid") + + def __init__(self, ir_path: Path): + if isinstance(ir_path, Path): + self._key = ir_path + self._uid = hash(ir_path) + + else: + raise ValueError("ir_path must be of type Path") + + @property + def key(self) -> Path: + return self._key + + @property + def uid(self) -> int: + return self._uid + + def __hash__(self) -> int: + return self._uid + + def __eq__(self, other: Any) -> bool: + if isinstance(other, IRHash): + return hash(self) == hash(other) + + if isinstance(other, BaseIRModule): + return hash(self) == hash(other.path) + + return False + + def __repr__(self) -> str: + txt = Path() + + for p in reversed(self._key.parts): + if p == "src": + break + + txt = Path(p) / txt + + return f"[{txt}#{str(self._uid)[-8:]}]" + + +################### +# IR BASE CLASSES # +################### + + +class BaseIRModule(ABC): + """Base abstract class for IR module definitions.""" + + _path: Path + _symbol_table: SymbolTable + _main: BaseIRBlock + + @property + def path(self) -> Path: + return self._path + + @property + def uid(self) -> int: + return hash(self._path) + + @property + def symbol_table(self) -> SymbolTable: + return self._symbol_table + + @property + def main(self) -> BaseIRBlock: + return self._main + + def __hash__(self) -> int: + return hash((hash(self._path), hash(self._symbol_table), hash(self._main))) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Symbol | CompositeSymbol | BaseFnCheck | Path | Any) -> bool: + return item in self._symbol_table.type or item in self._symbol_table.fn + + @abstractmethod + def __str__(self) -> str: + raise NotImplementedError() + + +class BaseIR(ABC): + """ + Base class for the IR. + + IR holds information about the main code execution (as an IR block), or a symbol + table containing type definitions or function definitions, and a reference table + to point the definitions of types or functions from other IRs. + """ + + _ref_table: RefTable + _module: BaseIRModule + + @property + def module(self) -> BaseIRModule: + return self._module + + @property + def ref_table(self) -> RefTable: + return self._ref_table + + @abstractmethod + def __repr__(self) -> str: + raise NotImplementedError() + + +########################### +# REFERENCE TABLE CLASSES # +########################### + + +class RefTypeTable: + """Reference to types from another IR""" + + _table: dict[Symbol | CompositeSymbol, IRHash] + __slots__ = ("_table",) + + def __init__(self): + self._table = dict() + + def add_ref(self, type_name: Symbol | CompositeSymbol, ir_path: Path) -> None: + if isinstance(type_name, Symbol | CompositeSymbol) and isinstance(ir_path, Path): + self._table[type_name] = IRHash(ir_path) + + else: + raise ValueError(f"wrong reference type table input ({type_name})") + + def get_irpath(self, type_name: Symbol | CompositeSymbol) -> Path: + return self.get_irhash(type_name).key + + def get_irhash(self, type_name: Symbol | CompositeSymbol) -> IRHash: + return self._table[type_name] + + def __hash__(self) -> int: + return hash(self._table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, RefTypeTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Symbol | CompositeSymbol | Any) -> bool: + return item in self._table + + def __len__(self) -> int: + return len(self._table) + + def __iter__(self) -> Iterable[tuple[Symbol | CompositeSymbol, IRHash]]: + return iter(self._table.items()) + + +class RefFnTable: + """Reference to functions from another IR""" + + _table: dict[BaseFnCheck, IRHash] + __slots__ = ("_table",) + + def __init__(self): + self._table = dict() + + def add_ref(self, fn_name: BaseFnCheck, ir_path: Path) -> None: + if isinstance(fn_name, BaseFnCheck) and isinstance(ir_path, Path): + self._table[fn_name] = IRHash(ir_path) + + else: + raise ValueError(f"wrong reference type table input ({fn_name})") + + def get_irpath(self, fn_name: BaseFnCheck) -> Path: + return self.get_irhash(fn_name).key + + def get_irhash(self, fn_name: BaseFnCheck) -> IRHash: + return self._table[fn_name] + + def __hash__(self) -> int: + return hash(self._table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, RefFnTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Symbol | CompositeSymbol | BaseFnCheck) -> bool: + match item: + case BaseFnCheck(): + return item in self._table + + case Symbol() | CompositeSymbol(): + for k in self._table: + if item == k.name: + return True + + return False + + case _: + return False + + def __len__(self) -> int: + return len(self._table) + + def __iter__(self) -> Iterable[tuple[BaseFnCheck, IRHash]]: + return iter(self._table.items()) + + +class RefTable: + """To store reference for types and functions from another IR""" + + _types: RefTypeTable + _fns: RefFnTable + __slots__ = ("_types", "_fns") + + def __init__(self, *, type_ref: RefTypeTable | None = None, fn_ref: RefFnTable | None = None): + self._types = type_ref or RefTypeTable() + self._fns = fn_ref or RefFnTable() + + @property + def types(self) -> RefTypeTable: + return self._types + + @property + def fns(self) -> RefFnTable: + return self._fns + + def __hash__(self) -> int: + return hash(hash(self._types) + hash(self._fns)) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, RefTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Symbol | CompositeSymbol | BaseFnCheck) -> bool: + return item in self._types or item in self._fns diff --git a/python/src/hhat_lang/core/code/ast.py b/python/src/hhat_lang/core/code/ast.py deleted file mode 100644 index d84b356a..00000000 --- a/python/src/hhat_lang/core/code/ast.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from typing import Iterable - - -class AST(ABC): - """ - Abstract syntax tree for the Heather parser. Consuming the - lexer and generating the AST to structure the code so the IR - can be generated for the Evaluator to execute the code. - - All the AST code should inherit from this class, including Node - and Terminal child classes. - """ - - _name: str - _value: tuple[str | AST | tuple[AST, ...], ...] | tuple[str] - - @property - def name(self) -> str: - return self._name - - @property - def value(self) -> tuple[str | AST | tuple[AST, ...], ...] | tuple[str]: - return self._value - - def __iter__(self) -> Iterable: - yield from self._value - - -class Node(AST): - def __repr__(self) -> str: - res = " ".join(str(k) for k in self.value) - return f"{self.name}({res})" - - -class Terminal(AST): - def __repr__(self) -> str: - res = f"[{self.name}]" if self.name != self.value[0] else "" - return f"{self.__class__.__name__}{res}{self.value[0]}" diff --git a/python/src/hhat_lang/core/code/base.py b/python/src/hhat_lang/core/code/base.py new file mode 100644 index 00000000..ed9f9b7e --- /dev/null +++ b/python/src/hhat_lang/core/code/base.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Iterable, Iterator + +from hhat_lang.core.data.core import ( + CompositeSymbol, + ObjArray, + Symbol, + Tmp, + SimpleObj, +) + + +class BaseFnKey: + """ + Base class for functions definition on memory's SymbolTable. + Provide functions a signature. + + Given a function:: + + fn sum (a:u64 b:u64) u64 { ::add(a b) } + + The function key object is as follows:: + + BaseFnKey( + name=Symbol("sum"), + type=Symbol("u64"), + args_names=(Symbol("a"), Symbol("b"),), + args_types=(Symbol("u64"), Symbol("u64"),) + ) + + When trying to retrieve the function data, use ``BaseFnCheck`` + parent instance instead: + + """ + + _name: Symbol | CompositeSymbol + _type: Symbol | CompositeSymbol + _args_types: tuple | tuple[Symbol | CompositeSymbol, ...] + _args_names: tuple | tuple[Symbol, ...] + _hash_value: int + + # TODO: implement code for comparison of out of order args_names + + def __init__( + self, + fn_name: Symbol | CompositeSymbol, + fn_type: Symbol | CompositeSymbol, + args_names: tuple | tuple[Symbol, ...], + args_types: tuple | tuple[Symbol | CompositeSymbol, ...], + ): + # check correct types for each argument before proceeding + assert ( + isinstance(fn_name, Symbol | CompositeSymbol) + and isinstance(fn_type, Symbol | CompositeSymbol) + and all(isinstance(k, Symbol) for k in args_names) + and all(isinstance(p, Symbol | CompositeSymbol) for p in args_types) + ), ( + f"Wrong types provided for function definition on SymbolTable:\n" + f" name: {fn_name}\n type: {fn_type}\n args types: {args_types}\n" + f" args names: {args_names}\n", + ) + + self._name = fn_name + self._type = fn_type + self._args_names = args_names + self._args_types = args_types + self._hash_value = hash((hash(self._name), hash(self._args_types))) + + @property + def name(self) -> Symbol | CompositeSymbol: + return self._name + + @property + def type(self) -> Symbol | CompositeSymbol: + return self._type + + @property + def args_types(self) -> tuple | tuple[Symbol | CompositeSymbol, ...]: + return self._args_types + + @property + def args_names(self) -> tuple | tuple[Symbol, ...]: + return self._args_names + + def complement_name(self, text: str) -> None: + """ + To finish off the function name for temporary 'variables', aka ``Tmp`` instances. + """ + + if isinstance(self._name, Tmp): + self._name.append_to_name(text) + + def __hash__(self) -> int: + return self._hash_value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseFnKey | BaseFnCheck): + return hash(self) == hash(other) + + return False + + def has_args(self, args: tuple[Symbol, ...]) -> bool: + return set(self._args_names) == set(args) + + def __iter__(self) -> Iterator[tuple[Symbol, Symbol | CompositeSymbol]]: + return iter(zip(self.args_names, self.args_types)) + + def __repr__(self) -> str: + return ( + f"{self.name}:{self.type}(" + f"{' '.join(f'{k}:{v}' for k, v in zip(self.args_names, self.args_types))})" + ) + + +class BaseFnCheck: + """ + Base function class to check and retrieve a given function from the SymbolTable. + """ + + _name: Symbol | CompositeSymbol + _args_types: tuple | tuple[Symbol | CompositeSymbol, ...] + _hash_value: int + __slots__ = ("_name", "_args_types", "_hash_value") + + def __init__( + self, + fn_name: Symbol | CompositeSymbol, + args_types: tuple | tuple[Symbol | CompositeSymbol, ...], + ): + # checks types correctness + assert isinstance(fn_name, Symbol | CompositeSymbol) and all( + isinstance(p, Symbol | CompositeSymbol) for p in args_types + ), ( + f"Wrong types provided for function retrieval on SymbolTable:\n" + f" name: {fn_name}\n args types: {args_types}\n", + ) + + self._name = fn_name + self._args_types = args_types + self._hash_value = hash((hash(self._name), hash(self._args_types))) + + @property + def name(self) -> Symbol | CompositeSymbol: + return self._name + + def transform( + self, fn_type: Symbol | CompositeSymbol, args_names: tuple[Symbol, ...] + ) -> BaseFnKey: + if all(isinstance(p, Symbol | CompositeSymbol) for p in args_names) and isinstance( + fn_type, Symbol | CompositeSymbol + ): + return BaseFnKey( + fn_name=self.name, + fn_type=fn_type, + args_types=self._args_types, + args_names=args_names, + ) + raise ValueError(f"cannot transform FnKey with fn type {fn_type} and args {args_names}") + + def check_args_types(self, *values: Symbol | CompositeSymbol) -> bool: + """Check whether ``*values`` have the same values as in function args types""" + + return len(values) == len(self._args_types) and all( + k == v for k, v in zip(values, self._args_types) + ) + + def __hash__(self) -> int: + return self._hash_value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseFnCheck): + return hash(self) == hash(other) + + return False + + def __repr__(self) -> str: + args = ", ".join(f"{t}" for t in self._args_types) + return f"fn(name={self.name}, args=({args}))" + + +class BaseOptnCheck: + """ + Base function with arguments as options (optn) class to check + a given optn from the SymbolTable. + """ + + # TODO: implement it + + +class BaseBdnCheck: + """ + Base function with arguments and body (bdn) class to check + a given bdn from the SymbolTable. + """ + + +class BaseOptBdnCheck: + """ + Base function with arguments and options in the body (optbdn) class to check + a given optbdn from the SymbolTable + """ + + +class BaseIRBlock(ABC): + """ + Base for IR block classes. + """ + + _name: BaseIRBlockFlag + args: tuple[SimpleObj | ObjArray | BaseIRBlock | BaseIRInstr, ...] + + @property + def name(self) -> BaseIRBlockFlag: + return self._name + + @abstractmethod + def append(self, data: Any, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError("base IRBlock append method must be implemented") + + def __hash__(self) -> int: + return hash((hash(self._name), hash(self.args))) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseIRBlock): + return hash(self) == hash(other) + + return False + + def __iter__(self) -> Iterator: + return iter(self.args) + + def __len__(self) -> int: + return len(self.args) + + def __getitem__(self, item: Any) -> Any: + return self.args[item] + + @abstractmethod + def __repr__(self) -> str: + raise NotImplementedError() + + +class BaseIRBlockFlag(Enum): + """ + Base for IR block flag classes. Should be used to define types of IR blocks. + """ + + +class BaseIRFlag(Enum): + """ + Base for IR flag classes. It should be used to create enums for instructions, + such as ``CALL``, ``DECLARE``, ``ASSIGN``, ``RETURN``, etc. + """ + + +class BaseIRInstr(ABC): + """ + Base IR instruction classes. + """ + + _name: BaseIRFlag + args: tuple[BaseIRBlock | SimpleObj | ObjArray, ...] | tuple + _hash_value: int + + def __init__(self, *args: Any, **kwargs: Any): + self._hash_value = hash((hash(self.name), hash(self.args))) + + @property + def name(self) -> Any: + return self._name + + def __hash__(self) -> int: + return self._hash_value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseIRInstr): + return hash(self) == hash(other) + + return False + + def __iter__(self) -> Iterable: + return iter(self.args) + + @abstractmethod + def __repr__(self) -> str: + raise NotImplementedError() diff --git a/python/src/hhat_lang/core/code/instructions.py b/python/src/hhat_lang/core/code/instructions.py index 1210b26b..52e207e6 100644 --- a/python/src/hhat_lang/core/code/instructions.py +++ b/python/src/hhat_lang/core/code/instructions.py @@ -1,14 +1,23 @@ from __future__ import annotations -from typing import Any from abc import ABC, abstractmethod +from enum import Enum, auto +from typing import Any from hhat_lang.core import DataParadigm from hhat_lang.core.code.utils import InstrStatus +class QInstrFlag(Enum): + """Flags describing special quantum instruction behavior.""" + + NONE = auto() + SKIP_GEN_ARGS = auto() + + class BaseInstr(ABC): """Base instruction class""" + name: str _instr_status: InstrStatus @@ -18,25 +27,30 @@ def status(self) -> InstrStatus: @property @abstractmethod - def is_quantum(self) -> bool: - ... + def is_quantum(self) -> bool: ... @property @abstractmethod - def paradigm(self) -> DataParadigm: - ... + def paradigm(self) -> DataParadigm: ... @abstractmethod - def __call__(self, *args: Any, **kwargs: Any) -> Any: - ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... class QInstr(BaseInstr, ABC): """Quantum instruction base class""" + flag: QInstrFlag = QInstrFlag.NONE + def __init__(self): self._instr_status = InstrStatus.NOT_STARTED + @property + def skip_gen_args(self) -> bool: + """Whether argument generation should be skipped for this instruction.""" + + return self.flag == QInstrFlag.SKIP_GEN_ARGS + @property def is_quantum(self) -> bool: return True diff --git a/python/src/hhat_lang/core/code/ir.py b/python/src/hhat_lang/core/code/ir.py deleted file mode 100644 index fe3b1939..00000000 --- a/python/src/hhat_lang/core/code/ir.py +++ /dev/null @@ -1,262 +0,0 @@ -from __future__ import annotations - -from typing import Any, Iterable, Callable -from abc import ABC, abstractmethod -from enum import Enum, auto - -from hhat_lang.core.data.core import Symbol, CompositeSymbol -from hhat_lang.core.types.abstract_base import BaseTypeDataStructure - - -class BlockIRFlag(Enum): - INSTR_BLOCK = auto() - CONTROLFLOW_BLOCK = auto() - CLOSURE_BLOCK = auto() - CALL_BLOCK = auto() - - -class InstrIRFlag(Enum): - ASSIGN = auto() - DECLARE = auto() - DECLARE_ASSIGN = auto() - CALL = auto() - CONTROLFLOW = auto() - TEST_COND = auto() - LOOP = auto() - LOOP_COND = auto() - RETURN = auto() - - -class InstrIR(ABC): - """ - To hold individual instructions and their arguments (if any). - """ - - _name: Symbol | CompositeSymbol - _args: ArgsIR - _flag: InstrIRFlag - - @property - def name(self) -> Symbol | CompositeSymbol: - return self._name - - @property - def args(self) -> ArgsIR: - return self._args - - @property - def flag(self) -> InstrIRFlag: - return self._flag - - -class BlockIR(ABC): - """ - To hold tuple of instructions (`InstrIR`) and blocks (`BlockIR`). - """ - - _instrs: tuple[InstrIR | BlockIR, ...] - - def __getitem__(self, item: int) -> InstrIR | BlockIR: - return self._instrs[item] - - def __iter__(self) -> Iterable: - yield from self._instrs - - -class ArgsIR(ABC): - """ - To hold instructions arguments. - """ - - _args: tuple[Any, ...] - - def __contains__(self, arg: Any) -> bool: - return arg in self._args - - def __iter__(self) -> Iterable: - yield from self._args - - -class BodyIR: - """ - Body IR for functions body and `main`. - """ - - _data: list[InstrIR | BlockIR] | list - - def __init__(self): - self._data = [] - - def push(self, new_item: Any, to_instr_fn: Callable | None = None) -> None: - if not isinstance(new_item, InstrIR | BlockIR): - - if to_instr_fn is not None: - new_item = to_instr_fn(new_item) - - else: - raise ValueError( - "'to_instr_fn' argument must not be None if the item is not 'InstrIR'." - ) - - self._data.append(new_item) - - def __iter__(self) -> Iterable: - yield from self._data - - -#################### -# type annotations # -#################### - -TypeTable = dict[Symbol | CompositeSymbol, BaseTypeDataStructure] -""" -Type annotation for `TypeTable`. It holds all the types used throughout the program. - -A dictionary where the keys are the type names (`Symbol`, `CompositeSymbol`) and the - values are their data structure (`BaseTypeDataStructure`). -""" - -FnTable = dict[ - tuple[ # key: define the function header - Symbol | CompositeSymbol, # first element: function name - Symbol | CompositeSymbol, # second element: function type - tuple | tuple[Symbol | CompositeSymbol | tuple[ - Symbol, Symbol | CompositeSymbol - ], ...] # third element: args; empty args, only types args, name and type pairs - ], - BodyIR] # value: the function body -""" -Type annotation for `FnTable`, a function table that holds all the program functions. - -It consists in a dictionary where the key is: - -- first element: function name (`Symbol`, `CompositeSymbol`) -- second element: function type (`Symbol`, `CompositeSymbol`) -- third element: args, which can be empty, only types or name-type pairs - (tuple of `Symbol`, `CompositeSymbol` or tuple pairs with `Symbol` and - `Symbol` or `CompositeSymbol`) - -and the dictionary value is body of the function. -""" - - -############# -# IR TABLES # -############# - -class TypeIR: - """To format, store and retrieve all types used in the program.""" - - _data: TypeTable - - def __init__(self): - self._data = dict() - - @property - def table(self) -> TypeTable: - return self._data - - def push(self, new_type: BaseTypeDataStructure): - self[new_type.name] = new_type - - def get(self, name: Symbol | CompositeSymbol) -> BaseTypeDataStructure: - return self[name] - - def __setitem__(self, key: Symbol | CompositeSymbol, value: BaseTypeDataStructure) -> None: - if isinstance(key, (Symbol, CompositeSymbol)) and isinstance(value, BaseTypeDataStructure): - if key not in self._data: - self._data[key] = value - - else: - print("[[LOG:IR]] ignore adding the same type in the type table.") - - else: - raise ValueError( - "type table needs symbol/composite symbol as key and data structure as value." - ) - - def __getitem__(self, key: Symbol | CompositeSymbol) -> BaseTypeDataStructure: - return self._data[key] - - def __contains__(self, key: Symbol | CompositeSymbol) -> bool: - return key in self._data - - -class BaseFnIR(ABC): - """ - Function IR class: handles the function table data. It is an abstract class, because - it's up to the dialect to implement how it wants to handle function call, e.g. - whether arguments can be passed in order, a name and value pair in any order, etc. - """ - - _data: FnTable - - @property - def table(self) -> FnTable: - return self._data - - @abstractmethod - def push(self, *ags: Any, **kwargs: Any) -> Any: - pass - - @abstractmethod - def get(self, item: Any) -> Any: - pass - - @abstractmethod - def __setitem__(self, key: Any, value: Any) -> None: - pass - - @abstractmethod - def __getitem__(self, key: Any) -> Any: - pass - - @abstractmethod - def __contains__(self, item: Any) -> bool: - pass - - -class BaseIR(ABC): - """ - Where the IR code lies. It contains `TypeIR` (type table), `FnIR` (function table), - and the `main` code. - """ - - _data: BodyIR - _type_table: TypeIR - _fn_table: BaseFnIR - - def __init__(self): - self._data = BodyIR() - self._type_table = TypeIR() - self._fn_table = BaseFnIR() - - @property - def types(self) -> TypeIR: - return self._type_table - - @property - def fns(self) -> BaseFnIR: - return self._fn_table - - @property - def main(self) -> BodyIR: - return self._data - - def add_type(self, name: Symbol | CompositeSymbol, data: BaseTypeDataStructure) -> None: - self._type_table[name] = data - - @abstractmethod - def add_fn( - self, - *, - fn_name: Symbol, - fn_type: Symbol | CompositeSymbol, - fn_args: Any, - body: Any, - ) -> None: - ... - - def add_body(self, body: Any) -> None: - for k in body: - self._data.push(k) diff --git a/python/src/hhat_lang/core/code/ir_block.py b/python/src/hhat_lang/core/code/ir_block.py new file mode 100644 index 00000000..eaf8d2ea --- /dev/null +++ b/python/src/hhat_lang/core/code/ir_block.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import auto +from typing import Any + +from hhat_lang.core.code.base import ( + BaseIRBlock, + BaseIRBlockFlag, + BaseIRFlag, + BaseIRInstr, +) +from hhat_lang.core.data.core import ( + ObjArray, + Literal, + SimpleObj, +) + +#################### +# IR INSTR SECTION # +#################### + + +class IRFlag(BaseIRFlag): + """ + Used to identify the ``IRBaseInstr`` child class purpose. Ex: a ``CallInstr`` + class is defined with its name as ``IRFlag.FN_CALL``. + """ + + NULL = auto() + BUILTIN_FN_CALL = auto() + BUILTIN_OPTN_CALL = auto() + BUILTIN_BDN_CALL = auto() + BUILTIN_OPTBDN_CALL = auto() + + FN_CALL = auto() + """function with arguments (fn), defined as ``caller(args*)``""" + + CAST = auto() + """casting some data to a type, defined as ``data*type``""" + + ASSIGN = auto() + """assigning a variable as ``var=expr``""" + + DECLARE = auto() + """declaring a variable, defined as ``var:type``""" + + DECLARE_ASSIGN = auto() + """declaring and assigning a variable in one step, defined as ``var:type=expr``""" + + ARGS = auto() + """simple arguments (variables, literals, etc)""" + + ARG_VALUE = auto() + """argument names with values, defined as ``arg=value`` or ``arg:value``""" + + OPTION_EXPR = auto() + """option expression ``option:expr``""" + # COND = auto() + # MATCH = auto() + + BDN_CALL = auto() + """function body-ion (bdn), defined as ``caller(args*){body*}``""" + + OPTBDN_CALL = auto() + """ + function with arguments and options in the body (optbdn), defined as + ``caller(args*){option_expr*}`` + """ + + OPTN_CALL = auto() + """function with arguments as options (optn), defined as ``caller(option_expr*)""" + + RETURN = auto() + """returning something (variable, literal, expr) from a function, defined as ``::expr``""" + + +#################### +# IR BLOCK SECTION # +#################### + + +class IRBlockFlag(BaseIRBlockFlag): + """Define all valid IR block flags for IR blocks""" + + BODY = auto() + ARGS = auto() + ARGS_VALUES = auto() + OPTION = auto() + RETURN = auto() + MODIFIER = auto() + MODIFIER_ARGS = auto() + + +class IRBlock(BaseIRBlock, ABC): + """ + IR blocks + """ + + _name: IRBlockFlag + + def append(self, data: Any, *args: Any, **kwargs: Any) -> None: + match data: + case IRBlock() | IRInstr() | Literal(): + self.args += (data,) + + case _: + raise NotImplementedError(f"data of type {type(data)} not imeplemented") + + +class IRInstr(BaseIRInstr): + """ + Base class for IR instructions. Custom IR instructions names must adhere to + IRFlag enum attributes. For example:: + + + class DeclareInstr(IRInstr): + def __init__(self, ...): + ... + super().__init__(..., name=IRFlag.DECLARE) + """ + + _name: IRFlag + args: tuple[IRBlock | SimpleObj | ObjArray, ...] | tuple + + def __init__( + self, + *args: IRBlock | BaseIRInstr | SimpleObj | ObjArray, + name: IRFlag, + ): + if all( + isinstance(k, IRBlock | BaseIRInstr | SimpleObj | ObjArray) for k in args + ) and isinstance(name, IRFlag): + self._name = name + self.args = args + super().__init__() + + else: + raise ValueError( + f"IR instr {self.__class__.__name__} must received name as {type(name)}," + f" args as {[type(k) for k in args]}. Check for correct types." + ) + + def __repr__(self) -> str: + return f"{self.name}({', '.join(str(k) for k in self.args)})" diff --git a/python/src/hhat_lang/core/code/ir_custom.py b/python/src/hhat_lang/core/code/ir_custom.py new file mode 100644 index 00000000..88f87eeb --- /dev/null +++ b/python/src/hhat_lang/core/code/ir_custom.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from hhat_lang.core.code.base import BaseIRInstr +from hhat_lang.core.code.ir_block import ( + IRBlock, + IRBlockFlag, +) +from hhat_lang.core.data.core import ( + CompositeSymbol, + Literal, + LiteralArray, + ObjArray, + SimpleObj, + Symbol, +) + + +class ArgsValuesBlock(IRBlock): + _name = IRBlockFlag.ARGS_VALUES + + args: ( + tuple[ + Symbol, + ..., + ] + | tuple + ) + values: tuple[SimpleObj | ObjArray | IRBlock | BaseIRInstr, ...] | tuple + + def __init__( + self, + *args: tuple[ + Symbol | ModifierBlock, + CompositeSymbol | Literal | LiteralArray | IRBlock | BaseIRInstr, + ], + ): + self.args = () + self.values = () + + for k in args: + match k[0]: + case Symbol(): + self.args += (k[0],) + + case ModifierBlock(): + self.args += (k[0],) + + case _: + raise ValueError("args values block's args must be symbol or modifier block ") + + match k[1]: + case ( + Symbol() + | CompositeSymbol() + | Literal() + | LiteralArray() + | IRBlock() + | BaseIRInstr() + ): + self.values += (k[1],) + + case _: + raise ValueError( + f"args values block's values must be symbol, literal, ir block or ir instr {k[1]} {type(k[1])}" + ) + + def __repr__(self) -> str: + return f"ARG-VALUE#[{' '.join(f'{a}:{v}' for a, v in zip(self.args, self.values))}]" + + +class BodyBlock(IRBlock): + _name = IRBlockFlag.BODY + + def __init__(self, *args: IRBlock | BaseIRInstr): + if all(isinstance(k, IRBlock | BaseIRInstr) for k in args): + if len(args) == 1 and isinstance(args[0], BodyBlock): + self.args = args[0].args + + else: + self.args = args + + else: + raise ValueError( + f"args must be block or instruction, but got {tuple(type(k) for k in args)}" + ) + + def __repr__(self) -> str: + return "\n".join(str(k) for k in self.args) + + +class ReturnBlock(IRBlock): + _name = IRBlockFlag.RETURN + + args: tuple[SimpleObj | ObjArray | IRBlock | BaseIRInstr, ...] + + def __init__(self, *args: SimpleObj | ObjArray | IRBlock | BaseIRInstr): + if all(isinstance(k, SimpleObj | ObjArray | IRBlock | BaseIRInstr) for k in args): + self.args = args + + else: + raise ValueError("return block got wrong object types") + + def __repr__(self) -> str: + return f"RETURN#[{' '.join(str(k) for k in self.args)}]" + + +class ModifierBlock(IRBlock): + _name = IRBlockFlag.MODIFIER + + args: tuple[Symbol | CompositeSymbol | BaseIRInstr, ModifierArgsBlock] + + def __init__(self, obj: Symbol | CompositeSymbol | BaseIRInstr, args: ModifierArgsBlock): + if isinstance(obj, Symbol | CompositeSymbol | BaseIRInstr) and isinstance( + args, ModifierArgsBlock + ): + self.args = (obj, args) + + else: + raise ValueError(f"modifier block cannot have types {type(obj)} and {type(args)}") + + @property + def obj(self) -> Symbol | CompositeSymbol | BaseIRInstr: + return self.args[0] + + @property + def mods(self) -> ModifierArgsBlock: + return self.args[1] + + def __repr__(self) -> str: + return f"{self.obj}<{self.mods}>" + + +class ModifierArgsBlock(IRBlock): + _name = IRBlockFlag.MODIFIER_ARGS + + args: tuple[Symbol | CompositeSymbol, ...] | ArgsValuesBlock | ArgsBlock # type: ignore [assignment] + + def __init__(self, args: tuple[Symbol | CompositeSymbol, ...] | ArgsValuesBlock | ArgsBlock): + if isinstance(args, ArgsValuesBlock | ArgsBlock) or all( + isinstance(k, Symbol | CompositeSymbol) for k in args + ): + self.args = args + + else: + raise ValueError( + f"modifier args must be made of ArgsValuesBlock elements, " + f"not {[type(k) for k in args]}" + ) + + def __repr__(self) -> str: + return " ".join(str(k) for k in self.args) + + +class ArgsBlock(IRBlock): + _name = IRBlockFlag.ARGS + + args: tuple[SimpleObj | ObjArray | ArgsValuesBlock | IRBlock | BaseIRInstr, ...] | tuple + + def __init__(self, *args: SimpleObj | ObjArray | ArgsValuesBlock | IRBlock | BaseIRInstr): + if all(isinstance(k, SimpleObj | ObjArray | IRBlock | BaseIRInstr) for k in args): + self.args = args + + else: + raise ValueError( + f"args must be block or instruction, but got {tuple(type(k) for k in args)}" + ) + + def __repr__(self) -> str: + return " ".join(str(k) for k in self.args) + + +class OptionBlock(IRBlock): + _name = IRBlockFlag.OPTION + + args: ( # type: ignore [assignment] + tuple[ + tuple[SimpleObj | ObjArray | IRBlock | BaseIRInstr, ...], + IRBlock | BaseIRInstr, + ] + | tuple + ) + + def __init__( + self, + option: SimpleObj | ObjArray | IRBlock | BaseIRInstr, + block: IRBlock | BaseIRInstr, + ): + if isinstance(option, SimpleObj | ObjArray | IRBlock | BaseIRInstr) and isinstance( + block, SimpleObj | ObjArray | IRBlock | BaseIRInstr + ): + self.args = (option, block) + + else: + raise ValueError(f"option ({type(option)}) or block ({type(block)}) is of wrong type.") + + @property + def option(self) -> SimpleObj | ObjArray | IRBlock | BaseIRInstr: + return self.args[0] + + @property + def block(self) -> SimpleObj | ObjArray | IRBlock | BaseIRInstr: + return self.args[1] + + def __repr__(self) -> str: + return f"OPTION#[{self.args[0]}:{self.args[1]}]" diff --git a/python/src/hhat_lang/core/code/ir_graph.py b/python/src/hhat_lang/core/code/ir_graph.py new file mode 100644 index 00000000..cbd73bbe --- /dev/null +++ b/python/src/hhat_lang/core/code/ir_graph.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Iterator + +from hhat_lang.core.code.abstract import BaseIR, IRHash +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.code.utils import ResultPHF, gen_phf, get_hash +from hhat_lang.core.data.core import ( + CompositeSymbol, + Symbol, +) + +#################### +# IR GRAPH CLASSES # +#################### + + +class IRNode: + """ + Stores node key as ``IRHash`` and value as ``BaseIRModule`` child instance. + + Use ``key`` attribute to retrieve its ``IRHash`` value, when checking a type + or function. Use ``uid`` attribute to retrieve the hash value from its internal + ``IRModule`` instance, when comparing between ``IRNode``. + """ + + _uid: int + _irhash: IRHash + _ir: BaseIR + _path: Path + __slots__ = ("_irhash", "_ir", "_uid", "_path") + + def __init__(self, node: BaseIR): + self._uid = node.module.uid + self._path = node.module.path + self._irhash = IRHash(self._path) + self._ir = node + + @property + def irhash(self) -> IRHash: + return self._irhash + + @property + def ir(self) -> BaseIR: + return self._ir + + @property + def uid(self) -> int: + return self._uid + + @property + def path(self) -> Path: + return self._path + + def __hash__(self) -> int: + return self._uid + + def __eq__(self, other: Any) -> bool: + if isinstance(other, IRHash | IRNode): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Symbol | CompositeSymbol | BaseFnCheck | Path | Any) -> bool: + return item in self._ir.module or item == self._path + + def __repr__(self) -> str: + return f"Node({self.irhash})" + + +class NodeSet: + """ + Efficiently store ``IRNode`` elements together with the perfect hash function (PHF) + ``ResultPHF`` instance. + """ + + _data: tuple[IRNode, ...] | tuple + _phf: ResultPHF | None + + def __init__(self, *data: IRNode, phf: ResultPHF | None = None): + if all(isinstance(k, IRNode) for k in data) and isinstance(phf, ResultPHF) or phf is None: + self._data = data + self._phf = phf + + else: + raise ValueError("node set accepts only IRNode instances") + + @property + def phf(self) -> ResultPHF: + if self._phf is not None: + return self._phf + + raise ValueError("node's phf instance is null") + + @classmethod + def new_set(cls, *data: IRNode, phf: ResultPHF) -> NodeSet: + return cls(*data, phf=phf) + + def __contains__( + self, + item: (IRHash | IRNode | Path | tuple[Path, Symbol | CompositeSymbol | BaseFnCheck]), + ) -> bool: + match item: + case IRNode(): + return item in self._data + + case IRHash(): + for node in self._data: + if item == node.irhash: + return True + + case Path(): + for node in self._data: + if item == node.path: + return True + + case tuple(): + for node in self._data: + _path = item[0] + _symbol = item[1] + + if _path == node.path and _symbol in node.ir.module: + return True + + return False + + def __getitem__(self, item: IRHash | int) -> IRNode | None: + if isinstance(item, IRHash): + if self._phf is not None: + res = get_hash(hash(item), self.phf) + + if res is not None: + return self._data[res] + + raise ValueError( + "node set must have phf attribute defined and item hash mush not be null" + ) + + # assuming it is integer + return self._data[item] + + def __len__(self) -> int: + return len(self._data) + + def __iter__(self) -> Iterator[IRNode]: + return iter(self._data) + + +class IRGraph: + """ + Graph to hold IR instances as nodes and their relationship as edges. The relationship + (stored in ``RefTable``) happens when a type or function is imported from another IR + module. + """ + + _is_built: bool + _nodes: NodeSet + _tmp_nodes: tuple[IRNode, ...] | tuple + _main_node: IRHash + + def __init__(self): + self._is_built = False + self._nodes = NodeSet() + self._tmp_nodes = () + + @property + def nodes(self) -> NodeSet: + """Last node in a program will always be its 'main' file.""" + return self._nodes + + @property + def main_node(self) -> IRHash: + return self._main_node + + @property + def is_built(self) -> bool: + return self._is_built + + def add_node(self, ir: BaseIR) -> IRHash: + """Add an IR to the graph node.""" + + node = IRNode(ir) + self._tmp_nodes += (node,) + return node.irhash + + def add_main_node(self, ir: BaseIR) -> IRHash: + """Add main IR to the graph node.""" + self._main_node = self.add_node(ir) + return self._main_node + + def _check_refs(self) -> bool: + """ + Check references inside the node set so there are no missing IRs to build the ir graph. + """ + + for node in self._nodes: + for _, irhash in node.ir.ref_table.types: + if irhash not in self._nodes: + return False + + for _, irhash in node.ir.ref_table.fns: + if irhash not in self._nodes: + return False + + return True + + def build(self) -> None: + """Build IR graph for performance and optimization purposes.""" + + if not self._is_built and len(self._tmp_nodes) > 0: + node_res, node_phf = gen_phf(self._tmp_nodes) + self._nodes = NodeSet.new_set(*node_res, phf=node_phf) # type: ignore [arg-type] + self._tmp_nodes = () + + if self._check_refs(): + self._is_built = True + + else: + raise ValueError("missing nodes to build the ir graph") + + else: + raise ValueError( + f"ir graph is already built ({self._is_built}) or no nodes were found" + f" (total nodes: {len(self._tmp_nodes)})." + ) + + def update(self, cur_node_key: IRHash, new_node: BaseIR) -> None: + """ + TODO: implement it + + Update to a new node (IR module) from a given current node key (``IRHash``) + + Args: + cur_node_key: + new_node: + """ + + raise NotImplementedError() + + def get_fns(self, module_path: Path, item: Symbol) -> tuple[BaseFnCheck, ...]: + """ + Get all functions from a module with name given by 'item' argument, and + return as a tuple of those function signatures. + """ + + # incomplete ir graph will have self._tmp_nodes filled in; otherwise, self._nodes + _nodes = self._tmp_nodes or self._nodes + + for tmp_node in _nodes: + if module_path == tmp_node.path and item in tmp_node.ir.module: + fns = tmp_node.ir.module.symbol_table.fn.get(item) + + if isinstance(fns, dict): + return tuple(fn for fn in fns) + + raise ValueError(f"item {item} not found in ir node at {module_path}") + + def __contains__(self, item: Any) -> bool: + if isinstance(item, Symbol | BaseFnCheck): + for tmp_node in self._tmp_nodes: + if item in tmp_node: + return True + + if isinstance(item, Path): + for tmp_node in self._tmp_nodes: + if item in tmp_node: + return True + + return False + + def __repr__(self) -> str: + max_n = str(len(self.nodes)) + txt = "".join( + f"\nN#{'0' * (len(max_n) - len(str(n)))}{n}{k.ir}" for n, k in enumerate(self.nodes) + ) + return f"==============\n=*=IR GRAPH=*=\n==============\n{txt}\n" diff --git a/python/src/hhat_lang/core/code/symbol_table.py b/python/src/hhat_lang/core/code/symbol_table.py new file mode 100644 index 00000000..9ec537cd --- /dev/null +++ b/python/src/hhat_lang/core/code/symbol_table.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +from collections import OrderedDict +from typing import Any, Iterable + +from hhat_lang.core.code.base import BaseFnCheck, BaseFnKey +from hhat_lang.core.data.core import CompositeSymbol, Symbol +from hhat_lang.core.data.fn_def import BuiltinFnDef, FnDef, ModifierDef +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.types.abstract_base import BaseTypeDef + + +class TypeTable: + _table: OrderedDict[Symbol | CompositeSymbol, BaseTypeDef] + __slots__ = ("_table",) + + def __init__(self): + self._table = OrderedDict() + + @property + def table(self) -> OrderedDict[Symbol | CompositeSymbol, BaseTypeDef]: + return self._table + + def add(self, name: Symbol | CompositeSymbol, data: BaseTypeDef) -> None: + if isinstance(name, Symbol | CompositeSymbol) and isinstance(data, BaseTypeDef): + if name not in self.table: + self.table[name] = data + + else: + raise ValueError( + f"type {name} must be symbol/composite symbol and its data must be " + f"known type structure" + ) + + def get( + self, name: Symbol | CompositeSymbol, default: Any | None = None + ) -> BaseTypeDef | Any | None: + return self.table.get(name, default) + + def __hash__(self) -> int: + return hash(self.table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, TypeTable): + return hash(self) == hash(other) + + return False + + def __getitem__(self, item: Symbol | CompositeSymbol) -> BaseTypeDef | Any | None: + return self.get(item) + + def __contains__(self, item: Any) -> bool: + return item in self.table + + def __len__(self) -> int: + return len(self.table) + + def __iter__(self) -> Iterable: + return iter(self.table.items()) + + def __repr__(self) -> str: + content = "\n ".join(f"{v}" for v in self.table.values()) + return f"\n - types:\n {content}\n" + + +class FnTable: + """ + This class holds functions definitions as ``BaseFnCheck`` for function + entry (function name, type and argument types) and its body (content). + + Together with ``TypeTable``, ``SymbolTable`` and ``IRModule`` it provides + the base for an IR object picturing the full code. + """ + + _table: OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, FnDef | BuiltinFnDef]] + __slots__ = ("_table",) + + def __init__(self): + self._table = OrderedDict() + + @property + def table( + self, + ) -> OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, FnDef | BuiltinFnDef]]: + return self._table + + def add(self, fn_entry: BaseFnCheck, data: FnDef | BuiltinFnDef) -> None: + if isinstance(data, FnDef | BuiltinFnDef): + if isinstance(fn_entry, BaseFnCheck): + if fn_entry.name in self.table: + self.table[fn_entry.name].update({fn_entry: data}) + + else: + self.table[fn_entry.name] = {fn_entry: data} + + elif isinstance(fn_entry, BaseFnKey): + new_fn_entry = BaseFnCheck(fn_name=fn_entry.name, args_types=fn_entry.args_types) + if fn_entry.name in self.table: + self.table[fn_entry.name].update({new_fn_entry: data}) + + else: + self.table[fn_entry.name] = {new_fn_entry: data} + + else: + raise ValueError(f"fn_entry is of wrong type ({type(fn_entry)})") + + def get( + self, + fn_entry: Symbol | CompositeSymbol | BaseFnCheck, + default: Any | None = None, + ) -> FnDef | BuiltinFnDef | dict[BaseFnCheck, FnDef | BuiltinFnDef] | None: + match fn_entry: + case Symbol() | CompositeSymbol(): + return self.table.get(fn_entry, default) + + case BaseFnCheck(): + if fn_entry.name in self.table: + return self.table[fn_entry.name].get(fn_entry, default) + + raise ValueError(f"cannot retrieve fn {fn_entry}") + + def __hash__(self) -> int: + return hash(self.table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, FnTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Any) -> bool: + match item: + case Symbol() | CompositeSymbol(): + return item in self._table + + case BaseFnCheck(): + return item in self._table[item.name] + + case _: + return False + + def __len__(self) -> int: + return sum(len(k) for k in self.table.values()) + + def __iter__(self) -> Iterable: + return iter((p, q) for v in self.table.values() for p, q in v.items()) + + def __repr__(self) -> str: + content = "\n ".join( + f"{k}:\n {v}" for h in self.table.values() for k, v in h.items() + ) + return f"\n - fns:\n {content}" + + +class ConstTable: + """ + This class holds all constants in a module + """ + + _table: OrderedDict[Symbol | CompositeSymbol, BaseDataContainer] + __slots__ = ("_table",) + + def __init__(self): + self._table = OrderedDict() + + @property + def table(self) -> OrderedDict[Symbol | CompositeSymbol, BaseDataContainer]: + return self._table + + def add(self, item: BaseDataContainer) -> None: + if isinstance(item, BaseDataContainer) and item.is_constant: + self._table[item.name] = item + + raise ValueError( + f"data must be constant to be added to ConstTable; {item.name} ({item.type}) is not." + ) + + def get( + self, item: Symbol | CompositeSymbol, default: Any | None = None + ) -> BaseDataContainer | Any | None: + return self._table.get(item, default) + + def __getitem__(self, item: Symbol | CompositeSymbol) -> BaseDataContainer | Any | Any: + return self.get(item) + + def __hash__(self) -> int: + return hash(self.table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ConstTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Any) -> bool: + return item in self.table + + def __len__(self) -> int: + return len(self.table) + + def __iter__(self) -> Iterable: + return iter(self.table.items()) + + +class MetaModTable: + """ + This class holds all meta modules in a module. + """ + + _table: OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, FnDef]] + __slots__ = ("_table",) + + def __init__(self): + self._table = OrderedDict() + + @property + def table(self) -> OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, FnDef]]: + return self._table + + def add(self, fn_entry: BaseFnCheck, data: FnDef) -> None: + # TODO: check whether it needs more specific information (copied from FnTable) + + if isinstance(data, FnDef): + if isinstance(fn_entry, BaseFnCheck): + if fn_entry.name in self.table: + self.table[fn_entry.name].update({fn_entry: data}) + + else: + self.table[fn_entry.name] = {fn_entry: data} + + elif isinstance(fn_entry, BaseFnKey): + new_fn_entry = BaseFnCheck(fn_name=fn_entry.name, args_types=fn_entry.args_types) + if fn_entry.name in self.table: + self.table[fn_entry.name].update({new_fn_entry: data}) + + else: + self.table[fn_entry.name] = {new_fn_entry: data} + + else: + raise ValueError(f"fn_entry is of wrong type ({type(fn_entry)})") + + def get( + self, + fn_entry: Symbol | CompositeSymbol | BaseFnCheck, + default: Any | None = None, + ) -> FnDef | dict[BaseFnCheck, FnDef] | None: + # TODO: check if it needs more information (copied from FnTable) + + match fn_entry: + case Symbol() | CompositeSymbol(): + return self.table.get(fn_entry, default) + + case BaseFnCheck(): + if fn_entry.name in self.table: + return self.table[fn_entry.name].get(fn_entry, default) + + raise ValueError(f"cannot retrieve fn {fn_entry}") + + def __hash__(self) -> int: + return hash(self.table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, MetaModTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Any) -> bool: + match item: + case Symbol() | CompositeSymbol(): + return item in self._table + + case BaseFnCheck(): + return item in self._table[item.name] + + case _: + return False + + def __len__(self) -> int: + return sum(len(k) for k in self.table.values()) + + def __iter__(self) -> Iterable: + return iter((p, q) for v in self.table.values() for p, q in v.items()) + + +class ModifierTable: + """ + This class holds all modifiers in a module + """ + + _table: OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, ModifierDef]] + __slots__ = ("_table",) + + def __init__(self): + self._table = OrderedDict() + + @property + def table( + self, + ) -> OrderedDict[Symbol | CompositeSymbol, dict[BaseFnCheck, ModifierDef]]: + return self._table + + def add(self, fn_entry: BaseFnCheck, data: ModifierDef) -> None: + if isinstance(data, ModifierDef) and isinstance(fn_entry, BaseFnCheck): + if fn_entry.name in self.table: + self.table[fn_entry.name].update({fn_entry: data}) + + else: + self.table[fn_entry.name] = {fn_entry: data} + + def get( + self, item: Symbol | CompositeSymbol | BaseFnCheck, default: Any | None = None + ) -> ModifierDef | dict[BaseFnCheck, ModifierDef] | None: + match item: + case Symbol() | CompositeSymbol(): + return self.table.get(item, default) + + case BaseFnCheck(): + if item.name in self.table: + return self.table[item.name].get(item, default) + + raise ValueError(f"cannot retrieve fn {item}") + + def __hash__(self) -> int: + return hash(self.table) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ModifierTable): + return hash(self) == hash(other) + + return False + + def __contains__(self, item: Any) -> bool: + match item: + case Symbol() | CompositeSymbol(): + return item in self._table + + case BaseFnCheck(): + return item in self._table[item.name] + + case _: + return False + + def __len__(self) -> int: + return sum(len(k) for k in self.table.values()) + + def __iter__(self) -> Iterable: + return iter((p, q) for v in self.table.values() for p, q in v.items()) + + +class SymbolTable: + """To store types and functions""" + + _types: TypeTable + _fns: FnTable + _consts: ConstTable + _metamods: MetaModTable + _modifiers: ModifierTable + __slots__ = ("_types", "_fns", "_consts", "_metamods", "_modifiers") + + def __init__(self): + self._types = TypeTable() + self._fns = FnTable() + self._consts = ConstTable() + self._metamods = MetaModTable() + self._modifiers = ModifierTable() + + @property + def type(self) -> TypeTable: + return self._types + + @property + def fn(self) -> FnTable: + return self._fns + + @property + def const(self) -> ConstTable: + return self._consts + + @property + def metamod(self) -> MetaModTable: + return self._metamods + + @property + def modifiers(self) -> ModifierTable: + return self._modifiers + + def __hash__(self) -> int: + return hash((self._types, self._fns, self._consts, self._metamods, self._modifiers)) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, SymbolTable): + return hash(self) == hash(other) + + return False diff --git a/python/src/hhat_lang/core/code/tools.py b/python/src/hhat_lang/core/code/tools.py new file mode 100644 index 00000000..0b7a1315 --- /dev/null +++ b/python/src/hhat_lang/core/code/tools.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Mapping + +from hhat_lang.core.code.abstract import ( + IRHash, + RefTable, +) +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.code.ir_graph import ( + IRGraph, + IRNode, +) +from hhat_lang.core.data.core import ( + Symbol, + CompositeSymbol, +) +from hhat_lang.core.data.fn_def import ( + FnDef, + BuiltinFnDef, +) +from hhat_lang.core.types.abstract_base import BaseTypeDef + + +def get_type( + node_key: IRHash, importing: Symbol | CompositeSymbol, ir_graph: IRGraph +) -> BaseTypeDef | None: + """ + Import a type ``importing`` from an IR module's hash value ``node_key``. Return + the type instance or ``None`` if not found. + """ + + node: IRNode | None = ir_graph.nodes[node_key] + + if node is not None: + return node.ir.module.symbol_table.type.get(importing) + + return None + + +def get_fn( + node_key: IRHash, importing: BaseFnCheck, ir_graph: IRGraph +) -> FnDef | BuiltinFnDef | dict[BaseFnCheck, FnDef | BuiltinFnDef] | None: + """ + Import a function check instance ``importing`` from an IR module's hash value ``node_key``. + + Args: + node_key: the ``IRHash`` instance + importing: the function ``BaseFnCheck`` instance + ir_graph: the program's ``IRGraph`` + + Returns: + A ``FnDef`` instance or ``None``, if no function is found. + """ + + node: IRNode | None = ir_graph.nodes[node_key] + + if node is not None: + return node.ir.module.symbol_table.fn.get(importing) + + return None + + +def build_reftable( + types: Mapping[Symbol | CompositeSymbol, Path] | None = None, + fns: Mapping[BaseFnCheck, Path] | None = None, +) -> RefTable: + types = types or dict() + fns = fns or dict() + ref_table = RefTable() + + for type_name, ir_ref in types.items(): + ref_table.types.add_ref(type_name, ir_ref) + + for f_name, ir_ref in fns.items(): + ref_table.fns.add_ref(f_name, ir_ref) + + return ref_table diff --git a/python/src/hhat_lang/core/code/utils.py b/python/src/hhat_lang/core/code/utils.py index 49785596..5a7a647d 100644 --- a/python/src/hhat_lang/core/code/utils.py +++ b/python/src/hhat_lang/core/code/utils.py @@ -1,9 +1,15 @@ from __future__ import annotations +import sys from enum import IntEnum, auto +from typing import Hashable class InstrStatus(IntEnum): + """ + Instruction status. To be used for asynchronous operations. + """ + NOT_STARTED = auto() RUNNING = auto() TIMEOUT = auto() @@ -12,22 +18,167 @@ class InstrStatus(IntEnum): ERROR = auto() -def check_quantum_type_correctness(names: tuple[str, ...]) -> None: +####################################### +# PERFECT HASH FUNCTION (PHF) SECTION # +####################################### + + +def get_phf_prime(tuple_len: int) -> int: """ - Check whether the quantum and classical symbols follow the rules: - - a quantum data can have classical data - - a classical data cannot have a quantum one + Retrieve a prime for the perfect hash function (PHF) algorithm. Use the tuple length + to check which primer number to use, which must be bigger than ``tuple_len``. + + Probably a relatively good size project may have a few hundreds items (types and + functions combined). By that time, python will not be useful to interpret the code + anyway, but we never know what things will come out of it. """ - prev_quantum = False - cur_quantum = False - for n, name in enumerate(names): + if tuple_len <= 2**5: + return 37 + + if tuple_len <= 2**6: + return 67 + + if tuple_len <= 2**8: + return 257 + + if tuple_len <= 2**12: + return 4_099 + + if tuple_len <= 2**14: + return 16_411 + + # I don't think this number below will ever be needed, but for future references + return 1_048_583 + + +PHF_A_LIMIT = 1_000_000 +"""perfect hash function (PHF) parameter ``a`` limit""" + +# only compatible with 64- or 128-bit systems +PHF_R_LIMIT = 127 if sys.maxsize > 2**64 else 61 +"""perfect hash function (PHF) parameter ``r`` limit""" + + +class ResultPHF: + """Hold PHF result values""" + + _a: int + _r: int + _n: int + _prime: int + __slots__ = ("_a", "_r", "_n", "_prime") + + def __init__(self, *, a: int, r: int, prime: int, n: int): + self._a = a + self._r = r + self._prime = prime + self._n = n + + @property + def a(self) -> int: + return self._a + + @property + def r(self) -> int: + return self._r + + @property + def n(self) -> int: + return self._n + + @property + def prime(self) -> int: + return self._prime + + def __repr__(self) -> str: + return f"PHF(a={self.a}, r={self.r}, prime={self.prime}, n={self.n})" + + +def get_hash_with_args(value: int, a: int, r: int, n: int, prime: int) -> int: + """ + Calculate the hash without a ``ResultPHF`` instance, but with the args. Use it when + finding the correct args to produce the PHF values (``a`` and ``r``) combination. + """ + + p = value * a + return ((p ^ (p >> r)) % prime) % n + + +def _gen_res_a_r_phf( + group_tuple: tuple[Hashable, ...], + tuple_len: int, + a: int, + r: int, + prime: int, +) -> tuple[Hashable, ...]: + """ + Generate a perfect hash function (PHF) tuple. + + Args: + group_tuple: the tuple of IR hashes, or IR hashes and symbol/function check tuple-pairs + tuple_len: + a: an integer parameter to define the index for each element in the ``group_tuple`` + r: another integer parameter to define the index for each element in the ``group_tuple`` + prime: the prime number used to define the index for each element in the ``group_tuple`` + + Returns: + A tuple with the ``group_tuple`` ordered by their PHF index. Empty tuple if the PHF + could not be found. + """ + + collision: bool = False + res_list: list = [None for _ in range(tuple_len)] + + for obj in group_tuple: + h = get_hash_with_args(hash(obj), a, r, tuple_len, prime) + + if obj not in res_list and res_list[h] is None: + res_list[h] = obj + + else: + collision = True + break + + if not collision and None not in res_list: + return tuple(res_list) + + return () + + +def gen_phf( + group_tuple: tuple[Hashable, ...], +) -> tuple[tuple[Hashable, ...], ResultPHF]: + """ + Generate the perfect hash function (PHF). Each ``group_tuple`` element will be ordered + in a new tuple according to its newly calculated hash value. Each element has exactly + one unique index number that wil define its position in the new tuple. + + Args: + group_tuple: a tuple with IR hash elements, or IR hash and symbol/function + check tuple-pairs + + Returns: + A resulting tuple with the elements positioned in their respective index number + inside the tuple, and a ``ResultPHF`` instance with the ``a`` and ``r`` parameters + to retrieve the hash values of each element. + """ + + tuple_len: int = len(group_tuple) + prime = get_phf_prime(tuple_len) + + for a in range(1, PHF_A_LIMIT): + for r in range(PHF_R_LIMIT): + res_list = _gen_res_a_r_phf(group_tuple, tuple_len, a, r, prime) + + if res_list: + return tuple(res_list), ResultPHF(a=a, r=r, prime=prime, n=tuple_len) + + raise ValueError("could not find satisfactory parameter values to generate the PHF") + - if n != 0 and cur_quantum and not prev_quantum: - raise ValueError( - f"{name} is an attribute from a non-quantum symbol. " - f"Cannot have a quantum attribute from a classical symbol." - ) +def get_hash(value: int, phf: ResultPHF) -> int: + """Retrieve a number given a value hash (``value``) and a phf instance (``ResultPHF``)""" - prev_quantum = True if cur_quantum else False - cur_quantum = True if name.startswith("@") else False + p = value * phf.a + return ((p ^ (p >> phf.r)) % phf.prime) % phf.n diff --git a/python/src/hhat_lang/dialects/heather/toolchain/pygments/lexer/__init__.py b/python/src/hhat_lang/core/compiler/__init__.py similarity index 100% rename from python/src/hhat_lang/dialects/heather/toolchain/pygments/lexer/__init__.py rename to python/src/hhat_lang/core/compiler/__init__.py diff --git a/python/src/hhat_lang/core/compiler/core.py b/python/src/hhat_lang/core/compiler/core.py new file mode 100644 index 00000000..78c72240 --- /dev/null +++ b/python/src/hhat_lang/core/compiler/core.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + + +class BaseCompiler(ABC): + """ + Abstract class for the compiler that holds all compilation information, such + as: the list of other compilers used (classical, for other dialects, and + quantum), the list of executors (that evaluate the IR code for classical + and quantum) the compiler can use, quantum specs for available devices, + backends, quantum languages + """ + + @abstractmethod + def parse(self) -> Any: + raise NotImplementedError() + + @abstractmethod + def evaluate(self) -> Any: + raise NotImplementedError() diff --git a/python/src/hhat_lang/low_level/target_backend/qiskit/openqasm/__init__.py b/python/src/hhat_lang/core/config/__init__.py similarity index 100% rename from python/src/hhat_lang/low_level/target_backend/qiskit/openqasm/__init__.py rename to python/src/hhat_lang/core/config/__init__.py diff --git a/python/src/hhat_lang/core/config/base.py b/python/src/hhat_lang/core/config/base.py new file mode 100644 index 00000000..8beb57ad --- /dev/null +++ b/python/src/hhat_lang/core/config/base.py @@ -0,0 +1,644 @@ +""" +Q: Why does this file exist? + +A: To keep configuration files and data under strict keys and values, +and expected behavior. This way, generating or retrieving a configuration +file for new dialects, low-level languages, target backends, with +multiple options will always follow the same recipe. +""" + +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass, field, asdict +from importlib import import_module +from pathlib import Path +from types import ModuleType +from typing import Any, Callable, Optional +from functools import wraps + +from hhat_lang.core.config.utils import read_file +from hhat_lang.core.lowlevel.abstract_qlang import BaseLLQManager, BaseLLQ + + +settings_classes: dict[str, Callable[[], Any]] = dict() +""" +A dictionary containing classes for each setting:: + - 'default' for current settings, + - 'dialect' contains the DialectSettings, + - 'llq' contains LLQSettings, + - 'backend' contains TargetBackendSettings, +""" + + +######################################## +# CONSTRUCTORS AND AUXILIARY FUNCTIONS # +######################################## + + +def insert_setting_class(config_name: str) -> Callable: + """ + Inserts setting class to the settings classes dictionary + according to the key name (str). + """ + + def decorator(fn: Any) -> Callable: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return fn(*args, **kwargs) + + settings_classes[config_name] = wrapper + return wrapper + + return decorator + + +@insert_setting_class("project_root") +def _build_project_root(settings: dict) -> tuple[Path]: + return (Path(settings.get("project_root")),) + + +@insert_setting_class("default") +def _build_default_obj( + settings: dict, +) -> tuple[CurrentDialect, CurrentLLQ, CurrentBackend]: + """ + Define the default ('current_settings') configuration data to be used by the + project to compile and execute code. + """ + + dialect, llq, backend = _retrieve_current_settings_data(settings) + + return ( + CurrentDialect(next(iter(dialect.keys())), **next(iter(dialect.values()))), + CurrentLLQ(), + CurrentBackend(), + ) + + +def _retrieve_current_settings_data(settings: dict) -> tuple[dict, dict, dict]: + """ + Retrieve project current settings to build the right objects and feed the program executor. + + Args: + settings: dictionary containing the settings from project's configuration file + + Returns: + A tuple with dialect, llq and backend data dictionaries + """ + + current: dict | None = settings.get("current_settings", None) + + if current is None: + raise ValueError("could not find 'current_settings' key on the project configuration file.") + + dialect_list: list[str] = current["dialect"]["dir"] + llq_list: list[list[str]] = list(k["dir"] for k in current["llq"]) + backend_list: list[list[str]] = list(k["dir"] for k in current["backend"]) + + dialect_data: dict = _retrieve_data_from_settings(settings, "dialect", dialect_list) + llq_data: dict[tuple[str, ...], dict] = _retrieve_data_from_composed_settings( + settings=settings, which_data="llq", data_list=llq_list + ) + backend_data: dict[tuple[str, ...], dict] = _retrieve_data_from_composed_settings( + settings=settings, which_data="backend", data_list=backend_list + ) + return dialect_data, llq_data, backend_data + + +def _retrieve_data_from_settings( + settings: dict, which_data: str, data_dir: list[str] +) -> dict[tuple[str, ...], dict]: + """ + Retrieve a single dictionary entry from project settings. + + Args: + settings: + which_data: + data_dir: + + Returns: + The dictionary where key is the data reference name tuple + and the value is data from settings. + """ + + data: dict = ( + settings[which_data] + if (avail := settings.get("available_settings", None)) is None + else avail[which_data] + ) + + for name in data_dir: + data = data[name] + + return {tuple(data_dir): data} + + +def _retrieve_data_from_composed_settings( + settings: dict, which_data: str, data_list: list[list[str]] +) -> dict[tuple[str, ...], dict]: + """ + Retrieve a list of dictionary entries from project settings. + + Args: + settings: + which_data: + data_list: + + Returns: + The dictionary where the keys are the data reference name tuple and + the values are data from settings. + """ + + data: dict = ( + settings[which_data] + if (avail := settings.get("available_settings", None)) is None + else avail[which_data] + ) + res: dict[tuple[str, ...], dict] = dict() + + for data_dir in data_list: + for name in data_dir: + data = data[name] + + res.update({tuple(data_dir): data}) + + return res + + +@insert_setting_class("dialect") +def _build_dialects_obj(dialects: dict) -> DialectSettings: + _opts: tuple[DialectOptions, ...] = () + + for name_folder, data in dialects.items(): + for version_folder, content in data.items(): + _opts += DialectOptions( + name=content["name"], + version=content["version"], + version_type=content["version_type"], + package_name=content["package_name"] or "", + name_folder=name_folder, + version_folder=version_folder, + ) + + return DialectSettings(*_opts) + + +@insert_setting_class("llq") +def _build_llq_obj(llqs: dict) -> LLQSettings: + _opts: tuple[LLQOptions, ...] = () + for name_folder, data in llqs.items(): + for version_folder, content in data.items(): + _opts += LLQOptions( + name=content["name"], + version=content["version"], + name_folder=name_folder, + version_folder=version_folder, + package_name=content["package_name"], + ) + + return LLQSettings(*_opts) + + +@insert_setting_class("backend") +def _build_backend_obj(backends: dict) -> TargetBackendSettings: + def _get_executor(executor: dict) -> ExecutorOptions: + shots = ShotsSettings(*tuple(ShotsOptions(**shots_opt) for shots_opt in executor["shots"])) + # implement pass + # passes = PassSettings(*tuple(PassOptions(**pass_opt) for pass_opt in executor["pass"])) + passes = PassSettings() + # implement cast + # casts = CastSettings(*tuple(CastOptions(**cast_opt) for cast_opt in executor["cast"])) + casts = CastSettings() + + return ExecutorOptions(shots=shots, passes=passes, cast=casts) + + def _get_device(device: dict) -> DeviceSettings: + _res = tuple( + DeviceOptions(name=name, max_num_qubits=dv["max_num_qubits"], metadata=dict()) + for name, dv in device.items() + ) + + return DeviceSettings(*_res) + + _opts: tuple[BackendOptions, ...] = () + + for backend_folder, data in backends.items(): + for name_folder, content in data.items(): + _opts += BackendOptions( + name=content["name"], + package_name=content["package_name"], + version=content["version"], + is_simulator=content["is_simulator"], + name_folder=name_folder, + backend_folder=backend_folder, + executor=_get_executor(content["executor"]), + device=_get_device(content["device"]), + ) + + return TargetBackendSettings(*_opts) + + +class ConstructorBaseHhatSettings: + def __new__(cls, setting: str = "default"): + match setting: + case "default": + dialect, llq, backend = settings_classes.get(setting) + return CurrentSettings(dialect=dialect, llq=llq, backend=backend) + + case _: + return settings_classes.get(setting) + + +class OuterSettings(ABC): + """ + Settings abstract class for outer elements, such as dialects, llq and backend classes + """ + + _opts: dict[tuple[str, str], Any] + + def __getitem__(self, item: tuple[str, str]) -> Any | None: + return self._opts.get(item) + + +class InnerSettings(ABC): + """ + Settings abstract class for inner elements, such as executors and devices classes + """ + + _opts: dict[str, Any] + + def __getitem__(self, item: str) -> Any: + return self._opts.get(item) + + +######################### +# BASE SETTINGS CLASSES # +######################### + + +class HhatProjectSettings: + """Class to hold all H-hat project settings (current and available ones).""" + + _project_root: Path + _current: CurrentSettings + _available: AvailableSettings + + def __init__(self, project_root: Path, current: CurrentSettings, available: AvailableSettings): + self._project_root = project_root + self._current = current + self._available = available + + @property + def project_root(self) -> Path: + return self._project_root + + @property + def current(self) -> CurrentSettings: + return self._current + + @property + def available(self) -> AvailableSettings: + return self._available + + @classmethod + def load(cls, settings_path: Path | str) -> HhatProjectSettings: + data = read_file(settings_path) + # TODO: finish implementing it + + +@dataclass +class CurrentSettings: + """ + Use HhatSettings as the main settings class to configure the whole project. + It contains settings for: + + - dialects + - low-level quantum languages (llq) + - target backends (backend) + + Data may be stored in configuration files, such as json, yaml, toml, etc. (according + to implementations available). They should be able to be retrieved by this very same + class through its ``load`` method. + + **Important**: it will load data contained in the "current_settings" section of the + configuration file. + """ + + dialect: DialectSettings + llq: LLQSettings + backend: TargetBackendSettings + + @classmethod + def load(cls, file: Path) -> CurrentSettings: + """ + Loads data from ``file`` (``Path`` type) to a new H-hat settings instance. + """ + + return ConstructorBaseHhatSettings(**read_file(file)) + + def serialize(self) -> dict: + return { + **self.dialect.serialize(), + **self.llq.serialize(), + **self.backend.serialize(), + } + + +class CurrentDialect: + _dialect: Any + _module: ModuleType + _opts: DialectOptions + + def __init__(self, dialect: list[str] | tuple[str, ...], **options: dict): + self._module = import_module(".".join(dialect)) + self._opts = DialectOptions(**options) + + @property + def dialect(self) -> ModuleType: + return self._dialect + + @property + def opts(self) -> DialectOptions: + return self._opts + + +class CurrentLLQ: + _module: dict[tuple[str, str], ModuleType] + _manager: dict[tuple[str, str], BaseLLQManager] + _llq: dict[tuple[str, str], BaseLLQ] + _opts: LLQSettings + + def __init__(self, llq_dir: tuple[tuple[str, str], ...], **options: dict): + self._opts = settings_classes["llq"] + self._module = dict() + self._manager = dict() + self._llq = dict() + + for ld in llq_dir: + self._module = {ld: import_module(".".join(ld))} + # every module must contain the following ``.base.LLQManager`` and ``.base.LLQ`` + self._manager = {ld: self._module[ld].base.LLQManager()} + self._llq = {ld: self._module[ld].base.LLQ()} + + +class CurrentBackend: + _executor: Callable + + +class AvailableSettings: + pass + + +class DialectSettings(OuterSettings): + """ + General dialects settings class. Should hold the list of available + dialect options (``DialectOptions``). + """ + + _opts: dict[tuple[str, str], DialectOptions] + + def __init__(self, *dialects: DialectOptions): + for dialect in dialects: + key = (dialect.name_folder, dialect.version_folder) + if key not in self._opts: + self._opts[key] = dialect + + @property + def opts(self) -> dict[tuple[str, str], DialectOptions]: + return self._opts + + def serialize(self) -> dict: + res = dict() + + for v in self._opts.values(): + res.update(v.serialize()) + + return {"dialect": res} + + +@dataclass +class DialectOptions: + name: str + version: str + version_type: str + name_folder: str + version_folder: Optional[str] = None + package_name: str = field(default="") + + def serialize(self) -> dict: + return { + self.name_folder: { + self.version_folder: { + "name": self.name, + "version": self.version, + "version_type": self.version_type, + "package_name": self.package_name, + } + } + } + + +class LLQSettings(OuterSettings): + _opts: dict[tuple[str, str], LLQOptions] + + def __init__(self, *llqs: LLQOptions): + for llq in llqs: + key = (llq.name_folder, llq.version_folder) + if key not in self._opts: + self._opts[key] = llq + + @property + def opts(self) -> dict[tuple[str, str], LLQOptions]: + return self._opts + + def serialize(self) -> dict: + res = dict() + + for v in self._opts.values(): + res.update(v.serialize()) + + return {"llq": res} + + +@dataclass +class LLQOptions: + name: str + version: str + name_folder: str + version_folder: str + package_name: str = field(default="") + + def serialize(self) -> dict: + return { + self.name_folder: { + self.version_folder: { + "name": self.name, + "version": self.version, + "package_name": self.package_name, + } + } + } + + +class TargetBackendSettings(OuterSettings): + _opts: dict[tuple[str, str], BackendOptions] + + def __init__(self, *backends: BackendOptions): + for backend in backends: + key = (backend.backend_folder, backend.name_folder) + if key not in self._opts: + self._opts[key] = backend + + @property + def opts(self) -> dict[tuple[str, str], BackendOptions]: + return self._opts + + def serialize(self) -> dict: + res = dict() + + for v in self._opts.values(): + res.update(v.serialize()) + + return {"backend": res} + + +@dataclass +class BackendOptions: + name: str + package_name: str + version: str + is_simulator: bool + executor: ExecutorOptions + device: DeviceSettings + backend_folder: str + name_folder: str + + def serialize(self) -> dict: + return asdict(self) + + +@dataclass +class ExecutorOptions: + shots: ShotsSettings + passes: PassSettings + cast: CastSettings + + def serialize(self) -> dict: + return asdict(self) + + +class ShotsSettings(InnerSettings): + """ + Shots settings class. It keeps all the shots options classes under + a single umbrella. Ex:: + + shot_opt1 = ShotsOptions( + name="shots_per_qubit", + base=200, + min=1024, + max=None + ) + shot_opt2 = ShotsOptions( + name="fixed_shots", + base=2048, + min=2048, + max=2048 + ) + + shots_settings = ShotsSettings(shot_opt1, shot_opt2) + + + This will be translated into a configuration file (such as a json file) as:: + + ... + "executor": { + ... + "shots": { + "shots_per_qubit": { + "base": 20, + "min": 1024, + "max": null + }, + "fixed_shots": { + "base": 2048, + "min": 2048, + "max": 2048 + } + } + ... + } + ... + """ + + def __init__(self, *shots_opts: ShotsOptions): + for opt in shots_opts: + if opt.name not in self._opts: + self._opts[opt.name] = opt + + def serialize(self) -> dict: + res = dict() + + for v in self._opts.values(): + res.update(v.serialize()) + + return res + + +@dataclass +class ShotsOptions: + name: str + base: Optional[int] = None + min: Optional[int] = None + max: Optional[int] = None + + def serialize(self) -> dict: + return asdict(self) + + +class PassSettings(InnerSettings): + """TODO: implement it""" + + +@dataclass +class PassOptions: + """TODO: implement it""" + + +class CastSettings: + """TODO: implement it""" + + def __init__(self): + pass + + def serialize(self) -> dict: + return dict() + + +class DeviceSettings(InnerSettings): + """ + Hold all devices information available for a particular target backend. + """ + + def __init__(self, *devices: DeviceOptions): + for device in devices: + if device.name not in self._opts: + self._opts[device.name] = device + + def serialize(self) -> dict: + res = dict() + + for v in self._opts.values(): + res.update(v.serialize()) + + return res + + +@dataclass +class DeviceOptions: + name: str + max_num_qubits: int + metadata: dict = field(default_factory=dict) + + def serialize(self) -> dict: + return asdict(self) diff --git a/python/src/hhat_lang/core/config/utils.py b/python/src/hhat_lang/core/config/utils.py new file mode 100644 index 00000000..574e2e8f --- /dev/null +++ b/python/src/hhat_lang/core/config/utils.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from functools import wraps +from pathlib import Path +from typing import Callable, Any + +conversion_types: dict[str, Callable[[Path], dict | Any]] = dict() +""" +Dictionary to hold all the available conversion types used in H-hat configuration files. +The key is the name, e.g. "json", and the value is the function to execute the proper type +conversion, e.g. ``read_json`` . +""" + + +def insert_reader(ext: str) -> Callable: + def decorator(fn: Callable[[Path], dict | Any]) -> Callable: + @wraps(fn) + def wrapper(arg: Path) -> dict | Any: + return fn(arg) + + conversion_types[ext] = wrapper + return wrapper + + return decorator + + +@insert_reader("json") +def read_json(file: Path) -> dict: + import json + + return json.load(open(file)) + + +@insert_reader("toml") +def read_toml(file: Path) -> Any: + raise NotImplementedError("reading TOML config files for H-hat not implemented yet.") + + +@insert_reader("yaml") +def read_yaml(file: Path) -> dict: + raise NotImplementedError("reading YAML config files for H-hat not implemented yet.") + + +def read_file(file: Path) -> dict | Any: + """ + Reads H-hat's project configuration file. + """ + + read_fn: Callable[[Path], dict | Any] = conversion_types.get(file.name.split(".")[-1]) + return read_fn(file) diff --git a/python/src/hhat_lang/core/core_diagram.svg b/python/src/hhat_lang/core/core_diagram.svg new file mode 100644 index 00000000..3e11e400 --- /dev/null +++ b/python/src/hhat_lang/core/core_diagram.svg @@ -0,0 +1,3 @@ + + +
CORE
error handlers
process
interface
content
representation
code
data
memory
lowlevel
cast
compiler
types
fns
execution
imports
config
diff --git a/python/src/hhat_lang/core/data/core.py b/python/src/hhat_lang/core/data/core.py index b7f42db8..752addf7 100644 --- a/python/src/hhat_lang/core/data/core.py +++ b/python/src/hhat_lang/core/data/core.py @@ -1,9 +1,35 @@ +""" +Syntax examples made in this module are just for anecdotal purposes; +H-hat core's super-module does not have any syntax or grammar definitions. +""" + from __future__ import annotations +import struct +import sys from enum import Enum, auto +from functools import lru_cache, reduce from typing import Any, Iterable -ACCEPTABLE_VALUES: dict = { +from hhat_lang.core.data.utils import has_same_paradigm, isquantum +from hhat_lang.core.error_handlers.errors import ( + ArrayElemsNotSameError, + ArrayQuantumClassicalMixedError, + LiteralTypeMismatchError, +) + + +class AllNoneQuantum(Enum): + """ + Class to be used when checking whether an array is either fully quantum or classical, or mixed. + """ + + AllQuantum = auto() + NoneQuantum = auto() + Mixed = auto() + + +ACCEPTABLE_TYPE_VALUES: dict = { "int": (int,), "u16": (int,), "u32": (int,), @@ -18,95 +44,128 @@ class InvalidType: - """It just exists to be used as 'default' instance for the `ACCEPTABLE_VALUES` above.""" + """ + It just exists to be used as 'default' class check for the ``ACCEPTABLE_TYPE_VALUES`` dict. + """ pass class CompositeGroup(Enum): SymbolAttrs = auto() - Array = auto() + LiteralArray = auto() + + +################## +# SYMBOL SECTION # +################## -class WorkingData: +class Symbol: """ - Defines everything that can work as a literal, a variable, a function - or a type name. + It can be a variable, a function, a type, an argument or a parameter name. """ _value: str - _type: str _is_quantum: bool - _suppress_type: bool + _hash_value: int + __slots__ = ("_value", "_is_quantum", "_hash_value") + + def __init__(self, value: str): + self._value = value + self._is_quantum = value.startswith("@") + self._hash_value = hash(self._value) @property def value(self) -> str: return self._value - @property - def type(self) -> str: - return self._type - @property def is_quantum(self) -> bool: return self._is_quantum - def _op_bitwise(self, op: str, other: Any) -> bool: - if isinstance(other, self.__class__): - return getattr(self.value, op)(other.value) + def __hash__(self) -> int: + return self._hash_value - if isinstance(other, ACCEPTABLE_VALUES.get(self._type, InvalidType)): - return getattr(self.value, op)(other) + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return hash(self) == hash(other) return False - def __hash__(self) -> int: - return hash((self.value, self.type)) + def __bool__(self) -> bool: + """ + This method is for when using truthiness expressions, like:: - def __eq__(self, other: Any) -> bool: - return self._op_bitwise("__eq__", other) + Symbol("a") or Symbol("b") # returns `Symbol("a")` + Symbol("null") or Symbol("x") # returns `Symbol("b")` + Symbol("null") or None # returns `None` + """ - def __le__(self, other) -> bool: - return self._op_bitwise("__le__", other) + return self._value != "null" - def __ge__(self, other) -> bool: - return self._op_bitwise("__ge__", other) + def __repr__(self) -> str: + return f"{self._value}" - def __lt__(self, other) -> bool: - return self._op_bitwise("__lt__", other) - def __ne__(self, other) -> bool: - return self._op_bitwise("__ne__", other) +class Tmp(Symbol): + """ + To be used as a temporary symbol only. Especially useful when handling + quantum functions results without assigning the result to a variable. For + instance:: - def __repr__(self) -> str: - type_txt = "" if self.type is None or self._suppress_type else f":{self.type}" - return f"{self.value}{type_txt}" + @redim(@0) + + This should be defined as ``Tmp("@redim(@0)")`` symbol inside a data + container. + """ + + def append_to_name(self, text: str) -> Tmp: + self._value += f"({text})" + return self -class CompositeWorkingData: +class Alias(Symbol): + """ + Alias to a type or function name """ - Defines everything that can have multiple data grouped together, such as an array - of data, or a variable with attribute/method, or a type or function with their - namespace + + def __init__(self, value: str): + super().__init__(value) + + +class CompositeSymbol: """ + When a symbol has attributes, properties or methods. It can be an import:: - _group: tuple[str, ...] - _type: str - _group_type: CompositeGroup + use(type:math.arithmetic.{add sub}) + + it can be a type member:: + + point.x + sys-flag.ERROR + @bell_t.@source + """ + + _value: tuple[Symbol, ...] + _type: CompositeGroup _is_quantum: bool - _suppress_type: bool + _hash_value: int + __slots__ = ("_value", "_type", "_is_quantum", "_hash_value") - @property - def value(self) -> tuple[str, ...]: - return self._group + def __init__(self, value: tuple[Symbol, ...]): + self._value = value + self._type = CompositeGroup.SymbolAttrs + self._is_quantum = value[0].is_quantum + self._hash_value = hash((hash(self._value), hash(self._type), hash(self._is_quantum))) @property - def type(self) -> str: - return self._type + def value(self) -> tuple[Symbol, ...]: + return self._value @property - def group_type(self) -> CompositeGroup: - return self._group_type + def type(self) -> CompositeGroup: + return self._type @property def is_quantum(self) -> bool: @@ -114,48 +173,50 @@ def is_quantum(self) -> bool: def __eq__(self, other: Any) -> bool: if isinstance(other, self.__class__): - return ( - self._group == other._group - and self._type == other._type - and self._group_type == other._group_type - and self._is_quantum == other._is_quantum - ) + return hash(self) == hash(other) + return False def __hash__(self) -> int: - return hash((self._group, self._type, self._group_type, self._is_quantum)) + return self._hash_value def __iter__(self) -> Iterable: - yield from self._group + return iter(self._value) def __repr__(self) -> str: - txt = "" if self.type is None or self._suppress_type else f":{self.type}" - return " ".join(str(k) for k in self._group) + f"{txt}" + return ".".join(str(k) for k in self._value) -class Symbol(WorkingData): +class AsArray: """ - It can be a variable, a function, a type, an argument or a parameter name. + A class to resolve the representation of array of symbols, as in ``[u32]``. + Usually to be used for types definition. """ - def __init__(self, value: str, symbol_type: str | None = None): + _value: Symbol | CompositeSymbol + _is_quantum: bool + __slots__ = ("_value", "_is_quantum", "_hash_value") + + def __init__(self, value: Symbol | CompositeSymbol): self._value = value - self._type = symbol_type or "str" - self._is_quantum = True if value.startswith("@") else False - self._suppress_type = True + self._is_quantum = value.is_quantum + self._hash_value = hash((self.__class__.__name__, self._value)) + + @property + def value(self) -> Symbol | CompositeSymbol: + return self._value + def __hash__(self) -> int: + return self._hash_value -class CompositeSymbol(CompositeWorkingData): - """ - When a symbol has attributes, properties or methods. - """ + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return hash(self) == hash(other) - def __init__(self, value: tuple[str, ...]): - self._group = value - self._type = "str" - self._group_type = CompositeGroup.SymbolAttrs - self._is_quantum = True if all(k.startswith("@") for k in value) else False - self._suppress_type = True + return False + + def __repr__(self) -> str: + return f"[{self._value}]" class Atomic(Symbol): @@ -166,47 +227,270 @@ class Atomic(Symbol): pass -class CoreLiteral(WorkingData): +class Pointer(Symbol): + """A pointer representation.""" + + pass + + +class Reference(Symbol): + """A reference representation""" + + pass + + +################### +# LITERAL SECTION # +################### + + +class Literal: """ - Any defined literal by the dialect. + Any defined literal by the dialect. Examples:: + + 1 // integer + "hoi quantum!" // string + @4 // quantum unsigned integer + 19.0i // imaginary + + ``Literal``'s ``value`` should be the literal's string and ``lit_type`` argument + should be its type as a ``Symbol`` or ``CompositeSymbol`` instance:: + + Literal("1", Symbol("int")) // H-hat literal for integer 1 + Literal("hoi quantum!", Symbol("str")) // H-hat literal for string + Literal("@4", Symbol("@int")) // H-hat literal for quantum unsigned integer + Literal("19.0i", Symbol("imag")) // H-hat literal for imaginary """ - def __init__(self, value: str, lit_type: str): - if (value.startswith("@") and not lit_type.startswith("@")) or ( - not value.startswith("@") and lit_type.startswith("@") - ): - raise ValueError( - f"Literal got incompatible {value} value and type {lit_type}." - ) + _value: str + _type: Symbol | CompositeSymbol + _is_quantum: bool + _hash_value: int + __slots__ = ("_value", "_type", "_is_quantum", "_hash_value") - self._value = value - self._type = lit_type - self._is_quantum = True if lit_type.startswith("@") else False - self._suppress_type = False - self._bin_form = bin(int(value.strip("@")))[2:] + def __init__(self, value: str, lit_type: Symbol | CompositeSymbol): + if has_same_paradigm(value, lit_type): + self._value = value + self._type = lit_type + self._is_quantum = isquantum(value) + self._hash_value = hash((self.value, self.type)) + + else: + sys.exit(LiteralTypeMismatchError(value, lit_type)()) @property def value(self) -> str: return self._value + @property + def type(self) -> Symbol | CompositeSymbol: + return self._type + + @property + def is_quantum(self) -> bool: + return self._is_quantum + + @lru_cache + def transform_bin(self) -> str: + value: str + + try: + # works if integer + value = bin(int(self.value.strip("@")))[2:] + + except ValueError: + try: + # works if float + value = "".join(f"{k:08b}" for k in struct.pack(">d", float(self.value.strip("@")))) + + except ValueError: + # works if string + value = "".join(f"{ord(s):08b}" for s in self.value) + + return value + + def _op_bitwise(self, op: str, other: Any) -> bool: + # TODO: improve this function to account for custom checks, e.g. for quantum data + + if isinstance(other, self.__class__): + return getattr(self.value, op)(other.value) + + if isinstance(other, ACCEPTABLE_TYPE_VALUES.get(self._type, InvalidType)): + return getattr(self.value, op)(other) + + return False + + def __le__(self, other) -> bool: + return self._op_bitwise("__le__", other) + + def __ge__(self, other) -> bool: + return self._op_bitwise("__ge__", other) + + def __lt__(self, other) -> bool: + return self._op_bitwise("__lt__", other) + + def __ne__(self, other) -> bool: + return self._op_bitwise("__ne__", other) + + def __hash__(self) -> int: + return self._hash_value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return hash(self) == hash(other) + + return False + @property def bin(self) -> str: - return self._bin_form + return self.transform_bin() + + def __repr__(self) -> str: + return f"{self._value}:{self._type}" -class CompositeLiteral(CompositeWorkingData): +class LiteralArray: """ - Mostly to represent array of literals. + Represent an array of literals of the same type:: + + [1 2 3] + [0.2 2.0 200.2 2020.2] + ["h" "o" "i"] """ - pass + _value: tuple[Literal, ...] + _type: Symbol | CompositeSymbol + _is_quantum: bool + _hash_value: int + __slots__ = ("_value", "_type", "_is_quantum", "_hash_value") + + def __init__(self, value: tuple[Literal, ...]): + if has_same_type(value): + + match all_or_none_quantum(value): + case AllNoneQuantum.AllQuantum: + self._is_quantum = True + + case AllNoneQuantum.NoneQuantum: + self._is_quantum = False + + case AllNoneQuantum.Mixed: + sys.exit(ArrayQuantumClassicalMixedError(value)()) + + self._value = value + self._type = value[0].type + self._hash_value = hash(value) + + else: + sys.exit(ArrayElemsNotSameError(value)()) + + @property + def value(self) -> tuple[Literal, ...]: + return self._value + + @property + def type(self) -> Symbol | CompositeSymbol: + return self._type + + @property + def is_quantum(self) -> bool: + return self._is_quantum + + def __hash__(self) -> int: + return self._hash_value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return hash(self) == hash(other) + + return False + + def __iter__(self) -> Iterable: + return iter(self._value) + + def __repr__(self) -> str: + return f"[{' '.join(str(k) for k in self._value)}]" + + +###################################### +# TUPLE OF COMPOSITE OBJECTS SECTION # +###################################### -class CompositeMixData(CompositeWorkingData): +class ObjTuple: """ - Account for all sorts of data in an array or symbol a composition. - It can be an array with literals and variables, a symbol with - multiple attributes or methods (wonder if it's useful to have anyway). + Aggregate multiple data into a tuple. Data can be ``Symbol``, ``Literal``, + ``CompositeSymbol`` and ``CompositeLiteral``. Example:: + + [a b 1.0] + [3 5.0 -12] + [7i "hoi"] + + A composite tuple may not have a proper type definition; it should be thought + as an aggregate of heterogeneous objects. """ - pass + # TODO: implement it + + +############## +# UTIL TOOLS # +############## + +SymbolObj = Symbol | CompositeSymbol | AsArray +LiteralObj = Literal | LiteralArray +SimpleObj = Symbol | Literal +ObjArray = LiteralArray | AsArray + + +def has_correct_paradigm_ordering(*obj_list: SimpleObj | ObjArray) -> bool: + """ + Checks whether a tuple of ``Symbol`` or ``Literal`` has the correct paradigm ordering, + that is: quantum can contain classical but classical cannot contain quantum. Examples:: + + @some.@thing // correct + @other.thing // correct + some.thing // correct + @some.@long.thing // correct + @other.long.thing // correct + + other.@thing // incorrect + @other.long.@thing // incorrect + """ + + _is_quantum = obj_list[0].is_quantum + + for n, obj in enumerate(obj_list[1:]): + if obj.is_quantum and not _is_quantum: + return False + + _is_quantum = obj.is_quantum and _is_quantum + + return True + + +def all_or_none_quantum(value: tuple[Symbol | Literal, ...]) -> AllNoneQuantum: + """ + Check whether all elements are quantum (``AllNoneQuantum.AllQuantum``), + none are quantum (``AllNoneQuantum.NoneQuantum``), or mixed (``AllNoneQuantum.Mixed``). + """ + + if all(k.is_quantum for k in value): + return AllNoneQuantum.AllQuantum + + if any(k.is_quantum for k in value): + return AllNoneQuantum.Mixed + + return AllNoneQuantum.NoneQuantum + + +def has_same_type(value: tuple[Literal, ...]) -> bool: + """Check elements have the same type inside a literal tuple.""" + + def _check_type(_new: Literal | bool, _acc: Literal | bool) -> bool: + if isinstance(_new, Literal) and isinstance(_acc, Literal): + return _acc if _new.type == _acc.type else False + + return False + + return reduce(_check_type, value) diff --git a/python/src/hhat_lang/core/data/fn_def.py b/python/src/hhat_lang/core/data/fn_def.py new file mode 100644 index 00000000..08290de0 --- /dev/null +++ b/python/src/hhat_lang/core/data/fn_def.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +from functools import lru_cache +from typing import Any, Callable + +from hhat_lang.core.code.base import BaseFnCheck, BaseIRBlock +from hhat_lang.core.code.ir_custom import ArgsBlock, ArgsValuesBlock +from hhat_lang.core.data.core import ( + CompositeSymbol, + Literal, + ObjArray, + SimpleObj, + Symbol, +) +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.memory.core import MemoryManager + + +class FnDef: + """ + Function definition class + """ + + _name: Symbol | CompositeSymbol + _type: Symbol | CompositeSymbol + _body: BaseIRBlock + _fn_check: BaseFnCheck + _args: ArgsBlock + """ + function definition arguments must be a special kind of IRBlock + that has ``arg`` and ``value`` attributes and is iterable through + them. + """ + + def __init__( + self, + fn_name: Symbol | CompositeSymbol, + fn_args: ArgsBlock, + fn_body: BaseIRBlock, + fn_type: Symbol | CompositeSymbol | None = None, + ): + if ( + isinstance(fn_name, Symbol | CompositeSymbol) + and isinstance(fn_args, ArgsBlock) + and isinstance(fn_body, BaseIRBlock) + and isinstance(fn_type, Symbol | CompositeSymbol) + or fn_type is None + ): + self._name = fn_name + self._args = fn_args + self._body = fn_body + self._type = fn_type or Symbol("null") + self._fn_check = BaseFnCheck(fn_name=self.name, args_types=self.arg_values) + + else: + raise ValueError( + f"some fn definition type is wrong: " + f"{type(fn_name)} {type(fn_args)} {type(fn_type)} {type(fn_body)}" + ) + + @property + def name(self) -> Symbol | CompositeSymbol: + return self._name + + @property + def type(self) -> Symbol | CompositeSymbol: + return self._type + + @property + def args(self) -> ArgsBlock: + return self._args + + @property + def body(self) -> BaseIRBlock: + return self._body + + @property + @lru_cache + def arg_names(self) -> tuple[SimpleObj | ObjArray, ...]: + if hasattr(self._args, "args"): + return self._args.args # type: ignore [return-value] + + raise ValueError(f"wrong arg names from function definition {self._name}") + + @property + @lru_cache + def arg_values(self) -> tuple[Symbol | CompositeSymbol | BaseIRBlock, ...]: + if hasattr(self._args, "values"): + return self._args.values[0] + + return tuple(v.values[0] for v in self._args.args) + + @property + def fn_check(self) -> BaseFnCheck: + return self._fn_check + + def __repr__(self) -> str: + args = " ".join(str(k) for k in self.args) + fn_header = ( + f"FN-DEF NAME[{self.name}] ARGS[{args}] {f'TYPE[{self.type}]' if self.type else ''}" + ) + body = "\n ".join(str(k) for k in self.body) + return f"{fn_header}" + "\n " + f"{body}" + "\n" + + +class BuiltinFnDef: + """ + Built-in function class definition + """ + + _name: Symbol | CompositeSymbol + _type: Symbol | CompositeSymbol + _body: Callable + """``BuiltinFnDef`` callable""" + _fn_check: BaseFnCheck + _args: ArgsValuesBlock + """ + function definition arguments must be a special kind of IRBlock + that has ``arg`` and ``value`` attributes and is iterable through + them. + """ + + def __init__( + self, + fn_name: Symbol | CompositeSymbol, + fn_args: ArgsValuesBlock, + fn_body: Callable, + fn_type: Symbol | CompositeSymbol | None = None, + ): + if ( + isinstance(fn_name, Symbol | CompositeSymbol) + and isinstance(fn_args, ArgsValuesBlock) + and isinstance(fn_body, Callable) + and isinstance(fn_type, Symbol | CompositeSymbol) + or fn_type is None + ): + self._name = fn_name + self._args = fn_args + self._body = fn_body + self._type = fn_type or Symbol("null") + self._fn_check = BaseFnCheck(fn_name=self.name, args_types=self.arg_values) + + else: + raise ValueError( + f"some fn definition type is wrong: " + f"{type(fn_name)} {type(fn_args)} {type(fn_body)} {type(fn_body)}" + ) + + @property + def name(self) -> Symbol | CompositeSymbol: + return self._name + + @property + def type(self) -> Symbol | CompositeSymbol: + return self._type + + @property + def args(self) -> BaseIRBlock: + return self._args + + @property + def body(self) -> Callable: + """``BuiltinFnDef`` callable""" + return self._body + + @property + @lru_cache + def arg_names(self) -> tuple[SimpleObj | ObjArray, ...]: + if hasattr(self._args, "args"): + return self._args.args # type: ignore [return-value] + + raise ValueError(f"wrong arg names from function definition {self._name}") + + @property + @lru_cache + def arg_values(self) -> tuple[Symbol | CompositeSymbol | BaseIRBlock, ...]: + if hasattr(self._args, "values"): + return self._args.values + + raise ValueError(f"wrong arg values from function definition {self._name}") + + @property + def fn_check(self) -> BaseFnCheck: + return self._fn_check + + def __call__(self, *args: Any, mem: MemoryManager) -> Literal | BaseDataContainer: + return self._body(*args, mem=mem) + + def __repr__(self) -> str: + args = " ".join(str(k) for k in self.args) + fn_header = ( + f"FN-DEF NAME[{self.name}] ARGS[{args}]" + f" {f'TYPE[{self.type}]' if self.type else ''}" + ) + body = "\n " + return f"{fn_header}" + "\n " + f"{body}" + "\n" + + +class OptnDef: + """ + Function with arguments as options (optn) definition class + """ + + # TODO: implement it + + +class BdnDef: + """ + Function with arguments and body (bdn) definition class + """ + + # TODO: implement it + + +class OptBdnDef: + """ + Function with arguments and options in the body (optbdn) definition class + """ + + # TODO: implement it + + +class ModifierDef: + """ + Modifier function + """ + + # TODO: implement it diff --git a/python/src/hhat_lang/core/data/utils.py b/python/src/hhat_lang/core/data/utils.py index b8a08aa1..d3046942 100644 --- a/python/src/hhat_lang/core/data/utils.py +++ b/python/src/hhat_lang/core/data/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import ABC from enum import Enum, auto from typing import Any @@ -12,8 +13,10 @@ class VariableKind(Enum): def isquantum(data: Any) -> bool: + """Check if a given object is quantum""" + if isinstance(data, str): - return True if data.startswith("@") else False + return data.startswith("@") if hasattr(data, "is_quantum"): return data.is_quantum @@ -29,3 +32,7 @@ def has_same_paradigm(data1: Any, data2: Any) -> bool: return True return False + + +class AbstractDataContainer(ABC): + """Abstract data container. To prevent circular imports""" diff --git a/python/src/hhat_lang/core/data/variable.py b/python/src/hhat_lang/core/data/variable.py index 973615e5..b34d390a 100644 --- a/python/src/hhat_lang/core/data/variable.py +++ b/python/src/hhat_lang/core/data/variable.py @@ -1,10 +1,12 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import Any, Iterable -from hhat_lang.core.data.core import WorkingData, Symbol -from hhat_lang.core.data.utils import isquantum, VariableKind +from hhat_lang.core.code.base import BaseIRBlock, BaseIRInstr +from hhat_lang.core.code.ir_custom import BodyBlock +from hhat_lang.core.data.core import CompositeSymbol, Literal, Symbol, SimpleObj +from hhat_lang.core.data.utils import AbstractDataContainer, VariableKind, isquantum from hhat_lang.core.error_handlers.errors import ( ContainerVarError, ContainerVarIsImmutableError, @@ -13,17 +15,21 @@ VariableFreeingBorrowedError, VariableWrongMemberError, ) +from hhat_lang.core.types.utils import AbstractTypeDef, BaseTypeEnum from hhat_lang.core.utils import SymbolOrdered -class BaseDataContainer(ABC): +class BaseDataContainer(AbstractDataContainer): """Data container for constant and variables definitions.""" - _name: Symbol - _type: Symbol + _name: Symbol | CompositeSymbol + _type: Symbol | CompositeSymbol _ds: SymbolOrdered """_ds: data from data structure, e.g. member types and names""" + _ds_type: BaseTypeEnum + """_ds_type: type enum from the data structure""" + _data: SymbolOrdered """_data: where data will actually be stored""" @@ -35,8 +41,8 @@ class BaseDataContainer(ABC): _instr_counter: int """the counter for instructions so quantum instructions can be - processed in ordered fashion; especially useful for quantum or - appendable variables types. For instance, can be used on remote + processed in ordered fashion; especially useful for quantum or + appendable variables types. For instance, can be used on remote instructions for teleportation""" _transferred: bool @@ -49,12 +55,12 @@ class BaseDataContainer(ABC): """if the data is borrowed somewhere else""" @property - def name(self) -> Symbol: + def name(self) -> Symbol | CompositeSymbol: """name of the variable""" return self._name @property - def type(self) -> Symbol: + def type(self) -> Symbol | CompositeSymbol: """type of the variable""" return self._type @@ -108,11 +114,78 @@ def _check_array_prop(cls, data: Any): return False - def _check_assign_ds_vals( + @staticmethod + def _get_single_ds_member( + attr: Symbol | CompositeSymbol, + ) -> Symbol | CompositeSymbol: + return attr + + def _get_struct_ds_member(self, attr: Symbol | CompositeSymbol) -> Symbol | CompositeSymbol: + return self._ds[attr] + + def _get_correct_ds_member(self, attr: Symbol | CompositeSymbol) -> Symbol | CompositeSymbol: + if self._ds_type is BaseTypeEnum.SINGLE: + return self._get_single_ds_member(attr) + + if self._ds_type is BaseTypeEnum.STRUCT: + return self._get_struct_ds_member(attr) + + raise NotImplementedError() + + def _get_data_type(self, data: SimpleObj) -> Symbol | CompositeSymbol: + match data: + case Symbol(): + return data + + case Literal(): + return ( + Symbol(data.type) if isinstance(data.type, str) else CompositeSymbol(data.type) + ) + + case _: + raise NotImplementedError() + + def _check_and_assign_enum_val( + self, data: Symbol | CompositeSymbol | BaseDataContainer + ) -> bool: + """ + Check data from enum type and assign to variable container. + + Args: + - data: enum member as a ``Symbol``, ``CompositeSymbol`` or a struct data + + Returns: + ``True`` if successfully checked and assigned enum data to variable container. + """ + + match data: + case Symbol(): + if data in self._ds: + self._data[0] = data + return True + + case CompositeSymbol(): + if Symbol(data.value[-1]) in self._ds: + self._data[0] = Symbol(data.value[-1]) + return True + + case AbstractTypeDef() | BaseDataContainer(): + if data.name in self._ds: + self._data[0] = data + return True + + raise NotImplementedError() + + case _: + print(f"{type(data)}") + raise NotImplementedError() + + return False + + def _check_and_assign_ds_vals( self, data: Any, - attr_type: Symbol, - # tmp_container: SymbolOrdered + attr_type: Symbol | CompositeSymbol, ) -> bool: """ Check data structure when passing values only (equivalent to `fn(*args)`) and @@ -127,20 +200,40 @@ def _check_assign_ds_vals( False if there is no attribute type. Otherwise, true. """ - if data.type == attr_type: - - # is quantum or array data structure - if data.is_quantum or self._check_array_prop(data): + data_type = self._get_data_type(data) + if data_type == self._get_correct_ds_member(attr_type): + # is quantum + if data.is_quantum: if attr_type in self._ds: + if attr_type in self._data: + self._data[attr_type].append(data) + + else: + match data: + case BaseIRBlock() | BaseIRInstr() | Literal(): + self._data[attr_type] = BodyBlock(data) + case _: + raise ValueError( + f"trying to assign value {data} to {self._name} variable; " + f"value with type {type(data)} not expected" + ) + + self._instr_counter += 1 + return True + + return False + + # is array data structure + if self._check_array_prop(data): + if attr_type in self._ds: if attr_type in self._data: self._data[attr_type].append(data) else: self._data[attr_type] = [data] - self._instr_counter += 1 return True return False @@ -151,11 +244,10 @@ def _check_assign_ds_vals( return False - def _check_assign_ds_args_vals( + def _check_and_assign_ds_args_vals( self, key: Symbol, value: Any, - # tmp_container: SymbolOrdered ) -> bool: """ Check data structure when passing args and values, (equivalent to `fn(**args)`). @@ -168,17 +260,33 @@ def _check_assign_ds_args_vals( """ if key in self._ds: + # is quantum + if key.is_quantum: + if key in self._data: + self._data[key].append(value) - # is quantum or array data structure - if key.is_quantum or self._check_array_prop(value): + else: + match value: + case BaseIRBlock() | BaseIRInstr() | Literal(): + self._data[key] = BodyBlock(value) + + case _: + raise ValueError( + f"trying to assign value {value} to {self._name} variable; " + f"value with type {type(value)} not expected" + ) + + self._instr_counter += 1 + return True + # is array data structure + if self._check_array_prop(value): if key in self._data: self._data[key].append(value) else: self._data[key] = [value] - self._instr_counter += 1 return True # not quantum @@ -190,30 +298,37 @@ def _check_assign_ds_args_vals( return False @abstractmethod - def assign(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: - ... + def assign(self, *args: Any, **kwargs: Any) -> BaseDataContainer | ErrorHandler: + """Assign a data to the variable container""" + + raise NotImplementedError() @abstractmethod def get(self, *args: Any, **kwargs: Any) -> Any | ErrorHandler: - ... + """Retrieve variable content""" + + raise NotImplementedError() def __call__( self, *args: Any, - **kwargs: SymbolOrdered[WorkingData, WorkingData | BaseDataContainer], - ) -> None | ErrorHandler: + **kwargs: SymbolOrdered, + ) -> BaseDataContainer | ErrorHandler: return self.assign(*args, **kwargs) def __iter__(self) -> Iterable: - yield from self._data.items() + return iter(self._data.items()) + + def __repr__(self) -> str: + return f"{self.name}" @abstractmethod def borrow(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: - ... + raise NotImplementedError() @abstractmethod def transfer(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: - ... + raise NotImplementedError() def free(self) -> None | ErrorHandler: """Freeing the container (program going out of container's scope).""" @@ -234,45 +349,50 @@ class VariableTemplate: def __new__( cls, - var_name: Symbol, - type_name: Symbol, - type_ds: SymbolOrdered, + var_name: Symbol | CompositeSymbol, + type_name: Symbol | CompositeSymbol, + ds_data: SymbolOrdered, + ds_type: BaseTypeEnum, flag: VariableKind = VariableKind.IMMUTABLE, ) -> BaseDataContainer | ErrorHandler: - # quantum variables are, at least for now, always appendable and thus mutable if isquantum(var_name) and isquantum(type_name): - return AppendableVariable(var_name, type_name, type_ds, True) + return AppendableVariable(var_name, type_name, ds_data, ds_type, True) if not isquantum(var_name) and not isquantum(type_name): - match flag: - # constant, at least for now, cannot be quantum case VariableKind.CONSTANT: - return ConstantData(var_name, type_name, type_ds) + return ConstantData(var_name, type_name, ds_data, ds_type) case VariableKind.APPENDABLE: - return AppendableVariable(var_name, type_name, type_ds, False) + return AppendableVariable(var_name, type_name, ds_data, ds_type, False) case VariableKind.MUTABLE: - return MutableVariable(var_name, type_name, type_ds) + return MutableVariable(var_name, type_name, ds_data, ds_type) case VariableKind.IMMUTABLE: - return ImmutableVariable(var_name, type_name, type_ds) + return ImmutableVariable(var_name, type_name, ds_data, ds_type) # default for now is immutable case _: - return ImmutableVariable(var_name, type_name, type_ds) + return ImmutableVariable(var_name, type_name, ds_data, ds_type) return VariableCreationError(var_name, type_name) class ConstantData(BaseDataContainer): - def __init__(self, var_name: Symbol, type_name: Symbol, type_ds: SymbolOrdered): + def __init__( + self, + var_name: Symbol | CompositeSymbol, + type_name: Symbol | CompositeSymbol, + ds_data: SymbolOrdered, + ds_type: BaseTypeEnum, + ): self._name = var_name self._type = type_name - self._ds = type_ds + self._ds = ds_data + self._ds_type = ds_type self._data = SymbolOrdered() self._assigned = False self._is_constant = True @@ -289,7 +409,7 @@ def assign(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: raise NotImplementedError() def get(self, member: Symbol | None = None) -> Any | ErrorHandler: - member = next(iter(self._ds.keys())) if member is None else member + member = member or next(iter(self._ds.keys())) if member in self._data: return self._data[member] @@ -304,10 +424,17 @@ def transfer(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: class ImmutableVariable(BaseDataContainer): - def __init__(self, var_name: Symbol, type_name: Symbol, type_ds: SymbolOrdered): + def __init__( + self, + var_name: Symbol | CompositeSymbol, + type_name: Symbol | CompositeSymbol, + ds_data: SymbolOrdered, + ds_type: BaseTypeEnum, + ): self._name = var_name self._type = type_name - self._ds = type_ds + self._ds = ds_data + self._ds_type = ds_type self._data = SymbolOrdered() self._assigned = False self._is_constant = False @@ -322,32 +449,34 @@ def __init__(self, var_name: Symbol, type_name: Symbol, type_ds: SymbolOrdered): def assign( self, *args: Any, - **kwargs: SymbolOrdered[WorkingData, WorkingData | BaseDataContainer], - ) -> None | ErrorHandler: - + **kwargs: SymbolOrdered, + ) -> ImmutableVariable | ErrorHandler: if not self._assigned: + if self._ds_type is BaseTypeEnum.ENUM: + self._check_and_assign_enum_val(args[0]) + return self if len(args) == len(self._ds): - for k, d in zip(args, self._ds): - - if not self._check_assign_ds_vals(k, d): + if not self._check_and_assign_ds_vals(k, d): return ContainerVarError(self.name) elif len(kwargs) == len(self._ds): - for k, v in kwargs.items(): - - if not self._check_assign_ds_args_vals(Symbol(k), v): + if not self._check_and_assign_ds_args_vals(Symbol(k), v): return ContainerVarError(self.name) self._assigned = True - return None + return self return ContainerVarIsImmutableError(self.name) def get(self, member: Symbol | None = None) -> Any | ErrorHandler: - member = next(iter(self._ds.keys())) if member is None else member + if self._ds_type == BaseTypeEnum.ENUM: + # enum type only have a single data stored from its members + return self._data[0] + + member = member or next(iter(self._ds.keys())) if member in self._data: return self._data[member] @@ -364,13 +493,15 @@ def transfer(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: class MutableVariable(BaseDataContainer): def __init__( self, - var_name: Symbol, - type_name: Symbol, - type_ds: SymbolOrdered, + var_name: Symbol | CompositeSymbol, + type_name: Symbol | CompositeSymbol, + ds_data: SymbolOrdered, + ds_type: BaseTypeEnum, ): self._name = var_name self._type = type_name - self._ds = type_ds + self._ds = ds_data + self._ds_type = ds_type self._data = SymbolOrdered() self._assigned = False self._is_constant = False @@ -383,28 +514,36 @@ def __init__( self._instr_counter = 0 def assign( - self, *args: Any, **kwargs: dict[WorkingData, WorkingData | BaseDataContainer] - ) -> None | ErrorHandler: + self, *args: Any, **kwargs: dict[SimpleObj, SimpleObj | BaseDataContainer] + ) -> MutableVariable | ErrorHandler: + if self._ds_type == BaseTypeEnum.ENUM: + self._check_and_assign_enum_val(args[0]) + return self if len(args) == len(self._ds): - for k, d in zip(args, self._ds): - - if not self._check_assign_ds_vals(k, d): + if not self._check_and_assign_ds_vals(k, d): return ContainerVarError(self.name) elif len(kwargs) == len(self._ds): - for k, v in kwargs.items(): + k = Symbol("@" + k[3:]) if k.startswith("q__") else Symbol(k) - if not self._check_assign_ds_args_vals(Symbol(k), v): + if k in self._ds: + if not self._check_and_assign_ds_args_vals(k, v): + return ContainerVarError(self.name) + + else: return ContainerVarError(self.name) self._assigned = True - return None + return self def get(self, member: Symbol | None = None) -> Any | ErrorHandler: - member = next(iter(self._ds.keys())) if member is None else member + if self._ds_type == BaseTypeEnum.ENUM: + return self._data[0] + + member = member or next(iter(self._ds.keys())) if member in self._data: return self._data[member] @@ -421,14 +560,16 @@ def transfer(self, *args: Any, **kwargs: Any) -> None | ErrorHandler: class AppendableVariable(BaseDataContainer): def __init__( self, - var_name: Symbol, - type_name: Symbol, - type_ds: SymbolOrdered, + var_name: Symbol | CompositeSymbol, + type_name: Symbol | CompositeSymbol, + ds_data: SymbolOrdered, + ds_type: BaseTypeEnum, is_quantum: bool, ): self._name = var_name self._type = type_name - self._ds = type_ds + self._ds = ds_data + self._ds_type = ds_type self._data = SymbolOrdered() self._assigned = False self._is_constant = False @@ -443,28 +584,32 @@ def __init__( def assign( self, *args: Any, - **kwargs: SymbolOrdered[WorkingData, WorkingData | BaseDataContainer], - ) -> None | ErrorHandler: + **kwargs: SymbolOrdered, + ) -> AppendableVariable | ErrorHandler: + if self._ds_type == BaseTypeEnum.ENUM: + self._check_and_assign_enum_val(args[0]) + return self if len(args) == len(self._ds): - for k, d in zip(args, self._ds): - - if not self._check_assign_ds_vals(k, d): + if not self._check_and_assign_ds_vals(k, d): return ContainerVarError(self.name) elif len(kwargs) == len(self._ds): - for k, v in kwargs.items(): + k = Symbol("@" + k[3:]) if k.startswith("q__") else Symbol(k) - if not self._check_assign_ds_args_vals(Symbol(k), v): + if not self._check_and_assign_ds_args_vals(k, v): return ContainerVarError(self.name) self._assigned = True - return None + return self def get(self, member: Symbol | None = None) -> Any | ErrorHandler: - member = next(iter(self._ds.keys())) if member is None else member + if self._ds_type == BaseTypeEnum.ENUM: + return self._data[0] + + member = member or next(iter(self._ds.keys())) if member in self._data: return self._data[member] diff --git a/python/src/hhat_lang/core/data/variable2.py b/python/src/hhat_lang/core/data/variable2.py new file mode 100644 index 00000000..22e4fd04 --- /dev/null +++ b/python/src/hhat_lang/core/data/variable2.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.data.core import Symbol, CompositeSymbol +from hhat_lang.core.data.utils import AbstractDataContainer +from hhat_lang.core.types.abstract_base import BaseTypeDef + + +class DataCollection: + pass + + +class DataContainer(AbstractDataContainer): + """ + Data container for constant, variable and temporary data definitions. + """ + + _name: Symbol | CompositeSymbol + _type: BaseTypeDef + _data: DataCollection diff --git a/python/src/hhat_lang/core/error_handlers/errors.py b/python/src/hhat_lang/core/error_handlers/errors.py index 50d0798c..9587cb20 100644 --- a/python/src/hhat_lang/core/error_handlers/errors.py +++ b/python/src/hhat_lang/core/error_handlers/errors.py @@ -2,16 +2,20 @@ from abc import ABC, abstractmethod from enum import Enum, auto - -from hhat_lang.core.data.core import Symbol, WorkingData +from typing import Any class ErrorCodes(Enum): + LITERAL_TYPE_MISMATCH_ERROR = auto() + ARRAY_QUANTUM_CLASSICAL_MIXED_ERROR = auto() + ARRAY_ELEMS_NOT_SAME_ERROR = auto() + INDEX_UNKNOWN_ERROR = auto() INDEX_ALLOC_ERROR = auto() INDEX_VAR_HAS_INDEXES_ERROR = auto() INDEX_INVALID_VAR_ERROR = auto() + TYPE_MEMBER_OVERFLOW_ERROR = auto() TYPE_QUANTUM_ON_CLASSICAL_ERROR = auto() TYPE_AND_MEMBER_NO_MATCH = auto() TYPE_ADD_MEMBER_ERROR = auto() @@ -19,6 +23,10 @@ class ErrorCodes(Enum): TYPE_STRUCT_ASSIGN_ERROR = auto() TYPE_UNION_ASSIGN_ERROR = auto() TYPE_ENUM_ASSIGN_ERROR = auto() + TYPE_MEMBER_NOT_RESOLVED_ERROR = auto() + TYPE_MEMBER_ALREADY_EXISTS_ERROR = auto() + + TYPE_SYMBOL_CONVERSION_ERROR = auto() CONTAINER_VAR_ASSIGN_ERROR = auto() CONTAINER_VAR_IS_IMMUTABLE_ERROR = auto() @@ -31,19 +39,37 @@ class ErrorCodes(Enum): CAST_INT_OVERFLOW_ERROR = auto() CAST_ERROR = auto() + FUNCTION_WRONG_ARGS_TYPES_ERROR = auto() + FUNCTION_WRONG_DATA_ERROR = auto() + FUNCTION_EXECUTION_ERROR = auto() + + INVALID_DATA_CONTAINER_CAST_ERROR = auto() + INVALID_TYPE_CAST_ERROR = auto() + + STACK_FRAME_GET_ERROR = auto() + STACK_FRAME_NOT_FN_ERROR = auto() STACK_EMPTY_ERROR = auto() STACK_OVERFLOW_ERROR = auto() HEAP_INVALID_KEY_ERROR = auto() HEAP_EMPTY_ERROR = auto() + SYMBOLTABLE_INVALID_KEY_ERROR = auto() + INVALID_QUANTUM_COMPUTED_RESULT = auto() INSTR_NOTFOUND_ERROR = auto() INSTR_STATUS_ERROR = auto() + DATA_OVERFLOW_ERROR = auto() + + EVALUATOR_CAST_DATA_ERROR = auto() + EVALUATOR_CAST_WILDCARD_BUILTIN_TYPE_ERROR = auto() + + INTERPRETER_EVALUATION_ERROR = auto() + -class ErrorHandler(ABC): +class ErrorHandler(BaseException, ABC): def __init__(self, error_code: ErrorCodes): self.err_code = error_code @@ -52,18 +78,59 @@ def error_code(self) -> ErrorCodes: return self.err_code @abstractmethod - def __call__(self) -> str: ... + def __call__(self, **kwargs: Any) -> str: ... def __repr__(self) -> str: return f"Error<{self.err_code.name}:{self.err_code.value}>" +################# +# ERROR CLASSES # +################# + + +class LiteralTypeMismatchError(ErrorHandler): + def __init__(self, lit: Any, lit_type: Any): + self._lit = lit + self._lit_type = lit_type + super().__init__(ErrorCodes.LITERAL_TYPE_MISMATCH_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self}]]: literal {self._lit} and type {self._lit_type}" + f" mismatched paradigms; both need be classical or quantum." + ) + + +class ArrayQuantumClassicalMixedError(ErrorHandler): + def __init__(self, array: Any): + self._array = array + super().__init__(ErrorCodes.ARRAY_QUANTUM_CLASSICAL_MIXED_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self}]]: array {self._array} has quantum " + f"and classical data, which is invalid behavior." + ) + + +class ArrayElemsNotSameError(ErrorHandler): + def __init__(self, array: Any): + self._array = array + super().__init__(ErrorCodes.ARRAY_ELEMS_NOT_SAME_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self}]]: array {self._array} has not the" f" same type, which is invalid behavior." + ) + + class IndexUnknownError(ErrorHandler): def __init__(self): super().__init__(ErrorCodes.INDEX_UNKNOWN_ERROR) def __call__(self) -> str: - return f"[[{self.__class__.__name__}]]: Unknown error." + return f"[[{self.__class__.__name__}]]: Index unknown error." class IndexAllocationError(ErrorHandler): @@ -80,7 +147,7 @@ def __call__(self) -> str: class IndexVarHasIndexesError(ErrorHandler): - def __init__(self, var_name: WorkingData | str): + def __init__(self, var_name: Any): self._var = var_name super().__init__(ErrorCodes.INDEX_VAR_HAS_INDEXES_ERROR) @@ -89,7 +156,7 @@ def __call__(self) -> str: class IndexInvalidVarError(ErrorHandler): - def __init__(self, var_name: WorkingData | str): + def __init__(self, var_name: Any): self._var = var_name super().__init__(ErrorCodes.INDEX_INVALID_VAR_ERROR) @@ -97,22 +164,32 @@ def __call__(self) -> str: return f"[[{self.__class__.__name__}]]: Var '{self._var}' not in IndexManager." +class TypeMemberOverflowError(ErrorHandler): + """Cannot have quantum data inside classical data type. The opposite is valid.""" + + def __init__(self): + super().__init__(ErrorCodes.TYPE_MEMBER_OVERFLOW_ERROR) + + def __call__(self, type_name: Any, type_type: Any) -> str: + return ( + f"[[{self.__class__.__name__}]]: too many members for type {type_name} ({type_type})." + ) + + class TypeQuantumOnClassicalError(ErrorHandler): """Cannot have quantum data inside classical data type. The opposite is valid.""" - def __init__(self, q: WorkingData, c: WorkingData): + def __init__(self, q: Any, c: Any): super().__init__(ErrorCodes.TYPE_QUANTUM_ON_CLASSICAL_ERROR) self._q = q self._c = c def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: '{self._q}' cannot be inside '{self._c}'." - ) + return f"[[{self.__class__.__name__}]]: '{self._q}' cannot be inside '{self._c}'." class TypeAndMemberNoMatchError(ErrorHandler): - def __init__(self, m_type: WorkingData, m_member: WorkingData): + def __init__(self, m_type: Any, m_member: Any): super().__init__(ErrorCodes.TYPE_AND_MEMBER_NO_MATCH) self.m_type = m_type self.m_member = m_member @@ -125,7 +202,7 @@ def __call__(self) -> str: class TypeAddMemberError(ErrorHandler): - def __init__(self, member_name: WorkingData): + def __init__(self, member_name: Any): self._member = member_name super().__init__(ErrorCodes.TYPE_ADD_MEMBER_ERROR) @@ -134,7 +211,7 @@ def __call__(self) -> str: class TypeSingleError(ErrorHandler): - def __init__(self, type_name: WorkingData): + def __init__(self, type_name: Any): super().__init__(ErrorCodes.TYPE_SINGLE_ASSIGN_ERROR) self._type_name = type_name @@ -146,7 +223,7 @@ def __call__(self) -> str: class TypeStructError(ErrorHandler): - def __init__(self, type_name: WorkingData): + def __init__(self, type_name: Any): super().__init__(ErrorCodes.TYPE_STRUCT_ASSIGN_ERROR) self._type_name = type_name @@ -158,7 +235,7 @@ def __call__(self) -> str: class TypeUnionError(ErrorHandler): - def __init__(self, type_name: WorkingData): + def __init__(self, type_name: Any): super().__init__(ErrorCodes.TYPE_UNION_ASSIGN_ERROR) self._type_name = type_name @@ -170,7 +247,7 @@ def __call__(self) -> str: class TypeEnumError(ErrorHandler): - def __init__(self, type_name: WorkingData): + def __init__(self, type_name: Any): super().__init__(ErrorCodes.TYPE_ENUM_ASSIGN_ERROR) self._type_name = type_name @@ -181,31 +258,60 @@ def __call__(self) -> str: ) +class TypeMemberNotResolvedError(ErrorHandler): + def __init__(self, type_name: Any, type_member: Any): + super().__init__(ErrorCodes.TYPE_MEMBER_NOT_RESOLVED_ERROR) + self._type_name = type_name + self._type_member = type_member + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: member {self._type_member} cannot" + f" be resolved for type '{self._type_name}'." + ) + + +class TypeMemberAlreadyExistsError(ErrorHandler): + def __init__(self): + super().__init__(ErrorCodes.TYPE_MEMBER_ALREADY_EXISTS_ERROR) + + def __call__(self, name: Any, member_name: Any) -> str: + return f"[[{self.__class__.__name__}]]: member {member_name} already exists on type {name}." + + +class TypeSymbolConversionError(ErrorHandler): + def __init__(self, type_type: Any): + super().__init__(ErrorCodes.TYPE_SYMBOL_CONVERSION_ERROR) + self._type_type = type_type + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: symbol could not be converted; " + f"expected str or array of strs and got {self._type_type}." + ) + + class ContainerVarError(ErrorHandler): - def __init__(self, var_name: WorkingData): + def __init__(self, var_name: Any): super().__init__(ErrorCodes.CONTAINER_VAR_ASSIGN_ERROR) self._var_name = var_name def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: Error assigning value " - f"to variable '{self._var_name}'" - ) + name = self.__class__.__name__ + return f"[[{name}]]: Error assigning value to variable '{self._var_name}'" class ContainerVarIsImmutableError(ErrorHandler): - def __init__(self, var_name: WorkingData): + def __init__(self, var_name: Any): super().__init__(ErrorCodes.CONTAINER_VAR_IS_IMMUTABLE_ERROR) self._var_name = var_name def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: Variable '{self._var_name}' is immutable." - ) + return f"[[{self.__class__.__name__}]]: Variable '{self._var_name}' is immutable." class VariableWrongMemberError(ErrorHandler): - def __init__(self, var_name: WorkingData): + def __init__(self, var_name: Any): super().__init__(ErrorCodes.VARIABLE_WRONG_MEMBER_ERROR) self._var_name = var_name @@ -214,7 +320,7 @@ def __call__(self) -> str: class VariableCreationError(ErrorHandler): - def __init__(self, var_name: WorkingData, var_type: WorkingData): + def __init__(self, var_name: Any, var_type: Any): super().__init__(ErrorCodes.VARIABLE_CREATION_ERROR) self._var_name = var_name self._var_type = var_type @@ -227,7 +333,7 @@ def __call__(self) -> str: class VariableFreeingBorrowedError(ErrorHandler): - def __init__(self, var_name: WorkingData): + def __init__(self, var_name: Any): super().__init__(ErrorCodes.VARIABLE_FREEING_BORROWED_ERROR) self._var_name = var_name @@ -239,7 +345,7 @@ def __call__(self) -> str: class CastNegToUnsignedError(ErrorHandler): - def __init__(self, neg_value: WorkingData, unsigned_value: WorkingData): + def __init__(self, neg_value: Any, unsigned_value: Any): super().__init__(ErrorCodes.CAST_NEG_TO_UNSIGNED_ERROR) self._neg_value = neg_value self._unsigned_value = unsigned_value @@ -252,7 +358,7 @@ def __call__(self) -> str: class CastIntOverflowError(ErrorHandler): - def __init__(self, int_value: WorkingData, limit: Symbol): + def __init__(self, int_value: Any, limit: Any): super().__init__(ErrorCodes.CAST_INT_OVERFLOW_ERROR) self._int_value = int_value self._limit = limit @@ -265,7 +371,7 @@ def __call__(self) -> str: class CastError(ErrorHandler): - def __init__(self, type_cast: Symbol, data: WorkingData): + def __init__(self, type_cast: Any, data: Any): super().__init__(ErrorCodes.CAST_ERROR) self._type_cast = type_cast self._data = data @@ -274,24 +380,92 @@ def __call__(self) -> str: return f"[[{self.__class__.__name__}]]: Cannot cast {self._data} into {self._type_cast}." -class StackEmptyError(ErrorHandler): +class FnWrongArgsTypesError(ErrorHandler): + def __init__(self, values: Any, expected: Any): + self._values = values + self._expected = expected + super().__init__(ErrorCodes.FUNCTION_WRONG_ARGS_TYPES_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: wrong args types; expected {self._expected}," + f" but got {self._values}." + ) + + +class FnWrongDataError(ErrorHandler): + def __init__(self, values: Any): + self._values = values + super().__init__(ErrorCodes.FUNCTION_WRONG_DATA_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: wrong args types; expected literal or data container," + f" but got {self._values}." + ) + + +class InvalidDataContainerCastError(ErrorHandler): + def __init__(self, dc_type: Any, from_type: Any, to_type: Any): + self._dc_type = dc_type + self._from_type = from_type + self._to_type = to_type + super().__init__(ErrorCodes.INVALID_DATA_CONTAINER_CAST_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: invalid data container {self._dc_type} when" + f" casting from {self._from_type} to {self._to_type}." + ) + + +class InvalidTypeCastError(ErrorHandler): + def __init__(self, current: Any, expected: Any): + self._current = current + self._expected = expected + super().__init__(ErrorCodes.INVALID_TYPE_CAST_ERROR) + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: invalid type cast; expected type {self._expected}," + f" but got {self._current}." + ) + + +class StackFrameGetError(ErrorHandler): + def __init__(self, data: Any): + self._data = data + super().__init__(ErrorCodes.STACK_FRAME_GET_ERROR) + + def __call__(self) -> str: + return f"[[{self.__class__.__name__}]]: Stack frame could not retrieve data {self._data}." + + +class StackFrameNotFnError(ErrorHandler): def __init__(self): - super().__init__(ErrorCodes.STACK_EMPTY_ERROR) + super().__init__(ErrorCodes.STACK_FRAME_NOT_FN_ERROR) def __call__(self) -> str: return ( - f"[[{self.__class__.__name__}]]: Stack is empty." + f"[[{self.__class__.__name__}]]: Stack frame is not defined for functions," + f" but tried to used as if." ) +class StackEmptyError(ErrorHandler): + def __init__(self): + super().__init__(ErrorCodes.STACK_EMPTY_ERROR) + + def __call__(self) -> str: + return f"[[{self.__class__.__name__}]]: Stack is empty." + + class StackOverflowError(ErrorHandler): def __init__(self): super().__init__(ErrorCodes.STACK_OVERFLOW_ERROR) def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: Stack overflow." - ) + return f"[[{self.__class__.__name__}]]: Stack overflow." class HeapEmptyError(ErrorHandler): @@ -299,50 +473,121 @@ def __init__(self): super().__init__(ErrorCodes.HEAP_EMPTY_ERROR) def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: Heap is empty." - ) + return f"[[{self.__class__.__name__}]]: Heap is empty." class HeapInvalidKeyError(ErrorHandler): - def __init__(self, key: str | Symbol): + def __init__(self, key: Any): super().__init__(ErrorCodes.HEAP_INVALID_KEY_ERROR) self._key = key def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: key '{self._key}' is invalid." - ) + return f"[[{self.__class__.__name__}]]: key '{self._key}' is invalid." + + +class SymbolTableInvalidKeyError(ErrorHandler): + def __init__(self, key: Any, key_type: str): + super().__init__(ErrorCodes.SYMBOLTABLE_INVALID_KEY_ERROR) + self._key = key + self._key_type = key_type + + @classmethod + def Type(cls) -> str: + return "type" + + @classmethod + def Fn(cls) -> str: + return "fn" + + def __call__(self) -> str: + return f"[[{self.__class__.__name__}]]: key '{self._key}' is invalid for {self._key_type}." class InvalidQuantumComputedResult(ErrorHandler): - def __init__(self, qdata: str | Symbol): + def __init__(self, qdata: Any): super().__init__(ErrorCodes.INVALID_QUANTUM_COMPUTED_RESULT) self._qdata = qdata def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: quantum data {self._qdata} produced invalid result." - ) + return f"[[{self.__class__.__name__}]]: quantum data {self._qdata} produced invalid result." class InstrNotFoundError(ErrorHandler): - def __init__(self, name: str | Symbol): + def __init__(self, name: Any): super().__init__(ErrorCodes.INSTR_NOTFOUND_ERROR) self._name = name def __call__(self) -> str: - return ( - f"[[{self.__class__.__name__}]]: instr {self._name} not found" - ) + return f"[[{self.__class__.__name__}]]: instr {self._name} not found" class InstrStatusError(ErrorHandler): - def __init__(self, name: str | Symbol): + def __init__(self, name: Any): super().__init__(ErrorCodes.INSTR_STATUS_ERROR) self._name = name + def __call__(self) -> str: + return f"[[{self.__class__.__name__}]]: instr {self._name} has status error" + + +class FunctionExecutionError(ErrorHandler): + def __init__(self, *args: Any, fn_name: Any, reason: str): + super().__init__(ErrorCodes.FUNCTION_EXECUTION_ERROR) + self._name = fn_name + self._args = args + self._reason = reason + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: function {self._name} with args {self.args}" + f" failed due to: {self._reason}" + ) + + +class DataOverflowError(ErrorHandler): + def __init__(self, data: Any, data_type: Any, expected_type: Any): + super().__init__(ErrorCodes.DATA_OVERFLOW_ERROR) + self._data_type = data_type + self._expected_type = expected_type + self._data = data + def __call__(self) -> str: return ( - f"[[{self.__class__.__name__}]]: instr {self._name} has status error" + f"[[{self.__class__.__name__}]]: data {self._data} of type {self._data_type}," + f" but attempted to cast into type {self._expected_type} (data overflow)." ) + + +class EvaluatorCastDataError(ErrorHandler): + def __init__(self, data: Any): + super().__init__(ErrorCodes.EVALUATOR_CAST_DATA_ERROR) + self._name = type(data) + self._data = data + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: data {self._data} should be container" + f" or literal, but got {self._name} instead." + ) + + +class EvaluatorCastWildcardBuiltinTypeError(ErrorHandler): + def __init__(self, t_name: Any): + super().__init__(ErrorCodes.EVALUATOR_CAST_WILDCARD_BUILTIN_TYPE_ERROR) + self._name = t_name + + def __call__(self) -> str: + return ( + f"[[{self.__class__.__name__}]]: a precise type should be known, but" + f" a wildcard type was given ({self._name})." + ) + + +class InterpreterEvaluationError(ErrorHandler): + def __init__(self, error_where: str, msg: str): + super().__init__(ErrorCodes.INTERPRETER_EVALUATION_ERROR) + self._msg = msg + self._err = error_where + + def __call__(self) -> str: + return f"[[{self.__class__.__name__}]]<{self._err} error]>: {self._msg}" diff --git a/python/src/hhat_lang/core/execution/abstract_base.py b/python/src/hhat_lang/core/execution/abstract_base.py index 11035b6d..dcd7236b 100644 --- a/python/src/hhat_lang/core/execution/abstract_base.py +++ b/python/src/hhat_lang/core/execution/abstract_base.py @@ -1,33 +1,181 @@ from __future__ import annotations -from typing import Any from abc import ABC, abstractmethod +from typing import Any -from hhat_lang.core.code.ir import TypeIR, BaseFnIR +from hhat_lang.core.code.abstract import BaseIR +from hhat_lang.core.code.ir_graph import IRGraph, IRNode +from hhat_lang.core.data.core import CompositeSymbol, Symbol from hhat_lang.core.memory.core import MemoryManager -class BaseEvaluator(ABC): - _mem: MemoryManager - _type_table: TypeIR - _fn_table: BaseFnIR +################### +# BASE IR SECTION # +################### + + +class BaseIRManager(ABC): + """ + To manage IR code in a graph way, where nodes are IR from files and edges are the + connection between them (uni- or bidirectional). + """ + + _graph: IRGraph @property - def mem(self) -> MemoryManager: - return self._mem + def ir_graph(self) -> IRGraph: + return self._graph + + @abstractmethod + def add_ir(self, *args: Any, **kwargs: Any) -> Any: + """To add IR objects to the IR manager""" + + raise NotImplementedError() + + @abstractmethod + def link_ir( + self, + *refs: Symbol | CompositeSymbol, + ir_importing: BaseIR, + ir_imported: BaseIR, + **kwargs: Any, + ) -> Any: + """ + To link IR objects. When a file (``A``, importing) imports types or functions from + another file (``B``, imported), a directed edge is created from ``A`` to ``B``, that + is ``A`` holds a reference to ``B``, but not the other way around. + """ + + raise NotImplementedError() + + @abstractmethod + def update_ir(self, prev_ir: BaseIR, new_ir: BaseIR) -> Any: + """ + Update IR object to a new one. + """ + + raise NotImplementedError() + + +############################ +# BASE INTERPRETER SECTION # +############################ + + +class BaseInterpreter(ABC): + """ + An abstract execution class. The execution class must hold basic execution + attributes and functionalities, such as parsing and evaluating code. + + Each execution object holds information regarding available quantum devices specs, + quantum target backend and its quantum language as well as their specs, and the H-hat + dialect specs to parse the code and to evaluate it. + """ + + _depth_counter: int + """to count code depth, for memory and scope management; it must be >= 0""" @property - def type_table(self) -> TypeIR: - return self._type_table + def depth_counter(self) -> int: + """ + To count code depth, especially on recursive function calls. + + Returns: + The current integer of the depth counter + """ + + return self._depth_counter + + def inc_depth_counter(self) -> None: + self._depth_counter += 1 + + def dec_depth_counter(self) -> None: + self._depth_counter -= 1 + if self._depth_counter < 0: + raise ValueError("execution depth counter is < 0") + + @abstractmethod + def parse(self, *args: Any, code: str, **kwargs: Any) -> Any: + """ + Parsing the source code to some intermediate representation, + e.g. AST, IR, etc. + """ + + raise NotImplementedError() + + @abstractmethod + def evaluate(self, *args: Any, **kwargs: Any) -> Any: + """ + Evaluates the code using the evaluator instance defined by the + execution specs. + """ + + raise NotImplementedError() + + +######################### +# BASE EXECUTOR SECTION # +######################### + + +class BaseExecutor(ABC): + """ + An abstract executor class. + """ + + _cexec: BaseClassicalEvaluator + _qexec: BaseQuantumEvaluator @property - def fn_table(self) -> BaseFnIR: - return self._fn_table + def cexec(self) -> BaseClassicalEvaluator: + """Classical executor instance.""" + + return self._cexec + + @property + def qexec(self) -> BaseQuantumEvaluator: + """Quantum executor instance.""" + + return self._qexec @abstractmethod - def run(self, code: Any, **kwargs: Any) -> Any: + def run( + self, + *, + code: Any, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + **kwargs: Any, + ) -> Any: + """To be called only once, when calling the instance to execute the code.""" + + raise NotImplementedError() + + @abstractmethod + def walk( + self, + code: Any, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + **kwargs: Any, + ) -> Any: + """ + To run recursively and execute the code. Should not be called directly be + the user, but rather through `run` and internal methods. + """ + raise NotImplementedError() @abstractmethod def __call__(self, *args: Any, **kwargs: Any): raise NotImplementedError() + + +class BaseClassicalEvaluator(ABC): + """Base evaluator for overall classical instructions""" + + +class BaseQuantumEvaluator(ABC): + """Base evaluator for quantum programs""" diff --git a/python/src/hhat_lang/core/execution/abstract_program.py b/python/src/hhat_lang/core/execution/abstract_program.py index afe3ce89..b4514e1a 100644 --- a/python/src/hhat_lang/core/execution/abstract_program.py +++ b/python/src/hhat_lang/core/execution/abstract_program.py @@ -1,22 +1,108 @@ +from __future__ import annotations -from typing import Any from abc import ABC, abstractmethod +from typing import Any, Callable -from hhat_lang.core.code.ir import BlockIR -from hhat_lang.core.data.core import WorkingData +from hhat_lang.core.code.ir_graph import IRNode, IRGraph +from hhat_lang.core.data.core import Literal +from hhat_lang.core.data.variable import BaseDataContainer from hhat_lang.core.error_handlers.errors import ErrorHandler -from hhat_lang.core.execution.abstract_base import BaseEvaluator -from hhat_lang.core.memory.core import IndexManager -from hhat_lang.core.lowlevel.abstract_qlang import BaseLowLevelQLang +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.lowlevel.abstract_qlang import BaseLLQManager +from hhat_lang.core.memory.core import MemoryManager -class BaseProgram(ABC): - _qdata: WorkingData - _idx: IndexManager - _block: BlockIR - _executor: BaseEvaluator - _qlang: BaseLowLevelQLang +class BaseQuantumProgram(ABC): + """Base abstract class to handle quantum programs""" + + _qdata: BaseDataContainer | Literal + _executor: BaseExecutor + _qlang: BaseLLQManager + _mem: MemoryManager + _node: IRNode + _ir_graph: IRGraph @abstractmethod - def run(self) -> Any | ErrorHandler: - ... + def run(self, *args: Any, **kwargs: Any) -> Any | ErrorHandler: + raise NotImplementedError() + + +class QuantumProgram(BaseQuantumProgram): + """ + Class to handle quantum programs. + + It is intended to be used when casting quantum data to classical types:: + + @some-var*some-type + + The program coordinates code execution related to hybrid quantum and + classical instructions. If classical instructions are not present on + the target backend, it fallbacks to the H-hat's dialect implementation + of them; if not present, an error must be raised. For quantum instructions + they must be present on target backend, otherwise an error must be raised. + """ + + def __init__( + self, + qdata: BaseDataContainer | Literal, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + base_llq: Callable[ + [BaseDataContainer, MemoryManager, IRNode, IRGraph, BaseExecutor], + BaseLLQManager, + ], + executor: BaseExecutor, + ): + if ( + isinstance(qdata, BaseDataContainer | Literal) + and isinstance(mem, MemoryManager) + and isinstance(node, IRNode) + and isinstance(ir_graph, IRGraph) + and isinstance(base_llq, type) + and isinstance(executor, BaseExecutor) + ): + self._mem = mem + self._node = node + self._ir_graph = ir_graph + self._qdata = qdata + self._executor = executor + self._qlang = base_llq(qdata, mem, node, ir_graph, executor) + + else: + raise ValueError( + f"some type is invalid:\n" + f" - {qdata}: {type(qdata)}\n" + f" - {mem}: {type(mem)}\n" + f" - {node}: {type(node)}\n" + f" - {ir_graph}: {type(ir_graph)}" + ) + + @property + def mem(self) -> MemoryManager: + return self._mem + + @property + def executor(self) -> BaseExecutor: + return self._executor + + @property + def node(self) -> IRNode: + return self._node + + @property + def ir_graph(self) -> IRGraph: + return self._ir_graph + + @property + def qlang(self) -> BaseLLQManager: + return self._qlang + + def run(self, *args: Any, **kwargs: Any) -> Any | ErrorHandler: + """ + Dialects must implement their own ``Program`` class with this method. + """ + + raise NotImplementedError( + "dialect must implement 'run' method from (quantum) `Program` class." + ) diff --git a/python/src/hhat_lang/core/fns/__init__.py b/python/src/hhat_lang/core/fns/__init__.py new file mode 100644 index 00000000..8525ff7a --- /dev/null +++ b/python/src/hhat_lang/core/fns/__init__.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from pathlib import Path + + +BUILTIN_CORE_MODULE_ROOT_PATH = Path("src/.hat_core/") +""" +Built-in core module root path is to be used for essential functions, +such as ``print``, ``if``, ``match``, ``cast``, etc. + +These functions shall not be imported directly by the user; it must be +internally handled by the dialect compiler. +""" + +BUILTIN_STD_MODULE_ROOT_PATH = Path("src/.hat_std/") +""" +Built-in standard module root path is to be used for standard libraries, +such as ``math`` (with modules such as ``arithmetic``, ``trigonometry``, +``linear-algebra``, etc.), ``io`` (with modules such as ``socket``, etc.), etc. + +These functions must be explicitly imported by the user. +""" diff --git a/python/src/hhat_lang/core/fns/abstract_base.py b/python/src/hhat_lang/core/fns/abstract_base.py new file mode 100644 index 00000000..8081322c --- /dev/null +++ b/python/src/hhat_lang/core/fns/abstract_base.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + + +class BaseBuiltinFnContainer(ABC): + pass diff --git a/python/src/hhat_lang/core/fns/builtin_ir_builder.py b/python/src/hhat_lang/core/fns/builtin_ir_builder.py new file mode 100644 index 00000000..62dea9fe --- /dev/null +++ b/python/src/hhat_lang/core/fns/builtin_ir_builder.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +from hhat_lang.core.code.abstract import BaseIRModule, BaseIR, RefTable +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.core.code.tools import build_reftable +from hhat_lang.core.code.symbol_table import SymbolTable +from hhat_lang.core.fns.core import builtin_fns_path + + +def build_ir_module( + builtin_path: Path, + ir_module: Callable[[Path, SymbolTable, ...], BaseIRModule], + **kwargs: Any, +) -> BaseIRModule: + st = SymbolTable() + + for fn in builtin_fns_path[builtin_path]: + st.fn.add(fn.fn_check, fn) + + return ir_module(builtin_path, st, **kwargs) + + +def build_ir( + builtin_path: Path, + ir_module: Callable[[Path, SymbolTable, ...], BaseIRModule], + ir: Callable[[RefTable, BaseIRModule, ...], BaseIR], + **kwargs: Any, +) -> BaseIR: + """ + Constructs built-in functions IR instance. The ``ir_module`` argument must + be a dialect-specific IRModule class as well as the ``ir`` argument, a + dialect-specific ``IR`` class. If extra arguments are needed for either + ``ir_module`` or ``ir``, they must be placed as keyword arguments. + """ + + # TODO: if there is some function that depends on other references, place + # it in the reftable arguments below: + ref_table = build_reftable() + ir_module = build_ir_module(builtin_path=builtin_path, ir_module=ir_module, **kwargs) + return ir(ref_table, ir_module, **kwargs) + + +def gen_builtin_ir( + builtin_path: Path, + ir_graph: IRGraph, + ir_module: Callable[[Path, SymbolTable, ...], BaseIRModule], + ir: Callable[[RefTable, BaseIRModule, ...], BaseIR], + **kwargs: Any, +) -> None: + """ + Generates a specific built-in module IR instance. + + Args: + builtin_path: built-in module path, usually located at the *__init__.py* + from the dialect's target function folder, e.g. ``math.BUILTIN_FN_PATH`` + ir_graph: the IR graph + ir_module: the dialect-specific IR module as a callable (expecting at least + Path and SymbolTable objects) + ir: the dialect-specific IR as a callable (expecting at least RefTable and + IR module objects) + **kwargs: any extra arguments for both ir_module and ir arguments + """ + + ir_obj = build_ir(builtin_path=builtin_path, ir_module=ir_module, ir=ir, **kwargs) + ir_graph.add_node(ir_obj) + + +def gen_all_builtin_modules( + ir_graph: IRGraph, + ir_module: Callable[[Path, SymbolTable, ...], BaseIRModule], + ir: Callable[[RefTable, BaseIRModule, ...], BaseIR], + **kwargs: Any, +) -> None: + """ + Generates all the built-in modules at once. + + Args: + ir_graph: the IR graph + ir_module: the dialect-specific IR module as a callable (expecting at least + Path and SymbolTable objects) + ir: the dialect-specific IR as a callable (expecting at least RefTable and + IR module objects) + **kwargs: any extra arguments for both ir_module and ir arguments + """ + + for _path, _fn in builtin_fns_path.items(): + gen_builtin_ir(builtin_path=_path, ir_graph=ir_graph, ir_module=ir_module, ir=ir, **kwargs) diff --git a/python/src/hhat_lang/core/fns/core.py b/python/src/hhat_lang/core/fns/core.py new file mode 100644 index 00000000..9e00e12a --- /dev/null +++ b/python/src/hhat_lang/core/fns/core.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import Any, Callable +from functools import wraps +from pathlib import Path + +from hhat_lang.core.code.base import BaseFnKey +from hhat_lang.core.code.ir_custom import ArgsValuesBlock +from hhat_lang.core.data.core import Literal, Tmp +from hhat_lang.core.data.fn_def import BuiltinFnDef +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.memory.core import MemoryManager + + +builtin_fns_path: dict[Path, tuple[BuiltinFnDef, ...]] = dict() + + +def include_builtin_fn(fn_entry: BaseFnKey, fn_path: Path) -> Callable: + def decorator(fn: Callable) -> BuiltinFnDef: + """ + fn argument is the actual built-in function implementation, for instance:: + + builtin_fn_int_add( + CoreLiteral(1, lit_type="int"), + CoreLiteral(1, lit_type="int"), + mem=mem + ) + # outputs '2:int' (e.g. CoreLiteral(2, lit_type="int")) + """ + + @wraps(fn) + def wrapper(*args: Any, mem: MemoryManager) -> Literal | BaseDataContainer: + """ + Built-in function signature implementation. Returns the function call result. + """ + + if isinstance(fn_entry.name, Tmp): + fn_entry.complement_name(" ".join(str(k) for k in args)) + + return _builtin_fn_def(*args, mem=mem) + + args_values = ArgsValuesBlock( + *tuple((a, b) for a, b in zip(fn_entry.args_names, fn_entry.args_types)) + ) + _builtin_fn_def = BuiltinFnDef( + fn_name=fn_entry.name, + fn_args=args_values, + fn_type=fn_entry.type, + fn_body=wrapper, + ) + + if fn_path in builtin_fns_path: + builtin_fns_path[fn_path] += (_builtin_fn_def,) + + else: + builtin_fns_path[fn_path] = (_builtin_fn_def,) + + return _builtin_fn_def + + return decorator diff --git a/python/src/hhat_lang/core/imports/__init__.py b/python/src/hhat_lang/core/imports/__init__.py new file mode 100644 index 00000000..08d8b174 --- /dev/null +++ b/python/src/hhat_lang/core/imports/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .importer import TypeImporter + +__all__ = ["TypeImporter"] diff --git a/python/src/hhat_lang/core/imports/importer.py b/python/src/hhat_lang/core/imports/importer.py new file mode 100644 index 00000000..7787080c --- /dev/null +++ b/python/src/hhat_lang/core/imports/importer.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +from abc import ABC +from pathlib import Path +from typing import Callable, Iterable + +from arpeggio import ParserPython +from arpeggio.cleanpeg import ParserPEG + +from hhat_lang.core.code.abstract import BaseIR +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.core.data.core import CompositeSymbol, Symbol +from hhat_lang.core.fns.core import builtin_fns_path +from hhat_lang.toolchain.project import SOURCE_FOLDER_NAME, SOURCE_TYPES_PATH + + +class BaseImporter(ABC): + _base: Path + _project_root: Path + _parser_fn: Callable[ + [ + Callable[[Callable], ParserPEG | ParserPython], + Callable, + str, + Path, + Path, + IRGraph, + ], + BaseIR, + ] + _grammar_parser: Callable[[Callable], ParserPEG | ParserPython] + _program_rule: Callable + + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable[ + [ + Callable[[Callable], ParserPEG | ParserPython], + Callable, + str, + Path, + Path, + IRGraph, + ], + BaseIR, + ], + ) -> None: + self._project_root = project_root + self._grammar_parser = grammar_parser + self._parser_fn = parser_fn + self._program_rule = program_rule + + @property + def project_root(self) -> Path: + return self._project_root + + @property + def grammar_parser(self) -> Callable[[Callable], ParserPEG | ParserPython]: + return self._grammar_parser + + @property + def program_rule(self) -> Callable: + return self._program_rule + + @property + def parser_fn( + self, + ) -> Callable[ + [ + Callable[[Callable], ParserPEG | ParserPython], + Callable, + str, + Path, + Path, + IRGraph, + ], + BaseIR, + ]: + return self._parser_fn + + @classmethod + def _path_parts(cls, name: CompositeSymbol) -> tuple[tuple[str, ...], str, Symbol]: + parts = tuple(name.value) + + if len(parts) == 1: + # core built-in code + dirs: tuple[str, ...] = () + file_name = "core" + importer_name = parts[0] + + else: + dirs = tuple(k.value for k in parts[:-2]) + file_name = parts[-2].value + importer_name = parts[-1] + + return dirs, file_name, importer_name + + def _get_module_path(self, *path: Path | str) -> Path: + if len(path) == 1 and path == Path("core.hat"): + return path[0] + + return Path().joinpath(self._base, *path[:-1], str(path[-1]) + ".hat") + + def _add_module(self, module_path: Path, ir_graph: IRGraph) -> None: + """To add a new IR module to the graph based on the ``module_path``""" + raw_code: str = module_path.read_text() + self._parser_fn( + self._grammar_parser, + self._program_rule, + raw_code, + self._project_root, + module_path, + ir_graph, + ) + + +class ConstImporter(BaseImporter): + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable, + ): + self._base = Path(project_root).resolve() / SOURCE_FOLDER_NAME + super().__init__(project_root, grammar_parser, program_rule, parser_fn) + + def _retrieve_const_reference(self): + pass + + def import_consts(self): + pass + + +class TypeImporter(BaseImporter): + """Locate and load types under ``src/hat_types`` relative to a project. + + Each ``.hat`` file is scanned for ``type`` declarations and + ``use(type:...)`` statements. Referenced types are resolved recursively. + Circular imports are tolerated during discovery, but a missing type raises + ``FileNotFoundError`` or ``ValueError``. + """ + + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable, + ): + self._base = Path(project_root).resolve() / SOURCE_TYPES_PATH + super().__init__(project_root, grammar_parser, program_rule, parser_fn) + + def _retrieve_type_reference( + self, + name: CompositeSymbol, + ir_graph: IRGraph, + ) -> tuple[Symbol, Path]: + """ + Retrieve type references to be filled at the node's ``RefTypeTable``. + + Args: + name: the type path as ``CompositeSymbol`` + ir_graph: the ``IRGraph`` instance + + Returns: + A tuple with the type name and its module file path + """ + + dir_name, file_name, type_name = self._path_parts(name) + module_path: Path = self._get_module_path(*dir_name, file_name) + + if module_path not in ir_graph: + self._add_module(module_path, ir_graph) + + return type_name, module_path + + def import_types( + self, + names: Iterable[CompositeSymbol], + ir_graph: IRGraph, + ) -> dict[Symbol, Path]: + return dict(self._retrieve_type_reference(name, ir_graph) for name in names) + + +class FnImporter(BaseImporter): + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable, + ): + self._base = Path(project_root).resolve() / SOURCE_FOLDER_NAME + super().__init__(project_root, grammar_parser, program_rule, parser_fn) + + def _retrieve_fn_reference( + self, + name: CompositeSymbol, + ir_graph: IRGraph, + ) -> tuple[tuple[BaseFnCheck, Path], ...]: + """ + Retrieve function references to be filled at the node's ``RefFnTable``. + + Args: + name: the type path as ``CompositeSymbol`` + ir_graph: the ``IRGraph`` instance + + Returns: + Tuple of tuple-pairs containing the function check object and the module file path + """ + + # TODO: fix it as described below + # - for project-defined functions, include only project name and folders + # inside 'src' at the module path, like: "project/src/some_folder/some_file.hat" + # - for external functions: + # - core functions should be at "project/src/.hat_core/core.hat" + # - other modules, such as 'math', like "project/stc/.hat_std/math/other_file.hat" + # - built-in functions, types, etc should be only imported as the last step when + # building the ir graph + + module_path: Path + dir_name, file_name, fn_name = self._path_parts(name) + + module_path = Path(*dir_name) / (file_name + ".hat") + if module_path in builtin_fns_path: + return tuple((fn.fn_check, module_path) for fn in builtin_fns_path[module_path]) + + module_path = self._get_module_path(*dir_name, file_name) + if module_path not in ir_graph: + self._add_module(module_path, ir_graph) + + fns = ir_graph.get_fns(module_path, fn_name) + return tuple((fn, module_path) for fn in fns) + + def import_fns( + self, + names: Iterable[CompositeSymbol], + ir_graph: IRGraph, + ) -> dict[BaseFnCheck, Path]: + res: tuple | tuple[tuple[BaseFnCheck, Path]] = () + + for name in names: + res += self._retrieve_fn_reference(name, ir_graph) + + return dict(res) + + +class ModifierImporter(BaseImporter): + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable, + ): + self._base = Path(project_root).resolve() / SOURCE_FOLDER_NAME + super().__init__(project_root, grammar_parser, program_rule, parser_fn) + + def _retrieve_modifier_reference(self): + pass + + def import_modifiers(self): + pass + + +class MetaModImporter(BaseImporter): + def __init__( + self, + project_root: Path, + grammar_parser: Callable[[Callable], ParserPEG | ParserPython], + program_rule: Callable, + parser_fn: Callable, + ): + self._base = Path(project_root).resolve() / SOURCE_FOLDER_NAME + super().__init__(project_root, grammar_parser, program_rule, parser_fn) + + def _retrieve_metamod_reference(self): + pass + + def import_metamods(self): + pass diff --git a/python/src/hhat_lang/core/imports/utils.py b/python/src/hhat_lang/core/imports/utils.py new file mode 100644 index 00000000..6579fce2 --- /dev/null +++ b/python/src/hhat_lang/core/imports/utils.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Mapping +from typing import Any, Iterable + + +class BaseImports(ABC): + """Base class for importing types and functions""" + + types: Mapping + fns: Mapping diff --git a/python/src/hhat_lang/core/lowlevel/abstract_qlang.py b/python/src/hhat_lang/core/lowlevel/abstract_qlang.py index f6cb9708..43adbb6f 100644 --- a/python/src/hhat_lang/core/lowlevel/abstract_qlang.py +++ b/python/src/hhat_lang/core/lowlevel/abstract_qlang.py @@ -3,55 +3,63 @@ from abc import ABC, abstractmethod from typing import Any -from hhat_lang.core.data.core import WorkingData -from hhat_lang.core.execution.abstract_base import BaseEvaluator -from hhat_lang.core.memory.core import IndexManager -from hhat_lang.dialects.heather.code.simple_ir_builder.ir import IRBlock +from hhat_lang.core.code.ir_graph import IRNode, IRGraph +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.memory.core import MemoryManager -class BaseLowLevelQLang(ABC): +class BaseLLQManager(ABC): """ - Hold H-hat quantum data to transform into low-level + Manager to hold H-hat quantum data and transform it into low-level quantum-specific language. """ - _qdata: WorkingData - _num_idxs: int - _code: IRBlock - _idx: IndexManager - _executor: BaseEvaluator + _qdata: BaseDataContainer + _mem: MemoryManager + _node: IRNode + _ir_graph: IRGraph + _executor: BaseExecutor def __init__( self, - qvar: WorkingData, - code: IRBlock, - idx: IndexManager, - executor: BaseEvaluator, + qdata: BaseDataContainer, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + executor: BaseExecutor, *_args: Any, - **_kwargs: Any + **_kwargs: Any, ): - self._qdata = qvar - self._code = code - self._idx = idx + self._qdata = qdata + self._mem = mem self._executor = executor - self._num_idxs = len(self._idx.in_use_by.get(self._qdata, [])) + self._node = node + self._ir_graph = ir_graph @abstractmethod - def init_qlang(self) -> tuple[str, ...]: - ... + def compile(self, *args: Any, **kwargs: Any) -> BaseLLQ: + """ + The compile method should return a ``BaseQLang`` child class object. It then + can be used inside a target backend evaluator to execute code on simulator/device. + """ - @abstractmethod - def end_qlang(self) -> tuple[str, ...]: - ... + raise NotImplementedError() - @abstractmethod - def gen_instrs(self, *args: Any, **kwargs: Any) -> tuple[str, ...]: - ... - @abstractmethod - def gen_program(self, *args: Any, **kwargs: Any) -> str: - ... +class BaseLLQ(ABC): + """ + Base class for (low level) quantum language (aka OpenQASM, NetQASM, etc.) implementation. + """ + + def __init__(self, *args: Any, **kwargs: Any): + pass @abstractmethod - def __call__(self, *args: Any, **kwargs: Any) -> Any: - ... + def code(self) -> Any: + """ + Use this method to implement code generation for specific quantum language. + It should return the correct type for the target backend instance. + """ + + raise NotImplementedError() diff --git a/python/src/hhat_lang/core/memory/core.py b/python/src/hhat_lang/core/memory/core.py index 6a7fcc99..c36a110e 100644 --- a/python/src/hhat_lang/core/memory/core.py +++ b/python/src/hhat_lang/core/memory/core.py @@ -1,22 +1,36 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from collections import deque -from queue import LifoQueue +import sys +from abc import ABC +from collections import OrderedDict, deque +from copy import deepcopy +from enum import Enum, auto +from typing import Any, Hashable, cast from uuid import UUID +from hhat_lang.core.code.base import BaseFnCheck, BaseIRBlock from hhat_lang.core.data.core import ( - Symbol, CoreLiteral, CompositeLiteral, CompositeMixData, - WorkingData + LiteralArray, + ObjTuple, + CompositeSymbol, + ObjArray, + Literal, + Symbol, + SimpleObj, ) from hhat_lang.core.data.variable import BaseDataContainer from hhat_lang.core.error_handlers.errors import ( ErrorHandler, + FnWrongArgsTypesError, + HeapInvalidKeyError, IndexAllocationError, + IndexInvalidVarError, IndexUnknownError, IndexVarHasIndexesError, - HeapInvalidKeyError, IndexInvalidVarError, + StackFrameGetError, + StackFrameNotFnError, ) +from hhat_lang.core.utils import gen_uuid class PIDManager: @@ -25,10 +39,10 @@ class PIDManager: """ def new(self) -> UUID: - pass + raise NotImplementedError() def list(self) -> list[UUID]: - pass + raise NotImplementedError() class IndexManager: @@ -36,22 +50,34 @@ class IndexManager: Holds and manages information about the indexes (qubits) availability and allocation. Properties - - `max_number`: maximum number of allowed indexes - - `available`: deque with all the available indexes - - `allocated`: deque with all the allocated indexes - - `in_use_by`: dictionary containing the allocator variable as key and deque with allocated indexes as value + ``max_number``: maximum number of allowed indexes + + ``available``: deque with all the available indexes + + ``allocated``: deque with all the allocated indexes + + ``resources``: variable members and literals with respective number of indexes + requested + + ``in_use_by``: dictionary containing the allocator variable member as key and + deque with allocated indexes as value Methods - - `request`: given a variable (`Symbol`) and the number of indexes (`int`), allocate the number if it has enough space - - `free`: given a variable (`Symbol`), free all the allocated indexes + ``add``: add a variable member or literal with a requested number of indexes to + the resources dictionary + + ``request``: given a variable member (``Symbol``) and the number of indexes + (``int``), allocate the number if it has enough space + + ``free``: given a variable member (``Symbol``), free all the allocated indexes """ _max_num_index: int _num_allocated: int _available: deque _allocated: deque - _resources: dict[WorkingData, int] - _in_use_by: dict[WorkingData, deque] + _resources: dict[SimpleObj | ObjArray, int] + _in_use_by: dict[SimpleObj | ObjArray, deque] def __init__(self, max_num_index: int): self._max_num_index = max_num_index @@ -77,31 +103,44 @@ def allocated(self) -> deque: return self._allocated @property - def resources(self) -> dict[WorkingData, int]: + def resources(self) -> dict[SimpleObj | ObjArray, int]: """ - Dictionary containing the variable(s)/literal(s) and + Dictionary containing the variable members/literal(s) and the index amount requested. """ return self._resources @property - def in_use_by(self) -> dict[WorkingData, deque]: + def in_use_by(self) -> dict[SimpleObj | ObjArray, deque]: """ - Dictionary containing the variable(s)/literal(s) with + Dictionary containing the variable members/literal(s) with the deque of indexes provided. """ return self._in_use_by + def __getitem__(self, item: SimpleObj | ObjArray) -> deque | IndexInvalidVarError: + """Return the deque of indexes from a quantum data.""" + + if res := self._in_use_by.get(item, False): + return res # type: ignore [return-value] + + return IndexInvalidVarError(var_name=item) + + def __contains__(self, item: SimpleObj | ObjArray) -> bool: + """Checks whether there is item in the IndexManager.""" + + return item in self._in_use_by + def _alloc_idxs(self, num_idxs: int) -> deque | IndexAllocationError: - available = (self._max_num_index - self._num_allocated) + available = self._max_num_index - self._num_allocated if available >= num_idxs: - _data = tuple() + _data: tuple = tuple() for _ in range(0, num_idxs): - _data += self._available.popleft(), + _data += (self._available.popleft(),) self._num_allocated += 1 return deque( @@ -111,57 +150,55 @@ def _alloc_idxs(self, num_idxs: int) -> deque | IndexAllocationError: return IndexAllocationError(requested_idxs=num_idxs, max_idxs=available) - def _alloc_var(self, var_name: WorkingData, idxs_deque: deque) -> None: - self._in_use_by[var_name] = idxs_deque + def _alloc_var(self, member_name: SimpleObj | ObjArray, idxs_deque: deque) -> None: + self._in_use_by[member_name] = idxs_deque self._allocated.extend(idxs_deque) - def _has_var(self, var_name: WorkingData) -> bool: - return var_name in self._resources + def _has_var(self, member_name: SimpleObj | ObjArray) -> bool: + return member_name in self._resources - def _free_var(self, var_name: WorkingData) -> deque: + def _free_var(self, member_name: SimpleObj | ObjArray) -> deque: """ - Free variable's indexes and allocated deque with those indexes. + Free variable member's indexes and allocated deque with those indexes. """ - idxs = self._in_use_by.pop(var_name) + idxs = self._in_use_by.pop(member_name) for k in idxs: self._allocated.remove(k) return idxs - def add(self, var_name: WorkingData, num_idxs: int) -> None | ErrorHandler: + def add(self, member_name: SimpleObj | ObjArray, num_idxs: int) -> None | ErrorHandler: """ - Add a variable/literal with a given number of indexes required for it. + Add a variable member/literal with a given number of indexes required for it. The amount will be used upon request through the `request` method. """ if (self._num_allocated + num_idxs) <= self._max_num_index: - - if var_name not in self._resources: - self._resources[var_name] = num_idxs + if member_name not in self._resources: + self._resources[member_name] = num_idxs return None - return IndexVarHasIndexesError(var_name) + return IndexVarHasIndexesError(member_name) return IndexAllocationError(requested_idxs=num_idxs, max_idxs=self._num_allocated) - def request(self, var_name: WorkingData) -> deque | ErrorHandler: + def request(self, member_name: SimpleObj | ObjArray) -> deque | ErrorHandler: """ Request a number of indexes given by the `resources` property for - a variable `var_name`. + a variable member `var_name`. """ - if not (num_idxs := self._resources.get(var_name, False)): - return IndexInvalidVarError(var_name) + if not (num_idxs := self._resources.get(member_name, False)): + return IndexInvalidVarError(member_name) match x := self._alloc_idxs(num_idxs): - case deque(): - if not self._has_var(var_name): - return IndexInvalidVarError(var_name=var_name) + if not self._has_var(member_name): + return IndexInvalidVarError(var_name=member_name) - self._alloc_var(var_name, x) + self._alloc_var(member_name, x) return x case IndexAllocationError(): @@ -169,12 +206,12 @@ def request(self, var_name: WorkingData) -> deque | ErrorHandler: return IndexUnknownError() - def free(self, var_name: WorkingData) -> None: + def free(self, member_name: SimpleObj | ObjArray) -> None: """ - Free indexes from a given variable `var_name`. + Free indexes from a given variable member `var_name`. """ - idxs = self._free_var(var_name) + idxs = self._free_var(member_name) self._available.extend(idxs) self._num_allocated -= len(idxs) @@ -184,56 +221,258 @@ def free(self, var_name: WorkingData) -> None: ######################### -class BaseStack(ABC): - _data: LifoQueue +class StackFrame: + """Stack memory frame. To be used inside ``Stack`` instance whenever a new scope is needed""" + + _data: OrderedDict[ + SimpleObj | CompositeSymbol | BaseFnCheck, + BaseDataContainer | Literal | None, + ] + _fn_header: BaseFnCheck | None + _for_fn_use: bool + + def __init__(self, for_fn_use: bool = False): + self._data = OrderedDict() + self._for_fn_use = for_fn_use - @abstractmethod - def push(self, data: MemoryDataTypes) -> None: - pass + @property + def keys( + self, + ) -> tuple[SimpleObj | ObjArray | BaseFnCheck, ...] | tuple: + return tuple(self._data.keys()) - @abstractmethod - def pop(self) -> MemoryDataTypes: - pass + @property + def for_fn_use(self) -> bool: + return self._for_fn_use + + def add_no_assign(self, key: Symbol | CompositeSymbol) -> None: + if key not in self._data and isinstance(key, SimpleObj): + self._data[key] = None + + def add( + self, + key: Symbol | CompositeSymbol | Literal, + value: BaseDataContainer | Literal, + ) -> None: + if ( + isinstance(key, Symbol | CompositeSymbol | Literal) + and (key not in self._data or self._data[key] is None) # type: ignore [index] + and isinstance(value, BaseDataContainer | Literal) + ): + self._data[key] = value + + def add_fn_header(self, header: BaseFnCheck) -> None: + """First thing to be added on the stack frame instance if it is used for a function.""" + + if isinstance(header, BaseFnCheck): + self._fn_header = header + + def _check_fn_args_types(self, *values_types: BaseDataContainer | Literal) -> bool: + if self._for_fn_use: + return all( + cast(BaseFnCheck, self._fn_header).check_args_types( + k.type + if isinstance(k, BaseDataContainer) + else (Symbol(k.type) if isinstance(k.type, str) else CompositeSymbol(k.type)) + ) + for k in values_types + ) - @abstractmethod - def peek(self) -> MemoryDataTypes: - pass + sys.exit(StackFrameNotFnError()()) + def add_ordered(self, *values: BaseDataContainer | Literal) -> None: + """ + **Note**: to be used only for functions, on its startup parameters declaration. + + Use when no argument name is provided and the ``*values`` are assumed to be in + the correct order + """ + + if self._for_fn_use: + if self._check_fn_args_types(*values): + for k, v in zip(self._data, values): + self._data[k] = v + + return + + sys.exit( + FnWrongArgsTypesError( + values=values, + expected=cast(BaseFnCheck, self._fn_header)._args_types, + )() + ) + + # if no function-use stack frame defined, error is raised + sys.exit(StackFrameNotFnError()()) + + def get( + self, item: SimpleObj | CompositeSymbol | BaseFnCheck + ) -> BaseDataContainer | Literal | ErrorHandler: + return self._data.get(item) or StackFrameGetError(item) + + def pop(self) -> BaseDataContainer | Literal: + """Pops last value from ``StackFrame`` (data container or literal)""" + + return self._data.popitem()[-1] + + def __contains__(self, item: Any) -> bool: + return item in self._data + + +class Stack: + """ + Stack memory handling data inside frames according to scopes that appears in Lifo order. + """ + + class EntryType(Enum): + VALUE_ONLY = auto() + ARG_VALUE = auto() + + _data: list[StackFrame] | list + _entry_stack: ( + list[BaseDataContainer | Literal | tuple[Symbol, BaseDataContainer | Literal]] | list + ) + _entry_type: Stack.EntryType + _return_stack: list[BaseDataContainer | Literal] | list -class Stack(BaseStack): def __init__(self): - self._data = LifoQueue() + self._data = [] + self._entry_stack = [] + self._return_stack = [] - def push(self, data: MemoryDataTypes) -> None: - self._data.put(data) + def new(self, for_fn_use: bool = False) -> None: + """Push a new ``StackFrame`` instance to the stack""" - def pop(self) -> MemoryDataTypes: - return self._data.get() + self._data.append(StackFrame(for_fn_use)) - def peek(self) -> MemoryDataTypes: - """Expensive method to 'peek' the last item from the stack.""" + def push(self, data: BaseDataContainer | Literal) -> None: + """Push ``data`` into current stack's frame as its new last item""" - last_item = self._data.get() - self._data.put(last_item) - return last_item + if isinstance(data, BaseDataContainer): + self._data[-1].add(data.name, data) # type: ignore [arg-type] + else: + self._data[-1].add(data, data) -class BaseHeap(ABC): - _data: dict[Symbol, BaseDataContainer] + def get(self, item: SimpleObj | CompositeSymbol) -> BaseDataContainer | Literal: + """Retrieves data from the current stack frame""" - @abstractmethod - def set(self, key: Symbol, value: BaseDataContainer) -> None: - pass + match res := self._data[-1].get(item): + case ErrorHandler(): + sys.exit(res()) - @abstractmethod - def get(self, key: Symbol) -> BaseDataContainer: - pass + case _: + return res - def __getitem__(self, item: Symbol) -> BaseDataContainer: - return self.get(item) + def pop(self) -> BaseDataContainer | Literal: + """Pops last element from current ``StackFrame`` (either data container or literal)""" + return self._data[-1].pop() + + def set_fn_entry( + self, + *values: BaseDataContainer | Literal, + fn_header: BaseFnCheck, + **args_values: BaseDataContainer | Literal, + ) -> None: + """ + Set the function entry, i.e. it's arguments. It can be through only + arguments (``*values``) or through keyword arguments (``**args_values``). + + It will be consumed by the function once the stack frame is initialized + for it. + + Args: + *values: ``BaseDataContainer`` or ``CoreLiteral`` data + fn_header: ``BaseFnCheck`` instance + **args_values: ``BaseDataContainer`` or ``CoreLiteral`` data + """ + + assert (values and not args_values) or ( + not values and args_values + ), "stack frame must have either values org args values-pair" + + if isinstance(fn_header, BaseFnCheck): + self._data[-1].add_fn_header(fn_header) + + if values: + self._entry_stack.extend(value for value in values) + self._entry_type = Stack.EntryType.VALUE_ONLY + return + + self._entry_stack.extend((Symbol(arg), value) for arg, value in args_values.items()) + self._entry_type = Stack.EntryType.ARG_VALUE + + def get_fn_entry(self) -> None: + """ + Retrieve function entry for the function stack frame. + """ + + if self._data[-1].for_fn_use: + match self._entry_type: + case Stack.EntryType.ARG_VALUE: + for arg, value in self._entry_stack: + self._data[-1].add(arg, value) + + case Stack.EntryType.VALUE_ONLY: + self._data[-1].add_ordered(*self._entry_stack) + + sys.exit(StackFrameNotFnError()()) + + def set_fn_return(self, item: BaseDataContainer | Literal) -> None: + """ + Add a function return to a special space in the stack; to be + retrieved by the newest last stack frame + """ + + self._return_stack = [item] + + def get_fn_return(self) -> BaseDataContainer | Literal: + """ + After the function is finished and its return value is properly + addressed, this method must be used to clean the queue from + function returns. Its output is the value being hold (possibly + to be used by another stack frame). + """ + + return_res = deepcopy(self._return_stack)[0] + self._return_stack = [] + return return_res + + def free(self) -> None: + """Free last frame from stack""" + + self._data.pop() + + def __contains__(self, item: Any) -> bool: + """Always check in the last stack frame added""" + return item in self._data[-1] + + def __enter__(self) -> Stack: + """ + This method must be invoked only when ``MemoryManager.new_stack_fn`` method + is used with ``with``. + """ + + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any: + if not (exc_type or exc_val or exc_tb): + # TODO: improve this error handling + sys.exit(exc_type or exc_val or exc_tb) + + return_result = self.get_fn_return() + # free function context stack + self.free() + # push return result to the former previous stack (now current one again) + self.push(return_result) + + +class Heap: + """Heap memory handling data of dynamic size""" + + _data: dict[Symbol, BaseDataContainer] -class Heap(BaseHeap): def __init__(self): self._data = dict() @@ -245,10 +484,133 @@ def set(self, key: Symbol, value: BaseDataContainer) -> None | HeapInvalidKeyErr return None def get(self, key: Symbol) -> BaseDataContainer | HeapInvalidKeyError: - if not (var_data:= self._data.get(key, False)): + """ + Given a key, returns its data which can be a variable container (variable content), + a working data (symbol, literal) or composite working data. + """ + + if (var_data := self._data.get(key, None)) is None: return HeapInvalidKeyError(key=key) - return var_data + return var_data # type: ignore [return-value] + + def free(self, key: Symbol) -> HeapInvalidKeyError | None: + """ + To free a given key from the heap. It must be called every time the heap goes out of scope + """ + + if not self._data.pop(key, False): + return HeapInvalidKeyError(key=key) + + return None + + def __contains__(self, item: Symbol) -> bool: + return item in self._data + + def __getitem__(self, item: Symbol) -> BaseDataContainer: + match res := self.get(item): + case BaseDataContainer(): + return res + + case HeapInvalidKeyError(): + sys.exit(res()) + + case _: + raise ValueError("could not get heap value") + + +class ScopeValue: + """Holds a value for scopes""" + + _value: int + _counter: int + + def __init__(self, obj: Hashable, *, counter: int): + """ + Hold a value for scope. + + Args: + obj: object must be hashable + counter: from the execution counter, to keep track of scope nesting + """ + + self._value = gen_uuid(gen_uuid(obj) + counter) + self._counter = counter + + @property + def value(self) -> int: + return self._value + + @property + def counter(self) -> int: + return self._counter + + def __hash__(self) -> int: + return self._value + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ScopeValue): + return self._value == other._value + + if isinstance(other, int): + return self._value == other + + return False + + def __repr__(self) -> str: + return f"S#{self._value}" + + +class Scope: + """Defines a scope for stack and heap memory allocation""" + + _table: OrderedDict[ScopeValue, Heap] + + def __init__(self): + self._heap = dict() + + @property + def table(self) -> OrderedDict[ScopeValue, Heap]: + return self._table + + def new(self, scope: ScopeValue) -> Any: + """Define a new scope""" + if isinstance(scope, ScopeValue): + self._table[scope] = Heap() + + else: + # TODO: maybe create a error handler for it? + raise ValueError(f"value scope must be ScopeValue, got {type(scope)}") + + def last(self) -> ScopeValue: + """ + Get the last ``ScopeValue``, having an ``OrderedDict`` object, will always + return the key-value pairs in insertion order. + """ + + return next(reversed(self._table)) + + def free(self, scope: ScopeValue) -> ScopeValue | None: + """ + Free scope heap memory. Must be called every time the scope is finished. + + Returns: + The ``ScopeValue`` object where the return data was placed, + if ``to_return`` is set to ``True``. ``False`` by default. Otherwise, + ``None`` is returned + """ + + self._table.pop(scope) + return None + + def __len__(self) -> int: + return len(self._table) + + def __contains__(self, item: ScopeValue) -> bool: + return item in self._table + + def __getitem__(self, item: ScopeValue) -> Heap: + return self._table[item] ######################## @@ -257,49 +619,104 @@ def get(self, key: Symbol) -> BaseDataContainer | HeapInvalidKeyError: class BaseMemoryManager(ABC): - - _idx: IndexManager - _stack: BaseStack - _heap: BaseHeap - _pid: PIDManager + _heap: Scope + _stack: Stack + _cur_scope: ScopeValue @property - def index(self) -> IndexManager: - return self._idx + def heap(self) -> Scope: + return self._heap @property - def stack(self) -> BaseStack: + def stack(self) -> Stack: return self._stack @property - def heap(self) -> BaseHeap: - return self._heap + def cur_scope(self) -> ScopeValue: + return self._cur_scope - @property - def pid(self) -> PIDManager: - return self._pid + def new_fn_stack(self, *args: Any, fn_header: BaseFnCheck) -> Stack: + self._stack.new(for_fn_use=True) + self._stack.set_fn_entry(*args, fn_header=fn_header) + return self._stack class MemoryManager(BaseMemoryManager): - """Manages the stack, heap, pid, and index.""" + """Manages the stack and heap per scope, pid, and indexes.""" + + def __init__(self, *, ir_block: BaseIRBlock, depth_counter: int): + if isinstance(ir_block, BaseIRBlock) and isinstance(depth_counter, int): + self._stack = Stack() + self._heap = Scope() + self._cur_scope = ScopeValue(obj=ir_block, counter=depth_counter) + self._heap.new(self._cur_scope) + + else: + raise ValueError( + "memory manager needs IR block object, and execution code depth counter" + ) - def __init__(self, max_num_index: int): - self._stack = Stack() - self._heap = Heap() - self._pid = PIDManager() - self._idx = IndexManager(max_num_index) + def new_scope(self, ir_block: BaseIRBlock, depth_counter: int) -> ScopeValue: + scope_value = ScopeValue(ir_block, counter=depth_counter) + self._heap.new(scope_value) + self._cur_scope = scope_value + return scope_value - @property - def stack(self) -> BaseStack: - return self._stack + def free_scope(self, scope: ScopeValue, to_return: bool = False) -> None: + self._heap.free(scope=scope) - @property - def heap(self) -> BaseHeap: - return self._heap + if scope == self._cur_scope: + if len(self._heap) > 0: + self._cur_scope = self._heap.last() + + else: + # no more scope, the execution should have reached the end of the code + # TODO: double check later what to do in this case + pass + + def free_last_scope(self, to_return: bool = False) -> None: + if len(self._heap) > 0: + last_scope = self._heap.last() + self._heap.free(scope=last_scope) + + if len(self._heap) > 0: + self._cur_scope = self._heap.last() + + else: + # TODO: what to do next + pass + + else: + raise ValueError("trying to free last scope, but no more scope is left; mind is empty") + + +class QuantumMemoryManager(MemoryManager): + """ + A quantum version of memory manager to execute quantum programs containing both classical + and quantum instructions. It is a superset of ``MemoryManager`` because it includes + ``IndexManager``. + """ + + _idx: IndexManager + + def __init__(self, *, ir_block: BaseIRBlock, max_num_index: int, depth_counter: int = 0): + if isinstance(max_num_index, int): + self._idx = IndexManager(max_num_index) + super().__init__(ir_block=ir_block, depth_counter=depth_counter) + + else: + raise ValueError(f"max num index must be integer, got {type(max_num_index)}") @property def idx(self) -> IndexManager: return self._idx -MemoryDataTypes = BaseDataContainer | CoreLiteral | CompositeLiteral | Symbol | CompositeMixData +MemoryDataTypes = BaseDataContainer | Literal | LiteralArray | Symbol | ObjTuple +""" +- BaseDataContainer +- CoreLiteral +- CompositeLiteral +- Symbol +- CompositeMixData +""" diff --git a/python/src/hhat_lang/core/types/__init__.py b/python/src/hhat_lang/core/types/__init__.py index 08915859..1b5e0aa4 100644 --- a/python/src/hhat_lang/core/types/__init__.py +++ b/python/src/hhat_lang/core/types/__init__.py @@ -1,5 +1,4 @@ from __future__ import annotations - -# for now, consider pointer size of 32bits instead of 64 -POINTER_SIZE = 32 +# consider pointer size of 64 bits +POINTER_SIZE = 64 diff --git a/python/src/hhat_lang/core/types/abstract_base.py b/python/src/hhat_lang/core/types/abstract_base.py index a11258cf..d4deac80 100644 --- a/python/src/hhat_lang/core/types/abstract_base.py +++ b/python/src/hhat_lang/core/types/abstract_base.py @@ -1,13 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Iterable +from typing import Any, Generic, Iterable, TypeVar -from hhat_lang.core.data.core import WorkingData, Symbol, CompositeSymbol -from hhat_lang.core.data.utils import VariableKind -from hhat_lang.core.data.variable import BaseDataContainer, VariableTemplate -from hhat_lang.core.error_handlers.errors import ErrorHandler -from hhat_lang.core.utils import SymbolOrdered +from hhat_lang.core.data.core import Symbol +from hhat_lang.core.types.utils import AbstractTypeDef, BaseTypeEnum class Size: @@ -22,6 +19,9 @@ def __init__(self, size: int): def size(self) -> int: return self._size + def __repr__(self) -> str: + return f"Size({self.size})" + class QSize: """ @@ -52,25 +52,35 @@ def add_max(self, max_num: int) -> None: if isinstance(max_num, int) and self._max is None: self._max = max_num + def __repr__(self) -> str: + return f"QSize(min={self.min}{f'|max={self.max}' if self.max else ''})" + + +######################################### +# TYPES MEMBERS AND CONTAINERS SECTIONS # +######################################### + -class BaseTypeDataStructure(ABC): - """Base type class for data structures, such as single, struct, enum and union.""" +# To facilitate type annotation checks, some generic type values are defined +T = TypeVar("T") # type name +C = TypeVar("C") # container type +M = TypeVar("M") # member name + + +class BaseTypeDef(AbstractTypeDef, Generic[T, M]): + """ + Base data type class to be used by single, struct, enum, etc. type definitions + """ - _name: Symbol | CompositeSymbol - _type_container: SymbolOrdered + _name: Symbol + _t_type: BaseTypeEnum + _container: BaseTypeDataBin _is_quantum: bool _is_builtin: bool - _size: Size | None - _qsize: QSize | None - _array_type: bool - - def __init__( - self, name: Symbol | CompositeSymbol, is_builtin: bool = False, array_type: bool = False - ): - self._name = name - self._is_quantum = name.is_quantum - self._is_builtin = is_builtin - self._array_type = array_type + _size: Size + _qsize: QSize + _is_array: bool + _is_size_set: bool = False @property def name(self) -> Symbol: @@ -80,40 +90,71 @@ def name(self) -> Symbol: def is_quantum(self) -> bool: return self._is_quantum + @property + def is_array(self) -> bool: + return self._is_array + @property def is_builtin(self) -> bool: return self._is_builtin @property - def size(self) -> Size | None: + def size(self) -> Size: return self._size @property - def qsize(self) -> QSize | None: + def qsize(self) -> QSize: return self._qsize - @property - def is_array(self) -> bool: - return self._array_type + @abstractmethod + def add_member(self, type_name: T | None, member_name: M | None, **kwargs: Any) -> BaseTypeDef: + raise NotImplementedError() - @property - def members(self) -> tuple: - return tuple(k for k in self) + def set_sizes(self, size: Size, qsize: QSize | None = None) -> BaseTypeDef: + self._size = size + self._qsize = qsize or QSize(0, 0) + self._is_size_set = True + return self + + def is_size_set(self) -> bool: + return self._is_size_set + + def __contains__(self, item: Any) -> bool: + return item in self._container @abstractmethod - def add_member(self, member_type: Any, member_name: Any) -> Any | ErrorHandler: ... + def __iter__(self) -> Iterable: + raise NotImplementedError() @abstractmethod - def __call__( - self, - *args: Any, - var_name: str, - flag: VariableKind, - **kwargs: dict[WorkingData, WorkingData | VariableTemplate], - ) -> BaseDataContainer | ErrorHandler: ... + def __repr__(self) -> str: + raise NotImplementedError() - def __contains__(self, item: Any) -> bool: - return item in self._type_container +class BaseTypeDataBin(ABC, Generic[T, C, M]): + """ + Base type data bin class to be used by single, struct, enum, etc. + member/type information storage. + """ + + _container: C + + @property + def container(self) -> C: + return self._container + + @abstractmethod + def add_member(self, **kwargs: Any) -> BaseTypeDataBin: + """ + Add a new member to the data bin. It can contain ``type_name`` (of type ``T``), + ``member_name`` (of type ``M``), or only one of them. + """ + raise NotImplementedError() + + @abstractmethod + def __getitem__(self, item: Symbol | int) -> T: + raise NotImplementedError() + + @abstractmethod def __iter__(self) -> Iterable: - yield from self._type_container.items() + raise NotImplementedError() diff --git a/python/src/hhat_lang/core/types/builtin.py b/python/src/hhat_lang/core/types/builtin.py deleted file mode 100644 index cece3b12..00000000 --- a/python/src/hhat_lang/core/types/builtin.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -from collections import OrderedDict -from typing import Any, Callable, Iterable - -from hhat_lang.core.data.core import CoreLiteral, WorkingData, Symbol -from hhat_lang.core.data.variable import BaseDataContainer, VariableTemplate -from hhat_lang.core.error_handlers.errors import ( - ErrorHandler, - TypeSingleError, - CastNegToUnsignedError, - CastIntOverflowError, - CastError, -) -from hhat_lang.core.types import POINTER_SIZE -from hhat_lang.core.types.abstract_base import BaseTypeDataStructure -from hhat_lang.core.types.abstract_base import Size, QSize - -############### -# DEFINITIONS # -############### - -# classical symbol -S_INT = Symbol("int") -S_BOOL = Symbol("bool") -S_U16 = Symbol("u16") -S_U32 = Symbol("u32") -S_U64 = Symbol("u64") - -# quantum symbol -S_QINT = Symbol("@int") -S_QBOOL = Symbol("@bool") -S_QU2 = Symbol("@u2") -S_QU3 = Symbol("@u3") -S_QU4 = Symbol("@u4") - -# sets -int_types: set = {S_INT, S_U16, S_U32, S_U64} -qint_types: set = {S_QINT, S_QU2, S_QU3, S_QU4} - - -###################################### -# BUILT-IN DATA STRUCTURE STRUCTURES # -###################################### - - -class BuiltinSingleDS(BaseTypeDataStructure): - def __init__(self, name: Symbol, bitsize: Size | None = None, qsize: QSize | None = None): - super().__init__(name, is_builtin=True) - self._type_container: list = [name] - self._bitsize = bitsize - self._qsize = qsize if qsize is not None else QSize(0, 0) - - @property - def bitsize(self) -> Size | None: - return self._bitsize - - def cast_from( - self, data: WorkingData, cast_fn: Callable - ) -> CoreLiteral | BaseDataContainer: - """Cast data to this type.""" - - return cast_fn(self, data) - - def add_member(self, *args: Any) -> BuiltinSingleDS | ErrorHandler: - return self - - def __call__( - self, - *args: Any, - var_name: Symbol, - **kwargs: dict[WorkingData, WorkingData | VariableTemplate], - ) -> BaseDataContainer | ErrorHandler: - if len(args) == 1: - x = args[0] - - if x.type == self._type_container[0]: - variable = VariableTemplate( - var_name=var_name, - type_name=self.name, - type_ds=OrderedDict({x.type: self._type_container}), - is_mutable=True, - ) - variable(*args) - return variable - - return TypeSingleError(self._name) - - def __contains__(self, item: Any) -> bool: - pass - - def __iter__(self) -> Iterable: - pass - - -################## -# CAST FUNCTIONS # -################## - - -def int_to_uN( - ds: BuiltinSingleDS, data: CoreLiteral | BaseDataContainer -) -> CoreLiteral | BaseDataContainer | ErrorHandler: - - if ds.bitsize is not None: - max_value = 1 << ds.bitsize.size - - if isinstance(data, CoreLiteral): - - if data < 0: - return CastNegToUnsignedError(data, ds.members[0][1]) - - if data < max_value: - return CoreLiteral(data.value, ds.name.value) - - return CastIntOverflowError(data, ds.name) - - if isinstance(data, BaseDataContainer): - val = data.get() - if data.type in int_types: - - if val < 0: - return CastNegToUnsignedError(val, ds.members[0][1]) - - if val < max_value: - return CoreLiteral(val.name, ds.name.value) - - return CastIntOverflowError(val, ds.name) - - return CastError(ds.name, val) - - # something else? - raise NotImplementedError() - - -####################### -# BUILT-IN DATA TYPES # -####################### - -# classical -Int = BuiltinSingleDS(Symbol("int")) -Bool = BuiltinSingleDS(Symbol("bool"), Size(8)) -U16 = BuiltinSingleDS(Symbol("u16"), Size(16)) -U32 = BuiltinSingleDS(Symbol("u32"), Size(32)) -U64 = BuiltinSingleDS(Symbol("u64"), Size(64)) - -# quantum -QBool = BuiltinSingleDS(Symbol("@bool"), Size(POINTER_SIZE), qsize=QSize(1)) -QU2 = BuiltinSingleDS(Symbol("@u2"), Size(POINTER_SIZE), qsize=QSize(2)) -QU3 = BuiltinSingleDS(Symbol("@u3"), Size(POINTER_SIZE), qsize=QSize(3)) -QU4 = BuiltinSingleDS(Symbol("@u4"), Size(POINTER_SIZE), qsize=QSize(4)) diff --git a/python/src/hhat_lang/core/types/builtin_base.py b/python/src/hhat_lang/core/types/builtin_base.py new file mode 100644 index 00000000..0cf08073 --- /dev/null +++ b/python/src/hhat_lang/core/types/builtin_base.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from typing import Any, Iterable + +from hhat_lang.core.data.core import Symbol +from hhat_lang.core.data.utils import isquantum +from hhat_lang.core.types.abstract_base import BaseTypeDef, QSize, Size +from hhat_lang.core.types.core import ( + SingleDataBin, + SingleT, + StructDataBin, + StructM, + StructT, +) +from hhat_lang.core.types.utils import BaseTypeEnum + +# DEFINITIONS # +############### + +# classical symbol +S_INT = Symbol("int") +S_BOOL = Symbol("bool") +S_U16 = Symbol("u16") +S_U32 = Symbol("u32") +S_U64 = Symbol("u64") +S_I16 = Symbol("i16") +S_I32 = Symbol("i32") +S_I64 = Symbol("i64") +S_F32 = Symbol("f32") +S_F64 = Symbol("f64") + +# quantum symbol +S_QINT = Symbol("@int") +S_QBOOL = Symbol("@bool") +S_QU2 = Symbol("@u2") +S_QU3 = Symbol("@u3") +S_QU4 = Symbol("@u4") + +# sets +int_types: set = {S_INT, S_U16, S_U32, S_U64, S_I16, S_I32, S_I64} +float_types: set = {S_F32, S_F64} +qint_types: set = {S_QINT, S_QU2, S_QU3, S_QU4} + + +###################################### +# BUILT-IN DATA STRUCTURE STRUCTURES # +###################################### + + +class BuiltinSingleTypeDef(BaseTypeDef[SingleT, None]): + _container: SingleDataBin + _t_type = BaseTypeEnum.SINGLE + _is_builtin = True + + def __init__(self, name: Symbol, size: Size, qsize: QSize | None = None): + self._name = name + self._is_quantum = isquantum(name) + self._container = SingleDataBin() + # built-in single is a member of itself + self._container.add_member(name) + self.set_sizes(size, qsize) + + def add_member(self, **kwargs: Any) -> BaseTypeDef: + return self + + def __iter__(self) -> Iterable: + return iter(self._container) + + def __repr__(self) -> str: + return f"{self._name}:{self._container[0]}" + + +class BuiltinStructTypeDef(BaseTypeDef[StructT, StructM]): + _container: StructDataBin + _t_type = BaseTypeEnum.STRUCT + _is_builtin = True + + def __init__(self, name: Symbol, size: Size, qsize: QSize | None = None): + self._name = name + self._is_quantum = isquantum(name) + self._container = StructDataBin() + self.set_sizes(size, qsize) + + def add_member(self, type_name: StructT, member_name: StructM, **kwargs: Any) -> BaseTypeDef: + self._container.add_member(type_name=type_name, member_name=member_name) + return self + + def __iter__(self) -> Iterable: + return iter(self._container) + + def __repr__(self) -> str: + members = "{" + " ".join(f"{k}:{v}" for k, v in self) + "}" + return f"{self._name}{members}" diff --git a/python/src/hhat_lang/core/types/builtin_conversion.py b/python/src/hhat_lang/core/types/builtin_conversion.py new file mode 100644 index 00000000..af6349c6 --- /dev/null +++ b/python/src/hhat_lang/core/types/builtin_conversion.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import cast + +from hhat_lang.core.data.core import Literal, Symbol, SimpleObj +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import ( + CastError, + CastIntOverflowError, + CastNegToUnsignedError, + ErrorHandler, +) +from hhat_lang.core.types.builtin_base import BuiltinSingleTypeDef, int_types + +################################### +# COMPATIBLE CONVERTABLE TYPES # +################################### + +compatible_types: dict[Symbol, tuple[Symbol, ...]] = { + Symbol("int"): ( + Symbol("u16"), + Symbol("u32"), + Symbol("u64"), + Symbol("i16"), + Symbol("i32"), + Symbol("i64"), + ), + Symbol("float"): (Symbol("f32"), Symbol("f64")), + Symbol("@int"): (Symbol("@u2"), Symbol("@u3"), Symbol("@u4")), +} +"""dictionary to establish the relation between generic types (``int``, ``float``, ``@int``) +as their possible convertible types""" + + +################## +# CAST FUNCTIONS # +################## + + +def int_to_uN( + ds: BuiltinSingleTypeDef, data: Literal | BaseDataContainer +) -> Literal | BaseDataContainer | ErrorHandler: + if ds.bitsize is not None: + max_value = 1 << ds.bitsize.size + + if isinstance(data, Literal): + if data < 0: + return CastNegToUnsignedError(data, ds.members[0][1]) + + if data < max_value: + lit_type = cast(str, ds.name.value) + return Literal(data.value, lit_type) + + return CastIntOverflowError(data, ds.name) + + if isinstance(data, BaseDataContainer): + val = data.get() + if data.type in int_types: + match val: + case ErrorHandler(): + return val + + case Literal(): + if val < 0: + return CastNegToUnsignedError(val, ds.members[0][1]) + + if val < max_value: + lit_type = cast(str, ds.name.value) + return Literal(val.value, lit_type) + + return CastIntOverflowError(val, ds.name) + + return CastError(ds.name, val) + + # something else? + raise NotImplementedError() diff --git a/python/src/hhat_lang/core/types/builtin_types.py b/python/src/hhat_lang/core/types/builtin_types.py new file mode 100644 index 00000000..444bc7d6 --- /dev/null +++ b/python/src/hhat_lang/core/types/builtin_types.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from hhat_lang.core.data.core import AsArray, Symbol +from hhat_lang.core.types import POINTER_SIZE +from hhat_lang.core.types.abstract_base import QSize, Size +from hhat_lang.core.types.builtin_base import BuiltinSingleTypeDef, BuiltinStructTypeDef + +####################### +# BUILT-IN DATA TYPES # +####################### + +# ---------- # +# classical # +# ---------- # + +# BASIC SINGLE TYPES +Int = BuiltinSingleTypeDef(Symbol("int"), Size(64)) +Bool = BuiltinSingleTypeDef(Symbol("bool"), Size(8)) +U16 = BuiltinSingleTypeDef(Symbol("u16"), Size(16)) +U32 = BuiltinSingleTypeDef(Symbol("u32"), Size(32)) +U64 = BuiltinSingleTypeDef(Symbol("u64"), Size(64)) +I16 = BuiltinSingleTypeDef(Symbol("i16"), Size(16)) +I32 = BuiltinSingleTypeDef(Symbol("i32"), Size(32)) +I64 = BuiltinSingleTypeDef(Symbol("i64"), Size(64)) +Float = BuiltinSingleTypeDef(Symbol("float"), Size(64)) +F32 = BuiltinSingleTypeDef(Symbol("f32"), Size(32)) +F64 = BuiltinSingleTypeDef(Symbol("f64"), Size(64)) + +# HASHMAP TYPES +HashKey = BuiltinSingleTypeDef(Symbol("hash-key"), Size(POINTER_SIZE)) +HashValue = BuiltinSingleTypeDef(Symbol("hash-value"), Size(POINTER_SIZE)) +HashSet = ( + BuiltinStructTypeDef(Symbol("hash-set"), Size(POINTER_SIZE)) + .add_member(type_name=HashKey, member_name=Symbol("key")) + .add_member(type_name=HashValue, member_name=Symbol("value")) +) +""" +```HashSet`` ("hash-set") is the holder of hash keys and hash values +for ``HashMap`` ("hashmap"). +""" +HashMap = BuiltinStructTypeDef(Symbol("hashmap"), Size(POINTER_SIZE)).add_member( + type_name=AsArray(Symbol("hash-set")), member_name=Symbol("data") +) +""" +``HashMap`` ("hashmap") is the common hashmap(dictionary) data structure +""" + + +# SAMPLE TYPES +HashKeyInt = BuiltinSingleTypeDef(Symbol("hash-key_int"), Size(64)) +HashValueInt = BuiltinSingleTypeDef(Symbol("hash-value_int"), Size(64)) +HashSetInt = ( + BuiltinStructTypeDef(Symbol("hash-set_int"), Size(POINTER_SIZE)) + .add_member(type_name=HashKeyInt, member_name=Symbol("key")) + .add_member(type_name=HashValueInt, member_name=Symbol("value")) +) +""" +```HashSetInt`` ("hash-set_int") is the holder of integer hash keys and hash values +for ``Sample`` ("sample"). +""" +Sample = BuiltinStructTypeDef(Symbol("sample"), Size(POINTER_SIZE)).add_member( + type_name=AsArray(Symbol("hash-set_int")), member_name=Symbol("data") +) +""" +``Sample`` is an efficient and fast hashmap(dictionary)-like data structure that knows all +its keys beforehand, so it can compute its exact length; The keys (index number) and values +(count number) are always integer values:: + + type sample { data:[hash-set_int] } + + type hash-set_int { + key:hash-key_int + value:hash-value_int + } + + type hash-key_int:u64 + + type hash-value_int:u64 + +""" + + +# -------- # +# quantum # +# -------- # + +# BASIC SINGLE TYPES +QBool = BuiltinSingleTypeDef(Symbol("@bool"), Size(POINTER_SIZE), qsize=QSize(1, 1)) +QU2 = BuiltinSingleTypeDef(Symbol("@u2"), Size(POINTER_SIZE), qsize=QSize(2, 2)) +QU3 = BuiltinSingleTypeDef(Symbol("@u3"), Size(POINTER_SIZE), qsize=QSize(3, 3)) +QU4 = BuiltinSingleTypeDef(Symbol("@u4"), Size(POINTER_SIZE), qsize=QSize(4, 4)) +QInt = BuiltinSingleTypeDef( + Symbol("@int"), + Size(POINTER_SIZE), + qsize=QSize(min_num=QU2.qsize.min, max_num=QU4.qsize.max), +) +""" +``QInt`` (``@int``) represents a generic quantum integer, where the minimum qsize is the +minimum of quantum integer type (``@u2``) and the maximum is the maximum of the biggest +quantum integer available. +""" + + +# ---------------------------------- # +# list with all built-in data types # +# ---------------------------------- # + +builtins_types = { + # classical + Symbol("int"): Int, + Symbol("float"): Float, + Symbol("bool"): Bool, + Symbol("u16"): U16, + Symbol("u32"): U32, + Symbol("u64"): U64, + Symbol("i16"): I16, + Symbol("i32"): I32, + Symbol("i64"): I64, + Symbol("f32"): F32, + Symbol("f64"): F64, + Symbol("hash-set"): HashSet, + Symbol("hash-key"): HashKey, + Symbol("hash-value"): HashValue, + Symbol("hashmap"): HashMap, + Symbol("hash-set_int"): HashSetInt, + Symbol("hash-key_int"): HashKeyInt, + Symbol("hash-value_int"): HashValueInt, + Symbol("sample"): Sample, + # quantum + Symbol("@bool"): QBool, + Symbol("@int"): QInt, + Symbol("@u2"): QU2, + Symbol("@u3"): QU3, + Symbol("@u4"): QU4, +} +"""a dictionary where keys are the available types as str and the values are their classes""" diff --git a/python/src/hhat_lang/core/types/core.py b/python/src/hhat_lang/core/types/core.py index 668bc025..8892632b 100644 --- a/python/src/hhat_lang/core/types/core.py +++ b/python/src/hhat_lang/core/types/core.py @@ -1,26 +1,27 @@ from __future__ import annotations -from collections import OrderedDict -from typing import Any - -from hhat_lang.core.data.core import Symbol, WorkingData, CompositeSymbol -from hhat_lang.core.data.utils import has_same_paradigm, isquantum, VariableKind -from hhat_lang.core.data.variable import BaseDataContainer, VariableTemplate +import sys +from types import NoneType +from typing import Any, Iterable + +from hhat_lang.core.data.core import ( + CompositeSymbol, + Literal, + Symbol, + SymbolObj, +) +from hhat_lang.core.data.utils import isquantum from hhat_lang.core.error_handlers.errors import ( ErrorHandler, - TypeAndMemberNoMatchError, - TypeQuantumOnClassicalError, - TypeSingleError, - TypeStructError, + TypeMemberAlreadyExistsError, + TypeMemberOverflowError, ) -from hhat_lang.core.types.abstract_base import BaseTypeDataStructure, QSize, Size +from hhat_lang.core.types.abstract_base import BaseTypeDataBin, BaseTypeDef, M +from hhat_lang.core.types.utils import BaseTypeEnum from hhat_lang.core.utils import SymbolOrdered -def is_valid_member( - datatype: BaseTypeDataStructure, - member: str | Symbol | CompositeSymbol -) -> bool: +def is_valid_member(datatype: BaseTypeDef, member: str | Symbol | CompositeSymbol) -> bool: """ Check if a datatype member is valid for the given datatype, e.g. quantum datatype supports classical members, but a classical datatype cannot contain @@ -33,172 +34,194 @@ def is_valid_member( return True -class SingleDS(BaseTypeDataStructure): - def __init__( - self, name: Symbol | CompositeSymbol, size: Size | None = None, qsize: QSize | None = None - ): - super().__init__(name) - self._size = size - self._qsize = qsize - self._type_container: OrderedDict[Symbol | CompositeSymbol, Symbol | CompositeSymbol] = OrderedDict() +################## +# SINGLE SECTION # +################## + +# types annotation for SingleDataBin +SingleT = Symbol | CompositeSymbol +SingleC = tuple[Symbol | CompositeSymbol] | tuple +SingleM = NoneType + + +class SingleDataBin(BaseTypeDataBin[SingleT, SingleC, SingleM]): + _container: SingleC + _locked: bool + + def __init__(self): + self._container = () + self._locked = False + + def add_member(self, type_name: SingleT, **kwargs: Any) -> SingleDataBin | ErrorHandler: + if not self._locked: + self._container += (type_name,) + self._locked = True + return self + + return TypeMemberOverflowError() + + def __getitem__(self, item: Any) -> SingleT: + return self._container[0] + + def __iter__(self) -> Iterable: + return iter(self._container) + + +class SingleTypeDef(BaseTypeDef[SingleT, None]): + _container: SingleDataBin + _t_type = BaseTypeEnum.SINGLE + + def __init__(self, name: Symbol): + self._name = name + self._is_quantum = isquantum(name) + self._container = SingleDataBin() + + def add_member(self, type_name: SingleT | None, **kwargs: Any) -> SingleTypeDef: + match res := self._container.add_member(type_name=type_name): + case TypeMemberOverflowError(): + sys.exit(res(self._name, self._t_type)) + + case _: + return self + + def __iter__(self) -> Iterable: + return iter(self._container) + + def __repr__(self) -> str: + return f"{self._name}:{self._container[0]}" + + +################## +# STRUCT SECTION # +################## + +StructT = SymbolObj +StructC = SymbolOrdered[Symbol, SymbolObj] +StructM = Symbol + + +class StructDataBin(BaseTypeDataBin[StructT, StructC, StructM]): + _container: StructC + _num_members: int + + def __init__(self): + self._container = SymbolOrdered() + self._num_members = 0 def add_member( - self, member_type: BaseTypeDataStructure, _member_name: None = None - ) -> SingleDS | ErrorHandler: - - if not is_valid_member(self, member_type.name): - return TypeQuantumOnClassicalError(member_type.name, self.name) - - self._type_container[self.name] = member_type.name - return self - - def __call__( - self, - *args: Any, - var_name: Symbol, - flag: VariableKind = VariableKind.IMMUTABLE, - **kwargs: dict[WorkingData, WorkingData | BaseDataContainer], - ) -> BaseDataContainer | ErrorHandler: - if len(args) == 1: - x = args[0] - - if x.type == self._type_container[self.name]: - variable = VariableTemplate( - var_name=var_name, - type_name=self.name, - type_ds=SymbolOrdered({Symbol(x.type): self._type_container}), - flag=flag, - ) - variable(*args) - return variable - - return TypeSingleError(self._name) - - -class ArrayDS(BaseTypeDataStructure): - """This is an array data structure, to be thought as [u64] to represent an array of u64.""" - - def __init__( - self, name: Symbol | CompositeSymbol, size: Size | None = None, qsize: QSize | None = None - ): - super().__init__(name, array_type=True) - self._size = size - self._qsize = qsize - self._type_container: OrderedDict[Symbol | CompositeSymbol, Symbol | CompositeSymbol] = OrderedDict() - - def add_member(self, member_type: Any, member_name: Any) -> Any | ErrorHandler: - raise NotImplementedError() - - def __call__( - self, - *args: Any, - var_name: str, - flag: VariableKind = VariableKind.IMMUTABLE, - **kwargs: dict[WorkingData, WorkingData | VariableTemplate], - ) -> BaseDataContainer | ErrorHandler: - raise NotImplementedError() - - -class StructDS(BaseTypeDataStructure): - def __init__( - self, name: Symbol | CompositeSymbol, size: Size | None = None, qsize: QSize | None = None - ): - super().__init__(name) - self._size = size - self._qsize = qsize - self._type_container: SymbolOrdered[Symbol | CompositeSymbol, Symbol | CompositeSymbol] = SymbolOrdered() + self, type_name: StructT, member_name: StructM, **kwargs: Any + ) -> BaseTypeDataBin | ErrorHandler: + if member_name not in self._container: + self._container[member_name] = type_name + self._num_members += 1 + return self + + return TypeMemberAlreadyExistsError() + + def __getitem__(self, item: Symbol) -> StructT: + return self._container[item] + + def __iter__(self) -> Iterable: + return iter(self._container.items()) + + +class StructTypeDef(BaseTypeDef[StructT, StructM]): + _num_members: int + _container: StructDataBin + _t_type = BaseTypeEnum.STRUCT + + def __init__(self, name: Symbol, num_members: int): + self._name = name + self._num_members = num_members + self._is_quantum = isquantum(name) + self._container = StructDataBin() + + def add_member(self, type_name: StructT, member_name: StructM, **kwargs: Any) -> StructTypeDef: + if self._num_members > 0: + match res := self._container.add_member(type_name=type_name, member_name=member_name): + case TypeMemberAlreadyExistsError(): + sys.exit(res(self._name, member_name)) + + case _: + self._num_members -= 1 + return self + + sys.exit(TypeMemberOverflowError()(self._name, self._t_type)) + + def __iter__(self) -> Iterable: + return iter(self._container) + + def __repr__(self) -> str: + members = "{" + " ".join(f"{k}:{v}" for k, v in self) + "}" + return f"{self._name}{members}" + + +################ +# ENUM SECTION # +################ + +EnumT = Literal | StructDataBin +EnumC = SymbolOrdered[Symbol, Symbol | StructTypeDef] +EnumM = Symbol | StructDataBin + + +class EnumDataBin(BaseTypeDataBin[EnumT, EnumC, EnumM]): + _container: EnumC + _counter: int + + def __init__(self, num_members: int): + self._data = SymbolOrdered() + self._counter = 1 if num_members else 0 def add_member( - self, member_type: BaseTypeDataStructure, member_name: Symbol | CompositeSymbol - ) -> StructDS | ErrorHandler: + self, member_name: EnumM | None, **kwargs: Any + ) -> BaseTypeDataBin | ErrorHandler: + if member_name not in self._container: + match member_name: + case Symbol(): + self._counter *= 2 + self._container[member_name] = self._counter - # check if type and name are consistent, i.e. both quantum or classical - if has_same_paradigm(member_type, member_name): + case StructTypeDef(): + self._container[member_name.name] = member_name - if is_valid_member(self, member_type.name): - self._type_container[member_name] = member_type.name - return self + return self + + return TypeMemberAlreadyExistsError() + + def __getitem__(self, item: Symbol) -> EnumT: + return self._container[item] + + def __iter__(self) -> Iterable: + return iter(self._container.items()) + + +class EnumTypeDef(BaseTypeDef[EnumT, StructM]): + _num_members: int + _container: EnumDataBin + _t_type = BaseTypeEnum.ENUM + + def __init__(self, name: Symbol, num_members: int): + self._name = name + self._num_members = num_members + self._is_quantum = isquantum(name) + self._container = EnumDataBin(num_members) + + def add_member(self, member_name: M | None, **kwargs: Any) -> BaseTypeDef: + if self._num_members > 0: + match res := self._container.add_member(member_name): + case TypeMemberAlreadyExistsError(): + sys.exit(res(self._name, member_name)) + + case _: + self._num_members -= 1 + return self + + sys.exit(TypeMemberOverflowError()(self._name, self._t_type)) + + def __iter__(self) -> Iterable: + return iter(self._container) - return TypeQuantumOnClassicalError(member_type.name, self.name) - - return TypeAndMemberNoMatchError(member_type.name, self.name) - - def __call__( - self, - *args: Any, - var_name: Symbol, - flag: VariableKind = VariableKind.IMMUTABLE, - **kwargs: dict[WorkingData, WorkingData | BaseDataContainer], - ) -> BaseDataContainer | ErrorHandler: - container: SymbolOrdered = SymbolOrdered() - - if len(args) == len(self._type_container): - for k, (g, c) in zip(args, self._type_container.items()): - - if k.type == c: - container[g] = k - - else: - return TypeStructError(self._name) - - if len(kwargs) == len(self._type_container): - for n, (k, v) in enumerate(kwargs.items()): - - if k in self._type_container: - container[k] = v - - else: - return TypeStructError(self._name) - - variable = VariableTemplate( - var_name=var_name, - type_name=self._name, - type_ds=self._type_container, - flag=flag, - ) - variable(**container) - return variable - - -class UnionDS(BaseTypeDataStructure): - def __init__( - self, name: Symbol | CompositeSymbol, size: Size | None = None, qsize: QSize | None = None - ): - super().__init__(name) - self._size = size - self._qsize = qsize - self._type_container = SymbolOrdered() - - def add_member(self, member_type: str, member_name: str) -> UnionDS: - raise NotImplementedError() - - def __call__( - self, - *args: Any, - var_name: str, - flag: VariableKind = VariableKind.IMMUTABLE, - **kwargs: dict[WorkingData, WorkingData | BaseDataContainer], - ) -> BaseDataContainer | ErrorHandler: - raise NotImplementedError() - - -class EnumDS(BaseTypeDataStructure): - def __init__( - self, name: Symbol | CompositeSymbol, size: Size | None = None, qsize: QSize | None = None - ): - super().__init__(name) - self._size = size - self._qsize = qsize - self._type_container = SymbolOrdered() - - def add_member(self, member_type: str, member_name: str) -> EnumDS: - raise NotImplementedError() - - def __call__( - self, - *args: Any, - var_name: str, - flag: VariableKind = VariableKind.IMMUTABLE, - **kwargs: dict[WorkingData, WorkingData | BaseDataContainer], - ) -> BaseDataContainer | ErrorHandler: - raise NotImplementedError() + def __repr__(self) -> str: + members = "{" + " ".join(f"{k}" if isinstance(v, int) else f"{v}" for k, v in self) + "}" + return f"{self._name}{members}" diff --git a/python/src/hhat_lang/core/types/resolve_sizes.py b/python/src/hhat_lang/core/types/resolve_sizes.py deleted file mode 100644 index 4c3ef3a0..00000000 --- a/python/src/hhat_lang/core/types/resolve_sizes.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from hhat_lang.core.code.ir import TypeTable -from hhat_lang.core.types.abstract_base import BaseTypeDataStructure - - -def _size_resolver(): - pass - - -def _qsize_resolver(ds: BaseTypeDataStructure, table: TypeTable) -> int | None: - if ds.qsize.max is None: - qsize_max = 0 - - for _, member_type in ds: - res = _qsize_resolver(table[member_type], table) - - if res: - qsize_max += res - - ds.qsize.max = qsize_max or None - - return ds.qsize.max - - -def ct_size() -> Any: - """Compile-time size resolver.""" - - pass - - -def ct_qsize(ds: BaseTypeDataStructure, type_table: TypeTable) -> Any: - """Compile-time qsize resolver.""" - - pass - - -def runtime_size() -> Any: - """Runtime size resolver.""" - - pass - - -def runtime_qsize() -> Any: - """Runtime qsize resolver.""" - - pass diff --git a/python/src/hhat_lang/core/types/resolvers.py b/python/src/hhat_lang/core/types/resolvers.py new file mode 100644 index 00000000..3ebf4679 --- /dev/null +++ b/python/src/hhat_lang/core/types/resolvers.py @@ -0,0 +1,90 @@ +""" +Collection of functions to resolve types and sizes (``Size`` and ``QSize``). + +This is the step after all the types are stored in the ``SymbolTable``'s ``TypeTable`` +of all the modules. It will look up for unresolved type members inside types, that is +types that are not built-in. + +It checks for the right types definitions and sizes so all types are defined and +ready to be used during program execution. +""" + +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.code.ir_graph import IRGraph, IRNode +from hhat_lang.core.code.tools import get_type +from hhat_lang.core.code.symbol_table import TypeTable, SymbolTable +from hhat_lang.core.error_handlers.errors import ( + ErrorHandler, + TypeMemberNotResolvedError, +) +from hhat_lang.core.types.abstract_base import BaseTypeDef + + +################# +# Resolve types # +################# + + +def resolve_members( + table: TypeTable, +) -> None | ErrorHandler: + """ + To resolve type members so everything is defined when the program is executed + """ + + # return TypeMemberNotResolvedError() + + +################# +# Resolve sizes # +################# + + +def _size_resolver(): + pass + + +def _qsize_resolver(ds: BaseTypeDef, node: IRNode, ir_graph: IRGraph) -> int | None: + if ds.qsize is not None: + if ds.qsize.max is None: + qsize_max = 0 + + for _, member_type in ds: + if t := get_type(node.irhash, member_type, ir_graph): + res = _qsize_resolver(ds=t, node=node, ir_graph=ir_graph) + + if res: + qsize_max += res + + ds.qsize.add_max(qsize_max) + + return ds.qsize.max + + raise ValueError("Quantum type must have QSize defined.") + + +def ct_size() -> Any: + """Compile-time size resolver.""" + + pass + + +def ct_qsize(ds: BaseTypeDef, type_table: TypeTable) -> Any: + """Compile-time qsize resolver.""" + + pass + + +def runtime_size() -> Any: + """Runtime size resolver.""" + + pass + + +def runtime_qsize() -> Any: + """Runtime qsize resolver.""" + + pass diff --git a/python/src/hhat_lang/core/types/utils.py b/python/src/hhat_lang/core/types/utils.py new file mode 100644 index 00000000..1dd0c4f8 --- /dev/null +++ b/python/src/hhat_lang/core/types/utils.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from abc import ABC +from enum import Enum, auto + + +class BaseTypeEnum(Enum): + """Enum data type structures for ``BaseTypeDataStructure`` instances""" + + SINGLE = auto() + STRUCT = auto() + ENUM = auto() + UNION = auto() + + REMOTE_UNION = auto() + """ + ``REMOTE_UNION``: a new data structure to be used in the future to handle remote + quantum data; name yet to be settled + """ + + +class AbstractTypeDef(ABC): + """Abstract data type structure. To avoid circular imports.""" + + _t_type: BaseTypeEnum + + @property + def type(self) -> BaseTypeEnum: + return self._t_type diff --git a/python/src/hhat_lang/core/utils.py b/python/src/hhat_lang/core/utils.py index 7ff84a83..9d08a656 100644 --- a/python/src/hhat_lang/core/utils.py +++ b/python/src/hhat_lang/core/utils.py @@ -1,44 +1,68 @@ from __future__ import annotations +import uuid from abc import ABC, abstractmethod from collections import OrderedDict from collections.abc import Mapping -from typing import Any, Iterator +from typing import Any, Hashable, Iterator, TypeVar +from uuid import NAMESPACE_OID -from hhat_lang.core.data.core import Symbol, CompositeSymbol +from hhat_lang.core.data.core import CompositeSymbol, Symbol, SimpleObj from hhat_lang.core.error_handlers.errors import ErrorHandler -class SymbolOrdered(Mapping): +def gen_uuid(obj: Hashable) -> int: + return int(uuid.uuid5(NAMESPACE_OID, f"{obj}").hex, 16) + + +Key = TypeVar("Key") +Value = TypeVar("Value") + + +class SymbolOrdered[Key, Value](Mapping): """ A special OrderedDict that accepts Symbol as keys but transforms them as str to unpack the class. Useful for building data structures such - as `SingleDS`, `StructDS`, etc. + as ``SingleTypeDef``, ``StructTypeDef``, etc. """ - _data: OrderedDict[Symbol, Any] + _data: OrderedDict[SimpleObj | Symbol | CompositeSymbol | int, Any] def __init__(self, data: dict | OrderedDict | None = None): self._data = OrderedDict() if data is None else OrderedDict(data) - def __setitem__(self, key: str | Symbol | CompositeSymbol, value: Any) -> None: + def __setitem__( + self, key: int | str | SimpleObj | Symbol | CompositeSymbol, value: Any + ) -> None: if isinstance(key, str): self._data[Symbol(key)] = value elif isinstance(key, (Symbol, CompositeSymbol)): self._data[key] = value + elif isinstance(key, SimpleObj): + self._data[key] = value + + elif isinstance(key, int): + self._data[key] = value + else: raise ValueError(f"{key} ({type(key)}) is not valid key for data structures.") - def __getitem__(self, key: str | Symbol | CompositeSymbol) -> Any: + def __getitem__(self, key: int | str | SimpleObj | Symbol | CompositeSymbol) -> Any: if isinstance(key, str): return self._data[Symbol(key)] if isinstance(key, (Symbol, CompositeSymbol)): return self._data[key] - raise ValueError(key) + if isinstance(key, SimpleObj): + return self._data[key] + + if isinstance(key, int): + return self._data[key] + + raise KeyError(key) def __len__(self) -> int: return len(self._data) @@ -48,14 +72,13 @@ def items(self) -> Iterator: def keys(self) -> Iterator: for k in self._data.keys(): - yield k.value + yield k.value if not isinstance(k, int) else k def values(self) -> Iterator: yield from self._data.values() def __iter__(self) -> Iterator: - for k in self._data: - yield k # .name + return iter(k for k in self._data) def __repr__(self) -> str: return str(self._data) @@ -68,8 +91,7 @@ def __init__(self, value: Any): self.value = value @abstractmethod - def result(self) -> Any: - ... + def result(self) -> Any: ... class Ok(Result): @@ -84,4 +106,3 @@ class Error(Result): def result(self) -> ErrorHandler: return self.value - diff --git a/python/src/hhat_lang/dialects/README.md b/python/src/hhat_lang/dialects/README.md new file mode 100644 index 00000000..94611806 --- /dev/null +++ b/python/src/hhat_lang/dialects/README.md @@ -0,0 +1,19 @@ +# Dialects + +The `dialects` directory holds information about each existing dialect implemented inside H-hat ecosystem. + +Each dialect must observe some common organization and convention, folder- and file-wise. As in [`core`](../core/README.md), there are common folders, such as `cast`, `code`, `execution`. However, new ones to account for the language proper implementation, handling code execution and auxiliary features, such as `grammar`, `parsing` and `toolchain`. + +On `cast` folder, dialect-specific implementation of: +- built-in and user defined convertion functions +- general classes for types of cast (classical to classical, classical to quantum, quantum to classical and quantum to quantum) + +On `code` folder: +- built-in functions implementation +- intermediate representation (IR) implementation + +On `execution` folder: +- classical evaluators and quantum programs + +On `grammar` folder: +- grammar and syntax definitions for the dialect, considering at least the minimum requirements set by the core rules diff --git a/python/src/hhat_lang/dialects/heather/cast/__init__.py b/python/src/hhat_lang/dialects/heather/cast/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/cast/base.py b/python/src/hhat_lang/dialects/heather/cast/base.py new file mode 100644 index 00000000..548c3bd6 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/cast/base.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from hhat_lang.core.cast.base import ( + BaseCastC2C, + BaseCastQ2C, + BaseCastQ2Q, + BaseCastC2Q, + CastFnType, +) +from hhat_lang.core.code.ir_graph import IRNode, IRGraph +from hhat_lang.core.data.core import Literal +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.memory.core import MemoryManager +from hhat_lang.core.types.abstract_base import BaseTypeDef +from hhat_lang.dialects.heather.cast.conversion_protocols.builtin_fns import ( + cast_fns_dict, +) + + +class CastC2C(BaseCastC2C): + def __int__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + d_type: str = data.type.value if isinstance(data, BaseDataContainer) else data.type + cast_fn: CastFnType = cast_fns_dict[(d_type, to_type.name.value)] + + super().__init__( + data=data, + to_type=to_type, + cast_fn=cast_fn, + mem=mem, + node=node, + ir_graph=ir_graph, + ) + + def cast(self) -> CastC2C: + # TODO: implement it + + return self + + +class CastQ2C(BaseCastQ2C): + def __int__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + """ + Class to cast from quantum data to classical type. + + Args: + data: variable (``DataBaseContainer``) or literal (``CoreLiteral``) + to_type: the actual type (``BaseTypeDataStructure``) you want to cast to + mem: the memory manager (``MemoryManager``) the current scope/program is using + node: the node (``IRNode``) where the program is located + ir_graph: the program's ir graph (``IRGraph``) + """ + + d_type: str = data.type.value if isinstance(data, BaseDataContainer) else data.type + cast_fn: CastFnType = cast_fns_dict[(d_type, to_type.name.value)] + + super().__init__( + data=data, + to_type=to_type, + cast_fn=cast_fn, + mem=mem, + node=node, + ir_graph=ir_graph, + ) + + def cast(self) -> CastQ2C: + # TODO: implement it + + return self + + +class CastQ2Q(BaseCastQ2Q): + def __init__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + d_type: str = data.type.value if isinstance(data, BaseDataContainer) else data.type + cast_fn: CastFnType = cast_fns_dict[(d_type, to_type.name.value)] + + super().__init__( + data=data, + to_type=to_type, + cast_fn=cast_fn, + # mem=mem, + # node=node, + # ir_graph=ir_graph + ) + + def cast(self) -> BaseCastQ2Q: + return self + + +class CastC2Q(BaseCastC2Q): + def __init__( + self, + data: BaseDataContainer | Literal, + to_type: BaseTypeDef, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + ): + d_type: str = data.type.value if isinstance(data, BaseDataContainer) else data.type + cast_fn: CastFnType = cast_fns_dict[(d_type, to_type.name.value)] + super().__init__(data=data, to_type=to_type, cast_fn=cast_fn) + + def cast(self) -> BaseCastC2Q: + return self diff --git a/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/__init__.py b/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/base.py b/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/base.py new file mode 100644 index 00000000..5448a18a --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/base.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.cast.base import get_min_count, get_max_count, BaseBitString + + +class BitString(BaseBitString): + """Handle bit string data for Heather dialect.""" + + # TODO: refactor later to place the backend platform-specific logic be handled by them + + @staticmethod + def _get_data(data: Any) -> dict: + return getattr(data, "c", None) or getattr(data, "meas", dict()) + + def _get_res(self, data: Any) -> Any: + if hasattr(data, "shape") and hasattr(data, "size"): + return self._get_data(data) + + if hasattr(data, "data"): + return self._get_res(data.data) + + raise NotImplementedError(f"could not get bit string counts for data of type {type(data)}") + + def get_counts(self) -> dict: + return self._get_res(self._sample) + + +def get_min_res(sample: BitString) -> Any: + res = get_min_count(sample) + + +def get_max_res(sample: BitString) -> Any: + res = get_max_count(sample) diff --git a/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/builtin_fns.py b/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/builtin_fns.py new file mode 100644 index 00000000..bca2357e --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/cast/conversion_protocols/builtin_fns.py @@ -0,0 +1,600 @@ +from __future__ import annotations + +import sys +import ctypes +from functools import wraps +from typing import Any, NoReturn, Callable + +from hhat_lang.core.cast.base import CastFnType +from hhat_lang.core.data.core import Literal +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import ( + InvalidDataContainerCastError, + EvaluatorCastWildcardBuiltinTypeError, + EvaluatorCastDataError, + DataOverflowError, + InterpreterEvaluationError, +) + +# TODO: implement complex data conversion functions as well + + +############### +# DEFINITIONS # +############### + +U32_MAX: int = 2 << 31 +I32_MAX: int = (2 << 31) - 1 +U64_MAX: int = 2 << 63 +I64_MAX: int = (2 << 63) - 1 +F32_MAX: float = (1 - 2 ** (-24)) * (2 << 127) +# (not representable in python) F64_MAX = (1 - 2**(-54)) * (2**1024) + +U32_MIN: int = 0 +I32_MIN: int = -(2 << 31) +U64_MIN: int = 0 +I64_MIN: int = -(2 << 63) +F32_MIN: float = -F32_MAX +# (not representable in python) F64_MIN = -F64_MAX + + +######################## +# FUNCTIONS DICTIONARY # +######################## + + +cast_fns_dict: dict[tuple[str, str], CastFnType] = dict() +""" +Dictionary to hold all the available cast functions. +The key is a pair tuple with 'from_type' and 'to_type', and +the value is the function to perform the casting operation. +""" + + +def insert_cast_fns(entry_types: tuple[str, str]) -> Callable: + """ + Decorator function to store entry_types (tuple) as key and the function + as value in the ``cast_fns_dict`` dictionary. + """ + + # check whether entry data has the correct type and shape + if not (isinstance(entry_types, tuple) and len(entry_types) == 2): + raise InterpreterEvaluationError( + error_where="appending cast functions", + msg=( + f"a cast function must provide a tuple with 'from_type' and " + f"'to_type' types names, but got {entry_types} ({type(entry_types)})." + ), + ) + + def decorator(fn: Callable) -> Callable: + @wraps(fn) + def wrapper(data: BaseDataContainer | Literal | Any) -> Literal: + return fn(data) + + cast_fns_dict[entry_types] = wrapper + return wrapper + + return decorator + + +######################## +# CONVENIENT FUNCTIONS # +######################## + + +def _invalid_case_cast(data: Any, f_type: str, t_type: str) -> NoReturn: + sys.exit(InvalidDataContainerCastError(data, f_type, t_type)()) + + +def _cast_to( + data: BaseDataContainer | Literal | Any, + cast_fn: Callable, + from_type: str, + to_type: str, +) -> Literal: + """ + Simple casting function using a ``cast_fn`` function to convert ``data`` + from ``from_type`` to ``to_type``. + """ + + match data: + case BaseDataContainer(): + literal: Literal = next(iter(data.data)) + return Literal(str(cast_fn(literal.value)), to_type) + + case Literal(): + return Literal(str(cast_fn(data.value)), to_type) + + case _: + return _invalid_case_cast(data, from_type, to_type) + + +##################### +# BOOLEAN FUNCTIONS # +##################### + + +@insert_cast_fns(("bool", "int")) +def bool_to_int(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to int. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as int + """ + + return _cast_to(data, int, "bool", "int") + + +@insert_cast_fns(("bool", "float")) +def bool_to_float(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to float. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as float + """ + + return _cast_to(data, float, "bool", "float") + + +@insert_cast_fns(("bool", "u32")) +def bool_to_u32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to u32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as u32 + """ + + return _cast_to(data, int, "bool", "u32") + + +@insert_cast_fns(("bool", "i32")) +def bool_to_i32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to i32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as i32 + """ + + return _cast_to(data, int, "bool", "i32") + + +@insert_cast_fns(("bool", "f32")) +def bool_to_f32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to f32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as f32 + """ + + return _cast_to(data, float, "bool", "f32") + + +@insert_cast_fns(("bool", "f64")) +def bool_to_f64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from bool to f64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as f64 + """ + + return _cast_to(data, float, "bool", "f64") + + +@insert_cast_fns(("int", "bool")) +def int_to_bool(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from any int to bool. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as bool + """ + + from_type: str + match data: + case BaseDataContainer(): + literal: Literal = next(iter(data.data)) + from_type = literal.type + + case Literal(): + from_type = data.type + + case _: + sys.exit(EvaluatorCastDataError(data)()) + + return _cast_to(data, int, from_type, "bool") + + +@insert_cast_fns(("float", "bool")) +def float_to_bool(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from any float to bool. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal data as bool + """ + + from_type: str + match data: + case BaseDataContainer(): + literal: Literal = next(iter(data.data)) + from_type = literal.type + + case Literal(): + from_type = data.type + + case _: + sys.exit(EvaluatorCastDataError(data)()) + + return _cast_to(data, float, from_type, "bool") + + +##################### +# INTEGER FUNCTIONS # +##################### + + +@insert_cast_fns(("int", "float")) +def int_to_float(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from int to float. Data is expected to be + a literal of type int (it must be checked by this function caller.) + + Args: + data: CoreLiteral + + Returns: + Literal data as a float + """ + + match data: + case BaseDataContainer(): + # Probably itś not base data container, because the integer type should + # be known already (u32, i32, u64, i64, ...) instead of generic int + sys.exit(EvaluatorCastWildcardBuiltinTypeError("int")()) + + case Literal(): + return Literal(str(int(data.value)), "float") + + case _: + _invalid_case_cast(data, "int", "float") + + +def _cast_to_smaller_bitsize( + data: BaseDataContainer | Literal | Any, + type_fn: Callable, + cast_fn: Callable, + min_value: Any, + max_value: Any, + data_type: str, + to_type: str, +) -> Literal: + """ + Cast wildcard type data given function its type (``type_fn``), + a ``cast_fn`` (ctypes function) and number of bits (32, 64) to + a specific type. + """ + + value: Any + match data: + case BaseDataContainer(): + value = type_fn(next(iter(data.data))) + + case Literal(): + value = type_fn(data.value) + + case _: + sys.exit(EvaluatorCastDataError(data)()) + + if value > max_value or value < min_value: + sys.exit(DataOverflowError(data, data_type, to_type)()) + + return Literal(str(cast_fn(value).value), to_type) + + +@insert_cast_fns(("int", "u32")) +def int_to_u32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from int to u32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as u32 + """ + + return _cast_to_smaller_bitsize(data, int, ctypes.c_uint32, U32_MIN, U32_MAX, "int", "u32") + + +@insert_cast_fns(("int", "i32")) +def int_to_i32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from int to i32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as i32 + """ + + return _cast_to_smaller_bitsize(data, int, ctypes.c_int32, I32_MIN, I32_MAX, "int", "i32") + + +@insert_cast_fns(("int", "u64")) +def int_to_u64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from int to u64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as u64 + """ + + return _cast_to_smaller_bitsize(data, int, ctypes.c_uint64, U64_MIN, U64_MAX, "int", "u64") + + +@insert_cast_fns(("int", "i64")) +def int_to_i64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from int to i64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as i64 + """ + + return _cast_to_smaller_bitsize(data, int, ctypes.c_int64, I64_MIN, I64_MAX, "int", "i64") + + +@insert_cast_fns(("u32", "float")) +def u32_to_float(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from u32 to float. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as float + """ + + return _cast_to(data, float, "u32", "float") + + +@insert_cast_fns(("u32", "f32")) +def u32_to_f32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from u32 to f32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f32 + """ + + return _cast_to(data, float, "u32", "f32") + + +@insert_cast_fns(("u32", "f64")) +def u32_to_f64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from u32 to f64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f64 + """ + + return _cast_to(data, float, "u32", "f64") + + +@insert_cast_fns(("u64", "f32")) +def u64_to_f32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from u64 to f32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f32 + """ + + return _cast_to_smaller_bitsize(data, float, ctypes.c_float, F32_MIN, F32_MAX, "u64", "f32") + + +@insert_cast_fns(("u64", "f64")) +def u64_to_f64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from u64 to f64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f64 + """ + + return _cast_to(data, float, "u64", "f64") + + +@insert_cast_fns(("i32", "f32")) +def i32_to_f32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from i32 to f32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f32 + """ + + return _cast_to(data, float, "i32", "f32") + + +@insert_cast_fns(("i32", "f64")) +def i32_to_f64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from i32 to f64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f64 + """ + + return _cast_to(data, float, "i32", "f64") + + +@insert_cast_fns(("i64", "f32")) +def i64_to_f32(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from i64 to f32. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f32 + """ + + return _cast_to_smaller_bitsize(data, float, ctypes.c_float, F32_MIN, F32_MAX, "i64", "f32") + + +@insert_cast_fns(("i64", "f64")) +def i64_to_f64(data: BaseDataContainer | Literal | Any) -> Literal: + """ + Cast conversion function from i64 to f64. + + Args: + data: BaseDataContainer or CoreLiteral + + Returns: + Literal as f64 + """ + + return _cast_to(data, float, "i64", "f64") + + +################### +# FLOAT FUNCTIONS # +################### + + +@insert_cast_fns(("float", "int")) +def float_to_int(data: BaseDataContainer | Literal | Any) -> float: + """ + Cast conversion function to convert float to int + Args: + data: + + Returns: + + """ + + +##################### +# HASHMAP FUNCTIONS # +##################### + + +@insert_cast_fns(("hashmap", "int")) +def hashmap_to_int(data: BaseDataContainer | Any) -> Literal: + """ + + Args: + data: + + Returns: + + """ + + raise NotImplementedError() + + +@insert_cast_fns(("hashmap", "float")) +def hashmap_to_float(data: BaseDataContainer | Any) -> Literal: + """ + + Args: + data: + + Returns: + + """ + + raise NotImplementedError() + + +#################### +# SAMPLE FUNCTIONS # +#################### + + +@insert_cast_fns(("sample", "int")) +def sample_to_int(data: BaseDataContainer | Any) -> Literal: + """ + + Args: + data: + + Returns: + + """ + + raise NotImplementedError() + + +@insert_cast_fns(("sample", "float")) +def sample_to_float(data: BaseDataContainer | Any) -> Literal: + """ + + Args: + data: + + Returns: + + """ + + raise NotImplementedError() diff --git a/python/src/hhat_lang/dialects/heather/code/ast.py b/python/src/hhat_lang/dialects/heather/code/ast.py deleted file mode 100644 index 492b903e..00000000 --- a/python/src/hhat_lang/dialects/heather/code/ast.py +++ /dev/null @@ -1,302 +0,0 @@ -from __future__ import annotations - -from hhat_lang.core.code.ast import AST, Node, Terminal - - -############### -# AST CLASSES # -############### - - -class Id(Terminal): - def __init__(self, value: str): - self._value = (value,) - self._name = value - - -class CompositeId(Node): - def __init__(self, *names: Id): - self._value = names - self._name = self.__class__.__name__ - - -class CompositeIdWithClosure(Node): - """ - Used for calling many attributes/properties from the same Id root, for instance: - - ``` - user-info.{host port} - dataset.{obj-name pos.{x y}} - ``` - - As showed above, it can be nested. - """ - - def __init__(self, *values: Id | CompositeId, name: Id | CompositeId): - self._value = (name, values) - self._name = self.__class__.__name__ - - -class ArgValuePair(Node): - def __init__(self, arg: Id, value: ValueType): - self._value = (arg, value) - self._name = self.__class__.__name__ - - -class OnlyValue(Node): - def __init__(self, value: ValueType): - self._value = (value,) - self._name = self.__class__.__name__ - - -class Modifier(Node): - def __init__(self, *modifiers: ArgValuePair): - self._values = modifiers - self._name = self.__class__.__name__ - - -class ModifiedId(Node): - """ - A modifier is in the form of `< value ... >` or `< arg:value ... >`. - It is intended to change the behavior of the modified, which can be a - variable, a type or a function call. - """ - - def __init__(self, name: Id | CompositeId, modifier: Modifier): - self._value = (name, modifier) - self._name = self.__class__.__name__ - - -class Literal(Terminal): - def __init__(self, value: str, value_type: str): - self._value = (value,) - self._name = value_type - - -class Array(Node): - pass - - -class Hash(Node): - pass - - -class Cast(Node): - """ - A special syntax sugar that is intended to change the type of what it is - being applied to (usually a variable). The most important use case is to - cast a quantum data to a classical type. - """ - - def __init__(self, name: TypeType, cast_to: TypeType): - self._value = (name, cast_to) - self._name = self.__class__.__name__ - - -class Expr(Node): - def __init__(self, *expr: AST): - self._value = expr - self._name = self.__class__.__name__ - - -class Declare(Node): - def __init__(self, var_name: Id, var_type: TypeType): - self._value = (var_name, var_type) - self._name = self.__class__.__name__ - - -class Assign(Node): - def __init__(self, var_name: TypeType, expr: Expr): - self._value = (var_name, expr) - self._name = self.__class__.__name__ - - -class DeclareAssign(Node): - def __init__( - self, - var_name: Id, - var_type: TypeType, - expr: Expr, - ): - self._value = (var_name, var_type, expr) - self._name = self.__class__.__name__ - - -class CallArgs(Node): - def __init__(self, *args: ArgValuePair | OnlyValue): - self._values = args - self._name = self.__class__.__name__ - - -class Call(Node): - def __init__(self, caller: TypeType, args: CallArgs): - self._value = (caller, args) - self._name = self.__class__.__name__ - - -class MethodCallArgs(Node): - def __init__(self, *args: ArgValuePair | OnlyValue): - self._values = args - self._name = self.__class__.__name__ - - -class MethodCall(Node): - def __init__(self, self_caller: TypeType, args: CallArgs): - self._value = (self_caller, args) - self._name = self.__class__.__name__ - - -class InsideOption(Node): - def __init__(self, option: Expr, body: Body): - self._value = (option, body) - self._name = self.__class__.__name__ - - -class CallWithBodyOptions(Node): - def __init__( - self, - *call_options: InsideOption, - caller: TypeType, - args: CallArgs, - ): - self._value = (caller, args, call_options) - self._name = self.__class__.__name__ - - -class CallWithArgsBodyOptions(Node): - def __init__(self, *arg_options: InsideOption, caller: TypeType): - self._value = (caller, arg_options) - self._name = self.__class__.__name__ - - -class CallWithBody(Node): - def __init__( - self, caller: TypeType, args: CallArgs, body: Body - ): - self._value = (caller, args, body) - self._name = self.__class__.__name__ - - -class ArgTypePair(Node): - def __init__(self, arg_name: Id, arg_type: TypeType): - self._value = (arg_name, arg_type) - self._name = self.__class__.__name__ - - -class FnArgs(Node): - def __init__(self, *args: ArgTypePair): - self._values = args - self._name = self.__class__.__name__ - - -class FnDef(Node): - def __init__( - self, - fn_name: Id, - fn_type: TypeType, - args: FnArgs, - body: Body, - ): - self._value = (fn_name, fn_type, args, body) - self._name = self.__class__.__name__ - - -class TypeMember(Node): - def __init__(self, member_name: Id, member_type: TypeType): - self._value = (member_name, member_type) - self._name = self.__class__.__name__ - - -class SingleTypeMember(Node): - def __init__(self, member_type: TypeType): - self._value = (member_type,) - self._name = self.__class__.__name__ - - -class EnumTypeMember(Node): - def __init__(self, member_name: Id): - self._value = (member_name,) - self._name = self.__class__.__name__ - - -class TypeDef(Node): - def __init__( - self, - *members: TypeMember | SingleTypeMember | EnumTypeMember, - type_name: TypeType, - type_ds: Id, - ): - self._value = (type_name, type_ds, members) - self._name = self.__class__.__name__ - - -class TypeImport(Node): - def __init__(self, type_list: tuple[Id | CompositeId | CompositeIdWithClosure]): - self._value = type_list - self._name = self.__class__.__name__ - - -class FnImport(Node): - def __init__(self, fn_list: tuple[Id | CompositeId | CompositeIdWithClosure]): - self._value = fn_list - self._name = self.__class__.__name__ - - -class Imports(Node): - """ - Importing types and then functions to the program. - """ - - def __init__(self, *, type_import: tuple[TypeImport, ...], fn_import: tuple[FnImport, ...]): - self._value = (type_import, fn_import) - self._name = self.__class__.__name__ - - -class Body(Node): - """ - Body of a closure. - """ - - def __init__(self, *body: BodyType): - self._values = body - self._name = self.__class__.__name__ - - -class Main(Node): - """ - The `main` closure, where the main execution lives. - """ - - def __init__(self, *body: AST): - self._value = body - self._name = self.__class__.__name__ - - -class Program(Node): - def __init__(self, *, main: Main, imports: Imports): - self._value = (imports, main) - self._name = self.__class__.__name__ - - -##################### -# DESCRIPTIVE TYPES # -##################### - -ValueType = Id | CompositeId | ModifiedId | Literal | Array | Hash - -TypeType = Id | CompositeId | ModifiedId - -BodyType = ( - Call - | CallWithBody - | CallWithBodyOptions - | Expr - | MethodCall - | Declare - | Assign - | DeclareAssign - | Array - | Hash - | Literal - | Id -) diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/__init__.py new file mode 100644 index 00000000..662e6e90 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from hhat_lang.core.fns import BUILTIN_CORE_MODULE_ROOT_PATH + + +CORE_MODULE_PATH = BUILTIN_CORE_MODULE_ROOT_PATH / "core.hat" diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/fn_def.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/fn_def.py new file mode 100644 index 00000000..971d3a10 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/core/fn_def.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.data.core import SimpleObj, ObjArray, Symbol + + +def builtin_fn__match(*option_body: Any, match_arg: Any) -> Symbol: + # TODO: implement match (``optbdn_t`` type) + return Symbol("empty") diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/__init__.py new file mode 100644 index 00000000..34de1f2d --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from hhat_lang.dialects.heather.code.builtins.fns.io.fn_def import builtin_fn__print + + +__all__ = ["builtin_fn__print"] diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/fn_def.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/fn_def.py new file mode 100644 index 00000000..a7896626 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/io/fn_def.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.data.core import SimpleObj, ObjArray, Symbol + + +def builtin_fn__print(*args: SimpleObj | ObjArray, **_: Any) -> Symbol: + # transforming WorkingData/CompositeWorkingData into python objects + for k in args: + match k: + case SimpleObj(): + print(k.value, end="") + + case ObjArray(): + print(*k.value, end="") + + case _: + raise NotImplementedError(f"print with {type(k)} not implemented") + + print() + return Symbol("empty") diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/__init__.py new file mode 100644 index 00000000..339dadbf --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from hhat_lang.core.fns import BUILTIN_STD_MODULE_ROOT_PATH + + +MATH_MODULE_PATH = BUILTIN_STD_MODULE_ROOT_PATH / "math" + + +######################## +# CONSTRUCTORS SECTION # +######################## + +# TODO: create a class to work as the "math" module diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/__init__.py new file mode 100644 index 00000000..1cf3afdd --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from hhat_lang.dialects.heather.code.builtins.fns.math import MATH_MODULE_PATH + + +ARITHMETIC_MODULE_PATH = MATH_MODULE_PATH / "arithmetic.hat" diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/fn_def.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/fn_def.py new file mode 100644 index 00000000..f4054525 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/arithmetic/fn_def.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import sys +from functools import reduce +from typing import Any + +from hhat_lang.core.code.base import BaseFnKey +from hhat_lang.core.data.core import ( + Literal, + Symbol, +) +from hhat_lang.core.error_handlers.errors import FunctionExecutionError +from hhat_lang.core.fns.core import include_builtin_fn +from hhat_lang.core.memory.core import MemoryManager + +from hhat_lang.dialects.heather.code.builtins.fns.math.arithmetic import ( + ARITHMETIC_MODULE_PATH, +) + + +#################### +# ADDITION SECTION # +#################### + + +def _add_res(*args: Literal, mem: MemoryManager) -> str: + if len(args) >= 2: + return str(reduce(lambda x, y: x + float(y.value), args[1:], float(args[0].value))) + + sys.exit( + FunctionExecutionError( + *args, fn_name="add", reason="operation needs more than 1 argument" + )() + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("add"), + fn_type=Symbol("int"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_add(*args: Literal, mem: MemoryManager) -> Literal: + """Add two integer numbers `a+b` and return an integer `c`.""" + return Literal( + str(reduce(lambda x, y: x + int(y.value), args[1:], int(args[0].value))), + lit_type="int", + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("add"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_add(*args: Literal, mem: MemoryManager) -> Literal: + return Literal(_add_res(*args, mem=mem), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("add"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_float_add(*args: Literal, mem: MemoryManager) -> Literal: + return Literal(_add_res(*args, mem=mem), lit_type="float") + + +####################### +# SUBTRACTION SECTION # +####################### + + +def _sub_res(*args: Literal, mem: MemoryManager) -> str: + if len(args) >= 2: + return str(reduce(lambda x, y: x - float(y.value), args[1:], float(args[0].value))) + + sys.exit( + FunctionExecutionError( + *args, fn_name="sub", reason="operation needs more than 1 argument" + )() + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("sub"), + fn_type=Symbol("int"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_sub(*args: Literal, mem: MemoryManager) -> Literal: + return Literal( + str(reduce(lambda x, y: x - int(y.value), args[1:], int(args[0].value))), + lit_type="int", + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("sub"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_sub(*args: Literal, mem: MemoryManager) -> Any: + return Literal(_sub_res(*args, mem=mem), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("sub"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_float_sub(*args: Literal, mem: MemoryManager) -> Any: + return Literal(_sub_res(*args, mem=mem), lit_type="float") + + +########################## +# MULTIPLICATION SECTION # +########################## + + +def _mul_res(*args: Literal, mem: MemoryManager) -> str: + if len(args) >= 2: + return str(reduce(lambda x, y: x * float(y.value), args[1:], float(args[0].value))) + + sys.exit( + FunctionExecutionError( + *args, fn_name="mul", reason="operation needs more than 1 argument" + )() + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("mul"), + fn_type=Symbol("int"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_mul(*args: Literal, mem: MemoryManager) -> Literal: + return Literal( + str(reduce(lambda x, y: x * int(y.value), args[1:], int(args[0].value))), + lit_type="int", + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("mul"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_mul(*args: Any, mem: MemoryManager) -> Literal: + return Literal(_mul_res(*args, mem=mem), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("mul"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_float_mul(*args: Any, mem: MemoryManager) -> Literal: + return Literal(_mul_res(*args, mem=mem), lit_type="float") + + +#################### +# DIVISION SECTION # +#################### + + +def _div_res(*args: Literal, mem: MemoryManager) -> str: + if len(args) >= 2: + return str(reduce(lambda x, y: x / float(y.value), args[1:], float(args[0].value))) + + sys.exit( + FunctionExecutionError( + *args, fn_name="div", reason="operation needs more than 1 argument" + )() + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("div"), + fn_type=Symbol("int"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_div(*args: Literal, mem: MemoryManager) -> Literal: + return Literal( + str(reduce(lambda x, y: x // int(y.value), args[1:], int(args[0].value))), + lit_type="int", + ) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("div"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_div(*args: Literal, mem: MemoryManager) -> Literal: + return Literal(_div_res(*args, mem=mem), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("div"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_float_div(*args: Literal, mem: MemoryManager) -> Literal: + return Literal(_div_res(*args, mem=mem), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("div"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_int_div(*args: Literal, mem: MemoryManager) -> Literal: + return Literal(_div_res(*args, mem=mem), lit_type="float") + + +################# +# POWER SECTION # +################# + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("pow"), + fn_type=Symbol("int"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_pow(base: Literal, power: Literal, mem: MemoryManager) -> Literal: + return Literal(str(int(base.value) ** int(power.value)), lit_type="int") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("pow"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_pow(base: Literal, power: Literal, mem: MemoryManager) -> Literal: + return Literal(str(float(base.value) ** float(power.value)), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("pow"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("int"), Symbol("float")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_int_float_pow(base: Literal, power: Literal, mem: MemoryManager) -> Literal: + return Literal(str(int(base.value) ** float(power.value)), lit_type="float") + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Symbol("pow"), + fn_type=Symbol("float"), + args_names=(Symbol("a"), Symbol("b")), + args_types=(Symbol("float"), Symbol("int")), + ), + fn_path=ARITHMETIC_MODULE_PATH, +) +def builtin_fn_float_int_pow(base: Literal, power: Literal, mem: MemoryManager) -> Literal: + return Literal(str(float(base.value) ** int(power.value)), lit_type="float") diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/__init__.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/__init__.py new file mode 100644 index 00000000..8dc07f09 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from hhat_lang.dialects.heather.code.builtins.fns.math import MATH_MODULE_PATH + + +TRIGONOMETRY_MODULE_PATH = MATH_MODULE_PATH / "trigonometry.hat" diff --git a/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/fn_def.py b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/fn_def.py new file mode 100644 index 00000000..824bcb76 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/builtins/fns/math/trigonometry/fn_def.py @@ -0,0 +1,16 @@ +from __future__ import annotations + + +# TODO: implement trigonometric functions + +################## +# COSINE SECTION # +################## + +################ +# SINE SECTION # +################ + +############### +# TAN SECTION # +############### diff --git a/python/src/hhat_lang/dialects/heather/code/ir_builder.py b/python/src/hhat_lang/dialects/heather/code/ir_builder.py deleted file mode 100644 index 448cace6..00000000 --- a/python/src/hhat_lang/dialects/heather/code/ir_builder.py +++ /dev/null @@ -1,445 +0,0 @@ -""" -In this file there are three distinct sections: - -1. The building functions to get from AST to something that IR and the IR tables can handle; -2. The actual IR tables builders, namely types and functions; and -3. The main code builder where the `main` closure lies. -""" - -from __future__ import annotations - -from typing import Any - -from hhat_lang.core.code.ast import AST -from hhat_lang.core.data.core import ( - Symbol, - CompositeSymbol, - CoreLiteral, -) -from hhat_lang.dialects.heather.parsing.imports import ( - parse_imports, parse_types, parse_types_compositeid, - parse_types_compositeidwithclosure -) -from hhat_lang.dialects.heather.code.ast import ( - Id, - CompositeId, - CompositeIdWithClosure, - Cast, - ArgValuePair, - OnlyValue, - Modifier, - ModifiedId, - Literal, - Array, - Hash, - Expr, - Declare, - Assign, - DeclareAssign, - CallArgs, - Call, - MethodCallArgs, - MethodCall, - InsideOption, - CallWithBodyOptions, - CallWithBody, - ArgTypePair, - FnArgs, - FnDef, - TypeMember, - SingleTypeMember, - EnumTypeMember, - TypeDef, - FnImport, - TypeImport, - Imports, - Body, - Main, - Program, - ValueType, - TypeType, - BodyType, -) -# for now just a simple IR for the interpreter suffices -from hhat_lang.dialects.heather.code.simple_ir_builder.ir import IR -from hhat_lang.dialects.heather.code.simple_ir_builder.builder import ( - define_id, - define_compositeid, - define_literal, - define_argvaluepair, -) - -# TODO: include other implementation modules for the IR, as below. -# - each one of them should contain all the named functions -# - the correct IR module should be read from some configuration file -""" -from hhat_heather.code.mlir_ir import define_id -... -""" - - -######################################################## -# FUNCTION BUILDERS FROM AST TO ACTUAL CODE FOR THE IR # -######################################################## - -def _build_id(code: Id) -> Symbol: - return define_id(code) - - -def _build_compositeid(code: CompositeId) -> CompositeSymbol: - return define_compositeid(code) - - -def _build_argvaluepair(code: ArgValuePair) -> tuple[Symbol, Any]: - return define_argvaluepair(code) - - -def _build_onlyvalue(code: OnlyValue) -> Symbol | CompositeSymbol | CoreLiteral | Any: - return _build_valuetype(code.value[0]) - - -def _build_modifier(code: Modifier) -> tuple[tuple[Symbol, Any], ...]: - mods = () - - for mod in code.value: - - arg, value = _build_argvaluepair(mod) - mods += ((arg, value),) - - return mods - - -def _build_modifiedid(code: ModifiedId) -> Any: - pass - - -def _build_literal(code: Literal) -> CoreLiteral: - return define_literal(code) - - -def _build_array(code: Array) -> Any: - pass - - -def _build_hash(code: Hash) -> Any: - pass - - -def _build_expr(code: Expr) -> Any: - pass - - -def _build_declare(code: Declare) -> Any: - var_name = _build_id(code.value[0]) - var_type = _build_typetype(code.value[1]) - - -def _build_assign(code: Assign) -> Any: - pass - - -def _build_declareassign(code: DeclareAssign) -> Any: - pass - - -def _build_callargs(code: CallArgs) -> Any: - pass - - -def _build_call(code: Call) -> Any: - pass - - -def _build_methodcallargs(code: MethodCallArgs) -> Any: - pass - - -def _build_methodcall(code: MethodCall) -> Any: - pass - - -def _build_insideoption(code: InsideOption) -> Any: - pass - - -def _build_callwithbodyoptions(code: CallWithBodyOptions) -> Any: - pass - - -def _build_callwithbody(code: CallWithBody) -> Any: - pass - - -def _build_argtypepair(code: ArgTypePair) -> tuple[Symbol, Any]: - pass - - -def _build_fnargs(code: FnArgs) -> Any: - pass - - -def _build_fndef(code: FnDef) -> Any: - pass - - -def _build_typemember(code: TypeMember) -> Any: - pass - - -def _build_singletypemember(code: SingleTypeMember) -> Any: - pass - - -def _build_enumtypemember(code: EnumTypeMember) -> Any: - pass - - -def _build_typedef(code: TypeDef) -> Any: - pass - - -def _build_typeimport(code: TypeImport) -> Any: - for k in code: - - match k: - case Id(): - pass - - case CompositeId(): - res = parse_types_compositeid(k) - - case CompositeIdWithClosure(): - res = parse_types_compositeidwithclosure(k) - - -def _build_fnimport(code: FnImport) -> Any: - pass - - -def _build_imports(code: Imports) -> Any: - for k in code: - - match k: - case TypeImport(): - return _build_typeimport(k) - - case FnImport(): - return _build_fnimport(k) - - case _: - raise ValueError(f"invalid import syntax\n =>\n{k}\n") - - -def _build_body(code: Body) -> Any: - for k in code: - - _build_bodytype(k) - - -############################## -# TYPES DESCRIPTORS BUILDERS # -############################## - - -def _build_valuetype(code: ValueType) -> Any: - """Build based on the `ValueType` type descriptor.""" - - match tmp := code: - - case Id(): - return _build_id(tmp) - - case CompositeId(): - return _build_compositeid(tmp) - - case ModifiedId(): - return _build_modifiedid(tmp) - - case Literal(): - return _build_literal(tmp) - - case Array(): - raise NotImplementedError("array not implemented") - - case Hash(): - raise NotImplementedError("hash not implemented") - - case _: - raise ValueError(f"unknown '{code}'.") - - -def _build_typetype(code: TypeType) -> Any: - """Build based on the `TypeType` type descriptor.""" - - match code: - case Id(): - return _build_id(code) - - case CompositeId(): - return _build_compositeid(code) - - case ModifiedId(): - return _build_modifiedid(code) - - case _: - raise NotImplementedError() - - -def _build_bodytype(code: BodyType) -> Any: - """Build based on the `BodyType` type descriptor.""" - - match k := code: - case Expr(): - _build_expr(k) - - case Declare(): - _build_declare(k) - - case Assign(): - _build_assign(k) - - case DeclareAssign(): - _build_declareassign(k) - - case Call(): - _build_call(k) - - case MethodCall(): - _build_methodcall(k) - - case CallWithBody(): - _build_callwithbody(k) - - case CallWithBodyOptions(): - _build_callwithbodyoptions(k) - - -################## -# TABLE BUILDERS # -################## - -def build_typetable(code: AST) -> Any: - for k in code: - - match k: - - case Id(): - pass - - case CompositeId(): - pass - - case ArgValuePair(): - pass - - case OnlyValue(): - pass - - case Modifier(): - pass - - case ModifiedId(): - pass - - case Literal(): - pass - - case Array(): - pass - - case Hash(): - pass - - case Expr(): - pass - - case Declare(): - pass - - case Assign(): - pass - - case DeclareAssign(): - pass - - case CallArgs(): - pass - - case Call(): - pass - - case MethodCallArgs(): - pass - - case MethodCall(): - pass - - case InsideOption(): - pass - - case CallWithBodyOptions(): - pass - - case CallWithBody(): - pass - - case ArgTypePair(): - pass - - case FnArgs(): - pass - - case TypeMember(): - pass - - case TypeDef(): - pass - - case Imports(): - pass - - case Body(): - pass - - case Main(): - pass - - case Program(): - pass - - case _: - raise NotImplementedError() - - -def build_fntable(code: AST) -> Any: - fn_name = code.value[0] - fn_type = code.value[1] - fn_args = _build_fnargs(code.value[2]) - fn_body = _build_body(code.value[3]) - - -############# -# MAIN CODE # -############# - -def build_main(code: AST) -> Any: - ir = IR() - - for k in code: - - match k: - - case Imports(): - res = parse_imports(k) - - case Body(): - pass - - case Main(): - pass - - case Program(): - pass - - case _: - raise NotImplementedError() diff --git a/python/src/hhat_lang/dialects/heather/code/mlir_builder/ir.py b/python/src/hhat_lang/dialects/heather/code/mlir_builder/ir.py deleted file mode 100644 index 316230d7..00000000 --- a/python/src/hhat_lang/dialects/heather/code/mlir_builder/ir.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Implement MLIR to be used on `ir_builder.py`.""" - -from __future__ import annotations - -from hhat_heather.code.ast import ( - Id, - # types descriptors -) - - -def define_id(code: Id): - raise NotImplementedError() diff --git a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/builder.py b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/builder.py deleted file mode 100644 index c57f766a..00000000 --- a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/builder.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -from typing import Any, cast - -from hhat_lang.dialects.heather.code.ast import ( - Id, - CompositeId, - Literal, - ArgValuePair, - ValueType, - ModifiedId, - Array, - Hash, - Declare, - Assign, - DeclareAssign, -) -from hhat_lang.core.code.utils import check_quantum_type_correctness -from hhat_lang.core.data.core import Symbol, CompositeSymbol, CoreLiteral - - -######################### -# IR DEFINING FUNCTIONS # -######################### - -def define_id(code: Id) -> Symbol: - name: str = cast(str, code.value[0]) - return Symbol(name) - - -def define_compositeid(code: CompositeId) -> CompositeSymbol: - names: tuple[str, ...] = cast(tuple, code.value) - check_quantum_type_correctness(names) - return CompositeSymbol(names) - - -def define_literal(code: Literal) -> CoreLiteral: - return CoreLiteral(code.value[0], code.name) - - -def define_argvaluepair(code: ArgValuePair) -> tuple[Symbol, Any]: - arg_name = cast(Id, code.value[0]) - arg = define_id(arg_name) - - value = define_valuetype(code.value[1]) - - return arg, value - - -def define_valuetype(code: ValueType) -> Any: - match code: - - case Id(): - return define_id(code) - - case CompositeId(): - return define_compositeid(code) - - case Literal(): - return define_literal(code) - - case ModifiedId(): - raise NotImplementedError() - - case Array(): - raise NotImplementedError() - - case Hash(): - raise NotImplementedError() - - case _: - raise ValueError(f"unknown '{code}'.") - - -def define_declare(code: Declare) -> Any: - pass - - -def define_assign(code: Assign) -> Any: - pass - - -def define_declareassign(code: DeclareAssign) -> Any: - pass - diff --git a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/ir.py b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/ir.py deleted file mode 100644 index 0a7754b8..00000000 --- a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/ir.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -Simple IR implementation. Intended to be simple for AST conversion and -readiness for the evaluator. -""" - -from __future__ import annotations - -from typing import Any, Iterable - -from hhat_lang.core.code.ast import AST -from hhat_lang.core.code.ir import BaseIR, BaseFnIR, InstrIR, ArgsIR, InstrIRFlag, BlockIR -from hhat_lang.core.data.core import Symbol, CompositeSymbol, CoreLiteral, CompositeLiteral - - -class IRInstr(InstrIR): - def __init__(self, name: Symbol | CompositeSymbol, args: IRArgs, flag: InstrIRFlag): - if ( - isinstance(name, (Symbol, CompositeSymbol)) - and isinstance(args, IRArgs) - and isinstance(flag, InstrIRFlag) - ): - self._name = name - self._args = args - self._flag = flag - - -class IRArgs(ArgsIR): - def __init__(self, *args: Symbol | CompositeSymbol | CoreLiteral | CompositeLiteral): - if all( - isinstance(k, (Symbol, CompositeSymbol, CoreLiteral, CompositeLiteral)) - for k in args - ) or len(args) == 0: - self._args = args - - -class IRBlock(BlockIR): - def __init__(self): - self._instrs = tuple() - - def add_instr(self, instr: IRInstr | IRBlock) -> None: - if isinstance(instr, IRInstr | IRBlock): - self._instrs += (instr,) - - -################ -# IR BASE CODE # -################ - -def compile_to_ir(code: AST) -> IR: - pass - - -class FnIR(BaseFnIR): - def __init__(self): - self._data = dict() - - def push(self, *ags: Any, **kwargs: Any) -> Any: - pass - - def get(self, item: Any) -> Any: - pass - - def __setitem__(self, key: Any, value: Any) -> None: - pass - - def __getitem__(self, key: Symbol | CompositeSymbol) -> Any: - pass - - def __contains__(self, item: Any) -> bool: - pass - - -class IR(BaseIR): - """ - The IR class that contains all the relevant code to be evaluated, including - types, functions and `main` body. An evaluator class must use this one to - execute classical instructions. - """ - - def __init__(self): - super().__init__() - - def add_fn( - self, - *, - fn_name: Symbol, - fn_type: Symbol | CompositeSymbol, - fn_args: Any, - body: IRBlock, - ) -> None: - ... diff --git a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir.py b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir.py new file mode 100644 index 00000000..0c54d104 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir.py @@ -0,0 +1,875 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any, cast, Callable + +from hhat_lang.core.code.abstract import BaseIR, BaseIRModule, IRHash, RefTable +from hhat_lang.core.code.base import ( + BaseFnCheck, + BaseIRInstr, +) +from hhat_lang.core.code.ir_block import ( + IRFlag, + IRBlock, + IRInstr, +) +from hhat_lang.core.code.ir_custom import ( + ArgsValuesBlock, + BodyBlock, + ReturnBlock, + ModifierBlock, + ArgsBlock, + OptionBlock, +) +from hhat_lang.core.code.ir_graph import ( + IRGraph, + IRNode, +) +from hhat_lang.core.code.tools import ( + get_type, + get_fn, +) +from hhat_lang.core.code.symbol_table import SymbolTable +from hhat_lang.core.data.core import ( + LiteralArray, + CompositeSymbol, + ObjArray, + Literal, + Symbol, + SimpleObj, +) +from hhat_lang.core.data.fn_def import FnDef +from hhat_lang.core.data.utils import VariableKind +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import HeapInvalidKeyError +from hhat_lang.core.memory.core import MemoryManager +from hhat_lang.core.types.abstract_base import BaseTypeDef +from hhat_lang.core.types.builtin_conversion import compatible_types +from hhat_lang.core.types.builtin_types import builtins_types +from hhat_lang.dialects.heather.cast.base import CastQ2C, CastC2C, CastC2Q, CastQ2Q + +# from hhat_lang.dialects.heather.code.builtins.fns import BUILTIN_FN_DICT + + +########################### +# IR INSTRUCTIONS CLASSES # +########################### + + +class BuiltinInstr(BaseIRInstr): + def __init__(self, *args: Any, name: Symbol, flag: IRFlag): + self.args = (name, args) + self._name = flag + super().__init__() + + @property + def builtin_name(self) -> Symbol: + return cast(Symbol, self.args[0]) + + @property + def builtin_args(self) -> tuple[Any, ...] | tuple: + return self.args[1:] + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **kwargs: Any) -> Any: + # """ + # + # Args: + # mem: ``MemoryManager`` instance + # node: ``IRNode`` instance + # ir_graph: ``IRGraph`` instance + # **kwargs: extra arguments for the function to work + # + # Returns: + # Whatever the built-in function should return + # """ + # + # fns_dict: dict[tuple, Callable] = BUILTIN_FN_DICT[self.builtin_name.value] + # args = _resolve_call_args(*self.builtin_args, mem=mem, node=node, ir_graph=ir_graph) + # args_types = _resolve_call_args_types(*args) + # + # builtin_fn: Callable = fns_dict[args_types] + # # TODO: call builtin_fn() directly or through _handle_call_instr()? + # builtin_fn(*self.builtin_args) + + def __repr__(self) -> str: + return f"{self.name}({' '.join(str(k) for k in self.args)})" + + +class CastInstr(IRInstr): + def __init__( + self, + data: SimpleObj | ObjArray | ModifierBlock | BaseIRInstr, + to_type: Symbol | CompositeSymbol | ModifierBlock, + ): + if isinstance(data, SimpleObj | ObjArray | ModifierBlock | BaseIRInstr) and isinstance( + to_type, Symbol | CompositeSymbol | ModifierBlock + ): + super().__init__(data, to_type, name=IRFlag.CAST) + + else: + raise ValueError( + f"cast operation cannot contain {data} ({type(data)}) " + f"and {to_type} ({type(to_type)})" + ) + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **kwargs: Any) -> None: + # _data = _resolve_expr_to_data(self.args[0], mem, node, ir_graph) + # _type = _resolve_type(self.args[1], mem, node, ir_graph) + # _resolve_cast(_data, to_type=_type, mem=mem, node=node, ir_graph=ir_graph) + + +class CallInstr(IRInstr): + def __init__( + self, + name: Symbol | CompositeSymbol | ModifierBlock, + *, + args: ArgsBlock | ArgsValuesBlock | SimpleObj | ObjArray | None = None, + option: OptionBlock | None = None, + body: BodyBlock | None = None, + ): + instr_args: tuple[IRBlock | BaseIRInstr | SimpleObj] | tuple + + if args is not None and option is None and body is None: + instr_args = (args,) + flag = IRFlag.FN_CALL + + elif args is None and option is not None and body is None: + instr_args = (option,) + flag = IRFlag.OPTN_CALL + + elif args is not None and option is not None and body is None: + instr_args = (args, option) + flag = IRFlag.OPTBDN_CALL + + elif option is None and body is not None: + instr_args = (args, body) + flag = IRFlag.BDN_CALL + + else: + raise ValueError( + f"cannot contain option ({type(option)}) and body ({type(body)}) " + f"in the same instruction." + ) + + super().__init__(name, *instr_args, name=flag) + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **_: Any) -> None: + # match self.name: + # case IRFlag.BUILTIN_FN_CALL: + # args, fn_header = self._set_fn_call(ir_graph, mem, node) + # fn_def = get_fn(node_key=node.irhash, importing=fn_header, ir_graph=ir_graph) + # + # # set new stack for function context; it's freed when exiting the context + # with mem.new_fn_stack(*args, fn_header=fn_header): + # _resolve_builtin_fn(fn_def=fn_def, mem=mem, node=node, ir_graph=ir_graph) + # + # case IRFlag.FN_CALL: + # args, fn_header = self._set_fn_call(ir_graph, mem, node) + # mem.stack.new(for_fn_use=True) + # mem.stack.set_fn_entry(*args, fn_header=fn_header) + # _handle_call_instr( + # fn_header=fn_header, + # mem=mem, + # node=node, + # ir_graph=ir_graph, + # flag=self.name, + # ) + # mem.stack.free() + # + # # TODO: implement case for IRFlag.OPTN_CALL, IRFlag.BDN_CALL, IRFlag.OPTBDN_CALL + # + # # case IRFlag.OPTN_CALL: + # # pass + # # + # # case IRFlag.BDN_CALL: + # # pass + # # + # # case IRFlag.OPTBDN_CALL: + # # pass + # + # case _: + # raise NotImplementedError( + # f"resolve method from CallInstr for {self.name} not implemented" + # ) + # + # def _set_fn_call( + # self, ir_graph: IRGraph, mem: MemoryManager, node: IRNode + # ) -> tuple[tuple, BaseFnCheck]: + # caller: Symbol | CompositeSymbol | ModifierBlock = ( + # cast(Symbol | CompositeSymbol, self.args[0]) # type: ignore [assignment] + # if isinstance(self.args[0], Symbol | CompositeSymbol) + # else ( + # cast(ModifierBlock, self.args[0]).name + # if isinstance(self.args[0], ModifierBlock) + # else sys.exit("call instr error") + # ) + # ) + # args: tuple = self.args[1:] + # resolved_args = _resolve_call_args(*args, mem=mem, node=node, ir_graph=ir_graph) + # resolved_args_types = _resolve_call_args_types(*resolved_args) + # + # fn_header = BaseFnCheck(fn_name=caller, args_types=resolved_args_types) + # return args, fn_header + + +class DeclareInstr(IRInstr): + def __init__( + self, + var: Symbol | ModifierBlock, + var_type: Symbol | CompositeSymbol | ModifierBlock, + ): + if isinstance(var, Symbol | ModifierBlock) and isinstance( + var_type, Symbol | CompositeSymbol | ModifierBlock + ): + super().__init__(var, var_type, name=IRFlag.DECLARE) + + else: + raise ValueError( + f"var must be symbol, got {type(var)} and var type must be symbol" + f" or composite symbol, got {type(var_type)}" + ) + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **_: Any) -> None: + # var: Symbol | ModifierBlock = cast(Symbol | ModifierBlock, self.args[0]) + # var_type_symbol: Symbol | CompositeSymbol = cast(Symbol | CompositeSymbol, self.args[1]) + # _declare_variable(var, var_type_symbol, mem, node.irhash, ir_graph) + + +class AssignInstr(IRInstr): + def __init__( + self, + var: Symbol | ModifierBlock, + value: SimpleObj | ObjArray | IRBlock, + ): + if isinstance(var, Symbol | ModifierBlock) and isinstance( + value, SimpleObj | ObjArray | IRBlock + ): + super().__init__(var, value, name=IRFlag.ASSIGN) + + else: + raise ValueError( + f"var must be symbol, got {type(var)} and " + f"value must be working data or composite working data, got {type(value)}" + ) + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **_: Any) -> None: + # # TODO: refactor this + # + # var: Symbol = cast(Symbol, self.args[0]) + # variable = mem.scope.heap[mem.cur_scope].get(var) + # mem.scope.stack[mem.cur_scope].push(self.args[1]) + # + # # # resolve value to check and assign the correct type + # # new_args = _get_assign_datatype( + # # var_type=variable.type, + # # value=value, + # # heap_table=heap_table, + # # types_table=types_table + # # ) + # # # set new arguments + # # self.args = (self.args[0], *new_args) + # + # _assign_variable(variable=variable, mem=mem, node=node, ir_graph=ir_graph) + + +class DeclareAssignInstr(IRInstr): + def __init__( + self, + var: Symbol | ModifierBlock, + var_type: Symbol | CompositeSymbol | ModifierBlock, + value: SimpleObj | ObjArray | BaseIRInstr | IRBlock, + ): + if ( + isinstance(var, Symbol | ModifierBlock) + and isinstance(var_type, Symbol | CompositeSymbol | ModifierBlock) + and isinstance(value, SimpleObj | ObjArray | BaseIRInstr | IRBlock) + ): + super().__init__(var, var_type, value, name=IRFlag.DECLARE_ASSIGN) + + else: + raise ValueError( + f"var must be symbol, got {type(var)}, " + f"var type must be symbol or composite symbol, got {type(var_type)} and " + f"value must be working data or composite working data, got {type(value)}" + ) + + # def resolve(self, mem: MemoryManager, node: IRNode, ir_graph: IRGraph, **_: Any) -> None: + # var: Symbol = cast(Symbol, self.args[0]) + # var_type_symbol: Symbol | CompositeSymbol = cast(Symbol | CompositeSymbol, self.args[1]) + # _declare_variable(var, var_type_symbol, mem, node.irhash, ir_graph) + # variable: BaseDataContainer = cast(BaseDataContainer, mem.stack.get(var)) + # mem.stack.push(variable) + # _assign_variable(variable=variable, mem=mem, node=node, ir_graph=ir_graph) + + +############## +# IR CLASSES # +############## + + +class IRModule(BaseIRModule): + def __init__( + self, + path: Path, + symboltable: SymbolTable, + main: BodyBlock | None = None, + ): + self._path = path + self._symbol_table = symboltable + self._main = main or BodyBlock() + + def __str__(self) -> str: + st = "" + if self.symbol_table.type: + st += f"{self.symbol_table.type}" + + if self.symbol_table.fn: + st += f"{self.symbol_table.fn}" + + if st: + st = f"\n - symbol table:{st}" + + main = "" + if self.main: + main += "\n - main:\n " + main += "\n ".join(str(k) for k in self.main) + main += "\n" + + return f"{IRHash(self._path)}{st}{main}" + + +class IR(BaseIR): + """Hold all the IR content: IR blocks, IR types and IR functions""" + + def __init__( + self, + *, + ref_table: RefTable, + ir_module: IRModule, + ): + if isinstance(ir_module, IRModule) and isinstance(ref_table, RefTable): + self._module = ir_module + self._ref_table = ref_table + + else: + raise ValueError("cannot have main IR block and symbol table in the same IR") + + def __repr__(self) -> str: + rf = "" + + if self.ref_table.types: + rf += "\n".join(f" {t}:{t_def}" for t, t_def in self.ref_table.types) + + if self.ref_table.fns: + rf += "\n".join(f" {f}:{f_def}" for f, f_def in self.ref_table.fns) + + if rf: + rf = f"\n ref table:\n{rf}\n" + + module = f"\n module:{self.module}" + + return f"\n=IR:start={rf}{module}=IR:end=\n" + + +################## +# MISC FUNCTIONS # +################## + + +# def _declare_variable( +# var: Symbol | CompositeSymbol | ModifierBlock, +# var_type_symbol: Symbol | CompositeSymbol, +# mem: MemoryManager, +# node_hash: IRHash, +# ir_graph: IRGraph, +# ) -> None: +# """ +# Convenient function for resolving variable declaration during the execution execution +# and store it on the memory for further use. +# +# Args: +# var: the actual variable; must be a ``Symbol`` or ``ModifierBlock`` object +# var_type_symbol: +# mem: ``MemoryManager`` object +# node_hash: +# ir_graph: +# """ +# +# # we just need the variable for now +# var_symbol = cast( +# Symbol | CompositeSymbol, var.args[0] if isinstance(var, ModifierBlock) else var +# ) +# # TODO: make use of the modifier property through a new code logic later +# +# if var_symbol in mem.stack: +# raise ValueError(f"{var_symbol} already in scope memory; cannot re-declare variable") +# +# var_type = get_type(node_key=node_hash, importing=var_type_symbol, ir_graph=ir_graph) +# +# match var_type: +# case None: +# raise ValueError( +# f"var type {var_type} not found on available custom and built-in types" +# ) +# +# case BaseTypeDef(): +# var_container = var_type( +# var_name=var_symbol, +# # TODO: use the modifier to define variable flag and define a default as well +# flag=VariableKind.MUTABLE, +# ) +# +# match var_container: +# case BaseDataContainer(): +# mem.stack.push(var_container) +# +# case _: +# raise ValueError(f"{var_container}") +# +# case _: +# raise NotImplementedError( +# f"{var_type} ({type(var_type)}) not implemented yet for variable declaration" +# ) +# +# +# def _get_assign_datatype( +# var_type: Symbol | CompositeSymbol, +# value: SimpleObj | ObjArray | BaseIRInstr | IRBlock, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> Symbol | Literal | Literal | LiteralArray | BaseDataContainer: +# """ +# Convenient function to: (1) check whether the data being assigned to the variable has +# the correct type, and to (2) resolve any instruction and block. +# +# For instance, ``int`` data type can be converted to any of the valid integer types, +# such as ``u64``, ``i64``, so on. However, if the data provided is a ``float`` and the +# variable is an integer (e.g. ``u64``), it cannot be converted implicitly, so an error +# will be raised. 'Convertible' data types should be done so explicitly on code, +# with ``*`` (cast) operation, ex:: +# +# var1:u32 = 4.0*u32 +# var2:f32 = 255*f32 +# +# Data should be prepared to be inserted into the variable container, so any caller or +# casting should be resolved here. +# +# Args: +# var_type: ``CompositeSymbol`` (or ``Symbol``) object of the variable type +# value: data name as ``WorkingData``, ``CompositeWorkingData``, ``BaseIRInstr`` or +# ``IRBlock`` object to be assigned to the variable +# mem: ``MemoryManager`` object +# node: +# ir_graph: +# +# Returns: +# The data name with adjusted type (if possible) or raise an error, in case data +# is not compatible +# """ +# +# new_instr: BaseIRInstr +# +# match value: +# case Symbol(): +# res_var = mem.heap[mem.cur_scope].get(value) +# +# match res_var: +# case HeapInvalidKeyError(): +# raise ValueError(f"variable {value} is not declared yet") +# +# case _: +# if res_var.type == var_type: +# return value +# +# case CompositeSymbol(): +# raise NotImplementedError("composite symbol on variable assignment not implemented yet") +# +# case Literal(): +# data_type = ( +# Symbol(value.type) if isinstance(value.type, str) else CompositeSymbol(value.type) +# ) +# data_type_tuple = compatible_types.get(data_type, None) or (data_type,) # type: ignore [arg-type] +# +# if var_type in data_type_tuple: +# dt_ds = builtins_types.get(data_type) # type: ignore [arg-type] +# +# if dt_ds: +# mem.symbol.type.add(data_type, dt_ds) +# +# else: +# raise ValueError(f"invalid type {data_type}") +# +# return Literal(value.value, data_type.value) +# +# case LiteralArray(): +# raise NotImplementedError("composite literal on variable assignment not implemente yet") +# +# case BaseIRInstr(): +# new_args: tuple[SimpleObj | ObjArray | BaseDataContainer] | tuple = () +# +# for k in value: +# new_args += ( +# _get_assign_datatype( +# var_type=var_type, +# value=k, +# mem=mem, +# node=node, +# ir_graph=ir_graph, +# ), +# ) +# +# new_instr = value.__class__(*new_args, name=value.name) +# new_instr.resolve(mem, node, ir_graph) +# +# return mem.scope.stack[mem.cur_scope].pop() +# +# case BodyBlock() | ArgsBlock() | ArgsValuesBlock(): +# new_blocks: tuple[SimpleObj | ObjArray | BaseDataContainer] | tuple = () +# +# for k in value: +# new_blocks += ( +# _get_assign_datatype( +# var_type=var_type, +# value=k, +# mem=mem, +# node=node, +# ir_graph=ir_graph, +# ), +# ) +# +# new_instr = cast(IRInstr, value.__class__(*new_blocks)) +# new_instr.resolve(mem=mem, node=node, ir_graph=ir_graph) +# +# return mem.scope.stack[mem.cur_scope].pop() +# +# case OptionBlock(): +# # FIXME: implement option block +# raise NotImplementedError() +# +# case _: +# raise NotImplementedError( +# f"{value} ({type(value)}) on variable assignment with undefined implementation" +# ) +# +# raise ValueError(f"data {value} to be assigned is not compatible with target type {var_type}") +# +# +# def _assign_variable( +# *, +# variable: BaseDataContainer, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# **arg_values: Any, +# ) -> None: +# """ +# Convenient function to assign a value to a variable. It calls checks for any +# data incompatibility and resolvers for any instructions or blocks to be yet +# evaluated. +# +# Args: +# variable: the variable container object +# mem: +# node: +# ir_graph: +# **arg_values: Any extra argument used +# """ +# +# args: SimpleObj | ObjArray | BaseIRInstr | IRBlock = mem.scope.stack[mem.cur_scope].pop() +# new_args: tuple = ( +# _get_assign_datatype( +# var_type=variable.type, +# value=args, +# mem=mem, +# node=node, +# ir_graph=ir_graph, +# ), +# ) +# +# if len(new_args) > 0 and len(arg_values) == 0: +# variable.assign(*new_args) +# +# elif len(new_args) == 0 and len(arg_values) > 0: +# variable.assign(**arg_values) +# +# else: +# raise NotImplementedError( +# f"should not have arguments and argument-value together when " +# f"assigning variable {variable}" +# ) +# +# +# def _get_type_from_data( +# data: BaseDataContainer | Literal, +# ) -> Symbol | CompositeSymbol: +# if isinstance(data, Literal): +# return data.type +# +# if isinstance(data, BaseDataContainer): +# return data.type +# +# sys.exit(f"unknown arg value on call args resolution ({type(data)})") +# +# +# def _resolve_type( +# data: Symbol | CompositeSymbol | IRBlock | IRInstr, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> BaseTypeDef: +# """""" +# +# match data: +# case Symbol() | CompositeSymbol(): +# res = get_type(node.irhash, data, ir_graph) +# +# if res: +# return res +# +# raise ValueError(f"type {data} not found") +# +# case _: +# raise ValueError(f"unexpected/unknown type {type(data)}") +# +# +# def _resolve_cast( +# data: BaseDataContainer | Literal, +# to_type: BaseTypeDef, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> None: +# # cast_op: BaseCastOperator +# +# if data.is_quantum: +# if to_type.is_quantum: +# # cast_op = CastQ2Q(data=data, to_type=to_type, mem=mem, node=node, ir_graph=ir_graph) +# cast_op: type[CastQ2Q] = CastQ2Q +# +# else: +# # cast_op = CastQ2C(data=data, to_type=to_type, mem=mem, node=node, ir_graph=ir_graph) +# cast_op: type[CastQ2C] = CastQ2C +# +# else: +# if to_type.is_quantum: +# # cast_op = CastC2Q(data=data, to_type=to_type, mem=mem, node=node, ir_graph=ir_graph) +# cast_op: type[CastC2Q] = CastC2Q +# +# else: +# # cast_op = CastC2C(data=data, to_type=to_type, mem=mem, node=node, ir_graph=ir_graph) +# cast_op: type[CastC2C] = CastC2C +# +# cast_data = ( +# cast_op(data=data, to_type=to_type, mem=mem, node=node, ir_graph=ir_graph) +# .flush() +# .retrieve_cast_data() +# ) +# mem.stack.push(cast_data) +# +# +# def _resolve_expr_to_data( +# expr: IRBlock | BaseIRInstr | Symbol | CompositeSymbol | Literal | BaseDataContainer, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> Literal | BaseDataContainer: +# """Resolve expression (core literal, symbol, ir block, etc) into actual data""" +# +# match expr: +# case IRBlock(): +# res: Literal | BaseDataContainer | None = None +# +# for k in expr: +# res = _resolve_expr_to_data(*k, mem=mem, node=node, ir_graph=ir_graph) +# +# if res: +# return res +# +# raise ValueError("empty ir block on expr to unwrap data") +# +# case BaseIRInstr(): +# expr.resolve(mem, node, ir_graph) +# return mem.stack.get_fn_return() +# +# case Symbol() | CompositeSymbol(): +# return mem.heap.table[mem.heap.last()].get(expr) +# +# case Literal() | BaseDataContainer(): +# return expr +# +# case _: +# raise NotImplementedError("could not resolve casting expr to data") +# +# +# def _resolve_call_args( +# *args: IRBlock | BaseIRInstr | SimpleObj | ObjArray, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> tuple[Literal | BaseDataContainer, ...] | tuple: +# """ +# Convenient function to resolve call arguments. +# +# Args: +# *args: +# mem: ``MemoryManager`` object +# node: +# ir_graph: +# """ +# +# resolved_args: tuple[Symbol | CompositeSymbol, ...] | tuple = () +# +# for arg in args: +# resolved_args += _resolve_expr_to_data(arg, mem, node, ir_graph) +# +# return resolved_args +# +# +# def _resolve_call_args_types( +# *args: Literal | BaseDataContainer, +# ) -> tuple[Symbol | CompositeSymbol] | tuple: +# """ +# Resolve types from call arguments +# """ +# +# resolved_types: tuple[Symbol | CompositeSymbol] | tuple = () +# +# for arg in args: +# match arg: +# case Literal() | BaseDataContainer(): +# resolved_types += (_get_type_from_data(arg),) +# +# case _: +# raise ValueError(f"unknown arg to retrieve type from ({type(arg)})") +# +# return resolved_types +# +# +# def _handle_call_instr( +# fn_header: BaseFnCheck, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# flag: IRFlag, +# ) -> None: +# """ +# Convenient function to handle call instruction and evaluated it. +# +# Args: +# fn_header: the caller header (name and arguments types) +# mem: ``MemoryManager`` object +# node: +# ir_graph: +# flag: ``IRFlag`` enum value +# """ +# +# match flag: +# case IRFlag.BUILTIN_FN_CALL: +# fn_def = get_fn(node_key=node.irhash, importing=fn_header, ir_graph=ir_graph) +# _resolve_builtin_fn(fn_def=fn_def, mem=mem, node=node, ir_graph=ir_graph) +# +# case IRFlag.BUILTIN_OPTN_CALL: +# pass +# +# case IRFlag.BUILTIN_BDN_CALL: +# pass +# +# case IRFlag.BUILTIN_OPTBDN_CALL: +# pass +# +# case IRFlag.FN_CALL: +# args_types: tuple[SimpleObj] | tuple = () +# args: tuple[BaseDataContainer] | tuple = () +# +# mem.stack.new(for_fn_use=True) +# mem.stack.set_fn_entry() +# +# fn_header = fn_header[0] if isinstance(fn_header, ModifierBlock) else fn_header +# fn_entry = BaseFnCheck(fn_name=fn_header, args_types=()) +# fn_def = get_fn(node_key=node.irhash, importing=fn_entry, ir_graph=ir_graph) +# _resolve_fn_block( +# data=cast(IRBlock, fn_def.body), mem=mem, node=node, ir_graph=ir_graph +# ) +# +# # for _ in range(number_args): +# # res = mem.stack.pop() +# # args += (res,) +# # +# # if isinstance(res, CoreLiteral): +# # args_types += (res.type,) +# # +# # elif isinstance(res, Symbol): +# # args_types += (res,) +# # +# # # TODO: implement modifier resolution before proceeding on function definition +# # +# # caller = caller[0] if isinstance(caller, ModifierBlock) else caller +# # fn_entry = BaseFnCheck( +# # fn_name=caller, +# # args_types=args_types, +# # ) +# # fn_block: IRBlock = cast(IRBlock, mem.symbol.fn.get(fn_entry, None)) +# # +# # if fn_block is None: +# # raise ValueError( +# # f"function {caller} with arg type signature {args_types} not found" +# # ) +# # +# # # FIXME: depth_counter value needs to come from the execution global depth counter +# # fn_scope = mem.new_scope(fn_block, depth_counter=1) +# # _resolve_fn_block(fn_block, mem, node, ir_graph) +# # mem.free_last_scope(to_return=True) +# +# case IRFlag.BDN_CALL: +# pass +# +# case IRFlag.OPTBDN_CALL: +# pass +# pass +# +# +# def _resolve_builtin_fn(fn_def: FnDef, mem: MemoryManager, node: IRNode, ir_graph: IRGraph) -> None: +# """ +# Resolve built-in functions. As they do not have ``IRBlock`` or +# ``IRInstr`` instances, they are treated separated. +# """ +# +# +# def _resolve_fn_block( +# data: IRBlock | BaseIRInstr | Literal | BaseDataContainer, +# mem: MemoryManager, +# node: IRNode, +# ir_graph: IRGraph, +# ) -> None: +# """ +# Convenient function to resolve function blocks. Whenever it's called from outside, +# a new scope from ``MemoryManager`` must be created and freed after it finishes +# execution and return to the outside scope. +# +# Args: +# data: IR block or IR instruction object +# mem: ``MemoryManager`` object +# node: +# ir_graph: +# """ +# +# match data: +# case ReturnBlock(): +# num_returns = len(data) +# for k in data: +# _resolve_fn_block(k, mem, node, ir_graph) +# +# for _ in range(num_returns): +# mem.stack.set_fn_return(mem.stack.pop()) +# +# case IRBlock(): +# for k in data: +# _resolve_fn_block(k, mem, node, ir_graph) +# +# case BaseIRInstr(): +# data.resolve(mem=mem, node=node, ir_graph=ir_graph) +# +# case Literal() | BaseDataContainer(): +# mem.stack.push(data) diff --git a/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir_builder.py b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir_builder.py new file mode 100644 index 00000000..2b404776 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/code/simple_ir_builder/new_ir_builder.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from pathlib import Path + +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.code.tools import build_reftable +from hhat_lang.core.code.symbol_table import SymbolTable +from hhat_lang.core.data.core import CompositeSymbol, Symbol +from hhat_lang.core.data.fn_def import FnDef, BuiltinFnDef +from hhat_lang.core.types.abstract_base import BaseTypeDef +from hhat_lang.dialects.heather.code.simple_ir_builder.new_ir import ( + IR, + IRModule, +) +from hhat_lang.core.code.ir_custom import BodyBlock +from hhat_lang.dialects.heather.parsing.utils import FnsDict, TypesDict + +TypesTyping = TypesDict | dict[Symbol | CompositeSymbol, Path] +FnsTyping = FnsDict | dict[BaseFnCheck, Path] + + +def build_ir_module( + *, + path: Path | str, + types: tuple[BaseTypeDef, ...] | None = None, + fns: tuple[FnDef | BuiltinFnDef, ...] | None = None, + main: BodyBlock | None = None, +) -> IRModule: + """ + Build an ``IRModule`` instance from types, functions and/or main body block. + + Args: + path: the ``IRModule`` path instance or str + types: tuple of ``BaseTypeDataStructure`` elements or ``None``. Default ``None`` + fns: tuple of ``FnDef`` elements or ``None``. Default ``None`` + main: a ``BodyBlock`` instance or ``None``. Default ``None`` + + Returns: + The ``IRModule`` instance + """ + + types = types or () + fns = fns or () + st = SymbolTable() + path = path if isinstance(path, Path) else Path(path) + + for t in types: + st.type.add(t.name, t) + + for f in fns: + st.fn.add(f.fn_check, f) + + return IRModule(path=path, symboltable=st, main=main) + + +def build_ir( + *, + path: Path | str, + ref_types: TypesTyping | None = None, + ref_fns: FnsTyping | None = None, + types: tuple[BaseTypeDef, ...] | None = None, + fns: tuple[FnDef | BuiltinFnDef, ...] | None = None, + main: BodyBlock | None = None, +) -> IR: + ref_table = build_reftable(types=ref_types, fns=ref_fns) + ir_module = build_ir_module(path=path, types=types, fns=fns, main=main) + return IR(ref_table=ref_table, ir_module=ir_module) diff --git a/python/src/hhat_lang/dialects/heather/code/ssa_ir_builder/ir.py b/python/src/hhat_lang/dialects/heather/code/ssa_ir_builder/ir.py deleted file mode 100644 index c3ee7c0a..00000000 --- a/python/src/hhat_lang/dialects/heather/code/ssa_ir_builder/ir.py +++ /dev/null @@ -1,282 +0,0 @@ -from __future__ import annotations - -from typing import Any, Iterable - -from hhat_lang.core.data.core import Symbol, CompositeSymbol, CoreLiteral - - -# TODO: continue to implement this approach in the future - - -class SSACounter: - _count: int - - def __init__(self): - # count starts at -1 to align with list indexes - self._count = -1 - - def inc(self): - self._count += 1 - return self._count - - def reset(self): - """Use this mainly when testing code""" - self._count = -1 - - -class IRModifier: - """ - Modifier generates some extra behaviors for the data it's applied on. - - Data can be a variable, a type, a function/instruction/operation, or - anything that has a `Symbol` and can be manipulated at runtime. - """ - - _ssa: SSA - _mods: dict[int | Symbol, Any] | dict - - def __init__(self, ssa: SSA, *, amods: tuple | None = None, kmods: dict | None = None): - self._ssa = ssa - - # check whether amods (mod arguments) is not empty: - if amods: - - for n, p in enumerate(amods): - if isinstance(p, (Symbol, CompositeSymbol, SSA)): - self._mods[n] = p - - else: - raise ValueError(f"unsupported mod for {self._ssa}: {p}") - - # check whether kmods (mod key-value pairs) is not empty instead: - elif kmods: - - for k, v in kmods.items(): - if ( - isinstance(k, Symbol) - and isinstance(v, (Symbol, CompositeSymbol, SSA, CoreLiteral)) - ): - self._mods[k] = v - - else: - raise ValueError(f"unsupported mod and param for {self._ssa}: {k} -> {v}") - - # if nothing is provided, the mod is empty: - else: - # no modifier generated; empty data - self._mods = dict() - - @property - def ssa(self) -> SSA: - return self._ssa - - @property - def symbol(self) -> Symbol: - return self._ssa.symbol - - @property - def mods(self) -> dict[int | Symbol, Any] | dict: - return self._mods - - def __repr__(self) -> str: - mod_repr = " ".join(f"{k}:{v}" for k, v in self._mods.items()) if self._mods else "" - return f"$mod({self.ssa})[{mod_repr}]" - - -class SSA: - """ - SSA form data value. - - Contains the `Symbol` and the `SSACounter` index. Optionally, it will - either have a `SSAPhi` or an `IRModifier`, but cannot have both. - """ - - _symbol: Symbol - _idx: int | None - _phi: SSAPhi | None - _mod: IRModifier | None - - def __init__(self, value: Symbol): - if isinstance(value, Symbol): - self._phi = None - self._symbol = value - self._idx = None - self._mod = None - - else: - raise ValueError("SSA value must be a symbol.") - - @property - def symbol(self) -> Symbol: - return self._symbol - - @property - def name(self) -> str: - return self._symbol.value - - @property - def idx(self) -> int: - return self._idx - - @property - def phi(self) -> None | SSAPhi: - return self._phi - - @property - def mod(self) -> None | IRModifier: - return self._mod - - def set_idx(self, idx: int) -> None: - if self.idx is None: - self._idx = idx - - @classmethod - def get_ssa(cls, value: Symbol | SSAPhi) -> SSA: - """Get a new SSA from a Symbol or a SSAPhi.""" - - if isinstance(value, SSAPhi): - new_ssa = SSA(value.symbol) - new_ssa.set_phi(value) - return new_ssa - - return SSA(value) - - def set_phi(self, value: SSAPhi) -> None: - if isinstance(value, SSAPhi): - self._phi = value - - else: - raise ValueError(f"{value} is not a Phi/SSAPhi ({type(value)}).") - - def set_mod(self, value: IRModifier) -> None: - if isinstance(value, IRModifier): - self._mod = value - - else: - raise ValueError(f"{value} is not a modifier ({type(value)}).") - - def __eq__(self, other: Any) -> bool: - if isinstance(other, SSA): - return self._symbol == other._symbol and self._idx == other._idx - return False - - def __hash__(self) -> int: - return hash((self._symbol, self._idx)) - - def __repr__(self) -> str: - phi = f"<{self.phi}>" if self.phi else "" - mod = f"<{self.mod}>" if self.mod else "" - return f"%{self.name}#{self.idx}{phi or mod}" - - -class SSAPhi: - """ - The phi function for disambiguity between a variable coming from a control flow. - """ - - _symbol: Symbol - _args: tuple[SSA, ...] - - def __init__(self, *vars: SSA): - if self._check_args(*vars): - self._args = vars - self._symbol = vars[0].symbol - - else: - raise ValueError("SSAPhi must contain the same variables.") - - @property - def symbol(self) -> Symbol: - return self._symbol - - def _check_args(self, *args: SSA) -> bool: - return len(set(k.name for k in args)) == 1 - - def __eq__(self, other: Any) -> bool: - if isinstance(other, SSAPhi): - return self._symbol == other._symbol and self._args == other._args - return False - - def __hash__(self) -> int: - return hash((self._symbol, self._args)) - - def __repr__(self) -> str: - return f"ø({','.join(str(k) for k in self._args)})" - - -class IRVar: - """ - Holds a list of all the SSA forms for a given variable. Each SSA form - index matches the list index, so it's easy to compare, retrieve or do - optimizations with it. - """ - - _symbol: Symbol - _data: list[SSA, ...] | list - _ssa_counter: SSACounter - - def __init__(self, symbol: Symbol): - self._symbol = symbol - self._data = [] - self._ssa_counter = SSACounter() - - @property - def symbol(self) -> Symbol: - return self._symbol - - @property - def data(self) -> list[SSA, ...]: - return self._data - - def ssa_inc(self) -> int: - return self._ssa_counter.inc() - - def push(self, name: Symbol | SSA | SSAPhi | IRModifier) -> None: - match name: - case SSA(): - if self.symbol == name.symbol: - name.set_idx(self.ssa_inc()) - self._data.append(name) - return - - case IRModifier(): - if self.symbol == name.symbol: - new_ssa = SSA.get_ssa(name.symbol) - new_ssa.set_idx(self.ssa_inc()) - new_ssa.set_mod(name) - self._data.append(new_ssa) - return - - case SSAPhi(): - if self.symbol == name.symbol: - new_ssa = SSA.get_ssa(name) - new_ssa.set_idx(self.ssa_inc()) - self._data.append(new_ssa) - return - - case Symbol(): - if self.symbol == name: - new_ssa = SSA.get_ssa(name) - new_ssa.set_idx(self.ssa_inc()) - self._data.append(new_ssa) - return - - case _: - raise ValueError( - f"IRVar only accepts Symbol, SSA, SSAPhi or IRModifier." - f" ({name} ({type(name)}))" - ) - - raise ValueError("IRVar cannot accept a different symbol (variable).") - - def __len__(self) -> int: - return len(self._data) - - def __getitem__(self, index: int) -> SSA: - return self._data[index] - - def __iter__(self) -> Iterable: - yield from self._data - - def __repr__(self) -> str: - return f"var:{self.symbol}.{self._data}" diff --git a/python/src/hhat_lang/dialects/heather/compiler/__init__.py b/python/src/hhat_lang/dialects/heather/compiler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/compiler/core.py b/python/src/hhat_lang/dialects/heather/compiler/core.py new file mode 100644 index 00000000..61d72cce --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/compiler/core.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import Callable + +from arpeggio import ParserPython + +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.core.config.base import HhatProjectSettings +from hhat_lang.core.fns.builtin_ir_builder import gen_all_builtin_modules +from hhat_lang.dialects.heather.code.simple_ir_builder.new_ir import IRModule, IR +from hhat_lang.dialects.heather.grammar.fn_grammar import fn_program +from hhat_lang.dialects.heather.parsing.ir_visitor import parser_grammar_code, parse + + +def compile_project_ir( + project_settings: HhatProjectSettings, + raw_code: str, + grammar_parser: Callable[[Callable], ParserPython] | None = None, + program_rule: Callable | None = None, +) -> IRGraph: + """ + Parse the whole project (including built-in modules), generating an IR graph instance. + + Args: + project_settings: ``HhatProjectSettings`` instance + raw_code: code as str + grammar_parser: + program_rule: + + Returns: + An ``IRGraph`` instance for the project. + """ + + grammar_parser = grammar_parser or parser_grammar_code + program_rule = program_rule or fn_program + ir_module = IRModule + ir = IR + ir_graph = IRGraph() + + gen_all_builtin_modules(ir_graph=ir_graph, ir_module=ir_module, ir=ir) + parse( + grammar_parser=grammar_parser, + program_rule=program_rule, + raw_code=raw_code, + project_root=project_settings.project_root, + module_path=project_settings.project_root / "src" / "main.hat", + ir_graph=ir_graph, + ) + ir_graph.build() + return ir_graph diff --git a/python/src/hhat_lang/dialects/heather/execution/__init__.py b/python/src/hhat_lang/dialects/heather/execution/__init__.py new file mode 100644 index 00000000..0995c923 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/execution/__init__.py @@ -0,0 +1,17 @@ +""" +Use to execute classical functions test. It follows the regular code execution path:: + + raw code -> AST -> IR + _______|__________ + | | + v v + quantum branch classical branch + | |-> memory + | |-> executor + |-> program + |-> executor + | -> low level language + | -> target (simulator or QPU device) + | -> classical branch (if needed) + +""" diff --git a/python/src/hhat_lang/dialects/heather/execution/classical/__init__.py b/python/src/hhat_lang/dialects/heather/execution/classical/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/execution/classical/executor.py b/python/src/hhat_lang/dialects/heather/execution/classical/executor.py new file mode 100644 index 00000000..98dc122c --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/execution/classical/executor.py @@ -0,0 +1,27 @@ +""" +Execute classical branch instructions. Quantum branch may use it +to execute classical instructions that are not supported by the +quantum low level language and/or the target backend. +""" + +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.memory.core import MemoryManager +from hhat_lang.core.code.ir_block import IRBlock + + +class Executor(BaseExecutor): + def __init__(self, mem: MemoryManager, **_kwargs: Any): + self._mem = mem + + def walk(self, code: Any, mem: MemoryManager, **kwargs: Any) -> Any: + pass + + def run(self, code: IRBlock, **kwargs: Any) -> Any: + pass + + def __call__(self, code: IRBlock, **kwargs: Any) -> Any: + pass diff --git a/python/src/hhat_lang/dialects/heather/execution/executor.py b/python/src/hhat_lang/dialects/heather/execution/executor.py new file mode 100644 index 00000000..7bd9a46f --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/execution/executor.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.code.ir_graph import IRGraph, IRNode +from hhat_lang.core.execution.abstract_base import ( + BaseExecutor, + BaseClassicalEvaluator, + BaseQuantumEvaluator, +) +from hhat_lang.core.memory.core import MemoryManager + + +class Executor(BaseExecutor): + def __init__(self): + self._cexec = CExecutor() + self._qexec = QExecutor() + + def run( + self, + *, + code: Any, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + **kwargs: Any, + ) -> None: + self.walk(code, mem, node, ir_graph) + + def walk( + self, + code: Any, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + **kwargs: Any, + ) -> Any: + pass + + def __call__(self, *args: Any, **kwargs: Any): + pass + + +class CExecutor(BaseClassicalEvaluator): + pass + + +class QExecutor(BaseQuantumEvaluator): + pass diff --git a/python/src/hhat_lang/dialects/heather/execution/new_ir.py b/python/src/hhat_lang/dialects/heather/execution/new_ir.py new file mode 100644 index 00000000..20e51899 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/execution/new_ir.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import Any + +from hhat_lang.core.code.abstract import BaseIR, IRHash +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.core.data.core import CompositeSymbol, Symbol +from hhat_lang.core.execution.abstract_base import BaseIRManager +from hhat_lang.dialects.heather.code.simple_ir_builder.new_ir import IR + + +class IRManager(BaseIRManager): + """Handle IR codes for Heather dialect through ``IRGraph`` instance""" + + def __init__(self): + self._graph = IRGraph() + + def add_ir(self, ir: IR) -> None: + """ + Add a single IR + + Args: + ir: the ``IR`` instance to be added to the manager + """ + + self._graph.add_node(ir) + + def link_ir( + self, + *refs: Symbol | CompositeSymbol, + ir_importing: BaseIR, + ir_imported: BaseIR, + **kwargs: Any, + ) -> None: + """ + Link two IRs, where one is the importer (importing IR) and the other is the imported IR. + + Args: + *refs: list of symbols of types or functions from the ``ir_imported`` instance + ir_importing: the ``IR`` instance that will import data from another ``IR`` instance + ir_imported: the imported ``IR``, which contains the data to be imported + **kwargs: Extra data if needed + """ + + importing = IRHash.get_key(ir_importing) + imported = IRHash.get_key(ir_imported) + self._graph.add_edge(*refs, node_key=importing, link_key=imported) + + def update_ir(self, prev_ir: IR, new_ir: IR) -> None: + """ + Update a current ``IR`` instance for a new ``IR`` instance. It will replace the current + one on the IR graph as well, from its node position to all the current's relationships. + + Args: + prev_ir: (the current, or to be) previous ``IR`` instance + new_ir: the new ``IR`` instance, to replace the current ``IR`` instance + """ + + prev_key = IRHash.get_key(prev_ir) + self._graph.update_node(prev_key, new_ir) diff --git a/python/src/hhat_lang/dialects/heather/execution/quantum/__init__.py b/python/src/hhat_lang/dialects/heather/execution/quantum/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/dialects/heather/execution/quantum/program.py b/python/src/hhat_lang/dialects/heather/execution/quantum/program.py new file mode 100644 index 00000000..488f1bb5 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/execution/quantum/program.py @@ -0,0 +1,96 @@ +""" +Quantum program is handler for quantum data/variable that executes +the quantum content done by a classical casting request. For example:: + + u32*@2 + +casts a quantum data `@2` into a `u32` data type. The same as:: + + u32*@redim(@1<@u3>) + +will cast the resulting set of instructions from `@redim(@1<@u3>)` +into `u32`. It also is valid for quantum variables:: + + @v1:@u3 = @redim(@0) + number:u32 = u32*@v1 + + +The quantum program workflow is as follows: + +- Instructions are analyzed according to the low level language and target +backend support (lower level counterparts, LLC) + + - If classical instructions are supported, they will be handled by those + - If not, they will fall back into this dialect's classical branch execution + +- Memory is handled by the dialect and shared when appropriate to the LCC +- All the quantum-specific optimizations are handled by the LLC +- Quantum instructions are then executed and results are collected +- Casting protocols apply the according source type to target type at the results +- Results are sent back to the execution workflow as the target type data + + +""" + +from __future__ import annotations + +from typing import Any, Callable + +from hhat_lang.core.code.ir_graph import IRNode, IRGraph +from hhat_lang.core.data.core import Literal +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import ErrorHandler +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.execution.abstract_program import ( + QuantumProgram as CoreQuantumProgram, +) +from hhat_lang.core.lowlevel.abstract_qlang import BaseLLQManager, BaseLLQ +from hhat_lang.core.memory.core import MemoryManager + +# TODO: the imports below must come from the config file, not hardcoded +from hhat_lang.low_level.target_backend.qiskit.aer_simulator.code_evaluator import ( + execute_program, +) + + +class QuantumProgram(CoreQuantumProgram): + def __init__( + self, + *, + qdata: BaseDataContainer | Literal, + mem: MemoryManager, + node: IRNode, + ir_graph: IRGraph, + executor: BaseExecutor, + llq: Callable[ + [BaseDataContainer, MemoryManager, IRNode, IRGraph, BaseExecutor], + BaseLLQManager, + ], + ): + """ + Quantum program for Heather dialect. + + Args: + qdata: a quantum literal or quantum variable + mem: ``MemoryManager`` object + node: ``IRNode`` object + ir_graph: ``IRGraph`` object + executor: dialect-specific code executor + llq: Low-level quantum manager callable + """ + super().__init__( + qdata=qdata, + mem=mem, + node=node, + ir_graph=ir_graph, + base_llq=llq, + executor=executor, + ) + + def run(self, debug: bool = False) -> Any | ErrorHandler: + qlang_code: BaseLLQ = self._qlang.compile() + + if debug: + print(qlang_code) + + return execute_program(qlang_code, self._qdata, debug) diff --git a/python/src/hhat_lang/dialects/heather/grammar/__init__.py b/python/src/hhat_lang/dialects/heather/grammar/__init__.py index dad61911..5fdfdadf 100644 --- a/python/src/hhat_lang/dialects/heather/grammar/__init__.py +++ b/python/src/hhat_lang/dialects/heather/grammar/__init__.py @@ -2,3 +2,12 @@ # Heather whitespaces WHITESPACE = "\n\t ,;" +# comments +SINGLE_COMMENT = r"\/\/([^\n]*)\n" +MULTILINE_COMMENT = r"\/\-.*?\-\/" +# etc +STRING = r'"([^"]*)"' +INT = r"-?([1-9]\d*|0)" +FLOAT = r"-?\d+\.\d+" + +QINT = r"\@-?([1-9]\d*|0)" diff --git a/python/src/hhat_lang/dialects/heather/grammar/const_grammar.py b/python/src/hhat_lang/dialects/heather/grammar/const_grammar.py new file mode 100644 index 00000000..89e80dd8 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/grammar/const_grammar.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import Any + +from arpeggio import Kwd, EOF, OneOrMore, Optional, ZeroOrMore + +from hhat_lang.dialects.heather.grammar.fn_grammar import imports +from hhat_lang.dialects.heather.grammar.generic_grammar import ( + simple_id, + full_id, + expr, +) + + +def const_program() -> Any: + return ZeroOrMore(imports), ZeroOrMore(const_def), EOF + + +def const_def() -> Any: + return Kwd("const"), simple_id, ":", full_id, "=", expr diff --git a/python/src/hhat_lang/dialects/heather/grammar/fn_grammar.py b/python/src/hhat_lang/dialects/heather/grammar/fn_grammar.py new file mode 100644 index 00000000..39703e4c --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/grammar/fn_grammar.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from typing import Any + +from arpeggio import EOF, Kwd, OneOrMore, Optional, ZeroOrMore + +from hhat_lang.dialects.heather.grammar.generic_grammar import ( + assign, + assign_ds, + assignargs, + body, + const_import, + declare, + declareassign, + declareassign_ds, + expr, + full_id, + id_composite_value, + many_import, + metafn_import, + metamod_import, + option, + simple_id, + single_import, +) +from hhat_lang.dialects.heather.grammar.type_grammar import typeimport + + +def fn_program() -> Any: + return ZeroOrMore(imports), ZeroOrMore(fns), Optional(main), EOF + + +def imports() -> Any: + return ( + Kwd("use"), + "(", + OneOrMore([typeimport, fnimport, const_import, metafn_import, metamod_import]), + ")", + ) + + +def fnimport() -> Any: + return Kwd("fn"), ":", [single_import, many_import] + + +def fns() -> Any: + return Kwd("fn"), simple_id, fnargs, Optional(full_id), fn_body + + +def fnargs() -> Any: + return "(", ZeroOrMore(argtype), ")" + + +def argtype() -> Any: + return full_id, ":", id_composite_value + + +def fn_body() -> Any: + return ( + "{", + ZeroOrMore( + [ + fn_return, + declareassign, + declareassign_ds, + declare, + assignargs, + assign_ds, + assign, + expr, + ] + ), + "}", + ) + + +def fn_return() -> Any: + return "::", expr + + +def metafn_def() -> Any: + """ + Meta function grammar definition:: + + metafn (fn_t | optn_t | bdn_t | optbdn_t) + + where + + - ``fn_t`` type is for (common) function + - ``optn_t`` type is for functions with arguments as options + - ``bdn_t`` type is for functions with arguments and body + - ``optbdn_t`` type is for functions with arguments and options in the body + """ + + return ( + Kwd("metafn"), + simple_id, + fnargs, + [Kwd("fn_t"), Kwd("optn_t"), Kwd("bdn_t"), Kwd("optbdn_t")], + metafn_body, + ) + + +def metafn_body() -> Any: + return ( + "{", + OneOrMore( + [ + option, + declareassign, + declareassign_ds, + declare, + assignargs, + assign_ds, + assign, + expr, + ] + ), + "}", + ) + + +def modifier_def() -> Any: + return Kwd("modifier"), simple_id, modifier_args, metafn_body + + +def modifier_args() -> Any: + return ( + "(", + Kwd("self"), + ZeroOrMore(argtype), + [ + Kwd("fn_t"), + Kwd("optn_t"), + Kwd("bdn_t"), + Kwd("optbdn_t"), + Kwd("var_t"), + Kwd("type_t"), + Kwd("data_t"), + ], + ")", + ) + + +def main() -> Any: + return Kwd("main"), body diff --git a/python/src/hhat_lang/dialects/heather/grammar/generic_grammar.py b/python/src/hhat_lang/dialects/heather/grammar/generic_grammar.py new file mode 100644 index 00000000..53f427e6 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/grammar/generic_grammar.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +from typing import Any + +from arpeggio import Kwd, OneOrMore, Optional, ZeroOrMore +from arpeggio import RegExMatch as _ + +from hhat_lang.dialects.heather.grammar import ( + FLOAT, + INT, + MULTILINE_COMMENT, + QINT, + SINGLE_COMMENT, + STRING, +) + + +def id_composite_value() -> Any: + return [("[", full_id, "]"), full_id] + + +def callargs() -> Any: + return full_id, "=", valonly + + +def valonly() -> Any: + return [array, full_id, literal] + + +def array() -> Any: + return "[", ZeroOrMore([literal, composite_id_with_closure, full_id]), "]" + + +def simple_id() -> Any: + return _(r"@?[a-zA-Z][a-zA-Z0-9\-_]*") + + +def trait_name_id() -> Any: + return _(r"@?[A-Z][a-zA-Z0-9\-_]*") + + +def trait_id() -> Any: + """ + Trait id always starts with ``#`` and an uppercase letter, ex: ``#Printable``. + It can also contain multiple trait ids, as: ``#[Printable Integers]`` + """ + return ( + "#", + [trait_name_id, ("[", OneOrMore(trait_name_id), "]")], + Optional(modifier), + ) + + +def composite_id() -> Any: + return simple_id, OneOrMore(".", simple_id) + + +def composite_id_with_closure() -> Any: + return ( + [full_id], + ".", + "{", + OneOrMore([composite_id_with_closure, composite_id, full_id]), + "}", + ) + + +def composite_id_with_values() -> Any: + return ( + full_id, + ".", + "[", + [ + OneOrMore( + [ + ([literal, simple_id], capped_range, [literal, simple_id]), + valonly, + ] + ), + ([full_id, literal], variadic), + (variadic, [full_id, literal]), + ], + "]", + ) + + +def modifier() -> Any: + return "<", [ref, pointer, variadic, OneOrMore([callargs, valonly])], ">" + + +def full_id() -> Any: + return [composite_id, simple_id], Optional(modifier) + + +def ref() -> Any: + return Kwd("&") + + +def pointer() -> Any: + return Kwd("*") + + +def variadic() -> Any: + return Kwd("...") + + +def capped_range() -> Any: + return Kwd("..") + + +def literal() -> Any: + return ( + [t_float, t_null, t_bool, t_str, t_int, qt_bool, qt_int], + Optional(":", composite_id), + ) + + +def t_null() -> Any: + return Kwd("null") + + +def t_bool() -> Any: + return [Kwd("true"), Kwd("false")] + + +def t_str() -> Any: + return _(STRING) + + +def t_int() -> Any: + return _(INT) + + +def t_float() -> Any: + return _(FLOAT) + + +def qt_bool() -> Any: + return [Kwd("@true"), Kwd("@false")] + + +def qt_int() -> Any: + return _(QINT) + + +def comment() -> Any: + return [_(SINGLE_COMMENT), _(MULTILINE_COMMENT)] + + +def single_import() -> Any: + return [composite_id_with_closure, full_id] + + +def many_import() -> Any: + return "[", OneOrMore(single_import), "]" + + +def body() -> Any: + return ( + "{", + ZeroOrMore([declareassign, declareassign_ds, declare, assign, expr]), + "}", + ) + + +def expr() -> Any: + return [ + cast, + assign_ds, + call_optn, + call_optbdn, + call_bdn, + call, + array, + full_id, + literal, + ] + + +def declare() -> Any: + return simple_id, Optional(modifier), ":", full_id + + +def assign() -> Any: + return full_id, "=", expr + + +def assign_ds() -> Any: + return full_id, ".{", [OneOrMore(assignargs), OneOrMore(expr)], "}" + + +def declareassign() -> Any: + return simple_id, Optional(modifier), ":", full_id, "=", expr + + +def declareassign_ds() -> Any: + return ( + simple_id, + Optional(modifier), + ":", + full_id, + "=", + ".{", + OneOrMore(assignargs), + "}", + ) + + +def cast() -> Any: + return [call, literal, full_id], "*", full_id + + +def call() -> Any: + return full_id, "(", args, ")", Optional(modifier) + + +def args() -> Any: + return ZeroOrMore([callargs, cast, call, valonly]) + + +def assignargs() -> Any: + return full_id, "=", expr + + +def option() -> Any: + return [call, array, full_id], ":", [body, expr] + + +def call_optbdn() -> Any: + return full_id, "(", args, ")", "{", OneOrMore(option), "}" + + +def call_optn() -> Any: + return full_id, "(", OneOrMore(option), ")" + + +def call_bdn() -> Any: + return full_id, "(", args, ")", body + + +def const_import() -> Any: + return Kwd("const"), ":", [single_import, many_import] + + +def metafn_import() -> Any: + return Kwd("metafn"), ":", [single_import, many_import] + + +def metamod_import() -> Any: + return Kwd("metamod"), ":", [single_import, many_import] diff --git a/python/src/hhat_lang/dialects/heather/grammar/grammar.peg b/python/src/hhat_lang/dialects/heather/grammar/grammar.peg deleted file mode 100644 index 14cb50d1..00000000 --- a/python/src/hhat_lang/dialects/heather/grammar/grammar.peg +++ /dev/null @@ -1,56 +0,0 @@ -program = imports* (type_file*) / (fns* main?) EOF - -imports = ( 'use' '(' (typeimport / fnimport)+ ')' )* -typeimport = 'type' ':' ( single_import / many_import ) -fnimport = 'fn' ':' ( single_import / many_import ) -single_import = composite_id_with_closure / id -many_import = '[' single_import+ ']' - -type_file = 'type' ( typesingle / typestruct / typeenum / typeunion ) -typesingle = simple_id ':' id_composite_value -typestruct = simple_id '{' typesingle* '}' -typeenum = simple_id '{' enummember* '}' -typeunion = simple_id 'union' '{' unionmember* '}' -enummember = simple_id / typestruct -unionmember = typesingle / typestruct / typeenum - -fns = 'fn' simple_id fnargs id? body -fnargs = '(' argtype* ')' -argtype = simple_id ':' id_composite_value - -id_composite_value = ( '[' id ']' ) / id - -main = 'main' body - -body = '{' (declare / assign / declareassign / expr)* '}' -expr = cast / call / callwithbody / callwithbodyoptions / callwithargsbodyoptions / array -declare = simple_id modifier? ':' id -assign = id '=' expr -declareassign = simple_id modifier? ':' id '=' expr -cast = id '*' ( call / literal / id ) -call = id '(' args* ')' -args = callargs / call / valonly -callargs = simple_id ':' valonly -valonly = array / literal / id -callwithbodyoptions = id '(' args* ')' body -callwithargsbodyoptions = id '(' (expr body)+ ')' -callwithbody = id body - -array = '[' ( literal / composite_id_with_closure / id )* ']' - -simple_id = r'@?[a-zA-Z][a-zA-Z0-9\-_]*' -composite_id = simple_id ('.' simple_id)+ -composite_id_with_closure = ( simple_id / composite_id ) '.' '{' ( simple_id / composite_id / composite_id_with_closure ) '}' -modifier = '<' ( valonly+ / callargs+ ) '>' -id = ( composite_id / simple_id ) modifier? - -literal = null / bool / str / int / float / q__bool / q__int -null = 'null' -bool = 'true' / 'false' -str = r'"([^"]*)"' -int = r'-?([1-9]\d*|0)' -float = r'0(\.\d+)?|[1-9]\d*(\.\d+)?' -q__bool = '@true' / '@false' -q__int = r'-?\@([1-9]\d*|0)' - -comment = ( r'\/\/([^\n]*)\n' ) / ( r'\/\-.*?\-\/' ) \ No newline at end of file diff --git a/python/src/hhat_lang/dialects/heather/grammar/metamod_grammar.py b/python/src/hhat_lang/dialects/heather/grammar/metamod_grammar.py new file mode 100644 index 00000000..fed355de --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/grammar/metamod_grammar.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import Any + +from arpeggio import Kwd, EOF, OneOrMore, Optional, ZeroOrMore, RegExMatch as _ + +from hhat_lang.dialects.heather.grammar.generic_grammar import ( + trait_name_id, + simple_id, + modifier, +) +from hhat_lang.dialects.heather.grammar.type_grammar import typeimport + + +def metamod_program() -> Any: + return ZeroOrMore(imports), ZeroOrMore(metamod_def), EOF + + +def imports() -> Any: + return Kwd("use"), "(", OneOrMore(typeimport), ")" + + +def metamod_def() -> Any: + """ + Meta module definition:: + + metamod + + example:: + + metamod Printable { fn: print } + + metamod Numeric { + fn: { add sub mul div pow } + } + """ + + return Kwd("metamod"), trait_name_id, metamod_body + + +def metamod_body() -> Any: + return "{", ZeroOrMore([metamod_fn_header]), "}" + + +def metamod_fn_header() -> Any: + return Kwd("fn"), ":", [id_mod_opt, fn_header_body] + + +def fn_header_body() -> Any: + return "{", OneOrMore(id_mod_opt), "}" + + +def id_mod_opt() -> Any: + return simple_id, Optional(modifier) diff --git a/python/src/hhat_lang/dialects/heather/grammar/type_grammar.py b/python/src/hhat_lang/dialects/heather/grammar/type_grammar.py new file mode 100644 index 00000000..3bace5cd --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/grammar/type_grammar.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import Any + +from arpeggio import Kwd +from arpeggio.peg import EOF, OneOrMore, ZeroOrMore + +from hhat_lang.dialects.heather.grammar.generic_grammar import ( + id_composite_value, + many_import, + simple_id, + single_import, +) + + +def type_program() -> Any: + return ZeroOrMore(imports), ZeroOrMore(type_file), EOF + + +def imports() -> Any: + return Kwd("use"), "(", OneOrMore(typeimport), ")" + + +def typeimport() -> Any: + return Kwd("type"), ":", [single_import, many_import] + + +def type_file() -> Any: + return Kwd("type"), [typesingle, typestruct, typeenum] + + +def typesingle() -> Any: + return simple_id, ":", id_composite_value + + +def typemember() -> Any: + return simple_id, ":", id_composite_value + + +def typestruct() -> Any: + return simple_id, "{", ZeroOrMore(typemember), "}" + + +def typeenum() -> Any: + return simple_id, "{", ZeroOrMore(enummember), "}" + + +def enummember() -> Any: + return [simple_id, typestruct] diff --git a/python/src/hhat_lang/dialects/heather/interpreter/classical/executor.py b/python/src/hhat_lang/dialects/heather/interpreter/classical/executor.py index 56918a64..4611dd87 100644 --- a/python/src/hhat_lang/dialects/heather/interpreter/classical/executor.py +++ b/python/src/hhat_lang/dialects/heather/interpreter/classical/executor.py @@ -8,12 +8,12 @@ from typing import Any -from hhat_lang.core.code.ir import BodyIR, BlockIR, TypeIR, BaseFnIR -from hhat_lang.core.execution.abstract_base import BaseEvaluator +from hhat_lang.core.code.ir import BaseFnIR, BlockIR, BodyIR, TypeIR +from hhat_lang.core.execution.abstract_base import BaseExecutor from hhat_lang.core.memory.core import MemoryManager -class Evaluator(BaseEvaluator): +class Executor(BaseExecutor): def __init__(self, mem: MemoryManager, type_table: TypeIR, fn_table: BaseFnIR): self._mem = mem self._type_table = type_table diff --git a/python/src/hhat_lang/dialects/heather/interpreter/quantum/program.py b/python/src/hhat_lang/dialects/heather/interpreter/quantum/program.py index 80227206..db2df940 100644 --- a/python/src/hhat_lang/dialects/heather/interpreter/quantum/program.py +++ b/python/src/hhat_lang/dialects/heather/interpreter/quantum/program.py @@ -17,7 +17,8 @@ The quantum program workflow is as follows: -- Instructions are analyzed according to the low level language and target backend support (lower level counterparts, LLC) +- Instructions are analyzed according to the low level language and target +backend support (lower level counterparts, LLC) - If classical instructions are supported, they will be handled by those - If not, they will fall back into this dialect's classical branch interpreter @@ -33,36 +34,45 @@ from __future__ import annotations -from typing import Any, Callable, Type +from typing import Any, Type from hhat_lang.core.code.ir import BlockIR -from hhat_lang.core.data.core import Symbol, WorkingData +from hhat_lang.core.data.core import SimpleObj from hhat_lang.core.error_handlers.errors import ErrorHandler -from hhat_lang.core.execution.abstract_base import BaseEvaluator -from hhat_lang.core.execution.abstract_program import BaseProgram -from hhat_lang.core.lowlevel.abstract_qlang import BaseLowLevelQLang -from hhat_lang.core.memory.core import IndexManager - +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.execution.abstract_program import BaseQuantumProgram +from hhat_lang.core.lowlevel.abstract_qlang import BaseLLQManager +from hhat_lang.core.memory.core import BaseStack, IndexManager, Stack, SymbolTable from hhat_lang.dialects.heather.code.simple_ir_builder.ir import IRBlock # TODO: the imports below must come from the config file, not hardcoded -from hhat_lang.low_level.target_backend.qiskit.openqasm.code_executor import ( +from hhat_lang.low_level.target_backend.qiskit.aer_simulator.code_evaluator import ( execute_program, ) -class Program(BaseProgram): +class Program(BaseQuantumProgram): def __init__( self, *, - qdata: WorkingData, + qdata: SimpleObj, idx: IndexManager, block: IRBlock, - executor: BaseEvaluator, - qlang: Type[BaseLowLevelQLang[WorkingData, IRBlock | BlockIR, IndexManager, BaseEvaluator]], + executor: BaseExecutor, + symboltable: SymbolTable, + qlang: Type[ # type: ignore [type-arg] + BaseLLQManager[ + SimpleObj, + IRBlock | BlockIR, + IndexManager, + BaseExecutor, + Stack, + SymbolTable, + ] + ], ): if ( - isinstance(qdata, WorkingData) + isinstance(qdata, SimpleObj) and isinstance(idx, IndexManager) and isinstance(block, IRBlock) ): @@ -70,11 +80,24 @@ def __init__( self._idx = idx self._block = block self._executor = executor - self._qlang = qlang(self._qdata, self._block, self._idx, self._executor) + self._qstack = Stack() + self._symbol = symboltable + self._qlang = qlang( + self._qdata, + self._block, + self._idx, + self._executor, + self._qstack, + self._symbol, + ) else: raise ValueError(f"Quantum program got invalid parameters: {qdata=} | {idx=} {block=}") + @property + def qstack(self) -> BaseStack: + return self._qstack + def run(self, debug: bool = False) -> Any | ErrorHandler: qlang_code = self._qlang.gen_program() diff --git a/python/src/hhat_lang/dialects/heather/parsing/imports.py b/python/src/hhat_lang/dialects/heather/parsing/imports.py deleted file mode 100644 index c5830481..00000000 --- a/python/src/hhat_lang/dialects/heather/parsing/imports.py +++ /dev/null @@ -1,47 +0,0 @@ -"""To handle the `imports` part, for both types and functions""" - -from __future__ import annotations - -import os -from functools import reduce -from operator import iconcat -from pathlib import Path -from typing import Any - -from hhat_lang.core.code.ast import AST - -from hhat_lang.dialects.heather.code.ast import ( - Imports, - CompositeId, - CompositeIdWithClosure -) - - -def parse_types(code: Any) -> Any: - pass - - -def parse_types_compositeid(code: CompositeId) -> Any: - # get the type path from the code - type_path = Path(*reduce(iconcat, code.value, ())) - # join the type path with its full path from the project path - type_path = Path(".").resolve() / "hhat_types" / type_path - # add .hat for the type file name (which should be the last item in the tuple) - file_name = type_path.name + ".hat" - full_path = type_path.parent / file_name - - if full_path.exists(): - data = open(full_path, "r").read() - - -def parse_types_compositeidwithclosure(code: CompositeIdWithClosure) -> Any: - pass - - -def parse_fns(code: Any) -> Any: - pass - - -def parse_imports(code: Imports) -> Any: - pass - diff --git a/python/src/hhat_lang/dialects/heather/parsing/ir_visitor.py b/python/src/hhat_lang/dialects/heather/parsing/ir_visitor.py new file mode 100644 index 00000000..3773b3bb --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/parsing/ir_visitor.py @@ -0,0 +1,789 @@ +"""Attempt to parse directly to IR""" + +from __future__ import annotations + +from itertools import chain +from pathlib import Path +from typing import Any, Callable + +from arpeggio import ( + NonTerminal, + ParserPython, + PTNodeVisitor, + SemanticActionResults, + Terminal, + visit_parse_tree, +) +from typing_extensions import deprecated + +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.code.ir_block import ( + IRBlock, + IRInstr, +) +from hhat_lang.core.code.ir_custom import ( + ArgsBlock, + ArgsValuesBlock, + BodyBlock, + ModifierArgsBlock, + ModifierBlock, + OptionBlock, + ReturnBlock, +) +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.core.data.core import ( + CompositeSymbol, + Literal, + LiteralArray, + ObjArray, + Pointer, + Reference, + SimpleObj, + Symbol, +) +from hhat_lang.core.data.fn_def import BuiltinFnDef, FnDef +from hhat_lang.core.error_handlers.errors import ErrorHandler +from hhat_lang.core.imports import TypeImporter +from hhat_lang.core.imports.importer import FnImporter +from hhat_lang.core.types.abstract_base import BaseTypeDef, QSize, Size +from hhat_lang.core.types.builtin_types import builtins_types +from hhat_lang.core.types.core import EnumTypeDef, SingleTypeDef, StructTypeDef +from hhat_lang.dialects.heather.code.simple_ir_builder.new_ir import ( + IR, + AssignInstr, + CallInstr, + CastInstr, + DeclareAssignInstr, + DeclareInstr, +) +from hhat_lang.dialects.heather.code.simple_ir_builder.new_ir_builder import build_ir +from hhat_lang.dialects.heather.grammar import WHITESPACE +from hhat_lang.dialects.heather.grammar.fn_grammar import fn_program +from hhat_lang.dialects.heather.grammar.generic_grammar import comment +from hhat_lang.dialects.heather.grammar.type_grammar import type_program +from hhat_lang.dialects.heather.parsing.utils import FnsDict, ImportDicts, TypesDict + +######################### + + +def read_grammar() -> str: + grammar_path = Path(__file__).parent.parent / "grammar" / "grammar.peg" + + if grammar_path.exists(): + return open(grammar_path, "r").read() + + raise ValueError("No grammar found on the grammar directory.") + + +############################ +# PARSING WITH PYTHON CODE # +############################ + + +def parser_grammar_code(program_fn: Callable) -> ParserPython: + """ + + Args: + program_fn: the function that starts the grammar (probably "_program"). + + Returns: + The ``ParserPython`` constructor + """ + + return ParserPython(program_fn, comment_def=comment, ws=WHITESPACE, memoization=True) + + +################### +# PARSER FUNCTION # +################### + + +def parse( + grammar_parser: Callable[[Callable], ParserPython], + program_rule: Callable, + raw_code: str, + project_root: Path, + module_path: Path, + ir_graph: IRGraph, +) -> IR: + """ + Used to parse code according to some grammar. + + Args: + grammar_parser: function like + ``hhat_lang.dialects.heather.parsing.ir_visitor.parser_grammar_code`` that + receives a program rule (a function that starts the grammar), and returns a + ``ParserPython`` instance + program_rule: the function that starts the grammar (probably ``_program`` + raw_code: the code to be parsed as str + project_root: ``Path`` object of the project root path + module_path: ``Path`` object of the current module path + ir_graph: the project ``IRGraph`` + + Returns: + An ``IR`` instance + """ + + parse_tree = grammar_parser(program_rule).parse(raw_code) + return visit_parse_tree( + parse_tree, + ParserIRVisitor( + grammar_parser, + project_root, + module_path, + ir_graph, + ), + ) + + +########################### +# PARSER IR VISITOR CLASS # +########################### + + +class ParserIRVisitor(PTNodeVisitor): + """Visitor for parsing using IR code logic instead of AST's""" + + # TODO: split the ParserIRVisitor class for different grammars + + _root: Path + _module_path: Path + _ir_graph: IRGraph + _grammar: Callable[[Callable], ParserPython] + + def __init__( + self, + grammar_parser: Callable[[Callable], ParserPython], + project_root: Path, + module_path: Path, + ir_graph: IRGraph, + ): + super().__init__() + self._grammar = grammar_parser + self._root = project_root + self._module_path = module_path + self._ir_graph = ir_graph + + @property + def grammar(self) -> Callable[[Callable], ParserPython]: + return self._grammar + + @property + def project_root(self) -> Path: + return self._root + + @property + def module_path(self) -> Path: + return self._module_path + + @property + def ir_graph(self) -> IRGraph: + return self._ir_graph + + def visit_type_program(self, _: NonTerminal, child: SemanticActionResults) -> IR: + refs: dict[Symbol | CompositeSymbol, Path] = dict() + types: tuple[BaseTypeDef, ...] | tuple = () + + for k in child: + match k: + case ImportDicts(): + refs.update(k.types) + + case BaseTypeDef(): + types += (k,) + + case _: + print(f"[type-program] ?? {type(k)}") + + visited_ir = build_ir( + path=self._module_path, + ref_types=refs, + ref_fns=dict(), + types=types, + fns=(), + main=None, + ) + + self._ir_graph.add_node(visited_ir) + + return visited_ir + + def visit_fn_program(self, _: NonTerminal, child: SemanticActionResults) -> IR: + main: BodyBlock | None = None + ref_types: dict[Symbol | CompositeSymbol, Path] = dict() + ref_fns: dict[BaseFnCheck, Path] = dict() + types: tuple[BaseTypeDef, ...] | tuple = () + fns: tuple[FnDef | BuiltinFnDef, ...] | tuple = () + + for k in child: + match k: + case IRBlock(): + if isinstance(k, BodyBlock): + main = k + + case ImportDicts(): + ref_types.update(k.types) + ref_fns.update(k.fns) + + case BaseTypeDef(): + types += (k,) + + case FnDef() | BuiltinFnDef(): + fns += (k,) + + case _: + print(f"[?] unknown child of type {type(k)}") + + visited_ir = build_ir( + path=self._module_path, + ref_types=ref_types, + ref_fns=ref_fns, + types=types, + fns=fns, + main=main, + ) + + if isinstance(main, BodyBlock): + self._ir_graph.add_main_node(visited_ir) + + else: + self._ir_graph.add_node(visited_ir) + + return visited_ir + + def visit_type_file(self, _: NonTerminal, child: SemanticActionResults) -> BaseTypeDef: + return child[0] + + def visit_typesingle( + self, _: NonTerminal, child: SemanticActionResults + ) -> SingleTypeDef | ErrorHandler: + # TODO: implement a better resolver to account for custom and circular imports; + # for now, just check if it's built-in. + + btype = builtins_types[child[1]] + single = SingleTypeDef(name=child[0]) + return single.add_member(btype) + + def visit_typemember( + self, _: NonTerminal, child: SemanticActionResults + ) -> tuple[Symbol | CompositeSymbol | BaseTypeDef, Symbol | CompositeSymbol]: + # Fow now, it will try to fetch built-in types, otherwise it will + # save the check for later + member_type = builtins_types.get(child[1], child[1]) + member_name = child[0] + return member_type, member_name + + def visit_typestruct(self, _: NonTerminal, child: SemanticActionResults) -> BaseTypeDef: + struct = StructTypeDef(name=child[0], num_members=len(child[1:])) + + # second, populate struct + for t, m in child[1:]: + struct.add_member(t, m) + + return struct + + def visit_enummember( + self, _: NonTerminal, child: SemanticActionResults + ) -> Symbol | CompositeSymbol | BaseTypeDef: + # should have just one + if len(child) != 1: + raise ValueError("enum member must be a single attribute") + + return child[0] + + def visit_typeenum(self, _: NonTerminal, child: SemanticActionResults) -> BaseTypeDef: + enum_ds = EnumTypeDef(name=child[0], num_members=len(child[1:])) + + for member in child[1:]: + enum_ds.add_member(member) + + return enum_ds + + def visit_typeunion(self, _: NonTerminal, child: SemanticActionResults) -> Any: + raise NotImplementedError() + + def visit_fns(self, _: NonTerminal, child: SemanticActionResults) -> FnDef: + if len(child) == 4: + return FnDef(fn_name=child[0], fn_args=child[1], fn_type=child[2], fn_body=child[3]) + + return FnDef(fn_name=child[0], fn_args=child[1], fn_type=None, fn_body=child[2]) + + def visit_fnargs(self, _: NonTerminal, child: SemanticActionResults) -> ArgsBlock: + return ArgsBlock(*child) + + def visit_argtype(self, _: NonTerminal, child: SemanticActionResults) -> ArgsValuesBlock: + return ArgsValuesBlock((child[0], child[1])) + + def visit_fn_body(self, _: NonTerminal, child: SemanticActionResults) -> BodyBlock: + return BodyBlock(*child) + + def visit_body(self, _: NonTerminal, child: SemanticActionResults) -> BodyBlock: + values: tuple[IRInstr | IRBlock, ...] | tuple = () + for k in child: + match k: + case IRInstr(): + values += (k,) + + case IRBlock(): + values += (k,) + + case _: + print(f" -> something else: {k} ({type(k)})") + + return BodyBlock(*values) + + def visit_declare(self, _: NonTerminal, child: SemanticActionResults) -> DeclareInstr: + if len(child) == 2: + return DeclareInstr(var=child[0], var_type=child[1]) + + if len(child) == 3: + return DeclareInstr(var=ModifierBlock(obj=child[0], args=child[1]), var_type=child[2]) + + raise ValueError("declaring variable must have only variable and its type") + + def visit_assign(self, _: NonTerminal, child: SemanticActionResults) -> AssignInstr: + return AssignInstr(var=child[0], value=child[1]) + + def visit_assign_ds(self, _: NonTerminal, child: SemanticActionResults) -> AssignInstr: + return AssignInstr(var=child[0], value=ArgsBlock(*child[1:])) + + def visit_declareassign( + self, _: NonTerminal, child: SemanticActionResults + ) -> DeclareAssignInstr: + if len(child) == 3: + return DeclareAssignInstr(var=child[0], var_type=child[1], value=child[2]) + + if len(child) == 4: + return DeclareAssignInstr( + var=ModifierBlock(obj=child[0], args=child[1]), + var_type=child[2], + value=child[3], + ) + + raise ValueError("declaring and assigning cannot contain more than 4 elements") + + def visit_declareassign_ds(self, _: NonTerminal, child: SemanticActionResults) -> Any: + if isinstance(child[1], ModifierArgsBlock): + return DeclareAssignInstr( + var=ModifierBlock(obj=child[0], args=child[1]), + var_type=child[2], + value=ArgsBlock(*child[3:]) if len(child) > 4 else child[3], + ) + + return DeclareAssignInstr(var=child[0], var_type=child[1], value=ArgsBlock(*child[2:])) + + def visit_fn_return(self, _: NonTerminal, child: SemanticActionResults) -> ReturnBlock: + return ReturnBlock(*child) + + def visit_expr( + self, _: NonTerminal, child: SemanticActionResults + ) -> IRBlock | IRInstr | Symbol | CompositeSymbol: + # returning the child; there should exist only one element + return child[0] + + def visit_cast(self, _: NonTerminal, child: SemanticActionResults) -> CastInstr: + return CastInstr(data=child[0], to_type=child[1]) + + def visit_call(self, _: NonTerminal, child: SemanticActionResults) -> CallInstr | ModifierBlock: + if len(child) == 1: + # only the caller is present + return CallInstr(name=child[0]) + + if len(child) == 2: + # possible cases: trait_id, args, or modifier + match child[1]: + # args option + case ArgsBlock() | ArgsValuesBlock(): + return CallInstr(name=child[0], args=child[1]) + + # modifier option + case ModifierArgsBlock(): + return ModifierBlock(obj=CallInstr(name=child[0]), args=child[1]) + + # trait_id option + case Symbol() | CompositeSymbol() | ModifierBlock(): + raise NotImplementedError("trait not implemented yet") + + case _: + raise NotImplementedError("unknown case") + + if len(child) == 3: + # possible cases: trait_id and args, trait_id and modifier, args and modifier + match (child[1], child[2]): + # args and modifier + case (ArgsBlock() | ArgsValuesBlock(), ModifierArgsBlock()): + return ModifierBlock(CallInstr(name=child[0], args=child[1]), args=child[2]) + + # trait and something + case _: + raise NotImplementedError() + + if len(child) == 4: + # everything together :) + raise NotImplementedError() + + raise ValueError("call cannot have len 0 or > 4") + + def visit_trait_id(self, _: NonTerminal, child: SemanticActionResults) -> Any: + raise NotImplementedError() + + def visit_args( + self, _: NonTerminal, child: SemanticActionResults + ) -> ArgsBlock | ArgsValuesBlock: + argsvalues: tuple[ArgsValuesBlock, ...] | tuple = () + args: tuple[SimpleObj | ObjArray | IRInstr | ModifierBlock] | tuple = () + + for k in child: + match k: + case ArgsValuesBlock(): + argsvalues += (k,) + + case IRInstr() | ModifierBlock(): + args += (k,) + + case Symbol() | CompositeSymbol() | Literal() | LiteralArray(): + args += (k,) + + case _: + raise ValueError(f"unexpected value from args ({k}, {type(k)})") + + if len(argsvalues) != 0 and len(args) != 0: + raise ValueError( + "arguments in functino call cannot mix key-value pairs with value only" + ) + + if args: + return ArgsBlock(*args) + + return ArgsValuesBlock(*argsvalues) + + def visit_assignargs(self, _: NonTerminal, child: SemanticActionResults) -> ArgsValuesBlock: + if len(child) == 2: + return ArgsValuesBlock((child[0], child[1])) + + raise ValueError("assigning arg with value cannot have more than an argument and a value") + + def visit_callargs(self, _: NonTerminal, child: SemanticActionResults) -> ArgsValuesBlock: + return ArgsValuesBlock((child[0], child[1])) + + def visit_valonly(self, _: NonTerminal, child: SemanticActionResults) -> SimpleObj | ObjArray: + return child[0] + + def visit_option(self, _: NonTerminal, child: SemanticActionResults) -> OptionBlock: + assert len(child) == 2, "Option grammar must have one option and one block" + return OptionBlock(option=child[0], block=child[1]) + + def visit_call_bdn(self, _: NonTerminal, child: SemanticActionResults) -> CallInstr: + raise NotImplementedError() + + def visit_call_optbdn(self, _: NonTerminal, child: SemanticActionResults) -> CallInstr: + args: tuple = () + body: BodyBlock | None = None + option: tuple[OptionBlock] | tuple = () + + for k in child[1:]: + match k: + case ArgsBlock() | ArgsValuesBlock(): + args += (k,) + + case BodyBlock(): + body = k + + case OptionBlock(): + option += (k,) + + case _: + raise ValueError(f"unexpected value on call with body options {k} ({type(k)})") + + body = BodyBlock(*option) if option else body + args_entry = ArgsBlock(*args) if args else None + return CallInstr(name=child[0], args=args_entry, body=body) + + def visit_call_optn(self, _: NonTerminal, child: SemanticActionResults) -> CallInstr: + return CallInstr(name=child[0], option=child[1]) + + def visit_id_composite_value( + self, node: NonTerminal, child: SemanticActionResults + ) -> CompositeSymbol | tuple: + # Id composite value should have only one value + return child[0] + + def visit_imports(self, _: NonTerminal, child: SemanticActionResults) -> ImportDicts: + types = TypesDict() + fns = FnsDict() + + for k in child: + match k: + case TypesDict(): + types.update(k) + + case FnsDict(): + fns.update(k) + + parsed_imports = ImportDicts(types=types, fns=fns) + return parsed_imports + + def visit_typeimport(self, _: NonTerminal, child: SemanticActionResults) -> TypesDict: + if isinstance(child[0], tuple): + types = TypesDict() + importer = TypeImporter(self._root, self._grammar, type_program, parse) + res = importer.import_types(child[0], self._ir_graph) + + for k, v in res.items(): + types[k] = v + + return types + + raise ValueError("type import not tuple?") + + def visit_fnimport(self, _: NonTerminal, child: SemanticActionResults) -> FnsDict: + if isinstance(child[0], tuple): + fns = FnsDict() + importer = FnImporter(self._root, self._grammar, fn_program, parse) + res = importer.import_fns(child[0], self._ir_graph) + + for k, v in res.items(): + fns[k] = v + + return fns + + raise ValueError("fn import no tuple?") + + def visit_single_import( + self, _: NonTerminal, child: SemanticActionResults + ) -> tuple[Symbol | CompositeSymbol] | tuple: + if len(child) > 1: + raise ValueError("single import cannot contain more than one import") + + return child[0] if isinstance(child[0], tuple) else (child[0],) + + def visit_many_import(self, _: NonTerminal, child: SemanticActionResults) -> tuple: + return tuple(chain.from_iterable(child)) + + def visit_main(self, _: NonTerminal, child: SemanticActionResults) -> IRBlock | tuple: + if len(child) == 0: + return () + + return BodyBlock(*child) + + def visit_composite_id_with_closure( + self, _: NonTerminal, child: SemanticActionResults + ) -> tuple[CompositeSymbol, ...] | tuple: + _root: tuple[Symbol, ...] = () + + match parent_id := child[0]: + case Symbol(): + _root = (parent_id,) + + case CompositeSymbol(): + _root = parent_id.value + + case _: + raise ValueError( + f"something went wrong on composite id with closure {parent_id} ({type(parent_id)})" + ) + + def _compose_id_group( + data: Symbol | tuple | list | CompositeSymbol, + ) -> tuple[tuple[Symbol, ...], ...]: + ids = () + + for p in data: + + match p: + case Symbol(): + ids += ((p,),) + + case CompositeSymbol(): + ids += (p.value,) + + case tuple() | list(): + ids += _compose_id_group(p) + + case _: + raise ValueError(f"unexpected composite id children {p} ({type(p)})") + + return ids + + res: tuple[tuple[Symbol, ...], ...] = _compose_id_group(child[1:]) + final_ids: tuple[CompositeSymbol, ...] = tuple(CompositeSymbol(_root + k) for k in res) + return final_ids + + def visit_full_id( + self, _: NonTerminal, child: SemanticActionResults + ) -> Symbol | CompositeSymbol | ModifierBlock: + if len(child) == 1: + return child[0] + + if len(child) == 2: + return ModifierBlock(obj=child[0], args=child[1]) + + raise NotImplementedError("symbol with modifier not implemented yet") + + def visit_modifier(self, _: NonTerminal, child: SemanticActionResults) -> ModifierArgsBlock: + return ModifierArgsBlock(tuple(k for k in child)) + + def visit_array(self, _: NonTerminal, child: SemanticActionResults) -> ObjArray: + raise NotImplementedError("array not implemented yet") + + def visit_composite_id(self, _: NonTerminal, child: SemanticActionResults) -> CompositeSymbol: + return CompositeSymbol(value=_resolve_data_to_symbol(child)) + + def visit_simple_id(self, node: Terminal, _: None) -> Symbol: + return Symbol(value=node.value) + + def visit_ref(self, node: Terminal, _: None) -> Symbol: + return Reference(value=node.value) + + def visit_pointer(self, node: Terminal, _: None) -> Symbol: + return Pointer(value=node.value) + + def visit_literal(self, _: NonTerminal, child: SemanticActionResults) -> Literal | LiteralArray: + if len(child) == 1: + return child[0] + + if len(child) == 2: + if isinstance(child[0], Literal) and isinstance(child[1], Symbol | CompositeSymbol): + return Literal(value=child[0].value, lit_type=child[1]) + + raise ValueError(f"unknown literal {child}") + + def visit_complex(self, _: NonTerminal, child: SemanticActionResults) -> LiteralArray: + raise NotImplementedError("complex type not implemented yet") + + def visit_t_null(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("null")) + + def visit_t_bool(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("bool")) + + def visit_t_str(self, node: NonTerminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("str")) + + def visit_t_int(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("int")) + + def visit_t_float(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("float")) + + def visit_t_imag(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("imag")) + + def visit_qt_bool(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("@bool")) + + def visit_qt_int(self, node: Terminal, _: None) -> Literal: + return Literal(value=node.value, lit_type=Symbol("@int")) + + +def _resolve_data_to_symbol( + data: ( + SemanticActionResults | tuple | SimpleObj | CompositeSymbol | ModifierBlock | ObjArray | str + ), +) -> tuple | tuple[Symbol | CompositeSymbol, ...]: + match data: + case Symbol() | CompositeSymbol(): + return (data,) + + case Literal(): + return (data.type,) + + case LiteralArray(): + return _resolve_data_to_symbol(data.value) + + case ModifierBlock(): + return (data,) + + case SemanticActionResults() | tuple(): + pure_data: tuple | tuple[Symbol, ...] = () + + for k in data: + match k: + case Symbol() | CompositeSymbol(): + pure_data += (k,) + + # case str(): + # pure_data += (k,) + + case LiteralArray(): + pure_data += k.value + + return pure_data + + # case str(): + # return (data,) + + case _: + raise NotImplementedError() + + +@deprecated("'_fetch_struct_size_qsize' function will be deprecated, don't use it") +def _fetch_struct_size_qsize( + obj: SemanticActionResults | tuple[Symbol | CompositeSymbol | BaseTypeDef], +) -> tuple[Size | None, QSize | None]: + """ + Fetch size and qsize attributes for struct data type. If members are not built-in types, + it will be skipped and be resolved later on, when all the types are defined. + + Args: + obj: the parser holder of type members + + Returns: + A tuple of ``size`` and ``qsize``. They can be ``Size`` or ``None``, and + ``QSize`` or ``None``, respectively. + """ + + count_size = 0 + count_qsize_min = 0 + count_qsize_max = 0 + + # first, count the size and qsize + for member_type, member_name in obj: + if isinstance(member_type, BaseTypeDef): + count_size += member_type.size.size + count_qsize_min += member_type.qsize.min + count_qsize_max += member_type.qsize.max or 0 + + size = Size(count_size) if count_size > 0 else None + qsize = ( + None + if count_qsize_min == 0 and count_qsize_max == 0 + else QSize(count_qsize_min, count_qsize_max or None) + ) + return size, qsize + + +@deprecated("'_fetch_enum_size_qsize' function will be deprecated soon, don't use it") +def _fetch_enum_size_qsize( + obj: SemanticActionResults | tuple[Symbol | CompositeSymbol | BaseTypeDef], +) -> tuple[Size | None, QSize | None]: + """ + Fetch size and qsize attributes for enum data type. If members are not built-in types, + it will be skipped and be resolved later on, when all the types are defined. + + Args: + obj: the parser holder of type members + + Returns: + A tuple of ``size`` and ``qsize``. They can be ``Size`` or ``None``, and + ``QSize`` or ``None``, respectively. + """ + + count_size = 0 + count_qsize_min = 0 + count_qsize_max = 0 + + for member in obj: + if isinstance(member, BaseTypeDef): + if member.size.size > count_size: + count_size = member.size.size + + if member.qsize.min > count_qsize_min: + count_qsize_min = member.qsize.min + + if member.qsize.max is not None and member.qsize.max > count_qsize_max: + count_qsize_max = member.qsize.max + + size = Size(count_size) if count_size > 0 else None + qsize = None if count_qsize_min == 0 else QSize(count_qsize_min, count_qsize_max or None) + return size, qsize diff --git a/python/src/hhat_lang/dialects/heather/parsing/run.py b/python/src/hhat_lang/dialects/heather/parsing/run.py deleted file mode 100644 index 378ee156..00000000 --- a/python/src/hhat_lang/dialects/heather/parsing/run.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from arpeggio import visit_parse_tree -from arpeggio.cleanpeg import ParserPEG - -from hhat_lang.core.code.ast import AST - -from hhat_lang.dialects.heather.grammar import WHITESPACE -from hhat_lang.dialects.heather.parsing.visitor import ParserVisitor - - -def read_grammar() -> str: - grammar_path = Path(__file__).parent.parent / "grammar" / "grammar.peg" - - if grammar_path.exists(): - return open(grammar_path, "r").read() - - raise ValueError("No grammar found on the grammar directory.") - - -def parse_grammar() -> ParserPEG: - grammar = read_grammar() - return ParserPEG( - language_def=grammar, - root_rule_name="program", - comment_rule_name="comment", - reduce_tree=True, - ws=WHITESPACE - ) - - -def parse(raw_code: str) -> AST: - parser = parse_grammar() - parse_tree = parser.parse(raw_code) - return visit_parse_tree(parse_tree, ParserVisitor()) - - -def parse_file(file: str | Path) -> AST: - with open(file, "r") as f: - data = f.read() - - return parse(data) diff --git a/python/src/hhat_lang/dialects/heather/parsing/utils.py b/python/src/hhat_lang/dialects/heather/parsing/utils.py new file mode 100644 index 00000000..95968a54 --- /dev/null +++ b/python/src/hhat_lang/dialects/heather/parsing/utils.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path +from typing import Any, Iterable, Iterator + +from hhat_lang.core.code.base import BaseFnCheck +from hhat_lang.core.data.core import Symbol +from hhat_lang.core.imports.utils import BaseImports + + +class ImportDicts(BaseImports): + def __init__(self, types: TypesDict, fns: FnsDict): + if isinstance(types, TypesDict) and isinstance(fns, FnsDict): + self.types = types + self.fns = fns + + +class TypesDict(Mapping): + """ + A special dict-like class that holds types definitions, with key as + ``CompositeSymbol`` and value as ``BaseTypeDataStructure`` object. + """ + + _data: dict[Symbol, Path] + + def __init__(self, data: dict | None = None): + self._data = data if isinstance(data, dict) else dict() + + def __setitem__(self, key: Symbol, value: Path) -> None: + if isinstance(key, Symbol) and isinstance(value, Path): + self._data[key] = value + + else: + raise ValueError(f"{key} ({type(key)}) is not valid key for types") + + def __getitem__(self, key: Symbol, /) -> Path: + if isinstance(key, Symbol): + return self._data[key] + + raise KeyError(key) + + def __len__(self) -> int: + return len(self._data) + + def items(self) -> Iterator: + yield from self._data.items() + + def keys(self) -> Iterator: + return iter(self._data.keys()) + + def values(self) -> Iterator: + return iter(self._data.values()) + + def update(self, data: Mapping) -> None: + self._data.update({k: v for k, v in data.items()}) + + def __iter__(self) -> Iterable: + yield from self._data.keys() + + def __repr__(self) -> str: + return str(self._data) + + +class FnsDict(Mapping): + """ + A special dict-like class that holds functions definitions, with key + as ``BaseFnKey`` and value as ``FnDef`` or ``BuiltinFnDef`` object. + """ + + _data: dict[BaseFnCheck, Path] + + def __init__(self, data: dict | None = None): + self._data = data if isinstance(data, dict) else dict() + + def __setitem__(self, key: BaseFnCheck, value: Path) -> None: + if isinstance(key, BaseFnCheck) and isinstance(value, Path): + if key.name in self._data: + self._data[key] = value + + else: + self._data.update({key: value}) + + else: + raise ValueError(f"{key} ({type(key)}) is not valid key for types") + + def __getitem__(self, key: BaseFnCheck, /) -> Path: + if isinstance(key, BaseFnCheck): + return self._data[key] + + raise KeyError(key) + + def __len__(self) -> int: + return len(self._data) + + def _items(self) -> Iterable: + return iter(self._data.items()) + + def items(self) -> Iterable: + return iter(self._data.items()) + + def keys(self) -> Iterator: + return iter(self._data.keys()) + + def values(self) -> Iterator: + return iter(self._data.values()) + + def update(self, data: Mapping) -> None: + self._data.update({k: v for k, v in data.items()}) + + def __iter__(self) -> Iterable: + """Iterates over the (BaseFnKey, FnDef) pairs""" + yield from self._items() + + def __contains__(self, item: Any) -> bool: + return item in self._data.keys() + + def __repr__(self) -> str: + return str(self._data) diff --git a/python/src/hhat_lang/dialects/heather/parsing/visitor.py b/python/src/hhat_lang/dialects/heather/parsing/visitor.py deleted file mode 100644 index 4e10e160..00000000 --- a/python/src/hhat_lang/dialects/heather/parsing/visitor.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from arpeggio import PTNodeVisitor, NonTerminal, SemanticActionResults - -from hhat_lang.core.code.ast import AST - -from hhat_lang.dialects.heather.code.ast import ( - Program, - Main, - Imports, - TypeImport, - TypeDef, - TypeMember, - Id, - CompositeId, - ArgValuePair, - ArgTypePair, - SingleTypeMember, - EnumTypeMember, -) - - -class ParserVisitor(PTNodeVisitor): - def visit_program(self, node: NonTerminal, child: SemanticActionResults) -> AST: - pass diff --git a/python/src/hhat_lang/low_level/README.md b/python/src/hhat_lang/low_level/README.md new file mode 100644 index 00000000..013625cb --- /dev/null +++ b/python/src/hhat_lang/low_level/README.md @@ -0,0 +1,12 @@ +# Low-level + +Low-level holds all the code implementation for the low-level quantum language (LLQ) and the target backend. + +## Low-level quantum language (LLQ) + +LLQ's can be defined as gate-based (digital) or Hamiltonian-based (analog) instructions to be later translated as pulse sequences (or equivalent) on a given target backend. H-hat instructions must be independent of LLQs and target backends definitions, so a same program can potentially be executed on different platforms and devices. + + +## Target backend + +A target backend is the interface for a vendor's hardware, simulator or emulator device. It must have at least one LLQ to read instructions from. diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/__init__.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/__init__.py index 02b0f978..983a0afb 100644 --- a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/__init__.py +++ b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/__init__.py @@ -1,6 +1,5 @@ from __future__ import annotations - DEFAULT_HEADER = """ OPENQASM 2.0; include "qelib1.inc"; diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/base.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/base.py new file mode 100644 index 00000000..83e1416a --- /dev/null +++ b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/base.py @@ -0,0 +1,343 @@ +from __future__ import annotations + +import importlib +import inspect +from functools import lru_cache +from typing import Any, Callable, Iterable, cast + +from hhat_lang.core.code.instructions import ( + QInstrFlag, +) +from hhat_lang.core.code.utils import InstrStatus +from hhat_lang.core.data.core import ( + LiteralArray, + ObjTuple, + CompositeSymbol, + Literal, + Symbol, +) +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import ( + ErrorHandler, + InstrNotFoundError, + InstrStatusError, +) +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.lowlevel.abstract_qlang import BaseLLQManager, BaseLLQ +from hhat_lang.core.utils import Error, Ok, Result +from hhat_lang.core.code.ir_block import ( + IRBlock, + IRInstr, +) + + +class LLQManager(BaseLLQManager): + """ + Low-level quantum manager for OpenQASM v2. It generates the ``QLang`` object + (that currently holds the Qiskit's ``QuantumCircuit`` object). Ex:: + llq = LLQManager() + qlang = llq.compile() # QLang + circ = qlang.code() # qiskit.QuantumCircuit + """ + + def compile(self, *args: Any, **kwargs: Any) -> BaseLLQ: + """compile code contained in quantum data""" + pass + + +class LLQ(BaseLLQ): + """ + QLang class for OpenQASM v2 code generation (currently through + Qiskit's ``QuantumCircuit`` object). Use ``code`` method to generate + the circuit. + """ + + def __init__(self): + super().__init__() + + @lru_cache + def code(self) -> Any: + pass + + +class LowLeveQLang: + # TODO: to be removed; legacy code + + def init_qlang(self) -> tuple[str, ...]: + code_list = ( + "OPENQASM 2.0;", + 'include "qelib1.inc";', + f"qreg q[{self._num_idxs}];", + f"creg c[{self._num_idxs}];", # for now, creg num == qreg num + ) + + return code_list + + def end_qlang(self) -> tuple[str, ...]: + """Provides the end of the code""" + + # TODO: check whether some qubits were previously measured and + # handle the rest appropriately + + return ("measure q -> c;",) + + def _gen_literal_int(self, literal: Literal) -> tuple[str, ...]: + if literal in self._idx: + (literal.type) + return tuple(f"x q[{n}];" for n, k in enumerate(literal.bin) if k == "1") + + return ("",) + + def _gen_literal_bool(self, literal: Literal) -> tuple[str, ...]: + return tuple("x q[") + + def gen_literal(self, literal: Literal, **_kwargs: Any) -> tuple[str, ...] | ErrorHandler: + """Generate QASM code from literal data""" + + match literal.type: + case "@int" | "@u2" | "@u3" | "@u4": + return self._gen_literal_int(literal) + + case "@bool": + return self._gen_literal_bool(literal) + + case _: + raise NotImplementedError("Generating quantum literal not implemented yet.") + + return tuple(f"x q[{n}];" for n, k in enumerate(literal.bin) if k == "1") + + def gen_var( + self, var: BaseDataContainer | Symbol, executor: BaseExecutor + ) -> tuple[str, ...] | ErrorHandler: + """Generate QASM code from variable data""" + + var_data = executor.mem.heap[var if isinstance(var, Symbol) else var.name] + code_tuple: tuple[str, ...] = () + + for member, data in cast(Iterable[tuple[Any, Any]], var_data): + match data: + case Symbol(): + d_res = self.gen_var(data, executor=self._executor) + + if isinstance(d_res, tuple): + code_tuple += d_res + + else: + return d_res + + case Literal(): + d_res = self.gen_literal(data) + + if isinstance(d_res, tuple): + code_tuple += d_res + + else: + return d_res + + case CompositeSymbol(): + # TODO: implement it + raise NotImplementedError() + + case LiteralArray(): + # TODO: implement it + raise NotImplementedError() + + case ObjTuple(): + # TODO: implement it + raise NotImplementedError() + + case IRInstr(): + match res := self.gen_instrs(instr=data, executor=self._executor): + case Ok(): + code_tuple += res.result() + + case Error(): + return res.result() + + case ErrorHandler(): + return res + + return code_tuple + + def gen_args(self, args: tuple[Any, ...], **kwargs: Any) -> Result | ErrorHandler: + code_tuple: tuple[str, ...] = () + + for k in args: + match k: + case Symbol(): + res = self.gen_var(k, executor=self._executor) + + if isinstance(res, tuple): + code_tuple += res + + else: + return res + + case Literal(): + res = self.gen_literal(k) + + if isinstance(res, tuple): + code_tuple += res + + else: + return res + + case CompositeSymbol(): + # TODO: implement it + raise NotImplementedError() + + case LiteralArray(): + # TODO: implement it + raise NotImplementedError() + + case ObjTuple(): + # TODO: implement it + raise NotImplementedError() + + case IRInstr(): + match instr_res := self.gen_instrs(instr=k, **kwargs): + case Ok(): + code_tuple += instr_res.result() + + case Error(): + return instr_res.result() + + case ErrorHandler(): + return instr_res + + case _: + # unknown case, needs investigation + raise NotImplementedError() + + return Ok(code_tuple) + + def gen_instrs(self, *, instr: IRInstr | IRBlock, **kwargs: Any) -> Result | ErrorHandler: + """ + Transforms each of the instructions into an OpenQASM v2 code or + evaluate the code using the `executor` (H-hat dialect native + executor) if it is classical instruction not supported by OpenQASM v2. + + Args: + instr: InstrIR or BlockIR + **kwargs: anything else + + Returns: + A tuple with OpenQASM v2 code strings + """ + + if not isinstance(instr, IRInstr): + return InstrNotFoundError(getattr(instr, "name", None)) + + instr_module = importlib.import_module( + name="hhat_lang.low_level.quantum_lang.openqasm.v2.instructions", + ) + + for name, obj in inspect.getmembers(instr_module, inspect.isclass): + if (x := getattr(obj, "name", False)) and x == instr.name: + skip_gen = getattr(obj, "flag", QInstrFlag.NONE) == QInstrFlag.SKIP_GEN_ARGS + + if skip_gen: + args: tuple[Any, ...] = tuple(cast(Iterable[Any], instr.args)) + if len(args) != 2: + return InstrStatusError(instr.name) + + mask, body = args + + body_cls = None + for n, o in inspect.getmembers(instr_module, inspect.isclass): + if getattr(o, "name", False) == body: + body_cls = o + break + + if body_cls is None: + return InstrNotFoundError(body) + + res_instr, res_status = obj()( + idxs=self._idx.in_use_by[self._qdata], + mask=mask, + body_instr=body_cls(), + executor=self._executor, + ) + else: + res_instr, res_status = obj()( + idxs=self._idx.in_use_by[self._qdata], + executor=self._executor, + ) + + if res_status == InstrStatus.DONE: + return Ok(res_instr) + + return InstrStatusError(instr.name) + + # if openQASMv2.0 does not have the instruction, then falls + # back to H-hat dialect to execute it + else: + # TODO: falls back to dialect execution + raise NotImplementedError(f"low-level qlang instr error: {x} ({type(x)})") + + return InstrNotFoundError(instr.name) + + def gen_program(self, **kwargs: Any) -> str: + """ + Produces the program as a string code written in OpenQASM v2. + + Args: + **kwargs: any metadata that can be useful + + Returns: + A string with the OpenQASM v2 code. + """ + + body_code = "" + + instr_module = importlib.import_module( + "hhat_lang.low_level.quantum_lang.openqasm.v2.instructions" + ) + + for instr in self._code: # type: ignore [attr-defined] + instr_cls = None + for name, obj in inspect.getmembers(instr_module, inspect.isclass): + if getattr(obj, "name", False) == instr.name: + instr_cls = obj + break + + skip_gen = False + if instr_cls is not None: + skip_gen = getattr(instr_cls, "flag", QInstrFlag.NONE) == QInstrFlag.SKIP_GEN_ARGS + + if instr.args and not skip_gen: + match gen_args := self.gen_args(instr.args): + case Ok(): + if gen_args.result(): + body_code += "\n".join(gen_args.result()) + "\n" + + # TODO: implement it better + case Error(): + raise ValueError(gen_args.result()) + + case ErrorHandler(): + raise gen_args + + match gen_instr := self.gen_instrs(instr=instr, idx=self._idx, executor=self._executor): + case Ok(): + body_code += "\n".join(gen_instr.result()) + + case Error(): + raise gen_instr.result() + + # TODO: implement it better + case ErrorHandler(): + raise gen_instr + + if not body_code: + return "" + + code = "" + code += "\n".join(self.init_qlang()) + "\n\n" + code += body_code + code += "\n" + code += "\n".join(self.end_qlang()) + "\n" + return code + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + pass diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/__init__.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/__init__.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/__init__.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/__init__.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/__init__.py new file mode 100644 index 00000000..4c68ec44 --- /dev/null +++ b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from hhat_lang.core.fns import BUILTIN_CORE_MODULE_ROOT_PATH + + +LLQ_MODULE_PATH = BUILTIN_CORE_MODULE_ROOT_PATH / "llq.hat" diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/fn_def.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/fn_def.py new file mode 100644 index 00000000..4582b903 --- /dev/null +++ b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/code/builtins/fns/core/fn_def.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +import sys + +from hhat_lang.core.code.base import BaseFnKey +from hhat_lang.core.data.core import Literal, Symbol, Tmp +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import FnWrongDataError +from hhat_lang.core.fns.core import include_builtin_fn +from hhat_lang.core.memory.core import MemoryManager +from hhat_lang.low_level.quantum_lang.openqasm.v2.code.builtins.fns.core import ( + LLQ_MODULE_PATH, +) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@redim"), + fn_type=Symbol("@bool"), + args_names=(Symbol("@a"),), + args_types=(Symbol("@bool"),), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qbool_qredim( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + # can only accept one argument + match arg := args[0]: + case Literal(): + pass + + case BaseDataContainer(): + pass + + case _: + sys.exit(FnWrongDataError(type(args[0]))()) + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@redim"), + fn_type=Symbol("@int"), + args_names=(Symbol("@a"),), + args_types=(Symbol("@int"),), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qint_qredim( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@redim"), + fn_type=Symbol("@u2"), + args_names=(Symbol("@a"),), + args_types=(Symbol("@u2"),), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu2_qredim( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@redim"), + fn_type=Symbol("@u3"), + args_names=(Symbol("@a"),), + args_types=(Symbol("@u3"),), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu3_qredim( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@redim"), + fn_type=Symbol("@u4"), + args_names=(Symbol("@a"),), + args_types=(Symbol("@u4"),), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu4_qredim( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@sync"), + fn_type=Symbol("@bell_t"), + args_names=(Symbol("@s"), Symbol("@t")), + args_types=(Symbol("@bool"), Symbol("@bool")), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qbool_qsync( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@sync"), + fn_type=Symbol("@ghz2_t"), + args_names=(Symbol("@s"), Symbol("@t")), + args_types=(Symbol("@u2"), Symbol("@u2")), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu2_qsync( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@sync"), + fn_type=Symbol("@ghz3_t"), + args_names=(Symbol("@s"), Symbol("@t")), + args_types=(Symbol("@u3"), Symbol("@u3")), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu3_qsync( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass + + +@include_builtin_fn( + fn_entry=BaseFnKey( + fn_name=Tmp("@sync"), + fn_type=Symbol("@ghz4_t"), + args_names=(Symbol("@s"), Symbol("@t")), + args_types=(Symbol("@u4"), Symbol("@u4")), + ), + fn_path=LLQ_MODULE_PATH, +) +def builtin_fn_qu4_qsync( + *args: Literal | BaseDataContainer, mem: MemoryManager +) -> BaseDataContainer: + pass diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/instructions.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/instructions.py index fe8e643d..d24d518e 100644 --- a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/instructions.py +++ b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/instructions.py @@ -2,50 +2,99 @@ from typing import Any -from hhat_lang.core.code.instructions import QInstr, CInstr +from hhat_lang.core.code.instructions import CInstr, QInstr, QInstrFlag from hhat_lang.core.code.utils import InstrStatus -from hhat_lang.core.execution.abstract_base import BaseEvaluator - +from hhat_lang.core.data.core import ( + LiteralArray, + ObjTuple, + Literal, + Symbol, +) +from hhat_lang.core.data.variable import BaseDataContainer +from hhat_lang.core.error_handlers.errors import ( + HeapInvalidKeyError, + IndexUnknownError, +) +from hhat_lang.core.execution.abstract_base import BaseExecutor +from hhat_lang.core.memory.core import MemoryDataTypes +from hhat_lang.core.utils import Error, Ok, Result ########################## # CLASSICAL INSTRUCTIONS # ########################## + class If(CInstr): name = "if" @staticmethod - def _instr(cond_test: str, instr: str) -> str: + def _instr(cond_test: str | tuple[str, ...], instr: str | tuple[str, ...]) -> str: return f"if({cond_test}) {instr};" def _translate_instrs( self, - cond_test: tuple[str, ...], - instrs: tuple[str, ...], - **kwargs: Any + cond_test: tuple[MemoryDataTypes], + instrs: tuple[MemoryDataTypes], + **kwargs: Any, ) -> tuple[tuple[str, ...], InstrStatus]: """ Translate `If` instruction. Number of condition tests (`cond_test`) must match the number of instructions (`instrs`). """ - return ( - tuple( - self._instr(c, i) for c, i in zip(cond_test, instrs) - ), - InstrStatus.DONE - ) + transformed_instrs: tuple[str, ...] = () + + for c, i in zip(cond_test, instrs): + c_value: str | tuple[str, ...] + + match c: + case BaseDataContainer(): + c_value = c.name.value + + case Literal() | Symbol(): + c_value = c.value + + case LiteralArray() | ObjTuple(): + raise NotImplementedError() + + case _: + raise NotImplementedError() + + i_value: str | tuple[str, ...] + + match i: + case BaseDataContainer(): + i_value = i.name.value + + case Literal() | Symbol(): + i_value = i.value + + case LiteralArray() | ObjTuple(): + raise NotImplementedError() + + case _: + raise NotImplementedError() + + transformed_instrs += (self._instr(c_value, i_value),) + + return transformed_instrs, InstrStatus.DONE def __call__( - self, - *, - executor: BaseEvaluator, - **kwargs: Any + self, *, executor: BaseExecutor, **kwargs: Any ) -> tuple[tuple[str, ...], InstrStatus]: """Transforms `if` instruction to openQASMv2.0 code.""" self._instr_status = InstrStatus.RUNNING - instrs, status = self._translate_instrs(**kwargs) + + # conditional test must be in the first position of the stack + cond_test = executor.mem.stack.pop() + cond_test_tuple = cond_test if isinstance(cond_test, tuple) else (cond_test,) + + # instructions must be in the following position of the stack + if_instrs = executor.mem.stack.pop() + if_instrs_tuple = if_instrs if isinstance(if_instrs, tuple) else (if_instrs,) + + instrs, status = self._translate_instrs(cond_test=cond_test_tuple, instrs=if_instrs_tuple) self._instr_status = status return instrs, status @@ -54,6 +103,7 @@ def __call__( # QUANTUM INSTRUCTIONS # ######################## + class QRedim(QInstr): name = "@redim" @@ -61,17 +111,11 @@ class QRedim(QInstr): def _instr(idx: int) -> str: return f"h q[{idx}];" - def _translate_instrs( - self, - idxs: tuple[int, ...] - ) -> tuple[tuple[str, ...], InstrStatus]: + def _translate_instrs(self, idxs: tuple[int, ...]) -> tuple[tuple[str, ...], InstrStatus]: return tuple(self._instr(k) for k in idxs), InstrStatus.DONE def __call__( - self, - *, - idxs: tuple[int, ...], - **_kwargs: Any + self, *, idxs: tuple[int, ...], **_kwargs: Any ) -> tuple[tuple[str, ...], InstrStatus]: """Transforms `@redim` instruction to openQASMv2.0 code""" @@ -89,8 +133,7 @@ def _instr(idxs: tuple[int, ...]) -> str: return f"cx q[{idxs[0]}], q[{idxs[1]}];" def _translate_instrs( - self, - idxs: tuple[tuple[int, ...], ...] + self, idxs: tuple[tuple[int, ...], ...] ) -> tuple[tuple[str, ...], InstrStatus]: return tuple(self._instr(k) for k in idxs), InstrStatus.DONE @@ -98,8 +141,8 @@ def __call__( self, *, idxs: tuple[tuple[int, ...], ...], - executor: BaseEvaluator, - **_kwargs: Any + executor: BaseExecutor, + **_kwargs: Any, ) -> tuple[tuple[str, ...], InstrStatus]: """Transforms `@sync` instruction to openQASMv2.0 code.""" @@ -118,11 +161,7 @@ class QIf(QInstr): name = "@if" def __call__( - self, - *, - idxs: tuple[int, ...], - executor: BaseEvaluator, - **kwargs: Any + self, *, idxs: tuple[int, ...], executor: BaseExecutor, **kwargs: Any ) -> tuple[tuple[str, ...], InstrStatus]: """Transforms `@if` instruction to openQASMv2.0 code.""" @@ -130,3 +169,139 @@ def __call__( self._instr_status = InstrStatus.RUNNING raise NotImplementedError() + + +class QNot(QInstr): + name = "@not" + + @staticmethod + def _instr(idx: int) -> str: + return f"x q[{idx}];" + + def _translate_instrs(self, idxs: tuple[int, ...]) -> tuple[tuple[str, ...], InstrStatus]: + return tuple(self._instr(k) for k in idxs), InstrStatus.DONE + + def __call__( + self, *, idxs: tuple[int, ...], **_kwargs: Any + ) -> tuple[tuple[str, ...], InstrStatus]: + """Transforms `@not` instruction to openQASMv2.0 code""" + self._instr_status = InstrStatus.RUNNING + instrs, status = self._translate_instrs(idxs) + self._instr_status = status + return instrs, status + + +class QNez(QInstr): + """Quantum not-equal-zero instruction.""" + + name = "@nez" + flag = QInstrFlag.SKIP_GEN_ARGS + + @staticmethod + def _get_mask_idxs( + mask: Literal | BaseDataContainer | Symbol, + num_idxs: int, + executor: BaseExecutor | None = None, + ) -> Result: + """Return indexes from ``mask`` that are non-zero. + + If ``mask`` is a variable or a symbol reference to a variable, the + current value is fetched from ``executor``'s memory manager. + """ + + match mask: + case Literal(): + lit = mask + + case Symbol() if mask.value in ("@true", "@false"): + bool_val = "@1" if mask.value == "@true" else "@0" + lit = Literal(bool_val, "@bool") + + case BaseDataContainer() | Symbol(): + if executor is None: + return Error(IndexUnknownError()) + + var = executor.mem.heap[mask if isinstance(mask, Symbol) else mask.name] + + if isinstance(var, HeapInvalidKeyError): + return Error(var) + + val = var.get(var.type if hasattr(var, "type") else None) + + if isinstance(val, list): + val = val[-1] + + if not isinstance(val, Literal): + return Error(IndexUnknownError()) + + lit = val + + case _: + return Error(IndexUnknownError()) + + mask_bits = lit.bin[::-1] + idxs: tuple[int, ...] = tuple( + i for i, bit in enumerate(mask_bits) if bit == "1" and i < num_idxs + ) + + return Ok(idxs) + + @staticmethod + def _instr(idx: int, body_instr: QInstr) -> str: + if hasattr(body_instr, "_instr"): + return body_instr._instr(idx) # type: ignore[attr-defined] + raise NotImplementedError("body instruction missing '_instr' method") + + def _translate_instrs( + self, + idxs: tuple[int, ...], + mask: Literal | BaseDataContainer | Symbol, + body_instr: QInstr, + executor: BaseExecutor | None = None, + **kwargs: Any, + ) -> tuple[tuple[str, ...], InstrStatus]: + """Translate ``@nez`` instruction.""" + + mask_res = self._get_mask_idxs(mask, len(idxs), executor) + + match mask_res: + case Ok(): + mask_idxs = mask_res.result() + + case Error(): + # error while obtaining mask indexes + return (mask_res.result(),), InstrStatus.ERROR # type: ignore[return-value] + + case _: + return tuple(), InstrStatus.ERROR + + if not mask_idxs: + return tuple(), InstrStatus.DONE + + selected = tuple(idxs[i] for i in mask_idxs) + return ( + tuple(self._instr(i, body_instr) for i in selected), + InstrStatus.DONE, + ) + + def __call__( + self, + *, + idxs: tuple[int, ...], + mask: Literal | BaseDataContainer | Symbol, + body_instr: QInstr, + executor: BaseExecutor | None = None, + **kwargs: Any, + ) -> tuple[tuple[str, ...], InstrStatus]: + """Transforms ``@nez`` instruction to OpenQASM v2.0 code.""" + + self._instr_status = InstrStatus.RUNNING + instrs, status = self._translate_instrs( + idxs=idxs, + mask=mask, + body_instr=body_instr, + executor=executor, + **kwargs, + ) + self._instr_status = status + return instrs, status diff --git a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/qlang.py b/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/qlang.py deleted file mode 100644 index 879c599d..00000000 --- a/python/src/hhat_lang/low_level/quantum_lang/openqasm/v2/qlang.py +++ /dev/null @@ -1,234 +0,0 @@ -from __future__ import annotations - -from typing import Any, Callable - -import importlib -import inspect - -from hhat_lang.core.code.ir import BlockIR, InstrIR, TypeIR, InstrIRFlag -from hhat_lang.core.code.utils import InstrStatus -from hhat_lang.core.data.core import ( - Symbol, CoreLiteral, CompositeSymbol, CompositeLiteral, - CompositeMixData -) -from hhat_lang.core.data.variable import BaseDataContainer -from hhat_lang.core.error_handlers.errors import ErrorHandler, InstrNotFoundError, InstrStatusError -from hhat_lang.core.execution.abstract_base import BaseEvaluator -from hhat_lang.core.lowlevel.abstract_qlang import BaseLowLevelQLang -from hhat_lang.core.memory.core import IndexManager, MemoryManager -from hhat_lang.core.utils import Result, Ok, Error -from hhat_lang.dialects.heather.code.ast import Literal - -from hhat_lang.dialects.heather.code.simple_ir_builder.ir import IRBlock, IRInstr, IRArgs - - -class LowLeveQLang(BaseLowLevelQLang): - def init_qlang(self) -> tuple[str, ...]: - code_list = ( - "OPENQASM 2.0;", - 'include "qelib1.inc";', - f"qreg q[{self._num_idxs}];", - f"creg c[{self._num_idxs}];", # for now, creg num == qreg num - ) - - return code_list - - def end_qlang(self) -> tuple[str, ...]: - """Provides the end of the code""" - - # TODO: check whether some qubits were previously measured and - # handle the rest appropriately - - return "measure q -> c;", - - def gen_literal(self, literal: CoreLiteral, **_kwargs: Any) -> tuple[str, ...] | ErrorHandler: - """Generate QASM code from literal data""" - - return tuple(f"x q[{n}];" for n, k in enumerate(literal.bin) if k == "1") - - def gen_var( - self, - var: BaseDataContainer, - executor: BaseEvaluator - ) -> tuple[str, ...] | ErrorHandler: - """Generate QASM code from variable data""" - - var_data = executor.mem.heap[var.name] - code_tuple = () - - for member, data in var_data: - - match data: - case Symbol(): - code_tuple += self.gen_var(data, executor=self._executor) - - case CoreLiteral(): - code_tuple += self.gen_literal(data) - - case CompositeSymbol(): - # TODO: implement it - raise NotImplementedError() - - case CompositeLiteral(): - # TODO: implement it - raise NotImplementedError() - - case CompositeMixData(): - # TODO: implement it - raise NotImplementedError() - - case InstrIR(): - - match res := self.gen_instrs(data, executor=self._executor): - case Ok(): - code_tuple += res.result() - - case Error(): - return res.result() - - case ErrorHandler(): - return res - - return code_tuple - - - def gen_args(self, args: tuple[Any, ...], **kwargs: Any) -> Result: - code_tuple = () - - for k in args: - - match k: - case Symbol(): - code_tuple += self.gen_var(k, executor=self._executor) - - case CoreLiteral(): - code_tuple += self.gen_literal(k) - - case CompositeSymbol(): - # TODO: implement it - raise NotImplementedError() - - case CompositeLiteral(): - # TODO: implement it - raise NotImplementedError() - - case CompositeMixData(): - # TODO: implement it - raise NotImplementedError() - - case InstrIR(): - - match res := self.gen_instrs(k, **kwargs): - case Ok(): - code_tuple += res.result() - - case Error(): - return res.result() - - case ErrorHandler(): - return res - - case _: - # unknown case, needs investigation - raise NotImplementedError() - - return Ok(code_tuple) - - def gen_instrs( - self, - instr: InstrIR | BlockIR, - **kwargs: Any - ) -> Result | ErrorHandler: - """ - Transforms each of the instructions into an OpenQASM v2 code or - evaluate the code using the `executor` (H-hat dialect native - executor) if it is classical instruction not supported by OpenQASM v2. - - Args: - instr: InstrIR or BlockIR - **kwargs: anything else - - Returns: - A tuple with OpenQASM v2 code strings - """ - - instr_module = importlib.import_module( - name="hhat_lang.low_level.quantum_lang.openqasm.v2.instructions", - ) - - for name, obj in inspect.getmembers(instr_module, inspect.isclass): - - if (x:= getattr(obj, "name", False)) and x == instr.name: - res_instr, res_status = obj()( - idxs=self._idx.in_use_by[self._qdata], - executor=self._executor, - ) - - if res_status == InstrStatus.DONE: - return Ok(res_instr) - - return InstrStatusError(instr.name) - - # if openQASMv2.0 does not have the instruction, then falls - # back to H-hat dialect to execute it - else: - # TODO: falls back to dialect execution - pass - - return InstrNotFoundError(instr.name) - - def gen_program( - self, - **kwargs: Any - ) -> str: - """ - Produces the program as a string code written in OpenQASM v2. - - Args: - **kwargs: any metadata that can be useful - - Returns: - A string with the OpenQASM v2 code. - """ - - code = "" - code += "\n".join(self.init_qlang()) + "\n" - - for instr in self._code: - - if instr.args: - - match gen_args := self.gen_args(instr.args): - - case Ok(): - code += "\n".join(gen_args.result()) + "\n" - - # TODO: implement it better - case Error(): - raise ValueError(gen_args.result()) - - case ErrorHandler(): - raise gen_args - - match gen_instr := self.gen_instrs( - instr=instr, - idx=self._idx, - executor=self._executor - ): - - case Ok(): - code += "\n".join(gen_instr.result()) - - case Error(): - raise gen_instr.result() - - # TODO: implement it better - case ErrorHandler(): - raise gen_instr - - code += "\n" - code += "\n".join(self.end_qlang()) + "\n" - return code - - def __call__(self, *args: Any, **kwargs: Any) -> Any: - pass diff --git a/python/src/hhat_lang/low_level/target_backend/qiskit/aer_simulator/__init__.py b/python/src/hhat_lang/low_level/target_backend/qiskit/aer_simulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/low_level/target_backend/qiskit/openqasm/code_executor.py b/python/src/hhat_lang/low_level/target_backend/qiskit/aer_simulator/code_evaluator.py similarity index 77% rename from python/src/hhat_lang/low_level/target_backend/qiskit/openqasm/code_executor.py rename to python/src/hhat_lang/low_level/target_backend/qiskit/aer_simulator/code_evaluator.py index d0434d3c..578a889e 100644 --- a/python/src/hhat_lang/low_level/target_backend/qiskit/openqasm/code_executor.py +++ b/python/src/hhat_lang/low_level/target_backend/qiskit/aer_simulator/code_evaluator.py @@ -2,17 +2,18 @@ from typing import Any -from qiskit import qasm2, QuantumCircuit, transpile -from qiskit.primitives.containers.pub_result import PubResult, DataBin +from qiskit import QuantumCircuit, qasm2, transpile +from qiskit.primitives.containers.pub_result import DataBin, PubResult +from qiskit.primitives.containers.bit_array import BitArray # TODO: to set the configuration's simulator instead of a fixed simulator from qiskit_aer import AerSimulator from qiskit_aer.primitives import SamplerV2 as Sampler -from hhat_lang.core.data.core import Symbol, WorkingData +from hhat_lang.core.data.core import Symbol, SimpleObj from hhat_lang.core.error_handlers.errors import ( + ErrorHandler, InvalidQuantumComputedResult, - ErrorHandler ) @@ -22,7 +23,7 @@ def load_qasm(code: str) -> QuantumCircuit: def sample_circuit( circuit: QuantumCircuit, - qdata: str | Symbol, + qdata: str | SimpleObj, metadata: dict[str, Any] | None = None, ) -> Any | ErrorHandler: """ @@ -44,18 +45,17 @@ def sample_circuit( if job_res: pub_res: PubResult = job_res[0] databin: DataBin = pub_res.data - res = (getattr(databin, "c", None) or getattr(databin, "meas", None)).get_counts() - return res + res = getattr(databin, "c", None) or getattr(databin, "meas", None) + + if res is not None: + res = res.get_counts() + return res # job_res is None, then something went wrong return InvalidQuantumComputedResult(qdata) -def execute_program( - code: str, - qdata: str | WorkingData, - debug: bool = False -) -> Any | ErrorHandler: +def execute_program(code: str, qdata: str | SimpleObj, debug: bool = False) -> Any | ErrorHandler: """ Execute the quantum program from a quantum data `qdata`. First, it is passed as a string of code as a plain OpenQASM v2.0 code, then transformed into a qiskit's @@ -67,7 +67,6 @@ def execute_program( res = sample_circuit(circ, qdata) match res: - # in case it had an error case InvalidQuantumComputedResult(): # TODO: define properly what to do next @@ -75,7 +74,6 @@ def execute_program( # should contain the counts with bitstrings as keys (`Counter`?) case _: - if debug: print(res) diff --git a/python/src/hhat_lang/toolchain/cli/cli.py b/python/src/hhat_lang/toolchain/cli/cli.py new file mode 100644 index 00000000..9db310a6 --- /dev/null +++ b/python/src/hhat_lang/toolchain/cli/cli.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Optional + +import typer +from rich import print +from rich.console import Console +from rich.panel import Panel + +from hhat_lang.toolchain.project.new import ( + create_new_fn_file, + create_new_project, + create_new_type_file, + create_new_const_file, +) +from hhat_lang.toolchain.project.run import run_project +from hhat_lang.toolchain.project.utils import get_proj_dir + +app = typer.Typer( + name="hat", + help="[bold royal_blue1]Command line interface for H-hat language toolchain[/bold royal_blue1]", + no_args_is_help=True, + rich_markup_mode="rich", +) + +console = Console() + + +def version_callback(value: bool) -> None: + if value: + print("[bold royal_blue1]H-hat Language Toolchain[/] version [bold royal_blue1]0.1.0[/]") + raise typer.Exit() + + +@app.callback() +def common( + version: bool = typer.Option( + None, + "--version", + "-v", + help="Show version and exit.", + callback=version_callback, + is_eager=True, + ), +) -> None: + """ + H-hat Language Toolchain - A quantum programming language toolchain + + Use 'hat help ' for detailed information about a command. + """ + pass + + +@app.command() +def help(command: Optional[str] = typer.Argument(None, help="Command to get help for")) -> None: + """ + Show help about commands. + + Examples: + hat help # Show all available commands + hat help new # Show help for the new command + hat help run # Show help for the run command + """ + if command is None: + console.print( + Panel.fit( + "[bold]H-hat Language Toolchain[/bold]\n\n" + "[bold]Available commands:[/bold]\n" + " [bold]new[/bold] Create a new project, file, or type file\n" + " [bold]run[/bold] Run the current H-hat project\n" + " [bold]help[/bold] Show this help message\n\n" + "Use [bold]hat help [/bold] for detailed information about a command.", + title="hat - Command Line Interface", + border_style="blue", + ) + ) + else: + # Simulate --help flag for the specified command + sys.argv = ["hat", command, "--help"] + try: + app() + except SystemExit: + pass + + +@app.command() +def new( + project_name: Optional[str] = typer.Argument(None, help="Name of the project to create"), + file_name: str = typer.Option(None, "--file", "-f", help="Create a new file"), + type_file: str = typer.Option(None, "--type", "-t", help="Create a new type file"), + const_file: str = typer.Option(None, "--const", "-c", help="Create a new constant file"), +) -> None: + """ + Create a new project, file, constant or type file. + + This command can create: + - A new H-hat project with required structure + - A new .hat file within the project + - A new type definition file within the project + - A new constant definition file within the project + + Examples: + hat new myproject # Create a new project + hat new -f myfile # Create a new file + hat new -t custom_type # Create a new type file + hat new -c some_constants # Create a new constant file + + All new files can be created inside one or nested directories. Directories may + exist or will be created. + """ + + try: + if project_name and not (file_name or type_file or const_file): + # Create new project + create_new_project(Path(project_name)) + console.print( + Panel( + f"Project [bold]{project_name}[/bold] created successfully!\n\n" + f"To get started, run:\n" + f" cd {project_name}\n" + f" hat run", + title="✓ Success", + border_style="green", + ) + ) + + elif file_name: + try: + proj_dir = get_proj_dir() + except ValueError as e: + console.print( + Panel( + str(e) + "\n\nPlease make sure you're inside a H-hat project directory.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + else: + create_new_fn_file(proj_dir, Path(file_name)) + console.print( + Panel( + f"File [bold]{file_name}.hat[/bold] created successfully!", + title="✓ Success", + border_style="green", + ) + ) + + elif type_file: + proj_dir = get_proj_dir() + try: + create_new_type_file(proj_dir, Path(type_file)) + console.print( + Panel( + f"Type file [bold]{type_file}.hat[/bold] created successfully!", + title="✓ Success", + border_style="green", + ) + ) + except ValueError as e: + console.print( + Panel( + str(e) + "\n\nPlease make sure you're inside a H-hat project directory.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + + elif const_file: + proj_dir = get_proj_dir() + try: + create_new_const_file(proj_dir, Path(const_file)) + console.print( + Panel( + f"Constant file [bold]{const_file}.hat[/bold] create successfully!", + title="✓ Success", + border_style="green", + ) + ) + except ValueError as e: + console.print( + Panel( + str(e) + "\n\nPlease make sure you're inside a H-hat project directory.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + + else: + console.print( + Panel( + "Please specify what to create (project, file, constant or type)\n\n" + "Examples:\n" + " hat new myproject # Create a new project\n" + " hat new -f module/myfile # Create a new file\n" + " hat new -c const_file # Create a new constant file\n" + " hat new -t custom_type # Create a new type file", + title="⚠ Missing Arguments", + border_style="yellow", + ) + ) + raise typer.Exit(1) + + except FileExistsError as e: + console.print( + Panel( + f"{str(e)}\n\nPlease choose a different name or remove the existing one.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + except Exception as e: + console.print( + Panel( + f"An unexpected error occurred: {str(e)}\n\n" + "If this persists, please report it as an issue.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + + +@app.command() +def run() -> None: + """ + Run the current H-hat project. + + This command must be executed from within a H-hat project directory + that contains a main.hat file. + + Example: + hat run # Run the current project + """ + try: + # make sure we are in the proj dir, throw err if not + get_proj_dir() + run_project() + console.print( + Panel( + "Project executed successfully!", + title="✓ Success", + border_style="green", + ) + ) + except FileNotFoundError: + console.print( + Panel( + "main.hat not found in current directory.\n\n" + "Make sure you're in a H-hat project directory with a main.hat file.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + except Exception as e: + console.print( + Panel( + f"An error occurred while running the project: {str(e)}\n\n" + "Please check your code for errors.", + title="⚠ Error", + border_style="red", + ) + ) + raise typer.Exit(1) + + +def main() -> None: + """Entry point for the CLI""" + app() diff --git a/python/src/hhat_lang/toolchain/config/__init__.py b/python/src/hhat_lang/toolchain/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/src/hhat_lang/toolchain/project/__init__.py b/python/src/hhat_lang/toolchain/project/__init__.py index e69de29b..da038750 100644 --- a/python/src/hhat_lang/toolchain/project/__init__.py +++ b/python/src/hhat_lang/toolchain/project/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +# base folder names +SOURCE_FOLDER_NAME = "src" +TYPES_FOLDER_NAME = "hat_types" +IMPORTS_FOLDER_NAME = ".hat_imports" # must be a hidden folder +DOCS_FOLDER_NAME = "docs" +TESTS_FOLDER_NAME = "tests" # future use +PROOFS_FOLDER_NAME = "proofs" # future use + +# files +MAIN_FILE_NAME = "main.hat" + +MAIN_DOC_FILE_NAME = f"{MAIN_FILE_NAME}.md" + +# paths +IMPORTS_PATH = f"{SOURCE_FOLDER_NAME}/{IMPORTS_FOLDER_NAME}" +SOURCE_TYPES_PATH = f"{SOURCE_FOLDER_NAME}/{TYPES_FOLDER_NAME}" +DOCS_TYPES_PATH = f"{DOCS_FOLDER_NAME}/{TYPES_FOLDER_NAME}" diff --git a/python/src/hhat_lang/toolchain/project/new.py b/python/src/hhat_lang/toolchain/project/new.py index af32a160..4091d5b9 100644 --- a/python/src/hhat_lang/toolchain/project/new.py +++ b/python/src/hhat_lang/toolchain/project/new.py @@ -6,16 +6,35 @@ from pathlib import Path from typing import Any +from hhat_lang.toolchain.project import ( + DOCS_FOLDER_NAME, + DOCS_TYPES_PATH, + MAIN_DOC_FILE_NAME, + MAIN_FILE_NAME, + SOURCE_FOLDER_NAME, + SOURCE_TYPES_PATH, + IMPORTS_FOLDER_NAME, + TESTS_FOLDER_NAME, + IMPORTS_PATH, +) from hhat_lang.toolchain.project.utils import str_to_path +def _is_project_scope(project_name: str | Path, some_path: Path) -> bool: + project_name = str_to_path(project_name) + + if some_path.is_relative_to(project_name): + return True + + return False + + ###################### # CREATE NEW PROJECT # ###################### -def create_new_project(project_name: str | Path) -> Any: - project_name = str_to_path(project_name) +def create_new_project(project_name: Path) -> Any: _create_template_folders(project_name) _create_template_files(project_name) @@ -25,36 +44,86 @@ def _create_template_folders(project_name: Path) -> Any: os.mkdir(project_name) # create project template structure - os.mkdir(project_name / "src") - os.mkdir(project_name / "src" / "hat_types") - os.mkdir(project_name / "src" / "hat_docs") - os.mkdir(project_name / "src" / "hat_docs" / "hat_types") - os.mkdir(project_name / "tests") + os.mkdir(project_name / SOURCE_FOLDER_NAME) + os.mkdir(project_name / SOURCE_TYPES_PATH) + os.mkdir(project_name / IMPORTS_PATH) + os.mkdir(project_name / DOCS_FOLDER_NAME) + os.mkdir(project_name / DOCS_TYPES_PATH) + # os.mkdir(project_name / TESTS_FOLDER_NAME) # TODO: once tests are implemented, include them # os.mkdir(project_name / "proofs") # TODO: once proofs are incorporated, include them def _create_template_files(project_name: Path) -> Any: - open(project_name / "src" / "main.hat", "w").close() - open(project_name / "src" / "hat_docs" / "main.hat.md", "w").close() + with open(project_name / SOURCE_FOLDER_NAME / MAIN_FILE_NAME, "w") as f: + f.write("main {\n\n}\n\n") + + with open(project_name / DOCS_FOLDER_NAME / MAIN_DOC_FILE_NAME, "w") as f: + f.write(f"# {project_name.name}\n\n") ################### # CREATE NEW FILE # ################### -def create_new_file(project_name: str | Path, file_name: str | Path) -> Any: - project_name = str_to_path(project_name) - file_name = str_to_path(file_name) - doc_file = file_name.parent / (file_name.name + ".md") - open(project_name / file_name, "w").close() - open(project_name / doc_file, "w").close() +def create_new_fn_file(project_root: Path, file_name: str | Path) -> Path: + file_name = str(file_name) + ".hat" + doc_file = file_name + ".md" + file_path: Path = project_root / SOURCE_FOLDER_NAME / file_name + if file_path.is_file(): + raise FileExistsError(f"File {file_path}.hat already exists") -def create_new_type_file(project_name: str | Path, file_name: str | Path) -> Any: - project_name = str_to_path(project_name) - file_name = str_to_path(file_name) - doc_file = file_name.parent / (file_name.name + ".md") + if file_path.parent != Path("."): + file_path.parent.mkdir(parents=True, exist_ok=True) + + doc_path = project_root / DOCS_FOLDER_NAME / doc_file + if doc_path.parent != Path("."): + doc_path.parent.mkdir(parents=True, exist_ok=True) + + open(file_path, "w").close() + open(doc_path, "w").close() + + return file_path + + +def create_new_type_file(project_path: Path, file_name: str | Path) -> Path: + file_name = str(file_name) + ".hat" + doc_file = file_name + ".md" + file_path: Path = project_path / SOURCE_TYPES_PATH / file_name + + if file_path.is_file(): + raise FileExistsError(f"File {file_path}.hat already exists") + + if file_path.parent != Path("."): + file_path.parent.mkdir(parents=True, exist_ok=True) + + doc_path = project_path / DOCS_TYPES_PATH / doc_file + if doc_path.parent != Path("."): + doc_path.parent.mkdir(parents=True, exist_ok=True) + + open(file_path, "w").close() + open(doc_path, "w").close() + + return file_path + + +def create_new_const_file(project_path: Path, file_name: str | Path) -> Path: + file_name = str(file_name) + ".hat" + doc_file = file_name + ".md" + file_path: Path = project_path / SOURCE_FOLDER_NAME / file_name + + if file_path.is_file(): + raise FileExistsError(f"File {file_path}.hat already exists") + + if file_path.parent != Path("."): + file_path.parent.mkdir(parents=True, exist_ok=True) + + doc_path = project_path / DOCS_FOLDER_NAME / doc_file + if doc_path.parent != Path("."): + doc_path.parent.mkdir(parents=True, exist_ok=True) + + open(file_path, "w").close() + open(doc_path, "w").close() - open(project_name / "src" / "hat_types" / file_name, "w").close() - open(project_name / "src" / "hat_docs" / "hat_types" / doc_file, "w").close() + return file_path diff --git a/python/src/hhat_lang/toolchain/project/run.py b/python/src/hhat_lang/toolchain/project/run.py new file mode 100644 index 00000000..ed9e7b42 --- /dev/null +++ b/python/src/hhat_lang/toolchain/project/run.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def run_project(): + raise NotImplementedError("no implementation yet") diff --git a/python/src/hhat_lang/toolchain/project/utils.py b/python/src/hhat_lang/toolchain/project/utils.py index d67cde13..5a25f189 100644 --- a/python/src/hhat_lang/toolchain/project/utils.py +++ b/python/src/hhat_lang/toolchain/project/utils.py @@ -5,3 +5,12 @@ def str_to_path(obj: str | Path) -> Path: return obj if isinstance(obj, Path) else Path(obj).resolve() + + +def get_proj_dir(path: Path | None = None) -> Path: + current = path or Path().absolute() + while current != current.parent: + if (current / "src" / "main.hat").exists(): + return current + current = current.parent + raise ValueError("Not inside a H-hat project directory or src/main.hat missing") diff --git a/python/tests/core/test_type_ds.py b/python/tests/core/test_type_ds.py index f2e30cb9..9f4fd1d5 100644 --- a/python/tests/core/test_type_ds.py +++ b/python/tests/core/test_type_ds.py @@ -2,26 +2,27 @@ from collections import OrderedDict -from hhat_lang.core.data.core import CoreLiteral, Symbol +from hhat_lang.core.data.core import CompositeSymbol, Literal, Symbol from hhat_lang.core.error_handlers.errors import ( TypeAndMemberNoMatchError, TypeQuantumOnClassicalError, VariableWrongMemberError, ) -from hhat_lang.core.types.builtin import QU3, U32 -from hhat_lang.core.types.core import SingleDS, StructDS - +from hhat_lang.core.types.builtin_types import QU3, U32, U64 +from hhat_lang.core.types.core import EnumTypeDef, SingleTypeDef, StructTypeDef +from hhat_lang.core.types.utils import BaseTypeEnum # TODO: refactor the types to use `BuiltinSingleDS` or respective data # types so properties can be compared and addressed properly. def test_single_ds() -> None: - lit_108 = CoreLiteral("108", "u32") + lit_108 = Literal("108", "u32") - user_type1 = SingleDS(name=Symbol("user_type1")) + user_type1 = SingleTypeDef(name=Symbol("user_type1")) user_type1.add_member(U32) - var1 = user_type1(lit_108, var_name=Symbol("var1")) + var1 = user_type1(var_name=Symbol("var1")) + var1.assign(lit_108) assert var1.name == Symbol("var1") assert var1.type == Symbol("user_type1") @@ -33,11 +34,16 @@ def test_single_ds() -> None: def test_single_ds_quantum() -> None: - lit_q2 = CoreLiteral("@2", "@u3") + lit_q2 = Literal("@2", "@u3") - qtype1 = SingleDS(name=Symbol("@type1")) + # type @type1:@u3 + qtype1 = SingleTypeDef(name=Symbol("@type1")) qtype1.add_member(QU3) - qvar1 = qtype1(lit_q2, var_name=Symbol("@var1")) + + # @var1:@type1 + qvar1 = qtype1(var_name=Symbol("@var1")) + # @var1 = @2:@u3 + qvar1.assign(lit_q2) assert qvar1.name == Symbol("@var1") assert qvar1.type == Symbol("@type1") @@ -50,17 +56,18 @@ def test_single_ds_quantum() -> None: def test_single_ds_quantum_wrong() -> None: - type1 = SingleDS(name=Symbol("type1")) + type1 = SingleTypeDef(name=Symbol("type1")) assert isinstance(type1.add_member(QU3), TypeQuantumOnClassicalError) def test_struct_ds() -> None: - lit_25 = CoreLiteral("25", "u32") - lit_17 = CoreLiteral("17", "u32") + lit_25 = Literal("25", "u32") + lit_17 = Literal("17", "u32") - point = StructDS(name=Symbol("point")) + point = StructTypeDef(name=Symbol("point")) point.add_member(U32, Symbol("x")).add_member(U32, Symbol("y")) - p = point(lit_25, lit_17, var_name=Symbol("p")) + p = point(var_name=Symbol("p")) + p.assign(x=lit_25, y=lit_17) assert p.name == Symbol("p") assert p.type == Symbol("point") @@ -72,20 +79,85 @@ def test_struct_ds() -> None: def test_struct_ds_quantum() -> None: - lit_8 = CoreLiteral("8", "u32") - lit_q2 = CoreLiteral("@2", "@u3") + lit_8 = Literal("8", "u32") + lit_q2 = Literal("@2", "@u3") - qsample = StructDS(name=Symbol("@sample")) - qsample.add_member(U32, Symbol("counts")).add_member(QU3, Symbol("@d")) - qvar = qsample(lit_8, lit_q2, var_name=Symbol("@var")) + # type @sample {counts:u32 @d:@u3} + qsample = StructTypeDef(name=Symbol("@sample")) + (qsample.add_member(U32, Symbol("counts")).add_member(QU3, Symbol("@d"))) + + # @var:@sample + qvar = qsample(var_name=Symbol("@var")) + # @var.{8 @2:@u3} + qvar.assign(lit_8, lit_q2) assert qvar.name == Symbol("@var") assert qvar.type == Symbol("@sample") + assert qvar._ds_type == BaseTypeEnum.STRUCT assert qvar.is_quantum is True assert qvar.data == OrderedDict({Symbol("counts"): lit_8, Symbol("@d"): [lit_q2]}) assert qvar.get(Symbol("counts")) == lit_8 and qvar.get(Symbol("@d")) == [lit_q2] + # @var2:@sample + qvar2 = qsample(var_name=Symbol("@var2")) + # @var2.{counts=8 @d=@2:@u3} + qvar2.assign(counts=lit_8, q__d=lit_q2) + + assert qvar2.name == Symbol("@var2") + assert qvar2.type == Symbol("@sample") + assert qvar2.is_quantum is True + assert qvar2.data == OrderedDict({Symbol("counts"): lit_8, Symbol("@d"): [lit_q2]}) + assert qvar2.get(Symbol("counts")) == lit_8 and qvar2.get(Symbol("@d")) == [lit_q2] + def test_struct_ds_quantum_wrong() -> None: - qtype = StructDS(name=Symbol("@type")) + qtype = StructTypeDef(name=Symbol("@type")) assert isinstance(qtype.add_member(QU3, Symbol("data")), TypeAndMemberNoMatchError) + + +def test_enum_ds() -> None: + connect_enum = CompositeSymbol(("command", "CONNECT")) + _connect = Symbol("CONNECT") + _join = Symbol("JOIN") + _quit = Symbol("QUIT") + + # type command {CONNECT JOIN QUIT} + command = EnumTypeDef(name=Symbol("command")) + command.add_member(_connect).add_member(_join).add_member(_quit) + + # opt:command + opt = command(var_name=Symbol("opt")) + # opt=command.CONNECT + opt.assign(connect_enum) + + assert opt.name == Symbol("opt") + assert opt.type == Symbol("command") + assert opt._ds_type is BaseTypeEnum.ENUM + assert opt.data == OrderedDict({0: _connect}) + assert opt.get() == _connect + assert opt.get("z") == _connect + assert opt.is_quantum is False + + +def test_enum_ds_with_struct() -> None: + _none = Symbol("NONE") + _res = StructTypeDef(name=Symbol("RESULT")).add_member(U64, Symbol("result")) + + # type option {NONE RESULT{result:u64}} + option = EnumTypeDef(name=Symbol("option")) + option.add_member(_none).add_member(_res) + + # var:option + var = option(var_name=Symbol("var")) + + res_val = _res(var_name=_res.name).assign(Literal("16", "u64")) + # var=option.RESULT.{16:u64} + var.assign(res_val) + + assert var.name == Symbol("var") + assert var.type == Symbol("option") + assert var._ds_type is BaseTypeEnum.ENUM + assert var.data == OrderedDict({0: res_val}) + assert var.get() == res_val + assert var.get("z") == res_val + assert var.is_quantum is False diff --git a/python/tests/dialects/heather/code/test_ssa_ir.py b/python/tests/dialects/heather/code/test_ssa_ir.py deleted file mode 100644 index e51e40b7..00000000 --- a/python/tests/dialects/heather/code/test_ssa_ir.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import pytest - -from hhat_lang.core.data.core import Symbol - -from hhat_lang.dialects.heather.code.ssa_ir_builder.ir import SSA, SSAPhi, IRVar - - -# TODO: include IRModifier tests - - -def test_ssa() -> None: - s = Symbol("s") - assert SSA(s) - - ssa = SSA(s) - - # SSA only accepts Symbol - with pytest.raises(ValueError): - SSA(SSAPhi(ssa, ssa)) - - # to get SSA out of SSAPhi, uses `get_ssa` method - assert SSA.get_ssa(SSAPhi(ssa, ssa)) - - # SSAPhi only accepts SSA - with pytest.raises(AttributeError): - SSAPhi(s, s) - - -def test_irvar() -> None: - s = Symbol("s") - irs = IRVar(s) - irs.push(s) - irs.push(s) - irs.push(s) - s2, s3 = irs.data[-2:] - irs.push(SSAPhi(s2, s3)) - - assert len(irs) == 4 - assert irs[-1].phi == SSAPhi(s2, s3), "IRVar index does not contain phi." - assert len(irs) -1 == irs[-1].idx, "IRVar index does not match SSA index." diff --git a/python/tests/dialects/heather/code_samples.py b/python/tests/dialects/heather/code_samples.py new file mode 100644 index 00000000..7138b565 --- /dev/null +++ b/python/tests/dialects/heather/code_samples.py @@ -0,0 +1,102 @@ +from __future__ import annotations + + +def math_floor_def() -> str: + return """ + fn floor (x:f64) i64 { + xi:i64 = x*i64 + ::if(and(ltz(x) ne(x xi*f64)):sub(xi 1) true:xi) + } + """ + + +def math_mod2pi_def() -> str: + return """ + fn mod-2pi (theta:f64) f64 { + two-pi:f64 = 6.283185307179586 + quot:i64 = floor(div(theta two-pi)) + ::sub(theta mul(two-pi quot*f64)) + } + """ + + +def math_modpi_def() -> str: + return """ + fn mod-pi (theta:f64) f64 { + pi:f64 = 3.141592653589793 + two-pi:f64 = 6.283185307179586 + quot:i64 = floor(div(add(theta pi) two-pi)) + ::sub(theta mul(two-pi quot*f64)) + } + """ + + +def math_abs_def() -> str: + return """ + fn abs (x:f64) f64 { + bit63:u64 = 9223372036854775807 // sub(pow(2 63) 1), clear sign bit + b:u64 + memcpy(b<&> x<&> sizeof(b)) + memcpy(x<&> b-and(b bit63)<&> sizeof(x)) + ::x + } + """ + + +def math_sin_def() -> str: + return """ + fn sin (theta:f64) f64 { + pi:f64 = 3.141592653589793 + pi2:f64 = pow(pi 2.0) + new-theta:f64 = mod-pi(theta) + abs-theta:f64 = if(ltz(new-theta):neg(new-theta) true:new-theta) + quad-approx:f64 = sub(div(4.0 pi) div(mul(4.0 abs(new-theta)) pi2)) + ::mul(new-theta quad-approx) + } + """ + + +def math1_types_def() -> str: + return """ + type point:i64 + type line {x:i32} + type surface:u64 + """ + + +def math2_types_def() -> str: + return """ + type plane {x:i32 y:i32} + """ + + +def math3_types_def() -> str: + return """ + type normal {dx:i32 dy:i32 dz:i32} + """ + + +def math4_types_def() -> str: + return """ + type space {x:i64 y:u64 z:i64} + type surface:u64 + type volume:u64 + """ + + +def math5_types_def() -> str: + return """ + type form {vol:u64} + """ + + +def io1_types_def() -> str: + return """ + type socket {raw:u32} + """ + + +def qstd1_types_def() -> str: + return """ + type @bell_t {@source:@bool @target:@bool} + """ diff --git a/python/tests/dialects/heather/conftest.py b/python/tests/dialects/heather/conftest.py new file mode 100644 index 00000000..ca66818d --- /dev/null +++ b/python/tests/dialects/heather/conftest.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Callable + +from pytest import fixture +from python.tests.dialects.heather.code_samples import ( + io1_types_def, + math1_types_def, + math2_types_def, + math3_types_def, + math4_types_def, + math5_types_def, + math_abs_def, + math_floor_def, + math_mod2pi_def, + math_modpi_def, + math_sin_def, + qstd1_types_def, +) +from python.tests.dialects.heather.parsing.utils import start_new_project + +RelativePath = Path | str + + +@fixture +def math_floor_fn() -> str: + return math_floor_def() + + +@fixture +def math_mod2pi_fn() -> str: + return math_mod2pi_def() + + +@fixture +def math_modpi_fn() -> str: + return math_modpi_def() + + +@fixture +def math_abs_fn() -> str: + return math_abs_def() + + +@fixture +def math_sin_fn() -> str: + return math_sin_def() + + +@fixture +def math_single_file1() -> tuple[RelativePath, str]: + return ( + "math", + f"{math_floor_def()}{math_mod2pi_def()}{math_modpi_def()}{math_abs_def()}{math_sin_def()}", + ) + + +@fixture +def empty_file() -> tuple[RelativePath, str]: + return "", "" + + +@fixture +def math1_types_file() -> tuple[RelativePath, str]: + return ( + "math/geometry/euclidian", + math1_types_def(), + ) + + +@fixture +def math2_types_file() -> tuple[RelativePath, str]: + return ( + "math/geometry/euclidian", + math2_types_def(), + ) + + +@fixture +def math3_types_file() -> tuple[RelativePath, str]: + return "math/geometry/differential", math3_types_def() + + +@fixture +def math4_types_file() -> tuple[RelativePath, str]: + return "math/geometry/differential", math4_types_def() + + +@fixture +def math5_types_file() -> tuple[RelativePath, str]: + return "math/geometry/euclidian", math5_types_def() + + +@fixture +def io1_types_file() -> tuple[RelativePath, str]: + return "std/io/net", io1_types_def() + + +@fixture +def qstd1_types_file() -> tuple[RelativePath, str]: + return "std/base", qstd1_types_def() + + +@fixture +def project_recipe1( + math4_types_file: tuple[RelativePath, str], math5_types_file: tuple[RelativePath, str] +) -> Callable: + def _run() -> Path: + main = """ + use (type:math.geometry.{euclidian.space differential.form}) + + main { + p:space =.{x=1.0 y=2.0 z=0.0} + } + """ + return start_new_project( + "project-test1", + types_files=(math4_types_file, math5_types_file), + fns_files=(), + main_file=main, + ) + + return _run + + +@fixture +def project_recipe2( + math1_types_file, + math2_types_file, + math3_types_file, + io1_types_file, + qstd1_types_file, + math_single_file1, +) -> Callable: + def _run() -> Path: + main = """ + use ( + type:[ + math.geometry.{euclidian.{line plane} differential.normal} + std.{io.net.socket base.@bell_t} + ] + fn:math.{sin floor} + ) + + main { + l:line=.{x=41} + p:plane + p.{x=250 y=600} + print(sin(0.0)) + @d:@bell_t=.{@source=@true @target=@false} + } + """ + return start_new_project( + "project-test2", + types_files=( + math1_types_file, + math2_types_file, + math3_types_file, + io1_types_file, + qstd1_types_file, + ), + fns_files=(math_single_file1,), + main_file=main, + ) + + return _run diff --git a/python/tests/dialects/heather/interpreter/quantum/test_program.py b/python/tests/dialects/heather/interpreter/quantum/test_program.py deleted file mode 100644 index 331c2e6e..00000000 --- a/python/tests/dialects/heather/interpreter/quantum/test_program.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from itertools import product - -import pytest - -from hhat_lang.core.code.ir import TypeIR, InstrIRFlag -from hhat_lang.core.data.core import Symbol, CoreLiteral -from hhat_lang.core.memory.core import MemoryManager -from hhat_lang.dialects.heather.code.simple_ir_builder.ir import IRBlock, FnIR, IRInstr, IRArgs -from hhat_lang.dialects.heather.interpreter.classical.executor import Evaluator -from hhat_lang.dialects.heather.interpreter.quantum.program import Program -from hhat_lang.low_level.quantum_lang.openqasm.v2.qlang import LowLeveQLang - - -def test_simple_empty_redim_program(MAX_ATOL_STATES_GATE: float) -> None: - qv = Symbol("@v") - - mem = MemoryManager(5) - mem.idx.add(qv, 1) - mem.idx.request(qv) - - ex = Evaluator(mem, TypeIR(), FnIR()) - - block = IRBlock() - block.add_instr(IRInstr(Symbol("@redim"), IRArgs(), InstrIRFlag.CALL)) - - program = Program(qdata=qv, idx=mem.idx, block=block, qlang=LowLeveQLang, executor=ex) - - res = program.run(debug=False) - assert (abs(res["1"] - res["0"])/(res["1"] + res["0"])) < MAX_ATOL_STATES_GATE - - -@pytest.mark.parametrize( - "ql", - [CoreLiteral("@0", "@u2"), CoreLiteral("@2", "@u2")] -) -def test_simple_literal_redim_program(ql: CoreLiteral, MAX_ATOL_STATES_GATE: float) -> None: - mem = MemoryManager(5) - mem.idx.add(ql, 2) - mem.idx.request(ql) - - ex = Evaluator(mem, TypeIR(), FnIR()) - - block = IRBlock() - block.add_instr(IRInstr(Symbol("@redim"), IRArgs(ql), InstrIRFlag.CALL)) - - program = Program(qdata=ql, idx=mem.idx, block=block, qlang=LowLeveQLang, executor=ex) - - res = program.run(debug=False) - - assert {"".join(k) for k in product("01", repeat=2)} == set(res.keys()) - assert all(abs(1/4 - k/sum(res.values())) < MAX_ATOL_STATES_GATE for k in res.values()) diff --git a/python/tests/dialects/heather/parsing/ex_fn01.hat b/python/tests/dialects/heather/parsing/ex_fn01.hat index 3620b96e..a8127f70 100644 --- a/python/tests/dialects/heather/parsing/ex_fn01.hat +++ b/python/tests/dialects/heather/parsing/ex_fn01.hat @@ -1 +1 @@ -fn sum (a:u64 b:u64) u64 { add(a b) } +fn sum (a:u64 b:u64) u64 { ::add(a b) } diff --git a/python/tests/dialects/heather/parsing/ex_fn02.hat b/python/tests/dialects/heather/parsing/ex_fn02.hat index df9dc457..30f927fa 100644 --- a/python/tests/dialects/heather/parsing/ex_fn02.hat +++ b/python/tests/dialects/heather/parsing/ex_fn02.hat @@ -1 +1 @@ -fn @sum (@a:@u3 @b:@u3) @u3 { @add(@a @b) } +fn @sum (@a:@u3 @b:@u3) @u3 { ::@add(@a @b) } diff --git a/python/tests/dialects/heather/parsing/ex_main02.hat b/python/tests/dialects/heather/parsing/ex_main02.hat index be62d9b7..1dd75593 100644 --- a/python/tests/dialects/heather/parsing/ex_main02.hat +++ b/python/tests/dialects/heather/parsing/ex_main02.hat @@ -1,4 +1,4 @@ main { // add numbers print(add(1 2)) -} \ No newline at end of file +} diff --git a/python/tests/dialects/heather/parsing/ex_main03.hat b/python/tests/dialects/heather/parsing/ex_main03.hat new file mode 100644 index 00000000..56332f5d --- /dev/null +++ b/python/tests/dialects/heather/parsing/ex_main03.hat @@ -0,0 +1,3 @@ +use (type:geometry.euclidian.space) + +main {} diff --git a/python/tests/dialects/heather/parsing/ex_main04.hat b/python/tests/dialects/heather/parsing/ex_main04.hat new file mode 100644 index 00000000..d329b0b5 --- /dev/null +++ b/python/tests/dialects/heather/parsing/ex_main04.hat @@ -0,0 +1,3 @@ +use (type:geometry.{euclidian.space differential.form}) + +main {} diff --git a/python/tests/dialects/heather/parsing/ex_main05.hat b/python/tests/dialects/heather/parsing/ex_main05.hat new file mode 100644 index 00000000..d7893661 --- /dev/null +++ b/python/tests/dialects/heather/parsing/ex_main05.hat @@ -0,0 +1,15 @@ +use ( + type:[ + geometry.{euclidian2.{line plane} differential2.normal} + std.{io.socket base.@sync_bool} + ] + fn:math.{sin floor} +) + +main { + l:line=.{x=41} + p:plane + p.{x=250 y=600} + print(sin(0.0)) + @d:@sync_bool=.{@source=@true @target=@false} +} diff --git a/python/tests/dialects/heather/parsing/test_code_parsing.py b/python/tests/dialects/heather/parsing/test_code_parsing.py new file mode 100644 index 00000000..469e1e5d --- /dev/null +++ b/python/tests/dialects/heather/parsing/test_code_parsing.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Callable + +import pytest + +from hhat_lang.core.code.ir_graph import IRGraph +from hhat_lang.dialects.heather.grammar.fn_grammar import fn_program +from hhat_lang.dialects.heather.parsing.ir_visitor import parse, parser_grammar_code + +from .utils import THIS_PATH + + +@pytest.mark.parametrize("project", ["project_recipe1", "project_recipe2"]) +def test_parse(project: Callable, request) -> None: + project = request.getfixturevalue(project) + _path: Path = project() + assert _path.exists() + + main_file = _path / "src" / "main.hat" + code = open(main_file, "r").read() + ir_graph = IRGraph() + + try: + parse(parser_grammar_code, fn_program, code, _path, main_file, ir_graph) + ir_graph.build() + print(ir_graph) + + except Exception as e: + shutil.rmtree(_path) + raise Exception(e) + + else: + shutil.rmtree(_path) diff --git a/python/tests/dialects/heather/parsing/test_parse.py b/python/tests/dialects/heather/parsing/test_parse.py deleted file mode 100644 index f2f6c75f..00000000 --- a/python/tests/dialects/heather/parsing/test_parse.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import pytest -from pathlib import Path - -from hhat_lang.dialects.heather.parsing.run import ( - parse_grammar, - parse, - parse_file, -) - -THIS = Path(__file__).parent - - -def test_parse_grammar() -> None: - assert parse_grammar() - - -@pytest.mark.parametrize( - "hat_file", - ["ex_type01.hat", "ex_type02.hat"] -) -def test_parse_type_sample_file(hat_file) -> None: - hat_file = (THIS / hat_file).resolve() - assert parse_file(hat_file) - - -@pytest.mark.parametrize( - "hat_file", - ["ex_fn01.hat", "ex_fn02.hat"] -) -def test_parse_fn_sample_file(hat_file) -> None: - hat_file = (THIS / hat_file).resolve() - assert parse_file(hat_file) - - -@pytest.mark.parametrize( - "hat_file", - ["ex_main01.hat", "ex_main02.hat"] -) -def test_parse_main_sample_file(hat_file) -> None: - hat_file = (THIS / hat_file).resolve() - assert parse_file(hat_file) diff --git a/python/tests/dialects/heather/parsing/utils.py b/python/tests/dialects/heather/parsing/utils.py new file mode 100644 index 00000000..152277db --- /dev/null +++ b/python/tests/dialects/heather/parsing/utils.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Callable + +from hhat_lang.toolchain.project.new import ( + create_new_fn_file, + create_new_project, + create_new_type_file, +) + +THIS_PATH = Path(__file__).parent + +RawCode = str +RelativePath = Path | str + + +def _create_type_file(project_path: Path, file: RelativePath) -> Path: + return create_new_type_file(project_path=project_path, file_name=file) + + +def _create_fn_file(project_path: Path, file: RelativePath) -> Path: + return create_new_fn_file(project_root=project_path, file_name=file) + + +def _append_file(file: Path, data: str) -> None: + with open(file, "a") as f: + f.write(data) + + +def _overwrite_file(file: Path, data: str) -> None: + with open(file, "w") as f: + f.write(data) + + +def _resolve_files( + fn: Callable[[Path, RelativePath], Path], + project_path: Path, + context_path: Path | str, + data: tuple[tuple[RelativePath, RawCode], ...], +): + for relative_path, code in data: + full_path = project_path / context_path + + if not (full_path / (relative_path + ".hat")).exists(): + fn(project_path, relative_path) + + _append_file(full_path / (relative_path + ".hat"), code) + + +def start_new_project( + name: str, + types_files: tuple[tuple[RelativePath, RawCode], ...], + fns_files: tuple[tuple[RelativePath, RawCode], ...], + main_file: str, +) -> Path: + project_path = Path(THIS_PATH / name).resolve() + create_new_project(project_name=project_path) + + _resolve_files(_create_type_file, project_path, "src/hat_types", types_files) + _resolve_files(_create_fn_file, project_path, "src", fns_files) + _overwrite_file(project_path / "src" / "main.hat", main_file) + + return project_path diff --git a/python/tests/lowlevel/qlang/openqasm/v2/test_lowlevelqlang.py b/python/tests/lowlevel/qlang/openqasm/v2/test_lowlevelqlang.py deleted file mode 100644 index a56c09cc..00000000 --- a/python/tests/lowlevel/qlang/openqasm/v2/test_lowlevelqlang.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -from hhat_lang.core.code.ir import TypeIR, InstrIRFlag -from hhat_lang.core.data.core import Symbol, CoreLiteral -from hhat_lang.core.memory.core import MemoryManager -from hhat_lang.dialects.heather.interpreter.classical.executor import Evaluator -from hhat_lang.dialects.heather.code.simple_ir_builder.ir import ( - FnIR, - IRBlock, - IRInstr, - IRArgs, -) -from hhat_lang.low_level.quantum_lang.openqasm.v2.qlang import LowLeveQLang - - -def test_gen_program_single_empty_redim() -> None: - code_snippet = """OPENQASM 2.0; -include "qelib1.inc"; -qreg q[1]; -creg c[1]; - -h q[0]; -measure q -> c; -""" - - qv = Symbol("@v") - - mem = MemoryManager(5) - mem.idx.add(qv, 1) - mem.idx.request(qv) - - ex = Evaluator(mem, TypeIR(), FnIR()) - - block = IRBlock() - block.add_instr(IRInstr(Symbol("@redim"), IRArgs(), InstrIRFlag.CALL)) - - qlang = LowLeveQLang(Symbol("@v"), block, mem.idx, ex) - res = qlang.gen_program() - - assert res == code_snippet - - -def test_gen_program_single_q0_redim() -> None: - code_snippet = """OPENQASM 2.0; -include "qelib1.inc"; -qreg q[1] -creg c[1] - -h q[0]; -measure q -> c; -""" - - mem = MemoryManager(5) - mem.idx.request(Symbol("@v"), 3) - - ex = Evaluator(mem, TypeIR(), FnIR()) - - block = IRBlock() - block.add_instr( - IRInstr( - name=Symbol("@redim"), - args=IRArgs(CoreLiteral(Symbol("@5").value, "@u3")), - flag=InstrIRFlag.CALL - ) - ) - - qlang = LowLeveQLang(Symbol("@v"), block, mem.idx, ex) - res = qlang.gen_program() - print(res) - # assert res == code_snippet diff --git a/python/tests/test-cli.py b/python/tests/test-cli.py new file mode 100644 index 00000000..fb3477a5 --- /dev/null +++ b/python/tests/test-cli.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from hhat_lang.toolchain.cli.cli import app + +runner = CliRunner() + + +@pytest.fixture +def temp_dir(): + """Provide a temporary directory for tests""" + original_cwd = os.getcwd() + temp_dir = "temp" + os.mkdir(temp_dir) + os.chdir(temp_dir) + yield temp_dir + os.chdir(original_cwd) + shutil.rmtree(temp_dir) + + +def test_help_command(): + """Test the help command displays all available commands""" + result = runner.invoke(app, ["help"]) + assert result.exit_code == 0 + assert "Available commands:" in result.stdout + assert "new" in result.stdout + assert "run" in result.stdout + assert "help" in result.stdout + + +def test_help_specific_command(): + """Test help for a specific command shows detailed information""" + result = runner.invoke(app, ["help", "new"]) + assert result.exit_code == 0 + assert "Create a new project, file, or type file" in result.stdout + assert "--file" in result.stdout + assert "--type" in result.stdout + + +def test_create_new_project(temp_dir): + """Test creating a new project succeeds""" + result = runner.invoke(app, ["new", "testproject"]) + assert result.exit_code == 0 + assert "created successfully" in result.stdout + assert (Path() / "testproject").exists() + assert (Path() / "testproject" / "src" / "main.hat").exists() + + +def test_create_project_exists(temp_dir): + """Test creating a project fails when directory exists""" + runner.invoke(app, ["new", "testproject"]) + # Try to create it again + result = runner.invoke(app, ["new", "testproject"]) + assert result.exit_code == 1 + assert "Error" in result.stdout + assert "exists" in result.stdout + + +def test_create_file_in_project(temp_dir): + """Test creating a new file inside a project directory""" + runner.invoke(app, ["new", "testproject"]) + os.chdir("testproject") + # Create a new file + result = runner.invoke(app, ["new", "-f", "module/testfile"]) + assert result.exit_code == 0 + assert "created successfully" in result.stdout + assert (Path() / "module" / "testfile.hat").exists() + + +def test_create_file_outside_project(temp_dir): + """Test creating a file fails outside project directory""" + result = runner.invoke(app, ["new", "-f", "testfile"]) + assert result.exit_code == 1 + assert "Error" in result.stdout + assert "project directory" in result.stdout + + +def test_create_existing_file(temp_dir): + """Test creating a file fails when it already exists""" + runner.invoke(app, ["new", "testproject"]) + os.chdir("testproject") + runner.invoke(app, ["new", "-f", "testfile"]) + result = runner.invoke(app, ["new", "-f", "testfile"]) + assert result.exit_code == 1 + assert "Error" in result.stdout + assert "already exists" in result.stdout + + +def test_create_type_file(temp_dir): + """Test creating a new type file inside a project directory""" + runner.invoke(app, ["new", "testproject"]) + os.chdir("testproject") + result = runner.invoke(app, ["new", "-t", "customtype"]) + assert result.exit_code == 0 + assert "created successfully" in result.stdout + assert "customtype.hat" in result.stdout + + +def test_run_project(temp_dir): + """Test running a project with empty main.hat""" + runner.invoke(app, ["new", "testproject"]) + os.chdir("testproject") + # Run the project + result = runner.invoke(app, ["run"]) + assert result.exit_code == 1 # expect an error + assert "Error" in result.stdout + assert "no implementation yet" in result.stdout + + +def test_run_outside_project(temp_dir): + """Test running outside a project directory fails""" + result = runner.invoke(app, ["run"]) + assert result.exit_code == 1 + assert "Error" in result.stdout + # We don't test for the exact error message since it's wrapped in a panel + # and the formatting might change + + +def test_version(): + """Test version flag shows version information""" + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 + assert "H-hat Language Toolchain" in result.stdout + assert "version" in result.stdout diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..dbce04f8 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,15 @@ +# Rust Implementation of H-hat + +Rust implementation of H-hat. It includes the rule system and key features for development of the language, and a reference dialect ([Heather](hhat_lang/src/hhat_dialects/src/hhat-heather/README.md)) to evaluate H-hat code. + +This implementation is currently in early development and may not be usable. You can clone and check it out for yourself, though: + +```bash +git clone https://github.com/hhat-lang/hhat_lang.git +``` + +There you can find this very code and also other language implementations. + +## Documentation + +Check out more at [H-hat's documentation](https://docs.hhat-lang.org). diff --git a/rust/hhat_lang/Cargo.toml b/rust/hhat_lang/Cargo.toml new file mode 100644 index 00000000..7cd87483 --- /dev/null +++ b/rust/hhat_lang/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "hhat_lang" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/rust/hhat_lang/src/README.md b/rust/hhat_lang/src/README.md new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/Cargo.toml b/rust/hhat_lang/src/hhat_core/Cargo.toml new file mode 100644 index 00000000..ebf55bb7 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "hhat_core" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dependencies.uuid] +version = "1.15.1" +# Lets you generate random UUIDs +features = [ + "v4", +] diff --git a/rust/hhat_lang/src/hhat_core/src/README.md b/rust/hhat_lang/src/hhat_core/src/README.md new file mode 100644 index 00000000..f638a473 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/README.md @@ -0,0 +1,29 @@ +# H-hat core features + +This crate serves to define H-hat's rule system and the language core features. + +## Refactoring phase + +For context during the code refactoring, what is actually being used and refactored: + +### Code that will be used with some rework, but mostly new work + +- [mem/core.rs](mem/core.rs) contains the structure and logic for memory blocks +- [mem/manager.rs](mem/manager.rs) contains the manager for all the memory-related constructs, such as memory block, heap, stack, index, etc +- [mem/defs.rs](mem/defs.rs) contains the constants for standard definitions for memory-based operations +- [mem/heap.rs](mem/heap.rs) contains the heap-related code +- [mem/index.rs](mem/index.rs) contains the index-relate code +- [mem/data.rs](mem/data.rs) contains the data-related code +- [utils.rs](utils.rs) contains some utilities constants and functions such as generating constant-size hashmaps and fullnames for the identifiers in the user program + + +### Code that needs complete rework + +- [mem/stack.rs](mem/stack.rs) contains the stack-related code; must replace old code for the working one, namely mem/core.rs's `MemBlock` code with its errors/success enum + + +### Stale code used for reference only + +Some codes can be used to build new and improved ones, such as: +- [mem/type_container.rs](mem/type_container.rs) contains an attempt to build some data structures +- [lib.rs](lib.rs) contains tests using mem/alloc.rs code must be rewritten to account for mem/core.rs code diff --git a/rust/hhat_lang/src/hhat_core/src/data/core.rs b/rust/hhat_lang/src/hhat_core/src/data/core.rs new file mode 100644 index 00000000..f8c10b13 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/data/core.rs @@ -0,0 +1,28 @@ + + +pub enum Item { + Atomic, + Symbol, + Literal, + CompositeSymbol, + CompositeLiteral, + CompositeMix, +} + +pub struct WorkingDataItem { + data: Item, + value: String, + data_type: String, + is_quantum: bool, + suppress_type: bool, +} + + +pub trait WorkingData { + fn new(); + fn push(); + fn pop(); + fn free(); + + fn is_quantum(&self) -> bool; +} diff --git a/rust/hhat_lang/src/hhat_core/src/data/mod.rs b/rust/hhat_lang/src/hhat_core/src/data/mod.rs new file mode 100644 index 00000000..98160373 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/data/mod.rs @@ -0,0 +1 @@ +mod core; diff --git a/rust/hhat_lang/src/hhat_core/src/instr/base.rs b/rust/hhat_lang/src/hhat_core/src/instr/base.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/instr/classical_instr.rs b/rust/hhat_lang/src/hhat_core/src/instr/classical_instr.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/instr/mod.rs b/rust/hhat_lang/src/hhat_core/src/instr/mod.rs new file mode 100644 index 00000000..76b4e1ee --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/instr/mod.rs @@ -0,0 +1,3 @@ +pub mod base; +pub mod classical_instr; +pub mod quantum_instr; diff --git a/rust/hhat_lang/src/hhat_core/src/instr/quantum_instr.rs b/rust/hhat_lang/src/hhat_core/src/instr/quantum_instr.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/lib.rs b/rust/hhat_lang/src/hhat_core/src/lib.rs new file mode 100644 index 00000000..72bacbbe --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/lib.rs @@ -0,0 +1,106 @@ +#![allow(dead_code, unused_attributes, unused_attributes, unused_variables)] + +mod instr; +mod mem; +mod utils; +mod data; +//---------------------- +// LET THE TESTS BEGIN +//---------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::mem::alloc::{free_memblock, AllocSuccess}; + use crate::mem::defs::MAX_MEMBLOCK_SIZE; + use std::ptr::NonNull; + + /// Create small memory blocks with [`mem::alloc::alloc_memblock`] and [`mem::base::MemBlock`] + #[test] + fn create_memblock_small() { + println!("Create memblock small:"); + + let align: usize = 8; + let block_size: usize = (1 << 4) as usize; // 16 + let block_ptr1 = mem::alloc::alloc_memblock(block_size, align); + let block_ptr2 = mem::alloc::alloc_memblock(block_size, align); + + let ptr1: NonNull = match block_ptr1 { + Ok(x) => { + println!( + " - allocated memory 1! {:} , {:}", + x.as_ptr() as u8, + x.addr() + ); + x + }, + Err(_) => { + return assert!(false); + } + }; + + let ptr2: NonNull = match block_ptr2 { + Ok(x) => { + println!( + " - allocated memory 2! {:}, {:}", + x.as_ptr() as u8, + x.addr() + ); + x + }, + Err(_) => return assert!(false), + }; + + let mut memblock: mem::base::MemBlock = match mem::base::MemBlock::new(block_size) { + Ok(x) => { + assert_eq!(x.get_size(), block_size); + { + println!(" - memblock ptr is equal to {:}", block_size + 16); + x + } + } + Err(_) => { + return assert!(false); + } + }; + + match free_memblock(block_size, align, ptr1) { + Ok(AllocSuccess::MemoryFreed) => assert!(true), + Ok(other) => assert!(false), + Err(_) => assert!(false), + }; + + match free_memblock(block_size, align, ptr2) { + Ok(AllocSuccess::MemoryFreed) => assert!(true), + Ok(other) => assert!(false), + Err(_) => assert!(false), + }; + + match memblock.free() { + Ok(AllocSuccess::MemoryFreed) => assert!(true), + Ok(other) => assert!(false), + Err(_) => assert!(false), + } + } + + /// Create the largest memory blocks enabled with [`mem::base::MemBlock`] + #[test] + fn create_memblock_large() { + println!("Create memblock large:"); + + assert!(MAX_MEMBLOCK_SIZE.is_power_of_two()); + + let mut memblock: mem::base::MemBlock = match mem::base::MemBlock::new(MAX_MEMBLOCK_SIZE) { + Ok(x) => { + assert_eq!(x.get_size(), MAX_MEMBLOCK_SIZE); + println!("memblock with the expected size {:}", MAX_MEMBLOCK_SIZE); + x + } + Err(_) => { + return assert!(false); + } + }; + + let _ = memblock.free(); + } +} diff --git a/rust/hhat_lang/src/hhat_core/src/mem/core.rs b/rust/hhat_lang/src/hhat_core/src/mem/core.rs new file mode 100644 index 00000000..cd6b67df --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/core.rs @@ -0,0 +1,242 @@ +use crate::mem::defs::{BlockSize, ALIGNMENT, MAX_MEMBLOCK_SIZE}; +use std::alloc::{alloc, dealloc, Layout}; +use std::mem::size_of_val; +use std::ptr::{read, write, NonNull}; + +/// A holder for memory block, its pointer in the [`std::alloc::GlobalAlloc`], +/// the default alignment and its total size. +/// +/// The `MemBlock` implementation is basically all unsafe since we are dealing with +/// the raw memory directly here. Although it is being checked internally to make +/// sure things don't go wild, unsafe reinforces the awareness the handler code must +/// have when interacting with it. +pub struct MemBlock { + ptr: NonNull, + size: BlockSize, + align: usize, + is_freed: bool, +} + +impl MemBlock { + /// create a new memory block with a given size (smaller than [`MAX_MEMBLOCK_SIZE`] + /// and power of two), and alignment size (power of two). + pub unsafe fn new(size: BlockSize, align: usize) -> Result { + // size must not exceed the maximum permitted block size + if size > MAX_MEMBLOCK_SIZE { + return Err(MemAllocError::InvalidBlockSize); + } + + // size must be power of two so a memory block can be properly allocated + if !size.is_power_of_two() { + return Err(MemAllocError::NotPowerOfTwo) + } + + match Self::alloc_memblock(size, align) { + Ok(value) => Ok(MemBlock { + ptr: value, + size, + align, + is_freed: false, + }), + Err(value) => Err(value), + } + + } + + unsafe fn alloc_memblock(size: BlockSize, align: usize) -> Result, MemAllocError> { + let layout: Layout = match Layout::from_size_align(size, align) { + Ok(layout) => layout, + Err(_) => return Err(MemAllocError::LayoutError), + }; + let ptr: *mut u8 = alloc(layout); + + if ptr.is_null() { + return Err(MemAllocError::NullPointer) + } + Ok(NonNull::new_unchecked(ptr)) + } + + pub unsafe fn free(&mut self) -> Result { + if !self.is_freed { + let layout: Layout = match Layout::from_size_align(self.size, ALIGNMENT) { + Ok(layout) => layout, + Err(_) => return Err(MemAllocError::LayoutError), + }; + + dealloc(self.ptr.as_ptr(), layout); + self.is_freed = true; + Ok(MemAllocSuccess::MemoryFreed) + } else { + Err(MemAllocError::MemoryAlreadyFreed) + } + } + + pub fn as_ptr(&self) -> *const u8 { + self.ptr.as_ptr() + } + + /// Push data `T` to the memory block and returns its pointer position + pub unsafe fn push(&mut self, data: T) -> Result { + let offset: usize = size_of_val(&data); + let ptr: *mut T = self.as_ptr().add(offset) as *mut T; + + if (ptr as usize) <= (self.as_ptr().add(self.size) as usize) { + write(ptr, data); + Ok(ptr as usize) + } else { + Err(MemAllocError::MemoryOverflow) + } + } + + /// Pops the last item from the memory. Because [`MemBlock`] is just a struct + /// to hold data and its pointer at the [`std::alloc::GlobalAlloc`], it doesn't + /// know where the last item is. It's up to the API above to define it, such as + /// stack memory or a heap memory struct. + /// + /// It returns a tuple as the data, the data size, and the updated cursor pointer. + pub unsafe fn pop( + &mut self, + cursor_ptr: usize, + ) -> Result<(T, usize, usize), MemAllocError> { + // read the data from the allocated memory space given a cursor pointer + let data: T = read(cursor_ptr as *const T); + + let data_size: usize = size_of_val(&data); + // get the new pointer position subtracted from the data memory space + let new_cursor_ptr: usize = (cursor_ptr as *const u8).sub(data_size) as usize; + + // the pointer handler (that called this very function) now has an updated + // pointer to use as its new cursor, for instance. + Ok((data, data_size, new_cursor_ptr)) + } + + /// Take a look at the data at some pointer position in the memory. The API calling + /// it should handle the right pointer position (the last written data, for a stack + /// memory API, for instance). The pointer is not updated since it's just peeking + /// into the memory. + pub unsafe fn peek(&mut self, ptr: usize) -> T { + // let mem_ptr: *const T = self.as_ptr().add(ptr) as *const T; + // read(mem_ptr) + read(ptr as *const T) + } +} + +impl Drop for MemBlock { + fn drop(&mut self) { + unsafe { + let _ = self.free(); + } + } +} + +#[derive(Debug)] +pub enum MemAllocError { + EmptyMemory, + InvalidBlockSize, + InvalidAlignment, + LayoutError, + MemoryAlreadyFreed, + MemoryOverflow, + NotEnoughMemory, + NotPowerOfTwo, + NullPointer, +} + +#[derive(Debug)] +pub enum MemAllocSuccess { + MemoryFreed, + MemoryAlreadyFreed, + DataPushedToMemory, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// test memory block allocation, writing, reading and de-allocation + #[test] + fn test_simple_memblock_operations() { + unsafe { + println!("== simple memblock alloc =="); + + let max_size = MAX_MEMBLOCK_SIZE; + assert_eq!(max_size, MAX_MEMBLOCK_SIZE); + + let mut memblock = MemBlock::new(max_size, 8usize).unwrap(); + println!(" - memblock"); + println!(" - [x] ptr: {}", memblock.as_ptr() as usize); + + let data_ptr = memblock.push(1u64).unwrap(); + println!(" - [x] input data: {}", 1u64); + assert!( + memblock.as_ptr().add(size_of_val(&1u64)) <= memblock.as_ptr().add(memblock.size) + ); + println!(" - [x] push data, received ptr: {:}", data_ptr); + + let retrieved_data = memblock.peek::(data_ptr); + assert_eq!(retrieved_data, 1u64); + println!(" - [x] peek data: {:}", retrieved_data); + + let (d, ds, p) = match memblock.pop::(data_ptr) { + Ok((x, y, z)) => (x, y, z), + Err(e) => panic!("{:?}", e), + }; + assert_eq!(d, 1u64); + assert_eq!(ds, 8); + assert_eq!(p, memblock.as_ptr() as usize); + println!(" - [x] pop data:"); + println!(" - - [x] retrieved data: {:}", d); + println!(" - - [x] retrieved data size: {:}", ds); + println!(" - - [x] retrieved new pointer: {:}", p); + + memblock.free().unwrap(); + println!(" - [x] memblock freed"); + + println!("====================="); + } + } + + /// test many memory blocks allocation, writing, reading and de-allocation + #[test] + fn test_many_memblock_operations() {} + + #[test] + fn test_struct_memblock_operations() { + unsafe { + #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + struct TestStruct { + x: u64, + y: u64 + } + + println!("=== complex memblock alloc ==="); + + let mut memblock = match MemBlock::new(MAX_MEMBLOCK_SIZE, 8usize) { + Ok(block) => block, + Err(err) => panic!("{:?}", err) + }; + println!(" - memblock"); + println!(" - [x] ptr: {}", memblock.as_ptr() as usize); + + let data_struct = TestStruct{x:1u64, y:65535u64}; + let data_ptr = memblock.push(data_struct.clone()).unwrap(); + println!(" - [x] input data: {:?}", data_struct); + println!(" - [x] push data, received ptr: {:}", data_ptr); + let retrieved_data = memblock.peek::(data_ptr); + assert_eq!(retrieved_data, data_struct); + println!(" - [x] peek data: {:?}", retrieved_data); + + let (d, ds, p) = match memblock.pop::(data_ptr) { + Ok((x, y, z)) => (x, y, z), + Err(e) => panic!("{:?}", e), + }; + assert_eq!(d, data_struct); + assert_eq!(ds, 16); + assert_eq!(p, memblock.as_ptr() as usize); + println!(" - [x] pop data:"); + println!(" - - [x] retrieved data: {:?}", d); + println!(" - - [x] retrieved data size: {:}", ds); + println!(" - - [x] retrieved new pointer: {:}", p); + } + } +} diff --git a/rust/hhat_lang/src/hhat_core/src/mem/data.rs b/rust/hhat_lang/src/hhat_core/src/mem/data.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/mem/defs.rs b/rust/hhat_lang/src/hhat_core/src/mem/defs.rs new file mode 100644 index 00000000..ca0f39eb --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/defs.rs @@ -0,0 +1,3 @@ +pub type BlockSize = usize; +pub const ALIGNMENT: usize = 8; // 64bit alignment +pub const MAX_MEMBLOCK_SIZE: usize = (1 << 16) as usize; // 64kB diff --git a/rust/hhat_lang/src/hhat_core/src/mem/heap.rs b/rust/hhat_lang/src/hhat_core/src/mem/heap.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/mem/index.rs b/rust/hhat_lang/src/hhat_core/src/mem/index.rs new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_core/src/mem/manager.rs b/rust/hhat_lang/src/hhat_core/src/mem/manager.rs new file mode 100644 index 00000000..e5d51dd6 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/manager.rs @@ -0,0 +1,3 @@ +/// Create a manager for all the memory matters, namely: +/// stack, heap, index, pid +pub struct MemoryManager {} diff --git a/rust/hhat_lang/src/hhat_core/src/mem/mod.rs b/rust/hhat_lang/src/hhat_core/src/mem/mod.rs new file mode 100644 index 00000000..d6dfac35 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/mod.rs @@ -0,0 +1,10 @@ +pub mod alloc; +pub mod base; +pub mod core; +mod data; +pub mod defs; +pub mod heap; +pub mod index; +pub mod manager; +pub mod stack; +pub mod type_container; diff --git a/rust/hhat_lang/src/hhat_core/src/mem/stack.rs b/rust/hhat_lang/src/hhat_core/src/mem/stack.rs new file mode 100644 index 00000000..ef482547 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/stack.rs @@ -0,0 +1,150 @@ +use std::ptr::write; + +use crate::mem::alloc::AllocError; +use crate::mem::base::MemBlock; +use crate::mem::defs::{ALIGNMENT, MAX_MEMBLOCK_SIZE}; + +/// The actual stack memory container +/// +/// It can hold several `StackPage`s according to what is needed. +/// +/// `pages` contains the `StackPage` data, whereas `page_index` is +/// responsible to hold information on which page to find some data +pub struct StackMemory { + pages: Vec, + cur_page: isize, +} + +impl StackMemory { + pub fn new(&mut self, max_size: usize) -> Self { + StackMemory { + pages: { + match max_size { + 0 => Vec::new(), + _ => { + let mut v: Vec = Vec::with_capacity(max_size); + // create a stack page with max memory size + let page: StackPage = match StackPage::new(MAX_MEMBLOCK_SIZE) { + Ok(x) => x, + Err(x) => panic!(""), + }; + v.push(page); + v + } + } + }, + cur_page: -1, + } + } + + /// When `StackMemory` needs to expand memory, a new page must be created. + /// A page is a `StackPage`. + pub fn new_page(&mut self, size: usize) -> Result<(), StackError> { + match StackPage::new(size) { + Ok(page) => { + self.pages.push(page); + let cur_page = self.cur_page; + self.cur_page = cur_page + 1; + Ok(()) + } + Err(err) => Err(err), + } + } + + pub fn page_cursor(&mut self, page_num: isize) -> *const u16 { + self.pages[page_num as usize].cursor() + } + + pub fn push(&mut self, data: T) { + unsafe { + todo!() + // figure out how to get the offset for `data` + // self.pages[-1].write(data, ); + } + } + + pub fn pop(&mut self) -> Option<()> { + todo!() + } +} + +/// Stack page memory +/// +/// To create a new `StackPage`, use `StackPage::new(size)` where `size` must be power +/// of two, `usize` type and bounded to [`MAX_MEMBLOCK_SIZE`]. +/// +/// For now, the stack page can support pointers of size u16. +pub struct StackPage { + memblock: MemBlock, + cursor: *const u16, + limit: *const u16, +} + +impl StackPage { + pub fn new(size: usize) -> Result { + match MemBlock::new(size) { + Ok(memblock) => Ok(StackPage { + memblock, + cursor: MAX_MEMBLOCK_SIZE as *const u16, + limit: MAX_MEMBLOCK_SIZE as *const u16, + }), + Err(x) => match x { + AllocError::ConstraintsNotSatisfied => Err(StackError::LayoutError), + AllocError::InvalidAlignment => Err(StackError::InvalidAlignment), + AllocError::InvalidBlockSize => Err(StackError::InvalidBlockSize), + AllocError::MemoryAlreadyFreed => Err(StackError::MemoryAlreadyFreed), + AllocError::NotEnoughMemory => Err(StackError::NotEnoughMemory), + AllocError::NotPowerOfTwo => Err(StackError::SizeNotPowerOfTwo), + AllocError::NullPointer => Err(StackError::NullPointer), + }, + } + } + + /// Makes available a space in the Stack's memory block of size `alloc_size` and + /// returns a pointer to it. + pub fn alloc(&mut self, alloc_size: usize) -> Result<*const u16, StackError> { + let start_ptr = self.memblock.as_ptr() as usize; + let cursor_ptr = self.cursor as usize; + + let next_ptr: usize = match cursor_ptr.checked_sub(alloc_size) { + Some(ptr) => ptr & ALIGNMENT, // align to word boundary + _ => return Err(StackError::NoNextPointer), + }; + + if next_ptr < start_ptr { + Err(StackError::NotEnoughMemory) + } else { + self.cursor = next_ptr as *const u16; + Ok(next_ptr as *const u16) + } + } + + /// write data to the stack page(?) + pub unsafe fn write(&mut self, data: T, offset: usize) -> *const T { + let p = self.memblock.as_ptr().add(offset) as *mut T; + write(p, data); + p + } + + /// To free the memory space given by the `ptr` pointer. + pub unsafe fn free(&mut self, ptr: *const u16) { + let _ = self.memblock.free(); + } + + pub fn cursor(&self) -> *const u16 { + self.cursor + } +} + +/// Possible stack errors to communicate +pub enum StackError { + InvalidBlockSize, + InvalidAlignment, + InvalidStackPage, + LayoutError, + MemoryAlreadyFreed, + NoNextPointer, + NotEnoughMemory, + NullPointer, + SizeNotPowerOfTwo, +} diff --git a/rust/hhat_lang/src/hhat_core/src/mem/type_container.rs b/rust/hhat_lang/src/hhat_core/src/mem/type_container.rs new file mode 100644 index 00000000..3b447f58 --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/mem/type_container.rs @@ -0,0 +1,58 @@ +use crate::utils::FullName; + +pub struct TypeContainer { + fullname: FullName, + type_ds: TypeDataStructure, +} + +/// Define the type container structure +pub trait TypeTemplate { + fn new() -> Self; + fn get_type() -> FullName; + fn ir_alloc(&mut self); +} + +type TypeFullName = FullName; + +pub struct SingleType { + fullname: FullName, + datatype: TypeFullName, + data: (), +} + +impl SingleType { + pub fn assign_data(&mut self, data: T) {} +} + +/// Type data structure for composing types. It can be +/// - `Single`: as `u16` or `bool`, they are simple, single-valued types. +/// - `Struct`: similar to `struct` in C or Rust, hold members as a name + type. +/// - `Enum`: similar to `enum` in Rust, can contain constant members or structs. +/// - `Union`: similar to `union` in C or Rust, holds members as a name + type, but +/// con only assign one of its members at once. +pub enum TypeDataStructure { + Single, + Struct(StructType), + Enum { members: Vec }, + Union { members: Vec }, +} + + +enum ConstantMember { + Constant, +} + +struct StructType { + members: Vec<(String, TypeFullName)>, +} + +enum EnumMember { + Constant(ConstantMember), + Struct(StructType), +} + +enum UnionMember { + Constant(ConstantMember), + Struct(StructType), + Enum(EnumMember), +} diff --git a/rust/hhat_lang/src/hhat_core/src/utils.rs b/rust/hhat_lang/src/hhat_core/src/utils.rs new file mode 100644 index 00000000..daf1ec8c --- /dev/null +++ b/rust/hhat_lang/src/hhat_core/src/utils.rs @@ -0,0 +1,105 @@ +// Utils for perfect hash function generation, naming scopes for +// identifiers such as variables, types and functions + +use std::collections::HashSet; + +//--------------------------- +// NAME & NAMESPACE SECTION +//--------------------------- + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FullName { + namespace: Vec, + name: String, +} + +//-------------------------------------------- +// CONSTANTS FOR PERFECT HASH FUNCTION LOGIC +//-------------------------------------------- + +const PHF_A_LIMIT: u64 = 1_000_000; +const PHF_R_LIMIT: u32 = 37; +const PRIME_CONVERTER: u64 = 257; + +/// transform string into u64 according to the formula +/// `value = value as u64 * PRIME_CONVERTER + char as u64` +/// where `value` is iterated over each of the chars +/// contained in the string +pub fn str_to_int(s: &String) -> u64 { + let mut value: u64 = 0; + + for ch in s.chars() { + value = value.wrapping_mul(PRIME_CONVERTER).wrapping_add(ch as u64); + } + + value +} + +/// Generates an u64 number to serve as the hash number for the given string +pub fn get_hash(s: &String, a: u64, r: u32, n: u64) -> u64 { + let x: u64 = str_to_int(s); + let product: u64 = x.wrapping_mul(a); + let mixed: u64 = product ^ (product >> r); + mixed % n +} + +/// Finds the perfect hash ([PHF wiki](https://en.wikipedia.org/wiki/Perfect_hash_function) +/// and [PHF paper](https://cmph.sourceforge.net/papers/esa09.pdf)) parameters `a` and `r` +/// for a given array of strings +pub fn find_phf_params(strs: Vec) -> Option<(u64, u32)> { + let n: u64 = strs.len() as u64; + + for a in 1..PHF_A_LIMIT { + for r in 0..PHF_R_LIMIT { + let mut seen: HashSet = HashSet::new(); + let mut collision: bool = false; + + for s in &strs { + let h: u64 = get_hash(s, a, r, n); + + if !seen.insert(h) { + collision = true; + break; + } + } + + if !collision { + return Some((a, r)); + } + } + } + None +} + +/// Generates the perfect hash results, returning tuple with a vector where the element +/// is the string and the index is the actual result we want (as an u64 number), the +/// `a` value as u64 number, used to retrieve the hash whenever needed, and the `r` +/// value as u32 number, also used to retrieve the hash for the string +pub fn gen_phf( + values: Vec, + vec_size: usize, +) -> Option<(Vec>, u64, u32)> { + let n: u64 = values.len() as u64; + + // check whether the array length `n` is equal `N` + if n as usize != vec_size { + panic!( + "Types list provided to the IRa types definition stack must be of a \ + fixed-sized as the constant N, i.e. the total number of strings parsed." + ) + } + + match find_phf_params(values.clone()) { + Some((a, r)) => { + let mut mapping: Vec> = Vec::with_capacity(n as usize); + + for s in &values { + let idx: usize = get_hash(s, a, r, n) as usize; + mapping[idx] = Some(s.clone()); + } + Some((mapping, a, r)) + } + + None => None, + } +} diff --git a/rust/hhat_lang/src/hhat_dialects/Cargo.toml b/rust/hhat_lang/src/hhat_dialects/Cargo.toml new file mode 100644 index 00000000..0cd94d1e --- /dev/null +++ b/rust/hhat_lang/src/hhat_dialects/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "hhat_dialects" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/rust/hhat_lang/src/hhat_dialects/src/README.md b/rust/hhat_lang/src/hhat_dialects/src/README.md new file mode 100644 index 00000000..e69de29b diff --git a/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/Cargo.toml b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/Cargo.toml new file mode 100644 index 00000000..b64c30e7 --- /dev/null +++ b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "hhat-heather" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/README.md b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/README.md new file mode 100644 index 00000000..2d5a280b --- /dev/null +++ b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/README.md @@ -0,0 +1 @@ +# H-hat's Heather dialect diff --git a/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/src/lib.rs b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/rust/hhat_lang/src/hhat_dialects/src/hhat-heather/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/rust/hhat_lang/src/hhat_dialects/src/lib.rs b/rust/hhat_lang/src/hhat_dialects/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/rust/hhat_lang/src/hhat_dialects/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/rust/hhat_lang/src/main.rs b/rust/hhat_lang/src/main.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/rust/hhat_lang/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/sandbox/test_project_config.json b/sandbox/test_project_config.json new file mode 100644 index 00000000..63db3acf --- /dev/null +++ b/sandbox/test_project_config.json @@ -0,0 +1,139 @@ +{ + "project_root": "sandbox", + "current_settings": { + "dialect": { + "name": "Heather", + "version": "0.3E", + "dir": ["heather"] + }, + "llq": [ + { + "name": "OpenQASM", + "version": "2.0", + "dir": [ + "openqasm", + "v2" + ] + } + ], + "backend": [ + { + "name": "AerSimulator", + "version": "0.17", + "dir": [ + "qiskit", + "aer_simulator" + ] + } + ] + }, + "available_settings": { + "dialect": { + "heather": { + "name": "Heather", + "version": "0.3E", + "version_type": "experimental", + "package_name": null + } + }, + "llq": { + "openqasm": { + "v2": { + "name": "OpenQASM", + "version": "2.0", + "package_name": null + }, + "v3": { + "name": "OpenQASM", + "version": "3.0", + "package_name": null + } + }, + "netqasm": { + "v2": { + "name": "NetQASM", + "version": "2.0.0", + "package_name": "netqasm" + } + } + }, + "backend": { + "qiskit": { + "aer_simulator": { + "name": "AerSimulator", + "package_name": "qiskit-aer", + "version": "0.17", + "is_simulator": true, + "executor": { + "shots": { + "shots_per_qubit": { + "base": 200, + "min": 1024, + "max": null + }, + "fixed_shots": { + "base": 4098, + "min": 4098, + "max": 4098 + } + }, + "pass_options": {}, + "cast": {} + }, + "device": { + "": { + "max_num_qubits": 29 + } + } + } + }, + "squidasm": { + "multithread": { + "name": "SquidASM Multithread", + "package_name": "squidasm", + "is_simulator": true, + "version": "0.13", + "executor": { + "max_num_qubits": 29, + "shots": { + "shots_per_qubit": { + "base": 50, + "min": 1024, + "max": null + }, + "fixed_shots": { + "base": 2048, + "min": 2048, + "max": 2048 + } + }, + "pass_options": {} + }, + "device": {} + }, + "stack": { + "name": "SquidASM Stack", + "package_name": "squidasm", + "is_simulator": true, + "version": "0.13", + "executor": { + "max_num_qubits": 29, + "shots": { + "shots_per_qubit": { + "base": 50, + "min": 1024, + "max": null + }, + "fixed_shots": { + "base": 2048, + "min": 2048, + "max": 2048 + } + } + }, + "device": {} + } + } + } + } +}