forked from katzen-cafe/iowo
Compare commits
195 commits
Author | SHA1 | Date | |
---|---|---|---|
e581a8e7cf | |||
d809d3b52d | |||
ef1a9f5029 | |||
662cb8ba0e | |||
fcf91f25e3 | |||
958857cb58 | |||
883b0c804e | |||
f7d05ead2c | |||
cee9b97dbf | |||
e5ccebe679 | |||
3164328568 | |||
c564d0f24c | |||
b8720b2df9 | |||
af6886214b | |||
ac75978c01 | |||
9b1f6a1dc1 | |||
fed8cf2466 | |||
91f766c18e | |||
becc4b4041 | |||
21bcf62ea5 | |||
34ddaacb58 | |||
ec2ff5778b | |||
a3ab844ba7 | |||
a693b57447 | |||
3412eb9395 | |||
ccc6d4f532 | |||
54401d2a21 | |||
18309ec919 | |||
0705702d4a | |||
31a044577a | |||
911339fc2a | |||
619b7acf94 | |||
b7bc0366c2 | |||
734a734f09 | |||
dddbcccf72 | |||
26996fbd00 | |||
d9a07c8898 | |||
db9228dec4 | |||
56ec11e143 | |||
1e9648966f | |||
a2695a2a11 | |||
dc44244e7b | |||
1e0741e600 | |||
3eee768ce1 | |||
eb7806572b | |||
1c6180aabc | |||
37651a83bc | |||
3e2c5946c8 | |||
1a533eb788 | |||
7bc603f7e7 | |||
d6bc644fb6 | |||
cfefab9fd0 | |||
0de076ace1 | |||
946ac879a7 | |||
f6da90a354 | |||
ed151c2e3c | |||
4bcaf945d7 | |||
29cdcfbe0c | |||
afd493be16 | |||
30f17773a8 | |||
db2643359c | |||
9af71ed3f4 | |||
8a541546d9 | |||
4df0118aa4 | |||
ba0da33509 | |||
9510d9254c | |||
e62b50a51a | |||
2bea3994c2 | |||
86b1481943 | |||
06c9094227 | |||
381ab45edc | |||
6d8b79e8f7 | |||
be637846b1 | |||
1711d17fa6 | |||
f7b61f9e0e | |||
2d59a7f560 | |||
9da157ff4a | |||
881a987b2f | |||
bfd4b3765f | |||
198c74c7ae | |||
8d7401531e | |||
b6e304fa78 | |||
ace69b0094 | |||
84448af714 | |||
ae60db7721 | |||
de008263ca | |||
ca84af4e1b | |||
ae86ae29ab | |||
02c5e9e159 | |||
0197df5ee2 | |||
919a3bb377 | |||
9ae8c2fbd3 | |||
9727ef82ca | |||
c31a158d9b | |||
aeeee54200 | |||
dc7d76dc26 | |||
e17fffb66b | |||
f59062cf88 | |||
384fef5a81 | |||
77bcb54b5e | |||
d87033d320 | |||
bf60bdd814 | |||
5368951254 | |||
a42ec014e5 | |||
01b1880089 | |||
56848a1b05 | |||
69f0baf425 | |||
98850ee1e9 | |||
d79383a7df | |||
10886be00a | |||
63d7993940 | |||
bbde1c84ca | |||
98f4a6cdeb | |||
1df57eba6b | |||
3e208335c3 | |||
3c529c3a1a | |||
de9ca81b65 | |||
c4207af8da | |||
23fadce867 | |||
98c9399ee4 | |||
8b6f9810df | |||
cdf42417da | |||
87343f0a38 | |||
5a209ab5af | |||
fa2893bc77 | |||
4fd35736d5 | |||
66639771ac | |||
29269e2fcd | |||
d8e08459a0 | |||
6395efbeab | |||
ca8cf0cc52 | |||
ee675de202 | |||
4db6eb4317 | |||
23ffbe39dc | |||
1c3012fb32 | |||
fcf7e909ee | |||
ccbccfb11b | |||
c0dd533d81 | |||
d1001ad90d | |||
77d1236720 | |||
5bf277cf14 | |||
8f3c426359 | |||
f445a03fb2 | |||
816602fb2e | |||
24ebca3e8d | |||
48458fd1c9 | |||
746c81ab60 | |||
0615ea653c | |||
6006f92d9c | |||
7c9dca0ae2 | |||
ea2e5d6075 | |||
6ccfaedb13 | |||
e67c80a6a9 | |||
96374b6491 | |||
4788278d86 | |||
33aa131b95 | |||
e7db9c38f3 | |||
92aa3b4a3a | |||
24ffe91b66 | |||
666b4f9cb6 | |||
53cc3f26dd | |||
ec3d1310bf | |||
e986f0fc1d | |||
41e21bac16 | |||
a9b69094cc | |||
47f6025963 | |||
9a2f982d40 | |||
5f95f36214 | |||
ef7ab3e239 | |||
fe96a17551 | |||
35695537bd | |||
cbbe2c3253 | |||
221ca09961 | |||
bebf2a97a4 | |||
b9ea83b1c6 | |||
6bd07b639b | |||
9233b0e339 | |||
b30cbb4d7b | |||
32b547f9fa | |||
3746726245 | |||
6217a984a2 | |||
388827a50e | |||
efdfb5705e | |||
70256a7bfc | |||
b91e697449 | |||
e1dc5ce132 | |||
22a655fd24 | |||
34fc4f1caf | |||
a3a7a00808 | |||
e7863402f3 | |||
2b3c74053e | |||
b92977d8f1 | |||
f046393af8 | |||
b4d48a598a | |||
49995bbc62 |
157 changed files with 10291 additions and 1773 deletions
3
.envrc
3
.envrc
|
@ -1 +1,2 @@
|
||||||
use flake . --impure
|
use flake . --impure
|
||||||
|
export TYPST_ROOT="$(pwd)/docs"
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -2,3 +2,7 @@
|
||||||
.direnv/
|
.direnv/
|
||||||
/target
|
/target
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
*.pdf
|
||||||
|
/docs/*.png
|
||||||
|
/testfiles/gen/*
|
||||||
|
!/testfiles/gen/.gitkeep
|
||||||
|
|
3
.helix/languages.toml
Normal file
3
.helix/languages.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[[language]]
|
||||||
|
name = "ron"
|
||||||
|
file-types = [ "rpl" ]
|
10
CODE_OF_CONDUCT.md
Normal file
10
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
See https://www.rust-lang.org/policies/code-of-conduct.
|
||||||
|
The current maintainers, that is,
|
||||||
|
|
||||||
|
- [@Schrottkatze](https://forge.katzen.cafe/schrottkatze)
|
||||||
|
- [@multisamplednight](https://forge.katzen.cafe/multisamplednight)
|
||||||
|
|
||||||
|
are the entities to email, message or talk to if you feel like any interaction in the context of
|
||||||
|
iOwO is not okay. We'll try to answer as soon as we can.
|
||||||
|
|
||||||
|
Please do **not** open an issue. Notify the maintainers privately instead.
|
120
CONTRIBUTING.md
Normal file
120
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
# Contributing to iOwO
|
||||||
|
|
||||||
|
Before we get started, thank you for thinking about doing so!
|
||||||
|
|
||||||
|
## Through an issue
|
||||||
|
|
||||||
|
- Be excellent to each other. Adhere to the [code of conduct].
|
||||||
|
- About the title: If you had 5 seconds to tell someone the essence of the issue, what would it be?
|
||||||
|
|
||||||
|
### Bugs
|
||||||
|
|
||||||
|
- Write out in detail which steps in which order are necessary to reproduce the bug.
|
||||||
|
- Include environmental information as well, in specific:
|
||||||
|
- How did you install iOwO?
|
||||||
|
- What version of iOwO are you running?
|
||||||
|
- What operating system are you running?
|
||||||
|
In the case of a Linux distro, mention the specific distro and when you last updated as well.
|
||||||
|
- If the bug causes a crash, try to get a backtrace or in worse cases, a coredump.
|
||||||
|
|
||||||
|
### Feature requests
|
||||||
|
|
||||||
|
- Be sure to include a motivation in which case your intended feature would be used
|
||||||
|
even if it seems obvious to you.
|
||||||
|
- Estimate what would be needed to implement the feature:
|
||||||
|
- Is it an addition to the language itself?
|
||||||
|
- Is it just a new command?
|
||||||
|
- Does it ground-breakingly change how iOwO works?
|
||||||
|
|
||||||
|
## Through a PR
|
||||||
|
|
||||||
|
1. Clone the repo.
|
||||||
|
2. Switch to a new appropiately named branch for what you want to do, using `git switch -c`.
|
||||||
|
3. Implement your code changes with your favorite code editor.
|
||||||
|
4. Try them with `cargo run`.
|
||||||
|
5. If there are errors or warnings, go to step 3. Commit occasionally.
|
||||||
|
6. Otherwise,
|
||||||
|
- if you have an account at https://forge.katzen.cafe,
|
||||||
|
1. fork the repo
|
||||||
|
2. set it up as a remote using `git remote add`
|
||||||
|
3. push using `git push @ -u`
|
||||||
|
- if you don't,
|
||||||
|
1. combine your patches using `git diff --patch` and throw them in a file
|
||||||
|
2. send that file to one of the maintainers per email
|
||||||
|
- alongside with a description of what it does
|
||||||
|
3. also mention in the mail that we should consider GitHub and GitLab mirrors,
|
||||||
|
referring to this line
|
||||||
|
|
||||||
|
### Tech stack
|
||||||
|
|
||||||
|
The techstack we operate on is
|
||||||
|
|
||||||
|
- [typst] for documents and concrete proposals
|
||||||
|
- [Rust] for the actual code
|
||||||
|
|
||||||
|
So if you want to contribute functionality, take a look at [The Rust Programming Language book]!
|
||||||
|
If you want to contribute thoughts and technical designs, then consider taking a ride through
|
||||||
|
[typst's excellent tutorial]!
|
||||||
|
|
||||||
|
For creative things, we suggest using whatever **you** are comfortable with.
|
||||||
|
Otherwise, we suggest these:
|
||||||
|
|
||||||
|
- [Inkscape], [GIMP] and [Blender] for promotional material like logos and posters
|
||||||
|
- [Penpot] for layouting prototypes and the like
|
||||||
|
|
||||||
|
[typst]: https://typst.app
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[The Rust Programming Language book]: https://doc.rust-lang.org/book/
|
||||||
|
[typst's excellent tutorial]: https://typst.app/docs/tutorial
|
||||||
|
|
||||||
|
[Inkscape]: https://inkscape.org/
|
||||||
|
[GIMP]: https://www.gimp.org/
|
||||||
|
[Blender]: https://www.blender.org/
|
||||||
|
[Penpot]: https://penpot.app/
|
||||||
|
|
||||||
|
## Politics
|
||||||
|
|
||||||
|
- Current maintainers are defined as the entities listed in the [code of conduct]
|
||||||
|
|
||||||
|
### PRs
|
||||||
|
|
||||||
|
- Every PR requires an approving review from a maintainer (that is not the author) before merge
|
||||||
|
- Maintainers can merge their own PRs
|
||||||
|
- But only after approval
|
||||||
|
|
||||||
|
### Major decisions
|
||||||
|
|
||||||
|
- All current maintainers have to agree **unanimously**.
|
||||||
|
- Agreement must be based on [informed consent].
|
||||||
|
- In effect, a maintainer has to understand what they agree to.
|
||||||
|
|
||||||
|
# Interacting with PRs
|
||||||
|
|
||||||
|
Remember, be respectful.
|
||||||
|
Entities invest their free time and motivation into making these changes,
|
||||||
|
treat them appropiately.
|
||||||
|
|
||||||
|
- Since we mostly work based on forks, [git's remotes] work fairly good.
|
||||||
|
- Replace things in pointy brackets (`<>`) respectively (and remove the pointy brackets).
|
||||||
|
|
||||||
|
## Initial steps for a new contributor or new local checkout
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git remote add <contributor-name> https://forge.katzen.cafe/<contributor-account>/iowo.git
|
||||||
|
git remote update <contributor-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## After setting up the remote
|
||||||
|
|
||||||
|
- You can repeat this step anytime you want to switch branches or update your local checkout.
|
||||||
|
- The PR branch is visible just below the PR title on Forgejo, after the colon (`:`).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git switch <pr-branch>
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[code of conduct]: ./CODE_OF_CONDUCT.md
|
||||||
|
[informed consent]: https://en.wikipedia.org/wiki/Informed_consent
|
||||||
|
[git's remotes]: https://git-scm.com/docs/git-remote
|
1931
Cargo.lock
generated
1931
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
33
Cargo.toml
33
Cargo.toml
|
@ -1,20 +1,31 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "pipeline-lang"
|
members = [
|
||||||
version = "0.1.0"
|
"crates/app",
|
||||||
edition = "2021"
|
"crates/eval",
|
||||||
|
"crates/ir",
|
||||||
|
"crates/lang",
|
||||||
|
"crates/svg-filters",
|
||||||
|
"crates/prowocessing",
|
||||||
|
"crates/executor-poc",
|
||||||
|
"crates/pawarser",
|
||||||
|
"crates/json-pawarser",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
[workspace.dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
petgraph = "0.6.4"
|
||||||
|
|
||||||
[dependencies]
|
# to enable all the lints below, this must be present in a workspace member's Cargo.toml:
|
||||||
logos = "0.13"
|
# [lints]
|
||||||
codespan-reporting = "0.11"
|
# workspace = true
|
||||||
clap = { version = "4.4.8", features = [ "derive" ] }
|
|
||||||
|
|
||||||
[lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "deny"
|
unsafe_code = "deny"
|
||||||
variant_size_differences = "warn"
|
variant_size_differences = "warn"
|
||||||
|
|
||||||
[lints.clippy]
|
[workspace.lints.clippy]
|
||||||
branches_sharing_code = "warn"
|
branches_sharing_code = "warn"
|
||||||
clone_on_ref_ptr = "warn"
|
clone_on_ref_ptr = "warn"
|
||||||
cognitive_complexity = "warn"
|
cognitive_complexity = "warn"
|
||||||
|
|
676
LICENSE
Normal file
676
LICENSE
Normal file
|
@ -0,0 +1,676 @@
|
||||||
|
The main program and its libraries are licensed under the AGPL 3.0
|
||||||
|
as listed below the separator line.
|
||||||
|
|
||||||
|
Media samples in the testfiles folder are licensed under CC BY-SA 4.0.
|
||||||
|
To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
The appropiate authors are given below, in the format of
|
||||||
|
`- file/path: author (link to author)`
|
||||||
|
|
||||||
|
- testfiles/rails.png: Schrottkatze (https://forge.katzen.cafe/schrottkatze)
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
iOwO: media pipeline toolset
|
||||||
|
Copyright (C) 2024 Schrottkatze, MultisampledNight and iOwO contributors
|
||||||
|
|
||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# iOwO
|
||||||
|
|
||||||
|
Tired of remembering if the argument order matters on this ImageMagick command?
|
||||||
|
Tired of having shell parsing interfere with what you actually want to do?
|
||||||
|
|
||||||
|
Fear no more, with iOwO (pronounced iowo (
|
||||||
|
we believe at least (we don't know what we're doing [please help]!)
|
||||||
|
)) you can just write a plain pipeline with [nushell]-ish syntax!
|
||||||
|
Or that's what we want it to be at least.
|
||||||
|
|
||||||
|
So, uh, grab a seat and a beverage, sit down, and maybe take a look around.
|
||||||
|
Although there's not much here — _yet_.
|
||||||
|
|
||||||
|
[please help]: ./CONTRIBUTING.md
|
||||||
|
[nushell]: https://www.nushell.sh/
|
||||||
|
|
23
crates/app/Cargo.toml
Normal file
23
crates/app/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "app"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ariadne = "0.4"
|
||||||
|
clap = { workspace = true, features = [ "derive", "env" ] }
|
||||||
|
dirs = "5"
|
||||||
|
eval = { path = "../eval" }
|
||||||
|
ir = { path = "../ir" }
|
||||||
|
prowocessing = { path = "../prowocessing"}
|
||||||
|
owo-colors = "4"
|
||||||
|
ron = "0.8"
|
||||||
|
serde = { workspace = true, features = [ "derive" ] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
time = { version = "0.3", features = [ "local-offset" ] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
93
crates/app/src/config.rs
Normal file
93
crates/app/src/config.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use self::config_file::{find_config_file, Configs};
|
||||||
|
pub(crate) use cli::CliConfigs;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod config_file;
|
||||||
|
|
||||||
|
/// this struct may hold all configuration
|
||||||
|
pub struct Config {
|
||||||
|
pub evaluator: eval::Available,
|
||||||
|
|
||||||
|
pub startup_msg: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Get the configs from all possible places (args, file, env...)
|
||||||
|
pub fn read(args: &CliConfigs) -> Self {
|
||||||
|
// let config = if let Some(config) = &args.config_path {
|
||||||
|
// Ok(config.clone())
|
||||||
|
// } else {
|
||||||
|
// find_config_file()
|
||||||
|
// };
|
||||||
|
let config = args
|
||||||
|
.config_path
|
||||||
|
.clone()
|
||||||
|
.ok_or(())
|
||||||
|
.or_else(|()| find_config_file());
|
||||||
|
|
||||||
|
// try to read a maybe existing config file
|
||||||
|
let config = config.ok().and_then(|path| {
|
||||||
|
Configs::read(path).map_or_else(
|
||||||
|
|e| {
|
||||||
|
eprintln!("Config error: {e:?}");
|
||||||
|
eprintln!("Proceeding with defaults or cli args...");
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Some,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(file) = config {
|
||||||
|
Self {
|
||||||
|
evaluator: args.evaluator.and(file.evaluator).unwrap_or_default(),
|
||||||
|
// this is negated because to an outward api, the negative is more intuitive,
|
||||||
|
// while in the source the other way around is more intuitive
|
||||||
|
startup_msg: !(args.no_startup_message || file.no_startup_message),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
startup_msg: !args.no_startup_message,
|
||||||
|
evaluator: args.evaluator.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod error {
|
||||||
|
/// Errors that can occur when reading configs
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Config {
|
||||||
|
/// The config dir doesn't exist
|
||||||
|
NoConfigDir,
|
||||||
|
/// We didn't find a config file in the config dir
|
||||||
|
NoConfigFileFound,
|
||||||
|
/// An io error happened while reading/opening it!
|
||||||
|
IoError(std::io::Error),
|
||||||
|
/// The given extension (via an argument) isn't known.
|
||||||
|
///
|
||||||
|
/// Occurs if the extension is neither `.json` nor `.ron` (including if there is no extension, in which case this will be `None`).
|
||||||
|
UnknownExtension(Option<String>),
|
||||||
|
/// Wrapper around an `Error` from `serde_json`
|
||||||
|
SerdeJsonError(serde_json::Error),
|
||||||
|
/// Wrapper around a `SpannedError` from `ron`
|
||||||
|
SerdeRonError(ron::error::SpannedError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Config {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::IoError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Config {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Self::SerdeJsonError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ron::error::SpannedError> for Config {
|
||||||
|
fn from(value: ron::error::SpannedError) -> Self {
|
||||||
|
Self::SerdeRonError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
crates/app/src/config/cli.rs
Normal file
20
crates/app/src/config/cli.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{builder::BoolishValueParser, ArgAction, Args};
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub(crate) struct CliConfigs {
|
||||||
|
/// How to actually run the pipeline.
|
||||||
|
/// Overrides the config file. Defaults to the debug evaluator.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub evaluator: Option<eval::Available>,
|
||||||
|
|
||||||
|
/// Read this config file.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub config_path: Option<PathBuf>,
|
||||||
|
/// Turn off the startup message.
|
||||||
|
///
|
||||||
|
/// The startup message is not constant and depends on the time.
|
||||||
|
#[arg(long, env = "NO_STARTUP_MESSAGE", action = ArgAction::SetTrue, value_parser = BoolishValueParser::new())]
|
||||||
|
pub no_startup_message: bool,
|
||||||
|
}
|
61
crates/app/src/config/config_file.rs
Normal file
61
crates/app/src/config/config_file.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error_reporting::{report_serde_json_err, report_serde_ron_err};
|
||||||
|
|
||||||
|
use super::error::{self, Config};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Configs {
|
||||||
|
#[serde(default = "default_example_value")]
|
||||||
|
pub example_value: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub no_startup_message: bool,
|
||||||
|
pub evaluator: Option<eval::Available>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// what the fuck serde why do i need this
|
||||||
|
fn default_example_value() -> i32 {
|
||||||
|
43
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the location of a config file and check if there is, in fact, a file
|
||||||
|
pub(super) fn find_config_file() -> Result<PathBuf, Config> {
|
||||||
|
let Some(config_path) = dirs::config_dir() else {
|
||||||
|
return Err(Config::NoConfigDir);
|
||||||
|
};
|
||||||
|
|
||||||
|
let ron_path = config_path.with_file_name("config.ron");
|
||||||
|
let json_path = config_path.with_file_name("config.json");
|
||||||
|
|
||||||
|
if Path::new(&ron_path).exists() {
|
||||||
|
Ok(ron_path)
|
||||||
|
} else if Path::new(&json_path).exists() {
|
||||||
|
Ok(json_path)
|
||||||
|
} else {
|
||||||
|
Err(Config::NoConfigFileFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configs {
|
||||||
|
pub fn read(p: PathBuf) -> Result<Self, error::Config> {
|
||||||
|
match p
|
||||||
|
.extension()
|
||||||
|
.map(|v| v.to_str().expect("config path to be UTF-8"))
|
||||||
|
{
|
||||||
|
Some("ron") => {
|
||||||
|
let f = fs::read_to_string(p)?;
|
||||||
|
ron::from_str(&f).or_else(|e| report_serde_ron_err(&f, &e))
|
||||||
|
}
|
||||||
|
Some("json") => {
|
||||||
|
let f = fs::read_to_string(p)?;
|
||||||
|
serde_json::from_str(&f).or_else(|e| report_serde_json_err(&f, &e))
|
||||||
|
}
|
||||||
|
e => Err(error::Config::UnknownExtension(e.map(str::to_owned))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
crates/app/src/error_reporting.rs
Normal file
42
crates/app/src/error_reporting.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use ron::error::Position;
|
||||||
|
|
||||||
|
/// Report an `Error` from the `serde_json` crate
|
||||||
|
pub fn report_serde_json_err(src: &str, err: &serde_json::Error) -> ! {
|
||||||
|
report_serde_err(src, err.line(), err.column(), err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report a `SpannedError` from the `ron` crate
|
||||||
|
pub fn report_serde_ron_err(src: &str, err: &ron::error::SpannedError) -> ! {
|
||||||
|
let Position { line, col } = err.position;
|
||||||
|
report_serde_err(src, line, col, err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic function for reporting serde type of errors
|
||||||
|
fn report_serde_err(src: &str, line: usize, col: usize, msg: String) -> ! {
|
||||||
|
use ariadne::{Label, Report, Source};
|
||||||
|
let offset = try_reconstruct_loc(src, line, col);
|
||||||
|
|
||||||
|
Report::build(ariadne::ReportKind::Error, "test", offset)
|
||||||
|
.with_label(Label::new(("test", offset..offset)).with_message("Something went wrong here!"))
|
||||||
|
.with_message(msg)
|
||||||
|
.with_note("We'd like to give better errors, but serde errors are horrible to work with...")
|
||||||
|
.finish()
|
||||||
|
.eprint(("test", Source::from(src)))
|
||||||
|
.expect("writing error to stderr failed");
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct a byte offset from the line + column numbers typical from serde crates
|
||||||
|
fn try_reconstruct_loc(src: &str, line_nr: usize, col_nr: usize) -> usize {
|
||||||
|
let (line_nr, col_nr) = (line_nr - 1, col_nr - 1);
|
||||||
|
|
||||||
|
src.lines()
|
||||||
|
.enumerate()
|
||||||
|
.fold(0, |acc, (i, line)| match i.cmp(&line_nr) {
|
||||||
|
std::cmp::Ordering::Less => acc + line.len() + 1,
|
||||||
|
std::cmp::Ordering::Equal => acc + col_nr,
|
||||||
|
std::cmp::Ordering::Greater => acc,
|
||||||
|
})
|
||||||
|
}
|
70
crates/app/src/main.rs
Normal file
70
crates/app/src/main.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use config::{CliConfigs, Config};
|
||||||
|
use dev::DevCommands;
|
||||||
|
use welcome_msg::print_startup_msg;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
mod error_reporting;
|
||||||
|
mod welcome_msg;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[command(flatten)]
|
||||||
|
configs: CliConfigs,
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Run {
|
||||||
|
/// What file contains the pipeline to evaluate.
|
||||||
|
source: PathBuf,
|
||||||
|
},
|
||||||
|
Dev {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: DevCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// TODO: proper error handling across the whole function
|
||||||
|
// don't forget to also look inside `Config`
|
||||||
|
let args = Args::parse();
|
||||||
|
let cfg = Config::read(&args.configs);
|
||||||
|
|
||||||
|
if cfg.startup_msg {
|
||||||
|
print_startup_msg();
|
||||||
|
}
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::Run { source } => {
|
||||||
|
let source = fs::read_to_string(source).expect("can't find source file");
|
||||||
|
let ir = ir::from_ron(&source).expect("failed to parse source to graph ir");
|
||||||
|
|
||||||
|
let mut machine = cfg.evaluator.pick();
|
||||||
|
machine.feed(ir);
|
||||||
|
machine.eval_full();
|
||||||
|
}
|
||||||
|
Commands::Dev {
|
||||||
|
command: dev_command,
|
||||||
|
} => dev_command.run(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod dev {
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub(crate) enum DevCommands {}
|
||||||
|
|
||||||
|
impl DevCommands {
|
||||||
|
pub fn run(self) {
|
||||||
|
println!("There are currently no dev commands.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
crates/app/src/welcome_msg.rs
Normal file
16
crates/app/src/welcome_msg.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use time::{Month, OffsetDateTime};
|
||||||
|
|
||||||
|
/// Print the startup message which changes depending on system time.
|
||||||
|
pub fn print_startup_msg() {
|
||||||
|
// now or fallback to utc
|
||||||
|
let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
if now.month() == Month::June {
|
||||||
|
// pride startup message
|
||||||
|
println!("Hello, thanks for using iOwO and happy pride month!");
|
||||||
|
// TODO: proper link with explaination
|
||||||
|
println!("Why pride month is important in {}", now.year());
|
||||||
|
} else {
|
||||||
|
println!("Hello, thanks for using iOwO! :3");
|
||||||
|
}
|
||||||
|
}
|
15
crates/eval/Cargo.toml
Normal file
15
crates/eval/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "eval"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { workspace = true, features = [ "derive" ] }
|
||||||
|
image = "0.24"
|
||||||
|
ir = { path = "../ir" }
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
45
crates/eval/src/kind/debug/instr/mod.rs
Normal file
45
crates/eval/src/kind/debug/instr/mod.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
pub mod read {
|
||||||
|
use image::{io::Reader as ImageReader, DynamicImage};
|
||||||
|
use ir::instruction::read::{Read, SourceType};
|
||||||
|
|
||||||
|
pub fn read(Read { source }: Read) -> DynamicImage {
|
||||||
|
// TODO: actual error handling
|
||||||
|
let img = ImageReader::open(match source {
|
||||||
|
SourceType::File(path) => path,
|
||||||
|
})
|
||||||
|
.expect("something went wrong :(((");
|
||||||
|
|
||||||
|
img.decode().expect("couldn't decode image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod write {
|
||||||
|
use image::{DynamicImage, ImageFormat};
|
||||||
|
use ir::instruction::write::{TargetFormat, TargetType, Write};
|
||||||
|
|
||||||
|
pub fn write(Write { target, format }: Write, input_data: &DynamicImage) {
|
||||||
|
// TODO: actual error handling
|
||||||
|
input_data
|
||||||
|
.save_with_format(
|
||||||
|
match target {
|
||||||
|
TargetType::File(path) => path,
|
||||||
|
},
|
||||||
|
match format {
|
||||||
|
TargetFormat::Jpeg => ImageFormat::Jpeg,
|
||||||
|
TargetFormat::Png => ImageFormat::Png,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("couldn't save file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod filters {
|
||||||
|
pub mod invert {
|
||||||
|
use image::DynamicImage;
|
||||||
|
|
||||||
|
pub fn invert(mut input_data: DynamicImage) -> DynamicImage {
|
||||||
|
input_data.invert();
|
||||||
|
input_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
crates/eval/src/kind/debug/mod.rs
Normal file
105
crates/eval/src/kind/debug/mod.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use ir::{
|
||||||
|
id,
|
||||||
|
instruction::{Filter, Kind},
|
||||||
|
GraphIr, Instruction, Map,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::value::Variant;
|
||||||
|
mod instr;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Evaluator {
|
||||||
|
ir: GraphIr,
|
||||||
|
|
||||||
|
/// What the output of each individual streamer, and as result its output sockets, is.
|
||||||
|
/// Grows larger as evaluation continues,
|
||||||
|
/// as there's no mechanism for purging never-to-be-used-anymore instructions yet.
|
||||||
|
evaluated: Map<id::Output, Variant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Evaluator for Evaluator {
|
||||||
|
fn feed(&mut self, ir: GraphIr) {
|
||||||
|
self.ir = ir;
|
||||||
|
self.evaluated.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_full(&mut self) {
|
||||||
|
// GraphIr::topological_sort returns InstructionRefs, which are mostly cool
|
||||||
|
// but we'd like to have them owned, so we can call Self::step without lifetime hassle
|
||||||
|
let queue: Vec<Instruction> = self
|
||||||
|
.ir
|
||||||
|
.topological_sort()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for instr in queue {
|
||||||
|
self.step(instr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Evaluator {
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn step(&mut self, instr: Instruction) {
|
||||||
|
// what inputs does this instr need? fetch them
|
||||||
|
let inputs: Vec<_> = instr
|
||||||
|
.input_sources()
|
||||||
|
.iter()
|
||||||
|
.map(|source| {
|
||||||
|
let source_socket = source
|
||||||
|
.as_ref()
|
||||||
|
.expect("all inputs to be connected when an instruction is ran");
|
||||||
|
self.evaluated
|
||||||
|
.get(source_socket)
|
||||||
|
.expect("toposort to yield later instrs only after previous ones")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// then actually do whatever the instruction should do
|
||||||
|
// NOTE: makes heavy use of index slicing,
|
||||||
|
// on the basis that ir::instruction::Kind::socket_count is correct
|
||||||
|
// TODO: make this a more flexible dispatch-ish arch
|
||||||
|
let output = match instr.kind {
|
||||||
|
Kind::Read(details) => Some(Variant::Image(instr::read::read(details))),
|
||||||
|
Kind::Write(details) => {
|
||||||
|
#[allow(irrefutable_let_patterns)] // will necessarily change
|
||||||
|
let Variant::Image(input) = inputs[0] else {
|
||||||
|
panic!("cannot only write images, but received: `{:?}`", inputs[0]);
|
||||||
|
};
|
||||||
|
instr::write::write(details, input);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Kind::Math(_) => todo!(),
|
||||||
|
Kind::Blend(_) => todo!(),
|
||||||
|
Kind::Noise(_) => todo!(),
|
||||||
|
Kind::Filter(filter_instruction) => match filter_instruction {
|
||||||
|
Filter::Invert => {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
let Variant::Image(input) = inputs[0] else {
|
||||||
|
panic!(
|
||||||
|
"cannot only filter invert images, but received: `{:?}`",
|
||||||
|
inputs[0]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Variant::Image(instr::filters::invert::invert(
|
||||||
|
input.clone(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(output) = output {
|
||||||
|
// TODO: very inaccurate, actually respect individual instructions.
|
||||||
|
// should be implied by a different arch
|
||||||
|
// TODO: all of those should not be public, offer some methods to get this on
|
||||||
|
// `Instruction` instead (can infer short-term based on Kind::socket_count)
|
||||||
|
let socket = id::Output(id::Socket {
|
||||||
|
belongs_to: instr.id,
|
||||||
|
idx: id::SocketIdx(0),
|
||||||
|
});
|
||||||
|
self.evaluated.insert(socket, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
crates/eval/src/kind/mod.rs
Normal file
1
crates/eval/src/kind/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod debug;
|
43
crates/eval/src/lib.rs
Normal file
43
crates/eval/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use ir::GraphIr;
|
||||||
|
|
||||||
|
mod kind;
|
||||||
|
mod value;
|
||||||
|
|
||||||
|
/// Can collapse a [`GraphIr`] in meaningful ways and do interesting work on it.
|
||||||
|
///
|
||||||
|
/// It's surprisingly difficult to find a fitting description for this.
|
||||||
|
pub trait Evaluator {
|
||||||
|
/// Take some [`GraphIr`] which will then be processed later.
|
||||||
|
/// May be called multiple times, in which the [`GraphIr`]s should add up.
|
||||||
|
// TODO: atm they definitely don't add up -- add some functionality to GraphIr to
|
||||||
|
// make it combine two graphs into one
|
||||||
|
fn feed(&mut self, ir: GraphIr);
|
||||||
|
|
||||||
|
/// Walk through the _whole_ [`GraphIr`] and run through each instruction.
|
||||||
|
fn eval_full(&mut self);
|
||||||
|
|
||||||
|
// TODO: for an LSP or the like, eval_single which starts at a given instr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The available [`Evaluator`]s.
|
||||||
|
///
|
||||||
|
/// Checklist for adding new ones:
|
||||||
|
///
|
||||||
|
/// 1. Create a new module under the [`kind`] module.
|
||||||
|
/// 2. Add a struct and implement [`Evaluator`] for it.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum Available {
|
||||||
|
/// Runs fully on the CPU. Single-threaded, debug-friendly and quick to implement.
|
||||||
|
#[default]
|
||||||
|
Debug,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Available {
|
||||||
|
/// Selects the [`Evaluator`] corresponding to this label.
|
||||||
|
#[must_use]
|
||||||
|
pub fn pick(&self) -> Box<dyn Evaluator> {
|
||||||
|
match self {
|
||||||
|
Self::Debug => Box::<kind::debug::Evaluator>::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
crates/eval/src/value/mod.rs
Normal file
12
crates/eval/src/value/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use image::DynamicImage;
|
||||||
|
|
||||||
|
/// Any runtime value that an instruction can input or output.
|
||||||
|
///
|
||||||
|
/// The name is taken from [Godot's `Variant` type],
|
||||||
|
/// which is very similar to this one.
|
||||||
|
///
|
||||||
|
/// [Godot's `Variant` type]: https://docs.godotengine.org/en/stable/classes/class_variant.html
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Variant {
|
||||||
|
Image(DynamicImage),
|
||||||
|
}
|
13
crates/executor-poc/Cargo.toml
Normal file
13
crates/executor-poc/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "executor-poc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
image = "0.25.1"
|
||||||
|
indexmap = "2.2.6"
|
||||||
|
nalgebra = "0.33.0"
|
||||||
|
petgraph.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
128
crates/executor-poc/src/lib.rs
Normal file
128
crates/executor-poc/src/lib.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use instructions::Instruction;
|
||||||
|
use petgraph::graph::DiGraph;
|
||||||
|
use types::Type;
|
||||||
|
|
||||||
|
trait Node {
|
||||||
|
fn inputs() -> IndexMap<String, Type>;
|
||||||
|
fn outputs() -> IndexMap<String, Type>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NodeGraph {
|
||||||
|
graph: DiGraph<Instruction, TypedEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TypedEdge {
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
typ: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod instructions {
|
||||||
|
//! This is the lowest level of the IR, the one the executor will use.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use indexmap::{indexmap, IndexMap};
|
||||||
|
pub enum Instruction {
|
||||||
|
// File handling
|
||||||
|
LoadFile,
|
||||||
|
SaveFile,
|
||||||
|
|
||||||
|
ColorMatrix,
|
||||||
|
PosMatrix,
|
||||||
|
|
||||||
|
Blend,
|
||||||
|
SplitChannels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Instruction {
|
||||||
|
fn inputs(&self) -> IndexMap<String, Type> {
|
||||||
|
match self {
|
||||||
|
Instruction::LoadFile => indexmap! {
|
||||||
|
"path" => Type::Path
|
||||||
|
},
|
||||||
|
Instruction::SaveFile => indexmap! {
|
||||||
|
"path" => Type::Path
|
||||||
|
},
|
||||||
|
|
||||||
|
Instruction::ColorMatrix => indexmap! {
|
||||||
|
"image" => Type::ImageData,
|
||||||
|
"matrix" => Type::Mat(4,5)
|
||||||
|
},
|
||||||
|
Instruction::PosMatrix => indexmap! {
|
||||||
|
"image" => Type::ImageData,
|
||||||
|
"matrix" => Type::Mat(2, 3),
|
||||||
|
},
|
||||||
|
|
||||||
|
Instruction::Blend => todo!(),
|
||||||
|
Instruction::SplitChannels => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn outputs(&self) -> IndexMap<String, Type> {
|
||||||
|
match self {
|
||||||
|
Instruction::LoadFile => indexmap! {
|
||||||
|
"image" => Type::ImageData
|
||||||
|
},
|
||||||
|
Instruction::SaveFile => indexmap! {},
|
||||||
|
|
||||||
|
Instruction::ColorMatrix => indexmap! {
|
||||||
|
"resut" => Type::ImageData
|
||||||
|
},
|
||||||
|
Instruction::PosMatrix => todo!(),
|
||||||
|
|
||||||
|
Instruction::Blend => todo!(),
|
||||||
|
Instruction::SplitChannels => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod types {
|
||||||
|
pub enum Type {
|
||||||
|
// TODO: later do lower level type system for this stuff?
|
||||||
|
// Image(Size, PixelType),
|
||||||
|
// // image data for processing.
|
||||||
|
// // always PixelType::Rgba32F
|
||||||
|
// ImageData(Size),
|
||||||
|
// // stuff that's still to be generated, not sized and no pixeltype
|
||||||
|
// ProceduralImage,
|
||||||
|
ImageData,
|
||||||
|
Text,
|
||||||
|
Integer,
|
||||||
|
Float,
|
||||||
|
Double,
|
||||||
|
Path,
|
||||||
|
Bool,
|
||||||
|
Vec(
|
||||||
|
// length,
|
||||||
|
u8,
|
||||||
|
),
|
||||||
|
Mat(
|
||||||
|
// Rows
|
||||||
|
u8,
|
||||||
|
// Columns
|
||||||
|
u8,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct Size {
|
||||||
|
// width: u16,
|
||||||
|
// height: u16,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Pixel types. Taken from variants [here](https://docs.rs/image/latest/image/pub enum.DynamicImage.html).
|
||||||
|
// pub enum PixelType {
|
||||||
|
// Luma8,
|
||||||
|
// LumaA8,
|
||||||
|
// Rgb8,
|
||||||
|
// Rgba8,
|
||||||
|
// Luma16,
|
||||||
|
// LumaA16,
|
||||||
|
// Rgb16,
|
||||||
|
// Rgba16,
|
||||||
|
// Rgb32F,
|
||||||
|
// #[default]
|
||||||
|
// Rgba32F,
|
||||||
|
// }
|
||||||
|
}
|
14
crates/ir/Cargo.toml
Normal file
14
crates/ir/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "ir"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
either = "1.9"
|
||||||
|
ron = "0.8"
|
||||||
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
84
crates/ir/src/id.rs
Normal file
84
crates/ir/src/id.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
//! Instance identification for instructions and their glue.
|
||||||
|
//!
|
||||||
|
//! Instructions as defined in [`crate::instruction::Kind`] and descendants are very useful,
|
||||||
|
//! but they cannot be directly used as vertices in the graph IR,
|
||||||
|
//! as there may easily be multiple instructions of the same kind in the same program.
|
||||||
|
//!
|
||||||
|
//! Instead, this module offers an alternative way to refer to specific instances:
|
||||||
|
//!
|
||||||
|
//! - [`Instruction`]s are effectively just a number floating in space,
|
||||||
|
//! incremented each time a new instruction is referred to.
|
||||||
|
//! - [`Socket`]s contain
|
||||||
|
//! - what [`Instruction`] they belong to
|
||||||
|
//! - which index they occupy on it
|
||||||
|
//!
|
||||||
|
//! The distinction between [`Input`] and [`Output`] is implemented
|
||||||
|
//! as them being different types.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// One specific instruction.
|
||||||
|
///
|
||||||
|
/// It does **not** contain what kind of instruction this is.
|
||||||
|
/// Refer to [`crate::instruction::Kind`] for this instead.
|
||||||
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Instruction(pub(super) u64);
|
||||||
|
|
||||||
|
impl fmt::Debug for Instruction {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "InstrId {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On an **instruction**, accepts incoming data.
|
||||||
|
///
|
||||||
|
/// An **instruction** cannot run if any of these are not connected.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Input(pub(super) Socket);
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
#[must_use]
|
||||||
|
pub fn socket(&self) -> &Socket {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On an **instruction**, returns outgoing data to be fed to [`Input`]s.
|
||||||
|
///
|
||||||
|
/// In contrast to [`Input`]s, [`Output`]s may be used or unused.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Output(pub Socket); // TODO: Restrict publicness to super
|
||||||
|
|
||||||
|
impl Output {
|
||||||
|
#[must_use]
|
||||||
|
pub fn socket(&self) -> &Socket {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unspecified socket on a specific **instruction**,
|
||||||
|
/// and where it is on that **instruction**.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Socket {
|
||||||
|
pub belongs_to: Instruction,
|
||||||
|
pub idx: SocketIdx,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a [`Socket`] is on one **instruction**.
|
||||||
|
///
|
||||||
|
/// Note that this does **not** identify a [`Socket`] globally.
|
||||||
|
/// There may be multiple [`Socket`]s sharing the same [`SocketIdx`],
|
||||||
|
/// but on different [`Instruction`]s.
|
||||||
|
///
|
||||||
|
/// This really only serves for denoting where a socket is,
|
||||||
|
/// when it's already clear which instruction is referred to.
|
||||||
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct SocketIdx(pub u16); // TODO: Restrict publicness to super
|
||||||
|
|
||||||
|
impl fmt::Debug for SocketIdx {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
81
crates/ir/src/instruction/mod.rs
Normal file
81
crates/ir/src/instruction/mod.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod read;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Kind {
|
||||||
|
// TODO: `read::Read` and `write::Write` hold real values atm -- they should actually
|
||||||
|
// point to `Const` instructions instead (which are... yet to be done...)
|
||||||
|
Read(read::Read),
|
||||||
|
Write(write::Write),
|
||||||
|
Math(Math),
|
||||||
|
Blend(Blend),
|
||||||
|
Noise(Noise),
|
||||||
|
Filter(Filter),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Math {
|
||||||
|
Add,
|
||||||
|
Subtract,
|
||||||
|
Multiply,
|
||||||
|
Divide,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Blend {
|
||||||
|
Normal,
|
||||||
|
Multiply,
|
||||||
|
Additive,
|
||||||
|
Overlay,
|
||||||
|
Screen,
|
||||||
|
Subtractive,
|
||||||
|
Difference,
|
||||||
|
Darken,
|
||||||
|
Lighten,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Noise {
|
||||||
|
Perlin,
|
||||||
|
Simplex,
|
||||||
|
Voronoi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Filter {
|
||||||
|
Invert,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: given that this basically matches on all instructions, we may need to use
|
||||||
|
// the visitor pattern in future here, or at least get them behind traits
|
||||||
|
// which should allow far more nuanced description
|
||||||
|
impl Kind {
|
||||||
|
/// Returns how many sockets this kind of instruction has.
|
||||||
|
#[must_use]
|
||||||
|
pub fn socket_count(&self) -> SocketCount {
|
||||||
|
match self {
|
||||||
|
Self::Read(_) => (0, 1),
|
||||||
|
Self::Write(_) => (1, 0),
|
||||||
|
Self::Math(_) | Self::Blend(_) => (2, 1),
|
||||||
|
Self::Noise(_) => {
|
||||||
|
todo!("how many arguments does noise take? how many outputs does it have?")
|
||||||
|
}
|
||||||
|
Self::Filter(Filter::Invert) => (1, 1),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many sockets are on an instruction?
|
||||||
|
pub struct SocketCount {
|
||||||
|
pub inputs: u16,
|
||||||
|
pub outputs: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(u16, u16)> for SocketCount {
|
||||||
|
fn from((inputs, outputs): (u16, u16)) -> Self {
|
||||||
|
Self { inputs, outputs }
|
||||||
|
}
|
||||||
|
}
|
12
crates/ir/src/instruction/read.rs
Normal file
12
crates/ir/src/instruction/read.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Read {
|
||||||
|
pub source: SourceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum SourceType {
|
||||||
|
File(PathBuf),
|
||||||
|
}
|
19
crates/ir/src/instruction/write.rs
Normal file
19
crates/ir/src/instruction/write.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Write {
|
||||||
|
pub target: TargetType,
|
||||||
|
pub format: TargetFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum TargetType {
|
||||||
|
File(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum TargetFormat {
|
||||||
|
Jpeg,
|
||||||
|
Png,
|
||||||
|
}
|
353
crates/ir/src/lib.rs
Normal file
353
crates/ir/src/lib.rs
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use instruction::SocketCount;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod id;
|
||||||
|
pub mod instruction;
|
||||||
|
pub mod semi_human;
|
||||||
|
|
||||||
|
pub type Map<K, V> = std::collections::BTreeMap<K, V>;
|
||||||
|
pub type Set<T> = std::collections::BTreeSet<T>;
|
||||||
|
|
||||||
|
/// Gives you a super well typed graph IR for a given human-readable repr.
|
||||||
|
///
|
||||||
|
/// Look at [`semi_human::GraphIr`] and the test files in the repo at `testfiles/`
|
||||||
|
/// to see what the RON should look like.
|
||||||
|
/// No, we don't want you to write out [`GraphIr`] in full by hand.
|
||||||
|
/// That's something for the machines to do.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the parsed source is not a valid human-readable graph IR.
|
||||||
|
pub fn from_ron(source: &str) -> ron::error::SpannedResult<GraphIr> {
|
||||||
|
let human_repr: semi_human::GraphIr = ron::from_str(source)?;
|
||||||
|
Ok(human_repr.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The toplevel representation of a whole pipeline.
|
||||||
|
///
|
||||||
|
/// # DAGs
|
||||||
|
///
|
||||||
|
/// Pipelines may not be fully linear. They may branch out and recombine later on.
|
||||||
|
/// As such, the representation for them which is currently used is a
|
||||||
|
/// [**D**irected **A**cyclic **G**raph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
|
||||||
|
///
|
||||||
|
/// For those who are already familiar with graphs, a DAG is one, except that:
|
||||||
|
///
|
||||||
|
/// - It is **directed**: Edges have a direction they point to.
|
||||||
|
/// In this case, edges point from the outputs of streamers to inputs of consumers.
|
||||||
|
/// - It is **acyclic**: Those directed edges may not form loops.
|
||||||
|
/// In other words, if one follows edges only in their direction, it must be impossible
|
||||||
|
/// to come back to an already visited node.
|
||||||
|
///
|
||||||
|
/// Here, if an edge points from _A_ to _B_ (`A --> B`),
|
||||||
|
/// then _A_ is called a **dependency** or an **input source** of _B_,
|
||||||
|
/// and _B_ is called a **dependent** or an **output target** of _A_.
|
||||||
|
///
|
||||||
|
/// The DAG also enables another neat operation:
|
||||||
|
/// [Topological sorting](https://en.wikipedia.org/wiki/Topological_sorting).
|
||||||
|
/// This allows to put the entire graph into a linear list,
|
||||||
|
/// where it's guaranteed that once a vertex is visited,
|
||||||
|
/// all dependencies of it will have been visited already as well.
|
||||||
|
///
|
||||||
|
/// The representation used here in specific is a bit more complicated,
|
||||||
|
/// since **instructions** directly aren't just connected to one another,
|
||||||
|
/// but their **sockets** are instead.
|
||||||
|
///
|
||||||
|
/// So the vertices of the DAG are the **sockets**
|
||||||
|
/// (which are either [`id::Input`] or [`id::Output`] depending on the direction),
|
||||||
|
/// and each **socket** in turn belongs to an **instruction**.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
/// - If you want to build one from scratch,
|
||||||
|
/// add a few helper methods like
|
||||||
|
/// constructing an empty one,
|
||||||
|
/// adding instructions and
|
||||||
|
/// adding edges
|
||||||
|
/// - If you want to construct one from an existing repr,
|
||||||
|
/// maybe you want to use [`semi_human::GraphIr`].
|
||||||
|
///
|
||||||
|
/// # Storing additional data
|
||||||
|
///
|
||||||
|
/// Chances are the graph IR seems somewhat fit to put metadata in it.
|
||||||
|
/// However, most likely you're interacting in context of some other system,
|
||||||
|
/// and also want to manage and index that data on your own.
|
||||||
|
///
|
||||||
|
/// As such, consider using _secondary_ maps instead.
|
||||||
|
/// That is, store in a data structure _you_ own a mapping
|
||||||
|
/// from [`id`]s
|
||||||
|
/// to whatever data you need.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct GraphIr {
|
||||||
|
/// "Backbone" storage of all **instruction** IDs to
|
||||||
|
/// what **kind of instruction** they are.
|
||||||
|
instructions: Map<id::Instruction, instruction::Kind>,
|
||||||
|
|
||||||
|
/// How the data flows forward. **Dependencies** map to **dependents** here.
|
||||||
|
edges: Map<id::Output, Set<id::Input>>,
|
||||||
|
/// How the data flows backward. **Dependents** map to **dependencies** here.
|
||||||
|
rev_edges: Map<id::Input, id::Output>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this impl block, but actually the whole module, screams for tests
|
||||||
|
impl GraphIr {
|
||||||
|
/// Look "backwards" in the graph,
|
||||||
|
/// and find out what instructions need to be done before this one.
|
||||||
|
/// The input slots are visited in order.
|
||||||
|
///
|
||||||
|
/// - The iterator returns individually [`Some`]`(`[`None`]`)` if the corresponding slot is
|
||||||
|
/// not connected.
|
||||||
|
///
|
||||||
|
/// The same caveats as for [`GraphIr::resolve`] apply.
|
||||||
|
#[must_use]
|
||||||
|
pub fn input_sources(
|
||||||
|
&self,
|
||||||
|
subject: &id::Instruction,
|
||||||
|
) -> Option<impl Iterator<Item = Option<&id::Output>> + '_> {
|
||||||
|
let (subject, kind) = self.instructions.get_key_value(subject)?;
|
||||||
|
let SocketCount { inputs, .. } = kind.socket_count();
|
||||||
|
|
||||||
|
Some((0..inputs).map(|idx| {
|
||||||
|
let input = id::Input(socket(subject, idx));
|
||||||
|
self.rev_edges.get(&input)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look "forwards" in the graph to see what other instructions this instruction feeds into.
|
||||||
|
///
|
||||||
|
/// The output slots represent the top-level iterator,
|
||||||
|
/// and each one's connections are emitted one level below.
|
||||||
|
///
|
||||||
|
/// Just [`Iterator::flatten`] if you are not interested in the slots.
|
||||||
|
///
|
||||||
|
/// The same caveats as for [`GraphIr::resolve`] apply.
|
||||||
|
#[must_use]
|
||||||
|
pub fn output_targets(
|
||||||
|
&self,
|
||||||
|
subject: &id::Instruction,
|
||||||
|
) -> Option<impl Iterator<Item = Option<&Set<id::Input>>> + '_> {
|
||||||
|
let (subject, kind) = self.instructions.get_key_value(subject)?;
|
||||||
|
let SocketCount { outputs, .. } = kind.socket_count();
|
||||||
|
|
||||||
|
Some((0..outputs).map(|idx| {
|
||||||
|
let output = id::Output(socket(subject, idx));
|
||||||
|
self.edges.get(&output)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the instruction corresponding to the given ID.
|
||||||
|
/// Returns [`None`] if there is no such instruction in this graph IR.
|
||||||
|
///
|
||||||
|
/// Theoretically this could be fixed easily at the expense of some memory
|
||||||
|
/// by just incrementing and storing some global counter,
|
||||||
|
/// however, at the moment there's no compelling reason
|
||||||
|
/// to actually have multiple [`GraphIr`]s at one point in time.
|
||||||
|
/// Open an issue if that poses a problem for you.
|
||||||
|
#[must_use]
|
||||||
|
pub fn resolve<'ir>(&'ir self, subject: &id::Instruction) -> Option<InstructionRef<'ir>> {
|
||||||
|
let (id, kind) = self.instructions.get_key_value(subject)?;
|
||||||
|
|
||||||
|
let input_sources = self.input_sources(subject)?.collect();
|
||||||
|
let output_targets = self.output_targets(subject)?.collect();
|
||||||
|
|
||||||
|
Some(InstructionRef {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
input_sources,
|
||||||
|
output_targets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the instruction this input belongs to.
|
||||||
|
///
|
||||||
|
/// The same caveats as for [`GraphIr::resolve`] apply.
|
||||||
|
#[must_use]
|
||||||
|
pub fn owner_of_input<'ir>(&'ir self, input: &id::Input) -> Option<InstructionRef<'ir>> {
|
||||||
|
self.resolve(&input.socket().belongs_to)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the instruction this output belongs to.
|
||||||
|
///
|
||||||
|
/// The same caveats as for [`GraphIr::resolve`] apply.
|
||||||
|
#[must_use]
|
||||||
|
pub fn owner_of_output<'ir>(&'ir self, output: &id::Output) -> Option<InstructionRef<'ir>> {
|
||||||
|
self.resolve(&output.socket().belongs_to)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the order in which the instructions could be visited
|
||||||
|
/// in order to ensure that all dependencies are resolved
|
||||||
|
/// before a vertex is visited.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if there are any cycles in the IR, as it needs to be a DAG.
|
||||||
|
#[must_use]
|
||||||
|
// yes, this function could probably return an iterator and be lazy
|
||||||
|
// no, not today
|
||||||
|
pub fn topological_sort(&self) -> Vec<InstructionRef> {
|
||||||
|
// count how many incoming edges each vertex has
|
||||||
|
let mut nonzero_input_counts: Map<_, NonZeroUsize> =
|
||||||
|
self.rev_edges
|
||||||
|
.iter()
|
||||||
|
.fold(Map::new(), |mut count, (input, _)| {
|
||||||
|
let _ = *count
|
||||||
|
.entry(input.socket().belongs_to.clone())
|
||||||
|
.and_modify(|count| *count = count.saturating_add(1))
|
||||||
|
.or_insert(NonZeroUsize::MIN);
|
||||||
|
count
|
||||||
|
});
|
||||||
|
|
||||||
|
// are there any unconnected ones we could start with?
|
||||||
|
// TODO: experiment if a VecDeque with some ordering fun is digested better by the executor
|
||||||
|
let no_inputs: Vec<_> = {
|
||||||
|
let nonzero: Set<_> = nonzero_input_counts.keys().collect();
|
||||||
|
let all: Set<_> = self.instructions.keys().collect();
|
||||||
|
all.difference(&nonzero).copied().cloned().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// then let's find the order!
|
||||||
|
let mut order = Vec::new();
|
||||||
|
let mut active_queue = no_inputs;
|
||||||
|
|
||||||
|
while let Some(current) = active_queue.pop() {
|
||||||
|
// now that this vertex is visited and resolved,
|
||||||
|
// make sure all dependents notice that
|
||||||
|
|
||||||
|
let dependents = self
|
||||||
|
.output_targets(¤t)
|
||||||
|
.expect("graph to be consistent")
|
||||||
|
.flatten()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
for dependent_input in dependents {
|
||||||
|
let dependent = &dependent_input.socket().belongs_to;
|
||||||
|
|
||||||
|
// how many inputs are connected to this dependent without us?
|
||||||
|
let count = nonzero_input_counts
|
||||||
|
.get_mut(dependent)
|
||||||
|
.expect("connected output must refer to non-zero input");
|
||||||
|
|
||||||
|
let new = NonZeroUsize::new(count.get() - 1);
|
||||||
|
if let Some(new) = new {
|
||||||
|
// aww, still some
|
||||||
|
*count = new;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// none, that means this one is free now! let's throw it onto the active queue then
|
||||||
|
let (now_active, _) = nonzero_input_counts
|
||||||
|
.remove_entry(dependent)
|
||||||
|
.expect("connected output must refer to non-zero input");
|
||||||
|
active_queue.push(now_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if this instruction is "well-fed", that is, has all the inputs it needs,
|
||||||
|
// and if not, panic
|
||||||
|
order.push(self.resolve(¤t).expect("graph to be consistent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
nonzero_input_counts.is_empty(),
|
||||||
|
concat!(
|
||||||
|
"topological sort didn't cover all instructions\n",
|
||||||
|
"either there are unconnected inputs, or there is a cycle\n",
|
||||||
|
"unresolved instructions:\n",
|
||||||
|
"{:#?}"
|
||||||
|
),
|
||||||
|
nonzero_input_counts,
|
||||||
|
);
|
||||||
|
|
||||||
|
order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an [`id::Socket`] a bit more tersely.
|
||||||
|
fn socket(id: &id::Instruction, idx: u16) -> id::Socket {
|
||||||
|
id::Socket {
|
||||||
|
belongs_to: id.clone(),
|
||||||
|
idx: id::SocketIdx(idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A full instruction bundeled in context, with its inputs and outputs.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Instruction {
|
||||||
|
pub id: id::Instruction,
|
||||||
|
pub kind: instruction::Kind,
|
||||||
|
|
||||||
|
// can't have these two public since then a user might corrupt their length
|
||||||
|
input_sources: Vec<Option<id::Output>>,
|
||||||
|
output_targets: Vec<Set<id::Input>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Instruction {
|
||||||
|
/// Where this instruction gets its inputs from.
|
||||||
|
///
|
||||||
|
/// [`None`] means that this input is unfilled,
|
||||||
|
/// and must be filled before the instruction can be ran.
|
||||||
|
#[must_use]
|
||||||
|
pub fn input_sources(&self) -> &[Option<id::Output>] {
|
||||||
|
&self.input_sources
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To whom outputs are sent.
|
||||||
|
#[must_use]
|
||||||
|
pub fn output_targets(&self) -> &[Set<id::Input>] {
|
||||||
|
&self.output_targets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Instruction`], but every single field is borrowed instead.
|
||||||
|
/// See its docs.
|
||||||
|
///
|
||||||
|
/// Use the [`From`] impl to handily convert into an [`Instruction`].
|
||||||
|
/// The other way around is unlikely to be wanted — since you already have an [`Instruction`],
|
||||||
|
/// chances are you just want to take a reference (`&`) of it.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct InstructionRef<'ir> {
|
||||||
|
pub id: &'ir id::Instruction,
|
||||||
|
pub kind: &'ir instruction::Kind,
|
||||||
|
|
||||||
|
input_sources: Vec<Option<&'ir id::Output>>,
|
||||||
|
output_targets: Vec<Option<&'ir Set<id::Input>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ir> InstructionRef<'ir> {
|
||||||
|
/// Where this instruction gets its inputs from.
|
||||||
|
///
|
||||||
|
/// [`None`] means that this input is unfilled,
|
||||||
|
/// and must be filled before the instruction can be ran.
|
||||||
|
#[must_use]
|
||||||
|
pub fn input_sources(&self) -> &[Option<&'ir id::Output>] {
|
||||||
|
&self.input_sources
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To whom outputs are sent.
|
||||||
|
#[must_use]
|
||||||
|
pub fn output_targets(&self) -> &[Option<&'ir Set<id::Input>>] {
|
||||||
|
&self.output_targets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// would love to use ToOwned but Rust has no specialization yet
|
||||||
|
// and it'd hurt a blanket impl of ToOwned otherwise
|
||||||
|
impl From<InstructionRef<'_>> for Instruction {
|
||||||
|
fn from(source: InstructionRef<'_>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: source.id.clone(),
|
||||||
|
kind: source.kind.clone(),
|
||||||
|
input_sources: source
|
||||||
|
.input_sources
|
||||||
|
.into_iter()
|
||||||
|
.map(Option::<&_>::cloned)
|
||||||
|
.collect(),
|
||||||
|
output_targets: source
|
||||||
|
.output_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|outputs| outputs.map(Clone::clone).unwrap_or_default())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
crates/ir/src/semi_human.rs
Normal file
87
crates/ir/src/semi_human.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
//! The midterm solution for source representation, until we've got a nice source frontend.
|
||||||
|
//!
|
||||||
|
//! Sacrifices type expressivity for the sake of typability in [RON] files.
|
||||||
|
//!
|
||||||
|
//! **If you want to construct a graph IR programmatically,
|
||||||
|
//! use [`crate::GraphIr`] directly instead,
|
||||||
|
//! as it gives you more control to specify where your instructions came from.**
|
||||||
|
//!
|
||||||
|
//! [RON]: https://docs.rs/ron/latest/ron/
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{id, instruction, Map, Set};
|
||||||
|
|
||||||
|
/// Semi-human-{read,writ}able [`crate::GraphIr`] with far less useful types.
|
||||||
|
///
|
||||||
|
/// **Do not use this if you want to programatically construct IR.**
|
||||||
|
/// Instead, directly use [`crate::GraphIr`].
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct GraphIr {
|
||||||
|
/// See [`crate::GraphIr::instructions`], just that a simple number is used for the ID instead
|
||||||
|
pub(crate) instructions: Map<u64, instruction::Kind>,
|
||||||
|
/// See [`crate::GraphIr::edges`], the forward edges.
|
||||||
|
/// RON wants you to type the set as if it were a list.
|
||||||
|
pub(crate) edges: Map<Socket, Set<Socket>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||||
|
pub struct Socket {
|
||||||
|
/// ID of the instruction this socket is on.
|
||||||
|
pub(crate) on: u64,
|
||||||
|
pub(crate) idx: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Socket> for id::Socket {
|
||||||
|
fn from(source: Socket) -> Self {
|
||||||
|
Self {
|
||||||
|
belongs_to: id::Instruction(source.on),
|
||||||
|
idx: id::SocketIdx(source.idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GraphIr> for crate::GraphIr {
|
||||||
|
fn from(source: GraphIr) -> Self {
|
||||||
|
let edges = source.edges.clone();
|
||||||
|
Self {
|
||||||
|
instructions: source
|
||||||
|
.instructions
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, kind)| (id::Instruction(id), kind))
|
||||||
|
.collect(),
|
||||||
|
edges: type_edges(source.edges),
|
||||||
|
rev_edges: reverse_and_type_edges(edges),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_edges(edges: Map<Socket, Set<Socket>>) -> Map<id::Output, Set<id::Input>> {
|
||||||
|
edges
|
||||||
|
.into_iter()
|
||||||
|
.map(|(output, inputs)| {
|
||||||
|
let output = id::Output(output.into());
|
||||||
|
let inputs = inputs.into_iter().map(Into::into).map(id::Input).collect();
|
||||||
|
(output, inputs)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reverse_and_type_edges(edges: Map<Socket, Set<Socket>>) -> Map<id::Input, id::Output> {
|
||||||
|
edges
|
||||||
|
.into_iter()
|
||||||
|
.fold(Map::new(), |mut rev_edges, (output, inputs)| {
|
||||||
|
let output = id::Output(output.into());
|
||||||
|
|
||||||
|
for input in inputs {
|
||||||
|
let input = id::Input(input.into());
|
||||||
|
let previous = rev_edges.insert(input, output.clone());
|
||||||
|
if let Some(previous) = previous {
|
||||||
|
// TODO: handle this with a TryFrom impl
|
||||||
|
panic!("two or more outputs referred to the same input {previous:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rev_edges
|
||||||
|
})
|
||||||
|
}
|
13
crates/json-pawarser/Cargo.toml
Normal file
13
crates/json-pawarser/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "json-pawarser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
logos = "0.14.2"
|
||||||
|
enumset = "1.1.3"
|
||||||
|
rowan = "0.15.15"
|
||||||
|
pawarser = { path = "../pawarser" }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
78
crates/json-pawarser/src/grammar.rs
Normal file
78
crates/json-pawarser/src/grammar.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use array::array;
|
||||||
|
use enumset::{enum_set, EnumSet};
|
||||||
|
use pawarser::parser::ParserBuilder;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
syntax_error::SyntaxError,
|
||||||
|
syntax_kind::{lex, SyntaxKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::object::object;
|
||||||
|
|
||||||
|
mod array;
|
||||||
|
mod object;
|
||||||
|
|
||||||
|
pub(crate) type Parser<'src> = pawarser::Parser<'src, SyntaxKind, SyntaxError>;
|
||||||
|
pub(crate) type CompletedMarker = pawarser::CompletedMarker<SyntaxKind, SyntaxError>;
|
||||||
|
|
||||||
|
const BASIC_VALUE_TOKENS: EnumSet<SyntaxKind> =
|
||||||
|
enum_set!(SyntaxKind::BOOL | SyntaxKind::NULL | SyntaxKind::NUMBER | SyntaxKind::STRING);
|
||||||
|
|
||||||
|
pub fn value(p: &mut Parser) -> bool {
|
||||||
|
if BASIC_VALUE_TOKENS.contains(p.current()) {
|
||||||
|
p.do_bump();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
object(p).or_else(|| array(p)).is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
test_utils::{check_parser, gen_checks},
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_lit() {
|
||||||
|
gen_checks! {value;
|
||||||
|
r#""helo world""# => r#"ROOT { STRING "\"helo world\""; }"#,
|
||||||
|
"42" => r#"ROOT { NUMBER "42"; }"#,
|
||||||
|
"null" => r#"ROOT { NULL "null"; }"#,
|
||||||
|
"true" => r#"ROOT { BOOL "true"; }"#,
|
||||||
|
"false" => r#"ROOT { BOOL "false"; }"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_utils {
|
||||||
|
use pawarser::parser::ParserBuilder;
|
||||||
|
|
||||||
|
use crate::syntax_kind::{lex, SyntaxKind};
|
||||||
|
|
||||||
|
use super::Parser;
|
||||||
|
|
||||||
|
macro_rules! gen_checks {
|
||||||
|
($fn_to_test:ident; $($in:literal => $out:literal),+) => {
|
||||||
|
$(crate::grammar::test_utils::check_parser($in, |p| { $fn_to_test(p); }, $out);)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) use gen_checks;
|
||||||
|
|
||||||
|
pub(super) fn check_parser(input: &str, parser_fn: fn(&mut Parser), expected_output: &str) {
|
||||||
|
let toks = lex(input);
|
||||||
|
let mut p: Parser = ParserBuilder::new(toks)
|
||||||
|
.add_meaningless(SyntaxKind::WHITESPACE)
|
||||||
|
.add_meaningless(SyntaxKind::NEWLINE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
parser_fn(&mut p);
|
||||||
|
|
||||||
|
let out = p.finish();
|
||||||
|
|
||||||
|
assert_eq!(format!("{out:?}").trim_end(), expected_output);
|
||||||
|
}
|
||||||
|
}
|
36
crates/json-pawarser/src/grammar/array.rs
Normal file
36
crates/json-pawarser/src/grammar/array.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::{syntax_error::SyntaxError, syntax_kind::SyntaxKind};
|
||||||
|
|
||||||
|
use super::{value, CompletedMarker, Parser};
|
||||||
|
|
||||||
|
pub(super) fn array(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let array_start = p.start("array");
|
||||||
|
|
||||||
|
if !p.eat(SyntaxKind::BRACKET_OPEN) {
|
||||||
|
array_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let el = p.start("arr_el");
|
||||||
|
value(p);
|
||||||
|
el.complete(p, SyntaxKind::ELEMENT);
|
||||||
|
|
||||||
|
while p.at(SyntaxKind::COMMA) {
|
||||||
|
let potential_trailing_comma = p.start("potential_trailing_comma");
|
||||||
|
|
||||||
|
p.eat(SyntaxKind::COMMA);
|
||||||
|
let maybe_el = p.start("arr_el");
|
||||||
|
if !value(p) {
|
||||||
|
maybe_el.abandon(p);
|
||||||
|
potential_trailing_comma.complete(p, SyntaxKind::TRAILING_COMMA);
|
||||||
|
} else {
|
||||||
|
maybe_el.complete(p, SyntaxKind::ELEMENT);
|
||||||
|
potential_trailing_comma.abandon(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(if !p.eat(SyntaxKind::BRACKET_CLOSE) {
|
||||||
|
array_start.error(p, SyntaxError::UnclosedArray)
|
||||||
|
} else {
|
||||||
|
array_start.complete(p, SyntaxKind::ARRAY)
|
||||||
|
})
|
||||||
|
}
|
92
crates/json-pawarser/src/grammar/object.rs
Normal file
92
crates/json-pawarser/src/grammar/object.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::{grammar::value, syntax_error::SyntaxError, syntax_kind::SyntaxKind};
|
||||||
|
|
||||||
|
use super::{CompletedMarker, Parser, BASIC_VALUE_TOKENS};
|
||||||
|
|
||||||
|
pub(super) fn object(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let obj_start = p.start("object");
|
||||||
|
|
||||||
|
if !p.eat(SyntaxKind::BRACE_OPEN) {
|
||||||
|
obj_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
member(p);
|
||||||
|
while p.at(SyntaxKind::COMMA) {
|
||||||
|
// not always an error, later configurable
|
||||||
|
let potential_trailing_comma = p.start("potential_trailing_comma");
|
||||||
|
p.eat(SyntaxKind::COMMA);
|
||||||
|
|
||||||
|
if member(p).is_none() {
|
||||||
|
potential_trailing_comma.complete(p, SyntaxKind::TRAILING_COMMA);
|
||||||
|
} else {
|
||||||
|
potential_trailing_comma.abandon(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(if p.eat(SyntaxKind::BRACE_CLOSE) {
|
||||||
|
obj_start.complete(p, SyntaxKind::OBJECT)
|
||||||
|
} else {
|
||||||
|
obj_start.error(p, SyntaxError::UnclosedObject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn member(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let member_start = p.start("member");
|
||||||
|
|
||||||
|
if p.at(SyntaxKind::BRACE_CLOSE) {
|
||||||
|
member_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
} else if p.at(SyntaxKind::STRING) {
|
||||||
|
let member_name_start = p.start("member_name");
|
||||||
|
p.eat(SyntaxKind::STRING);
|
||||||
|
member_name_start.complete(p, SyntaxKind::MEMBER_NAME);
|
||||||
|
} else {
|
||||||
|
return todo!("handle other tokens: {:?}", p.current());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.eat(SyntaxKind::COLON) {
|
||||||
|
todo!("handle wrong tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
let member_value_start = p.start("member_value_start");
|
||||||
|
if value(p) {
|
||||||
|
member_value_start.complete(p, SyntaxKind::MEMBER_VALUE);
|
||||||
|
Some(member_start.complete(p, SyntaxKind::MEMBER))
|
||||||
|
} else {
|
||||||
|
member_value_start.abandon(p);
|
||||||
|
let e = member_start.error(p, SyntaxError::MemberMissingValue);
|
||||||
|
Some(
|
||||||
|
e.precede(p, "member but failed already")
|
||||||
|
.complete(p, SyntaxKind::MEMBER),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::grammar::{
|
||||||
|
object::{member, object},
|
||||||
|
test_utils::gen_checks,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn object_basic() {
|
||||||
|
gen_checks! {object;
|
||||||
|
r#"{"a": "b"}"# => r#"ROOT { OBJECT { BRACE_OPEN "{"; MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; WHITESPACE " "; MEMBER_VALUE { STRING "\"b\""; } } BRACE_CLOSE "}"; } }"#,
|
||||||
|
r#"{"a": 42}"# => r#"ROOT { OBJECT { BRACE_OPEN "{"; MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; WHITESPACE " "; MEMBER_VALUE { NUMBER "42"; } } BRACE_CLOSE "}"; } }"#,
|
||||||
|
r#"{"a": "b""# => r#"ROOT { PARSE_ERR: UnclosedObject { BRACE_OPEN "{"; MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; WHITESPACE " "; MEMBER_VALUE { STRING "\"b\""; } } } }"#,
|
||||||
|
r#"{"a": }"# => r#"ROOT { OBJECT { BRACE_OPEN "{"; MEMBER { PARSE_ERR: MemberMissingValue { MEMBER_NAME { STRING "\"a\""; } COLON ":"; } } WHITESPACE " "; BRACE_CLOSE "}"; } }"#,
|
||||||
|
r#"{"a":"# => r#"ROOT { PARSE_ERR: UnclosedObject { BRACE_OPEN "{"; MEMBER { PARSE_ERR: MemberMissingValue { MEMBER_NAME { STRING "\"a\""; } COLON ":"; } } } }"#,
|
||||||
|
r#"{"a":true,}"# => r#"ROOT { OBJECT { BRACE_OPEN "{"; MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; MEMBER_VALUE { BOOL "true"; } } TRAILING_COMMA { COMMA ","; } BRACE_CLOSE "}"; } }"#
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn member_basic() {
|
||||||
|
gen_checks! {member;
|
||||||
|
r#""a": "b""# => r#"ROOT { MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; WHITESPACE " "; MEMBER_VALUE { STRING "\"b\""; } } }"#,
|
||||||
|
r#""a": 42"# => r#"ROOT { MEMBER { MEMBER_NAME { STRING "\"a\""; } COLON ":"; WHITESPACE " "; MEMBER_VALUE { NUMBER "42"; } } }"#,
|
||||||
|
r#""a":"# => r#"ROOT { MEMBER { PARSE_ERR: MemberMissingValue { MEMBER_NAME { STRING "\"a\""; } COLON ":"; } } }"#
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
crates/json-pawarser/src/lib.rs
Normal file
3
crates/json-pawarser/src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod grammar;
|
||||||
|
mod syntax_error;
|
||||||
|
mod syntax_kind;
|
11
crates/json-pawarser/src/syntax_error.rs
Normal file
11
crates/json-pawarser/src/syntax_error.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::syntax_kind::SyntaxKind;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SyntaxError {
|
||||||
|
UnclosedObject,
|
||||||
|
UnclosedArray,
|
||||||
|
DisallowedKeyType(SyntaxKind),
|
||||||
|
MemberMissingValue,
|
||||||
|
UnexpectedTrailingComma,
|
||||||
|
}
|
||||||
|
impl pawarser::parser::SyntaxError for SyntaxError {}
|
117
crates/json-pawarser/src/syntax_kind.rs
Normal file
117
crates/json-pawarser/src/syntax_kind.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use logos::Logos;
|
||||||
|
|
||||||
|
pub fn lex(src: &str) -> Vec<(SyntaxKind, &str)> {
|
||||||
|
let mut lex = SyntaxKind::lexer(src);
|
||||||
|
let mut r = Vec::new();
|
||||||
|
|
||||||
|
while let Some(tok_res) = lex.next() {
|
||||||
|
r.push((tok_res.unwrap_or(SyntaxKind::LEX_ERR), lex.slice()))
|
||||||
|
}
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(enumset::EnumSetType, Debug, Logos, PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
|
#[repr(u16)]
|
||||||
|
#[enumset(no_super_impls)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum SyntaxKind {
|
||||||
|
OBJECT,
|
||||||
|
MEMBER,
|
||||||
|
MEMBER_NAME,
|
||||||
|
MEMBER_VALUE,
|
||||||
|
|
||||||
|
ARRAY,
|
||||||
|
ELEMENT,
|
||||||
|
|
||||||
|
// SyntaxKinds for future json5/etc support
|
||||||
|
TRAILING_COMMA,
|
||||||
|
|
||||||
|
// Tokens
|
||||||
|
// Regexes adapted from [the logos handbook](https://logos.maciej.codes/examples/json_borrowed.html)
|
||||||
|
#[token("true")]
|
||||||
|
#[token("false")]
|
||||||
|
BOOL,
|
||||||
|
#[token("{")]
|
||||||
|
BRACE_OPEN,
|
||||||
|
#[token("}")]
|
||||||
|
BRACE_CLOSE,
|
||||||
|
#[token("[")]
|
||||||
|
BRACKET_OPEN,
|
||||||
|
#[token("]")]
|
||||||
|
BRACKET_CLOSE,
|
||||||
|
#[token(":")]
|
||||||
|
COLON,
|
||||||
|
#[token(",")]
|
||||||
|
COMMA,
|
||||||
|
#[token("null")]
|
||||||
|
NULL,
|
||||||
|
#[regex(r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?")]
|
||||||
|
NUMBER,
|
||||||
|
#[regex(r#""([^"\\]|\\["\\bnfrt]|u[a-fA-F0-9]{4})*""#)]
|
||||||
|
STRING,
|
||||||
|
|
||||||
|
// Whitespace tokens
|
||||||
|
#[regex("[ \\t\\f]+")]
|
||||||
|
WHITESPACE,
|
||||||
|
#[token("\n")]
|
||||||
|
NEWLINE,
|
||||||
|
|
||||||
|
// Error SyntaxKinds
|
||||||
|
LEX_ERR,
|
||||||
|
PARSE_ERR,
|
||||||
|
|
||||||
|
// Meta SyntaxKinds
|
||||||
|
ROOT,
|
||||||
|
EOF,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl pawarser::parser::SyntaxElement for SyntaxKind {
|
||||||
|
const SYNTAX_EOF: Self = Self::EOF;
|
||||||
|
|
||||||
|
const SYNTAX_ERROR: Self = Self::PARSE_ERR;
|
||||||
|
const SYNTAX_ROOT: Self = Self::ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SyntaxKind> for rowan::SyntaxKind {
|
||||||
|
fn from(kind: SyntaxKind) -> Self {
|
||||||
|
Self(kind as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rowan::SyntaxKind> for SyntaxKind {
|
||||||
|
fn from(raw: rowan::SyntaxKind) -> Self {
|
||||||
|
assert!(raw.0 <= SyntaxKind::EOF as u16);
|
||||||
|
#[allow(unsafe_code, reason = "The transmute is necessary here")]
|
||||||
|
unsafe {
|
||||||
|
std::mem::transmute::<u16, SyntaxKind>(raw.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::syntax_kind::{lex, SyntaxKind};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_object() {
|
||||||
|
const TEST_DATA: &str = r#"{"hello_world": "meow", "some_num":7.42}"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dbg!(lex(TEST_DATA)),
|
||||||
|
vec![
|
||||||
|
(SyntaxKind::BRACE_OPEN, "{"),
|
||||||
|
(SyntaxKind::STRING, "\"hello_world\""),
|
||||||
|
(SyntaxKind::COLON, ":"),
|
||||||
|
(SyntaxKind::WHITESPACE, " "),
|
||||||
|
(SyntaxKind::STRING, "\"meow\""),
|
||||||
|
(SyntaxKind::COMMA, ","),
|
||||||
|
(SyntaxKind::WHITESPACE, " "),
|
||||||
|
(SyntaxKind::STRING, "\"some_num\""),
|
||||||
|
(SyntaxKind::COLON, ":"),
|
||||||
|
(SyntaxKind::NUMBER, "7.42"),
|
||||||
|
(SyntaxKind::BRACE_CLOSE, "}")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
crates/lang/Cargo.toml
Normal file
25
crates/lang/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "lang"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
logos = "0.14"
|
||||||
|
petgraph = { workspace = true}
|
||||||
|
indexmap = "2.2.6"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
ariadne = "0.4.0"
|
||||||
|
ego-tree = "0.6.2"
|
||||||
|
rowan = "0.15.15"
|
||||||
|
drop_bomb = "0.1.5"
|
||||||
|
enumset = "1.1.3"
|
||||||
|
indoc = "2"
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
crossbeam = "0.8.4"
|
||||||
|
owo-colors = {version = "4", features = ["supports-colors"]}
|
||||||
|
strip-ansi-escapes = "0.2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
80
crates/lang/src/ast.rs
Normal file
80
crates/lang/src/ast.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind::*;
|
||||||
|
use crate::SyntaxNode;
|
||||||
|
use rowan::Language;
|
||||||
|
|
||||||
|
// Heavily modified version of https://github.com/rust-analyzer/rowan/blob/e2d2e93e16c5104b136d0bc738a0d48346922200/examples/s_expressions.rs#L250-L266
|
||||||
|
macro_rules! ast_nodes {
|
||||||
|
($($ast:ident, $kind:ident);+) => {
|
||||||
|
$(
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct $ast(SyntaxNode);
|
||||||
|
impl rowan::ast::AstNode for $ast {
|
||||||
|
type Language = crate::Lang;
|
||||||
|
|
||||||
|
fn can_cast(kind: <Self::Language as Language>::Kind) -> bool {
|
||||||
|
kind == $kind
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cast(node: SyntaxNode) -> Option<Self> {
|
||||||
|
if node.kind() == $kind {
|
||||||
|
Some(Self(node))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax(&self) -> &SyntaxNode {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ast_nodes!(
|
||||||
|
Def, DEF;
|
||||||
|
DefName, DEF_NAME;
|
||||||
|
DefBody, DEF_BODY;
|
||||||
|
|
||||||
|
Mod, MODULE;
|
||||||
|
ModName, MODULE_NAME;
|
||||||
|
ModBody, MODULE_BODY;
|
||||||
|
|
||||||
|
Use, USE;
|
||||||
|
UsePat, USE_PAT;
|
||||||
|
PatItem, PAT_ITEM;
|
||||||
|
PatGlob, PAT_GLOB;
|
||||||
|
PatGroup, PAT_GROUP;
|
||||||
|
|
||||||
|
Literal, LITERAL;
|
||||||
|
IntLit, INT_NUM;
|
||||||
|
FloatLit, FLOAT_NUM;
|
||||||
|
StringLit, STRING;
|
||||||
|
|
||||||
|
Matrix, MATRIX;
|
||||||
|
MatrixRow, MAT_ROW;
|
||||||
|
Vector, VEC;
|
||||||
|
List, LIST;
|
||||||
|
CollectionItem, COLLECTION_ITEM;
|
||||||
|
|
||||||
|
ParenthesizedExpr, PARENTHESIZED_EXPR;
|
||||||
|
Expression, EXPR;
|
||||||
|
|
||||||
|
Pipeline, PIPELINE;
|
||||||
|
|
||||||
|
Instruction, INSTR;
|
||||||
|
InstructionName, INSTR_NAME;
|
||||||
|
InstructionParams, INSTR_PARAMS;
|
||||||
|
|
||||||
|
AttributeSet, ATTR_SET;
|
||||||
|
Attribute, ATTR;
|
||||||
|
AttributeName, ATTR_NAME;
|
||||||
|
AttributeValue, ATTR_VALUE;
|
||||||
|
|
||||||
|
ParseError, PARSE_ERR;
|
||||||
|
LexError, LEX_ERR;
|
||||||
|
|
||||||
|
Root, ROOT;
|
||||||
|
Eof, EOF
|
||||||
|
);
|
25
crates/lang/src/lib.rs
Normal file
25
crates/lang/src/lib.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#![feature(type_alias_impl_trait, lint_reasons, box_into_inner)]
|
||||||
|
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind;
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
pub mod lst_parser;
|
||||||
|
pub mod world;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Lang {}
|
||||||
|
impl rowan::Language for Lang {
|
||||||
|
type Kind = SyntaxKind;
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
|
||||||
|
assert!(raw.0 <= SyntaxKind::ROOT as u16);
|
||||||
|
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
|
||||||
|
}
|
||||||
|
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
|
||||||
|
kind.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SyntaxNode = rowan::SyntaxNode<Lang>;
|
||||||
|
pub type SyntaxToken = rowan::SyntaxNode<Lang>;
|
||||||
|
pub type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
|
169
crates/lang/src/lst_parser.rs
Normal file
169
crates/lang/src/lst_parser.rs
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use drop_bomb::DropBomb;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
error::SyntaxError,
|
||||||
|
events::{Event, NodeKind},
|
||||||
|
input::Input,
|
||||||
|
syntax_kind::SyntaxKind,
|
||||||
|
};
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
pub mod syntax_kind;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod events;
|
||||||
|
pub mod grammar;
|
||||||
|
pub mod input;
|
||||||
|
pub mod output;
|
||||||
|
|
||||||
|
const PARSER_STEP_LIMIT: u32 = 4096;
|
||||||
|
|
||||||
|
pub struct Parser<'src, 'toks> {
|
||||||
|
input: Input<'src, 'toks>,
|
||||||
|
pos: usize,
|
||||||
|
events: Vec<Event>,
|
||||||
|
steps: Cell<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, 'toks> Parser<'src, 'toks> {
|
||||||
|
pub fn new(input: Input<'src, 'toks>) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
pos: 0,
|
||||||
|
events: Vec::new(),
|
||||||
|
steps: Cell::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> Vec<Event> {
|
||||||
|
self.events
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn nth(&self, n: usize) -> SyntaxKind {
|
||||||
|
self.step();
|
||||||
|
self.input.kind(self.pos + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eat_succeeding_ws(&mut self) {
|
||||||
|
self.push_ev(Event::Eat {
|
||||||
|
count: self.input.meaningless_tail_len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn current(&self) -> SyntaxKind {
|
||||||
|
self.step();
|
||||||
|
self.input.kind(self.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start(&mut self, name: &str) -> Marker {
|
||||||
|
let pos = self.events.len();
|
||||||
|
self.push_ev(Event::tombstone());
|
||||||
|
Marker::new(pos, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn at(&self, kind: SyntaxKind) -> bool {
|
||||||
|
self.nth_at(0, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn eat(&mut self, kind: SyntaxKind) -> bool {
|
||||||
|
if !self.at(kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.do_bump();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn nth_at(&self, n: usize, kind: SyntaxKind) -> bool {
|
||||||
|
self.nth(n) == kind
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_bump(&mut self) {
|
||||||
|
self.push_ev(Event::Eat {
|
||||||
|
count: self.input.preceding_meaningless(self.pos),
|
||||||
|
});
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_ev(&mut self, event: Event) {
|
||||||
|
self.events.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&self) {
|
||||||
|
let steps = self.steps.get();
|
||||||
|
assert!(steps <= PARSER_STEP_LIMIT, "the parser seems stuck...");
|
||||||
|
self.steps.set(steps + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Marker {
|
||||||
|
pos: usize,
|
||||||
|
bomb: DropBomb,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Marker {
|
||||||
|
pub(crate) fn new(pos: usize, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
bomb: DropBomb::new(format!("Marker {name} must be completed or abandoned")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_node(mut self, p: &mut Parser, kind: NodeKind) -> CompletedMarker {
|
||||||
|
self.bomb.defuse();
|
||||||
|
match &mut p.events[self.pos] {
|
||||||
|
Event::Start { kind: slot, .. } => *slot = kind.clone(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push_ev(Event::Finish);
|
||||||
|
|
||||||
|
CompletedMarker {
|
||||||
|
pos: self.pos,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn complete(self, p: &mut Parser<'_, '_>, kind: SyntaxKind) -> CompletedMarker {
|
||||||
|
self.close_node(p, NodeKind::Syntax(kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn error(self, p: &mut Parser, kind: SyntaxError) -> CompletedMarker {
|
||||||
|
self.close_node(p, NodeKind::Error(kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn abandon(mut self, p: &mut Parser<'_, '_>) {
|
||||||
|
self.bomb.defuse();
|
||||||
|
if self.pos == p.events.len() - 1 {
|
||||||
|
match p.events.pop() {
|
||||||
|
Some(Event::Start {
|
||||||
|
kind: NodeKind::Syntax(SyntaxKind::TOMBSTONE),
|
||||||
|
forward_parent: None,
|
||||||
|
}) => (),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct CompletedMarker {
|
||||||
|
pos: usize,
|
||||||
|
kind: NodeKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletedMarker {
|
||||||
|
pub(crate) fn precede(self, p: &mut Parser<'_, '_>, name: &str) -> Marker {
|
||||||
|
let new_pos = p.start(name);
|
||||||
|
|
||||||
|
match &mut p.events[self.pos] {
|
||||||
|
Event::Start { forward_parent, .. } => {
|
||||||
|
*forward_parent = Some(new_pos.pos - self.pos);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
new_pos
|
||||||
|
}
|
||||||
|
}
|
15
crates/lang/src/lst_parser/error.rs
Normal file
15
crates/lang/src/lst_parser/error.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum SyntaxError {
|
||||||
|
Expected(Vec<SyntaxKind>),
|
||||||
|
PipelineNeedsSink,
|
||||||
|
// if there was two space seperated items in a list
|
||||||
|
SpaceSepInList,
|
||||||
|
SemicolonInList,
|
||||||
|
CommaInMatOrVec,
|
||||||
|
UnterminatedTopLevelItem,
|
||||||
|
UnclosedModuleBody,
|
||||||
|
UnfinishedPath,
|
||||||
|
PathSepContainsSemicolon,
|
||||||
|
}
|
70
crates/lang/src/lst_parser/events.rs
Normal file
70
crates/lang/src/lst_parser/events.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind;
|
||||||
|
|
||||||
|
use super::error::SyntaxError;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
Start {
|
||||||
|
kind: NodeKind,
|
||||||
|
forward_parent: Option<usize>,
|
||||||
|
},
|
||||||
|
Finish,
|
||||||
|
Eat {
|
||||||
|
count: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum NodeKind {
|
||||||
|
Syntax(SyntaxKind),
|
||||||
|
Error(SyntaxError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeKind {
|
||||||
|
pub fn is_syntax(&self) -> bool {
|
||||||
|
matches!(self, Self::Syntax(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
matches!(self, Self::Error(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SyntaxKind> for NodeKind {
|
||||||
|
fn from(value: SyntaxKind) -> Self {
|
||||||
|
NodeKind::Syntax(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SyntaxError> for NodeKind {
|
||||||
|
fn from(value: SyntaxError) -> Self {
|
||||||
|
NodeKind::Error(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<SyntaxKind> for NodeKind {
|
||||||
|
fn eq(&self, other: &SyntaxKind) -> bool {
|
||||||
|
match self {
|
||||||
|
NodeKind::Syntax(s) => s == other,
|
||||||
|
NodeKind::Error(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<SyntaxError> for NodeKind {
|
||||||
|
fn eq(&self, other: &SyntaxError) -> bool {
|
||||||
|
match self {
|
||||||
|
NodeKind::Syntax(_) => false,
|
||||||
|
NodeKind::Error(e) => e == other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub(crate) fn tombstone() -> Self {
|
||||||
|
Self::Start {
|
||||||
|
kind: SyntaxKind::TOMBSTONE.into(),
|
||||||
|
forward_parent: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
crates/lang/src/lst_parser/grammar.rs
Normal file
38
crates/lang/src/lst_parser/grammar.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind::*;
|
||||||
|
|
||||||
|
use self::module::{mod_body, top_level_item};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
input::Input,
|
||||||
|
output::Output,
|
||||||
|
syntax_kind::{self, lex},
|
||||||
|
Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod expression;
|
||||||
|
mod module;
|
||||||
|
|
||||||
|
pub fn source_file(p: &mut Parser) {
|
||||||
|
let root = p.start("root");
|
||||||
|
|
||||||
|
mod_body(p);
|
||||||
|
// expression::expression(p, false);
|
||||||
|
p.eat_succeeding_ws();
|
||||||
|
|
||||||
|
root.complete(p, ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_parser(input: &str, parser_fn: fn(&mut Parser), output: &str) {
|
||||||
|
let toks = lex(input);
|
||||||
|
let mut parser = Parser::new(Input::new(&toks));
|
||||||
|
|
||||||
|
parser_fn(&mut parser);
|
||||||
|
|
||||||
|
let p_out = dbg!(parser.finish());
|
||||||
|
let o = Output::from_parser_output(toks, p_out);
|
||||||
|
|
||||||
|
let s = strip_ansi_escapes::strip_str(format!("{o:?}"));
|
||||||
|
assert_eq!(&s, output);
|
||||||
|
}
|
44
crates/lang/src/lst_parser/grammar/expression.rs
Normal file
44
crates/lang/src/lst_parser/grammar/expression.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use crate::lst_parser::{error::SyntaxError, syntax_kind::SyntaxKind::*, CompletedMarker, Parser};
|
||||||
|
|
||||||
|
use self::{collection::collection, instruction::instr, lit::literal, pipeline::PIPES};
|
||||||
|
|
||||||
|
mod collection;
|
||||||
|
mod instruction;
|
||||||
|
mod lit;
|
||||||
|
mod pipeline;
|
||||||
|
|
||||||
|
pub fn expression(p: &mut Parser, in_pipe: bool) -> Option<CompletedMarker> {
|
||||||
|
let expr = p.start("expr");
|
||||||
|
|
||||||
|
if atom(p).or_else(|| instr(p)).is_none() {
|
||||||
|
expr.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = expr.complete(p, EXPR);
|
||||||
|
|
||||||
|
if PIPES.contains(p.current()) && !in_pipe {
|
||||||
|
pipeline::pipeline(p, r)
|
||||||
|
} else {
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn atom(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
literal(p)
|
||||||
|
.or_else(|| collection(p))
|
||||||
|
.or_else(|| parenthesized_expr(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parenthesized_expr(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if p.eat(L_PAREN) {
|
||||||
|
let par_expr = p.start("parenthesized");
|
||||||
|
expression(p, false);
|
||||||
|
if !p.eat(R_PAREN) {
|
||||||
|
return Some(par_expr.error(p, SyntaxError::Expected(vec![R_PAREN])));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(par_expr.complete(p, PARENTHESIZED_EXPR));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
25
crates/lang/src/lst_parser/grammar/expression/collection.rs
Normal file
25
crates/lang/src/lst_parser/grammar/expression/collection.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use enumset::enum_set;
|
||||||
|
|
||||||
|
use crate::lst_parser::{
|
||||||
|
syntax_kind::{SyntaxKind::*, TokenSet},
|
||||||
|
CompletedMarker, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::{attr_set::attr_set, vec::vec_matrix_list};
|
||||||
|
|
||||||
|
mod attr_set;
|
||||||
|
mod vec;
|
||||||
|
|
||||||
|
const COLLECTION_START: TokenSet = enum_set!(L_BRACK | L_BRACE);
|
||||||
|
|
||||||
|
pub fn collection(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if !COLLECTION_START.contains(p.current()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(match p.current() {
|
||||||
|
L_BRACK => vec_matrix_list(p),
|
||||||
|
L_BRACE => attr_set(p),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use crate::lst_parser::{
|
||||||
|
error::SyntaxError,
|
||||||
|
grammar::expression::{atom, expression},
|
||||||
|
CompletedMarker, Marker, Parser,
|
||||||
|
SyntaxKind::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn attr_set(p: &mut Parser) -> CompletedMarker {
|
||||||
|
let start = p.start("attr_set_start");
|
||||||
|
assert!(p.eat(L_BRACE));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if attr(p).is_some() {
|
||||||
|
// TODO: handle others
|
||||||
|
if p.eat(COMMA) {
|
||||||
|
continue;
|
||||||
|
} else if p.eat(R_BRACE) {
|
||||||
|
return start.complete(p, ATTR_SET);
|
||||||
|
}
|
||||||
|
// TODO: check for newline and stuff following that for recov of others
|
||||||
|
} else if p.eat(R_BRACE) {
|
||||||
|
return start.complete(p, ATTR_SET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if p.at(IDENT) {
|
||||||
|
let attr_start = p.start("attr");
|
||||||
|
let attr_name_start = p.start("attr_name");
|
||||||
|
p.do_bump();
|
||||||
|
attr_name_start.complete(p, ATTR_NAME);
|
||||||
|
|
||||||
|
// TODO: handle comma, expr/atom, other
|
||||||
|
p.eat(COLON);
|
||||||
|
|
||||||
|
// TODO: handle failed expr parser too
|
||||||
|
let attr_value = p.start("attr_value");
|
||||||
|
let _ = expression(p, false);
|
||||||
|
attr_value.complete(p, ATTR_VALUE);
|
||||||
|
Some(attr_start.complete(p, ATTR))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::lst_parser::{
|
||||||
|
error::SyntaxError, grammar::expression::atom, CompletedMarker, Marker, Parser, SyntaxKind::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn vec_matrix_list(p: &mut Parser) -> CompletedMarker {
|
||||||
|
let start = p.start("vec_matrix_list_start");
|
||||||
|
assert!(p.eat(L_BRACK));
|
||||||
|
let row_start = p.start("matrix_row_start");
|
||||||
|
if let Some(item) = atom(p) {
|
||||||
|
item.precede(p, "coll_item_start")
|
||||||
|
.complete(p, COLLECTION_ITEM);
|
||||||
|
|
||||||
|
if p.at(COMMA) {
|
||||||
|
row_start.abandon(p);
|
||||||
|
return finish_list(p, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
finish_mat_or_vec(p, start, row_start)
|
||||||
|
} else if p.eat(R_BRACK) {
|
||||||
|
row_start.abandon(p);
|
||||||
|
start.complete(p, LIST)
|
||||||
|
} else {
|
||||||
|
row_start.abandon(p);
|
||||||
|
start.error(p, SyntaxError::Expected(vec![EXPR, R_BRACK]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_list(p: &mut Parser, list_start: Marker) -> CompletedMarker {
|
||||||
|
loop {
|
||||||
|
if p.eat(COMMA) {
|
||||||
|
if let Some(item) = atom(p) {
|
||||||
|
item.precede(p, "coll_item_start")
|
||||||
|
.complete(p, COLLECTION_ITEM);
|
||||||
|
} else if p.eat(R_BRACK) {
|
||||||
|
return list_start.complete(p, LIST);
|
||||||
|
}
|
||||||
|
} else if p.eat(R_BRACK) {
|
||||||
|
return list_start.complete(p, LIST);
|
||||||
|
} else if let Some(item) = atom(p) {
|
||||||
|
item.precede(p, "next_item")
|
||||||
|
.complete(p, COLLECTION_ITEM)
|
||||||
|
.precede(p, "err_space_sep")
|
||||||
|
.error(p, SyntaxError::SpaceSepInList);
|
||||||
|
} else if p.at(SEMICOLON) {
|
||||||
|
let semi_err = p.start("semicolon_err");
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
semi_err.error(p, SyntaxError::SemicolonInList);
|
||||||
|
if let Some(item) = atom(p) {
|
||||||
|
item.precede(p, "coll_item_start")
|
||||||
|
.complete(p, COLLECTION_ITEM);
|
||||||
|
} else if p.eat(R_BRACK) {
|
||||||
|
return list_start.complete(p, LIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle commas, general other wrong toks
|
||||||
|
fn finish_mat_or_vec(p: &mut Parser, coll_start: Marker, mut row_start: Marker) -> CompletedMarker {
|
||||||
|
let mut is_matrix = false;
|
||||||
|
let mut row_item_count = 1;
|
||||||
|
loop {
|
||||||
|
if let Some(item) = atom(p) {
|
||||||
|
item.precede(p, "coll_item_start")
|
||||||
|
.complete(p, COLLECTION_ITEM);
|
||||||
|
row_item_count += 1;
|
||||||
|
} else if p.at(SEMICOLON) {
|
||||||
|
is_matrix = true;
|
||||||
|
row_start.complete(p, MAT_ROW);
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
row_start = p.start("matrix_row_start");
|
||||||
|
row_item_count = 0;
|
||||||
|
} else if p.at(R_BRACK) {
|
||||||
|
if is_matrix && row_item_count == 0 {
|
||||||
|
row_start.abandon(p);
|
||||||
|
p.eat(R_BRACK);
|
||||||
|
return coll_start.complete(p, MATRIX);
|
||||||
|
} else if is_matrix {
|
||||||
|
row_start.complete(p, MAT_ROW);
|
||||||
|
p.eat(R_BRACK);
|
||||||
|
return coll_start.complete(p, MATRIX);
|
||||||
|
} else {
|
||||||
|
row_start.abandon(p);
|
||||||
|
p.eat(R_BRACK);
|
||||||
|
return coll_start.complete(p, VEC);
|
||||||
|
}
|
||||||
|
} else if p.at(COMMA) {
|
||||||
|
let err_unexpected_comma = p.start("err_unexpected_comma");
|
||||||
|
p.do_bump();
|
||||||
|
err_unexpected_comma.error(p, SyntaxError::CommaInMatOrVec);
|
||||||
|
} else {
|
||||||
|
let err_unexpected = p.start("err_unexpected_tok");
|
||||||
|
p.do_bump();
|
||||||
|
err_unexpected.error(p, SyntaxError::Expected(vec![EXPR, SEMICOLON, R_BRACK]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
crates/lang/src/lst_parser/grammar/expression/instruction.rs
Normal file
34
crates/lang/src/lst_parser/grammar/expression/instruction.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::lst_parser::{syntax_kind::SyntaxKind::*, CompletedMarker, Parser};
|
||||||
|
|
||||||
|
use super::{atom, lit::literal};
|
||||||
|
|
||||||
|
pub fn instr(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if !p.at(IDENT) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instr = p.start("instr");
|
||||||
|
|
||||||
|
instr_name(p);
|
||||||
|
instr_params(p);
|
||||||
|
|
||||||
|
Some(instr.complete(p, INSTR))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instr_name(p: &mut Parser) {
|
||||||
|
let instr_name = p.start("instr_name");
|
||||||
|
|
||||||
|
while p.at(IDENT) {
|
||||||
|
p.do_bump();
|
||||||
|
}
|
||||||
|
|
||||||
|
instr_name.complete(p, INSTR_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instr_params(p: &mut Parser) {
|
||||||
|
if let Some(start) = atom(p) {
|
||||||
|
while atom(p).is_some() {}
|
||||||
|
|
||||||
|
start.precede(p, "params_start").complete(p, INSTR_PARAMS);
|
||||||
|
}
|
||||||
|
}
|
59
crates/lang/src/lst_parser/grammar/expression/lit.rs
Normal file
59
crates/lang/src/lst_parser/grammar/expression/lit.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use enumset::enum_set;
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use crate::lst_parser::{
|
||||||
|
grammar::check_parser,
|
||||||
|
syntax_kind::{SyntaxKind::*, TokenSet},
|
||||||
|
CompletedMarker, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIT_TOKENS: TokenSet = enum_set!(INT_NUM | FLOAT_NUM | STRING);
|
||||||
|
|
||||||
|
pub fn literal(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if !LIT_TOKENS.contains(p.current()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lit = p.start("lit");
|
||||||
|
|
||||||
|
p.do_bump();
|
||||||
|
|
||||||
|
Some(lit.complete(p, LITERAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_lst_lit() {
|
||||||
|
check_parser(
|
||||||
|
"42",
|
||||||
|
|p| {
|
||||||
|
literal(p);
|
||||||
|
},
|
||||||
|
indoc! {r#"
|
||||||
|
LITERAL {
|
||||||
|
INT_NUM "42";
|
||||||
|
}
|
||||||
|
"#},
|
||||||
|
);
|
||||||
|
check_parser(
|
||||||
|
"3.14",
|
||||||
|
|p| {
|
||||||
|
literal(p);
|
||||||
|
},
|
||||||
|
indoc! {r#"
|
||||||
|
LITERAL {
|
||||||
|
FLOAT_NUM "3.14";
|
||||||
|
}
|
||||||
|
"#},
|
||||||
|
);
|
||||||
|
check_parser(
|
||||||
|
r#""Meow""#,
|
||||||
|
|p| {
|
||||||
|
literal(p);
|
||||||
|
},
|
||||||
|
indoc! {r#"
|
||||||
|
LITERAL {
|
||||||
|
STRING "\"Meow\"";
|
||||||
|
}
|
||||||
|
"#},
|
||||||
|
);
|
||||||
|
}
|
36
crates/lang/src/lst_parser/grammar/expression/pipeline.rs
Normal file
36
crates/lang/src/lst_parser/grammar/expression/pipeline.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use enumset::enum_set;
|
||||||
|
|
||||||
|
use crate::lst_parser::{
|
||||||
|
error::SyntaxError,
|
||||||
|
syntax_kind::{SyntaxKind::*, TokenSet},
|
||||||
|
CompletedMarker, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::expression;
|
||||||
|
|
||||||
|
pub fn pipeline(p: &mut Parser, start_expr: CompletedMarker) -> Option<CompletedMarker> {
|
||||||
|
if !pipe(p) {
|
||||||
|
return Some(start_expr);
|
||||||
|
}
|
||||||
|
let pipeline_marker = start_expr.precede(p, "pipeline_start");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if expression(p, true).is_none() {
|
||||||
|
return Some(pipeline_marker.error(p, SyntaxError::PipelineNeedsSink));
|
||||||
|
}
|
||||||
|
if !pipe(p) {
|
||||||
|
return Some(pipeline_marker.complete(p, PIPELINE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PIPES: TokenSet = enum_set!(PIPE | MAPPING_PIPE | NULL_PIPE);
|
||||||
|
|
||||||
|
fn pipe(p: &mut Parser) -> bool {
|
||||||
|
if PIPES.contains(p.current()) {
|
||||||
|
p.do_bump();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
191
crates/lang/src/lst_parser/grammar/module.rs
Normal file
191
crates/lang/src/lst_parser/grammar/module.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
use enumset::enum_set;
|
||||||
|
|
||||||
|
use crate::lst_parser::{
|
||||||
|
error::SyntaxError,
|
||||||
|
grammar::expression::expression,
|
||||||
|
syntax_kind::{SyntaxKind::*, TokenSet},
|
||||||
|
CompletedMarker, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOP_LEVEL_ITEM_START: TokenSet = enum_set!(DEF_KW | MOD_KW | USE_KW);
|
||||||
|
|
||||||
|
pub fn mod_body(p: &mut Parser) {
|
||||||
|
loop {
|
||||||
|
if top_level_item(p).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mod_decl(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let mod_start = p.start("module");
|
||||||
|
if !p.eat(MOD_KW) {
|
||||||
|
mod_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod_name = p.start("module_name");
|
||||||
|
if p.eat(IDENT) {
|
||||||
|
mod_name.complete(p, MODULE_NAME);
|
||||||
|
} else {
|
||||||
|
mod_name.error(p, SyntaxError::Expected(vec![IDENT]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod_body_marker = p.start("mod_body");
|
||||||
|
if p.eat(SEMICOLON) {
|
||||||
|
mod_body_marker.abandon(p);
|
||||||
|
Some(mod_start.complete(p, MODULE))
|
||||||
|
} else if p.eat(L_BRACE) {
|
||||||
|
mod_body(p);
|
||||||
|
if !p.eat(R_BRACE) {
|
||||||
|
mod_body_marker
|
||||||
|
.complete(p, MODULE_BODY)
|
||||||
|
.precede(p, "unclosed_mod_body_err")
|
||||||
|
.error(p, SyntaxError::UnclosedModuleBody);
|
||||||
|
} else {
|
||||||
|
mod_body_marker.complete(p, MODULE_BODY);
|
||||||
|
}
|
||||||
|
Some(mod_start.complete(p, MODULE))
|
||||||
|
} else {
|
||||||
|
Some(mod_start.error(p, SyntaxError::Expected(vec![MODULE_BODY])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top_level_item(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
if !TOP_LEVEL_ITEM_START.contains(p.current()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
def(p).or_else(|| mod_decl(p)).or_else(|| r#use(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn def(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let def_start = p.start("top_level_def");
|
||||||
|
if !p.eat(DEF_KW) {
|
||||||
|
def_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let def_name = p.start("def_name");
|
||||||
|
if p.eat(IDENT) {
|
||||||
|
def_name.complete(p, DEF_NAME);
|
||||||
|
} else {
|
||||||
|
def_name.error(p, SyntaxError::Expected(vec![IDENT]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_expected_eq = p.start("maybe_expect_eq");
|
||||||
|
if !p.eat(EQ) {
|
||||||
|
maybe_expected_eq.error(p, SyntaxError::Expected(vec![EQ]));
|
||||||
|
} else {
|
||||||
|
maybe_expected_eq.abandon(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = p.start("def_body");
|
||||||
|
if expression(p, false).is_some() {
|
||||||
|
body.complete(p, DEF_BODY);
|
||||||
|
} else {
|
||||||
|
body.error(p, SyntaxError::Expected(vec![DEF_BODY]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(if p.eat(SEMICOLON) {
|
||||||
|
def_start.complete(p, DEF)
|
||||||
|
} else if TOP_LEVEL_ITEM_START.contains(p.current()) || p.at(EOF) {
|
||||||
|
def_start
|
||||||
|
.complete(p, DEF)
|
||||||
|
.precede(p, "unterminated_tl_item")
|
||||||
|
.error(p, SyntaxError::UnterminatedTopLevelItem)
|
||||||
|
} else {
|
||||||
|
def_start
|
||||||
|
.complete(p, DEF)
|
||||||
|
.precede(p, "err_unexpected")
|
||||||
|
.error(p, SyntaxError::Expected(vec![SEMICOLON]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn r#use(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let use_start = p.start("use_start");
|
||||||
|
if !p.eat(USE_KW) {
|
||||||
|
use_start.abandon(p);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_pat(p).is_none() {
|
||||||
|
p.start("expected_use_pat")
|
||||||
|
.error(p, SyntaxError::Expected(vec![USE_PAT]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let use_item = use_start.complete(p, USE);
|
||||||
|
Some(if p.eat(SEMICOLON) {
|
||||||
|
use_item
|
||||||
|
} else if TOP_LEVEL_ITEM_START.contains(p.current()) || p.at(EOF) {
|
||||||
|
use_item
|
||||||
|
.precede(p, "unterminated_tl_item")
|
||||||
|
.error(p, SyntaxError::UnterminatedTopLevelItem)
|
||||||
|
} else {
|
||||||
|
use_item
|
||||||
|
.precede(p, "err_unexpected")
|
||||||
|
.error(p, SyntaxError::Expected(vec![SEMICOLON]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_pat(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let use_pat_marker = p.start("use_pat");
|
||||||
|
if !p.eat(IDENT) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if p.eat(PATH_SEP) {
|
||||||
|
if pat_item(p).is_none() {
|
||||||
|
break Some(use_pat_marker.error(p, SyntaxError::UnfinishedPath));
|
||||||
|
}
|
||||||
|
} else if p.at(SEMICOLON) && p.nth_at(1, COLON) {
|
||||||
|
let broken_sep = p.start("broken_path_sep");
|
||||||
|
let wrong_semi = p.start("semi_typo");
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
wrong_semi.error(p, SyntaxError::PathSepContainsSemicolon);
|
||||||
|
p.eat(COLON);
|
||||||
|
broken_sep.complete(p, PATH_SEP);
|
||||||
|
if pat_item(p).is_none() {
|
||||||
|
break Some(use_pat_marker.error(p, SyntaxError::UnfinishedPath));
|
||||||
|
}
|
||||||
|
} else if p.at(COLON) && p.nth_at(1, SEMICOLON) {
|
||||||
|
let broken_sep = p.start("broken_path_sep");
|
||||||
|
p.eat(COLON);
|
||||||
|
let wrong_semi = p.start("semi_typo");
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
wrong_semi.error(p, SyntaxError::PathSepContainsSemicolon);
|
||||||
|
broken_sep.complete(p, PATH_SEP);
|
||||||
|
if pat_item(p).is_none() {
|
||||||
|
break Some(use_pat_marker.error(p, SyntaxError::UnfinishedPath));
|
||||||
|
}
|
||||||
|
} else if p.at(SEMICOLON) && p.nth_at(1, SEMICOLON) {
|
||||||
|
let broken_sep = p.start("broken_path_sep");
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
p.eat(SEMICOLON);
|
||||||
|
broken_sep
|
||||||
|
.complete(p, PATH_SEP)
|
||||||
|
.precede(p, "semi_typo_err")
|
||||||
|
.error(p, SyntaxError::PathSepContainsSemicolon);
|
||||||
|
if pat_item(p).is_none() {
|
||||||
|
break Some(use_pat_marker.error(p, SyntaxError::UnfinishedPath));
|
||||||
|
}
|
||||||
|
} else if p.eat(SEMICOLON) {
|
||||||
|
break Some(use_pat_marker.complete(p, USE_PAT));
|
||||||
|
} else {
|
||||||
|
break Some(use_pat_marker.error(p, SyntaxError::Expected(vec![PATH_SEP, SEMICOLON])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pat_item(p: &mut Parser) -> Option<CompletedMarker> {
|
||||||
|
let item_start = p.start("pat_item_start");
|
||||||
|
if p.eat(IDENT) {
|
||||||
|
Some(item_start.complete(p, PAT_ITEM))
|
||||||
|
} else if p.eat(STAR) {
|
||||||
|
Some(item_start.complete(p, PAT_GLOB))
|
||||||
|
} else if p.eat(L_BRACE) {
|
||||||
|
todo!("write PAT_GROUPs")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
70
crates/lang/src/lst_parser/input.rs
Normal file
70
crates/lang/src/lst_parser/input.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use enumset::enum_set;
|
||||||
|
|
||||||
|
use crate::lst_parser::syntax_kind::SyntaxKind;
|
||||||
|
|
||||||
|
use super::syntax_kind::TokenSet;
|
||||||
|
|
||||||
|
pub struct Input<'src, 'toks> {
|
||||||
|
raw: &'toks Vec<(SyntaxKind, &'src str)>,
|
||||||
|
/// indices of the "meaningful" tokens (not whitespace etc)
|
||||||
|
/// includes newlines because those might indeed help with finding errors
|
||||||
|
meaningful: Vec<usize>,
|
||||||
|
/// indices of newlines for the purpose of easily querying them
|
||||||
|
/// can be helpful with missing commas etc
|
||||||
|
newlines: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MEANINGLESS_TOKS: TokenSet = enum_set!(SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE);
|
||||||
|
|
||||||
|
impl<'src, 'toks> Input<'src, 'toks> {
|
||||||
|
pub fn new(raw_toks: &'toks Vec<(SyntaxKind, &'src str)>) -> Self {
|
||||||
|
let meaningful = raw_toks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, tok)| {
|
||||||
|
if MEANINGLESS_TOKS.contains(tok.0) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let newlines = raw_toks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, tok)| match tok.0 {
|
||||||
|
SyntaxKind::NEWLINE => Some(i),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
raw: raw_toks,
|
||||||
|
meaningful,
|
||||||
|
newlines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used, reason = "meaningful indices cannot be invalid")]
|
||||||
|
pub(crate) fn kind(&self, idx: usize) -> SyntaxKind {
|
||||||
|
let Some(meaningful_idx) = self.meaningful.get(idx) else {
|
||||||
|
return SyntaxKind::EOF;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.raw.get(*meaningful_idx).unwrap().0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preceding_meaningless(&self, idx: usize) -> usize {
|
||||||
|
assert!(self.meaningful.len() > idx);
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.meaningful[idx] - self.meaningful[idx - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn meaningless_tail_len(&self) -> usize {
|
||||||
|
self.raw.len() - (self.meaningful.last().unwrap() + 1)
|
||||||
|
}
|
||||||
|
}
|
208
crates/lang/src/lst_parser/output.rs
Normal file
208
crates/lang/src/lst_parser/output.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use clap::builder;
|
||||||
|
use owo_colors::{unset_override, OwoColorize};
|
||||||
|
use rowan::{GreenNode, GreenNodeBuilder, GreenNodeData, GreenTokenData, Language, NodeOrToken};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
lst_parser::{input::MEANINGLESS_TOKS, syntax_kind::SyntaxKind},
|
||||||
|
Lang, SyntaxNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
error::SyntaxError,
|
||||||
|
events::{Event, NodeKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Output {
|
||||||
|
pub green_node: GreenNode,
|
||||||
|
pub errors: Vec<SyntaxError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Output {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut errs: Vec<&SyntaxError> = self.errors.iter().collect();
|
||||||
|
errs.reverse();
|
||||||
|
|
||||||
|
debug_print_green_node(NodeOrToken::Node(&self.green_node), f, 0, &mut errs, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INDENT_STR: &str = " ";
|
||||||
|
/// colored argument currently broken
|
||||||
|
fn debug_print_green_node(
|
||||||
|
node: NodeOrToken<&GreenNodeData, &GreenTokenData>,
|
||||||
|
f: &mut dyn std::fmt::Write,
|
||||||
|
lvl: i32,
|
||||||
|
errs: &mut Vec<&SyntaxError>,
|
||||||
|
colored: bool,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
for _ in 0..lvl {
|
||||||
|
f.write_str(INDENT_STR)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = match node {
|
||||||
|
NodeOrToken::Node(n) => {
|
||||||
|
let kind = Lang::kind_from_raw(node.kind());
|
||||||
|
if kind != SyntaxKind::PARSE_ERR {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{:?} {}",
|
||||||
|
Lang::kind_from_raw(node.kind()).bright_yellow().bold(),
|
||||||
|
"{".yellow()
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
let err = errs
|
||||||
|
.pop()
|
||||||
|
.expect("all error syntax nodes should correspond to an error")
|
||||||
|
.bright_red();
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{:?}{} {err:?} {}",
|
||||||
|
kind.bright_red().bold(),
|
||||||
|
":".red(),
|
||||||
|
"{".bright_red().bold()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
for c in n.children() {
|
||||||
|
debug_print_green_node(c, f, lvl + 1, errs, colored)?;
|
||||||
|
}
|
||||||
|
for _ in 0..lvl {
|
||||||
|
f.write_str(INDENT_STR)?;
|
||||||
|
}
|
||||||
|
if kind != SyntaxKind::PARSE_ERR {
|
||||||
|
write!(f, "{}", "}\n".yellow())
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", "}\n".bright_red().bold())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeOrToken::Token(t) => {
|
||||||
|
let tok = Lang::kind_from_raw(t.kind());
|
||||||
|
if MEANINGLESS_TOKS.contains(tok) {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{:?} {:?}{}",
|
||||||
|
Lang::kind_from_raw(t.kind()).white(),
|
||||||
|
t.text().white(),
|
||||||
|
";".white()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{:?} {:?}{}",
|
||||||
|
Lang::kind_from_raw(t.kind()).bright_cyan().bold(),
|
||||||
|
t.text().green(),
|
||||||
|
";".yellow()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Output {
|
||||||
|
pub fn debug_colored(&self) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut errs: Vec<&SyntaxError> = self.errors.iter().collect();
|
||||||
|
errs.reverse();
|
||||||
|
|
||||||
|
let _ = debug_print_green_node(
|
||||||
|
NodeOrToken::Node(&self.green_node),
|
||||||
|
&mut out,
|
||||||
|
0,
|
||||||
|
&mut errs,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
pub fn from_parser_output(
|
||||||
|
mut raw_toks: Vec<(SyntaxKind, &str)>,
|
||||||
|
mut events: Vec<Event>,
|
||||||
|
) -> Self {
|
||||||
|
let mut builder = GreenNodeBuilder::new();
|
||||||
|
let mut fw_parents = Vec::new();
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
raw_toks.reverse();
|
||||||
|
|
||||||
|
for i in 0..events.len() {
|
||||||
|
match mem::replace(&mut events[i], Event::tombstone()) {
|
||||||
|
Event::Start {
|
||||||
|
kind,
|
||||||
|
forward_parent,
|
||||||
|
} => {
|
||||||
|
if kind == SyntaxKind::TOMBSTONE && forward_parent.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fw_parents.push(kind);
|
||||||
|
let mut idx = i;
|
||||||
|
let mut fp = forward_parent;
|
||||||
|
while let Some(fwd) = fp {
|
||||||
|
idx += fwd as usize;
|
||||||
|
fp = match mem::replace(&mut events[idx], Event::tombstone()) {
|
||||||
|
Event::Start {
|
||||||
|
kind,
|
||||||
|
forward_parent,
|
||||||
|
} => {
|
||||||
|
fw_parents.push(kind);
|
||||||
|
forward_parent
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove whitespace bc it's ugly
|
||||||
|
while let Some((SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE, _)) =
|
||||||
|
raw_toks.last()
|
||||||
|
{
|
||||||
|
match events.iter_mut().find(|ev| matches!(ev, Event::Eat { .. })) {
|
||||||
|
Some(Event::Eat { count }) => *count -= 1,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tok, text): (SyntaxKind, &str) = raw_toks.pop().unwrap();
|
||||||
|
builder.token(tok.into(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
for kind in fw_parents.drain(..).rev() {
|
||||||
|
match kind {
|
||||||
|
NodeKind::Syntax(kind) if kind != SyntaxKind::TOMBSTONE => {
|
||||||
|
builder.start_node(kind.into())
|
||||||
|
}
|
||||||
|
NodeKind::Error(err) => {
|
||||||
|
errors.push(err);
|
||||||
|
builder.start_node(SyntaxKind::PARSE_ERR.into())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Finish => builder.finish_node(),
|
||||||
|
Event::Eat { count } => (0..count).for_each(|_| {
|
||||||
|
let (tok, text): (SyntaxKind, &str) = raw_toks.pop().unwrap();
|
||||||
|
builder.token(tok.into(), text);
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
green_node: builder.finish(),
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn syntax(&self) -> SyntaxNode {
|
||||||
|
SyntaxNode::new_root(self.green_node.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn errors(&self) -> Vec<SyntaxError> {
|
||||||
|
self.errors.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dissolve(self) -> (GreenNode, Vec<SyntaxError>) {
|
||||||
|
let Self { green_node, errors } = self;
|
||||||
|
(green_node, errors)
|
||||||
|
}
|
||||||
|
}
|
140
crates/lang/src/lst_parser/syntax_kind.rs
Normal file
140
crates/lang/src/lst_parser/syntax_kind.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use enumset::EnumSet;
|
||||||
|
use logos::Logos;
|
||||||
|
|
||||||
|
pub fn lex(src: &str) -> Vec<(SyntaxKind, &str)> {
|
||||||
|
let mut lex = SyntaxKind::lexer(src);
|
||||||
|
let mut r = Vec::new();
|
||||||
|
|
||||||
|
while let Some(tok_res) = lex.next() {
|
||||||
|
r.push((tok_res.unwrap_or(SyntaxKind::LEX_ERR), lex.slice()))
|
||||||
|
}
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(enumset::EnumSetType, Logos, Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
|
||||||
|
#[repr(u16)]
|
||||||
|
#[enumset(no_super_impls)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum SyntaxKind {
|
||||||
|
#[token("def")]
|
||||||
|
DEF_KW = 0,
|
||||||
|
DEF,
|
||||||
|
DEF_NAME,
|
||||||
|
DEF_BODY,
|
||||||
|
#[token("let")]
|
||||||
|
LET_KW,
|
||||||
|
#[token("in")]
|
||||||
|
IN_KW,
|
||||||
|
LET_IN,
|
||||||
|
#[token("::")]
|
||||||
|
PATH_SEP,
|
||||||
|
#[token("mod")]
|
||||||
|
MOD_KW,
|
||||||
|
MODULE,
|
||||||
|
MODULE_NAME,
|
||||||
|
MODULE_BODY,
|
||||||
|
USE,
|
||||||
|
#[token("use")]
|
||||||
|
USE_KW,
|
||||||
|
USE_PAT,
|
||||||
|
PAT_ITEM,
|
||||||
|
PAT_GLOB,
|
||||||
|
PAT_GROUP,
|
||||||
|
#[regex("[\\d]+")]
|
||||||
|
INT_NUM,
|
||||||
|
#[regex("[+-]?([\\d]+\\.[\\d]*|[\\d]*\\.[\\d]+)")]
|
||||||
|
FLOAT_NUM,
|
||||||
|
#[regex(r#""([^"\\]|\\["\\bnfrt]|u[a-fA-F0-9]{4})*""#)]
|
||||||
|
STRING,
|
||||||
|
MATRIX,
|
||||||
|
MAT_ROW,
|
||||||
|
VEC,
|
||||||
|
LIST,
|
||||||
|
// either of a vec, a matrix or a list
|
||||||
|
COLLECTION_ITEM,
|
||||||
|
PARENTHESIZED_EXPR,
|
||||||
|
EXPR,
|
||||||
|
LITERAL,
|
||||||
|
#[token("(")]
|
||||||
|
L_PAREN,
|
||||||
|
#[token(")")]
|
||||||
|
R_PAREN,
|
||||||
|
#[token("{")]
|
||||||
|
L_BRACE,
|
||||||
|
#[token("}")]
|
||||||
|
R_BRACE,
|
||||||
|
#[token("[")]
|
||||||
|
L_BRACK,
|
||||||
|
#[token("]")]
|
||||||
|
R_BRACK,
|
||||||
|
#[token("<")]
|
||||||
|
L_ANGLE,
|
||||||
|
#[token(">")]
|
||||||
|
R_ANGLE,
|
||||||
|
#[token("+")]
|
||||||
|
PLUS,
|
||||||
|
#[token("-")]
|
||||||
|
MINUS,
|
||||||
|
#[token("*")]
|
||||||
|
STAR,
|
||||||
|
#[token("/")]
|
||||||
|
SLASH,
|
||||||
|
#[token("%")]
|
||||||
|
PERCENT,
|
||||||
|
#[token("^")]
|
||||||
|
CARET,
|
||||||
|
INSTR,
|
||||||
|
INSTR_NAME,
|
||||||
|
INSTR_PARAMS,
|
||||||
|
ATTR_SET,
|
||||||
|
ATTR,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_VALUE,
|
||||||
|
#[regex("[a-zA-Z_]+[a-zA-Z_\\-\\d]*")]
|
||||||
|
IDENT,
|
||||||
|
#[regex("\\$[a-zA-Z0-9_\\-]+")]
|
||||||
|
VAR,
|
||||||
|
#[regex("\\@[a-zA-Z0-9_\\-]+")]
|
||||||
|
INPUT_VAR,
|
||||||
|
#[token("$")]
|
||||||
|
DOLLAR,
|
||||||
|
#[token("@")]
|
||||||
|
AT,
|
||||||
|
#[token(",")]
|
||||||
|
COMMA,
|
||||||
|
#[token("|")]
|
||||||
|
PIPE,
|
||||||
|
#[token("@|")]
|
||||||
|
MAPPING_PIPE,
|
||||||
|
#[token("!|")]
|
||||||
|
NULL_PIPE,
|
||||||
|
PIPELINE,
|
||||||
|
#[token("=")]
|
||||||
|
EQ,
|
||||||
|
#[token(":")]
|
||||||
|
COLON,
|
||||||
|
#[token(";")]
|
||||||
|
SEMICOLON,
|
||||||
|
#[token(".")]
|
||||||
|
DOT,
|
||||||
|
#[token("!")]
|
||||||
|
BANG,
|
||||||
|
#[regex("[ \\t\\f]+")]
|
||||||
|
WHITESPACE,
|
||||||
|
#[token("\n")]
|
||||||
|
NEWLINE,
|
||||||
|
PARSE_ERR,
|
||||||
|
LEX_ERR,
|
||||||
|
ROOT,
|
||||||
|
EOF,
|
||||||
|
TOMBSTONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TokenSet = EnumSet<SyntaxKind>;
|
||||||
|
|
||||||
|
impl From<SyntaxKind> for rowan::SyntaxKind {
|
||||||
|
fn from(kind: SyntaxKind) -> Self {
|
||||||
|
Self(kind as u16)
|
||||||
|
}
|
||||||
|
}
|
29
crates/lang/src/main.rs
Normal file
29
crates/lang/src/main.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use lang::lst_parser::{self, grammar, input, output::Output, syntax_kind};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let n = args.file.clone();
|
||||||
|
let f = fs::read_to_string(n.clone()).expect("failed to read file");
|
||||||
|
|
||||||
|
let toks = dbg!(syntax_kind::lex(&f));
|
||||||
|
let input = input::Input::new(&toks);
|
||||||
|
let mut parser = lst_parser::Parser::new(input);
|
||||||
|
|
||||||
|
grammar::source_file(&mut parser);
|
||||||
|
|
||||||
|
let p_out = dbg!(parser.finish());
|
||||||
|
let o = Output::from_parser_output(toks, p_out);
|
||||||
|
|
||||||
|
println!("{}", o.debug_colored());
|
||||||
|
|
||||||
|
// World::new(n);
|
||||||
|
}
|
27
crates/lang/src/world.rs
Normal file
27
crates/lang/src/world.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use self::files::{Files, OpenFileError};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod files;
|
||||||
|
|
||||||
|
struct World;
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn new(entry_point: &Path) -> Result<Self, WorldCreationError> {
|
||||||
|
let mut files = Files::default();
|
||||||
|
let (entry_point_id, errors) = files.add_file(entry_point)?;
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WorldCreationError {
|
||||||
|
FailedToOpenEntryPoint(OpenFileError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OpenFileError> for WorldCreationError {
|
||||||
|
fn from(value: OpenFileError) -> Self {
|
||||||
|
Self::FailedToOpenEntryPoint(value)
|
||||||
|
}
|
||||||
|
}
|
10
crates/lang/src/world/error.rs
Normal file
10
crates/lang/src/world/error.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{ast::ParseError, lst_parser::error::SyntaxError};
|
||||||
|
|
||||||
|
use super::files::{FileId, Loc, OpenFileError};
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
Syntax(Loc<ParseError>, SyntaxError),
|
||||||
|
OpenFileError(OpenFileError),
|
||||||
|
}
|
57
crates/lang/src/world/files.rs
Normal file
57
crates/lang/src/world/files.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod loc;
|
||||||
|
|
||||||
|
pub use loc::Loc;
|
||||||
|
use rowan::ast::AstNode;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ast::ParseError,
|
||||||
|
lst_parser::{self, error::SyntaxError, input, output::Output},
|
||||||
|
world::{error::Error, files::source_file::SourceFile},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Files {
|
||||||
|
inner: Vec<source_file::SourceFile>,
|
||||||
|
path_to_id_map: HashMap<PathBuf, FileId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Files {
|
||||||
|
pub fn add_file(&mut self, path: &Path) -> Result<(FileId, Vec<Error>), OpenFileError> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(OpenFileError::NotFound(path.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_id = FileId(self.inner.len());
|
||||||
|
let (source_file, errs) = match SourceFile::open(path) {
|
||||||
|
Ok((source_file, errs)) => {
|
||||||
|
let errs = errs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(ptr, err)| Error::Syntax(Loc::from_ptr(ptr, file_id), err))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(source_file, errs)
|
||||||
|
}
|
||||||
|
Err(e) => return Err(OpenFileError::IoError(path.to_path_buf(), e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.inner.push(source_file);
|
||||||
|
self.path_to_id_map.insert(path.to_path_buf(), file_id);
|
||||||
|
|
||||||
|
Ok((file_id, errs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum OpenFileError {
|
||||||
|
NotFound(PathBuf),
|
||||||
|
IoError(PathBuf, std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct FileId(usize);
|
||||||
|
|
||||||
|
mod source_file;
|
29
crates/lang/src/world/files/loc.rs
Normal file
29
crates/lang/src/world/files/loc.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use rowan::ast::{AstNode, AstPtr};
|
||||||
|
|
||||||
|
use crate::Lang;
|
||||||
|
|
||||||
|
use super::FileId;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Loc<N: AstNode<Language = Lang>> {
|
||||||
|
file: FileId,
|
||||||
|
syntax: AstPtr<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: AstNode<Language = Lang>> Loc<N> {
|
||||||
|
pub fn new(node: N, file: FileId) -> Self {
|
||||||
|
Self::from_ptr(AstPtr::new(&node), file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_ptr(ptr: AstPtr<N>, file: FileId) -> Self {
|
||||||
|
Self { file, syntax: ptr }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file(&self) -> FileId {
|
||||||
|
self.file
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn syntax(&self) -> AstPtr<N> {
|
||||||
|
self.syntax.clone()
|
||||||
|
}
|
||||||
|
}
|
113
crates/lang/src/world/files/source_file.rs
Normal file
113
crates/lang/src/world/files/source_file.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use crate::lst_parser::{self, grammar, input, syntax_kind};
|
||||||
|
use crate::SyntaxNode;
|
||||||
|
|
||||||
|
use crate::lst_parser::output::Output;
|
||||||
|
|
||||||
|
use crate::lst_parser::error::SyntaxError;
|
||||||
|
|
||||||
|
use crate::ast::ParseError;
|
||||||
|
|
||||||
|
use rowan::ast::{AstNode, AstPtr};
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::{fs, io};
|
||||||
|
|
||||||
|
use rowan::GreenNode;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub(crate) struct SourceFile {
|
||||||
|
pub(crate) path: PathBuf,
|
||||||
|
pub(crate) lst: rowan::GreenNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceFile {
|
||||||
|
pub(crate) fn open(p: &Path) -> io::Result<(Self, Vec<(AstPtr<ParseError>, SyntaxError)>)> {
|
||||||
|
assert!(p.exists());
|
||||||
|
|
||||||
|
let f = fs::read_to_string(p)?;
|
||||||
|
let (lst, errs) = Self::parse(f);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
path: p.to_path_buf(),
|
||||||
|
lst,
|
||||||
|
},
|
||||||
|
errs,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse(f: String) -> (GreenNode, Vec<(AstPtr<ParseError>, SyntaxError)>) {
|
||||||
|
let toks = syntax_kind::lex(&f);
|
||||||
|
let input = input::Input::new(&toks);
|
||||||
|
let mut parser = lst_parser::Parser::new(input);
|
||||||
|
|
||||||
|
grammar::source_file(&mut parser);
|
||||||
|
|
||||||
|
let p_out = parser.finish();
|
||||||
|
let (lst, errs) = Output::from_parser_output(toks, p_out).dissolve();
|
||||||
|
|
||||||
|
(lst.clone(), Self::find_errs(lst, errs))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_errs(
|
||||||
|
lst: GreenNode,
|
||||||
|
mut errs: Vec<SyntaxError>,
|
||||||
|
) -> Vec<(AstPtr<ParseError>, SyntaxError)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
errs.reverse();
|
||||||
|
|
||||||
|
let lst = SyntaxNode::new_root(lst);
|
||||||
|
Self::find_errs_recursive(&mut out, lst, &mut errs);
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_errs_recursive(
|
||||||
|
mut out: &mut Vec<(AstPtr<ParseError>, SyntaxError)>,
|
||||||
|
lst: SyntaxNode,
|
||||||
|
mut errs: &mut Vec<SyntaxError>,
|
||||||
|
) {
|
||||||
|
lst.children()
|
||||||
|
.filter_map(|c| ParseError::cast(c))
|
||||||
|
.for_each(|e| out.push((AstPtr::new(&e), errs.pop().unwrap())));
|
||||||
|
|
||||||
|
lst.children()
|
||||||
|
.for_each(|c| Self::find_errs_recursive(out, c, errs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::world::files::source_file::SourceFile;
|
||||||
|
|
||||||
|
fn check_find_errs(input: &str, expected: &[&str]) {
|
||||||
|
let (_, errs) = SourceFile::parse(input.to_string());
|
||||||
|
|
||||||
|
let errs = errs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(loc, err)| format!("{:?}@{:?}", err, loc.syntax_node_ptr().text_range()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
errs,
|
||||||
|
expected
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_errs() {
|
||||||
|
check_find_errs(
|
||||||
|
"def meow = ;\n mod ;",
|
||||||
|
&["Expected([DEF_BODY])@11..11", "Expected([IDENT])@18..18"],
|
||||||
|
);
|
||||||
|
|
||||||
|
check_find_errs(
|
||||||
|
"def awawa = a |",
|
||||||
|
&["UnterminatedTopLevelItem@0..15", "PipelineNeedsSink@12..15"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
12
crates/pawarser/Cargo.toml
Normal file
12
crates/pawarser/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "pawarser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rowan = "0.15.15"
|
||||||
|
drop_bomb = "0.1.5"
|
||||||
|
enumset = "1.1.3"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
8
crates/pawarser/src/lib.rs
Normal file
8
crates/pawarser/src/lib.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#![feature(iter_collect_into)]
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use parser::{
|
||||||
|
error::SyntaxError,
|
||||||
|
marker::{CompletedMarker, Marker},
|
||||||
|
Parser, SyntaxElement,
|
||||||
|
};
|
253
crates/pawarser/src/parser.rs
Normal file
253
crates/pawarser/src/parser.rs
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
use std::{cell::Cell, fmt, marker::PhantomData, mem};
|
||||||
|
|
||||||
|
use enumset::{EnumSet, EnumSetType};
|
||||||
|
use rowan::{GreenNode, GreenNodeBuilder};
|
||||||
|
|
||||||
|
use crate::parser::event::NodeKind;
|
||||||
|
|
||||||
|
use self::{event::Event, input::Input, marker::Marker};
|
||||||
|
pub use {error::SyntaxError, output::ParserOutput};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod event;
|
||||||
|
mod input;
|
||||||
|
pub mod marker;
|
||||||
|
pub mod output;
|
||||||
|
|
||||||
|
/// this is used to define some required SyntaxKinds like an EOF token or an error token
|
||||||
|
pub trait SyntaxElement
|
||||||
|
where
|
||||||
|
Self: EnumSetType
|
||||||
|
+ Into<rowan::SyntaxKind>
|
||||||
|
+ From<rowan::SyntaxKind>
|
||||||
|
+ fmt::Debug
|
||||||
|
+ Clone
|
||||||
|
+ PartialEq
|
||||||
|
+ Eq,
|
||||||
|
{
|
||||||
|
/// EOF value. This will be used by the rest of the parser library to represent an EOF.
|
||||||
|
const SYNTAX_EOF: Self;
|
||||||
|
/// Error value. This will be used as a placeholder for associated respective errors.
|
||||||
|
const SYNTAX_ERROR: Self;
|
||||||
|
const SYNTAX_ROOT: Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Parser<'src, SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> {
|
||||||
|
input: Input<'src, SyntaxKind>,
|
||||||
|
pos: usize,
|
||||||
|
events: Vec<Event<SyntaxKind, SyntaxErr>>,
|
||||||
|
step_limit: u32,
|
||||||
|
steps: Cell<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, 'toks, SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>
|
||||||
|
Parser<'src, SyntaxKind, SyntaxErr>
|
||||||
|
{
|
||||||
|
/// eat all meaningless tokens at the end of the file.
|
||||||
|
pub fn eat_succeeding_meaningless(&mut self) {
|
||||||
|
self.push_ev(Event::Eat {
|
||||||
|
count: self.input.meaningless_tail_len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get token from current position of the parser.
|
||||||
|
pub fn current(&self) -> SyntaxKind {
|
||||||
|
self.step();
|
||||||
|
self.input.kind(self.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self, name: &str) -> Marker {
|
||||||
|
let pos = self.events.len();
|
||||||
|
self.push_ev(Event::tombstone());
|
||||||
|
Marker::new(pos, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eat next token if it's of kind `kind` and return `true`.
|
||||||
|
/// Otherwise, `false`.
|
||||||
|
pub fn eat(&mut self, kind: SyntaxKind) -> bool {
|
||||||
|
if !self.at(kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.do_bump();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_bump(&mut self) {
|
||||||
|
self.push_ev(Event::Eat {
|
||||||
|
count: self.input.preceding_meaningless(self.pos),
|
||||||
|
});
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the token at the current parser position is of `kind`
|
||||||
|
pub fn at(&self, kind: SyntaxKind) -> bool {
|
||||||
|
self.nth_at(0, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the token that is `n` ahead is of `kind`
|
||||||
|
pub fn nth_at(&self, n: usize, kind: SyntaxKind) -> bool {
|
||||||
|
self.nth(n) == kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nth(&self, n: usize) -> SyntaxKind {
|
||||||
|
self.step();
|
||||||
|
self.input.kind(self.pos + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_ev(&mut self, event: Event<SyntaxKind, SyntaxErr>) {
|
||||||
|
self.events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&self) {
|
||||||
|
let steps = self.steps.get();
|
||||||
|
assert!(steps <= self.step_limit, "the parser seems stuck.");
|
||||||
|
self.steps.set(steps + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> ParserOutput<SyntaxKind, SyntaxErr> {
|
||||||
|
let Self {
|
||||||
|
input,
|
||||||
|
pos,
|
||||||
|
mut events,
|
||||||
|
step_limit,
|
||||||
|
steps,
|
||||||
|
} = self;
|
||||||
|
let (mut raw_toks, meaningless_tokens) = input.dissolve();
|
||||||
|
let mut builder = GreenNodeBuilder::new();
|
||||||
|
// TODO: document what the hell a forward parent is
|
||||||
|
let mut fw_parents = Vec::new();
|
||||||
|
let mut errors: Vec<SyntaxErr> = Vec::new();
|
||||||
|
raw_toks.reverse();
|
||||||
|
|
||||||
|
// always have an implicit root node to avoid [`GreenNodeBuilder::finish()`] panicking due to multiple root elements.
|
||||||
|
builder.start_node(SyntaxKind::SYNTAX_ROOT.into());
|
||||||
|
|
||||||
|
for i in 0..events.len() {
|
||||||
|
match mem::replace(&mut events[i], Event::tombstone()) {
|
||||||
|
Event::Start {
|
||||||
|
kind,
|
||||||
|
forward_parent,
|
||||||
|
} => {
|
||||||
|
if kind == NodeKind::Tombstone && forward_parent.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolving forward parents
|
||||||
|
// temporarily jump around with the parser index and replace them with tombstones
|
||||||
|
fw_parents.push(kind);
|
||||||
|
let mut idx = i;
|
||||||
|
let mut fp = forward_parent;
|
||||||
|
while let Some(fwd) = fp {
|
||||||
|
idx += fwd as usize;
|
||||||
|
fp = match mem::replace(&mut events[idx], Event::tombstone()) {
|
||||||
|
Event::Start {
|
||||||
|
kind,
|
||||||
|
forward_parent,
|
||||||
|
} => {
|
||||||
|
fw_parents.push(kind);
|
||||||
|
forward_parent
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear semantically meaningless tokens before the new tree node for aesthetic reasons
|
||||||
|
while raw_toks
|
||||||
|
.last()
|
||||||
|
.is_some_and(|v| meaningless_tokens.contains(v.0))
|
||||||
|
{
|
||||||
|
// update first next Eat event
|
||||||
|
match events.iter_mut().find(|ev| matches!(ev, Event::Eat { .. })) {
|
||||||
|
Some(Event::Eat { count }) => *count -= 1,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// put whitespace into lst
|
||||||
|
let (tok, text) = raw_toks.pop().unwrap();
|
||||||
|
builder.token(tok.into(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert forward parents into the tree in correct order
|
||||||
|
for kind in fw_parents.drain(..).rev() {
|
||||||
|
match kind {
|
||||||
|
NodeKind::Syntax(kind) => builder.start_node(kind.into()),
|
||||||
|
NodeKind::Error(err) => {
|
||||||
|
errors.push(err);
|
||||||
|
builder.start_node(SyntaxKind::SYNTAX_ERROR.into())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Finish => builder.finish_node(),
|
||||||
|
Event::Eat { count } => (0..count).for_each(|_| {
|
||||||
|
let (tok, text) = raw_toks.pop().unwrap();
|
||||||
|
builder.token(tok.into(), text);
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish SYNTAX_ROOT
|
||||||
|
builder.finish_node();
|
||||||
|
|
||||||
|
ParserOutput {
|
||||||
|
green_node: builder.finish(),
|
||||||
|
errors,
|
||||||
|
_syntax_kind: PhantomData::<SyntaxKind>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParserBuilder<
|
||||||
|
'src,
|
||||||
|
SyntaxKind: SyntaxElement,
|
||||||
|
// SyntaxErr: SyntaxError,
|
||||||
|
> {
|
||||||
|
raw_toks: Vec<(SyntaxKind, &'src str)>,
|
||||||
|
meaningless_token_kinds: EnumSet<SyntaxKind>,
|
||||||
|
step_limit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, SyntaxKind: SyntaxElement> ParserBuilder<'src, SyntaxKind> {
|
||||||
|
pub fn new(raw_toks: Vec<(SyntaxKind, &'src str)>) -> Self {
|
||||||
|
Self {
|
||||||
|
raw_toks,
|
||||||
|
meaningless_token_kinds: EnumSet::new(),
|
||||||
|
step_limit: 4096,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the parser step limit.
|
||||||
|
/// Defaults to 4096
|
||||||
|
pub fn step_limit(mut self, new: u32) -> Self {
|
||||||
|
self.step_limit = new;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_meaningless(mut self, kind: SyntaxKind) -> Self {
|
||||||
|
self.meaningless_token_kinds.insert(kind);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_meaningless_many(mut self, kind: Vec<SyntaxKind>) -> Self {
|
||||||
|
self.meaningless_token_kinds
|
||||||
|
.insert_all(kind.into_iter().collect());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build<SyntaxErr: SyntaxError>(self) -> Parser<'src, SyntaxKind, SyntaxErr> {
|
||||||
|
let Self {
|
||||||
|
raw_toks,
|
||||||
|
meaningless_token_kinds,
|
||||||
|
step_limit,
|
||||||
|
} = self;
|
||||||
|
Parser {
|
||||||
|
input: Input::new(raw_toks, Some(meaningless_token_kinds)),
|
||||||
|
pos: 0,
|
||||||
|
events: Vec::new(),
|
||||||
|
step_limit,
|
||||||
|
steps: Cell::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
crates/pawarser/src/parser/error.rs
Normal file
9
crates/pawarser/src/parser/error.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// A marker trait... for now!
|
||||||
|
// TODO: constrain that conversion to `NodeKind::Error` is enforced to be possible
|
||||||
|
pub trait SyntaxError
|
||||||
|
where
|
||||||
|
Self: fmt::Debug + Clone + PartialEq + Eq,
|
||||||
|
{
|
||||||
|
}
|
42
crates/pawarser/src/parser/event.rs
Normal file
42
crates/pawarser/src/parser/event.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use enumset::EnumSetType;
|
||||||
|
|
||||||
|
use super::{error::SyntaxError, SyntaxElement};
|
||||||
|
|
||||||
|
pub enum Event<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> {
|
||||||
|
Start {
|
||||||
|
kind: NodeKind<SyntaxKind, SyntaxErr>,
|
||||||
|
forward_parent: Option<usize>,
|
||||||
|
},
|
||||||
|
Finish,
|
||||||
|
Eat {
|
||||||
|
count: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> Event<SyntaxKind, SyntaxErr> {
|
||||||
|
pub fn tombstone() -> Self {
|
||||||
|
Self::Start {
|
||||||
|
kind: NodeKind::Tombstone,
|
||||||
|
forward_parent: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub enum NodeKind<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> {
|
||||||
|
Tombstone,
|
||||||
|
Syntax(SyntaxKind),
|
||||||
|
Error(SyntaxErr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> NodeKind<SyntaxKind, SyntaxErr> {
|
||||||
|
pub fn is_tombstone(&self) -> bool {
|
||||||
|
matches!(self, Self::Tombstone)
|
||||||
|
}
|
||||||
|
pub fn is_syntax(&self) -> bool {
|
||||||
|
matches!(self, Self::Syntax(_))
|
||||||
|
}
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
matches!(self, Self::Error(_))
|
||||||
|
}
|
||||||
|
}
|
67
crates/pawarser/src/parser/input.rs
Normal file
67
crates/pawarser/src/parser/input.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use enumset::{EnumSet, EnumSetType};
|
||||||
|
|
||||||
|
use super::SyntaxElement;
|
||||||
|
|
||||||
|
pub struct Input<'src, SyntaxKind: SyntaxElement> {
|
||||||
|
raw: Vec<(SyntaxKind, &'src str)>,
|
||||||
|
// enumset of meaningless tokens
|
||||||
|
semantically_meaningless: EnumSet<SyntaxKind>,
|
||||||
|
// indices of non-meaningless tokens
|
||||||
|
meaningful_toks: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, SyntaxKind: SyntaxElement> Input<'src, SyntaxKind> {
|
||||||
|
pub fn new(
|
||||||
|
raw_toks: Vec<(SyntaxKind, &'src str)>,
|
||||||
|
meaningless: Option<EnumSet<SyntaxKind>>,
|
||||||
|
) -> Self {
|
||||||
|
let mut meaningful_toks = Vec::new();
|
||||||
|
|
||||||
|
if let Some(meaningless) = meaningless {
|
||||||
|
let meaningful_toks = raw_toks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, tok)| (!meaningless.contains(tok.0)).then_some(i))
|
||||||
|
.collect_into(&mut meaningful_toks);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
raw: raw_toks,
|
||||||
|
semantically_meaningless: meaningless.unwrap_or_default(),
|
||||||
|
meaningful_toks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self, idx: usize) -> SyntaxKind {
|
||||||
|
let Some(meaningful_idx) = self.meaningful_toks.get(idx) else {
|
||||||
|
return SyntaxKind::SYNTAX_EOF;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.raw.get(*meaningful_idx).unwrap().0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preceding_meaningless(&self, idx: usize) -> usize {
|
||||||
|
assert!(self.meaningful_toks.len() > idx);
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
// maybe should be `self.meaningful_toks[idx]` instead??
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.meaningful_toks[idx] - self.meaningful_toks[idx - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the count of meaningless tokens at the end of the file.
|
||||||
|
pub fn meaningless_tail_len(&self) -> usize {
|
||||||
|
self.raw.len() - (self.meaningful_toks.last().unwrap() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dissolve(self) -> (Vec<(SyntaxKind, &'src str)>, EnumSet<SyntaxKind>) {
|
||||||
|
let Self {
|
||||||
|
raw,
|
||||||
|
semantically_meaningless,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
(raw, semantically_meaningless)
|
||||||
|
}
|
||||||
|
}
|
97
crates/pawarser/src/parser/marker.rs
Normal file
97
crates/pawarser/src/parser/marker.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use drop_bomb::DropBomb;
|
||||||
|
use rowan::SyntaxKind;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
error::SyntaxError,
|
||||||
|
event::{Event, NodeKind},
|
||||||
|
Parser, SyntaxElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Marker {
|
||||||
|
pos: usize,
|
||||||
|
bomb: DropBomb,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Marker {
|
||||||
|
pub(super) fn new(pos: usize, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
bomb: DropBomb::new(format!("Marker {name} must be completed or abandoned.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_node<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>(
|
||||||
|
mut self,
|
||||||
|
p: &mut Parser<SyntaxKind, SyntaxErr>,
|
||||||
|
kind: NodeKind<SyntaxKind, SyntaxErr>,
|
||||||
|
) -> CompletedMarker<SyntaxKind, SyntaxErr> {
|
||||||
|
self.bomb.defuse();
|
||||||
|
|
||||||
|
match &mut p.events[self.pos] {
|
||||||
|
Event::Start { kind: slot, .. } => *slot = kind.clone(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push_ev(Event::Finish);
|
||||||
|
CompletedMarker {
|
||||||
|
pos: self.pos,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>(
|
||||||
|
self,
|
||||||
|
p: &mut Parser<SyntaxKind, SyntaxErr>,
|
||||||
|
kind: SyntaxKind,
|
||||||
|
) -> CompletedMarker<SyntaxKind, SyntaxErr> {
|
||||||
|
self.close_node(p, NodeKind::Syntax(kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>(
|
||||||
|
self,
|
||||||
|
p: &mut Parser<SyntaxKind, SyntaxErr>,
|
||||||
|
kind: SyntaxErr,
|
||||||
|
) -> CompletedMarker<SyntaxKind, SyntaxErr> {
|
||||||
|
self.close_node(p, NodeKind::Error(kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abandon<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>(
|
||||||
|
mut self,
|
||||||
|
p: &mut Parser<SyntaxKind, SyntaxErr>,
|
||||||
|
) {
|
||||||
|
self.bomb.defuse();
|
||||||
|
|
||||||
|
// clean up empty tombstone event from marker
|
||||||
|
if self.pos == p.events.len() - 1 {
|
||||||
|
match p.events.pop() {
|
||||||
|
Some(Event::Start {
|
||||||
|
kind: NodeKind::Tombstone,
|
||||||
|
forward_parent: None,
|
||||||
|
}) => (),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CompletedMarker<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> {
|
||||||
|
pos: usize,
|
||||||
|
kind: NodeKind<SyntaxKind, SyntaxErr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> CompletedMarker<SyntaxKind, SyntaxErr> {
|
||||||
|
pub fn precede(self, p: &mut Parser<SyntaxKind, SyntaxErr>, name: &str) -> Marker {
|
||||||
|
let new_pos = p.start(name);
|
||||||
|
|
||||||
|
match &mut p.events[self.pos] {
|
||||||
|
Event::Start { forward_parent, .. } => {
|
||||||
|
// point forward parent of the node this marker completed to the new node
|
||||||
|
// will later be used to make the new node a parent of the current node.
|
||||||
|
*forward_parent = Some(new_pos.pos - self.pos)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
new_pos
|
||||||
|
}
|
||||||
|
}
|
73
crates/pawarser/src/parser/output.rs
Normal file
73
crates/pawarser/src/parser/output.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use std::{fmt, marker::PhantomData};
|
||||||
|
|
||||||
|
use rowan::{GreenNode, GreenNodeData, GreenTokenData, NodeOrToken};
|
||||||
|
|
||||||
|
use crate::{SyntaxElement, SyntaxError};
|
||||||
|
|
||||||
|
pub struct ParserOutput<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> {
|
||||||
|
pub green_node: GreenNode,
|
||||||
|
pub errors: Vec<SyntaxErr>,
|
||||||
|
pub(super) _syntax_kind: PhantomData<SyntaxKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError> std::fmt::Debug
|
||||||
|
for ParserOutput<SyntaxKind, SyntaxErr>
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut errs: Vec<&SyntaxErr> = self.errors.iter().collect();
|
||||||
|
errs.reverse();
|
||||||
|
debug_print_output::<SyntaxKind, SyntaxErr>(
|
||||||
|
NodeOrToken::Node(&self.green_node),
|
||||||
|
f,
|
||||||
|
0,
|
||||||
|
&mut errs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_print_output<SyntaxKind: SyntaxElement, SyntaxErr: SyntaxError>(
|
||||||
|
node: NodeOrToken<&GreenNodeData, &GreenTokenData>,
|
||||||
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
lvl: i32,
|
||||||
|
errs: &mut Vec<&SyntaxErr>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
for _ in 0..lvl {
|
||||||
|
f.write_str(" ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let maybe_newline = if f.alternate() { "\n" } else { " " };
|
||||||
|
|
||||||
|
match node {
|
||||||
|
NodeOrToken::Node(n) => {
|
||||||
|
let kind: SyntaxKind = node.kind().into();
|
||||||
|
if kind != SyntaxKind::SYNTAX_ERROR {
|
||||||
|
write!(f, "{:?} {{{maybe_newline}", kind)?;
|
||||||
|
} else {
|
||||||
|
let err = errs
|
||||||
|
.pop()
|
||||||
|
.expect("all error syntax nodes should correspond to an error");
|
||||||
|
|
||||||
|
write!(f, "{:?}: {err:?} {{{maybe_newline}", kind)?;
|
||||||
|
}
|
||||||
|
for c in n.children() {
|
||||||
|
debug_print_output::<SyntaxKind, SyntaxErr>(c, f, lvl + 1, errs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.alternate() {
|
||||||
|
for _ in 0..lvl {
|
||||||
|
f.write_str(" ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "}}{maybe_newline}")
|
||||||
|
}
|
||||||
|
NodeOrToken::Token(t) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{:?} {:?};{maybe_newline}",
|
||||||
|
Into::<SyntaxKind>::into(t.kind()),
|
||||||
|
t.text()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
crates/prowocessing/Cargo.toml
Normal file
13
crates/prowocessing/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "prowocessing"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
image = "0.24.8"
|
||||||
|
palette = "0.7.4"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
2
crates/prowocessing/src/experimental.rs
Normal file
2
crates/prowocessing/src/experimental.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod enum_based;
|
||||||
|
pub mod trait_based;
|
64
crates/prowocessing/src/experimental/enum_based.rs
Normal file
64
crates/prowocessing/src/experimental/enum_based.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
pub enum Instruction {
|
||||||
|
Uppercase,
|
||||||
|
Lowercase,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Pipeline {
|
||||||
|
pipeline: Vec<fn(String) -> String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline {
|
||||||
|
pub fn run(&self, val: String) -> String {
|
||||||
|
let mut current = val;
|
||||||
|
|
||||||
|
for instr in &self.pipeline {
|
||||||
|
current = instr(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PipelineBuilder {
|
||||||
|
pipeline: Vec<Instruction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipelineBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pipeline: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn insert(mut self, instr: Instruction) -> Self {
|
||||||
|
self.pipeline.push(instr);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&self) -> Pipeline {
|
||||||
|
fn uppercase(v: String) -> String {
|
||||||
|
str::to_uppercase(&v)
|
||||||
|
}
|
||||||
|
fn lowercase(v: String) -> String {
|
||||||
|
str::to_lowercase(&v)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
|
||||||
|
for item in &self.pipeline {
|
||||||
|
res.push(match item {
|
||||||
|
Instruction::Uppercase => uppercase,
|
||||||
|
Instruction::Lowercase => lowercase,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Pipeline { pipeline: res }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PipelineBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
11
crates/prowocessing/src/experimental/trait_based.rs
Normal file
11
crates/prowocessing/src/experimental/trait_based.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
//! An experiment for a hyper-modular trait-based architecture.
|
||||||
|
//!
|
||||||
|
//! Patterns defining this (or well, which I reference a lot while writing this):
|
||||||
|
//! - [Command pattern using trait objects](https://rust-unofficial.github.io/patterns/patterns/behavioural/command.html)
|
||||||
|
//! - [Builder pattern](https://rust-unofficial.github.io/patterns/patterns/creational/builder.html)
|
||||||
|
|
||||||
|
pub mod data;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod element;
|
||||||
|
pub mod ops;
|
||||||
|
pub mod pipeline;
|
5
crates/prowocessing/src/experimental/trait_based/data.rs
Normal file
5
crates/prowocessing/src/experimental/trait_based/data.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//! Definitions of the data transfer and storage types.
|
||||||
|
|
||||||
|
pub mod io;
|
||||||
|
|
||||||
|
pub mod raw;
|
53
crates/prowocessing/src/experimental/trait_based/data/io.rs
Normal file
53
crates/prowocessing/src/experimental/trait_based/data/io.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//! Types for element and pipeline IO
|
||||||
|
|
||||||
|
use std::{borrow::ToOwned, convert::Into};
|
||||||
|
|
||||||
|
use super::raw::Data;
|
||||||
|
|
||||||
|
/// Newtype struct with borrowed types for pipeline/element inputs, so that doesn't force a move or clone
|
||||||
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
pub struct Inputs<'a>(pub Vec<&'a Data>);
|
||||||
|
|
||||||
|
impl<'a> From<Vec<&'a Data>> for Inputs<'a> {
|
||||||
|
fn from(value: Vec<&'a Data>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Into<&'a Data>> From<T> for Inputs<'a> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(vec![value.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Outputs> for Inputs<'a> {
|
||||||
|
fn from(value: &'a Outputs) -> Self {
|
||||||
|
Self(value.0.iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for pipeline/element outputs
|
||||||
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
pub struct Outputs(pub Vec<Data>);
|
||||||
|
|
||||||
|
impl Outputs {
|
||||||
|
/// consume self and return inner value(s)
|
||||||
|
pub fn into_inner(self) -> Vec<Data> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Vec<Data>> for Outputs {
|
||||||
|
fn from(value: Vec<Data>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Into<Data>> From<T> for Outputs {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(vec![value.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Inputs<'_>> for Outputs {
|
||||||
|
fn from(value: Inputs) -> Self {
|
||||||
|
Self(value.0.into_iter().map(ToOwned::to_owned).collect())
|
||||||
|
}
|
||||||
|
}
|
20
crates/prowocessing/src/experimental/trait_based/data/raw.rs
Normal file
20
crates/prowocessing/src/experimental/trait_based/data/raw.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
//! Dynamic data storage and transfer types for use in [`io`]
|
||||||
|
|
||||||
|
// Dynamic data type
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Data {
|
||||||
|
String(String),
|
||||||
|
Int(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Data {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for Data {
|
||||||
|
fn from(value: i32) -> Self {
|
||||||
|
Self::Int(value)
|
||||||
|
}
|
||||||
|
}
|
29
crates/prowocessing/src/experimental/trait_based/element.rs
Normal file
29
crates/prowocessing/src/experimental/trait_based/element.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
//! The trait and type representations
|
||||||
|
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::experimental::trait_based::data::io::Inputs;
|
||||||
|
|
||||||
|
use super::data::io::Outputs;
|
||||||
|
|
||||||
|
pub(crate) trait PipelineElement {
|
||||||
|
/// return a static runner function pointer to avoid dynamic dispatch during pipeline execution - Types MUST match the signature
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs;
|
||||||
|
/// return the signature of the element
|
||||||
|
fn signature(&self) -> ElementSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type signature for an element used for static checking
|
||||||
|
pub(crate) struct ElementSignature {
|
||||||
|
pub inputs: Vec<TypeId>,
|
||||||
|
pub outputs: Vec<TypeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! signature {
|
||||||
|
($($inputs:ty),+ => $($outputs:ty),+) => (
|
||||||
|
ElementSignature {
|
||||||
|
inputs: vec![$(std::any::TypeId::of::<$inputs>(), )+],
|
||||||
|
outputs: vec![$(std::any::TypeId::of::<$outputs>(), )+]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
7
crates/prowocessing/src/experimental/trait_based/ops.rs
Normal file
7
crates/prowocessing/src/experimental/trait_based/ops.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
mod num;
|
||||||
|
mod str;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub(crate) use super::num::*;
|
||||||
|
pub(crate) use super::str::*;
|
||||||
|
}
|
62
crates/prowocessing/src/experimental/trait_based/ops/num.rs
Normal file
62
crates/prowocessing/src/experimental/trait_based/ops/num.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//! Operations on numeric data
|
||||||
|
use core::panic;
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::experimental::trait_based::{
|
||||||
|
data::{
|
||||||
|
io::{Inputs, Outputs},
|
||||||
|
raw::Data,
|
||||||
|
},
|
||||||
|
element::{ElementSignature, PipelineElement},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Addition
|
||||||
|
pub struct Add(pub i32);
|
||||||
|
impl PipelineElement for Add {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::Int(i0), Data::Int(i1), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
(i0 + i1).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(i32, i32 => i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtraction
|
||||||
|
pub struct Subtract(pub i32);
|
||||||
|
impl PipelineElement for Subtract {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::Int(i0), Data::Int(i1), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
(i0 + i1).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(i32, i32 => i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn input to string
|
||||||
|
pub struct Stringify;
|
||||||
|
impl PipelineElement for Stringify {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::Int(int), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
int.to_string().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(i32 => String)
|
||||||
|
}
|
||||||
|
}
|
59
crates/prowocessing/src/experimental/trait_based/ops/str.rs
Normal file
59
crates/prowocessing/src/experimental/trait_based/ops/str.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
//! Operation on String/text data
|
||||||
|
use crate::experimental::trait_based::{
|
||||||
|
data::{
|
||||||
|
io::{Inputs, Outputs},
|
||||||
|
raw::Data,
|
||||||
|
},
|
||||||
|
element::{ElementSignature, PipelineElement},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Concatenate the inputs
|
||||||
|
pub struct Concatenate(pub String);
|
||||||
|
impl PipelineElement for Concatenate {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::String(s0), Data::String(s1), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
format!("{s0}{s1}").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(String, String => String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn input text to uppercase
|
||||||
|
pub struct Upper;
|
||||||
|
impl PipelineElement for Upper {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::String(s), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
s.to_uppercase().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(String => String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn input text to lowercase
|
||||||
|
pub struct Lower;
|
||||||
|
impl PipelineElement for Lower {
|
||||||
|
fn runner(&self) -> fn(&Inputs) -> Outputs {
|
||||||
|
|input| {
|
||||||
|
let [Data::String(s), ..] = input.0[..] else {
|
||||||
|
panic!("Invalid data passed")
|
||||||
|
};
|
||||||
|
s.to_lowercase().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> ElementSignature {
|
||||||
|
signature!(String => String)
|
||||||
|
}
|
||||||
|
}
|
107
crates/prowocessing/src/experimental/trait_based/pipeline.rs
Normal file
107
crates/prowocessing/src/experimental/trait_based/pipeline.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use super::data::io::{Inputs, Outputs};
|
||||||
|
use super::element::PipelineElement;
|
||||||
|
use super::ops::prelude::*;
|
||||||
|
|
||||||
|
/// Builder for the pipelines that are actually run
|
||||||
|
///
|
||||||
|
/// TODO:
|
||||||
|
/// - Bind additional inputs if instruction has more then one and is passd without any additional
|
||||||
|
/// - allow binding to pointers to other pipelines?
|
||||||
|
/// - allow referencing earlier data
|
||||||
|
pub struct PipelineBuilder {
|
||||||
|
elements: Vec<Box<dyn PipelineElement>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipelineBuilder {
|
||||||
|
/// Create new, empty builder
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
elements: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert element into pipeline
|
||||||
|
fn insert<T: PipelineElement + 'static>(mut self, el: T) -> Self {
|
||||||
|
if let Some(previous_item) = self.elements.last() {
|
||||||
|
assert_eq!(
|
||||||
|
previous_item.signature().outputs[0],
|
||||||
|
el.signature().inputs[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.elements.push(Box::new(el));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert string concatenattion element
|
||||||
|
#[must_use]
|
||||||
|
pub fn concatenate(self, sec: String) -> Self {
|
||||||
|
self.insert(Concatenate(sec))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert string uppercase element
|
||||||
|
#[must_use]
|
||||||
|
pub fn upper(self) -> Self {
|
||||||
|
self.insert(Upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert string lowercase element
|
||||||
|
#[must_use]
|
||||||
|
pub fn lower(self) -> Self {
|
||||||
|
self.insert(Lower)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert numeric addition element
|
||||||
|
#[must_use]
|
||||||
|
#[allow(
|
||||||
|
clippy::should_implement_trait,
|
||||||
|
reason = "is not equivalent to addition"
|
||||||
|
)]
|
||||||
|
pub fn add(self, sec: i32) -> Self {
|
||||||
|
self.insert(Add(sec))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert numeric subtraction element
|
||||||
|
#[must_use]
|
||||||
|
pub fn subtract(self, sec: i32) -> Self {
|
||||||
|
self.insert(Subtract(sec))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert stringify element
|
||||||
|
#[must_use]
|
||||||
|
pub fn stringify(self) -> Self {
|
||||||
|
self.insert(Stringify)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the pipeline. Doesn't check again - `insert` should verify correctness.
|
||||||
|
pub fn build(&self) -> Pipeline {
|
||||||
|
let mut r = Vec::new();
|
||||||
|
|
||||||
|
self.elements.iter().for_each(|el| r.push(el.runner()));
|
||||||
|
|
||||||
|
Pipeline { runners: r }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PipelineBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runnable pipeline - at the core of this library
|
||||||
|
pub struct Pipeline {
|
||||||
|
runners: Vec<fn(&Inputs) -> Outputs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline {
|
||||||
|
/// run the pipeline
|
||||||
|
pub fn run(&self, inputs: Inputs) -> Outputs {
|
||||||
|
let mut out: Outputs = inputs.into();
|
||||||
|
|
||||||
|
for runner in &self.runners {
|
||||||
|
out = runner(&(&out).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
40
crates/prowocessing/src/lib.rs
Normal file
40
crates/prowocessing/src/lib.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//! # This is the image processing library for iOwO
|
||||||
|
//!
|
||||||
|
//! One of the design goals for this library is, however, to be a simple, generic image processing library.
|
||||||
|
//! For now, it's just indev... lets see what comes of it!
|
||||||
|
#![feature(lint_reasons)]
|
||||||
|
|
||||||
|
/// just some experiments, to test whether the architecture i want is even possible (or how to do it). probably temporary.
|
||||||
|
/// Gonna first try string processing...
|
||||||
|
pub mod experimental;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::experimental::{
|
||||||
|
enum_based,
|
||||||
|
trait_based::{self, data::io::Outputs},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enums() {
|
||||||
|
let builder = enum_based::PipelineBuilder::new().insert(enum_based::Instruction::Uppercase);
|
||||||
|
let upr = builder.build();
|
||||||
|
let upr_lowr = builder.insert(enum_based::Instruction::Lowercase).build();
|
||||||
|
|
||||||
|
assert_eq!(upr.run(String::from("Test")), String::from("TEST"));
|
||||||
|
assert_eq!(upr_lowr.run(String::from("Test")), String::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add() {
|
||||||
|
let pipe = trait_based::pipeline::PipelineBuilder::new()
|
||||||
|
.add(0)
|
||||||
|
.stringify()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pipe.run(vec![&2.into(), &3.into()].into()),
|
||||||
|
Outputs(vec![String::from("5").into()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
15
crates/svg-filters/Cargo.toml
Normal file
15
crates/svg-filters/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "svg-filters"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
csscolorparser = "0.6.2"
|
||||||
|
indexmap = "2.2.5"
|
||||||
|
petgraph = { workspace = true }
|
||||||
|
quick-xml = { version = "0.31.0", features = ["serialize"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
158
crates/svg-filters/src/codegen.rs
Normal file
158
crates/svg-filters/src/codegen.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use std::{
|
||||||
|
cmp,
|
||||||
|
collections::{BTreeSet, HashMap},
|
||||||
|
fmt::Display,
|
||||||
|
io::Read,
|
||||||
|
ops::Not,
|
||||||
|
};
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use petgraph::{
|
||||||
|
algo::toposort,
|
||||||
|
graph::DiGraph,
|
||||||
|
prelude::{EdgeIndex, NodeIndex},
|
||||||
|
};
|
||||||
|
use quick_xml::ElementWriter;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
types::{
|
||||||
|
graph::{edge::Edge, FilterGraph, NodeInput},
|
||||||
|
nodes::{primitives::WriteElement, CommonAttrs},
|
||||||
|
},
|
||||||
|
Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::error::CodegenError;
|
||||||
|
|
||||||
|
pub struct SvgDocument {
|
||||||
|
filters: HashMap<String, FilterGraph>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SvgDocument {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
filters: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used, reason = "we literally just did the insertion")]
|
||||||
|
pub fn create_filter(&mut self, id: impl ToString) -> &mut FilterGraph {
|
||||||
|
let filter = FilterGraph::new();
|
||||||
|
|
||||||
|
self.filters.insert(id.to_string(), filter);
|
||||||
|
self.filters.get_mut(&id.to_string()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_svg_pretty(&self) -> String {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let doc_writer = quick_xml::Writer::new_with_indent(&mut result, b' ', 2);
|
||||||
|
|
||||||
|
self.generate(doc_writer);
|
||||||
|
|
||||||
|
String::from_utf8_lossy(&result).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_svg(&self) -> String {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let doc_writer = quick_xml::Writer::new(&mut result);
|
||||||
|
|
||||||
|
self.generate(doc_writer);
|
||||||
|
|
||||||
|
String::from_utf8_lossy(&result).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate(&self, mut doc_writer: quick_xml::Writer<&mut Vec<u8>>) {
|
||||||
|
doc_writer
|
||||||
|
.create_element("svg")
|
||||||
|
.write_inner_content(|writer| {
|
||||||
|
self.filters
|
||||||
|
.iter()
|
||||||
|
.try_fold(writer, Self::gen_filter)
|
||||||
|
.map(|_| {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_filter<'w, 'r>(
|
||||||
|
writer: &'w mut quick_xml::Writer<&'r mut Vec<u8>>,
|
||||||
|
(id, graph): (&String, &FilterGraph),
|
||||||
|
) -> Result<&'w mut quick_xml::Writer<&'r mut Vec<u8>>, CodegenError> {
|
||||||
|
writer
|
||||||
|
.create_element("filter")
|
||||||
|
.with_attribute(("id", id.as_str()))
|
||||||
|
.write_inner_content(|writer| Self::graph_to_svg(writer, graph))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_to_svg(
|
||||||
|
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||||
|
graph: &FilterGraph,
|
||||||
|
) -> Result<(), CodegenError> {
|
||||||
|
let sorted = toposort(&graph.dag, None).expect("no cycles allowed in a DAG");
|
||||||
|
sorted
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|node_idx| {
|
||||||
|
graph
|
||||||
|
.dag
|
||||||
|
.node_weight(node_idx)
|
||||||
|
.and_then(|node| node.primitive())
|
||||||
|
.map(|(primitive, common_attrs)| (node_idx, primitive, common_attrs))
|
||||||
|
})
|
||||||
|
.try_fold(writer, |writer, (node_idx, primitive, common_attrs)| {
|
||||||
|
primitive.element_writer(
|
||||||
|
writer,
|
||||||
|
*common_attrs,
|
||||||
|
graph
|
||||||
|
.inputs(node_idx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.collect(),
|
||||||
|
graph
|
||||||
|
.outputs(node_idx)
|
||||||
|
.is_empty()
|
||||||
|
.not()
|
||||||
|
.then_some(format!("r{}", node_idx.index())),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convenience method to avoid fuckups during future changes
|
||||||
|
fn format_edge_idx(idx: EdgeIndex) -> String {
|
||||||
|
format!("edge{}", idx.index())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_node_idx(node_idx: NodeIndex) -> String {
|
||||||
|
format!("r{}", node_idx.index())
|
||||||
|
}
|
||||||
|
|
||||||
|
mod error {
|
||||||
|
use std::{error::Error, fmt::Display};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CodegenError {
|
||||||
|
QuickXmlError(quick_xml::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<quick_xml::Error> for CodegenError {
|
||||||
|
fn from(value: quick_xml::Error) -> Self {
|
||||||
|
Self::QuickXmlError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CodegenError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
CodegenError::QuickXmlError(e) => e.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for CodegenError {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SvgDocument {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
40
crates/svg-filters/src/lib.rs
Normal file
40
crates/svg-filters/src/lib.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#![feature(lint_reasons)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
pub mod util {
|
||||||
|
macro_rules! gen_attr {
|
||||||
|
($name:literal = $out:expr) => {
|
||||||
|
quick_xml::events::attributes::Attribute {
|
||||||
|
key: quick_xml::name::QName($name),
|
||||||
|
value: std::borrow::Cow::from(($out).to_string().into_bytes()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! gen_attrs {
|
||||||
|
($($name:literal: $out:expr),+) => {
|
||||||
|
vec![
|
||||||
|
$(gen_attr!($name = $out)),+
|
||||||
|
]
|
||||||
|
};
|
||||||
|
($($cond:expr => $name:literal: $out:expr),+) => {
|
||||||
|
{
|
||||||
|
let mut r = Vec::new();
|
||||||
|
$(if $cond {
|
||||||
|
r.push(gen_attr!($name = $out));
|
||||||
|
})+
|
||||||
|
r
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($other:ident; $($cond:expr => $name:literal: $out:expr),+) => {
|
||||||
|
$other.append(&mut gen_attrs![$($cond => $name: $out),+]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod codegen;
|
||||||
|
pub mod types;
|
||||||
|
pub use types::nodes::Node;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
65
crates/svg-filters/src/main.rs
Normal file
65
crates/svg-filters/src/main.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use svg_filters::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::{
|
||||||
|
primitives::{
|
||||||
|
blend::BlendMode,
|
||||||
|
color_matrix::ColorMatrixType,
|
||||||
|
component_transfer::TransferFn,
|
||||||
|
displacement_map::Channel,
|
||||||
|
turbulence::{NoiseType, StitchTiles},
|
||||||
|
},
|
||||||
|
standard_input::StandardInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let f = doc.create_filter("cmyk-chromabb");
|
||||||
|
|
||||||
|
let noise = f.turbulence(0., 0.1, 2, 0, StitchTiles::Stitch, NoiseType::FractalNoise);
|
||||||
|
let noise = f.component_transfer_rgba(
|
||||||
|
noise,
|
||||||
|
TransferFn::Discrete {
|
||||||
|
table_values: vec![0., 0.2, 0.4, 0.6, 0.8, 1.],
|
||||||
|
},
|
||||||
|
TransferFn::Discrete {
|
||||||
|
table_values: vec![0., 0.2, 0.4, 0.6, 0.8, 1.],
|
||||||
|
},
|
||||||
|
TransferFn::Discrete {
|
||||||
|
table_values: vec![0., 0.2, 0.4, 0.6, 0.8, 1.],
|
||||||
|
},
|
||||||
|
TransferFn::Linear {
|
||||||
|
slope: 0.,
|
||||||
|
intercept: 0.5,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let cyan = f.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 1., 0., 0., 0., //
|
||||||
|
0., 0., 1., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
let cyan = f.offset(cyan, 25., 0.);
|
||||||
|
let cyan = f.displacement_map(cyan, noise, 50., Channel::R, Channel::A);
|
||||||
|
|
||||||
|
let magenta = f.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
1., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 1., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
let magenta = f.displacement_map(magenta, noise, 50., Channel::R, Channel::A);
|
||||||
|
let magenta = f.offset(magenta, -25., 0.);
|
||||||
|
|
||||||
|
f.blend(cyan, magenta, BlendMode::Screen);
|
||||||
|
|
||||||
|
println!("{}", doc.generate_svg_pretty());
|
||||||
|
}
|
17
crates/svg-filters/src/tests.rs
Normal file
17
crates/svg-filters/src/tests.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
mod blend;
|
||||||
|
mod color_matrix;
|
||||||
|
mod complex;
|
||||||
|
mod component_transfer;
|
||||||
|
mod displacement_map;
|
||||||
|
mod flood;
|
||||||
|
mod gaussian_blur;
|
||||||
|
mod offset;
|
||||||
|
mod turbulence;
|
||||||
|
mod composite {}
|
||||||
|
mod convolve_matrix {}
|
||||||
|
mod diffuse_lighting {}
|
||||||
|
mod image {}
|
||||||
|
mod merge {}
|
||||||
|
mod morphology {}
|
||||||
|
mod specular_lighting {}
|
||||||
|
mod tile {}
|
20
crates/svg-filters/src/tests/blend.rs
Normal file
20
crates/svg-filters/src/tests/blend.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::{primitives::blend::BlendMode, standard_input::StandardInput},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offset_blend() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let blend = doc.create_filter("blend");
|
||||||
|
|
||||||
|
let offset0 = blend.offset(StandardInput::SourceGraphic, 100., 0.);
|
||||||
|
let offset1 = blend.offset(StandardInput::SourceGraphic, -100., 0.);
|
||||||
|
blend.blend(offset0, offset1, BlendMode::Multiply);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="blend"><feOffset dx="-100" dy="0" in="SourceGraphic" result="r7"/><feOffset dx="100" dy="0" in="SourceGraphic" result="r6"/><feBlend mode="multiply" in="r6" in2="r7"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
25
crates/svg-filters/src/tests/color_matrix.rs
Normal file
25
crates/svg-filters/src/tests/color_matrix.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::{primitives::color_matrix::ColorMatrixType, standard_input::StandardInput},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_greyscale_channel_extraction() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
let greyscale = doc.create_filter("greyscale");
|
||||||
|
|
||||||
|
greyscale.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
1., 0., 0., 0., 0., //
|
||||||
|
1., 0., 0., 0., 0., //
|
||||||
|
1., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="greyscale"><feColorMatrix values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0" in="SourceGraphic"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
51
crates/svg-filters/src/tests/complex.rs
Normal file
51
crates/svg-filters/src/tests/complex.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::{primitives::color_matrix::ColorMatrixType, standard_input::StandardInput},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chrom_abb() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
let chromabb = doc.create_filter("chromabb_gen");
|
||||||
|
|
||||||
|
let chan_r = chromabb.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
1., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
let offset_r = chromabb.offset(chan_r, 25., 0.);
|
||||||
|
let blur_r = chromabb.gaussian_blur_xy(offset_r, 5, 0);
|
||||||
|
|
||||||
|
let chan_b = chromabb.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 1., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
let offset_b = chromabb.offset(chan_b, -25., 0.);
|
||||||
|
let blur_b = chromabb.gaussian_blur_xy(offset_b, 5, 0);
|
||||||
|
|
||||||
|
let composite_rb = chromabb.composite_arithmetic(blur_r, blur_b, 0., 1., 1., 0.);
|
||||||
|
|
||||||
|
let chan_g = chromabb.color_matrix(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
ColorMatrixType::Matrix(Box::new([
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 1., 0., 0., 0., //
|
||||||
|
0., 0., 0., 0., 0., //
|
||||||
|
0., 0., 0., 1., 0.,
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
chromabb.composite_arithmetic(composite_rb, chan_g, 0., 1., 1., 0.);
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="chromabb_gen"><feColorMatrix values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceGraphic" result="r13"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" in="SourceGraphic" result="r9"/><feOffset dx="-25" dy="0" in="r9" result="r10"/><feGaussianBlur stdDeviation="5 0" in="r10" result="r11"/><feColorMatrix values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceGraphic" result="r6"/><feOffset dx="25" dy="0" in="r6" result="r7"/><feGaussianBlur stdDeviation="5 0" in="r7" result="r8"/><feComposite operator="arithmetic" k1="0" k2="1" k3="1" k4="0" in="r8" in2="r11" result="r12"/><feComposite operator="arithmetic" k1="0" k2="1" k3="1" k4="0" in="r12" in2="r13"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
36
crates/svg-filters/src/tests/component_transfer.rs
Normal file
36
crates/svg-filters/src/tests/component_transfer.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::primitives::{
|
||||||
|
component_transfer::{ComponentTransfer, TransferFn},
|
||||||
|
FePrimitive,
|
||||||
|
},
|
||||||
|
Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_comp_trans_simple() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let comptrans = doc.create_filter("comp_trans");
|
||||||
|
|
||||||
|
comptrans.add_node(Node::simple(FePrimitive::ComponentTransfer(
|
||||||
|
ComponentTransfer {
|
||||||
|
func_r: TransferFn::Table {
|
||||||
|
table_values: vec![0., 0.1, 0.4, 0.9],
|
||||||
|
},
|
||||||
|
func_g: TransferFn::Discrete {
|
||||||
|
table_values: vec![0.1, 0.3, 0.5, 0.7, 0.9],
|
||||||
|
},
|
||||||
|
func_b: TransferFn::Linear {
|
||||||
|
slope: 1.0,
|
||||||
|
intercept: 0.75,
|
||||||
|
},
|
||||||
|
func_a: TransferFn::Identity,
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="comp_trans"><feComponentTransfer><feFuncR type="table" tableValues="0 0.1 0.4 0.9"/><feFuncG type="discrete" tableValues="0.1 0.3 0.5 0.7 0.9"/><feFuncB type="linear" slope="1" intercept="0.75"/><feFuncA type="identity"/></feComponentTransfer></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
32
crates/svg-filters/src/tests/displacement_map.rs
Normal file
32
crates/svg-filters/src/tests/displacement_map.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::{
|
||||||
|
primitives::{
|
||||||
|
displacement_map::Channel,
|
||||||
|
turbulence::{NoiseType, StitchTiles},
|
||||||
|
},
|
||||||
|
standard_input::StandardInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_displacement_map_simple() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let displace = doc.create_filter("displace");
|
||||||
|
|
||||||
|
let simple_noise =
|
||||||
|
displace.turbulence(0.01, 0.01, 1, 0, StitchTiles::Stitch, NoiseType::Turbulence);
|
||||||
|
displace.displacement_map(
|
||||||
|
StandardInput::SourceGraphic,
|
||||||
|
simple_noise,
|
||||||
|
128.,
|
||||||
|
Channel::R,
|
||||||
|
Channel::R,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="displace"><feTurbulence baseFrequency="0.01 0.01" stitchTiles="stitch" result="r6"/><feDisplacementMap scale="128" xChannelSelector="R" yChannelSelector="R" in="SourceGraphic" in2="r6"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
17
crates/svg-filters/src/tests/flood.rs
Normal file
17
crates/svg-filters/src/tests/flood.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use csscolorparser::Color;
|
||||||
|
|
||||||
|
use crate::codegen::SvgDocument;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flood_simple() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let turbdispl = doc.create_filter("noiseDisplace");
|
||||||
|
|
||||||
|
turbdispl.flood(Color::new(0.9, 0.7, 0.85, 1.), 1.);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r##"<svg><filter id="noiseDisplace"><feFlood flood-color="#e6b3d9" flood-opacity="1"/></filter></svg>"##
|
||||||
|
);
|
||||||
|
}
|
13
crates/svg-filters/src/tests/gaussian_blur.rs
Normal file
13
crates/svg-filters/src/tests/gaussian_blur.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::{codegen::SvgDocument, types::nodes::standard_input::StandardInput};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_blur() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
let blur = doc.create_filter("blur");
|
||||||
|
|
||||||
|
blur.gaussian_blur_xy(StandardInput::SourceGraphic, 30, 30);
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="blur"><feGaussianBlur stdDeviation="30 30" in="SourceGraphic"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
14
crates/svg-filters/src/tests/offset.rs
Normal file
14
crates/svg-filters/src/tests/offset.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::{codegen::SvgDocument, types::nodes::standard_input::StandardInput};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offset_simple() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
let offset = doc.create_filter("offset");
|
||||||
|
|
||||||
|
offset.offset(StandardInput::SourceGraphic, 25., -25.);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="offset"><feOffset dx="25" dy="-25" in="SourceGraphic"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
25
crates/svg-filters/src/tests/turbulence.rs
Normal file
25
crates/svg-filters/src/tests/turbulence.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::{
|
||||||
|
codegen::SvgDocument,
|
||||||
|
types::nodes::primitives::turbulence::{NoiseType, StitchTiles},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_turbulence() {
|
||||||
|
let mut doc = SvgDocument::new();
|
||||||
|
|
||||||
|
let noise = doc.create_filter("noise");
|
||||||
|
|
||||||
|
noise.turbulence(
|
||||||
|
0.01,
|
||||||
|
0.01,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
StitchTiles::Stitch,
|
||||||
|
NoiseType::FractalNoise,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
doc.generate_svg(),
|
||||||
|
r#"<svg><filter id="noise"><feTurbulence baseFrequency="0.01 0.01" stitchTiles="stitch" type="fractalNoise"/></filter></svg>"#
|
||||||
|
);
|
||||||
|
}
|
6
crates/svg-filters/src/types.rs
Normal file
6
crates/svg-filters/src/types.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod length;
|
||||||
|
pub mod nodes;
|
||||||
|
|
||||||
|
// pub mod old;
|
||||||
|
|
||||||
|
pub mod graph;
|
143
crates/svg-filters/src/types/graph.rs
Normal file
143
crates/svg-filters/src/types/graph.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use petgraph::{prelude::NodeIndex, prelude::*};
|
||||||
|
|
||||||
|
use crate::Node;
|
||||||
|
|
||||||
|
use super::nodes::standard_input::StandardInput;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FilterGraph {
|
||||||
|
pub dag: DiGraph<Node, ()>,
|
||||||
|
source_graphic_idx: NodeIndex,
|
||||||
|
source_alpha_idx: NodeIndex,
|
||||||
|
background_image_idx: NodeIndex,
|
||||||
|
background_alpha_idx: NodeIndex,
|
||||||
|
fill_paint_idx: NodeIndex,
|
||||||
|
stroke_paint_idx: NodeIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FilterGraph {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum NodeInput {
|
||||||
|
Standard(StandardInput),
|
||||||
|
Idx(NodeIndex),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for NodeInput {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
NodeInput::Standard(s) => Debug::fmt(s, f),
|
||||||
|
NodeInput::Idx(idx) => write!(f, "r{}", idx.index()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StandardInput> for NodeInput {
|
||||||
|
fn from(value: StandardInput) -> Self {
|
||||||
|
Self::Standard(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NodeIndex> for NodeInput {
|
||||||
|
fn from(value: NodeIndex) -> Self {
|
||||||
|
Self::Idx(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterGraph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut dag = DiGraph::new();
|
||||||
|
|
||||||
|
let source_graphic_idx = dag.add_node(Node::StdInput(StandardInput::SourceGraphic));
|
||||||
|
let source_alpha_idx = dag.add_node(Node::StdInput(StandardInput::SourceAlpha));
|
||||||
|
let background_image_idx = dag.add_node(Node::StdInput(StandardInput::BackgroundImage));
|
||||||
|
let background_alpha_idx = dag.add_node(Node::StdInput(StandardInput::BackgroundAlpha));
|
||||||
|
let fill_paint_idx = dag.add_node(Node::StdInput(StandardInput::FillPaint));
|
||||||
|
let stroke_paint_idx = dag.add_node(Node::StdInput(StandardInput::StrokePaint));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dag,
|
||||||
|
source_graphic_idx,
|
||||||
|
source_alpha_idx,
|
||||||
|
background_image_idx,
|
||||||
|
background_alpha_idx,
|
||||||
|
fill_paint_idx,
|
||||||
|
stroke_paint_idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node(&mut self, node: Node) -> NodeIndex {
|
||||||
|
self.dag.add_node(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_input(&self, input: NodeInput) -> NodeIndex {
|
||||||
|
match input {
|
||||||
|
NodeInput::Standard(StandardInput::SourceGraphic) => self.source_graphic_idx,
|
||||||
|
NodeInput::Standard(StandardInput::SourceAlpha) => self.source_alpha_idx,
|
||||||
|
NodeInput::Standard(StandardInput::BackgroundImage) => self.background_image_idx,
|
||||||
|
NodeInput::Standard(StandardInput::BackgroundAlpha) => self.background_alpha_idx,
|
||||||
|
NodeInput::Standard(StandardInput::FillPaint) => self.fill_paint_idx,
|
||||||
|
NodeInput::Standard(StandardInput::StrokePaint) => self.stroke_paint_idx,
|
||||||
|
NodeInput::Idx(i) => i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::unwrap_used,
|
||||||
|
reason = "we only operate on values we know exist, so unwrapping is safe"
|
||||||
|
)]
|
||||||
|
pub fn inputs(&self, node_idx: NodeIndex) -> Vec<NodeInput> {
|
||||||
|
let mut inputs = self
|
||||||
|
.dag
|
||||||
|
.neighbors_directed(node_idx, Direction::Incoming)
|
||||||
|
.map(|input_idx| (self.dag.find_edge(input_idx, node_idx).unwrap(), input_idx))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
inputs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||||
|
|
||||||
|
inputs
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|(_, input_idx)| match self.dag.node_weight(input_idx).unwrap() {
|
||||||
|
Node::StdInput(s) => NodeInput::Standard(*s),
|
||||||
|
Node::Primitive { .. } => NodeInput::Idx(input_idx),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn outputs(&self, node_idx: NodeIndex) -> Vec<NodeIndex> {
|
||||||
|
self.dag
|
||||||
|
.neighbors_directed(node_idx, Direction::Outgoing)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source_graphic(&self) -> NodeIndex {
|
||||||
|
self.source_graphic_idx
|
||||||
|
}
|
||||||
|
pub fn source_alpha(&self) -> NodeIndex {
|
||||||
|
self.source_alpha_idx
|
||||||
|
}
|
||||||
|
pub fn background_image(&self) -> NodeIndex {
|
||||||
|
self.background_image_idx
|
||||||
|
}
|
||||||
|
pub fn background_alpha(&self) -> NodeIndex {
|
||||||
|
self.background_alpha_idx
|
||||||
|
}
|
||||||
|
pub fn fill_paint(&self) -> NodeIndex {
|
||||||
|
self.fill_paint_idx
|
||||||
|
}
|
||||||
|
pub fn stroke_paint(&self) -> NodeIndex {
|
||||||
|
self.stroke_paint_idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod abstracted_inputs;
|
||||||
|
|
||||||
|
pub mod edge;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue