From e2ea107db6a6f22e01cade8bf80f2520aa88c1b4 Mon Sep 17 00:00:00 2001 From: Kubat <mael.martin31@gmail.com> Date: Tue, 6 Jun 2023 18:03:33 +0200 Subject: [PATCH] VVS: Dump snapshot of VVS V4 --- src/Rust/Cargo.lock | 158 +-- src/Rust/Cargo.toml | 12 +- src/Rust/README.md | 19 +- src/Rust/vvs_ass/Cargo.toml | 3 +- src/Rust/vvs_ass/src/colors.rs | 42 +- src/Rust/vvs_ass/src/definitions.rs | 34 +- src/Rust/vvs_ass/src/drawing.rs | 29 +- src/Rust/vvs_ass/src/elements/line.rs | 78 +- src/Rust/vvs_ass/src/elements/lines.rs | 14 - src/Rust/vvs_ass/src/elements/mod.rs | 24 +- src/Rust/vvs_ass/src/elements/syllabe.rs | 76 +- src/Rust/vvs_ass/src/elements/syllabes.rs | 14 - src/Rust/vvs_ass/src/lib.rs | 17 +- src/Rust/vvs_ass/src/position.rs | 19 +- src/Rust/vvs_ass/src/reader/ass.rs | 297 +++-- src/Rust/vvs_ass/src/reader/json.rs | 7 +- src/Rust/vvs_ass/src/reader/mod.rs | 40 +- src/Rust/vvs_ass/src/styles.rs | 4 +- src/Rust/vvs_ass/src/tests.rs | 9 + src/Rust/vvs_ass/src/types.rs | 55 +- src/Rust/vvs_ass/src/values.rs | 102 +- src/Rust/vvs_ass/utils/empty.ass | 19 + src/Rust/vvs_cli/Cargo.toml | 3 +- src/Rust/vvs_cli/src/args.rs | 27 +- src/Rust/vvs_cli/src/config.rs | 115 ++ src/Rust/vvs_cli/src/lib.rs | 3 + src/Rust/vvs_cli/src/logger.rs | 51 +- src/Rust/vvs_cli/src/main.rs | 58 +- src/Rust/vvs_cli/src/parser.rs | 10 +- src/Rust/vvs_font/Cargo.toml | 1 - src/Rust/vvs_font/build.rs | 3 +- .../{OFL.txt => NotoSans-LICENCE-OFL.txt} | 0 src/Rust/vvs_font/src/font.rs | 22 +- src/Rust/vvs_font/src/lib.rs | 2 + src/Rust/vvs_font/src/rect.rs | 10 +- src/Rust/vvs_lua/Cargo.toml | 6 +- src/Rust/vvs_lua/build.rs | 7 +- src/Rust/vvs_lua/src/data/actions.rs | 10 +- src/Rust/vvs_lua/src/data/register.rs | 83 +- src/Rust/vvs_lua/src/dsl.rs | 26 +- src/Rust/vvs_lua/src/func/actions.rs | 25 +- src/Rust/vvs_lua/src/func/mod.rs | 1 + src/Rust/vvs_lua/src/func/register.rs | 54 +- src/Rust/vvs_lua/src/functions.rs | 232 +--- src/Rust/vvs_lua/src/jobs/actions.rs | 114 +- src/Rust/vvs_lua/src/jobs/mod.rs | 2 +- src/Rust/vvs_lua/src/jobs/register.rs | 48 +- src/Rust/vvs_lua/src/lib.rs | 2 +- src/Rust/vvs_lua/src/libs/bit.rs | 114 ++ src/Rust/vvs_lua/src/libs/hashset.rs | 15 +- src/Rust/vvs_lua/src/libs/mod.rs | 22 +- src/Rust/vvs_lua/src/libs/vivy.rs | 661 +--------- src/Rust/vvs_lua/src/libs/vivy/byte_code.rs | 331 +++++ src/Rust/vvs_lua/src/libs/vivy/graph.rs | 171 +++ src/Rust/vvs_lua/src/libs/vivy/runtime.rs | 1061 +++++++++++++++++ src/Rust/vvs_lua/src/lua_wrapper.rs | 319 +++-- src/Rust/vvs_lua/src/options/actions.rs | 17 +- src/Rust/vvs_lua/src/options/register.rs | 112 +- src/Rust/vvs_lua/src/toml_option.rs | 25 +- src/Rust/vvs_lua/src/traits.rs | 29 +- src/Rust/vvs_lua/src/types.rs | 11 +- src/Rust/vvs_lua/src/values.rs | 53 +- src/Rust/vvs_procmacro/src/lib.rs | 44 +- src/Rust/vvs_repl/Cargo.toml | 3 +- src/Rust/vvs_repl/src/error.rs | 5 +- src/Rust/vvs_repl/src/lib.rs | 48 +- src/Rust/vvs_repl/src/tables.rs | 6 +- src/Rust/vvs_utils/Cargo.toml | 5 + src/Rust/vvs_utils/src/lib.rs | 4 + src/Rust/vvs_utils/src/minmax.rs | 12 +- src/Rust/vvs_utils/src/xdg/config.rs | 384 ++++++ src/Rust/vvs_utils/src/xdg/folders.rs | 256 ++++ src/Rust/vvs_utils/src/xdg/mod.rs | 47 + src/Rust/vvs_utils/src/xdg/options.rs | 24 + src/Rust/vvs_utils/src/xdg/paths.rs | 208 ++++ src/Rust/vvs_utils/src/xdg/tests.rs | 1 + utils/vvs/retime.vvl | 23 +- utils/vvs/test2.vvs | 1 + 78 files changed, 4187 insertions(+), 1812 deletions(-) delete mode 100644 src/Rust/vvs_ass/src/elements/lines.rs delete mode 100644 src/Rust/vvs_ass/src/elements/syllabes.rs create mode 100644 src/Rust/vvs_ass/utils/empty.ass create mode 100644 src/Rust/vvs_cli/src/config.rs rename src/Rust/vvs_font/fonts/{OFL.txt => NotoSans-LICENCE-OFL.txt} (100%) create mode 100644 src/Rust/vvs_lua/src/libs/bit.rs create mode 100644 src/Rust/vvs_lua/src/libs/vivy/byte_code.rs create mode 100644 src/Rust/vvs_lua/src/libs/vivy/graph.rs create mode 100644 src/Rust/vvs_lua/src/libs/vivy/runtime.rs create mode 100644 src/Rust/vvs_utils/src/xdg/config.rs create mode 100644 src/Rust/vvs_utils/src/xdg/folders.rs create mode 100644 src/Rust/vvs_utils/src/xdg/mod.rs create mode 100644 src/Rust/vvs_utils/src/xdg/options.rs create mode 100644 src/Rust/vvs_utils/src/xdg/paths.rs create mode 100644 src/Rust/vvs_utils/src/xdg/tests.rs diff --git a/src/Rust/Cargo.lock b/src/Rust/Cargo.lock index 4b777861..b7f96ed7 100644 --- a/src/Rust/Cargo.lock +++ b/src/Rust/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -74,9 +74,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.2.7" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" +checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" dependencies = [ "clap_builder", "clap_derive", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.7" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstyle", "bitflags", @@ -98,36 +98,36 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.2.1" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a19591b2ab0e3c04b588a0e04ddde7b9eaa423646d1b4a8092879216bf47473" +checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "clap_lex" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clap_mangen" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4237e29de9c6949982ba87d51709204504fb8ed2fd38232fcb1e5bf7d4ba48c8" +checksum = "8f2e32b579dae093c2424a8b7e2bea09c89da01e1ce5065eb2f0a6f1cc15cc1f" dependencies = [ "clap", "roff", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", @@ -229,38 +229,29 @@ dependencies = [ "either", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.142" +version = "0.2.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81" [[package]] name = "linux-raw-sys" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "lua-src" -version = "544.0.1" +version = "546.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708ba3c844d5e9d38def4a09dd871c17c370f519b3c4b7261fbabe4a613a814c" +checksum = "8cb00c1380f1b4b4928dd211c07301ffa34872a239e590bd3219d9e5b213face" dependencies = [ "cc", ] @@ -282,9 +273,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mlua" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8ce6788556a67d90567809c7de94dfef2ff1f47ff897aeee935bcfbcdf5735" +checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577" dependencies = [ "bstr", "cc", @@ -335,9 +326,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "owned_ttf_parser" @@ -356,9 +347,9 @@ checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro-error" @@ -386,27 +377,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", @@ -415,9 +406,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "roff" @@ -433,9 +424,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.18" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", @@ -468,9 +459,9 @@ dependencies = [ [[package]] name = "scc" -version = "1.6.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a242e0a9cf55e2fd90e82409ae16c20e45d8c33d1be61cc3d2fe68de0f9ca128" +checksum = "5d9bf5e8953149d84e5bbcdbc48841b8a2fcf9790b6b5da09fe45a166e3bcc17" [[package]] name = "scopeguard" @@ -480,15 +471,29 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" dependencies = [ "serde", ] @@ -524,9 +529,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -560,14 +565,14 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "toml" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ "indexmap", "serde", @@ -578,18 +583,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ "indexmap", "serde", @@ -606,9 +611,9 @@ checksum = "44dcf002ae3b32cd25400d6df128c5babec3927cd1eb7ce813cfff20eb6c3746" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-segmentation" @@ -639,10 +644,11 @@ name = "vvs_ass" version = "0.4.0" dependencies = [ "anyhow", - "lazy_static", "log", "scc", + "serde", "thiserror", + "unicode-segmentation", "vvs_font", "vvs_procmacro", "vvs_utils", @@ -656,10 +662,11 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", - "lazy_static", "log", "scc", + "serde", "thiserror", + "toml", "vvs_font", "vvs_lua", "vvs_repl", @@ -671,7 +678,6 @@ name = "vvs_font" version = "0.4.0" dependencies = [ "ab_glyph", - "lazy_static", "log", "thiserror", "ttf-parser", @@ -684,8 +690,11 @@ dependencies = [ "log", "mlua", "paste", + "scc", + "serde", "thiserror", "toml", + "unicode-segmentation", "vvs_ass", "vvs_utils", ] @@ -696,7 +705,7 @@ version = "0.4.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -704,14 +713,19 @@ name = "vvs_repl" version = "0.4.0" dependencies = [ "log", + "mlua", "rustyline", "thiserror", - "vvs_lua", ] [[package]] name = "vvs_utils" version = "0.4.0" +dependencies = [ + "log", + "serde", + "thiserror", +] [[package]] name = "winapi" diff --git a/src/Rust/Cargo.toml b/src/Rust/Cargo.toml index ee090e74..43f31b29 100644 --- a/src/Rust/Cargo.toml +++ b/src/Rust/Cargo.toml @@ -18,13 +18,13 @@ edition = "2021" license = "MIT" [workspace.dependencies] -lazy_static = "^1" thiserror = "^1" anyhow = "^1" paste = "^1" log = "^0.4" scc = "^1.3" +unicode-segmentation = "^1" toml = { version = "^0.7", features = ["preserve_order"] } serde = { version = "^1", default-features = false, features = [ @@ -32,12 +32,20 @@ serde = { version = "^1", default-features = false, features = [ "derive", ] } +mlua = { version = "^0.8", features = [ + "luajit52", + "vendored", + "macros", + "send", +] } + [profile.release] strip = true debug = false lto = true -opt-level = "s" +opt-level = "z" codegen-units = 1 +panic = "abort" [profile.dev] debug = true diff --git a/src/Rust/README.md b/src/Rust/README.md index 766bbd0f..da141097 100644 --- a/src/Rust/README.md +++ b/src/Rust/README.md @@ -15,6 +15,8 @@ to `vvcc` by `cargo run --bin vvcc --`. # Misc +## Manpage + To get the `vvcc` manpage, just run: vvcc --manpage | man -l - @@ -23,6 +25,9 @@ To install it, you may run the following commands: mkdir -p $HOME/.local/share/man/man1/ vvcc --manpage > $HOME/.local/share/man/man1/vvcc.1 + man vvcc + +## Shell completion To get the completion scripts to source you can use the following commands. You can then source those files to get the completion working with your shell. @@ -41,7 +46,19 @@ To visualize the dependency graph of VivyScript, use the command: cargo install cargo-depgraph cargo depgraph --build-deps --dedup-transitive-deps | dot -Tpdf | zathura - +## To test the project + +The unit-tests can be ran on the project by running `cargo test`. The +integration tests and the unit-tests can be run together by using pcvs in the +[tests](tests) folder: + + pip install pcvs -- Install PCVS + (cd tests && pcvs run) -- Run in the correct folder + +You can also install PCVS by cloning the project and running `pip install .` +from its root. + # Licence - The VVS project is under the MIT licence -- The NotoSans fonts are distributed using the [OFL licence](utils/fonts/OFL.txt) +- The NotoSans fonts are distributed using the [OFL licence](vvs_font/fonts/NotoSans-LICENCE-OFL.txt) diff --git a/src/Rust/vvs_ass/Cargo.toml b/src/Rust/vvs_ass/Cargo.toml index 95e6f25c..a066523a 100644 --- a/src/Rust/vvs_ass/Cargo.toml +++ b/src/Rust/vvs_ass/Cargo.toml @@ -11,8 +11,9 @@ vvs_procmacro = { path = "../vvs_procmacro" } vvs_utils = { path = "../vvs_utils" } vvs_font = { path = "../vvs_font" } -lazy_static.workspace = true +unicode-segmentation.workspace = true thiserror.workspace = true anyhow.workspace = true +serde.workspace = true log.workspace = true scc.workspace = true diff --git a/src/Rust/vvs_ass/src/colors.rs b/src/Rust/vvs_ass/src/colors.rs index 71bfd019..380a9a1a 100644 --- a/src/Rust/vvs_ass/src/colors.rs +++ b/src/Rust/vvs_ass/src/colors.rs @@ -13,12 +13,7 @@ pub enum ASSColor { macro_rules! rgb { ($NAME: ident => $r: literal $g: literal $b: literal) => { - pub const $NAME: ASSColor = ASSColor::RGBA { - r: $r, - g: $g, - b: $b, - a: 0, - }; + pub const $NAME: ASSColor = ASSColor::RGBA { r: $r, g: $g, b: $b, a: 0 }; }; } @@ -44,6 +39,7 @@ impl ASSColor { fn skip_delimiters(str: &str) -> &str { str.trim() .trim_start_matches('#') + .trim_start_matches("&H") .trim_start_matches('&') .trim_end_matches('&') } @@ -51,22 +47,15 @@ impl ASSColor { pub fn try_from_rgba(str: impl AsRef<str>) -> Result<Self, String> { let color = Self::skip_delimiters(str.as_ref()); if !(color.len() == 6 || color.len() == 8) { - return Err(format!( - "invalid color string description: {}", - str.as_ref() - )); + return Err(format!("invalid color string description: {}", str.as_ref())); } Ok(Self::RGBA { - r: u8::from_str_radix(&color[0..2], 16) - .map_err(|err| format!("invalid red description: {err}"))?, - g: u8::from_str_radix(&color[2..4], 16) - .map_err(|err| format!("invalid green description: {err}"))?, - b: u8::from_str_radix(&color[4..6], 16) - .map_err(|err| format!("invalid blue description: {err}"))?, + r: u8::from_str_radix(&color[0..2], 16).map_err(|err| format!("invalid red description: {err}"))?, + g: u8::from_str_radix(&color[2..4], 16).map_err(|err| format!("invalid green description: {err}"))?, + b: u8::from_str_radix(&color[4..6], 16).map_err(|err| format!("invalid blue description: {err}"))?, a: (color.len() == 8) .then(|| { - u8::from_str_radix(&color[6..8], 16) - .map_err(|err| format!("invalid alpha description: {err}")) + u8::from_str_radix(&color[6..8], 16).map_err(|err| format!("invalid alpha description: {err}")) }) .unwrap_or(Ok(0))?, }) @@ -75,22 +64,15 @@ impl ASSColor { pub fn try_from_bgra(str: impl AsRef<str>) -> Result<Self, String> { let color = Self::skip_delimiters(str.as_ref()); if !(color.len() == 6 || color.len() == 8) { - return Err(format!( - "invalid color string description: {}", - str.as_ref() - )); + return Err(format!("invalid color string description: {}", str.as_ref())); } Ok(Self::RGBA { - r: u8::from_str_radix(&color[4..6], 16) - .map_err(|err| format!("invalid red description: {err}"))?, - g: u8::from_str_radix(&color[2..4], 16) - .map_err(|err| format!("invalid green description: {err}"))?, - b: u8::from_str_radix(&color[0..2], 16) - .map_err(|err| format!("invalid blue description: {err}"))?, + r: u8::from_str_radix(&color[4..6], 16).map_err(|err| format!("invalid red description: {err}"))?, + g: u8::from_str_radix(&color[2..4], 16).map_err(|err| format!("invalid green description: {err}"))?, + b: u8::from_str_radix(&color[0..2], 16).map_err(|err| format!("invalid blue description: {err}"))?, a: (color.len() == 8) .then(|| { - u8::from_str_radix(&color[6..8], 16) - .map_err(|err| format!("invalid alpha description: {err}")) + u8::from_str_radix(&color[6..8], 16).map_err(|err| format!("invalid alpha description: {err}")) }) .unwrap_or(Ok(0))?, }) diff --git a/src/Rust/vvs_ass/src/definitions.rs b/src/Rust/vvs_ass/src/definitions.rs index e8b5689f..dcd1b201 100644 --- a/src/Rust/vvs_ass/src/definitions.rs +++ b/src/Rust/vvs_ass/src/definitions.rs @@ -160,6 +160,12 @@ pub enum ScriptInfoKey { /// - 2: no word wrapping, \n \N both breaks /// - 3: same as 0, but lower line gets wider. WrapStyle, + + /// Undocumented, generated by Aegisub. + ScaledBorderAndShadow, + + /// Undocumented, generated by Aegisub. + YCbCrMatrix, } impl FromStr for ScriptInfoKey { @@ -169,20 +175,22 @@ impl FromStr for ScriptInfoKey { use ScriptInfoKey::*; match s.trim() { "Title" => Ok(Title), - "Original Script" => Ok(OriginalScript), - "Original Translation" => Ok(OriginalTranslation), - "Original Editing" => Ok(OriginalEditing), - "Original Timing" => Ok(OriginalTiming), - "Synch Point" => Ok(SynchPoint), - "Script Updated By" => Ok(ScriptUpdatedBy), - "Update Details" => Ok(UpdateDetails), - "Script Type" => Ok(ScriptType), + "OriginalTranslation" | "Original Translation" => Ok(OriginalTranslation), + "OriginalEditing" | "Original Editing" => Ok(OriginalEditing), + "ScriptUpdatedBy" | "Script Updated By" => Ok(ScriptUpdatedBy), + "OriginalTiming" | "Original Timing" => Ok(OriginalTiming), + "OriginalScript" | "Original Script" => Ok(OriginalScript), + "UpdateDetails" | "Update Details" => Ok(UpdateDetails), + "YCbCrMatrix" | "YCbCr Matrix" => Ok(YCbCrMatrix), + "SynchPoint" | "Synch Point" => Ok(SynchPoint), + "ScriptType" | "Script Type" => Ok(ScriptType), "Collisions" => Ok(Collisions), "PlayResY" => Ok(PlayResY), "PlayResX" => Ok(PlayResX), "PlayDepth" => Ok(PlayDepth), "Timer" => Ok(Timer), "WrapStyle" => Ok(WrapStyle), + "ScaledBorderAndShadow" => Ok(ScaledBorderAndShadow), _ => Err(format!("unknown Script Info key: {s}")), } } @@ -194,11 +202,11 @@ impl FromStr for ASSFileSection { fn from_str(s: &str) -> Result<Self, Self::Err> { use ASSFileSection::*; const TRIM_PAT: &[char] = &['[', ']', ' ', '\t']; - match s.trim_matches(TRIM_PAT) { - "Script Info" => Ok(ScriptInfo), - "Events" => Ok(Events), - "v4 Styles" | "v4 Styles+" => Ok(V4Styles), - _ => Err(format!("unknown section [{s}]")), + match s.trim_matches(TRIM_PAT).to_lowercase().as_str() { + "script info" => Ok(ScriptInfo), + "events" => Ok(Events), + "v4+ styles" | "v4 styles" | "v4 styles+" => Ok(V4Styles), + s => Err(format!("unknown section [{s}]")), } } } diff --git a/src/Rust/vvs_ass/src/drawing.rs b/src/Rust/vvs_ass/src/drawing.rs index a3c9a997..c2d2a882 100644 --- a/src/Rust/vvs_ass/src/drawing.rs +++ b/src/Rust/vvs_ass/src/drawing.rs @@ -125,34 +125,11 @@ impl std::fmt::Display for ASSDrawingCmd { ASSDrawingCmd::M { x, y } => write!(f, "m {x} {y}"), ASSDrawingCmd::N { x, y } => write!(f, "n {x} {y}"), ASSDrawingCmd::L { x, y } => write!(f, "l {x} {y}"), - ASSDrawingCmd::B { - x1, - y1, - x2, - y2, - x3, - y3, - } => write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3}"), - ASSDrawingCmd::S { - x1, - y1, - x2, - y2, - x3, - y3, - others, - } if others.is_empty() => { + ASSDrawingCmd::B { x1, y1, x2, y2, x3, y3 } => write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3}"), + ASSDrawingCmd::S { x1, y1, x2, y2, x3, y3, others } if others.is_empty() => { write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3}") } - ASSDrawingCmd::S { - x1, - y1, - x2, - y2, - x3, - y3, - others, - } => { + ASSDrawingCmd::S { x1, y1, x2, y2, x3, y3, others } => { let others = others .iter() .map(|(x, y)| format!("{x} {y}")) diff --git a/src/Rust/vvs_ass/src/elements/line.rs b/src/Rust/vvs_ass/src/elements/line.rs index f58e003d..63df87f0 100644 --- a/src/Rust/vvs_ass/src/elements/line.rs +++ b/src/Rust/vvs_ass/src/elements/line.rs @@ -1,14 +1,78 @@ -use crate::{elements::syllabes::ASSSyllabesPtr, ASSAuxTablePtr, ASSPositionPtr}; +use crate::{ASSAuxTable, ASSPosition, ASSSyllabePtr}; #[derive(Debug, Default, Clone, PartialEq)] pub struct ASSLine { - pub position: ASSPositionPtr, - pub content: ASSSyllabesPtr, - pub aux: ASSAuxTablePtr, + pub position: ASSPosition, + pub content: Vec<ASSSyllabePtr>, + pub aux: ASSAuxTable, + pub start: i64, + pub fini: i64, } -pub type ASSLinePtr = crate::Ptr<ASSLine>; +#[derive(Debug, Default, Clone)] +#[repr(transparent)] +pub struct ASSLinePtr(pub crate::Ptr<ASSLine>); -impl ASSLine { - crate::impl_into_ptr! { ASSLinePtr } +#[derive(Debug, Default, Clone)] +#[repr(transparent)] +pub struct ASSLines(pub Vec<ASSLinePtr>); + +impl PartialEq for ASSLinePtr { + fn eq(&self, other: &Self) -> bool { + *self.0.try_read().unwrap() == *other.0.try_read().unwrap() + } +} + +impl PartialEq for ASSLines { + fn eq(&self, Self(other): &Self) -> bool { + let Self(this) = self; + (this.len() != other.len()) && { + this.iter() + .zip(other.iter()) + .fold(true, |acc, (ASSLinePtr(this), ASSLinePtr(other))| { + acc && (*this.try_read().unwrap() == *other.try_read().unwrap()) + }) + } + } +} + +impl From<ASSLine> for ASSLinePtr { + fn from(value: ASSLine) -> Self { + ASSLinePtr(crate::ptr!(value)) + } +} + +impl ASSLines { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&mut self, value: impl Into<ASSLinePtr>) { + self.0.push(value.into()) + } +} + +impl Extend<ASSLinePtr> for ASSLines { + fn extend<T: IntoIterator<Item = ASSLinePtr>>(&mut self, iter: T) { + self.0.extend(iter) + } +} + +impl Extend<ASSLine> for ASSLines { + fn extend<T: IntoIterator<Item = ASSLine>>(&mut self, iter: T) { + self.extend(iter.into_iter().map(|line| ASSLinePtr(crate::ptr!(line)))) + } +} + +impl IntoIterator for ASSLines { + type Item = ASSLinePtr; + type IntoIter = <Vec<ASSLinePtr> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } } diff --git a/src/Rust/vvs_ass/src/elements/lines.rs b/src/Rust/vvs_ass/src/elements/lines.rs deleted file mode 100644 index cdd10df9..00000000 --- a/src/Rust/vvs_ass/src/elements/lines.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::{elements::line::ASSLinePtr, ASSAuxTablePtr, ASSPositionPtr}; - -#[derive(Debug, Default, Clone, PartialEq)] -pub struct ASSLines { - pub position: ASSPositionPtr, - pub content: Vec<ASSLinePtr>, - pub aux: ASSAuxTablePtr, -} - -pub type ASSLinesPtr = crate::Ptr<ASSLines>; - -impl ASSLines { - crate::impl_into_ptr! { ASSLinesPtr } -} diff --git a/src/Rust/vvs_ass/src/elements/mod.rs b/src/Rust/vvs_ass/src/elements/mod.rs index a411fa4c..2de364b6 100644 --- a/src/Rust/vvs_ass/src/elements/mod.rs +++ b/src/Rust/vvs_ass/src/elements/mod.rs @@ -1,35 +1,39 @@ mod line; -mod lines; mod syllabe; -mod syllabes; -pub use self::{line::*, lines::*, syllabe::*, syllabes::*}; +pub use self::{line::*, syllabe::*}; use crate::{definitions::ScriptInfoKey, ASSStyle}; use std::collections::HashMap; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct ASSContainer { - pub lines: ASSLinesPtr, + pub lines: ASSLines, pub script_info: HashMap<ScriptInfoKey, String>, pub styles: HashMap<String, ASSStyle>, } -pub type ASSContainerPtr = crate::Ptr<ASSContainer>; +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct ASSContainerPtr(pub crate::Ptr<ASSContainer>); impl ASSContainer { - crate::impl_into_ptr! { ASSContainerPtr } - /// Create an ASS container from its parts, they must be valide! pub(crate) fn from_parts( - lines: ASSLinesPtr, + lines: impl IntoIterator<Item = ASSLine>, script_info: HashMap<ScriptInfoKey, String>, styles: HashMap<String, ASSStyle>, ) -> Self { Self { - lines, + lines: ASSLines(lines.into_iter().map(|line| ASSLinePtr(crate::ptr!(line))).collect()), script_info, styles, } } } + +impl From<ASSContainer> for ASSContainerPtr { + fn from(value: ASSContainer) -> Self { + Self(crate::ptr!(value)) + } +} diff --git a/src/Rust/vvs_ass/src/elements/syllabe.rs b/src/Rust/vvs_ass/src/elements/syllabe.rs index 00fa4633..4c098045 100644 --- a/src/Rust/vvs_ass/src/elements/syllabe.rs +++ b/src/Rust/vvs_ass/src/elements/syllabe.rs @@ -1,14 +1,78 @@ -use crate::{ASSAuxTablePtr, ASSPositionPtr}; +use crate::{ASSAuxTable, ASSPosition}; #[derive(Debug, Default, Clone, PartialEq)] pub struct ASSSyllabe { - pub position: ASSPositionPtr, + pub position: ASSPosition, pub content: String, - pub aux: ASSAuxTablePtr, + pub aux: ASSAuxTable, + pub start: i64, + pub fini: i64, } -pub type ASSSyllabePtr = crate::Ptr<ASSSyllabe>; +#[derive(Debug, Default, Clone)] +#[repr(transparent)] +pub struct ASSSyllabePtr(pub crate::Ptr<ASSSyllabe>); -impl ASSSyllabe { - crate::impl_into_ptr! { ASSSyllabePtr } +#[derive(Debug, Default, Clone)] +#[repr(transparent)] +pub struct ASSSyllabes(pub Vec<ASSSyllabePtr>); + +impl PartialEq for ASSSyllabePtr { + fn eq(&self, other: &Self) -> bool { + *self.0.try_read().unwrap() == *other.0.try_read().unwrap() + } +} + +impl PartialEq for ASSSyllabes { + fn eq(&self, Self(other): &Self) -> bool { + let Self(this) = self; + (this.len() != other.len()) && { + this.iter() + .zip(other.iter()) + .fold(true, |acc, (ASSSyllabePtr(this), ASSSyllabePtr(other))| { + acc && (*this.try_read().unwrap() == *other.try_read().unwrap()) + }) + } + } +} + +impl From<ASSSyllabe> for ASSSyllabePtr { + fn from(value: ASSSyllabe) -> Self { + ASSSyllabePtr(crate::ptr!(value)) + } +} + +impl ASSSyllabes { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&mut self, value: impl Into<ASSSyllabePtr>) { + self.0.push(value.into()) + } +} + +impl Extend<ASSSyllabePtr> for ASSSyllabes { + fn extend<T: IntoIterator<Item = ASSSyllabePtr>>(&mut self, iter: T) { + self.0.extend(iter) + } +} + +impl Extend<ASSSyllabe> for ASSSyllabes { + fn extend<T: IntoIterator<Item = ASSSyllabe>>(&mut self, iter: T) { + self.extend(iter.into_iter().map(|syllabe| ASSSyllabePtr(crate::ptr!(syllabe)))) + } +} + +impl IntoIterator for ASSSyllabes { + type Item = ASSSyllabePtr; + type IntoIter = <Vec<ASSSyllabePtr> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } } diff --git a/src/Rust/vvs_ass/src/elements/syllabes.rs b/src/Rust/vvs_ass/src/elements/syllabes.rs deleted file mode 100644 index 8a8143d9..00000000 --- a/src/Rust/vvs_ass/src/elements/syllabes.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::{elements::syllabe::ASSSyllabePtr, ASSAuxTablePtr, ASSPositionPtr}; - -#[derive(Debug, Default, Clone, PartialEq)] -pub struct ASSSyllabes { - pub position: ASSPositionPtr, - pub content: Vec<ASSSyllabePtr>, - pub aux: ASSAuxTablePtr, -} - -pub type ASSSyllabesPtr = crate::Ptr<ASSSyllabes>; - -impl ASSSyllabes { - crate::impl_into_ptr! { ASSSyllabesPtr } -} diff --git a/src/Rust/vvs_ass/src/lib.rs b/src/Rust/vvs_ass/src/lib.rs index 0e1a05ae..eba3a485 100644 --- a/src/Rust/vvs_ass/src/lib.rs +++ b/src/Rust/vvs_ass/src/lib.rs @@ -1,4 +1,5 @@ //! ASS objects for Vivy. +#![forbid(unsafe_code)] mod colors; mod definitions; @@ -14,23 +15,13 @@ mod values; mod tests; pub use crate::{colors::*, drawing::*, elements::*, position::*, styles::*, types::*, values::*}; -pub use reader::{ass_lines_from_file, ASSElementReaderError}; +pub use reader::{ass_container_from_file, ass_container_from_str, ASSElementReaderError, ContainerFileType}; -pub type Ptr<T> = std::rc::Rc<std::cell::RefCell<T>>; +pub type Ptr<T> = std::sync::Arc<std::sync::RwLock<T>>; #[macro_export] macro_rules! ptr { ($expr: expr) => { - std::rc::Rc::new(std::cell::RefCell::new($expr)) + std::sync::Arc::new(std::sync::RwLock::new($expr)) }; } - -macro_rules! impl_into_ptr { - ($PTR: ident) => { - /// Wrap the struct into a pointer. - pub fn into_ptr(self) -> $PTR { - crate::ptr!(self) - } - }; -} -pub(crate) use impl_into_ptr; diff --git a/src/Rust/vvs_ass/src/position.rs b/src/Rust/vvs_ass/src/position.rs index a75eb9e6..bc335d23 100644 --- a/src/Rust/vvs_ass/src/position.rs +++ b/src/Rust/vvs_ass/src/position.rs @@ -13,14 +13,7 @@ pub enum ASSPosition { LinearMove { x1: i64, y1: i64, x2: i64, y2: i64 }, /// A linear movement with time step specified. - TimedMove { - x1: i64, - y1: i64, - x2: i64, - y2: i64, - from_ms: i64, - to_ms: i64, - }, + TimedMove { x1: i64, y1: i64, x2: i64, y2: i64, from_ms: i64, to_ms: i64 }, } /// The alignement of the object. @@ -55,7 +48,7 @@ pub enum ASSAlign { } /// Pointer used to store a position, to help with mutability with LUA wrappers. -pub type ASSPositionPtr = crate::Ptr<ASSPosition>; +pub type ASSPositionPtr = std::sync::Arc<ASSPosition>; impl std::str::FromStr for ASSAlign { type Err = String; @@ -71,9 +64,7 @@ impl std::str::FromStr for ASSAlign { "7" => Ok(ASSAlign::TL), "8" => Ok(ASSAlign::TC), "9" => Ok(ASSAlign::TR), - s => Err(format!( - "invalid value for ASSAlign, must be in `1..=9`, got: {s}" - )), + s => Err(format!("invalid value for ASSAlign, must be in `1..=9`, got: {s}")), } } } @@ -85,5 +76,7 @@ impl From<vvs_font::Point> for ASSPosition { } impl ASSPosition { - crate::impl_into_ptr! { ASSPositionPtr } + pub fn into_ptr(self) -> ASSPositionPtr { + std::sync::Arc::new(self) + } } diff --git a/src/Rust/vvs_ass/src/reader/ass.rs b/src/Rust/vvs_ass/src/reader/ass.rs index d024c677..c1927c02 100644 --- a/src/Rust/vvs_ass/src/reader/ass.rs +++ b/src/Rust/vvs_ass/src/reader/ass.rs @@ -1,23 +1,116 @@ use crate::{ definitions::{ASSEvent, ASSFileSection, ScriptInfoKey}, reader::ASSElementReader, - ASSColor, ASSContainer, ASSElementReaderError, ASSLines, ASSStyle, + ASSColor, ASSContainer, ASSElementReaderError, ASSLine, ASSStyle, }; use std::collections::HashMap; +use vvs_procmacro::EnumVariantFromStr; + +/// Line fields of the `V4+ Styles` line in the ASS file. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumVariantFromStr)] +enum V4PlusStyleFields { + Name, + Fontname, + Fontsize, + PrimaryColour, + SecondaryColour, + OutlineColour, + BackColour, + Bold, + Italic, + Underline, + StrikeOut, + ScaleX, + ScaleY, + Spacing, + Angle, + BorderStyle, + Outline, + Shadow, + Alignment, + MarginL, + MarginR, + MarginV, + Encoding, +} + +/// Line fields of the `V4+ Styles` line in the ASS file. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumVariantFromStr)] +enum EventFields { + Marked, + Layer, + Start, + End, + Style, + Name, + MarginL, + MarginR, + MarginV, + Effect, + Text, +} + +/// Helper wrapper struct to auto unwrap things when we get entries from the HashMap. +#[derive(Debug)] +struct UnwrapHashMap<'a, K>(HashMap<K, &'a str>) +where + K: Eq + Copy + std::hash::Hash + std::fmt::Debug; + +impl<'a, K: Eq + Copy + std::hash::Hash + std::fmt::Debug> UnwrapHashMap<'a, K> { + /// Get the element from the hashmap, if the element is not present just panics. + pub fn get(&self, key: K) -> &str { + self.0.get(&key).unwrap_or_else(|| panic!("failed to find key {key:?}")) + } + + /// Try to get the element from the hashmap, if the element is not present, returns the + /// specified default value instead. + pub fn get_or(&self, key: K, default: &'a str) -> &str { + self.0.get(&key).copied().unwrap_or(default) + } + + /// Try to get the element from the hashmap, if the element is present then tries to parse it. + pub fn parsed<T>( + &self, + key: K, + parser: fn(&str, name: &str) -> Result<T, ASSElementReaderError>, + name: &str, + ) -> Result<T, ASSElementReaderError> { + let Self(hashmap) = self; + let element = hashmap.get(&key).ok_or(ASSElementReaderError::Custom(format!( + "failed to find entry with key {key:?}" + )))?; + parser(element, name) + } + + /// Try to get the element from the hashmap, if the element is present then tries to parse it. + /// If the element is not present or the parsing failed, returns the default value. + pub fn parsed_or<T>( + &self, + key: K, + parser: fn(&str, &str) -> Result<T, ASSElementReaderError>, + name: &str, + default: T, + ) -> T { + self.parsed(key, parser, name).unwrap_or(default) + } +} /// Documentation available here: http://www.tcax.org/docs/ass-specs.html or in the `utils/manual` /// folder from the root of the vvs project. #[derive(Debug, Default)] pub struct ASSReader { section: Option<ASSFileSection>, + + styles_format: Vec<V4PlusStyleFields>, + events_format: Vec<EventFields>, + script_info: HashMap<ScriptInfoKey, String>, styles: HashMap<String, ASSStyle>, events: Vec<ASSEvent>, } fn parse_color(color: &str, name: &str) -> Result<ASSColor, ASSElementReaderError> { - ASSColor::try_from_bgra(color) - .map_err(|err| ASSElementReaderError::Custom(format!("invalid {name} color: {err}"))) + ASSColor::try_from_bgra(color).map_err(|err| ASSElementReaderError::Custom(format!("invalid {name} color: {err}"))) } fn parse_boolean(boolean: &str, name: &str) -> Result<bool, ASSElementReaderError> { @@ -31,34 +124,32 @@ fn parse_boolean(boolean: &str, name: &str) -> Result<bool, ASSElementReaderErro } fn parse_float(float: &str, name: &str) -> Result<f64, ASSElementReaderError> { - float.parse::<f64>().map_err(|err| { - ASSElementReaderError::Custom(format!("invalid float value for {name}: {err}")) - }) + float + .parse::<f64>() + .map_err(|err| ASSElementReaderError::Custom(format!("invalid float value for {name}: {err}"))) } fn parse_int(int: &str, name: &str) -> Result<i64, ASSElementReaderError> { - int.parse::<i64>().map_err(|err| { - ASSElementReaderError::Custom(format!("invalid integer value for {name}: {err}")) - }) + int.parse::<i64>() + .map_err(|err| ASSElementReaderError::Custom(format!("invalid integer value for {name}: {err}"))) } /// Parse dates in the `0:00:00:00` format fn parse_date(date: &str, name: &str) -> Result<i64, ASSElementReaderError> { - let [h, m, s, c] = &date.split(':').collect::<Vec<_>>()[..] else { - return Err(ASSElementReaderError::Custom(format!("invalid date for {name}: {date}"))) + let std_err = "invalid date for {name}, expected \"h:mm:ss.cc\", got {date:?}".to_string(); + let [h, m, sc] = &date.split(':').collect::<Vec<_>>()[..] else { + return Err(ASSElementReaderError::Custom(std_err)) + }; + let [s, c] = &sc.split('.').collect::<Vec<_>>()[..] else { + return Err(ASSElementReaderError::Custom(std_err)) }; let check_compnent = |str: &str, compnent: &str, len: usize| { if str.len() > len { - Err(ASSElementReaderError::Custom(format!( - "invalid date for {name}: {date}" - ))) + Err(ASSElementReaderError::Custom(std_err.clone())) } else { - let ret = str.parse::<u16>().map_err(|err| { - ASSElementReaderError::Custom(format!( - "invalid component {compnent} for date {name}: {err}" - )) - })?; - Ok(ret as i64) + Ok(str.parse::<u16>().map_err(|err| { + ASSElementReaderError::Custom(format!("invalid component {compnent} for date {name}: {err}")) + })? as i64) } }; let (h, m, s, c) = ( @@ -75,12 +166,13 @@ impl ASSReader { let Some((key, value)) = line.split_once(':') else { return Err(ASSElementReaderError::Custom(format!("invalid script info line: {line}"))) }; + let value = value.trim(); let key = match key .trim() .parse::<ScriptInfoKey>() .map_err(ASSElementReaderError::Custom)? { - ScriptInfoKey::ScriptType if value.ne("V4.00+") => { + ScriptInfoKey::ScriptType if value.ne("V4.00+") && value.ne("v4.00+") => { return Err(ASSElementReaderError::Custom(format!( "invalid value for key '{key:?}' in script info section: {value}" ))) @@ -97,49 +189,72 @@ impl ASSReader { "redefinition of key '{key:?}' in script info section" ))), None => { - self.script_info.insert(key, value.trim().to_string()); + self.script_info.insert(key, value.to_string()); Ok(()) } } } fn read_v4_style(&mut self, line: &str) -> Result<(), ASSElementReaderError> { - let fields: Vec<_> = line.trim().split(',').map(|str| str.trim()).collect(); - let [_, name, font_name, font_size, primary_color, secondary_color, outline_color, back_color, bold, italic, underline, strikeout, scalex, scaley, spacing, angle, border_style, outline, shadow, alignment, marginl, marginr, marginv, encoding] = &fields[..] else { - return Err(ASSElementReaderError::Custom(format!("invalid line composed of fields: {fields:#?}"))) + let line = if line.starts_with("Format:") { + let line = line.split_once(':').unwrap().1.trim(); + self.styles_format = line + .split(',') + .flat_map(|str| str.trim().parse::<V4PlusStyleFields>()) + .collect(); + return Ok(()); + } else { + line.split_once(':').unwrap().1.trim() }; - if encoding.ne(&"0") { + let fields: Vec<_> = line.trim().split(',').map(|str| str.trim()).collect(); + if fields.len() != self.styles_format.len() { return Err(ASSElementReaderError::Custom(format!( - "we expected the encoding '0', got: {encoding}" + "style format specified {} fields, got {} in the style description", + self.styles_format.len(), + fields.len() ))); } + let fields: UnwrapHashMap<V4PlusStyleFields> = + UnwrapHashMap(HashMap::from_iter(self.styles_format.iter().copied().zip(fields))); + let encoding = fields.get(V4PlusStyleFields::Encoding); + if encoding.ne("1") { + return Err(ASSElementReaderError::Custom(format!( + "we expected the encoding '1', got: {encoding}" + ))); + } + + let name = fields.get(V4PlusStyleFields::Name); let style = ASSStyle { name: name.to_string(), - font_name: font_name.to_string(), - font_size: parse_int(font_size, "font size")?, - primary_color: parse_color(primary_color, "primary")?, - secondary_color: parse_color(secondary_color, "secondary")?, - outline_color: parse_color(outline_color, "outline")?, - back_color: parse_color(back_color, "back")?, - bold: parse_boolean(bold, "bold")?, - italic: parse_boolean(italic, "italic")?, - underline: parse_boolean(underline, "underline")?, - strikeout: parse_boolean(strikeout, "strikeout")?, - scale_x: parse_float(scalex, "scale_x")?, - scale_y: parse_float(scaley, "scale_y")?, - spacing: parse_float(spacing, "spacing")?, - angle: parse_float(angle, "angle")?, - border_style: border_style + font_name: fields.get(V4PlusStyleFields::Fontname).to_string(), + font_size: fields.parsed(V4PlusStyleFields::Fontsize, parse_int, "font size")?, + primary_color: fields.parsed(V4PlusStyleFields::PrimaryColour, parse_color, "primary")?, + secondary_color: fields.parsed(V4PlusStyleFields::SecondaryColour, parse_color, "secondary")?, + outline_color: fields.parsed(V4PlusStyleFields::OutlineColour, parse_color, "outline")?, + back_color: fields.parsed(V4PlusStyleFields::BackColour, parse_color, "back")?, + bold: fields.parsed(V4PlusStyleFields::Bold, parse_boolean, "bold")?, + italic: fields.parsed(V4PlusStyleFields::Italic, parse_boolean, "italic")?, + underline: fields.parsed(V4PlusStyleFields::Underline, parse_boolean, "underline")?, + strikeout: fields.parsed(V4PlusStyleFields::StrikeOut, parse_boolean, "strikeout")?, + scale_x: fields.parsed(V4PlusStyleFields::ScaleX, parse_float, "scale_x")?, + scale_y: fields.parsed(V4PlusStyleFields::ScaleY, parse_float, "scale_y")?, + spacing: fields.parsed(V4PlusStyleFields::Spacing, parse_float, "spacing")?, + angle: fields.parsed(V4PlusStyleFields::Angle, parse_float, "angle")?, + border_style: fields + .get(V4PlusStyleFields::BorderStyle) + .parse() + .map_err(ASSElementReaderError::Custom)?, + outline: fields.parsed(V4PlusStyleFields::Outline, parse_float, "outline")?, + shadow: fields.parsed(V4PlusStyleFields::Shadow, parse_float, "shadow")?, + alignment: fields + .get(V4PlusStyleFields::Alignment) .parse() .map_err(ASSElementReaderError::Custom)?, - outline: parse_float(outline, "outline")?, - shadow: parse_float(shadow, "shadow")?, - alignment: alignment.parse().map_err(ASSElementReaderError::Custom)?, - margin_l: parse_int(marginl, "margin_l")?, - margin_r: parse_int(marginr, "margin_r")?, - margin_v: parse_int(marginv, "margin_v")?, + margin_l: fields.parsed(V4PlusStyleFields::MarginL, parse_int, "margin_l")?, + margin_r: fields.parsed(V4PlusStyleFields::MarginR, parse_int, "margin_r")?, + margin_v: fields.parsed(V4PlusStyleFields::MarginV, parse_int, "margin_v")?, }; match self.styles.insert(name.to_string(), style) { @@ -154,30 +269,54 @@ impl ASSReader { } fn read_event(&mut self, line: &str) -> Result<(), ASSElementReaderError> { - let fields: Vec<_> = line.trim().splitn(8, ',').map(|str| str.trim()).collect(); - let [marked, layer, start, end, style, name, effect, text] = &fields[..] else { - return Err(ASSElementReaderError::Custom(format!("invalid line composed of fields: {fields:#?}"))) + let line = if line.starts_with("Format:") { + let line = line.split_once(':').unwrap().1.trim(); + self.events_format = line + .split(',') + .flat_map(|str| str.trim().parse::<EventFields>()) + .collect(); + return match self.events_format.last() { + Some(EventFields::Text) => Ok(()), + _ => Err(ASSElementReaderError::Custom(format!( + "invalid format line: {:?}", + self.events_format + ))), + }; + } else { + line.split_once(':').unwrap().1.trim() }; + let fields = line.splitn(self.events_format.len(), ',').collect::<Vec<_>>(); + if fields.len() != self.events_format.len() { + return Err(ASSElementReaderError::Custom(format!( + "style format specified {} fields, got {} in the style description", + self.events_format.len(), + fields.len() + ))); + } + let fields: UnwrapHashMap<EventFields> = UnwrapHashMap(HashMap::from_iter( + self.events_format + .iter() + .copied() + .zip(fields.into_iter().map(|s| s.trim())), + )); self.events.push(ASSEvent { - marked: parse_boolean(marked, "marked")?, - layer: parse_int(layer, "layer")?, - start: parse_date(start, "start")?, - end: parse_date(end, "end")?, - style: style.to_string(), - name: name.to_string(), - effect: effect.to_string(), - text: text.to_string(), + marked: fields.parsed_or(EventFields::Marked, parse_boolean, "marked", false), + layer: fields.parsed_or(EventFields::Layer, parse_int, "layer", 0), + start: fields.parsed(EventFields::Start, parse_date, "start")?, + end: fields.parsed(EventFields::End, parse_date, "end")?, + style: fields.get_or(EventFields::Style, "Default").to_string(), + name: fields.get_or(EventFields::Name, "").to_string(), + effect: fields.get_or(EventFields::Effect, "").to_string(), + text: fields.get(EventFields::Text).to_string(), }); Ok(()) } } impl ASSElementReader for ASSReader { - fn try_read( - mut self, - file: impl std::io::BufRead, - ) -> Result<crate::ASSContainerPtr, ASSElementReaderError> { + fn try_read(mut self, file: impl std::io::BufRead) -> Result<ASSContainer, ASSElementReaderError> { // Parse the file + let mut skip_that_section = false; for line in file.lines() { let line = line.map_err(ASSElementReaderError::FailedToReadLine)?; let line = line.trim(); @@ -185,11 +324,16 @@ impl ASSElementReader for ASSReader { if line.is_empty() || line.starts_with(';') { continue; } else if line.starts_with('[') && line.ends_with(']') { - self.section = Some( - line.parse::<ASSFileSection>() - .map_err(ASSElementReaderError::Custom)?, - ); - } else { + skip_that_section = false; + self.section = match line.parse::<ASSFileSection>() { + Ok(section) => Some(section), + Err(err) => { + log::error!(target: "ass", "{err}"); + skip_that_section = true; + continue; + } + }; + } else if !skip_that_section { match self.section { Some(ASSFileSection::ScriptInfo) => self.read_script_info(line)?, Some(ASSFileSection::V4Styles) => self.read_v4_style(line)?, @@ -206,8 +350,7 @@ impl ASSElementReader for ASSReader { // Verify integrity + order events self.events.sort_by(|a, b| Ord::cmp(&a.start, &b.end)); if !self.styles.contains_key(ASSStyle::default_name()) { - self.styles - .insert(ASSStyle::default_name_string(), ASSStyle::default()); + self.styles.insert(ASSStyle::default_name_string(), ASSStyle::default()); } for ASSEvent { style, .. } in &mut self.events { if !self.styles.contains_key(style) { @@ -215,11 +358,13 @@ impl ASSElementReader for ASSReader { } } - Ok(ASSContainer::from_parts( - ASSLines::default().into_ptr(), - self.script_info, - self.styles, - ) - .into_ptr()) + // Convert events into lines... + let mut lines = Vec::with_capacity(self.events.len()); + for event in self.events { + let ASSEvent { start, end, .. } = event; + lines.push(ASSLine { start, fini: end, ..Default::default() }); + } + + Ok(ASSContainer::from_parts(lines, self.script_info, self.styles)) } } diff --git a/src/Rust/vvs_ass/src/reader/json.rs b/src/Rust/vvs_ass/src/reader/json.rs index e972d937..b6df2266 100644 --- a/src/Rust/vvs_ass/src/reader/json.rs +++ b/src/Rust/vvs_ass/src/reader/json.rs @@ -1,4 +1,4 @@ -use crate::{reader::ASSElementReader, ASSElementReaderError}; +use crate::{reader::ASSElementReader, ASSContainer, ASSElementReaderError}; /// Documentation available here: http://www.tcax.org/docs/ass-specs.html or in the `utils/manual` /// folder. @@ -6,10 +6,7 @@ use crate::{reader::ASSElementReader, ASSElementReaderError}; pub struct JSONReader {} impl ASSElementReader for JSONReader { - fn try_read( - self, - _file: impl std::io::BufRead, - ) -> Result<crate::ASSContainerPtr, ASSElementReaderError> { + fn try_read(self, _file: impl std::io::BufRead) -> Result<ASSContainer, ASSElementReaderError> { todo!() } } diff --git a/src/Rust/vvs_ass/src/reader/mod.rs b/src/Rust/vvs_ass/src/reader/mod.rs index fb96abcb..70f904a9 100644 --- a/src/Rust/vvs_ass/src/reader/mod.rs +++ b/src/Rust/vvs_ass/src/reader/mod.rs @@ -1,17 +1,25 @@ //! Read the content of an ASS file / a Vivy subtitle file and creates an //! [vvs_ass::elements::lines::ASSLinesPtr] structure accordingly. -use crate::ASSContainerPtr; +use crate::ASSContainer; use std::{ fs::File, io::{BufReader, Error as IoError}, path::{Path, PathBuf}, }; use thiserror::Error; +use vvs_procmacro::EnumVariantFromStr; mod ass; mod json; +#[derive(Debug, EnumVariantFromStr)] +pub enum ContainerFileType { + ASS, + VVSB, + Json, +} + #[derive(Debug, Error)] pub enum ASSElementReaderError { #[error("file has no extension: {0}")] @@ -31,28 +39,32 @@ pub enum ASSElementReaderError { } trait ASSElementReader { - fn try_read( - self, - file: impl std::io::BufRead, - ) -> Result<ASSContainerPtr, ASSElementReaderError>; + fn try_read(self, file: impl std::io::BufRead) -> Result<ASSContainer, ASSElementReaderError>; } -pub fn ass_lines_from_file( - file: impl AsRef<Path>, -) -> Result<ASSContainerPtr, ASSElementReaderError> { +pub fn ass_container_from_file(file: impl AsRef<Path>) -> Result<ASSContainer, ASSElementReaderError> { let file = file.as_ref(); let Some(extension) = file.extension() else { return Err(ASSElementReaderError::NoExtension(file.to_path_buf())); }; let content = BufReader::new( - File::open(file) - .map_err(|err| ASSElementReaderError::FailedToOpenFile(file.to_path_buf(), err))?, + File::open(file).map_err(|err| ASSElementReaderError::FailedToOpenFile(file.to_path_buf(), err))?, ); - match &extension.to_string_lossy()[..] { + match &extension.to_string_lossy().to_lowercase()[..] { "ass" => ass::ASSReader::default().try_read(content), "vvsb" | "json" => json::JSONReader::default().try_read(content), - extension => Err(ASSElementReaderError::UnknownExtension( - extension.to_string(), - )), + extension => Err(ASSElementReaderError::UnknownExtension(extension.to_string())), + } +} + +pub fn ass_container_from_str( + extension: ContainerFileType, + str: impl AsRef<str>, +) -> Result<ASSContainer, ASSElementReaderError> { + use ContainerFileType::*; + let content = BufReader::new(str.as_ref().as_bytes()); + match extension { + ASS => ass::ASSReader::default().try_read(content), + VVSB | Json => json::JSONReader::default().try_read(content), } } diff --git a/src/Rust/vvs_ass/src/styles.rs b/src/Rust/vvs_ass/src/styles.rs index 9b2a8ea7..81252927 100644 --- a/src/Rust/vvs_ass/src/styles.rs +++ b/src/Rust/vvs_ass/src/styles.rs @@ -105,9 +105,7 @@ impl std::str::FromStr for ASSBorderStyle { match s.trim() { "1" => Ok(ASSBorderStyle::OutlineAndDropShadow), "3" => Ok(ASSBorderStyle::OpaqueBox), - s => Err(format!( - "invalid value '{s}' for ASSBorderStyle, must be 1 or 3" - )), + s => Err(format!("invalid value '{s}' for ASSBorderStyle, must be 1 or 3")), } } } diff --git a/src/Rust/vvs_ass/src/tests.rs b/src/Rust/vvs_ass/src/tests.rs index e26c65d2..02e67175 100644 --- a/src/Rust/vvs_ass/src/tests.rs +++ b/src/Rust/vvs_ass/src/tests.rs @@ -74,4 +74,13 @@ mod color { eq! { b_target, b, "invalid convertion on blue for #{rgb}: {b_target} != {b}"} } } + + #[test] + fn test_parse_empty_ass() { + use crate::reader::ass_container_from_str; + let content = include_str!("../utils/empty.ass"); + if let Err(err) = ass_container_from_str(reader::ContainerFileType::ASS, content) { + panic!("{err}") + } + } } diff --git a/src/Rust/vvs_ass/src/types.rs b/src/Rust/vvs_ass/src/types.rs index dedd4a63..a3db2667 100644 --- a/src/Rust/vvs_ass/src/types.rs +++ b/src/Rust/vvs_ass/src/types.rs @@ -1,13 +1,10 @@ -use std::str::FromStr; -use thiserror::Error; -use vvs_procmacro::{EnumVariantCount, EnumVariantIter}; +use serde::{Deserialize, Serialize}; +use vvs_procmacro::{EnumVariantCount, EnumVariantFromStr, EnumVariantIter}; /// Represents the types of the ASS types that can be manipulated. By combining them we can create /// a tree of the ASS elements. We have: /// ```ignore /// - Lines -/// - pos: AssPosition -/// - aux: HashMap<String, AssAuxValue> /// - content: /// [0] := Line /// ... ... ... @@ -17,8 +14,6 @@ use vvs_procmacro::{EnumVariantCount, EnumVariantIter}; /// - aux: HashMap<String, AssAuxValue> /// - content: Syllabes /// - Syllabes -/// - pos: AssPosition -/// - aux: HashMap<String, AssAuxValue> /// - content: /// [0] := Syllabe /// ... ... ... @@ -31,7 +26,21 @@ use vvs_procmacro::{EnumVariantCount, EnumVariantIter}; /// /// We also derive the Ord/PartialOrd crates. The types are ordered from the Lines to the Syllabe. /// Thus if a type is greater than another one, the former must contains the latter. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumVariantCount, EnumVariantIter)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + EnumVariantCount, + EnumVariantIter, + EnumVariantFromStr, +)] pub enum ASSType { Lines = 0, Line = 1, @@ -60,13 +69,21 @@ impl ASSType { } } - /// Get the base type of the ASS type + /// Returns the base type. pub fn base_type(&self) -> Self { match self { ASSType::Lines | ASSType::Line => ASSType::Line, ASSType::Syllabes | ASSType::Syllabe => ASSType::Syllabe, } } + + /// Returns the vec type. + pub fn vec_type(&self) -> Self { + match self { + ASSType::Lines | ASSType::Line => ASSType::Lines, + ASSType::Syllabes | ASSType::Syllabe => ASSType::Syllabes, + } + } } impl AsRef<str> for ASSType { @@ -80,23 +97,3 @@ impl std::fmt::Display for ASSType { f.write_str(self.as_str()) } } - -#[derive(Debug, Error)] -pub enum ASSTypeFromStrError { - #[error("unknown ass structure type '{0}', expected: lines/line/syllabe/syllabes")] - Unknown(String), -} - -impl FromStr for ASSType { - type Err = ASSTypeFromStrError; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "line" => Ok(Self::Line), - "lines" => Ok(Self::Lines), - "syllabe" => Ok(Self::Syllabe), - "syllabes" => Ok(Self::Syllabes), - _ => Err(ASSTypeFromStrError::Unknown(s.to_string())), - } - } -} diff --git a/src/Rust/vvs_ass/src/values.rs b/src/Rust/vvs_ass/src/values.rs index 1dce8df8..32a16819 100644 --- a/src/Rust/vvs_ass/src/values.rs +++ b/src/Rust/vvs_ass/src/values.rs @@ -1,7 +1,8 @@ +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, convert::TryFrom}; /// The values that can be added to an ASS element. -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq, Serialize, Deserialize)] pub enum ASSAuxValue { Integer(i64), Floating(f64), @@ -13,14 +14,22 @@ pub enum ASSAuxValue { #[derive(Debug, Default, Clone, PartialEq)] pub struct ASSAuxTable(HashMap<String, ASSAuxValue>); -/// A pointer type for the [ASSAuxTable] type, for easy wrapping -pub type ASSAuxTablePtr = crate::Ptr<ASSAuxTable>; +impl std::fmt::Debug for ASSAuxValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Integer(arg0) => write!(f, "{arg0}"), + Self::Floating(arg0) => write!(f, "{arg0}"), + Self::String(arg0) => write!(f, "{arg0:?}"), + Self::Boolean(arg0) => write!(f, "{arg0}"), + } + } +} impl ASSAuxValue { pub fn type_str(&self) -> &'static str { match self { - ASSAuxValue::Integer(_) => "integer", ASSAuxValue::Floating(_) => "floating", + ASSAuxValue::Integer(_) => "integer", ASSAuxValue::Boolean(_) => "boolean", ASSAuxValue::String(_) => "string", } @@ -56,6 +65,78 @@ impl ASSAuxValue { } } +impl From<String> for ASSAuxValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From<&str> for ASSAuxValue { + fn from(value: &str) -> Self { + Self::String(value.to_string()) + } +} + +impl From<bool> for ASSAuxValue { + fn from(value: bool) -> Self { + Self::Boolean(value) + } +} + +impl From<f32> for ASSAuxValue { + fn from(value: f32) -> Self { + Self::Floating(value as f64) + } +} + +impl From<f64> for ASSAuxValue { + fn from(value: f64) -> Self { + Self::Floating(value) + } +} + +impl From<i64> for ASSAuxValue { + fn from(value: i64) -> Self { + Self::Integer(value) + } +} + +impl From<i32> for ASSAuxValue { + fn from(value: i32) -> Self { + Self::Integer(value as i64) + } +} + +impl From<u32> for ASSAuxValue { + fn from(value: u32) -> Self { + Self::Integer(value as i64) + } +} + +impl From<i16> for ASSAuxValue { + fn from(value: i16) -> Self { + Self::Integer(value as i64) + } +} + +impl From<u16> for ASSAuxValue { + fn from(value: u16) -> Self { + Self::Integer(value as i64) + } +} + +impl From<i8> for ASSAuxValue { + fn from(value: i8) -> Self { + Self::Integer(value as i64) + } +} + +impl From<u8> for ASSAuxValue { + fn from(value: u8) -> Self { + Self::Integer(value as i64) + } +} + impl std::fmt::Display for ASSAuxValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -72,10 +153,6 @@ impl ASSAuxTable { Default::default() } - pub fn into_ptr(self) -> ASSAuxTablePtr { - crate::ptr! { self } - } - pub fn set(&mut self, name: impl AsRef<str>, value: ASSAuxValue) { let name = name.as_ref(); let new = value.type_str(); @@ -107,6 +184,15 @@ impl ASSAuxTable { } } +impl IntoIterator for ASSAuxTable { + type Item = <HashMap<String, ASSAuxValue> as IntoIterator>::Item; + type IntoIter = <HashMap<String, ASSAuxValue> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + impl FromIterator<(String, ASSAuxValue)> for ASSAuxTable { fn from_iter<T: IntoIterator<Item = (String, ASSAuxValue)>>(iter: T) -> Self { Self(HashMap::from_iter(iter)) diff --git a/src/Rust/vvs_ass/utils/empty.ass b/src/Rust/vvs_ass/utils/empty.ass new file mode 100644 index 00000000..d57acc0b --- /dev/null +++ b/src/Rust/vvs_ass/utils/empty.ass @@ -0,0 +1,19 @@ +[Script Info] +; Script generated by Aegisub 3.3.3 +; http://www.aegisub.org/ +Title: Default Aegisub file +ScriptType: v4.00+ +WrapStyle: 0 +ScaledBorderAndShadow: yes +YCbCr Matrix: None + +[Aegisub Project Garbage] + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,48,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,, + diff --git a/src/Rust/vvs_cli/Cargo.toml b/src/Rust/vvs_cli/Cargo.toml index e7656212..cb68d735 100644 --- a/src/Rust/vvs_cli/Cargo.toml +++ b/src/Rust/vvs_cli/Cargo.toml @@ -16,9 +16,10 @@ vvs_repl = { path = "../vvs_repl" } vvs_font = { path = "../vvs_font" } vvs_utils = { path = "../vvs_utils" } -lazy_static.workspace = true thiserror.workspace = true anyhow.workspace = true +serde.workspace = true +toml.workspace = true log.workspace = true scc.workspace = true diff --git a/src/Rust/vvs_cli/src/args.rs b/src/Rust/vvs_cli/src/args.rs index 26fd7ad1..aefb7496 100644 --- a/src/Rust/vvs_cli/src/args.rs +++ b/src/Rust/vvs_cli/src/args.rs @@ -9,6 +9,7 @@ use std::path::PathBuf; , about , name = "vvcc" , group = clap::ArgGroup::new("action").args(["manpage", "shell", "font-file", "script.vvs"]) + , group = clap::ArgGroup::new("ass") .args(["subtitle.ass"]).conflicts_with_all(["manpage", "font-file"]) , group = clap::ArgGroup::new("repl") .args(["interactive"]) .conflicts_with_all(["shell", "manpage", "font-file"]) , group = clap::ArgGroup::new("opts") .args(["options.toml"]).conflicts_with_all(["shell", "manpage", "font-file"]) , group = clap::ArgGroup::new("infos") .args(["info"]) .conflicts_with_all(["shell", "manpage", "font-file"]) @@ -25,7 +26,19 @@ pub struct Args { )] pub script: Option<PathBuf>, - /// The option file that will be used when running the script + /// The input ASS file to execute the script on. + /// + /// The supported subtitle extensions are the ASS files (V4+) or the Json files exported by the + /// Vivy application. + #[arg ( short = 'f' + , long = "subtitle" + , action = clap::ArgAction::Set + , id = "subtitle.ass" + , value_parser = FileTypeValueParser::new("ass"), + )] + pub ass_file: Option<PathBuf>, + + /// The option file that will be used when running the script. #[arg( short = 't' , long = "option" , action = clap::ArgAction::Set @@ -34,6 +47,18 @@ pub struct Args { )] pub options: Option<PathBuf>, + /// The includes' folder list, in order. + /// + /// Will search for modules to import in those folders. Note that there is no need to add the + /// folder of the loaded script as it will automatically be added in the top position of the + /// include list. + #[arg( short = 'I' + , long = "include-path" + , action = clap::ArgAction::Append + , id = "include" + )] + pub include_folders: Vec<PathBuf>, + /// Launch vvcc REPL after loading the passed script if any. /// /// In REPL mode you must not touch to fields that begins by underscores, never. Those fields diff --git a/src/Rust/vvs_cli/src/config.rs b/src/Rust/vvs_cli/src/config.rs new file mode 100644 index 00000000..38b4d304 --- /dev/null +++ b/src/Rust/vvs_cli/src/config.rs @@ -0,0 +1,115 @@ +use crate::args::Args; +use clap_complete::Shell; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use vvs_utils::*; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigKaraMaker { + #[serde(rename = "name")] + kara_maker: String, + + email: Option<String>, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ConfigFile { + #[serde(rename = "includes")] + include_folders: Vec<PathBuf>, + + karamaker: ConfigKaraMaker, +} + +#[derive(Debug, Default)] +pub struct Config { + // From config::ConfigFile + pub kara_maker: String, + + // From args::Args + pub script: Option<PathBuf>, + pub ass_file: Option<PathBuf>, + pub options: Option<PathBuf>, + pub interactive: bool, + pub info: bool, + pub manpage: bool, + pub shell: Option<Shell>, + pub font_info: Option<Option<PathBuf>>, + pub verbose: u8, + + // Merge of the two + pub include_folders: Vec<PathBuf>, +} + +macro_rules! merge { + ($($what: ident $src: expr => $dest: expr, { $( $field: ident ),+ });+ $(;)?) => {{ + $(merge! { @@internal $what $( $field ),+ : $src => $dest });+; + }}; + + (@@internal override $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field = $src.$field; )+}}; + (@@internal flg_or $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field |= $src.$field; )+}}; + (@@internal flg_and $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field &= $src.$field; )+}}; + + (@@internal append $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field.extend($src.$field); )+}}; + + (@@internal max $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field = max_partial!($src.$field, $dest.$field); )+}}; + (@@internal min $( $field: ident ),+ : $src: expr => $dest: expr) => {{$( $dest.$field = min_partial!($src.$field, $dest.$field); )+}}; + + (@@internal set_if_not $( $field: ident),+ : $src: expr => $dest: expr) => {{$( + if $dest.$field.is_none() { + $dest.$field = $src.$field; + } + )+}}; +} + +impl Extend<ConfigFile> for ConfigFile { + fn extend<T: IntoIterator<Item = ConfigFile>>(&mut self, iter: T) { + iter.into_iter().for_each(|other| { + let (km, self_km) = (other.karamaker, &mut self.karamaker); + merge! { + append other => self, { include_folders }; + override km => self_km, { kara_maker }; + } + }); + } +} + +impl Extend<ConfigFile> for Config { + fn extend<T: IntoIterator<Item = ConfigFile>>(&mut self, iter: T) { + iter.into_iter().for_each(|cfg| { + let km = cfg.karamaker; + merge! { + append cfg => self, { include_folders }; + override km => self, { kara_maker }; + } + }); + } +} + +impl Extend<Args> for Config { + fn extend<T: IntoIterator<Item = Args>>(&mut self, iter: T) { + iter.into_iter().for_each(|args| { + merge! { + append args => self, { include_folders }; + set_if_not args => self, { script, ass_file, options, shell, font_info }; + max args => self, { verbose }; + override args => self, { info, manpage, interactive }; + } + }); + } +} + +impl ConfigFile { + pub fn serialize(&self) -> Result<String, Box<dyn std::error::Error>> { + Ok(toml::to_string_pretty(self).map_err(Box::new)?) + } + + pub fn deserialize(input: String) -> Result<Self, Box<dyn std::error::Error>> { + Ok(toml::from_str(&input).map_err(Box::new)?) + } +} + +impl Default for ConfigKaraMaker { + fn default() -> Self { + Self { kara_maker: "Viieux".to_string(), email: Default::default() } + } +} diff --git a/src/Rust/vvs_cli/src/lib.rs b/src/Rust/vvs_cli/src/lib.rs index 08c54c71..2c31f840 100644 --- a/src/Rust/vvs_cli/src/lib.rs +++ b/src/Rust/vvs_cli/src/lib.rs @@ -1,4 +1,7 @@ +#![forbid(unsafe_code)] + pub mod args; +pub mod config; pub mod logger; mod parser; diff --git a/src/Rust/vvs_cli/src/logger.rs b/src/Rust/vvs_cli/src/logger.rs index 270a8526..e0e0a408 100644 --- a/src/Rust/vvs_cli/src/logger.rs +++ b/src/Rust/vvs_cli/src/logger.rs @@ -1,7 +1,6 @@ -use lazy_static::lazy_static; use log::{Level, Metadata, Record, SetLoggerError}; use scc::HashSet; -use std::sync::atomic::AtomicU8; +use std::sync::{atomic::AtomicU8, OnceLock}; use thiserror::Error; #[derive(Debug, Error)] @@ -19,8 +18,10 @@ struct SimpleLoggerRef { inner: SimpleLogger, } -lazy_static! { - static ref LOGGER: SimpleLoggerRef = Default::default(); +static LOGGER: OnceLock<SimpleLoggerRef> = OnceLock::new(); + +fn logger() -> &'static SimpleLogger { + &LOGGER.get().unwrap().inner } impl std::fmt::Display for LoggerInitError { @@ -30,17 +31,19 @@ impl std::fmt::Display for LoggerInitError { } impl SimpleLogger { - fn write_str<S: AsRef<str>>(lvl: char, target: &str, content: S) { + fn write_str<S: AsRef<str>>(level: char, prefix: String, target: &str, content: S) { let prefix = if target.is_empty() { - format!("{lvl} ") + format!("{level} [{prefix}]") + } else if prefix.is_empty() { + format!("{level} [{target}] ") } else { - format!("{lvl} [{target}] ") + format!("{level} [{prefix} {target}] ") }; - content - .as_ref() - .lines() - .filter(|content| !content.trim().is_empty()) - .for_each(|content| eprintln!("{prefix}{content}")); + let mut content = content.as_ref().lines().filter(|content| !content.trim().is_empty()); + if let Some(content) = content.next() { + eprintln!("{prefix}{content}"); + } + content.for_each(|content| eprintln!("{level} {content}")); } fn level(&self) -> Level { @@ -68,10 +71,16 @@ impl log::Log for SimpleLogger { Level::Debug => '.', Level::Trace => ' ', }; + let prefix = match (record.file(), record.line()) { + (None, None) => "".to_string(), + (None, Some(line)) => format!("...+{line}"), + (Some(file), None) => format!("{file}+..."), + (Some(file), Some(line)) => format!("{file}+{line}"), + }; if let Some(s) = record.args().as_str() { - SimpleLogger::write_str(level, record.target(), s) + SimpleLogger::write_str(level, prefix, record.target(), s) } else { - SimpleLogger::write_str(level, record.target(), record.args().to_string()); + SimpleLogger::write_str(level, prefix, record.target(), record.args().to_string()); } } } @@ -80,22 +89,20 @@ impl log::Log for SimpleLogger { } pub fn level(lvl: u8) { - LOGGER - .inner - .level - .store(lvl, std::sync::atomic::Ordering::SeqCst); - log::set_max_level(LOGGER.inner.level().to_level_filter()); + logger().level.store(lvl, std::sync::atomic::Ordering::SeqCst); + log::set_max_level(LOGGER.get().unwrap().inner.level().to_level_filter()); } pub fn ignore_target(target: &'static str) { - let _ = LOGGER.inner.ignore_targets.insert(target); + let _ = logger().ignore_targets.insert(target); } pub fn init(level: Option<Level>) -> Result<(), LoggerInitError> { - log::set_logger(&LOGGER.inner) + LOGGER.set(Default::default()).expect("failed to set default logger..."); + log::set_logger(logger()) .map(|()| { log::set_max_level(match level { - None => LOGGER.inner.level().to_level_filter(), + None => logger().level().to_level_filter(), Some(level) => { match level { Level::Trace => self::level(3), diff --git a/src/Rust/vvs_cli/src/main.rs b/src/Rust/vvs_cli/src/main.rs index 72abf27b..3cf3f1a9 100644 --- a/src/Rust/vvs_cli/src/main.rs +++ b/src/Rust/vvs_cli/src/main.rs @@ -1,11 +1,16 @@ //! The VivyScript cli +#![forbid(unsafe_code)] use anyhow::{Context, Result}; -use vvs_cli::{args, logger}; +use vvs_cli::{ + args, + config::{Config, ConfigFile}, + logger, +}; +use vvs_utils::xdg::*; fn print_face_info(name: &str, font: &[u8]) -> Result<()> { - let font = - vvs_font::Font::try_from(font).with_context(|| format!("failed to parse font {name}"))?; + let font = vvs_font::Font::try_from(font).with_context(|| format!("failed to parse font {name}"))?; let font_types = font .font_types() @@ -29,16 +34,28 @@ fn print_face_info(name: &str, font: &[u8]) -> Result<()> { fn main() -> Result<()> { logger::init(None).map_err(Box::new)?; logger::ignore_target("rustyline"); - let args::Args { - interactive, - verbose, + + let mut config = Config::default(); + config.extend([ + XDGConfig::<ConfigFile, XDGConfigMergedSilent>::new("vvcc", ConfigFile::deserialize) + .file("vvcc.toml") + .read_or_default(ConfigFile::serialize), + ]); + config.extend([<args::Args as clap::Parser>::parse()]); + let Config { script, + ass_file, options, - shell, - manpage, + interactive, info, + manpage, + shell, font_info, - } = <args::Args as clap::Parser>::parse(); + verbose, + include_folders, + .. + } = config; + logger::level(verbose); if manpage { @@ -57,8 +74,8 @@ fn main() -> Result<()> { } else if let Some(font_info) = font_info { match font_info { Some(path) => { - let font = std::fs::read(&path) - .with_context(|| format!("failed to read font: {}", path.to_string_lossy()))?; + let font = + std::fs::read(&path).with_context(|| format!("failed to read font: {}", path.to_string_lossy()))?; print_face_info(&path.to_string_lossy(), &font)?; } None => { @@ -77,13 +94,18 @@ fn main() -> Result<()> { .with_context(|| "failed to print help message for vvcc"); } - let lua = vvs_lua::setup(match options { - Some(ref options) => { - log::debug!(target: "vvcc", "load option file: {}", options.to_string_lossy()); - Some(vvs_lua::TomlOptions::new_from_toml(options)?) - } - None => None, - }) + let lua = vvs_lua::setup( + ass_file, + match options { + Some(ref options) => { + log::debug!(target: "vvcc", "load option file: {}", options.to_string_lossy()); + Some(vvs_lua::TomlOptions::new_from_toml(options)?) + } + None => None, + }, + include_folders, + interactive, + ) .with_context(|| match options { Some(options) => format!( "failed to setup base runtime and/or load options: {}", diff --git a/src/Rust/vvs_cli/src/parser.rs b/src/Rust/vvs_cli/src/parser.rs index 73c9ba00..21f29bfd 100644 --- a/src/Rust/vvs_cli/src/parser.rs +++ b/src/Rust/vvs_cli/src/parser.rs @@ -26,10 +26,7 @@ impl TypedValueParser for FileTypeValueParser { ) -> Result<Self::Value, clap::Error> { let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd); if let Some(arg) = arg { - err.insert( - ContextKind::InvalidArg, - ContextValue::String(arg.to_string()), - ); + err.insert(ContextKind::InvalidArg, ContextValue::String(arg.to_string())); } match value.to_ascii_lowercase().to_str() { Some(value) => match value.trim().split('.').last() { @@ -38,10 +35,7 @@ impl TypedValueParser for FileTypeValueParser { .ok_or_else(|| { err.insert( ContextKind::InvalidValue, - ContextValue::String(format!( - "invalid extension {file_ext}, expected {}", - self.extension - )), + ContextValue::String(format!("invalid extension {file_ext}, expected {}", self.extension)), ); err }), diff --git a/src/Rust/vvs_font/Cargo.toml b/src/Rust/vvs_font/Cargo.toml index f1cd7653..838aa4cf 100644 --- a/src/Rust/vvs_font/Cargo.toml +++ b/src/Rust/vvs_font/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true description = "The font crate for VVS" [dependencies] -lazy_static.workspace = true thiserror.workspace = true log.workspace = true diff --git a/src/Rust/vvs_font/build.rs b/src/Rust/vvs_font/build.rs index 0ae1bc9d..aaf4757c 100644 --- a/src/Rust/vvs_font/build.rs +++ b/src/Rust/vvs_font/build.rs @@ -48,8 +48,7 @@ pub const fn embeded_fonts() -> &'static [(&'static str, &'static [u8])] {{ ); // Write - fs::write(out_dir.join("generated_font_utils.rs"), src_content) - .expect("failed to write generated source file"); + fs::write(out_dir.join("generated_font_utils.rs"), src_content).expect("failed to write generated source file"); // Rerun rerun_directory(&font_dir); diff --git a/src/Rust/vvs_font/fonts/OFL.txt b/src/Rust/vvs_font/fonts/NotoSans-LICENCE-OFL.txt similarity index 100% rename from src/Rust/vvs_font/fonts/OFL.txt rename to src/Rust/vvs_font/fonts/NotoSans-LICENCE-OFL.txt diff --git a/src/Rust/vvs_font/src/font.rs b/src/Rust/vvs_font/src/font.rs index d51bf15b..5fc71f22 100644 --- a/src/Rust/vvs_font/src/font.rs +++ b/src/Rust/vvs_font/src/font.rs @@ -27,8 +27,7 @@ impl<'a> TryFrom<&'a [u8]> for Font<'a> { fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> { Ok(Self { face: ttf_parser::Face::parse(data, 0).map_err(FontCreationError::TTFParserError)?, - font: ab_glyph::FontRef::try_from_slice(data) - .map_err(FontCreationError::ABGlyphError)?, + font: ab_glyph::FontRef::try_from_slice(data).map_err(FontCreationError::ABGlyphError)?, }) } } @@ -47,15 +46,10 @@ impl<'a> Font<'a> { /// Get the family names. pub fn family_names(&self) -> Vec<String> { let names = self.face.names(); - let filter = [ - ttf_parser::name_id::POST_SCRIPT_NAME, - ttf_parser::name_id::FULL_NAME, - ]; + let filter = [ttf_parser::name_id::POST_SCRIPT_NAME, ttf_parser::name_id::FULL_NAME]; names .into_iter() - .flat_map(|name| { - (filter.contains(&name.name_id) && name.is_unicode()).then(|| name.to_string()) - }) + .flat_map(|name| (filter.contains(&name.name_id) && name.is_unicode()).then(|| name.to_string())) .flatten() .collect() } @@ -102,14 +96,8 @@ impl<'a> Font<'a> { .map(|outlined| { let ab_glyph::Rect { min, max } = outlined.px_bounds(); Rect::new( - Point { - x: min.x.trunc() as i64, - y: min.y.trunc() as i64, - }, - Point { - x: max.x.trunc() as i64, - y: max.y.trunc() as i64, - }, + Point { x: min.x.trunc() as i64, y: min.y.trunc() as i64 }, + Point { x: max.x.trunc() as i64, y: max.y.trunc() as i64 }, ) }) .ok_or(FontError::FailedToOutline(pt, glyph)) diff --git a/src/Rust/vvs_font/src/lib.rs b/src/Rust/vvs_font/src/lib.rs index ba8e8634..b631d37b 100644 --- a/src/Rust/vvs_font/src/lib.rs +++ b/src/Rust/vvs_font/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + mod error; mod font; mod rect; diff --git a/src/Rust/vvs_font/src/rect.rs b/src/Rust/vvs_font/src/rect.rs index 28c7be4f..86454e17 100644 --- a/src/Rust/vvs_font/src/rect.rs +++ b/src/Rust/vvs_font/src/rect.rs @@ -45,17 +45,11 @@ impl Rect { impl Point { /// Returns the min components of the two [Point]. pub fn min(p1: Point, p2: Point) -> Point { - Point { - x: min(p1.x, p2.x), - y: min(p1.y, p2.y), - } + Point { x: min(p1.x, p2.x), y: min(p1.y, p2.y) } } /// Returns the max components of the two [Point]. pub fn max(p1: Point, p2: Point) -> Point { - Point { - x: max(p1.x, p2.x), - y: max(p1.y, p2.y), - } + Point { x: max(p1.x, p2.x), y: max(p1.y, p2.y) } } } diff --git a/src/Rust/vvs_lua/Cargo.toml b/src/Rust/vvs_lua/Cargo.toml index 9443ce92..481d1a0c 100644 --- a/src/Rust/vvs_lua/Cargo.toml +++ b/src/Rust/vvs_lua/Cargo.toml @@ -10,9 +10,11 @@ description = "The lua wrapper for VVS" vvs_utils = { path = "../vvs_utils" } vvs_ass = { path = "../vvs_ass" } +unicode-segmentation.workspace = true thiserror.workspace = true +serde.workspace = true paste.workspace = true toml.workspace = true +mlua.workspace = true log.workspace = true - -mlua = { version = "^0.8", features = ["luajit52", "vendored", "macros"] } +scc.workspace = true diff --git a/src/Rust/vvs_lua/build.rs b/src/Rust/vvs_lua/build.rs index 50064850..ee12de74 100644 --- a/src/Rust/vvs_lua/build.rs +++ b/src/Rust/vvs_lua/build.rs @@ -17,10 +17,9 @@ fn main() { let out_dir = Path::new(&env::var_os("OUT_DIR").expect("no OUT_DIR env variable...")) .canonicalize() .expect("failed to canonicalize OUT_DIR"); - let base_path = - PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("can't find the manifest path")) - .canonicalize() - .expect("failed to canonicalize CARGO_MANIFEST_DIR"); + let base_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("can't find the manifest path")) + .canonicalize() + .expect("failed to canonicalize CARGO_MANIFEST_DIR"); // Utils for the stdlib let stdlib_path = base_path.join("src/libs"); diff --git a/src/Rust/vvs_lua/src/data/actions.rs b/src/Rust/vvs_lua/src/data/actions.rs index d11fa665..f2ed025b 100644 --- a/src/Rust/vvs_lua/src/data/actions.rs +++ b/src/Rust/vvs_lua/src/data/actions.rs @@ -52,9 +52,9 @@ impl<'lua> FromLua<'lua> for RegisterDataValue { }); if value.ty().ne(&ty) { return Err(LuaError::RuntimeError(format!( - "tried to register a value of type '{}' when declaring the option of type '{ty}'", - value.ty() - ))); + "tried to register a value of type '{}' when declaring the option of type '{ty}'", + value.ty() + ))); } Ok(Self { doc, ty, value }) } @@ -62,9 +62,7 @@ impl<'lua> FromLua<'lua> for RegisterDataValue { _ => Err(LuaError::FromLuaConversionError { from: lua_value.type_name(), to: "RegisterDataValue", - message: Some( - "expected a string or a table with the 'data' instruction".to_string(), - ), + message: Some("expected a string or a table with the 'data' instruction".to_string()), }), } } diff --git a/src/Rust/vvs_lua/src/data/register.rs b/src/Rust/vvs_lua/src/data/register.rs index af8fbaf9..e51a022b 100644 --- a/src/Rust/vvs_lua/src/data/register.rs +++ b/src/Rust/vvs_lua/src/data/register.rs @@ -1,9 +1,8 @@ -use crate::{data::actions::RegisterDataValue, lua_wrapper::LuaAssAuxTablePtr}; +use crate::data::actions::RegisterDataValue; use mlua::prelude::*; use std::{ - cell::RefCell, collections::{HashMap, HashSet}, - rc::Rc, + sync::{Arc, RwLock}, }; use thiserror::Error; use vvs_ass::{ASSAuxTable, ASSType, ASSTYPE_LENGTH, ASSTYPE_VALUES}; @@ -12,7 +11,7 @@ use vvs_ass::{ASSAuxTable, ASSType, ASSTYPE_LENGTH, ASSTYPE_VALUES}; struct VivyDataValueHashMap { ass_type: ASSType, table: HashMap<String, RegisterDataValue>, - cached_table: LuaAssAuxTablePtr, + cached_table: ASSAuxTable, } #[derive(Debug, Error)] @@ -34,7 +33,7 @@ pub(crate) struct VivyDataRegister { will_register: HashSet<String>, } -pub(crate) type VivyDataRegisterPtr = Rc<RefCell<VivyDataRegister>>; +pub(crate) type VivyDataRegisterPtr = Arc<RwLock<VivyDataRegister>>; fn decode_name(name: &str) -> LuaResult<(&str, ASSType)> { let Some((ass_type, name)) = name.split_once(':') else { @@ -55,7 +54,7 @@ impl VivyDataValueHashMap { Self { ass_type: ty, table: Default::default(), - cached_table: LuaAssAuxTablePtr::from(ASSAuxTable::default().into_ptr()), + cached_table: ASSAuxTable::default(), } } } @@ -82,14 +81,11 @@ impl VivyDataRegister { pub fn compute_cached_tables(&mut self) { for ty in ASSTYPE_VALUES { let register = self.get_register_mut(*ty); - let table = LuaAssAuxTablePtr::from( - ASSAuxTable::from_iter( - register - .table - .iter() - .map(|(name, data)| (name.clone(), data.value.clone().into_inner())), - ) - .into_ptr(), + let table = ASSAuxTable::from_iter( + register + .table + .iter() + .map(|(name, data)| (name.clone(), data.value.clone().into_inner())), ); register.cached_table = table; } @@ -111,17 +107,15 @@ impl VivyDataRegister { /// Get the default aux table for an ass element type. Returns the cached table. After /// registering all the wanted values, you may use the /// [VivyDataRegister::compute_cached_tables] before calling this function. - pub fn get_table(&self, ass_type: ASSType) -> LuaAssAuxTablePtr { + pub fn get_table(&self, ass_type: ASSType) -> ASSAuxTable { self.get_register(ass_type).cached_table.clone() } /// Iter over the registered data names and types. pub fn iter_registered(&self) -> impl Iterator<Item = (ASSType, &String)> { - self.registered.iter().flat_map( - |VivyDataValueHashMap { - ass_type, table, .. - }| { std::iter::repeat(*ass_type).zip(table.keys()) }, - ) + self.registered + .iter() + .flat_map(|VivyDataValueHashMap { ass_type, table, .. }| std::iter::repeat(*ass_type).zip(table.keys())) } /// Get the register data for a specific [ASSType]. If the ass type is not correct the function @@ -155,11 +149,7 @@ impl VivyDataRegister { /// Register the data, returns an error if the data was already registered. If the data was not /// flagged as 'to register', also raise an error. - fn register_data( - &mut self, - name: impl AsRef<str>, - value: RegisterDataValue, - ) -> Result<(), VivyDataRegisterError> { + fn register_data(&mut self, name: impl AsRef<str>, value: RegisterDataValue) -> Result<(), VivyDataRegisterError> { let (name, ass_type) = decode_name(name.as_ref()).map_err(VivyDataRegisterError::Lua)?; let table = &mut self.get_register_mut(ass_type).table; match table.get(name) { @@ -184,11 +174,7 @@ impl VivyDataRegister { for (ty, data_name, format_name) in added { println!( " - data {format_name:padding$} = {}", - self.get_table(ty) - .as_inner() - .borrow() - .get(data_name) - .expect("vivy internal error"), + self.get_table(ty).get(data_name).expect("vivy internal error"), ); } } @@ -201,43 +187,34 @@ impl VivyDataRegister { impl LuaUserData for VivyDataRegister { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_function("_name", |lua, ()| lua.create_string("VivyDataRegister")); + methods.add_method_mut("will_register", |_, this, name: String| { - this.will_register_data(name.clone()) - .map_err(|err| match err { - VivyDataRegisterError::Lua(lua) => lua, - err => LuaError::RuntimeError(format!( - "failed to mark as 'to register' data '{name}': {err}" - )), - }) + this.will_register_data(name.clone()).map_err(|err| match err { + VivyDataRegisterError::Lua(lua) => lua, + err => LuaError::RuntimeError(format!("failed to mark as 'to register' data '{name}': {err}")), + }) }); - methods.add_method_mut( - "register", - |_, this, (name, value): (String, RegisterDataValue)| { - this.register_data(&name, value).map_err(|err| match err { - VivyDataRegisterError::Lua(lua) => lua, - err => { - LuaError::RuntimeError(format!("failed to register data '{name}': {err}")) - } - }) - }, - ) + methods.add_method_mut("register", |_, this, (name, value): (String, RegisterDataValue)| { + this.register_data(&name, value).map_err(|err| match err { + VivyDataRegisterError::Lua(lua) => lua, + err => LuaError::RuntimeError(format!("failed to register data '{name}': {err}")), + }) + }) } } #[test] fn test_data_register_creation() { let reg = VivyDataRegister::new(); - let reg: &VivyDataRegister = ®.borrow(); + let reg: &VivyDataRegister = ®.try_read().unwrap(); assert_eq!(*reg, Default::default()); macro_rules! test_item { ($item: ident) => { assert_eq!( - reg.registered - .get(ASSType::$item as usize) - .unwrap() - .ass_type, + reg.registered.get(ASSType::$item as usize).unwrap().ass_type, ASSType::$item ); }; diff --git a/src/Rust/vvs_lua/src/dsl.rs b/src/Rust/vvs_lua/src/dsl.rs index 67448bd7..cba49266 100644 --- a/src/Rust/vvs_lua/src/dsl.rs +++ b/src/Rust/vvs_lua/src/dsl.rs @@ -30,11 +30,7 @@ pub(crate) use value; use mlua::{chunk, prelude::*}; -pub(crate) fn into_readonly_table<'lua>( - lua: &'lua Lua, - name: LuaString<'lua>, - table: LuaTable<'lua>, -) -> LuaResult<LuaTable<'lua>> { +pub(crate) fn into_readonly_table<'lua>(lua: &'lua Lua, table: LuaTable<'lua>) -> LuaResult<LuaTable<'lua>> { let (proxy, mt) = (lua.create_table()?, lua.create_table()?); mt.raw_set("__index", table)?; mt.raw_set( @@ -44,27 +40,19 @@ pub(crate) fn into_readonly_table<'lua>( error("attempt to update a read-only table", 2) end }) - .set_name(format!("{}::ro-function", name.to_str()?))? .eval::<LuaFunction>()?, )?; proxy.set_metatable(Some(mt)); Ok(proxy) } -pub(crate) fn into_string_vec<'lua>( - lua: &'lua Lua, - value: LuaValue<'lua>, -) -> LuaResult<Vec<String>> { +pub(crate) fn into_string_vec<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult<Vec<String>> { match value { - ret @ LuaValue::Boolean(_) | ret @ LuaValue::Integer(_) | ret @ LuaValue::Number(_) => { - Ok(vec![lua - .coerce_string(ret)? - .ok_or(LuaError::RuntimeError( - "coertion to string failed".to_string(), - ))? - .to_string_lossy() - .to_string()]) - } + ret @ LuaValue::Boolean(_) | ret @ LuaValue::Integer(_) | ret @ LuaValue::Number(_) => Ok(vec![lua + .coerce_string(ret)? + .ok_or(LuaError::RuntimeError("coertion to string failed".to_string()))? + .to_string_lossy() + .to_string()]), LuaValue::String(str) => Ok(vec![str.to_string_lossy().to_string()]), LuaValue::Table(table) => Ok(table .sequence_values::<LuaString>() diff --git a/src/Rust/vvs_lua/src/func/actions.rs b/src/Rust/vvs_lua/src/func/actions.rs index 81c1aeab..892a0458 100644 --- a/src/Rust/vvs_lua/src/func/actions.rs +++ b/src/Rust/vvs_lua/src/func/actions.rs @@ -1,6 +1,6 @@ use crate::dsl; use mlua::prelude::*; -use std::rc::Rc; +use std::sync::Arc; /// Structure used to parse a function declaration, like in: /// - `func "toto" { function(...) ... end }` @@ -12,12 +12,11 @@ pub(super) struct FuncDeclaration<'lua> { /// Structure used to represent a function in memory. #[derive(Debug)] -#[allow(dead_code)] -pub(super) struct VivyFunc { - pub module: Option<String>, +pub(crate) struct VivyFunc { + pub module: String, pub name: String, pub doc: Option<String>, - pub function: Rc<LuaRegistryKey>, + pub function: Arc<LuaRegistryKey>, } impl<'lua> FromLua<'lua> for FuncDeclaration<'lua> { @@ -43,10 +42,7 @@ impl<'lua> FromLua<'lua> for FuncDeclaration<'lua> { .filter_map(|pair| Some(pair.ok()?.1)) .collect(); match &callback[..] { - [function] => Ok(Self { - doc, - function: function.clone(), - }), + [function] => Ok(Self { doc, function: function.clone() }), [] => Err(LuaError::RuntimeError( "expected a function to implement the vivy function export, got nothing".to_string(), )), @@ -60,12 +56,9 @@ impl<'lua> FromLua<'lua> for FuncDeclaration<'lua> { impl LuaUserData for VivyFunc { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method( - LuaMetaMethod::Call, - |lua, this, arguments: LuaMultiValue| { - lua.registry_value::<LuaFunction>(&this.function)? - .call::<_, LuaMultiValue>(arguments) - }, - ) + methods.add_meta_method(LuaMetaMethod::Call, |lua, this, arguments: LuaMultiValue| { + lua.registry_value::<LuaFunction>(&this.function)? + .call::<_, LuaMultiValue>(arguments) + }) } } diff --git a/src/Rust/vvs_lua/src/func/mod.rs b/src/Rust/vvs_lua/src/func/mod.rs index ca2913e6..6a2d643a 100644 --- a/src/Rust/vvs_lua/src/func/mod.rs +++ b/src/Rust/vvs_lua/src/func/mod.rs @@ -1,4 +1,5 @@ mod actions; mod register; +pub(crate) use actions::VivyFunc; pub(crate) use register::{VivyFuncRegister, VivyFuncRegisterPtr}; diff --git a/src/Rust/vvs_lua/src/func/register.rs b/src/Rust/vvs_lua/src/func/register.rs index d373c00a..80c63e7e 100644 --- a/src/Rust/vvs_lua/src/func/register.rs +++ b/src/Rust/vvs_lua/src/func/register.rs @@ -1,9 +1,8 @@ use crate::func::actions::{FuncDeclaration, VivyFunc}; use mlua::{chunk, prelude::*}; use std::{ - cell::RefCell, collections::{HashMap, HashSet}, - rc::Rc, + sync::{Arc, RwLock}, }; /// The structure used to register funcs and do the resolution at runtime. @@ -18,13 +17,13 @@ pub(crate) struct VivyFuncRegister { /// All the func that are declared in the current module. Must be cleared after a module is /// loaded. If its length is not equal to [VivyFuncRegister::func_names], then we have a problem. - local_funcs: HashMap<String, Rc<VivyFunc>>, + local_funcs: HashMap<String, Arc<RwLock<VivyFunc>>>, /// All func declared in all modules. - all_declared_funcs: Vec<Rc<VivyFunc>>, + all_declared_funcs: Vec<Arc<RwLock<VivyFunc>>>, } -pub(crate) type VivyFuncRegisterPtr = Rc<RefCell<VivyFuncRegister>>; +pub(crate) type VivyFuncRegisterPtr = Arc<RwLock<VivyFuncRegister>>; impl VivyFuncRegister { pub fn new() -> VivyFuncRegisterPtr { @@ -33,12 +32,7 @@ impl VivyFuncRegister { /// Export the local funcs into the named module. We also reset the content of the register for /// the next module. This should be called once, when we finished to parse the module. - pub(crate) fn export( - &mut self, - lua: &Lua, - name: &LuaString, - table: &LuaTable, - ) -> LuaResult<()> { + pub(crate) fn export(&mut self, lua: &Lua, name: &LuaString, table: &LuaTable) -> LuaResult<()> { if self.func_names.len() != self.local_funcs.len() { return Err(LuaError::RuntimeError(format!( "invalid func declarations in module '{}', declaration count doesn't match the registration count", @@ -50,21 +44,26 @@ impl VivyFuncRegister { let func = self.local_funcs.remove(&name).expect("undefined func"); table.raw_set( lua.create_string(&name)?, - lua.registry_value::<LuaFunction>(&func.function)?, + lua.registry_value::<LuaFunction>(&func.try_read().unwrap().function)?, )?; } Ok(()) } + + /// Iterate over all declared functions. + pub(crate) fn iter_declared(&self) -> std::slice::Iter<Arc<RwLock<VivyFunc>>> { + self.all_declared_funcs.iter() + } } impl LuaUserData for VivyFuncRegister { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_function("_name", |lua, ()| lua.create_string("VivyFuncRegister")); + methods.add_method_mut("will_register", |_, this, name: String| -> LuaResult<()> { if this.func_names.contains(&name) { - Err(LuaError::RuntimeError(format!( - "redefinition of func '{name}'" - ))) + Err(LuaError::RuntimeError(format!("redefinition of func '{name}'"))) } else { this.func_names.insert(name); Ok(()) @@ -81,27 +80,24 @@ impl LuaUserData for VivyFuncRegister { "try to register func '{name}' but it was not declared" ))); } else if this.local_funcs.contains_key(name) { - return Err(LuaError::RuntimeError(format!( - "try to register func '{name}' again" - ))); + return Err(LuaError::RuntimeError(format!("try to register func '{name}' again"))); } - let declaration = Rc::new(VivyFunc { - module: match lua - .load(chunk!(return vivy: ___current_module())) - .set_name("query current module name")? - .eval::<LuaValue>() - { - Ok(LuaValue::String(str)) => Some(str.to_string_lossy().to_string()), - _ => None, + let declaration = Arc::new(RwLock::new(VivyFunc { + module: match lua.load(chunk!(return vivy: ___current_module())).eval::<LuaValue>() { + Ok(LuaValue::String(str)) => str.to_string_lossy().to_string(), + _ => { + return Err(LuaError::RuntimeError(format!( + "failed to register function '{name}', failed to query the current module name" + ))) + } }, name: name.to_string(), doc: table.doc, function: lua.create_registry_value(table.function)?.into(), - }); + })); - this.local_funcs - .insert(name.to_string(), declaration.clone()); + this.local_funcs.insert(name.to_string(), declaration.clone()); this.all_declared_funcs.push(declaration); Ok(()) diff --git a/src/Rust/vvs_lua/src/functions.rs b/src/Rust/vvs_lua/src/functions.rs index e94a939f..cbf5bba4 100644 --- a/src/Rust/vvs_lua/src/functions.rs +++ b/src/Rust/vvs_lua/src/functions.rs @@ -1,223 +1,43 @@ -use crate::{ - data::VivyDataRegister, func::VivyFuncRegister, jobs::VivyJobRegister, libs, - options::VivyOptionRegister, TomlOptions, -}; +use crate::{libs::vivy::*, TomlOptions}; use mlua::{chunk, prelude::*}; -use std::path::Path; - -pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { - let (version, options, data, jobs, func) = ( - env!("CARGO_PKG_VERSION"), - VivyOptionRegister::new(options.unwrap_or_default()), - VivyDataRegister::new(), - VivyJobRegister::new(), - VivyFuncRegister::new(), - ); - let lua = unsafe { - // We use unsafe because we need to load the debug package. - // This also allows to load C things and dylib things, but because we nil the requires - // function it should be fine. - Lua::unsafe_new_with( - LuaStdLib::MATH - | LuaStdLib::TABLE - | LuaStdLib::STRING - | LuaStdLib::PACKAGE - | LuaStdLib::DEBUG, - LuaOptions::new().catch_rust_panics(true), - ) - }; - let (requires, warning, info) = ( - lua.create_function(libs::import)?, - lua.create_function(|_, msg: String| { - log::warn!(target: "vvs", "{msg}"); - Ok(()) - })?, - lua.create_function(|_, msg: String| { - log::info!(target: "vvs", "{msg}"); - Ok(()) - })?, - ); - - lua.load(chunk! { - local _debug = debug - require = nil - module = nil - _VERSION = nil - local options = $options - local data = $data - local jobs = $jobs - local func = $func - local ReadOnly = { - package = nil, - coroutine = nil, - debug = nil, - _VERSION = "Vivy " .. $version, - warning = $warning, - info = $info, - } - - // Import Vivy - $requires "vivy" - vivy:___set_options(options) - vivy:___set_data(data) - vivy:___set_jobs(jobs) - vivy:___set_func(func) - ReadOnly.vivy = vivy - vivy = nil - - package.loaded["package"] = nil - package.loaded["debug"] = nil - package.loaded["coroutine"] = nil - package.loaded["_G"] = nil - for name, _ in pairs(package.loaded) do - ReadOnly.vivy:___mark_loaded(name) - end - package = nil - - function ReadOnly._job_input_type (func) - local nparams = _debug.getinfo(func).nparams - if nparams ~= 1 then - error("a job should only have one argument, got " .. nparams, 2) - end - return _debug.getlocal(func, 1) - end - - // DSL keywords - - function ReadOnly.import (name) - if type(name) ~= "string" then - error("you must import a module by its name, got " .. type(name) .. " and not a string", 2) - end - $requires(name) - end - - function ReadOnly.main (table) - if type(table) ~= "table" then - error("you must specify the main workflow with a table", 2) - end - vivy:___main(table) - end - - function ReadOnly.func (name) - if type(name) ~= "string" then - error("you must specify the exported function name with a string", 2) - end - return function (table) - if type(table) ~= "table" then - error("you must specify the function description with a table", 2) - end - error("not implemented") - end - end - - function ReadOnly.job (name) - if type(name) ~= "string" then - error("you must specify the job name with a string", 2) - end - jobs:will_register(name) - return function (table) - if type(table) ~= "table" then - error("you must specify the job description with a table", 2) - end - jobs:register(name, table) - end - end - - function ReadOnly.data (name) - if type(name) ~= "string" then - error("you must specify the datum name with a string", 2) - end - data:will_register(name) - return function (table) - if type(table) ~= "table" then - error("you must specify the data description with a table", 2) - end - data:register(name, table) - end - end - - function ReadOnly.set (name) - if type(name) ~= "string" then - error("you must specify the option name with a string", 2) - end - options:will_set(name) - return function (table) - if type(table) ~= "table" then - error("you must specify the option specification description with a table", 2) - end - options:set(name, table) - end - end - - function ReadOnly.option (name) - if type(name) ~= "string" then - error("you must specify the option name with a string", 2) - end - options:will_register(name) - return function (table) - if type(table) ~= "table" then - error("you must specify the option description with a table", 2) - end - options:register(name, table) - end - end - - // Protect the metatable - - setmetatable(_G, { - __metatable = "don't touch to the global table", - __index = ReadOnly, - __newindex = function(t, n, v) - if rawget(ReadOnly, n) ~= nil then - error("keyword or read only variable '" .. n .. "'", 2) - end - if n:upper() ~= n then - error("global variables must be in uppercase, try '" .. n:upper() .. "'", 2) - end - rawset(t, n, v) - end, - }) +use std::path::{Path, PathBuf}; +use vvs_utils::either; + +pub fn setup( + ass_file: Option<impl AsRef<Path>>, + options: Option<TomlOptions>, + include_path: Vec<PathBuf>, + interactive_mode: bool, +) -> LuaResult<Lua> { + Vivy::setup_runtime(RuntimeOptions { + include_path, + ass_file: ass_file.map(|path| ASSFileOrInstance::File(path.as_ref().to_path_buf())), + creation_data: options.map(RuntimeCreationData::TomlFile), + mode: either!(interactive_mode => RuntimeMode::Interactive; RuntimeMode::Full), }) - .set_name("vivy prelude")? - .exec()?; - - Ok(lua) } pub fn load_user_script(lua: &Lua, script: Option<impl AsRef<Path>>) -> LuaResult<()> { if let Some(script) = script { let script = script.as_ref(); - match script.parent() { - Some(parent_folder) => { - let (path1, path2) = (parent_folder.join("?.vvl"), parent_folder.join("?.lua")); - let (path1, path2) = (path1.to_string_lossy(), path2.to_string_lossy()); - let name = format!("append paths {path1} and {path2}"); - lua.load(chunk! { - vivy:___append_to_path($path1) - vivy:___append_to_path($path2) - }) - .set_name(name)? - .exec()?; - } - None => { - log::warn!(target: "lua", "the script has no parent folder: {}", script.to_string_lossy()) - } + if let Some(parent_folder) = script.parent() { + let (path1, path2) = (parent_folder.join("?.vvl"), parent_folder.join("?.lua")); + let (path1, path2) = (path1.to_string_lossy(), path2.to_string_lossy()); + lua.load(chunk! { + vivy:___prepend_to_path($path1) + vivy:___prepend_to_path($path2) + }) + .exec()?; } let script_path = script.to_string_lossy(); + let script_path_lua = lua.create_string(script_path.as_ref())?; log::debug!(target: "lua", "load the script: {script_path}"); + lua.load(chunk! { vivy:___set_user_script($script_path_lua) }).exec()?; lua.load(script).set_name(script_path)?.exec()?; } Ok(()) } pub fn print_info(lua: &Lua) -> LuaResult<()> { - lua.load(mlua::chunk! { vivy:___print_info() }) - .set_name("vivy:___print_info")? - .exec() -} - -pub fn get_loaded(lua: &Lua) -> LuaResult<Vec<String>> { - lua.load(mlua::chunk! { vivy:___get_loaded() }) - .set_name("vivy:___get_loaded")? - .eval() + lua.load(mlua::chunk! { vivy:___print_info() }).exec() } diff --git a/src/Rust/vvs_lua/src/jobs/actions.rs b/src/Rust/vvs_lua/src/jobs/actions.rs index 2338638f..2d8c911a 100644 --- a/src/Rust/vvs_lua/src/jobs/actions.rs +++ b/src/Rust/vvs_lua/src/jobs/actions.rs @@ -1,6 +1,6 @@ use crate::{dsl, lua_wrapper::LuaAssType}; -use mlua::{chunk, prelude::*}; -use std::{rc::Rc, sync::Arc}; +use mlua::prelude::*; +use std::sync::Arc; use vvs_ass::ASSType; use vvs_utils::*; @@ -10,35 +10,36 @@ use vvs_utils::*; pub(super) struct JobDeclaration<'lua> { pub doc: Option<String>, pub function: LuaFunction<'lua>, - pub input_type: Option<ASSType>, - pub return_type: Option<ASSType>, + pub input_type: ASSType, + pub return_type: ASSType, } /// Structure used to represent a job in memory. #[derive(Debug)] -pub(super) struct VivyJob { - pub module: Option<String>, +pub(crate) struct VivyJob { + pub module: String, pub name: String, pub doc: Option<String>, - pub input_type: Option<ASSType>, - pub return_type: Option<ASSType>, - pub function: Rc<LuaRegistryKey>, + pub input_type: ASSType, + pub return_type: ASSType, + pub function: Arc<LuaRegistryKey>, } /// Structure used to represent a job that will be called on some values. Used because of execution /// order of lua, when we build the main tree the variables are not present, we need to declare /// them latter after the tree was validated. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Hash)] pub(crate) struct VivyCalledJob { + pub module: String, pub name: String, - pub function: Rc<LuaRegistryKey>, + pub function: Arc<LuaRegistryKey>, pub arguments: Vec<String>, - pub input_type: Option<ASSType>, - pub return_type: Option<ASSType>, + pub input_type: ASSType, + pub return_type: ASSType, } impl<'lua> FromLua<'lua> for JobDeclaration<'lua> { - fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> { + fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> { let LuaValue::Table(table) = lua_value else { return Err(LuaError::RuntimeError(format!( "expected a table to convert into a job registration, got a: {}", @@ -55,6 +56,13 @@ impl<'lua> FromLua<'lua> for JobDeclaration<'lua> { ))), }); + let getter = dsl::table_get!(table, "_getter" -> { + dsl::value!(Function fun) => fun, + _ => return Err(LuaError::RuntimeError( + "internal error: the '_getter' field should be present when registering a job...".to_string() + )), + }); + let callback: Vec<_> = table .pairs::<LuaString, LuaValue>() .filter_map(|res| match res { @@ -67,24 +75,22 @@ impl<'lua> FromLua<'lua> for JobDeclaration<'lua> { .collect(); match &callback[..] { [(return_type, function)] => Ok({ - let input_type = { - let func = function.clone(); - lua.load(chunk! { return _job_input_type($func) }) - .eval::<LuaString>()? - .to_str()? - .parse::<LuaAssType>() - .map_err(LuaError::RuntimeError)? - .into_inner() - }; + let input_type = getter + .call::<_, LuaString>(function.clone())? + .to_str()? + .parse::<LuaAssType>() + .map_err(LuaError::RuntimeError)? + .into_inner(); Self { doc, function: function.clone(), - return_type: Some(*return_type.as_inner()), - input_type: Some(input_type), + return_type: *return_type.as_inner(), + input_type, } }), [] => Err(LuaError::RuntimeError( - "expected a function to implement the job, got nothing".to_string(), + "expected a function to implement the job, got nothing, did you specify a valid ASS element name?" + .to_string(), )), _ => Err(LuaError::RuntimeError(format!( "expected a single function to implement the job, got {} functions", @@ -101,9 +107,7 @@ impl LuaUserData for VivyJob { LuaNil => vec![], LuaValue::String(name) => vec![name.to_string_lossy().to_string()], LuaValue::Table(names) => { - let (names, errs): (Vec<_>, Vec<_>) = names - .sequence_values::<LuaString>() - .partition(Result::is_ok); + let (names, errs): (Vec<_>, Vec<_>) = names.sequence_values::<LuaString>().partition(Result::is_ok); if let Some(Err(err)) = errs.into_iter().next() { return Err(err); } @@ -119,19 +123,10 @@ impl LuaUserData for VivyJob { ))) } }; - let signature = { - let name = match this.module { - Some(ref module) => format!("{module}.{}", this.name), - None => this.name.clone(), - }; - let signature = match (this.input_type, this.return_type) { - (None, None) => "() -> ()".to_string(), - (None, Some(rty)) => format!("() -> {rty}"), - (Some(ity), None) => format!("{ity} -> ()"), - (Some(ity), Some(rty)) => format!("{ity} -> {rty}"), - }; - format!("{name}: {signature}") - }; + let signature = format!( + "{}.{}: {} -> {}", + this.module, this.name, this.input_type, this.return_type + ); log::info!( target: "lua", "will execute {} job `{signature}` with argsuments: {arguments:?}", @@ -139,6 +134,7 @@ impl LuaUserData for VivyJob { ); Ok(VivyCalledJob { arguments, + module: this.module.clone(), name: this.name.clone(), function: this.function.clone(), input_type: this.input_type, @@ -148,36 +144,4 @@ impl LuaUserData for VivyJob { } } -impl LuaUserData for VivyCalledJob { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::Call, |lua, this, ()| { - if !lua - .load(chunk! { return vivy:___is_in_job_evaluation_mode() }) - .set_name("is in job evaluation mode?")? - .eval::<bool>()? - { - todo!() - } - lua.load(chunk! { vivy:___switch_to_execution_mode() }) - .set_name("switch to execution mode")? - .exec()?; - - let (name, arguments) = (this.name.clone(), this.arguments.clone()); - let function: LuaFunction = lua.registry_value(&this.function)?; - let (Some(input), Some(returns)) = (this.input_type, this.return_type) else { - return Err(LuaError::RuntimeError(format!( - "failed to get the input and return types for the job '{name}'" - ))); - }; - let (input, returns) = (LuaAssType::from(input), LuaAssType::from(returns)); - lua.load(chunk! { - vivy:___execute_job($name, $function, $arguments, $input, $returns) - }) - .eval::<LuaMultiValue>() - .map_err(|cause| LuaError::CallbackError { - traceback: "error in job execution".to_string(), - cause: Arc::new(cause), - }) - }); - } -} +impl LuaUserData for VivyCalledJob {} diff --git a/src/Rust/vvs_lua/src/jobs/mod.rs b/src/Rust/vvs_lua/src/jobs/mod.rs index f844ef32..772cbf0e 100644 --- a/src/Rust/vvs_lua/src/jobs/mod.rs +++ b/src/Rust/vvs_lua/src/jobs/mod.rs @@ -1,5 +1,5 @@ mod actions; mod register; -pub(crate) use actions::VivyCalledJob; +pub(crate) use actions::{VivyCalledJob, VivyJob}; pub(crate) use register::{VivyJobRegister, VivyJobRegisterPtr}; diff --git a/src/Rust/vvs_lua/src/jobs/register.rs b/src/Rust/vvs_lua/src/jobs/register.rs index 3cbe96dd..4579039d 100644 --- a/src/Rust/vvs_lua/src/jobs/register.rs +++ b/src/Rust/vvs_lua/src/jobs/register.rs @@ -1,9 +1,8 @@ use crate::jobs::actions::{JobDeclaration, VivyJob}; use mlua::{chunk, prelude::*}; use std::{ - cell::RefCell, collections::{HashMap, HashSet}, - rc::Rc, + sync::{Arc, RwLock}, }; /// The structure used to register jobs and do the resolution at runtime. @@ -18,13 +17,13 @@ pub(crate) struct VivyJobRegister { /// All the job that are declared in the current module. Must be cleared after a module is /// loaded. If its length is not equal to [VivyJobRegister::job_names], then we have a problem. - local_jobs: HashMap<String, Rc<RefCell<VivyJob>>>, + local_jobs: HashMap<String, Arc<RwLock<VivyJob>>>, /// All job declared in all modules. - all_declared_jobs: Vec<Rc<RefCell<VivyJob>>>, + all_declared_jobs: Vec<Arc<RwLock<VivyJob>>>, } -pub(crate) type VivyJobRegisterPtr = Rc<RefCell<VivyJobRegister>>; +pub(crate) type VivyJobRegisterPtr = Arc<RwLock<VivyJobRegister>>; impl VivyJobRegister { pub fn new() -> VivyJobRegisterPtr { @@ -33,12 +32,7 @@ impl VivyJobRegister { /// Export the local jobs into the named module. We also reset the content of the register for /// the next module. This should be called once, when we finished to parse the module. - pub(crate) fn export( - &mut self, - lua: &Lua, - name: &LuaString, - table: &LuaTable, - ) -> LuaResult<()> { + pub(crate) fn export(&mut self, lua: &Lua, name: &LuaString, table: &LuaTable) -> LuaResult<()> { if self.job_names.len() != self.local_jobs.len() { return Err(LuaError::RuntimeError(format!( "invalid job declarations in module '{}', declaration count doesn't match the registration count", @@ -55,15 +49,20 @@ impl VivyJobRegister { Ok(()) } + + /// Iterate over all declared jobs. + pub(crate) fn iter_declared(&self) -> std::slice::Iter<Arc<RwLock<VivyJob>>> { + self.all_declared_jobs.iter() + } } impl LuaUserData for VivyJobRegister { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_function("_name", |lua, ()| lua.create_string("VivyJobRegister")); + methods.add_method_mut("will_register", |_, this, name: String| -> LuaResult<()> { if this.job_names.contains(&name) { - Err(LuaError::RuntimeError(format!( - "redefinition of job '{name}'" - ))) + Err(LuaError::RuntimeError(format!("redefinition of job '{name}'"))) } else { this.job_names.insert(name); Ok(()) @@ -80,20 +79,14 @@ impl LuaUserData for VivyJobRegister { "try to register job '{name}' but it was not declared" ))); } else if this.local_jobs.contains_key(name) { - return Err(LuaError::RuntimeError(format!( - "try to register job '{name}' again" - ))); + return Err(LuaError::RuntimeError(format!("try to register job '{name}' again"))); } - let declaration = Rc::new(RefCell::new(VivyJob { - module: match lua - .load(chunk!(return vivy: ___current_module())) - .set_name("query current module name")? - .eval::<LuaValue>() - { - Ok(LuaValue::String(str)) => Some(str.to_string_lossy().to_string()), - _ => None, - }, + let declaration = Arc::new(RwLock::new(VivyJob { + module: lua + .load(chunk! { vivy:___current_module() }) + .eval::<LuaString>() + .map(|str| str.to_string_lossy().to_string())?, name: name.to_string(), doc: table.doc, function: lua.create_registry_value(table.function)?.into(), @@ -101,8 +94,7 @@ impl LuaUserData for VivyJobRegister { return_type: table.return_type, })); - this.local_jobs - .insert(name.to_string(), declaration.clone()); + this.local_jobs.insert(name.to_string(), declaration.clone()); this.all_declared_jobs.push(declaration); Ok(()) diff --git a/src/Rust/vvs_lua/src/lib.rs b/src/Rust/vvs_lua/src/lib.rs index e649a844..d752d5ee 100644 --- a/src/Rust/vvs_lua/src/lib.rs +++ b/src/Rust/vvs_lua/src/lib.rs @@ -23,5 +23,5 @@ mod values; pub(crate) mod dsl; pub(crate) mod lua_wrapper; -pub use functions::{get_loaded, load_user_script, print_info, setup}; +pub use functions::{load_user_script, print_info, setup}; pub use toml_option::TomlOptions; diff --git a/src/Rust/vvs_lua/src/libs/bit.rs b/src/Rust/vvs_lua/src/libs/bit.rs new file mode 100644 index 00000000..fe559385 --- /dev/null +++ b/src/Rust/vvs_lua/src/libs/bit.rs @@ -0,0 +1,114 @@ +//! Provides a bits implementation in Rust. We supports most of the LuaJIT's bit module, but use +//! i64 as the backing storage type, but all operations are unsigned (be carefull with the sign bit). + +use mlua::prelude::*; +use std::mem::transmute; + +/// Try to get a bit representation from the [LuaValue]. For integer numbers we transmute them into +/// [u64] instead of an [i64], it is safe to do so because they have the same storage size which is +/// of 64 bits. +fn get_bits(lua: &Lua, value: LuaValue) -> LuaResult<u64> { + match value { + LuaNil | LuaValue::Boolean(false) => Ok(0), + LuaValue::Boolean(true) => Ok(1), + LuaValue::Integer(integer) => Ok(unsafe { transmute::<i64, u64>(integer) }), + LuaValue::Number(number) => Ok(f64::to_bits(number)), + _ => Ok(unsafe { transmute::<i64, u64>(lua.coerce_integer(value)?.unwrap_or_default()) }), + } +} + +/// Converts the unsigned integer back into a [LuaValue]. +fn from_bits(value: u64) -> LuaResult<LuaValue<'static>> { + Ok(LuaValue::Integer(unsafe { transmute::<u64, i64>(value) })) +} + +crate::libs::required! { lua => { + let module = lua.create_table()?; + + module.raw_set("tobit", lua.create_function(|lua, value| { + from_bits(get_bits(lua, value)?) + })?)?; + + module.raw_set("tohex", lua.create_function(|lua, (value, n): (LuaValue, Option<i64>)| { + let value = get_bits(lua, value)?; + match n { + Some(n) if n >= 0 => { + let n: usize = n as usize; + lua.create_string(&format!("{value:0n$x}")) + }, + Some(n) => { + let n: usize = (-n) as usize; + lua.create_string(&format!("{value:0n$X}")) + } + None => lua.create_string(&format!("{value:x}")), + } + })?)?; + + module.raw_set("bnot", lua.create_function(|lua, value| { + from_bits(u64::wrapping_neg(get_bits(lua, value)?)) + })?)?; + + module.raw_set("bor", lua.create_function(|lua, values: LuaMultiValue| { + let mut ret = 0; + for value in values { + ret |= get_bits(lua, value)?; + } + from_bits(ret) + })?)?; + + module.raw_set("band", lua.create_function(|lua, mut values: LuaMultiValue| { + match values.pop_front() { + None => Err(LuaError::RuntimeError("you may pass at least one argument to the 'binary and' function".to_string())), + Some(ret) => { + let mut ret = get_bits(lua, ret)?; + for value in values { + ret &= get_bits(lua, value)?; + } + from_bits(ret) + } + } + })?)?; + + module.raw_set("bxor", lua.create_function(|lua, mut values: LuaMultiValue| { + match values.pop_front() { + None => Err(LuaError::RuntimeError("you may pass at least one argument to the 'binary xor' function".to_string())), + Some(ret) => { + let mut ret = get_bits(lua, ret)?; + for value in values { + ret ^= get_bits(lua, value)?; + } + from_bits(ret) + } + } + })?)?; + + module.raw_set("lshift", lua.create_function(|lua, (value, n): (LuaValue, u32)| { + from_bits(u64::wrapping_shl(get_bits(lua, value)?, n)) + })?)?; + + module.raw_set("rshift", lua.create_function(|lua, (value, n): (LuaValue, u32)| { + from_bits(u64::wrapping_shr(get_bits(lua, value)?, n)) + })?)?; + + module.raw_set("arshift", lua.create_function(|lua, (value, n): (LuaValue, u32)| { + Ok(LuaValue::Integer(i64::wrapping_shr( + unsafe { transmute::<u64, i64>(get_bits(lua, value)?) }, + n + ))) + })?)?; + + module.raw_set("bswap", lua.create_function(|lua, value: LuaValue| { + from_bits(u64::swap_bytes(get_bits(lua, value)?)) + })?)?; + + module.raw_set("rol", lua.create_function(|lua, (value, n): (LuaValue, u32)| { + from_bits(u64::rotate_left (get_bits(lua, value)?, n)) + })?)?; + + module.raw_set("ror", lua.create_function(|lua, (value, n): (LuaValue, u32)| { + from_bits(u64::rotate_right(get_bits(lua, value)?, n)) + })?)?; + + crate::dsl::into_readonly_table(lua, module) + .map(LuaValue::Table) +}} diff --git a/src/Rust/vvs_lua/src/libs/hashset.rs b/src/Rust/vvs_lua/src/libs/hashset.rs index 0ed0570d..f3477473 100644 --- a/src/Rust/vvs_lua/src/libs/hashset.rs +++ b/src/Rust/vvs_lua/src/libs/hashset.rs @@ -5,14 +5,9 @@ use mlua::{chunk, prelude::*}; crate::libs::required! { "hashset.lua": chunk! { local HashSet = {} - function HashSet.print(s) - if type(s) ~= "table" then error("expected a table") end - print(HashSet.tostring(s)) - end - HashSet.mt = { __add = function (a, b) - if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "set" then + if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "hashset" then error("attempt to `add' a hashset with a non-set value", 2) end local res = HashSet.new {} @@ -22,7 +17,7 @@ crate::libs::required! { "hashset.lua": chunk! { end, __mul = function (a, b) - if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "set" then + if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "hashset" then error("attempt to `mul' a hashset with a non-set value", 2) end local res = HashSet.new {} @@ -33,7 +28,7 @@ crate::libs::required! { "hashset.lua": chunk! { end, __tostring = function(hashset) - if type(hashset) ~= "table" then error("expected a table") end + if type(hashset) ~= "hashset" then error("expected a hashset") end local s = "{" local sep = "" for e in pairs(hashset) do @@ -44,14 +39,14 @@ crate::libs::required! { "hashset.lua": chunk! { end, __index = function (s, k) return rawget(s, k) ~= nil end, - __newindex = function (s, k, _) rawhashset(s, k, true) end, + __newindex = function (s, k, _) rawset(s, k, true) end, __metatable = "hashset", } function HashSet.new(t) if type(t) ~= "table" then error("expected a table") end local hashset = {} - hashsetmetatable(set, HashSet.mt) + setmetatable(hashset, HashSet.mt) for _, l in ipairs(t) do hashset[l] = true end return hashset end diff --git a/src/Rust/vvs_lua/src/libs/mod.rs b/src/Rust/vvs_lua/src/libs/mod.rs index 75ebb29d..faaed40d 100644 --- a/src/Rust/vvs_lua/src/libs/mod.rs +++ b/src/Rust/vvs_lua/src/libs/mod.rs @@ -1,10 +1,11 @@ //! Expose functionalities to VivyScript as modules that can be imported. +mod bit; mod hashset; mod rectangle; -mod vivy; +pub(super) mod vivy; -pub use self::vivy::{Vivy, VivyPtr}; +pub use self::vivy::Vivy; use mlua::{chunk, prelude::*}; macro_rules! required { @@ -22,9 +23,18 @@ macro_rules! required { } pub(self) use required; -pub fn import(lua: &Lua, name: LuaString) -> LuaResult<()> { - let name = core::str::from_utf8(name.as_bytes()) - .map_err(|err| LuaError::RuntimeError(format!("{err}")))?; +pub fn import(lua: &Lua, (name, table): (LuaString, LuaValue)) -> LuaResult<()> { + let name = core::str::from_utf8(name.as_bytes()).map_err(|err| LuaError::RuntimeError(format!("{err}")))?; + log::debug!(target: "lua", "trying to import module '{name}'"); + let table = match table { + LuaValue::Boolean(_) | LuaValue::Table(_) => table, + _ => { + return Err(LuaError::RuntimeError(format!( + "expected nil or a table, got: {}", + table.type_name() + ))) + } + }; if lua .load(chunk! { return vivy ~= nil and vivy:___is_already_imported($name) }) @@ -67,7 +77,7 @@ pub fn import(lua: &Lua, name: LuaString) -> LuaResult<()> { lua.load(chunk! { if $lib ~= nil then - rawset(_G, $name, $lib) + rawset(not $table and _G or $table, $name, $lib) end if vivy ~= nil then vivy:___mark_loaded($name) diff --git a/src/Rust/vvs_lua/src/libs/vivy.rs b/src/Rust/vvs_lua/src/libs/vivy.rs index a738639c..3bc5559c 100644 --- a/src/Rust/vvs_lua/src/libs/vivy.rs +++ b/src/Rust/vvs_lua/src/libs/vivy.rs @@ -1,662 +1,13 @@ //! Provides the vivy runtime. -use crate::{ - data::VivyDataRegisterPtr, - dsl, - func::VivyFuncRegisterPtr, - jobs::{VivyCalledJob, VivyJobRegisterPtr}, - lua_wrapper::{ - LuaAssContainerPtr, LuaAssLinePtr, LuaAssLinesPtr, LuaAssSyllabePtr, LuaAssSyllabesPtr, - LuaAssType, - }, - options::VivyOptionRegisterPtr, -}; -use mlua::{chunk, prelude::*}; -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - path::PathBuf, - rc::Rc, - sync::Arc, -}; -use vvs_ass::{ASSAuxTablePtr, ASSLine, ASSLines, ASSSyllabe, ASSSyllabes, ASSType}; -use vvs_utils::either; +mod byte_code; +mod graph; +mod runtime; -#[derive(Default, Debug)] -pub struct Vivy { - options: Option<VivyOptionRegisterPtr>, - data: Option<VivyDataRegisterPtr>, - jobs: Option<VivyJobRegisterPtr>, - func: Option<VivyFuncRegisterPtr>, - path: Vec<String>, +use mlua::prelude::*; +pub use runtime::Vivy; - variables: HashMap<String, LuaRegistryKey>, - is_in_job_evaluation: bool, - should_print_infos: bool, - main_once: bool, - - loaded: HashSet<String>, - loading: HashSet<String>, - current_module: Option<String>, -} - -pub type VivyPtr = Rc<RefCell<Vivy>>; - -#[derive(Debug)] -struct VivyExecGraph { - nodes: HashSet<String>, - edges: Vec<(String, Vec<String>, Rc<LuaRegistryKey>)>, - exits: Vec<String>, -} - -macro_rules! if_let_else_chain { - (@default: $default_expr: expr) => {{ $default_expr }}; - - ($var: ident <- $expr: expr => $var_expr: expr; - @default: $default_expr: expr - ) => { - if let Ok($var) = $expr { - $var_expr - } else { - $default_expr - } - }; - - ($var_first: ident <- $expr_first: expr => $var_expr_first: expr; - $($var: ident <- $expr: expr => $var_expr: expr);+; - @default: $default_expr: expr - ) => { - if let Ok($var_first) = $expr_first { - $var_expr_first - } else { if_let_else_chain! { - $($var <- $expr => $var_expr);+; - @default: $default_expr - } } - }; -} - -/// See [Vivy::execute_job] for an example on how to use this macro. -macro_rules! match_take { - ($expr: expr => - $(; $var: ident: $ty: ident => $var_expr: expr)+ - $(; @default => $default_expr: expr)? - ) => {{ - if_let_else_chain! { - $($var <- $expr.take::<$ty>() => $var_expr);+; - $(@default: $default_expr)? - } - }}; -} - -impl VivyExecGraph { - pub fn insert( - &mut self, - destination: impl ToString, - function: Rc<LuaRegistryKey>, - src: impl IntoIterator<Item = impl ToString>, - ) -> bool { - let dest = destination.to_string(); - let src: Vec<String> = src.into_iter().map(|str| str.to_string()).collect(); - log::debug!(target: "lua", "{dest:?} <--({function:?})--- {src:?}"); - if src.iter().filter(|src| either!(self.nodes.contains(src.as_str()) => false; { - log::error!(target: "lua", "source variable '{src}' is not present in the already assigned set"); - true - })).count().ne(&0) { - return false; - } else if !self.nodes.insert(destination.to_string()) { - log::error!(target: "lua", "re-assignation of variable '{dest}' detected"); - return false; - } - self.edges.push((dest, src, function)); - true - } -} - -impl Default for VivyExecGraph { - fn default() -> Self { - Self { - nodes: HashSet::from(["INIT".to_string()]), - edges: Default::default(), - exits: Default::default(), - } - } -} - -impl<'lua, I> TryFrom<(&'lua Lua, I, LuaValue<'lua>)> for VivyExecGraph -where - I: IntoIterator<Item = (usize, Option<LuaString<'lua>>, LuaValue<'lua>)>, -{ - type Error = LuaError; - - fn try_from((lua, work, ret): (&'lua Lua, I, LuaValue<'lua>)) -> Result<Self, Self::Error> { - let mut graph = Self { - exits: dsl::into_string_vec(lua, ret)?, - ..Default::default() - }; - for (idx, dest, src) in work { - let dest = dest.ok_or(LuaError::RuntimeError("".to_string()))?; - let dest = dest.to_string_lossy(); - let src = match src { - LuaValue::UserData(src) if src.is::<VivyCalledJob>() => { - src.take::<VivyCalledJob>().expect("very internal error") - } - _ => return Err(LuaError::RuntimeError("".to_string())), - }; - if !graph.insert(&dest, src.function, &src.arguments) { - return Err(LuaError::RuntimeError(format!( - "invalid instruction n°{} in main block, edge was: '{dest:?} <- {:?}'", - idx, src.arguments - ))); - } - } - let invalids = graph.exits.iter().filter(|exit| { - either!(graph.nodes.contains(exit.as_str()) => false; { - log::error!(target: "lua", "invalid exit node '{exit}', variable was never assigned"); - true - }) - }); - if invalids.count().ne(&0) { - Err(LuaError::RuntimeError(format!( - "invalid exit nodes for execution graph: {graph:#?}" - ))) - } else { - Ok(graph) - } - } -} - -impl Vivy { - /// Create a new instance of the vivy module, with a specified include path. - pub fn new_with_path(path: impl IntoIterator<Item = impl AsRef<str>>) -> VivyPtr { - Rc::new(RefCell::new(Self { - path: path - .into_iter() - .map(|str| str.as_ref().to_string()) - .collect(), - ..Default::default() - })) - } - - /// Create a new default ASS element of the asked type. - pub fn new_ass_element<'lua>(&self, lua: &'lua Lua, ty: ASSType) -> LuaResult<LuaValue<'lua>> { - macro_rules! new { - ($elem: ident) => { - paste::paste! {{ - let elem = [< ASS $elem >] ::default().into_ptr(); - elem.borrow_mut().aux = self.new_ass_aux_table(ty); - Ok( [< LuaAss $elem Ptr >] ::from(elem).to_lua(lua)?) - }} - }; - } - match ty { - ASSType::Lines => new!(Lines), - ASSType::Line => new!(Line), - ASSType::Syllabes => new!(Syllabes), - ASSType::Syllabe => new!(Syllabe), - } - } - - /// Create a new default ASS element aux table of the asked type. The table is filled with the - /// default options for the said element. - pub fn new_ass_aux_table(&self, ty: ASSType) -> ASSAuxTablePtr { - self.data - .as_ref() - .map(|data| data.borrow().get_table(ty)) - .unwrap_or_default() - .into_inner() - } - - /// Pack the arguments for a job, check for correct type, coerce if needed, etc. - fn pack_job_arguments<'lua>( - &self, - lua: &'lua Lua, - input: ASSType, - args: Vec<LuaString<'lua>>, - ) -> LuaResult<Vec<LuaValue<'lua>>> { - let (vars, errs): (Vec<_>, Vec<_>) = args - .into_iter() - .map(|arg| match self.variables.get(arg.to_str()?) { - Some(var) => match lua.registry_value::<LuaValue>(var)? { - var @ LuaValue::UserData(_) => Ok(var), - var => Err(LuaError::RuntimeError(format!( - "invalid type '{}' for job argument '{}'", - var.type_name(), - arg.to_str()? - ))), - }, - None => Err(LuaError::RuntimeError(format!( - "failed to find variable '{}' in execution state", - arg.to_str()? - ))), - }) - .partition(Result::is_ok); - if let Some(Err(err)) = errs.into_iter().next() { - return Err(err); - } - let mut args: Vec<LuaValue> = Default::default(); - for var in vars.into_iter().map(Result::unwrap) { - let LuaValue::UserData(var) = var else { - panic!("we should have a user data type here, got {}", var.type_name()) - }; - match input { - ASSType::Lines | ASSType::Line => match_take! { var => - ; var: LuaAssLinePtr => { args.push(var.to_lua(lua)?); continue } - ; var: LuaAssLinesPtr => { - for var in &var.into_inner().borrow().content { - args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); - } - continue - } - ; var: LuaAssContainerPtr => { - for var in &var.into_inner().borrow().lines.borrow().content { - args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); - } - continue - } - ; @default => { return Err(LuaError::RuntimeError( - "expected a line or lines, got syllabe or syllabes".to_string() - )) } - }, - ASSType::Syllabes | ASSType::Syllabe => match_take! { var => - ; var: LuaAssSyllabePtr => { args.push(var.to_lua(lua)?); continue } - ; var: LuaAssSyllabesPtr => { - for var in &var.into_inner().borrow().content { - args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); - } - continue - } - ; var: LuaAssLinePtr => { - for var in &var.into_inner().borrow().content.borrow().content { - args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); - } - continue - } - ; var: LuaAssLinesPtr => { - for line in var.into_inner().borrow().content.iter() { - for var in &line.borrow().content.borrow().content { - args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); - } - } - continue - } - ; var: LuaAssContainerPtr => { - for line in var.into_inner().borrow().lines.borrow().content.iter() { - for var in &line.borrow().content.borrow().content { - args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); - } - } - continue - } - ; @default => { return Err(LuaError::RuntimeError( - "expected a syllabe or syllabes, got line or lines".to_string() - )) } - }, - }; - } - match input { - ASSType::Lines | ASSType::Syllabes => { - log::debug!(target: "lua", "we already have flattened the input elements"); - Ok(args) - } - ASSType::Line | ASSType::Syllabe => { - log::debug!(target: "lua", "we need to pack the flattened elements into a parent element"); - let mut elem = self.new_ass_element(lua, input)?; - for arg in args { - elem = lua - .load(chunk! { local elem = $elem; elem.push($arg); return elem }) - .eval()?; - } - Ok(vec![elem]) - } - } - } - - fn unpack_job_returns<'lua>( - &self, - lua: &'lua Lua, - name: &str, - returns: ASSType, - output: impl Iterator<Item = LuaValue<'lua>>, - ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { - fn unwrap(var: LuaValue) -> LuaAnyUserData { - match var { - LuaValue::UserData(var) => var, - _ => unreachable!(), - } - } - - fn handle_user_data<'lua>( - lua: &'lua Lua, - returns: ASSType, - var: LuaAnyUserData<'lua>, - ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { - match returns { - ASSType::Lines | ASSType::Line => match_take! { var => - ; var: LuaAssLinePtr => Ok(vec![unwrap(var.to_lua(lua)?)]) - ; var: LuaAssLinesPtr => Ok(var.into_inner().borrow().content.iter().map(|line| - unwrap(LuaAssLinePtr::from(line.clone()).to_lua(lua).unwrap()) - ).collect()) - ; @default => Err(LuaError::RuntimeError( - "expected a line or lines, got syllabe or syllabes".to_string() - )) - }, - ASSType::Syllabes | ASSType::Syllabe => match_take! { var => - ; var: LuaAssSyllabePtr => Ok(vec![unwrap(var.to_lua(lua)?)]) - ; var: LuaAssSyllabesPtr => Ok(var.into_inner().borrow().content.iter().map(|line| - unwrap(LuaAssSyllabePtr::from(line.clone()).to_lua(lua).unwrap()) - ).collect()) - ; @default => Err(LuaError::RuntimeError( - "expected a syllabe or syllabes, got line or lines".to_string() - )) - }, - } - } - - let (output, errs): (Vec<_>, Vec<_>) = output - .map(|out: LuaValue<'lua>| match out { - LuaValue::UserData(out) => handle_user_data(lua, returns, out), - LuaValue::Table(outs) => { - let (outs, errs): (Vec<_>, Vec<_>) = outs - .sequence_values() - .map(|item| handle_user_data(lua, returns, item?)) - .partition(Result::is_ok); - if let Some(Err(err)) = errs.into_iter().next() { - return Err(err); - } - Ok(outs.into_iter().flat_map(Result::unwrap).collect()) - } - _ => Err(LuaError::RuntimeError(format!( - "invalid return type for job '{name}', got a value of type: {}", - out.type_name() - ))), - }) - .partition(Result::is_ok); - if let Some(Err(err)) = errs.into_iter().next() { - return Err(err); - } - let (output, errs): (Vec<_>, Vec<_>) = output.into_iter().partition(Result::is_ok); - if let Some(Err(err)) = errs.into_iter().next() { - return Err(err); - } - let output = output.into_iter().flat_map(Result::unwrap).collect(); - - match returns { - ASSType::Line | ASSType::Syllabe => Ok(output), - ty @ ASSType::Lines | ty @ ASSType::Syllabes => { - let mut elem = self.new_ass_element(lua, ty)?; - for out in output { - elem = lua - .load(chunk! { - local elem = $elem - elem.push($out) - elem - }) - .eval()?; - } - Ok(vec![unwrap(elem)]) - } - } - } - - /// Execute a job with the named arguments. The types of the arguments will be checked and it - /// will be up to this function to create the apply loop if needed. The returned values will - /// also be flattened if needed. - fn execute_job<'lua>( - &mut self, - lua: &'lua Lua, - name: &str, - job: LuaFunction<'lua>, - input: ASSType, - returns: ASSType, - args: Vec<LuaString<'lua>>, - ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { - let (output, errs): (Vec<_>, Vec<_>) = self - .pack_job_arguments(lua, input, args)? - .into_iter() - .map(|arg| { - let ret = job.call::<LuaValue, LuaValue>(arg); - self.is_in_job_evaluation = true; - ret - }) - .partition(Result::is_ok); - if let Some(Err(err)) = errs.into_iter().next() { - return Err(LuaError::CallbackError { - traceback: format!("error found while executing job '{name}'"), - cause: Arc::new(err), - }); - } - self.unpack_job_returns(lua, name, returns, output.into_iter().map(Result::unwrap)) - } -} - -impl LuaUserData for Vivy { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - macro_rules! add_setter { - ($n: ident: $p: ident; - $($name: ident: $ptr: ident);+$(;)? - ) => { - add_setter! { $n: $p } - add_setter! { $($name: $ptr);+ } - }; - - ($name: ident: $ptr: ident $(;)?) => { - methods.add_method_mut(concat!("___set_", stringify!($name)), |_, this, $name: $ptr| { - this.$name = Some($name); - Ok(()) - }); - }; - } - - macro_rules! add_getter { - ($n: literal: $t: ident => $e: expr; - $($name: literal: $this: ident => $expr: expr);+$(;)? - ) => { - add_getter! { $n: $t => $e } - add_getter! { $($name: $this => $expr);+ } - }; - - ($name: literal: $this: ident => $expr: expr $(;)?) => { - methods.add_method($name, |_, $this, ()| Ok($expr)); - }; - } - - add_setter! { - options: VivyOptionRegisterPtr; - data: VivyDataRegisterPtr; - jobs: VivyJobRegisterPtr; - func: VivyFuncRegisterPtr; - } - - add_getter! { - "___get_loaded": this => this.loaded.clone(); - "___get_path": this => this.path.clone(); - "___is_in_job_evaluation_mode": this => this.is_in_job_evaluation; - } - - methods.add_method_mut("___append_to_path", |_, this, pattern: String| { - this.path.push(pattern); - Ok(()) - }); - - methods.add_method_mut("___mark_loaded", |_, this, module: String| { - this.loading.remove(&module); - Ok(()) - }); - - methods.add_method_mut("___set_current_module", |_, this, module: LuaValue| { - this.current_module = match module { - LuaValue::String(module) => Some(module.to_string_lossy().to_string()), - _ => None, - }; - Ok(()) - }); - - methods.add_method("___current_module", |lua, this, ()| { - Ok(match this.current_module { - Some(ref current_module) => current_module.clone().to_lua(lua)?, - None => LuaValue::Nil, - }) - }); - - methods.add_method("___run_export_module", |lua, this, name: LuaString| { - let table = lua.create_table()?; - this.jobs - .as_ref() - .map(|jobs| jobs.borrow_mut().export(lua, &name, &table)) - .unwrap_or(Ok(()))?; - this.func - .as_ref() - .map(|func| func.borrow_mut().export(lua, &name, &table)) - .unwrap_or(Ok(()))?; - let table = dsl::into_readonly_table(lua, name.clone(), table)?; - lua.globals().raw_set(name, table)?; - - Ok(()) - }); - - methods.add_method_mut( - "___execute_job", - |lua, - this, - (name, job, args, input, returns): ( - LuaString, - LuaFunction, - Vec<LuaString>, - LuaAssType, - LuaAssType, - )| { - this.execute_job( - lua, - name.to_str()?, - job, - input.into_inner(), - returns.into_inner(), - args, - ) - }, - ); - - methods.add_method_mut("___import", |lua, this, name: String| -> LuaResult<bool> { - if this.loading.contains(&name) { - return Err(LuaError::RuntimeError(format!( - "circular dependency found with module '{name}'" - ))); - } - this.loading.insert(name.clone()); - this.path - .iter() - .find_map(|path| { - let path = PathBuf::from(path.replace('?', &name)); - let was_lua_file = path - .extension() - .map(|ext| ext.eq("lua")) - .unwrap_or_default(); - (path.exists() && path.is_file()).then(|| { - let path_name = path.to_string_lossy(); - match lua.load(&path).set_name(&path_name)?.exec() { - Ok(()) => { - this.loaded.insert(name.clone()); - Ok(was_lua_file) - } - Err(err) => Err(LuaError::RuntimeError(format!( - "failed to load module '{name}' at location: {path_name}\n{err}" - ))), - } - }) - }) - .unwrap_or(Err(LuaError::RuntimeError(format!( - "failed to find or load module named '{name}' in path: {:#?}", - this.path - )))) - }); - - methods.add_method_mut("___is_already_imported", |_, this, module: String| { - Ok(this.loaded.contains(&module)) - }); - - methods.add_method_mut("___switch_to_execution_mode", |_, this, ()| { - this.is_in_job_evaluation - .then(|| { - this.is_in_job_evaluation = false; - Ok(()) - }) - .unwrap_or_else(|| { - Err(LuaError::RuntimeError( - "already in execution mode, internal error".to_string(), - )) - }) - }); - - methods.add_method_mut("___print_info", |_, this, ()| { - this.should_print_infos = true; - Ok(()) - }); - - methods.add_method_mut("___main", |lua, this, table: LuaTable| -> LuaResult<()> { - if this.main_once { - return Err(LuaError::RuntimeError( - "multiple 'main' blocks detected".to_string(), - )); - } - - // Finish setup - if let Some(ref options) = this.options { - let options = options.borrow(); - for option in options.iter_registered() { - let value = match options.resolve(option) { - Some(value) => value.to_lua(lua)?, - None => LuaValue::Nil, - }; - lua.globals().raw_set(option.as_str(), value)?; - } - } - - if let Some(ref data) = this.data { - data.borrow_mut().compute_cached_tables() - } - - // Print infos - if this.should_print_infos { - println!(" <<< Information Report >>>"); - println!(" # Misc"); - println!( - " - version {}", - lua.globals().get::<_, String>("_VERSION")? - ); - println!(" - packages {}", { - let mut packages = this.loaded.iter().cloned().collect::<Vec<_>>(); - packages.sort(); - let packages = packages.join(", "); - packages - }); - if let Some(options) = this.options.as_ref() { - options.borrow().print_info(); - } - if let Some(data) = this.data.as_ref() { - data.borrow().print_info(); - } - this.should_print_infos = false; - } - - // Do the work - this.is_in_job_evaluation = true; - let table_last_idx = table.raw_len() - 1; - let (work, ret): (Vec<_>, Vec<_>) = table - .pairs::<LuaValue, LuaValue>() - .enumerate() - .flat_map(|(idx, pair)| { - let (dest, src) = pair.ok()?; - Some((idx, lua.coerce_string(dest).unwrap_or_default(), src)) - }) - .partition(|(idx, ..)| i64::try_from(*idx).unwrap() != table_last_idx); - let [(_, _, ret)] = &ret[..] else { unreachable!("invalid return statements, should only be one: {ret:#?}") }; - let exec_graph = VivyExecGraph::try_from((lua, work, ret.clone()))?; - log::error!(target: "lua", "implement execution logic for: {exec_graph:#?}"); - this.is_in_job_evaluation = false; - Ok(()) - }); - } -} +pub(crate) use runtime::{ASSFileOrInstance, RuntimeCreationData, RuntimeMode, RuntimeOptions}; crate::libs::required! { lua => lua .create_userdata(Vivy::new_with_path(["./?.vvl", "./?.lua"])) diff --git a/src/Rust/vvs_lua/src/libs/vivy/byte_code.rs b/src/Rust/vvs_lua/src/libs/vivy/byte_code.rs new file mode 100644 index 00000000..10cb96d4 --- /dev/null +++ b/src/Rust/vvs_lua/src/libs/vivy/byte_code.rs @@ -0,0 +1,331 @@ +use crate::{ + func::VivyFunc, + jobs::VivyJob, + lua_wrapper::{LuaAssAuxValue, LuaAssType}, +}; +use mlua::{chunk, prelude::*}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::SystemTime, +}; + +/// The bytecode of a function, with its up-values. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(super) struct FunctionByteCode { + content: Vec<u8>, + upvalues: HashMap<i64, FunctionByteCode>, +} + +/// The name of an exported symbol. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub(super) struct SymbolName { + pub module: String, + pub name: String, +} + +/// An exported symbol. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub(super) enum Symbol { + Function { + bytecode: FunctionByteCode, + }, + + Job { + bytecode: FunctionByteCode, + input_type: LuaAssType, + output_type: LuaAssType, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub(super) struct Module { + pub name: String, + pub path: Option<PathBuf>, + pub mtime: Option<SystemTime>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScriptByteCode { + /// The path to the main vivy script file. + pub(super) path: PathBuf, + + /// The last modification time of the vivy script file, at the time this whole structure was + /// created. + pub(super) mtime: Option<SystemTime>, + + /// List of imported modules, the import order is not relevent, they will be re-imported in + /// other lua states if needed. + pub(super) imports: Vec<Module>, + + /// List of callable symbols. + pub(super) symbols: HashMap<SymbolName, Symbol>, + + /// Stores the options and constants that must registered in the global state in an imutable + /// way. + pub(super) globals: HashMap<String, LuaAssAuxValue>, + + /// Stores the auxiliary data on ASS types and their default values. + pub(super) data: Vec<(String, LuaAssType, LuaAssAuxValue)>, +} + +impl FunctionByteCode { + pub fn try_new( + getters @ (upvalues_getter, func_bc_dump): (&LuaRegistryKey, &LuaRegistryKey), + lua: &Lua, + func: LuaFunction, + ) -> LuaResult<Self> { + let (upvalues_getter, func_bc_dump) = ( + lua.registry_value::<LuaFunction>(upvalues_getter)?, + lua.registry_value::<LuaFunction>(func_bc_dump)?, + ); + let (content, upvalues): (LuaString, LuaTable) = lua + .load(chunk! { return $func_bc_dump($func), $upvalues_getter($func) }) + .eval()?; + Ok(Self { + content: content.as_bytes().to_vec(), + upvalues: HashMap::from_iter(upvalues.pairs::<i64, LuaFunction>().filter_map(|pair| { + let (idx, function) = pair.ok()?; + Some((idx, Self::try_new(getters, lua, function).ok()?)) + })), + }) + } +} + +impl FromStr for SymbolName { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.split_once('.') { + Some((module, name)) if !module.is_empty() && !name.is_empty() => { + Ok(Self { module: module.to_string(), name: name.to_string() }) + } + Some(_) => Err(format!("invalid symbol name found: {s}")), + None => Err(format!("can't find the module name in the exported symbol: {s}")), + } + } +} + +impl std::fmt::Display for SymbolName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.module, self.name) + } +} + +impl Module { + /// Create a module entry from a path, this is an external module. + pub fn from_file(name: impl ToString, path: impl Into<PathBuf>) -> Self { + let path = path.into(); + Self { + name: name.to_string(), + mtime: std::fs::metadata(&path) + .map(|mt| mt.modified().ok()) + .unwrap_or_default(), + path: Some(path), + } + } + + /// Create a module entry from just a name, this is an internal module. + pub fn from_internal(name: impl ToString) -> Self { + Self { name: name.to_string(), path: None, mtime: None } + } + + /// Get the path of the module if it is an external one. + pub fn path(&self) -> Option<&Path> { + self.path.as_ref().map(|path| path.as_ref()) + } + + /// Get the name of the module. + #[allow(dead_code)] + pub fn name(&self) -> &str { + &self.name + } + + /// Get the modification time of the file used to build this module. Note that internal modules + /// don't have modification times and even if the module is external it may not have a + /// modification time if the platform doesn't support modification times... + pub fn mtime(&self) -> Option<SystemTime> { + self.mtime + } + + /// Was the module a lua file? Can be true only if this is an external module. + pub fn was_lua_file(&self) -> bool { + self.path + .as_ref() + .map(|path| path.extension().map(|ext| ext.eq("runtime")).unwrap_or_default()) + .unwrap_or_default() + } + + /// Was the module a vivy file? Vivy files can only be external ones and we can only parse vivy + /// or lua files. + pub fn was_vivy_file(&self) -> bool { + self.is_external() && !self.was_lua_file() + } + + /// Is the module an internal one? + pub fn is_internal(&self) -> bool { + !self.is_external() + } + + /// Is the module an external one? + pub fn is_external(&self) -> bool { + self.path.is_some() + } + + /// Tells whether the byte code is up to date with the file it was generated from. The rules + /// are the following: + /// - if the byte code has no storage file, then it is an internal module, we consider that we + /// have ABI compatibility (this is a bold statement that will break at some point). + /// - if the byte code has no mtime, we can't check so we need to recompute the bytecode. + /// - if we have a mtime and a path that have a valide mtime, we check if the file was modified + /// after the production of the byte code, if it's not the case we don't have to recompute + /// the byte code. In any other case we must recompute the byte code. + pub fn was_modified(&self) -> bool { + let ret = match (self.path(), self.mtime()) { + (Some(path), Some(mtime)) => matches!(std::fs::metadata(path), + Ok(mt) if matches!(mt.modified(), Ok(fmtime) if fmtime > mtime)), + _ => false, + }; + if ret { + log::error!(target: "runtime", "module '{}' was modified", self.name); + } + ret + } +} + +impl ScriptByteCode { + /// Same logic as [Module::was_modified]. But we consider the script byte code outdated if any + /// of its dependency changed. + pub fn was_modified(&self) -> bool { + let main_was_modified = matches!((std::fs::metadata(&self.path), self.mtime), + (Ok(mt), Some(mtime)) if matches!(mt.modified(), Ok(fmtime) if fmtime > mtime), + ); + if main_was_modified { + log::error!(target: "runtime", "main module '{}' was modified", self.path.to_string_lossy()); + } + let imports_where_modified = self.imports.iter().any(|module| module.was_modified()); + main_was_modified || imports_where_modified + } +} + +impl<'lua> ToLua<'lua> for FunctionByteCode { + fn to_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> { + let Self { content, upvalues } = self; + let (set_upvalue, loadstring): (LuaFunction, LuaFunction) = ( + lua.named_registry_value("_set_upvalue")?, + lua.named_registry_value("_loadstring")?, + ); + let content = lua.create_string(&content)?; + let content: LuaFunction = lua.load(chunk! { $loadstring($content) }).eval()?; + for (idx, bytecode) in upvalues { + set_upvalue.call((content.clone(), idx, bytecode.to_lua(lua)?))?; + } + Ok(LuaValue::Function(content)) + } +} + +impl<'lua> ToLua<'lua> for ScriptByteCode { + /// The script bytecode will be loaded as a table, where each item in the table will be one + /// module where the key is the name of the module. + /// + /// In addition to the modules, we have: + /// - The globals (options, constants, etc) will be in the special field `_CONSTANTS`. It won't + /// collide with modules because the underscore is not allowed in module names. + /// - For each module, the functions will be stored in the `_FUNCTIONS` field, and the jobs in + /// the `_JOBS` field. + /// - The extensions of the ASS elements are stored in the `_ASS` field as a sequence. + /// + /// For example, we may get: + /// + /// ```lua,no_run,skip + /// { + /// "_CONSTANTS": { "a": 1, ... } + /// "_ASS": { 1: { "line", "integer", 0 }, ... } + /// "toto": { "_FUNCTIONS": { "b": ..., ... }, "_JOBS": { ... } } + /// "foo": { } + /// } + /// ``` + fn to_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> { + if self.was_modified() { + return Err(LuaError::RuntimeError(format!( + "can't restore state for script '{}', files where modified after the bytecode was generated", + self.path.to_string_lossy() + ))); + } + let (state, state_ass, state_constants) = (lua.create_table()?, lua.create_table()?, lua.create_table()?); + + // Set constants and ASS extensions + for (name, value) in self.globals { + state_constants.raw_set(name, value)?; + } + for (name, ass_type, value) in self.data { + let element = lua.create_table()?; + element.raw_push(name)?; + element.raw_push(ass_type)?; + element.raw_push(value)?; + state_ass.raw_push(element)?; + } + + // Import needed modules, i.e. internal ones and lua ones, the vvl ones are already dumped + // and we just need to load the bytecode. + for module in self.imports { + if !module.was_vivy_file() { + // Import the module because it couldn't be dumped (pure lua files and internal modules) + if module.was_modified() { + return Err(LuaError::RuntimeError(format!( + "can't restore state for module '{}', file was modified after bytecode generation", + module.name + ))); + } + let name = module.name; + lua.load(chunk! { import($name) }).exec()?; + } else { + // Create the table, will be populated latter. + let table = lua.create_table()?; + table.raw_set("_FUNCTIONS", lua.create_table()?)?; + table.raw_set("_JOBS", lua.create_table()?)?; + state.raw_set(module.name, table)?; + } + } + + // Load functions and jobs from dumped modules. + for (SymbolName { module, name }, symbol) in self.symbols { + log::debug!(target: "runtime", "load symbol '{module}.{name}': {symbol:?}"); + let module_table: LuaTable = state.raw_get(module.as_str())?; + match symbol { + Symbol::Function { bytecode } => { + module_table.raw_get::<_, LuaTable>("_FUNCTIONS")?.raw_set( + name.as_str(), + VivyFunc { + module, + name: name.clone(), + doc: None, + function: Arc::new(lua.create_registry_value(bytecode.to_lua(lua)?)?), + }, + )?; + } + + Symbol::Job { bytecode, input_type, output_type } => { + module_table.raw_get::<_, LuaTable>("_JOBS")?.raw_set( + name.as_str(), + VivyJob { + module, + name: name.clone(), + doc: None, + input_type: input_type.into_inner(), + return_type: output_type.into_inner(), + function: Arc::new(lua.create_registry_value(bytecode.to_lua(lua)?)?), + }, + )?; + } + } + } + + state.raw_set("_ASS", state_ass)?; + state.raw_set("_CONSTANTS", state_constants)?; + Ok(LuaValue::Table(state)) + } +} diff --git a/src/Rust/vvs_lua/src/libs/vivy/graph.rs b/src/Rust/vvs_lua/src/libs/vivy/graph.rs new file mode 100644 index 00000000..9af8795d --- /dev/null +++ b/src/Rust/vvs_lua/src/libs/vivy/graph.rs @@ -0,0 +1,171 @@ +use crate::{dsl, jobs::VivyCalledJob}; +use mlua::prelude::*; +use scc::HashSet as SccHashSet; +use std::{collections::HashSet as StdHashSet, sync::Arc}; +use thiserror::Error; +use vvs_utils::either; + +/// Execution graph deduced from the user inputs. +/// The returned item will be of the form: +/// - destination variable name +/// - input names +/// - function registry key +/// The iterator supports multi-threading in the sens that: +/// - When queried it will returns a job that can be executed by knowing which jobs are left and +/// which variables are already computed. +/// - If no work is available but jobs still need to be executed, an error is returned: +/// [VEGIterError::Again] +#[derive(Debug)] +pub struct VivyExecGraph { + nexts: SccHashSet<Arc<(String, VivyCalledJob)>>, + exits: SccHashSet<String>, + computed: SccHashSet<String>, +} + +#[derive(Debug, Error)] +pub enum VivyExecGraphIteratorError { + /// If no work is available but jobs still need to be executed. + #[error("check latter for available work")] + Again, + + /// Work completed for the specified exit nodes, even if some jobs can be executed... + #[error( + "some jobs can be executed, but the minimal things to do has already been done for the output to be correct" + )] + Completed, + + /// Like [Self::Completed], but we have no more work to do. + #[error("the iterator is empty and all the needed values have been computed for the result to be complete")] + Empty, + + /// The work is incomplete, all needed variables where not produced. + #[error("the iterator is empty but not all needed variables have been computed for the output to be correct")] + Incomplete, +} + +impl VivyExecGraph { + /// Get the next job to execute, or an error... + pub(crate) fn next(&self) -> Result<(String, VivyCalledJob), VivyExecGraphIteratorError> { + use VivyExecGraphIteratorError::*; + if self.nexts.is_empty() { + Err(either!(self.unfulfilled_exits() != 0 => Incomplete; Empty)) + } else if self.unfulfilled_exits() == 0 { + Err(Completed) + } else { + self.pop_next_edge() + .map(|ptr| Arc::try_unwrap(ptr).expect("we should only have one strong ref")) + .ok_or(Again) + } + } + + /// Marks the computation of a variable as "completed". If the variable was really marked as + /// completed for the first time, then returns true, returns false otherwise. + pub fn completed(&self, var: impl ToString) -> bool { + self.computed + .insert(var.to_string()) + .map_err(|var| log::error!(target: "runtime", "variable {var:?} was already completed")) + .is_ok() + } + + /// Tells whever a variable was already completed (see [Self::completed]). + fn is_computed(&self, var: &str) -> bool { + self.computed.contains(var) + } + + /// The number of the exit variables that are not completed (see [Self::completed]). + fn unfulfilled_exits(&self) -> usize { + let mut ret = 0; + self.exits.for_each(|var| ret += (!self.is_computed(var)) as usize); + ret + } + + /// Get the next edges that can be executed. + fn pop_next_edge(&self) -> Option<Arc<(String, VivyCalledJob)>> { + let mut ret = None; + self.nexts.scan(|val| { + if ret.is_none() && val.1.arguments.iter().all(|var| self.is_computed(var)) { + ret = Some(val.clone()); + } + }); + log::debug!(target: "vvs", "next edge is {ret:?}, try to remove it if it was not already picked up by another thread"); + ret.into_iter().flat_map(|ret| self.nexts.remove(&ret)).next() + } +} + +impl<'lua, I> TryFrom<(&'lua Lua, I, LuaValue<'lua>)> for VivyExecGraph +where + I: IntoIterator<Item = (usize, Option<LuaString<'lua>>, LuaValue<'lua>)>, +{ + type Error = LuaError; + + fn try_from((lua, work, ret): (&'lua Lua, I, LuaValue<'lua>)) -> Result<Self, Self::Error> { + #[derive(Debug, Default)] + struct Builder { + nodes: StdHashSet<String>, + edges: Vec<(String, VivyCalledJob)>, + exits: Vec<String>, + } + + impl Builder { + fn insert(&mut self, destination: impl ToString, function: VivyCalledJob) -> bool { + let dest = destination.to_string(); + if function.arguments.iter().filter(|src| { + either!(self.nodes.contains(src.as_str()) => false; { + log::error!(target: "runtime", "source variable '{src}' is not present in the already assigned set"); + true + }) + }).count().ne(&0) { + false + } else if !self.nodes.insert(dest.clone()) { + log::error!(target: "runtime", "re-assignation of variable '{dest}' detected"); + false + } else { + self.edges.push((dest, function)); + true + } + } + } + + let mut graph = Builder { exits: dsl::into_string_vec(lua, ret)?, ..Default::default() }; + for (idx, dest, src) in work { + let dest = dest.ok_or(LuaError::RuntimeError("".to_string()))?; + let dest = dest.to_string_lossy(); + let src = match src { + LuaValue::UserData(src) if src.is::<VivyCalledJob>() => { + src.take::<VivyCalledJob>().expect("very internal error") + } + _ => return Err(LuaError::RuntimeError("".to_string())), + }; + if !graph.insert(&dest, src) { + return Err(LuaError::RuntimeError(format!( + "invalid instruction n°{idx} in main block, destination variable was: {dest:?}" + ))); + } + } + let invalids = graph.exits.iter().filter(|exit| { + either!(graph.nodes.contains(exit.as_str()) => false; { + log::error!(target: "runtime", "invalid exit node '{exit}', variable was never assigned"); + true + }) + }); + if invalids.count().ne(&0) { + Err(LuaError::RuntimeError(format!( + "invalid exit nodes for execution graph: {graph:#?}" + ))) + } else { + let (exits, nexts, computed) = ( + SccHashSet::with_capacity(graph.exits.len()), + SccHashSet::with_capacity(graph.edges.len()), + SccHashSet::with_capacity(graph.edges.len()), + ); + for var in graph.exits { + exits.insert(var).expect("very internal error"); + } + for var in graph.edges { + nexts.insert(Arc::new(var)).expect("very internal error"); + } + computed.insert("INIT".to_string()).expect("very internal error"); + Ok(Self { computed, nexts, exits }) + } + } +} diff --git a/src/Rust/vvs_lua/src/libs/vivy/runtime.rs b/src/Rust/vvs_lua/src/libs/vivy/runtime.rs new file mode 100644 index 00000000..dc81c86f --- /dev/null +++ b/src/Rust/vvs_lua/src/libs/vivy/runtime.rs @@ -0,0 +1,1061 @@ +use crate::{ + data::{VivyDataRegister, VivyDataRegisterPtr}, + dsl, + func::{VivyFuncRegister, VivyFuncRegisterPtr}, + jobs::{VivyCalledJob, VivyJobRegister, VivyJobRegisterPtr}, + libs::vivy::{ + byte_code::{FunctionByteCode, Module, ScriptByteCode, Symbol, SymbolName}, + graph::{VivyExecGraph, VivyExecGraphIteratorError}, + }, + lua_wrapper::{ + LuaAssAuxValue, LuaAssContainerPtr, LuaAssLinePtr, LuaAssLines, LuaAssSyllabePtr, LuaAssSyllabes, LuaAssType, + }, + options::{VivyOptionRegister, VivyOptionRegisterPtr}, + TomlOptions, +}; +use mlua::{chunk, prelude::*}; +use scc::HashMap as SccHashMap; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + sync::Arc, + time::SystemTime, +}; +use vvs_ass::{ASSAuxTable, ASSLine, ASSSyllabe, ASSType, ASSTYPE_VALUES}; + +/// We have different ways of passing the ASS file we want to handle, either it's not already +/// parsed and we have a file path, or it was already parsed/constructed and we have its in-memory +/// representation. +#[derive(Debug, Clone)] +pub(crate) enum ASSFileOrInstance { + /// The path to the ASS file, parsing it may fail and errors should be handled at the vivy + /// runtime creation. + File(PathBuf), + + /// The ASS file was already parsed. It is a valide ASS representation. + Instance(LuaAssContainerPtr), +} + +/// The mode of the runtime to create. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RuntimeMode { + /// The full runtime, with DSL keywords, used to parse modules' definitions. + #[default] + Full, + + /// The minimal runtime, used for executing what was parsed and dumped. + Compute, + + /// Like the full runtime, but for REPL (some restrictions are lifted like you can declare + /// global variables, etc). + Interactive, +} + +/// What to use to create the runtime. +#[derive(Debug)] +pub(crate) enum RuntimeCreationData { + /// Init a brand new runtime with only a toml file. + TomlFile(TomlOptions), + + /// Init from a dumped state. + ByteCode(ScriptByteCode), +} + +#[derive(Default, Debug)] +pub(crate) struct RuntimeOptions { + /// The ASS file to use. + pub ass_file: Option<ASSFileOrInstance>, + + /// What to use to init the runtime. + pub creation_data: Option<RuntimeCreationData>, + + /// The include path specified by the user, will be appened after the $PWD and $BASEDIR. + pub include_path: Vec<PathBuf>, + + /// The mode of the runtime to create, depends on its usage. + pub mode: RuntimeMode, +} + +#[derive(Default, Debug)] +pub struct Vivy { + options: Option<VivyOptionRegisterPtr>, + data: Option<VivyDataRegisterPtr>, + jobs: Option<VivyJobRegisterPtr>, + func: Option<VivyFuncRegisterPtr>, + path: Vec<String>, + + variables: SccHashMap<String, Arc<LuaRegistryKey>>, + upvalues_getter: Option<LuaRegistryKey>, + func_bc_dump: Option<LuaRegistryKey>, + + ass_file: Option<ASSFileOrInstance>, + + should_print_infos: bool, + main_once: bool, + + // We refcell those because we want to avoid the reborow mut of the import process. + loaded: RefCell<HashMap<String, Module>>, + loading: RefCell<HashSet<String>>, + current_module: RefCell<Option<String>>, + + script_path: Option<PathBuf>, + script_path_mtime: Option<SystemTime>, +} + +macro_rules! if_let_else_chain { + (@default: $default_expr: expr) => {{ $default_expr }}; + + ($var: ident <- $expr: expr => $var_expr: expr; + @default: $default_expr: expr + ) => { + if let Ok($var) = $expr { $var_expr } else { $default_expr } + }; + + ($var_first: ident <- $expr_first: expr => $var_expr_first: expr; + $($var: ident <- $expr: expr => $var_expr: expr);+; + @default: $default_expr: expr + ) => { + if let Ok($var_first) = $expr_first { + $var_expr_first + } else { if_let_else_chain! { + $($var <- $expr => $var_expr);+; + @default: $default_expr + } } + }; +} + +/// See [Vivy::execute_job] for an example on how to use this macro. +macro_rules! match_borrow { + ($expr: expr => + $(; $var: ident: $ty: ident => $var_expr: expr)+ + $(; @default => $default_expr: expr)? + ) => {{ + if_let_else_chain! { + $($var <- $expr.borrow::<$ty>() => $var_expr);+; + $(@default: $default_expr)? + } + }}; +} + +impl Vivy { + /// Create a new instance of the vivy module, with a specified include path. + pub fn new_with_path(path: impl IntoIterator<Item = impl AsRef<str>>) -> Self { + Self { + path: path.into_iter().map(|str| str.as_ref().to_string()).collect(), + ..Default::default() + } + } + + /// Setup the Vivy runtime, creating the Lua state. You can setup the complete runtime, used to + /// parse declared jobs and all this stuff with the DSL-like keywords, or the minimal one used + /// to handle the computations. + pub(crate) fn setup_runtime(opts: RuntimeOptions) -> LuaResult<Lua> { + // The common things to create + let lua = unsafe { + // We use unsafe because we need to load the debug package. This also allows to load C + // things and dylib things, but because we nil the requires function it should be fine. + Lua::unsafe_new_with( + LuaStdLib::STRING | LuaStdLib::MATH | LuaStdLib::TABLE | LuaStdLib::PACKAGE | LuaStdLib::DEBUG, + LuaOptions::new().catch_rust_panics(true), + ) + }; + let (include_path, version, requires, warning, info, get_type_name) = ( + opts.include_path + .into_iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::<Vec<_>>(), + env!("CARGO_PKG_VERSION"), + lua.create_function(crate::libs::import)?, + lua.create_function(|_, msg: String| { + log::warn!(target: "vvs", "{msg}"); + Ok(()) + })?, + lua.create_function(|_, msg: String| { + log::info!(target: "vvs", "{msg}"); + Ok(()) + })?, + lua.create_function(|lua, value: LuaValue| match value { + LuaValue::UserData(value) => { + let Ok(mt) = value.get_metatable() else { return lua.create_string("userdata") }; + let Ok(name) = mt.get::<_, LuaFunction>("_name") else { return lua.create_string("userdata:_unamed") }; + lua.load(chunk! { "userdata:" .. $name() }).eval() + } + value => lua.create_string(value.type_name()), + })?, + ); + + // Remenber the mode flags for latter. + let (interactive_mode, minimal_mode) = match opts.mode { + RuntimeMode::Full => (false, false), + RuntimeMode::Compute => (false, true), + RuntimeMode::Interactive => (true, false), + }; + + // Mode specific constructs + let (options, data, jobs, func) = match opts.mode { + // In compute mode, we only need the dumps, ignoring almost all the things. + RuntimeMode::Compute => (LuaNil, LuaNil, LuaNil, LuaNil), + + // For the full or interactive runtime we need the DSL things, with the registers. + RuntimeMode::Full | RuntimeMode::Interactive => { + let options = match opts.creation_data { + Some(RuntimeCreationData::TomlFile(ref options)) => options.clone(), + _ => Default::default(), + }; + ( + VivyOptionRegister::new(options).to_lua(&lua)?, + VivyDataRegister::new().to_lua(&lua)?, + VivyJobRegister::new().to_lua(&lua)?, + VivyFuncRegister::new().to_lua(&lua)?, + ) + } + }; + + // Handle the ASS file. + let ass_value = match opts.ass_file { + Some(ASSFileOrInstance::File(file)) => { + LuaValue::String(lua.create_string(file.to_str().ok_or_else(|| { + LuaError::RuntimeError(format!( + "the path for the ASS file is not a valid UTF8 string: {}", + file.to_string_lossy() + )) + })?)?) + } + Some(ASSFileOrInstance::Instance(instance)) => instance.to_lua(&lua)?, + None => LuaNil, + }; + + lua.load(chunk! { + local _debug = debug + local ReadOnly = { + _VERSION = "Vivy " .. $version, + warning = $warning, + info = $info, + type = $get_type_name, + } + + local function _job_input_type (func) + local nparams = _debug.getinfo(func).nparams + if nparams ~= 1 then + error("a job should only have one argument, got " .. nparams, 2) + end + return _debug.getlocal(func, 1) + end + + local function _get_upvalues(func) + local ret, upval, getupval = {}, 1, _debug.getupvalue + while getupval(func, upval) do + ret[upval] = getupval(func, upval) + upval = upval + 1 + end + return ret + end + + local function _set_upvalue(func, idx, value) + _debug.setupvalue(func, idx, value) + end + + // Import Vivy + $requires("vivy", false) + vivy:___set_ass_file($ass_value) + vivy:___set_options($options) + vivy:___set_data($data) + vivy:___set_jobs($jobs) + vivy:___set_func($func) + vivy:___set_upvalues_callback(_get_upvalues, _set_upvalue) + vivy:___set_func_bc_dump_callback(string.dump, loadstring) + for _, folder in ipairs($include_path) do vivy:___append_to_path(folder) end + ReadOnly.vivy = vivy + + for _, l in ipairs { "package", "debug", "coroutine", "string", "_G" } do + package.loaded[l] = nil + end + for name, _ in pairs(package.loaded) do + ReadOnly.vivy:___mark_loaded(name) + end + + // DSL keywords + function ReadOnly.import (name) + if type(name) ~= "string" then + error("you must import a module by its name, got " .. type(name) .. " and not a string", 2) + end + $requires(name, ReadOnly) + end + + if $minimal_mode ~= true then + function ReadOnly.main (table) + if type(table) ~= "table" then + error("you must specify the main workflow with a table", 2) + end + vivy:___main(table) + end + + function ReadOnly.func (name) + if type(name) ~= "string" then + error("you must specify the exported function name with a string", 2) + end + return function (table) + if type(table) ~= "table" then + error("you must specify the function description with a table", 2) + end + // error("not implemented") + end + end + + function ReadOnly.job (name) + if type(name) ~= "string" then + error("you must specify the job name with a string", 2) + end + $jobs:will_register(name) + return function (table) + if type(table) ~= "table" then + error("you must specify the job description with a table", 2) + end + table._getter = _job_input_type + $jobs:register(name, table) + end + end + + function ReadOnly.data (name) + if type(name) ~= "string" then + error("you must specify the datum name with a string", 2) + end + $data:will_register(name) + return function (table) + if type(table) ~= "table" then + error("you must specify the data description with a table", 2) + end + $data:register(name, table) + end + end + + function ReadOnly.set (name) + if type(name) ~= "string" then + error("you must specify the option name with a string", 2) + end + $options:will_set(name) + return function (table) + if type(table) ~= "table" then + error("you must specify the option specification description with a table", 2) + end + $options:set(name, table) + end + end + + function ReadOnly.option (name) + if type(name) ~= "string" then + error("you must specify the option name with a string", 2) + end + $options:will_register(name) + return function (table) + if type(table) ~= "table" then + error("you must specify the option description with a table", 2) + end + $options:register(name, table) + end + end + end + + // Protect the metatable and control what is in the global table and force to use the + // proxy table... + setmetatable(_G, { + __metatable = "don't touch to the global table", + __index = ReadOnly, + __newindex = function(_, n, v) + if rawget(ReadOnly, n) ~= nil then + error("keyword or read only variable '" .. n .. "'", 2) + elseif type(n) ~= "string" then + error("global variables names must be strings", 2) + elseif $interactive_mode then + rawset(_G, n, v) + else + error("you can't create global variables", 2) + end + end, + }) + + for _, n in ipairs { + "debug", "vivy", "coroutine", "package", "string", + "module", "type", "_VERSION", "newproxy", + "require", "pcall", "xpcall", "loadfile", "load", "loadstring", "dofile", + } do + rawset(_G, n, nil) + end + + for _, n in ipairs { + "table", "math", // Modules + "next", "pairs", "ipairs", // Iteration stuff + "tonumber", "tostring", "print", // Printing stuff + "assert", "error", // Error handling + "unpack", "select", // Functions for tables + "gcinfo", "collectgarbage", // Should we expose the garbase collector? + "getmetatable", "setmetatable", "getfenv", "setfenv", // Handle metatables and other cryptic stuff + "rawget", "rawequal", "rawlen", "rawset" // rawset should be last... + } do + rawset(ReadOnly, n, _G[n]) + rawset(_G, n, nil) + end + }) + .set_name("vivy")? + .exec()?; + + match (opts.mode, opts.creation_data) { + (RuntimeMode::Compute, Some(RuntimeCreationData::TomlFile(_))) => { + return Err(LuaError::RuntimeError( + "can't use a toml file to init the runtime in compute mode".to_string(), + )) + } + (RuntimeMode::Compute, Some(RuntimeCreationData::ByteCode(bc))) => { + bc.to_lua(&lua)?; + } + (RuntimeMode::Interactive | RuntimeMode::Full, Some(RuntimeCreationData::ByteCode(_))) => todo!(), + _ => {} + } + log::debug!(target: "runtime", "finished runtime setup in mode {:?}", opts.mode); + + Ok(lua) + } + + /// Create a new default ASS element of the asked type. + #[allow(dead_code)] + pub fn new_ass_element<'lua>(&self, lua: &'lua Lua, ty: ASSType) -> LuaResult<LuaValue<'lua>> { + macro_rules! new { + ($elem: ident) => { + paste::paste! {{ + let mut elem = [< ASS $elem >] ::default(); + elem.aux = self.new_ass_aux_table(ASSType::Line); + [< LuaAss $elem Ptr >] (elem.into()).to_lua(lua) + }} + }; + } + match ty { + ASSType::Line => new!(Line), + ASSType::Syllabe => new!(Syllabe), + ASSType::Lines => LuaAssLines::default().to_lua(lua), + ASSType::Syllabes => LuaAssSyllabes::default().to_lua(lua), + } + } + + /// Create a new default ASS element aux table of the asked type. The table is filled with the + /// default options for the said element. + pub fn new_ass_aux_table(&self, ty: ASSType) -> ASSAuxTable { + self.data + .as_ref() + .map(|data| data.try_read().unwrap().get_table(ty)) + .unwrap_or_default() + } + + /// Get the associated variables, get by names. + fn get_vars<'lua>(&self, lua: &'lua Lua, args: &[impl AsRef<str>]) -> LuaResult<Vec<LuaValue<'lua>>> { + let (vars, errs): (Vec<_>, Vec<_>) = args + .iter() + .map(|arg| match self.variables.read(arg.as_ref(), |_, v| v.clone()) { + Some(key) => match lua.registry_value::<LuaValue>(&*key)? { + var @ LuaValue::UserData(_) => { + log::debug!(target: "runtime", "{} -> {key:?}", arg.as_ref()); + Ok(var) + } + var => Err(LuaError::RuntimeError(format!( + "invalid type '{}' for job argument '{}'", + var.type_name(), + arg.as_ref() + ))), + }, + None => Err(LuaError::RuntimeError(format!( + "failed to find variable '{}' in execution state", + arg.as_ref() + ))), + }) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + Ok(vars.into_iter().map(Result::unwrap).collect()) + } + + /// Pack the arguments for a job, check for correct type, coerce if needed, etc. + fn pack_job_arguments<'lua>( + &self, + lua: &'lua Lua, + input: ASSType, + input_args: Vec<String>, + ) -> LuaResult<Vec<LuaValue<'lua>>> { + let mut args: Vec<LuaValue> = Default::default(); + for var in self.get_vars(lua, &input_args)? { + let LuaValue::UserData(var) = var else { + panic!("we should have a user data type here, got {}", var.type_name()) + }; + match input { + ASSType::Lines | ASSType::Line => match_borrow! { var => + ; _var: LuaAssLinePtr => { args.push(LuaValue::UserData(var.clone())); } + ; var: LuaAssLines => { + for var in &var.as_inner().0 { + args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); + } + } + + ; var: LuaAssContainerPtr => { + for var in var.0.0.try_read().unwrap().lines.0.iter() { + args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); + } + } + + ; @default => { + let var = var.clone(); + let ty = lua.load(chunk!(type($var))).eval::<LuaString>()?; + let ty = ty.to_str()?; + return Err(LuaError::RuntimeError(format!("expected a line or lines, got {ty}"))) + } + }, + + ASSType::Syllabes | ASSType::Syllabe => match_borrow! { var => + ; _var: LuaAssSyllabePtr => { args.push(LuaValue::UserData(var.clone())); } + ; var: LuaAssSyllabes => { + for var in &var.as_inner().0 { + args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); + } + } + + ; var: LuaAssLinePtr => { + for var in &var.as_inner().0.try_read().unwrap().content { + args.push(LuaAssSyllabePtr(var.clone()).to_lua(lua)?); + } + } + ; var: LuaAssLines => for line in &var.as_inner().0 { + for var in &line.0.try_read().unwrap().content { + args.push(LuaAssSyllabePtr(var.clone()).to_lua(lua)?); + } + } + + ; var: LuaAssContainerPtr => for line in var.as_inner().0.try_read().unwrap().lines.0.iter() { + for var in &line.0.try_read().unwrap().content { + args.push(LuaAssSyllabePtr(var.clone()).to_lua(lua)?); + } + } + + ; @default => { + let var = var.clone(); + let ty = lua.load(chunk!(type($var))).eval::<LuaString>()?; + let ty = ty.to_str()?; + return Err(LuaError::RuntimeError(format!("expected a line or lines, got {ty}"))) + } + }, + }; + } + Ok(args) + } + + /// Unpack the return values from a job to form the correct list by taking into account what + /// should be returned by the job. + fn unpack_job_returns<'lua>( + &self, + lua: &'lua Lua, + name: &str, + returns: ASSType, + output: impl Iterator<Item = LuaValue<'lua>>, + ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + fn unwrap(var: LuaValue) -> LuaAnyUserData { + match var { + LuaValue::UserData(var) => var, + _ => unreachable!(), + } + } + + fn handle_user_data<'lua>( + lua: &'lua Lua, + returns: ASSType, + var: LuaAnyUserData<'lua>, + ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + match returns { + ASSType::Lines | ASSType::Line => match_borrow! { var => + ; var: LuaAssLinePtr => Ok(vec![unwrap(var.clone().to_lua(lua)?)]) + ; var: LuaAssLines => Ok(var.as_inner().0.iter().map(|line| + unwrap(LuaAssLinePtr::from(line.clone()).to_lua(lua).unwrap()) + ).collect()) + ; @default => Err(LuaError::RuntimeError( + "expected a line or lines".to_string() + )) + }, + ASSType::Syllabes | ASSType::Syllabe => match_borrow! { var => + ; var: LuaAssSyllabePtr => Ok(vec![unwrap(var.clone().to_lua(lua)?)]) + ; var: LuaAssSyllabes => Ok(var.as_inner().0.iter().map(|line| + unwrap(LuaAssSyllabePtr::from(line.clone()).to_lua(lua).unwrap()) + ).collect()) + ; @default => Err(LuaError::RuntimeError( + "expected a syllabe or syllabes".to_string() + )) + }, + } + } + + let (output, errs): (Vec<_>, Vec<_>) = output + .map(|out: LuaValue<'lua>| match out { + LuaValue::UserData(out) => handle_user_data(lua, returns, out), + LuaValue::Table(outs) => { + let (outs, errs): (Vec<_>, Vec<_>) = outs + .sequence_values() + .map(|item| handle_user_data(lua, returns, item?)) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + Ok(outs.into_iter().flat_map(Result::unwrap).collect()) + } + _ => Err(LuaError::RuntimeError(format!( + "invalid return type for job '{name}', got a value of type: {}", + out.type_name() + ))), + }) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + let (output, errs): (Vec<_>, Vec<_>) = output.into_iter().partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + Ok(output.into_iter().flat_map(Result::unwrap).collect()) + } + + /// Execute a job with the named arguments. The types of the arguments will be checked and it + /// will be up to this function to create the apply loop if needed. The returned values will + /// also be flattened if needed. + fn execute_job<'lua>(&self, lua: &'lua Lua, job: VivyCalledJob) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + let VivyCalledJob { module: _, name, function, arguments, input_type, return_type } = job; + let function: LuaFunction = lua.registry_value(&function)?; + let (output, errs): (Vec<_>, Vec<_>) = self + .pack_job_arguments(lua, input_type, arguments)? + .into_iter() + .map(|arg| function.call::<LuaValue, LuaValue>(arg)) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + self.unpack_job_returns(lua, &name, return_type, output.into_iter().map(Result::unwrap)) + } + + /// Register a Vivy execution variable. + fn register_variable<'lua>(&self, lua: &'lua Lua, name: impl ToString, value: impl ToLua<'lua>) -> LuaResult<()> { + let (key, name) = ( + Arc::new(lua.create_registry_value(value.to_lua(lua)?)?), + name.to_string(), + ); + log::debug!(target: "runtime", "create variable {name:?} as {key:?}"); + self.variables + .insert(name, key) + .map_err(|(var, _)| LuaError::RuntimeError(format!("variable {var:?} already exists..."))) + } + + /// Export all registered options into Lua's global table. + fn set_options(&self, lua: &Lua) -> LuaResult<()> { + self.options + .as_ref() + .map(|options| { + let options = options + .try_read() + .map_err(|err| LuaError::RuntimeError(format!("failed to lock option register: {err}")))?; + for name in options.iter_registered() { + let opt = options.resolve(name).ok_or_else(|| { + LuaError::RuntimeError(format!( + "the option {name:?} should have been defined in the toml config but was not" + )) + })?; + log::debug!(target: "runtime", "set option {name:?} as {opt:?}"); + lua.globals().raw_set(name.as_str(), opt)?; + } + Ok(()) + }) + .unwrap_or_else(|| Err(LuaError::RuntimeError("no option register found...".to_string()))) + } + + /// Create the byte code structure associated to the user's script. + fn create_byte_code(&self, lua: &Lua) -> LuaResult<ScriptByteCode> { + let getters = ( + self.upvalues_getter.as_ref().unwrap(), + self.func_bc_dump.as_ref().unwrap(), + ); + let Some(ref path) = self.script_path else { + return Err(LuaError::RuntimeError(match &*self.current_module.borrow() { + Some(ref module) => format!("no script file found for module '{module}'"), + None => "no main script file found".to_string(), + })) + }; + let Some(ref jobs) = self.jobs else { return Err(LuaError::RuntimeError("no jobs registry found in the runtime".to_string())) }; + let Some(ref func) = self.func else { return Err(LuaError::RuntimeError("no functions registry found in the runtime".to_string())) }; + let Some(ref data) = self.data else { return Err(LuaError::RuntimeError("no data registry found in the runtime".to_string())) }; + let Some(ref opts) = self.options else { return Err(LuaError::RuntimeError("no option registry found in the runtime".to_string())) }; + + fn log_error<T>(res: LuaResult<T>) -> Option<T> { + res.map_err(|err| log::error!(target: "runtime", "{err}")).ok() + } + + let data = data.try_read().unwrap(); + let data = ASSTYPE_VALUES.iter().copied().flat_map(|ass_type| { + data.get_table(ass_type) + .into_iter() + .map(move |(name, default)| (name, LuaAssType(ass_type), LuaAssAuxValue(default))) + }); + + let opts = opts.try_read().unwrap(); + let globals = opts.iter_registered().flat_map(|name| { + opts.resolve(name) + .into_iter() + .filter_map(|value| Some((name.clone(), LuaAssAuxValue(value.try_into().ok()?)))) + }); + + let (jobs, func) = (jobs.try_read().unwrap(), func.try_read().unwrap()); + let jobs = jobs.iter_declared().filter_map(|job_ptr| { + let job = job_ptr.try_read().unwrap(); + let ident = SymbolName { module: job.module.clone(), name: job.name.clone() }; + let function = log_error(lua.registry_value(&job.function))?; + let job = Symbol::Job { + bytecode: log_error(FunctionByteCode::try_new(getters, lua, function))?, + input_type: LuaAssType(job.input_type), + output_type: LuaAssType(job.return_type), + }; + Some((ident, job)) + }); + let func = func.iter_declared().filter_map(|func_ptr| { + let func = func_ptr.try_read().unwrap(); + let ident = SymbolName { module: func.module.clone(), name: func.name.clone() }; + let function = log_error(lua.registry_value(&func.function))?; + let function = Symbol::Function { + bytecode: log_error(FunctionByteCode::try_new(getters, lua, function))?, + }; + Some((ident, function)) + }); + + Ok(ScriptByteCode { + path: path.clone(), + data: data.collect(), + mtime: self.script_path_mtime, + imports: self.loaded.borrow().values().cloned().collect(), + globals: globals.collect(), + symbols: HashMap::from_iter(jobs.chain(func)), + }) + } + + /// Execute a graph. This is the what handles the work to do that was declared by the user. + /// From here we need an ASS file (or the JSON file, or any sub-title able file). + fn execute_graph(&self, lua: &Lua, graph: VivyExecGraph) -> LuaResult<()> { + let ass = match self.ass_file.as_ref() { + None => { + return Err(LuaError::RuntimeError( + "no ASS file was specified, can't execute the user script".to_string(), + )) + } + Some(ASSFileOrInstance::Instance(instance)) => instance.clone(), + Some(ASSFileOrInstance::File(file)) => { + if !file.exists() || !file.is_file() { + return Err(LuaError::RuntimeError(format!( + "specified ASS file doesn't exist: {}", + file.to_string_lossy() + ))); + } + let file_name = file.to_string_lossy(); + LuaAssContainerPtr::from( + vvs_ass::ass_container_from_file(file) + .map_err(|err| LuaError::RuntimeError(format!("invalid ass file '{file_name}': {err}")))?, + ) + } + }; + self.register_variable(lua, "INIT", ass)?; + + 'working: loop { + use VivyExecGraphIteratorError::*; + match graph.next() { + Ok((dest, job)) => { + log::info!(target: "runtime", "{dest} := {job:?}"); + self.register_variable(lua, &dest, self.execute_job(lua, job)?)?; + graph.completed(dest); + } + Err(Empty | Completed) => break 'working, + Err(Again) => { + log::debug!(target: "runtime", "no work to do for now..."); + continue 'working; + } + Err(err) => return Err(LuaError::RuntimeError(format!("script failed: {err}"))), + } + } + + Ok(()) + } + + /// Format the loaded modules, internal modules are preceded with the '@' character. + fn format_loaded(&self) -> Vec<String> { + self.loaded + .borrow() + .iter() + .map(|(name, module)| { + if module.is_internal() { + format!("@{name}") + } else if module.was_lua_file() { + format!("lua:{name}") + } else if module.was_vivy_file() { + format!("vvl:{name}") + } else { + unreachable!() + } + }) + .collect() + } +} + +impl LuaUserData for Vivy { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_function("_name", |lua, ()| lua.create_string("Vivy")); + + macro_rules! add_setter { + ($n: ident: $p: ident; + $($name: ident: $ptr: ident);+$(;)? + ) => { + add_setter! { $n: $p } + add_setter! { $($name: $ptr);+ } + }; + + ($name: ident: $ptr: ident $(;)?) => { + methods.add_method_mut(concat!("___set_", stringify!($name)), |_, this, $name: Option<$ptr>| { + this.$name = $name; + Ok(()) + }); + }; + + ($name: literal ($($arg: ident: $ty: ty),+) $lua: ident, $this: ident => $expr: expr) => { + methods.add_method_mut($name, |$lua, $this, ($($arg),+,): ($($ty),+,)| { + $expr; + Ok(()) + }); + }; + } + + macro_rules! add_getter { + ($n: literal: $l: ident , $t: ident => $e: expr; + $($name: literal: $lua: ident , $this: ident => $expr: expr);+$(;)? + ) => { + add_getter! { $n: $l, $t => $e } + add_getter! { $($name: $lua, $this => $expr);+ } + }; + + ($name: literal: $lua: ident, $this: ident => $expr: expr $(;)?) => { + methods.add_method($name, |$lua, $this, ()| Ok($expr)); + }; + } + + add_setter! { + options: VivyOptionRegisterPtr; + data: VivyDataRegisterPtr; + jobs: VivyJobRegisterPtr; + func: VivyFuncRegisterPtr; + } + + add_setter! { "___set_ass_file" (value: LuaValue) _lua, this => { + match value { + LuaValue::String(file) => { + let file = PathBuf::from(file.to_string_lossy().as_ref()); + this.ass_file = Some(ASSFileOrInstance::File(file)); + } + LuaValue::UserData(ud) => { + if let Ok(instance) = ud.take::<LuaAssContainerPtr>() { + this.ass_file = Some(ASSFileOrInstance::Instance(instance)); + } + } + _ => {} + } + }} + add_setter! { "___set_user_script" (file: LuaString) _lua, this => { + let file = PathBuf::from(file.to_string_lossy().as_ref()); + this.script_path_mtime = std::fs::metadata(&file).map(|mt| mt.modified().ok()).unwrap_or_default(); + this.script_path = Some(file); + }} + add_setter! { "___prepend_to_path" (pattern: String) _lua, this => this.path.insert(0, pattern) } + add_setter! { "___append_to_path" (pattern: String) _lua, this => this.path.push(pattern) } + add_setter! { "___mark_loaded" (modname: String) _lua, this => { + this.loading.borrow_mut().remove(&modname); + this.loaded.borrow_mut().entry(modname).or_insert_with_key(|modname| Module::from_internal(modname)); + }} + add_setter! { "___set_upvalues_callback" (get: LuaFunction, set: LuaFunction) lua, this => { + this.upvalues_getter = Some(lua.create_registry_value(get)?); + lua.set_named_registry_value("_set_upvalue", set)?; + }} + add_setter! { "___set_func_bc_dump_callback" (dump: LuaFunction, load: LuaFunction) lua, this => { + this.func_bc_dump = Some(lua.create_registry_value(dump)?); + lua.set_named_registry_value("_loadstring", load)?; + }} + add_setter! { "___set_current_module" (module: LuaValue) _lua, this => + this.current_module.replace(match module { + LuaValue::String(module) => { + let module = module.to_string_lossy().to_string(); + log::debug!(target: "runtime", "set the current module to '{module}'"); + Some(module) + } + value => { + log::debug!(target: "runtime", "can't set the current module name with a value of type: {}", value.type_name()); + None + } + }) + } + + add_getter! { + "___get_loaded": _lua, this => this.format_loaded().into_iter().collect::<Vec<_>>(); + "include_path": _lua, this => this.path.clone(); + "loaded_modules": lua, this => { + let table = lua.create_table()?; + for (module, desc) in this.loaded.borrow().iter().map(|(name, module)| { + if module.is_internal() { (name, "internal" ) } + else if module.was_lua_file() { (name, "runtime" ) } + else if module.was_vivy_file() { (name, "vivy library") } + else { unreachable!() } + }) { + table.raw_set(module.as_str(), desc)?; + } + table + } + } + + methods.add_method_mut("___print_info", |_, this, ()| { + this.should_print_infos = true; + Ok(()) + }); + + methods.add_method("___is_already_imported", |_, this, module: String| { + Ok(this.loaded.borrow().contains_key(&module)) + }); + + methods.add_method("___current_module", |lua, this, ()| { + match &*this.current_module.borrow() { + Some(ref current_module) => lua.create_string(current_module.as_str()).map(LuaValue::String), + None => { + log::debug!(target: "runtime", "can't get the current module name even if vivy is initialized..."); + Ok(LuaValue::Nil) + } + } + }); + + methods.add_method("___run_export_module", |lua, this, name: LuaString| { + let table = lua.create_table()?; + this.jobs + .as_ref() + .map(|jobs| jobs.try_write().unwrap().export(lua, &name, &table)) + .unwrap_or(Ok(()))?; + this.func + .as_ref() + .map(|func| func.try_write().unwrap().export(lua, &name, &table)) + .unwrap_or(Ok(()))?; + let table = dsl::into_readonly_table(lua, table)?; + lua.globals().raw_set(name, table)?; + + Ok(()) + }); + + methods.add_method("___import", |lua, this, name: String| -> LuaResult<bool> { + if !name.chars().all(|c| c.is_ascii_alphanumeric()) { + return Err(LuaError::RuntimeError(format!( + "invalid module name '{name}', must be an ascii alphanumeric string" + ))); + } else if this.loading.borrow().contains(&name) { + return Err(LuaError::RuntimeError(format!( + "circular dependency found with module '{name}'" + ))); + } + for path in &this.path { + let path = PathBuf::from(path.replace('?', &name)); + let module = Module::from_file(&name, &path); + if path.exists() && path.is_file() { + this.loading.borrow_mut().insert(name.clone()); + return match lua.load(&path).set_name(path.to_string_lossy())?.exec() { + Ok(()) => { + let ret = module.was_lua_file(); + this.loaded.borrow_mut().insert(name, module); + Ok(ret) + } + Err(err) => { + this.loading.borrow_mut().remove(&name); + Err(LuaError::RuntimeError(format!( + "failed to load module '{name}' at location: {}\n{err}", + path.to_string_lossy() + ))) + } + }; + } + } + Err(LuaError::RuntimeError(format!( + "failed to find or load module named '{name}' in path: {:#?}", + this.path + ))) + }); + + methods.add_method("___main", |lua, this, table: LuaTable| -> LuaResult<()> { + if this.main_once { + return Err(LuaError::RuntimeError( + "multiple 'main' blocks detected".to_string(), + )); + } + + // Finish setup + if let Some(ref options) = this.options { + let options = options.try_read().unwrap(); + for option in options.iter_registered() { + let value = match options.resolve(option) { + Some(value) => value.to_lua(lua)?, + None => LuaValue::Nil, + }; + lua.globals().raw_set(option.as_str(), value)?; + } + } + + if let Some(ref data) = this.data { + data.try_write().unwrap().compute_cached_tables() + } + + // Print infos + if this.should_print_infos { + println!(" <<< Information Report >>>"); + println!(" # Misc"); + println!( + " - version {}", + lua.globals().get::<_, String>("_VERSION")? + ); + println!( + " - packages {}", + this.format_loaded().into_iter().collect::<Vec<_>>().join(", ") + ); + if let Some(options) = this.options.as_ref() { + options.try_read().unwrap().print_info(); + } + if let Some(data) = this.data.as_ref() { + data.try_read().unwrap().print_info(); + } + } + + // Set options! + this.set_options(lua)?; + + // Do the work + let table_last_idx = table.raw_len() - 1; + let (work, ret): (Vec<_>, Vec<_>) = table + .pairs::<LuaValue, LuaValue>() + .enumerate() + .flat_map(|(idx, pair)| { + let (dest, src) = pair.ok()?; + Some((idx, lua.coerce_string(dest).unwrap_or_default(), src)) + }) + .partition(|(idx, ..)| i64::try_from(*idx).unwrap() != table_last_idx); + let [(_, _, ret)] = &ret[..] else { unreachable!("invalid return statements, should only be one: {ret:#?}") }; + let vivy_byte_code = this.create_byte_code(lua)?; + log::debug!(target: "runtime", "user's script associated bytecode structure: {vivy_byte_code:#?}"); + + let exec_lua = Vivy::setup_runtime(RuntimeOptions { + ass_file: this.ass_file.clone(), + creation_data: Some(RuntimeCreationData::ByteCode(vivy_byte_code)), + include_path: this.path.iter().map(PathBuf::from).collect(), + mode: RuntimeMode::Compute + })?; + log::debug!(target: "runtime", "created an execution state from generated bytecode: {exec_lua:?}"); + + let exec_graph = VivyExecGraph::try_from((lua, work, ret.clone()))?; + log::debug!(target: "runtime", "user's script associated execution graph: {exec_graph:#?}"); + this.execute_graph(lua, exec_graph) + }); + } +} diff --git a/src/Rust/vvs_lua/src/lua_wrapper.rs b/src/Rust/vvs_lua/src/lua_wrapper.rs index 63479414..7fd968d2 100644 --- a/src/Rust/vvs_lua/src/lua_wrapper.rs +++ b/src/Rust/vvs_lua/src/lua_wrapper.rs @@ -2,25 +2,34 @@ use crate::traits::TypedValue; use mlua::prelude::*; -use std::str::FromStr; -use vvs_ass::{ - ASSAuxTablePtr, ASSAuxValue, ASSContainerPtr, ASSLinePtr, ASSLinesPtr, ASSPositionPtr, - ASSSyllabePtr, ASSSyllabesPtr, ASSType, -}; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, sync::Arc}; +use unicode_segmentation::UnicodeSegmentation; +use vvs_ass::{ASSAuxValue, ASSContainer, ASSContainerPtr, ASSLinePtr, ASSLines, ASSSyllabePtr, ASSSyllabes, ASSType}; macro_rules! wrap_ass2lua { - ($($type: ident $(-> $($trait: ident),+)? - $(: $fields: ident -> $add_fields: expr - , $methods: ident -> $add_methods: expr)? + ($( + $(#[$meta:meta])* + struct $type: ident + ($fields: ident, $methods: ident) + -> $add_fields: expr + , $add_methods: expr );+ $(;)?) => { - $(wrap_ass2lua!(@@private $type $($($trait),+)? $(; $fields -> $add_fields ; $methods -> $add_methods)?);)+ + $(wrap_ass2lua!(@@private $(#[$meta])* $type; $fields -> $add_fields ; $methods -> $add_methods);)+ }; - (@@decl $type: ident $($trait: ident),*) => { + (@@decl $(#[$meta:meta])* $type: ident) => { paste::paste! { - #[derive(Debug, Clone, PartialEq, $($trait),*)] + #[derive(Clone)] + $(#[$meta])* #[repr(transparent)] - pub(crate) struct [< LuaAss $type >] ([< ASS $type >]); + pub(crate) struct [< LuaAss $type >] (pub(crate) [< ASS $type >]); + + impl std::fmt::Debug for [< LuaAss $type >] { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } impl [< LuaAss $type >] { #[allow(dead_code)] @@ -42,108 +51,172 @@ macro_rules! wrap_ass2lua { } }; - (@@private $type: ident $($trait: ident),*) => { - wrap_ass2lua!(@@decl $type $($trait),*); - paste::paste! { impl LuaUserData for [< LuaAss $type >] { } } - }; - - (@@private $type: ident $($trait: ident),* - ; $fields : ident -> $add_fields : expr - ; $methods: ident -> $add_methods: expr) => { - wrap_ass2lua!(@@decl $type $($trait),*); + (@@private $(#[$meta:meta])* $type: ident + ; $fields : ident -> $add_fields : expr + ; $methods: ident -> $add_methods: expr + ) => { + wrap_ass2lua!(@@decl $(#[$meta])* $type); paste::paste! { impl LuaUserData for [< LuaAss $type >] { - fn add_fields <'lua, F: LuaUserDataFields <'lua, Self>>($fields: &mut F) { $add_fields } - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>($methods: &mut M) { $add_methods } + fn add_fields <'lua, F: LuaUserDataFields <'lua, Self>>($fields: &mut F) { + $add_fields + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>($methods: &mut M) { + $methods.add_meta_function("_name", |lua, ()| { lua.create_string(stringify!($type)) }); + $add_methods + } } } }; } macro_rules! add_method { - ($fields: expr => @aux) => { - add_method! { $fields => aux @ LuaAssAuxTablePtr } + ($methods: expr => @aux) => { + add_method! { $methods => "aux" (_lua, _this, ()) -> { + todo!() + }} }; ($methods: expr => @copy) => { add_method! { - method $methods => "copy" (_lua, this) -> { - Ok(Self((&*this).borrow().clone().into_ptr())) + $methods => "copy" (lua, this, ()) -> { + Self(this.clone()).to_lua(lua) } } }; - ($fields: expr => $field: ident @ $wrapper: ident) => { - add_method! { get $fields => $field @ $wrapper } - add_method! { set $fields => $field @ $wrapper } + ($methods: expr => + $name: literal ($lua: ident, $this: ident, ($($arg_1: ident: $ty_1: ty)? $(, $($arg: ident: $ty: ty),+)?)) -> $expr: expr + ) => { + #[allow(unused_parens)] + $methods.add_method($name, + |$lua, Self($this), ($($arg_1)? $(, $($arg),+,)?): ($($ty_1)? $(, $($ty),+,)?)| -> LuaResult<LuaValue> { $expr } + ) }; - (get $fields: expr => $field: ident @ $wrapper: ident) => { - $fields.add_field_method_get(stringify!($field), |_, Self(this)| { - Ok($wrapper(this.borrow().$field.clone())) - }); + (mut $methods: expr => + $name: literal ($lua: ident, $this: ident, ($($arg_1: ident: $ty_1: ty)? $(, $($arg: ident: $ty: ty),+)?)) -> $expr: expr + ) => { + #[allow(unused_parens)] + $methods.add_method_mut($name, + |$lua, Self($this), ($($arg_1)? $(, $($arg),+,)?): ($($ty_1)? $(, $($ty),+,)?)| -> LuaResult<LuaValue> { $expr } + ) }; +} - (set $fields: expr => $field: ident @ $wrapper: ident) => { - $fields.add_field_method_set(stringify!($field), |_, Self(this), $wrapper(val)| { - this.borrow_mut().$field = val; - Ok(()) - }); +macro_rules! add_field { + (get $fields: expr => $this: ident . $val: ident => $expr: expr) => { + $fields.add_field_method_get(stringify!($val), |_, $this| Ok($expr)) }; - (method $methods: expr => $name: literal ($lua: ident, $this: ident) -> $expr: expr) => { - $methods.add_method($name, |$lua, Self($this), ()| $expr) + (set $fields: expr => $lua: ident, $this: ident . $val: ident: $ty: ty => $expr: expr) => { + #[allow(unused_mut)] + $fields.add_field_method_set(stringify!($val), |$lua, mut $this, $val: $ty| { + $expr; + Ok(()) + }) }; } wrap_ass2lua! { - Type; - AuxValue; - AuxTablePtr -> Default: - _f -> {}, - m -> { - add_method! { m => @copy } - }; - PositionPtr: - _f -> {}, - m -> { - add_method! { m => @copy } - }; - // The ASS types - ContainerPtr; - LinePtr: - f -> { - add_method! { f => @aux } - add_method! { f => position @ LuaAssPositionPtr } - }, - m -> { - add_method! { m => @copy } - }; - LinesPtr: - f -> { - add_method! { f => @aux } - add_method! { f => position @ LuaAssPositionPtr } - }, - m -> { - add_method! { m => @copy } - add_method! { method m => "push" (_lua, _this) -> { Ok(()) }} - }; - SyllabePtr: - f -> { - add_method! { f => @aux } - add_method! { f => position @ LuaAssPositionPtr } - }, - m -> { - add_method! { m => @copy } - }; - SyllabesPtr: - f -> { - add_method! { f => @aux } - add_method! { f => position @ LuaAssPositionPtr } - }, - m -> { - add_method! { m => @copy } - }; + #[doc = "Wraps the ASSType for internal usage, won't be exposed to the user"] + #[derive(PartialEq, Eq, Serialize, Deserialize)] + struct Type (_f, _m) -> {}, {}; + + #[doc = "Wraps the ASSAuxValue for internal usage, won't be exposed to the user"] + #[derive(PartialEq, Serialize, Deserialize)] + struct AuxValue (_f, _m) -> {}, {}; + + #[doc = "Wraps the ASSContainerPtr for internal usage, won't be exposed to the user"] + struct ContainerPtr (_f, _m) -> {}, {}; + + #[doc = "Wraps the ASSLines container to expose it to lua code"] + #[derive(Default, PartialEq)] + struct Lines (_f, m) -> {}, { + add_method! { m => @copy } + add_method! { mut m => "push" (_lua, this, (item: LuaValue)) -> match item { + LuaNil => { + log::warn!(target: "lua", "pushing nothing into Lines..."); + Ok(LuaNil) + } + LuaValue::Table(items) => { + this.extend(items.sequence_values::<LuaAssLinePtr>().flat_map(|line| Some(line.ok()?.0))); + Ok(LuaNil) + } + LuaValue::UserData(item) if item.is::<LuaAssLinePtr>() => { + this.push(item.take::<LuaAssLinePtr>().unwrap().0); + Ok(LuaNil) + } + LuaValue::UserData(item) if item.is::<LuaAssLines>() => { + this.extend(item.take::<LuaAssLines>().unwrap().0); + Ok(LuaNil) + } + _ => Err(LuaError::RuntimeError(format!("expected a Line or a table of Line to push into lines, got {}", item.type_name()))) + }} + + m.add_meta_method(LuaMetaMethod::Pairs, Self::pairs); + m.add_meta_method(LuaMetaMethod::IPairs, Self::pairs); + }; + + #[doc = "Wraps the ASSSyllabes container to expose it to lua code"] + #[derive(Default, PartialEq)] + struct Syllabes (_f, m) -> {}, { + add_method! { m => @copy } + add_method! { mut m => "push" (_lua, this, (item: LuaValue)) -> match item { + LuaNil => { + log::warn!(target: "lua", "pushing nothing into Syllabes..."); + Ok(LuaNil) + } + LuaValue::Table(items) => { + this.extend(items.sequence_values::<LuaAssSyllabePtr>().flat_map(|line| Some(line.ok()?.0))); + Ok(LuaNil) + } + LuaValue::UserData(item) if item.is::<LuaAssSyllabePtr>() => { + this.push(item.take::<LuaAssSyllabePtr>().unwrap().0); + Ok(LuaNil) + } + LuaValue::UserData(item) if item.is::<LuaAssSyllabes>() => { + this.extend(item.take::<LuaAssSyllabes>().unwrap().0); + Ok(LuaNil) + } + _ => Err(LuaError::RuntimeError(format!("expected a Syllabe or a table of Syllabe to push into lines, got {}", item.type_name()))) + }} + + m.add_meta_method(LuaMetaMethod::Pairs, Self::pairs); + m.add_meta_method(LuaMetaMethod::IPairs, Self::pairs); + }; + + #[doc = "Wraps the ASSLinePtr to expose it to lua code"] + #[derive(Default)] + struct LinePtr (f, m) -> { + add_field! { get f => this.start => this.0.0.try_read().unwrap().start } + add_field! { get f => this.fini => this.0.0.try_read().unwrap().fini } + + add_field! { set f => lua, this.start: LuaValue => this.0.0.try_write().unwrap().start = lua.coerce_integer(start)?.unwrap_or_default() } + add_field! { set f => lua, this.fini: LuaValue => this.0.0.try_write().unwrap().fini = lua.coerce_integer(fini)?.unwrap_or_default() } + }, { + add_method! { m => @aux } + add_method! { m => @copy } + + m.add_meta_method(LuaMetaMethod::Pairs, Self::pairs); + m.add_meta_method(LuaMetaMethod::IPairs, Self::pairs); + }; + + #[doc = "Wraps the ASSSyllabePtr to expose it to lua code"] + #[derive(Default)] + struct SyllabePtr (f, m) -> { + add_field! { get f => this.start => this.0.0.try_read().unwrap().start } + add_field! { get f => this.fini => this.0.0.try_read().unwrap().fini } + + add_field! { set f => lua, this.start: LuaValue => this.0.0.try_write().unwrap().start = lua.coerce_integer(start)?.unwrap_or_default() } + add_field! { set f => lua, this.fini: LuaValue => this.0.0.try_write().unwrap().fini = lua.coerce_integer(fini)?.unwrap_or_default() } + }, { + add_method! { m => @aux } + add_method! { m => @copy } + + m.add_meta_method(LuaMetaMethod::Pairs, Self::pairs); + m.add_meta_method(LuaMetaMethod::IPairs, Self::pairs); + }; } impl TypedValue for LuaAssAuxValue { @@ -156,7 +229,7 @@ impl FromStr for LuaAssType { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { - Ok(Self(s.parse().map_err(|err| format!("{err}"))?)) + Ok(Self(s.parse()?)) } } @@ -165,8 +238,74 @@ impl<'lua> TryFrom<LuaString<'lua>> for LuaAssType { fn try_from(value: LuaString<'lua>) -> Result<Self, Self::Error> { match value.to_str() { - Ok(bt) => Ok(Self(bt.parse().map_err(|err| format!("{err}"))?)), + Ok(bt) => Ok(Self(bt.parse()?)), _ => Err("can't build a vvs_lua::BaseType from an invalid utf8 string".to_string()), } } } + +impl LuaAssLinePtr { + fn pairs<'lua>(lua: &'lua Lua, this: &Self, _: ()) -> LuaResult<LuaValue<'lua>> { + let ptr = this.0 .0.clone(); + let (mut i, n) = (Arc::new(0), ptr.try_read().unwrap().content.len()); + Ok(LuaValue::Function(lua.create_function_mut(move |lua, ()| { + *Arc::get_mut(&mut i).unwrap() += 1; + if *i <= n { + if let Some(item) = ptr.try_read().unwrap().content.get(*i - 1) { + return (*i, LuaAssSyllabePtr(item.clone())).to_lua_multi(lua); + } + } + LuaNil.to_lua_multi(lua) + })?)) + } +} + +impl LuaAssLines { + fn pairs<'lua>(lua: &'lua Lua, this: &Self, _: ()) -> LuaResult<LuaValue<'lua>> { + let (mut i, n) = (Arc::new(0), this.0.len()); + Ok(LuaValue::Function(lua.create_function_mut(move |lua, ()| { + *Arc::get_mut(&mut i).unwrap() += 1; + if *i <= n { + todo!() + } else { + LuaNil.to_lua_multi(lua) + } + })?)) + } +} + +impl LuaAssSyllabePtr { + fn pairs<'lua>(lua: &'lua Lua, this: &Self, _: ()) -> LuaResult<LuaValue<'lua>> { + let ptr = this.0 .0.clone(); + let (mut i, n) = (Arc::new(0), ptr.try_read().unwrap().content.len()); + Ok(LuaValue::Function(lua.create_function_mut(move |lua, ()| { + *Arc::get_mut(&mut i).unwrap() += 1; + if *i <= n { + if let Some(item) = ptr.try_read().unwrap().content.graphemes(true).nth(*i - 1) { + return (*i, item).to_lua_multi(lua); + } + } + LuaNil.to_lua_multi(lua) + })?)) + } +} + +impl LuaAssSyllabes { + fn pairs<'lua>(lua: &'lua Lua, this: &Self, _: ()) -> LuaResult<LuaValue<'lua>> { + let (mut i, n) = (Arc::new(0), this.0.len()); + Ok(LuaValue::Function(lua.create_function_mut(move |lua, ()| { + *Arc::get_mut(&mut i).unwrap() += 1; + if *i <= n { + todo!() + } else { + LuaNil.to_lua_multi(lua) + } + })?)) + } +} + +impl From<ASSContainer> for LuaAssContainerPtr { + fn from(value: ASSContainer) -> Self { + Self(ASSContainerPtr(vvs_ass::ptr!(value))) + } +} diff --git a/src/Rust/vvs_lua/src/options/actions.rs b/src/Rust/vvs_lua/src/options/actions.rs index ca0f4c97..ad3d33d3 100644 --- a/src/Rust/vvs_lua/src/options/actions.rs +++ b/src/Rust/vvs_lua/src/options/actions.rs @@ -41,10 +41,7 @@ pub(crate) struct NamedSetOptionValue { impl SetOptionValue { pub fn named(self, name: String) -> NamedSetOptionValue { - NamedSetOptionValue { - name, - value: self.value, - } + NamedSetOptionValue { name, value: self.value } } pub fn is_none(&self) -> bool { @@ -54,16 +51,12 @@ impl SetOptionValue { impl RegisterOptionValue { pub fn named(self, name: String) -> NamedRegisterOptionValue { - NamedRegisterOptionValue { - name, - doc: self.doc, - ty: self.ty, - value: self.value, - } + NamedRegisterOptionValue { name, doc: self.doc, ty: self.ty, value: self.value } } } impl NamedSetOptionValue { + #[allow(dead_code)] pub fn is_none(&self) -> bool { self.value.is_none() } @@ -133,9 +126,7 @@ impl<'lua> FromLua<'lua> for RegisterOptionValue { _ => Err(LuaError::FromLuaConversionError { from: lua_value.type_name(), to: "RegisterOptionValue", - message: Some( - "expected a string or a table with the 'option' instruction".to_string(), - ), + message: Some("expected a string or a table with the 'option' instruction".to_string()), }), } } diff --git a/src/Rust/vvs_lua/src/options/register.rs b/src/Rust/vvs_lua/src/options/register.rs index a58f7eef..00f18c2c 100644 --- a/src/Rust/vvs_lua/src/options/register.rs +++ b/src/Rust/vvs_lua/src/options/register.rs @@ -6,7 +6,10 @@ use crate::{ values::Value, }; use mlua::prelude::*; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use super::actions::{NamedRegisterOptionValue, NamedSetOptionValue}; @@ -18,28 +21,22 @@ pub(crate) struct VivyOptionRegister { toml_overrides: TomlOptions, } -pub(crate) type VivyOptionRegisterPtr = Rc<RefCell<VivyOptionRegister>>; +pub(crate) type VivyOptionRegisterPtr = Arc<RwLock<VivyOptionRegister>>; impl VivyOptionRegister { + /// Create a new option register. pub fn new(toml_overrides: TomlOptions) -> VivyOptionRegisterPtr { - Rc::new(RefCell::new(Self { - toml_overrides, - ..Default::default() - })) + Arc::new(RwLock::new(Self { toml_overrides, ..Default::default() })) } /// Iter over the registered option names. - pub fn iter_registered( - &self, - ) -> std::collections::hash_map::Keys<String, NamedRegisterOptionValue> { + pub fn iter_registered(&self) -> std::collections::hash_map::Keys<String, NamedRegisterOptionValue> { self.registered.keys() } /// Returns toml values that where defined in the passed toml file but where not declared in /// the loaded scripts. - pub(crate) fn iter_undefined_toml_options( - &self, - ) -> impl Iterator<Item = (&str, TomlOptionsValue)> { + pub(crate) fn iter_undefined_toml_options(&self) -> impl Iterator<Item = (&str, TomlOptionsValue)> { self.toml_overrides .iter() .filter(|(name, _)| !self.registered.contains_key(*name)) @@ -49,9 +46,7 @@ impl VivyOptionRegister { /// return [None]. pub fn get_docstring<S: AsRef<str>>(&self, name: S) -> Option<impl IntoIterator<Item = &str>> { match self.registered.get(name.as_ref()) { - Some(NamedRegisterOptionValue { doc, .. }) => { - doc.as_ref().map(|str| str.lines().map(|line| line.trim())) - } + Some(NamedRegisterOptionValue { doc, .. }) => doc.as_ref().map(|str| str.lines().map(|line| line.trim())), None => { log::error!(target: "lua", "option {} not defined!", name.as_ref()); None @@ -63,12 +58,9 @@ impl VivyOptionRegister { /// defined we return [None]. pub fn get_type<S: AsRef<str>>(&self, name: S) -> Option<Type> { match self.registered.get(name.as_ref()) { - Some(NamedRegisterOptionValue { ty, value, .. }) => value - .as_ref() - .map(TypedValue::ty) - .into_iter() - .chain(*ty) - .next(), + Some(NamedRegisterOptionValue { ty, value, .. }) => { + value.as_ref().map(TypedValue::ty).into_iter().chain(*ty).next() + } None => { log::error!(target: "lua", "option {} not defined!", name.as_ref()); None @@ -82,29 +74,30 @@ impl VivyOptionRegister { /// The following are checked in order: /// - is the option overriden by the user with a `set { name, val = ... }` instruction? /// - is the option overriden in the toml file? + /// - if the option should have been in the toml file and is not then returns [None]. /// - returns the default value of the option if it was declared with one /// /// Note: if the option is not defined or no value is found, [None] will be returned. pub fn resolve<S: AsRef<str>>(&self, name: S) -> Option<Value> { let name = name.as_ref(); let NamedRegisterOptionValue { ty, value, .. } = self.registered.get(name)?; - if let Some(NamedSetOptionValue { value, .. }) = self.user_overrides.get(name) { Self::check_type(ty, value.as_ref().map(|value| value as &dyn TypedValue))?; if let Some(value) = value { log::debug!(target: "lua", "returning the user override for option '{name}'"); - return Some(value.clone()); + Some(value.clone()) + } else if let Some(option) = self.toml_overrides.get(name) { + Self::check_type(ty, Some(&option as &dyn TypedValue))?; + log::debug!(target: "lua", "returning the toml override for option '{name}'"); + Some(option.to_value()) + } else { + log::debug!(target: "lua", "the option '{name}' should have been in the toml file but is nowhere to be found"); + None } + } else { + log::debug!(target: "lua", "returning the original declared value for option '{name}'"); + value.clone() } - - if let Some(option) = self.toml_overrides.get(name) { - Self::check_type(ty, Some(&option as &dyn TypedValue))?; - log::debug!(target: "lua", "returning the toml override for option '{name}'"); - return Some(option.to_value()); - } - - log::debug!(target: "lua", "returning the original declared value for option '{name}'"); - value.clone() } /// Register an user override for an option. A double register is an error if the previous @@ -112,27 +105,16 @@ impl VivyOptionRegister { /// Registering an option that was not declared is an error. /// /// Note: The following code is valid in Lua: `set "a"; set { "a", value = 1 }` - fn register_user_override( - &mut self, - name: String, - value: SetOptionValue, - ) -> Result<(), String> { + fn register_user_override(&mut self, name: String, value: SetOptionValue) -> Result<(), String> { if self.registered.get(&name).is_none() { return Err(format!( "can't register option '{name}' as it was not previously declared" )); } - if let Some( - old_value @ NamedSetOptionValue { - value: Some(old), .. - }, - ) = self.user_overrides.get(&name) - { - if !(old_value.is_none() || value.is_none()) { - return Err(format!( - "override already present for option '{name}': {old}" - )); + if let Some(NamedSetOptionValue { value: Some(old), .. }) = self.user_overrides.get(&name) { + if !value.is_none() { + return Err(format!("override already present for option '{name}': {old}")); } } @@ -147,16 +129,18 @@ impl VivyOptionRegister { if let Some(old) = self.registered.get(&value.name) { if PartialEq::ne(old, &value) && (!old.is_none()) { - return Err(format!("option '{}' was already registered\n- old value is {old:?}\n- new value is {value:?}", value.name)); + return Err(format!( + "option '{}' was already registered\n- old value is {old:?}\n- new value is {value:?}", + value.name + )); } } - Self::check_type( - &value.ty, - value.value.as_ref().map(|val| val as &dyn TypedValue), - ) - .ok_or_else(|| { + Self::check_type(&value.ty, value.value.as_ref().map(|val| val as &dyn TypedValue)).ok_or_else(|| { let (ty, val_ty) = (value.ty.unwrap(), value.value.as_ref().unwrap().ty()); - format!("option '{}' was declared with type '{ty}' but was defaulted with value of type '{val_ty}'", value.name) + format!( + "option '{}' was declared with type '{ty}' but was defaulted with value of type '{val_ty}'", + value.name + ) })?; log::debug!(target: "lua", "register option {} with: {value:?}", value.name); self.registered.insert(value.name.to_string(), value); @@ -186,7 +170,7 @@ impl VivyOptionRegister { .map(|opt| format!(" : {} = {opt}", opt.ty())) .unwrap_or_else(|| { self.get_type(name) - .map(|ty| format!(" : {ty}")) + .map(|ty| format!(" : {ty} = !")) .unwrap_or(" : nil".to_string()) }); let doc = self @@ -212,10 +196,7 @@ impl VivyOptionRegister { impl LuaUserData for VivyOptionRegister { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("resolve", |_, this, name: String| { - this.resolve(&name) - .ok_or(LuaError::RuntimeError(format!("option '{name}' not found"))) - }); + methods.add_meta_function("_name", |lua, ()| lua.create_string("VivyOptionRegister")); methods.add_method_mut("will_set", |_, this, name: String| { this.register_user_override(name, SetOptionValue::default()) @@ -228,16 +209,11 @@ impl LuaUserData for VivyOptionRegister { }); methods.add_method_mut("set", |_, this, (name, value): (String, SetOptionValue)| { - this.register_user_override(name, value) - .map_err(LuaError::RuntimeError) + this.register_user_override(name, value).map_err(LuaError::RuntimeError) }); - methods.add_method_mut( - "register", - |_, this, (name, value): (String, RegisterOptionValue)| { - this.register_option(name, value) - .map_err(LuaError::RuntimeError) - }, - ); + methods.add_method_mut("register", |_, this, (name, value): (String, RegisterOptionValue)| { + this.register_option(name, value).map_err(LuaError::RuntimeError) + }); } } diff --git a/src/Rust/vvs_lua/src/toml_option.rs b/src/Rust/vvs_lua/src/toml_option.rs index 5312c4a6..fb77a742 100644 --- a/src/Rust/vvs_lua/src/toml_option.rs +++ b/src/Rust/vvs_lua/src/toml_option.rs @@ -40,32 +40,19 @@ impl TomlOptions { .map(|(name, value)| (name.as_str(), TomlOptionsValue(value))) } - fn verify_toml_table( - table: &toml::map::Map<String, toml::Value>, - ) -> Result<(), TomlOptionsError> { + fn verify_toml_table(table: &toml::map::Map<String, toml::Value>) -> Result<(), TomlOptionsError> { for (name, value) in table { match value { - toml::Value::Datetime(_) => { - return Err(TomlOptionsError::InvalidValue(name.clone(), "Datetime")) - } - toml::Value::Table(_) => { - return Err(TomlOptionsError::InvalidValue(name.clone(), "Table")) - } + toml::Value::Datetime(_) => return Err(TomlOptionsError::InvalidValue(name.clone(), "Datetime")), + toml::Value::Table(_) => return Err(TomlOptionsError::InvalidValue(name.clone(), "Table")), toml::Value::Array(array) => { for value in array { match value { toml::Value::Datetime(_) => { - return Err(TomlOptionsError::InvalidArray( - name.clone(), - "Datetime", - )) - } - toml::Value::Array(_) => { - return Err(TomlOptionsError::InvalidArray(name.clone(), "Array")) - } - toml::Value::Table(_) => { - return Err(TomlOptionsError::InvalidArray(name.clone(), "Table")) + return Err(TomlOptionsError::InvalidArray(name.clone(), "Datetime")) } + toml::Value::Array(_) => return Err(TomlOptionsError::InvalidArray(name.clone(), "Array")), + toml::Value::Table(_) => return Err(TomlOptionsError::InvalidArray(name.clone(), "Table")), _ => {} } } diff --git a/src/Rust/vvs_lua/src/traits.rs b/src/Rust/vvs_lua/src/traits.rs index 0901ec53..08e2b56c 100644 --- a/src/Rust/vvs_lua/src/traits.rs +++ b/src/Rust/vvs_lua/src/traits.rs @@ -1,7 +1,7 @@ //! General traits -use crate::{libs::Vivy, types::*}; -use vvs_ass::{ptr, ASSLine, ASSLines, ASSSyllabe, ASSSyllabes, Ptr}; +use crate::types::*; +use vvs_ass::{ptr, Ptr}; /// A trait for typed values that can be used to pass informations between Lua and Rust code. pub trait TypedValue { @@ -41,7 +41,7 @@ pub trait FromSingleElement { type SingleElement; /// Instanciate an element from a single child. - fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self; + fn from_single_element(single: Self::SingleElement) -> Self; } impl<T: FromSingleElement> FromSingleElement for Ptr<T> @@ -50,28 +50,9 @@ where { type SingleElement = Ptr<<T as FromSingleElement>::SingleElement>; - fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self { + fn from_single_element(single: Self::SingleElement) -> Self { ptr!(FromSingleElement::from_single_element( - vivy, - (*single.borrow()).clone() + (*single.try_read().unwrap()).clone() )) } } - -macro_rules! impl_from_single_elem { - ($from: ident => $to: ident) => { - impl FromSingleElement for paste::paste! { [< ASS $to >] } { - type SingleElement = paste::paste! { [< ASS $from >] }; - fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self { - Self { - position: single.position.clone(), - content: vec![ptr!(single)], - aux: vivy.new_ass_aux_table(vvs_ass::ASSType::$to), - } - } - } - }; -} - -impl_from_single_elem! { Line => Lines } -impl_from_single_elem! { Syllabe => Syllabes } diff --git a/src/Rust/vvs_lua/src/types.rs b/src/Rust/vvs_lua/src/types.rs index 38c60f82..6fcfafc3 100644 --- a/src/Rust/vvs_lua/src/types.rs +++ b/src/Rust/vvs_lua/src/types.rs @@ -11,7 +11,7 @@ pub enum BaseType { } /// Types that are exposed for interactions between Lua and Rust code. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum Type { Base(BaseType), @@ -21,6 +21,15 @@ pub enum Type { Array, } +impl std::fmt::Debug for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Base(arg0) => arg0.fmt(f), + Self::Array => write!(f, "Array"), + } + } +} + impl AsRef<str> for Type { fn as_ref(&self) -> &str { match self { diff --git a/src/Rust/vvs_lua/src/values.rs b/src/Rust/vvs_lua/src/values.rs index 777cae15..594bcbbd 100644 --- a/src/Rust/vvs_lua/src/values.rs +++ b/src/Rust/vvs_lua/src/values.rs @@ -3,9 +3,10 @@ use crate::{traits::TypedValue, types::*}; use mlua::prelude::*; use std::fmt::Write; +use vvs_ass::ASSAuxValue; /// The base values that can be passed to/from Lua code. -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq)] pub enum BaseValue { Integer(i64), Floating(f64), @@ -14,12 +15,32 @@ pub enum BaseValue { } /// The values that can be passed to/from Lua code. -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq)] pub enum Value { Base(BaseValue), Array(Vec<BaseValue>), } +impl std::fmt::Debug for BaseValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Integer(arg0) => write!(f, "{arg0}"), + Self::Floating(arg0) => write!(f, "{arg0}"), + Self::String(arg0) => write!(f, "{arg0:?}"), + Self::Boolean(arg0) => write!(f, "{arg0}"), + } + } +} + +impl std::fmt::Debug for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Base(arg0) => write!(f, "{arg0:?}"), + Self::Array(arg0) => write!(f, "{arg0:?}"), + } + } +} + impl AsRef<str> for BaseType { fn as_ref(&self) -> &str { match self { @@ -60,8 +81,7 @@ impl TryFrom<&toml::Value> for Value { toml::Value::Float(number) => Ok(Self::Base(BaseValue::Floating(*number))), toml::Value::Boolean(boolean) => Ok(Self::Base(BaseValue::Boolean(*boolean))), toml::Value::Array(vals) => { - let (vals, errors): (Vec<_>, Vec<_>) = - vals.iter().map(Value::try_from).partition(Result::is_ok); + let (vals, errors): (Vec<_>, Vec<_>) = vals.iter().map(Value::try_from).partition(Result::is_ok); if !errors.is_empty() { errors .into_iter() @@ -122,8 +142,7 @@ impl<'lua> FromLua<'lua> for Value { | base @ LuaValue::String(_) => Ok(Self::Base(BaseValue::from_lua(base, lua)?)), LuaValue::Table(vals) => { - let (vals, errors): (Vec<_>, Vec<_>) = - vals.sequence_values::<BaseValue>().partition(Result::is_ok); + let (vals, errors): (Vec<_>, Vec<_>) = vals.sequence_values::<BaseValue>().partition(Result::is_ok); if let Some(err) = errors.into_iter().map(Result::unwrap_err).next() { return Err(err); } @@ -183,3 +202,25 @@ impl std::fmt::Display for Value { } } } + +impl From<BaseValue> for ASSAuxValue { + fn from(value: BaseValue) -> Self { + match value { + BaseValue::Floating(val) => ASSAuxValue::Floating(val), + BaseValue::Integer(val) => ASSAuxValue::Integer(val), + BaseValue::Boolean(val) => ASSAuxValue::Boolean(val), + BaseValue::String(val) => ASSAuxValue::String(val), + } + } +} + +impl TryInto<ASSAuxValue> for Value { + type Error = String; + + fn try_into(self) -> Result<ASSAuxValue, Self::Error> { + match self { + Value::Base(base) => Ok(base.into()), + Value::Array(_) => Err("can't convert an array value into an ASSAuxValue".to_string()), + } + } +} diff --git a/src/Rust/vvs_procmacro/src/lib.rs b/src/Rust/vvs_procmacro/src/lib.rs index 3d267f43..64bbd40d 100644 --- a/src/Rust/vvs_procmacro/src/lib.rs +++ b/src/Rust/vvs_procmacro/src/lib.rs @@ -1,10 +1,13 @@ +#![forbid(unsafe_code)] + use proc_macro::TokenStream; use proc_macro2::*; use quote::*; +use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(EnumVariantCount)] pub fn derive_enum_variant_count(input: TokenStream) -> TokenStream { - let syn_item = syn::parse::<syn::DeriveInput>(input).expect("failed to parse input with syn"); + let syn_item = parse_macro_input!(input as DeriveInput); let (len, name) = match syn_item.data { syn::Data::Enum(enum_item) => ( enum_item.variants.len(), @@ -20,7 +23,7 @@ pub fn derive_enum_variant_count(input: TokenStream) -> TokenStream { #[proc_macro_derive(EnumVariantIter)] pub fn derive_enum_variant_iter(input: TokenStream) -> TokenStream { - let syn_item = syn::parse::<syn::DeriveInput>(input).expect("failed to parse input with syn"); + let syn_item = parse_macro_input!(input as DeriveInput); let (enum_name, name, content) = match syn_item.data { syn::Data::Enum(enum_item) => ( syn_item.ident.clone(), @@ -43,3 +46,40 @@ pub fn derive_enum_variant_iter(input: TokenStream) -> TokenStream { }; quote! { pub const #name : &[#enum_name] = #content; }.into() } + +#[proc_macro_derive(EnumVariantFromStr)] +pub fn derive_enum_variant_from_str(input: TokenStream) -> TokenStream { + let syn_item = parse_macro_input!(input as DeriveInput); + let enum_item = match syn_item.data { + syn::Data::Enum(enum_item) => enum_item, + _ => panic!("EnumVariantFromStr only works on Enums"), + }; + let name = syn_item.ident; + let match_branches = enum_item + .variants + .into_iter() + .map(|variant| { + format!( + "{:?} => Ok({name}::{}),", + variant.ident.to_string().to_lowercase(), + variant.ident + ) + }) + .chain(Some("_ => Err(format!(\"unknown field '{{s}}'\")),".to_string())) + .collect::<Vec<_>>() + .join(""); + let match_branches = syn::parse_str::<syn::Expr>(&format!( + "match s.trim().to_lowercase().as_str() {{ {match_branches} }}" + )) + .expect("failed generation of enum's FromStr implementation"); + quote! { + impl std::str::FromStr for #name { + type Err = std::string::String; + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + #match_branches + } + + } + } + .into() +} diff --git a/src/Rust/vvs_repl/Cargo.toml b/src/Rust/vvs_repl/Cargo.toml index 14ec9b1a..8ef9155b 100644 --- a/src/Rust/vvs_repl/Cargo.toml +++ b/src/Rust/vvs_repl/Cargo.toml @@ -8,10 +8,9 @@ description = "REPL implementation for VVS" [dependencies] log.workspace = true +mlua.workspace = true thiserror.workspace = true -vvs_lua = { path = "../vvs_lua" } - rustyline = { version = "^11", default-features = false, features = [ "case_insensitive_history_search", ] } diff --git a/src/Rust/vvs_repl/src/error.rs b/src/Rust/vvs_repl/src/error.rs index 3866f72c..e9f379ba 100644 --- a/src/Rust/vvs_repl/src/error.rs +++ b/src/Rust/vvs_repl/src/error.rs @@ -1,9 +1,10 @@ +use mlua::prelude::*; use thiserror::Error; #[derive(Debug, Error)] pub enum ReplError { - Lua(vvs_lua::mlua::Error), - Prompt(&'static str, vvs_lua::mlua::Error), + Lua(LuaError), + Prompt(&'static str, LuaError), ReadLine(rustyline::error::ReadlineError), } diff --git a/src/Rust/vvs_repl/src/lib.rs b/src/Rust/vvs_repl/src/lib.rs index 6b271382..44e293c1 100644 --- a/src/Rust/vvs_repl/src/lib.rs +++ b/src/Rust/vvs_repl/src/lib.rs @@ -1,9 +1,10 @@ //! The REPL for VivyScript +#![forbid(unsafe_code)] use crate::{error::ReplError, tables::*}; +use mlua::prelude::*; use rustyline::{error::ReadlineError, history::DefaultHistory, Editor}; use std::sync::atomic::AtomicBool; -use vvs_lua::mlua::prelude::*; mod error; mod tables; @@ -24,31 +25,28 @@ impl REPL { } fn set_prompt_multiline(&self) { - self.prompt_multiline - .store(true, std::sync::atomic::Ordering::Relaxed); + self.prompt_multiline.store(true, std::sync::atomic::Ordering::Relaxed); } fn set_prompt_singleline(&self) { - self.prompt_multiline - .store(false, std::sync::atomic::Ordering::Relaxed); + self.prompt_multiline.store(false, std::sync::atomic::Ordering::Relaxed); } fn get_prompt(&self) -> Result<String, ReplError> { - let mut packages = vvs_lua::get_loaded(&self.lua).map_err(|lua| { - ReplError::Prompt("failed to get the load path from the vivy runtime", lua) - })?; + let mut packages: Vec<String> = self + .lua + .load(mlua::chunk! { vivy:___get_loaded() }) + .eval() + .map_err(|lua| ReplError::Prompt("failed to get the load path from the vivy runtime", lua))?; packages.sort(); - let prompt = match self - .prompt_multiline - .load(std::sync::atomic::Ordering::Relaxed) - { + let prompt = match self.prompt_multiline.load(std::sync::atomic::Ordering::Relaxed) { true => ">>", false => ">", }; Ok(if packages.is_empty() { - format!("vivy {}", prompt) + format!("vivy {} ", prompt) } else { format!("vivy {{ {} }} {} ", packages.join(", "), prompt) }) @@ -80,8 +78,17 @@ impl REPL { LuaValue::String(string) => format!("{string:?}"), LuaValue::Table(table) => { let inner = table.pairs::<LuaValue, LuaValue>().flat_map(|pair| { - pair.ok() - .map(|(key, value)| (TableKey(key), TableValue(value.type_name()))) + pair.ok().map(|(key, value)| { + let value = match value { + LuaNil => "nil".to_string(), + LuaValue::Boolean(boolean) => format!("{boolean}"), + LuaValue::Integer(num) => format!("{num}"), + LuaValue::Number(num) => format!("{num}"), + LuaValue::String(str) => format!("{:?}", str.to_string_lossy()), + value => value.type_name().to_string(), + }; + (TableKey(key), TableValue(value)) + }) }); format!("{}", TableFormatter(inner.collect())) } @@ -91,11 +98,7 @@ impl REPL { } _ => value.type_name().to_string(), }; - values - .into_iter() - .map(handle_value) - .collect::<Vec<_>>() - .join(", ") + values.into_iter().map(handle_value).collect::<Vec<_>>().join(", ") } /// Handle a command sent by the user. Returns an error if the readline or the lua encountered @@ -129,10 +132,7 @@ impl REPL { return Ok(false); } - Err(LuaError::SyntaxError { - incomplete_input: true, - .. - }) => { + Err(LuaError::SyntaxError { incomplete_input: true, .. }) => { // continue reading input and append it to `line` line.push('\n'); // separate input lines self.set_prompt_multiline(); diff --git a/src/Rust/vvs_repl/src/tables.rs b/src/Rust/vvs_repl/src/tables.rs index f3825a61..3d970ffb 100644 --- a/src/Rust/vvs_repl/src/tables.rs +++ b/src/Rust/vvs_repl/src/tables.rs @@ -1,7 +1,7 @@ -use vvs_lua::mlua::prelude::*; +use mlua::prelude::*; pub(crate) struct TableKey<'lua>(pub LuaValue<'lua>); -pub(crate) struct TableValue(pub &'static str); +pub(crate) struct TableValue(pub String); pub(crate) struct TableFormatter<'lua>(pub Vec<(TableKey<'lua>, TableValue)>); impl<'lua> std::fmt::Debug for TableKey<'lua> { @@ -18,7 +18,7 @@ impl<'lua> std::fmt::Debug for TableKey<'lua> { impl std::fmt::Debug for TableValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0) + f.write_str(&self.0) } } diff --git a/src/Rust/vvs_utils/Cargo.toml b/src/Rust/vvs_utils/Cargo.toml index 592c9a48..b27c119f 100644 --- a/src/Rust/vvs_utils/Cargo.toml +++ b/src/Rust/vvs_utils/Cargo.toml @@ -5,3 +5,8 @@ authors.workspace = true edition.workspace = true license.workspace = true description = "Utility crate for VVS" + +[dependencies] +thiserror.workspace = true +serde.workspace = true +log.workspace = true diff --git a/src/Rust/vvs_utils/src/lib.rs b/src/Rust/vvs_utils/src/lib.rs index fad61c68..2130d45f 100644 --- a/src/Rust/vvs_utils/src/lib.rs +++ b/src/Rust/vvs_utils/src/lib.rs @@ -1,8 +1,12 @@ +#![forbid(unsafe_code)] + mod angles; mod assert; mod conds; mod minmax; +pub mod xdg; + pub use angles::*; pub use assert::*; pub use conds::*; diff --git a/src/Rust/vvs_utils/src/minmax.rs b/src/Rust/vvs_utils/src/minmax.rs index 80252cee..70263bb9 100644 --- a/src/Rust/vvs_utils/src/minmax.rs +++ b/src/Rust/vvs_utils/src/minmax.rs @@ -1,19 +1,11 @@ #[inline] pub fn min<T: PartialOrd>(a: T, b: T) -> T { - if a < b { - a - } else { - b - } + crate::either!(a < b => a; b) } #[inline] pub fn max<T: PartialOrd>(a: T, b: T) -> T { - if a > b { - a - } else { - b - } + crate::either!(a > b => a; b) } #[macro_export] diff --git a/src/Rust/vvs_utils/src/xdg/config.rs b/src/Rust/vvs_utils/src/xdg/config.rs new file mode 100644 index 00000000..3e15ff69 --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/config.rs @@ -0,0 +1,384 @@ +//! Utilities to extend the [XDGFolder] thing with config files: take into account the +//! deserialization, merge of configs and others. + +use crate::xdg::{MaybeFolderList, XDGError, XDGFindBehaviour, XDGFolder}; +use serde::{Deserialize, Serialize}; +use std::rc::Rc; + +/// Search configurations in all config folders and merge them. +#[derive(Debug)] +pub struct XDGConfigMerged; + +/// Only get the config file with higher priority, which means searching in order: +/// 1. $XDG_CONFIG_HOME <- which has a default value. +/// 2. $XDG_CONFIG_DIRS <- which has a default value. +#[derive(Debug)] +pub struct XDGConfigFirst; + +/// Search configurations in all config folders and merge them. If the parsing for a configuration +/// file failed then log it and continue execution like normal, i.e. fail silently. +#[derive(Debug)] +pub struct XDGConfigMergedSilent; + +pub type DeserializeFunctionPtr<'a, Format> = + Rc<dyn Fn(String) -> Result<Format, Box<dyn std::error::Error>> + 'static>; + +/// We will write one impl block for merged searches. +mod private_merged { + use crate::xdg::*; + use serde::Deserialize; + use std::collections::VecDeque; + + /// Search with merging. + pub trait Sealed: Sized { + fn try_read<'a, Format>(config: &XDGConfig<'a, Format, Self>) -> Result<Format, XDGError> + where + Format: for<'de> Deserialize<'de> + Extend<Format>; + } + + impl Sealed for XDGConfigMergedSilent { + fn try_read<'a, Format>(config: &XDGConfig<'a, Format, Self>) -> Result<Format, XDGError> + where + Format: for<'de> Deserialize<'de> + Extend<Format>, + { + use XDGError::*; + let result = config.search_files()?.into_iter().filter_map(|file| { + let file = std::fs::read_to_string(file) + .map_err(|err| log::error!(target: "xdg", "{}", ConfigIO(config.app.to_string(), config.get_file().to_string(), err))) + .ok()?; + config.deserialize.as_ref()(file) + .map_err(|err| log::error!(target: "xdg", "{}", DeserializeError(config.app.to_string(), config.get_file().to_string(), err))) + .ok() + }); + + let mut result: VecDeque<Format> = result.collect(); + let mut first = result + .pop_front() + .ok_or(ConfigNotFound(config.app.to_string(), config.get_file().to_string()))?; + first.extend(result); + Ok(first) + } + } + + impl Sealed for XDGConfigMerged { + fn try_read<'a, Format>(config: &XDGConfig<'a, Format, Self>) -> Result<Format, XDGError> + where + Format: for<'de> Deserialize<'de> + Extend<Format>, + { + use XDGError::*; + let mut result: VecDeque<Format> = Default::default(); + for file in config.search_files()?.into_iter() { + let file = std::fs::read_to_string(file) + .map_err(|err| ConfigIO(config.app.to_string(), config.get_file().to_string(), err))?; + let file = config.deserialize.as_ref()(file) + .map_err(|err| DeserializeError(config.app.to_string(), config.get_file().to_string(), err))?; + result.push_back(file); + } + let mut first = result + .pop_front() + .ok_or(ConfigNotFound(config.app.to_string(), config.get_file().to_string()))?; + first.extend(result); + Ok(first) + } + } +} + +/// A struct to contain informations about the app we are querying the config for. The behaviour of +/// the search can be changed. +/// --- +/// On the `Format` generic parameter: +/// - If the `Format` parameter implements [Serialize], then you can write or provide a default +/// that will be written to disk (or fail silently...) +/// - If the `Format` parameter implements [Serialize] and [Default], you can depend on the +/// default value if an error occured in the deserialization. +/// --- +/// On the `Merged` generic parameter: +/// - If the `Merged` parameter is [XDGConfigFirst], then only the file with the higher priority +/// is parsed. +/// - If the `Merged` parameter is [XDGConfigMerged] and `Format` implements [Extend] on itself, +/// then all the found files are parsed and then merged, an error is returned on the first +/// failure of the deserialization process. +pub struct XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de>, +{ + /// The application name. + app: &'a str, + + /// The file name. If not present will be defaulted to `config` at resolution time. + file: Option<&'a str>, + + /// Deserializer function. + deserialize: DeserializeFunctionPtr<'a, Format>, + + /// Stores the format for deserialization. + _type: std::marker::PhantomData<Format>, + + /// Stores the format for deserialization. + _merged: std::marker::PhantomData<Merged>, +} + +impl<'a, Format, Merged> std::fmt::Debug for XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XDGConfig") + .field("app", &self.app) + .field("file", &self.get_file()) + .field("merged", &self._merged) + .finish() + } +} + +impl<'a, Format, Merged> XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de>, +{ + /// By default we say that the default config file is ${FOLDER}/${APP}/config like many + /// applications do. + const DEFAULT_FILE: &'static str = "config"; + + /// Create a new [XDGConfig] helper. + pub fn new( + app: &'a str, + deserialize: impl Fn(String) -> Result<Format, Box<dyn std::error::Error>> + 'static, + ) -> Self { + Self { + app, + deserialize: Rc::new(deserialize), + file: Default::default(), + _type: Default::default(), + _merged: Default::default(), + } + } + + /// Change the file to resolve. + pub fn file(&mut self, file: &'a str) -> &mut Self { + self.file = Some(file); + self + } + + /// Get the name of the config file that we will try to read. Returns the default if not set by + /// the user. + pub fn get_file(&self) -> &str { + match self.file.as_ref() { + Some(file) => file, + None => Self::DEFAULT_FILE, + } + } + + /// Search all config files in all locations... + fn search_files(&self) -> Result<MaybeFolderList, XDGError> { + XDGFolder::ConfigDirs.find(self.app, self.get_file(), XDGFindBehaviour::ExistingOnly) + } + + /// Prepare config folders. + pub fn prepare_folder(&self) -> impl IntoIterator<Item = <MaybeFolderList as IntoIterator>::Item> { + XDGFolder::ConfigDirs.prepare_folder() + } +} + +impl<'a, Format> XDGConfig<'a, Format, XDGConfigFirst> +where + Format: for<'de> Deserialize<'de>, +{ + /// Try to read the config file and deserialize it. + pub fn try_read(&self) -> Result<Format, XDGError> { + let Some(file) = self.search_files()?.into_first() else { + return Err(XDGError::ConfigNotFound( + self.app.to_string(), + self.get_file().to_string() + )); + }; + let file = std::fs::read_to_string(file) + .map_err(|err| XDGError::ConfigIO(self.app.to_string(), self.get_file().to_string(), err))?; + self.deserialize.as_ref()(file) + .map_err(|err| XDGError::DeserializeError(self.app.to_string(), self.get_file().to_string(), err)) + } +} + +impl<'a, Format> XDGConfig<'a, Format, XDGConfigFirst> +where + Format: for<'de> Deserialize<'de> + Serialize, +{ + /// Try to read the config file and deserialize it. If an error is encountred at any point, log + /// it and return the provided default. If needed the default value is written on the disk, + /// note that this operation may fail silently. + pub fn read_or( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + default: Format, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.write_silent(serialize, default) + }) + } + + /// Try to read the config file and deserialize it. If an error is encountred at any point, log + /// it and return the provided default. If needed the default value is written on the disk, + /// note that this operation may fail silently. + pub fn read_or_else( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + cb: impl FnOnce() -> Format, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.write_silent(serialize, cb()) + }) + } + + /// Write a value to the default config file location, the one with the higher priority + /// (usually the user's one) + fn write( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + value: &Format, + ) -> Result<(), XDGError> { + let content = serialize(value) + .map_err(|err| XDGError::SerializeError(self.app.to_string(), self.get_file().to_string(), err))?; + let Some(path) = XDGFolder::ConfigDirs + .find(self.app, self.get_file(), XDGFindBehaviour::FirstOrCreate)? + .into_first() else { unreachable!( + "the user must have at least one location to place a config file, permission or fs quota problem?" + )}; + + match std::fs::create_dir_all(path.parent().ok_or(XDGError::ConfigFileHasNoParentFolder( + self.app.to_string(), + self.get_file().to_string(), + ))?) { + Err(err) if !matches!(err.kind(), std::io::ErrorKind::AlreadyExists) => { + return Err(XDGError::ConfigIO( + self.app.to_string(), + self.get_file().to_string(), + err, + )) + } + _ => {} + } + + std::fs::write(path, content) + .map_err(|err| XDGError::ConfigIO(self.app.to_string(), self.get_file().to_string(), err))?; + + Ok(()) + } + + /// Same as [XDGConfig::write] but log any error and fail silently, returning the value that + /// was attempted to be written to disk. + fn write_silent( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + value: Format, + ) -> Format { + if let Err(err) = self.write(serialize, &value) { + log::error!(target: "xdg", "failed to write default with err: {err}") + } + value + } +} + +impl<'a, Format> XDGConfig<'a, Format, XDGConfigFirst> +where + Format: for<'de> Deserialize<'de> + Serialize + Default, +{ + /// Try to read the config file and deserialize it. If an error is encountred at any point, log + /// it and return the default. If needed the default value is written on the disk, note that + /// this operation may fail silently. + pub fn read_or_default( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.write_silent(serialize, Default::default()) + }) + } +} + +// XDGConfigMerged + XDGConfigMergedSilent + +impl<'a, Format, Merged> XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de> + Extend<Format>, + Merged: private_merged::Sealed, +{ + /// When trying to read or write the default, we write the file with the same logic as the + /// [XDGConfigFirst] variant, we add this function to reduce the code to write for the write + /// logic... + fn to_config_first(&self) -> XDGConfig<Format, XDGConfigFirst> { + XDGConfig::<Format, XDGConfigFirst> { + app: self.app, + file: self.file, + deserialize: self.deserialize.clone(), + _type: std::marker::PhantomData, + _merged: std::marker::PhantomData, + } + } + + /// Try to read the config files and deserialize them. If one config file failed to be + /// deserialized then returns an error if the merged was not silent, or just ignore the file if + /// the merge was silent. If no config file where found this is an error. All the files are + /// merged, this operation can't fail, implementations must fail silently or have sane defaults + /// and prefer files with higher priority. + pub fn try_read(&self) -> Result<Format, XDGError> { + Merged::try_read(self) + } +} + +impl<'a, Format, Merged> XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de> + Extend<Format> + Serialize, + Merged: private_merged::Sealed, +{ + /// Try to read the config files and deserialize them. If an error is encountred at any point, + /// log it and return the provided default if the merge was not silent, skip the file if it was + /// silent. If needed the default value is written on the disk, note that this operation may + /// fail silently. + pub fn read_or( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + default: Format, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.to_config_first().write_silent(serialize, default) + }) + } + + /// Try to read the config files and deserialize them. If an error is encountred at any point, + /// log it and return the provided default if the merge was not silent, skip the file if it was + /// silent. If needed the default value is written on the disk, note that this operation may + /// fail silently. + pub fn read_or_else( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + cb: impl FnOnce() -> Format, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.to_config_first().write_silent(serialize, cb()) + }) + } +} + +impl<'a, Format, Merged> XDGConfig<'a, Format, Merged> +where + Format: for<'de> Deserialize<'de> + Extend<Format> + Serialize + Default, + Merged: private_merged::Sealed, +{ + /// Try to read the config files and deserialize them. If an error is encountred at any point, + /// log it and return the provided default if the merge was not silent, skip the file if it was + /// silent. If needed the default value is written on the disk, note that this operation may + /// fail silently. + pub fn read_or_default( + &self, + serialize: impl FnOnce(&Format) -> Result<String, Box<dyn std::error::Error>>, + ) -> Format { + self.try_read().unwrap_or_else(|err| { + log::error!(target: "xdg", "read error, return default value to user: {err}"); + self.to_config_first().write_silent(serialize, Default::default()) + }) + } +} diff --git a/src/Rust/vvs_utils/src/xdg/folders.rs b/src/Rust/vvs_utils/src/xdg/folders.rs new file mode 100644 index 00000000..c517830c --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/folders.rs @@ -0,0 +1,256 @@ +//! The main provider for resolving files with the XDG specification + some utilities. + +use crate::{ + either, + xdg::{MaybeFolderList, XDGError, XDGFindBehaviour}, +}; +use std::{io::ErrorKind as IoErrorKind, path::PathBuf}; + +/// The type of folder we want. Here are some remarks from the specification about file resolution: +/// +/// - If, when attempting to write a file, the destination directory is non-existent an attempt +/// should be made to create it with permission 0700. If the destination directory exists already +/// the permissions should not be changed. The application should be prepared to handle the case +/// where the file could not be written, either because the directory was non-existent and could +/// not be created, or for any other reason. In such case it may choose to present an error +/// message to the user. +/// - When attempting to read a file, if for any reason a file in a certain directory is +/// unaccessible, e.g. because the directory is non-existent, the file is non-existent or the +/// user is not authorized to open the file, then the processing of the file in that directory +/// should be skipped. If due to this a required file could not be found at all, the application +/// may choose to present an error message to the user. +/// - A specification that refers to $XDG_DATA_DIRS or $XDG_CONFIG_DIRS should define what the +/// behaviour must be when a file is located under multiple base directories. It could, for +/// example, define that only the file under the most important base directory should be used or, +/// as another example, it could define rules for merging the information from the different +/// files. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum XDGFolder { + /// $XDG_DATA_HOME defines the base directory relative to which user-specific data files should + /// be stored. If $XDG_DATA_HOME is either not set or empty, a default equal to + /// $HOME/.local/share should be used. + DataHome, + + /// $XDG_CONFIG_HOME defines the base directory relative to which user-specific configuration + /// files should be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to + /// $HOME/.config should be used. + ConfigHome, + + /// $XDG_STATE_HOME defines the base directory relative to which user-specific state files + /// should be stored. If $XDG_STATE_HOME is either not set or empty, a default equal to + /// $HOME/.local/state should be used. + /// + /// The $XDG_STATE_HOME contains state data that should persist between (application) restarts, + /// but that is not important or portable enough to the user that it should be stored in + /// $XDG_DATA_HOME. It may contain: + /// - actions history (logs, history, recently used files, …) + /// - current state of the application that can be reused on a restart (view, layout, open + /// files, undo history, …) + StateHome, + + /// $XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data + /// files in addition to the $XDG_DATA_HOME base directory. The directories in $XDG_DATA_DIRS + /// should be seperated with a colon ':'. + /// + /// If $XDG_DATA_DIRS is either not set or empty, a value equal to + /// /usr/local/share/:/usr/share/ should be used. + DataDirs, + + /// $XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for + /// configuration files in addition to the $XDG_CONFIG_HOME base directory. The directories in + /// $XDG_CONFIG_DIRS should be seperated with a colon ':'. + /// + /// If $XDG_CONFIG_DIRS is either not set or empty, a value equal to /etc/xdg should be used. + ConfigDirs, + + /// $XDG_CACHE_HOME defines the base directory relative to which user-specific non-essential + /// data files should be stored. If $XDG_CACHE_HOME is either not set or empty, a default equal + /// to $HOME/.cache should be used. + CacheHome, + + /// $XDG_RUNTIME_DIR defines the base directory relative to which user-specific non-essential + /// runtime files and other file objects (such as sockets, named pipes, ...) should be stored. + /// The directory MUST be owned by the user, and he MUST be the only one having read and write + /// access to it. Its Unix access mode MUST be 0700. + /// + /// The lifetime of the directory MUST be bound to the user being logged in. It MUST be created + /// when the user first logs in and if the user fully logs out the directory MUST be removed. + /// If the user logs in more than once he should get pointed to the same directory, and it is + /// mandatory that the directory continues to exist from his first login to his last logout on + /// the system, and not removed in between. Files in the directory MUST not survive reboot or a + /// full logout/login cycle. + /// + /// The directory MUST be on a local file system and not shared with any other system. The + /// directory MUST by fully-featured by the standards of the operating system. More + /// specifically, on Unix-like operating systems AF_UNIX sockets, symbolic links, hard links, + /// proper permissions, file locking, sparse files, memory mapping, file change notifications, + /// a reliable hard link count must be supported, and no restrictions on the file name + /// character set should be imposed. Files in this directory MAY be subjected to periodic + /// clean-up. To ensure that your files are not removed, they should have their access time + /// timestamp modified at least once every 6 hours of monotonic time or the 'sticky' bit should + /// be set on the file. + /// + /// If $XDG_RUNTIME_DIR is not set applications should fall back to a replacement directory + /// with similar capabilities and print a warning message. Applications should use this + /// directory for communication and synchronization purposes and should not place larger files + /// in it, since it might reside in runtime memory and cannot necessarily be swapped out to + /// disk. + RuntimeDir, +} + +impl XDGFolder { + /// The list separator. + const SEPARATOR: char = ':'; + + /// Get the folders or folder list that the user asked for. If the env variable is not present, + /// falls back to the default value. + /// + /// Note that the folder or folders doesn't exist, this function won't try to create them. + pub fn get_folder(&self) -> MaybeFolderList { + use XDGFolder::*; + let home = crate::xdg::home_folder(); + match std::env::var(self.env_var_name()) { + Ok(folder_list) if self.is_list() => MaybeFolderList::from_str_list(&folder_list, Self::SEPARATOR), + Ok(folder) => MaybeFolderList::Folder(PathBuf::from(folder)), + Err(_) => match self { + DataHome => MaybeFolderList::Folder(home.join(".local/share")), + ConfigHome => MaybeFolderList::Folder(home.join(".config")), + StateHome => MaybeFolderList::Folder(home.join(".local/state")), + CacheHome => MaybeFolderList::Folder(home.join(".cache")), + RuntimeDir => panic!("failed to find the env variable $XDG_RUNTIME_DIR"), + DataDirs => MaybeFolderList::from_iter(["/usr/local/share", "/usr/share"]), + ConfigDirs => MaybeFolderList::from("/etc/xdg"), + }, + } + } + + /// Get the folders list of what you asked for, but try to create them and only return existing + /// ones. The thing returned is a thing that can be iterate over to facilitate chaining. + pub fn prepare_folder(&self) -> impl IntoIterator<Item = <MaybeFolderList as IntoIterator>::Item> { + self.get_folder().into_iter().filter_map(|folder| { + if folder.is_dir() { + Some(folder) + } else { + std::fs::create_dir_all(&folder) + .map(|()| folder) + .map_err(|err| log::error!(target: "xdg", "{err}")) + .ok() + } + }) + } + + /// Get the file associated with the said application in the specified folders. An option can + /// be passed to specify behaviour in the search or file/folder creation processes. + pub fn find( + &self, + app: impl AsRef<str>, + file: impl AsRef<str>, + opt: XDGFindBehaviour, + ) -> Result<MaybeFolderList, XDGError> { + enum FindState { + CanCreate(PathBuf), + Exists(PathBuf), + } + impl<'a> std::iter::Extend<&'a FindState> for MaybeFolderList { + fn extend<T: IntoIterator<Item = &'a FindState>>(&mut self, iter: T) { + self.extend(iter.into_iter().map(|m| match m { + FindState::CanCreate(path) | FindState::Exists(path) => path.clone(), + })); + } + } + + // DataHome is more important than DataDirs in the search process... Idem for the Config + // env var family... + let matches: Vec<_> = match self { + XDGFolder::DataDirs => XDGFolder::DataHome.get_folder(), + XDGFolder::ConfigDirs => XDGFolder::ConfigHome.get_folder(), + _ => MaybeFolderList::Empty, + } + .into_iter() + .chain(self.get_folder().into_iter()) + .flat_map(|path| { + let path = path.join(app.as_ref()); + if let Err(err) = std::fs::create_dir_all(&path) { + if !matches!(err.kind(), IoErrorKind::PermissionDenied | IoErrorKind::AlreadyExists) { + log::error!(target: "xdg", + "failed to create app folder for {{{self}}}/{}/{} as {}", + app.as_ref(), file.as_ref(), path.to_string_lossy() + ); + return None; + } + } + let path = path.join(file.as_ref()); + Some(either!(path.exists() => FindState::Exists(path); FindState::CanCreate(path))) + }) + .collect(); + + // For now, default behaviour is to return the first file found or try to create it, the + // first file created will be returned (if we can't create the file in system folders it's + // not an error...) + + let (mut exists, mut can_create): (MaybeFolderList, MaybeFolderList) = + matches.iter().partition(|m| matches!(m, FindState::Exists(_))); + use XDGFindBehaviour::*; + match (exists.is_some(), can_create.is_some()) { + // [XDGFindBehaviour::FirstOrCreate] case... + (true, false) if opt == FirstOrCreate => Ok(exists.into_first().into_iter().collect()), + (false, true) if opt == FirstOrCreate => Ok(can_create.into_first().into_iter().collect()), + + // Should we return the one list that is not empty? + (true, false) if matches!(opt, AllFiles | ExistingOnly) => Ok(exists), + (false, true) if matches!(opt, AllFiles | NonExistingOnly) => Ok(can_create), + + // Complicated case + (true, true) => match opt { + AllFiles => { + exists.append(&mut can_create); + Ok(exists) + } + + FirstOrCreate => Ok(exists.into_first().into_iter().collect()), + ExistingOnly => Ok(exists), + NonExistingOnly => Ok(can_create), + }, + + // The list that we want is empty, or all lists are empty, returns nothing -> should it + // really be an error? + _ => Err(XDGError::NotFound( + *self, + app.as_ref().to_string(), + file.as_ref().to_string(), + )), + } + } + + /// Is this variable a folder list? + pub fn is_list(&self) -> bool { + use XDGFolder::*; + matches!(self, DataDirs | ConfigDirs) + } + + /// Get the env variable name associated with the folder or folder list. + pub fn env_var_name(&self) -> &str { + self.as_ref() + } +} + +impl AsRef<str> for XDGFolder { + fn as_ref(&self) -> &str { + match self { + XDGFolder::DataHome => "XDG_DATA_HOME", + XDGFolder::StateHome => "XDG_STATE_HOME", + XDGFolder::CacheHome => "XDG_CACHE_HOME", + XDGFolder::ConfigHome => "XDG_CONFIG_HOME", + + XDGFolder::DataDirs => "XDG_DATA_DIRS", + XDGFolder::ConfigDirs => "XDG_CONFIG_DIRS", + XDGFolder::RuntimeDir => "XDG_RUNTIME_DIR", + } + } +} + +impl std::fmt::Display for XDGFolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "${}", self.as_ref()) + } +} diff --git a/src/Rust/vvs_utils/src/xdg/mod.rs b/src/Rust/vvs_utils/src/xdg/mod.rs new file mode 100644 index 00000000..bc1e04b2 --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/mod.rs @@ -0,0 +1,47 @@ +//! Utility functions to follow the freedesktop specifications on config folders and such: +//! https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + +#[cfg(test)] +mod tests; + +mod config; +mod folders; +mod options; +mod paths; + +pub use config::*; +pub use folders::*; +pub use options::*; +pub use paths::*; + +use std::{io::Error as IoError, path::PathBuf}; +use thiserror::Error; + +/// Get the user's home folder. Panics if the env variable was not found or it can't be +/// canonicalized. +pub fn home_folder() -> PathBuf { + PathBuf::from(std::env::var("HOME").expect("failed to get the $HOME env variable")) + .canonicalize() + .expect("failed to canonicalize the $HOME folder path") +} + +#[derive(Debug, Error)] +pub enum XDGError { + #[error("failed to find file {2} for application {1} with folder family {0}")] + NotFound(XDGFolder, String, String), + + #[error("failed to find config file {1} for application {0}")] + ConfigNotFound(String, String), + + #[error("failed to find parent folder of config file {1} for application {0}")] + ConfigFileHasNoParentFolder(String, String), + + #[error("io error for config file {1} for application {0}: {2}")] + ConfigIO(String, String, IoError), + + #[error("deserialization failed on file {1} for application {0} with: {2}")] + DeserializeError(String, String, Box<dyn std::error::Error>), + + #[error("serialization failed on file {1} for application {0} with: {2}")] + SerializeError(String, String, Box<dyn std::error::Error>), +} diff --git a/src/Rust/vvs_utils/src/xdg/options.rs b/src/Rust/vvs_utils/src/xdg/options.rs new file mode 100644 index 00000000..3985de17 --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/options.rs @@ -0,0 +1,24 @@ +//! Options that can be passed to some functions to control the behaviour. + +/// Control the behaviour of the [XDGFolder::find] function on conflicts. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum XDGFindBehaviour { + /// Returns only the files that already exists. If no file is found, returns the file with the + /// post priority that can be created, usually where the user want this file to exists. If you + /// don't want to merge config/data files/folders this can be a sane default. + #[default] + FirstOrCreate, + + /// Returns only the files that already exists. If you want to merge config/data files/folders + /// this can be a sane default. + ExistingOnly, + + /// Only returns files that don't already exists and may be created. Note that even if a file + /// can be created by the user, it won't necessarily means that you will be able to, keep in + /// mind that race conditions may occur here. + NonExistingOnly, + + /// Returns all files, the ones that exist and the ones that may be created. The user will need + /// to handle the returned files as he sees fit. + AllFiles, +} diff --git a/src/Rust/vvs_utils/src/xdg/paths.rs b/src/Rust/vvs_utils/src/xdg/paths.rs new file mode 100644 index 00000000..d7982949 --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/paths.rs @@ -0,0 +1,208 @@ +//! A PathBuf container as a sum type. + +use std::path::{Path, PathBuf}; + +/// An enum to store maybe multiple folders, or none, with utility functions. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum MaybeFolderList { + /// No folder. + #[default] + Empty, + + /// Only one folder. + Folder(PathBuf), + + /// Many folders. + Many(PathBuf, Vec<PathBuf>), +} + +/// An interator over [MaybeFolderList]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MaybeFolderListIterator<'a> { + next: Option<&'a PathBuf>, + list: &'a [PathBuf], +} + +impl MaybeFolderList { + /// Is there no folder? + pub fn is_none(&self) -> bool { + matches!(self, MaybeFolderList::Empty) + } + + /// Is there folders? + pub fn is_some(&self) -> bool { + !self.is_none() + } + + /// Get the first folder, or the last folder. If no folder is present just returns [None]. + pub fn first(&self) -> Option<&Path> { + use MaybeFolderList::*; + match self { + Empty => None, + Folder(f) | Many(f, _) => Some(f.as_ref()), + } + } + + /// Append another [MaybeFolderList] at the end of a [MaybeFolderList]. Empties the other + /// variable and try to reuse memory. + pub fn append(&mut self, other: &mut MaybeFolderList) { + use MaybeFolderList::*; + + match (self, other) { + (this @ Empty, other) => match other { + Empty => {} + Folder(f1) => { + *this = Folder(f1.clone()); + *other = Empty; + } + Many(f1, fs) => { + *this = Many(f1.clone(), fs.drain(..).collect()); + *other = Empty; + } + }, + + (_, Empty) => {} + + (Many(_, fs), other @ Folder(_)) => { + let Folder(f2) = other else { unreachable!() }; + fs.push(f2.to_path_buf()); + *other = Empty; + } + (Many(_, fs), other @ Many(_, _)) => { + let Many(f2, ref mut fs2) = other else { unreachable!() }; + fs.reserve(fs2.len() + 1); + fs.push(f2.clone()); + fs.append(fs2); + *other = Empty; + } + + (this @ Folder(_), other) => { + let Folder(f1) = this else { unreachable!() }; + match other { + Empty => unreachable!(), + Folder(f2) => *this = Many(f1.clone(), vec![f2.clone()]), + Many(f2, ref mut fs) => { + fs.insert(0, f2.clone()); + *this = Many(f1.clone(), fs.drain(..).collect()); + } + } + *other = Empty; + } + } + } + + /// Get the first folder, or the last folder. If no folder is present just returns [None]. The + /// difference from the [MaybeFolderList::first] function is that this function consumes the + /// [MaybeFolderList]. + pub fn into_first(self) -> Option<PathBuf> { + use MaybeFolderList::*; + match self { + Empty => None, + Folder(f) | Many(f, _) => Some(f), + } + } + + /// Get an iterator over the contained folders. + pub fn iter(&self) -> MaybeFolderListIterator { + let (next, list): (_, &[_]) = match self { + MaybeFolderList::Empty => (None, &[]), + MaybeFolderList::Folder(f) => (Some(f), &[]), + MaybeFolderList::Many(f, fs) => (Some(f), &fs[..]), + }; + MaybeFolderListIterator { next, list } + } + + /// Create a list of folders from a description string and a separator. + pub(super) fn from_str_list(list: &str, separator: char) -> Self { + let mut folders = list.split(separator); + match folders.next() { + None => MaybeFolderList::Empty, + Some(first) => MaybeFolderList::Many(PathBuf::from(first), folders.map(PathBuf::from).collect()), + } + } +} + +impl std::iter::Extend<PathBuf> for MaybeFolderList { + fn extend<T: IntoIterator<Item = PathBuf>>(&mut self, iter: T) { + use MaybeFolderList::*; + let mut iter = iter.into_iter(); + match self { + Empty => { + if let Some(next) = iter.next() { + *self = Many(next, iter.collect()); + } + } + Folder(f1) => *self = Many(f1.clone(), iter.collect()), + Many(_, fs) => fs.extend(iter), + } + } +} + +impl<'a> std::iter::Extend<&'a MaybeFolderList> for MaybeFolderList { + fn extend<T: IntoIterator<Item = &'a MaybeFolderList>>(&mut self, iter: T) { + use MaybeFolderList::*; + let other = iter.into_iter().flat_map(|iter| iter.iter()); + match self { + Empty => *self = other.collect(), + Folder(f1) => *self = Many(f1.clone(), other.cloned().collect()), + Many(_, ref mut fs) => fs.extend(other.cloned()), + } + } +} + +impl<'a, S> From<S> for MaybeFolderList +where + S: AsRef<str> + 'a, +{ + fn from(value: S) -> Self { + MaybeFolderList::Folder(PathBuf::from(value.as_ref())) + } +} + +impl<'a, P> FromIterator<P> for MaybeFolderList +where + P: AsRef<Path> + 'a, +{ + fn from_iter<T: IntoIterator<Item = P>>(iter: T) -> Self { + let mut folders = iter.into_iter(); + match folders.next() { + None => MaybeFolderList::Empty, + Some(first) => MaybeFolderList::Many( + first.as_ref().to_path_buf(), + folders.map(|p| p.as_ref().to_path_buf()).collect(), + ), + } + } +} + +impl IntoIterator for MaybeFolderList { + type Item = PathBuf; + type IntoIter = <Vec<PathBuf> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + MaybeFolderList::Empty => vec![].into_iter(), + MaybeFolderList::Folder(f) => vec![f].into_iter(), + MaybeFolderList::Many(f, mut fs) => { + fs.insert(0, f); + fs.into_iter() + } + } + } +} + +impl<'a> Iterator for MaybeFolderListIterator<'a> { + type Item = &'a PathBuf; + + fn next(&mut self) -> Option<Self::Item> { + let next = self.next?; + match self.list.split_first() { + Some((next, list)) => { + self.next = Some(next); + self.list = list; + } + None => self.next = None, + } + Some(next) + } +} diff --git a/src/Rust/vvs_utils/src/xdg/tests.rs b/src/Rust/vvs_utils/src/xdg/tests.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/Rust/vvs_utils/src/xdg/tests.rs @@ -0,0 +1 @@ + diff --git a/utils/vvs/retime.vvl b/utils/vvs/retime.vvl index 8ad09905..79f6c8be 100644 --- a/utils/vvs/retime.vvl +++ b/utils/vvs/retime.vvl @@ -10,6 +10,14 @@ option "after" { type = "number", default = 300 } -- `line.aux.number` here. Note that name collision will raise an error... data "line:number" { type = "number", default = 0 } +-- You may declare functions like that, this is the only way of making them +-- visible to the exterior of the module and even inside the module +func "my_function" { + function() + print "Ok" + end +} + -- To declare a job we must give it a name. The function to use must follow -- some rules. It must only have one argument, the item on which it must be -- called (line, syllabe, lines, syllabes). The function must returns one type @@ -21,7 +29,7 @@ job "preline" { local pre_line = line:copy() pre_line.start = line.start - before pre_line.fini = line.start - for syl in pre_line:syllabes() do + for _, syl in ipairs(pre_line) do syl.start = syl.start - before syl.fini = line.start end @@ -34,7 +42,7 @@ job "postline" { local post_line = line:copy() post_line.start = line.fini post_line.fini = line.fini + after - for syl in post_line:syllabes() do + for _, syl in ipairs(post_line) do syl.start = line.fini syl.fini = syl.fini + after end @@ -56,3 +64,14 @@ job "countlines" { return lines end } + +-- Dummy job here to expose the way of calling internally defined functions, +-- it's like calling a function from another module, but with "self". The "self" +-- table will automatically swapped to the correct one when calling a job from +-- different modules. +job "dummy-job" { + lines = function(lines) + self.my_function() -- Call the previously defined "my_function" + return lines + end +} diff --git a/utils/vvs/test2.vvs b/utils/vvs/test2.vvs index 35ed801e..c9f7f552 100644 --- a/utils/vvs/test2.vvs +++ b/utils/vvs/test2.vvs @@ -24,5 +24,6 @@ job "ziplines" { main { prelines = retime.preline "INIT", postlines = retime.postline "INIT", + -- Left jobs and functions are exported as the "self" module when calling the main block. self.ziplines { "prelines", "INIT", "postlines" }, } -- GitLab