forked from katzen-cafe/iowo
Compare commits
194 commits
Author | SHA1 | Date | |
---|---|---|---|
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/
|
||||
/target
|
||||
.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]
|
||||
name = "pipeline-lang"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/app",
|
||||
"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]
|
||||
logos = "0.13"
|
||||
codespan-reporting = "0.11"
|
||||
clap = { version = "4.4.8", features = [ "derive" ] }
|
||||
# to enable all the lints below, this must be present in a workspace member's Cargo.toml:
|
||||
# [lints]
|
||||
# workspace = true
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
variant_size_differences = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
branches_sharing_code = "warn"
|
||||
clone_on_ref_ptr = "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