Compare commits
20
Commits
main
..
87fef44c68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
+173
-6
@@ -12,25 +12,76 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal"
|
name = "bumpalo"
|
||||||
version = "0.1.0"
|
version = "3.20.3"
|
||||||
dependencies = [
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
"decodal-core",
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-core"
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
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 +110,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"
|
||||||
|
|||||||
+13
-7
@@ -2,12 +2,18 @@
|
|||||||
members = [
|
members = [
|
||||||
"crates/decodal-core",
|
"crates/decodal-core",
|
||||||
"crates/decodal-cli",
|
"crates/decodal-cli",
|
||||||
|
"crates/decodal-wasm",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "2"
|
||||||
|
|
||||||
[profile.release]
|
[workspace.package]
|
||||||
opt-level = "z"
|
version = "0.1.0"
|
||||||
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,55 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.0", 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,7 +1,14 @@
|
|||||||
[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"]
|
||||||
|
|||||||
@@ -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,154 @@ 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, _))
|
||||||
|
) && (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 {
|
||||||
if lower.inclusive {
|
constraint: Constraint::Compare(
|
||||||
CompareOp::Gte
|
if lower.inclusive {
|
||||||
} else {
|
CompareOp::Gte
|
||||||
CompareOp::Gt
|
} else {
|
||||||
},
|
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 {
|
||||||
if upper.inclusive {
|
constraint: Constraint::Compare(
|
||||||
CompareOp::Lte
|
if upper.inclusive {
|
||||||
} else {
|
CompareOp::Lte
|
||||||
CompareOp::Lt
|
} else {
|
||||||
},
|
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 +198,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 +233,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 +286,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 +310,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 +336,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 +347,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_regex_constraints_without_intersection_check() {
|
fn detects_non_integer_int_bound() {
|
||||||
let constraints = normalize_constraints(
|
assert!(
|
||||||
alloc::vec![
|
normalize_constraints(
|
||||||
Constraint::Regex(String::from("^a$")),
|
alloc::vec![
|
||||||
Constraint::Regex(String::from("^b$")),
|
entry(Constraint::Type(PrimitiveType::Int)),
|
||||||
],
|
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Float(1.5))),
|
||||||
Span::default(),
|
],
|
||||||
)
|
Span::default(),
|
||||||
.unwrap();
|
)
|
||||||
assert_eq!(constraints.len(), 2);
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
+737
-78
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,
|
||||||
@@ -67,9 +77,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,7 +91,7 @@ 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();
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let Some(ch) = self.peek() else {
|
let Some(ch) = self.peek() else {
|
||||||
@@ -134,16 +148,57 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
b'&' => {
|
b'&' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
TokenKind::Amp
|
if self.consume(b'&') {
|
||||||
|
TokenKind::AmpAmp
|
||||||
|
} else {
|
||||||
|
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 +219,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 +398,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::*;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
|||||||
pub use constraints::normalize_constraints;
|
pub use constraints::normalize_constraints;
|
||||||
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 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};
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -108,6 +115,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,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.0", 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 = ''
|
||||||
|
|||||||
@@ -117,3 +117,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 は通常の値選択として扱い、エラー内容に依存した実行時分岐は避ける。
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled be
|
|||||||
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 とは別の型として扱う。
|
||||||
|
|||||||
@@ -9,15 +9,100 @@ 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.
|
||||||
|
Workspace support crates such as `decodal-cli` and `decodal-wasm` are not published for 0.1.0.
|
||||||
|
The project is dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo publish -p decodal
|
||||||
|
```
|
||||||
|
|
||||||
|
Before publishing, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fmt --check
|
||||||
|
cargo test
|
||||||
|
cargo check -p decodal --no-default-features
|
||||||
|
cargo publish -p decodal --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/layouts/ManualLayout.astro
|
||||||
|
site/decodal-site/src/lib/docs.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.
|
||||||
|
|
||||||
|
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
|
||||||
@@ -73,7 +158,7 @@ When the Decodal syntax changes:
|
|||||||
|
|
||||||
## 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 +172,7 @@ The shell provides:
|
|||||||
- `clippy`
|
- `clippy`
|
||||||
- `node`
|
- `node`
|
||||||
- `npm`
|
- `npm`
|
||||||
|
- `wasm-pack`
|
||||||
|
- `lld`
|
||||||
- `tree-sitter`
|
- `tree-sitter`
|
||||||
- `nixfmt`
|
- `nixfmt`
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`++` は配列要素を変換しない。
|
||||||
|
左辺の要素の後に右辺の要素が並ぶ。
|
||||||
|
|||||||
@@ -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;
|
||||||
|
```
|
||||||
@@ -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) を参照する。
|
||||||
|
|
||||||
## `&`: 制約合成
|
## `&`: 制約合成
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,15 @@ rec
|
|||||||
主要な演算子は以下である。
|
主要な演算子は以下である。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
& 制約合成
|
+ - * / 四則演算
|
||||||
// patch 合成
|
++ 配列結合
|
||||||
default fallback 指定
|
! && || 論理演算
|
||||||
=> 関数
|
== != < <= > >= 比較式
|
||||||
. フィールド参照 / ドットパス定義
|
& 制約合成
|
||||||
|
// patch 合成
|
||||||
|
default fallback 指定
|
||||||
|
=> 関数
|
||||||
|
. フィールド参照 / ドットパス定義
|
||||||
```
|
```
|
||||||
|
|
||||||
演算子の優先順位は未確定である。
|
演算子の優先順位は [合成演算子](./operators.md) で定義する。
|
||||||
詳細は [合成演算子](./operators.md) で定義する。
|
|
||||||
|
|||||||
@@ -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,16 @@
|
|||||||
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 +59,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 +167,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.
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ rustPlatform.buildRustPackage {
|
|||||||
|
|
||||||
cargoBuildFlags = [
|
cargoBuildFlags = [
|
||||||
"-p"
|
"-p"
|
||||||
"decodal"
|
"decodal-cli"
|
||||||
];
|
];
|
||||||
|
|
||||||
doInstallCheck = true;
|
doInstallCheck = true;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
});
|
||||||
Generated
+7937
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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 && wrangler pages deploy dist --project-name ${CLOUDFLARE_PROJECT_NAME:-decodal-site} --branch ${CLOUDFLARE_BRANCH:-master}",
|
||||||
|
"deploy:wasm": "npm run build:wasm && npm run deploy"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"astro": "^4.16.18",
|
||||||
|
"marked": "^12.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"wrangler": "^4.104.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import { nav } from '../lib/docs.js';
|
||||||
|
import '../style.css';
|
||||||
|
|
||||||
|
const { title = 'Decodal', active = '', playground = 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="/docs/introduction/">Decodal</a>
|
||||||
|
<nav class="topnav">
|
||||||
|
<a href="/docs/introduction/">Docs</a>
|
||||||
|
<a href="/playground/">Playground</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class={playground ? 'layout playground-layout' : 'layout'}>
|
||||||
|
{!playground && (
|
||||||
|
<aside class="sidebar">
|
||||||
|
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||||
|
<nav set:html={renderNav(nav)} />
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
<main class={playground ? 'playground' : ''}>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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: 'Language Specification',
|
||||||
|
slug: 'language',
|
||||||
|
children: [
|
||||||
|
{ title: 'Syntax', slug: 'language/syntax' },
|
||||||
|
{
|
||||||
|
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,203 @@
|
|||||||
|
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 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,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/docs/introduction/');
|
||||||
|
---
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
|
|
||||||
|
<label class="pane input-pane">
|
||||||
|
<span id="active-file">Input</span>
|
||||||
|
<div class="editor-wrap">
|
||||||
|
<pre id="source-highlight" aria-hidden="true"></pre>
|
||||||
|
<textarea id="source" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<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,207 @@
|
|||||||
|
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
|
||||||
|
import { highlightDecodal } from '../lib/highlight.js';
|
||||||
|
import { playgroundExamples } from './playground-examples.js';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'decodal-playground-project-v1';
|
||||||
|
const starterProject = playgroundExamples[0];
|
||||||
|
|
||||||
|
const source = document.getElementById('source');
|
||||||
|
const sourceHighlight = document.getElementById('source-highlight');
|
||||||
|
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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
updateHighlight();
|
||||||
|
|
||||||
|
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 setActiveFile(path) {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (project.files[normalized] === undefined) return;
|
||||||
|
project.activePath = normalized;
|
||||||
|
source.value = project.files[normalized];
|
||||||
|
activeFile.textContent = normalized;
|
||||||
|
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
||||||
|
updateHighlight();
|
||||||
|
syncHighlightScroll();
|
||||||
|
renderFileTree();
|
||||||
|
saveProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHighlight() {
|
||||||
|
sourceHighlight.innerHTML = `${highlightDecodal(source.value)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncHighlightScroll() {
|
||||||
|
sourceHighlight.scrollTop = source.scrollTop;
|
||||||
|
sourceHighlight.scrollLeft = source.scrollLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
function execute() {
|
||||||
|
project.files[project.activePath] = source.value;
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
source.addEventListener('input', () => {
|
||||||
|
project.files[project.activePath] = source.value;
|
||||||
|
updateHighlight();
|
||||||
|
syncHighlightScroll();
|
||||||
|
saveProject();
|
||||||
|
});
|
||||||
|
source.addEventListener('scroll', syncHighlightScroll);
|
||||||
|
source.addEventListener('keydown', (event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') execute();
|
||||||
|
});
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
: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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
|
min-height: calc(100vh - 56px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
background: var(--editor-bg);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap pre,
|
||||||
|
.editor-wrap textarea {
|
||||||
|
border: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
inset: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
position: absolute;
|
||||||
|
tab-size: 2;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap pre {
|
||||||
|
color: var(--editor-text);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap textarea {
|
||||||
|
background: transparent;
|
||||||
|
caret-color: var(--editor-text);
|
||||||
|
color: transparent;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap textarea::selection {
|
||||||
|
background: var(--selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-right: 0;
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
.playground-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# wasm-pack output is committed for the playground build.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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>;
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/* @ts-self-types="./decodal_wasm.d.ts" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} source
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function evaluate(source) {
|
||||||
|
let deferred2_0;
|
||||||
|
let deferred2_1;
|
||||||
|
try {
|
||||||
|
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.evaluate(ptr0, len0);
|
||||||
|
deferred2_0 = ret[0];
|
||||||
|
deferred2_1 = ret[1];
|
||||||
|
return getStringFromWasm0(ret[0], ret[1]);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} entry
|
||||||
|
* @param {string} files_json
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function evaluateProject(entry, files_json) {
|
||||||
|
let deferred3_0;
|
||||||
|
let deferred3_1;
|
||||||
|
try {
|
||||||
|
const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(files_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.evaluateProject(ptr0, len0, ptr1, len1);
|
||||||
|
deferred3_0 = ret[0];
|
||||||
|
deferred3_1 = ret[1];
|
||||||
|
return getStringFromWasm0(ret[0], ret[1]);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function __wbg_get_imports() {
|
||||||
|
const import0 = {
|
||||||
|
__proto__: null,
|
||||||
|
__wbindgen_init_externref_table: function() {
|
||||||
|
const table = wasm.__wbindgen_externrefs;
|
||||||
|
const offset = table.grow(4);
|
||||||
|
table.set(0, undefined);
|
||||||
|
table.set(offset + 0, undefined);
|
||||||
|
table.set(offset + 1, null);
|
||||||
|
table.set(offset + 2, true);
|
||||||
|
table.set(offset + 3, false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
__proto__: null,
|
||||||
|
"./decodal_wasm_bg.js": import0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
return decodeText(ptr >>> 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||||
|
let numBytesDecoded = 0;
|
||||||
|
function decodeText(ptr, len) {
|
||||||
|
numBytesDecoded += len;
|
||||||
|
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||||
|
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
numBytesDecoded = len;
|
||||||
|
}
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextEncoder = new TextEncoder();
|
||||||
|
|
||||||
|
if (!('encodeInto' in cachedTextEncoder)) {
|
||||||
|
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
let wasmModule, wasmInstance, wasm;
|
||||||
|
function __wbg_finalize_init(instance, module) {
|
||||||
|
wasmInstance = instance;
|
||||||
|
wasm = instance.exports;
|
||||||
|
wasmModule = module;
|
||||||
|
cachedUint8ArrayMemory0 = null;
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
} catch (e) {
|
||||||
|
const validResponse = module.ok && expectedResponseType(module.type);
|
||||||
|
|
||||||
|
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else { throw e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedResponseType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'basic': case 'cors': case 'default': return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSync(module) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (module !== undefined) {
|
||||||
|
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||||
|
({module} = module)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
|
module = new WebAssembly.Module(module);
|
||||||
|
}
|
||||||
|
const instance = new WebAssembly.Instance(module, imports);
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_init(module_or_path) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (module_or_path !== undefined) {
|
||||||
|
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||||
|
({module_or_path} = module_or_path)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module_or_path === undefined) {
|
||||||
|
module_or_path = new URL('decodal_wasm_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||||
|
module_or_path = fetch(module_or_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSync, __wbg_init as default };
|
||||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const evaluate: (a: number, b: number) => [number, number];
|
||||||
|
export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
|
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||||
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wbindgen_start: () => void;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "decodal-wasm",
|
||||||
|
"type": "module",
|
||||||
|
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.hareworks.net/Hare/Decodal"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"decodal_wasm_bg.wasm",
|
||||||
|
"decodal_wasm.js",
|
||||||
|
"decodal_wasm.d.ts"
|
||||||
|
],
|
||||||
|
"main": "decodal_wasm.js",
|
||||||
|
"types": "decodal_wasm.d.ts",
|
||||||
|
"sideEffects": [
|
||||||
|
"./snippets/*"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"decodal",
|
||||||
|
"wasm",
|
||||||
|
"dsl",
|
||||||
|
"config"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user