Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4b9247c45 | ||
|
|
375bc80a57 | ||
|
|
335ecde816 | ||
|
|
4142fcd46a | ||
|
|
2d06dc3277 | ||
|
|
37237dc1f7 | ||
|
|
2966d5b979 | ||
|
|
ea858ad28e | ||
|
|
871d342e15 | ||
|
|
6d80c66144 | ||
|
|
8e1cb01f52 | ||
|
|
26d8495631 | ||
|
|
c4332ffb9d | ||
|
|
33ea8b94f9 | ||
|
|
720c71157e | ||
|
|
ee031bd2e8 | ||
|
|
a07f4c48aa | ||
|
|
fe127428f9 | ||
|
|
0b76a600b1 | ||
|
|
ada3578a1d | ||
|
|
b8404df047 | ||
|
|
87fef44c68 | ||
|
|
6e2363632b | ||
|
|
c26f9ca50a | ||
|
|
f928028007 | ||
|
|
d8af980f7d | ||
|
|
a2a6dee025 | ||
|
|
cc5ab63922 | ||
|
|
8b3df21cfa | ||
|
|
01ad6dca52 | ||
|
|
6da0ec4c77 | ||
|
|
683151f6bd | ||
|
|
9e728fb634 | ||
|
|
2fe54bda62 | ||
|
|
3f7dd7c692 | ||
|
|
dc28cddbff | ||
|
|
aa6e5c51c1 | ||
|
|
4ab47e5719 | ||
|
|
19c9de1601 | ||
|
|
0e873fbd51 | ||
|
|
4020b7c2d5 |
@@ -3,3 +3,9 @@
|
|||||||
/.env
|
/.env
|
||||||
/.yoi
|
/.yoi
|
||||||
/result
|
/result
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
site/decodal-site/dist
|
||||||
|
|
||||||
|
# Astro
|
||||||
|
site/decodal-site/.astro
|
||||||
|
|||||||
Generated
+183
-5
@@ -11,26 +11,88 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal"
|
name = "decodal"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal-core",
|
"decodal-derive",
|
||||||
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-core"
|
name = "decodal-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex",
|
"decodal",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-derive"
|
||||||
|
version = "0.1.2"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-wasm"
|
||||||
|
version = "0.1.2"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.4"
|
version = "1.12.4"
|
||||||
@@ -59,3 +121,119 @@ name = "regex-syntax"
|
|||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|||||||
+14
-7
@@ -2,12 +2,19 @@
|
|||||||
members = [
|
members = [
|
||||||
"crates/decodal-core",
|
"crates/decodal-core",
|
||||||
"crates/decodal-cli",
|
"crates/decodal-cli",
|
||||||
|
"crates/decodal-wasm",
|
||||||
|
"crates/decodal-derive",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "2"
|
||||||
|
|
||||||
[profile.release]
|
[workspace.package]
|
||||||
opt-level = "z"
|
version = "0.1.2"
|
||||||
lto = true
|
edition = "2024"
|
||||||
codegen-units = 1
|
rust-version = "1.85"
|
||||||
strip = true
|
license = "MIT OR Apache-2.0"
|
||||||
panic = "abort"
|
repository = "https://gitea.hareworks.net/Hare/Decodal"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
serde_json = "1"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
|||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Decodal contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Decodal
|
||||||
|
|
||||||
|
Decodal is a small deterministic DSL for describing, composing, validating, and materializing structured data.
|
||||||
|
|
||||||
|
It is designed around a lightweight Rust library:
|
||||||
|
|
||||||
|
- host-supplied imports through `SourceLoader`
|
||||||
|
- no filesystem access in the library core
|
||||||
|
- concrete and abstract values with constraints and defaults
|
||||||
|
- deterministic expression evaluation
|
||||||
|
- optional regex support behind a Cargo feature
|
||||||
|
- browser playground support through WebAssembly
|
||||||
|
|
||||||
|
## Library crate
|
||||||
|
|
||||||
|
Embedded hosts should depend on `decodal` and provide imports with a `SourceLoader`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
decodal = "0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Derive support
|
||||||
|
|
||||||
|
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.1", features = ["derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Decodal, DecodalDecode, DecodalSchema, Engine};
|
||||||
|
|
||||||
|
#[derive(Decodal)]
|
||||||
|
struct Service {
|
||||||
|
name: String,
|
||||||
|
#[decodal(gt = 443, default = 8443)]
|
||||||
|
port: i64,
|
||||||
|
#[decodal(rename = "feature.enable", default = true)]
|
||||||
|
feature_enabled: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The derive implements:
|
||||||
|
|
||||||
|
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
|
||||||
|
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
A standalone CLI is kept in this repository as the `decodal-cli` workspace package.
|
||||||
|
It builds a `decodal` binary, but it is not the primary crates.io package.
|
||||||
|
|
||||||
|
Run a Decodal file from the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-cli -- examples/advanced/main.dcdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable optional regex support when needed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web playground
|
||||||
|
|
||||||
|
The static documentation site and browser playground live under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
site/decodal-site/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under either of:
|
||||||
|
|
||||||
|
- Apache License, Version 2.0
|
||||||
|
- MIT license
|
||||||
|
|
||||||
|
at your option.
|
||||||
@@ -1,11 +1,23 @@
|
|||||||
[package]
|
[package]
|
||||||
|
name = "decodal-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Command-line interface for the Decodal data description language."
|
||||||
|
keywords = ["decodal", "dsl", "config", "cli"]
|
||||||
|
categories = ["command-line-utilities", "config"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
name = "decodal"
|
name = "decodal"
|
||||||
version = "0.1.0"
|
path = "src/main.rs"
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
regex = ["decodal-core/regex"]
|
regex = ["decodal/regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal-core = { path = "../decodal-core" }
|
decodal = { version = "0.1.2", path = "../decodal-core" }
|
||||||
|
|||||||
@@ -4,19 +4,22 @@ use std::{
|
|||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use decodal_core::{Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceLoader, Span};
|
use decodal::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
||||||
|
format_diagnostic_with,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
match run() {
|
match run() {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_diagnostic(&error);
|
eprintln!("{error}");
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<(), Diagnostic> {
|
fn run() -> Result<(), String> {
|
||||||
let mut args = env::args().skip(1);
|
let mut args = env::args().skip(1);
|
||||||
let first = args.next().unwrap_or_else(|| String::from("--help"));
|
let first = args.next().unwrap_or_else(|| String::from("--help"));
|
||||||
match first.as_str() {
|
match first.as_str() {
|
||||||
@@ -25,7 +28,7 @@ fn run() -> Result<(), Diagnostic> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
println!("Decodal {}", decodal_core::version());
|
println!("Decodal {}", decodal::version());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"check" => {
|
"check" => {
|
||||||
@@ -51,12 +54,19 @@ fn run() -> Result<(), Diagnostic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn materialize_path(path: &str) -> Result<Data, Diagnostic> {
|
fn materialize_path(path: &str) -> Result<Data, String> {
|
||||||
let root = read_root_source(path)?;
|
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
|
||||||
|
let root_name = root.name.clone();
|
||||||
let mut engine = Engine::new(FsLoader);
|
let mut engine = Engine::new(FsLoader);
|
||||||
let module = engine.add_root_source(root.key, root.name, &root.source)?;
|
let module = engine
|
||||||
let value = engine.eval_module(module)?;
|
.add_root_source(root.key, root.name, &root.source)
|
||||||
engine.materialize(&value)
|
.map_err(|error| format_diagnostic_with_root(&error, &root_name))?;
|
||||||
|
let value = engine
|
||||||
|
.eval_module(module)
|
||||||
|
.map_err(|error| engine.format_diagnostic(&error))?;
|
||||||
|
engine
|
||||||
|
.materialize(&value)
|
||||||
|
.map_err(|error| engine.format_diagnostic(&error))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||||
@@ -139,15 +149,12 @@ fn print_help() {
|
|||||||
println!(" decodal - Read DCDL source from stdin");
|
println!(" decodal - Read DCDL source from stdin");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_diagnostic(error: &Diagnostic) {
|
fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String {
|
||||||
eprintln!(
|
format_diagnostic_with(error, |source| (source == SourceId(0)).then_some(root_name))
|
||||||
"error[{kind:?}] {source}:{start}..{end}: {message}",
|
}
|
||||||
kind = error.kind,
|
|
||||||
source = error.span.source.0,
|
fn format_raw_diagnostic(error: Diagnostic) -> String {
|
||||||
start = error.span.start,
|
format_diagnostic_with(&error, |_| None)
|
||||||
end = error.span.end,
|
|
||||||
message = error.message,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_data(data: &Data, indent: usize) {
|
fn print_data(data: &Data, indent: usize) {
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "decodal-core"
|
name = "decodal"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
edition = "2024"
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Parser, evaluator, and embedding API for the Decodal data description language."
|
||||||
|
keywords = ["decodal", "dsl", "config", "schema"]
|
||||||
|
categories = ["config", "parser-implementations"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
std = []
|
std = []
|
||||||
|
derive = ["dep:decodal-derive"]
|
||||||
regex = ["std", "dep:regex"]
|
regex = ["std", "dep:regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
decodal-derive = { version = "0.1.1", path = "../decodal-derive", optional = true }
|
||||||
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }
|
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use decodal_core::{Data, EmptyLoader, Engine, HostValue};
|
use decodal::{Data, EmptyLoader, Engine, HostValue};
|
||||||
|
|
||||||
fn main() -> decodal_core::Result<()> {
|
fn main() -> decodal::Result<()> {
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
|
||||||
engine.bind_global(
|
engine.bind_global(
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ pub enum Expr {
|
|||||||
scrutinee: ExprId,
|
scrutinee: ExprId,
|
||||||
arms: Vec<MatchArm>,
|
arms: Vec<MatchArm>,
|
||||||
},
|
},
|
||||||
|
Unary {
|
||||||
|
op: UnaryOp,
|
||||||
|
expr: ExprId,
|
||||||
|
},
|
||||||
Binary {
|
Binary {
|
||||||
op: BinaryOp,
|
op: BinaryOp,
|
||||||
lhs: ExprId,
|
lhs: ExprId,
|
||||||
@@ -117,8 +121,27 @@ pub enum Literal {
|
|||||||
Bool(bool),
|
Bool(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum UnaryOp {
|
||||||
|
Neg,
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BinaryOp {
|
pub enum BinaryOp {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Concat,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
LogicalAnd,
|
||||||
|
LogicalOr,
|
||||||
And,
|
And,
|
||||||
Patch,
|
Patch,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,103 +3,157 @@ use alloc::{string::String, vec::Vec};
|
|||||||
use crate::{
|
use crate::{
|
||||||
Diagnostic, DiagnosticKind, Span,
|
Diagnostic, DiagnosticKind, Span,
|
||||||
ast::CompareOp,
|
ast::CompareOp,
|
||||||
runtime::{Constraint, LiteralValue, PrimitiveType},
|
runtime::{Constraint, ConstraintEntry, LiteralValue, PrimitiveType},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn normalize_constraints(
|
pub fn normalize_constraints(
|
||||||
constraints: Vec<Constraint>,
|
constraints: Vec<ConstraintEntry>,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> crate::Result<Vec<Constraint>> {
|
) -> crate::Result<Vec<ConstraintEntry>> {
|
||||||
let mut primitive = None;
|
let mut primitive: Option<(PrimitiveType, Span)> = None;
|
||||||
let mut lower: Option<Bound> = None;
|
let mut lower: Option<(Bound, Span)> = None;
|
||||||
let mut upper: Option<Bound> = None;
|
let mut upper: Option<(Bound, Span)> = None;
|
||||||
let mut rest = Vec::new();
|
let mut rest = Vec::new();
|
||||||
|
|
||||||
for constraint in constraints {
|
for entry in constraints {
|
||||||
match constraint {
|
match entry.constraint {
|
||||||
Constraint::Type(next) => match primitive {
|
Constraint::Type(next) => match primitive {
|
||||||
Some(current) if current != next => {
|
Some((current, current_span)) if current != next => {
|
||||||
return Err(Diagnostic::new(
|
return Err(Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
span,
|
span,
|
||||||
"primitive type constraints conflict",
|
"primitive type constraints conflict",
|
||||||
));
|
)
|
||||||
|
.with_label(current_span, "first primitive constraint")
|
||||||
|
.with_label(entry.span, "conflicting primitive constraint"));
|
||||||
}
|
}
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
None => primitive = Some(next),
|
None => primitive = Some((next, entry.span)),
|
||||||
},
|
},
|
||||||
Constraint::Compare(op, value) => {
|
Constraint::Compare(op, value) => {
|
||||||
let number = Number::from_literal(&value).ok_or_else(|| {
|
let number = Number::from_literal(&value).ok_or_else(|| {
|
||||||
Diagnostic::new(
|
Diagnostic::new(
|
||||||
DiagnosticKind::TypeMismatch,
|
DiagnosticKind::TypeMismatch,
|
||||||
span,
|
entry.span,
|
||||||
"comparison constraints require numeric literals",
|
"comparison constraints require numeric literals",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
match op {
|
match op {
|
||||||
CompareOp::Gt => merge_lower(&mut lower, Bound::new(number, false)),
|
CompareOp::Gt => {
|
||||||
CompareOp::Gte => merge_lower(&mut lower, Bound::new(number, true)),
|
merge_lower(&mut lower, (Bound::new(number, false), entry.span))
|
||||||
CompareOp::Lt => merge_upper(&mut upper, Bound::new(number, false)),
|
}
|
||||||
CompareOp::Lte => merge_upper(&mut upper, Bound::new(number, true)),
|
CompareOp::Gte => {
|
||||||
|
merge_lower(&mut lower, (Bound::new(number, true), entry.span))
|
||||||
|
}
|
||||||
|
CompareOp::Lt => {
|
||||||
|
merge_upper(&mut upper, (Bound::new(number, false), entry.span))
|
||||||
|
}
|
||||||
|
CompareOp::Lte => {
|
||||||
|
merge_upper(&mut upper, (Bound::new(number, true), entry.span))
|
||||||
|
}
|
||||||
CompareOp::Eq => {
|
CompareOp::Eq => {
|
||||||
merge_lower(&mut lower, Bound::new(number, true));
|
merge_lower(&mut lower, (Bound::new(number, true), entry.span));
|
||||||
merge_upper(&mut upper, Bound::new(number, true));
|
merge_upper(&mut upper, (Bound::new(number, true), entry.span));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Constraint::Regex(pattern) => rest.push(Constraint::Regex(pattern)),
|
Constraint::Regex(pattern) => rest.push(ConstraintEntry {
|
||||||
Constraint::BuiltinPredicate(name) => rest.push(Constraint::BuiltinPredicate(name)),
|
constraint: Constraint::Regex(pattern),
|
||||||
|
span: entry.span,
|
||||||
|
}),
|
||||||
|
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::BuiltinPredicate(name),
|
||||||
|
span: entry.span,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(primitive, Some(PrimitiveType::String | PrimitiveType::Bool))
|
if matches!(
|
||||||
&& (lower.is_some() || upper.is_some())
|
primitive,
|
||||||
|
Some((
|
||||||
|
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
|
||||||
|
_
|
||||||
|
))
|
||||||
|
) && (lower.is_some() || upper.is_some())
|
||||||
{
|
{
|
||||||
return Err(Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
span,
|
span,
|
||||||
"numeric comparison constraints conflict with non-numeric primitive type",
|
"numeric comparison constraints conflict with non-numeric primitive type",
|
||||||
));
|
);
|
||||||
|
if let Some((_, primitive_span)) = primitive {
|
||||||
|
diagnostic = diagnostic.with_label(primitive_span, "non-numeric primitive constraint");
|
||||||
|
}
|
||||||
|
if let Some((_, lower_span)) = lower {
|
||||||
|
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
if let Some((_, upper_span)) = upper {
|
||||||
|
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
return Err(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
||||||
if primitive == Some(PrimitiveType::Int)
|
if matches!(primitive, Some((PrimitiveType::Int, _)))
|
||||||
&& lower
|
&& lower
|
||||||
.iter()
|
.iter()
|
||||||
.chain(upper.iter())
|
.chain(upper.iter())
|
||||||
.any(|bound| !matches!(bound.number, Number::Int(_)))
|
.any(|(bound, _)| !matches!(bound.number, Number::Int(_)))
|
||||||
{
|
{
|
||||||
return Err(Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
span,
|
span,
|
||||||
"Int comparison constraints must use integer literals",
|
"Int comparison constraints must use integer literals",
|
||||||
));
|
);
|
||||||
|
if let Some((_, primitive_span)) = primitive {
|
||||||
|
diagnostic = diagnostic.with_label(primitive_span, "Int primitive constraint");
|
||||||
|
}
|
||||||
|
if let Some((bound, bound_span)) = lower {
|
||||||
|
if !matches!(bound.number, Number::Int(_)) {
|
||||||
|
diagnostic = diagnostic.with_label(bound_span, "non-integer comparison constraint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((bound, bound_span)) = upper {
|
||||||
|
if !matches!(bound.number, Number::Int(_)) {
|
||||||
|
diagnostic = diagnostic.with_label(bound_span, "non-integer comparison constraint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_bounds_non_empty(primitive, lower, upper, span)?;
|
ensure_bounds_non_empty(primitive, lower, upper, span)?;
|
||||||
|
|
||||||
let mut normalized = Vec::new();
|
let mut normalized = Vec::new();
|
||||||
if let Some(primitive) = primitive {
|
if let Some((primitive, primitive_span)) = primitive {
|
||||||
normalized.push(Constraint::Type(primitive));
|
normalized.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::Type(primitive),
|
||||||
|
span: primitive_span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some(lower) = lower {
|
if let Some((lower, lower_span)) = lower {
|
||||||
normalized.push(Constraint::Compare(
|
normalized.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::Compare(
|
||||||
if lower.inclusive {
|
if lower.inclusive {
|
||||||
CompareOp::Gte
|
CompareOp::Gte
|
||||||
} else {
|
} else {
|
||||||
CompareOp::Gt
|
CompareOp::Gt
|
||||||
},
|
},
|
||||||
lower.number.into_literal(),
|
lower.number.into_literal(),
|
||||||
));
|
),
|
||||||
|
span: lower_span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some(upper) = upper {
|
if let Some((upper, upper_span)) = upper {
|
||||||
normalized.push(Constraint::Compare(
|
normalized.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::Compare(
|
||||||
if upper.inclusive {
|
if upper.inclusive {
|
||||||
CompareOp::Lte
|
CompareOp::Lte
|
||||||
} else {
|
} else {
|
||||||
CompareOp::Lt
|
CompareOp::Lt
|
||||||
},
|
},
|
||||||
upper.number.into_literal(),
|
upper.number.into_literal(),
|
||||||
));
|
),
|
||||||
|
span: upper_span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
normalized.extend(rest);
|
normalized.extend(rest);
|
||||||
Ok(normalized)
|
Ok(normalized)
|
||||||
@@ -147,18 +201,22 @@ impl Number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_lower(current: &mut Option<Bound>, next: Bound) {
|
fn merge_lower(current: &mut Option<(Bound, Span)>, next: (Bound, Span)) {
|
||||||
match current {
|
match current {
|
||||||
None => *current = Some(next),
|
None => *current = Some(next),
|
||||||
Some(current_bound) if is_stricter_lower(next, *current_bound) => *current_bound = next,
|
Some((current_bound, _)) if is_stricter_lower(next.0, *current_bound) => {
|
||||||
|
*current = Some(next)
|
||||||
|
}
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_upper(current: &mut Option<Bound>, next: Bound) {
|
fn merge_upper(current: &mut Option<(Bound, Span)>, next: (Bound, Span)) {
|
||||||
match current {
|
match current {
|
||||||
None => *current = Some(next),
|
None => *current = Some(next),
|
||||||
Some(current_bound) if is_stricter_upper(next, *current_bound) => *current_bound = next,
|
Some((current_bound, _)) if is_stricter_upper(next.0, *current_bound) => {
|
||||||
|
*current = Some(next)
|
||||||
|
}
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,28 +236,32 @@ fn is_stricter_upper(next: Bound, current: Bound) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_bounds_non_empty(
|
fn ensure_bounds_non_empty(
|
||||||
primitive: Option<PrimitiveType>,
|
primitive: Option<(PrimitiveType, Span)>,
|
||||||
lower: Option<Bound>,
|
lower: Option<(Bound, Span)>,
|
||||||
upper: Option<Bound>,
|
upper: Option<(Bound, Span)>,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
if primitive == Some(PrimitiveType::Int) {
|
if matches!(primitive, Some((PrimitiveType::Int, _))) {
|
||||||
let min = lower.map(int_lower_bound).unwrap_or(i128::from(i64::MIN));
|
let min = lower
|
||||||
let max = upper.map(int_upper_bound).unwrap_or(i128::from(i64::MAX));
|
.map(|(bound, _)| int_lower_bound(bound))
|
||||||
|
.unwrap_or(i128::from(i64::MIN));
|
||||||
|
let max = upper
|
||||||
|
.map(|(bound, _)| int_upper_bound(bound))
|
||||||
|
.unwrap_or(i128::from(i64::MAX));
|
||||||
if min > max {
|
if min > max {
|
||||||
return Err(empty_numeric_bounds(span));
|
return Err(empty_numeric_bounds(span, lower, upper));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(lower), Some(upper)) = (lower, upper) {
|
if let (Some((lower_bound, _)), Some((upper_bound, _))) = (lower, upper) {
|
||||||
let lower_value = lower.number.as_f64();
|
let lower_value = lower_bound.number.as_f64();
|
||||||
let upper_value = upper.number.as_f64();
|
let upper_value = upper_bound.number.as_f64();
|
||||||
if lower_value > upper_value {
|
if lower_value > upper_value {
|
||||||
return Err(empty_numeric_bounds(span));
|
return Err(empty_numeric_bounds(span, lower, upper));
|
||||||
}
|
}
|
||||||
if lower_value == upper_value && !(lower.inclusive && upper.inclusive) {
|
if lower_value == upper_value && !(lower_bound.inclusive && upper_bound.inclusive) {
|
||||||
return Err(empty_numeric_bounds(span));
|
return Err(empty_numeric_bounds(span, lower, upper));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -227,12 +289,23 @@ fn int_upper_bound(bound: Bound) -> i128 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_numeric_bounds(span: Span) -> Diagnostic {
|
fn empty_numeric_bounds(
|
||||||
Diagnostic::new(
|
span: Span,
|
||||||
|
lower: Option<(Bound, Span)>,
|
||||||
|
upper: Option<(Bound, Span)>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
span,
|
span,
|
||||||
String::from("numeric comparison constraints have an empty intersection"),
|
String::from("numeric comparison constraints have an empty intersection"),
|
||||||
)
|
);
|
||||||
|
if let Some((_, lower_span)) = lower {
|
||||||
|
diagnostic = diagnostic.with_label(lower_span, "lower bound constraint");
|
||||||
|
}
|
||||||
|
if let Some((_, upper_span)) = upper {
|
||||||
|
diagnostic = diagnostic.with_label(upper_span, "upper bound constraint");
|
||||||
|
}
|
||||||
|
diagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -240,13 +313,20 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::runtime::LiteralValue;
|
use crate::runtime::LiteralValue;
|
||||||
|
|
||||||
|
fn entry(constraint: Constraint) -> ConstraintEntry {
|
||||||
|
ConstraintEntry {
|
||||||
|
constraint,
|
||||||
|
span: Span::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_primitive_conflict() {
|
fn detects_primitive_conflict() {
|
||||||
assert!(
|
assert!(
|
||||||
normalize_constraints(
|
normalize_constraints(
|
||||||
alloc::vec![
|
alloc::vec![
|
||||||
Constraint::Type(PrimitiveType::Int),
|
entry(Constraint::Type(PrimitiveType::Int)),
|
||||||
Constraint::Type(PrimitiveType::String),
|
entry(Constraint::Type(PrimitiveType::String)),
|
||||||
],
|
],
|
||||||
Span::default(),
|
Span::default(),
|
||||||
)
|
)
|
||||||
@@ -259,9 +339,9 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
normalize_constraints(
|
normalize_constraints(
|
||||||
alloc::vec![
|
alloc::vec![
|
||||||
Constraint::Type(PrimitiveType::Int),
|
entry(Constraint::Type(PrimitiveType::Int)),
|
||||||
Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10)),
|
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10))),
|
||||||
Constraint::Compare(CompareOp::Lt, LiteralValue::Int(11)),
|
entry(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(5))),
|
||||||
],
|
],
|
||||||
Span::default(),
|
Span::default(),
|
||||||
)
|
)
|
||||||
@@ -270,15 +350,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_regex_constraints_without_intersection_check() {
|
fn detects_non_integer_int_bound() {
|
||||||
let constraints = normalize_constraints(
|
assert!(
|
||||||
|
normalize_constraints(
|
||||||
alloc::vec![
|
alloc::vec![
|
||||||
Constraint::Regex(String::from("^a$")),
|
entry(Constraint::Type(PrimitiveType::Int)),
|
||||||
Constraint::Regex(String::from("^b$")),
|
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Float(1.5))),
|
||||||
],
|
],
|
||||||
Span::default(),
|
Span::default(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.is_err()
|
||||||
assert_eq!(constraints.len(), 2);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use alloc::string::String;
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::span::Span;
|
use crate::span::Span;
|
||||||
|
|
||||||
@@ -9,6 +9,13 @@ pub struct Diagnostic {
|
|||||||
pub kind: DiagnosticKind,
|
pub kind: DiagnosticKind,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
pub labels: Vec<DiagnosticLabel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DiagnosticLabel {
|
||||||
|
pub span: Span,
|
||||||
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic {
|
impl Diagnostic {
|
||||||
@@ -17,12 +24,21 @@ impl Diagnostic {
|
|||||||
kind,
|
kind,
|
||||||
span,
|
span,
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
|
labels: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn syntax(span: Span, message: impl Into<String>) -> Self {
|
pub fn syntax(span: Span, message: impl Into<String>) -> Self {
|
||||||
Self::new(DiagnosticKind::Syntax, span, message)
|
Self::new(DiagnosticKind::Syntax, span, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
|
||||||
|
self.labels.push(DiagnosticLabel {
|
||||||
|
span,
|
||||||
|
message: message.into(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ impl HostValue {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn object_from_paths<I, N>(fields: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (N, HostValue)>,
|
||||||
|
N: Into<String>,
|
||||||
|
{
|
||||||
|
let mut root = Vec::new();
|
||||||
|
for (path, value) in fields {
|
||||||
|
insert_path(&mut root, &path.into(), value);
|
||||||
|
}
|
||||||
|
Self::Object(root)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn string_type() -> Self {
|
pub fn string_type() -> Self {
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||||
}
|
}
|
||||||
@@ -79,6 +91,10 @@ impl HostValue {
|
|||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn array_type() -> Self {
|
||||||
|
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Array))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||||
}
|
}
|
||||||
@@ -162,3 +178,38 @@ impl HostValue {
|
|||||||
self.default(Self::bool(value))
|
self.default(Self::bool(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HostField {
|
||||||
|
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
|
||||||
|
let mut parts = path.splitn(2, '.');
|
||||||
|
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(tail) = parts.next() {
|
||||||
|
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||||
|
if let HostValue::Object(children) = &mut field.value {
|
||||||
|
insert_path(children, tail, value);
|
||||||
|
} else {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
insert_path(&mut children, tail, value);
|
||||||
|
field.value = HostValue::Object(children);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
insert_path(&mut children, tail, value);
|
||||||
|
fields.push(HostField::new(head, HostValue::Object(children)));
|
||||||
|
}
|
||||||
|
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||||
|
field.value = value;
|
||||||
|
} else {
|
||||||
|
fields.push(HostField::new(head, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+728
-65
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,18 @@ pub enum TokenKind {
|
|||||||
Dot,
|
Dot,
|
||||||
Colon,
|
Colon,
|
||||||
Equal,
|
Equal,
|
||||||
|
EqualEqual,
|
||||||
|
Bang,
|
||||||
|
BangEqual,
|
||||||
Arrow,
|
Arrow,
|
||||||
Amp,
|
Amp,
|
||||||
|
AmpAmp,
|
||||||
|
PipePipe,
|
||||||
|
Plus,
|
||||||
|
PlusPlus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
SlashSlash,
|
SlashSlash,
|
||||||
Gt,
|
Gt,
|
||||||
Gte,
|
Gte,
|
||||||
@@ -52,6 +62,7 @@ pub struct Lexer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Lexer<'a> {
|
impl<'a> Lexer<'a> {
|
||||||
|
#[cfg(test)]
|
||||||
pub fn new(source: &'a str) -> Self {
|
pub fn new(source: &'a str) -> Self {
|
||||||
Self::with_source_id(SourceId(0), source)
|
Self::with_source_id(SourceId(0), source)
|
||||||
}
|
}
|
||||||
@@ -67,9 +78,13 @@ impl<'a> Lexer<'a> {
|
|||||||
|
|
||||||
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
||||||
let mut tokens = Vec::new();
|
let mut tokens = Vec::new();
|
||||||
|
let mut previous = None;
|
||||||
loop {
|
loop {
|
||||||
let token = self.next_token()?;
|
let token = self.next_token(previous.as_ref())?;
|
||||||
let is_eof = token.kind == TokenKind::Eof;
|
let is_eof = token.kind == TokenKind::Eof;
|
||||||
|
if !is_eof {
|
||||||
|
previous = Some(token.kind.clone());
|
||||||
|
}
|
||||||
tokens.push(token);
|
tokens.push(token);
|
||||||
if is_eof {
|
if is_eof {
|
||||||
return Ok(tokens);
|
return Ok(tokens);
|
||||||
@@ -77,8 +92,12 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_token(&mut self) -> Result<Token> {
|
fn next_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
||||||
self.skip_ws_and_comments();
|
self.skip_ws_and_comments();
|
||||||
|
self.next_non_ws_token(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_non_ws_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let Some(ch) = self.peek() else {
|
let Some(ch) = self.peek() else {
|
||||||
return Ok(Token {
|
return Ok(Token {
|
||||||
@@ -134,16 +153,57 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
b'&' => {
|
b'&' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
|
if self.consume(b'&') {
|
||||||
|
TokenKind::AmpAmp
|
||||||
|
} else {
|
||||||
TokenKind::Amp
|
TokenKind::Amp
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
b'|' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.consume(b'|') {
|
||||||
|
TokenKind::PipePipe
|
||||||
|
} else {
|
||||||
|
return Err(Diagnostic::syntax(
|
||||||
|
self.span(start, self.pos),
|
||||||
|
"expected '|' after '|'",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'+' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.consume(b'+') {
|
||||||
|
TokenKind::PlusPlus
|
||||||
|
} else {
|
||||||
|
TokenKind::Plus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'-' => {
|
||||||
|
self.pos += 1;
|
||||||
|
TokenKind::Minus
|
||||||
|
}
|
||||||
|
b'*' => {
|
||||||
|
self.pos += 1;
|
||||||
|
TokenKind::Star
|
||||||
|
}
|
||||||
b'=' => {
|
b'=' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'>') {
|
if self.consume(b'>') {
|
||||||
TokenKind::Arrow
|
TokenKind::Arrow
|
||||||
|
} else if self.consume(b'=') {
|
||||||
|
TokenKind::EqualEqual
|
||||||
} else {
|
} else {
|
||||||
TokenKind::Equal
|
TokenKind::Equal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
b'!' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.consume(b'=') {
|
||||||
|
TokenKind::BangEqual
|
||||||
|
} else {
|
||||||
|
TokenKind::Bang
|
||||||
|
}
|
||||||
|
}
|
||||||
b'>' => {
|
b'>' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'=') {
|
if self.consume(b'=') {
|
||||||
@@ -164,6 +224,8 @@ impl<'a> Lexer<'a> {
|
|||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'/') {
|
if self.consume(b'/') {
|
||||||
TokenKind::SlashSlash
|
TokenKind::SlashSlash
|
||||||
|
} else if previous.is_some_and(token_can_end_expr) {
|
||||||
|
TokenKind::Slash
|
||||||
} else {
|
} else {
|
||||||
self.lex_regex(start)?
|
self.lex_regex(start)?
|
||||||
}
|
}
|
||||||
@@ -341,6 +403,23 @@ fn is_ident_continue(c: u8) -> bool {
|
|||||||
c.is_ascii_alphanumeric() || c == b'_'
|
c.is_ascii_alphanumeric() || c == b'_'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn token_can_end_expr(kind: &TokenKind) -> bool {
|
||||||
|
matches!(
|
||||||
|
kind,
|
||||||
|
TokenKind::Ident(_)
|
||||||
|
| TokenKind::Int(_)
|
||||||
|
| TokenKind::Float(_)
|
||||||
|
| TokenKind::String(_)
|
||||||
|
| TokenKind::Regex(_)
|
||||||
|
| TokenKind::True
|
||||||
|
| TokenKind::False
|
||||||
|
| TokenKind::Underscore
|
||||||
|
| TokenKind::RBrace
|
||||||
|
| TokenKind::RBracket
|
||||||
|
| TokenKind::RParen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -7,22 +7,28 @@ pub mod constraints;
|
|||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod lexer;
|
mod lexer;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod span;
|
pub mod span;
|
||||||
|
pub mod typed;
|
||||||
|
|
||||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
||||||
pub use constraints::normalize_constraints;
|
pub use constraints::normalize_constraints;
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use decodal_derive::Decodal;
|
||||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||||
pub use embedding::{HostField, HostValue};
|
pub use embedding::{HostField, HostValue};
|
||||||
pub use eval::Engine;
|
pub use eval::{Engine, format_diagnostic_with};
|
||||||
pub use lexer::{Lexer, Token, TokenKind};
|
|
||||||
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
||||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||||
pub use span::{SourceId, Span};
|
pub use span::{SourceId, Span};
|
||||||
|
pub use typed::{
|
||||||
|
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
|
||||||
|
decode_path, prefix_decode_error,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn version() -> &'static str {
|
pub fn version() -> &'static str {
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
SourceId, Span,
|
SourceId, Span,
|
||||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param},
|
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
|
||||||
diagnostic::{Diagnostic, Result},
|
diagnostic::{Diagnostic, Result},
|
||||||
lexer::{Lexer, Token, TokenKind},
|
lexer::{Lexer, Token, TokenKind},
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,110 @@ impl Parser {
|
|||||||
let rhs = self.parse_expr(r_bp)?;
|
let rhs = self.parse_expr(r_bp)?;
|
||||||
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
|
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
|
||||||
lhs = match kind {
|
lhs = match kind {
|
||||||
|
InfixKind::Add => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Add,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Sub => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Sub,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Mul => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Mul,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Div => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Div,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Concat => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Concat,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Equal => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Equal,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::NotEqual => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::NotEqual,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Greater => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Greater,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::GreaterEqual => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::GreaterEqual,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::Less => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::Less,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::LessEqual => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::LessEqual,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::LogicalAnd => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::LogicalAnd,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
InfixKind::LogicalOr => self.ast.push(
|
||||||
|
Expr::Binary {
|
||||||
|
op: BinaryOp::LogicalOr,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
InfixKind::And => self.ast.push(
|
InfixKind::And => self.ast.push(
|
||||||
Expr::Binary {
|
Expr::Binary {
|
||||||
op: BinaryOp::And,
|
op: BinaryOp::And,
|
||||||
@@ -173,6 +277,28 @@ impl Parser {
|
|||||||
TokenKind::Let => self.parse_let(token.span),
|
TokenKind::Let => self.parse_let(token.span),
|
||||||
TokenKind::Match => self.parse_match(token.span),
|
TokenKind::Match => self.parse_match(token.span),
|
||||||
TokenKind::Import => self.parse_import(token.span),
|
TokenKind::Import => self.parse_import(token.span),
|
||||||
|
TokenKind::Minus => {
|
||||||
|
let expr = self.parse_expr(17)?;
|
||||||
|
let span = token.span.join(self.ast.span(expr));
|
||||||
|
Ok(self.ast.push(
|
||||||
|
Expr::Unary {
|
||||||
|
op: UnaryOp::Neg,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
TokenKind::Bang => {
|
||||||
|
let expr = self.parse_expr(17)?;
|
||||||
|
let span = token.span.join(self.ast.span(expr));
|
||||||
|
Ok(self.ast.push(
|
||||||
|
Expr::Unary {
|
||||||
|
op: UnaryOp::Not,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
|
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
|
||||||
let op = match token.kind {
|
let op = match token.kind {
|
||||||
TokenKind::Gt => CompareOp::Gt,
|
TokenKind::Gt => CompareOp::Gt,
|
||||||
@@ -181,7 +307,7 @@ impl Parser {
|
|||||||
TokenKind::Lte => CompareOp::Lte,
|
TokenKind::Lte => CompareOp::Lte,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let value = self.parse_expr(8)?;
|
let value = self.parse_expr(12)?;
|
||||||
let span = token.span.join(self.ast.span(value));
|
let span = token.span.join(self.ast.span(value));
|
||||||
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
|
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
|
||||||
}
|
}
|
||||||
@@ -406,6 +532,19 @@ impl Parser {
|
|||||||
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
||||||
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
||||||
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
||||||
|
TokenKind::PipePipe => Some((InfixKind::LogicalOr, 7, 8)),
|
||||||
|
TokenKind::AmpAmp => Some((InfixKind::LogicalAnd, 9, 10)),
|
||||||
|
TokenKind::EqualEqual => Some((InfixKind::Equal, 11, 12)),
|
||||||
|
TokenKind::BangEqual => Some((InfixKind::NotEqual, 11, 12)),
|
||||||
|
TokenKind::Gt => Some((InfixKind::Greater, 11, 12)),
|
||||||
|
TokenKind::Gte => Some((InfixKind::GreaterEqual, 11, 12)),
|
||||||
|
TokenKind::Lt => Some((InfixKind::Less, 11, 12)),
|
||||||
|
TokenKind::Lte => Some((InfixKind::LessEqual, 11, 12)),
|
||||||
|
TokenKind::PlusPlus => Some((InfixKind::Concat, 12, 13)),
|
||||||
|
TokenKind::Plus => Some((InfixKind::Add, 13, 14)),
|
||||||
|
TokenKind::Minus => Some((InfixKind::Sub, 13, 14)),
|
||||||
|
TokenKind::Star => Some((InfixKind::Mul, 15, 16)),
|
||||||
|
TokenKind::Slash => Some((InfixKind::Div, 15, 16)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,6 +637,19 @@ impl Parser {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum InfixKind {
|
enum InfixKind {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Concat,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
LogicalAnd,
|
||||||
|
LogicalOr,
|
||||||
And,
|
And,
|
||||||
Patch,
|
Patch,
|
||||||
Default,
|
Default,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::{ExprId, ast::CompareOp};
|
use crate::{ExprId, Span, ast::CompareOp};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ModuleId(pub u32);
|
pub struct ModuleId(pub u32);
|
||||||
@@ -37,6 +37,7 @@ pub struct ObjectValue {
|
|||||||
pub struct ObjectField {
|
pub struct ObjectField {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: ThunkId,
|
pub value: ThunkId,
|
||||||
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -54,10 +55,16 @@ pub struct FunctionParam {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct AbstractValue {
|
pub struct AbstractValue {
|
||||||
pub constraints: Vec<Constraint>,
|
pub constraints: Vec<ConstraintEntry>,
|
||||||
pub default: Option<ThunkId>,
|
pub default: Option<ThunkId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ConstraintEntry {
|
||||||
|
pub constraint: Constraint,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Constraint {
|
pub enum Constraint {
|
||||||
Type(PrimitiveType),
|
Type(PrimitiveType),
|
||||||
@@ -72,6 +79,7 @@ pub enum PrimitiveType {
|
|||||||
Int,
|
Int,
|
||||||
Float,
|
Float,
|
||||||
Bool,
|
Bool,
|
||||||
|
Array,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -108,6 +116,7 @@ pub struct EnvId(pub u32);
|
|||||||
pub struct Thunk {
|
pub struct Thunk {
|
||||||
pub kind: ThunkKind,
|
pub kind: ThunkKind,
|
||||||
pub state: ThunkState,
|
pub state: ThunkState,
|
||||||
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
use alloc::{format, string::String, vec::Vec};
|
||||||
|
|
||||||
|
use crate::{Data, HostValue};
|
||||||
|
|
||||||
|
pub trait DecodalSchema {
|
||||||
|
fn decodal_schema() -> HostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DecodalDecode: Sized {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoHostValue {
|
||||||
|
fn into_host_value(self) -> HostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DecodeError {
|
||||||
|
pub path: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodeError {
|
||||||
|
pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
path: path.into(),
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at_type(path: &str, expected: &'static str) -> Self {
|
||||||
|
Self::new(path, format!("expected {expected}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for DecodeError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
if self.path.is_empty() {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}: {}", self.path, self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl std::error::Error for DecodeError {}
|
||||||
|
|
||||||
|
pub fn decode_path<T: DecodalDecode>(data: &Data, path: &str) -> DecodeResult<T> {
|
||||||
|
let value = data_at_path(data, path).ok_or_else(|| DecodeError::new(path, "missing field"))?;
|
||||||
|
T::decodal_decode(value).map_err(|error| prefix_error(path, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_at_path<'a>(data: &'a Data, path: &str) -> Option<&'a Data> {
|
||||||
|
let mut current = data;
|
||||||
|
for part in path.split('.') {
|
||||||
|
if part.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let Data::Object(fields) = current else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
current = &fields.iter().find(|field| field.name == part)?.value;
|
||||||
|
}
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefix_decode_error(path: &str, error: DecodeError) -> DecodeError {
|
||||||
|
prefix_error(path, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
|
||||||
|
if error.path.is_empty() {
|
||||||
|
error.path = String::from(path);
|
||||||
|
} else if !path.is_empty() {
|
||||||
|
error.path = format!("{path}.{}", error.path);
|
||||||
|
}
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalSchema for String {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::string_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for String {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::String(value) => Ok(value.clone()),
|
||||||
|
_ => Err(DecodeError::at_type("", "String")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for String {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::string(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for &str {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::string(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_int_decode {
|
||||||
|
($($ty:ty),* $(,)?) => {
|
||||||
|
$(
|
||||||
|
impl DecodalSchema for $ty {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::int_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for $ty {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Int(value) => <$ty>::try_from(*value)
|
||||||
|
.map_err(|_| DecodeError::new("", "integer value is out of range")),
|
||||||
|
_ => Err(DecodeError::at_type("", "Int")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for $ty {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::int(self as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
|
||||||
|
|
||||||
|
impl DecodalSchema for f64 {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::float_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for f64 {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Float(value) => Ok(*value),
|
||||||
|
Data::Int(value) => Ok(*value as f64),
|
||||||
|
_ => Err(DecodeError::at_type("", "Float")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for f64 {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::float(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalSchema for f32 {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::float_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for f32 {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
f64::decodal_decode(data).map(|value| value as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for f32 {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::float(f64::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalSchema for bool {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::bool_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for bool {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Bool(value) => Ok(*value),
|
||||||
|
_ => Err(DecodeError::at_type("", "Bool")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHostValue for bool {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::bool(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DecodalSchema for Vec<T> {
|
||||||
|
fn decodal_schema() -> HostValue {
|
||||||
|
HostValue::array_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DecodalDecode> DecodalDecode for Vec<T> {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Array(items) => items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, item)| {
|
||||||
|
T::decodal_decode(item)
|
||||||
|
.map_err(|error| prefix_error(&format!("[{index}]"), error))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => Err(DecodeError::at_type("", "Array")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
|
||||||
|
fn into_host_value(self) -> HostValue {
|
||||||
|
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-derive"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Derive macro for generating Decodal schemas and typed decoders from Rust structs."
|
||||||
|
keywords = ["decodal", "derive", "proc-macro", "schema"]
|
||||||
|
categories = ["config", "development-tools::procedural-macro-helpers"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
decodal = { version = "0.1", path = "../decodal-core" }
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
Data, DeriveInput, Expr, Fields, LitStr, Result, Type, parse_macro_input, spanned::Spanned,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[proc_macro_derive(Decodal, attributes(decodal))]
|
||||||
|
pub fn derive_decodal(input: TokenStream) -> TokenStream {
|
||||||
|
match expand_decodal(parse_macro_input!(input as DeriveInput)) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||||
|
let name = input.ident;
|
||||||
|
let fields = match input.data {
|
||||||
|
Data::Struct(data) => match data.fields {
|
||||||
|
Fields::Named(fields) => fields.named,
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
data.struct_token.span(),
|
||||||
|
"Decodal derive supports structs with named fields only",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
name.span(),
|
||||||
|
"Decodal derive supports structs only",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut schema_fields = Vec::new();
|
||||||
|
let mut decode_fields = Vec::new();
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
let ident = field.ident.expect("named field");
|
||||||
|
let ty = field.ty;
|
||||||
|
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
|
||||||
|
let path = attrs.rename.clone();
|
||||||
|
let schema_value = schema_expr(&ty, &attrs)?;
|
||||||
|
let decode_value = decode_expr(&ty, &path, &attrs);
|
||||||
|
|
||||||
|
schema_fields.push(quote! {
|
||||||
|
(#path, { #schema_value })
|
||||||
|
});
|
||||||
|
decode_fields.push(quote! {
|
||||||
|
#ident: #decode_value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
|
||||||
|
fn decodal_schema() -> ::decodal::HostValue {
|
||||||
|
::decodal::HostValue::object_from_paths([
|
||||||
|
#(#schema_fields),*
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #impl_generics ::decodal::DecodalDecode for #name #ty_generics #where_clause {
|
||||||
|
fn decodal_decode(data: &::decodal::Data) -> ::decodal::DecodeResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
#(#decode_fields),*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FieldAttrs {
|
||||||
|
rename: String,
|
||||||
|
default: Option<DefaultAttr>,
|
||||||
|
constraints: Vec<ConstraintAttr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldAttrs {
|
||||||
|
fn from_attrs(attrs: &[syn::Attribute], fallback_name: &str) -> Result<Self> {
|
||||||
|
let mut output = Self {
|
||||||
|
rename: fallback_name.to_string(),
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for attr in attrs {
|
||||||
|
if !attr.path().is_ident("decodal") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attr.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("rename") {
|
||||||
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
|
output.rename = value.value();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if meta.path.is_ident("default") {
|
||||||
|
if meta.input.peek(syn::Token![=]) {
|
||||||
|
let expr: Expr = meta.value()?.parse()?;
|
||||||
|
output.default = Some(DefaultAttr::Expr(expr));
|
||||||
|
} else {
|
||||||
|
output.default = Some(DefaultAttr::Default);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(kind) = ConstraintKind::from_path(&meta.path) {
|
||||||
|
let expr: Expr = meta.value()?.parse()?;
|
||||||
|
output.constraints.push(ConstraintAttr { kind, expr });
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(meta.error("unsupported decodal attribute"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DefaultAttr {
|
||||||
|
Default,
|
||||||
|
Expr(Expr),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConstraintAttr {
|
||||||
|
kind: ConstraintKind,
|
||||||
|
expr: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum ConstraintKind {
|
||||||
|
Gt,
|
||||||
|
Gte,
|
||||||
|
Lt,
|
||||||
|
Lte,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConstraintKind {
|
||||||
|
fn from_path(path: &syn::Path) -> Option<Self> {
|
||||||
|
if path.is_ident("gt") {
|
||||||
|
Some(Self::Gt)
|
||||||
|
} else if path.is_ident("gte") {
|
||||||
|
Some(Self::Gte)
|
||||||
|
} else if path.is_ident("lt") {
|
||||||
|
Some(Self::Lt)
|
||||||
|
} else if path.is_ident("lte") {
|
||||||
|
Some(Self::Lte)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn method(self) -> syn::Ident {
|
||||||
|
match self {
|
||||||
|
Self::Gt => format_ident!("gt"),
|
||||||
|
Self::Gte => format_ident!("gte"),
|
||||||
|
Self::Lt => format_ident!("lt"),
|
||||||
|
Self::Lte => format_ident!("lte"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
|
||||||
|
let mut tokens = quote! {
|
||||||
|
let mut schema = <#ty as ::decodal::DecodalSchema>::decodal_schema();
|
||||||
|
};
|
||||||
|
|
||||||
|
for constraint in &attrs.constraints {
|
||||||
|
let method = constraint.kind.method();
|
||||||
|
let expr = &constraint.expr;
|
||||||
|
tokens.extend(quote! {
|
||||||
|
schema = schema.#method(#expr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(default) = &attrs.default {
|
||||||
|
let default_expr = match default {
|
||||||
|
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
|
||||||
|
DefaultAttr::Expr(expr) => quote! { (#expr) },
|
||||||
|
};
|
||||||
|
tokens.extend(quote! {
|
||||||
|
schema = schema
|
||||||
|
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
|
||||||
|
.expect("Decodal derive generated a valid default");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.extend(quote! { schema });
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
|
||||||
|
let missing = if let Some(default) = &attrs.default {
|
||||||
|
match default {
|
||||||
|
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
|
||||||
|
DefaultAttr::Expr(expr) => quote! { ::core::convert::Into::into(#expr) },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! { return Err(::decodal::DecodeError::new(#path, "missing field")); }
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
if let Some(value) = ::decodal::data_at_path(data, #path) {
|
||||||
|
<#ty as ::decodal::DecodalDecode>::decodal_decode(value)
|
||||||
|
.map_err(|error| ::decodal::prefix_decode_error(#path, error))?
|
||||||
|
} else {
|
||||||
|
#missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||||
|
use decodal_derive::Decodal;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct Service {
|
||||||
|
name: String,
|
||||||
|
#[decodal(gt = 443, default = 8443)]
|
||||||
|
port: i64,
|
||||||
|
#[decodal(default = true, rename = "feature.enable")]
|
||||||
|
feature_enabled: bool,
|
||||||
|
#[decodal(default)]
|
||||||
|
tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derives_schema_and_decode() {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("Service", Service::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"
|
||||||
|
Service & {
|
||||||
|
name = "api";
|
||||||
|
port = 9443;
|
||||||
|
feature.enable = false;
|
||||||
|
tags = ["web", "prod"];
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let service = Service::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
service,
|
||||||
|
Service {
|
||||||
|
name: "api".into(),
|
||||||
|
port: 9443,
|
||||||
|
feature_enabled: false,
|
||||||
|
tags: vec!["web".into(), "prod".into()],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_are_available_in_schema() {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("Service", Service::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"
|
||||||
|
Service & {
|
||||||
|
name = "api";
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let service = Service::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(service.port, 8443);
|
||||||
|
assert!(service.feature_enabled);
|
||||||
|
assert!(service.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_reports_field_path() {
|
||||||
|
let data = Data::Object(vec![]);
|
||||||
|
let error = Service::decodal_decode(&data).unwrap_err();
|
||||||
|
assert_eq!(error.path, "name");
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-wasm"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "WebAssembly wrapper for evaluating Decodal in browser playgrounds."
|
||||||
|
keywords = ["decodal", "wasm", "dsl", "config"]
|
||||||
|
categories = ["wasm", "config"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.1.2", path = "../decodal-core" }
|
||||||
|
serde_json.workspace = true
|
||||||
|
wasm-bindgen.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
||||||
|
Span, format_diagnostic_with,
|
||||||
|
};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn evaluate(source: &str) -> String {
|
||||||
|
encode_result(evaluate_inner(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = evaluateProject)]
|
||||||
|
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
||||||
|
encode_result(evaluate_project_inner(entry, files_json))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_result(result: Result<String, String>) -> String {
|
||||||
|
match result {
|
||||||
|
Ok(output) => format!("{{\"ok\":true,\"output\":{}}}", json_string(&output)),
|
||||||
|
Err(error) => format!("{{\"ok\":false,\"error\":{}}}", json_string(&error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_inner(source: &str) -> Result<String, String> {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
let module = match engine.add_root_source("playground", "playground", source) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(error) => return Err(format_diagnostic_with_root(&error, "playground")),
|
||||||
|
};
|
||||||
|
let value = match engine.eval_module(module) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(engine.format_diagnostic(&error)),
|
||||||
|
};
|
||||||
|
let data = match engine.materialize(&value) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(error) => return Err(engine.format_diagnostic(&error)),
|
||||||
|
};
|
||||||
|
Ok(format_data(&data, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, String> {
|
||||||
|
let raw_files: BTreeMap<String, String> = serde_json::from_str(files_json)
|
||||||
|
.map_err(|error| format!("failed to read playground files: {error}"))?;
|
||||||
|
let mut files = BTreeMap::new();
|
||||||
|
for (path, source) in raw_files {
|
||||||
|
let path = normalize_path(&path).ok_or_else(|| format!("invalid file path `{path}`"))?;
|
||||||
|
files.insert(path, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = normalize_path(entry).ok_or_else(|| format!("invalid entry path `{entry}`"))?;
|
||||||
|
let source = files
|
||||||
|
.get(&entry)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format!("entry file `{entry}` was not found"))?;
|
||||||
|
|
||||||
|
let mut engine = Engine::new(VirtualLoader { files });
|
||||||
|
let module = match engine.add_root_source(entry.clone(), entry.clone(), &source) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(error) => return Err(format_diagnostic_with_root(&error, &entry)),
|
||||||
|
};
|
||||||
|
let value = match engine.eval_module(module) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(engine.format_diagnostic(&error)),
|
||||||
|
};
|
||||||
|
let data = match engine.materialize(&value) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(error) => return Err(engine.format_diagnostic(&error)),
|
||||||
|
};
|
||||||
|
Ok(format_data(&data, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct VirtualLoader {
|
||||||
|
files: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceLoader for VirtualLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedSource> {
|
||||||
|
let key = resolve_import(current_key, specifier).ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("invalid import path `{specifier}`"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let source = self.files.get(&key).cloned().ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("import `{specifier}` resolved to `{key}`, but that file does not exist"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(LoadedSource {
|
||||||
|
key: key.clone(),
|
||||||
|
name: key,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_import(current_key: Option<&str>, specifier: &str) -> Option<String> {
|
||||||
|
if specifier.starts_with('/') {
|
||||||
|
return normalize_path(specifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut base = String::new();
|
||||||
|
if let Some(current_key) = current_key {
|
||||||
|
if let Some((parent, _file)) = current_key.rsplit_once('/') {
|
||||||
|
base.push_str(parent);
|
||||||
|
base.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.push_str(specifier);
|
||||||
|
normalize_path(&base)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_path(path: &str) -> Option<String> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
let normalized = path.replace('\\', "/");
|
||||||
|
for part in normalized.split('/') {
|
||||||
|
match part {
|
||||||
|
"" | "." => {}
|
||||||
|
".." => {
|
||||||
|
parts.pop()?;
|
||||||
|
}
|
||||||
|
part => parts.push(part),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(parts.join("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_diagnostic_with_root(diagnostic: &decodal::Diagnostic, root_name: &str) -> String {
|
||||||
|
format_diagnostic_with(diagnostic, |source| {
|
||||||
|
(source == SourceId(0)).then_some(root_name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_data(data: &Data, indent: usize) -> String {
|
||||||
|
match data {
|
||||||
|
Data::String(value) => json_string(value),
|
||||||
|
Data::Int(value) => value.to_string(),
|
||||||
|
Data::Float(value) => value.to_string(),
|
||||||
|
Data::Bool(value) => value.to_string(),
|
||||||
|
Data::Array(items) => {
|
||||||
|
if items.is_empty() {
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
let mut out = String::from("[\n");
|
||||||
|
for (index, item) in items.iter().enumerate() {
|
||||||
|
out.push_str(&" ".repeat(indent + 2));
|
||||||
|
out.push_str(&format_data(item, indent + 2));
|
||||||
|
if index + 1 != items.len() {
|
||||||
|
out.push(',');
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(&" ".repeat(indent));
|
||||||
|
out.push(']');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
Data::Object(fields) => {
|
||||||
|
if fields.is_empty() {
|
||||||
|
return String::from("{}");
|
||||||
|
}
|
||||||
|
let mut out = String::from("{\n");
|
||||||
|
for (index, field) in fields.iter().enumerate() {
|
||||||
|
out.push_str(&" ".repeat(indent + 2));
|
||||||
|
out.push_str(&json_string(&field.name));
|
||||||
|
out.push_str(": ");
|
||||||
|
out.push_str(&format_data(&field.value, indent + 2));
|
||||||
|
if index + 1 != fields.len() {
|
||||||
|
out.push(',');
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(&" ".repeat(indent));
|
||||||
|
out.push('}');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_string(value: &str) -> String {
|
||||||
|
let mut out = String::from("\"");
|
||||||
|
for ch in value.chars() {
|
||||||
|
match ch {
|
||||||
|
'"' => out.push_str("\\\""),
|
||||||
|
'\\' => out.push_str("\\\\"),
|
||||||
|
'\n' => out.push_str("\\n"),
|
||||||
|
'\r' => out.push_str("\\r"),
|
||||||
|
'\t' => out.push_str("\\t"),
|
||||||
|
ch if ch.is_control() => {
|
||||||
|
use core::fmt::Write;
|
||||||
|
let _ = write!(out, "\\u{:04x}", ch as u32);
|
||||||
|
}
|
||||||
|
ch => out.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push('"');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{evaluate_project_inner, normalize_path, resolve_import};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_virtual_paths() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_path("/schemas/../main.dcdl"),
|
||||||
|
Some("main.dcdl".into())
|
||||||
|
);
|
||||||
|
assert_eq!(normalize_path("../main.dcdl"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_imports_relative_to_current_file() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_import(Some("schemas/service.dcdl"), "./types.dcdl"),
|
||||||
|
Some("schemas/types.dcdl".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluates_project_imports() {
|
||||||
|
let files = r#"{
|
||||||
|
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 9443; }",
|
||||||
|
"schemas/service.dcdl":"Service = { name = String default \"api\"; port = Int & > 443 default 8443; }"
|
||||||
|
}"#;
|
||||||
|
let output = evaluate_project_inner("main.dcdl", files).unwrap();
|
||||||
|
assert!(output.contains("\"port\": 9443"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_diagnostics_use_virtual_file_names() {
|
||||||
|
let files = r#"{
|
||||||
|
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 80; }",
|
||||||
|
"schemas/service.dcdl":"Service = { port = Int & > 443 default 8443; }"
|
||||||
|
}"#;
|
||||||
|
let error = evaluate_project_inner("main.dcdl", files).unwrap_err();
|
||||||
|
assert!(error.contains("main.dcdl:"));
|
||||||
|
assert!(error.contains("schemas/service.dcdl:"));
|
||||||
|
assert!(!error.contains("source 0:"));
|
||||||
|
assert!(!error.contains("source 1:"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@ pkgs.mkShell {
|
|||||||
cargo
|
cargo
|
||||||
clippy
|
clippy
|
||||||
git
|
git
|
||||||
|
lld
|
||||||
nodejs
|
nodejs
|
||||||
nixfmt
|
nixfmt
|
||||||
rustc
|
rustc
|
||||||
rustfmt
|
rustfmt
|
||||||
tree-sitter
|
tree-sitter
|
||||||
|
wasm-pack
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Components
|
||||||
|
|
||||||
|
Decodal is split into a small runtime core and separate syntax tooling components.
|
||||||
|
The public surface is intentionally organized by use case: execute Decodal with Rust or WebAssembly, edit Decodal on the Web with Lezer and CodeMirror, and integrate Decodal into general-purpose editors with Tree-sitter.
|
||||||
|
|
||||||
|
## Runtime components
|
||||||
|
|
||||||
|
### Rust crate
|
||||||
|
|
||||||
|
The `decodal` crate is the primary Rust runtime and embedding API.
|
||||||
|
It owns parsing, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits.
|
||||||
|
Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-core/
|
||||||
|
crates/decodal-derive/
|
||||||
|
```
|
||||||
|
|
||||||
|
`decodal-derive` provides optional derive macros for Rust struct integration.
|
||||||
|
It is a companion to the runtime crate rather than an editor or syntax-highlighting component.
|
||||||
|
|
||||||
|
### WebAssembly package
|
||||||
|
|
||||||
|
`decodal-wasm` exposes the runtime to browsers.
|
||||||
|
The documentation site playground uses it to evaluate Decodal entirely in the browser.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-wasm/
|
||||||
|
site/decodal-site/src/wasm/
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated files in `site/decodal-site/src/wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
|
||||||
|
The WebAssembly package is for execution, not syntax highlighting.
|
||||||
|
|
||||||
|
## Web editor components
|
||||||
|
|
||||||
|
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
|
||||||
|
This is the source of syntax highlighting, folding, indentation, and editor syntax tree behavior in the browser UI.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
editors/lezer-decodal/decodal.grammar
|
||||||
|
packages/decodal-codemirror/src/decodal.js
|
||||||
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
|
packages/decodal-codemirror/src/decodal-parser.terms.js
|
||||||
|
```
|
||||||
|
|
||||||
|
The npm package is `decodal-codemirror`.
|
||||||
|
The JSR package is `@hare/decodal-codemirror`.
|
||||||
|
|
||||||
|
The Lezer grammar is derived from the canonical grammar documentation, but it is not a literal copy of the EBNF.
|
||||||
|
Precedence and token conflict handling are represented in the Lezer grammar in the form CodeMirror needs.
|
||||||
|
|
||||||
|
## General editor components
|
||||||
|
|
||||||
|
Tree-sitter is the portable editor-integration grammar.
|
||||||
|
Editors such as Zed, Neovim, Helix, and Emacs should consume this component when they need Decodal parsing or highlighting outside the Web playground.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
editors/tree-sitter-decodal/grammar.js
|
||||||
|
editors/tree-sitter-decodal/queries/highlights.scm
|
||||||
|
editors/tree-sitter-decodal/queries/locals.scm
|
||||||
|
editors/tree-sitter-decodal/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated parser sources under `editors/tree-sitter-decodal/src/` are committed so editor integrations can consume the grammar without regenerating it first.
|
||||||
|
|
||||||
|
## Canonical grammar
|
||||||
|
|
||||||
|
The human-readable grammar lives in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
doc/manual/souce/language/grammar.md
|
||||||
|
```
|
||||||
|
|
||||||
|
This EBNF is the language-level reference.
|
||||||
|
The Rust parser, Lezer grammar, and Tree-sitter grammar should be kept aligned with it, but each implementation may encode precedence and recovery behavior in the form required by its parser generator or runtime.
|
||||||
|
|
||||||
|
## What is not a public component
|
||||||
|
|
||||||
|
The Rust lexer is an implementation detail of the Rust parser.
|
||||||
|
Decodal does not expose a standalone public tokenizer API for editor tooling.
|
||||||
|
Consumers that need syntax information should use the component that matches their environment:
|
||||||
|
|
||||||
|
- Rust execution and embedding: `decodal`
|
||||||
|
- Browser execution: `decodal-wasm`
|
||||||
|
- Web editor syntax: Lezer / CodeMirror
|
||||||
|
- General editor syntax: Tree-sitter
|
||||||
|
|
||||||
|
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
||||||
@@ -59,8 +59,9 @@ Config = MyConfig & {
|
|||||||
|
|
||||||
## default の合成
|
## default の合成
|
||||||
|
|
||||||
初期方針では、`&` による異なる default 同士の合成は conflict とする。
|
`&` で片方だけが default を持つ場合、その default を保持する。
|
||||||
同じ default は同一候補として扱ってよい。
|
同じ thunk 由来の default 同士は同一 default として保持する。
|
||||||
|
異なる default 同士は conflict とする。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
merge_default(None, None) -> None
|
merge_default(None, None) -> None
|
||||||
@@ -117,3 +118,9 @@ Abstract { constraints, default: None }
|
|||||||
|
|
||||||
materialize は default を採用する唯一の段階である。
|
materialize は default を採用する唯一の段階である。
|
||||||
通常評価中に明示値が得られた場合、default は採用されない。
|
通常評価中に明示値が得られた場合、default は採用されない。
|
||||||
|
|
||||||
|
## Diagnostic context
|
||||||
|
|
||||||
|
Composition and materialization keep source spans for object fields, constraints, defaults, and thunks where possible.
|
||||||
|
When a conflict occurs, the diagnostic should identify the operation span and related participant spans, such as the left field, right field, constraint, or default value.
|
||||||
|
For object materialization errors, diagnostics also include the field path being processed when known.
|
||||||
|
|||||||
@@ -10,10 +10,20 @@ Diagnostic {
|
|||||||
kind: DiagnosticKind
|
kind: DiagnosticKind
|
||||||
span: Span
|
span: Span
|
||||||
message: String
|
message: String
|
||||||
notes: Vec<Note>
|
labels: Vec<DiagnosticLabel>
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagnosticLabel {
|
||||||
|
span: Span
|
||||||
|
message: String
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`span` は primary location を示す。
|
||||||
|
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
||||||
|
`labels` は同じ error に関係する追加 location を示す。
|
||||||
|
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
||||||
|
|
||||||
代表的な diagnostic kind:
|
代表的な diagnostic kind:
|
||||||
|
|
||||||
- syntax error
|
- syntax error
|
||||||
@@ -38,6 +48,18 @@ Result<RuntimeValue, Diagnostic>
|
|||||||
|
|
||||||
これにより、制約違反、未定義識別子、循環依存、import 失敗などが通常値として流れることを避ける。
|
これにより、制約違反、未定義識別子、循環依存、import 失敗などが通常値として流れることを避ける。
|
||||||
|
|
||||||
|
## 合成と materialize の diagnostic
|
||||||
|
|
||||||
|
合成や materialize の失敗は、以下を示す。
|
||||||
|
|
||||||
|
1. どの段階で失敗したか: composition、patch、materialization
|
||||||
|
2. どの field path を処理中だったか
|
||||||
|
3. どの constraint、value、default が衝突したか
|
||||||
|
4. なぜ合成または materialize できないか
|
||||||
|
|
||||||
|
例えば default が constraint を満たさない場合は、constraint の位置と default value の位置の両方を label として持つ。
|
||||||
|
object field の合成で concrete value が衝突する場合は、左辺 field と右辺 field の位置を label として持つ。
|
||||||
|
|
||||||
## `try / catch` は core に入れない
|
## `try / catch` は core に入れない
|
||||||
|
|
||||||
汎用 `try / catch` は core に入れない。
|
汎用 `try / catch` は core に入れない。
|
||||||
@@ -47,21 +69,6 @@ fallback は有限で明示的な仕組みに限定する。
|
|||||||
|
|
||||||
- `default`: 未指定値の fallback。
|
- `default`: 未指定値の fallback。
|
||||||
- `match`: 有限 pattern に基づく分岐。
|
- `match`: 有限 pattern に基づく分岐。
|
||||||
- optional import: ファイル不存在など、限定された失敗だけを fallback 可能にする候補。
|
|
||||||
- optional field access: field 不在だけを fallback 可能にする候補。
|
|
||||||
- union / tagged schema: 複数 schema の選択を明示的に表す将来候補。
|
|
||||||
|
|
||||||
## optional fallback の扱い
|
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
|
||||||
|
それらは diagnostic として報告する。
|
||||||
optional import や optional field access を導入する場合も、捕捉できる失敗は限定する。
|
|
||||||
|
|
||||||
例として optional import は、ファイル不存在だけを fallback 可能にし、parse error や import 先の制約違反は diagnostic として報告する方がよい。
|
|
||||||
|
|
||||||
```text
|
|
||||||
optional import:
|
|
||||||
file not found -> fallback
|
|
||||||
parse error -> diagnostic
|
|
||||||
eval error -> diagnostic
|
|
||||||
```
|
|
||||||
|
|
||||||
この方針により、fallback は通常の値選択として扱い、エラー内容に依存した実行時分岐は避ける。
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ let / function env
|
|||||||
```
|
```
|
||||||
|
|
||||||
Module top-level bindings shadow prelude bindings.
|
Module top-level bindings shadow prelude bindings.
|
||||||
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
Primitive type names such as `String`, `Int`, `Float`, `Bool`, and `Array` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
||||||
|
|
||||||
## Global bindings
|
## Global bindings
|
||||||
|
|
||||||
The host can bind values before adding or evaluating user sources.
|
The host can bind values before adding or evaluating user sources.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use decodal_core::{EmptyLoader, Engine, HostValue};
|
use decodal::{EmptyLoader, Engine, HostValue};
|
||||||
|
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
|
||||||
@@ -87,6 +87,46 @@ Concrete(Object {
|
|||||||
|
|
||||||
This matches the runtime model used for Decodal source-defined schema objects.
|
This matches the runtime model used for Decodal source-defined schema objects.
|
||||||
|
|
||||||
|
## Typed Rust integration
|
||||||
|
|
||||||
|
Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
|
||||||
|
The derive implements two traits from the `decodal` crate:
|
||||||
|
|
||||||
|
- `DecodalSchema`: builds a `HostValue` schema that can be passed to `Engine::bind_global`.
|
||||||
|
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||||
|
|
||||||
|
#[derive(Decodal)]
|
||||||
|
struct Service {
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[decodal(gt = 443, default = 8443)]
|
||||||
|
port: i64,
|
||||||
|
|
||||||
|
#[decodal(rename = "feature.enable", default = true)]
|
||||||
|
feature_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine.bind_global("Service", Service::decodal_schema())?;
|
||||||
|
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
let data = engine.materialize(&value)?;
|
||||||
|
let service = Service::decodal_decode(&data)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported field attributes are intentionally small:
|
||||||
|
|
||||||
|
- `rename = "path.to.field"`
|
||||||
|
- `default`
|
||||||
|
- `default = value`
|
||||||
|
- numeric constraints: `gt`, `gte`, `lt`, `lte`
|
||||||
|
|
||||||
|
The derive does not add host callbacks or reflection.
|
||||||
|
It only generates schema construction and typed decoding code.
|
||||||
|
|
||||||
## SourceLoader and prelude together
|
## SourceLoader and prelude together
|
||||||
|
|
||||||
`SourceLoader` and host prelude bindings are independent mechanisms.
|
`SourceLoader` and host prelude bindings are independent mechanisms.
|
||||||
|
|||||||
@@ -1,10 +1,43 @@
|
|||||||
# Features
|
# Features
|
||||||
|
|
||||||
Decodal keeps the embedded core small by making heavy functionality opt-in.
|
Decodal は embedded use と小さい runtime を優先する。
|
||||||
|
言語機能を追加するときは、値の合成・検証・materialization に直接必要なものを core に残し、重い依存や高度な推論は optional feature または外部 tooling に分ける。
|
||||||
|
|
||||||
|
## Core feature boundary
|
||||||
|
|
||||||
|
Core に入れる機能は、基本的に deterministic な value transformation に限る。
|
||||||
|
|
||||||
|
- arithmetic / logical / comparison operators
|
||||||
|
- array concat
|
||||||
|
- object / constraint composition
|
||||||
|
- default materialization
|
||||||
|
- pure function evaluation
|
||||||
|
- host supplied import evaluation
|
||||||
|
|
||||||
|
Core に入れないものは以下である。
|
||||||
|
|
||||||
|
- filesystem / network / environment access
|
||||||
|
- time / random
|
||||||
|
- mutation
|
||||||
|
- reflection or existence probing
|
||||||
|
- arbitrary host function calls
|
||||||
|
- symbolic constraint solving beyond simple normalization
|
||||||
|
|
||||||
|
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。
|
||||||
|
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
|
||||||
|
|
||||||
|
## Constraint reasoning
|
||||||
|
|
||||||
|
Constraint normalization は軽量な範囲に留める。
|
||||||
|
primitive type conflict や明らかな numeric bound conflict は合成時に検出してよい。
|
||||||
|
一方で、symbolic arithmetic、boolean algebra、regex intersection、array length dependent typing のような重い推論は行わない。
|
||||||
|
|
||||||
|
評価済みの concrete value に対する検証は runtime / materialization で行う。
|
||||||
|
静的に完全な型検査フェーズを増やすのではなく、parse、evaluate、compose、materialize の各段階で自然に分かる error を diagnostic として返す。
|
||||||
|
|
||||||
## Core defaults
|
## Core defaults
|
||||||
|
|
||||||
`decodal-core` defaults to `std` only.
|
`decodal` defaults to `std` only.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[features]
|
[features]
|
||||||
@@ -13,7 +46,7 @@ std = []
|
|||||||
regex = ["std", "dep:regex"]
|
regex = ["std", "dep:regex"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Building `decodal-core` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
|
Building `decodal` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
|
||||||
|
|
||||||
## Regex
|
## Regex
|
||||||
|
|
||||||
@@ -21,7 +54,7 @@ Regex constraints are implemented behind the `regex` feature.
|
|||||||
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
|
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
```
|
```
|
||||||
|
|
||||||
Regex constraints are accumulated during `&` composition.
|
Regex constraints are accumulated during `&` composition.
|
||||||
@@ -30,5 +63,5 @@ Concrete strings must match every regex constraint attached to the abstract valu
|
|||||||
|
|
||||||
## CLI features
|
## CLI features
|
||||||
|
|
||||||
`decodal-cli` exposes a matching `regex` feature that enables `decodal-core/regex`.
|
`decodal-cli` exposes a matching `regex` feature that enables `decodal/regex`.
|
||||||
The feature is not enabled by default so the default CLI binary remains small.
|
The feature is not enabled by default so the default CLI binary remains small.
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ RuntimeValue =
|
|||||||
`Concrete` は明示的な値である。
|
`Concrete` は明示的な値である。
|
||||||
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
||||||
|
|
||||||
|
Decodal は `unknown`、`any`、`null` のような「存在するが意味が未確定な値」を runtime value として持たない。
|
||||||
|
識別子や field が解決できない場合は、その場で diagnostic になる。
|
||||||
|
未解決値を後続の演算へ流して推論することはしない。
|
||||||
|
|
||||||
## ConcreteValue
|
## ConcreteValue
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -32,7 +36,11 @@ object は concrete structure として扱う。
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
ObjectValue:
|
ObjectValue:
|
||||||
fields: Map<Symbol, ThunkId>
|
fields: Map<Symbol, ObjectField>
|
||||||
|
|
||||||
|
ObjectField:
|
||||||
|
value: ThunkId
|
||||||
|
span: Span
|
||||||
```
|
```
|
||||||
|
|
||||||
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
||||||
@@ -57,9 +65,14 @@ Concrete(Object {
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
AbstractValue {
|
AbstractValue {
|
||||||
constraints: Vec<Constraint>
|
constraints: Vec<ConstraintEntry>
|
||||||
default: Option<ThunkId>
|
default: Option<ThunkId>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConstraintEntry {
|
||||||
|
constraint: Constraint
|
||||||
|
span: Span
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`default` は `AbstractValue` にだけ存在する。
|
`default` は `AbstractValue` にだけ存在する。
|
||||||
@@ -88,6 +101,21 @@ port = 8000;
|
|||||||
Concrete(Int(8000))
|
Concrete(Int(8000))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Runtime scope
|
||||||
|
|
||||||
|
Decodal runtime は application runtime ではなく、pure value evaluator である。
|
||||||
|
同じ source、同じ import sources、同じ host globals が与えられた場合、評価結果は決定的である。
|
||||||
|
|
||||||
|
runtime が扱う責務は以下に限る。
|
||||||
|
|
||||||
|
- expression を評価する。
|
||||||
|
- thunk を必要に応じて force する。
|
||||||
|
- concrete / abstract value を合成する。
|
||||||
|
- materialize 時に constraint を検証する。
|
||||||
|
|
||||||
|
runtime は filesystem、network、environment variable、time、random、mutation を扱わない。
|
||||||
|
core における import は host supplied source を受け取る境界であり、filesystem access ではない。
|
||||||
|
|
||||||
## Constraint
|
## Constraint
|
||||||
|
|
||||||
constraint は concrete value とは別の型として扱う。
|
constraint は concrete value とは別の型として扱う。
|
||||||
|
|||||||
@@ -84,5 +84,5 @@ module root や field は thunk として保持され、参照されたときだ
|
|||||||
関数引数は thunk として関数の environment に束縛する。
|
関数引数は thunk として関数の environment に束縛する。
|
||||||
関数本体で引数が参照されたときだけ force する。
|
関数本体で引数が参照されたときだけ force する。
|
||||||
|
|
||||||
任意の関数呼び出し結果をグローバルに memoize する必要はない。
|
関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
field に束縛された関数呼び出し結果は、その field thunk の評価結果として memoize される。
|
field に束縛された関数呼び出し結果は、その field thunk の評価結果として memoize される。
|
||||||
|
|||||||
+163
-10
@@ -9,15 +9,160 @@ Run the normal Rust checks from the repository root.
|
|||||||
```sh
|
```sh
|
||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
cargo test
|
cargo test
|
||||||
cargo check -p decodal-core --no-default-features
|
cargo check -p decodal --no-default-features
|
||||||
nix flake check
|
nix flake check
|
||||||
```
|
```
|
||||||
|
|
||||||
Regex support is optional and should be tested explicitly when touched.
|
Regex support is optional and should be tested explicitly when touched.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test -p decodal-core --features regex
|
cargo test -p decodal --features regex
|
||||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
|
```
|
||||||
|
|
||||||
|
## crates.io release
|
||||||
|
|
||||||
|
The primary crates.io package is `decodal`, which contains the embeddable library.
|
||||||
|
`decodal-derive` provides optional derive macros for Rust struct integration and is published only when the derive crate changes.
|
||||||
|
Workspace support crates such as `decodal-cli` and the Rust source crate `decodal-wasm` are not published to crates.io.
|
||||||
|
The generated WebAssembly package under `site/decodal-site/src/wasm/` is published to npm and JSR.
|
||||||
|
The project is dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo publish -p decodal
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish `decodal-derive` first only when that crate has a new version:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo publish -p decodal-derive
|
||||||
|
```
|
||||||
|
|
||||||
|
Before publishing, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fmt --check
|
||||||
|
cargo test
|
||||||
|
cargo check -p decodal --no-default-features
|
||||||
|
cargo publish -p decodal --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
If `decodal-derive` changed, also run `cargo publish -p decodal-derive --dry-run`.
|
||||||
|
|
||||||
|
## Web site and playground
|
||||||
|
|
||||||
|
The Astro documentation site and browser playground are kept in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
site/decodal-site/
|
||||||
|
```
|
||||||
|
|
||||||
|
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
|
||||||
|
The playground loads `decodal-wasm` and evaluates DCDL entirely in the browser.
|
||||||
|
|
||||||
|
Important files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
site/decodal-site/src/pages/docs/[...slug].astro
|
||||||
|
site/decodal-site/src/pages/playground.astro
|
||||||
|
site/decodal-site/src/scripts/playground.js
|
||||||
|
site/decodal-site/src/scripts/playground-examples.js
|
||||||
|
site/decodal-site/src/layouts/ManualLayout.astro
|
||||||
|
site/decodal-site/src/lib/docs.js
|
||||||
|
site/decodal-site/src/lib/highlight.js
|
||||||
|
packages/decodal-codemirror/src/decodal.js
|
||||||
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
|
crates/decodal-wasm/src/lib.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the WebAssembly package before building the site:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd site/decodal-site
|
||||||
|
npm install
|
||||||
|
npm run build:wasm
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run build:wasm` writes generated files into:
|
||||||
|
|
||||||
|
```text
|
||||||
|
site/decodal-site/src/wasm/
|
||||||
|
```
|
||||||
|
|
||||||
|
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm package first.
|
||||||
|
|
||||||
|
Publish the generated WebAssembly package from its package directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd site/decodal-site/src/wasm
|
||||||
|
npm publish
|
||||||
|
npx jsr publish
|
||||||
|
```
|
||||||
|
|
||||||
|
Run dry-runs first when preparing a release:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm pack --dry-run
|
||||||
|
npx jsr publish --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
The npm package name is `decodal-wasm`.
|
||||||
|
The JSR package name is `@hare/decodal-wasm`.
|
||||||
|
JSR currently expects a single SPDX license identifier in `jsr.json`; the WebAssembly package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
|
The playground editor uses the `decodal-codemirror` package with the generated Lezer parser in `packages/decodal-codemirror/src/decodal-parser.js`.
|
||||||
|
The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
|
||||||
|
|
||||||
|
Publish the CodeMirror package from its package directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd packages/decodal-codemirror
|
||||||
|
npm install
|
||||||
|
npm publish
|
||||||
|
npx jsr publish
|
||||||
|
```
|
||||||
|
|
||||||
|
Run dry-runs first when preparing a release:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm pack --dry-run
|
||||||
|
npx jsr publish --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
The npm package name is `decodal-codemirror`.
|
||||||
|
The JSR package name is `@hare/decodal-codemirror`.
|
||||||
|
JSR currently expects a single SPDX license identifier in `jsr.json`; the CodeMirror package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
|
The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time.
|
||||||
|
|
||||||
|
To run the site locally:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd site/decodal-site
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy the static site to Cloudflare Pages with Wrangler direct upload:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd site/decodal-site
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
The deploy script runs `npm run build` and then uploads `dist/` to the Pages project named `decodal-site` on branch `master`.
|
||||||
|
Cloudflare Pages treats this as production when the project production branch is `master`.
|
||||||
|
Use `CLOUDFLARE_PROJECT_NAME` when deploying to a differently named Pages project.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CLOUDFLARE_PROJECT_NAME=my-pages-project npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If the committed WASM package must be regenerated before deploy, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run deploy:wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tree-sitter grammar
|
## Tree-sitter grammar
|
||||||
@@ -64,16 +209,22 @@ The generated parser files under `editors/tree-sitter-decodal/src/` are committe
|
|||||||
|
|
||||||
When the Decodal syntax changes:
|
When the Decodal syntax changes:
|
||||||
|
|
||||||
1. Update `grammar.js`.
|
1. Update the canonical EBNF in `doc/manual/souce/language/grammar.md`.
|
||||||
2. Run `npx tree-sitter generate`.
|
2. Update the Rust parser/lexer as needed.
|
||||||
3. Add or update corpus tests in `corpus/`.
|
3. Update `editors/tree-sitter-decodal/grammar.js` and run `npx tree-sitter generate` / `npx tree-sitter test`.
|
||||||
4. Update highlight queries in `queries/highlights.scm` if token names changed.
|
4. Update `editors/lezer-decodal/decodal.grammar` and regenerate the CodeMirror parser:
|
||||||
5. Run `npx tree-sitter test`.
|
|
||||||
6. Run Rust checks from the repository root if the language parser or examples also changed.
|
```sh
|
||||||
|
cd site/decodal-site
|
||||||
|
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o ../../packages/decodal-codemirror/src/decodal-parser.js
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add or update corpus/tests/examples.
|
||||||
|
6. Run Rust and site checks from the repository root.
|
||||||
|
|
||||||
## Development shell
|
## Development shell
|
||||||
|
|
||||||
The Nix development shell includes Rust tooling, Node.js, and Tree-sitter CLI tooling.
|
The Nix development shell includes Rust tooling, Node.js, Tree-sitter CLI tooling, and wasm-pack tooling.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix develop
|
nix develop
|
||||||
@@ -87,5 +238,7 @@ The shell provides:
|
|||||||
- `clippy`
|
- `clippy`
|
||||||
- `node`
|
- `node`
|
||||||
- `npm`
|
- `npm`
|
||||||
|
- `wasm-pack`
|
||||||
|
- `lld`
|
||||||
- `tree-sitter`
|
- `tree-sitter`
|
||||||
- `nixfmt`
|
- `nixfmt`
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
## 目次
|
## 目次
|
||||||
|
|
||||||
1. [Introduction](./introduction.md)
|
1. [Introduction](./introduction.md)
|
||||||
2. [Language Specification](./language/index.md)
|
2. [Components](./components.md)
|
||||||
|
3. [Language Specification](./language/index.md)
|
||||||
1. [Lexical Structure and Syntax](./language/syntax.md)
|
1. [Lexical Structure and Syntax](./language/syntax.md)
|
||||||
2. [Value](./language/value/index.md)
|
2. [Value](./language/value/index.md)
|
||||||
1. [String](./language/value/string.md)
|
1. [String](./language/value/string.md)
|
||||||
@@ -35,7 +36,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
9. [Materialization and Errors](./language/materialization-and-errors.md)
|
9. [Materialization and Errors](./language/materialization-and-errors.md)
|
||||||
10. [Naming Conventions](./language/naming.md)
|
10. [Naming Conventions](./language/naming.md)
|
||||||
11. [Examples](./language/examples.md)
|
11. [Examples](./language/examples.md)
|
||||||
3. [Implementation Design](./design/index.md)
|
4. [Implementation Design](./design/index.md)
|
||||||
1. [Execution Pipeline](./design/execution-pipeline.md)
|
1. [Execution Pipeline](./design/execution-pipeline.md)
|
||||||
2. [Runtime Model](./design/runtime-model.md)
|
2. [Runtime Model](./design/runtime-model.md)
|
||||||
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
|
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
|
||||||
@@ -43,5 +44,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
||||||
6. [Embedding API](./design/embedding-api.md)
|
6. [Embedding API](./design/embedding-api.md)
|
||||||
7. [Features](./design/features.md)
|
7. [Features](./design/features.md)
|
||||||
4. [Development](./development.md)
|
5. [Development](./development.md)
|
||||||
5. [Open Issues](./open-issues.md)
|
6. [Open Issues](./open-issues.md)
|
||||||
|
|||||||
@@ -188,11 +188,12 @@ RuntimeValue =
|
|||||||
|
|
||||||
## default の合成
|
## default の合成
|
||||||
|
|
||||||
同じフィールドに複数の `default` が合成された場合の詳細規則は未確定である。
|
`default` の合成規則は以下である。
|
||||||
現時点の単純な方針は以下である。
|
|
||||||
|
|
||||||
- `&` による default 同士の衝突はエラーにする。
|
- `&` で片方だけが default を持つ場合、その default を保持する。
|
||||||
- 同一 default 値は許可してよい。
|
- `&` で両方が異なる default を持つ場合、conflict になる。
|
||||||
- `//` による patch では右辺 default が左辺 default を置き換える。
|
- `Abstract & Concrete` が成功した場合、結果は concrete value になり default は消える。
|
||||||
|
- `//` では右辺が左辺を置き換える。object/object の場合は field ごとに再帰 patch されるため、右辺 field の default が左辺 field の default を置き換える。
|
||||||
|
|
||||||
この方針により、`&` は制約を保った合成、`//` は上書き操作として説明できる。
|
`default` thunk は materialize 時に必要になった時点で評価する。
|
||||||
|
評価された default value は、同じ abstract value に残っている制約を満たす必要がある。
|
||||||
|
|||||||
@@ -74,5 +74,5 @@ Error 評価失敗
|
|||||||
関数引数は thunk として渡せる。
|
関数引数は thunk として渡せる。
|
||||||
関数本体内で引数が参照されたときに評価する。
|
関数本体内で引数が参照されたときに評価する。
|
||||||
|
|
||||||
任意の関数呼び出し結果をグローバルに memoize することは必須ではない。
|
関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
ただし、フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
ただし、フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
## 基本的な設定スキーマ
|
## 基本的な設定スキーマ
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
rec {
|
|
||||||
Host = IPv4Address;
|
Host = IPv4Address;
|
||||||
|
|
||||||
Port = Int & >= 1 & <= 65535;
|
Port = Int & >= 1 & <= 65535;
|
||||||
@@ -19,7 +18,6 @@ rec {
|
|||||||
fuga = Int default 10;
|
fuga = Int default 10;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
NewConfig = MyConfig & {
|
NewConfig = MyConfig & {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
@@ -33,28 +31,20 @@ disabled_config = NewConfig & {
|
|||||||
enabled_config = NewConfig;
|
enabled_config = NewConfig;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 関数と文字列生成
|
## 関数と制約
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
let
|
let
|
||||||
maybe_hw = String & /Hello! .*/;
|
Port = Int & >= 1 & <= 65535;
|
||||||
part = {
|
add_offset = (base: Port, offset: Int) => base + offset;
|
||||||
greet = String;
|
|
||||||
target = String;
|
|
||||||
};
|
|
||||||
mk_hw = (part: part) =>
|
|
||||||
maybe_hw & "${part.greet}! ${part.target}";
|
|
||||||
in
|
in
|
||||||
mk_hw({
|
add_offset(8000, 80)
|
||||||
greet = "Hello";
|
|
||||||
target = "World";
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
評価結果:
|
評価結果:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
"Hello! World"
|
8080
|
||||||
```
|
```
|
||||||
|
|
||||||
## match
|
## match
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Arithmetic Expression
|
||||||
|
|
||||||
|
Decodal supports arithmetic over concrete numeric values.
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
{
|
||||||
|
workers = 2 + 2;
|
||||||
|
timeout = 30.0 / 2;
|
||||||
|
port = 8000 + 80;
|
||||||
|
negative = -1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
- `+` addition
|
||||||
|
- `-` subtraction
|
||||||
|
- `*` multiplication
|
||||||
|
- `/` division
|
||||||
|
- unary `-` negation
|
||||||
|
|
||||||
|
`*` and `/` bind tighter than `+` and `-`.
|
||||||
|
Parentheses can be used to make grouping explicit.
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
2 + 3 * 4 # 14
|
||||||
|
(2 + 3) * 4 # 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Numeric behavior
|
||||||
|
|
||||||
|
Arithmetic requires concrete `Int` or `Float` operands.
|
||||||
|
`Int + Int`, `Int - Int`, and `Int * Int` produce `Int` when no overflow occurs.
|
||||||
|
Mixed `Int` / `Float` arithmetic produces `Float`.
|
||||||
|
Division always produces `Float`.
|
||||||
|
|
||||||
|
Division by zero and integer overflow are evaluation errors.
|
||||||
|
|
||||||
|
Arithmetic expressions can be used anywhere a concrete numeric expression is expected, including defaults and numeric constraints.
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
port = Int & > 4000 + 42 default 8080;
|
||||||
|
```
|
||||||
@@ -7,9 +7,21 @@ array expression は、順序付きの値の列を表す。
|
|||||||
["a", "b", "c"]
|
["a", "b", "c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 未確定事項
|
## Array concat
|
||||||
|
|
||||||
- 配列要素の制約表現。
|
`++` は concrete array 同士を連結する。
|
||||||
- 異種配列を許可するか。
|
|
||||||
- `//` による patch を右辺置換だけにするか。
|
```dcdl
|
||||||
- append / prepend / remove などの操作を提供するか。
|
base = ["read", "write"];
|
||||||
|
extra = ["admin"];
|
||||||
|
roles = base ++ extra;
|
||||||
|
```
|
||||||
|
|
||||||
|
`roles` は以下と同じ値になる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
["read", "write", "admin"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`++` は配列要素を変換しない。
|
||||||
|
左辺の要素の後に右辺の要素が並ぶ。
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
function call expression は、関数値を引数に適用する式である。
|
function call expression は、関数値を引数に適用する式である。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
mk_hw({
|
increment(41)
|
||||||
greet = "Hello";
|
|
||||||
target = "World";
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 評価
|
## 評価
|
||||||
@@ -14,4 +11,5 @@ mk_hw({
|
|||||||
引数は thunk として渡せる。
|
引数は thunk として渡せる。
|
||||||
関数本体内で引数が参照されたときに評価する。
|
関数本体内で引数が参照されたときに評価する。
|
||||||
|
|
||||||
任意の関数呼び出し結果をグローバルに memoize することは必須ではない。
|
関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
|
フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
function expression は、引数を受け取り式を返す値である。
|
function expression は、引数を受け取り式を返す値である。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
(part: {
|
(value: Int) => value + 1
|
||||||
greet = String;
|
|
||||||
target = String;
|
|
||||||
}) =>
|
|
||||||
"${part.greet}! ${part.target}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
関数仕様の詳細は [関数](../functions.md) に置く。
|
関数仕様の詳細は [関数](../functions.md) に置く。
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ let expression は、ローカル束縛を作る。
|
|||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
let
|
let
|
||||||
part = {
|
base = 8000;
|
||||||
greet = "Hello";
|
offset = 80;
|
||||||
target = "World";
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
"${part.greet}! ${part.target}"
|
base + offset
|
||||||
```
|
```
|
||||||
|
|
||||||
## 評価
|
## 評価
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Logical and Comparison Expressions
|
||||||
|
|
||||||
|
Decodal supports boolean logic over concrete `Bool` values and comparison over concrete scalar values.
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
{
|
||||||
|
is_prod = env == "prod";
|
||||||
|
high_port = port > 9000;
|
||||||
|
enabled = is_prod && high_port;
|
||||||
|
disabled = !enabled;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logical operators
|
||||||
|
|
||||||
|
- `!expr` negates a concrete `Bool`.
|
||||||
|
- `lhs && rhs` returns boolean AND.
|
||||||
|
- `lhs || rhs` returns boolean OR.
|
||||||
|
|
||||||
|
`&&` and `||` short-circuit: the right-hand side is evaluated only when needed.
|
||||||
|
Logical operands must evaluate to concrete `Bool` values.
|
||||||
|
|
||||||
|
## Comparison operators
|
||||||
|
|
||||||
|
- `==`
|
||||||
|
- `!=`
|
||||||
|
- `<`
|
||||||
|
- `<=`
|
||||||
|
- `>`
|
||||||
|
- `>=`
|
||||||
|
|
||||||
|
`==` and `!=` compare concrete scalar values: `String`, `Bool`, `Int`, and `Float`.
|
||||||
|
`Int` and `Float` can be compared to each other numerically.
|
||||||
|
|
||||||
|
Ordering operators `<`, `<=`, `>`, and `>=` compare concrete numeric values only.
|
||||||
|
They are separate from prefix comparison constraints such as `> 443`.
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
port = Int & > 443 default 9443;
|
||||||
|
is_high = port > 9000;
|
||||||
|
```
|
||||||
@@ -12,4 +12,5 @@ config.feature_hoge.enable
|
|||||||
左側の式を object として評価し、指定された field を参照する。
|
左側の式を object として評価し、指定された field を参照する。
|
||||||
参照先 field は必要になるまで評価されない。
|
参照先 field は必要になるまで評価されない。
|
||||||
|
|
||||||
存在しない field への参照をエラーにするか、open schema として扱うかは未確定である。
|
存在しない field への参照は diagnostic になる。
|
||||||
|
Decodal は unknown / Any のような値を伝播せず、field の有無を曖昧にしない。
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# String Interpolation Expression
|
# String Interpolation
|
||||||
|
|
||||||
string interpolation は、文字列内に式を埋め込む候補機能である。
|
文字列補間は初期実装には含めない。
|
||||||
|
|
||||||
```dcdl
|
Decodal の string literal は、現時点では literal text として扱う。
|
||||||
"${part.greet}! ${part.target}"
|
式を埋め込む構文は定義しない。
|
||||||
```
|
|
||||||
|
|
||||||
## 評価
|
必要になった場合は、文字列連結や明示的な formatting function として別途設計する。
|
||||||
|
|
||||||
補間式の評価タイミングは通常の遅延評価に従う。
|
|
||||||
文字列補間を初期実装に含めるかは未確定である。
|
|
||||||
|
|||||||
@@ -5,54 +5,47 @@
|
|||||||
## 構文
|
## 構文
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
(part: {
|
(value: Int) => value + 1
|
||||||
greet = String;
|
|
||||||
target = String;
|
|
||||||
}) =>
|
|
||||||
"${part.greet}! ${part.target}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
関数呼び出しは通常の呼び出し構文で行う。
|
関数呼び出しは通常の呼び出し構文で行う。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
mk_hw({
|
let
|
||||||
greet = "Hello";
|
increment = (value: Int) => value + 1;
|
||||||
target = "World";
|
in
|
||||||
})
|
increment(41)
|
||||||
```
|
```
|
||||||
|
|
||||||
複数引数の構文は候補として以下を想定する。
|
複数引数も指定できる。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
(
|
(input_a: { hoge = Int & >= 0; }, input_b: { fuga = Int; }) =>
|
||||||
input_a: {
|
|
||||||
hoge = Int & >= 0;
|
|
||||||
},
|
|
||||||
input_b: {
|
|
||||||
fuga = 20;
|
|
||||||
}
|
|
||||||
) =>
|
|
||||||
{
|
{
|
||||||
# ...
|
hoge = input_a.hoge;
|
||||||
|
fuga = input_b.fuga;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 関数の意味
|
## 関数の意味
|
||||||
|
|
||||||
関数は値として扱える。
|
関数は runtime value として扱えるが、最終データとして materialize することはできない。
|
||||||
ただし、最終データとして関数値を出力できるかどうかは別途定める。
|
未適用の関数値が materialize 対象に残っている場合は diagnostic になる。
|
||||||
設定ファイルを materialize する段階では、未適用の関数値は出力不能な値として扱うのが自然である。
|
|
||||||
|
関数値は opaque であり、関数値同士の等価性は提供しない。
|
||||||
|
`&` で関数値同士を合成すると conflict になる。
|
||||||
|
|
||||||
## 評価方針
|
## 評価方針
|
||||||
|
|
||||||
関数は以下の方針を基本とする。
|
関数は以下の方針で評価する。
|
||||||
|
|
||||||
- 関数は純粋である。
|
- 関数は純粋である。
|
||||||
- 関数はレキシカルスコープを持つ。
|
- 関数はレキシカルスコープを持つ。
|
||||||
- 関数は定義時の環境を参照として保持する。
|
- 関数は定義時の環境を参照として保持する。
|
||||||
- 引数は必要になるまで評価しない。
|
- 引数は thunk として渡され、必要になるまで評価されない。
|
||||||
- フィールドに束縛された関数呼び出し結果は、そのフィールド評価結果として memoize される。
|
- 関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
- 任意の関数呼び出しそのものをグローバルに memoize することは必須ではない。
|
- フィールドに束縛された関数呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
||||||
|
- 再帰的な依存は thunk cycle として diagnostic になる。
|
||||||
|
|
||||||
関数値の内部モデル例:
|
関数値の内部モデル例:
|
||||||
|
|
||||||
@@ -63,29 +56,3 @@ Function {
|
|||||||
env: EnvRef
|
env: EnvRef
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 関数と重さ
|
|
||||||
|
|
||||||
関数の文法・パーサー自体は大きくない。
|
|
||||||
実装上の重さは、主に評価モデルと意味論から発生する。
|
|
||||||
|
|
||||||
注意点:
|
|
||||||
|
|
||||||
- クロージャが環境を掴む。
|
|
||||||
- 引数を lazy にするか strict にするかを決める必要がある。
|
|
||||||
- 関数呼び出し結果をどこまで memoize するかを決める必要がある。
|
|
||||||
- 再帰関数を許可するかを決める必要がある。
|
|
||||||
- 関数値同士の `&` をどう扱うかを決める必要がある。
|
|
||||||
- 関数値を object に入れたとき、materialize 可能かを決める必要がある。
|
|
||||||
- import 循環と関数適用が絡んだときの cycle detection が必要になる。
|
|
||||||
|
|
||||||
軽量に保つため、初期仕様では以下の制限を検討できる。
|
|
||||||
|
|
||||||
- 関数は pure。
|
|
||||||
- lexical closure は許可。
|
|
||||||
- 引数は thunk として渡す。
|
|
||||||
- 関数本体は必要になるまで評価しない。
|
|
||||||
- 関数値は opaque。
|
|
||||||
- 関数同士の `&` は、同一関数参照以外は conflict。
|
|
||||||
- 関数値は最終データとして出力できない。
|
|
||||||
- 再帰は cycle error として扱う、または v1 では禁止する。
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Grammar
|
||||||
|
|
||||||
|
This page is the canonical grammar reference for Decodal syntax.
|
||||||
|
Parser implementations such as the Rust parser, Tree-sitter grammar, and Lezer grammar should follow this grammar and may add implementation-specific precedence annotations where needed.
|
||||||
|
|
||||||
|
## Lexical grammar
|
||||||
|
|
||||||
|
```ebnf
|
||||||
|
source_character = ? any Unicode scalar value ? ;
|
||||||
|
newline = "\n" | "\r\n" | "\r" ;
|
||||||
|
space = " " | "\t" | newline ;
|
||||||
|
comment = "#" , { ? any character except newline ? } ;
|
||||||
|
|
||||||
|
digit = "0" … "9" ;
|
||||||
|
letter = "A" … "Z" | "a" … "z" ;
|
||||||
|
identifier = letter , { letter | digit | "_" } ;
|
||||||
|
|
||||||
|
integer = digit , { digit } ;
|
||||||
|
float = digit , { digit } , "." , digit , { digit } ;
|
||||||
|
|
||||||
|
string = '"' , { string_character | escape } , '"' ;
|
||||||
|
string_character = ? any character except '"', "\\", or newline ? ;
|
||||||
|
escape = "\\" , source_character ;
|
||||||
|
|
||||||
|
regex = "/" , regex_character , { regex_character } , "/" ;
|
||||||
|
regex_character = escape | ? any character except "/", "\\", or newline ? ;
|
||||||
|
```
|
||||||
|
|
||||||
|
Whitespace and comments separate tokens and are otherwise ignored by the parser.
|
||||||
|
|
||||||
|
## Syntactic grammar
|
||||||
|
|
||||||
|
```ebnf
|
||||||
|
module = { statement } ;
|
||||||
|
statement = field_definition , [ ";" ]
|
||||||
|
| expression , [ ";" ] ;
|
||||||
|
|
||||||
|
expression = default_expression ;
|
||||||
|
|
||||||
|
default_expression = patch_expression , [ "default" , default_expression ] ;
|
||||||
|
patch_expression = compose_expression , { "//" , compose_expression } ;
|
||||||
|
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
|
||||||
|
|
||||||
|
logical_or_expression = logical_and_expression , { "||" , logical_and_expression } ;
|
||||||
|
logical_and_expression = comparison_expression , { "&&" , comparison_expression } ;
|
||||||
|
comparison_expression = concat_expression , [ comparison_operator , concat_expression ] ;
|
||||||
|
concat_expression = additive_expression , { "++" , additive_expression } ;
|
||||||
|
additive_expression = multiplicative_expression , { ( "+" | "-" ) , multiplicative_expression } ;
|
||||||
|
multiplicative_expression = unary_expression , { ( "*" | "/" ) , unary_expression } ;
|
||||||
|
|
||||||
|
unary_expression = [ "!" | "-" ] , postfix_expression ;
|
||||||
|
postfix_expression = primary_expression , { call_suffix | path_suffix } ;
|
||||||
|
call_suffix = "(" , [ argument_list ] , ")" ;
|
||||||
|
path_suffix = "." , identifier ;
|
||||||
|
|
||||||
|
primary_expression = literal
|
||||||
|
| identifier
|
||||||
|
| comparison_constraint
|
||||||
|
| object
|
||||||
|
| array
|
||||||
|
| let_expression
|
||||||
|
| function_expression
|
||||||
|
| match_expression
|
||||||
|
| import_expression
|
||||||
|
| "(" , expression , ")" ;
|
||||||
|
|
||||||
|
literal = string | integer | float | "true" | "false" | regex ;
|
||||||
|
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||||
|
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
|
||||||
|
|
||||||
|
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
|
||||||
|
field_definition = field_path , "=" , expression ;
|
||||||
|
field_path = identifier , { "." , identifier } ;
|
||||||
|
|
||||||
|
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
|
||||||
|
|
||||||
|
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
|
||||||
|
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
|
||||||
|
parameter_list = parameter , { "," , parameter } , [ "," ] ;
|
||||||
|
parameter = identifier , [ ":" , expression ] ;
|
||||||
|
|
||||||
|
match_expression = "match" , expression , "{" , [ match_arm , { ";" , match_arm } , [ ";" ] ] , "}" ;
|
||||||
|
match_arm = pattern , ":" , expression ;
|
||||||
|
pattern = "_" | expression ;
|
||||||
|
|
||||||
|
import_expression = "import" , string ;
|
||||||
|
argument_list = expression , { "," , expression } , [ "," ] ;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Precedence
|
||||||
|
|
||||||
|
Precedence is highest first.
|
||||||
|
|
||||||
|
1. function call and field path reference
|
||||||
|
2. unary `!` and `-`
|
||||||
|
3. `*` and `/`
|
||||||
|
4. `+` and `-`
|
||||||
|
5. `++`
|
||||||
|
6. `==`, `!=`, `<`, `<=`, `>`, `>=`
|
||||||
|
7. `&&`
|
||||||
|
8. `||`
|
||||||
|
9. `&`
|
||||||
|
10. `//`
|
||||||
|
11. `default`
|
||||||
|
|
||||||
|
Binary operators are left-associative except `default`, which is right-associative.
|
||||||
|
|
||||||
|
## Tooling mapping
|
||||||
|
|
||||||
|
Syntax tooling should derive token categories from this grammar rather than making a tool-specific grammar canonical.
|
||||||
|
Tree-sitter and Lezer grammars are implementation artifacts that follow this page.
|
||||||
@@ -95,4 +95,5 @@ match value {
|
|||||||
通常の式はエラー内容に基づいて分岐できない。
|
通常の式はエラー内容に基づいて分岐できない。
|
||||||
|
|
||||||
汎用 `try / catch` は core には含めない。
|
汎用 `try / catch` は core には含めない。
|
||||||
fallback は `default`、`match`、および将来的な optional import / optional field access のような限定された仕組みで表現する。
|
fallback は `default` と `match` で表現する。
|
||||||
|
optional import や optional field access は core には含めない。
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ result = schema;
|
|||||||
```
|
```
|
||||||
|
|
||||||
この場合、`result` の右辺の `schema` は同じ module の top-level field `schema` を参照する。
|
この場合、`result` の右辺の `schema` は同じ module の top-level field `schema` を参照する。
|
||||||
通常の object literal 内の field を暗黙に recursive scope にするかは別仕様とする。
|
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない。
|
||||||
|
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
|
||||||
|
|
||||||
## SourceLoader
|
## SourceLoader
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,79 @@
|
|||||||
# 合成演算子
|
# 演算子
|
||||||
|
|
||||||
この章では、`&` と `//` の意味を定義する。
|
この章では、Decodal の演算子の意味を定義する。
|
||||||
|
|
||||||
|
## 演算子一覧
|
||||||
|
|
||||||
|
| 演算子 | 形 | 種類 | 対象 | 結果 / 意味 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `.` | `object.field` | field reference | object / abstract object | field value |
|
||||||
|
| call | `fn(arg)` | function call | function | function result |
|
||||||
|
| `!` | `!expr` | unary logical | concrete `Bool` | concrete `Bool` |
|
||||||
|
| `-` | `-expr` | unary arithmetic | concrete `Int` / `Float` | negated number |
|
||||||
|
| `*` | `lhs * rhs` | arithmetic | concrete `Int` / `Float` | numeric product |
|
||||||
|
| `/` | `lhs / rhs` | arithmetic | concrete `Int` / `Float` | `Float` quotient |
|
||||||
|
| `+` | `lhs + rhs` | arithmetic | concrete `Int` / `Float` | numeric sum |
|
||||||
|
| `-` | `lhs - rhs` | arithmetic | concrete `Int` / `Float` | numeric difference |
|
||||||
|
| `++` | `lhs ++ rhs` | array concat | concrete arrays | concatenated array |
|
||||||
|
| `==` | `lhs == rhs` | equality | concrete scalar | concrete `Bool` |
|
||||||
|
| `!=` | `lhs != rhs` | equality | concrete scalar | concrete `Bool` |
|
||||||
|
| `<` | `lhs < rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
|
||||||
|
| `<=` | `lhs <= rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
|
||||||
|
| `>` | `lhs > rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
|
||||||
|
| `>=` | `lhs >= rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
|
||||||
|
| `>` | `> value` | comparison constraint | numeric constraint value | abstract constraint |
|
||||||
|
| `>=` | `>= value` | comparison constraint | numeric constraint value | abstract constraint |
|
||||||
|
| `<` | `< value` | comparison constraint | numeric constraint value | abstract constraint |
|
||||||
|
| `<=` | `<= value` | comparison constraint | numeric constraint value | abstract constraint |
|
||||||
|
| `&&` | `lhs && rhs` | logical | concrete `Bool` | short-circuit AND |
|
||||||
|
| `||` | `lhs || rhs` | logical | concrete `Bool` | short-circuit OR |
|
||||||
|
| `&` | `lhs & rhs` | composition | value / constraint / object | constraint-preserving composition |
|
||||||
|
| `//` | `lhs // rhs` | patch | object / value | right-biased structural patch |
|
||||||
|
| `default` | `base default fallback` | default | abstract value | materialization fallback |
|
||||||
|
|
||||||
|
`concrete scalar` は `String`、`Bool`、`Int`、`Float` を指す。
|
||||||
|
|
||||||
|
## 優先順位
|
||||||
|
|
||||||
|
優先順位は高い順に以下である。
|
||||||
|
|
||||||
|
1. 関数呼び出しとフィールド参照
|
||||||
|
2. unary `!` `-`
|
||||||
|
3. `*` `/`
|
||||||
|
4. `+` `-`
|
||||||
|
5. `++`
|
||||||
|
6. `==` `!=` `<` `<=` `>` `>=`
|
||||||
|
7. `&&`
|
||||||
|
8. `||`
|
||||||
|
9. `&`
|
||||||
|
10. `//`
|
||||||
|
11. `default`
|
||||||
|
|
||||||
|
同じ優先順位の二項演算子は左結合である。
|
||||||
|
`default` は右結合である。
|
||||||
|
|
||||||
|
## Arithmetic operators
|
||||||
|
|
||||||
|
`+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。
|
||||||
|
詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。
|
||||||
|
|
||||||
|
## Array concat operator
|
||||||
|
|
||||||
|
`++` は concrete array 同士を連結する演算子である。
|
||||||
|
要素は変換されず、左辺の要素の後に右辺の要素が並ぶ。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
["read", "write"] ++ ["admin"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logical and comparison operators
|
||||||
|
|
||||||
|
`!` `&&` `||` は concrete `Bool` に対する論理演算である。
|
||||||
|
`&&` と `||` は短絡評価される。
|
||||||
|
|
||||||
|
`==` `!=` は concrete scalar value を比較する。
|
||||||
|
`<` `<=` `>` `>=` は concrete numeric value を比較する。
|
||||||
|
詳しくは [Logical and Comparison Expressions](./expression/logical-and-comparison.md) を参照する。
|
||||||
|
|
||||||
## `&`: 制約合成
|
## `&`: 制約合成
|
||||||
|
|
||||||
@@ -110,19 +183,10 @@ Patched = Base // {
|
|||||||
|
|
||||||
## object 全体の置換
|
## object 全体の置換
|
||||||
|
|
||||||
`//` が deep patch である場合、object 全体を置き換えたいときの escape hatch が必要になる。
|
`//` では object/object は常に deep patch される。
|
||||||
|
object field 全体を特別に置き換えるための `replace(...)` 構文や組み込み関数は core には含めない。
|
||||||
|
|
||||||
候補として、`replace(...)` を組み込み関数として提供する。
|
object 全体を別構造にしたい場合は、patch 対象より外側で値を作り直す。
|
||||||
|
|
||||||
```dcdl
|
|
||||||
Replaced = Base // {
|
|
||||||
feature_hoge = replace({
|
|
||||||
enable = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
この場合、`feature_hoge` は再帰 patch されず、右辺の object に丸ごと置き換わる。
|
|
||||||
|
|
||||||
## `&` と `//` の使い分け
|
## `&` と `//` の使い分け
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 構文と字句
|
# 構文と字句
|
||||||
|
|
||||||
この章では、表層構文の方針をまとめる。
|
この章では、表層構文の方針をまとめる。
|
||||||
厳密な EBNF は未確定であり、今後このファイルに詳細化する。
|
厳密な構文は [Grammar](./grammar.md) の EBNF を参照する。
|
||||||
|
|
||||||
## 言語名と拡張子
|
## 言語名と拡張子
|
||||||
|
|
||||||
@@ -93,9 +93,9 @@ config.feature_hoge.enable
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 予約語候補
|
## 予約語
|
||||||
|
|
||||||
以下は予約語または予約構文として扱う候補である。
|
以下は予約語として扱う。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
let
|
let
|
||||||
@@ -105,16 +105,17 @@ import
|
|||||||
default
|
default
|
||||||
true
|
true
|
||||||
false
|
false
|
||||||
rec
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`rec` の扱いは未確定である。
|
|
||||||
|
|
||||||
## 演算子
|
## 演算子
|
||||||
|
|
||||||
主要な演算子は以下である。
|
主要な演算子は以下である。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
+ - * / 四則演算
|
||||||
|
++ 配列結合
|
||||||
|
! && || 論理演算
|
||||||
|
== != < <= > >= 比較式
|
||||||
& 制約合成
|
& 制約合成
|
||||||
// patch 合成
|
// patch 合成
|
||||||
default fallback 指定
|
default fallback 指定
|
||||||
@@ -122,5 +123,4 @@ default fallback 指定
|
|||||||
. フィールド参照 / ドットパス定義
|
. フィールド参照 / ドットパス定義
|
||||||
```
|
```
|
||||||
|
|
||||||
演算子の優先順位は未確定である。
|
演算子の優先順位は [合成演算子](./operators.md) で定義する。
|
||||||
詳細は [合成演算子](./operators.md) で定義する。
|
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ ratio = Float;
|
|||||||
threshold = Float default 0.5;
|
threshold = Float default 0.5;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 未確定事項
|
## Int との関係
|
||||||
|
|
||||||
`Int` と `Float` の暗黙変換を許可するかは未確定である。
|
`Int` と `Float` は primitive type constraint としては別の型である。
|
||||||
軽量実装では、両者を明確に分ける方が単純である。
|
`Float` constraint は concrete `Float` を要求し、`Int` constraint は concrete `Int` を要求する。
|
||||||
|
|
||||||
|
数値演算や比較式では `Int` と `Float` を同じ numeric value として扱える。
|
||||||
|
混在した四則演算の結果は `Float` になる。
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ name = String;
|
|||||||
retry = Int default 3;
|
retry = Int default 3;
|
||||||
ratio = Float;
|
ratio = Float;
|
||||||
enable = Bool default true;
|
enable = Bool default true;
|
||||||
|
tags = Array;
|
||||||
```
|
```
|
||||||
|
|
||||||
現在の primitive type は `String`、`Int`、`Float`、`Bool` である。
|
現在の primitive type は `String`、`Int`、`Float`、`Bool`、`Array` である。
|
||||||
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
||||||
|
|
||||||
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
|
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ greeting = String default "hello";
|
|||||||
message = String & /Hello! .*/;
|
message = String & /Hello! .*/;
|
||||||
```
|
```
|
||||||
|
|
||||||
正規表現制約を必須機能にするかは未確定である。
|
正規表現制約の検証は Rust crate の `regex` feature で有効化される。
|
||||||
|
feature が無効な場合、正規表現制約は具体 string に対して検証できず diagnostic になる。
|
||||||
|
|||||||
@@ -1,60 +1,23 @@
|
|||||||
# 未確定事項
|
# 未確定事項
|
||||||
|
|
||||||
今後決める必要がある事項を管理する。
|
今後決める必要がある事項を管理する。
|
||||||
詳細化するときは、各項目を該当する仕様ファイルへ移動または反映する。
|
実装または仕様方針が固まった項目は、該当する仕様ファイルへ反映してここから外す。
|
||||||
|
|
||||||
## 構文
|
## Materialize target and host projection
|
||||||
|
|
||||||
- 正式な字句・構文仕様。
|
Decodal の評価結果は、制約・default・関数値を含む中間値になり得る。
|
||||||
- 演算子の優先順位。
|
そのため、Decodal 単独で常に「最終成果物」を一意に決めるのではなく、host 側が期待する型や出力形式を与えて materialize / decode する経路を明確にする必要がある。
|
||||||
- `rec` の扱い。
|
|
||||||
- コメント構文を `#` のみにするか。
|
|
||||||
|
|
||||||
## 型・制約
|
決めること:
|
||||||
|
|
||||||
- 配列要素の制約表現。
|
- Rust API で評価結果の field/path を選択して decode / materialize できるようにするか。
|
||||||
- object の open/closed schema の扱い。
|
- `decodal-derive` の struct schema と評価結果を合成して decode する経路を、主要な materialize path として位置づけるか。
|
||||||
- 正規表現を必須機能にするか optional feature にするか。
|
- CLI / WASM では target path を指定して JSON-compatible value へ materialize する形にするか。
|
||||||
- 代表的な組み込み述語の範囲。
|
- 制約や関数値が残った値を出力したい場合、materialize ではなく inspect/debug API として分けるか。
|
||||||
|
|
||||||
## default
|
現時点の案:
|
||||||
|
|
||||||
- `default` 同士の conflict 解決規則。
|
- Rust では `evaluate -> select field/path -> expected schema と合成 -> decode` を主経路にする。
|
||||||
- `&` による default 合成の厳密な規則。
|
- CLI / WASM では、明示された target path または module 全体を JSON-compatible value として materialize する。
|
||||||
- `//` による default 置換の厳密な規則。
|
- materialize できない unresolved abstract value、default のない制約値、関数値は diagnostic にする。
|
||||||
- default thunk の評価失敗をどの段階で報告するか。
|
- Decodal 言語内には materialize 構文を追加せず、host API / CLI / WASM の責務として扱う。
|
||||||
|
|
||||||
## 演算子
|
|
||||||
|
|
||||||
- `//` による制約・default の置換詳細。
|
|
||||||
- `replace(...)` を採用するか、別構文を設けるか。
|
|
||||||
- 配列に対する patch 操作を右辺置換だけにするか。
|
|
||||||
- 配列 append / prepend / remove などを提供するか。
|
|
||||||
|
|
||||||
## 関数
|
|
||||||
|
|
||||||
- 関数値の最終出力可否。
|
|
||||||
- 再帰関数を許可するか。
|
|
||||||
- 関数同士の `&` の扱い。
|
|
||||||
- 関数値の等価性。
|
|
||||||
- 関数呼び出し結果の memoize 範囲。
|
|
||||||
|
|
||||||
## 評価
|
|
||||||
|
|
||||||
- thunk のエラー memoize 方針。
|
|
||||||
- import cache の単位。
|
|
||||||
- 循環 import の診断メッセージ。
|
|
||||||
- materialize 対象の範囲指定方法。
|
|
||||||
|
|
||||||
## match
|
|
||||||
|
|
||||||
- match の網羅性チェックを行うか。
|
|
||||||
- 到達不能分岐を警告するか。
|
|
||||||
- パターン構文の範囲。
|
|
||||||
|
|
||||||
## エラー処理
|
|
||||||
|
|
||||||
- optional import を導入するか。
|
|
||||||
- optional field access を導入するか。
|
|
||||||
- optional fallback が捕捉できる失敗の範囲。
|
|
||||||
- エラー報告に制約由来の説明をどこまで含めるか。
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# lezer-decodal
|
||||||
|
|
||||||
|
Lezer implementation of the Decodal grammar for CodeMirror 6.
|
||||||
|
|
||||||
|
The canonical grammar is documented in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../doc/manual/souce/language/grammar.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate the CodeMirror parser from the site directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ../../site/decodal-site
|
||||||
|
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o ../../packages/decodal-codemirror/src/decodal-parser.js
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated parser is committed with the `decodal-codemirror` package because both the playground and external Web editors consume it.
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@top Source { Statement* }
|
||||||
|
|
||||||
|
Statement {
|
||||||
|
FieldDefinition Semicolon? |
|
||||||
|
Expression Semicolon?
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression { DefaultExpression }
|
||||||
|
|
||||||
|
DefaultExpression {
|
||||||
|
PatchExpression |
|
||||||
|
PatchExpression !default Default DefaultExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchExpression {
|
||||||
|
ComposeExpression |
|
||||||
|
PatchExpression !patch SlashSlash ComposeExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeExpression {
|
||||||
|
LogicalOrExpression |
|
||||||
|
ComposeExpression !compose Amp LogicalOrExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
LogicalOrExpression {
|
||||||
|
LogicalAndExpression |
|
||||||
|
LogicalOrExpression !or PipePipe LogicalAndExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
LogicalAndExpression {
|
||||||
|
ComparisonExpression |
|
||||||
|
LogicalAndExpression !and AmpAmp ComparisonExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
ComparisonExpression {
|
||||||
|
ConcatExpression |
|
||||||
|
ConcatExpression !compare CompareOperator ConcatExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
ConcatExpression {
|
||||||
|
AdditiveExpression |
|
||||||
|
ConcatExpression !concat PlusPlus AdditiveExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
AdditiveExpression {
|
||||||
|
MultiplicativeExpression |
|
||||||
|
AdditiveExpression !add (Plus | Minus) MultiplicativeExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiplicativeExpression {
|
||||||
|
UnaryExpression |
|
||||||
|
MultiplicativeExpression !multiply (Star | Slash) UnaryExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
UnaryExpression {
|
||||||
|
PostfixExpression |
|
||||||
|
(Bang | Minus) !unary UnaryExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
PostfixExpression {
|
||||||
|
PrimaryExpression |
|
||||||
|
PostfixExpression !call CallSuffix |
|
||||||
|
PostfixExpression !path Dot Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
CallSuffix { LParen ArgumentList? RParen }
|
||||||
|
ArgumentList { Expression (Comma Expression)* Comma? }
|
||||||
|
|
||||||
|
PrimaryExpression {
|
||||||
|
Literal |
|
||||||
|
Identifier |
|
||||||
|
ComparisonConstraint |
|
||||||
|
Object |
|
||||||
|
Array |
|
||||||
|
LetExpression |
|
||||||
|
FunctionExpression |
|
||||||
|
MatchExpression |
|
||||||
|
ImportExpression |
|
||||||
|
LParen Expression RParen
|
||||||
|
}
|
||||||
|
|
||||||
|
Literal { String | Integer | Float | True | False | Regex }
|
||||||
|
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
|
||||||
|
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression }
|
||||||
|
|
||||||
|
Object { LBrace (FieldDefinition (Semicolon FieldDefinition)* Semicolon?)? RBrace }
|
||||||
|
FieldDefinition { FieldPath Equal Expression }
|
||||||
|
FieldPath { Identifier !fieldPath (Dot Identifier)* }
|
||||||
|
|
||||||
|
Array { LBracket (Expression (Comma Expression)* Comma?)? RBracket }
|
||||||
|
|
||||||
|
LetExpression { Let FieldDefinitionList In Expression }
|
||||||
|
FieldDefinitionList { (FieldDefinition Semicolon)* }
|
||||||
|
|
||||||
|
FunctionExpression { LParen ParameterList? RParen Arrow Expression }
|
||||||
|
ParameterList { Parameter (Comma Parameter)* Comma? }
|
||||||
|
Parameter { Identifier Colon Expression }
|
||||||
|
|
||||||
|
MatchExpression { Match Expression LBrace (MatchArm (Semicolon MatchArm)* Semicolon?)? RBrace }
|
||||||
|
MatchArm { Pattern Colon Expression }
|
||||||
|
Pattern { Underscore | Expression }
|
||||||
|
|
||||||
|
ImportExpression { Import String }
|
||||||
|
|
||||||
|
@precedence {
|
||||||
|
fieldPath @left,
|
||||||
|
default @right,
|
||||||
|
patch @left,
|
||||||
|
compose @left,
|
||||||
|
or @left,
|
||||||
|
and @left,
|
||||||
|
compare @left,
|
||||||
|
concat @left,
|
||||||
|
add @left,
|
||||||
|
multiply @left,
|
||||||
|
unary,
|
||||||
|
call @left,
|
||||||
|
path @left
|
||||||
|
}
|
||||||
|
|
||||||
|
@tokens {
|
||||||
|
Let { "let" }
|
||||||
|
In { "in" }
|
||||||
|
Match { "match" }
|
||||||
|
Import { "import" }
|
||||||
|
Default { "default" }
|
||||||
|
True { "true" }
|
||||||
|
False { "false" }
|
||||||
|
|
||||||
|
Identifier { $[A-Za-z_] $[A-Za-z0-9_]* }
|
||||||
|
Integer { $[0-9]+ }
|
||||||
|
Float { $[0-9]+ "." $[0-9]+ }
|
||||||
|
String { '"' (!["\\\n] | "\\" _)* '"' }
|
||||||
|
Regex { "/" (![/\\\n] | "\\" _)+ "/" }
|
||||||
|
Comment { "#" ![\n]* }
|
||||||
|
|
||||||
|
LBrace { "{" }
|
||||||
|
RBrace { "}" }
|
||||||
|
LBracket { "[" }
|
||||||
|
RBracket { "]" }
|
||||||
|
LParen { "(" }
|
||||||
|
RParen { ")" }
|
||||||
|
Semicolon { ";" }
|
||||||
|
Comma { "," }
|
||||||
|
Dot { "." }
|
||||||
|
Colon { ":" }
|
||||||
|
Equal { "=" }
|
||||||
|
EqualEqual { "==" }
|
||||||
|
Bang { "!" }
|
||||||
|
BangEqual { "!=" }
|
||||||
|
Arrow { "=>" }
|
||||||
|
Amp { "&" }
|
||||||
|
AmpAmp { "&&" }
|
||||||
|
PipePipe { "||" }
|
||||||
|
Plus { "+" }
|
||||||
|
PlusPlus { "++" }
|
||||||
|
Minus { "-" }
|
||||||
|
Star { "*" }
|
||||||
|
Slash { "/" }
|
||||||
|
SlashSlash { "//" }
|
||||||
|
Gt { ">" }
|
||||||
|
Gte { ">=" }
|
||||||
|
Lt { "<" }
|
||||||
|
Lte { "<=" }
|
||||||
|
Underscore { "_" }
|
||||||
|
|
||||||
|
whitespace { @whitespace+ }
|
||||||
|
|
||||||
|
@precedence { Let, In, Match, Import, Default, True, False, Float, Integer, Underscore, Identifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
@skip { whitespace | Comment }
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# tree-sitter-decodal
|
||||||
|
|
||||||
|
Tree-sitter implementation of the Decodal grammar.
|
||||||
|
|
||||||
|
The canonical grammar is documented in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../doc/manual/souce/language/grammar.md
|
||||||
|
```
|
||||||
|
|
||||||
|
This grammar is an editor/tooling implementation of that EBNF reference.
|
||||||
|
Generated parser files under `src/` are committed.
|
||||||
@@ -74,3 +74,87 @@ base // {
|
|||||||
body: (literal (string)))
|
body: (literal (string)))
|
||||||
(match_arm
|
(match_arm
|
||||||
body: (literal (string))))))))
|
body: (literal (string))))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Arithmetic
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
value = 1 + 2 * 3;
|
||||||
|
grouped = (1 + 2) / -3;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (literal (integer))
|
||||||
|
right: (binary_expression
|
||||||
|
left: (literal (integer))
|
||||||
|
right: (literal (integer)))))
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (parenthesized_expression
|
||||||
|
(binary_expression
|
||||||
|
left: (literal (integer))
|
||||||
|
right: (literal (integer))))
|
||||||
|
right: (unary_expression
|
||||||
|
operand: (literal (integer)))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Logical and comparison
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
enabled = env == "prod" && replicas > 1;
|
||||||
|
disabled = !enabled || false;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (binary_expression
|
||||||
|
left: (identifier)
|
||||||
|
right: (literal (string)))
|
||||||
|
right: (binary_expression
|
||||||
|
left: (identifier)
|
||||||
|
right: (literal (integer)))))
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (unary_expression
|
||||||
|
operand: (identifier))
|
||||||
|
right: (literal (boolean))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Array concat
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
roles = ["read"] ++ ["write", "admin"];
|
||||||
|
ports = [8000 + 80] ++ [9443];
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (array
|
||||||
|
(literal (string)))
|
||||||
|
right: (array
|
||||||
|
(literal (string))
|
||||||
|
(literal (string)))))
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (binary_expression
|
||||||
|
left: (array
|
||||||
|
(binary_expression
|
||||||
|
left: (literal (integer))
|
||||||
|
right: (literal (integer))))
|
||||||
|
right: (array
|
||||||
|
(literal (integer)))))))
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
// Tree-sitter implementation of the canonical EBNF grammar in
|
||||||
|
// doc/manual/souce/language/grammar.md.
|
||||||
|
|
||||||
const PREC = {
|
const PREC = {
|
||||||
DEFAULT: 1,
|
DEFAULT: 1,
|
||||||
PATCH: 2,
|
PATCH: 2,
|
||||||
AND: 3,
|
COMPOSE: 3,
|
||||||
CALL: 7,
|
OR: 4,
|
||||||
PATH: 8,
|
LOGICAL_AND: 5,
|
||||||
|
COMPARE: 6,
|
||||||
|
CONCAT: 7,
|
||||||
|
ADD: 8,
|
||||||
|
MUL: 9,
|
||||||
|
UNARY: 10,
|
||||||
|
CALL: 11,
|
||||||
|
PATH: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
function commaSep(rule) {
|
function commaSep(rule) {
|
||||||
@@ -52,6 +62,7 @@ module.exports = grammar({
|
|||||||
$.parenthesized_expression,
|
$.parenthesized_expression,
|
||||||
$.call_expression,
|
$.call_expression,
|
||||||
$.path_expression,
|
$.path_expression,
|
||||||
|
$.unary_expression,
|
||||||
$.binary_expression,
|
$.binary_expression,
|
||||||
$.default_expression,
|
$.default_expression,
|
||||||
),
|
),
|
||||||
@@ -159,13 +170,48 @@ module.exports = grammar({
|
|||||||
field('field', $.identifier),
|
field('field', $.identifier),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
comparison_constraint: $ => prec(6, seq(
|
comparison_constraint: $ => prec.right(PREC.UNARY + 1, seq(
|
||||||
field('operator', choice('>', '>=', '<', '<=')),
|
field('operator', choice('>', '>=', '<', '<=')),
|
||||||
field('value', choice($.integer, $.float)),
|
field('value', $._expression),
|
||||||
|
)),
|
||||||
|
|
||||||
|
unary_expression: $ => prec(PREC.UNARY, seq(
|
||||||
|
field('operator', choice('-', '!')),
|
||||||
|
field('operand', $._expression),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
binary_expression: $ => choice(
|
binary_expression: $ => choice(
|
||||||
prec.left(PREC.AND, seq(
|
prec.left(PREC.OR, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', token(prec(2, '||'))),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.LOGICAL_AND, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', token(prec(2, '&&'))),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.COMPARE, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', choice('==', '!=', '>', '>=', '<', '<=')),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.CONCAT, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', token(prec(2, '++'))),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.ADD, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', choice('+', '-')),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.MUL, seq(
|
||||||
|
field('left', $._expression),
|
||||||
|
field('operator', choice('*', '/')),
|
||||||
|
field('right', $._expression),
|
||||||
|
)),
|
||||||
|
prec.left(PREC.COMPOSE, seq(
|
||||||
field('left', $._expression),
|
field('left', $._expression),
|
||||||
field('operator', '&'),
|
field('operator', '&'),
|
||||||
field('right', $._expression),
|
field('right', $._expression),
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
[
|
[
|
||||||
"&"
|
"&"
|
||||||
"//"
|
"//"
|
||||||
|
"+"
|
||||||
|
"-"
|
||||||
|
"*"
|
||||||
|
"/"
|
||||||
|
"++"
|
||||||
|
"&&"
|
||||||
|
"||"
|
||||||
|
"!"
|
||||||
|
"=="
|
||||||
|
"!="
|
||||||
"=>"
|
"=>"
|
||||||
"="
|
"="
|
||||||
">"
|
">"
|
||||||
|
|||||||
+299
-8
@@ -127,6 +127,10 @@
|
|||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "path_expression"
|
"name": "path_expression"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "unary_expression"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "binary_expression"
|
"name": "binary_expression"
|
||||||
@@ -712,7 +716,7 @@
|
|||||||
},
|
},
|
||||||
"call_expression": {
|
"call_expression": {
|
||||||
"type": "PREC_LEFT",
|
"type": "PREC_LEFT",
|
||||||
"value": 7,
|
"value": 11,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
@@ -782,7 +786,7 @@
|
|||||||
},
|
},
|
||||||
"path_expression": {
|
"path_expression": {
|
||||||
"type": "PREC_LEFT",
|
"type": "PREC_LEFT",
|
||||||
"value": 8,
|
"value": 12,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
@@ -810,8 +814,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"comparison_constraint": {
|
"comparison_constraint": {
|
||||||
"type": "PREC",
|
"type": "PREC_RIGHT",
|
||||||
"value": 6,
|
"value": 11,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
@@ -843,19 +847,44 @@
|
|||||||
{
|
{
|
||||||
"type": "FIELD",
|
"type": "FIELD",
|
||||||
"name": "value",
|
"name": "value",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unary_expression": {
|
||||||
|
"type": "PREC",
|
||||||
|
"value": 10,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
"content": {
|
"content": {
|
||||||
"type": "CHOICE",
|
"type": "CHOICE",
|
||||||
"members": [
|
"members": [
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "STRING",
|
||||||
"name": "integer"
|
"value": "-"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "STRING",
|
||||||
"name": "float"
|
"value": "!"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operand",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -863,6 +892,268 @@
|
|||||||
"binary_expression": {
|
"binary_expression": {
|
||||||
"type": "CHOICE",
|
"type": "CHOICE",
|
||||||
"members": [
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 4,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "TOKEN",
|
||||||
|
"content": {
|
||||||
|
"type": "PREC",
|
||||||
|
"value": 2,
|
||||||
|
"content": {
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "||"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 5,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "TOKEN",
|
||||||
|
"content": {
|
||||||
|
"type": "PREC",
|
||||||
|
"value": 2,
|
||||||
|
"content": {
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "&&"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 6,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "CHOICE",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "!="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": ">"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": ">="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "<"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "<="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 7,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "TOKEN",
|
||||||
|
"content": {
|
||||||
|
"type": "PREC",
|
||||||
|
"value": 2,
|
||||||
|
"content": {
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "++"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 8,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "CHOICE",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "+"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "-"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 9,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "left",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
|
"content": {
|
||||||
|
"type": "CHOICE",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "right",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "PREC_LEFT",
|
"type": "PREC_LEFT",
|
||||||
"value": 3,
|
"value": 3,
|
||||||
|
|||||||
+308
-2
@@ -66,6 +66,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -137,6 +141,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -144,13 +152,65 @@
|
|||||||
"multiple": false,
|
"multiple": false,
|
||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "!=",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "&",
|
"type": "&",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "&&",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "*",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "+",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "++",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "-",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "/",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "//",
|
"type": "//",
|
||||||
"named": false
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "<",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "<=",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "==",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": ">",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": ">=",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "||",
|
||||||
|
"named": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -217,6 +277,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -294,6 +358,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -361,6 +429,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -396,11 +468,67 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
"type": "float",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -474,6 +602,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -540,6 +672,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -622,6 +758,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -709,6 +849,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -807,6 +951,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -916,6 +1064,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -986,6 +1138,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1058,6 +1214,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1155,6 +1315,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1237,6 +1401,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1318,6 +1486,10 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1394,14 +1566,120 @@
|
|||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true,
|
||||||
|
"fields": {
|
||||||
|
"operand": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "!",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "-",
|
||||||
|
"named": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "!",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "!=",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "&",
|
"type": "&",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "&&",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "(",
|
"type": "(",
|
||||||
"named": false
|
"named": false
|
||||||
@@ -1410,14 +1688,34 @@
|
|||||||
"type": ")",
|
"type": ")",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "*",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "+",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "++",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": ",",
|
"type": ",",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "-",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": ".",
|
"type": ".",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "/",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "//",
|
"type": "//",
|
||||||
"named": false
|
"named": false
|
||||||
@@ -1442,6 +1740,10 @@
|
|||||||
"type": "=",
|
"type": "=",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "==",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "=>",
|
"type": "=>",
|
||||||
"named": false
|
"named": false
|
||||||
@@ -1522,6 +1824,10 @@
|
|||||||
"type": "{",
|
"type": "{",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "||",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "}",
|
"type": "}",
|
||||||
"named": false
|
"named": false
|
||||||
|
|||||||
Generated
+8262
-3020
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ This example exercises multiple current Decodal features together:
|
|||||||
Run it with:
|
Run it with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run -q -p decodal -- examples/advanced/main.dcdl
|
cargo run -q -p decodal-cli -- examples/advanced/main.dcdl
|
||||||
```
|
```
|
||||||
|
|
||||||
The entrypoint is `main.dcdl`.
|
The entrypoint is `main.dcdl`.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
workers = 2 + 2;
|
||||||
|
memory_gib = 1.5 * 4;
|
||||||
|
port = 9000 + 443;
|
||||||
|
timeout_seconds = 30 / 2;
|
||||||
|
negative_offset = -3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
base_roles = ["read", "write"];
|
||||||
|
extra_roles = ["admin"];
|
||||||
|
roles = ["read", "write"] ++ ["admin"];
|
||||||
|
ports = [8000 + 80] ++ [9000 + 443];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
let
|
||||||
|
env = "prod";
|
||||||
|
replicas = 3;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
is_prod = env == "prod";
|
||||||
|
scaled = replicas > 1;
|
||||||
|
enabled = env == "prod" && replicas > 1;
|
||||||
|
disabled = !(env == "prod" && replicas > 1);
|
||||||
|
safe = env != "dev" || replicas >= 1;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
Regex constraints are optional. Run this example with the `regex` feature enabled:
|
Regex constraints are optional. Run this example with the `regex` feature enabled:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
```
|
```
|
||||||
|
|
||||||
Without the feature, regex validation returns an unsupported feature diagnostic.
|
Without the feature, regex validation returns an unsupported feature diagnostic.
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ let
|
|||||||
in
|
in
|
||||||
rustPlatform.buildRustPackage {
|
rustPlatform.buildRustPackage {
|
||||||
pname = "decodal";
|
pname = "decodal";
|
||||||
version = "0.1.0";
|
version = "0.1.1";
|
||||||
|
|
||||||
src = lib.cleanSourceWith {
|
src = lib.cleanSourceWith {
|
||||||
src = srcRoot;
|
src = srcRoot;
|
||||||
@@ -37,7 +37,7 @@ rustPlatform.buildRustPackage {
|
|||||||
|
|
||||||
cargoBuildFlags = [
|
cargoBuildFlags = [
|
||||||
"-p"
|
"-p"
|
||||||
"decodal"
|
"decodal-cli"
|
||||||
];
|
];
|
||||||
|
|
||||||
doInstallCheck = true;
|
doInstallCheck = true;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# decodal-codemirror
|
||||||
|
|
||||||
|
CodeMirror 6 language support for Decodal.
|
||||||
|
|
||||||
|
This package provides the Lezer parser metadata, highlighting tags, indentation rules, and block folding used by the Decodal Web playground.
|
||||||
|
It does not include the Decodal runtime or WebAssembly evaluator.
|
||||||
|
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install decodal-codemirror
|
||||||
|
```
|
||||||
|
|
||||||
|
JSR:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
deno add jsr:@hare/decodal-codemirror
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { basicSetup } from 'codemirror';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { decodal } from 'decodal-codemirror';
|
||||||
|
|
||||||
|
new EditorView({
|
||||||
|
doc: 'Server = { port = Int default 8080; };',
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
decodal(),
|
||||||
|
],
|
||||||
|
parent: document.querySelector('#editor'),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
decodal,
|
||||||
|
decodalLanguage,
|
||||||
|
decodalHighlightStyle,
|
||||||
|
} from 'decodal-codemirror';
|
||||||
|
```
|
||||||
|
|
||||||
|
`decodal()` accepts a small options object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
decodal({ highlight: false })
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when you want the language support without the bundled highlight style.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@hare/decodal-codemirror",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"license": "MIT",
|
||||||
|
"exports": "./src/mod.ts",
|
||||||
|
"imports": {
|
||||||
|
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
|
||||||
|
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
|
||||||
|
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"include": [
|
||||||
|
"README.md",
|
||||||
|
"src/mod.ts",
|
||||||
|
"src/decodal.js",
|
||||||
|
"src/decodal.d.ts",
|
||||||
|
"src/decodal.js.d.ts",
|
||||||
|
"src/decodal-parser-jsr.js",
|
||||||
|
"src/decodal-parser.js",
|
||||||
|
"src/decodal-parser.terms.js",
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "decodal-codemirror",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "decodal-codemirror",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.43.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||||
|
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "decodal-codemirror",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"description": "CodeMirror 6 language support for Decodal.",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://gitea.hareworks.net/Hare/Decodal.git",
|
||||||
|
"directory": "packages/decodal-codemirror"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/decodal.d.ts",
|
||||||
|
"import": "./src/decodal.js"
|
||||||
|
},
|
||||||
|
"./parser": "./src/decodal-parser.js",
|
||||||
|
"./terms": "./src/decodal-parser.terms.js"
|
||||||
|
},
|
||||||
|
"types": "./src/decodal.d.ts",
|
||||||
|
"files": [
|
||||||
|
"README.md",
|
||||||
|
"src/"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"decodal",
|
||||||
|
"codemirror",
|
||||||
|
"lezer"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
|
import {LRParser} from "npm:@lezer/lr@^1.4.10"
|
||||||
|
export const parser = LRParser.deserialize({
|
||||||
|
version: 14,
|
||||||
|
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
|
||||||
|
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
|
||||||
|
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
|
||||||
|
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
|
||||||
|
maxTerm: 85,
|
||||||
|
skippedNodes: [0,1],
|
||||||
|
repeatNodeCount: 7,
|
||||||
|
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
|
||||||
|
tokenizers: [0, 1],
|
||||||
|
topRules: {"Source":[0,2]},
|
||||||
|
tokenPrec: 2481
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
|
import {LRParser} from "@lezer/lr"
|
||||||
|
export const parser = LRParser.deserialize({
|
||||||
|
version: 14,
|
||||||
|
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
|
||||||
|
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
|
||||||
|
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
|
||||||
|
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
|
||||||
|
maxTerm: 85,
|
||||||
|
skippedNodes: [0,1],
|
||||||
|
repeatNodeCount: 7,
|
||||||
|
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
|
||||||
|
tokenizers: [0, 1],
|
||||||
|
topRules: {"Source":[0,2]},
|
||||||
|
tokenPrec: 2481
|
||||||
|
})
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
|
export const
|
||||||
|
Comment = 1,
|
||||||
|
Source = 2,
|
||||||
|
Statement = 3,
|
||||||
|
FieldDefinition = 4,
|
||||||
|
FieldPath = 5,
|
||||||
|
Identifier = 6,
|
||||||
|
Dot = 7,
|
||||||
|
Equal = 8,
|
||||||
|
Expression = 9,
|
||||||
|
DefaultExpression = 10,
|
||||||
|
PatchExpression = 11,
|
||||||
|
ComposeExpression = 12,
|
||||||
|
LogicalOrExpression = 13,
|
||||||
|
LogicalAndExpression = 14,
|
||||||
|
ComparisonExpression = 15,
|
||||||
|
ConcatExpression = 16,
|
||||||
|
AdditiveExpression = 17,
|
||||||
|
MultiplicativeExpression = 18,
|
||||||
|
UnaryExpression = 19,
|
||||||
|
PostfixExpression = 20,
|
||||||
|
PrimaryExpression = 21,
|
||||||
|
Literal = 22,
|
||||||
|
String = 23,
|
||||||
|
Integer = 24,
|
||||||
|
Float = 25,
|
||||||
|
True = 26,
|
||||||
|
False = 27,
|
||||||
|
Regex = 28,
|
||||||
|
ComparisonConstraint = 29,
|
||||||
|
Lt = 30,
|
||||||
|
Lte = 31,
|
||||||
|
Gt = 32,
|
||||||
|
Gte = 33,
|
||||||
|
Object = 34,
|
||||||
|
LBrace = 35,
|
||||||
|
Semicolon = 36,
|
||||||
|
RBrace = 37,
|
||||||
|
Array = 38,
|
||||||
|
LBracket = 39,
|
||||||
|
Comma = 40,
|
||||||
|
RBracket = 41,
|
||||||
|
LetExpression = 42,
|
||||||
|
Let = 43,
|
||||||
|
FieldDefinitionList = 44,
|
||||||
|
In = 45,
|
||||||
|
FunctionExpression = 46,
|
||||||
|
LParen = 47,
|
||||||
|
ParameterList = 48,
|
||||||
|
Parameter = 49,
|
||||||
|
Colon = 50,
|
||||||
|
RParen = 51,
|
||||||
|
Arrow = 52,
|
||||||
|
MatchExpression = 53,
|
||||||
|
Match = 54,
|
||||||
|
MatchArm = 55,
|
||||||
|
Pattern = 56,
|
||||||
|
Underscore = 57,
|
||||||
|
ImportExpression = 58,
|
||||||
|
Import = 59,
|
||||||
|
CallSuffix = 60,
|
||||||
|
ArgumentList = 61,
|
||||||
|
Bang = 62,
|
||||||
|
Minus = 63,
|
||||||
|
Star = 64,
|
||||||
|
Slash = 65,
|
||||||
|
Plus = 66,
|
||||||
|
PlusPlus = 67,
|
||||||
|
CompareOperator = 68,
|
||||||
|
EqualEqual = 69,
|
||||||
|
BangEqual = 70,
|
||||||
|
AmpAmp = 71,
|
||||||
|
PipePipe = 72,
|
||||||
|
Amp = 73,
|
||||||
|
SlashSlash = 74,
|
||||||
|
Default = 75
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { HighlightStyle } from '@codemirror/language';
|
||||||
|
import { LRLanguage, LanguageSupport } from '@codemirror/language';
|
||||||
|
|
||||||
|
export declare const decodalLanguage: LRLanguage;
|
||||||
|
export declare const decodalHighlightStyle: HighlightStyle;
|
||||||
|
|
||||||
|
export interface DecodalOptions {
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function decodal(options?: DecodalOptions): LanguageSupport;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { LRLanguage, LanguageSupport, HighlightStyle, syntaxHighlighting, foldNodeProp, indentNodeProp } from '@codemirror/language';
|
||||||
|
import { styleTags, tags as t } from '@lezer/highlight';
|
||||||
|
import { parser } from './decodal-parser.js';
|
||||||
|
|
||||||
|
const parserWithMetadata = parser.configure({
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
'Let In Match Import Default': t.keyword,
|
||||||
|
'True False': t.bool,
|
||||||
|
Identifier: t.variableName,
|
||||||
|
String: t.string,
|
||||||
|
Regex: t.regexp,
|
||||||
|
'Integer Float': t.number,
|
||||||
|
Comment: t.lineComment,
|
||||||
|
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
||||||
|
'LBrace RBrace': t.brace,
|
||||||
|
'LBracket RBracket': t.squareBracket,
|
||||||
|
'LParen RParen': t.paren,
|
||||||
|
'Semicolon Comma Dot Colon': t.punctuation,
|
||||||
|
}),
|
||||||
|
indentNodeProp.add({
|
||||||
|
Object: context => context.column(context.node.from) + context.unit,
|
||||||
|
Array: context => context.column(context.node.from) + context.unit,
|
||||||
|
MatchExpression: context => context.column(context.node.from) + context.unit,
|
||||||
|
LetExpression: context => context.column(context.node.from) + context.unit,
|
||||||
|
}),
|
||||||
|
foldNodeProp.add({
|
||||||
|
Object: foldDelimited('{', '}'),
|
||||||
|
Array: foldDelimited('[', ']'),
|
||||||
|
MatchExpression: foldDelimited('{', '}'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function foldDelimited(open, close) {
|
||||||
|
return (node, state) => {
|
||||||
|
const text = state.doc.sliceString(node.from, node.to);
|
||||||
|
const openIndex = text.indexOf(open);
|
||||||
|
const closeIndex = text.lastIndexOf(close);
|
||||||
|
if (openIndex < 0 || closeIndex <= openIndex) return null;
|
||||||
|
const from = node.from + openIndex + open.length;
|
||||||
|
const to = node.from + closeIndex;
|
||||||
|
return from < to ? { from, to } : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodalLanguage = LRLanguage.define({
|
||||||
|
parser: parserWithMetadata,
|
||||||
|
languageData: {
|
||||||
|
commentTokens: { line: '#' },
|
||||||
|
closeBrackets: { brackets: ['(', '[', '{', '"'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const decodalHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
||||||
|
{ tag: t.variableName, color: 'inherit' },
|
||||||
|
{ tag: t.bool, color: '#fbbf24' },
|
||||||
|
{ tag: t.number, color: '#fbbf24' },
|
||||||
|
{ tag: t.string, color: '#86efac' },
|
||||||
|
{ tag: t.regexp, color: '#86efac' },
|
||||||
|
{ tag: t.lineComment, color: '#94a3b8', fontStyle: 'italic' },
|
||||||
|
{ tag: t.operator, color: '#f9a8d4' },
|
||||||
|
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function decodal(options = {}) {
|
||||||
|
const { highlight = true } = options;
|
||||||
|
return new LanguageSupport(
|
||||||
|
decodalLanguage,
|
||||||
|
highlight ? [syntaxHighlighting(decodalHighlightStyle)] : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { HighlightStyle, LRLanguage, LanguageSupport } from '@codemirror/language';
|
||||||
|
|
||||||
|
export declare const decodalLanguage: LRLanguage;
|
||||||
|
export declare const decodalHighlightStyle: HighlightStyle;
|
||||||
|
|
||||||
|
export interface DecodalOptions {
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function decodal(options?: DecodalOptions): LanguageSupport;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
||||||
|
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
|
||||||
|
import { parser } from './decodal-parser-jsr.js';
|
||||||
|
|
||||||
|
const parserWithMetadata = parser.configure({
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
'Let In Match Import Default': t.keyword,
|
||||||
|
'True False': t.bool,
|
||||||
|
Identifier: t.variableName,
|
||||||
|
String: t.string,
|
||||||
|
Regex: t.regexp,
|
||||||
|
'Integer Float': t.number,
|
||||||
|
Comment: t.lineComment,
|
||||||
|
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
||||||
|
'LBrace RBrace': t.brace,
|
||||||
|
'LBracket RBracket': t.squareBracket,
|
||||||
|
'LParen RParen': t.paren,
|
||||||
|
'Semicolon Comma Dot Colon': t.punctuation,
|
||||||
|
}),
|
||||||
|
indentNodeProp.add({
|
||||||
|
Object: context => context.column(context.node.from) + context.unit,
|
||||||
|
Array: context => context.column(context.node.from) + context.unit,
|
||||||
|
MatchExpression: context => context.column(context.node.from) + context.unit,
|
||||||
|
LetExpression: context => context.column(context.node.from) + context.unit,
|
||||||
|
}),
|
||||||
|
foldNodeProp.add({
|
||||||
|
Object: foldDelimited('{', '}'),
|
||||||
|
Array: foldDelimited('[', ']'),
|
||||||
|
MatchExpression: foldDelimited('{', '}'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function foldDelimited(open: string, close: string) {
|
||||||
|
return (node: { from: number; to: number }, state: { doc: { sliceString(from: number, to: number): string } }) => {
|
||||||
|
const text = state.doc.sliceString(node.from, node.to);
|
||||||
|
const openIndex = text.indexOf(open);
|
||||||
|
const closeIndex = text.lastIndexOf(close);
|
||||||
|
if (openIndex < 0 || closeIndex <= openIndex) return null;
|
||||||
|
const from = node.from + openIndex + open.length;
|
||||||
|
const to = node.from + closeIndex;
|
||||||
|
return from < to ? { from, to } : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodalLanguage: LRLanguage = LRLanguage.define({
|
||||||
|
parser: parserWithMetadata,
|
||||||
|
languageData: {
|
||||||
|
commentTokens: { line: '#' },
|
||||||
|
closeBrackets: { brackets: ['(', '[', '{', '"'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
||||||
|
{ tag: t.variableName, color: 'inherit' },
|
||||||
|
{ tag: t.bool, color: '#fbbf24' },
|
||||||
|
{ tag: t.number, color: '#fbbf24' },
|
||||||
|
{ tag: t.string, color: '#86efac' },
|
||||||
|
{ tag: t.regexp, color: '#86efac' },
|
||||||
|
{ tag: t.lineComment, color: '#94a3b8', fontStyle: 'italic' },
|
||||||
|
{ tag: t.operator, color: '#f9a8d4' },
|
||||||
|
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
export interface DecodalOptions {
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodal(options: DecodalOptions = {}): LanguageSupport {
|
||||||
|
const { highlight = true } = options;
|
||||||
|
return new LanguageSupport(
|
||||||
|
decodalLanguage,
|
||||||
|
highlight ? [syntaxHighlighting(decodalHighlightStyle)] : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
preserveSymlinks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+8115
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "decodal-site",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev --host 0.0.0.0",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview --host 0.0.0.0",
|
||||||
|
"build:wasm": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../site/decodal-site/src/wasm --release",
|
||||||
|
"deploy": "npm run build && node scripts/deploy-pages.mjs",
|
||||||
|
"deploy:wasm": "npm run build:wasm && npm run deploy"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10",
|
||||||
|
"astro": "^4.16.18",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"decodal-codemirror": "file:../../packages/decodal-codemirror",
|
||||||
|
"marked": "^12.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@lezer/generator": "^1.8.0",
|
||||||
|
"wrangler": "^4.104.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const projectName = process.env.CLOUDFLARE_PROJECT_NAME ?? 'decodal-site';
|
||||||
|
const branch = process.env.CLOUDFLARE_BRANCH ?? 'master';
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'wrangler',
|
||||||
|
['pages', 'deploy', 'dist', '--project-name', projectName, '--branch', branch],
|
||||||
|
{ stdio: 'inherit', shell: process.platform === 'win32' },
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
import { nav } from '../lib/docs.js';
|
||||||
|
import '../style.css';
|
||||||
|
|
||||||
|
const { title = 'Decodal', active = '', playground = false, frontpage = false } = Astro.props;
|
||||||
|
|
||||||
|
function renderNav(items, prefix = []) {
|
||||||
|
return `<ul class="nav-tree">${items
|
||||||
|
.map((item, index) => {
|
||||||
|
const number = [...prefix, index + 1];
|
||||||
|
const href = `/docs/${item.slug}/`;
|
||||||
|
const activeClass = active === item.slug ? ' class="active"' : '';
|
||||||
|
const children = item.children ? renderNav(item.children, number) : '';
|
||||||
|
return `<li><a${activeClass} href="${href}"><span class="nav-number">${number.join('.')}.</span>${item.title}</a>${children}</li>`;
|
||||||
|
})
|
||||||
|
.join('')}</ul>`;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<a class="brand" href="/">Decodal</a>
|
||||||
|
<nav class="topnav">
|
||||||
|
<a href="/docs/introduction/">Docs</a>
|
||||||
|
<a href="/playground/">Playground</a>
|
||||||
|
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class={playground || frontpage ? 'layout playground-layout' : 'layout'}>
|
||||||
|
{!playground && !frontpage && (
|
||||||
|
<aside class="sidebar">
|
||||||
|
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||||
|
<nav set:html={renderNav(nav)} />
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
<main class={playground ? 'playground' : frontpage ? 'frontpage' : ''}>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<span>2026 Hare</span>
|
||||||
|
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
import { escapeAttribute, highlightCode } from './highlight.js';
|
||||||
|
|
||||||
|
const modules = import.meta.glob('../../../../doc/manual/souce/**/*.md', {
|
||||||
|
query: '?raw',
|
||||||
|
import: 'default',
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const docs = Object.fromEntries(
|
||||||
|
Object.entries(modules).map(([path, content]) => {
|
||||||
|
const slug = path
|
||||||
|
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/souce\//, '')
|
||||||
|
.replace(/\.md$/, '')
|
||||||
|
.replace(/\/index$/, '');
|
||||||
|
return [slug || 'index', content];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const nav = [
|
||||||
|
{ title: 'Introduction', slug: 'introduction' },
|
||||||
|
{ title: 'Components', slug: 'components' },
|
||||||
|
{
|
||||||
|
title: 'Language Specification',
|
||||||
|
slug: 'language',
|
||||||
|
children: [
|
||||||
|
{ title: 'Syntax', slug: 'language/syntax' },
|
||||||
|
{ title: 'Grammar', slug: 'language/grammar' },
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
slug: 'language/value',
|
||||||
|
children: [
|
||||||
|
{ title: 'String', slug: 'language/value/string' },
|
||||||
|
{ title: 'Int', slug: 'language/value/int' },
|
||||||
|
{ title: 'Float', slug: 'language/value/float' },
|
||||||
|
{ title: 'Bool', slug: 'language/value/bool' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Expression',
|
||||||
|
slug: 'language/expression',
|
||||||
|
children: [
|
||||||
|
{ title: 'Literal', slug: 'language/expression/literal' },
|
||||||
|
{ title: 'Identifier', slug: 'language/expression/identifier' },
|
||||||
|
{ title: 'Path Reference', slug: 'language/expression/path-reference' },
|
||||||
|
{ title: 'Object', slug: 'language/expression/object' },
|
||||||
|
{ title: 'Array', slug: 'language/expression/array' },
|
||||||
|
{ title: 'Function', slug: 'language/expression/function' },
|
||||||
|
{ title: 'Function Call', slug: 'language/expression/function-call' },
|
||||||
|
{ title: 'Let', slug: 'language/expression/let' },
|
||||||
|
{ title: 'Match', slug: 'language/expression/match' },
|
||||||
|
{ title: 'Import', slug: 'language/expression/import' },
|
||||||
|
{ title: 'Composition', slug: 'language/expression/composition' },
|
||||||
|
{ title: 'Default', slug: 'language/expression/default' },
|
||||||
|
{ title: 'Arithmetic', slug: 'language/expression/arithmetic' },
|
||||||
|
{ title: 'Logical and Comparison', slug: 'language/expression/logical-and-comparison' },
|
||||||
|
{ title: 'String Interpolation', slug: 'language/expression/string-interpolation' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ title: 'Constraints and Defaults', slug: 'language/constraints-and-defaults' },
|
||||||
|
{ title: 'Operators', slug: 'language/operators' },
|
||||||
|
{ title: 'Functions', slug: 'language/functions' },
|
||||||
|
{ title: 'Modules and Imports', slug: 'language/modules-and-imports' },
|
||||||
|
{ title: 'Evaluation Semantics', slug: 'language/evaluation' },
|
||||||
|
{ title: 'Materialization and Errors', slug: 'language/materialization-and-errors' },
|
||||||
|
{ title: 'Naming', slug: 'language/naming' },
|
||||||
|
{ title: 'Examples', slug: 'language/examples' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Implementation Design',
|
||||||
|
slug: 'design',
|
||||||
|
children: [
|
||||||
|
{ title: 'Execution Pipeline', slug: 'design/execution-pipeline' },
|
||||||
|
{ title: 'Runtime Model', slug: 'design/runtime-model' },
|
||||||
|
{ title: 'Thunk and Lazy Evaluation', slug: 'design/thunk-and-lazy-evaluation' },
|
||||||
|
{ title: 'Composition and Materialization', slug: 'design/composition-and-materialization' },
|
||||||
|
{ title: 'Diagnostics and Fallback', slug: 'design/diagnostics-and-fallback' },
|
||||||
|
{ title: 'Embedding API', slug: 'design/embedding-api' },
|
||||||
|
{ title: 'Features', slug: 'design/features' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ title: 'Development', slug: 'development' },
|
||||||
|
{ title: 'Open Issues', slug: 'open-issues' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
renderer.code = (code, language = '') => {
|
||||||
|
const normalizedLanguage = language.split(/\s+/)[0] ?? '';
|
||||||
|
const className = normalizedLanguage ? ` class="language-${escapeAttribute(normalizedLanguage)}"` : '';
|
||||||
|
return `<pre class="code-block"><code${className}>${highlightCode(code, normalizedLanguage)}</code></pre>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.codespan = (code) => `<code>${code}</code>`;
|
||||||
|
|
||||||
|
marked.setOptions({ gfm: true, renderer });
|
||||||
|
|
||||||
|
export function allDocSlugs() {
|
||||||
|
return Object.keys(docs).filter((slug) => slug !== 'index');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderMarkdown(slug) {
|
||||||
|
const source = docs[slug] ?? docs.index;
|
||||||
|
const html = marked.parse(source ?? '# Not found\n');
|
||||||
|
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
|
||||||
|
const target = normalizeDocLink(slug, href);
|
||||||
|
return `href="/docs/${target}/${hash}"`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDocLink(currentSlug, href) {
|
||||||
|
const base = currentSlug.includes('/') ? currentSlug.split('/').slice(0, -1) : [];
|
||||||
|
const parts = [...base, ...href.split('/')];
|
||||||
|
const out = [];
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part || part === '.') continue;
|
||||||
|
if (part === '..') out.pop();
|
||||||
|
else out.push(part);
|
||||||
|
}
|
||||||
|
if (out[out.length - 1] === 'index') out.pop();
|
||||||
|
return out.join('/') || 'index';
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']);
|
||||||
|
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool']);
|
||||||
|
const DECODAL_LITERALS = new Set(['true', 'false']);
|
||||||
|
|
||||||
|
const HTML_ESCAPE = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function escapeHtml(value) {
|
||||||
|
return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPE[char]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeAttribute(value) {
|
||||||
|
return escapeHtml(value).replace(/`/g, '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightCode(code, language = '') {
|
||||||
|
const normalized = language.toLowerCase();
|
||||||
|
if (normalized === 'dcdl' || normalized === 'decodal') return highlightDecodal(code);
|
||||||
|
if (normalized === 'sh' || normalized === 'bash' || normalized === 'shell') return highlightShell(code);
|
||||||
|
return escapeHtml(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightDecodalTokens(source, tokens) {
|
||||||
|
let html = '';
|
||||||
|
let cursor = 0;
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.start > cursor) html += escapeHtml(source.slice(cursor, token.start));
|
||||||
|
const text = source.slice(token.start, token.end);
|
||||||
|
const kind = tokenClass(token.kind, text);
|
||||||
|
html += kind ? `<span class="${kind}">${escapeHtml(text)}</span>` : escapeHtml(text);
|
||||||
|
cursor = token.end;
|
||||||
|
}
|
||||||
|
if (cursor < source.length) html += escapeHtml(source.slice(cursor));
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenClass(kind, text) {
|
||||||
|
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword';
|
||||||
|
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Array'].includes(text)) return 'tok-type';
|
||||||
|
if (kind === 'ident') return '';
|
||||||
|
if (['true', 'false'].includes(kind)) return 'tok-literal';
|
||||||
|
if (['int', 'float'].includes(kind)) return 'tok-number';
|
||||||
|
if (kind === 'string') return 'tok-string';
|
||||||
|
if (kind === 'regex') return 'tok-regex';
|
||||||
|
if (kind === 'comment') return 'tok-comment';
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'equal',
|
||||||
|
'equal_equal',
|
||||||
|
'bang',
|
||||||
|
'bang_equal',
|
||||||
|
'arrow',
|
||||||
|
'amp',
|
||||||
|
'amp_amp',
|
||||||
|
'pipe_pipe',
|
||||||
|
'plus',
|
||||||
|
'plus_plus',
|
||||||
|
'minus',
|
||||||
|
'star',
|
||||||
|
'slash',
|
||||||
|
'slash_slash',
|
||||||
|
'gt',
|
||||||
|
'gte',
|
||||||
|
'lt',
|
||||||
|
'lte',
|
||||||
|
'dot',
|
||||||
|
'colon',
|
||||||
|
'semicolon',
|
||||||
|
'comma',
|
||||||
|
'l_brace',
|
||||||
|
'r_brace',
|
||||||
|
'l_bracket',
|
||||||
|
'r_bracket',
|
||||||
|
'l_paren',
|
||||||
|
'r_paren',
|
||||||
|
].includes(kind)
|
||||||
|
) {
|
||||||
|
return 'tok-operator';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightDecodal(source) {
|
||||||
|
let html = '';
|
||||||
|
let index = 0;
|
||||||
|
let canEndExpression = false;
|
||||||
|
|
||||||
|
while (index < source.length) {
|
||||||
|
const char = source[index];
|
||||||
|
const next = source[index + 1];
|
||||||
|
|
||||||
|
if (char === '#') {
|
||||||
|
const end = readUntilLineEnd(source, index);
|
||||||
|
html += token('comment', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '"') {
|
||||||
|
const end = readString(source, index);
|
||||||
|
html += token('string', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
canEndExpression = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '/' && next && next !== '/' && !canEndExpression) {
|
||||||
|
const end = readRegex(source, index);
|
||||||
|
html += token('regex', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
canEndExpression = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumberStart(source, index)) {
|
||||||
|
const end = readNumber(source, index);
|
||||||
|
html += token('number', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
canEndExpression = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIdentifierStart(char)) {
|
||||||
|
const end = readIdentifier(source, index);
|
||||||
|
const ident = source.slice(index, end);
|
||||||
|
if (DECODAL_KEYWORDS.has(ident)) html += token('keyword', ident);
|
||||||
|
else if (DECODAL_TYPES.has(ident)) html += token('type', ident);
|
||||||
|
else if (DECODAL_LITERALS.has(ident)) html += token('literal', ident);
|
||||||
|
else html += escapeHtml(ident);
|
||||||
|
canEndExpression = true;
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOperatorStart(char)) {
|
||||||
|
const end = readOperator(source, index);
|
||||||
|
html += token('operator', source.slice(index, end));
|
||||||
|
canEndExpression = /[})\]]/.test(char);
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += escapeHtml(char);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightShell(source) {
|
||||||
|
return source
|
||||||
|
.split(/(\n)/)
|
||||||
|
.map((line) => {
|
||||||
|
if (line === '\n') return line;
|
||||||
|
const commentIndex = line.indexOf('#');
|
||||||
|
const code = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
|
||||||
|
const comment = commentIndex >= 0 ? line.slice(commentIndex) : '';
|
||||||
|
return `${highlightShellCode(code)}${comment ? token('comment', comment) : ''}`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightShellCode(source) {
|
||||||
|
const escaped = escapeHtml(source);
|
||||||
|
return escaped
|
||||||
|
.replace(/("[^&]*(?:&(?!quot;)[^&]*)*"|'[^']*')/g, '<span class="tok-string">$1</span>')
|
||||||
|
.replace(/(^|\s)(--?[A-Za-z0-9][A-Za-z0-9-]*)/g, '$1<span class="tok-operator">$2</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function token(kind, value) {
|
||||||
|
return `<span class="tok-${kind}">${escapeHtml(value)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readUntilLineEnd(source, start) {
|
||||||
|
const end = source.indexOf('\n', start);
|
||||||
|
return end < 0 ? source.length : end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(source, start) {
|
||||||
|
let index = start + 1;
|
||||||
|
while (index < source.length) {
|
||||||
|
if (source[index] === '\\') {
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (source[index] === '"') return index + 1;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRegex(source, start) {
|
||||||
|
let index = start + 1;
|
||||||
|
let inClass = false;
|
||||||
|
while (index < source.length) {
|
||||||
|
const char = source[index];
|
||||||
|
if (char === '\\') {
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '[') inClass = true;
|
||||||
|
else if (char === ']') inClass = false;
|
||||||
|
else if (char === '/' && !inClass) {
|
||||||
|
index += 1;
|
||||||
|
while (/[A-Za-z]/.test(source[index] ?? '')) index += 1;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
if (char === '\n') return index;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumberStart(source, index) {
|
||||||
|
const char = source[index];
|
||||||
|
const next = source[index + 1];
|
||||||
|
const prev = source[index - 1];
|
||||||
|
if (/[0-9]/.test(char)) return true;
|
||||||
|
return char === '-' && /[0-9]/.test(next ?? '') && !isIdentifierPart(prev ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(source, start) {
|
||||||
|
let index = start;
|
||||||
|
if (source[index] === '-') index += 1;
|
||||||
|
while (/[0-9]/.test(source[index] ?? '')) index += 1;
|
||||||
|
if (source[index] === '.' && /[0-9]/.test(source[index + 1] ?? '')) {
|
||||||
|
index += 1;
|
||||||
|
while (/[0-9]/.test(source[index] ?? '')) index += 1;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentifierStart(char) {
|
||||||
|
return /[A-Za-z_]/.test(char ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentifierPart(char) {
|
||||||
|
return /[A-Za-z0-9_-]/.test(char ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readIdentifier(source, start) {
|
||||||
|
let index = start + 1;
|
||||||
|
while (isIdentifierPart(source[index])) index += 1;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperatorStart(char) {
|
||||||
|
return '&=<>!|:;,.{}()[]+-*/'.includes(char ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOperator(source, start) {
|
||||||
|
let index = start + 1;
|
||||||
|
if ('&|/+'.includes(source[start]) && source[index] === source[start]) return index + 1;
|
||||||
|
if ((source[start] === '<' || source[start] === '>' || source[start] === '=' || source[start] === '!') && source[index] === '=') {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import ManualLayout from '../../layouts/ManualLayout.astro';
|
||||||
|
import { allDocSlugs, renderMarkdown } from '../../lib/docs.js';
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return allDocSlugs().map((slug) => ({
|
||||||
|
params: { slug },
|
||||||
|
props: { slug },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slug } = Astro.props;
|
||||||
|
const html = renderMarkdown(slug);
|
||||||
|
---
|
||||||
|
<ManualLayout title={`Decodal - ${slug}`} active={slug}>
|
||||||
|
<article class="markdown" set:html={html} />
|
||||||
|
</ManualLayout>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/docs/introduction/');
|
||||||
|
---
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
import ManualLayout from '../layouts/ManualLayout.astro';
|
||||||
|
|
||||||
|
const example = `Port = Int >= 1 & <= 65535 default 8080;
|
||||||
|
|
||||||
|
Server = {
|
||||||
|
host = String default "localhost";
|
||||||
|
port = Port;
|
||||||
|
};
|
||||||
|
|
||||||
|
Production = Server & {
|
||||||
|
host = "example.com";
|
||||||
|
};`;
|
||||||
|
---
|
||||||
|
<ManualLayout title="Decodal" frontpage>
|
||||||
|
<section class="hero">
|
||||||
|
<h1>Decodal keeps configuration, constraints, and defaults in one composable value.</h1>
|
||||||
|
<p class="hero-copy">
|
||||||
|
Define structured data with constraints, compose it with patches, and materialize the result from Rust,
|
||||||
|
WebAssembly, or the browser playground.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a class="button primary" href="/playground/">Try the playground</a>
|
||||||
|
<a class="button secondary" href="/docs/introduction/">Read the manual</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-packages" aria-label="Official packages">
|
||||||
|
<h2>Official packages</h2>
|
||||||
|
<div class="package-links">
|
||||||
|
<a href="https://crates.io/crates/decodal">
|
||||||
|
<span>Rust crate</span>
|
||||||
|
<strong>crates.io/crates/decodal</strong>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/decodal-wasm">
|
||||||
|
<span>npm package</span>
|
||||||
|
<strong>npmjs.com/package/decodal-wasm</strong>
|
||||||
|
</a>
|
||||||
|
<a href="https://jsr.io/@hare/decodal-wasm@0.1.2">
|
||||||
|
<span>JSR package</span>
|
||||||
|
<strong>jsr.io/@hare/decodal-wasm</strong>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/decodal-codemirror">
|
||||||
|
<span>CodeMirror package</span>
|
||||||
|
<strong>npmjs.com/package/decodal-codemirror</strong>
|
||||||
|
</a>
|
||||||
|
<a href="https://jsr.io/@hare/decodal-codemirror@0.1.2">
|
||||||
|
<span>CodeMirror on JSR</span>
|
||||||
|
<strong>jsr.io/@hare/decodal-codemirror</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-grid" aria-label="Decodal overview">
|
||||||
|
<article class="home-card">
|
||||||
|
<h2>What it is</h2>
|
||||||
|
<p>
|
||||||
|
Decodal is a small data language for configuration shapes that need validation, defaults,
|
||||||
|
lazy imports, and predictable composition.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="home-card">
|
||||||
|
<h2>Where it runs</h2>
|
||||||
|
<p>
|
||||||
|
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for the Web editor,
|
||||||
|
and Tree-sitter for general editor integration.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="home-card">
|
||||||
|
<h2>Start here</h2>
|
||||||
|
<p>
|
||||||
|
Open the playground to run examples in your browser, or read the components page to see how the runtime and
|
||||||
|
editor pieces fit together.
|
||||||
|
</p>
|
||||||
|
<p><a href="/docs/components/">View components →</a></p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-example" aria-label="Decodal example">
|
||||||
|
<div>
|
||||||
|
<h2>A compact example</h2>
|
||||||
|
<p>
|
||||||
|
Constraints and defaults are values. Compose them with concrete data and materialize when the host knows what
|
||||||
|
output shape it expects.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<pre><code>{example}</code></pre>
|
||||||
|
</section>
|
||||||
|
</ManualLayout>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import ManualLayout from '../layouts/ManualLayout.astro';
|
||||||
|
---
|
||||||
|
<ManualLayout title="Decodal Playground" playground>
|
||||||
|
<section class="playground-page">
|
||||||
|
<div class="playground-header">
|
||||||
|
<div>
|
||||||
|
<h1>Playground</h1>
|
||||||
|
<p>Virtual files are evaluated in the browser through WebAssembly. Use import paths such as <code>./schemas/service.dcdl</code>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="playground-actions">
|
||||||
|
<label class="example-picker">
|
||||||
|
<span>Example</span>
|
||||||
|
<select id="example-select" aria-label="Playground example"></select>
|
||||||
|
</label>
|
||||||
|
<button id="load-example" type="button">Load</button>
|
||||||
|
<p id="status" class="status">Loading WASM...</p>
|
||||||
|
<button id="run" disabled>Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playground-shell">
|
||||||
|
<aside class="file-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>Files</span>
|
||||||
|
<button id="new-file" type="button">New</button>
|
||||||
|
</div>
|
||||||
|
<div id="file-tree" class="file-tree"></div>
|
||||||
|
<button id="delete-file" class="danger-button" type="button">Delete file</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="pane input-pane">
|
||||||
|
<span id="active-file">Input</span>
|
||||||
|
<div id="editor" class="code-editor"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pane output-pane">
|
||||||
|
<span>Output</span>
|
||||||
|
<pre id="output"></pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
import '../scripts/playground.js';
|
||||||
|
</script>
|
||||||
|
</ManualLayout>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
export const playgroundExamples = [
|
||||||
|
{
|
||||||
|
id: 'service-deployment',
|
||||||
|
title: 'Service deployment',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
schema = import "./schemas/service.dcdl";
|
||||||
|
prod = import "./env/prod.dcdl";
|
||||||
|
shared = import "./shared/tags.dcdl";
|
||||||
|
in
|
||||||
|
schema.Service & prod.ServicePatch & {
|
||||||
|
name = "api";
|
||||||
|
image = "registry.example.com/api:2026-06-24";
|
||||||
|
port = 9000 + 443;
|
||||||
|
public = prod.is_public && 9000 + 443 > 9000;
|
||||||
|
tags = shared.common ++ ["api", "edge"];
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'schemas/service.dcdl': `Service = {
|
||||||
|
name = String;
|
||||||
|
image = String;
|
||||||
|
port = Int & > 443 default 8443;
|
||||||
|
replicas = Int & > 0 default 2;
|
||||||
|
public = Bool default false;
|
||||||
|
feature.metrics = Bool default true;
|
||||||
|
feature.tracing = Bool default false;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'env/prod.dcdl': `is_public = true;
|
||||||
|
|
||||||
|
ServicePatch = {
|
||||||
|
replicas = 3;
|
||||||
|
feature.tracing = true;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'shared/tags.dcdl': `common = ["decodal", "prod"];
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access-policy',
|
||||||
|
title: 'Access policy',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
policy = import "./policy/base.dcdl";
|
||||||
|
teams = import "./policy/teams.dcdl";
|
||||||
|
env = "prod";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
service = "payments";
|
||||||
|
readers = policy.defaultReaders ++ teams.observability;
|
||||||
|
writers = policy.defaultWriters ++ teams.payments;
|
||||||
|
admins = match env {
|
||||||
|
"prod": policy.breakglassAdmins;
|
||||||
|
_: teams.platform;
|
||||||
|
};
|
||||||
|
audit.required = env == "prod";
|
||||||
|
audit.retention_days = match env {
|
||||||
|
"prod": 365;
|
||||||
|
_: 30;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'policy/base.dcdl': `defaultReaders = ["group:engineering", "group:support"];
|
||||||
|
defaultWriters = ["group:platform"];
|
||||||
|
breakglassAdmins = ["user:oncall-primary", "user:oncall-secondary"];
|
||||||
|
`,
|
||||||
|
'policy/teams.dcdl': `platform = ["group:platform-admins"];
|
||||||
|
payments = ["group:payments-api", "group:payments-sre"];
|
||||||
|
observability = ["group:observability"];
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edge-worker',
|
||||||
|
title: 'Edge worker config',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
schema = import "./schemas/worker.dcdl";
|
||||||
|
routes = import "./routes.dcdl";
|
||||||
|
production = true;
|
||||||
|
in
|
||||||
|
schema.Worker & {
|
||||||
|
name = "decodal-docs";
|
||||||
|
compatibility_date = "2026-06-15";
|
||||||
|
workers_dev = !production;
|
||||||
|
route.host = routes.primary_host;
|
||||||
|
route.paths = routes.docs_paths ++ routes.playground_paths;
|
||||||
|
cache.ttl_seconds = 60 * 60;
|
||||||
|
cache.bypass = !production;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'schemas/worker.dcdl': `Worker = {
|
||||||
|
name = String;
|
||||||
|
compatibility_date = String;
|
||||||
|
workers_dev = Bool default false;
|
||||||
|
route.host = String;
|
||||||
|
cache.ttl_seconds = Int & >= 0 default 300;
|
||||||
|
cache.bypass = Bool default false;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'routes.dcdl': `primary_host = "decodal.example.com";
|
||||||
|
docs_paths = ["/docs/*", "/assets/*"];
|
||||||
|
playground_paths = ["/playground/*"];
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data-pipeline',
|
||||||
|
title: 'Data pipeline',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
schema = import "./schemas/pipeline.dcdl";
|
||||||
|
presets = import "./presets/batch.dcdl";
|
||||||
|
env = "staging";
|
||||||
|
in
|
||||||
|
schema.Pipeline & presets.Batch & {
|
||||||
|
name = "events-rollup";
|
||||||
|
source.topic = "events.raw";
|
||||||
|
sink.table = "analytics.events_daily";
|
||||||
|
workers = match env {
|
||||||
|
"prod": 8;
|
||||||
|
"staging": 3;
|
||||||
|
_: 1;
|
||||||
|
};
|
||||||
|
alerts.enabled = env != "dev";
|
||||||
|
transforms = presets.standardTransforms ++ ["dedupe", "aggregate_daily"];
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'schemas/pipeline.dcdl': `Pipeline = {
|
||||||
|
name = String;
|
||||||
|
source.topic = String;
|
||||||
|
sink.table = String;
|
||||||
|
workers = Int & > 0 default 1;
|
||||||
|
batch.size = Int & >= 100 default 1000;
|
||||||
|
batch.timeout_seconds = Int & > 0 default 60;
|
||||||
|
alerts.enabled = Bool default false;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'presets/batch.dcdl': `standardTransforms = ["parse_json", "validate_schema", "enrich_metadata"];
|
||||||
|
|
||||||
|
Batch = {
|
||||||
|
batch.size = 5000;
|
||||||
|
batch.timeout_seconds = 30 + 30;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'diagnostics',
|
||||||
|
title: 'Diagnostics: invalid port',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
schema = import "./schemas/service.dcdl";
|
||||||
|
in
|
||||||
|
schema.Service & {
|
||||||
|
name = "broken-api";
|
||||||
|
port = 442;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'schemas/service.dcdl': `Service = {
|
||||||
|
name = String;
|
||||||
|
port = Int & > 443 default 8443;
|
||||||
|
replicas = Int & > 0 default 2;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
|
||||||
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
|
import { keymap } from '@codemirror/view';
|
||||||
|
import { decodal } from 'decodal-codemirror';
|
||||||
|
import { playgroundExamples } from './playground-examples.js';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'decodal-playground-project-v1';
|
||||||
|
const starterProject = playgroundExamples[0];
|
||||||
|
|
||||||
|
const editorHost = document.getElementById('editor');
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const run = document.getElementById('run');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const fileTree = document.getElementById('file-tree');
|
||||||
|
const activeFile = document.getElementById('active-file');
|
||||||
|
const newFile = document.getElementById('new-file');
|
||||||
|
const deleteFile = document.getElementById('delete-file');
|
||||||
|
const exampleSelect = document.getElementById('example-select');
|
||||||
|
const loadExample = document.getElementById('load-example');
|
||||||
|
|
||||||
|
const project = loadProject();
|
||||||
|
|
||||||
|
const editorTheme = EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
backgroundColor: '#0f172a',
|
||||||
|
color: '#e5e7eb',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.5',
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
'.cm-content': {
|
||||||
|
caretColor: '#e5e7eb',
|
||||||
|
minHeight: '100%',
|
||||||
|
padding: '12px',
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: '#111827',
|
||||||
|
borderRight: '1px solid #334155',
|
||||||
|
color: '#94a3b8',
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
'&.cm-focused .cm-cursor': {
|
||||||
|
borderLeftColor: '#e5e7eb',
|
||||||
|
},
|
||||||
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||||
|
backgroundColor: 'rgb(59 130 246 / 0.35)',
|
||||||
|
},
|
||||||
|
}, { dark: true });
|
||||||
|
|
||||||
|
const editor = new EditorView({
|
||||||
|
doc: project.files[project.activePath] ?? '',
|
||||||
|
parent: editorHost,
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
decodal(),
|
||||||
|
editorTheme,
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Mod-Enter',
|
||||||
|
run() {
|
||||||
|
execute();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (!update.docChanged) return;
|
||||||
|
project.files[project.activePath] = update.state.doc.toString();
|
||||||
|
saveProject();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const example of playgroundExamples) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = example.id;
|
||||||
|
option.textContent = example.title;
|
||||||
|
exampleSelect.append(option);
|
||||||
|
}
|
||||||
|
exampleSelect.value = starterProject.id;
|
||||||
|
|
||||||
|
setActiveFile(project.activePath);
|
||||||
|
renderFileTree();
|
||||||
|
|
||||||
|
function loadProject() {
|
||||||
|
try {
|
||||||
|
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null');
|
||||||
|
if (stored && stored.files && typeof stored.activePath === 'string') {
|
||||||
|
const files = normalizeFiles(stored.files);
|
||||||
|
const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath;
|
||||||
|
if (activePath) return { files, activePath };
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Fall back to the starter project.
|
||||||
|
}
|
||||||
|
return { files: cloneFiles(starterProject.files), activePath: starterProject.activePath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneFiles(files) {
|
||||||
|
return Object.fromEntries(Object.entries(files).map(([path, source]) => [path, source]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFiles(files) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(files)
|
||||||
|
.filter(([_path, value]) => typeof value === 'string')
|
||||||
|
.map(([path, value]) => [normalizePath(path), value])
|
||||||
|
.filter(([path]) => path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProject() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExampleProject(exampleId) {
|
||||||
|
const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject;
|
||||||
|
project.files = cloneFiles(example.files);
|
||||||
|
project.activePath = example.activePath;
|
||||||
|
exampleSelect.value = example.id;
|
||||||
|
setActiveFile(project.activePath);
|
||||||
|
output.textContent = '';
|
||||||
|
output.classList.remove('error');
|
||||||
|
execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path) {
|
||||||
|
const parts = [];
|
||||||
|
for (const part of String(path).replaceAll('\\', '/').split('/')) {
|
||||||
|
if (!part || part === '.') continue;
|
||||||
|
if (part === '..') parts.pop();
|
||||||
|
else parts.push(part);
|
||||||
|
}
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditorText() {
|
||||||
|
return editor.state.doc.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorText(value) {
|
||||||
|
editor.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveFile(path) {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (project.files[normalized] === undefined) return;
|
||||||
|
if (project.activePath && project.files[project.activePath] !== undefined) {
|
||||||
|
project.files[project.activePath] = getEditorText();
|
||||||
|
}
|
||||||
|
project.activePath = normalized;
|
||||||
|
setEditorText(project.files[normalized]);
|
||||||
|
activeFile.textContent = normalized;
|
||||||
|
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
||||||
|
renderFileTree();
|
||||||
|
saveProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
function execute() {
|
||||||
|
project.files[project.activePath] = getEditorText();
|
||||||
|
saveProject();
|
||||||
|
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
|
||||||
|
output.textContent = result.ok ? result.output : result.error;
|
||||||
|
output.classList.toggle('error', !result.ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFileTree() {
|
||||||
|
const tree = buildTree(Object.keys(project.files).sort());
|
||||||
|
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTree(paths) {
|
||||||
|
const root = { name: '', children: new Map(), path: '' };
|
||||||
|
for (const path of paths) {
|
||||||
|
const parts = path.split('/');
|
||||||
|
let node = root;
|
||||||
|
let currentPath = '';
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||||
|
if (!node.children.has(part)) {
|
||||||
|
node.children.set(part, { name: part, children: new Map(), path: currentPath, file: index + 1 === parts.length });
|
||||||
|
}
|
||||||
|
node = node.children.get(part);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTreeList(children) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
for (const child of [...children.values()].sort(compareNodes)) {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
if (child.file) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = child.path === project.activePath ? 'file active' : 'file';
|
||||||
|
button.textContent = child.name;
|
||||||
|
button.title = child.path;
|
||||||
|
button.addEventListener('click', () => setActiveFile(child.path));
|
||||||
|
item.append(button);
|
||||||
|
} else {
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'folder';
|
||||||
|
label.textContent = `${child.name}/`;
|
||||||
|
item.append(label, renderTreeList(child.children));
|
||||||
|
}
|
||||||
|
list.append(item);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNodes(a, b) {
|
||||||
|
if (a.file !== b.file) return a.file ? 1 : -1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
run.disabled = false;
|
||||||
|
status.textContent = '';
|
||||||
|
execute();
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
run.addEventListener('click', execute);
|
||||||
|
loadExample.addEventListener('click', () => {
|
||||||
|
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
||||||
|
if (!example) return;
|
||||||
|
if (!confirm(`Load example "${example.title}"? This replaces the current virtual files.`)) return;
|
||||||
|
loadExampleProject(example.id);
|
||||||
|
});
|
||||||
|
newFile.addEventListener('click', () => {
|
||||||
|
const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? '');
|
||||||
|
if (!path) return;
|
||||||
|
if (project.files[path] !== undefined) {
|
||||||
|
setActiveFile(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
project.files[path] = 'value = "new";\n';
|
||||||
|
setActiveFile(path);
|
||||||
|
});
|
||||||
|
deleteFile.addEventListener('click', () => {
|
||||||
|
if (Object.keys(project.files).length <= 1) return;
|
||||||
|
if (!confirm(`Delete ${project.activePath}?`)) return;
|
||||||
|
delete project.files[project.activePath];
|
||||||
|
setActiveFile(Object.keys(project.files).sort()[0]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
--bg: #f7f7f8;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--text: #1f2328;
|
||||||
|
--heading: #111827;
|
||||||
|
--muted: #4b5563;
|
||||||
|
--subtle: #64748b;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--link: #2563eb;
|
||||||
|
--topbar-bg: #111827;
|
||||||
|
--topbar-text: #ffffff;
|
||||||
|
--topbar-link: #dbeafe;
|
||||||
|
--active-bg: #dbeafe;
|
||||||
|
--active-text: #1d4ed8;
|
||||||
|
--nav-number: #94a3b8;
|
||||||
|
--shadow: 0 8px 24px rgb(15 23 42 / 0.05);
|
||||||
|
--inline-code-bg: #eef2ff;
|
||||||
|
--inline-code-text: #3730a3;
|
||||||
|
--editor-bg: #0f172a;
|
||||||
|
--editor-text: #e5e7eb;
|
||||||
|
--danger: #b91c1c;
|
||||||
|
--error: #fecaca;
|
||||||
|
--selection: rgb(59 130 246 / 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0b1120;
|
||||||
|
--surface: #111827;
|
||||||
|
--text: #d1d5db;
|
||||||
|
--heading: #f9fafb;
|
||||||
|
--muted: #9ca3af;
|
||||||
|
--subtle: #94a3b8;
|
||||||
|
--border: #334155;
|
||||||
|
--link: #60a5fa;
|
||||||
|
--topbar-bg: #020617;
|
||||||
|
--topbar-text: #f8fafc;
|
||||||
|
--topbar-link: #bfdbfe;
|
||||||
|
--active-bg: #1e3a8a;
|
||||||
|
--active-text: #bfdbfe;
|
||||||
|
--nav-number: #64748b;
|
||||||
|
--shadow: 0 8px 24px rgb(0 0 0 / 0.25);
|
||||||
|
--inline-code-bg: #1e293b;
|
||||||
|
--inline-code-text: #bfdbfe;
|
||||||
|
--danger: #fca5a5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--topbar-bg);
|
||||||
|
color: var(--topbar-text);
|
||||||
|
display: flex;
|
||||||
|
height: 56px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
color: var(--topbar-text);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav a {
|
||||||
|
color: var(--topbar-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--subtle);
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
gap: 14px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
|
min-height: calc(100vh - 104px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-layout {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: var(--surface);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 22px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
color: var(--heading);
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tree {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tree .nav-tree {
|
||||||
|
margin: 4px 0 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tree a {
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
gap: 6px;
|
||||||
|
line-height: 1.35;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-number {
|
||||||
|
color: var(--nav-number);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tree a.active {
|
||||||
|
background: var(--active-bg);
|
||||||
|
color: var(--active-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tree a.active .nav-number {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.playground {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 920px;
|
||||||
|
padding: 24px 36px 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h1,
|
||||||
|
.markdown h2,
|
||||||
|
.markdown h3 {
|
||||||
|
color: var(--heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown pre,
|
||||||
|
.output-pane pre {
|
||||||
|
background: var(--editor-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--editor-text);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown :not(pre) > code {
|
||||||
|
background: var(--inline-code-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--inline-code-text);
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
display: block;
|
||||||
|
margin: 20px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown th,
|
||||||
|
.markdown td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown th {
|
||||||
|
background: var(--inline-code-bg);
|
||||||
|
color: var(--heading);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown tr:nth-child(even) td {
|
||||||
|
background: color-mix(in srgb, var(--surface) 92%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-keyword {
|
||||||
|
color: #93c5fd;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-type {
|
||||||
|
color: #67e8f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-literal,
|
||||||
|
.tok-number {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-string,
|
||||||
|
.tok-regex {
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-comment {
|
||||||
|
color: var(--nav-number);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-operator {
|
||||||
|
color: #f9a8d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-page {
|
||||||
|
height: calc(100vh - 76px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-header h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-header p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-header code {
|
||||||
|
background: var(--inline-code-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--inline-code-text);
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-picker {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
gap: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-picker select {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--link);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--topbar-text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 220px minmax(360px, 1.25fr) minmax(320px, 1fr);
|
||||||
|
height: calc(100% - 52px);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-panel,
|
||||||
|
.pane {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header,
|
||||||
|
.pane > span {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header button {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree > ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .folder {
|
||||||
|
color: var(--subtle);
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .file {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
display: block;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 5px 7px;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .file:hover,
|
||||||
|
.file-tree .file.active {
|
||||||
|
background: var(--active-bg);
|
||||||
|
color: var(--active-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-button {
|
||||||
|
background: transparent;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--danger);
|
||||||
|
padding: 9px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor {
|
||||||
|
background: var(--editor-bg);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor .cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor .cm-line {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor .cm-activeLine {
|
||||||
|
background: rgb(59 130 246 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor .cm-activeLineGutter {
|
||||||
|
background: rgb(59 130 246 / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-pane pre {
|
||||||
|
border-radius: 0;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-pane pre.error {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontpage {
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 5rem 0 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(2.4rem, 6vw, 5rem);
|
||||||
|
line-height: 0.95;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 950px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-copy {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 1.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.9rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.8rem 1.15rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary {
|
||||||
|
background: var(--link);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-packages {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-packages h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-links {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-links a {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-links span {
|
||||||
|
color: var(--muted);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-links strong {
|
||||||
|
color: var(--text);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card,
|
||||||
|
.home-example {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card h2,
|
||||||
|
.home-example h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card p:last-child,
|
||||||
|
.home-example p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-example {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-example pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-right: 0;
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
.playground-shell,
|
||||||
|
.home-grid,
|
||||||
|
.home-example,
|
||||||
|
.package-links {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
padding-top: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# wasm-pack output is committed for the playground build.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Decodal
|
||||||
|
|
||||||
|
Decodal is a small deterministic DSL for describing, composing, validating, and materializing structured data.
|
||||||
|
|
||||||
|
It is designed around a lightweight Rust library:
|
||||||
|
|
||||||
|
- host-supplied imports through `SourceLoader`
|
||||||
|
- no filesystem access in the library core
|
||||||
|
- concrete and abstract values with constraints and defaults
|
||||||
|
- deterministic expression evaluation
|
||||||
|
- optional regex support behind a Cargo feature
|
||||||
|
- browser playground support through WebAssembly
|
||||||
|
|
||||||
|
## Library crate
|
||||||
|
|
||||||
|
Embedded hosts should depend on `decodal` and provide imports with a `SourceLoader`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
decodal = "0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Derive support
|
||||||
|
|
||||||
|
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.1", features = ["derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Decodal, DecodalDecode, DecodalSchema, Engine};
|
||||||
|
|
||||||
|
#[derive(Decodal)]
|
||||||
|
struct Service {
|
||||||
|
name: String,
|
||||||
|
#[decodal(gt = 443, default = 8443)]
|
||||||
|
port: i64,
|
||||||
|
#[decodal(rename = "feature.enable", default = true)]
|
||||||
|
feature_enabled: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The derive implements:
|
||||||
|
|
||||||
|
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
|
||||||
|
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
A standalone CLI is kept in this repository as the `decodal-cli` workspace package.
|
||||||
|
It builds a `decodal` binary, but it is not the primary crates.io package.
|
||||||
|
|
||||||
|
Run a Decodal file from the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-cli -- examples/advanced/main.dcdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable optional regex support when needed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web playground
|
||||||
|
|
||||||
|
The static documentation site and browser playground live under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
site/decodal-site/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under either of:
|
||||||
|
|
||||||
|
- Apache License, Version 2.0
|
||||||
|
- MIT license
|
||||||
|
|
||||||
|
at your option.
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export function evaluate(source: string): string;
|
||||||
|
|
||||||
|
export function evaluateProject(entry: string, files_json: string): string;
|
||||||
|
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly evaluate: (a: number, b: number) => [number, number];
|
||||||
|
readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
|
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||||
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wbindgen_start: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the given `module`, which can either be bytes or
|
||||||
|
* a precompiled `WebAssembly.Module`.
|
||||||
|
*
|
||||||
|
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {InitOutput}
|
||||||
|
*/
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user