diff --git a/.gitignore b/.gitignore index b716794..1720687 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,15 @@ /target # 可执行文件 +# * 🚩【2024-04-07 08:42:13】目前内置被忽略的CIN可执行文件,用于稳定的测试代码编写 executables/ + +# 临时文件 +*tempCodeRunnerFile* + +# 日志文件 +*.log +*.log.* + +# 测试用文件 +\[test\]* diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2808a0b..50d6283 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,12 @@ { "recommendations": [ "swellaby.vscode-rust-test-adapter", - "nyxiative.rust-and-friends" + "nyxiative.rust-and-friends", + "itsyaasir.rust-feature-toggler", + "rust-lang.rust-analyzer", + "aaron-bond.better-comments", + "laktak.hjson", + "tanh.hjson-formatter", + "pkief.material-icon-theme" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dd63ce2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,64 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug级单元测试", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=babel_nar" + ], + "filter": { + "name": "babel_nar", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug可执行文件「CIN启动器」", + "cargo": { + "args": [ + "build", + "--bin=cin_launcher", + "--package=babel_nar" + ], + "filter": { + "name": "cin_launcher", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug级单元测试「CIN启动器」", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=cin_launcher", + "--package=babel_nar" + ], + "filter": { + "name": "cin_launcher", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1428978 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "rust-analyzer.cargo.features": "all", + "editor.formatOnSave": true, + "cSpell.words": [ + "Addrs", + "boardcaster", + "canonicalize", + "clearscreen", + "confy", + "cxin", + "deser", + "Errno", + "hasher", + "hjson", + "Nalifier", + "nanos", + "openjunars", + "rfind", + "runpy", + "setopname", + "thiserror", + "traceback", + "tstate", + "whatwarmer" + ], + "rust-analyzer.linkedProjects": [ + ".\\Cargo.toml", + // ".\\Cargo.toml" + ], +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e260f4d..b7bfb4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,1257 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + [[package]] name = "babel_nar" -version = "0.1.0" +version = "0.20.1" +dependencies = [ + "anyhow", + "clap", + "clearscreen", + "colored", + "deser-hjson", + "lazy_static", + "nar_dev_utils", + "narsese", + "navm", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "thiserror", + "ws", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "clearscreen" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f3f22f1a586604e62efd23f78218f3ccdecf7a33c4500db2d37d85a24fe994" +dependencies = [ + "nix", + "terminfo", + "thiserror", + "which", + "winapi 0.3.9", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "deser-hjson" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94aac4095c08ded7e4b9ba7fc2b2929f11b94bb96897ca188b0f64e01688e1" +dependencies = [ + "serde", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "nar_dev_utils" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1662c34eaa2853e1342dc1bbedf0373dc4315d1b0b7637e2c25cf424c9642355" + +[[package]] +name = "narsese" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2b29328c5c47d1b94893a9a85a58bee0165fde6c87490b0d01a3c00c41ac2b" +dependencies = [ + "lazy_static", + "nar_dev_utils", +] + +[[package]] +name = "navm" +version = "0.11.0" +dependencies = [ + "anyhow", + "nar_dev_utils", + "narsese", + "serde", + "serde_json", +] + +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.13", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs", + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "ws" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fe90c75f236a0a00247d5900226aea4f2d7b05ccc34da9e7a8880ff59b5848" +dependencies = [ + "byteorder", + "bytes", + "httparse", + "log", + "mio", + "mio-extras", + "rand 0.7.3", + "sha-1", + "slab", + "url", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/Cargo.toml b/Cargo.toml index 536ef43..505979b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,180 @@ [package] name = "babel_nar" -version = "0.1.0" +version = "0.20.1" edition = "2021" +description = """ +Implementation and application supports of the NAVM model +""" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +readme = "README.md" +keywords = ["NARS", "Non-Axiomatic-Reasoning", "NAVM"] + +license = "MIT OR Apache-2.0" +categories = [ + "parser-implementations", # 解析器实现 | 各CIN方言 + "development-tools", # 开发工具 + "command-line-utilities", # CLI应用 +] # 🔗 +repository = "https://github.com/ARCJ137442/BabelNAR.rs" + +# Cargo文档参考: + +## 必要的依赖 ## [dependencies] +# 用于错误处理 +thiserror = "1.0.58" +anyhow = "1.0.81" +clearscreen = "2.0.1" + +[dependencies.nar_dev_utils] +# 【2024-03-13 21:17:55】实用库现在独立为`nar_dev_utils` +version = "0" # * ✅现已发布至`crates.io` +# *🚩【2024-03-21 09:26:38】启用所有 +# path = "../NAR-dev-util" +# git = "https://github.com/ARCJ137442/NAR-dev-util" +# ! 【2024-03-23 19:19:01】似乎Rust-Analyzer无法获取私有仓库数据 +features = [ "bundled" ] # 启用所有特性 + +[dependencies.narsese] +# ! 本地依赖可以不添加版本 +# 载入Narsese API,引入其中所有部分 +version = "0" # * ✅现已发布至`crates.io` +# path = "../Narsese.rs" +# git = "https://github.com/ARCJ137442/Narsese.rs" +# ! 【2024-03-23 19:19:01】似乎Rust-Analyzer无法获取私有仓库数据 +features = [ + # * 🚩【2024-03-29 09:52:56】在「方言词法折叠」中,需要使用其中的常量 + "enum_narsese", + # * 📌承继NAVM + "lexical_narsese", +] + +[dependencies.navm] +# ! 本地依赖可以不添加版本 +# 载入NAVM API,引入「非公理虚拟机」模型 +path = "../NAVM.rs" +# git = "https://github.com/ARCJ137442/NAVM.rs" +# ! 【2024-03-23 19:19:01】似乎Rust-Analyzer无法获取私有仓库数据 +features = [] # ! 【2024-03-21 09:24:51】暂时没有特性 + +## 依赖特性的可选依赖 ## + +# Rust版本的正则表达式 +# * 🎯用于解析提取NARS输出 +# * 📄OpenNARS、ONA、PyNARS +[dependencies.regex] +version = "1.10.4" +optional = true + +# 用于实现「静态含闭包常量」 +# * 🎯初次引入:NARS-Python 方言格式 +# * 🔗:https://stackoverflow.com/questions/73260997/rust-boxed-closure-in-global-variable +[dependencies.lazy_static] +version = "1.4.0" +optional = true + +# Rust版本的PEG解析器 +# * 🎯用于对接一些NARS方言的解析 +# * 📄OpenNARS(操作语法)、ONA(中缀语法) +[dependencies.pest] +version = "2.7.8" +optional = true + +# Rust版本的PEG解析器(派生宏) +[dependencies.pest_derive] +version = "2.7.8" +optional = true + +# 命令行支持/彩色终端 +[dependencies.colored] +version = "2.1.0" +optional = true + +# 命令行支持/(H)JSON配置解析 +[dependencies.serde] +version = "1.0.197" +optional = true +features = ["derive"] + +[dependencies.serde_json] +version = "1.0.115" +optional = true + +[dependencies.deser-hjson] +version = "2.2.4" +optional = true + +# 命令行支持/Websocket服务 +[dependencies.ws] +version = "0.9.2" +optional = true + +# 命令行支持/命令行参数解析 +[dependencies.clap] +version = "4.5.4" +features = ["derive"] +optional = true + +### 定义库的特性 ### +[features] + +## 默认启用的特性 ## +default = [ "bundled" ] # * 默认启用所有(可选禁用) +## 大杂烩 ## +bundled = [ + "cin_implements", # 各大CIN的NAVM实现 + "cli_support", # 命令行支持 + "test_tools", # 测试工具集 +] + +## 各个独立的特性 ## + +# 具体接口实现(虚拟机启动器) # +# ✅OpenNARS +# ✅ONA +# ✅PyNARS +# ✅NARS-Python(不稳定) +# ✅OpenJunars(不稳定) +cin_implements = [ + "opennars", + "ona", + "pynars", + "nars_python", + "openjunars", +] +# ✅OpenNARS接口 +opennars = [ + "regex", + "pest", "pest_derive", +] +# ✅ONA接口 +ona = [ + "regex", + "pest", "pest_derive", +] +# ✅PyNARS接口 +pynars = [ + "regex", + # "pest", # ! 【2024-03-27 20:52:17】无需特别解析方言:其输出即为CommonNarsese +] +# ✅NARS-Python接口(不稳定) +nars_python = [ + "lazy_static", # 这个「词法Narsese」也在用 +] +# ✅OpenJunars接口(不稳定) +openjunars = [] + +# 命令行支持 # +cli_support = [ + "colored", # 命令行io 彩色打印 + "serde", "serde_json", "deser-hjson", # 配置文件解析 + "ws", # 命令行io Websocket服务 + "clap" # 命令行参数解析 +] + +# 测试工具集 # +test_tools = [ + # 统一`.nal`格式 + "pest", "pest_derive", +] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..96bcd34 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2024 ARCJ137442 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 8c7134b..e589b58 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,9 @@ # BabelNAR.rs -[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) - -该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。 - -基于[**NAVM.rs**](https://github.com/ARCJ137442/NAVM.rs)的CIN(NARS计算机实现)接口 - -- 前身为[**BabelNAR.jl**](https://github.com/ARCJ137442/BabelNAR.jl) -- 旨在方便连接各类CIN,并通过**Websocket**等服务提供**通用统一交互接口**。 - -## 概念 +English | [简体中文](README.zh-cn.md) -### CIN (Computer Implement of NARS) - -- 「NARS计算机实现」之英文缩写 -- 指代所有**实现NARS**的计算机软件系统 - - 不要求完整实现NAL 1~9 - -### ***CommonNarsese*** - -🔗参考[**NAVM.jl**的对应部分](https://github.com/ARCJ137442/navm.jl?tab=readme-ov-file#commonnarsese) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) -## 参考 +Implementation and application supports of the NAVM model -- [BabelNAR](https://github.com/ARCJ137442/BabelNAR.jl) -- [NAVM.rs](https://github.com/ARCJ137442/NAVM.rs) +⚠️【2024-04-03 15:12:55】**This documentation is still in progress. For full and latest content, please refer to [the Simplified Chinese version](README.zh-cn.md).** diff --git a/README.zh-cn.md b/README.zh-cn.md new file mode 100644 index 0000000..1e1d5c3 --- /dev/null +++ b/README.zh-cn.md @@ -0,0 +1,65 @@ +# BabelNAR.rs + +[English](README.md) | 简体中文 + +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) + +该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。 + +[**NAVM.rs**](https://github.com/ARCJ137442/NAVM.rs)对[CIN](#cin-computer-implement-of-nars)的**启动器**、**运行时**及应用程序实现 + +- 前身为[**BabelNAR.jl**](https://github.com/ARCJ137442/BabelNAR.jl) +- ✨为「非公理虚拟机模型」提供程序实现 +- ✨统一各[CIN](#cin-computer-implement-of-nars)的**输入输出**形式,聚合使用各大NARS实现 + +## 概念 + +### CIN (Computer Implement of NARS) + +- 「NARS计算机实现」之英文缩写 +- 指代所有**实现NARS**的计算机软件系统 + - 不要求完整实现NAL 1~9 + +### ***CommonNarsese*** + +🔗参考[**NAVM.jl**的对应部分](https://github.com/ARCJ137442/navm.jl?tab=readme-ov-file#commonnarsese) + +## 各CIN对接情况 + +🕒最后更新时间:【2024-03-26 01:43:28】 + +| CIN | 实现方法 | 进程安全 | 输入转译 | 输出转译 | +| :---------- | :---------: | :--: | :--: | :--: | +| OpenNARS | `java -jar` | ✅ | ✅ | 🚧 | +| ONA | 直接启动exe | ✅ | ✅ | 🚧 | +| PyNARS | `python -m` | ✅ | 🚧 | 🚧 | +| NARS-Python | 直接启动exe | ❓ | ✅ | ❌ | +| OpenJunars | `julia` | ✅ | ❌ | ❌ | + +注: + +- 🚧输入输出转译功能仍然在从[BabelNAR_Implements](https://github.com/ARCJ137442/BabelNAR_Implements.jl)迁移 +- ❓NARS-Python的exe界面可能会在终止后延时关闭 +- ❌基于`julia`启动OpenJunars脚本`launch.jl`时,对「输出捕获」尚未有成功记录 +- ❌目前对NARS-Python的「输出捕获」尚未有成功记录 + +## CLI测试:各CIN完成度评估 + +🕒最后更新时间:【2024-04-07 16:52:29】 + +| | 简单演绎 | 高阶演绎 | 自变量消除 | 时间归纳 | 简单操作 | 时序操作 | +| :--- | :--: | :--: | :--: | :--: | :--: | :--: | +| 原理 | 继承关系的传递性 | 蕴含关系的蕴含保真 | 代入消元 | 前后事件的联系 | 直接要求「做某事」 | 在「发生某事,做某事,目标达成」中学会「若发生某事,就做某事」 | +| 对应NAL内容 | NAL-1 | NAL-5 | NAL-5 + NAL-6 | NAL-7 | NAL-8 | NAL-7 + NAL-8 | +| 语句输入 | ` B>.` + ` C>.` | `< B> ==> D>>.` + ` B>.` | `< $1> ==> <$1 --> C>>.` + ` B>.` | ` B>. :\|:` + ` D>. :\|:` | `<(*, ...) --> ^left>! :\|:` | `A. :\|:` + `<(*, {SELF}) --> ^left>. :\|:` + `G. :\|:` + `<(&/, A, <(*, ...) --> ^left>) ==> G>?` + `G! :\|:` | +| 预期输出 | ` C>.` | ` D>.` | ` C>.` | `< B> =/> D>>.` | EXE `<(*, ...) --> ^left> :\|:` | EXE `<(&/, A, <(*, ...) --> ^left>) ==> G>` | +| OpenNARS(3.0.4) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| ONA | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | +| PyNARS | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| CXinNARS | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | + +## 参考 + +- [BabelNAR](https://github.com/ARCJ137442/BabelNAR.jl) +- [BabelNAR_Implements](https://github.com/ARCJ137442/BabelNAR_Implements.jl) +- [NAVM.rs](https://github.com/ARCJ137442/NAVM.rs) diff --git a/scripts/windows/release-launch-cxin_js.cmd b/scripts/windows/release-launch-cxin_js.cmd new file mode 100644 index 0000000..efde7ce --- /dev/null +++ b/scripts/windows/release-launch-cxin_js.cmd @@ -0,0 +1,2 @@ +@echo off +".\..\..\target\release\babelnar_cli.exe" -c ".\..\..\src\tests\cli\config\cin_cxin_js.hjson" diff --git a/scripts/windows/release-launch-ona.cmd b/scripts/windows/release-launch-ona.cmd new file mode 100644 index 0000000..9cd064b --- /dev/null +++ b/scripts/windows/release-launch-ona.cmd @@ -0,0 +1,2 @@ +@echo off +".\..\..\target\release\babelnar_cli.exe" -c ".\..\..\src\tests\cli\config\cin_ona.hjson" diff --git a/scripts/windows/release-launch-opennars.cmd b/scripts/windows/release-launch-opennars.cmd new file mode 100644 index 0000000..65180b7 --- /dev/null +++ b/scripts/windows/release-launch-opennars.cmd @@ -0,0 +1,2 @@ +@echo off +".\..\..\target\release\babelnar_cli.exe" -c ".\..\..\src\tests\cli\config\cin_opennars.hjson" diff --git a/scripts/windows/release-launch-pynars.cmd b/scripts/windows/release-launch-pynars.cmd new file mode 100644 index 0000000..9aa05ce --- /dev/null +++ b/scripts/windows/release-launch-pynars.cmd @@ -0,0 +1,2 @@ +@echo off +".\..\..\target\release\babelnar_cli.exe" -c ".\..\..\src\tests\cli\config\cin_pynars.hjson" diff --git a/scripts/windows/release-launch.cmd b/scripts/windows/release-launch.cmd new file mode 100644 index 0000000..5a64442 --- /dev/null +++ b/scripts/windows/release-launch.cmd @@ -0,0 +1,2 @@ +@echo off +".\..\..\target\release\babelnar_cli.exe" diff --git a/src/bin/babelnar_cli/arg_parse.rs b/src/bin/babelnar_cli/arg_parse.rs new file mode 100644 index 0000000..2270b28 --- /dev/null +++ b/src/bin/babelnar_cli/arg_parse.rs @@ -0,0 +1,316 @@ +//! BabelNAR CLI的命令行(参数 & 配置)解析支持 +//! * ⚠️【2024-04-01 14:31:09】特定于二进制crate,目前不要并入[`babel_nar`] +//! * 🚩【2024-04-04 03:03:58】现在移出所有与「启动配置」相关的逻辑到[`super::vm_config`] + +use crate::{load_config_extern, read_config_extern, LaunchConfig}; +use babel_nar::println_cli; +use clap::Parser; +use std::{ + env::{current_dir, current_exe}, + path::PathBuf, +}; + +/// 基于[`clap`]的命令行参数数据 +// 配置命令行解析器 +#[derive(Parser)] +#[command(name = "BabelNAR CLI")] +#[command(about = "BabelNAR's Cmdline Interface", long_about = None)] +#[command(version, about, long_about = None)] +// 其它 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CliArgs { + // 配置文件路径 + // * ✨可支持加载多个配置 + // * ⚠️需要重复使用`-c` + // * ✅会以「重复使用`-c`」的顺序被载入 + // * 🚩【2024-04-01 13:07:18】具有最高加载优先级 + // * 📌剩余的是和exe同目录的`json`文件 + // ! 📝此处的文档字符串会被用作`-h`的说明 + /// Configuration file path in JSON (multiple supported by call it multiple times) + #[arg(short, long, value_name = "FILE")] + pub config: Vec, + + // 禁用默认配置 + // * 禁用与exe同目录的配置文件 + // * 📜默认为`false` + // * 📌行为 + // * 没有 ⇒ `false` + // * 有  ⇒ `true` + /// Disable the default configuration file in the same directory as exe + #[arg(short, long)] + pub disable_default: bool, + // ! 🚩【2024-04-02 11:36:18】目前除了「配置加载」外,莫将任何「NAVM实现特定,可以内置到『虚拟机配置』的字段放这儿」 +} + +/// 默认的「启动配置」关键词 +/// * 🎯在「自动追加扩展名」的机制下,可以进行自动补全 +/// * 🚩【2024-04-04 05:28:45】目前仍然难以直接在[`PathBuf`]中直接追加字符串 +/// * 多词如`BabelNAR-launch`需要使用`-`而非`.`:后者会被识别为「`.launch`扩展名」,导致无法进行「自动补全」 +pub const DEFAULT_CONFIG_KEYWORD: &str = "BabelNAR-launch"; + +/// 获取「默认启动配置」文件 +/// * 🎯更灵活地寻找可用的配置文件 +/// * exe当前目录下 | 工作目录下 +/// * `BabelNAR.launch.(h)json` +pub fn try_load_default_config() -> Option { + // 检查一个目录 + #[inline(always)] + fn in_one_root(root: PathBuf) -> Option { + // 计算路径:同目录下 + let path = match root.is_dir() { + true => root.join(DEFAULT_CONFIG_KEYWORD), + false => root.with_file_name(DEFAULT_CONFIG_KEYWORD), + }; + // 尝试读取,静默失败 + read_config_extern(&path).ok() + } + // 寻找第一个可用的配置文件 + [current_dir(), current_exe()] + // 转换为迭代器 + .into_iter() + // 筛去转换失败的 + .flatten() + // 尝试获取其中的一个有效配置,然后(惰性)返回「有效配置」 + .filter_map(in_one_root) + // 只取第一个(最先遍历的根路径优先) + .next() +} + +/// 加载配置 +/// * 🚩按照一定优先级顺序进行覆盖(从先到后) +/// * 命令行参数中指定的配置文件 +/// * 默认配置文件路径 | 可以在`disable_default = true`的情况下传入任意字串作占位符 +pub fn load_config(args: &CliArgs) -> LaunchConfig { + // 构建返回值 | 全`None` + let mut result = LaunchConfig::new(); + // 尝试从命令行参数中读取再合并配置 | 仅提取出其中`Some`的项 + args.config + // 尝试加载配置文件,对错误采取「警告并抛掉」的策略 + .iter() + .map(PathBuf::as_ref) + .filter_map(load_config_extern) + // 逐个从「命令行参数指定的配置文件」中合并 + .for_each(|config| result.merge_from(&config)); + // 若未禁用,尝试读取再合并默认启动配置 + if !args.disable_default { + // * 🚩读取失败⇒警告&无动作 | 避免多次空合并 + try_load_default_config().inspect(|config_extern| result.merge_from(config_extern)); + } + // 展示加载的配置 | 以便调试(以防其它地方意外插入别的配置) + if result.is_empty() { + println_cli!([Log] "未加载任何外部配置"); + } else { + match serde_json::to_string(&result) { + Ok(json) => println_cli!([Log] "外部配置已加载:{json}",), + Err(e) => println_cli!([Warn] "展示加载的配置时出现预期之外的错误: {e}"), + } + } + // 返回 + result +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use babel_nar::tests::*; + use nar_dev_utils::fail_tests; + + /// 测试/参数解析 + mod arg_parse { + use super::*; + use config_paths::*; + + fn _test_arg_parse(args: &[&str], expected: &CliArgs) { + // ! 📝此处必须前缀一个「自身程序名」 + let args = CliArgs::parse_from([&["test.exe"], args].concat()); + assert_eq!(dbg!(args), *expected) + } + + // 快捷测试宏 + macro_rules! test_arg_parse { + // 成功测试 + { + $( $args:expr => $expected:expr $(;)? )* + } => { + $( + _test_arg_parse(&$args, &$expected); + )* + }; + // 失败测试 + { + $args:expr + } => { + // 直接使用默认构造,解析成功了大概率报错 + _test_arg_parse(&$args, &CliArgs::default()) + }; + } + + /// 测试/打印帮助 + #[test] + fn test_arg_parse_help() { + _test_arg_parse(&["--help"], &CliArgs::default()); + } + #[test] + fn test_arg_parse_help2() { + _test_arg_parse(&["-h"], &CliArgs::default()); + } + + /// 测试/成功的解析 + #[test] + fn test_arg_parse() { + test_arg_parse! { + ["-c", ARG_PARSE_TEST] + => CliArgs { + config: vec![ARG_PARSE_TEST.into()], + ..Default::default() + }; + // 多个配置:重复使用`-c`/`--config`,按使用顺序填充 + ["-c", "1", "--config", "2"] + => CliArgs { + config: vec!["1".into(), "2".into()], + ..Default::default() + }; + // 禁用默认配置:使用`-d`/`--disable-default` + ["-d"] + => CliArgs { + disable_default: true, + ..Default::default() + }; + }; + } + + // 失败解析 + fail_tests! { + fail_缺少参数 test_arg_parse!(["-c"]); + fail_参数名不对 test_arg_parse!(["--c"]); + fail_缺少参数2 test_arg_parse!(["--config"]); + 多个参数没各自前缀 test_arg_parse!(["-c", "1", "2"]); + } + } + + /// 测试/加载配置 + mod read_config { + use super::*; + use crate::vm_config::*; + use crate::LaunchConfigWebsocket; + use config_paths::*; + use nar_dev_utils::manipulate; + + /// 测试/加载配置 + fn load(args: &[&str]) -> LaunchConfig { + // 读取配置 | 自动填充第一个命令行参数作为「当前程序路径」 + let args = CliArgs::parse_from([&["test.exe"], args].concat()); + let config = load_config(&args); + dbg!(config) + } + + /// 实用测试宏 + macro_rules! test { + // 成功测试 + { $( [ $($arg:expr $(,)? )* ] => $expected:expr $(;)? )* } => { + $( assert_eq!(load(&[ $($arg ),* ]), $expected); )* + }; + // 失败测试 | 总是返回默认值 + { $( $args:expr $(;)? )* } => { + $( assert_eq!(load(&$args), LaunchConfig::default()); )* + }; + } + + /// 测试 + #[test] + fn test() { + let expected_current_dir = manipulate!( + current_dir().unwrap() + => .push("src") + => .push("tests") + => .push("cli") + => .push("executables") + ); + // 成功测试 + test! { + // 单个配置文件 + ["-c" ARG_PARSE_TEST "-d"] => LaunchConfig { + translators: Some( + LaunchConfigTranslators::Same( + "opennars".into(), + ), + ), + command: Some(LaunchConfigCommand { + cmd: "java".into(), + cmd_args: Some(vec![ + "-Xmx1024m".into(), + "-jar".into(), + "nars.jar".into() + ]), + current_dir: Some(expected_current_dir.clone()), + }), + ..Default::default() + }; + ["-c" WEBSOCKET "-d"] => LaunchConfig { + websocket: Some(LaunchConfigWebsocket { + host: "localhost".into(), + port: 8080, + }), + ..Default::default() + }; + // 两个配置文件合并 + [ + "-d" + "-c" ARG_PARSE_TEST + "-c" WEBSOCKET + ] => LaunchConfig { + translators: Some( + LaunchConfigTranslators::Same( + "opennars".into(), + ), + ), + command: Some(LaunchConfigCommand { + cmd: "java".into(), + cmd_args: Some(vec![ + "-Xmx1024m".into(), + "-jar".into(), + "nars.jar".into() + ]), + current_dir: Some(expected_current_dir.clone()), + }), + websocket: Some(LaunchConfigWebsocket { + host: "localhost".into(), + port: 8080, + }), + ..Default::default() + }; + // 三个配置文件合并 + [ + "-d" + "-c" ARG_PARSE_TEST + "-c" WEBSOCKET + "-c" PRELUDE_TEST + ] => LaunchConfig { + translators: Some( + LaunchConfigTranslators::Same( + "opennars".into(), + ), + ), + command: Some(LaunchConfigCommand { + cmd: "java".into(), + cmd_args: Some(vec![ + "-Xmx1024m".into(), + "-jar".into(), + "nars.jar".into() + ]), + current_dir: Some(expected_current_dir.clone()), + }), + websocket: Some(LaunchConfigWebsocket { + host: "localhost".into(), + port: 8080, + }), + user_input: Some(false), + auto_restart: Some(false), + strict_mode: Some(true), + ..Default::default() + } + } + } + } +} diff --git a/src/bin/babelnar_cli/config_launcher.rs b/src/bin/babelnar_cli/config_launcher.rs new file mode 100644 index 0000000..a754a9c --- /dev/null +++ b/src/bin/babelnar_cli/config_launcher.rs @@ -0,0 +1,279 @@ +//! 用于从「启动参数」启动NAVM运行时 + +use crate::{ + read_config_extern, search_configs, LaunchConfig, LaunchConfigCommand, LaunchConfigTranslators, + RuntimeConfig, SUPPORTED_CONFIG_EXTENSIONS, +}; +use anyhow::{anyhow, Result}; +use babel_nar::{ + cin_implements::{ + common::generate_command, cxin_js, nars_python, native, ona, openjunars, opennars, pynars, + }, + cli_support::{cin_search::name_match::name_match, io::readline_iter::ReadlineIter}, + eprintln_cli, println_cli, + runtimes::{ + api::{InputTranslator, IoTranslators}, + CommandVm, OutputTranslator, + }, +}; +use nar_dev_utils::pipe; +use navm::{ + cmd::Cmd, + output::Output, + vm::{VmLauncher, VmRuntime}, +}; +use std::path::{Path, PathBuf}; + +/// (若缺省)要求用户手动填充配置项 +pub fn polyfill_config_from_user(config: &mut LaunchConfig, cwd: Option>) { + if config.need_polyfill() { + // * 先搜索已有的文件 | 不开启 + let search = |verbose| { + // 执行搜索 + let searched_configs = cwd + .as_ref() + .map(|p| search_configs(p.as_ref(), SUPPORTED_CONFIG_EXTENSIONS, verbose)); + // 转换为数组并返回 + match searched_configs { + Some(Ok(v)) => v.into_iter().collect(), + _ => vec![], + } + }; + // 第一次搜索 + let mut searched_configs = search(false); + // * 🚩【2024-04-03 19:33:20】目前是要求输入配置文件位置 + const HINT: &str = "现在需要输入配置文件位置。\n 示例:「BabelNAR.launch.json」\n 若搜索到已有配置文件,可输入其在方括号内的索引,如「0」\n 可直接按下回车,以查看详细搜索过程"; + const PROMPT: &str = "配置文件位置: "; + // 提示(不会频繁打印) + println_cli!([Info] "{}", HINT); + for line in ReadlineIter::new(PROMPT) { + // 检验输入 + let line = match line { + Err(e) => { + eprintln_cli!([Error] "输入无效:{e}"); + continue; + } + Ok(l) => l, + }; // ! 不能直接加`.trim()`,临时变量会被抛掉 + let line = line.trim(); + if let Ok(i) = line.parse::() { + if i < searched_configs.len() { + println_cli!([Info] "已选择搜索到的第「{i}」个配置:{:?}", searched_configs[i]) + } + // 返回结果 + *config = searched_configs[i].clone(); + break; + } + // 输入为空⇒详细搜索配置⇒重新回到循环 + if line.is_empty() { + searched_configs = search(true); + println_cli!([Info] "{}", HINT); + continue; + } + // 检验路径 + let path = PathBuf::from(line); + if !path.is_file() { + eprintln_cli!([Error] "文件「{path:?}」不存在"); + continue; + } + // 读取配置文件 + let content = match read_config_extern(&path) { + Ok(config) => config, + Err(e) => { + eprintln_cli!([Error] "配置文件「{path:?}」读取失败:{e}"); + continue; + } + }; + // 读取成功⇒覆盖,返回 + *config = content; + break; + } + } +} + +/// 从「启动参数」中启动 +/// * 🚩在转换中确认参数 +/// * ⚙️返回(启动后的运行时, 转换后的『运行时配置』) +/// * ❌无法使用`impl TryInto`统一「启动参数」与「运行参数」 +/// * 📌即便:对于「运行时参数」,[`TryInto::try_into`]始终返回自身 +/// * 📝然而:对自身的[`TryInto`]错误类型总是[`std::convert::Infallible`] +/// * ❗错误类型不一致,无法统一返回 +pub fn launch_by_config( + config: impl TryInto, +) -> Result<(impl VmRuntime, RuntimeConfig)> { + // 转换启动配置 + let config: RuntimeConfig = config.try_into()?; + + // * 🚩【2024-04-07 10:13:51】目前通过「设置exe工作路径」切换到启动环境中 + if let Some(path) = &config.command.current_dir { + std::env::set_current_dir(path)?; + } + + // 生成虚拟机 + let runtime = launch_by_runtime_config(&config)?; + + // 返回 + Ok((runtime, config)) +} + +pub fn launch_by_runtime_config(config: &RuntimeConfig) -> Result { + // 生成虚拟机 + let config_command = &config.command; + let mut vm = load_command_vm(config_command)?; + + // 配置虚拟机 + // * 🚩【2024-04-04 03:17:43】现在「转译器」成了必选项,所以必定会有配置 + config_launcher_translators(&mut vm, &config.translators)?; + + // 启动虚拟机 + let runtime = vm.launch()?; + Ok(runtime) +} + +/// 从「启动参数/启动命令」启动「命令行虚拟机」 +/// * ❓需要用到「具体启动器实现」吗 +pub fn load_command_vm(config: &LaunchConfigCommand) -> Result { + // 构造指令 + let command = generate_command( + &config.cmd, + // ! 🚩【2024-04-07 12:35:41】不能再设置工作目录:已在[`launch_by_config`]处设置 + // * 否则会导致「目录名称无效」 + // config.current_dir.as_ref(), + None::<&str>, + // 🚩获取其内部数组的引用,或使用一个空数组作迭代器(无法简化成[`unwrap_or`]) + match &config.cmd_args { + Some(v) => v.iter(), + // ↓此处`unwrap_or_default`默认使用一个空数组作为迭代器 + None => [].iter(), + }, + ); + // 构造虚拟机 + let vm = command.into(); + // 返回 + Ok(vm) +} + +/// 从「启动参数/输入输出转译器」配置「命令行虚拟机」 +/// * 🚩【2024-04-02 01:03:54】此处暂时需要**硬编码**现有的CIN实现 +/// * 🏗️后续可能支持定义自定义转译器(long-term) +/// * ⚠️可能会有「转译器没找到/转译器加载失败」等 +/// * 📌【2024-04-02 01:49:46】此处需要暂时借用所有权 +pub fn config_launcher_translators( + vm: &mut CommandVm, + config: &LaunchConfigTranslators, +) -> Result<()> { + Ok(pipe! { + // 获取转译器 + get_translator_by_name(config) => {?}# + // 设置转译器 + => [vm.translators](_) + // 返回成功 + }) +} + +/// 从「转译器名」检索「输入输出转译器」 +/// * 🚩继续分派到「输入转译器检索」与「输出转译器检索」 +pub fn get_translator_by_name(config: &LaunchConfigTranslators) -> Result { + let name_i = match config { + LaunchConfigTranslators::Same(input) | LaunchConfigTranslators::Separated { input, .. } => { + input + } + }; + let name_o = match config { + LaunchConfigTranslators::Same(output) + | LaunchConfigTranslators::Separated { output, .. } => output, + }; + Ok(IoTranslators { + input_translator: get_input_translator_by_name(name_i.as_str())?, + output_translator: get_output_translator_by_name(name_o.as_str())?, + }) +} + +/// 输入转译器的索引字典类型 +/// * 📌结构:`[(转译器名, 输入转译器, 输出转译器)]` +pub type TranslatorDict<'a> = &'a [( + &'a str, + fn(Cmd) -> Result, + fn(String) -> Result, +)]; +/// 输入转译器的索引字典 +/// * 🚩静态存储映射,后续遍历可有序可无序 +pub const TRANSLATOR_DICT: TranslatorDict = &[ + ("Native", native::input_translate, native::output_translate), + ( + "OpenNARS", + opennars::input_translate, + opennars::output_translate, + ), + ("ONA", ona::input_translate, ona::output_translate), + ( + "NARS-Python", + nars_python::input_translate, + nars_python::output_translate, + ), + ( + "NARSPython", + nars_python::input_translate, + nars_python::output_translate, + ), + ("PyNARS", pynars::input_translate, pynars::output_translate), + ( + "OpenJunars", + openjunars::input_translate, + openjunars::output_translate, + ), + ( + "CXinJS", + cxin_js::input_translate, + cxin_js::output_translate, + ), +]; + +pub fn get_input_translator_by_name(cin_name: &str) -> Result> { + // 根据「匹配度」的最大值选取 + let translator = TRANSLATOR_DICT + .iter() + .max_by_key(|(name, _, _)| name_match(name, cin_name)) + .ok_or_else(|| anyhow!("未找到输入转译器"))? + .1; // 输入转译器 + Ok(Box::new(translator)) +} + +pub fn get_output_translator_by_name(cin_name: &str) -> Result> { + // 根据「匹配度」的最大值选取 + let translator = TRANSLATOR_DICT + .iter() + .max_by_key(|(name, _, _)| name_match(name, cin_name)) + .ok_or_else(|| anyhow!("未找到输出转译器"))? + .2; // 输出转译器 + Ok(Box::new(translator)) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use nar_dev_utils::{asserts, f_parallel}; + + #[test] + fn t() { + dbg!(format!("{:p}", opennars::input_translate as fn(_) -> _)); + } + + /// 测试 + /// * 🚩仅能测试「是否查找成功」,无法具体地比较函数是否相同 + /// * 📝函数在被装进[`Box`]后,对原先结构的完整引用就丧失了 + #[test] + fn test() { + fn t(name: &str) { + asserts! { + get_input_translator_by_name(name).is_ok() + get_output_translator_by_name(name).is_ok() + } + } + f_parallel![ + t; + "opennars"; "ona"; "nars-python"; "narsPython"; "pynars"; "openjunars"; "cxinJS" + ]; + } +} diff --git a/src/bin/babelnar_cli/config_search.rs b/src/bin/babelnar_cli/config_search.rs new file mode 100644 index 0000000..077f0fa --- /dev/null +++ b/src/bin/babelnar_cli/config_search.rs @@ -0,0 +1,94 @@ +//! CIN自动搜索 + +use crate::{read_config_extern, LaunchConfig}; +use anyhow::Result; +use babel_nar::{ + cli_support::cin_search::{name_match::is_name_match, path_walker::PathWalkerV1}, + println_cli, +}; +use nar_dev_utils::ToDebug; +use std::path::{Path, PathBuf}; + +pub fn search_configs>( + start: &Path, + allowed_extension_names: impl IntoIterator, + verbose: bool, +) -> Result> { + // 允许的扩展名 + let extension_names = allowed_extension_names.into_iter().collect::>(); + // 深入条件 + fn deep_criterion(path: &Path) -> bool { + path.file_name() + .is_some_and(|name| name.to_str().is_some_and(|s| is_name_match("nars", s))) + } + + // 构建遍历者,加上条件 + let walker = PathWalkerV1::new(start, deep_criterion).unwrap(); + + let is_extension_match = |path: &PathBuf| { + path.extension().is_some_and(|ext| { + ext.to_str().is_some_and(|ext_str| { + extension_names + .iter() + .any(|name| is_name_match(name.as_ref(), ext_str)) + }) + }) + }; + + // 遍历(成功的) + let mut c = 0; + let mut c_valid = 0; + let mut valid_non_empty_configs = vec![]; + for path in walker.flatten().filter(is_extension_match) { + if verbose { + println_cli!([Log] "正在搜索 {path:?}"); + } + if let Ok(config) = read_config_extern(&path) { + c_valid += 1; + if !config.is_empty() { + if verbose { + println_cli!([Info] "搜索到配置文件:{config:?}"); + } + valid_non_empty_configs.push(config); + } + } + c += 1; + } + + // 输出搜索结果 + println_cli!( + [Info] + "一共搜索了{c}个文件,其中 {c_valid} 个文件符合条件,{} 个非空", + &valid_non_empty_configs.len() + ); + match valid_non_empty_configs.is_empty() { + true => println_cli!([Info] "未搜索到任何有效配置。"), + false => { + println_cli!([Info] "已搜索到以下有效配置:"); + for (i, config) in valid_non_empty_configs.iter().enumerate() { + // TODO: 后续或许在其中添加描述信息? + let information = config.description.clone().unwrap_or(config.to_debug()); + println_cli!([Info] "【{i}】 {information}"); + } + } + } + + // 返回 + Ok(valid_non_empty_configs) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use babel_nar::tests::config_paths::ARG_PARSE_TEST; + // use std::env::current_dir; + + #[test] + fn test_path_walker_v1() { + // 测试`config`目录下的文件 + let start = ARG_PARSE_TEST; + // * 📌起始目录即项目根目录 + search_configs(&PathBuf::from(start), ["json", "hjson"], true).expect("搜索出错"); + } +} diff --git a/src/bin/babelnar_cli/main.rs b/src/bin/babelnar_cli/main.rs new file mode 100644 index 0000000..bfb6c5f --- /dev/null +++ b/src/bin/babelnar_cli/main.rs @@ -0,0 +1,373 @@ +//! BabelNAR 命令行接口 +//! * ✨提供对BabelNAR的命令行支持 +//! +//! ## 命令行参数语法 +//! +//! ``` +//! usage: BabelNAR [OPTIONS] +//! ``` + +use anyhow::Result; +use babel_nar::{eprintln_cli, println_cli}; +use clap::Parser; +use std::io::Result as IoResult; +use std::thread::sleep; +use std::time::Duration; +use std::{env, path::PathBuf}; + +nar_dev_utils::mods! { + // 启动参数 + use vm_config; + // 命令行解析 + use arg_parse; + // 配置(自动)搜索 + use config_search; + // 从配置启动 + use config_launcher; + // 运行时交互、管理 + use runtime_manage; + // Websocket服务端 + use websocket_server; +} + +/// 主入口 +pub fn main() -> Result<()> { + // 以默认参数启动 + main_args(env::current_dir(), env::args()) +} + +/// 以特定参数开始命令行主程序 +/// * 🚩此处只应该有自[`env`]传入的参数 +/// * 🚩【2024-04-01 14:25:38】暂时用不到「当前工作路径」 +pub fn main_args(cwd: IoResult, args: impl Iterator) -> Result<()> { + // 解包当前工作目录 + let cwd = cwd + .inspect_err(|e| println_cli!([Warn] "无法获取当前工作目录:{e}")) + .ok(); + + // (Windows下)启用终端颜色 + let _ = colored::control::set_virtual_terminal(true) + .inspect_err(|_| eprintln_cli!([Error] "无法启动终端彩色显示。。")); + + // 解析命令行参数 + let args = CliArgs::parse_from(args); + + // 读取配置 | with 默认配置文件 + let mut config = load_config(&args); + + // 是否向用户展示「详细信息」 | 用于等待、提示等 + let user_verbose = config.user_input.is_none() || config.user_input.unwrap(); + + // 用户填充配置项 | 需要用户输入、工作路径(🎯自动搜索) + polyfill_config_from_user(&mut config, cwd); + + // 清屏,预备启动 + if user_verbose { + println_cli!([Info] "配置加载完毕!程序将在1s后启动。。。"); + sleep(Duration::from_secs(1)); + } + let _ = clearscreen::clear().inspect_err(|e| eprintln_cli!([Warn] "清屏失败:{e}")); + + // 从配置项启动 | 复制一个新配置,不会附带任何非基础类型开销 + let (runtime, config) = match launch_by_config(config.clone()) { + // 启动成功⇒返回 + Ok((r, c)) => (r, c), + // 启动失败⇒打印错误信息,等待并退出 + Err(e) => { + println_cli!([Error] "NARS运行时启动错误:{e}"); + // 空配置/启用用户输入⇒延时提示 + if user_verbose { + println_cli!([Info] "程序将在 3 秒后自动退出。。。"); + sleep(Duration::from_secs(3)); + } + return Err(e); + } + }; + + // 运行时交互、管理 + let manager = RuntimeManager::new(runtime, config.clone()); + let result = loop_manage(manager, &config); + + // 启用用户输入时延时提示 + if config.user_input { + println_cli!([Info] "程序将在 5 秒后自动退出。。。"); + sleep(Duration::from_secs(3)); + } + + // 返回结果 + result +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use babel_nar::tests::config_paths::*; + use nar_dev_utils::list; + + /// 测试入口/ONA + /// * 🎯通用、可复用的启动代码 + /// * 🎯跨不同CIN通用 + /// * 🎯跨同CIN不同测试通用 + pub fn main(cin_config_path: &str, other_args: &[&str]) -> Result<()> { + babel_nar::exists_or_exit!("./executables"); + // 以默认参数启动 + main_args( + env::current_dir(), + [ + &["BabelNAR-cli.exe", "-d", "-c", cin_config_path], + other_args, + ] + .concat() + .into_iter() + .map(str::to_string), + ) + } + + /// 测试入口/多配置加载 + /// * 🎯多「虚拟机启动配置」合并 + /// * 🎯预引入NAL + pub fn main_configs(cin_config_path: &str, other_config_paths: &[&str]) -> Result<()> { + let args = list![ + [ + // 第二个文件,搭建测试环境 + "-c", + config_path, + // 第三个文件,指示预加载 + "-c", + config_path, + ] + for config_path in (other_config_paths) + ] + .concat(); + main(cin_config_path, &args) + } + + /// 批量生成「预引入NAL」 + macro_rules! cin_tests { + ( + $cin_path:expr; + $( + $(#[$attr:meta])* + $name:ident => $config_path:expr $(;)? + )* + ) => { + /// 主Shell + /// * 🎯正常BabelNAR CLI shell启动 + /// * 🎯正常用户命令行交互体验 + #[test] + pub fn main_shell() -> Result<()> { + main($cin_path, &[]) + } + + + /// Matriangle服务器 + /// * 🎯复现先前基于Matriangle环境的NARS实验 + #[test] + pub fn main_matriangle_server() -> Result<()> { + // 以默认参数启动 + main_configs($cin_path, &[MATRIANGLE_SERVER]) + } + + $( + $(#[$attr])* + #[test] + pub fn $name() -> Result<()> { + main_configs($cin_path, &[PRELUDE_TEST, $config_path]) + } + )* + }; + } + + /// 测试/ONA + mod ona { + use super::*; + + cin_tests! { + ONA; + + /// 简单演绎 + /// * 📝✅【2024-04-07 14:56:04】成功 + nal_de => NAL_SIMPLE_DEDUCTION + + /// 高阶演绎 + /// * 📝✅【2024-04-07 14:56:04】成功 + nal_hi => NAL_HIGHER_DEDUCTION + + /// 自变量消除 + /// * 📝✅【2024-04-07 16:03:47】成功 + nal_ie => NAL_I_VAR_ELIMINATION + + /// 时间归纳 + /// * 📝✅【2024-04-07 15:22:28】成功 + nal_te => NAL_TEMPORAL_INDUCTION + + /// 简单操作 + /// * 📝❌【2024-04-07 16:15:53】失败:推理不出任何内容 + nal_so => NAL_SIMPLE_OPERATION + + /// 操作 + /// * 📝✅【2024-04-07 14:57:50】成功,但少许问题 + /// * 📝【2024-04-07 14:17:21】目前ONA面对其中的「经验问句」没有回答 + /// * ⚠️在启用`REG left`注册操作后,反而从成功变为失败 + nal_op => NAL_OPERATION + } + } + + /// 测试/OpenNARS + mod opennars { + use super::*; + + cin_tests! { + OPENNARS; + + /// 简单演绎 + /// * 📝✅【2024-04-07 14:59:37】成功 + nal_de => NAL_SIMPLE_DEDUCTION + + /// 高阶演绎 + /// * 📝✅【2024-04-07 14:59:44】成功 + nal_hi => NAL_HIGHER_DEDUCTION + + /// 自变量消除 + /// * 📝✅【2024-04-07 16:01:15】成功 + nal_ie => NAL_I_VAR_ELIMINATION + + /// 时间归纳 + /// * 📝✅【2024-04-07 15:22:28】成功 + nal_te => NAL_TEMPORAL_INDUCTION + + /// 简单操作 + /// * 📝✅【2024-04-07 16:13:39】成功 + nal_so => NAL_SIMPLE_OPERATION + + /// 操作 + /// * 📝✅【2024-04-07 14:59:53】成功 + nal_op => NAL_OPERATION + } + } + + /// 测试/PyNARS + mod pynars { + use super::*; + + cin_tests! { + PYNARS; + + /// 简单演绎 + /// * 📝✅【2024-04-07 17:11:22】成功 + nal_de => NAL_SIMPLE_DEDUCTION + + /// 高阶演绎 + /// * 📝✅【2024-04-07 17:11:36】成功 + nal_hi => NAL_HIGHER_DEDUCTION + + /// 自变量消除 + /// * 📝❌【2024-04-07 16:01:15】失败:啥推理都没有 + nal_ie => NAL_I_VAR_ELIMINATION + + /// 时间归纳 + /// * 📝❌【2024-04-07 16:13:52】失败:只会回答`D>. :\: %1.000;0.900%` + nal_te => NAL_TEMPORAL_INDUCTION + + /// 简单操作 + /// * 📝❌【2024-04-07 16:13:42】失败:没有任何回答 + nal_so => NAL_SIMPLE_OPERATION + + /// 操作 + /// * 📝❌【2024-04-07 14:39:49】目前仍测试失败 + /// * 📌PyNARS自身对NAL-7、NAL-8支持尚不完善 + /// * 📌PyNARS中操作`left`并非默认已注册 + /// * ❌【2024-04-07 14:41:54】补充:追加了也不行 + nal_op => NAL_OPERATION + } + } + + /// 测试/CXinJS + mod cxin_js { + use super::*; + + cin_tests! { + CXIN_JS; + + /// 简单演绎 + /// * 📝❌【2024-04-07 14:37:49】失败:导出了结论,但没法回答 + nal_de => NAL_SIMPLE_DEDUCTION + + /// 高阶演绎 + /// * 📝❌【2024-04-07 14:37:49】失败:只能导出到`B>?` + /// * 📌即便是五百步,也推不出来 + nal_hi => NAL_HIGHER_DEDUCTION + + /// 自变量消除 + /// * 📝❌【2024-04-07 16:01:15】失败:仅推理到`C>?`,并且遇到「XXX is not a function」错误 + nal_ie => NAL_I_VAR_ELIMINATION + + /// 时间归纳 + /// * 📝❌失败:解析即报错——不支持`=/>` + nal_te => NAL_TEMPORAL_INDUCTION + + /// 简单操作 + /// * 📝❌【2024-04-07 16:16:24】失败:推理不出任何内容 + /// * 💭还会把「目标」解析成「判断」…… + nal_so => NAL_SIMPLE_OPERATION + + /// 操作 + /// * 📝❌目前仍测试失败 + /// * 📌PyNARS自身对NAL-7、NAL-8支持尚不完善 + /// * 📌PyNARS中操作`left`并非默认已注册 + /// * 📝❌【2024-04-07 14:37:49】失败:自身就不支持 + nal_op => NAL_OPERATION + } + } + + /// 测试/原生IL-1 + mod native_il_1 { + use super::*; + + cin_tests! { + NATIVE_IL_1; + + /// 简单演绎 + /// * 📝✅【2024-04-09 21:12:10】成功 + nal_de => NAL_SIMPLE_DEDUCTION + + /// 高阶演绎 + /// * 📝❌【2024-04-09 21:12:32】失败:尚不支持 + nal_hi => NAL_HIGHER_DEDUCTION + + /// 自变量消除 + /// * 📝❌【2024-04-09 21:12:32】失败:尚不支持 + nal_ie => NAL_I_VAR_ELIMINATION + + /// 时间归纳 + /// * 📝❌【2024-04-09 21:12:32】失败:尚不支持 + nal_te => NAL_TEMPORAL_INDUCTION + + /// 简单操作 + /// * 📝❌【2024-04-09 21:12:32】失败:尚不支持 + nal_so => NAL_SIMPLE_OPERATION + + /// 操作 + /// * 📝❌【2024-04-09 21:12:32】失败:尚不支持 + nal_op => NAL_OPERATION + } + } + + // ! ❌【2024-04-07 14:39:20】接口完成度不高的NARS-Python、OpenJunars暂不进行测试 + + /// 测试入口/带Websocket Shell + /// * 🎯正常BabelNAR CLI shell启动 + /// * 🎯用户命令行交互体验(并存) + /// * 🎯Websocket通信 + #[test] + pub fn main_websocket() -> Result<()> { + // 以默认参数启动 + main_args( + env::current_dir(), + ["test.exe", "-d", "-c", ONA, "-c", WEBSOCKET] + .into_iter() + .map(str::to_string), + ) + } +} diff --git a/src/bin/babelnar_cli/runtime_manage.rs b/src/bin/babelnar_cli/runtime_manage.rs new file mode 100644 index 0000000..652e6ba --- /dev/null +++ b/src/bin/babelnar_cli/runtime_manage.rs @@ -0,0 +1,502 @@ +//! 启动后运行时的(交互与)管理 + +use super::websocket_server::*; +use crate::{launch_by_runtime_config, InputMode, LaunchConfigPreludeNAL, RuntimeConfig}; +use anyhow::{anyhow, Result}; +use babel_nar::{ + cli_support::{ + error_handling_boost::error_anyhow, + io::{ + navm_output_cache::{ArcMutex, OutputCache}, + readline_iter::ReadlineIter, + }, + }, + eprintln_cli, if_let_err_eprintln_cli, println_cli, + runtimes::TranslateError, + test_tools::{nal_format::parse, put_nal, VmOutputCache}, +}; +use nar_dev_utils::{if_return, manipulate, pipe, ResultBoost}; +use navm::{ + cmd::Cmd, + vm::{VmRuntime, VmStatus}, +}; +use std::{ + fmt::Debug, + ops::ControlFlow::{self, Break, Continue}, + path::Path, + sync::{Arc, Mutex}, + thread::{self, sleep, JoinHandle}, + time::Duration, +}; + +/// 运行时管理器 +/// * 🎯在一个数据结构中封装「虚拟机运行时」与「配置信息」 +/// * 📌只负责**单个运行时**的运行管理 +/// * 🚩不负责「终止、重启运行时」等过程 +#[derive(Debug, Clone)] +pub struct RuntimeManager +where + // ! 🚩【2024-04-02 14:51:23】需要`Send + Sync`进行多线程操作,需要`'static`保证生命周期 + R: VmRuntime + Send + Sync + 'static, +{ + /// 内部封装的虚拟机运行时 + /// * 🏗️后续可能会支持「同时运行多个虚拟机」 + /// * 🚩多线程共享:输入/输出 + pub(crate) runtime: ArcMutex, + + /// 内部封装的「命令行参数」 + /// * 🎯用于从命令行中加载配置 + /// * 🚩只读 + pub(crate) config: Arc, + + /// 内部缓存的「NAVM输出」 + /// * 🎯用于NAL测试 + /// * 🚩多线程共享 + pub(crate) output_cache: ArcMutex, +} + +impl RuntimeManager +where + R: VmRuntime + Send + Sync + 'static, +{ + /// 构造函数 + /// * 🎯由此接管虚拟机实例、配置的所有权 + pub fn new(runtime: R, config: RuntimeConfig) -> Self { + Self { + runtime: Arc::new(Mutex::new(runtime)), + config: Arc::new(config), + // 创建的同时增加侦听器 + output_cache: Self::new_output_cache(), + } + } + + /// 新建一个「输出缓存」 + /// * 🚩创建缓存⇒增加侦听器⇒装入[`ArcMutex`] + /// * 🎯避免 + fn new_output_cache() -> ArcMutex { + pipe! { + manipulate!( + // 产生一个新的「输出缓存」 + OutputCache::default() + // 添加侦听器 + => Self::add_output_listener + ) + // 装入ArcMutex + => Mutex::new => Arc::new + } + } + + /// 增加「打印输出」侦听器 + /// * 🎯(与Websocket一同)分离「输出侦听」逻辑 + /// * 🎯统一给管理者添加功能 + /// * ❓后续可配置 + fn add_output_listener(output_cache: &mut OutputCache) { + output_cache.output_handlers.add_handler(|output| { + // 打印输出 + println_cli!(&output); + // 继续返回 + Some(output) + }); + } + + /// 【主函数】在运行时启动后,对其进行管理 + /// * 🎯健壮性:更多「警告/重来」而非`panic` + /// * 🎯用户友好:尽可能隐藏底层内容 + /// * 如错误堆栈 + /// * 📌主要逻辑 + /// * `.nal`脚本预加载 + /// * 用户的运行时交互 + /// * Websocket服务端 + /// * 🚩【2024-04-03 00:33:41】返回的[`Result`]作为程序的终止码 + /// * `Ok(Ok(..))` ⇒ 程序正常终止 + /// * `Ok(Err(..))` ⇒ 程序异常终止 + /// * `Err(..)` ⇒ 程序异常中断 + pub fn manage(&mut self) -> Result> { + // 生成「读取输出」子线程 | 📌必须最先 + let thread_read = self.spawn_read_output()?; + + // 预置输入 | ⚠️阻塞 + let prelude_result = self.prelude_nal(); + match prelude_result { + // 预置输入要求终止⇒终止 + Break(result) => return Ok(result), + // 预置输入发生错误⇒展示 & 继续 + Continue(Err(e)) => println_cli!([Error] "预置NAL输入发生错误:{e}"), + Continue(Ok(..)) => (), + } + + // 虚拟机被终止 & 无用户输入 ⇒ 程序退出 + if let VmStatus::Terminated(..) = self.runtime.lock().transform_err(error_anyhow)?.status() + { + if !self.config.user_input { + // 直接返回,使程序退出 + return Ok(Ok(())); + } + } + + // 生成「Websocket服务」子线程(若有连接) + let thread_ws = self.try_spawn_ws_server()?; + + // 生成「用户输入」子线程 + let mut thread_input = None; + if self.config.user_input { + thread_input = Some(self.spawn_user_input()?); + } + + // ! 🚩不要在主线程开始用户输入 + + // 等待子线程结束,并抛出其抛出的错误 + // ! 🚩【2024-04-02 15:09:32】错误处理交给外界 + thread_read.join().transform_err(error_anyhow)??; + if let Some(thread_ws) = thread_ws { + thread_ws.join().transform_err(error_anyhow)?? + } + if let Some(thread_input) = thread_input { + thread_input.join().transform_err(error_anyhow)??; + } + + // 正常运行结束 + Ok(Ok(())) + } + + /// 预置NAL + /// * 🎯用于自动化调取`.nal`文件进行测试 + /// * 🚩【2024-04-03 10:28:18】使用[`ControlFlow`]对象以控制「是否提前返回」和「返回的结果」 + /// * 📌[`Continue`] => 使用「警告&忽略」的方式处理[`Result`] => 继续(用户输入/Websocket服务端) + /// * 📌[`Break`] => 告知调用者「需要提前结束」 + /// * 📌[`Break`]([`Ok`]) => 正常退出 + /// * 📌[`Break`]([`Err`]) => 异常退出(报错) + pub fn prelude_nal(&mut self) -> ControlFlow, Result<()>> { + let config = &*self.config; + + /// 尝试获取结果并返回 + /// * 🎯对错误返回`Break(Err(错误))`而非`Err(错误)` + macro_rules! try_break { + // 统一逻辑 + ($v:expr => $e_id:ident $e:expr) => { + match $v { + // 获取成功⇒返回并继续 + Ok(v) => v, + // 获取失败⇒ 告知「异常结束」 + Err($e_id) => return Break(Err($e)), + } + }; + // 两种错误分派方法 + ($v:expr) => { try_break!($v => e e.into()) }; + (anyhow $v:expr) => { try_break!($v => e error_anyhow(e)) }; // * 🎯针对`PoisonError` + } + + // 尝试获取运行时引用 | 仅有其它地方panic了才会停止 + let runtime = &mut *try_break!(anyhow self.runtime.lock()); + + // 仅在有预置NAL时开始 + if let Some(prelude_nal) = &config.prelude_nal { + // 尝试获取输出缓冲区引用 | 仅有其它地方panic了才会停止 + let output_cache = + &mut *try_break!(OutputCache::unlock_arc_mutex(&mut self.output_cache)); + + // 读取内容 + let nal = match prelude_nal { + // 文件⇒尝试读取文件内容 | ⚠️此处创建了一个新值,所以要统一成`String` + LaunchConfigPreludeNAL::File(path) => { + try_break!(std::fs::read_to_string(path) => e { + println_cli!([Error] "读取预置NAL文件 {path:?} 发生错误:{e}"); + // 继续(用户输入/Websocket服务端) + e.into() + }) + } + // 纯文本⇒直接引入 + LaunchConfigPreludeNAL::Text(nal) => nal.to_string(), + }; + + // 获取「NAL执行路径」 + // * 🎯在「预置NAL」中执行「保存文件」时,决定以哪个路径为「相对路径起点」 + let nal_file_path = match prelude_nal { + // 文件⇒基于文件路径 + LaunchConfigPreludeNAL::File(path) => { + path.parent().unwrap_or(&self.config.config_path) + } + // 纯文本⇒直接引入 + LaunchConfigPreludeNAL::Text(..) => &self.config.config_path, + }; + + // 输入NAL并处理 + // * 🚩【2024-04-03 11:10:44】遇到错误,统一上报 + // * 根据「严格模式」判断要「继续」还是「终止」 + let put_result = + Self::input_nal_to_vm(runtime, &nal, output_cache, config, nal_file_path); + match self.config.strict_mode { + false => Continue(put_result), + true => Break(put_result), + } + } + // 否则自动返回「正常」 + else { + // 返回 | 正常继续 + Continue(Ok(())) + } + } + + /// 生成「读取输出」子线程 + pub fn spawn_read_output(&mut self) -> Result>> { + // 准备引用 + let runtime = self.runtime.clone(); + let output_cache = self.output_cache.clone(); + + // 启动线程 + let thread = thread::spawn(move || { + loop { + // 尝试获取运行时引用 | 仅有其它地方panic了才会停止 + let mut runtime = runtime.lock().transform_err(error_anyhow)?; + + // 若运行时已终止,返回终止信号 + if let VmStatus::Terminated(result) = runtime.status() { + // * 🚩【2024-04-02 21:48:07】↓下面没法简化:[`anyhow::Result`]拷贝之后还是引用 + match result { + Ok(..) => break Ok(()), + Err(e) => break Err(anyhow!("NAVM运行时已终止:{e}")), + } + } + + // 尝试拉取所有NAVM运行时输出 + while let Ok(Some(output)) = runtime + .try_fetch_output() + .inspect_err(|e| eprintln_cli!([Error] "尝试拉取NAVM运行时输出时发生错误:{e}")) + { + // 缓存输出 + // * 🚩在缓存时格式化输出 + match output_cache.lock() { + Ok(mut output_cache) => output_cache.put(output)?, + Err(e) => eprintln_cli!([Error] "缓存NAVM运行时输出时发生错误:{e}"), + } + } + } + }); + + // 返回启动的线程 + Ok(thread) + } + + /// 生成「Websocket服务」子线程 + pub fn try_spawn_ws_server(&mut self) -> Result>>> { + // 若有⇒启动 + if self.config.websocket.is_some() { + let thread = spawn_ws_server(self)?; + return Ok(Some(thread)); + } + + // 完成,即便没有启动 + Ok(None) + } + + /// 生成「用户输入」子线程 + pub fn spawn_user_input(&mut self) -> Result>> { + // 准备引用 + // ! 📝不能在此外置「可复用引用」变量:borrowed data escapes outside of method + let runtime = self.runtime.clone(); + let config = self.config.clone(); + let output_cache = self.output_cache.clone(); + + // 启动线程 + let thread = thread::spawn(move || { + // 主循环 + // ! 📝不能在此中出现裸露的`MutexGuard`对象:其并非线程安全 + // * ✅可使用`&(mut) *`重引用语法,从`MutexGuard`转换为线程安全的引用 + // * ✅对`Arc`使用`&*`同理:可以解包成引用,以便后续统一传递值的引用 + // ! 不建议在此启用提示词:会被异步的输出所打断 + for io_result in ReadlineIter::default() { + // 从迭代器中读取一行 + let line = io_result?; + let line = line.trim(); // ! 这两句无法合并:临时变量的引用问题 + + // 尝试获取运行时引用 | 仅有其它地方panic了才会停止 + // ! 📝PoisonError无法在线程中传递 + let runtime = &mut *runtime + .lock() + .transform_err(|e| anyhow!("获取运行时引用时发生错误:{e:?}"))?; + + // 若运行时已终止,返回终止信号 + if let VmStatus::Terminated(result) = runtime.status() { + // * 🚩【2024-04-02 21:48:07】↓下面没法简化:[`anyhow::Result`]拷贝之后还是引用 + match result { + Ok(..) => return Ok(()), + Err(e) => return Err(anyhow!("NAVM运行时已终止:{e}")), + } + } + + // 尝试获取输出缓冲区引用 | 仅有其它地方panic了才会停止 + // ! 🚩【2024-04-02 19:27:01】及早报错:即便无关紧要,也停止 + let output_cache = &mut *output_cache + .lock() + .transform_err(|e| anyhow!("获取NAVM输出缓存时发生错误:{e}"))?; + + // 非空⇒解析输入并执行 + if !line.is_empty() { + if_let_err_eprintln_cli!( + // * 🚩【2024-04-09 22:11:41】置入时以「配置文件所在目录」为NAL工作目录 + Self::input_line_to_vm(runtime, line, &config, output_cache, &config.config_path) + => e => [Error] "输入过程中发生错误:{e}" + ); + } + } + + // 返回 + Ok(()) + }); + + // 返回启动的线程 + Ok(thread) + } + + /// 置入一行输入 + /// * 📄`nal_root_path`:从NAL文件加载⇒NAL文件所在路径;用户输入⇒配置文件所在路径 + pub fn input_line_to_vm( + runtime: &mut R, + line: &str, + config: &RuntimeConfig, + output_cache: &mut OutputCache, + nal_root_path: &Path, + ) -> Result<()> { + // 向运行时输入 + match config.input_mode { + // NAVM指令 + // * ✨【2024-04-09 22:48:01】转义输入:使用(NAVM指令不可能用的)前缀「/」以重新启用「NAL输入」 + InputMode::Cmd => match line.starts_with('/') { + true => { + Self::input_nal_to_vm(runtime, &line[1..], output_cache, config, nal_root_path) + } + false => Self::input_cmd_to_vm(runtime, line), + }, + // NAL输入 + InputMode::Nal => { + Self::input_nal_to_vm(runtime, line, output_cache, config, nal_root_path) + } + } + } + + /// 像NAVM实例输入NAVM指令 + fn input_cmd_to_vm(runtime: &mut R, line: &str) -> Result<()> { + let cmd = + Cmd::parse(line).inspect_err(|e| eprintln_cli!([Error] "NAVM指令解析错误:{e}"))?; + runtime + .input_cmd(cmd) + .inspect_err(|e| eprintln_cli!([Error] "NAVM指令执行错误:{e}")) + } + + /// 向NAVM实例输入NAL(输入) + /// * 🎯预置、用户输入、Websocket输入 + /// * 🎯严格模式 + /// * 📌要么是「有失败 + 非严格模式 ⇒ 仅报告错误」 + /// * 📌要么是「有一个失败 + 严格模式 ⇒ 返回错误」 + /// * ⚠️可能有多行 + fn input_nal_to_vm( + runtime: &mut R, + input: &str, + output_cache: &mut OutputCache, + config: &RuntimeConfig, + nal_root_path: &Path, // 📄从NAL文件加载⇒NAL文件所在路径;用户输入⇒配置文件所在路径 + ) -> Result<()> { + // 解析输入,并遍历解析出的每个NAL输入 + for input in parse(input) { + // 尝试解析NAL输入 + match input { + // 错误⇒根据严格模式处理 + Err(e) => { + // 无论是否严格模式,都报告错误 + eprintln_cli!([Error] "解析NAL输入时发生错误:{e}"); + // 严格模式下提前返回 + if_return! { config.strict_mode => Err(e) } + } + Ok(nal) => { + // 尝试置入NAL输入 | 为了错误消息,必须克隆 + let put_result = put_nal( + runtime, + nal.clone(), + output_cache, + config.user_input, + nal_root_path, + ); + // 处理错误 + if let Err(e) = put_result { + // 无论是否严格模式,都报告错误 + eprintln_cli!([Error] "置入NAL输入「{nal:?}」时发生错误:{e}"); + // 严格模式下考虑上报错误 + if config.strict_mode { + match e.downcast_ref::() { + // * 🚩在「不支持的指令」时仅警告 + // * 🎯**兼容尽可能多的CIN版本** + Some(TranslateError::UnsupportedInput(..)) => {} + // * 🚩在「其他错误」时直接返回 + _ => return Err(e), + } + } + } + } + } + } + // 正常返回 + Ok(()) + } +} + +/// 重启虚拟机 +/// * 🚩消耗原先的虚拟机管理者,返回一个新的管理者 +/// * 🚩【2024-04-02 20:25:21】目前对「终止先前虚拟机」持放松态度 +/// * 📝从`ArcMutex>`中拿取值的所有权:[`Arc::try_unwrap`] + [`Mutex::into_inner] +/// * 🔗参考: +pub fn restart_manager( + manager: RuntimeManager, +) -> Result> { + // 尝试终止先前的虚拟机 + // ! ❌[`Arc::try_unwrap`]的返回值包括`VmRuntime`,所以连[`Debug`]都不支持 + // ! ❌【2024-04-02 20:33:01】目前测试中`Arc::into_inner`基本总是失败(线程里还有引用) + // * 🚩【2024-04-02 20:33:18】现在通过修改NAVM API,不再需要获取运行时所有权了(销毁交给) + // let old_runtime_mutex = + // Arc::into_inner(manager.runtime).ok_or(anyhow!("runtime Arc解包失败"))?; + // let mut old_runtime = old_runtime_mutex.into_inner()?; + let old_runtime = &mut *manager + .runtime + .lock() + .transform_err(|e| anyhow!("runtime Mutex解锁失败:{e:?}"))?; + old_runtime.terminate()?; + + // 启动新的虚拟机 + let config_ref = &*manager.config; + let new_runtime = launch_by_runtime_config(config_ref)?; + let new_manager = RuntimeManager::new(new_runtime, config_ref.clone()); + + // 返回 + Ok(new_manager) +} + +/// 根据配置(的「是否重启」选项)管理(一系列)虚拟机实例 +pub fn loop_manage( + mut manager: RuntimeManager, + config: &RuntimeConfig, +) -> Result<()> { + match manager.manage() { + // 返回了「结果」⇒解包并传递结果 + Ok(result) => result, + // 发生错误⇒尝试处理 + Err(e) => { + // 打印错误信息 + println_cli!([Error] "运行时发生错误:{e}"); + // 尝试重启 + if config.auto_restart { + println_cli!([Info] "程序将在 2 秒后自动重启。。。"); + sleep(Duration::from_secs(2)); + let new_manager = match restart_manager(manager) { + Ok(manager) => manager, + Err(e) => { + println_cli!([Error] "重启失败:{e}"); + return Err(anyhow!("NAVM运行时发生错误,且重启失败:{e}")); + } + }; + // 重启之后继续循环 + return loop_manage(new_manager, config); + } + // 正常返回 + Ok(()) + } + } +} diff --git a/src/bin/babelnar_cli/vm_config.rs b/src/bin/babelnar_cli/vm_config.rs new file mode 100644 index 0000000..3beeae0 --- /dev/null +++ b/src/bin/babelnar_cli/vm_config.rs @@ -0,0 +1,787 @@ +//! BabelNAR CLI的启动配置 +//! * ✨格式支持 +//! * ✅JSON +//! * 🎯用于配置表示,❗不用于命令行解析 +//! * ⚠️【2024-04-01 14:31:09】特定于二进制crate,目前不要并入[`babel_nar`] +//! +//! ## ⚙️内容 +//! +//! Rust结构: +//! +//! * 📌转译器组合? +//! * (互斥)单个值?(输入输出相同) `opennars` / `ona` / `nars-python` / `pynars` / `openjunars` / `cxin-js` +//! * (互斥)输入输出单独配置? +//! * 输入 `opennars` / `ona` / `nars-python` / `pynars` / `openjunars` / `cxin-js` +//! * 输出 `opennars` / `ona` / `nars-python` / `pynars` / `openjunars` / `cxin-js` +//! * 📌启动命令? +//! * 命令 `XXX.exe` / `python` / `java` / `node` / ... +//! * 命令参数? `["-m", 【Python模块】]` / `["-jar", 【Jar路径】]` +//! * 工作目录? `root/path/to/current_dir` | 🎯用于Python模块 +//! * 📌预置NAL? +//! * (互斥)文件路径? `root/path/to/file` | 与下边「纯文本」互斥 +//! * (互斥)纯文本? `"'/VOL 0"` +//! * 📌Websocket参数? | ✅支持ipv6 +//! * 主机地址 `localhost` `192.168.1.1` `fe80::abcd:fade:dad1` +//! * 连接端口 `3040` +//! +//! TypeScript声明: +//! +//! ```ts +//! type LaunchConfig = { +//! translators?: LaunchConfigTranslators, +//! command?: LaunchConfigCommand, +//! websocket?: LaunchConfigWebsocket, +//! preludeNAL?: LaunchConfigPreludeNAL, +//! userInput?: boolean +//! inputMode?: InputMode +//! autoRestart?: boolean +//! } +//! +//! type InputMode = 'cmd' | 'nal' +//! +//! type LaunchConfigTranslators = string | { +//! // ↓虽然`in`是JavaScript/TypeScript/Rust的关键字,但仍可在此直接使用 +//! in: string, +//! out: string, +//! } +//! +//! type LaunchConfigCommand = { +//! cmd: string, +//! cmdArgs?: string[], +//! currentDir?: string, +//! } +//! type LaunchConfigWebsocket = { +//! host: string, +//! port: number, // Uint16 +//! } +//! // ↓ 文件、纯文本 二选一 +//! type LaunchConfigPreludeNAL = { +//! file?: string, +//! text?: string, +//! } +//! ``` + +use anyhow::{anyhow, Result}; +use babel_nar::println_cli; +use nar_dev_utils::{if_return, pipe, OptionBoost, ResultBoost}; +use serde::{Deserialize, Serialize}; +use std::{ + fs::read_to_string, + path::{Component, Path, PathBuf}, +}; + +/// 允许的配置文件扩展名 +/// * 🚩【2024-04-07 18:30:24】目前支持JSON与HJSON +/// * 📌其顺序决定了在「扩展名优先补充」中的遍历顺序 +/// * 📄当`a.hjson`与`a.json`存在时,`a`优先补全为`a.hjson` +pub const SUPPORTED_CONFIG_EXTENSIONS: &[&str] = &["hjson", "json"]; + +/// 工具宏/批量拷贝性合并 +/// * 🎯简化重复的`对象.方法`调用 +/// * 📄参考[`Option::coalesce_clone`] +macro_rules! coalesce_clones { + { + // 合并的方向 + $other:ident => $this:ident; + // 要合并的键 + $($field:ident)* + } => { $( $this.$field.coalesce_clone(&$other.$field); )* }; +} + +/// NAVM虚拟机(运行时)启动配置 +/// * 🎯启动完整的NAVM实例,并附带相关运行时配置 +/// * ✨启动时数据提供 +/// * ✨运行时数据提供 +/// * 📍【2024-04-04 02:17:10】现在所有都是**可选**的 +/// * 🎯用于无损合并从键值对中加载而来的配置 +/// * 📄`true`可以在识别到`null`时替换`null`,而无需管其是否为默认值 +/// * 🚩在启动时会转换为「运行时配置」,并在此时检查完整性 +/// * 📌这意味着其总是能派生[`Default`] +/// * ⚠️其中的所有**相对路径**,在[`read_config_extern`]中都基于**配置文件自身** +/// * 🎯不论CLI自身所处何处,均保证配置读取稳定 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LaunchConfig { + /// 配置的加载路径 + /// * 🎯用于记录「基于配置自身的配置路径」 + /// * 🚩从文件中加载⇒`Some(配置文件所在目录)` + /// * 🚩从其它加载⇒[`None`] + /// * 📌不在反序列化(解析)时解析 + #[serde(skip)] + #[serde(default)] + pub config_path: Option, + + /// 启动配置的文本描述 + /// * 🎯在自动搜索时呈现给用户 + /// * 📌一般是单行文本 + /// + /// * ❓I18n 国际化 + pub description: Option, + + /// 转译器组合 + /// * 🚩使用字符串模糊匹配 + pub translators: Option, + + /// 启动命令 + pub command: Option, + + /// Websocket参数 + /// * 🚩【2024-04-03 18:21:00】目前对客户端输出JSON + pub websocket: Option, + + /// 预置NAL + #[serde(rename = "preludeNAL")] // * 📝serde配置中,`rename`优先于`rename_all` + pub prelude_nal: Option, + + /// 启用用户输入 + /// * 🎯控制该实例是否需要(来自用户的)交互式输入 + /// * 🚩【2024-04-04 02:19:36】默认值由「运行时转换」决定 + /// * 🎯兼容「多启动配置合并」 + pub user_input: Option, + + /// 输入模式 + /// * 🚩对输入(不论交互还是Websocket)采用的解析模式 + /// * 📄纯NAVM指令的解析 + /// * 🎯兼容旧`BabelNAR.jl`服务端 + /// * 🚩【2024-04-04 02:19:36】默认值由「运行时转换」决定 + /// * 🎯兼容「多启动配置合并」 + #[serde(default)] + pub input_mode: Option, + + /// 自动重启 + /// * 🎯程序健壮性:用户的意外输入,不会随意让程序崩溃 + /// * 🚩在虚拟机终止(收到「终止」输出)时,自动用配置重启虚拟机 + /// * 🚩【2024-04-04 02:19:36】默认值由「运行时转换」决定 + /// * 🎯兼容「多启动配置合并」 + pub auto_restart: Option, + + /// 严格模式 + /// * 🎯测试敏感性:测试中的「预期失败」可以让程序上报异常 + /// * 🚩在「预引入NAL」等场景中,若出现「预期失败」则程序直接异常退出 + /// * 🚩【2024-04-04 02:19:36】默认值由「运行时转换」决定 + /// * 🎯兼容「多启动配置合并」 + pub strict_mode: Option, +} + +/// 使用`const`常量存储「空启动配置」 +/// * 🎯用于启动配置的「判空」逻辑 +/// * ✅与此同时,实现了「有提醒的后期维护」 +/// * 📌后续若新增字段,此处会因「缺字段」立即报错 +const EMPTY_LAUNCH_CONFIG: LaunchConfig = LaunchConfig { + config_path: None, + description: None, + translators: None, + command: None, + websocket: None, + prelude_nal: None, + user_input: None, + input_mode: None, + auto_restart: None, + strict_mode: None, +}; + +/// NAVM虚拟机(运行时)运行时配置 +/// * 🎯没有任何非必要的空值 +/// * 🚩自[`LaunchConfig`]加载而来 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeConfig { + /// 配置的加载路径 + /// * 🎯用于记录「基于配置自身的配置路径」 + /// * 🚩从文件中加载⇒配置文件所在目录 + /// * 🚩从其它加载⇒[`PathBuf::new`] + #[serde(skip)] + pub config_path: PathBuf, + + /// 转译器组合 + /// * 🚩运行时必须提供转译器 + /// * 📌【2024-04-04 02:11:44】即便是所谓「默认」转译器,使用「及早报错」避免非预期运行 + pub translators: LaunchConfigTranslators, + + /// 启动命令 + /// * 🚩运行时必须有一个启动命令 + /// * 🚩内部可缺省 + pub command: LaunchConfigCommand, + + /// Websocket参数(可选) + /// * 🚩允许无:不启动Websocket服务器 + pub websocket: Option, + + /// 预置NAL + /// * 🚩允许无:不预置NAL测试文件 + #[serde(rename = "preludeNAL")] // * 📝serde配置中,`rename`优先于`rename_all` + pub prelude_nal: Option, + + /// 启用用户输入 + /// * 🚩必选:[`None`]将视为默认值 + /// * 📜默认值:`true`(启用) + #[serde(default = "bool_true")] + pub user_input: bool, + + /// 输入模式 + /// * 🚩必选:[`None`]将视为默认值 + /// * 📜默认值:`"nal"` + #[serde(default)] + pub input_mode: InputMode, + + /// 自动重启 + /// * 🚩必选:[`None`]将视为默认值 + /// * 📜默认值:`false`(关闭) + #[serde(default = "bool_false")] + pub auto_restart: bool, + + /// 严格模式 + /// * 🚩必选:[`None`]将视为默认值 + /// * 📜默认值:`false`(关闭) + #[serde(default = "bool_false")] + pub strict_mode: bool, +} + +/// 布尔值`true` +/// * 🎯配置解析中「默认为`true`」的默认值指定 +/// * 📝serde中,`#[serde(default)]`使用的是[`bool::default`]而非容器的`default` +/// * 因此需要指定一个函数来初始化 +#[inline(always)] +const fn bool_true() -> bool { + true +} + +/// 布尔值`false` +/// * 🎯配置解析中「默认为`false`」的默认值指定 +/// * 📝serde中,`#[serde(default)]`使用的是[`bool::default`]而非容器的`default` +/// * 因此需要指定一个函数来初始化(false特别标识) +#[inline(always)] +const fn bool_false() -> bool { + false +} + +/// 尝试将启动时配置[`LaunchConfig`]转换成运行时配置[`RuntimeConfig`] +/// * 📌默认项:存在默认值,如「启用用户输入」「不自动重启」 +/// * 📌必选项:要求必填值,如「转译器组」「启动命令」 +/// * ⚠️正是此处可能报错 +/// * 📌可选项:仅为可选值,如「Websocket」「预引入NAL」 +impl TryFrom for RuntimeConfig { + type Error = anyhow::Error; + + fn try_from(config: LaunchConfig) -> Result { + Ok(Self { + // * 路径承袭:空值自动补默认值(空白) + config_path: config.config_path.unwrap_or_default(), + // * 🚩必选项统一用`ok_or(..)?` + translators: config.translators.ok_or(anyhow!("启动配置缺少转译器"))?, + command: config.command.ok_or(anyhow!("启动配置缺少启动命令"))?, + // * 🚩可选项直接置入 + websocket: config.websocket, + prelude_nal: config.prelude_nal, + // * 🚩默认项统一用`unwrap_or` + // 默认启用用户输入 + user_input: config.user_input.unwrap_or(true), + // 输入模式传递默认值 + input_mode: config.input_mode.unwrap_or_default(), + // 不自动重启 + auto_restart: config.auto_restart.unwrap_or(false), + // 不开启严格模式 + strict_mode: config.strict_mode.unwrap_or(false), + }) + } +} + +/// NAVM实例的输入类型 +/// * 🎯处理用户输入、Websocket输入的解析方式 +/// * 📜默认值:`nal` +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +// #[serde(untagged)] // ! 🚩【2024-04-02 18:14:16】不启用方通过:本质上是几个字符串里选一个 +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum InputMode { + /// (NAVM)指令 + /// * 📄类型:[`navm::cmd::Cmd`] + #[serde(rename = "cmd")] + Cmd, + /// `.nal`输入 + /// * 📜默认值 + /// * 📄类型:[`babel_nar::test_tools::NALInput`] + #[serde(rename = "nal")] + #[default] + Nal, +} + +/// 转译器组合 +/// * 🚩【2024-04-01 11:20:36】目前使用「字符串+内置模糊匹配」进行有限的「转译器支持」 +/// * 🚧尚不支持自定义转译器 +#[derive(Serialize, Deserialize)] +#[serde(untagged)] // 🔗参考: +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LaunchConfigTranslators { + /// 🚩单个字符串⇒输入输出使用同一个转译配置 + Same(String), + + /// 🚩一个对象⇒输入和输出分别使用不同的转译配置 + Separated { + #[serde(rename = "in")] + input: String, + #[serde(rename = "out")] + output: String, + }, +} + +/// 启动命令 +/// * ❓后续可能支持「自动搜索」 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LaunchConfigCommand { + /// 命令 + /// * 直接对应[`std::process::Command`] + /// * 🚩[`Default`]中默认对应空字串 + pub cmd: String, + + /// 命令的参数(可选) + pub cmd_args: Option>, + + /// 工作目录(可选) + /// * 🎯可用于Python模块 + /// * 🚩【2024-04-07 10:13:59】现在用于「基于配置文件的相对路径」 + /// * 📌被主程序在启动时用于「设置自身工作目录」 + pub current_dir: Option, +} + +/// Websocket参数 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LaunchConfigWebsocket { + /// 主机地址 + /// * 📄`localhost` + /// * 📄`192.168.0.0` + /// * 📄`fe80::abcd:fade:dad1` + pub host: String, + + /// 连接端口 + /// * 🚩采用十六位无符号整数 + /// * 📄范围:0 ~ 65535 + /// * 🔗参考: + pub port: u16, +} + +/// 预置NAL +/// * 🚩在CLI启动后自动执行 +/// * 📝[`serde`]允许对枚举支持序列化/反序列化 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] // 🔗参考: +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LaunchConfigPreludeNAL { + /// 从文件路径导入 + /// * 📌键名:`file` + /// * 📌类型:路径 + #[serde(rename = "file")] + File(PathBuf), + + /// 从文本解析 + /// * 📌键名:`text` + /// * 📌类型:纯文本(允许换行等) + #[serde(rename = "text")] + Text(String), +} + +/// 启动配置 +impl LaunchConfig { + /// 零参构造函数 + /// * 🚩使用[`Default`]提供默认空数据 + pub fn new() -> Self { + Self::default() + } + + /// 判断配置是否为空 + /// * 📌本质:判断字段是否全为[`None`] + /// * 🚩直接与「空配置」相匹配 + pub fn is_empty(&self) -> bool { + self == &EMPTY_LAUNCH_CONFIG + } + + /// (尝试)从(H)JSON字符串构造 + /// * 🚩【2024-04-04 03:43:01】现在使用[`deser_hjson`]兼容`json`且一并兼容`hjson` + /// * 🔗有关`hjson`格式: + pub fn from_json_str(json: &str) -> Result { + Ok(deser_hjson::from_str(json)?) + } + + /// 判断其自身是否需要用户填充 + /// * 🎯用于在「启动NAVM运行时」时避免「参数无效」情况 + /// * 📌原则:必填参数不能为空 + /// * 🚩判断「启动时必要项」是否为空 + pub fn need_polyfill(&self) -> bool { + // 启动命令非空 + self.command.is_none() || + // 输入输出转译器非空 + self.translators.is_none() + // ! Websocket为空⇒不启动Websocket服务器 + // ! 预加载NAL为空⇒不预加载NAL + } + + /// 变基一个相对路径 + /// * 🚩将`config_path`的路径作为自身[`Path::is_relative`]的根路径 + /// * 📌引入[`Path::canonicalize`]解决「`path/test/../a` => `path/a`」的问题 + /// * 📌总是将相对路径(按照以`config_path`为根路径)展开成绝对路径 + #[inline(always)] + pub fn rebase_relative_path(config_path: &Path, relative_path: &mut PathBuf) -> Result<()> { + // 若`relative_path`非相对路径,直接返回 + if_return! { relative_path.is_absolute() => Ok(()) } + // 先绝对化「配置根路径」 + let mut new_path = config_path.canonicalize()?; + // 遍历「相对路径」的组分,追加/上溯路径 + for component in relative_path.components() { + match component { + // 当前文件夹⇒跳过 + Component::CurDir => continue, + // 上一级文件夹⇒上溯 + Component::ParentDir => { + new_path.pop(); + } + // 其它⇒增加组分 + _ => new_path.push(component), + } + } + + // * ❌无法通过真正治本的「前缀替换」行事:[`PrefixComponent`]全为私有字段,无法构建⇒无法构建`Component` + // let new_path = new_path + // .components() + // .map(|com| match com { + // Component::Prefix(prefix) => { + // let prefix = match prefix.kind() { + // Prefix::VerbatimUNC(a, b) => Prefix::UNC(a, b), + // Prefix::VerbatimDisk(name) => Prefix::Disk(name), + // kind => kind, + // }; + // Component::from(prefix) + // } + // _ => com, + // }) + // .collect::(); + + // 转换回字符串,然后删除`canonicalize`产生的多余前缀 + // * ⚠️【2024-04-07 13:51:16】删除原因:JVM、Python等启动命令不能处理带`\\?\【盘符】:`、`\\.\【盘符】:`前缀的路径 + // * 📌即便其实际上为「Verbatim UNC prefixes」 + // * 🔗参考: + // * 🔗参考: + // 先转换为字符串 + if let Some(path) = new_path.to_str() { + new_path = path + // 删去无用前缀 + .trim_start_matches(r"\\?\") + .trim_start_matches(r"\\.\") + // 转换回路径 + .into(); + } + // 赋值 + *relative_path = new_path; + Ok(()) + } + + /// 变基配置中所含的路径,从其它地方变为「以配置文件自身为根」的绝对路径 + /// * 🎯解决「配置中的**相对路径**仅相对于exe而非配置文件本身」的问题 + /// * 🎯将配置中相对路径的**根目录**从「exe」变更到配置文件本身 + /// * 📌原则:由此消灭所有相对路径,均以「配置文件自身路径」为根,转换为绝对路径 + /// * 一同决定的还有其中的[`Self::config_path`]字段 + pub fn rebase_relative_path_from(&mut self, config_path: &Path) -> Result<()> { + // 配置所在目录 + if let Some(root) = config_path.parent() { + self.config_path = Some(root.to_path_buf()); + } + // 预加载NAL + if let Some(LaunchConfigPreludeNAL::File(ref mut path)) = &mut self.prelude_nal { + Self::rebase_relative_path(config_path, path)?; + } + // 启动命令 + if let Some(LaunchConfigCommand { + current_dir: Some(ref mut path), + .. + }) = &mut self.command + { + Self::rebase_relative_path(config_path, path)?; + } + // 返回成功 + Ok(()) + } + + /// 变基路径,但基于所有权 + /// * 📌总体逻辑:[`Self`]→[`Self`] + /// * ⚠️有可能会出错(引入[`Path::canonicalize`]) + pub fn rebase_path_from_owned(mut self, config_path: &Path) -> Result { + self.rebase_relative_path_from(config_path)?; + Ok(self) + } + + /// 从另一个配置中并入配置 + /// * 📌优先级:`other` > `self` + /// * 🚩合并逻辑:`Some(..)` => `None` + /// * 当并入者为`Some`,自身为`None`时,合并`Some`中的值 + /// * ✨对【内部含有可选键】的值,会**递归深入** + pub fn merge_from(&mut self, other: &Self) { + // 合并所有内部Option | 使用工具宏简化语法 + coalesce_clones! { + other => self; + translators + // command // ! 此键需递归处理 + websocket + prelude_nal + user_input + input_mode + auto_restart + strict_mode + } + // 递归合并所有【含有可选键】的值 + LaunchConfigCommand::merge_as_key(&mut self.command, &other.command); + } +} + +impl LaunchConfigCommand { + /// 从另一个配置中并入配置 + /// * 🚩`Some(..)` => `None` + pub fn merge_from(&mut self, other: &Self) { + coalesce_clones! { + other => self; + cmd_args + current_dir + } + } + + /// 作为一个键,从另一个配置中并入配置 + /// * 🚩`Some(..)` => `None` + /// * 适用于自身为[`Option`]的情况 + pub fn merge_as_key(option: &mut Option, other: &Option) { + // 先处理「自身为`None`」的情况 + option.coalesce_clone(other); + // 双重`inspect` + if let (Some(config_self), Some(config_other)) = (option, other) { + config_self.merge_from(config_other); + } + } +} + +/// 从外部JSON文件中加载启动配置 +/// * 🎯错误处理 & 错误⇒空置 +/// * 🚩在遇到错误时会发出警告 +/// * ⚠️若无需打印警告(并手动处理错误),请使用[`read_config_extern`] +/// * ⚠️其中的所有**相对路径**,在[`read_config_extern`]中都基于**配置文件自身** +/// * 🎯不论CLI自身所处何处,均保证配置读取稳定 +pub fn load_config_extern(path: &Path) -> Option { + // Ok⇒Some,Err⇒警告+None + read_config_extern(path).ok_or_run(|e| { + // 根据错误类型进行分派 // + // 文件读写错误 + if let Some(e) = e.downcast_ref::() { + match e.kind() { + std::io::ErrorKind::NotFound => { + println_cli!([Warn] "未在路径 {path:?} 找到外部配置,返回空配置……"); + } + _ => println_cli!([Warn] "读取外部配置时出现预期之外的错误: {}", e), + } + } + // 配置解析错误/serde + else if let Some(e) = e.downcast_ref::() { + match e.classify() { + serde_json::error::Category::Syntax => { + println_cli!([Warn] "外部配置文件格式错误,返回空配置……"); + } + _ => println_cli!([Warn] "解析外部配置时出现预期之外的错误: {}", e), + } + } + // 配置解析错误/hjson + else if let Some(e) = e.downcast_ref::() { + match e { + deser_hjson::Error::Syntax { .. } => { + println_cli!([Warn] "外部配置文件格式错误,使用空配置……"); + } + deser_hjson::Error::Io { .. } => { + println_cli!([Warn] "外部配置文件读取错误,使用空配置……"); + } + _ => println_cli!([Warn] "解析外部配置时出现预期之外的错误: {}", e), + } + } + // 其它 + else { + println_cli!([Warn] "加载外部配置时出现预期之外的错误: {}", e) + } + // 空置 + }) +} + +/// 从外部JSON文件中读取启动配置 +/// * 🎯仅涉及具体读取逻辑,不涉及错误处理 +/// * ⚠️其中的所有**相对路径**,在[`read_config_extern`]中都基于**配置文件自身** +/// * 🎯不论CLI自身所处何处,均保证配置读取稳定 +pub fn read_config_extern(path: &Path) -> Result { + // 尝试读取外部启动配置,并尝试解析 + pipe! { + path + // 尝试补全路径 + => try_complete_path + // 尝试读取文件内容 + => read_to_string + => {?}# + // 尝试解析JSON配置 + => #{&} + => LaunchConfig::from_json_str + => {?}# + // 变基相对路径,从「基于CLI自身」到「基于配置文件自身」 + => .rebase_path_from_owned(path.parent().ok_or(anyhow!("无效的根路径!"))?) + => {?}# + // 返回Ok(转换为`anyhow::Result`) + => Ok + } + // ! 若需使用`confy`,必须封装 + // * 🚩目前无需使用`confy`:可以自动创建配置文件,但个人希望其路径与exe同目录 + // Ok(confy::load_path(path)?) // ! 必须封装 +} + +/// 尝试对无扩展名的路径添加扩展名 +/// * 🎯用于自动匹配`.json`与`.hjson` +/// * ❌不能用于「多扩展名」的情况,如`BabelNAR.launch` +/// * 此处会认定是「有扩展名」而不会补全 +pub fn try_complete_path(path: &Path) -> PathBuf { + // 创建路径缓冲区 + let path = path.to_path_buf(); + // 当扩展名为空时补全 + if path.extension().is_none() { + // 尝试用已有的扩展名填充文件名 + for extension in SUPPORTED_CONFIG_EXTENSIONS { + // 尝试补全为指定扩展名 | 无扩展名⇒追加,有扩展名⇒替换 + let path_ = path.with_extension(extension); + if_return! { path_.exists() => path_ } + } + } + path +} + +/// 单元测试 +#[cfg(test)] +pub mod tests { + use super::*; + use anyhow::Result; + use babel_nar::tests::*; + use nar_dev_utils::asserts; + + /// 实用测试宏 + macro_rules! test_parse { + { $( $data:expr => $expected:expr )* } => { + $( + _test(&$data, &$expected).expect("测试失败"); + )* + }; + } + + fn _test(data: &str, expected: &LaunchConfig) -> Result<()> { + // Some JSON input data as a &str. Maybe this comes from the user. + let parsed = LaunchConfig::from_json_str(data)?; + + dbg!(&parsed); + assert_eq!(parsed, *expected); + + Ok(()) + } + + /// 测试/解析 + /// * 🎯JSON/HJSON的解析逻辑 + #[test] + fn test_parse() { + test_parse! { + // 平凡情况/空 + "{}" => LaunchConfig::new() + "{}" => LaunchConfig::default() + "{}" => EMPTY_LAUNCH_CONFIG + // 完整情况 + r#" + { + "translators": "opennars", + "command": { + "cmd": "java", + "cmdArgs": ["-Xmx1024m", "-jar", "nars.jar"], + "currentDir": "root/nars/test" + }, + "websocket": { + "host": "localhost", + "port": 8080 + }, + "preludeNAL": { + "text": "'/VOL 0" + } + }"# => LaunchConfig { + translators: Some(LaunchConfigTranslators::Same("opennars".into())), + command: Some(LaunchConfigCommand { + cmd: "java".into(), + cmd_args: Some(vec!["-Xmx1024m".into(), "-jar".into(), "nars.jar".into()]), + current_dir: Some("root/nars/test".into()) + }), + websocket: Some(LaunchConfigWebsocket{ + host: "localhost".into(), + port: 8080 + }), + prelude_nal: Some(LaunchConfigPreludeNAL::Text("'/VOL 0".into())), + ..Default::default() + } + // 测试`translators`、`prelude_nal`的其它枚举 + r#" + { + "translators": { + "in": "opennars", + "out": "ona" + }, + "command": { + "cmd": "root/nars/open_ona.exe" + }, + "preludeNAL": { + "file": "root/nars/prelude.nal" + } + }"# => LaunchConfig { + translators: Some(LaunchConfigTranslators::Separated { + input: "opennars".into(), + output: "ona".into() + }), + command: Some(LaunchConfigCommand { + cmd: "root/nars/open_ona.exe".into(), + ..Default::default() + }), + prelude_nal: Some(LaunchConfigPreludeNAL::File("root/nars/prelude.nal".into())), + ..Default::default() + } + r#" + { + "inputMode": "cmd" + }"# => LaunchConfig { + input_mode: Some(InputMode::Cmd), + ..Default::default() + } + r#"{ + "autoRestart": true, + "userInput": false + }"# => LaunchConfig { + auto_restart: Some(true), + user_input: Some(false), + ..Default::default() + } + } + /* + "file": "root/path/to/file" + */ + } + + /// 测试/读取 + /// * 🎯相对**配置文件**的路径表示 + /// * 🎯被重定向到`./executables`,以便启动其下的`.jar`文件 + #[test] + fn test_read() { + // 使用OpenNARS配置文件的路径作测试 + let path: PathBuf = config_paths::OPENNARS.into(); + let launch_config = read_config_extern(&path).expect("路径读取失败"); + let expected_path = "./executables".into(); + asserts! { + // * 🎯启动命令中的「当前目录」应该被追加到配置自身的路径上 + // * ✅即便拼接后路径是`"./src/tests/cli/config\\root/nars/test"`,也和上边的路径相等 + launch_config.command.unwrap().current_dir => Some(expected_path) + } + } +} diff --git a/src/bin/babelnar_cli/websocket_server.rs b/src/bin/babelnar_cli/websocket_server.rs new file mode 100644 index 0000000..70ade4a --- /dev/null +++ b/src/bin/babelnar_cli/websocket_server.rs @@ -0,0 +1,292 @@ +//! BabelNAR CLI的Websocket交互逻辑 +//! * 🎯为BabelNAR CLI实现Websocket IO +//! * 🎯实现专有的Websocket服务端逻辑 + +use crate::{LaunchConfigWebsocket, RuntimeConfig, RuntimeManager}; +use anyhow::Result; +use babel_nar::{ + cli_support::{ + error_handling_boost::error_anyhow, + io::{ + navm_output_cache::{ArcMutex, OutputCache}, + websocket::to_address, + }, + }, + eprintln_cli, if_let_err_eprintln_cli, println_cli, +}; +use navm::{output::Output, vm::VmRuntime}; +use std::{ + sync::Arc, + thread::{self, JoinHandle}, +}; +use ws::{Factory, Handler, Sender}; + +/// 工具宏:尝试执行,如果失败则上抛错误 +/// * 🎯在「无法使用[`anyhow::Result`]上抛错误」的情况下适用 +macro_rules! try_or_return_err { + ($value:expr; $e_id:ident => $($error_msg:tt)*) => { + match $value { + Ok(value) => value, + Err($e_id) => { + // 生成并输出错误信息 + let error_msg = format!($($error_msg)*); + println_cli!([Error] "{error_msg}"); + // 转换错误 | 使用Websocket的「内部错误」以指示是CLI的错误 + let error = ws::Error::new(ws::ErrorKind::Internal, error_msg); + return Err(error); + } + } + }; +} + +/// 通信用代码 +/// * 🎯统一有关「通信消息格式」的内容 +/// * 📌形式:JSON**对象数组** +/// * ⚠️【2024-04-08 19:08:15】即便一次只回传一条消息,也需包装上方括号`[{...}]` +#[inline] +pub fn format_output_message(output: &Output) -> String { + // 包装成「对象数组」 + format!("[{}]", output.to_json_string()) +} + +/// 入口代码 +/// * 🎯生成一个Websocket服务端线程 +/// * ⚠️此处要求**manager.config.websocket**必须非空,否则会直接panic +/// * 🚩此处手动生成Websocket服务端并启动:提升其「待发消息缓冲区」容量到24576 +/// * ❗【2024-04-09 01:20:57】问题缘起:服务端在「突然收到大量消息需要重发」时,可能会直接阻塞线程 +/// * 📌【2024-04-09 01:21:37】现在通过配置「最大连接数」与「队列大小」以**暂时缓解**此问题 +/// * 🔗参考: +/// * 🔗GitHub issue: +pub fn spawn_ws_server(manager: &mut RuntimeManager) -> Result>> +where + R: VmRuntime + Send + Sync, +{ + // 提取并合并地址 + let LaunchConfigWebsocket { host, port } = manager + .config + .websocket + .as_ref() + .expect("尝试在无配置时启动Websocket服务器"); + let address = to_address(host, *port); + + // 获取服务端「处理者工厂」 + // * 🚩拷贝[`Arc`] + let server = WSServer { + runtime: manager.runtime.clone(), + output_cache: manager.output_cache.clone(), + config: manager.config.clone(), + }; + + // 生成定制版的Websocket服务端 + // * 🎯获取生成的[`WebSocket`](服务端)对象,调用[`WebSocket::boardcaster`]方法快速广播 + // * ❌【2024-04-08 23:23:08】无法独立为单独的函数:此中NAVM运行时「R」的生命周期问题(难以参与推导) + let (handle, sender) = { + let factory = server; + let address = address.clone(); + let ws_setting = ws::Settings { + // * 📝使用`ws::Builder`结合`ws::Settings`生成配置 + // * ✅在配置中调节「队列大小」以扩宽「连续消息接收限制」 + // * 默认:100(最大连接)×5(最长队列)→500条后阻塞 + // * 🚩【2024-04-09 01:03:52】现在调整成「最多32个连接,每个连接最多768条消息」 + // * ⚠️仍然会在24576条消息后产生阻塞——但相比原先500条,情况少很多 + max_connections: 0x20, + queue_size: 0x300, + ..Default::default() + }; + let server = ws::Builder::new() + .with_settings(ws_setting) + .build(factory)?; + let sender = server.broadcaster(); + let handle = thread::spawn(move || { + server.listen(address)?; + // ! ❌此处不能缩并:必须转换为`anyhow::Error` + Ok(()) + }); + (handle, sender) + }; + println_cli!([Info] "Websocket服务器已在 {:?} 启动", address); + + // 向(服务端自身)「输出缓存」添加侦听器 + if_let_err_eprintln_cli! { + // ! 此处需要可变的`manager` + register_listener(&mut manager.output_cache, sender) + => e => [Error] "无法为服务端注册侦听器:{e}" + } + + // 返回线程句柄 + Ok(handle) +} + +/// 一个Websocket连接 +/// * 🎯处理单个Websocket连接 +#[derive(Debug)] +pub struct Connection +where + R: VmRuntime + Send + Sync, +{ + /// 所涉及的运行时 + pub(crate) runtime: ArcMutex, + + /// 所涉及的运行时配置 + pub(crate) config: Arc, + + /// 所涉及的运行时 + pub(crate) output_cache: ArcMutex, + // /// 连接(服务端这方的)发送者 + // /// * 🚩【2024-04-03 19:44:58】现在不再需要 + // pub(crate) sender: Sender, + /// 连接id + pub(crate) id: u32, +} + +impl Handler for Connection +where + R: VmRuntime + Send + Sync + 'static, +{ + fn on_shutdown(&mut self) { + println_cli!([Info] "Websocket连接已关停") + } + + fn on_open(&mut self, shake: ws::Handshake) -> ws::Result<()> { + if let Some(addr) = shake.remote_addr()? { + println_cli!([Info] "Websocket连接已打开:{addr}") + } + Ok(()) + } + + fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> { + println_cli!([Debug] "Websocket收到消息:{msg}"); + // 获取所需的参数信息 | 在此时独占锁 + let runtime = &mut *try_or_return_err!(self.runtime.lock(); poison => "在Websocket连接中获取运行时失败:{poison}"); + let config = &self.config; + let output_cache = &mut *try_or_return_err!(self.output_cache.lock(); err => "在Websocket连接中获取输出缓存失败:{err}"); + + // 输入信息,并监控缓存的新输出 + // * 📝【2024-04-08 22:10:17】现在查明「Websocket线程阻塞」问题在Websocket「回传发送者」的`send`调用中 + if_let_err_eprintln_cli! { + RuntimeManager::input_line_to_vm( + runtime, + &msg.to_string(), + config, + output_cache, + &config.config_path + ) + => err => [Error] "在Websocket连接中输入「{msg}」时发生错误:{err}" + } + + Ok(()) + } + + fn on_close(&mut self, code: ws::CloseCode, reason: &str) { + println_cli!([Info] "Websocket连接关闭(退出码:{code:?};原因:「{reason}」)"); + } + + fn on_error(&mut self, err: ws::Error) { + // Ignore connection reset errors by default, but allow library clients to see them by + // overriding this method if they want + if let ws::ErrorKind::Io(ref err) = err.kind { + if let Some(104) = err.raw_os_error() { + return; + } + } + + println_cli!([Error] "连接发生错误:{err:?}"); + } + + fn on_timeout(&mut self, event: ws::util::Token) -> ws::Result<()> { + println_cli!([Warn] "连接超时:{:?}", event); + Ok(()) + } + + fn on_new_timeout(&mut self, _: ws::util::Token, _: ws::util::Timeout) -> ws::Result<()> { + // default implementation discards the timeout handle + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct WSServer +where + R: VmRuntime, +{ + /// 所涉及的虚拟机运行时 + pub(crate) runtime: ArcMutex, + + /// 所涉及的虚拟机配置 + pub(crate) config: Arc, + + /// 所涉及的输出缓存 + pub(crate) output_cache: ArcMutex, +} + +/// 向所有「回传发送者」广播NAVM输出 +/// * 🎯回传所侦听到的NAVM输出 +pub(crate) fn broadcast_to_senders( + // senders: &mut ArcMutex, + broadcaster: &mut Sender, + output: &Output, +) -> Result<()> { + let output_str = format_output_message(output); + + // println_cli!([Debug] "🏗️正在向接收者回传消息:\n{output_str}"); + // * 通过一个`broadcaster`直接向所有连接广播消息 + if_let_err_eprintln_cli! { + broadcaster.send(output_str.to_string()) + => e => [Error] "广播消息失败:{e}" + }; + + // println_cli!([Debug] "✅向接收者回传消息完成:\n{output_str}"); + + Ok(()) +} + +/// 向「输出缓存」注册侦听器 +/// * 🎯绑定侦听器到输出缓存中,以便在「侦听器有输出」时广播 +/// * 🎯现在只有「输出缓存」会留存:因为`WebSocket.broadcaster`只在服务器启动后创建 +pub(crate) fn register_listener( + output_cache: &mut ArcMutex, + mut broadcaster: Sender, +) -> Result<()> { + // 尝试解包「输出缓存」 + let output_cache = &mut *output_cache.lock().map_err(error_anyhow)?; + output_cache.output_handlers.add_handler(move |output| { + // 广播 + if_let_err_eprintln_cli! { + broadcast_to_senders(&mut broadcaster, &output) + => e => [Error] "Websocket回传广播到发送者时出现错误:{:?}", e + } + // 返回 + Some(output) + }); + Ok(()) +} + +impl Factory for WSServer +where + R: VmRuntime + Send + Sync + 'static, +{ + type Handler = Connection; + + fn connection_made(&mut self, sender: Sender) -> Connection { + let id = sender.connection_id(); + println_cli!([Info] "Websocket连接已在id {id} 处建立"); + // 返回连接 + Connection { + runtime: self.runtime.clone(), + config: self.config.clone(), + output_cache: self.output_cache.clone(), + id, + } + } + + fn on_shutdown(&mut self) { + // 打印消息 + println_cli!([Info] "Websocket服务器已关停") + } + + fn connection_lost(&mut self, handler: Self::Handler) { + eprintln_cli!([Error] "与id为 {} 的客户端断开连接!", handler.id); + } +} + +// TODO: ❓【2024-04-07 12:42:51】单元测试不好做:网络连接难以被模拟 diff --git a/src/bin/cin_launcher/main.rs b/src/bin/cin_launcher/main.rs new file mode 100644 index 0000000..d45516c --- /dev/null +++ b/src/bin/cin_launcher/main.rs @@ -0,0 +1,151 @@ +//! 一个一站式启动各CIN的启动器 +//! * 🎯方便启动、管理各「作为NAVM运行时的CIN」的聚合终端 +//! * 📌用于集成原先「BabelNAR」「BabelNAR_Implements」两个库 +//! * ✨自动根据可执行文件、配置文件、用户输入猜测CIN类型(字符串匹配) +//! * ✨自动查找(可能)可用的CIN可执行文件(文件搜索) +//! * 📌可根据「匹配度」排名 +//! * ✨自动启动并管理CIN +//! * 📌可保存/加载「常用CIN」配置 +//! +//! * 🚩目前用于敏捷原型开发 +#![allow(unused)] + +use anyhow::Result; +use babel_nar::{ + cin_implements::{ona::ONA, opennars::OpenNARS, pynars::PyNARS}, + eprintln_cli, println_cli, + runtimes::CommandVmRuntime, + tests::cin_paths::{ONA, OPENNARS, PYNARS_ROOT}, +}; +use nar_dev_utils::*; +use navm::{ + cmd::Cmd, + output::Output, + vm::{VmLauncher, VmRuntime}, +}; +use std::{fmt::Debug, io::stdin}; + +const TEST_PATH_OPENNARS: &str = OPENNARS; +const TEST_PATH_ONA: &str = ONA; +const TEST_PATH_PYNARS: (&str, &str) = (PYNARS_ROOT, "pynars.ConsolePlus"); + +/// 启动并获取NARS +/// * 🚩【2024-03-27 18:55:07】目前就返回一个测试用的运行时 +/// * 🎯敏捷开发用 +fn get_nars() -> impl VmLauncher { + // OpenNARS::new(TEST_PATH_OPENNARS) + PyNARS::new(TEST_PATH_PYNARS.0, TEST_PATH_PYNARS.1) + // ONA::new(TEST_PATH_ONA) +} + +fn put_cmd_to_nars(nars: &mut impl VmRuntime, cmd: Cmd) -> Result<()> { + nars.input_cmd(cmd) +} + +/// 主函数 +/// * 🚩【2024-04-02 20:58:07】现在更完整的支持交给BabelNAR CLI,此文件用于敏捷开发 +fn main() { + // 不断开始🔥 + loop { + start(); + } +} + +/// 开始 +fn start() { + let nars = get_nars().launch().expect("无法启动虚拟机"); + shell(nars); +} + +/// 打印错误 +fn println_error(e: &impl Debug) { + println!("{e:?}"); +} + +/// 交互式命令行 +fn shell(mut nars: CommandVmRuntime) { + let stdin = stdin(); + let mut input = String::new(); + let mut line; + + 'main: while stdin.read_line(&mut input).is_ok() { + // 一行 + line = input.as_str(); + + // 非空⇒解析出NAVM指令,作为输入执行 + if !line.trim().is_empty() { + if let Ok(cmd) = Cmd::parse(line) + .inspect_err(|e| eprintln_cli!([Error] "解析NAVM指令时发生错误:{e}")) + { + let _ = put_cmd_to_nars(&mut nars, cmd) + .inspect_err(|e| eprintln_cli!([Error] "执行NAVM指令时发生错误:{e}")); + } + } + + // 尝试拉取所有NAVM运行时输出 + while let Ok(Some(output)) = nars + .try_fetch_output() + .inspect_err(|e| eprintln_cli!([Error] "拉取NAVM运行时输出时发生错误:{e}")) + { + println!("{output:?}"); + if let Output::TERMINATED { description } = output { + println_cli!([Info] "NAVM已终止运行:{description}"); + nars.terminate(); + break 'main; // ! 这个告诉Rust编译器,循环必将在此结束 + } + } + + // 清空缓冲区 + input.clear(); + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use babel_nar::cin_implements::cxin_js::CXinJS; + use babel_nar::cin_implements::pynars::PyNARS; + use narsese::conversion::string::impl_lexical::format_instances::FORMAT_ASCII; + use navm::cmd::Cmd; + use navm::vm::VmLauncher; + + fn test_set(mut nars: impl VmRuntime, test_set: Vec) { + for cmd in test_set { + nars.input_cmd(cmd); + } + std::thread::sleep(std::time::Duration::from_secs(5)); + while let Ok(Some(o)) = nars.try_fetch_output() { + println!("{}", format_navm_output(o)); + } + } + + fn format_navm_output(o: Output) -> String { + // 以「有无Narsese」作区分 + match o.get_narsese() { + // * 🚩有Narsese⇒包含Narsese + Some(nse) => format!( + "[{}] (( {} )) {}", + o.type_name(), + FORMAT_ASCII.format_narsese(nse), + o.raw_content() + ), + // * 🚩无⇒仅包含内容 + None => format!("[{}] {}", o.type_name(), o.raw_content()), + } + } + + fn parse_cmd_lines(narsese: impl AsRef) -> Vec { + let narsese = narsese.as_ref(); + let mut result = vec![]; + + for line in narsese.split('\n').map(str::trim).filter(|s| !s.is_empty()) { + match Cmd::parse(line) { + Ok(cmd) => result.push(cmd), + Err(e) => println!("{e}"), + } + } + + result + } +} diff --git a/src/bin/ws_server_test/main.rs b/src/bin/ws_server_test/main.rs new file mode 100644 index 0000000..52c0ccd --- /dev/null +++ b/src/bin/ws_server_test/main.rs @@ -0,0 +1,167 @@ +use std::{ + cell::RefCell, + thread::{self, sleep}, + time::Duration, +}; +extern crate ws; + +fn main() { + /// 单次训练过程 + fn train(sender: ws::Sender) -> impl Fn(ws::Message) -> Result<(), ws::Error> { + // 尝试注册操作 + let _ = sender.send("REG left".to_string()); + let _ = sender.send("REG right".to_string()); + + // 预先经验 + for _ in 0..5 { + // 背景事件 + let _ = sender.send("NSE b>. :|:".to_string()); + // 自身操作 + let _ = sender.send("NSE <(*, {SELF}) --> ^left>. :|:".to_string()); + let _ = sender.send("NSE <(*, {SELF}) --> ^right>. :|:".to_string()); + // 一定间隔 + let _ = sender.send("CYC 10".to_string()); + // 自身状态 + let _ = sender.send("NSE <{SELF} --> [good]>. :|:".to_string()); + } + // 再间隔一段时间,开始训练 + let _ = sender.send("CYC 100".to_string()); + + let sender2 = sender.clone(); + // 生成一个不断发送消息的线程 + thread::spawn(move || loop { + let _ = sender2.send("NSE b>. :|:".to_string()); + let _ = sender2.send("CYC 10".to_string()); + let _ = sender2.send("NSE <{SELF} --> [good]>! :|:".to_string()); + // let _ = sender2.send("NSE <{SELF} --> [good]>>? :|:".to_string()); + thread::sleep(Duration::from_secs_f64(0.03)); + }); + + // * 📝Websocket Handler不能可变,就用RefCell实现内部可变性 + let right_side = RefCell::new(false); + let num_good = RefCell::new(0_usize); + let output_steps = RefCell::new(0_usize); + let minimum_fitness_period = RefCell::new(usize::MAX); + const MAX_GOOD: usize = 20; + move |msg: ws::Message| { + // println!("Got message: {}", msg); + let msg = msg.to_string(); + // 记录步数 + let output_steps = &mut *output_steps.borrow_mut(); + *output_steps += 1; + // 操作 + if msg.contains("EXE") { + // 左右操作状态 + let left = msg.contains(r#"["left","{SELF}"]"#); + let right = msg.contains(r#"["right","{SELF}"]"#); + if !left && !right { + return Ok(()); + } + let minimum_fitness_period = &mut *minimum_fitness_period.borrow_mut(); + // * 🔬可以尝试「左右颠倒」以观察NARS的适应能力 + let num_good = &mut *num_good.borrow_mut(); + let right_side = &mut *right_side.borrow_mut(); + let lr = if *right_side { "right" } else { "left" }; + // 奖励 + if left && !*right_side || right && *right_side { + let _ = sender.send("NSE <{SELF} --> [good]>. :|: %1.0; 0.5%".to_string()); + println!("good\t{lr}\tfor {num_good}!\t{minimum_fitness_period}"); + *num_good += 1; + // 改变模式 + if *num_good > MAX_GOOD { + let b = *right_side; + *right_side = !b; + *num_good = 0; + // 一个轮回⇒以「轮回数」记录「适应性」 + if b { + *minimum_fitness_period = *minimum_fitness_period.min(output_steps); + *output_steps = 0; + } + } + } + // 惩罚 + else { + let _ = sender.send("NSE <{SELF} --> [good]>. :|: %0.0; 0.5%".to_string()); + println!("bad\t{lr}\tfor {num_good}!\t{minimum_fitness_period}"); + } + } + // out.close(CloseCode::Normal) + Ok(()) + } + } + + // 循环 + loop { + let _ = ws::connect("ws://127.0.0.1:8765", train); + // 连接失败则延迟等待 + sleep(Duration::from_secs(1)); + } +} + +#[test] +fn test_overwhelming_nse() { + loop { + let _ = ws::connect("ws://127.0.0.1:8765", |sender| { + // 生成一个不断发送消息的线程 + thread::spawn(move || loop { + let _ = sender.send("NSE A.".to_string()); + let _ = sender.send("NSE B.".to_string()); + let _ = sender.send("NSE A?".to_string()); + }); + + // handle received message + move |msg| { + println!("Got message: {}", msg); + // out.close(CloseCode::Normal) + Ok(()) + } + }); + sleep(Duration::from_secs(1)); + } +} + +/// 压力测试 +/// * 🔗GitHub issue: +#[test] +fn main_server() { + // A client that sends tons of messages to the server + thread::spawn(move || { + let _ = ws::connect("ws://127.0.0.1:3012", |sender| { + let mut num_send = 0_usize; + // Generate a thread that constantly sends messages for testing + thread::spawn(move || loop { + num_send += 1; + // The content is just for example, the actual situation has more variety + let _ = sender.send(format!("overwhelming message #{num_send}!")); + }); + + // Handle nothing + move |_| Ok(()) + }); + }); + + // A server that echoes messages back to the client + ws::Builder::new() + .with_settings(ws::Settings { + max_connections: 0x40, + // * ↓Change this setting to `usize::MAX` actually can't be allowed: It might run out of memory + queue_size: 0x300, + // ! ↓Even enabled it, it still can't stop the blocking + panic_on_queue: true, + ..Default::default() + }) + .build(|sender: ws::Sender| { + // handle received message + move |msg| { + println!("Got message: {}", msg); + println!("from {sender:?}"); + // ! It will block on ↓this line when the `SyncSender` is full + let _ = sender.send(msg); + // * ↑If uncomment this line of code, the server will not be blocked + Ok(()) + } + }) + .unwrap() + .listen("127.0.0.1:3012") + .unwrap(); +} diff --git a/src/cin_implements/common/exe.rs b/src/cin_implements/common/exe.rs new file mode 100644 index 0000000..9cbb427 --- /dev/null +++ b/src/cin_implements/common/exe.rs @@ -0,0 +1,47 @@ +//! 可执行文件(exe)启动器 +//! * 🎯适用于任何直接从可执行文件(可能带参数)启动的CIN +//! * 📄ONA +//! * 📄NARS-Python +//! * 🚩【2024-03-28 10:00:00】暂且只需提供[`Command`]生成函数 +//! * ❗没必要使用新的数据结构 + +use crate::runtimes::{CommandVm, IoTranslators}; +use nar_dev_utils::manipulate; +use std::{ffi::OsStr, path::Path, process::Command}; + +/// 根据配置统一生成[`Command`]对象 +/// * 📌「配置」的定义 +/// * exe路径(可能不直接是可执行文件的路径) +/// * 当前文件夹(设置命令启动时的工作目录) +/// * 命令行参数(可以为空) +pub fn generate_command( + exe_path: impl AsRef, + current_dir: Option>, + args: impl IntoIterator, +) -> Command +where + S: AsRef, +{ + // 构造指令 + let mut command = Command::new(exe_path.as_ref()); + + // 设置路径 + if let Some(current_dir) = current_dir { + command.current_dir(current_dir); + } + + // 设置附加参数 + // * 📝这里的`args`、`arg都返回的可变借用。。 + command.args(args); + + // 返回 + command +} + +/// 根据「输入输出转译器」构建[`CommandVm`]对象 +pub fn generate_command_vm(command: Command, translators: impl Into) -> CommandVm { + manipulate!( + CommandVm::from(command) + => .translators(translators) + ) +} diff --git a/src/cin_implements/common/java.rs b/src/cin_implements/common/java.rs new file mode 100644 index 0000000..b8a1ff4 --- /dev/null +++ b/src/cin_implements/common/java.rs @@ -0,0 +1,79 @@ +//! Java jar启动器 +//! * 🎯通用于任何基于jar文件的CIN,不仅仅是OpenNARS +//! * 🎯封装「NAVM运行时启动过程」中有关「Java启动环境配置」的部分 +//! * 🚩从jar文件启动NARS +//! * 🚩【2024-03-27 15:31:02】取消「初始音量」的特化配置,将其变成一个「命令行参数生成器」而非独立的「启动器」 + +use crate::runtimes::CommandGenerator; +use std::{path::PathBuf, process::Command}; + +/// 启动Java运行时的命令 +const COMMAND_JAVA: &str = "java"; + +/// jar文件启动的默认指令参数 +/// * 🎯默认预置指令:`java -Xmx1024m -jar [.jar文件路径]` +/// * 🚩实际上"-Xmx1024m"非必要 +const COMMAND_ARGS_JAVA: [&str; 1] = ["-jar"]; + +/// Java运行时启动配置参数:初始堆大小/最小堆大小 +#[inline(always)] +fn command_arg_xms(size: usize) -> String { + format!("-Xms{size}m") +} + +/// Java运行时启动配置参数:最大堆大小 +#[inline(always)] +fn command_arg_xmx(size: usize) -> String { + format!("-Xmx{size}m") +} + +/// Java jar启动器 +/// * 🎯以Java运行时专有形式启动虚拟机运行时 +/// * 📄基于jar文件启动OpenNARS Shell +/// * 默认预置指令:`java -jar [.jar文件路径] [..其它jar启动参数]` +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CommandGeneratorJava { + /// jar文件路径 + /// * 📌必须有 + jar_path: PathBuf, + /// Java运行时的初始堆大小/最小堆大小 + /// * 📄在Java指令中的参数:`-Xms[数值]m` + /// * 🚩可能没有:此时不会附加参数 + min_heap_size: Option, + /// Java运行时的最大堆大小 + /// * 📄在Java指令中的参数:`-Xmx[数值]m` + /// * 🚩可能没有:此时不会附加参数 + max_heap_size: Option, +} + +impl CommandGeneratorJava { + /// 构造函数 + pub fn new(jar_path: impl Into) -> Self { + Self { + // 转换为路径 + jar_path: jar_path.into(), + // 其它全是`None` + ..Default::default() + } + } +} + +/// 根据自身生成命令 +impl CommandGenerator for CommandGeneratorJava { + fn generate_command(&self) -> Command { + // 构造指令 + let mut command_java = Command::new(COMMAND_JAVA); + // * 📝这里的`args`、`arg都返回的可变借用。。 + command_java.args(COMMAND_ARGS_JAVA).arg(&self.jar_path); + + // 选择性添加参数 + if let Some(size) = self.min_heap_size { + command_java.arg(command_arg_xms(size)); + } + if let Some(size) = self.max_heap_size { + command_java.arg(command_arg_xmx(size)); + } + + command_java + } +} diff --git a/src/cin_implements/common/julia.rs b/src/cin_implements/common/julia.rs new file mode 100644 index 0000000..c2e6d7a --- /dev/null +++ b/src/cin_implements/common/julia.rs @@ -0,0 +1,48 @@ +//! Julia 模块启动器 +//! * 🎯通用于任何基于Julia源码的CIN,不仅仅是OpenJunars +//! * 🎯封装「NAVM运行时启动过程」中有关「Julia启动环境配置」的部分 +//! * 🚩从Julia脚本(`.jl`)启动NARS + +use crate::runtimes::CommandGenerator; +use std::{path::PathBuf, process::Command}; + +/// 启动Julia运行时的命令 +const COMMAND_JULIA: &str = "julia"; + +/// ! Julia启动脚本无需附加参数 + +/// OpenJunars运行时启动器 +/// * 🎯配置OpenJunars专有的东西 +/// * 🎯以Julia模块形式启动OpenJunars +/// * 📌没有内置的「音量」配置 +/// * 🚩【2024-03-25 08:55:07】基于Julia模块文件启动OpenJunars +/// * 默认预置指令:``julia [`.jl`脚本文件路径]`` +/// * 🚩【2024-03-25 09:15:07】删去[`Default`]派生:因为可能导致无效的路径 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CommandGeneratorJulia { + /// Julia脚本文件路径 + jl_path: PathBuf, +} + +impl CommandGeneratorJulia { + pub fn new(jl_path: impl Into) -> Self { + Self { + // 转换为路径 + jl_path: jl_path.into(), + } + } +} + +/// 转换为Julia启动命令 +impl CommandGenerator for CommandGeneratorJulia { + fn generate_command(&self) -> Command { + // 构造指令 + let mut command_julia = Command::new(COMMAND_JULIA); + + // 填入路径参数 + command_julia.arg(&self.jl_path); + + // 返回 + command_julia + } +} diff --git a/src/cin_implements/common/mod.rs b/src/cin_implements/common/mod.rs new file mode 100644 index 0000000..7e95a91 --- /dev/null +++ b/src/cin_implements/common/mod.rs @@ -0,0 +1,16 @@ +//! 所有对接CIN实现的共用模块 +//! * 🎯共通的exe、Java、Python、Julia 和 Node.js + +// 平铺导出元素 +util::pub_mod_and_pub_use! { + // exe + exe + // Java + java + // Python + python + // Julia + julia + // Node.js + node_js +} diff --git a/src/cin_implements/common/node_js.rs b/src/cin_implements/common/node_js.rs new file mode 100644 index 0000000..3f1a7df --- /dev/null +++ b/src/cin_implements/common/node_js.rs @@ -0,0 +1,61 @@ +//! Node.js 模块启动器 +//! * 🎯通用于任何基于Node.js脚本的CIN +//! * 🎯封装「NAVM运行时启动过程」中有关「Node.js启动环境配置」的部分 +//! * 🚩从Node.js脚本(`.js`)启动NARS + +use crate::runtimes::CommandGenerator; +use std::{path::PathBuf, process::Command}; + +/// 启动Node.js运行时的命令 +const COMMAND_NODE: &str = "node"; + +/// ! Node.js启动脚本无需附加参数 + +/// CXinNARS运行时启动器 +/// * 🎯配置CXinNARS专有的东西 +/// * 🎯以Node.js模块形式启动CXinNARS +/// * 📌没有内置的「音量」配置 +/// * 🚩【2024-03-25 08:55:07】基于Node.js模块文件启动CXinNARS +/// * 默认预置指令:``node [`.js`脚本文件路径]`` +/// * 🚩【2024-03-25 09:15:07】删去[`Default`]派生:因为可能导致无效的路径 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CommandGeneratorNodeJS { + /// Node.js脚本文件路径 + js_path: PathBuf, + /// 附加的命令行参数 + /// * 📄CXinNARS中用到了`shell`参数 + extra_args: Vec, +} + +impl CommandGeneratorNodeJS { + pub fn new( + js_path: impl Into, + extra_args: impl IntoIterator>, + ) -> Self { + Self { + // 转换为路径 + js_path: js_path.into(), + extra_args: extra_args + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect::>(), + } + } +} + +/// 转换为Node.js启动命令 +impl CommandGenerator for CommandGeneratorNodeJS { + fn generate_command(&self) -> Command { + // 构造指令 + let mut command_nodejs = Command::new(COMMAND_NODE); + + // 填入路径参数 + command_nodejs.arg(&self.js_path); + + // 填入其它参数 + command_nodejs.args(&self.extra_args); + + // 返回 + command_nodejs + } +} diff --git a/src/cin_implements/common/python.rs b/src/cin_implements/common/python.rs new file mode 100644 index 0000000..647ea4d --- /dev/null +++ b/src/cin_implements/common/python.rs @@ -0,0 +1,59 @@ +//! Python模块 启动器 +//! * 🎯通用于任何基于Python源码的CIN,不仅仅是PyNARS +//! * 🎯封装「NAVM运行时启动过程」中有关「Python启动环境配置」的部分 +//! * 🚩从Python模块(`.py`脚本)启动NARS + +use crate::runtimes::CommandGenerator; +use std::{path::PathBuf, process::Command}; + +/// 启动Python运行时的命令 +const COMMAND_PYTHON: &str = "python"; + +/// 启动Python模块的默认指令参数 +/// * 🎯默认预置指令:`python -m [当前工作目录下的Python模块]` +const COMMAND_ARGS_PYTHON: [&str; 1] = ["-m"]; + +/// Python启动命令生成器 +/// * 🎯以Python模块形式生成启动命令 +/// * 🚩【2024-03-25 08:55:07】基于Python模块文件启动NARS +/// * 默认预置指令:`python -m [Python模块根目录] [Python模块路径]` +/// * 🚩【2024-03-25 09:15:07】删去[`Default`]派生:因为可能导致无效的路径 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CommandGeneratorPython { + /// 根目录 + /// * 📄`root/home/dev/pynars` + root_path: PathBuf, + + /// 模块路径 + /// * 📌相对根目录而言 + /// * 📄`pynars.Console` + /// * 📄`root_path` + `pynars.Console` => `root_path/pynars/Console` + module_path: String, +} + +impl CommandGeneratorPython { + pub fn new(root_path: impl Into, module_path: &str) -> Self { + Self { + // 转换为路径 + root_path: root_path.into(), + // 转换为字符串 + module_path: module_path.to_string(), + } + } +} + +/// 启动到「命令行运行时」 +impl CommandGenerator for CommandGeneratorPython { + fn generate_command(&self) -> Command { + // 构造指令 + let mut command = Command::new(COMMAND_PYTHON); + command + // * 🚩设置指令工作目录 + // * 📝`python -m`无法自行指定所执行的工作目录,必须在`Command`中设置 + .current_dir(&self.root_path) // 以此设置当前工作目录 + .args(COMMAND_ARGS_PYTHON) + .arg(&self.module_path); + + command + } +} diff --git a/src/cin_implements/cxin_js/launcher.rs b/src/cin_implements/cxin_js/launcher.rs new file mode 100644 index 0000000..2957f49 --- /dev/null +++ b/src/cin_implements/cxin_js/launcher.rs @@ -0,0 +1,57 @@ +//! CXinNARS.js运行时的启动器 +//! * 🎯允许CXinNARS.js对原先运行时特别配置功能,同时也支持为CXinNARS.js定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 + +use super::{input_translate, output_translate}; +use crate::{ + cin_implements::common::{generate_command_vm, CommandGeneratorNodeJS}, + runtimes::{CommandGenerator, CommandVmRuntime}, +}; +use anyhow::Result; +use navm::vm::VmLauncher; +use std::path::PathBuf; +use util::pipe; + +/// CXinNARS.js Shell启动的默认指令参数 +/// * 🎯默认预置指令:`[.js文件路径] shell` +const COMMAND_ARGS_CXIN_NARS: [&str; 1] = ["shell"]; + +/// CXinNARS.js运行时启动器 +/// * 🎯配置CXinNARS.js专有的东西 +/// * 🚩基于js文件启动CXinNARS.js Shell +/// * 默认预置指令:`[.js文件路径] shell` +/// * 🚩【2024-03-25 08:51:30】目前保留原有缩写的大小写风格,与OpenNARS、PyNARS一致 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CXinJS { + /// Node.js命令生成器 + command_generator: CommandGeneratorNodeJS, +} + +// ! 🚩【2024-03-25 09:37:22】目前暂时不提取至「VmExe」:预置的`shell`参数需要被处理 +impl CXinJS { + /// 构造函数 + pub fn new(js_path: impl Into) -> Self { + Self { + command_generator: CommandGeneratorNodeJS::new(js_path, COMMAND_ARGS_CXIN_NARS), + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for CXinJS { + fn launch(self) -> Result { + // 构造并启动虚拟机 + pipe! { + self.command_generator + // 构造指令 | 预置的指令参数 + => .generate_command() + // * 🚩固定的「输入输出转译器」 + => generate_command_vm(_, (input_translate, output_translate)) + // 🔥启动 + => .launch() + } + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/cxin_js/mod.rs b/src/cin_implements/cxin_js/mod.rs new file mode 100644 index 0000000..6274245 --- /dev/null +++ b/src/cin_implements/cxin_js/mod.rs @@ -0,0 +1,79 @@ +//! 「非公理虚拟机」的ONA运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 + +// 转译器 +util::mod_and_pub_use! { + // 转译器 + translators + // 启动器 + launcher +} + +/// 单元测试 +#[cfg(test)] +mod tests { + #![allow(unused)] + use super::*; + use crate::runtimes::{ + tests::{await_fetch_until, input_cmd_and_await_contains, test_simple_answer}, + CommandVmRuntime, + }; + use narsese::lexical_nse_task as nse_task; + use navm::{ + cmd::Cmd, + vm::{VmLauncher, VmRuntime}, + }; + + /// 测试用路径 + const CXIN_NARS_JS_PATH: &str = r"..\cxin-nars-py-to-ts\src\cxin-nars-shell.js"; + + /// 通用/启动VM + /// * 🚩【2024-04-02 04:16:04】测试用代码无需返回[`Result`] + fn launch_vm() -> CommandVmRuntime { + // 从别的地方获取js路径 + let js_path = CXIN_NARS_JS_PATH; + // 一行代码启动CxinNARS + CXinJS::new(js_path).launch().expect("无法启动虚拟机") + } + + /// 测试/专用 + #[test] + fn test() { + let vm = launch_vm(); + // 进入专用测试 + _test_cxin_js(vm) + } + + /// 专用测试/CXinNARS.js + pub fn _test_cxin_js(mut vm: CommandVmRuntime) { + // 专有闭包 | ⚠️无法再提取出另一个闭包:重复借用问题 + let mut input_cmd_and_await = + |cmd, contains| input_cmd_and_await_contains(&mut vm, cmd, contains); + // input_cmd_and_await(Cmd::VOL(0), ""); + input_cmd_and_await(Cmd::NSE(nse_task!( B>.)), "B>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>.)), "C>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>?)), "C>?"); + input_cmd_and_await(Cmd::CYC(20), ""); // * CYC无需自动等待 + + // 等待回答(字符串) + await_fetch_until(&mut vm, |_o, raw_content| { + // ! ❌【2024-03-28 09:51:48】目前CXinNARS能输出导出结论,但无法输出ANSWER + /* matches!(_o, Output::ANSWER { .. }) && */ + raw_content.contains("C>.") + }); + + // 终止虚拟机 + vm.terminate().expect("无法终止虚拟机"); + println!("Virtual machine terminated..."); + } + + /// 测试/通用 | 基于Narsese + #[test] + fn test_universal() { + // // 启动OpenNARS虚拟机 + // let vm = launch_vm(); + // // 使用通用测试逻辑 + // test_simple_answer(vm) + } +} diff --git a/src/cin_implements/cxin_js/translators.rs b/src/cin_implements/cxin_js/translators.rs new file mode 100644 index 0000000..3185a70 --- /dev/null +++ b/src/cin_implements/cxin_js/translators.rs @@ -0,0 +1,161 @@ +//! CXinNARS.js在「命令行运行时」的转译器 +//! * 🎯维护与CXinNARS.js Shell的交互 +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! ## 输出样例 +//! +//! * `Input: <<(* x) --> ^left> ==> A>. Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000` +//! * `Derived: <<(* x) --> ^left> ==> good>>. Priority=0.245189 Truth: frequency=1.000000, confidence=0.810000` +//! * `Answer: C>. creationTime=2 Truth: frequency=1.000000, confidence=0.447514` +//! * `Answer: None.` +//! * `^deactivate executed with args` +//! * `^left executed with args (* {SELF})` +//! * `^left executed with args ({SELF} * x)` +//! * `decision expectation=0.616961 implication: <((<{SELF} --> [left_blocked]> &/ ^say) &/ <(* {SELF}) --> ^left>) =/> <{SELF} --> [SAFE]>>. Truth: frequency=0.978072 confidence=0.394669 dt=1.000000 precondition: <{SELF} --> [left_blocked]>. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=50` + +use crate::runtimes::TranslateError; +use anyhow::Result; +use narsese::{ + conversion::string::impl_lexical::{format_instances::FORMAT_ASCII, structs::ParseResult}, + lexical::Narsese, +}; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; +use regex::Regex; +use util::{if_return, pipe}; + +/// CXinNARS.js的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「CXinNARS.js Shell输入」 +/// * 📝[`IoProcess`]会自动将输入追加上换行符 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + Cmd::CYC(n) => n.to_string(), + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // * 📌【2024-03-24 22:57:18】基本足够支持 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// CXinNARS.js的「输出转译」函数 +/// * 🎯用于将CXinNARS.js Shell的输出(字符串)转译为「NAVM输出」 +/// * 🚩直接根据选取的「头部」进行匹配 +pub fn output_translate(content_raw: String) -> Result { + // 特别处理:终止信号 + // * 📄"node:internal/modules/cjs/loader:1080\n throw err" + // * ❌【2024-03-28 09:00:23】似乎不可行:打开时的错误无法被捕捉 + if_return! { + // 模块未找到 + content_raw.contains("Error: Cannot find module") => Ok(Output::TERMINATED { description: content_raw }) + } + // 匹配「输出类型」的正则表达式 + // * ✅此处的「尾部」不会有前导空格(若识别出了「头部」) + let line_r = Regex::new(r"\[(\w+)\]\s*(.*)").unwrap(); + let head; + let tail; + if let Some(captures) = line_r.captures(&content_raw) { + head = captures[1].to_lowercase(); + tail = captures[2].to_owned(); + } else { + head = String::new(); + tail = content_raw.clone(); + } + // 根据「头部」生成输出 + let output = match head.as_str() { + "answer" => Output::ANSWER { + // 先提取其中的Narsese + narsese: segment_narsese(&head, &tail), + // 然后传入整个内容 + content_raw, + }, + "in" => Output::IN { + // 先提取其中的Narsese + narsese: segment_narsese(&head, &tail), + // 然后传入整个内容 + content: tail, + }, + "out" => Output::OUT { + // 先提取其中的Narsese + narsese: segment_narsese(&head, &tail), + // 然后传入整个内容 + content_raw: tail, + }, + "comment" => Output::COMMENT { content: tail }, + "err" | "error" => Output::ERROR { description: tail }, + "exe" => Output::EXE { + operation: parse_operation(&tail), + content_raw: tail, + }, + // 若是连续的「头部」⇒识别为「未归类」类型 + _ if !content_raw.contains(char::is_whitespace) => Output::UNCLASSIFIED { + r#type: head, + // 尝试自动捕获Narsese + narsese: match try_segment_narsese(&tail) { + Some(Ok(narsese)) => Some(narsese), + _ => None, + }, + content: tail, + }, + // 其它 + _ => Output::OTHER { + content: content_raw, + }, + }; + // 返回 + Ok(output) +} + +/// (CXinNARS.js)从原始输出中解析操作 +pub fn parse_operation(content_raw: &str) -> Operation { + #![allow(unused_variables)] + todo!("CXinNARS.js暂不支持NAL-8") +} + +fn segment_narsese(head: &str, tail: &str) -> Option { + match try_segment_narsese(tail) { + Some(Ok(narsese)) => Some(narsese), + Some(Err(e)) => { + println!("【{head}】在解析Narsese时出现错误:{e}"); + None + } + None => { + println!("【{head}】未匹配到输出中的Narsese块"); + None + } + } +} + +/// 分割 & 解析Narsese +/// * 🎯提供解析CXinNARS中Narsese的方法 +/// * ❗不包含任何副作用(如打印) +/// * 🚩先通过正则表达式从模式`Narsese{{ 【Narsese内容】 }}【Narsese类型】`中分解出Narsese +/// * 🚩再通过标准ASCII解析器解析 +pub fn try_segment_narsese(input: &str) -> Option { + let re_narsese = Regex::new(r"Narsese\{\{ (.+) \}\}").unwrap(); + pipe!( + // 尝试从模式中提取Narsese + re_narsese.captures(input) + // 提取Narsese + => .map( + // 尝试解析Narsese + |captures| try_parse_narsese(&captures[1]) + ) + ) +} + +/// (尝试)从输出中解析出Narsese +/// * ❌【2024-03-27 22:01:18】目前引入[`anyhow::Error`]会出问题:不匹配/未满足的特征 +pub fn try_parse_narsese(narsese: &str) -> ParseResult { + // 提取并解析Narsese字符串 + FORMAT_ASCII.parse(narsese) +} diff --git a/src/cin_implements/mod.rs b/src/cin_implements/mod.rs new file mode 100644 index 0000000..29b9f6c --- /dev/null +++ b/src/cin_implements/mod.rs @@ -0,0 +1,46 @@ +//! 对各CIN实现「非公理虚拟机」模型 +//! * 🎯基于「NAVM指令/NAVM输出↔字符串」的转换 +//! +//! ! ⚠️关键问题:进程残留 +//! * ✅单元测试中利用`taskkill`初步解决 +//! * ❌【2024-03-25 13:36:30】集成测试`cargo t --all-features`中未能解决 +//! * ❗// ! ↑【少用乃至不用这条命令】 +//! +//! * 🚩【2024-04-09 20:47:04】基本完成对「复用」「性能」与「简洁」的兼顾 +//! * 📌复用:将OpenNARS、ONA抽象成「基于jar的启动逻辑」「基于exe的启动逻辑」等方式,以便后续重复使用 +//! * 📄case:目前ONA、NARS-Python都是基于exe的启动方式 +//! * 📌性能:避免过多的封装、粗暴复合导致的空间浪费 +//! * 📄case:「启动器套启动器」在尝试抽象出「exe启动器」时,因为「没法预先指定转译器」在「复用『设置转译器』函数」时 +//! * ❌不希望在「exe启动器」「jar启动器」中重复套【包含一长串函数闭包】 +//! * 📌简洁:代码简明易懂,方便调用方使用 +//! * 📄case:期望能有形如`ONA::new(path).launch()`的语法 +//! * 💭不希望出现「强行模拟」的情况,如`mod ONA {pub fn new(..) {..}}` +//! * ❌不希望因此再全小写/封装命名空间,如`impls::ona::new` +//! * ❓目前的问题:在Rust基于「特征」的组合式设计哲学下,如何进行兼顾三者的优秀设计 + +util::mods! { + // 共用代码 + pub common; + + // 原生 + pub native; + + // OpenNARS + pub opennars; + + // ONA + pub ona; + + // NARS-Python + pub nars_python; + + // PyNARS + pub pynars; + + // OpenJunars + pub openjunars; + + // CXinNARS.js + pub cxin_js; + +} diff --git a/src/cin_implements/nars_python/dialect.rs b/src/cin_implements/nars_python/dialect.rs new file mode 100644 index 0000000..7150131 --- /dev/null +++ b/src/cin_implements/nars_python/dialect.rs @@ -0,0 +1,36 @@ +//! 用于存储NARS-Python的方言格式 +//! * 🚩【2024-03-26 01:31:44】本质上就是陈述括弧改变了而已 + +use narsese::conversion::string::impl_lexical::{ + format_instances::create_format_ascii, NarseseFormat, +}; +use narsese::lexical::Narsese; + +#[cfg(feature = "lazy_static")] +lazy_static::lazy_static! { + /// NARS-Python的方言格式 + /// * 🚩仅在`lazy_static`启用时开启 + pub static ref FORMAT: NarseseFormat = create_format_nars_python(); +} + +pub fn create_format_nars_python() -> NarseseFormat { + let mut f = create_format_ascii(); + f.statement.brackets = ("(".into(), ")".into()); + f +} + +/// 获取NARS-Python的方言格式 +/// * 🚩使用`lazy_static`定义的静态常量,无需重复初始化 +/// * 🚩否则总是创建一个新的「Narsese格式」 +#[cfg(feature = "lazy_static")] +pub fn format_in_nars_python(narsese: &Narsese) -> String { + FORMAT.format_narsese(narsese) +} + +/// 获取NARS-Python的方言格式 +/// * 🚩否则总是创建一个新的「Narsese格式」 +#[cfg(not(feature = "lazy_static"))] +pub fn format_in_nars_python(narsese: &Narsese) -> String { + // 创建格式,并立即格式化Narsese + create_format_nars_python().format_narsese(narsese) +} diff --git a/src/cin_implements/nars_python/launcher.rs b/src/cin_implements/nars_python/launcher.rs new file mode 100644 index 0000000..a0d9370 --- /dev/null +++ b/src/cin_implements/nars_python/launcher.rs @@ -0,0 +1,52 @@ +//! NARS-Python运行时的启动器 +//! * 🎯允许NARS-Python对原先运行时特别配置功能,同时也支持为NARS-Python定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 + +use super::{input_translate, output_translate}; +use crate::runtimes::{CommandVm, CommandVmRuntime}; +use anyhow::Result; +use nar_dev_utils::manipulate; +use navm::vm::VmLauncher; +use std::path::PathBuf; + +// ! NARS-Python作为一个独立的`main.exe`,没有默认的启动参数 + +/// NARS-Python运行时启动器 +/// * 🎯配置NARS-Python专有的东西 +/// * 🚩基于exe文件启动NARS-Python exe +/// * 🚩【2024-03-25 08:51:30】目前保留原有缩写的大小写风格,与OpenNARS、PyNARS一致 +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NARSPython { + /// exe文件路径 + exe_path: PathBuf, +} + +// ! 🚩【2024-03-25 09:37:22】目前暂时不提取至「VmExe」:参考`impl_runtime`根目录说明 + +impl NARSPython { + /// 构造函数 + pub fn new(exe_path: impl Into) -> Self { + Self { + // 转换为路径 + exe_path: exe_path.into(), + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for NARSPython { + fn launch(self) -> Result { + // 构造指令,并启动虚拟机 + manipulate!( + CommandVm::new(self.exe_path) + // * 🚩固定的「输入输出转译器」 + => .input_translator(input_translate) + => .output_translator(output_translate) + ) + // 🔥启动 + .launch() + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/nars_python/mod.rs b/src/cin_implements/nars_python/mod.rs new file mode 100644 index 0000000..469211c --- /dev/null +++ b/src/cin_implements/nars_python/mod.rs @@ -0,0 +1,96 @@ +//! 「非公理虚拟机」的NARS-Python运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 + +// 转译器 +util::mod_and_pub_use! { + // 方言(Narsese格式) + dialect + // 转译器 + translators + // 启动器 + launcher +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::{runtimes::CommandVmRuntime, tests::cin_paths::NARS_PYTHON}; + use narsese::conversion::string::impl_lexical::shortcuts::*; + use navm::{ + cmd::Cmd, + vm::{VmLauncher, VmRuntime}, + }; + + #[test] + fn test() { + // 从别的地方获取exe路径 + let exe_path = NARS_PYTHON; + // 一行代码启动NARS-Python + let vm = NARSPython::new(exe_path).launch().expect("无法启动虚拟机"); + // 运行专有测试 + _test_nars_python(vm) + } + + /// 测试/NARS-Python + /// 【2024-03-27 18:29:42】最近一次输出(NARS-Python控制台): + /// + /// ```text + /// IN: SentenceID:0:ID (A --> B). %1.00;0.90% + /// IN: SentenceID:1:ID (B --> C). %1.00;0.90% + /// IN: SentenceID:2:ID (A --> C)? + /// OUT: SentenceID:3:ID (A --> C). %1.00;0.81% + /// ``` + /// + /// ! ❌仍然无法截获其输出 + pub(crate) fn _test_nars_python(mut vm: CommandVmRuntime) { + // 等待几秒钟,让exe的界面显示出来 + std::thread::sleep(std::time::Duration::from_secs(2)); + + vm.input_cmd(Cmd::NSE(nse_task!( B>.))) + .expect("无法输入NAVM指令"); + vm.input_cmd(Cmd::NSE(nse_task!( C>.))) + .expect("无法输入NAVM指令"); + vm.input_cmd(Cmd::NSE(nse_task!( C>?))) + .expect("无法输入NAVM指令"); + + std::thread::sleep(std::time::Duration::from_secs(4)); + + // 终止虚拟机运行时 + vm.terminate().expect("无法终止虚拟机"); + } + + /* // ! 【2024-03-26 01:44:27】NARS-Python输出崩溃的内容: + running 1 test + Started process: 65784 + Traceback (most recent call last): + File "main.py", line 122, in + File "main.py", line 118, in main + File "NARS.py", line 54, in run + File "NARS.py", line 63, in do_working_cycle + File "InputChannel.py", line 74, in process_pending_sentence + File "InputChannel.py", line 87, in process_sentence + File "NARS.py", line 247, in process_task + File "NARS.py", line 323, in process_question_task + File "NARS.py", line 491, in process_sentence_semantic_inference + File "NARSInferenceEngine.py", line 73, in do_semantic_inference_two_premise + AttributeError: 'NoneType' object has no attribute 'frequency' + [38676] Failed to execute script 'main' due to unhandled exception! + Fatal Python error: could not acquire lock for <_io.BufferedReader name=''> at interpreter shutdown, possibly due to daemon threads + Python runtime state: finalizing (tstate=00000213FB525D60) + + Thread 0x00017e0c (most recent call first): + File "InputChannel.py", line 25 in get_user_input + File "threading.py", line 870 in run + File "threading.py", line 932 in _bootstrap_inner + File "threading.py", line 890 in _bootstrap + + Current thread 0x00013918 (most recent call first): + + 成功: 已终止 PID 为 65784 的进程。 + test cin_implements::nars_python::tests::test ... ok + + test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 10 filtered out; finished in 6.56s + */ +} diff --git a/src/cin_implements/nars_python/translators.rs b/src/cin_implements/nars_python/translators.rs new file mode 100644 index 0000000..9e396a2 --- /dev/null +++ b/src/cin_implements/nars_python/translators.rs @@ -0,0 +1,79 @@ +//! NARS-Python在「命令行运行时」的转译器 +//! * 🎯维护与NARS-Python exe的交互 +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! ## 输出样例 +//! +//! * `EXE: ^left based on desirability: 0.9` +//! * `PROCESSED GOAL: SentenceID:2081:ID ({SELF} --> [SAFE])! :|: %1.00;0.03%from SentenceID:2079:ID ({SELF} --> [SAFE])! :|: %1.00;0.00%,SentenceID:2080:ID ({SELF} --> [SAFE])! :|: %1.00;0.02%,` +//! * `PREMISE IS TRUE: ((*,{SELF}) --> ^right)` +//! * `PREMISE IS SIMPLIFIED ({SELF} --> [SAFE]) FROM (&|,({SELF} --> [SAFE]),((*,{SELF}) --> ^right))` + +use super::format_in_nars_python; +use crate::runtimes::TranslateError; +use anyhow::Result; +use narsese::lexical::Narsese; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; + +/// NARS-Python的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「NARS-Python输入」 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 使用「末尾」将自动格式化任务(可兼容「空预算」的形式) + // * ✅【2024-03-26 01:44:49】目前采用特定的「方言格式」解决格式化问题 + Cmd::NSE(narsese) => format_in_nars_python(&Narsese::Task(narsese)), + // CYC指令:运行指定周期数 + // ! NARS-Python同样是自动步进的 + Cmd::CYC(n) => n.to_string(), + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // ! 🚩【2024-03-27 22:42:56】不使用[`anyhow!`]:打印时会带上一大堆调用堆栈 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// NARS-Python的「输出转译」函数 +/// * 🎯用于将NARS-Python的输出(字符串)转译为「NAVM输出」 +/// * ❌【2024-03-29 19:45:41】目前尚未能从NARS-Python有效获得输出 +pub fn output_translate(content: String) -> Result { + // 根据冒号分隔一次,然后得到「头部」 + let head = content.split_once(':').unwrap_or(("", "")).0.to_lowercase(); + // 根据「头部」生成输出 + let output = match &*head { + // TODO: 有待适配 + "answer" => Output::ANSWER { + // TODO: 有待捕获转译 + narsese: None, + content_raw: content, + }, + "derived" => Output::OUT { + // TODO: 有待捕获转译 + narsese: None, + content_raw: content, + }, + "input" => Output::IN { + // TODO: 有待捕获转译 + narsese: None, + content, + }, + "exe" => Output::EXE { + // TODO: 有待捕获转译 + operation: Operation::new("UNKNOWN", [].into_iter()), + content_raw: content, + }, + "err" | "error" => Output::ERROR { + description: content, + }, + _ => Output::OTHER { content }, + }; + // 返回 + Ok(output) +} diff --git a/src/cin_implements/native/mod.rs b/src/cin_implements/native/mod.rs new file mode 100644 index 0000000..f9889ff --- /dev/null +++ b/src/cin_implements/native/mod.rs @@ -0,0 +1,9 @@ +//! NAVM原生的虚拟机(转译器) +//! * ✨Cmd输入转译:直接将[`Cmd`]转换为字符串形式 +//! * ✨NAVM_JSON输出转译:基于[`serde_json`]直接从JSON字符串读取[`Output`] +//! * 📌没有固定的启动器:仅通过「命令行启动器」即可启动 + +util::mods! { + // 输入输出转译 + pub pub translators; +} diff --git a/src/cin_implements/native/translators.rs b/src/cin_implements/native/translators.rs new file mode 100644 index 0000000..bfd42da --- /dev/null +++ b/src/cin_implements/native/translators.rs @@ -0,0 +1,27 @@ +//! 输入输出转译 +//! * ✨Cmd输入转译:直接将[`Cmd`]转换为字符串形式 +//! * ✨NAVM_JSON输出转译:基于[`serde_json`]直接从JSON字符串读取[`Output`] + +use anyhow::Result; +use navm::{cmd::Cmd, output::Output}; +extern crate serde_json; + +/// Cmd输入转译 +/// * 🚩直接将[`Cmd`]转换为字符串形式 +/// * 📌总是成功 +pub fn input_translate(cmd: Cmd) -> Result { + Ok(cmd.to_string()) +} + +/// NAVM_JSON输出转译 +/// * 🚩基于[`serde_json`]直接从JSON字符串读取[`Output`] +pub fn output_translate(content_raw: String) -> Result { + match serde_json::from_str(&content_raw) { + // 解析成功⇒返回 + Ok(output) => Ok(output), + // 解析失败⇒转为`OTHER` + Err(..) => Ok(Output::OTHER { + content: content_raw, + }), + } +} diff --git a/src/cin_implements/ona/dialect.rs b/src/cin_implements/ona/dialect.rs new file mode 100644 index 0000000..74c560d --- /dev/null +++ b/src/cin_implements/ona/dialect.rs @@ -0,0 +1,385 @@ +//! ONA方言 +//! * 🎯解析ONA输出,如 +//! * 📄以空格分隔的词项:`(* {SELF})` +//! * 📄`({SELF} * x)` + +use crate::runtimes::TranslateError; +use anyhow::{Ok, Result}; +use narsese::{ + conversion::string::{ + impl_enum::format_instances::FORMAT_ASCII, impl_lexical::structs::MidParseResult, + }, + lexical::{Budget, Narsese, Term, Truth}, +}; +use pest::{iterators::Pair, Parser}; +use pest_derive::Parser; + +#[derive(Parser)] // ! ↓ 必须从项目根目录开始 +#[grammar = "src/cin_implements/ona/dialect_ona.pest"] +pub struct DialectParser; + +/// 使用[`pest`]将输入的「ONA方言」转换为「词法Narsese」 +/// 以ONA的语法解析出Narsese +/// * 📌重点在「用空格分隔乘积词项/中缀情形」的语法 +/// * 📄`(* {SELF})` +/// * 📄`({SELF} * x)` +pub fn parse(input: &str) -> Result { + // 语法解析 + let pair = DialectParser::parse(Rule::narsese, input)?.next().unwrap(); + + // 语法折叠 + let folded = fold_pest(pair)?; + + // 返回 + Ok(folded) +} + +/// 将[`pest`]解析出的[`Pair`]辅助折叠到「词法Narsese」中 +fn fold_pest(pest_parsed: Pair) -> Result { + let mut mid_result = MidParseResult { + budget: None, + term: None, + punctuation: None, + stamp: None, + truth: None, + }; + fold_pest_procedural(pest_parsed, &mut mid_result)?; + match mid_result.fold() { + Some(narsese) => Ok(narsese), + None => TranslateError::err_anyhow("无效的中间结果"), + } +} + +/// 过程式折叠[`pest`]词法值 +/// * 🎯向「中间解析结果」填充元素,而无需考虑元素的顺序与返回值类型 +pub(super) fn fold_pest_procedural(pair: Pair, result: &mut MidParseResult) -> Result<()> { + match pair.as_rule() { + // 不会被匹配的`_{..}`元素 + Rule::WHITESPACE | Rule::narsese | Rule::budget_content | Rule::term => { + unreachable!("规则{:?}不会被匹配到!{pair:?}", pair.as_rule()) + } + // Narsese:转发 | 📝语法文件中前缀`_`的,若为纯内容则自动忽略,若内部有元素则自动提取 + // Rule::narsese => fold_pest_procedural(pair.into_inner().next().unwrap(), result), + // 任务⇒所有内部元素递归 | 安装「预算值」「语句」 + Rule::task => { + for pair in pair.into_inner() { + fold_pest_procedural(pair, result)?; + } + } + // 预算⇒尝试解析并填充预算 + Rule::budget => result.budget = Some(fold_pest_budget(pair)?), + // 语句⇒所有内部元素递归 | 安装「词项」「标点」「时间戳」「真值」 + Rule::sentence => { + for pair in pair.into_inner() { + fold_pest_procedural(pair, result)?; + } + } + // 词项⇒提取其中的元素 | 安装 原子 / 复合 / 陈述 | ✅pest自动解包 + // Rule::term => fold_pest_procedural(pair.into_inner().next().unwrap(), result), + Rule::statement => result.term = Some(fold_pest_statement(pair)?), + Rule::compound => result.term = Some(fold_pest_compound(pair)?), + Rule::atom => result.term = Some(fold_pest_atom(pair)?), + // 时间戳 / 标点 ⇒ 直接插入 + Rule::punctuation => result.punctuation = Some(pair.as_str().into()), + Rule::stamp => result.stamp = Some(pair.as_str().into()), + // 真值 ⇒ 解析 ~ 插入 + Rule::truth => result.truth = Some(fold_pest_truth(pair)?), + // 仅出现在内部解析中的不可达规则 + _ => unreachable!("仅出现在内部解析的不可达规则!{:?}{pair}", pair.as_rule()), + } + Ok(()) +} + +/// 折叠[`pest`]真值 +pub(super) fn fold_pest_truth(pair: Pair) -> Result { + let mut v = Truth::new(); + for pair_value_str in pair.into_inner() { + v.push(pair_value_str.as_str().to_string()); + } + Ok(v) +} + +/// 折叠[`pest`]预算值 +pub(super) fn fold_pest_budget(pair: Pair) -> Result { + let mut v = Budget::new(); + for pair_value_str in pair.into_inner() { + v.push(pair_value_str.as_str().to_string()); + } + Ok(v) +} + +/// 折叠[`pest`]词项 +/// * 🎯用于「复合词项/陈述」内部词项的解析 +/// * 📌原子、复合、陈述均可 +pub(super) fn fold_pest_term(pair: Pair) -> Result { + // 根据规则分派 + match pair.as_rule() { + Rule::atom => fold_pest_atom(pair), + Rule::compound => fold_pest_compound(pair), + Rule::statement => fold_pest_statement(pair), + _ => unreachable!("词项只有可能是原子、复合与陈述 | {pair}"), + } +} + +/// 折叠[`pest`]原子词项 +pub(super) fn fold_pest_atom(pair: Pair) -> Result { + let mut prefix = String::new(); + let mut name = String::new(); + for pair in pair.into_inner() { + let pair_str = pair.as_str(); + match pair.as_rule() { + Rule::atom_prefix => prefix.push_str(pair_str), + Rule::atom_content => name.push_str(pair_str), + // 占位符 + Rule::placeholder => { + prefix.push('_'); + if pair_str.len() > 1 { + name.push_str(&pair_str[1..]); + } + } + _ => unreachable!("原子词项只可能有「占位符」或「前缀+名称(内容)」两种 | {pair}"), + } + } + Ok(Term::Atom { prefix, name }) +} + +/// 折叠[`pest`]复合词项 +/// * 🚩【2024-03-29 09:42:36】因「需要通过规则识别『外延集/内涵集』」通过「进一步向下分发」细化被折叠对象 +pub(super) fn fold_pest_compound(pair: Pair) -> Result { + let pair = pair.into_inner().next().unwrap(); + match pair.as_rule() { + Rule::compound_common => { + // * 🚩通用复合词项:连接词 词项... + let mut pairs = pair.into_inner(); + let connecter = pairs.next().unwrap().as_str().into(); + let mut terms = vec![]; + // 遍历剩下的元素 + for pair in pairs { + terms.push(fold_pest_term(pair)?); + } + Ok(Term::Compound { connecter, terms }) + } + Rule::compound_binary => { + // * 🆕ONA特有的「二元复合词项」 + let mut pairs = pair.into_inner(); + // 第一个是左边的词项 + let left = fold_pest_term(pairs.next().unwrap())?; + // 连接词 + let connecter = pairs.next().unwrap().as_str().to_string(); + // 第二个是右边的词项 + let right = fold_pest_term(pairs.next().unwrap())?; + // 构造 & 返回 + Ok(Term::Compound { + connecter, + terms: vec![left, right], + }) + } + Rule::ext_set => { + let mut terms = vec![]; + for pair in pair.into_inner() { + terms.push(fold_pest_term(pair)?); + } + // 构造 & 返回 + // * 🚩【2024-03-29 09:51:46】使用「枚举Narsese」的语法内容,避免硬编码 + Ok(Term::Set { + left_bracket: FORMAT_ASCII.compound.brackets_set_extension.0.into(), + terms, + right_bracket: FORMAT_ASCII.compound.brackets_set_extension.1.into(), + }) + } + Rule::int_set => { + let mut terms = vec![]; + for pair in pair.into_inner() { + terms.push(fold_pest_term(pair)?); + } + // 构造 & 返回 + // * 🚩【2024-03-29 09:51:46】使用「枚举Narsese」的语法内容,避免硬编码 + Ok(Term::Set { + left_bracket: FORMAT_ASCII.compound.brackets_set_intension.0.into(), + terms, + right_bracket: FORMAT_ASCII.compound.brackets_set_intension.1.into(), + }) + } + _ => unreachable!("复合词项只可能是「通用」「操作」「外延集」「内涵集」四种 | {pair}"), + } +} + +/// 折叠[`pest`]陈述 +pub(super) fn fold_pest_statement(pair: Pair) -> Result { + // ! 陈述结构保证:主词+系词+谓词 + let mut pairs = pair.into_inner(); + // 🚩顺序折叠 + let subject = fold_pest_term(pairs.next().unwrap())?; + let copula = pairs.next().unwrap().as_str(); + let predicate = fold_pest_term(pairs.next().unwrap())?; + // 创建 + Ok(Term::new_statement(copula, subject, predicate)) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + use narsese::conversion::string::impl_lexical::format_instances::FORMAT_ASCII; + use util::first; + + /// 测试/方言解析器 🚧 + #[test] + fn test_dialect_parser() { + // 统计用 + let mut 直接相等的个数: usize = 0; + let mut 删去空格与分隔符后相等的个数: usize = 0; + let mut 去掉空格与分隔符_字符重排后相等的个数: usize = 0; // * 🚩【2024-03-29 15:08:24】用于辨别「中缀运算符⇒重排」的情况 + let mut 形式有变的 = vec![]; + + // 📄部分改自OpenNARS`long_term_stability.nal` + // 📄部分源自ONA`Nalifier_ex1.nal`、`NAR_Nalifier_ex1.nal`、 + let narseses = " + (* {SELF}) + ({SELF} * x) + + <(&|,<(*,{SELF},$1,FALSE)-->^want>,<(*,{SELF},$1)-->^anticipate>) =|> <(*,{SELF},$1) --> afraid_of>>. + B>. + {A, B} + <{tim} --> (/,livingIn,_,{graz})>. %0% + <<(*,$1,sunglasses) --> own> ==> <$1 --> [aggressive]>>. + <(*,{tom},sunglasses) --> own>. + <<$1 --> [aggressive]> ==> <$1 --> murder>>. + <<$1 --> (/,livingIn,_,{graz})> ==> <$1 --> murder>>. + <{?who} --> murder>? + <{tim} --> (/,livingIn,_,{graz})>. + <{tim} --> (/,livingIn,_,{graz})>. %0% + <<(*,$1,sunglasses) --> own> ==> <$1 --> [aggressive]>>. + <(*,{tom},(&,[black],glasses)) --> own>. + <<$1 --> [aggressive]> ==> <$1 --> murder>>. + <<$1 --> (/,livingIn,_,{graz})> ==> <$1 --> murder>>. + (&,[black],glasses)>. + <{?who} --> murder>? + <(*,toothbrush,plastic) --> made_of>. + <(&/,<(*,$1,plastic) --> made_of>,<(*,{SELF},$1)-->^lighter>) =/> <$1 --> [heated]>>. + <<$1 --> [heated]> =/> <$1 --> [melted]>>. + <<$1 --> [melted]> <|> <$1 --> [pliable]>>. + <(&/,<$1 --> [pliable]>,<(*,{SELF},$1)-->^reshape>) =/> <$1 --> [hardened]>>. + <<$1 --> [hardened]> =|> <$1 --> [unscrewing]>>. + object>. + (&&,<#1 --> object>,<#1 --> [unscrewing]>)! + <{SELF} --> [hurt]>! %0% + <{SELF} --> [hurt]>. :|: %0% + <(&/,<(*,{SELF},wolf) --> close_to>,+1000) =/> <{SELF} --> [hurt]>>. + <(*,{SELF},wolf) --> close_to>. :|: + <(&|,<(*,{SELF},$1,FALSE)-->^want>,<(*,{SELF},$1)-->^anticipate>) =|> <(*,{SELF},$1) --> afraid_of>>. + <(*,{SELF},?what) --> afraid_of>? + A>. :|: %1.00;0.90% + B>. :|: %1.00;0.90% + C>. :|: %1.00;0.90% + A>. :|: %1.00;0.90% + B>. :|: %1.00;0.90% + C>>? + <(*,cup,plastic) --> made_of>. + object>. + [bendable]>. + [bendable]>. + object>. + <(&/,<(*,$1,plastic) --> made_of>,<(*,{SELF},$1)-->^lighter>) =/> <$1 --> [heated]>>. + <<$1 --> [heated]> =/> <$1 --> [melted]>>. + <<$1 --> [melted]> <|> <$1 --> [pliable]>>. + <(&/,<$1 --> [pliable]>,<(*,{SELF},$1)-->^reshape>) =/> <$1 --> [hardened]>>. + <<$1 --> [hardened]> =|> <$1 --> [unscrewing]>>. + (&&,<#1 --> object>,<#1 --> [unscrewing]>)! + + <{redInst} |-> [red]>. :|: %1.0% + <{redInst} |-> [green]>. :|: %0.0% + <{redInst} |-> [blue]>. :|: %0.0% + <{greenInst} |-> [red]>. :|: %0.0% + <{greenInst} |-> [green]>. :|: %1.0% + <{greenInst} |-> [blue]>. :|: %0.0% + <{blueInst} |-> [red]>. :|: %0.0% + <{blueInst} |-> [green]>. :|: %0.0% + <{blueInst} |-> [blue]>. :|: %1.0% + <{newColor} |-> [red]>. :|: %0.0% + <{newColor} |-> [green]>. :|: %0.0% + <{newColor} |-> [blue]>. :|: %0.1% + <{?what} <-> {newColor}>? :|: + <{blueInst} <-> {newColor}>. :|: %1.000000;0.810000% + <({blueInst} * {newColor}) --> (+ blue)>? :|: + <({blueInst} * {newColor}) --> (+ blue)>. :|: %1.000000;0.810000% + + <{cat} --> [meowing]>. :|: %0.6% + <{cat} --> [barking]>. :|: %0.0% + <(<({#1} ~ {#2}) --> [meowing]> &/ <({SELF} * #1) --> ^say>) =/> G>. + G! :|: + <{dog} --> [barking]>. :|: %1.0% + <{dog} --> [meowing]>. :|: %0.3% + <({cat} ~ {dog}) --> [meowing]>? :|: + <({cat} ~ {dog}) --> [meowing]>. :|: %1.000000;0.810000% + G! :|: + ({SELF} * cat) + + <( [left]> &/ ^right) =/> [free]>>. + <( [right]> &/ ^left) =/> [free]>>. + <( [front]> &/ ^left) =/> [free]>>. + <(( [open]> &/ [free]>) &/ ^forward) =/> G>. + <( [hold]> &/ <({SELF} * $obj) --> ^goto>) =/> <$obj --> [left]>>. + <( [hold]> &/ <({SELF} * $obj) --> ^goto>) =/> <$obj --> [front]>>. + <( [hold]> &/ <({SELF} * $obj) --> ^goto>) =/> <$obj --> [right]>>. + <(( [open]> &/ [left]>) &/ <({SELF} * bottle) --> ^pick>) =/> G>. + <(( [open]> &/ [front]>) &/ <({SELF} * bottle) --> ^pick>) =/> G>. + <(( [open]> &/ [right]>) &/ <({SELF} * bottle) --> ^pick>) =/> G>. + <(( [hold]> &/ [left]>) &/ ^drop) =/> G>. + <(( [hold]> &/ [front]>) &/ ^drop) =/> G>. + <(( [hold]> &/ [right]>) &/ ^drop) =/> G>. + + " + // 初步数据处理 + .split('\n') + .map(str::trim) + .filter(|l| !l.is_empty()); + + // 开始测试解析 + let 去掉空格与分隔符 = |s: &str| { + s.chars() + .filter(|c| !c.is_whitespace() && *c != ',') + .collect::() + }; + let 去掉空格与分隔符_字符集合 = |s: &str| { + s.chars() + .filter(|c| !c.is_whitespace() && *c != ',') + .collect::>() + }; + for narsese in narseses { + let parsed = parse(narsese).expect("pest解析失败!"); + let parsed_str = FORMAT_ASCII.format_narsese(&parsed); + // 对齐并展示 + println!(" {narsese:?}\n => {:?}", parsed_str); + + first! { + narsese == parsed_str => 直接相等的个数 += 1, + 去掉空格与分隔符(narsese) == 去掉空格与分隔符(&parsed_str) => 删去空格与分隔符后相等的个数 += 1, + 去掉空格与分隔符_字符集合(narsese) == 去掉空格与分隔符_字符集合(&parsed_str) => 去掉空格与分隔符_字符重排后相等的个数 += 1, + _ => 形式有变的.push((去掉空格与分隔符(narsese), 去掉空格与分隔符(&parsed_str), parsed)), + } + } + + // 报告 + println!("✅直接相等的个数:{直接相等的个数}"); + println!("✅删去空格与分隔符后相等的个数:{删去空格与分隔符后相等的个数}"); + println!( + "✅去掉空格与分隔符_字符重排后相等的个数:{去掉空格与分隔符_字符重排后相等的个数}" + ); + println!("⚠️形式有变的个数:{}", 形式有变的.len()); + for (n, (narsese, parsed_str, parsed)) in 形式有变的.iter().enumerate() { + // 报告形式有变的 + println!(" {n}:\n\t{narsese:?}\n =?>\t{:?}", parsed_str); + // 报告长度明显变化的 + let len_diff = parsed_str.len().abs_diff(narsese.len()); + if len_diff as f64 / narsese.len() as f64 > 0.5 { + println!("❗长度有较大变化( 变化量={len_diff} ):{parsed:#?}"); + } + } + // ! 🚩【2024-03-29 15:12:43】现在假设「去掉空格与分隔符、字符重排后必定相等」 + assert!(形式有变的.is_empty(), "❌出现形式有变的解析结果!"); + println!("测试完毕!"); + } +} diff --git a/src/cin_implements/ona/dialect_ona.pest b/src/cin_implements/ona/dialect_ona.pest new file mode 100644 index 0000000..d716c00 --- /dev/null +++ b/src/cin_implements/ona/dialect_ona.pest @@ -0,0 +1,138 @@ +//! ONA方言语法 +//! * 🎯从ONA输出中解析Narsese +//! * 📌复合词项的「中缀形式」「空格分隔」形式 +//! * 📄以空格分隔的词项:`(* {SELF})` +//! * 📄`({SELF} * x)` + +/// 空白符 | 所有Unicode空白符,解析前忽略 +WHITESPACE = _{ WHITE_SPACE } + +/// 总入口:词法Narsese | 优先级:任务 > 语句 > 词项 +narsese = _{ + task + | sentence + | term +} + +/// 任务:有预算的语句 +task = { + budget ~ sentence +} + +/// 预算值 | 不包括「空字串」隐含的「空预算」 +budget = { + "$" ~ budget_content ~ "$" +} + +/// 预算值内容 +budget_content = _{ + (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) + | "" // 空预算(但带括号) +} + +/// 通用于真值、预算值的项 | 用作内部数值,不约束取值范围 +truth_budget_term = @{ (ASCII_DIGIT | ".")+ } + +/// 语句 = 词项 标点 时间戳? 真值? +sentence = { + term ~ punctuation ~ stamp? ~ truth? +} + +/// 词项 = 陈述 | 复合 | 原子 +term = _{ + statement + | compound + | atom +} + +/// 陈述 = <词项 系词 词项> | 非**原子规则**,强制其内表达式忽略空格 +statement = !{ + "<" ~ term ~ copula ~ term ~ ">" +} + +/// 陈述系词 +copula = @{ + (punct_sym ~ "-" ~ punct_sym) // 继承/相似/实例/属性/实例属性 + + | (punct_sym ~ "=" ~ punct_sym) // 蕴含/等价 + + | ("=" ~ punct_sym ~ ">") // 时序性蕴含 + + | ("<" ~ punct_sym ~ ">") // 时序性等价 +} + +/// 标点符号 | 用于「原子词项前缀」「复合词项连接词」和「陈述系词」 +punct_sym = { (PUNCTUATION | SYMBOL) } + +/// 复合 = (连接词, 词项...) | {外延集...} | [内涵集...] +/// * 🆕对ONA兼容形如`(^op, {SELF}, LEFT)`的输出语法 +/// * 🚩此处不进行「静默内联」:便于在「折叠函数」中向下分派 +/// * 📝使用前缀「$」停止忽略空格,以使用「可选分隔符」 +/// TODO: 【2024-04-03 11:50:06】对「像」可能需要特别的语法 +/// * 📄`[OUT] Derived: <{SELF} --> (^left /1 P)>. :|: occurrenceTime=21 Priority=0.301667 Truth: frequency=1.000000, confidence=0.810000` +/// * 📄`[OUT] Derived:

(^left /2 {SELF})>. :|` +compound = !{ + compound_common + | compound_binary + | ext_set + | int_set +} + +/// 通用的复合词项 +compound_common = ${ "(" ~ connecter ~ optional_separator ~ term_list ~ ")" } + +/// 通用的「词项列表」 | 静默展开 +term_list = _{ term ~ (optional_separator ~ term)* } + +/// 可选的分隔符 +optional_separator = _{ + ("," ~ WHITESPACE*) + | WHITESPACE+ +} + +/// 🆕ONA特定的「二元中缀表达法」 +/// * 🚩【2024-03-29 09:40:38】目前通用成`(A, B, C)` => `<(*, B, C) --> A>`的转换方式 +compound_binary = { "(" ~ term ~ connecter ~ term ~ ")" } + +/// 外延集 | 📌【2024-03-29 09:39:39】pest代码折叠中会丢掉所有「不被规则捕获的字符串信息」 +ext_set = { "{" ~ term_list ~ "}" } + +/// 内涵集 +int_set = { "[" ~ term_list ~ "]" } + +/// 复合词项连接词 | ⚠️不包括「逗号」与「圆括号」 +connecter = @{ (!"," ~ !"(" ~ !")" ~ punct_sym)* } + +/// 原子 = 前缀(可选) 内容 +atom = { + placeholder // 占位符 + + | (atom_prefix ~ atom_content) // 变量/间隔/操作…… + + | atom_content // 词语 +} + +/// 占位符 = 纯下划线字符串 +placeholder = @{ "_"+ } + +/// 原子词项前缀 +atom_prefix = @{ punct_sym+ } + +/// 原子词项内容 | 已避免与「复合词项系词」相冲突 +atom_content = @{ atom_char ~ (!copula ~ atom_char)* } + +/// 能作为「原子词项内容」的字符 +atom_char = { LETTER | NUMBER | "_" | "-" } + +/// 标点 +punctuation = { (PUNCTUATION | SYMBOL) } + +/// 时间戳 | 空时间戳会直接在「语句」中缺省 +stamp = { + ":" ~ (!":" ~ ANY)+ ~ ":" +} + +/// 真值 | 空真值会直接在「语句」中缺省 +truth = { + "%" ~ (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) ~ "%" +} diff --git a/src/cin_implements/ona/launcher.rs b/src/cin_implements/ona/launcher.rs new file mode 100644 index 0000000..dd6d87d --- /dev/null +++ b/src/cin_implements/ona/launcher.rs @@ -0,0 +1,77 @@ +//! ONA运行时的启动器 +//! * 🎯允许ONA对原先运行时特别配置功能,同时也支持为ONA定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 + +use super::{input_translate, output_translate}; +use crate::{ + cin_implements::common::{generate_command, generate_command_vm}, + runtimes::CommandVmRuntime, +}; +use anyhow::Result; +use navm::{ + cmd::Cmd, + vm::{VmLauncher, VmRuntime}, +}; +use std::path::PathBuf; +use util::pipe; + +/// ONA Shell启动的默认指令参数 +/// * 🎯默认预置指令:`[.exe文件路径] shell` +const COMMAND_ARGS_ONA: [&str; 1] = ["shell"]; + +/// ONA运行时启动器 +/// * 🎯配置ONA专有的东西 +/// * 🚩基于exe文件启动ONA Shell +/// * 默认预置指令:`[.exe文件路径] shell` +/// * 🚩【2024-03-25 08:51:30】目前保留原有缩写的大小写风格,与OpenNARS、PyNARS一致 +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ONA { + /// exe文件路径 + exe_path: PathBuf, + /// ONA Shell的初始音量 + /// * 🚩可能没有:此时不会输入指令 + initial_volume: Option, +} + +// ! 🚩【2024-03-25 09:37:22】目前暂时不提取至「VmExe」:预置的`shell`参数需要被处理 +// * ✅【2024-03-27 16:07:48】现在通过作为工具的`generate_command`部分实现了代码复用 + +impl ONA { + /// 构造函数 + pub fn new(exe_path: impl Into) -> Self { + Self { + // 转换为路径 + exe_path: exe_path.into(), + // 其它全是`None` + ..Default::default() + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for ONA { + fn launch(self) -> Result { + // 构造并启动虚拟机 + let mut runtime = pipe! { + self.exe_path + // 构造指令 | 预置的指令参数 + => generate_command(_, None::, COMMAND_ARGS_ONA.into_iter().by_ref()) + // * 🚩固定的「输入输出转译器」 + => generate_command_vm(_, (input_translate, output_translate)) + // 🔥启动 + => .launch() + }?; + + // 选择性设置初始音量 + if let Some(volume) = self.initial_volume { + // 输入指令,并在执行错误时打印信息 + if let Err(e) = runtime.input_cmd(Cmd::VOL(volume)) { + println!("无法设置初始音量「{volume}」:{e}"); + } + }; + Ok(runtime) + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/ona/mod.rs b/src/cin_implements/ona/mod.rs new file mode 100644 index 0000000..67259d5 --- /dev/null +++ b/src/cin_implements/ona/mod.rs @@ -0,0 +1,52 @@ +//! 「非公理虚拟机」的ONA运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 + +// 转译器 +util::mod_and_pub_use! { + // 转译器 + translators + // 启动器 + launcher + // 方言 | 【2024-03-27 18:42:50】使用`pest`库解析特殊语法 + dialect +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + runtimes::{ + tests::{_test_ona, test_simple_answer}, + CommandVmRuntime, + }, + tests::cin_paths::ONA as EXE_PATH_ONA, + }; + use navm::vm::VmLauncher; + + /// 工具/启动ONA,获得虚拟机运行时 + fn launch_vm() -> CommandVmRuntime { + // 从别的地方获取exe路径 + let exe_path = EXE_PATH_ONA; + // 一行代码启动ONA + ONA::new(exe_path).launch().expect("无法启动虚拟机") + } + + #[test] + fn test() { + // 启动ONA虚拟机 + let vm = launch_vm(); + // 直接复用之前对ONA的测试 + _test_ona(vm) + } + + /// 测试/通用 | 基于Narsese + #[test] + fn test_universal() { + // 启动ONA虚拟机 + let vm = launch_vm(); + // 使用通用测试逻辑 + test_simple_answer(vm) + } +} diff --git a/src/cin_implements/ona/translators.rs b/src/cin_implements/ona/translators.rs new file mode 100644 index 0000000..0721261 --- /dev/null +++ b/src/cin_implements/ona/translators.rs @@ -0,0 +1,678 @@ +//! ONA在「命令行运行时」的转译器 +//! * 🎯维护与ONA Shell的交互 +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! ## 输出样例 +//! +//! * `Input: <<(* x) --> ^left> ==> A>. Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000` +//! * `Derived: <<(* x) --> ^left> ==> good>>. Priority=0.245189 Truth: frequency=1.000000, confidence=0.810000` +//! * `Answer: C>. creationTime=2 Truth: frequency=1.000000, confidence=0.447514` +//! * `Answer: None.` +//! * `^deactivate executed with args` +//! * `^left executed with args (* {SELF})` +//! * `^left executed with args ({SELF} * x)` +//! * `decision expectation=0.616961 implication: <((<{SELF} --> [left_blocked]> &/ ^say) &/ <(* {SELF}) --> ^left>) =/> <{SELF} --> [SAFE]>>. Truth: frequency=0.978072 confidence=0.394669 dt=1.000000 precondition: <{SELF} --> [left_blocked]>. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=50` +//! +//! ## 其它杂项 +//! +//! 💭【2024-03-29 16:58:01】ONA中「注册操作」可以被翻译成`*setopname 操作ID ^操作符名`的形式 +//! * ⚠️但需要自行保证「操作ID」不重复 +//! * 📄`*setopname 1 ^left` +//! * 🔗参见 + +use super::dialect::parse as parse_dialect_ona; +use crate::{ + cin_implements::ona::{fold_pest_compound, DialectParser, Rule}, + cli_support::io::output_print::OutputType, + runtimes::TranslateError, +}; +use anyhow::Result; +use narsese::lexical::{Narsese, Term}; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; +use pest::Parser; +use regex::{Captures, Regex}; +#[cfg(not(test))] +use util::OptionBoost; +use util::{if_return, pipe}; + +/// ONA已内置的操作列表 +/// * 🎯避免「重复操作注册」 +/// * 🎯【2024-04-07 23:12:56】兼容PyNARS的同时,不将自身搞崩 +/// * 📄首次出现场景:Matriangle Websocket服务器链接 +/// * 🔗参考: +pub const OPERATOR_NAME_LIST: &[&str] = &[ + "left", + "right", + "up", + "down", + "say", + "pick", + "drop", + "go", + "activate", + "deactivate", +]; + +/// ONA的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「ONA Shell输入」 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + // ! ONA Shell同样是自动步进的 + Cmd::CYC(n) => n.to_string(), + // VOL指令:调整音量 + Cmd::VOL(n) => format!("*volume={n}"), + // REG指令:注册操作 + Cmd::REG { name } => match OPERATOR_NAME_LIST.contains(&name.as_str()) { + true => String::new(), + false => format!("*setopname {} ^{name}", hash_operator_id(&name)), + }, + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // * 📌【2024-03-24 22:57:18】基本足够支持 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// 🔗参见 +/// ```c +/// //Maximum amount of operations which can be registered +/// #define OPERATIONS_MAX 10 +/// ``` +static mut NEXT_OPERATOR_ID: usize = 0; +const OPERATIONS_MAX: usize = 10; + +/// 从「操作名」到「唯一操作数值ID」 +/// * 🎯用于保证操作ID不重复 +/// * 📌尽可能保证一一映射:操作名(字符串) ↔ 操作ID(无符号整数) +/// +/// * 🚩现在因ONA的「操作符数量限制」不推荐直接用散列函数 +/// * 📄取余后的已知散列冲突:`^op = ^op2` +/// * 🚩【2024-03-29 17:13:41】目前使用「循环取余」尽可能避免「索引越界」 +/// * ⚠️仍然避免不了「操作重复」 +/// * 🚩【2024-03-29 17:19:43】目前采用「及早失败」策略,"let it crash" +/// +/// * 📌ONA中「操作ID」的范围:1..OPERATIONS_MAX +fn hash_operator_id(_: &str) -> usize { + // ! 静态可变量是不安全方法:无法避免数据竞争 + // SAFETY: 实际使用时只需保证 + unsafe { + NEXT_OPERATOR_ID += 1; + NEXT_OPERATOR_ID %= OPERATIONS_MAX; + NEXT_OPERATOR_ID + 1 + } + // ! 🚩【2024-03-29 17:12:28】弃用 + // use std::hash::{DefaultHasher, Hash, Hasher}; + // let mut hasher = DefaultHasher::new(); + // op_name.hash(&mut hasher); + // (hasher.finish() % 10) as usize +} + +/// 测试/获取注册的操作符id +#[test] +fn test_hash_operator_id() { + dbg!([ + hash_operator_id("left"), + hash_operator_id("left"), + hash_operator_id("right"), + hash_operator_id("op"), + hash_operator_id("op2"), + hash_operator_id("oq"), + ]); +} + +/// ONA的「输出转译」函数 +/// * 🎯用于将ONA Shell的输出(字符串)转译为「NAVM输出」 +/// * 🚩直接根据选取的「头部」进行匹配 +/// 超参数:严格模式 +/// * 🚩测试环境下「输出Narsese解析失败」会上报错误 +/// TODO: 解决`Input: <(* {SELF}) --> ^left>. :|: occurrenceTime=119 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000` +pub fn output_translate(content_raw: String) -> Result { + // 特别处理 + if_return! { + // 终止信号 + content_raw.contains("Test failed.") => Ok(Output::TERMINATED { description: content_raw }) + // 操作索引越界 + // * 📄`Operator index out of bounds, it can only be between 1 and OPERATIONS_MAX!` + content_raw.contains("Operator index out of bounds") => Ok(Output::ERROR { description: content_raw }) + } + // 根据冒号分隔一次,然后得到「头部」 + let (head, tail) = content_raw.split_once(':').unwrap_or(("", "")); + // 根据「头部」生成输出 + let output = match head.to_lowercase().as_str() { + "answer" => Output::ANSWER { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + // * 🚩ONA会输出带有误导性的`Answer: None.` + // * 看起来是回答,实际上不是 + narsese: match content_raw.contains("Answer: None.") { + true => None, + false => parse_narsese_ona(head, tail)?, + }, + // 然后传入整个内容 + content_raw, + }, + "derived" => Output::OUT { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: parse_narsese_ona(head, tail)?, + // 然后传入整个内容 + content_raw, + }, + "input" => Output::IN { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: parse_narsese_ona(head, tail)?, + content: content_raw, + }, + "err" | "error" => Output::ERROR { + description: content_raw, + }, + // * 🚩对于「操作」的特殊语法 + // * 🚩【2024-04-02 18:45:17】仅截取`executed with args`,不截取`executed by NAR` + _ if content_raw.contains("executed with args") => Output::EXE { + operation: parse_operation_ona(&content_raw)?, + content_raw, + }, + // * 🚩对于「决策预期→ANTICIPATE」的特殊语法 + // * 🚩【2024-04-02 18:45:17】仅截取`executed with args`,不截取`executed by NAR` + _ if content_raw.contains("decision expectation=") => Output::UNCLASSIFIED { + r#type: "ANTICIPATE".into(), + narsese: parse_anticipate_ona(&content_raw)?, + content: content_raw, + }, + // 若是连续的「头部」⇒识别为「未归类」类型 + _ if !content_raw.contains(char::is_whitespace) => Output::UNCLASSIFIED { + r#type: head.into(), + content: content_raw, + // 不尝试捕获Narsese | 💭后续或许可以自动捕获? + narsese: None, + }, + // 其它 + _ => Output::OTHER { + content: content_raw, + }, + }; + // 返回 + Ok(output) +} + +/// (ONA)从原始输出中解析操作 +/// * 📄`^deactivate executed with args` +/// * 📄`^left executed with args (* {SELF})` +/// * 📄`^left executed with args ({SELF} * x)` +/// * ❌`right executed by NAR` +pub fn parse_operation_ona(content_raw: &str) -> Result { + // 匹配ONA输出中的「操作」⇒转换 | 操作名 | 操作参数(Narsese复合词项⇒提取组分,变成字符串) + let re_operation = Regex::new(r"\^([^\s]+)\s*executed with args\s*(.*)").unwrap(); + let captures = re_capture(&re_operation, content_raw.trim())?; + // ! 即便是测试环境下,也有可能是[`None`](但只在测试环境下返回[`Err`]并报错) + match captures { + Some(captures) => { + // 操作名称 + let operator_name = captures[1].into(); + // 操作参数 + let params = match captures[2].trim() { + // 空字串⇒空参数组 + "" => vec![], + // 否则⇒作为复合词项解析 + term_str => pipe! { + // 获取操作参数字符串 + term_str + // 基于[`pest`]的词法解析 + => DialectParser::parse(Rule::narsese, _) + => {?}# // 后缀语法:抛出错误/解包 + => .next() + => .unwrap() + // 折叠到「词法Narsese」 + => fold_pest_compound + => {?}# // 后缀语法:抛出错误/解包 + // 提取出词项 + => extract_params + }, + }; + // 返回 + Ok(Operation { + operator_name, + params, + }) + } + // 「未知操作」的占位符 | 仅在生产环境中返回 + None => Ok(Operation { + operator_name: "UNKNOWN".into(), + params: vec![], + }), + } +} + +/// (ONA)从原始输出中解析「ANTICIPATE」预期 +/// * 🚩通过「前缀正则截取」分割并解析随后Narsese获得 +/// * 📄`"decision expectation=0.502326 implication: <((<{SELF} --> [good]> &/ b>) &/ <(* {SELF}) --> ^left>) =/> <{SELF} --> [good]>>. Truth: frequency=0.872512 confidence=0.294720 dt=12.000000 precondition: (<{SELF} --> [good]> &/ b>). :|: Truth: frequency=1.000000 confidence=0.360000 occurrenceTime=35124\n"` +/// * 📄`"decision expectation=0.578198 implication: <(a &/ ^left) =/> g>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: a. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=4\n"` +pub fn parse_anticipate_ona(content_raw: &str) -> Result> { + // 正则捕获 + let re_operation = Regex::new(r"implication:\s*(.*)\s*dt=").unwrap(); + let captures = re_capture(&re_operation, content_raw.trim())?; + match captures { + Some(captures) => { + // 获取内容 + let narsese_content = captures[1].to_string(); + // 解析 + let parse_result = + parse_narsese_ona("ANTICIPATE", narsese_content.trim()).inspect_err(|e| { + OutputType::Error.eprint_line(&format!("ONA「预期」解析失败:{e}")); + }); + // 返回 + parse_result + } + // 截取失败的情形 + None => { + OutputType::Error.eprint_line(&format!("ONA「预期」正则捕获失败:{content_raw:?}")); + Ok(None) + } + } +} +/// 操作参数提取 +/// * 🎯从一个解析出来的词项中提取出「操作参数列表」 +/// * 🚩测试环境中仅允许「复合词项」被解包 +#[cfg(test)] +fn extract_params(params: Term) -> Vec { + match params { + Term::Compound { terms, .. } => terms, + _ => unreachable!("ONA的「操作参数」只能由「复合词项」承载"), + } +} + +/// 操作参数提取 +/// * 🎯从一个解析出来的词项中提取出「操作参数列表」 +/// * 🚩测试环境中仅允许「复合词项」被解包 +/// * 🚩生产环境中允许多种词项形式(原子词项⇒仅含其自身的参数列表) +#[cfg(not(test))] +fn extract_params(params: Term) -> Vec { + match params { + Term::Compound { terms, .. } => terms, + Term::Set { terms, .. } => terms, + Term::Statement { + subject, predicate, .. + } => vec![*subject, *predicate], + Term::Atom { .. } => vec![params], + } +} + +/// 正则捕获 +/// * 🎯用于在测试环境中启用「严格模式」(无法匹配⇒报错) +/// * 🚩测试环境中会上抛错误 +/// * 🚩生产环境中仅打印错误消息 +#[cfg(not(test))] +fn re_capture<'a>(re: &'a Regex, haystack: &'a str) -> Result>> { + Ok(re + .captures(haystack) + .inspect_none(|| println!("使用正则表达式「{re}」无法捕获「{haystack}」"))) +} + +/// 正则捕获 +/// * 🎯用于在测试环境中启用「严格模式」(无法匹配⇒报错) +/// * 🚩测试环境中会上抛错误 +/// * 🚩生产环境中仅打印错误消息 +#[cfg(test)] +fn re_capture<'a>(re: &'a Regex, haystack: &'a str) -> Result>> { + use anyhow::anyhow; + match re.captures(haystack) { + // * 🚩↓因为这里要包一层[`Some`],所以无法使用[`Option::ok_or`] + Some(captures) => Ok(Some(captures)), + None => Err(anyhow!("无法使用正则表达式「{re}」捕获「{haystack}」")), + } +} + +/// (ONA)从原始输出中解析Narsese +/// * 🎯用于结合`#[cfg]`控制「严格模式」 +/// * 🚩生产环境下「Narsese解析出错」仅打印错误信息 +#[cfg(not(test))] +pub fn parse_narsese_ona(head: &str, tail: &str) -> Result> { + use util::ResultBoost; + // ! ↓下方会转换为None + Ok(try_parse_narsese(tail).ok_or_run(|e| println!("【{head}】在解析Narsese时出现错误:{e}"))) +} + +/// (ONA)从原始输出中解析Narsese +/// * 🎯用于结合`#[cfg]`控制「严格模式」 +/// * 🚩测试环境下「Narsese解析出错」会上抛错误 +#[cfg(test)] +pub fn parse_narsese_ona(_: &str, tail: &str) -> Result> { + // ! ↓下方会上抛错误 + Ok(Some(try_parse_narsese(tail)?)) +} + +/// (尝试)从输出中解析出Narsese +/// * ❌【2024-03-27 22:01:18】目前引入[`anyhow::Error`]会出问题:不匹配/未满足的特征 +pub fn try_parse_narsese(tail: &str) -> Result { + // 提取并解析Narsese字符串 + pipe! { + tail + // 重整 + => #{&} + => reform_output_to_narsese + // 解析方言 + => #{&} + => parse_dialect_ona + } +} + +/// 重整ONA输出到合法Narsese +/// * 🎯通过「重整→正确解析」的方式,实现初步输出解析兼容 +/// * 🚩【2024-03-25 21:38:39】目前使用正则表达式[`regex`]库 +/// * 🚩【2024-03-25 21:38:52】目前仅基于正则表达式做文本替换 +/// * 📌参数`tail`不附带`Answer:`等部分 +fn reform_output_to_narsese(out: &str) -> String { + // 构造正则表达式(实现中只会编译一次) // + // 匹配ONA输出中的「真值」⇒转换 + let re_truth = Regex::new(r"Truth:\s*frequency=([0-9.]+),\s*confidence=([0-9.]+)").unwrap(); + // 匹配ONA输出的「创建时间」⇒删去 + let re_creation_t = Regex::new(r"creationTime=([0-9.]+)\s+").unwrap(); + // 匹配ONA输出的「时间递进」⇒删去 + let re_dt = Regex::new(r"dt=([0-9.]+)\s+").unwrap(); + // 匹配ONA输出的「优先级」⇒删去 + let re_priority = Regex::new(r"Priority=([0-9.]+)\s+").unwrap(); + + // 两次替换 // + pipe! { + out + // 重建真值表达式 + => [re_truth.replace_all](_, |caps: ®ex::Captures<'_>| { + // * 第`0`个是正则表达式匹配的整个内容 + let f = &caps[1]; + let c = &caps[2]; + // 重建CommonNarsese合法的真值 + format!("%{f};{c}%") + }) + => #{&} + // 删去非必要的「创建时间」 + => [re_creation_t.replace_all](_, "") + => #{&} // 必须借用 + // 删去非必要的「递进时间」 + => [re_dt.replace_all](_, "") + => #{&} // 必须借用 + // 删去非必要的「优先级」 + => [re_priority.replace_all](_, "") + // 剪切前后空白符 + => .trim() + // 返回字符串 // + => .into() + } +} + +/// 单元测试 +#[cfg(test)] +mod test { + use super::*; + use narsese::conversion::string::impl_lexical::format_instances::FORMAT_ASCII; + use util::asserts; + + /// 测试/正则重整 + #[test] + fn test_regex_reform() { + let inp = " C>. creationTime=2 Truth: frequency=1.000000, confidence=0.447514"; + let s = pipe! { + inp + => reform_output_to_narsese + => .chars() + => .into_iter() + => .filter(|c|!c.is_whitespace()) + // => .collect::() // ! ❌暂时不支持「完全限定语法」 + } + .collect::(); + + // 断言 + asserts! { + s => "C>.%1.000000;0.447514%", + } + } + + /// 测试/输出解析 + #[test] + fn test_output_parse() { + // 📄输出源自ONA测试文件`whatwarmer.nal`与ONA的命令行交互 + let outputs = " + [warm]>. :|: %0.8% + Input: [warm]>. :|: occurrenceTime=1 Priority=1.000000 Truth: frequency=0.800000, confidence=0.900000 + [warm]>. :|: %0.8% + Input: [warm]>. :|: occurrenceTime=2 Priority=1.000000 Truth: frequency=0.800000, confidence=0.900000 + [warm]>. :|: %0.8% + Input: [warm]>. :|: occurrenceTime=3 Priority=1.000000 Truth: frequency=0.800000, confidence=0.900000 + [warm]>. :|: %0.3% + Input: [warm]>. :|: occurrenceTime=4 Priority=1.000000 Truth: frequency=0.300000, confidence=0.900000 + Derived: dt=1.000000 < [$1]> =/> [$1]>>. Priority=0.120425 Truth: frequency=0.300000, confidence=0.254517 + Derived: dt=1.000000 < [warm]> =/> [warm]>>. Priority=0.120425 Truth: frequency=0.300000, confidence=0.254517 + Derived: b>. :|: occurrenceTime=4 Priority=0.246973 Truth: frequency=0.800000, confidence=0.162760 + Derived: a>. :|: occurrenceTime=4 Priority=0.194273 Truth: frequency=0.300000, confidence=0.341412 + Derived: b>. :|: occurrenceTime=4 Priority=0.189423 Truth: frequency=0.279070, confidence=0.357855 + Derived: a>. :|: occurrenceTime=4 Priority=0.189423 Truth: frequency=0.279070, confidence=0.357855 + Derived: <(b | a) --> [warm]>. :|: occurrenceTime=4 Priority=0.099456 Truth: frequency=0.240000, confidence=0.648000 + Derived: <(a | b) --> [warm]>. :|: occurrenceTime=4 Priority=0.099456 Truth: frequency=0.240000, confidence=0.648000 + Derived: <(b & a) --> [warm]>. :|: occurrenceTime=4 Priority=0.219984 Truth: frequency=0.860000, confidence=0.648000 + Derived: <(a & b) --> [warm]>. :|: occurrenceTime=4 Priority=0.219984 Truth: frequency=0.860000, confidence=0.648000 + Derived: <(b ~ a) --> [warm]>. :|: occurrenceTime=4 Priority=0.064464 Truth: frequency=0.060000, confidence=0.648000 + Derived: <(a ~ b) --> [warm]>. :|: occurrenceTime=4 Priority=0.161664 Truth: frequency=0.560000, confidence=0.648000 + Derived: <(a * b) --> (+ warm)>. :|: occurrenceTime=4 Priority=0.247200 Truth: frequency=1.000000, confidence=0.648000 + Derived: < [$1]> ==> [$1]>>. :|: occurrenceTime=4 Priority=0.108382 Truth: frequency=0.300000, confidence=0.341412 + Derived: < [$1]> ==> [$1]>>. :|: occurrenceTime=4 Priority=0.137782 Truth: frequency=0.800000, confidence=0.162760 + Derived: < [$1]> <=> [$1]>>. :|: occurrenceTime=4 Priority=0.105676 Truth: frequency=0.279070, confidence=0.357855 + Derived: < [$1]> <=> [$1]>>. :|: occurrenceTime=4 Priority=0.105676 Truth: frequency=0.279070, confidence=0.357855 + Derived: ( [#1]> && [#1]>). :|: occurrenceTime=4 Priority=0.083228 Truth: frequency=0.240000, confidence=0.648000 + Derived: ( [#1]> && [#1]>). :|: occurrenceTime=4 Priority=0.083228 Truth: frequency=0.240000, confidence=0.648000 + <(?1 ~ ?2) --> [warm]>? :|: + Input: <(?1 ~ ?2) --> [warm]>? :|: + Answer: <(a ~ b) --> [warm]>. :|: occurrenceTime=4 creationTime=4 Truth: frequency=0.560000, confidence=0.648000 + ^pick. :|: + Input: ^pick. :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G. :|: + Input: G. :|: occurrenceTime=6 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 <( [warm]> &/ ^pick) =/> G>. Priority=0.185124 Truth: frequency=1.000000, confidence=0.186952 + Derived: dt=1.000000 <(<(a | b) --> [warm]> &/ ^pick) =/> G>. Priority=0.149877 Truth: frequency=1.000000, confidence=0.069427 + Derived: dt=1.000000 <( b> &/ ^pick) =/> G>. Priority=0.177205 Truth: frequency=1.000000, confidence=0.059471 + Derived: dt=1.000000 <( a> &/ ^pick) =/> G>. Priority=0.175070 Truth: frequency=1.000000, confidence=0.047999 + Derived: dt=1.000000 <( b> &/ ^pick) =/> G>. Priority=0.174870 Truth: frequency=1.000000, confidence=0.046913 + Derived: dt=1.000000 <( a> &/ ^pick) =/> G>. Priority=0.174870 Truth: frequency=1.000000, confidence=0.046913 + Derived: dt=1.000000 <(<(b | a) --> [warm]> &/ ^pick) =/> G>. Priority=0.149877 Truth: frequency=1.000000, confidence=0.069427 + Derived: dt=1.000000 <( [warm]> &/ ^pick) =/> G>. Priority=0.168996 Truth: frequency=1.000000, confidence=0.109355 + Derived: dt=1.000000 <(<(a & b) --> [warm]> &/ ^pick) =/> G>. Priority=0.170733 Truth: frequency=1.000000, confidence=0.183101 + Derived: dt=1.000000 <(<(b ~ a) --> [warm]> &/ ^pick) =/> G>. Priority=0.142227 Truth: frequency=1.000000, confidence=0.019374 + Derived: dt=1.000000 <(<(a ~ b) --> [warm]> &/ ^pick) =/> G>. Priority=0.161554 Truth: frequency=1.000000, confidence=0.136690 + Derived: dt=1.000000 <(<(a * b) --> (+ warm)> &/ ^pick) =/> G>. Priority=0.174542 Truth: frequency=1.000000, confidence=0.200929 + Derived: dt=1.000000 <(( [#1]> && [#1]>) &/ ^pick) =/> G>. Priority=0.134326 Truth: frequency=1.000000, confidence=0.069427 + Derived: dt=1.000000 <(( [#1]> && [#1]>) &/ ^pick) =/> G>. Priority=0.134326 Truth: frequency=1.000000, confidence=0.069427 + Derived: dt=1.000000 <(( [warm]> &/ [warm]>) &/ ^pick) =/> G>. Priority=0.134326 Truth: frequency=1.000000, confidence=0.069427 + Derived: dt=1.000000 <(<(b & a) --> [warm]> &/ ^pick) =/> G>. Priority=0.170733 Truth: frequency=1.000000, confidence=0.183101 + Derived: dt=3.000000 < [warm]> =/> G>. Priority=0.208187 Truth: frequency=1.000000, confidence=0.199438 + Derived: dt=2.000000 <<(a | b) --> [warm]> =/> G>. Priority=0.162890 Truth: frequency=1.000000, confidence=0.075969 + Derived: dt=2.000000 < b> =/> G>. Priority=0.206921 Truth: frequency=1.000000, confidence=0.065217 + Derived: dt=2.000000 < a> =/> G>. Priority=0.204202 Truth: frequency=1.000000, confidence=0.052770 + Derived: dt=2.000000 < b> =/> G>. Priority=0.203948 Truth: frequency=1.000000, confidence=0.051588 + Derived: dt=2.000000 < a> =/> G>. Priority=0.203948 Truth: frequency=1.000000, confidence=0.051588 + Derived: dt=2.000000 <<(b | a) --> [warm]> =/> G>. Priority=0.162890 Truth: frequency=1.000000, confidence=0.075969 + Derived: dt=2.000000 <<(a * b) --> (+ warm)> =/> G>. Priority=0.191425 Truth: frequency=1.000000, confidence=0.213712 + Derived: dt=2.000000 <( [#1]> && [#1]>) =/> G>. Priority=0.142122 Truth: frequency=1.000000, confidence=0.075969 + Derived: dt=2.000000 <( [#1]> && [#1]>) =/> G>. Priority=0.142122 Truth: frequency=1.000000, confidence=0.075969 + Derived: dt=2.000000 <( [warm]> &/ [warm]>) =/> G>. Priority=0.142122 Truth: frequency=1.000000, confidence=0.075969 + Derived: dt=2.000000 <<(b & a) --> [warm]> =/> G>. Priority=0.187089 Truth: frequency=1.000000, confidence=0.195491 + Derived: dt=2.000000 < [warm]> =/> G>. Priority=0.189098 Truth: frequency=1.000000, confidence=0.118623 + Derived: dt=2.000000 <<(a & b) --> [warm]> =/> G>. Priority=0.187089 Truth: frequency=1.000000, confidence=0.195491 + Derived: dt=2.000000 <<(b ~ a) --> [warm]> =/> G>. Priority=0.153812 Truth: frequency=1.000000, confidence=0.021435 + Derived: dt=2.000000 <<(a ~ b) --> [warm]> =/> G>. Priority=0.176536 Truth: frequency=1.000000, confidence=0.147400 + <(<(a ~ b) --> [warm]> &/ ^pick) =/> G>? + Input: <(<(a ~ b) --> [warm]> &/ ^pick) =/> G>? + Answer: <(<(a ~ b) --> [warm]> &/ ^pick) =/> G>. creationTime=6 Truth: frequency=1.000000, confidence=0.136690 + + a. :|: + Input: a. :|: occurrenceTime=1 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + ^left. :|: + Input: ^left. :|: occurrenceTime=2 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + g. :|: + Input: g. :|: occurrenceTime=3 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 <(a &/ ^left) =/> g>. Priority=0.254962 Truth: frequency=1.000000, confidence=0.241351 + Derived: dt=2.000000 g>. Priority=0.335353 Truth: frequency=1.000000, confidence=0.254517 + a. :|: + Input: a. :|: occurrenceTime=4 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 a>. Priority=0.348301 Truth: frequency=1.000000, confidence=0.282230 + Derived: dt=1.000000 <(a &/ g) =/> a>. Priority=0.246000 Truth: frequency=1.000000, confidence=0.213712 + g! :|: + Input: g! :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + decision expectation=0.578198 implication: <(a &/ ^left) =/> g>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: a. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=4 + ^left executed with args + Input: ^left. :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + A. :|: + Input: A. :|: occurrenceTime=7 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=2.000000 <((g &/ a) &/ ^left) =/> A>. Priority=0.201969 Truth: frequency=1.000000, confidence=0.174792 + Derived: dt=2.000000 <(a &/ ^left) =/> A>. Priority=0.246000 Truth: frequency=1.000000, confidence=0.213712 + Derived: dt=2.000000 <((a &/ g) &/ ^left) =/> A>. Priority=0.191125 Truth: frequency=1.000000, confidence=0.127972 + Derived: dt=2.000000 <(g &/ ^left) =/> A>. Priority=0.237903 Truth: frequency=1.000000, confidence=0.186952 + Derived: dt=3.000000 <(g &/ a) =/> A>. Priority=0.237903 Truth: frequency=1.000000, confidence=0.186952 + Derived: dt=3.000000 A>. Priority=0.323287 Truth: frequency=1.000000, confidence=0.226692 + Derived: dt=4.000000 <(a &/ g) =/> A>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=4.000000 A>. Priority=0.312281 Truth: frequency=1.000000, confidence=0.199438 + <(*, {SELF}) --> ^left>. :|: + Input: <(* {SELF}) --> ^left>. :|: occurrenceTime=8 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: (* {SELF}). :|: occurrenceTime=8 Priority=0.182344 Truth: frequency=1.000000, confidence=0.293146 + G. :|: + Input: G. :|: occurrenceTime=9 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 <(((g &/ A) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.134179 Truth: frequency=1.000000, confidence=0.068411 + Derived: dt=1.000000 <((a &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.144347 Truth: frequency=1.000000, confidence=0.090215 + Derived: dt=1.000000 <(((g &/ a) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.134179 Truth: frequency=1.000000, confidence=0.068411 + Derived: dt=1.000000 <((g &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.141953 Truth: frequency=1.000000, confidence=0.074873 + Derived: dt=1.000000 <(((a &/ A) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.136267 Truth: frequency=1.000000, confidence=0.082685 + Derived: dt=1.000000 <(((a &/ g) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.131034 Truth: frequency=1.000000, confidence=0.046051 + Derived: dt=1.000000 <((A &/ ^left) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.154562 Truth: frequency=1.000000, confidence=0.150345 + Derived: dt=4.000000 <(a &/ ^left) =/> G>. Priority=0.230723 Truth: frequency=1.000000, confidence=0.161649 + Derived: dt=4.000000 <((g &/ a) &/ ^left) =/> G>. Priority=0.191125 Truth: frequency=1.000000, confidence=0.127972 + Derived: dt=4.000000 <(g &/ ^left) =/> G>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=4.000000 <((a &/ g) &/ ^left) =/> G>. Priority=0.183193 Truth: frequency=1.000000, confidence=0.090215 + Derived: dt=1.000000 <((g &/ A) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.150597 Truth: frequency=1.000000, confidence=0.127972 + Derived: dt=1.000000 <(a &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.166364 Truth: frequency=1.000000, confidence=0.161649 + Derived: dt=1.000000 <((g &/ a) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.150597 Truth: frequency=1.000000, confidence=0.127972 + Derived: dt=1.000000 <(g &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.161849 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=1.000000 <((a &/ A) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.154562 Truth: frequency=1.000000, confidence=0.150345 + Derived: dt=1.000000 <((a &/ g) &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.144347 Truth: frequency=1.000000, confidence=0.090215 + Derived: dt=1.000000 <(A &/ <(* {SELF}) --> ^left>) =/> G>. Priority=0.183842 Truth: frequency=1.000000, confidence=0.241351 + Derived: dt=2.000000 <(g &/ A) =/> G>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=5.000000 G>. Priority=0.302437 Truth: frequency=1.000000, confidence=0.173382 + Derived: dt=5.000000 <(g &/ a) =/> G>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=6.000000 G>. Priority=0.293787 Truth: frequency=1.000000, confidence=0.149042 + Derived: dt=2.000000 <(a &/ A) =/> G>. Priority=0.230723 Truth: frequency=1.000000, confidence=0.161649 + Derived: dt=1.000000 <(* {SELF}) =/> G>. Priority=0.195713 Truth: frequency=1.000000, confidence=0.148415 + Derived: dt=6.000000 <(a &/ g) =/> G>. Priority=0.214505 Truth: frequency=1.000000, confidence=0.098268 + Derived: dt=2.000000 G>. Priority=0.335353 Truth: frequency=1.000000, confidence=0.254517 + A. :|: + Input: A. :|: occurrenceTime=10 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=2.000000 <((a &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.141953 Truth: frequency=1.000000, confidence=0.074873 + Derived: dt=2.000000 <(((g &/ a) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.132453 Truth: frequency=1.000000, confidence=0.056268 + Derived: dt=2.000000 <(((g &/ A) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.132453 Truth: frequency=1.000000, confidence=0.056268 + Derived: dt=2.000000 <(((a &/ g) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.129874 Truth: frequency=1.000000, confidence=0.037532 + Derived: dt=2.000000 <((g &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.139967 Truth: frequency=1.000000, confidence=0.061748 + Derived: dt=2.000000 <(((a &/ A) &/ ^left) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.134179 Truth: frequency=1.000000, confidence=0.068411 + Derived: dt=2.000000 <(a &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.161849 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=2.000000 <((g &/ a) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.147209 Truth: frequency=1.000000, confidence=0.107901 + Derived: dt=2.000000 <((g &/ A) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.147209 Truth: frequency=1.000000, confidence=0.107901 + Derived: dt=2.000000 <((a &/ g) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.141953 Truth: frequency=1.000000, confidence=0.074873 + Derived: dt=2.000000 <(g &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.157967 Truth: frequency=1.000000, confidence=0.117083 + Derived: dt=2.000000 <((a &/ A) &/ <(* {SELF}) --> ^left>) =/> A>. Priority=0.150597 Truth: frequency=1.000000, confidence=0.127972 + Derived: dt=5.000000 <(a &/ ^left) =/> A>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Revised: dt=3.113558 <(a &/ ^left) =/> A>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.301794 + Derived: dt=5.000000 <((g &/ a) &/ ^left) =/> A>. Priority=0.186825 Truth: frequency=1.000000, confidence=0.107901 + Revised: dt=3.090418 <((g &/ a) &/ ^left) =/> A>. Priority=0.186825 Truth: frequency=1.000000, confidence=0.249682 + Derived: dt=5.000000 <((a &/ g) &/ ^left) =/> A>. Priority=0.180156 Truth: frequency=1.000000, confidence=0.074873 + Revised: dt=3.066382 <((a &/ g) &/ ^left) =/> A>. Priority=0.180156 Truth: frequency=1.000000, confidence=0.185459 + Derived: dt=5.000000 <(g &/ ^left) =/> A>. Priority=0.219076 Truth: frequency=1.000000, confidence=0.117083 + Revised: dt=3.097308 <(g &/ ^left) =/> A>. Priority=0.219076 Truth: frequency=1.000000, confidence=0.266081 + Derived: dt=6.000000 A>. Priority=0.293787 Truth: frequency=1.000000, confidence=0.149042 + Revised: dt=4.100474 A>. Priority=0.293787 Truth: frequency=0.980787, confidence=0.323166 + Derived: dt=1.000000 A>. Priority=0.348301 Truth: frequency=1.000000, confidence=0.282230 + Derived: dt=2.000000 <(* {SELF}) =/> A>. Priority=0.190743 Truth: frequency=1.000000, confidence=0.126225 + Derived: dt=1.000000 <(A &/ G) =/> A>. Priority=0.246000 Truth: frequency=1.000000, confidence=0.213712 + Derived: dt=1.000000 <(g &/ G) =/> A>. Priority=0.219076 Truth: frequency=1.000000, confidence=0.117083 + Derived: dt=1.000000 <((* {SELF}) &/ G) =/> A>. Priority=0.170371 Truth: frequency=1.000000, confidence=0.116545 + Derived: dt=7.000000 <(a &/ g) =/> A>. Priority=0.210665 Truth: frequency=1.000000, confidence=0.081831 + Revised: dt=5.053462 <(a &/ g) =/> A>. Priority=0.210665 Truth: frequency=0.983303, confidence=0.202427 + Derived: dt=7.000000 A>. Priority=0.286301 Truth: frequency=1.000000, confidence=0.126793 + Revised: dt=5.084493 A>. Priority=0.286301 Truth: frequency=0.981712, confidence=0.286567 + Derived: dt=1.000000 <(a &/ G) =/> A>. Priority=0.224460 Truth: frequency=1.000000, confidence=0.138259 + Derived: dt=6.000000 <(g &/ a) =/> A>. Priority=0.219076 Truth: frequency=1.000000, confidence=0.117083 + Revised: dt=4.077649 <(g &/ a) =/> A>. Priority=0.219076 Truth: frequency=0.982085, confidence=0.269626 + G! :|: + Input: G! :|: occurrenceTime=11 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=4.000000 (* {SELF})>. Priority=0.182921 Truth: frequency=1.000000, confidence=0.088860 + Derived: dt=4.000000 <(g &/ a) =/> (* {SELF})>. Priority=0.161381 Truth: frequency=1.000000, confidence=0.067330 + Derived: dt=5.000000 <(a &/ g) =/> (* {SELF})>. Priority=0.157655 Truth: frequency=1.000000, confidence=0.045286 + Derived: dt=5.000000 (* {SELF})>. Priority=0.179929 Truth: frequency=1.000000, confidence=0.073708 + decision expectation=0.578198 implication: <(A &/ <(* {SELF}) --> ^left>) =/> G>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: A. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=10 + ^left executed with args (* {SELF}) + Input: <(* {SELF}) --> ^left>. :|: occurrenceTime=11 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: (* {SELF}). :|: occurrenceTime=11 Priority=0.120799 Truth: frequency=1.000000, confidence=0.175147 + + A. :|: + Input: A. :|: occurrenceTime=1 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + <(*, {SELF}) --> ^left>. :|: + Input: <(* {SELF}) --> ^left>. :|: occurrenceTime=2 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G. :|: + Input: G. :|: occurrenceTime=3 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + A. :|: + Input: A. :|: occurrenceTime=4 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G! :|: + Input: G! :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + decision expectation=0.578198 implication: <(A &/ <(* {SELF}) --> ^left>) =/> G>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: A. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=4 + ^left executed with args (* {SELF}) + Input: <(* {SELF}) --> ^left>. :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + + A2. :|: + Input: A2. :|: occurrenceTime=8 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + <(*, {SELF}, P) --> ^left>. :|: + Input: <({SELF} * P) --> ^left>. :|: occurrenceTime=9 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G2. :|: + Input: G2. :|: occurrenceTime=10 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + A2. :|: + Input: A2. :|: occurrenceTime=11 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G2! :|: + Input: G2! :|: occurrenceTime=12 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + decision expectation=0.578198 implication: <(A2 &/ <({SELF} * P) --> ^left>) =/> G2>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: A2. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=11 + ^left executed with args ({SELF} * P) + Input: <({SELF} * P) --> ^left>. :|: occurrenceTime=12 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + + A. :|: + Input: A. :|: occurrenceTime=1 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + <(*, {SELF}) --> ^op>. :|: + Input: <(* {SELF}) --> ^op>. :|: occurrenceTime=2 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + G. :|: + Input: G. :|: occurrenceTime=3 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 <(A &/ <(* {SELF}) --> ^op>) =/> G>. Priority=0.183842 Truth: frequency=1.000000, confidence=0.241351 + Derived: dt=2.000000 G>. Priority=0.335353 Truth: frequency=1.000000, confidence=0.254517 + A. :|: + Input: A. :|: occurrenceTime=4 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + Derived: dt=1.000000 A>. Priority=0.348301 Truth: frequency=1.000000, confidence=0.282230 + Derived: dt=1.000000 <(A &/ G) =/> A>. Priority=0.246000 Truth: frequency=1.000000, confidence=0.213712 + G! :|: + Input: G! :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + decision expectation=0.578198 implication: <(A &/ <(* {SELF}) --> ^op>) =/> G>. Truth: frequency=1.000000 confidence=0.241351 dt=1.000000 precondition: A. :|: Truth: frequency=1.000000 confidence=0.900000 occurrenceTime=4 + ^op executed with args (* {SELF}) + Input: <(* {SELF}) --> ^op>. :|: occurrenceTime=5 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000 + " // 【2024-03-29 16:58:32】省略的「操作注册」语法:`*setopname 1 ^op` + // 初步数据处理 + .split('\n') + .map(str::trim) + .filter(|l| !l.is_empty()); + + // 开始测试解析 + for output in outputs { + // ! 测试环境下[`parse_narsese_ona`]会强制要求「Narsese内容解析成功」 + let o = output_translate(output.into()).expect("输出解析失败"); + if let Some(narsese) = o.get_narsese() { + println!("{}", FORMAT_ASCII.format_narsese(narsese)) + } + } + } +} diff --git a/src/cin_implements/openjunars/launcher.rs b/src/cin_implements/openjunars/launcher.rs new file mode 100644 index 0000000..3123b4d --- /dev/null +++ b/src/cin_implements/openjunars/launcher.rs @@ -0,0 +1,57 @@ +//! OpenJunars 启动器 +//! * 🎯允许OpenJunars对原先运行时特别配置功能,同时也支持为OpenJunars定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 +//! * 🚩通过[`CommandGeneratorJulia`]管理启动参数 + +use super::{input_translate, output_translate}; +use crate::{ + cin_implements::common::CommandGeneratorJulia, + runtimes::{CommandGenerator, CommandVm, CommandVmRuntime}, +}; +use anyhow::Result; +use nar_dev_utils::manipulate; +use navm::vm::VmLauncher; +use std::path::PathBuf; + +/// OpenJunars运行时启动器 +/// * 🎯配置OpenJunars专有的东西 +/// * 🎯以Julia模块形式启动OpenJunars +/// * 📌没有内置的「音量」配置 +/// * 🚩【2024-03-25 08:55:07】基于Julia模块文件启动OpenJunars +/// * 默认预置指令:``julia [`.jl`脚本文件路径]`` +/// * 🚩【2024-03-25 09:15:07】删去[`Default`]派生:因为可能导致无效的路径 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct OpenJunars { + /// Julia脚本文件路径 + command_generator: CommandGeneratorJulia, +} + +impl OpenJunars { + pub fn new(jl_path: impl Into) -> Self { + Self { + // 转换为路径 + command_generator: CommandGeneratorJulia::new(jl_path), + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for OpenJunars { + fn launch(self) -> Result { + // 构造指令 + let command = self.command_generator.generate_command(); + + // 构造并启动虚拟机 + manipulate!( + CommandVm::from(command) + // * 🚩固定的「输入输出转译器」 + => .input_translator(input_translate) + => .output_translator(output_translate) + ) + // 🔥启动 + .launch() + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/openjunars/mod.rs b/src/cin_implements/openjunars/mod.rs new file mode 100644 index 0000000..f9fad07 --- /dev/null +++ b/src/cin_implements/openjunars/mod.rs @@ -0,0 +1,54 @@ +//! 「非公理虚拟机」的OpenJunars运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 + +// 转译器 +util::mod_and_pub_use! { + // 转译器 + translators + // 启动器 + launcher +} + +/// 单元测试 +#[cfg(test)] +mod tests { + #![allow(unused)] + + use super::*; + use crate::{runtimes::CommandVmRuntime, tests::cin_paths::OPENJUNARS}; + use narsese::conversion::string::impl_lexical::shortcuts::*; + use navm::{ + cmd::Cmd, + vm::{VmLauncher, VmRuntime}, + }; + + #[test] + fn test() { + // 从别的地方获取jl路径 + let jl_path = OPENJUNARS; + // 一行代码启动OpenJunars + let vm = OpenJunars::new(jl_path).launch().expect("无法启动虚拟机"); + // 运行专有测试 + // ! ❌【2024-03-25 13:56:21】目前无法截取到Julia运行时输出,弃用 + _test_open_junars(vm) + } + + /// 测试/OpenJunars + pub(crate) fn _test_open_junars(mut vm: CommandVmRuntime) { + // ! ❌【2024-03-25 13:55:57】无效:似乎无法截取到Julia运行时输出 + + vm.input_cmd(Cmd::NSE(nse_task!( B>.))) + .expect("无法输入指令"); + + // 等待四秒钟,让Junars启动 + std::thread::sleep(std::time::Duration::from_secs(1)); + + vm.input_cmd(Cmd::NSE(nse_task!( B>.))) + .expect("无法输入指令"); + std::thread::sleep(std::time::Duration::from_secs(6)); + + // 终止虚拟机运行时 + vm.terminate().expect("无法终止虚拟机"); + } +} diff --git a/src/cin_implements/openjunars/translators.rs b/src/cin_implements/openjunars/translators.rs new file mode 100644 index 0000000..1a25206 --- /dev/null +++ b/src/cin_implements/openjunars/translators.rs @@ -0,0 +1,70 @@ +//! OpenJunars在「命令行运行时」的转译器 +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! TODO: 🚧自OpenNARS复制而来,一些地方需要特别适配 + +use anyhow::Result; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; + +use crate::runtimes::TranslateError; + +/// OpenJunars的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「OpenJunars Shell输入」 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + Cmd::CYC(n) => format!(":c {n}"), + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // * 📌【2024-03-24 22:57:18】基本足够支持 + // ! 🚩【2024-03-27 22:42:56】不使用[`anyhow!`]:打印时会带上一大堆调用堆栈 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// OpenJunars的「输出转译」函数 +/// * 🎯用于将OpenJunars Shell的输出(字符串)转译为「NAVM输出」 +/// * 🚩直接根据选取的「头部」进行匹配 +pub fn output_translate(content: String) -> Result { + // 根据冒号分隔一次,然后得到「头部」 + let head = content.split_once(':').unwrap_or(("", "")).0.to_lowercase(); + // 根据「头部」生成输出 + let output = match &*head { + "answer" => Output::ANSWER { + // TODO: 有待捕获转译 + narsese: None, + content_raw: content, + }, + "out" => Output::OUT { + // TODO: 有待捕获转译 + narsese: None, + content_raw: content, + }, + "in" => Output::IN { + // TODO: 有待捕获转译 + narsese: None, + content, + }, + "exe" => Output::EXE { + // TODO: 有待捕获转译 + operation: Operation::new("UNKNOWN", [].into_iter()), + content_raw: content, + }, + "err" | "error" => Output::ERROR { + description: content, + }, + _ => Output::OTHER { content }, + }; + // 返回 + Ok(output) +} diff --git a/src/cin_implements/opennars/dialect.rs b/src/cin_implements/opennars/dialect.rs new file mode 100644 index 0000000..f1d6bab --- /dev/null +++ b/src/cin_implements/opennars/dialect.rs @@ -0,0 +1,323 @@ +//! OpenNARS方言 +//! * 🎯解析OpenNARS输出,如 +//! * 📄特有的「操作」语法:`(^left, {SELF})` => `<(*, {SELF}) --> ^left>` + +use crate::runtimes::TranslateError; +use anyhow::{Ok, Result}; +use narsese::{ + conversion::string::{ + impl_enum::format_instances::FORMAT_ASCII, impl_lexical::structs::MidParseResult, + }, + lexical::{Budget, Narsese, Term, Truth}, +}; +use pest::{iterators::Pair, Parser}; +use pest_derive::Parser; + +#[derive(Parser)] // ! ↓ 必须从项目根目录开始 +#[grammar = "src/cin_implements/opennars/dialect_opennars.pest"] +pub struct DialectParser; + +/// 使用[`pest`]将输入的「OpenNARS方言」转换为「词法Narsese」 +/// 以OpenNARS的语法解析出Narsese +/// * 📌重点在其简写的「操作」语法`(^left, {SELF}, x)` => `<(*, {SELF}, x) --> ^left>` +pub fn parse(input: &str) -> Result { + // 语法解析 + let pair = DialectParser::parse(Rule::narsese, input)?.next().unwrap(); + + // 语法折叠 + let folded = fold_pest(pair)?; + + // 返回 + Ok(folded) +} + +/// 将[`pest`]解析出的[`Pair`]辅助折叠到「词法Narsese」中 +fn fold_pest(pest_parsed: Pair) -> Result { + let mut mid_result = MidParseResult { + budget: None, + term: None, + punctuation: None, + stamp: None, + truth: None, + }; + fold_pest_procedural(pest_parsed, &mut mid_result)?; + match mid_result.fold() { + Some(narsese) => Ok(narsese), + None => TranslateError::err_anyhow("无效的中间结果"), + } +} + +/// 过程式折叠[`pest`]词法值 +/// * 🎯向「中间解析结果」填充元素,而无需考虑元素的顺序与返回值类型 +fn fold_pest_procedural(pair: Pair, result: &mut MidParseResult) -> Result<()> { + match pair.as_rule() { + // 不会被匹配的`_{..}`元素 + Rule::WHITESPACE | Rule::narsese | Rule::budget_content | Rule::term => { + unreachable!("规则{:?}不会被匹配到!{pair:?}", pair.as_rule()) + } + // Narsese:转发 | 📝语法文件中前缀`_`的,若为纯内容则自动忽略,若内部有元素则自动提取 + // Rule::narsese => fold_pest_procedural(pair.into_inner().next().unwrap(), result), + // 任务⇒所有内部元素递归 | 安装「预算值」「语句」 + Rule::task => { + for pair in pair.into_inner() { + fold_pest_procedural(pair, result)?; + } + } + // 预算⇒尝试解析并填充预算 + Rule::budget => result.budget = Some(fold_pest_budget(pair)?), + // 语句⇒所有内部元素递归 | 安装「词项」「标点」「时间戳」「真值」 + Rule::sentence => { + for pair in pair.into_inner() { + fold_pest_procedural(pair, result)?; + } + } + // 词项⇒提取其中的元素 | 安装 原子 / 复合 / 陈述 | ✅pest自动解包 + // Rule::term => fold_pest_procedural(pair.into_inner().next().unwrap(), result), + Rule::statement => result.term = Some(fold_pest_statement(pair)?), + Rule::compound => result.term = Some(fold_pest_compound(pair)?), + Rule::atom => result.term = Some(fold_pest_atom(pair)?), + // 时间戳 / 标点 ⇒ 直接插入 + Rule::punctuation => result.punctuation = Some(pair.as_str().into()), + Rule::stamp => result.stamp = Some(pair.as_str().into()), + // 真值 ⇒ 解析 ~ 插入 + Rule::truth => result.truth = Some(fold_pest_truth(pair)?), + // 仅出现在内部解析中的不可达规则 + _ => unreachable!("仅出现在内部解析的不可达规则!{:?}{pair}", pair.as_rule()), + } + Ok(()) +} + +/// 折叠[`pest`]真值 +fn fold_pest_truth(pair: Pair) -> Result { + let mut v = Truth::new(); + for pair_value_str in pair.into_inner() { + v.push(pair_value_str.as_str().to_string()); + } + Ok(v) +} + +/// 折叠[`pest`]预算值 +fn fold_pest_budget(pair: Pair) -> Result { + let mut v = Budget::new(); + for pair_value_str in pair.into_inner() { + v.push(pair_value_str.as_str().to_string()); + } + Ok(v) +} + +/// 折叠[`pest`]词项 +/// * 🎯用于「复合词项/陈述」内部词项的解析 +/// * 📌原子、复合、陈述均可 +fn fold_pest_term(pair: Pair) -> Result { + // 根据规则分派 + match pair.as_rule() { + Rule::atom => fold_pest_atom(pair), + Rule::compound => fold_pest_compound(pair), + Rule::statement => fold_pest_statement(pair), + _ => unreachable!("词项只有可能是原子、复合与陈述 | {pair}"), + } +} + +/// 折叠[`pest`]原子词项 +fn fold_pest_atom(pair: Pair) -> Result { + let mut prefix = String::new(); + let mut name = String::new(); + for pair in pair.into_inner() { + let pair_str = pair.as_str(); + match pair.as_rule() { + Rule::atom_prefix => prefix.push_str(pair_str), + Rule::atom_content => name.push_str(pair_str), + // 占位符 + Rule::placeholder => { + prefix.push('_'); + if pair_str.len() > 1 { + name.push_str(&pair_str[1..]); + } + } + _ => unreachable!("原子词项只可能有「占位符」或「前缀+名称(内容)」两种 | {pair}"), + } + } + Ok(Term::Atom { prefix, name }) +} + +/// 折叠[`pest`]复合词项 +/// * 🚩【2024-03-29 09:42:36】因「需要通过规则识别『外延集/内涵集』」通过「进一步向下分发」细化被折叠对象 +fn fold_pest_compound(pair: Pair) -> Result { + let pair = pair.into_inner().next().unwrap(); + match pair.as_rule() { + Rule::compound_common => { + // * 🚩通用复合词项:连接词 词项... + let mut pairs = pair.into_inner(); + let connecter = pairs.next().unwrap().as_str().into(); + let mut terms = vec![]; + // 遍历剩下的元素 + for pair in pairs { + terms.push(fold_pest_term(pair)?); + } + Ok(Term::Compound { connecter, terms }) + } + Rule::compound_operation => { + // * 🆕OpenNARS特有的「操作」词项简写... + let mut pairs = pair.into_inner(); + // 第一个词项应该是谓词 + let predicate = fold_pest_term(pairs.next().unwrap())?; + // 解析主词的组分 + let mut subject_terms = vec![]; + // 遍历剩下的元素 + for pair in pairs { + subject_terms.push(fold_pest_term(pair)?); + } + // 构造 & 返回 + // * 🚩【2024-03-29 09:51:46】使用「枚举Narsese」的语法内容,避免硬编码 + Ok(Term::Statement { + copula: FORMAT_ASCII.statement.copula_inheritance.into(), + subject: Box::new(Term::Compound { + connecter: FORMAT_ASCII.compound.connecter_product.into(), + terms: subject_terms, + }), + predicate: Box::new(predicate), + }) + } + Rule::ext_set => { + let mut terms = vec![]; + for pair in pair.into_inner() { + terms.push(fold_pest_term(pair)?); + } + // 构造 & 返回 + // * 🚩【2024-03-29 09:51:46】使用「枚举Narsese」的语法内容,避免硬编码 + Ok(Term::Set { + left_bracket: FORMAT_ASCII.compound.brackets_set_extension.0.into(), + terms, + right_bracket: FORMAT_ASCII.compound.brackets_set_extension.1.into(), + }) + } + Rule::int_set => { + let mut terms = vec![]; + for pair in pair.into_inner() { + terms.push(fold_pest_term(pair)?); + } + // 构造 & 返回 + // * 🚩【2024-03-29 09:51:46】使用「枚举Narsese」的语法内容,避免硬编码 + Ok(Term::Set { + left_bracket: FORMAT_ASCII.compound.brackets_set_intension.0.into(), + terms, + right_bracket: FORMAT_ASCII.compound.brackets_set_intension.1.into(), + }) + } + _ => unreachable!("复合词项只可能是「通用」「操作」「外延集」「内涵集」四种 | {pair}"), + } +} + +/// 折叠[`pest`]陈述 +fn fold_pest_statement(pair: Pair) -> Result { + // ! 陈述结构保证:主词+系词+谓词 + let mut pairs = pair.into_inner(); + // 🚩顺序折叠 + let subject = fold_pest_term(pairs.next().unwrap())?; + let copula = pairs.next().unwrap().as_str(); + let predicate = fold_pest_term(pairs.next().unwrap())?; + // 创建 + Ok(Term::new_statement(copula, subject, predicate)) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use narsese::conversion::string::impl_lexical::format_instances::FORMAT_ASCII; + use util::first; + + /// 测试/方言解析器 🚧 + #[test] + fn test_dialect_parser() { + // 统计用 + let mut 直接相等的个数: usize = 0; + let mut 删去空格后相等的个数: usize = 0; + let mut 形式有变的 = vec![]; + + // 📄部分源自`long_term_stability.nal` + let narseses = " + _ + __ + ___ + + <(&|,(^want,{SELF},$1,FALSE),(^anticipate,{SELF},$1)) =|> <(*,{SELF},$1) --> afraid_of>>. + B>. + {A, B} + <{tim} --> (/,livingIn,_,{graz})>. %0% + <<(*,$1,sunglasses) --> own> ==> <$1 --> [aggressive]>>. + <(*,{tom},sunglasses) --> own>. + <<$1 --> [aggressive]> ==> <$1 --> murder>>. + <<$1 --> (/,livingIn,_,{graz})> ==> <$1 --> murder>>. + <{?who} --> murder>? + <{tim} --> (/,livingIn,_,{graz})>. + <{tim} --> (/,livingIn,_,{graz})>. %0% + <<(*,$1,sunglasses) --> own> ==> <$1 --> [aggressive]>>. + <(*,{tom},(&,[black],glasses)) --> own>. + <<$1 --> [aggressive]> ==> <$1 --> murder>>. + <<$1 --> (/,livingIn,_,{graz})> ==> <$1 --> murder>>. + (&,[black],glasses)>. + <{?who} --> murder>? + <(*,toothbrush,plastic) --> made_of>. + <(&/,<(*,$1,plastic) --> made_of>,(^lighter,{SELF},$1)) =/> <$1 --> [heated]>>. + <<$1 --> [heated]> =/> <$1 --> [melted]>>. + <<$1 --> [melted]> <|> <$1 --> [pliable]>>. + <(&/,<$1 --> [pliable]>,(^reshape,{SELF},$1)) =/> <$1 --> [hardened]>>. + <<$1 --> [hardened]> =|> <$1 --> [unscrewing]>>. + object>. + (&&,<#1 --> object>,<#1 --> [unscrewing]>)! + <{SELF} --> [hurt]>! %0% + <{SELF} --> [hurt]>. :|: %0% + <(&/,<(*,{SELF},wolf) --> close_to>,+1000) =/> <{SELF} --> [hurt]>>. + <(*,{SELF},wolf) --> close_to>. :|: + <(&|,(^want,{SELF},$1,FALSE),(^anticipate,{SELF},$1)) =|> <(*,{SELF},$1) --> afraid_of>>. + <(*,{SELF},?what) --> afraid_of>? + A>. :|: %1.00;0.90% + B>. :|: %1.00;0.90% + C>. :|: %1.00;0.90% + A>. :|: %1.00;0.90% + B>. :|: %1.00;0.90% + C>>? + <(*,cup,plastic) --> made_of>. + object>. + [bendable]>. + [bendable]>. + object>. + <(&/,<(*,$1,plastic) --> made_of>,(^lighter,{SELF},$1)) =/> <$1 --> [heated]>>. + <<$1 --> [heated]> =/> <$1 --> [melted]>>. + <<$1 --> [melted]> <|> <$1 --> [pliable]>>. + <(&/,<$1 --> [pliable]>,(^reshape,{SELF},$1)) =/> <$1 --> [hardened]>>. + <<$1 --> [hardened]> =|> <$1 --> [unscrewing]>>. + (&&,<#1 --> object>,<#1 --> [unscrewing]>)! + " + // 初步数据处理 + .split('\n') + .map(str::trim) + .filter(|l| !l.is_empty()); + + // 开始测试解析 + let 去掉空格 = |s: &str| s.chars().filter(|c| !c.is_whitespace()).collect::(); + for narsese in narseses { + let parsed = parse(narsese).expect("pest解析失败!"); + let parsed_str = FORMAT_ASCII.format_narsese(&parsed); + // 对齐并展示 + println!(" {narsese:?}\n => {:?}", parsed_str); + + first! { + narsese == parsed_str => 直接相等的个数 += 1, + 去掉空格(narsese) == 去掉空格(&parsed_str) => 删去空格后相等的个数 += 1, + _ => 形式有变的.push((去掉空格(narsese), 去掉空格(&parsed_str))), + } + } + + // 报告 + println!("✅直接相等的个数:{直接相等的个数}"); + println!("✅删去空格后相等的个数:{删去空格后相等的个数}"); + println!("⚠️形式有变的个数:{}", 形式有变的.len()); + for (n, (narsese, parsed_str)) in 形式有变的.iter().enumerate() { + // 报告形式有变的 + println!(" {n}:\n\t{narsese:?}\n =?>\t{:?}", parsed_str); + } + println!("测试完毕!"); + } +} diff --git a/src/cin_implements/opennars/dialect_opennars.pest b/src/cin_implements/opennars/dialect_opennars.pest new file mode 100644 index 0000000..1680e2f --- /dev/null +++ b/src/cin_implements/opennars/dialect_opennars.pest @@ -0,0 +1,126 @@ +//! OpenNARS方言语法 +//! * 🎯从OpenNARS输出中解析Narsese +//! * 📌「NARS操作」的简写`(^op, param, arg)` + +/// 空白符 | 所有Unicode空白符,解析前忽略 +WHITESPACE = _{ WHITE_SPACE } + +/// 总入口:词法Narsese | 优先级:任务 > 语句 > 词项 +narsese = _{ + task + | sentence + | term +} + +/// 任务:有预算的语句 +task = { + budget ~ sentence +} + +/// 预算值 | 不包括「空字串」隐含的「空预算」 +budget = { + "$" ~ budget_content ~ "$" +} + +/// 预算值内容 +budget_content = _{ + (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) + | "" // 空预算(但带括号) +} + +/// 通用于真值、预算值的项 | 用作内部数值,不约束取值范围 +truth_budget_term = @{ (ASCII_DIGIT | ".")+ } + +/// 语句 = 词项 标点 时间戳? 真值? +sentence = { + term ~ punctuation ~ stamp? ~ truth? +} + +/// 词项 = 陈述 | 复合 | 原子 +term = _{ + statement + | compound + | atom +} + +/// 陈述 = <词项 系词 词项> +statement = { + "<" ~ term ~ copula ~ term ~ ">" +} + +/// 陈述系词 +copula = @{ + (punct_sym ~ "-" ~ punct_sym) // 继承/相似/实例/属性/实例属性 + + | (punct_sym ~ "=" ~ punct_sym) // 蕴含/等价 + + | ("=" ~ punct_sym ~ ">") // 时序性蕴含 + + | ("<" ~ punct_sym ~ ">") // 时序性等价 +} + +/// 标点符号 | 用于「原子词项前缀」「复合词项连接词」和「陈述系词」 +punct_sym = { (PUNCTUATION | SYMBOL) } + +/// 复合 = (连接词, 词项...) | {外延集...} | [内涵集...] +/// * 🆕对OpenNARS兼容形如`(^op, {SELF}, LEFT)`的输出语法 +/// * 🚩此处不进行「静默内联」:便于在「折叠函数」中向下分派 +compound = { + compound_common + | compound_operation + | ext_set + | int_set +} + +/// 通用的复合词项 +compound_common = { ("(" ~ connecter ~ "," ~ term_list ~ ")") } + +/// 通用的「词项列表」 | 静默展开 +term_list = _{ term ~ ("," ~ term)* } + +/// 🆕OpenNARS特定的「操作简写」输出 +/// * 🚩【2024-03-29 09:40:38】目前通用成`(A, B, C)` => `<(*, B, C) --> A>`的转换方式 +compound_operation = { "(" ~ term_list ~ ")" } + +/// 外延集 | 📌【2024-03-29 09:39:39】pest代码折叠中会丢掉所有「不被规则捕获的字符串信息」 +ext_set = { "{" ~ term_list ~ "}" } + +/// 内涵集 +int_set = { "[" ~ term_list ~ "]" } + +/// 复合词项连接词 +connecter = @{ punct_sym ~ (!"," ~ punct_sym)* } + +/// 原子 = 前缀(可选) 内容 +atom = { + placeholder // 占位符 + + | (atom_prefix ~ atom_content) // 变量/间隔/操作…… + + | atom_content // 词语 +} + +/// 占位符 = 纯下划线字符串 +placeholder = @{ "_"+ } + +/// 原子词项前缀 +atom_prefix = @{ punct_sym+ } + +/// 原子词项内容 | 已避免与「复合词项系词」相冲突 +atom_content = @{ atom_char ~ (!copula ~ atom_char)* } + +/// 能作为「原子词项内容」的字符 +atom_char = { LETTER | NUMBER | "_" | "-" } + +/// 标点 +punctuation = { (PUNCTUATION | SYMBOL) } + +/// 时间戳 | 空时间戳会直接在「语句」中缺省 +stamp = { + ":" ~ (!":" ~ ANY)+ ~ ":" +} + +/// 真值 | 空真值会直接在「语句」中缺省 +truth = { + "%" ~ (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) ~ "%" +} diff --git a/src/cin_implements/opennars/launcher.rs b/src/cin_implements/opennars/launcher.rs new file mode 100644 index 0000000..181af44 --- /dev/null +++ b/src/cin_implements/opennars/launcher.rs @@ -0,0 +1,75 @@ +//! OpenNARS 启动器 +//! * 🎯允许OpenNARS对原先运行时特别配置功能,同时也支持为OpenNARS定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 +//! * 🚩通过[`CommandGeneratorJava`]管理启动参数 + +use super::{input_translate, output_translate}; +use crate::{ + cin_implements::common::CommandGeneratorJava, + runtimes::{CommandGenerator, CommandVm, CommandVmRuntime}, +}; +use anyhow::Result; +use nar_dev_utils::manipulate; +use navm::{ + cmd::Cmd, + vm::{VmLauncher, VmRuntime}, +}; +use std::path::PathBuf; + +/// OpenNARS Shell启动器 +/// * 🎯配置OpenNARS专有的东西 +/// * 🚩基于jar文件启动OpenNARS Shell +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct OpenNARS { + /// Java [`Command`]生成器 + /// * 📌必须有(包含jar文件路径) + command_generator: CommandGeneratorJava, + /// NARS的初始音量 + /// * 🚩可能没有:此时不会输入指令 + initial_volume: Option, +} + +impl OpenNARS { + /// 构造函数 + pub fn new(jar_path: impl Into) -> Self { + Self { + // 传入路径 + command_generator: CommandGeneratorJava::new(jar_path), + // 其它沿用默认配置 + ..Default::default() + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for OpenNARS { + fn launch(self) -> Result { + // 构造指令 + // * 🚩细致的Java参数配置,都外包给[`CommandGeneratorJava`] + let command_java = self.command_generator.generate_command(); + + // 构造并启动虚拟机 + let mut vm = manipulate!( + CommandVm::from(command_java) + // * 🚩固定的「输入输出转译器」 + => .input_translator(input_translate) + => .output_translator(output_translate) + ) + // 🔥启动 + .launch()?; + + // 设置初始音量 + if let Some(volume) = self.initial_volume { + // 输入指令,并在执行错误时打印信息 + if let Err(e) = vm.input_cmd(Cmd::VOL(volume)) { + println!("无法设置初始音量「{volume}」:{e}"); + } + }; + + // 返回 + Ok(vm) + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/opennars/mod.rs b/src/cin_implements/opennars/mod.rs new file mode 100644 index 0000000..dd2d9b1 --- /dev/null +++ b/src/cin_implements/opennars/mod.rs @@ -0,0 +1,53 @@ +//! 「非公理虚拟机」的OpenNARS运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 + +// 转译器 +util::mod_and_pub_use! { + // 转译器 + translators + // 启动器 + launcher + // 方言 + dialect +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + runtimes::{ + tests::{_test_opennars, test_simple_answer}, + CommandVmRuntime, + }, + tests::cin_paths::OPENNARS as JAR_PATH_OPENNARS, + }; + use navm::vm::VmLauncher; + + /// 工具/启动OpenNARS,获得虚拟机运行时 + fn launch_vm() -> CommandVmRuntime { + // 从别的地方获取jar路径 + let jar_path = JAR_PATH_OPENNARS; + // 一行代码启动OpenNARS + OpenNARS::new(jar_path).launch().expect("无法启动虚拟机") + } + + /// 测试 + #[test] + fn test() { + // 启动OpenNARS虚拟机 + let vm = launch_vm(); + // 直接复用之前对OpenNARS的测试 + _test_opennars(vm) + } + + /// 测试/通用 | 基于Narsese + #[test] + fn test_universal() { + // 启动OpenNARS虚拟机 + let vm = launch_vm(); + // 使用通用测试逻辑 + test_simple_answer(vm) + } +} diff --git a/src/cin_implements/opennars/translators.rs b/src/cin_implements/opennars/translators.rs new file mode 100644 index 0000000..e579a03 --- /dev/null +++ b/src/cin_implements/opennars/translators.rs @@ -0,0 +1,201 @@ +//! OpenNARS在「命令行运行时」的转译器 +//! * 🎯维护与OpenNARS Shell的交互 +//! * https://github.com/ARCJ137442/opennars-304/blob/master/src/main/java/org/opennars/main/Shell.java +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! ## 输出样例 +//! +//! * `IN: B>. %1.00;0.90% {-1 : (-7995324758518856376,0)}` +//! * `OUT: B>. %1.00;0.90% {-1 : (-7995324758518856376,0)}` +//! * `Answer: C>. %1.00;0.81% {1584885193 : (-7995324758518856376,0);(-7995324758518856376,1)}` +//! * `EXE: $1.00;0.99;1.00$ ^left([{SELF}])=null` +//! * `ANTICIPATE: <{SELF} --> [SAFE]>` +//! * `CONFIRM: <{SELF} --> [SAFE]><{SELF} --> [SAFE]>` +//! * `DISAPPOINT: <{SELF} --> [SAFE]>` +//! * `Executed based on: $0.2904;0.1184;0.7653$ <(&/,<{SELF} --> [right_blocked]>,+7,(^left,{SELF}),+55) =/> <{SELF} --> [SAFE]>>. %1.00;0.53%` +//! * `EXE: $0.11;0.33;0.57$ ^left([{SELF}, a, b, (/,^left,a,b,_)])=null` + +use super::dialect::parse as parse_dialect_opennars; +use crate::runtimes::TranslateError; +use anyhow::Result; +use narsese::lexical::{Narsese, Term}; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; +use regex::Regex; +use util::ResultBoost; + +/// OpenNARS的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「OpenNARS Shell输入」 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + // ! OpenNARS Shell是自动步进的 + Cmd::CYC(n) => n.to_string(), + // VOL指令:调整音量 + Cmd::VOL(n) => format!("*volume={n}"), + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // * 📌【2024-03-24 22:57:18】基本足够支持 + // ! 🚩【2024-03-27 22:42:56】不使用[`anyhow!`]:打印时会带上一大堆调用堆栈 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// OpenNARS的「输出转译」函数 +/// * 🎯用于将OpenNARS Shell的输出(字符串)转译为「NAVM输出」 +/// * 🚩直接根据选取的「头部」进行匹配 +pub fn output_translate(content_raw: String) -> Result { + // 根据冒号分隔一次,然后得到「头部」 + let (head, tail) = content_raw.split_once(':').unwrap_or(("", &content_raw)); + let tail = tail.trim(); + // 根据「头部」生成输出 + let output = match &*head.to_uppercase() { + "IN" => Output::IN { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: parse_narsese_opennars(head, tail)?, + // 然后传入整个内容 + content: content_raw, + }, + "OUT" => { + // 返回 + Output::OUT { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: parse_narsese_opennars(head, tail)?, + // 然后传入整个内容 + content_raw, + } + } + "ANSWER" => Output::ANSWER { + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: parse_narsese_opennars(head, tail)?, + // 然后传入整个内容 + content_raw, + }, + "EXE" => Output::EXE { + operation: parse_operation_opennars(tail.trim_start()), + content_raw, + }, + // ! 🚩【2024-03-27 19:40:37】现在将ANTICIPATE降级到`UNCLASSIFIED` + "ANTICIPATE" => Output::UNCLASSIFIED { + // 指定的头部 + r#type: "ANTICIPATE".to_string(), + // 先提取其中的Narsese | ⚠️借用了`content_raw` + narsese: try_parse_narsese(tail) + .ok_or_run(|e| println!("【{head}】在解析Narsese「{tail}」时出现错误:{e}")), + // 然后传入整个内容 + content: content_raw, + }, + "ERR" | "ERROR" => Output::ERROR { + description: content_raw, + }, + // * 🚩利用OpenNARS常见输出「全大写」的特征,兼容「confirm」与「disappoint」 + upper if !head.is_empty() && head == upper => Output::UNCLASSIFIED { + r#type: head.to_string(), + content: content_raw, + // 默认不捕获Narsese + narsese: None, + }, + // 其它 + _ => Output::OTHER { + content: content_raw, + }, + }; + // 返回 + Ok(output) +} + +/// (OpenNARS)从原始输出中解析Narsese +/// * 🎯用于结合`#[cfg]`控制「严格模式」 +/// * 🚩生产环境下「Narsese解析出错」仅打印错误信息 +#[cfg(not(test))] +pub fn parse_narsese_opennars(head: &str, tail: &str) -> Result> { + use util::ResultBoost; + // ! ↓下方会转换为None + Ok(try_parse_narsese(tail) + .ok_or_run(|e| println!("【{head}】在解析Narsese「{tail}」时出现错误:{e}"))) +} + +/// (OpenNARS)从原始输出中解析Narsese +/// * 🎯用于结合`#[cfg]`控制「严格模式」 +/// * 🚩测试环境下「Narsese解析出错」会上抛错误 +#[cfg(test)] +pub fn parse_narsese_opennars(_: &str, tail: &str) -> Result> { + // ! ↓下方会上抛错误 + Ok(Some(try_parse_narsese(tail)?)) +} + +/// 在OpenNARS输出中解析出「NARS操作」 +/// * 📄`$0.11;0.33;0.57$ ^left([{SELF}, a, b, (/,^left,a,b,_)])=null` +/// * 🚩【2024-03-29 22:45:11】目前能提取出其中的预算值,但实际上暂且不需要 +pub fn parse_operation_opennars(tail: &str) -> Operation { + // * 构建正则表达式(仅一次编译) + let r = Regex::new(r"(\$[0-9.;]+\$)\s*\^(\w+)\(\[(.*)\]\)=").unwrap(); + + // 构建返回值(参数) + let mut params = vec![]; + + // 提取输出中的字符串 + let c = r.captures(tail); + // let budget; + let operator_name; + let params_str; + if let Some(c) = c { + // 提取 + // budget = &c[1]; + operator_name = c[2].to_string(); + params_str = &c[3]; + // 尝试解析 + for param in params_str.split(", ") { + match parse_term_from_operation(param) { + Ok(term) => params.push(term), + // ? 【2024-03-27 22:29:43】↓是否要将其整合到一个日志系统中去 + Err(e) => println!("【EXE】在解析Narsese时出现错误:{e}"), + } + } + } else { + operator_name = String::new(); + } + + // 返回 + Operation { + operator_name, + params, + } +} + +/// 从操作参数中解析出Narsese词项 +fn parse_term_from_operation(term_str: &str) -> Result { + // 首先尝试解析出Narsese + let parsed = parse_dialect_opennars(term_str)?; + // 其次尝试将其转换成Narsese词项 + parsed + .try_into_term() + .transform_err(TranslateError::error_anyhow) +} + +/// 切分尾部字符串,并(尝试)从中解析出Narsese +/// * 🎯对OpenNARS中的「时间戳/证据基」做切分 +/// * 📄`<{SELF} --> [satisfied]>! :|: %1.00;0.90% {1269408|1269408 : (-8058943780727144183,628)}` +/// * 🚩现在无需考虑:[`pest`]会自动忽略无关前缀 +/// * ❌在「无证据基case」如`ANTICIPATE: <{powerup_bad_x} --> [seen]>`中报错:把`{`截掉了 +/// * 📌此中`tail`已做好行切分 +fn try_parse_narsese(tail: &str) -> Result { + // 提取并解析Narsese字符 + // 提取解析结果 + let narsese = parse_dialect_opennars(tail); + match narsese { + // 解析成功⇒提取 & 返回 + Ok(narsese) => Ok(narsese), + // 解析失败⇒打印错误日志 | 返回None + Err(err) => Err(TranslateError::from(err).into()), + } +} diff --git a/src/cin_implements/pynars/launcher.rs b/src/cin_implements/pynars/launcher.rs new file mode 100644 index 0000000..ed04dfc --- /dev/null +++ b/src/cin_implements/pynars/launcher.rs @@ -0,0 +1,56 @@ +//! Python模块 启动器 +//! * 📌PyNARS运行时的启动器 +//! * 🎯允许PyNARS对原先运行时特别配置功能,同时也支持为PyNARS定制配置 +//! * 🚩只憎加「启动器」类型,而不增加「运行时」类型 +//! * ✨不同启动器可以启动到相同运行时 +//! * 🚩通过[`CommandGeneratorPython`]管理启动参数 + +use super::{input_translate, output_translate}; +use crate::{ + cin_implements::common::CommandGeneratorPython, + runtimes::{CommandGenerator, CommandVm, CommandVmRuntime}, +}; +use anyhow::Result; +use nar_dev_utils::manipulate; +use navm::vm::VmLauncher; +use std::path::PathBuf; + +/// PyNARS运行时启动器 +/// * 🎯配置PyNARS专有的东西 +/// * 🎯以Python模块形式启动PyNARS +/// * 📌没有内置的「音量」配置 +/// * ⚠️该配置参考的是PyNARS的`ConsolePlus`模块 +/// * 🚩【2024-03-25 08:55:07】基于Python模块文件启动PyNARS Shell +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PyNARS { + /// 命令生成器 + command_generator: CommandGeneratorPython, +} + +impl PyNARS { + pub fn new(root_path: impl Into, module_path: &str) -> Self { + Self { + command_generator: CommandGeneratorPython::new(root_path, module_path), + } + } +} + +/// 启动到「命令行运行时」 +impl VmLauncher for PyNARS { + fn launch(self) -> Result { + // 构造指令 + let command = self.command_generator.generate_command(); + + // 构造并启动虚拟机 + manipulate!( + CommandVm::from(command) + // * 🚩固定的「输入输出转译器」 + => .input_translator(input_translate) + => .output_translator(output_translate) + ) + // 🔥启动 + .launch() + } +} + +// ! 单元测试见[`super`] diff --git a/src/cin_implements/pynars/mod.rs b/src/cin_implements/pynars/mod.rs new file mode 100644 index 0000000..e51cac7 --- /dev/null +++ b/src/cin_implements/pynars/mod.rs @@ -0,0 +1,58 @@ +//! 「非公理虚拟机」的PyNARS运行时 +//! * 🚩只提供「一行启动」的功能封装 +//! * 🎯无需自行配置「输入输出转译器」 +//! +//! * ❌【2024-03-25 13:00:14】目前无法在Rust侧解决「杀死子进程后,Python继续输出无关信息」的问题 +//! * 📄主要形式:子进程结束后打印错误堆栈,输出`OSError: [Errno 22] Invalid argument` +//! * ❗无法被Rust捕获,可能是Python运行时的问题(输出未链接到管道) + +// 转译器 +util::mod_and_pub_use! { + // 转译器 + translators + // 启动器 + launcher +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + runtimes::{ + tests::{_test_pynars, test_simple_answer}, + CommandVmRuntime, + }, + tests::cin_paths::{PYNARS_MODULE, PYNARS_ROOT}, + }; + use navm::vm::VmLauncher; + + /// 工具/启动PyNARS,获得虚拟机运行时 + fn launch_vm() -> CommandVmRuntime { + // 从别的地方获取Python模块根目录、模块自身路径 + let root_path = PYNARS_ROOT; + let module_path = PYNARS_MODULE; + // 一行代码启动PyNARS | `python -m pynars.Console` @ "..\..\PyNARS-dev" + PyNARS::new(root_path, module_path) + .launch() + .expect("无法启动虚拟机") + } + + /// 测试/先前PyNARS测试 + #[test] + fn test() { + // 启动PyNARS虚拟机 + let vm = launch_vm(); + // 直接复用之前对PyNARS的测试 + _test_pynars(vm) + } + + /// 测试/通用 | 基于Narsese + #[test] + fn test_universal() { + // 启动PyNARS虚拟机 + let vm = launch_vm(); + // 使用通用测试逻辑 + test_simple_answer(vm) + } +} diff --git a/src/cin_implements/pynars/translators.rs b/src/cin_implements/pynars/translators.rs new file mode 100644 index 0000000..8243dd7 --- /dev/null +++ b/src/cin_implements/pynars/translators.rs @@ -0,0 +1,308 @@ +//! PyNARS在「命令行运行时」的转译器 +//! * 🎯维护与PyNARS的交互 +//! * 📌基于命令行输入输出的字符串读写 +//! * ✨NAVM指令→字符串 +//! * ✨字符串→NAVM输出 +//! +//! ## 输出样例 +//! +//! * 📄`\u{1b}[90mInput: \u{1b}[39m\u{1b}[48;2;124;10;10m 0.90 \u{1b}[49m\u{1b}[48;2;10;124;10m 0.90 \u{1b}[49m\u{1b}[48;2;10;10;137m 1.00 \u{1b}[49m\u{1b}[36mIN :\u{1b}[39mC>?\r\n` +//! * 📄`\u{1b}[90mInput: \u{1b}[39m \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[34mINFO :\u{1b}[39m\u{1b}[38;5;249mRun 5 cycles.\u{1b}[39m\r\n` +//! * 📄`\u{1b}[48;2;106;10;10m 0.75 \u{1b}[49m\u{1b}[48;2;10;41;10m 0.25 \u{1b}[49m\u{1b}[48;2;10;10;102m 0.72 \u{1b}[49m\u{1b}[33mOUT :\u{1b}[39mA>. %1.000;0.448%\r\n` +//! * 📄`\u{1b}[48;2;134;10;10m 0.98 \u{1b}[49m\u{1b}[48;2;10;124;10m 0.90 \u{1b}[49m\u{1b}[48;2;10;10;125m 0.90 \u{1b}[49m\u{1b}[32mANSWER:\u{1b}[39mC>. %1.000;0.810%\r\n` +//! * 📄` \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[32mEXE :\u{1b}[39m<(*, 0)-->^op> = $0.022;0.232;0.926$ <(*, 0)-->^op>! :\\: %1.000;0.853% {7: 2, 0, 1}\r\n` + +use crate::runtimes::TranslateError; +use anyhow::{anyhow, Result}; +use narsese::{ + api::ExtractTerms, + conversion::string::{ + impl_enum::format_instances::FORMAT_ASCII as FORMAT_ASCII_ENUM, + impl_lexical::format_instances::FORMAT_ASCII, + }, + lexical::{Narsese, Term}, +}; +use navm::{ + cmd::Cmd, + output::{Operation, Output}, +}; +use regex::{Captures, Regex}; +use util::{pipe, JoinTo}; + +/// PyNARS的「输入转译」函数 +/// * 🎯用于将统一的「NAVM指令」转译为「PyNARS输入」 +pub fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + // * 📌PyNARS需要手动指定步进数 + Cmd::CYC(n) => n.to_string(), + // VOL指令:调整音量 + // ! ⚠️该指令仅适用于`ConsolePlus` + Cmd::VOL(n) => format!("/volume {n}"), + // REG指令:注册操作符 + // * 📄Input: /register name + // * `Operator ^name was successfully registered without code` + Cmd::REG { name, .. } => format!("/register {name}"), + // 注释 ⇒ 忽略 | ❓【2024-04-02 22:43:05】可能需要打印,但这样却没法统一IO(到处print的习惯不好) + Cmd::REM { .. } => String::new(), + // 其它类型 + // * 📌【2024-03-24 22:57:18】基本足够支持 + // ! 🚩【2024-03-27 22:42:56】不使用[`anyhow!`]:打印时会带上一大堆调用堆栈 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转译 + Ok(content) +} + +/// 预处理 +/// * 🎯去掉输出字串中语义无关的杂项 +/// * 📄ANSI转义序列 +pub fn preprocess(s: &str) -> String { + // ! `\e` => `\u{1b}` + let re = Regex::new(r"\u{1b}\[[0-9;]*m").unwrap(); + pipe! { + s + // 去掉ANSI转义序列 + => [re.replace_all](_, "") + // 去掉前后缀空白符 + => .trim() + // 转换为字符串 + => .to_string() + } +} + +/// 尝试获取输出类型(「头」文本) +/// * 🚩输入:[`preprocess`]预处理后的文本 +/// * 🎯尝试获取「类型」字符串,若无则返回[`None`] +fn try_get_output_type(preprocessed: &str) -> Option { + // 截获输出类型,忽略前边的预算值 + let re2 = Regex::new(r"[0-9\s|]*(\w+)\s*:").unwrap(); + pipe! { + preprocessed + // 捕获 + => [re2.captures](_) + // 转换为字符串 + => .map(|captures|captures[1].into()) + } +} + +/// 尝试获取输出中的Narsese +/// * 🚩输入:[`preprocess`]预处理后的文本 +/// * 🎯尝试获取「Narsese」值 +fn try_get_narsese(preprocessed: &str) -> Result { + // 删去无用内容,并替换成预算值 | 三个预算+一个头 + // * 🚩【2024-03-30 00:15:24】开头必须是`[^0-9.]*`,以避免吃掉预算值「`0.98`⇒`8`」💥 + let re_trim_and_budget = + Regex::new(r"^[^0-9.]*([0-9.]+)[\s|]+([0-9.]+)[\s|]+([0-9.]+)[\s|]+\w+\s*:\s*").unwrap(); + let trimmed = re_trim_and_budget + // 删去其中无用的内容,并重整其中的预算值 // + .replace(preprocessed, |s: &Captures| { + // 创建「预算值」字串 + let mut budget = FORMAT_ASCII_ENUM.task.budget_brackets.0.to_string(); + + // 构造迭代器 + let mut s = s.iter(); + s.next(); // 消耗掉第一个「被匹配到的字符串」 + + // 遍历所有匹配到的「预算内容」 + s.flatten() + // 全部转换成「字串切片」 + .map(|c| c.as_str()) + // 拼接到已预置好「预算起始括弧」的字符串中 + .join_to(&mut budget, FORMAT_ASCII_ENUM.task.budget_separator); + + // 最后加入并返回 + budget + FORMAT_ASCII_ENUM.task.budget_brackets.1 + }) + .to_string(); + let parsed_narsese = FORMAT_ASCII.parse(&trimmed)?; + Ok(parsed_narsese) +} + +/// 获取输出中的Narsese +/// * 🎯根据「测试环境」与「生产环境」启用不同的模式 +/// * 🚩测试环境中「解析失败」会报错(成功了总返回[`Some`]) +/// * 🚩生产环境中「解析失败」仅提示(然后返回[`None`]) +#[cfg(not(test))] +fn get_narsese(preprocessed: &str) -> Result> { + use util::ResultBoost; + // * 🚩解析失败⇒提示⇒返回[`None`] + Ok(try_get_narsese(preprocessed).ok_or_run(|e| println!("尝试解析Narsese错误:{e}"))) +} + +/// 获取输出中的Narsese +/// * 🎯根据「测试环境」与「生产环境」启用不同的模式 +/// * 🚩测试环境中「解析失败」会报错(成功了总返回[`Some`]) +/// * 🚩生产环境中「解析失败」仅提示(然后返回[`None`]) +#[cfg(test)] +fn get_narsese(preprocessed: &str) -> Result> { + // * 🚩解析失败会上抛,成功了总是返回[`Some`] + Ok(Some(try_get_narsese(preprocessed)?)) +} + +/// 尝试获取输出中的「Narsese操作」 +/// * 🎯截获PyNARS中的「EXE」部分 +/// * 📄` \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[32mEXE :\u{1b}[39m<(*, 0)-->^op> = $0.022;0.232;0.926$ <(*, 0)-->^op>! :\\: %1.000;0.853% {7: 2, 0, 1}\r\n` +/// * 📄"executed: arguments=, task=$0.000;0.339;0.950$ <(*, 0, 1, 2, 3)-->^op>! %1.000;0.853% {None: 7, 4, 5}, memory=. the \"task\" will be returned\r\n" +/// * 📄` \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[32mEXE :\u{1b}[39m<(*, 0, 1, 2, 3)-->^op> = $0.000;0.339;0.950$ <(*, 0, 1, 2, 3)-->^op>! %1.000;0.853% {None: 7, 4, 5}\r\n` +/// * 📄"executed: arguments=, task=$0.220;0.232;0.926$ <(*, 0)-->^op>! :\\: %1.000;0.853% {7: 2, 0, 1}, memory=. the \"task\" will be returned\r\n" +fn try_get_operation(preprocessed: &str) -> Result { + let re_operation = Regex::new(r"EXE\s*:\s*(.+) = ").unwrap(); + let op = re_operation + .captures(preprocessed) + .unwrap() + .get(1) + .unwrap() + .as_str(); + let op = FORMAT_ASCII.parse(op).unwrap().try_into_term().unwrap(); + match op { + // * 📄`<(*, 0)-->^op>` + Term::Statement { + subject, predicate, .. + } => { + // 从主词提取操作参数 + let params = subject.extract_terms_to_vec(); + // 从谓词提取操作名 + let operator_name = match *predicate { + Term::Atom { name, .. } => name, + _ => return Err(anyhow!("陈述谓词不是原子词项")), + }; + Ok(Operation { + operator_name, + params, + }) + } + _ => Err(anyhow::anyhow!("无效的「操作表示」词项:{op:?}")), + } +} + +/// 获取输出中的「Narsese操作」 +/// * 🎯获取名称及其参数 +/// * 🎯根据「测试环境」与「生产环境」启用不同的模式 +/// * 🚩测试环境中「解析失败」会报错(成功了总返回[`Some`]) +/// * 🚩生产环境中「解析失败」仅提示(然后返回[`None`]) +#[cfg(not(test))] +fn get_operation(preprocessed: &str) -> Operation { + // * 🚩解析失败仅提示,然后返回「空操作」 + try_get_operation(preprocessed).unwrap_or_else(|e| { + println!("尝试从「{preprocessed}」解析Narsese操作错误:{e}"); + // 空操作 + Operation { + operator_name: "".into(), + params: vec![], + } + }) +} + +/// 获取输出中的Narsese +/// * 🎯根据「测试环境」与「生产环境」启用不同的模式 +/// * 🚩测试环境中「解析失败」会报错(成功了总返回[`Some`]) +/// * 🚩生产环境中「解析失败」仅提示(然后返回[`None`]) +#[cfg(test)] +fn get_operation(preprocessed: &str) -> Operation { + // * 🚩解析失败会直接报错 + try_get_operation(preprocessed) + .unwrap_or_else(|e| panic!("无法从「{preprocessed}」解析出Narsese操作:{e}")) +} + +/// PyNARS的「输出转译」函数 +/// * 🎯用于将PyNARS的输出(字符串)转译为「NAVM输出」 +/// * 🚩直接根据选取的「头部」进行匹配 +/// # * 去除其中的ANSI转义序列,如:`\e[39m` # 并去除前后多余空格 +/// local actual_line::String = strip(replace(line, r"\e\[[0-9;]*m" => "")) +/// #= 去除后样例: +/// * `0.70 0.25 0.60 OUT :<(*, x)-->^left>>. %1.000;0.200%` +/// * INFO : Loading RuleMap ... +/// * EXE :<(*, x)-->^left> = $0.016;0.225;0.562$ <(*, x)-->^left>! %1.000;0.125% {None: 3, 1, 2} +/// * EXE :<(*, 1, 2, 3)-->^left> = $0.000;0.225;0.905$ <(*, 1, 2, 3)-->^left>! %1.000;0.287% {None: 2, 1, 0} +/// * EXE :<(*, {SELF}, [good])-->^f> = $0.026;0.450;0.905$ <(*, {SELF}, [good])-->^f>! %1.000;0.810% {None: 2, 1} +/// =# +/// +/// # * 特殊处理「信息」"INFO":匹配「INFO」开头的行 样例:`INFO : Loading RuleMap ...` +pub fn output_translate(content: String) -> Result { + // 预处理 | 利用变量遮蔽,在输出中屏蔽ANSI转义序列 + let content = preprocess(&content); + // 根据冒号分隔一次,然后得到「头部」 + let head = pipe! { + &content + // 获取输出类型 + => try_get_output_type + // 统一转成小写 | ✅无需`trim`:在`try_get_output_type`中使用正则表达式保证 + => .map(|s|s.to_lowercase()) + }; + // 取切片 | ❌不能使用闭包,因为闭包无法返回引用 + let head = match &head { + Some(s) => s, + None => "", + }; + // 根据「头部」生成输出 + let output = match head { + "answer" => Output::ANSWER { + narsese: get_narsese(&content)?, + content_raw: content, + }, + "achieved" => Output::ACHIEVED { + narsese: get_narsese(&content)?, + content_raw: content, + }, + "out" => Output::OUT { + narsese: get_narsese(&content)?, + content_raw: content, + }, + "input" | "in" => Output::IN { + narsese: get_narsese(&content)?, + content, + }, + "info" => Output::INFO { message: content }, + "exe" => Output::EXE { + operation: get_operation(&content), + content_raw: content, + }, + "err" | "error" => Output::ERROR { + description: content, + }, + _ => Output::OTHER { content }, + }; + // 返回 + Ok(output) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + + /// 测试/尝试获取输出 + #[test] + fn test_try_get_output() { + test("\u{1b}[48;2;110;10;10m 0.78 \u{1b}[49m\u{1b}[48;2;10;41;10m 0.25 \u{1b}[49m\u{1b}[48;2;10;10;125m 0.90 \u{1b}[49m\u{1b}[33mOUT :\u{1b}[39mC>. %1.000;0.810%\r\n"); + test("|0.80|0.50|0.95| IN : A. %1.000;0.900%"); + test("\u{1b}[90mInput: \u{1b}[39m\u{1b}[48;2;124;10;10m 0.90 \u{1b}[49m\u{1b}[48;2;10;124;10m 0.90 \u{1b}[49m\u{1b}[48;2;10;10;137m 1.00 \u{1b}[49m\u{1b}[36mIN :\u{1b}[39mC>?\r\n"); + test("0.98 0.90 0.90 ANSWER:C>. %1.000;0.810%"); + + fn test(inp: &str) { + let preprocessed = preprocess(inp); + let _ = " 0.78 0.25 0.90 OUT :C>. %1.000;0.810%\r\n"; + dbg!(&preprocessed); + let t = try_get_output_type(&preprocessed); + dbg!(&t); + + // 删去无用内容,并替换成预算值 | 三个预算+一个头 + dbg!(try_get_narsese(&preprocessed).expect("Narsese解析失败!")); + } + } + + /// 测试/尝试获取操作 + #[test] + fn test_try_get_operation() { + test(" \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[32mEXE :\u{1b}[39m<(*, 0)-->^op> = $0.022;0.232;0.926$ <(*, 0)-->^op>! :\\: %1.000;0.853% {7: 2, 0, 1}\r\n"); + test(" \u{1b}[49m \u{1b}[49m \u{1b}[49m\u{1b}[32mEXE :\u{1b}[39m<(*, 0, 1, 2, 3)-->^op> = $0.000;0.339;0.950$ <(*, 0, 1, 2, 3)-->^op>! %1.000;0.853% {None: 7, 4, 5}\r\n"); + fn test(inp: &str) { + let inp = preprocess(inp); + let op = try_get_operation(&inp).unwrap(); + dbg!(op); + } + } +} diff --git a/src/cli_support/cin_search/anyhow_vm.rs b/src/cli_support/cin_search/anyhow_vm.rs new file mode 100644 index 0000000..1150271 --- /dev/null +++ b/src/cli_support/cin_search/anyhow_vm.rs @@ -0,0 +1,125 @@ +//! 🚩【2024-03-30 23:36:48】曾经的尝试: +//! * 所有「路径构建器」都返回一个动态的「虚拟机启动器」类型 +//! * 启动时只需在一个「Anyhow虚拟机启动器」列表中选择 + +trait Turned { + fn say(&self); +} +trait Unturned { + type Target: Turned; + fn turn(self) -> Self::Target; + fn turn_box(self: Box) -> Box; + fn turn_box_sized(self: Box) -> Box + where + Self: Sized, + { + Box::new(self.turn()) + } +} +struct U(usize); +struct T(usize); +impl Turned for T { + fn say(&self) { + print!("I'm T({})", self.0) + } +} +impl Unturned for U { + type Target = T; + fn turn(self) -> T { + T(self.0) + } + fn turn_box(self: Box) -> Box { + self.turn_box_sized() + } +} +struct AnyhowUnturned { + inner: Box>, +} +struct AnyhowTurned { + inner: Box, +} +impl Turned for AnyhowTurned { + fn say(&self) { + self.inner.say() + } +} +impl Unturned for AnyhowUnturned { + type Target = AnyhowTurned; + fn turn(self) -> AnyhowTurned { + AnyhowTurned { + inner: self.inner.turn_box(), + } + } + + fn turn_box(self: Box) -> Box { + self.turn_box_sized() + } +} +impl> From for AnyhowUnturned { + fn from(value: U) -> Self { + Self { + inner: Box::new(value), + } + } +} +struct AnyhowUnturned2 { + inner: AnyhowTurned, +} + +fn main() { + let unturned: AnyhowUnturned<_> = U(1).into(); +} + +// pub struct AnyhowLauncher<'a, Runtime: VmRuntime + 'a> { +// pub launcher: Box + 'a>, +// } + +// impl<'a, Runtime: VmRuntime + 'a> AnyhowLauncher<'a, Runtime> { +// pub fn new(launcher: impl VmLauncher + 'a) -> Self +// where +// Launcher: VmLauncher + 'a, +// { +// Self { +// launcher: Box::new(launcher), +// } +// } +// } + +// /// ! Box不能充当`VmLauncher`的参数:未实现`VmRuntime` +// impl<'a, Runtime: VmRuntime + 'a> VmLauncher> for AnyhowLauncher<'a, Runtime> { +// fn launch(self) -> AnyhowRuntime<'a> { +// AnyhowRuntime { +// inner: Box::new(self.launcher.launch()), +// } +// } +// } + +// struct AnyhowRuntime<'a> { +// inner: Box, +// } + +// impl AnyhowRuntime<'_> { +// fn new(inner: impl VmRuntime) -> Self { +// Self { +// inner: Box::new(inner), +// } +// } +// } + +// impl VmRuntime for AnyhowRuntime<'_> { +// fn input_cmd(&mut self, cmd: navm::cmd::Cmd) -> anyhow::Result<()> { +// self.inner.input_cmd(cmd) +// } + +// fn fetch_output(&mut self) -> anyhow::Result { +// self.inner.fetch_output() +// } + +// fn try_fetch_output(&mut self) -> anyhow::Result> { +// self.inner.try_fetch_output() +// } + +// fn terminate(self) -> anyhow::Result<()> { +// self.inner.terminate() +// } +// } diff --git a/src/cli_support/cin_search/impls_path_builder/mod.rs b/src/cli_support/cin_search/impls_path_builder/mod.rs new file mode 100644 index 0000000..2d47af7 --- /dev/null +++ b/src/cli_support/cin_search/impls_path_builder/mod.rs @@ -0,0 +1,74 @@ +//! 存储各CIN的「路径构建器」 +//! * ✅OpenNARS +//! * ✅ONA +//! TODO: PyNARS +//! TODO: CXinNARS +//! * 🚩【2024-03-31 01:27:09】其它接口完成度不高的CIN,暂时弃了 + +use crate::cli_support::cin_search::{ + name_match::is_name_match, path_builder::CinPathBuilder, path_walker::PathWalker, +}; +use navm::vm::{VmLauncher, VmRuntime}; +use std::path::Path; + +util::mods! { + // OpenNARS + use pub path_builder_opennars; + // ONA + use pub path_builder_ona; +} + +// 深入条件 +pub fn file_name_matches(path: &Path, name: &str) -> bool { + path.file_name().is_some_and(|name_os| { + name_os + .to_str() + .is_some_and(|name_str| is_name_match(name, name_str)) + }) +} + +/// 从遍历者中找到匹配的所有启动器 +/// * 🎯仅搜索出「可能有效,故构建好」的启动器 +pub fn launchers_from_walker>( + path_walker: impl PathWalker, + path_builder: impl CinPathBuilder, +) -> Vec<(L, usize)> { + path_walker + .to_iter_fn() + .filter_map(Result::ok) + .filter_map(|p| path_builder.try_construct_from_path(&p)) + .collect::>() +} + +/// 类似[`launchers_from_walker`],但根据返回的「匹配度」从高到底排序 +pub fn launchers_from_walker_sorted>( + path_walker: impl PathWalker, + path_builder: impl CinPathBuilder, +) -> Vec { + // 获取 & 排序 + let mut launchers = launchers_from_walker(path_walker, path_builder); + launchers.sort_by(|(_, a), (_, b)| b.cmp(a)); // ←此处是倒序 + // 提取左侧元素 + launchers.into_iter().map(|(l, _)| l).collect::>() +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::cli_support::cin_search::path_walker::PathWalkerV1; + use std::env::current_dir; + + #[test] + fn test() { + let path_walker = PathWalkerV1::new(¤t_dir().unwrap(), |path| { + file_name_matches(path, "nars") + }) + .unwrap(); + + dbg!(launchers_from_walker_sorted( + path_walker, + PathBuilderOpenNARS + )); + } +} diff --git a/src/cli_support/cin_search/impls_path_builder/path_builder_ona.rs b/src/cli_support/cin_search/impls_path_builder/path_builder_ona.rs new file mode 100644 index 0000000..07d05fd --- /dev/null +++ b/src/cli_support/cin_search/impls_path_builder/path_builder_ona.rs @@ -0,0 +1,85 @@ +//! 用于ONA的路径构建器 + +use crate::cli_support::cin_search::{ + name_match::{name_match, name_match_only_contains}, + path_builder::CinPathBuilder, +}; +use crate::{cin_implements::ona::ONA, runtimes::CommandVmRuntime}; +use nar_dev_utils::{if_return, OptionBoost}; +use std::path::Path; + +/// ONA路径构建器 +/// * 🎯判别路径并构建ONA启动器 +pub struct PathBuilderONA; + +impl PathBuilderONA { + // 匹配文件名 + #[inline(always)] + fn match_name(name: &str) -> usize { + // 常用的`NAR.exe` + (if name == "NAR.exe" { 10 } else { 0 }) + // 综合,只需「均不满足⇒0」即可 + + name_match("ona", name) + + name_match_only_contains("opennars-for-application", name) + + name_match_only_contains("opennars_for_application", name) + } + + /// 检查文件匹配度 + fn valid_exe(path: &Path) -> usize { + // ! 不一定是本地存在的文件 + if_return! { !path.extension().is_some_and(|ex| ex == "exe") => 0} + // 名称匹配`ona` + path.file_name().map_unwrap_or( + |name_os| name_os.to_str().map_unwrap_or(Self::match_name, 0), + 0, + ) + } +} + +impl CinPathBuilder for PathBuilderONA { + type Runtime = CommandVmRuntime; + type Launcher = ONA; + + fn match_path(&self, path: &Path) -> usize { + // ! 与本地文件系统有关 + // 不是本地的文件⇒0 + if_return! { !path.is_file() => 0 } + // 否则⇒查看exe匹配度 + Self::valid_exe(path) + } + + fn construct_from_path(&self, path: &Path) -> Self::Launcher { + ONA::new(path) + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use nar_dev_utils::{f_parallel, fail_tests}; + use std::path::Path; + + /// 工具/测试单个路径 + fn test_matched(path: &str) { + let path = Path::new(path); + assert!(dbg!(PathBuilderONA::valid_exe(path)) > 0); + } + + /// 测试/名称匹配 + #[test] + fn test_match() { + f_parallel![ + test_matched; + "../NAR.exe"; + "../opennars-for-applications.exe"; + "../ona.exe"; + "ona_old.exe"; + ]; + } + + fail_tests! { + 无效扩展名 test_matched("../opennars.exe"); + 无效名称 test_matched("../NARust.exe"); + } +} diff --git a/src/cli_support/cin_search/impls_path_builder/path_builder_opennars.rs b/src/cli_support/cin_search/impls_path_builder/path_builder_opennars.rs new file mode 100644 index 0000000..ad6350d --- /dev/null +++ b/src/cli_support/cin_search/impls_path_builder/path_builder_opennars.rs @@ -0,0 +1,82 @@ +//! 用于OpenNARS的路径构建器 + +use crate::{ + cin_implements::opennars::OpenNARS, + cli_support::cin_search::{name_match::name_match, path_builder::CinPathBuilder}, + runtimes::CommandVmRuntime, +}; +use nar_dev_utils::{if_return, OptionBoost}; +use std::path::Path; + +/// OpenNARS路径构建器 +/// * 🎯判别路径并构建OpenNARS启动器 +pub struct PathBuilderOpenNARS; + +impl PathBuilderOpenNARS { + // 匹配文件名 + #[inline(always)] + fn match_name(name: &str) -> usize { + // 二者综合,只需「二者均不满足⇒0」即可 + name_match("opennars", name) + name_match("open_nars", name) + } + + /// 检查文件匹配度 + fn valid_jar(path: &Path) -> usize { + // ! 不一定是本地存在的文件 + if_return! { !path.extension().is_some_and(|ex| ex == "jar") => 0} + // 名称匹配`opennars` + path.file_name().map_unwrap_or( + |name_os| name_os.to_str().map_unwrap_or(Self::match_name, 0), + 0, + ) + } +} + +impl CinPathBuilder for PathBuilderOpenNARS { + type Runtime = CommandVmRuntime; + type Launcher = OpenNARS; + + fn match_path(&self, path: &Path) -> usize { + // ! 与本地文件系统有关 + // 不是本地的文件⇒0 + if_return! { !path.is_file() => 0 } + // 否则⇒查看jar匹配度 + Self::valid_jar(path) + } + + fn construct_from_path(&self, path: &Path) -> Self::Launcher { + OpenNARS::new(path) + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use nar_dev_utils::{f_parallel, fail_tests}; + use std::path::Path; + + /// 工具/测试单个路径 + fn test_matched(path: &str) { + let path = Path::new(path); + assert!(dbg!(PathBuilderOpenNARS::valid_jar(path)) > 0); + } + + /// 测试/名称匹配 + #[test] + fn test_match() { + f_parallel![ + test_matched; + "../opennars-304-T-modified.jar"; + "../OpenNARS-3.0.4-Snapshot.jar"; + "../opennars.jar"; + "open_nars.jar"; + "opennars-3.0.4-SNAPSHOT.jar"; + ]; + } + + fail_tests! { + 无效扩展名 test_matched("../opennars-304-T-modified.jar.exe"); + 无效名称 test_matched("../ona-T-modified.jar"); + } +} diff --git a/src/cli_support/cin_search/mod.rs b/src/cli_support/cin_search/mod.rs new file mode 100644 index 0000000..feb8598 --- /dev/null +++ b/src/cli_support/cin_search/mod.rs @@ -0,0 +1,22 @@ +//! CIN启动器中有关「CIN路径构建(搜索)」的逻辑 +//! * ✨根据「CIN路径构建器」搜索(判别)系统中已存在的CIN实现(并自动构建) +//! * 🚩输入:搜索起点(一般是编译后exe所在文件夹) +//! * 🚩输出:NAVM启动器列表 +//! * ❓【2024-03-30 19:12:29】是否要考虑返回更细化的「CIN实例位置」而非「CIN启动器」,以避免额外的性能开销? + +// 导出模块 +util::mods! { + // anyhow | 弃用 + // anyhow_vm; + // 名称匹配 + pub name_match; + + // 路径遍历器 + pub path_walker; + + // 路径构建器 + pub path_builder; + + // 路径构建器的各CIN实现 + "cin_implements" => pub impls_path_builder; +} diff --git a/src/cli_support/cin_search/name_match.rs b/src/cli_support/cin_search/name_match.rs new file mode 100644 index 0000000..e6a0341 --- /dev/null +++ b/src/cli_support/cin_search/name_match.rs @@ -0,0 +1,79 @@ +//! 封装「名称匹配」逻辑 +//! * 🎯用于「语义化」「模糊化」的字符串匹配 +//! * ✨无视大小写匹配 +//! * 📄"opennars"匹配"OpenNARS" +//! * ✨「含于」与「包含」匹配 +//! * 📄"opennars"匹配"OpenNARS 3.0.4"(含于)与"nars"(包含) +//! * ✨返回一个「匹配度」的数值 +//! * `0`统一表示「未匹配」 +//! * 剩余值可用于排序 + +use nar_dev_utils::{first, if_return}; + +/// 名称匹配 +/// * 🎯用于「语义化」「模糊化」的字符串匹配 +/// * ✨无视大小写匹配 +/// * 📄"opennars"匹配"OpenNARS" +/// * ✨「含于」与「包含」匹配 +/// * 📄"opennars"匹配"OpenNARS 3.0.4"(含于)与"nars"(包含) +/// * ⚙️返回一个「匹配度」的数值 +/// * `0`统一表示「未匹配」 +/// * 剩余值可用于排序 +pub fn name_match(name: &str, target: &str) -> usize { + // 完全相等⇒最高级 + if_return! { + // 完全相等⇒高 + name == target => 6 + // 包含于⇒中 + target.contains(name) => 4 + // 包含⇒低 + name.contains(target) => 2 + } + + // 忽略大小写的情况 | 忽略大小写,降一个匹配度 + let name = name.to_lowercase(); + let target = target.to_lowercase(); + + first! { + // 完全相等⇒高 + name == target => 5, + // 包含于⇒中 + target.contains(&name) => 3, + // 包含⇒低 + name.contains(&target) => 1, + // 否则⇒不匹配 + _ => 0, + } +} + +/// 名称匹配/仅「含于」 +/// * 🚩与[`name_match`]类似,但仅「含于」而不适配「包含」 +/// * 🎯用于「长串名称作为内部关键词」的匹配 +pub fn name_match_only_contains(name: &str, target: &str) -> usize { + // 完全相等⇒最高级 + if_return! { + // 完全相等⇒高 + name == target => 4 + // 含于⇒低 + target.contains(name) => 2 + } + + // 忽略大小写的情况 | 忽略大小写,降一个匹配度 + let name = name.to_lowercase(); + let target = target.to_lowercase(); + + first! { + // 完全相等⇒高 + name == target => 3, + // 含于⇒低 + target.contains(&name) => 1, + // 否则⇒不匹配 + _ => 0, + } +} + +/// 判断「是否匹配」,不管「匹配度」多少 +/// * 🚩直接复用逻辑,以牺牲一定性能为代价 +pub fn is_name_match(name: &str, target: &str) -> bool { + name_match(name, target) > 0 +} diff --git a/src/cli_support/cin_search/path_builder.rs b/src/cli_support/cin_search/path_builder.rs new file mode 100644 index 0000000..004805f --- /dev/null +++ b/src/cli_support/cin_search/path_builder.rs @@ -0,0 +1,66 @@ +//! 统一的「路径构建器」逻辑 + +use navm::vm::{VmLauncher, VmRuntime}; +use std::path::Path; + +/// CIN路径构建器 +/// * 🚩本身不承担「遍历路径」的任务,只负责 +/// * 📌判断是否「可以用于构建NAVM运行时」 +/// * 📌从某路径构建「NAVM启动器」 +/// * ❌【2024-03-30 19:05:48】放弃「通用启动器/通用运行时」的适配尝试 +/// * 📝目前[`CinSearch::Launcher`]总是要带上[`CinSearch::Runtime`]作类型参数 +/// * 📌堵点:难以定义一个使用`Box>`封装的`AnyhowVmLauncher`类型 +/// * 📍问题领域:特征对象及其转换 +/// * ❓一个可能的参考:[`anyhow`]对「错误类型」的统一 +/// * ❌【2024-03-30 21:24:10】尝试仍然失败:有关`Box`的所有权转换问题 +/// * 🔗技术参考1: +pub trait CinPathBuilder { + /// 搜索结果的启动器类型 + /// * 📌启动后变为[`CinSearch::Runtime`]运行时类型 + type Launcher: VmLauncher; + + /// 搜索结果的运行时类型 + type Runtime: VmRuntime; + + /// 路径匹配 + /// * 🎯匹配某路径(可能是文件夹,也可能是文件)是否可用于「构建NAVM启动器」 + /// * ⚠️与**该路径是否存在**有关 + /// * 📌需要访问本地文件系统 + /// * 📄一些CIN可能要求判断其子目录的文件(附属文件) + /// * ⚙️返回「匹配度」 + /// * 📌`0`⇒不匹配,其它⇒不同程度的匹配 + /// * 🎯对接「名称匹配」中的「匹配度」 + /// * ✨可用于后续排序 + fn match_path(&self, path: &Path) -> usize; + + /// 用于检查路径是否匹配 + /// * 🔗参见[`match_path`] + fn is_path_matched(&self, path: &Path) -> bool { + self.match_path(path) > 0 + } + + /// 路径构建 + /// * 🎯从某个路径构建出一个NAVM启动器 + /// * ✅除路径以外,其它参数可作默认 + /// * 📄OpenNARS的「Java最大堆大小」 + /// + /// # Panics + /// + /// ⚠️需要保证[`is_path_matched`]为真 + /// * 为假时可能`panic` + fn construct_from_path(&self, path: &Path) -> Self::Launcher; + + /// 尝试路径构建 + /// * 🚩返回一个[`Option`] + /// * 能构建⇒返回构建后的结果 `Some((启动器, 匹配度))` + /// * 无法构建⇒返回[`None`] + #[inline] + fn try_construct_from_path(&self, path: &Path) -> Option<(Self::Launcher, usize)> { + match self.match_path(path) { + // 不匹配⇒无 + 0 => None, + // 匹配⇒元组 + n => Some((self.construct_from_path(path), n)), + } + } +} diff --git a/src/cli_support/cin_search/path_walker.rs b/src/cli_support/cin_search/path_walker.rs new file mode 100644 index 0000000..5b2e250 --- /dev/null +++ b/src/cli_support/cin_search/path_walker.rs @@ -0,0 +1,240 @@ +//! 路径遍历器 +//! * 🎯用于分离「路径查找」与「CIN识别」两功能 +//! * 📌「路径遍历器」负责「提供路径,并有选择地 深入/跳出 路径」 + +use anyhow::{Error, Result}; +use std::path::{Path, PathBuf}; + +/// 抽象的「路径遍历」特征 +/// * ✨允许「迭代出下一个路径」 +/// * 🏗️后续可能会添加更多特性,如「根据结果调整遍历策略」等 +pub trait PathWalker { + /// ✨返回「下一个路径」 + /// * 可能为空,也可能返回错误 + fn next_path(&mut self) -> Result>; + + /// 类似迭代器的`next`方法 + /// * 🎯对标`Iterator>` + /// * 🚩【2024-03-31 01:03:04】是「没法为`impl PathWalker`自动实现`Iterator`」的补偿 + fn iter_next_path(&mut self) -> Option> { + match self.next_path() { + // 正常情况 + Ok(Some(path)) => Some(Ok(path)), + // 中途报错⇒返回错误 + Err(e) => Some(Err(e)), + // 终止⇒真正终止 + Ok(None) => None, + } + } + + /// 利用[`std::iter::from_fn`]将自身转换为迭代器,而无需实现[`Iterator`]特征 + /// * 🎯便于在`impl PathWalker`中使用 + #[inline] + fn to_iter_fn<'a>(mut self) -> impl Iterator> + 'a + where + Self: Sized + 'a, + { + std::iter::from_fn(move || self.iter_next_path()) + } +} + +/// 初代路径遍历器 +/// * ✨使用「渐近回退性扫描」机制,总体为「深度优先」 +/// * 📌「起始目录」一般为exe所在目录 +/// * 🚩从「起始目录」开始,扫描其下子目录 +/// * 递归深入、迭代出文件夹与文件 +/// * 🚩若「起始目录」已扫描完毕,向上「条件扫描」父目录 +/// * 遍历其【直接包含】的文件/文件夹 +/// * 若有满足特定「可深入条件」的文件夹,则深入扫描该文件夹(仍然是「条件扫描」) +/// * 🚩父目录扫描完毕后,继续扫描父目录 +pub struct PathWalkerV1<'a> { + // 父目录堆栈 + ancestors_stack: Vec, + + /// 待遍历目录的堆栈 + to_visit_stack: Vec, + + /// 可深入条件 + deep_criterion: Box bool + Send + Sync + 'a>, + + /// 当前在遍历目录的迭代器 + current_dir_iter: Box>>, +} + +impl<'a> PathWalkerV1<'a> { + pub fn new( + start: &Path, + deep_criterion: impl Fn(&Path) -> bool + Send + Sync + 'a, + ) -> Result { + // 计算根目录 + // * 🚩不是文件夹⇒向上寻找根目录 + let mut root = start; + while !root.is_dir() { + root = root.parent().unwrap(); + } + // 构造路径堆栈 + let mut ancestors_stack = root.ancestors().map(Path::to_owned).collect::>(); + ancestors_stack.reverse(); // 从「当前→根」转为「根→当前」,先遍历当前,再遍历根 + // 拿出目录 + let root = match ancestors_stack.pop() { + Some(path) => path, + None => return Err(Error::msg("起始目录无效")), + }; + let deep_criterion = Box::new(deep_criterion); + let current_dir_iter = Box::new(Self::new_path_iter(&root)?); + Ok(Self { + ancestors_stack, + to_visit_stack: vec![], // 空栈初始化 + deep_criterion, + current_dir_iter, + }) + } + + /// ✨构造路径迭代器 + /// * 🎯尽可能让异常变得可处理:避免`unwrap` + fn new_path_iter(path: &Path) -> Result>> { + Ok(std::fs::read_dir(path)?.map(|e| match e { + Ok(entry) => Ok(entry.path()), + Err(e) => Err(e.into()), + })) + } + + /// 可能返回[`None`]的[`Self::next`] + /// * 🎯应对「切换到父目录的迭代器后,首个迭代结果还是[`None`]」的情况 + /// * 🚩解决方案:再次[`Self::poll_path`] + fn poll_path(&mut self) -> PathPollResult { + // ! ❌【2024-03-30 22:34:04】目前没法稳定地使用`?` + match self.current_dir_iter.next() { + // 正常情况 + Some(Ok(path)) => { + // 如果「值得深入」⇒预备在后续深入 + if path.is_dir() && (self.deep_criterion)(&path) { + self.to_visit_stack.push(path.clone()) + } + // 返回 + PathPollResult::Some(path) + } + // 中途报错情况 + Some(Err(e)) => PathPollResult::Err(e), + // 没有⇒尝试切换路径 + None => self.try_switch_current_path(), + } + } + + /// 尝试切换路径 + /// * 切换到一个新的路径 + fn try_switch_current_path(&mut self) -> PathPollResult { + match self.to_visit_stack.pop() { + // 「待检查路径」有⇒尝试pop一个,构造并切换到新的迭代器 + Some(path) => match self.change_current_path(&path) { + Ok(()) => PathPollResult::None, // 构造了就收手,无需立马查看里边有无路径 + Err(e) => PathPollResult::Err(e), + }, + // 「待检查路径」没有⇒尝试从「祖先路径」中尝试pop一个 + None => match self.ancestors_stack.pop() { + // 「祖先路径」有⇒尝试pop一个,构造并切换到新的迭代器 + Some(path) => match self.change_current_path(&path) { + Ok(()) => PathPollResult::None, // 构造了就收手,无需立马查看里边有无路径 + Err(e) => PathPollResult::Err(e), + }, // 「祖先路径」没有⇒终止 + None => PathPollResult::Ended, + }, + } + } + + /// 尝试更改到某个目录(的迭代器) + fn change_current_path(&mut self, path: &Path) -> Result<()> { + let iter = Self::new_path_iter(path)?; + self.current_dir_iter = Box::new(iter); + Ok(()) + } +} + +/// 枚举「路径遍历」结果 +/// * 🎯用于「路径遍历器」的返回值 +pub enum PathPollResult { + /// 拿到了一个路径 + Some(PathBuf), + /// 尝试拿,但没拿到路径 + None, + /// 尝试拿,但发生错误 + Err(Error), + /// 结束了 + Ended, +} + +impl From> for PathPollResult { + fn from(value: Option) -> Self { + match value { + Some(path) => Self::Some(path), + None => Self::None, + } + } +} + +impl From> for PathPollResult { + fn from(value: Result) -> Self { + match value { + Ok(path) => Self::Some(path), + Err(e) => Self::Err(e), + } + } +} + +impl PathWalker for PathWalkerV1<'_> { + fn next_path(&mut self) -> Result> { + // 持续不断poll自身,压缩掉其中的`None`项 + loop { + match self.poll_path() { + // 正常返回路径 + PathPollResult::Some(path) => break Ok(Some(path)), + // 没有⇒继续循环(压缩掉) + PathPollResult::None => continue, + // 报错⇒返回错误 + PathPollResult::Err(e) => break Err(e), + // 终止⇒返回终止信号 + PathPollResult::Ended => break Ok(None), + } + } + } +} + +/// 实现迭代器,返回所有「搜索结果」 +impl Iterator for PathWalkerV1<'_> { + type Item = Result; + fn next(&mut self) -> Option> { + self.iter_next_path() + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::cli_support::cin_search::name_match::is_name_match; + use std::env::current_dir; + + fn _test_path_walker_v1(start: impl Into) { + // 起始目录 + let start = &start.into(); + // 深入条件 + fn deep_criterion(path: &Path) -> bool { + path.file_name() + .is_some_and(|name| name.to_str().is_some_and(|s| is_name_match("nars", s))) + } + // 构建遍历者,加上条件 + let walker = PathWalkerV1::new(start, deep_criterion).unwrap(); + // 打印遍历者的「祖先列表」 + println!("{:?}", walker.ancestors_stack); + // 遍历 + for path in walker { + println!("{path:?}"); + } + } + + #[test] + fn test_path_walker_v1() { + // 测试当前路径 + _test_path_walker_v1(current_dir().unwrap()); + } +} diff --git a/src/cli_support/error_handling_boost.rs b/src/cli_support/error_handling_boost.rs new file mode 100644 index 0000000..6b6e282 --- /dev/null +++ b/src/cli_support/error_handling_boost.rs @@ -0,0 +1,36 @@ +//! 增强的快捷错误处理 +//! * 🎯用于(在命令行)快速处理、输出各种错误 + +use crate::cli_support::io::output_print::OutputType; +use anyhow::{anyhow, Error}; +use std::fmt::Debug; + +/// 打印错误 +/// * 🚩在标准错误中打印基于[`Debug`]的信息 +/// * 🎯快速表示「报错而非panic」 +/// * 🚩【2024-04-02 18:59:19】不建议使用:不应向用户打印大量错误堆栈信息 +/// * ✨替代用法可参考[`crate::eprintln_cli`] +#[deprecated = "不建议使用:不应向用户打印大量错误堆栈信息"] +pub fn println_error(e: &impl Debug) { + // ! 无法在此直接使用:macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths + // * 🚩【2024-04-02 16:33:47】目前处理办法:直接展开 + println!("{}", OutputType::Error.format_line(&format!("{e:?}"))); +} + +/// 打印错误 +/// * 🚩在标准错误中打印基于[`Debug`]的信息 +/// * 🎯快速表示「报错而非panic」 +/// * 🎯用于「传入所有权而非不可变引用」的[`Result::unwrap_or_else`] +/// * 🚩【2024-04-02 18:59:19】不建议使用:不应向用户打印大量错误堆栈信息 +/// * ✨替代用法可参考[`crate::eprintln_cli`] +#[deprecated = "不建议使用:不应向用户打印大量错误堆栈信息"] +pub fn println_error_owned(e: impl Debug) { + println!("{}", OutputType::Error.format_line(&format!("{e:?}"))); +} + +/// 将错误转换为[`anyhow::Error`] +/// * 🚩将错误转换为[`Debug`]信息,装入[`anyhow::Error`]中 +/// * 🎯在线程通信中安全抛出未实现[`Send`]的[`std::sync::PoisonError`] +pub fn error_anyhow(e: impl Debug) -> Error { + anyhow!("{e:?}") +} diff --git a/src/cli_support/io/mod.rs b/src/cli_support/io/mod.rs new file mode 100644 index 0000000..f29c48f --- /dev/null +++ b/src/cli_support/io/mod.rs @@ -0,0 +1,17 @@ +//! 用于管理启动器的输入输出 +//! * ✨终端美化相关 +//! + +util::mods! { + // 输出打印 + pub output_print; + + // 读取行迭代器 + pub readline_iter; + + // NAVM输出缓存 + pub navm_output_cache; + + // Websocket支持 + pub websocket; +} diff --git a/src/cli_support/io/navm_output_cache.rs b/src/cli_support/io/navm_output_cache.rs new file mode 100644 index 0000000..ed6e862 --- /dev/null +++ b/src/cli_support/io/navm_output_cache.rs @@ -0,0 +1,118 @@ +//! NAVM输出缓存 +//! * 🎯一站式存储、展示与管理NAVM的输出 +//! * 🎯可被其它二进制库所复用 + +use crate::{ + cli_support::error_handling_boost::error_anyhow, + output_handler::flow_handler_list::{FlowHandlerList, HandleResult}, + test_tools::VmOutputCache, +}; +use anyhow::Result; +use nar_dev_utils::ResultBoost; +use navm::output::Output; +use std::{ + ops::ControlFlow, + sync::{Arc, Mutex, MutexGuard}, +}; + +/// 线程间可变引用计数的别名 +pub type ArcMutex = Arc>; + +/// 输出缓存 +/// * 🎯统一「加入输出⇒打印输出」的逻辑 +/// * 🚩仅封装一个[`Vec`],而不对其附加任何[`Arc`]、[`Mutex`]的限定 +/// * ❌【2024-04-03 01:43:13】[`Arc`]必须留给[`RuntimeManager`]:需要对其中键的值进行引用 +#[derive(Debug)] +pub struct OutputCache { + /// 内部封装的输出数组 + /// * 🚩【2024-04-03 01:43:41】不附带任何包装类型,仅包装其自身 + pub(crate) inner: Vec, + + /// 流式侦听器列表 + /// * 🎯用于功能解耦、易分派的「NAVM输出处理」 + /// * 📌可在此过程中对输出进行拦截、转换等操作 + /// * 🎯CLI输出打印 + /// * 🎯Websocket输出回传(JSON) + pub output_handlers: FlowHandlerList, +} + +/// 功能实现 +impl OutputCache { + /// 构造函数 + pub fn new(inner: Vec) -> Self { + Self { + inner, + output_handlers: FlowHandlerList::new(), + } + } + + /// 不可变借用内部 + pub fn borrow_inner(&self) -> &Vec { + &self.inner + } + + /// 可变借用内部 + pub fn borrow_inner_mut(&mut self) -> &mut Vec { + &mut self.inner + } + + /// 默认[`Arc`]<[`Mutex`]> + pub fn default_arc_mutex() -> ArcMutex { + Arc::new(Mutex::new(Self::default())) + } + + /// 从[`Arc`]<[`Mutex`]>中解锁 + pub fn unlock_arc_mutex(arc_mutex: &mut ArcMutex) -> Result> { + arc_mutex.lock().transform_err(error_anyhow) + } + + /// 静默存入输出 + /// * 🎯内部可用的「静默存入输出」逻辑 + /// * 🚩【2024-04-03 01:07:55】不打算封装了 + pub fn put_silent(&mut self, output: Output) -> Result<()> { + // 加入输出 + self.inner.push(output); + Ok(()) + } +} + +/// 默认构造:空数组 +impl Default for OutputCache { + fn default() -> Self { + Self::new(vec![]) + } +} + +/// 实现「输出缓存」 +/// * 🚩【2024-04-03 14:33:50】不再涉及任何[`Arc`]或[`Mutex`] +impl VmOutputCache for OutputCache { + /// 存入输出 + /// * 🎯统一的「打印输出」逻辑 + /// * 🚩【2024-04-03 01:07:55】不打算封装了 + fn put(&mut self, output: Output) -> Result<()> { + // 交给处理者处理 + let r = self.output_handlers.handle(output); + match r { + // 通过⇒静默加入输出 + HandleResult::Passed(output) => self.put_silent(output), + // 被消耗⇒提示 + HandleResult::Consumed(index) => Ok(println!("NAVM输出在[{index}]位置被拦截。")), + } + } + + /// 遍历输出 + /// * 🚩不是返回迭代器,而是用闭包开始计算 + fn for_each(&self, mut f: impl FnMut(&Output) -> ControlFlow) -> Result> { + // 遍历 + for output in self.inner.iter() { + // 基于控制流的运行 + match f(output) { + ControlFlow::Break(value) => return Ok(Some(value)), + ControlFlow::Continue(()) => {} + } + } + + // 返回 + Ok(None) + } +} diff --git a/src/cli_support/io/output_print.rs b/src/cli_support/io/output_print.rs new file mode 100644 index 0000000..884919f --- /dev/null +++ b/src/cli_support/io/output_print.rs @@ -0,0 +1,192 @@ +//! 输出打印 +//! * 🎯用于规范化、统一、美化CLI输出 +//! * 📌不仅仅是NAVM的输出 +//! +//! ## 输出美化参考 +//! +//! 输出美化逻辑参考了如下Julia代码: +//! +//! ```julia +//! """ +//! 用于高亮「输出颜色」的字典 +//! """ +//! const output_color_dict = Dict([ +//! NARSOutputType.IN => :light_white +//! NARSOutputType.OUT => :light_white +//! NARSOutputType.EXE => :light_cyan +//! NARSOutputType.ANTICIPATE => :light_yellow +//! NARSOutputType.ANSWER => :light_green +//! NARSOutputType.ACHIEVED => :light_green +//! NARSOutputType.INFO => :white +//! NARSOutputType.COMMENT => :white +//! NARSOutputType.ERROR => :light_red +//! NARSOutputType.OTHER => :light_black # * 未识别的信息 +//! # ! ↓这俩是OpenNARS附加的 +//! "CONFIRM" => :light_blue +//! "DISAPPOINT" => :light_magenta +//! ]) +//! +//! """ +//! 用于分派「颜色反转」的集合 +//! """ +//! const output_reverse_color_dict = Set([ +//! NARSOutputType.EXE +//! NARSOutputType.ANSWER +//! NARSOutputType.ACHIEVED +//! ]) +//! ``` +//! +//! * 最后更新:【2024-04-02 15:54:23】 +//! * 参考链接: + +use colored::Colorize; +use navm::output::Output; +use std::fmt::Display; + +/// 统一的「CLI输出类型」 +#[derive(Debug, Clone, Copy)] +pub enum OutputType<'a> { + /// NAVM输出 + /// * 🚩【2024-04-02 15:42:44】目前因NAVM的[`Output`]仅有`enum`结构而无「类型」标签, + /// * 无法复用NAVM的枚举 + Vm(&'a str), + /// CLI错误 + Error, + /// CLI警告 + Warn, + /// CLI信息 + Info, + /// CLI日志 + Log, + /// CLI debug + Debug, +} + +impl OutputType<'_> { + /// 自身的字符串形式 + /// * 🎯作为输出的「头部」 + pub fn as_str(&self) -> &str { + match self { + OutputType::Vm(s) => s, + OutputType::Error => "ERROR", + OutputType::Warn => "WARN", + OutputType::Info => "INFO", + OutputType::Debug => "DEBUG", + OutputType::Log => "LOG", + } + } + + /// 格式化CLI输出 + /// * 🎯封装标准输出形式:`[类型] 内容` + /// * 🎯封装命令行美化逻辑 + #[inline(always)] + pub fn format_line(&self, msg: &str) -> impl Display { + self.to_colored_str(format!("[{}] {}", self.as_str(), msg)) + } + + /// 从NAVM输出格式化 + /// * 🎯封装「从NAVM输出打印」 + #[inline(always)] + pub fn format_from_navm_output(out: &Output) -> impl Display { + OutputType::from(out).format_line(out.raw_content().trim_end()) + } + + /// 基于[`colored`]的输出美化 + /// * 🎯用于CLI的彩色输出 + /// * 🔗参考Julia版本 + pub fn to_colored_str(&self, message: String) -> impl Display { + match self.as_str() { + // CLI独有 + "DEBUG" => message.bright_blue(), + "WARN" => message.bright_yellow(), + "LOG" => message.bright_black(), + // NAVM输出 + "IN" | "OUT" => message.bright_white(), + "EXE" => message.bright_cyan().reversed(), + "ANSWER" | "ACHIEVED" => message.bright_green().reversed(), + "INFO" => message.cyan(), + "COMMENT" => message.white(), + "ERROR" => message.red(), + "TERMINATED" => message.bright_white().reversed().blink(), + // ↓OpenNARS附加 + "ANTICIPATE" => message.bright_yellow(), + "CONFIRM" => message.bright_blue(), + "DISAPPOINT" => message.bright_magenta(), + // 默认 / 其它 + "OTHER" => message.bright_black(), + _ => message.bright_white(), + } + // 参考Julia,始终加粗 + .bold() + } + + /// ✨格式化打印CLI输出 + /// * 🎯BabelNAR CLI + #[inline] + pub fn print_line(&self, message: &str) { + println!("{}", self.format_line(message)); + } + + /// ✨格式化打印NAVM输出 + /// * 🎯BabelNAR CLI + #[inline] + pub fn print_from_navm_output(out: &Output) { + println!("{}", Self::format_from_navm_output(out)); + } + + /// ✨格式化打印CLI输出(标准错误) + /// * 🎯BabelNAR CLI + #[inline] + pub fn eprint_line(&self, message: &str) { + eprintln!("{}", self.format_line(message)); + } + + /// ✨格式化打印NAVM输出(标准错误) + /// * 🎯BabelNAR CLI + #[inline] + pub fn eprint_from_navm_output(out: &Output) { + eprintln!("{}", Self::format_from_navm_output(out)); + } +} + +/// 快捷打印宏 +#[macro_export] +macro_rules! println_cli { + ([$enum_type_name:ident] $($tail:tt)*) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::$enum_type_name.print_line(&format!($($tail)*)); + }; + ($navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::print_from_navm_output($navm_output); + }; +} + +/// 快捷打印宏/标准错误 +#[macro_export] +macro_rules! eprintln_cli { + ([$enum_type_name:ident] $($tail:tt)*) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::$enum_type_name.eprint_line(&format!($($tail)*)); + }; + ($navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::eprint_from_navm_output($navm_output); + }; +} + +/// 快捷打印宏/当输出为`Err`时打印,当Ok时为值 +#[macro_export] +macro_rules! if_let_err_eprintln_cli { + { $value:expr => $e:ident => $($tail:tt)* } => { + if let Err($e) = $value { + eprintln_cli!($($tail)*); + } + }; +} + +impl<'a> From<&'a Output> for OutputType<'a> { + fn from(out: &'a Output) -> Self { + OutputType::Vm(out.type_name()) + } +} diff --git a/src/cli_support/io/readline_iter.rs b/src/cli_support/io/readline_iter.rs new file mode 100644 index 0000000..bfae0d0 --- /dev/null +++ b/src/cli_support/io/readline_iter.rs @@ -0,0 +1,61 @@ +//! 读取行迭代器 +//! * 🎯以迭代器的语法获取、处理用户输入 +//! * ❌【2024-04-03 14:28:02】放弃「泛型化改造」:[`Stdin`]能`read_line`,但却没实现[`std::io::BufRead`] + +use crate::cli_support::io::output_print::OutputType; +use std::io::{stdin, stdout, Result as IoResult, Stdin, Write}; + +/// 读取行迭代器 +/// * 🚩每迭代一次,请求用户输入一行 +/// * ✨自动清空缓冲区 +/// * ❌无法在【不复制字符串】的情况下实现「迭代出所输入内容」的功能 +/// * ❌【2024-04-02 03:49:56】无论如何都无法实现:迭代器物件中引入就必须碰生命周期 +/// * 🚩最终仍需复制字符串:调用处方便使用 +/// * ❓是否需要支持提示词 +#[derive(Debug)] +pub struct ReadlineIter { + /// 内置的「输入内容缓冲区」 + buffer: String, + /// 内置的「标准输入」 + stdin: Stdin, + /// 输入提示词 + prompt: String, +} + +impl ReadlineIter { + pub fn new(prompt: impl Into) -> Self { + Self { + buffer: String::new(), + stdin: stdin(), + prompt: prompt.into(), + } + } +} + +impl Default for ReadlineIter { + fn default() -> Self { + Self::new("") + } +} + +/// 实现迭代器 +impl Iterator for ReadlineIter { + type Item = IoResult; + + fn next(&mut self) -> Option { + // 清空缓冲区 + self.buffer.clear(); + // 打印提示词 + print!("{}", self.prompt); + if let Err(e) = stdout().flush() { + OutputType::Warn.print_line(&format!("无法冲洗输出: {e}")); + } + // 读取一行 + // * 📝`stdin()`是懒加载的,只会获取一次,随后返回的都是引用对象 + if let Err(e) = self.stdin.read_line(&mut self.buffer) { + return Some(Err(e)); + } + // 返回 + Some(IoResult::Ok(self.buffer.clone())) + } +} diff --git a/src/cli_support/io/websocket.rs b/src/cli_support/io/websocket.rs new file mode 100644 index 0000000..7d57e9d --- /dev/null +++ b/src/cli_support/io/websocket.rs @@ -0,0 +1,214 @@ +//! 基于[`ws`]为CLI提供Websocket IO支持 +//! * ✨简单的「地址生成」「服务端启动」等逻辑 +//! * ⚠️不涉及具体业务代码 +//! * 📝【2024-04-03 16:01:37】可以启动IPv6服务端,但尚且没有测试方法 +//! * 📌语法:`[主机地址]:连接端口` +//! * 📄示例:`[::]:3012` +//! * 🔗参考: + +use anyhow::Result; +use std::{ + fmt, + net::ToSocketAddrs, + thread::{self, JoinHandle}, +}; +use ws::{Factory, Handler, Sender, WebSocket}; + +/// 从「主机地址」与「连接端口」格式化到「完整地址」 +/// * ✨兼容IPv4 和 IPv6 +/// * 📌端口号为十六位无符号整数(0~65535,含两端) +pub fn to_address(host: &str, port: u16) -> String { + match is_ipv6_host(host) { + // IPv6 + true => format!("[{host}]:{port}"), + // IPv4 + false => format!("{host}:{port}"), + } +} + +/// 判断**主机地址**是否为IPv6地址 +/// * 🚩【2024-04-03 17:20:59】目前判断标准:地址中是否包含冒号 +/// * 📄`::1` +/// * 📄`fe80::abcd:fade:dad1` +pub fn is_ipv6_host(host: &str) -> bool { + host.contains(':') +} + +/// 生成一个Websocket监听线程 +/// * 🎯简单生成一个Websocket监听线程 +/// * ⚠️线程在生成后立即开始运行 +#[inline] +pub fn spawn_on(addr: A, message_listener: F) -> JoinHandle> +where + A: ToSocketAddrs + fmt::Debug + Send + Sync + 'static, + F: FnMut(Sender) -> H + Send + Sync + 'static, + H: Handler, +{ + spawn_server(addr, message_listener) +} + +/// 生成一个Websocket监听线程,使用特定的服务端 +/// * 🎯使用自定义的Websocket「工厂」[`Factory`]生成服务端与连接处理者(侦听器) +/// * 📄地址格式:`127.0.0.1:8080`、`localhost:8080` +/// * ⚠️线程在生成后立即开始运行 +/// * 📝与[`ws::listen`]不同的是:允许在[`factory`]处自定义各种连接、侦听逻辑 +/// * 🔗参考: +#[inline] +pub fn spawn_server(address: A, factory: F) -> JoinHandle> +where + A: ToSocketAddrs + fmt::Debug + Send + Sync + 'static, + F: Factory + Send + Sync + 'static, + H: Handler, +{ + thread::spawn(move || { + let server = WebSocket::new(factory)?; + server.listen(address)?; + Ok(()) + }) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn main() { + let t = spawn_on("127.0.0.1:3012", |sender| { + println!("Websocket启动成功"); + move |msg| { + println!("Received: {}", msg); + sender.send(msg) + } + }); + t.join().expect("Websocket失败!").expect("Websocket出错!"); + } + + /// 📄简单的回传连接处理者 + /// * 🎯处理单个Websocket连接 + #[derive(Debug)] + struct EchoHandler { + sender: Sender, + } + + impl Handler for EchoHandler { + fn on_shutdown(&mut self) { + println!("Handler received WebSocket shutdown request."); + } + + fn on_open(&mut self, shake: ws::Handshake) -> ws::Result<()> { + if let Some(addr) = shake.remote_addr()? { + println!("Connection with {} now open", addr); + } + Ok(()) + } + + fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> { + println!("Received message {:?}", msg); + self.sender.send(msg)?; + Ok(()) + } + + fn on_close(&mut self, code: ws::CloseCode, reason: &str) { + println!("Connection closing due to ({:?}) {}", code, reason); + } + + fn on_error(&mut self, err: ws::Error) { + // Ignore connection reset errors by default, but allow library clients to see them by + // overriding this method if they want + if let ws::ErrorKind::Io(ref err) = err.kind { + if let Some(104) = err.raw_os_error() { + return; + } + } + + eprintln!("{:?}", err); + } + + fn on_request(&mut self, req: &ws::Request) -> ws::Result { + println!("Handler received request:\n{}", req); + ws::Response::from_request(req) + } + + fn on_response(&mut self, res: &ws::Response) -> ws::Result<()> { + println!("Handler received response:\n{}", res); + Ok(()) + } + + fn on_timeout(&mut self, event: ws::util::Token) -> ws::Result<()> { + println!("Handler received timeout token: {:?}", event); + Ok(()) + } + + fn on_new_timeout(&mut self, _: ws::util::Token, _: ws::util::Timeout) -> ws::Result<()> { + // default implementation discards the timeout handle + Ok(()) + } + + fn on_frame(&mut self, frame: ws::Frame) -> ws::Result> { + println!("Handler received: {}", frame); + // default implementation doesn't allow for reserved bits to be set + if frame.has_rsv1() || frame.has_rsv2() || frame.has_rsv3() { + Err(ws::Error::new( + ws::ErrorKind::Protocol, + "Encountered frame with reserved bits set.", + )) + } else { + Ok(Some(frame)) + } + } + + fn on_send_frame(&mut self, frame: ws::Frame) -> ws::Result> { + println!("Handler will send: {}", frame); + // default implementation doesn't allow for reserved bits to be set + if frame.has_rsv1() || frame.has_rsv2() || frame.has_rsv3() { + Err(ws::Error::new( + ws::ErrorKind::Protocol, + "Encountered frame with reserved bits set.", + )) + } else { + Ok(Some(frame)) + } + } + } + + struct EchoFactory; + + impl Factory for EchoFactory { + type Handler = EchoHandler; + + fn connection_made(&mut self, sender: Sender) -> EchoHandler { + dbg!(EchoHandler { sender }) + } + + fn on_shutdown(&mut self) { + println!("Factory received WebSocket shutdown request."); + } + + fn client_connected(&mut self, ws: Sender) -> Self::Handler { + self.connection_made(ws) + } + + fn server_connected(&mut self, ws: Sender) -> Self::Handler { + self.connection_made(ws) + } + + fn connection_lost(&mut self, handler: Self::Handler) { + println!("Connection lost of {handler:?}"); + } + } + + #[test] + fn test_echo_localhost() -> Result<()> { + let ws = WebSocket::new(EchoFactory)?; + ws.listen("localhost:3012")?; + Ok(()) + } + + #[test] + fn test_echo_ipv6() -> Result<()> { + let ws = WebSocket::new(EchoFactory)?; + ws.listen("[::]:3012")?; + Ok(()) + } +} diff --git a/src/cli_support/mod.rs b/src/cli_support/mod.rs new file mode 100644 index 0000000..026385e --- /dev/null +++ b/src/cli_support/mod.rs @@ -0,0 +1,14 @@ +//! 命令行支持 +//! * 🎯通用、可选地复用「CIN启动器」等「命令行工具」的内容 +//! * 🎯亦可为后续基于UI的应用提供支持 + +util::mods! { + // CIN搜索 + pub cin_search; + + // 输入输出 + pub io; +} + +// 错误处理增强 +pub mod error_handling_boost; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6f24266 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,132 @@ +//! 主模块 +//! * ✨进程IO库 +//! * ✨通用运行时 +//! * ✨运行时的各类实现(可选) + +// 实用库别名 +pub extern crate nar_dev_utils as util; + +util::mods! { + // 必选模块 // + + // 进程IO + pub process_io; + + // NAVM运行时 + pub runtimes; + + // 输出处理者 + pub output_handler; + + // 可选模块 // + + // 各CIN的启动器、运行时实现 + "cin_implements" => pub cin_implements; + + // 命令行支持 + "cli_support" => pub cli_support; + + // 测试工具集 + "test_tools" => pub test_tools; +} + +/// 单元测试 +/// * 🎯为下属单元测试提供测试支持 +/// * 📄测试用配置文件的名称及路径 +/// * 📄各测试用CIN的内部路径(`executables`) +/// * ❌【2024-04-07 08:50:07】已知问题:不同crate的`[cfg(test)]`代码无法互通 +/// * 🚩【2024-04-07 08:52:36】当下解决方案:禁用`#[cfg(test)]` +/// * 📌以**十数个常量**的编译成本,换得**更方便的测试可维护性**(无需复制代码) +// #[cfg(test)] +pub mod tests { + #![allow(unused_variables)] + + /// 实用宏/简化字符串常量 + macro_rules! str_const { + ($( + $(#[$m:meta])* + $name:ident = $value:literal $(;)? + )*) => {$( + $(#[$m])* + pub const $name: &str = $value; + )*}; + } + + /// 测试用配置文件路径 + /// * 🎯后续其它地方统一使用该处路径 + /// * 📌相对路径の根目录:项目根目录(`Cargo.toml`所在目录) + /// * ⚠️只与配置文件路径有关,不与CIN位置有关 + /// * 💭后续若在不同工作环境中,需要调整配置文件中有关「CIN位置」的信息 + /// * ⚠️此处所涉及的CIN不附带于源码中,而是**另行发布** + /// * ❗部分CIN涉及c + pub mod config_paths { + str_const! { + + /// 用于「启动参数解析」的测试环境 + ARG_PARSE_TEST = + "./src/tests/cli/config/_arg_parse_test.opennars.hjson" + + /// OpenNARS + OPENNARS = "./src/tests/cli/config/cin_opennars.hjson" + /// ONA + ONA = "./src/tests/cli/config/cin_ona.hjson" + /// PyNARS + PYNARS = "./src/tests/cli/config/cin_pynars.hjson" + /// CXinJS + CXIN_JS = "./src/tests/cli/config/cin_cxin_js.hjson" + /// 原生IL-1 + NATIVE_IL_1 = "./src/tests/cli/config/cin_native_il_1.hjson" + + /// 预引入/NAL测试环境 + PRELUDE_TEST = "./src/tests/cli/config/prelude_test.hjson" + /// NAL/简单演绎 + NAL_SIMPLE_DEDUCTION = "./src/tests/cli/config/nal_simple_deduction.hjson" + /// NAL/高阶演绎 + NAL_HIGHER_DEDUCTION = "./src/tests/cli/config/nal_higher_deduction.hjson" + /// NAL/自变量消除 + NAL_I_VAR_ELIMINATION = "./src/tests/cli/config/nal_i_var_elimination.hjson" + /// NAL/时间归纳 + NAL_TEMPORAL_INDUCTION = "./src/tests/cli/config/nal_temporal_induction.hjson" + /// NAL/操作 + NAL_OPERATION = "./src/tests/cli/config/nal_operation.hjson" + /// NAL/简单操作 + NAL_SIMPLE_OPERATION = "./src/tests/cli/config/nal_simple_operation.hjson" + + /// Websocket + WEBSOCKET = "./src/tests/cli/config/websocket.hjson" + /// Matriangle服务器 + MATRIANGLE_SERVER = "./src/tests/cli/config/matriangle_server.hjson" + } + } + + /// 测试用CIN路径 + /// * 🎯后续其它地方统一使用该处路径 + /// * 🎯存储测试用的本地CIN + /// * ⚠️该处CIN被自动忽略,不附带于源码中,需要另外的运行时包以启动 + /// * 📌相对路径の根目录:项目根目录(`Cargo.toml`所在目录) + pub mod cin_paths { + str_const! { + OPENNARS = "./executables/opennars-304-T-modified.jar" + ONA = "./executables/ONA.exe" + PYNARS_ROOT = "./executables/PyNARS" + PYNARS_MODULE = "pynars.ConsolePlus" + NARS_PYTHON = "./executables/nars-python-main.exe" + CXIN_JS = "./executables/cxin-nars-shell.js" + OPENJUNARS = "./executables/OpenJunars/run.jl" + } + } + + /// 测试用宏/找不到路径即退出 + /// * 🚩输入一个`&str`,构建`&Path`并在其不存在时退出程序,或返回该`&Path`对象 + #[macro_export] + macro_rules! exists_or_exit { + ($path:expr) => {{ + let path = std::path::Path::new($path); + if !path.exists() { + println!("所需路径 {path:?} 不存在,已自动退出"); + std::process::exit(0) + } + path + }}; + } +} diff --git a/src/output_handler/flow_handler_list.rs b/src/output_handler/flow_handler_list.rs new file mode 100644 index 0000000..0bfcbb0 --- /dev/null +++ b/src/output_handler/flow_handler_list.rs @@ -0,0 +1,224 @@ +//! 模块:流式处理者列表 +//! * 🎯用于流式处理物件,并在这其中灵活控制处理流程 +//! * 📌组合式处理流程:多个处理者在一个处理函数中处理 +//! * 📌截断式消耗过程:处理的物件可能会在中途被处理者消耗 +//! +//! ? 【2024-03-23 14:45:53】是否需要整合进[`nar_dev_utils`]中去 + +use std::marker::PhantomData; + +/// 枚举:处理结果 +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum HandleResult { + /// 物件通过了所有处理者,并最终返回 + Passed(Item), + /// 物件在处理中途被消耗,指示「消耗了物件的处理者」 + Consumed(HandlerIndex), +} + +/// 统一表示「输出处理者」 +/// * 🎯简化类型表示 +/// * 🚩【2024-04-08 21:04:47】因需进行线程共享,此闭包必须附带`Send`和`Sync` +pub type DynOutputHandler = dyn FnMut(Item) -> Option + Send + Sync; + +/// 流式处理者列表 +/// * 🚩处理者的特征约束:`FnMut(Item) -> Option` +/// * 📝不能显式声明「处理者」类型 +/// * ❗若作为泛型参数,则意味着「需要统一所有类型」 +/// * 📌而各个闭包彼此之间类型都是不同的 +pub struct FlowHandlerList { + /// 存储所有的处理者 + /// * 🚩使用[`Box`]以容纳不同类型的闭包 + handlers: Vec>>, + + /// 用于对未直接作为字段的`Item`类型的占位符 + /// * 🔗标准库文档: + _marker: PhantomData, +} + +/// 实现调试呈现 +impl std::fmt::Debug for FlowHandlerList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FlowHandlerList(num={})", self.handlers.len()) + } +} + +impl FlowHandlerList { + /// 构造函数/从某个[`Box`]迭代器中构造 + /// * ℹ️若需构造一个空列表,可使用[`FlowHandlerList::default`] + /// * 📝【2024-03-23 15:09:58】避免不了装箱:存储的是特征对象,不能不装箱就迭代 + /// * ❌【2024-03-23 15:31:48】停用:对参数`([Box::new(|x| Some(x)),],)`也无法使用 + /// * 🚩已改用快捷构造宏 + pub fn new() -> Self { + Self::from_vec(vec![]) + } + + /// 构造函数/直接从[`Vec`]构造 + /// * 需要自己手动装箱 + /// * ℹ️若需构造一个空列表,可使用[`FlowHandlerList::default`] + pub fn from_vec(vec: Vec>>) -> Self { + Self { + handlers: vec, + _marker: PhantomData, + } + } + + // 核心逻辑 // + + /// 【核心】处理 + /// * 🚩主要思路:不断让`Item`值通过各个处理者,直到「全部通过」或「有处理者消耗」 + /// * ⚙️返回值:全部通过后的物件 / 被消耗的处理者索引 + /// * 📝实际上也可不用额外的`let item`,直接使用传入所有权的参数变量 + pub fn handle(&mut self, mut item: Item) -> HandleResult { + // // 预置好物件变量 + // let mut item = item; + // 逐个遍历处理者 + for (index, handler) in self.handlers.iter_mut().enumerate() { + // 调用处理者处理物件,并对返回值做分支 + match handler(item) { + // 有返回值⇒继续 + // ! 这里的返回值有可能已【不是】原来的那个了 + Some(new_item) => item = new_item, + // 没返回值⇒报告处理者所在索引 + None => return HandleResult::Consumed(index), + } + } + // 最终通过 + HandleResult::Passed(item) + } + + // 对「处理者列表」的操作 // + + /// 获取某个位置的处理者(不可变) + pub fn get_handler(&self, index: usize) -> Option<&DynOutputHandler> { + // 获取指定位置的box,然后将其转为索引 + self.handlers.get(index).map(Box::as_ref) + } + + // ! 【2024-03-23 15:16:08】废稿:可变引用的生命周期类型是【invariant】的 + // * 📝生命周期中`'self : 'handler`不代表`&mut 'self` + // * 🔗参考: + // /// 获取某个位置的处理者(可变) + // /// * ℹ️[`Self::get_handler`]的可变引用版本 + // pub fn get_handler_mut( + // &mut self, + // index: usize, + // ) -> Option<&mut DynOutputHandler> { + // self.handlers.get_mut(index).map(Box::as_mut) + // } + + /// 添加新的处理者 + /// * ⚠️虽然结构体定义时无需对「处理者」类型约束为`'static`静态周期, + /// * 但此处传入作为参数(的函数指针)是需要的 + pub fn add_handler( + &mut self, + handler: impl FnMut(Item) -> Option + Send + Sync + 'static, + ) { + self.handlers.push(Box::new(handler)) + } +} + +/// 默认构造函数:空数组 +impl Default for FlowHandlerList { + fn default() -> Self { + Self::new() + } +} + +/// 快捷构造宏 +#[macro_export] +macro_rules! flow_handler_list { + [ $($handler:expr),* $(,)? ] => { + // * ❌【2024-03-23 15:34:04】暂时不使用`$crate`:模块路径尚未固定 + FlowHandlerList::from_vec( + vec![$(Box::new($handler)),*] + ) + }; +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use util::*; + use HandleResult::*; + + /// 基础功能测试 + #[test] + fn test_flow_handler_list() { + // * 📝`|x| Some(x)`可以直接使用构造函数调用,写成`Some` + let handler1 = Some; + let handler2 = |x| Some(x + 1); + let handler3 = |x| if x > 1 { Some(x) } else { None }; + + let mut list = FlowHandlerList::new(); + + asserts! { + // 第一个闭包 + list.add_handler(handler1) => (), + list.handle(0) =>Passed(0), + // 第二个闭包 + list.add_handler(handler2) => (), + list.handle(0) => Passed(1), + // 第三个闭包 + list.add_handler(handler3) => (), + list.handle(0) => Consumed(2), // 被消耗,索引在最后一个 + list.handle(1) => Passed(2), // 通过 + } + + let mut list = flow_handler_list![ + Some, + |x: usize| Some(x + 1), + |x| Some(dbg!(x)), + |x: usize| Some(x - 1), + ]; + + asserts! { + list.handle(0) => Passed(0) + } + } + + /// 联动「NAVM输出」测试 + #[test] + fn test_navm_output() { + use narsese::conversion::string::impl_lexical::shortcuts::*; + use navm::output::*; + // 构造输出 + let answer = Output::ANSWER { + content_raw: " B>.".into(), + narsese: Some(nse!( B>.)), // * ✨直接使用新版快捷构造宏 + }; + let out = Output::OUT { + content_raw: " C>".into(), + narsese: Some(nse!( C>.)), + }; + // 构造处理者列表 + let mut list = flow_handler_list![ + // 展示 + |out: Output| Some(dbg!(out)), + // 截获回答 + |out| match out { + Output::ANSWER { + content_raw, + narsese, + } => { + println!("截获到回答:{content_raw:?} | {narsese:?}"); + None + } + _ => Some(out), + }, + // 展示 + |out| { + println!("这是其它输出:{out:?}"); + Some(out) + }, + ]; + // 测试处理 + asserts! { + // 回答被截获 + list.handle(answer) => Consumed(1), + // 其它被通过 + list.handle(out.clone()) => Passed(out), + } + } +} diff --git a/src/output_handler/mod.rs b/src/output_handler/mod.rs new file mode 100644 index 0000000..902a8fc --- /dev/null +++ b/src/output_handler/mod.rs @@ -0,0 +1,5 @@ +//! 存储有关「输出捕获」的工具 +//! * 🎯辅助封装NAVM的「指令-输出 通道」结构 + +// 流式处理者列表 +pub mod flow_handler_list; diff --git a/src/process_io/io_process.rs b/src/process_io/io_process.rs new file mode 100644 index 0000000..95a407f --- /dev/null +++ b/src/process_io/io_process.rs @@ -0,0 +1,630 @@ +//! 一个简单的「IO子进程」类型 +//! +//! ## 功能 +//! +//! * ✅封装「标准IO读写」「进程通信」「线程阻塞」等逻辑 +//! * ✨支持「输出侦听」与「输出通道」两种输出处理方式 +//! +//! ## 疑难问题 +//! +//! * ❗进程残留:可能在调用`kill`方法后,子进程并未真正被杀死 +//! * 🚩【2024-03-25 13:29:14】目前解决方案:调用系统`taskkill`指令,利用进程id强制终止 +//! * ⚠️【2024-03-25 13:32:50】 + +use std::{ + error::Error, + ffi::OsStr, + fmt::{self, Debug, Display, Formatter}, + io::{BufRead, BufReader, ErrorKind, Result as IoResult, Write}, + process::{Child, ChildStdin, ChildStdout, Command, ExitStatus, Stdio}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, + thread::{self, JoinHandle}, +}; +// use util::*; +use anyhow::Result; +use util::ResultBoost; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct IoProcessError(String); +impl Display for IoProcessError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl Error for IoProcessError {} + +fn err(e: impl Debug) -> anyhow::Error { + IoProcessError(format!("{e:?}")).into() +} + +/// 统一定义「输出侦听器」的类型 +type OutputListener = dyn FnMut(String) + Send + Sync; + +/// 简化定义`Arc< Mutex>` +type ArcMutex = Arc>; + +/// 构建一个「IO进程」 +/// * 📌只是作为一个基于配置的「子进程启动器」存在 +/// * 作为真正的`IoProcessManager`的launcher/builder +/// +/// ! 因为有「系统指令」与「函数闭包」,无法派生任何常规宏 +pub struct IoProcess { + /// 内部封装的「进程指令」对象 + command: Command, + /// 内部配置的「输出侦听器」 + out_listener: Option>, +} + +impl IoProcess { + /// 构造函数 + /// * 🚩从路径构造实体 + /// * 📌直接生成[`Command`]对象,无需额外配置 + pub fn new(program_path: impl AsRef) -> Self { + // 实际上是构建了一个新[`Command`]对象 + let command = Command::new(program_path); + Self::from(command) + } + + /// 添加命令行参数 + pub fn arg(mut self, arg: impl AsRef) -> Self { + // 添加参数 + self.command.arg(arg); + // 返回自身以便链式调用 + self + } + + /// 添加输出侦听器 + /// * 📌此处因生命周期问题(难以绑定`listener`到`self`)设置`F`的约束为`'static` + pub fn out_listener(mut self, listener: F) -> Self + where + F: FnMut(String) + Send + Sync + 'static, + { + // 字段赋值 + self.out_listener = Some(Box::new(listener)); + // 返回自身以便链式调用 + self + } + + /// 启动 + /// * 🚩通过[`Self::try_launch`]尝试启动,然后直接解包 + /// * 🚩【2024-04-02 04:11:27】现在为方便反馈处理错误,重新变为[`Result`]类型 + /// * 📄路径问题:启动路径不合法 等 + pub fn launch(self) -> Result { + // 尝试启动 + Ok(self.try_launch()?) + } + + /// 启动 + /// * 🚩此处只负责创建子进程[`Child`], + /// * ⚠️不负责对子进程的控制(监听、通道)等 + pub fn try_launch(mut self) -> std::io::Result { + // 创建一个子进程 + let child = + // 指令+参数 + self.command + // 输入输出 + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + // 产生进程 + .spawn()?; + println!("Started process: {}", child.id()); + + // 获取输出侦听器 + let out_listener = self.out_listener; + + // 创建「子进程管理器」对象 + Ok(IoProcessManager::new(child, out_listener)) + } +} + +/// 实现/从[`Command`]对象转换为[`IoProcess`] +/// * ✅这里的[`Command`]必定是未被启动的:Launch之后会变成[`Child`]类型 +/// * 📝即便一些地方没法使用`command.into()`,也可使用`IoProcess::from(command)` +impl From for IoProcess { + fn from(command: Command) -> Self { + Self { + // 置入命令 + command, + // 侦听器空置 + out_listener: None, + } + } +} + +/// 子进程管理器 +/// * 🎯负责 +/// * 统一管理子进程 +/// * 封装提供易用的(字符串)输入输出接口 +/// * 🚩现在兼容「输出侦听」与「输出通道」两处 +/// * 🎯「输出侦听」用于「需要**响应式**即时处理输出,但又不想阻塞主进程/开新进程」时 +/// * 🎯「输出通道」用于「需要封装『并发异步获取』延迟处理输出,兼容已有异步并发模型」时 +/// * 📝【2024-04-02 20:40:35】使用[`Option`]应对「可能会移动所有权」的情形 +/// * 📄在「线程消耗」的场景中,有时需要「消耗线程,重启新线程」,此时就需要[`Option`]确保销毁 +#[allow(dead_code)] +pub struct IoProcessManager { + /// 正在管理的子进程 + process: Child, + + /// 子进程的「写(到子进程的)输入」守护线程 + thread_write_in: Option>, + + /// 子进程的「读(到子进程的)输出」守护线程 + /// * 🚩现在兼容「侦听器」「通道」两种模式,重新必要化 + thread_read_out: Option>, + // thread_read_out: JoinHandle<()>, + /// 子线程的终止信号 + termination_signal: ArcMutex, + + /// 子进程输出的「接收者」 + /// * 🚩子进程发送给外部侦听器,同时由外部接收 + /// * 在将输出发送给侦听器时,会在此留下备份 + /// * ⚠️如果直接调用[`Receiver::recv`]方法,可能会导致线程阻塞 + child_out: Mutex>, + // ! 【2024-03-23 19:31:56】现在兼容「输出侦听」与「输出通道」二者 + /// 子进程输入的「发送者」 + /// * 🚩子进程接收来自外部发送的消息,由外部发送 + child_in: Mutex>, + // /// 子进程的「输出监听器」 + // out_listener: Option>, + // ! 【2024-03-22 09:54:22】↑现在使用「输出侦听器」模式,此字段数据存储在`thread_read_out`中 + // /// 输出计数 + // /// * 🎯用于追踪输出数量,以便在不阻塞[`Self::child_out`] + // num_output: ArcMutex, + // ! ✅【2024-03-24 01:20:11】↑现在因[`Receiver::try_recv`],无需使用此计数 + // * 📌【2024-03-24 01:20:38】并且,这个计数在测试中还偶发有不稳定行为(可能遗漏计数) +} + +impl IoProcessManager { + // * 初始化 * // + + /// 构造方法 + /// * 🚩从「子进程」与「输出侦听器」构造「进程管理者」 + pub fn new(mut child: Child, out_listener: Option>) -> Self { + // 提取子进程的标准输入输出 + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + + // 创建通道 + // * 📌IO流向:从左到右 + // ! 🚩【2024-03-22 09:53:12】现在采用「输出侦听器」的方法,不再需要封装通道 + let (child_out, out_sender) = channel(); + let (in_receiver, child_in) = channel(); + + // 生成「终止信号」共享数据 + let termination_signal = Arc::new(Mutex::new(false)); + + // // 生成「输出计数」共享数据 + // let num_output = Arc::new(Mutex::new(0)); + + // 生成进程的「读写守护」(线程) + let thread_write_in = Some(IoProcessManager::spawn_thread_write_in( + stdin, + child_in, + termination_signal.clone(), + )); + let thread_read_out = Some(IoProcessManager::spawn_thread_read_out( + stdout, + child_out, + out_listener, + termination_signal.clone(), + // num_output.clone(), + )); + // let thread_read_out = + // out_listener.map(|listener| IoProcessManager::spawn_thread_read_out(stdout, listener)); + // ! 🚩【2024-03-23 19:33:45】↑现在兼容「侦听器」「通道」二者 + + // 构造并返回自身 + Self { + process: child, + thread_read_out, + thread_write_in, + // 捕获通道的两端 + child_out: Mutex::new(out_sender), + child_in: Mutex::new(in_receiver), + // out_listener, + // ! 【2024-03-22 09:53:50】↑不再于自身存储「输出侦听器」,而是存储在`thread_read_out`中 + // 共享变量 + termination_signal, + // num_output, + // ! 【2024-03-24 01:24:58】↑不再使用「输出计数」:有时会遗漏输出,并且有`try_recv`的更可靠方案 + } + } + + /// 生成一个子线程,管理子进程的标准输入,接收通道另一端输出 + /// * 📌读输入,写进程 | stdin >>> child_in_receiver + #[inline] + fn spawn_thread_write_in( + stdin: ChildStdin, + child_in_receiver: Receiver, + termination_signal: ArcMutex, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + // 从通道接收者读取输入 | 从「进程消息发送者」向进程发送文本 + let mut stdin = stdin; + // ! 注意:这个`for`循环是阻塞的 + for line in child_in_receiver { + // 检查终止信号 | ⚠️不要在终止后还发消息 + if *termination_signal.lock().expect("无法锁定终止信号") { + // println!("子进程收到终止信号"); + break; + } + // 写入输出 + if let Err(e) = stdin.write_all(line.as_bytes()) { + match e.kind() { + // * 🚩进程已关闭⇒退出 + // TODO: 🏗️外包「错误处理」逻辑 + ErrorKind::BrokenPipe => { + println!("子进程已关闭"); + break; + } + // 其它 + _ => println!("子进程写入错误:{e}"), + } + } + } + }) + } + + /// 生成一个子线程,管理子进程的标准输出,传送输出的消息到另一端 + /// * 📌写输出 | child_out_sender >>> stdout + /// * 🚩【2024-03-23 20:46:38】现在「侦听器」与「通道」并行运作 + /// * 📌核心逻辑 + /// * 通过「缓冲区读取器」[`BufReader`]读取子进程输出 + /// * 不断尝试读取,直到有内容 + /// * 朝通道[`Sender`]发送内容 + #[inline] + fn spawn_thread_read_out( + stdout: ChildStdout, + child_out_sender: Sender, + out_listener: Option>, + termination_signal: ArcMutex, + // num_output: ArcMutex, + ) -> thread::JoinHandle<()> { + // 将Option包装成一个新的函数 + // ! ⚠️【2024-03-23 19:54:43】↓类型注释是必须的:要约束闭包类型一致 + let mut listener_code: Box = match out_listener { + // * 🚩先前有⇒实际执行 | 仅在实际有值时拷贝并传送给侦听器 + Some(mut listener) => Box::new(move |s: &String| listener(s.clone())), + // * 🚩先前无⇒空函数 + None => Box::new(move |_| {}), + }; + // 启动线程 + thread::spawn(move || { + // 创建缓冲区读取器 | ⚠️【2024-03-23 23:42:08】这里的`BufReader`不能简化 + // * 📝`ChildStdout`没有`read_line`功能,但可以通过`BufReader`封装 + let mut stdout_reader = BufReader::new(stdout); + + // 创建缓冲区 | 🎯可持续使用 + let mut buf = String::new(); + + // 持续循环 + loop { + // 从子进程「标准输出」读取输入 + // * ⚠️会阻塞:`read_line` + // * 📄在ONA处不阻塞,但在OpenNARS时阻塞 + // * 🔗 + match stdout_reader.read_line(&mut buf) { + // 没有任何输入⇒检查终止信号 + // * 📌不能在这里中断,需要检查终止信号 + // * 🚩【2024-03-24 01:48:19】目前**允许**在进程终止时获取其输出 + // * 一般侦听器都能侦听到 + Ok(0) => { + if *termination_signal.lock().expect("无法锁定终止信号") { + // println!("子进程收到终止信号"); + break; + } + } + // 有效输入 + Ok(_) => { + // ! 🚩现在兼容「侦听器」「通道」二者 + // 先侦听 | 只传递引用,仅在「实际有侦听器」时拷贝消息 + listener_code(&buf); + // 向「进程消息接收者」传递消息(实际上是「输出」) + if let Err(e) = child_out_sender.send(buf.clone()) { + println!("无法向主进程发送消息:{e:?}"); + break; + } + // // 输出计数 + // match num_output.lock() { + // Ok(mut n) => *n += 1, + // Err(e) => println!("无法对子进程输出计数:{e:?}"), + // } + // ! 【2024-03-24 01:42:46】现在取消「输出计数」机制:计数可能不准确,并且被`try_recv`取代 + } + // 报错⇒处理错误 + Err(e) => { + // 只是「不包含字符」(过早读取)⇒跳过 + let message = e.to_string(); + if message.contains("stream did not contain") { + // 什么都不做 + } else { + println!("无法接收子进程输出:{e:?} in「{buf}」"); + break; + } + } + } + // 清空缓冲区 + buf.clear(); + } + }) + } + + // * 正常运作 * // + + /// 获取子进程id + /// * 🚩调用[`Child::id`]方法 + pub fn id(&self) -> u32 { + self.process.id() + } + + /// (从「输出通道」中)拉取一个输出 + /// * 🎯用于(阻塞式等待)从子进程中收取输出信息 + /// * 🚩以字符串形式报告错误 + /// * ⚠️【2024-03-24 01:22:02】先前基于自身内置`num_output`的计数方法不可靠:有时会遗漏计数 + /// * ❌[`std::sync::PoisonError`]未实现[`Send`],无法被[`anyhow::Error`]直接捕获 + /// * ❌[`std::sync::mpsc::RecvError`]未实现[`From`],无法转换为[`anyhow::Error`] + pub fn fetch_output(&mut self) -> Result { + // 访问自身「子进程输出」字段 + self.child_out + // 互斥锁锁定 + .lock() + .transform_err(err)? + // 通道接收者接收 + .recv() + .transform_err(err) + } + + /// 尝试(从「输出通道」中)拉取一个输出 + /// * 🎯保证不会发生「线程阻塞」 + /// * 🚩类似[`Self::fetch_output`],但仅在「有输出」时拉取 + /// * 📝[`Receiver`]自带的[`Receiver::try_recv`]就做了这件事 + /// * ⚠️【2024-03-24 01:22:02】先前基于自身内置`num_output`的计数方法不可靠:有时会遗漏计数 + pub fn try_fetch_output(&mut self) -> Result> { + // 访问自身「子进程输出」字段,但加上`try` + let out = self + .child_out + // 互斥锁锁定 + .lock() + .transform_err(err)? + // 通道接收者接收 + .try_recv() + .ok(); + // ! ↑此处使用`ok`是为了区分「锁定错误」与「通道无输出」 + // 返回 + Ok(out) + } + + /// 向子进程写入数据(字符串) + /// * 🚩通过使用自身「子进程输入」的互斥锁,从中输入数据 + /// * ⚙️返回空,或返回字符串形式的错误(互斥锁错误) + /// * ⚠️此方法需要【自行尾缀换行符】,否则不被视作有效输入 + /// * 📄要触发输入,需传入" B>.\n"而非" B>." + pub fn put(&self, input_line: impl ToString) -> Result<()> { + // 从互斥锁中获取输入 + // * 🚩等待直到锁定互斥锁,最终在作用域结束(MutexGuard析构)时释放(解锁) + // ! ❌【2024-03-23 23:59:20】此处的闭包无法简化成函数指针 + self.child_in + // 锁定以获取`Sender` + .lock() + .transform_err(err)? + // 发送 | 📄文档说此处不会阻塞: + .send(input_line.to_string()) + .transform_err(err) + // * ✅【2024-04-08 22:46:04】有关「线程死锁」的问题已定位:`ws`库中的`Sender.send`方法使用`std::mpsc::SyncSender`导致阻塞 + } + + /// 向子进程写入**一行**数据(字符串) + /// * 🚩功能同[`Self::put`],但会自动加上换行符 + /// * 📌类似[`print`]和[`println`]的关系 + /// * ⚠️此方法在输入后【自动添加换行符】 + /// * 📄传入" B>."将自动转换成" B>.\n" + /// * ✅以此总是触发输入 + pub fn put_line(&self, input: impl ToString) -> Result<()> { + self.put(format!("{}\n", input.to_string())) + } + + /// 等待子进程结束 + /// * 🚩调用[`Child::wait`]方法 + /// * ⚠️对于【不会主动终止】的子进程,此举可能导致调用者死锁 + pub fn wait(&mut self) -> IoResult { + self.process.wait() + } + + /// 杀死自身 + /// * 🚩设置终止信号,通知子线程(以及标准IO)终止 + /// * 🚩调用[`Child::kill`]方法,终止子进程 + /// * ~~⚠️将借走自身所有权,终止并销毁自身~~ + /// * 🚩【2024-04-02 20:37:28】如今不再消耗自身所有权 + /// * ✅【2024-04-02 20:36:40】现在通过「将字段类型变为[`Option`]」安全借走子线程所有权 + /// * 📌销毁自身的逻辑,交给调用方处理 + /// + /// * ❓不稳定:有时会导致「野进程」的情况 + pub fn kill(&mut self) -> Result<()> { + // ! ❌【2024-03-23 21:08:56】暂不独立其中的逻辑:无法脱开对`self`的借用 + // ! 📌更具体而言:对其中两个线程`thread_write_in`、`thread_read_out`的部分借用 + // 向子线程发送终止信号 // + let mut signal = self.termination_signal.lock().transform_err(err)?; + *signal = true; + drop(signal); // ! 手动释放锁 + // * 📝【2024-03-24 00:15:10】必须手动释放锁,否则会导致后续线程死锁 + + // ! 解除子线程「write_stdin」的阻塞 + // * 有可能在程序崩溃后还发信息,此时是`SendError` + let _ = self + .put("\n") + .inspect_err(|e| println!("向「进程读取」子线程发送消息失败!{e}")); + + // 等待子线程终止 // + // * 🚩【2024-03-24 18:49:31】现在强制销毁持有的两个子线程,不再等待其结束 + // * 📌主要原因:在测试OpenNARS时,发现`thread_read_out`仍然会阻塞(无法等待) + // * 📌并且一时难以修复:难点在`BufReader.read_line`如何非阻塞/可终止化 + // ! ℹ️信息 from Claude3:无法简单以此终止子线程 + // * 🚩【2024-04-02 20:31:24】现在通过「字段类型转为[`Option`]」的方法,安全拿取所有权并销毁 + drop( + self.thread_write_in + .take() + .map(|t| t.join().transform_err(err)), + ); // * ✅目前这个是可以终止的 + drop(self.thread_read_out.take()); + + // * 📝此时子线程连同「子进程的标准输入输出」一同关闭, + // * 子进程自身可以做输出 + // * 📄如:ONA在最后会打印程序运行报告 + // * ⚠️这意味着「输出侦听器」仍然能对其输出产生响应 + + // 杀死子进程 // + // * 【2024-03-25 13:22:12】尝试使用`taskkill`强制杀死子进程(不会影响后边的kill) + // * 📌启动失败也不影响:主要目的是在系统层面防止「进程残留」 + // * 📄【2024-03-25 13:23:41】目前对OpenNARS有效(Java进程得到了有效终止) + // * ❗可能是系统特定的 + if let Ok(child) = Command::new("taskkill") + // 强制终止id为子进程id的进程 + .args(["-F", "-PID", &self.process.id().to_string()]) + .spawn() + { + // 等待taskkill杀死子进程 + if let Err(err) = child.wait_with_output() { + println!("指令执行失败!{err:?}"); + } + } + // * 🚩通用:调用`Child`对象的`kill`方法 + self.process.kill().transform_err(err) + } +} + +/// 单元测试 +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::tests::cin_paths::ONA as PATH_ONA; + use std::{ + process::exit, + sync::{Arc, Mutex}, + }; + + /// 测试工具/等待子进程输出,直到输出满足条件 + pub fn await_fetch_until(process: &mut IoProcessManager, criterion: impl Fn(String) -> bool) { + loop { + let out = process.fetch_output().expect("无法拉取输出"); + println!("fetch到其中一个输出: {out:?}"); + if criterion(out) { + break; + } + } + } + + /// 实用测试工具:启动一个ONA,并附带「输出缓存」 + fn launch_ona() -> (IoProcessManager, ArcMutex>) { + // 输出缓存 + let outputs = Arc::new(Mutex::new(vec![])); + let outputs_inner = outputs.clone(); + // 从一个系统指令开始构建子进程 + let process = IoProcess::new(PATH_ONA) + // 添加命令参数 + .arg("shell") + // 添加输出监听器 | 简单回显 + // ! 【2024-03-22 10:06:38】基于「输出侦听器」的情形,若需要与外部交互,则会遇到所有权/生命周期问题 + // * 📄子进程与子进程外部(如此处的主进程)的问题 + // * ✅【2024-03-22 10:16:32】↑已使用`Arc`解决 + .out_listener(move |output: String| { + outputs_inner + .lock() + .expect("无法锁定 outputs_inner") + .push(output.clone()); + print!("[OUT] {}", output); + }); + // 启动子进程并返回 + (process.launch().expect("ONA启动失败"), outputs) + } + + /// 标准案例:ONA交互 + /// + /// ## 测试输入 + /// + /// ```plaintext + /// B>. + /// C>. + /// C>? + /// ``` + /// + /// ## 预期输出 + /// + /// ```plaintext + /// Answer: C>. creationTime=2 Truth: frequency=1.000000, confidence=0.810000 + /// ``` + /// + /// ## 笔记 + /// + /// * 📝[`Arc`]能满足[`Sync`]+[`Send`],但R[`efCell`]不满足 + /// * ❌无法使用`Arc>`组合 + /// * 📝[`Mutex`]能进行进程交互,但无法共享引用 + /// * 🚩最终使用`ArcMutex`作为进程交互的共享引用 + /// * 📌[`Arc`]允许被拷贝并移动入闭包(共享引用,超越生命周期) + /// * 📌[`Mutex`]允许进程间共享的内部可变性(运行时借用检查) + #[test] + fn test_ona() { + // 创建子进程 + let (mut process, outputs) = launch_ona(); + + // 测试:输入输出 // + let output_must_contains = |s: &str| { + let outputs = outputs.lock().expect("无法锁定 outputs"); + let line = outputs + .iter() + .find(|line| line.contains(s)) + .expect("没有指定的输出!"); + println!("检验「{s:?}」成功!所在之处:{line:?}"); + }; + + // 先置入输入 + let input = " B>."; + process.put_line(input).expect("无法放置输入"); + await_fetch_until(&mut process, |s| s.contains(input)); + + // 中途检验 + output_must_contains(" B>."); + + // 继续输入 + process.put(" C>.\n").expect("无法放置输入"); + await_fetch_until(&mut process, |s| s.contains(" C>.")); + + process.put(" C>?\n").expect("无法放置输入"); + await_fetch_until(&mut process, |s| s.contains(" C>?")); + + // 不断fetch直到有答案 + const EXPECTED_ANSWER: &str = "Answer: C>."; + await_fetch_until(&mut process, |s| s.contains(EXPECTED_ANSWER)); + + // 最后检验 | 因为是缓存,所以会成功 + output_must_contains(EXPECTED_ANSWER); + + // // 等待结束 + // process.wait(); + + // 读取其中缓冲区里边的数据(多了会阻塞!) + { + let r = process.child_out.lock().unwrap(); + for _ in r.try_iter() { + let line = r.recv().expect("接收失败!"); + print!("从输出中读取到的一行(多了会阻塞!):{line}"); + } + // * 此处自动释放锁 + } + + // // 等待1秒并强制结束 + // println!("Waiting for 1 seconds and then killing the process..."); + // sleep_secs(1); + // ! 【2024-03-24 01:39:45】现在由于`await_until_output`的存在,已无需手动等待 + process.kill().expect("无法杀死进程"); + println!("Process killed."); + + // 读取检验输出 | 杀死进程后还有 + dbg!(&*outputs); + + // 退出 + exit(0); + } +} diff --git a/src/process_io/mod.rs b/src/process_io/mod.rs new file mode 100644 index 0000000..a822416 --- /dev/null +++ b/src/process_io/mod.rs @@ -0,0 +1,9 @@ +//! 用于封装抽象「进程通信」逻辑 +//! 示例代码来源:https://www.nikbrendler.com/rust-process-communication/ +//! * 📌基于「通道」的「子进程+专职读写的子线程」通信逻辑 +//! + +util::pub_mod_and_pub_use! { + // 输入输出进程 + io_process +} diff --git a/src/runtimes/command_vm/api/command_generator.rs b/src/runtimes/command_vm/api/command_generator.rs new file mode 100644 index 0000000..fa88fd6 --- /dev/null +++ b/src/runtimes/command_vm/api/command_generator.rs @@ -0,0 +1,11 @@ +//! 定义一个抽象特征,用于「命令行虚拟机」的命令参数/执行环境 自动生成 + +use std::process::Command; + +/// 命令生成器 +/// * 🎯主管[`Command`]对象的**模板化生成** +/// * 📄OpenNARS使用`java`命令生成器,允许在指定转译器的同时自定义Java启动参数 +pub trait CommandGenerator { + /// 通过自身内部参数,生成指令参数 + fn generate_command(&self) -> Command; +} diff --git a/src/runtimes/command_vm/api/mod.rs b/src/runtimes/command_vm/api/mod.rs new file mode 100644 index 0000000..48f0cf1 --- /dev/null +++ b/src/runtimes/command_vm/api/mod.rs @@ -0,0 +1,10 @@ +//! 定义有关「命令行虚拟机」的抽象API +//! * 【2024-03-26 22:33:19】总体想法:💡一个「转译器集成包」+「命令行参数生成器」⇒统一复用的「IO进程启动器」 +//! * 📌转译器集成包:用于将「输入转译器」与「输出转译器」打包成一个统一类型的值以传入 + +util::pub_mod_and_pub_use! { + // 转译器 + translators + // 命令行参数生成器 + command_generator +} diff --git a/src/runtimes/command_vm/api/translators.rs b/src/runtimes/command_vm/api/translators.rs new file mode 100644 index 0000000..a953ed6 --- /dev/null +++ b/src/runtimes/command_vm/api/translators.rs @@ -0,0 +1,199 @@ +//! 定义有关「输入输出转译器」的API +//! * ✨类型别名 +//! * ✨特制结构 +//! * ✨特有错误类型 + +use anyhow::Result; +use navm::{cmd::Cmd, output::Output}; +use std::error::Error; +use thiserror::Error; + +/// [`Cmd`]→进程输入 转译器 +/// * 🚩现在不再使用特征,以便在`Option>`中推断类型 +/// * 📝若给上边类型传入值`None`,编译器无法自动推导合适的类型 +/// * 📌要求线程稳定 +/// * 只有转译功能,没有其它涉及外部的操作(纯函数) +pub type InputTranslator = dyn Fn(Cmd) -> Result + Send + Sync; + +/// 进程输出→[`Output`]转译器 +/// * 🚩现在不再使用特征,以便在`Option>`中推断类型 +/// * 📝若给上边类型传入值`None`,编译器无法自动推导合适的类型 +/// * 📌要求线程稳定 +/// * 只有转译功能,没有其它涉及外部的操作(纯函数) +pub type OutputTranslator = dyn Fn(String) -> Result + Send + Sync; + +/// 默认输入转译器 +/// * 🎯给「输入输出转译器」提供「默认选项」 +/// * 🚩按照NAVM指令原样输入:调用[`Cmd::to_string`]原样转换成字符串 +pub fn default_input_translate(cmd: Cmd) -> Result { + Ok(cmd.to_string()) +} + +/// 默认输出转译器 +/// * 🎯给「输入输出转译器」提供「默认选项」 +/// * 🚩不含任何实质转译逻辑,原样保留在「其它」输出中 +pub fn default_output_translate(content: String) -> Result { + Ok(Output::OTHER { content }) +} + +/// 获取「默认输入转译器」 +/// * 🎯统一提供默认值 +/// * 🚩使用函数指针,以优化先前「创建闭包」产生的性能开销 +pub fn default_input_translator() -> Box { + Box::new(default_input_translate) +} + +/// 获取「默认输出转译器」 +/// * 🎯统一提供默认值 +/// * 🚩使用函数指针,以优化先前「创建闭包」产生的性能开销 +pub fn default_output_translator() -> Box { + Box::new(default_output_translate) +} + +/// IO转译器配置 +/// * 🎯封装并简化其它地方的`translator: impl Fn(...) -> ... + ...`逻辑 +/// * 📝【2024-03-27 10:38:41】无论何时都不推荐直接用`impl Fn`作为字段类型 +/// * ⚠️直接使用会意味着「需要编译前确定类型」 +/// * ❌这会【非必要地】要求一些【不直接传入闭包】的「默认初始化」方法具有类型标注 +pub struct IoTranslators { + pub input_translator: Box, + pub output_translator: Box, +} + +impl IoTranslators { + /// 构造函数 + /// * 🎯基于位置参数构造结构体 + /// * 🎯无需在调用方引入`Box::new` + /// * 📌需要直接传入闭包(要求全局周期`'static`) + pub fn new(i: I, o: O) -> Self + where + I: Fn(Cmd) -> Result + Send + Sync + 'static, + O: Fn(String) -> Result + Send + Sync + 'static, + { + Self { + input_translator: Box::new(i), + output_translator: Box::new(o), + } + } +} + +impl Default for IoTranslators { + /// 构造一个默认的「转译器组合」 + /// * 🎯默认生成的转译器 + /// * 输入:直接将NAVM指令转换为字符串 + /// * 输出:直接把字符串纳入「其它」输出 + /// * 📝【2024-03-27 10:34:02】下方`IoTranslators`无法换成`Self` + /// * `Self`意味着其带有类型约束 + /// * 📝【2024-03-27 10:37:37】不能直接使用裸露的闭包对象 + /// * 每个闭包都有不同类型⇒必须强迫使用泛型 + /// * 使用泛型⇒难以定义通用的[`Self::default`]方法 + fn default() -> IoTranslators { + IoTranslators { + input_translator: Box::new(|cmd| Ok(cmd.to_string())), + output_translator: Box::new(|content| Ok(Output::OTHER { content })), + } + } +} + +/// 从二元组转换 +/// * 🎯用于后续参数传入[`IoTranslators`]时,可以用`impl Into`,并且仍允许类似位置参数的效果 +/// * case: `fn set_translators(translators: impl Into)` +/// * call: `set_translators((in_translator, out_translator))` +/// * 📄[`super::super::CommandVm::translators`] +impl From<(I, O)> for IoTranslators +where + I: Fn(Cmd) -> Result + Send + Sync + 'static, + O: Fn(String) -> Result + Send + Sync + 'static, +{ + fn from(value: (I, O)) -> Self { + Self::new(value.0, value.1) + } +} + +/// 错误类型 +mod translate_error { + use super::*; + use anyhow::anyhow; + + /// 统一封装「转译错误」 + /// * 🎯用于在[`anyhow`]下封装字符串,不再使用裸露的[`String`]类型 + /// * 🎯用于可识别的错误,并在打印时直接展示原因 + /// * ⚠️若直接使用[`anyhow::anyhow`],会打印一大堆错误堆栈 + /// * 🚩【2024-04-02 22:05:30】现在展开成枚举 + /// * 🎯以便捕获方识别为「警告」 + #[derive(Debug, Error)] + pub enum TranslateError { + /// 不支持的NAVM指令 + /// * 📌一般处理方法:警告+静默置空 + /// * 📌用「调用者的处理场合」判断是「输入转译不支持」还是「输出转译不支持」 + #[error("不支持的NAVM指令:\"{0}\"")] + UnsupportedInput(Cmd), + /// 解析错误 + /// * 🎯表示原先的「转译错误」 + #[error("NAVM转译错误:「{0}」")] + ParseError(#[from] anyhow::Error), + } + + // ! ❌弃用:为一个泛型参数实现转换,会导致其它「泛型实现」无法使用 + // /// 灵活地从字符串转换为[`TranslateError`] + // impl> From for TranslateError { + // fn from(value: S) -> Self { + // Self::ParseError(value.as_ref().to_string()) + // } + // } + /// 灵活地从字符串转换为[`TranslateError`] + impl From<&'_ str> for TranslateError { + fn from(value: &'_ str) -> Self { + Self::ParseError(anyhow!("{value}")) + } + } + impl From<&'_ String> for TranslateError { + fn from(value: &'_ String) -> Self { + Self::ParseError(anyhow!("{value}")) + } + } + + /// 灵活地从[`Error`]转换为[`TranslateError`] + impl TranslateError { + /// 从[`Error`]转换为[`TranslateError`] + /// * 🚩目前还是调用 + pub fn from_error(value: impl Error) -> Self { + Self::from(&value.to_string()) + } + + /// 从[`Error`]转换为[`anyhow::Error`] + /// * 🚩【2024-04-02 22:39:47】此处「转换为[`anyhow::Error`]的需求」就是`Error + Send + Sync + 'static` + pub fn error_anyhow(value: impl Error + Send + Sync + 'static) -> anyhow::Error { + Self::ParseError(value.into()).into() + } + + /// 从「一切可以转换为其自身的值」构建[`anyhow::Result`] + pub fn err_anyhow(from: S) -> anyhow::Result + where + Self: From, + { + Err(Self::from(from).into()) + } + /// 从[`Self::from`]转换到[`anyhow::Error`] + /// * 🚩封装为自身类型 + /// * ❗实际上`.into()`比`::anyhow`短 + /// * 📌尽可能用前者 + pub fn anyhow(value: impl Into) -> anyhow::Error { + // ! ❌【2024-03-27 22:59:51】不能使用`Self::from(value).into`:`AsRef`不一定实现`Into` + anyhow::Error::from(value.into()) + } + } +} +pub use translate_error::*; + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + // TODO: 【2024-03-27 22:56:26】有待完善 + let _t1 = IoTranslators::default(); + } +} diff --git a/src/runtimes/command_vm/launcher.rs b/src/runtimes/command_vm/launcher.rs new file mode 100644 index 0000000..9628a37 --- /dev/null +++ b/src/runtimes/command_vm/launcher.rs @@ -0,0 +1,83 @@ +//! 命令行虚拟机(构建者) + +use super::{InputTranslator, IoTranslators, OutputTranslator}; +use crate::process_io::IoProcess; +use anyhow::Result; +use navm::{cmd::Cmd, output::Output}; +use std::{ffi::OsStr, process::Command}; + +/// 命令行虚拟机(构建者) +/// * 🎯配置化构造[`CommandVmRuntime`] +/// * 封装内部「输入输出进程」的「输出侦听器」逻辑 +/// * 🚩有关「启动」的流程,放在「虚拟机运行时」[`super::runtime`]中 +pub struct CommandVm { + /// 内部存储的「输入输出进程」 + pub(super) io_process: IoProcess, + + /// [`Cmd`]→进程输入 转译器 + pub(super) input_translator: Option>, + + /// 进程输出→[`Output`]转译器 + pub(super) output_translator: Option>, +} + +impl CommandVm { + /// 构造函数 + /// * 🚩接收一个可执行文件路径 + /// * 📌直接生成[`IoProcess`]对象,无需额外配置 + pub fn new(program_path: impl AsRef) -> Self { + let io_process = IoProcess::new(program_path); + Self::from(io_process) + } + + /// 配置/输入转译器 + /// * 💭何时Rust能给特征起别名。。 + /// * 🚩【2024-04-04 02:06:57】不再需要借走所有权 + /// * ✅链式操作现在可以使用[`util::manipulate`]简化 + pub fn input_translator( + &mut self, + translator: impl Fn(Cmd) -> Result + Send + Sync + 'static, + ) { + self.input_translator = Some(Box::new(translator)); + } + + /// 配置/输出转译器 + /// * 🚩【2024-04-04 02:06:57】不再需要借走所有权 + /// * ✅链式操作现在可以使用[`util::manipulate`]简化 + pub fn output_translator( + &mut self, + translator: impl Fn(String) -> Result + Send + Sync + 'static, + ) { + self.output_translator = Some(Box::new(translator)); + } + + /// 配置/输入输出转译器组 + pub fn translators(&mut self, translators: impl Into) { + // 一次实现俩 + let translators = translators.into(); + // 直接赋值 + self.input_translator = Some(translators.input_translator); + self.output_translator = Some(translators.output_translator); + } +} + +/// 实现/从[`IoProcess`]对象转换为[`CommandVm`]对象 +/// * ✅这里的[`IoProcess`]必定是未被启动的:Launch之后会变成其它类型 +impl From for CommandVm { + fn from(io_process: IoProcess) -> Self { + Self { + // IO进程 + io_process, + // 其它所有置空 + input_translator: None, + output_translator: None, + } + } +} + +/// 实现/从[`Command`]对象转换为[`CommandVm`]对象 +impl From for CommandVm { + fn from(command: Command) -> Self { + Self::from(IoProcess::from(command)) + } +} diff --git a/src/runtimes/command_vm/mod.rs b/src/runtimes/command_vm/mod.rs new file mode 100644 index 0000000..d0cacbf --- /dev/null +++ b/src/runtimes/command_vm/mod.rs @@ -0,0 +1,11 @@ +//! 基于「进程通信」与「IO转译器」的「命令行运行时」 +//! * 🎯基于进程通信与各CIN交互 + +util::pub_mod_and_pub_use! { + // 抽象API + api + // 启动器 + launcher + // 运行时 + runtime +} diff --git a/src/runtimes/command_vm/runtime.rs b/src/runtimes/command_vm/runtime.rs new file mode 100644 index 0000000..3a5cbf9 --- /dev/null +++ b/src/runtimes/command_vm/runtime.rs @@ -0,0 +1,550 @@ +//! 命令行虚拟机 运行时 +//! * ✨核心内容 +//! * ⇄ 基于「进程通信」的消息互转 +//! * 📌核心IO流程: +//! 1. NAVM指令[`Cmd`] >>> 进程输入 >>> 子进程 +//! 2. 子进程 >>> 进程输出 >>> NAVM输出[`Output`] +//! * 🚩实现方式:两处转译器 + +use super::{ + default_input_translator, default_output_translator, CommandVm, InputTranslator, + OutputTranslator, +}; +use crate::process_io::IoProcessManager; +use anyhow::{anyhow, Result}; +use nar_dev_utils::if_return; +use navm::{ + cmd::Cmd, + output::Output, + vm::{VmLauncher, VmRuntime, VmStatus}, +}; + +/// 命令行虚拟机运行时 +/// * 🎯封装「进程通信」逻辑 +pub struct CommandVmRuntime { + /// 封装的「进程管理者」 + /// * 🚩使用[`IoProcessManager`]封装「进程通信」的逻辑细节 + process: IoProcessManager, + + /// [`Cmd`]→进程输入 转译器 + input_translator: Box, + + /// 进程输出→[`Output`]转译器 + /// * 🚩【2024-03-24 02:06:27】至于「输出侦听」等后续处理,外置给其它专用「处理者」 + output_translator: Box, + + /// 用于指示的「状态」变量 + status: VmStatus, +} + +impl VmRuntime for CommandVmRuntime { + fn input_cmd(&mut self, cmd: Cmd) -> Result<()> { + // 尝试转译 + let input = (self.input_translator)(cmd)?; + // 当输入非空时,置入转译结果 + // * 🚩【2024-04-03 02:20:48】目前用「空字串」作为「空输入」的情形 + // TODO: 后续或将让「转译器」返回`Option` + // 空⇒提前返回 + if_return! { input.is_empty() => Ok(()) } + // 置入 + // * 🚩没有换行符 + // * 📌【2024-04-07 23:43:59】追踪「Websocket进程阻塞」漏洞:问题不在此,在`ws::Sender::send`处 + self.process.put_line(input) + } + + fn fetch_output(&mut self) -> Result { + let s = self.process.fetch_output()?; + (self.output_translator)(s) + } + + fn try_fetch_output(&mut self) -> Result> { + let s = self.process.try_fetch_output()?; + // 匹配分支 + match s { + // 有输出⇒尝试转译并返回 + Some(s) => Ok(Some({ + // 转译输出 + let output = (self.output_translator)(s)?; + // * 当输出为「TERMINATED」时,将自身终止状态置为「TERMINATED」 + if let Output::TERMINATED { description } = &output { + // ! 🚩【2024-04-02 21:39:56】目前将所有「终止」视作「意外终止」⇒返回`Err` + self.status = VmStatus::Terminated(Err(anyhow!(description.clone()))); + } + // 传出输出 + output + })), + // 没输出⇒没输出 | ⚠️注意:不能使用`map`,否则`?`穿透不出闭包 + None => Ok(None), + } + } + + fn status(&self) -> &VmStatus { + &self.status + } + + fn terminate(&mut self) -> Result<()> { + // 杀死子进程 + self.process.kill()?; + + // (杀死后)设置状态 + // * 🚩【2024-04-02 21:42:30】目前直接覆盖状态 + self.status = VmStatus::Terminated(Ok(())); + + // 返回「终止完成」 + Ok(()) + } +} + +/// 构建功能:启动命令行虚拟机 +impl VmLauncher for CommandVm { + fn launch(self) -> Result { + Ok(CommandVmRuntime { + // 状态:正在运行 + status: VmStatus::Running, + // 启动内部的「进程管理者」 + process: self.io_process.launch()?, + // 输入转译器 + input_translator: self + .input_translator + // 解包or使用默认值 + // * 🚩【2024-04-04 02:02:53】似乎不应有如此默认行为:后续若配置载入失败,将难以识别问题 + .unwrap_or(default_input_translator()), + // 输出转译器 + output_translator: self + .output_translator + // 解包or使用默认值 + // * 🚩【2024-04-04 02:02:53】似乎不应有如此默认行为:后续若配置载入失败,将难以识别问题 + .unwrap_or(default_output_translator()), + // * 🚩【2024-03-24 02:06:59】目前到此为止:只需处理「转译」问题 + }) + } +} + +/// 单元测试 +/// * 🎯作为任何NAVM运行时的共用测试包 +/// * 🚩【2024-03-29 23:23:12】进一步开放:仍然只限定在「测试」环境中使用 +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + cin_implements::common::generate_command, + runtimes::TranslateError, + tests::cin_paths::{OPENNARS, PYNARS_MODULE, PYNARS_ROOT}, + }; + use nar_dev_utils::manipulate; + use narsese::{ + api::{GetBudget, GetPunctuation, GetStamp, GetTerm, GetTruth}, + conversion::{ + inter_type::lexical_fold::TryFoldInto, + string::{ + impl_enum::format_instances::FORMAT_ASCII as FORMAT_ASCII_ENUM, + impl_lexical::{format_instances::FORMAT_ASCII, shortcuts::*}, + }, + }, + enum_narsese::{ + Budget as EnumBudget, Narsese as EnumNarsese, Sentence as EnumSentence, + Task as EnumTask, Truth as EnumTruth, + }, + lexical::Narsese, + }; + use std::process::Command; + use util::first; + + // ! 🚩【2024-04-07 12:09:44】现在路径统一迁移到`lib.rs`的`tests`模块下 + + const COMMAND_JAVA: &str = "java"; + const COMMAND_ARGS_JAVA: [&str; 2] = ["-Xmx1024m", "-jar"]; + + /// 实用测试工具/等待 + pub fn await_fetch_until( + vm: &mut CommandVmRuntime, + criterion: impl Fn(&Output, &str) -> bool, + ) -> Output { + // 不断拉取输出 + // TODO: 💭【2024-03-24 18:21:28】后续可以结合「流式处理者列表」做集成测试 + loop { + // 拉取输出及其内容 | ⚠️必要时等待(阻塞!) + let output = vm.fetch_output().unwrap(); + let raw_content = output.raw_content(); + // 展示输出 + match &output { + // 特别显示「回答」 + Output::ANSWER { .. } => println!("捕获到回答!内容:{output:?}"), + // 特别显示「操作」 + Output::EXE { operation, .. } => { + println!( + "捕获到操作!操作名称:{:?},内容:{:?}", + operation.operator_name, + operation + .params + .iter() + .map(|param| FORMAT_ASCII.format_term(param)) + .collect::>() + ) + } + _ => println!("捕获到其它输出!内容:{output:?}"), + } + // 包含⇒结束 + if criterion(&output, raw_content) { + break output; + } + } + } + + /// 实用测试工具/输入并等待 + pub fn input_cmd_and_await( + vm: &mut CommandVmRuntime, + cmd: Cmd, + criterion: impl Fn(&Output, &str) -> bool, + ) -> Output { + // 构造并输入任务 + vm.input_cmd(cmd).expect("无法输入指令!"); + // 「contains」非空⇒等待 + await_fetch_until(vm, criterion) + } + + /// 实用测试工具/输入并等待「是否包含」 + /// * 🚩`input_cmd_and_await`的简单封装 + /// * 🎯【2024-03-24 18:38:50】用于「输出转换」尚未成熟时 + #[inline(always)] + pub fn input_cmd_and_await_contains( + vm: &mut CommandVmRuntime, + cmd: Cmd, + expected_contains: &str, + ) -> Option { + // 空预期⇒直接输入 + // * 🎯在后边测试中统一使用闭包,并且不会因此「空头拉取输出」 + // * 📄【2024-03-24 18:47:20】有过「之前的CYC把Answer拉走了,导致后边的Answer等不到」的情况 + // * ⚠️不能简化:区别在「是否会拉取输入,即便条件永真」 + match expected_contains.is_empty() { + true => { + vm.input_cmd(cmd).expect("无法输入NAVM指令!"); + None + } + false => Some(input_cmd_and_await(vm, cmd, |_, raw_content| { + raw_content.contains(expected_contains) + })), + } + } + + /// 实用测试工具/输入并等待「Narsese回显」 + /// * 🚩`input_cmd_and_await`的简单封装 + /// * ✅【2024-03-29 22:55:11】现在「输出转换」已经成熟(可以提取出Narsese) + /// * 🚩通过「转换为『枚举Narsese』」以实现判等逻辑(主要为「语义相等」) + #[inline(always)] + pub fn input_cmd_and_await_narsese( + vm: &mut CommandVmRuntime, + cmd: Cmd, + expected: Narsese, + ) -> Output { + // 预先构建预期 + let expected = expected + .clone() + .try_fold_into(&FORMAT_ASCII_ENUM) + .expect("作为预期的词法Narsese无法折叠!"); + // 输入 & 等待 + input_cmd_and_await(vm, cmd, |out, _| { + // 有Narsese + out.get_narsese().is_some_and(|out| { + // 且与预期一致 + out.clone() // 必须复制:折叠消耗自身 + .try_fold_into(&FORMAT_ASCII_ENUM) + .is_ok_and(|out| is_expected_narsese(&expected, &out)) + }) + }) + } + + /// 判断「输出是否(在Narsese语义层面)符合预期」 + /// * 🎯词法Narsese⇒枚举Narsese,以便从语义上判断 + pub fn is_expected_narsese_lexical(expected: &Narsese, out: &Narsese) -> bool { + // 临时折叠预期 + let expected = (expected.clone().try_fold_into(&FORMAT_ASCII_ENUM)) + .expect("作为预期的词法Narsese无法折叠!"); + // 与预期一致 + (out.clone() // 必须复制:折叠消耗自身 + .try_fold_into(&FORMAT_ASCII_ENUM)) + .is_ok_and(|out| is_expected_narsese(&expected, &out)) + } + + /// 判断「输出是否(在Narsese层面)符合预期」 + /// * 🎯预期词项⇒只比较词项,语句⇒只比较语句,…… + pub fn is_expected_narsese(expected: &EnumNarsese, out: &EnumNarsese) -> bool { + match ((expected), (out)) { + // 词项⇒只比较词项 | 直接判等 + (EnumNarsese::Term(term), ..) => term == out.get_term(), + // 语句⇒只比较语句 + // ! 仍然不能直接判等:真值/预算值 + ( + EnumNarsese::Sentence(s_exp), + EnumNarsese::Sentence(s_out) | EnumNarsese::Task(EnumTask(s_out, ..)), + ) => is_expected_sentence(s_exp, s_out), + // 任务⇒直接判断 + // ! 仍然不能直接判等:真值/预算值 + (EnumNarsese::Task(t_exp), EnumNarsese::Task(t_out)) => is_expected_task(t_exp, t_out), + // 所有其它情况⇒都是假 + (..) => false, + } + } + + /// 判断输出的任务是否与预期任务相同 + /// * 🎯用于细粒度判断「预算值」「语句」的预期 + pub fn is_expected_task(expected: &EnumTask, out: &EnumTask) -> bool { + // 预算 + is_expected_budget(expected.get_budget(), out.get_budget()) + // 语句 + && is_expected_sentence(expected.get_sentence(), out.get_sentence()) + } + + /// 判断输出的语句是否与预期语句相同 + /// * 🎯用于细粒度判断「真值」的预期 + pub fn is_expected_sentence(expected: &EnumSentence, out: &EnumSentence) -> bool { + // 词项判等 + ((expected.get_term())==(out.get_term())) + // 标点相等 + && expected.get_punctuation() == out.get_punctuation() + // 时间戳相等 + && expected.get_stamp()== out.get_stamp() + // 真值兼容 | 需要考虑「没有真值可判断」的情况 + && match (expected.get_truth(),out.get_truth()) { + // 都有⇒判断「真值是否符合预期」 + (Some(t_e), Some(t_o)) => is_expected_truth(t_e, t_o), + // 都没⇒肯定真 + (None, None) => true, + // 有一个没有⇒肯定假 + _ => false, + } + } + + /// 判断「输出是否在真值层面符合预期」 + /// * 🎯空真值的语句,应该符合「固定真值的语句」的预期——相当于「通配符」 + pub fn is_expected_truth(expected: &EnumTruth, out: &EnumTruth) -> bool { + match (expected, out) { + // 预期空真值⇒通配 + (EnumTruth::Empty, ..) => true, + // 预期单真值 + (EnumTruth::Single(f_e), EnumTruth::Single(f_o) | EnumTruth::Double(f_o, ..)) => { + f_e == f_o + } + // 预期双真值 + (EnumTruth::Double(..), EnumTruth::Double(..)) => expected == out, + // 其它情况 + _ => false, + } + } + + /// 判断「输出是否在预算值层面符合预期」 + /// * 🎯空预算的语句,应该符合「固定预算值的语句」的预期——相当于「通配符」 + pub fn is_expected_budget(expected: &EnumBudget, out: &EnumBudget) -> bool { + match (expected, out) { + // 预期空预算⇒通配 + (EnumBudget::Empty, ..) => true, + // 预期单预算 + ( + EnumBudget::Single(p_e), + EnumBudget::Single(p_o) | EnumBudget::Double(p_o, ..) | EnumBudget::Triple(p_o, ..), + ) => p_e == p_o, + // 预期双预算 + ( + EnumBudget::Double(p_e, d_e), + EnumBudget::Double(p_o, d_o) | EnumBudget::Triple(p_o, d_o, ..), + ) => p_e == p_o && d_e == d_o, + // 预期三预算 + (EnumBudget::Triple(..), EnumBudget::Triple(..)) => expected == out, + // 其它情况 + _ => false, + } + } + + /// 示例测试 | OpenNARS + /// * 🚩通过Java命令启动 + #[test] + fn test_opennars() { + // 构造指令 + let mut command_java = Command::new(COMMAND_JAVA); + // * 📝这里的`args`、`arg都返回的可变借用。。 + command_java + .args(COMMAND_ARGS_JAVA) + .arg(OPENNARS) + // OpenNARS的默认参数 | ["null", "null", "null", "null"] + // * 🔗https://github.com/opennars/opennars/blob/master/src/main/java/org/opennars/main/Shell.java + // * ✅fixed「额外参数」问题:之前「IO进程」的测试代码`.arg("shell")`没删干净 + // .args(["null", "null", "null", "null"]) + ; + // dbg!(&command_java); + + /// 临时构建的「输入转换」函数 + /// * 🎯用于转换`VOL 0`⇒`*volume=0`,避免大量输出造成进程卡顿 + fn input_translate(cmd: Cmd) -> Result { + let content = match cmd { + // 直接使用「末尾」,此时将自动格式化任务(可兼容「空预算」的形式) + Cmd::NSE(..) => cmd.tail(), + // CYC指令:运行指定周期数 + Cmd::CYC(n) => n.to_string(), + // VOL指令:调整音量 + Cmd::VOL(n) => format!("*volume={n}"), + // 其它类型 + _ => return Err(TranslateError::UnsupportedInput(cmd).into()), + }; + // 转换 + Ok(content) + } + + /// 临时构建的「输出转换」函数 + fn output_translate(content: String) -> Result { + // 读取输出 + let output = first! { + // 捕获Answer + content.contains("Answer") => Output::ANSWER { content_raw: content, narsese: None }, + // 捕获OUT + content.contains("OUT") => Output::OUT { content_raw: content, narsese: None }, + // 其它情况 + _ => Output::OTHER { content }, + }; + // 返回 + Ok(output) + } + + // 构造并启动虚拟机 + let vm = manipulate!( + CommandVm::from(command_java) + // 输入转译器 + => .input_translator(input_translate) + // 输出转译器 + => .output_translator(output_translate) + ) + // 🔥启动 + .launch() + .expect("无法启动虚拟机"); + _test_opennars(vm); + } + + /// 通用测试/OpenNARS + pub fn _test_opennars(mut vm: CommandVmRuntime) { + // 专有闭包 | ⚠️无法再提取出另一个闭包:重复借用问题 + let mut input_cmd_and_await = + |cmd, contains| input_cmd_and_await_contains(&mut vm, cmd, contains); + // ! ✅【2024-03-25 13:54:36】现在内置进OpenNARS启动器,不再需要执行此操作 + input_cmd_and_await(Cmd::NSE(nse_task!( B>.)), " B>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>.)), " C>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>?)), " C>?"); + input_cmd_and_await(Cmd::CYC(5), ""); // * CYC无需自动等待 + + // 等待回答(字符串) + await_fetch_until(&mut vm, |_, s| { + s.contains("Answer") && s.contains(" C>.") + }); + + // 终止虚拟机 + vm.terminate().expect("无法终止虚拟机"); + println!("Virtual machine terminated..."); + } + + /// 示例测试 | PyNARS + /// * 🚩通过Python命令从**内置文件**启动 + #[test] + fn test_pynars() { + let vm = manipulate!( + CommandVm::from(generate_command("python", Some(PYNARS_ROOT), ["-m", PYNARS_MODULE])) + // 输入转译器:直接取其尾部 + => .input_translator(|cmd| Ok(cmd.tail())) + // 暂无输出转译器 + // => .output_translator(output_translate) + ) + // 🔥启动 + .launch() + .expect("无法启动虚拟机"); + // 可复用的测试逻辑 + _test_pynars(vm); + } + + /// 通用测试/ONA + pub fn _test_ona(mut vm: CommandVmRuntime) { + // 专有闭包 | ⚠️无法再提取出另一个闭包:重复借用问题 + let mut input_cmd_and_await = + |cmd, contains| input_cmd_and_await_contains(&mut vm, cmd, contains); + // input_cmd_and_await(Cmd::VOL(0), ""); + input_cmd_and_await(Cmd::NSE(nse_task!( B>.)), " B>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>.)), " C>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>?)), " C>?"); + input_cmd_and_await(Cmd::CYC(5), ""); // * CYC无需自动等待 + + // 等待回答(字符串) + await_fetch_until(&mut vm, |o, raw_content| { + matches!(o, Output::ANSWER { .. }) && raw_content.contains(" C>.") + }); + + // 终止虚拟机 + vm.terminate().expect("无法终止虚拟机"); + println!("Virtual machine terminated..."); + } + + /// 通用测试/PyNARS + pub fn _test_pynars(mut vm: CommandVmRuntime) { + // // 睡眠等待 + // // std::thread::sleep(std::time::Duration::from_secs(1)); + // ! ↑现在无需睡眠等待:输入会自动在初始化后写入子进程 + + // 专有闭包 + let mut input_cmd_and_await = + |cmd, contains| input_cmd_and_await_contains(&mut vm, cmd, contains); + + // 构造并输入任务 | 输入进PyNARS后变成了紧凑版本 + input_cmd_and_await(Cmd::NSE(nse_task!( B>.)), "B>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>.)), "C>."); + input_cmd_and_await(Cmd::NSE(nse_task!( C>?)), "C>?"); + input_cmd_and_await(Cmd::CYC(5), ""); // * CYC无需自动等待 + + // 等待回答 + await_fetch_until(&mut vm, |_, s| { + s.contains("ANSWER") && s.contains("C>.") + }); + + // 打印所有输出 + while let Some(output) = vm.try_fetch_output().unwrap() { + println!("{:?}", output); + } + + // 终止虚拟机 + vm.terminate().expect("无法终止虚拟机"); + println!("Virtual machine terminated..."); + // * 📝在实际测试中会使Python报错「EOFError: EOF when reading a line」 + /* // * ✅但这不影响(不会被「命令行虚拟机」捕获为输出) + traceback (most recent call last): + File "", line 198, in _run_module_as_main + File "", line 88, in _run_code + */ + } + + /// 通用测试/简单回答 | 基于Narsese + /// * 📌考察NARS最基础的「继承演绎推理」 + pub fn test_simple_answer(mut vm: CommandVmRuntime) { + // 构造并输入任务 | 输入进PyNARS后变成了紧凑版本 + let _ = vm.input_cmd(Cmd::VOL(0)); // * 尝试静音 + input_cmd_and_await_narsese(&mut vm, Cmd::NSE(nse_task!( B>.)), nse!( B>.)); + input_cmd_and_await_narsese(&mut vm, Cmd::NSE(nse_task!( C>.)), nse!( C>.)); + input_cmd_and_await_narsese(&mut vm, Cmd::NSE(nse_task!( C>?)), nse!( C>?)); + vm.input_cmd(Cmd::CYC(5)).expect("无法输入CYC指令"); // * CYC无需自动等待 + + // 等待回答 + let expected_answer = nse!( C>.); + await_fetch_until(&mut vm, |output, _| match output { + Output::ANSWER { narsese: out, .. } => { + is_expected_narsese_lexical( + &expected_answer, + // ! 不允许回答内容为空 | 必须拷贝再比对 + &out.clone().expect("预期的回答内容为空!"), + ) + } + _ => false, + }); + + // 打印所有输出 + while let Some(output) = vm.try_fetch_output().unwrap() { + println!("{:?}", output); + } + + // 终止虚拟机 + vm.terminate().expect("无法终止虚拟机"); + println!("Virtual machine terminated..."); + } +} diff --git a/src/runtimes/mod.rs b/src/runtimes/mod.rs new file mode 100644 index 0000000..bf5d808 --- /dev/null +++ b/src/runtimes/mod.rs @@ -0,0 +1,8 @@ +//! 用于封装表示「非公理虚拟机」的通用运行时支持 +//! * 📌不与特定的CIN相关 +//! * 📄一个「命令行运行时」可同时适用于OpenNARS、ONA、NARS-Python…… + +util::mods! { + // 命令行运行时 + pub pub command_vm; +} diff --git a/src/test_tools/mod.rs b/src/test_tools/mod.rs new file mode 100644 index 0000000..1f4b2ab --- /dev/null +++ b/src/test_tools/mod.rs @@ -0,0 +1,15 @@ +//! 有关NAVM的**测试工具集**支持 +//! * 🎯提供可复用的测试用代码 +//! * 🎯提供自动化测试工具 +//! * 🎯提供一种通用的`.nal`测试方法 +//! * ✅存量支持:兼容大部分OpenNARS、ONA的`.nal`文件 +//! * ✨增量特性:基于NAVM提供新的测试语法 + +util::mods! { + // 结构定义 + pub pub structs; + // NAL格式支持 + pub nal_format; + // NAVM交互 + pub pub vm_interact; +} diff --git a/src/test_tools/nal_format/mod.rs b/src/test_tools/nal_format/mod.rs new file mode 100644 index 0000000..f9698fb --- /dev/null +++ b/src/test_tools/nal_format/mod.rs @@ -0,0 +1,313 @@ +//! 基于NAVM的「统一`.nal`格式」支持 +//! * ✨语法(解析)支持 +//! * 🎯提供一种(部分)兼容现有`.nal`格式文件的语法 +//! * ⚠️对其中所有Narsese部分使用CommonNarsese「通用纳思语」:不兼容方言 + +use std::{result::Result::Err as StdErr, result::Result::Ok as StdOk, time::Duration}; + +use super::structs::*; +use anyhow::{Ok, Result}; +use narsese::{ + conversion::string::impl_lexical::format_instances::FORMAT_ASCII, + lexical::{Narsese, Sentence, Task}, +}; +use navm::{cmd::Cmd, output::Operation}; +use pest::{iterators::Pair, Parser}; +use pest_derive::Parser; +use util::{first, pipe}; + +#[derive(Parser)] // ! ↓ 必须从项目根目录开始 +#[grammar = "src/test_tools/nal_format/nal_grammar.pest"] +pub struct NALParser; + +/// 使用[`pest`]将整个`.nal`文件内容转换为[`NALInput`]结果序列 +/// * ✨也可只输入一行,用以解析单个[`NALInput`] +/// * 📌重点在其简写的「操作」语法`(^left, {SELF}, x)` => `<(*, {SELF}, x) --> ^left>` +pub fn parse(input: &str) -> Vec> { + input + // 切分并过滤空行 + .split('\n') + .map(str::trim) + .filter(|line| !line.is_empty()) + // 逐行解析 + .map(parse_single) + // 收集所有结果 + .collect::>() +} + +pub fn parse_single(line: &str) -> Result { + // 解析一行 + pipe! { + line + // 从一行输入解析到[`pest`]的一个[`Pairs`] + => NALParser::parse(Rule::nal_input, _) + // 发现错误即上抛 + => {?}# + // 🚩只对应[`Rule::nal_input`]规则,因此只会有一个[`Pair`],不会有其它情形 + => .next() + => .unwrap() + // 折叠,返回结果 + => fold_pest + } +} + +/// 将[`pest`]解析出的[`Pair`]辅助折叠到「词法Narsese」中 +/// * 🚩只需处理单行输入:行与行之间分开解析,避免上下文污染 +/// * 📌只会存在如下主要情况 +/// * `cyc_uint`:`CYC`语法糖,亦兼容原`.nal`格式 +/// * `narsese`:`NSE`语法糖,亦兼容原`.nal`格式 +/// * `comment`:各类或「魔法」或「非魔法」的注释 +fn fold_pest(pair: Pair) -> Result { + // * 🚩【2024-04-02 18:33:05】此处不用再`trim`了:入口`parse`已经做过 + let pair_str = pair.as_str(); + match pair.as_rule() { + // 一行的无符号整数 // + Rule::cyc_uint => { + // 仅取数字部分 + let n: usize = pair_str.parse()?; + // * 🚩作为`CYC`语法糖 + let input = NALInput::Put(Cmd::CYC(n)); + Ok(input) + } + // 一行的Narsese // + Rule::narsese => { + // 作为CommonNarsese,直接取字符串,然后调用CommonNarsese ASCII解析器 + // * 🚩【2024-03-31 16:37:32】虽可能有失灵活性,但代码上更显通用 + let narsese = pair_str; + let narsese = FORMAT_ASCII.parse(narsese)?.try_into_task_compatible()?; + // * 🚩作为`NSE`语法糖 + let input = NALInput::Put(Cmd::NSE(narsese)); + Ok(input) + } + // 各种魔法注释 // + // 单纯的行注释:`REM`语法糖 + Rule::comment_raw => { + // 仅取注释部分 + // ! 不能用`to_string`:后者只会显示其总体信息,而非捕获相应字符串切片 + let comment = pair_str.into(); + // * 🚩作为`REM`语法糖 + let input = NALInput::Put(Cmd::REM { comment }); + Ok(input) + } + // 魔法注释/置入指令 + Rule::comment_navm_cmd => { + // 取其中第一个`comment_raw`元素 | 一定只有唯一一个`comment_raw` + let comment_raw = pair.into_inner().next().unwrap(); + // 仅取注释部分 + let line = comment_raw.as_str().trim(); + // * 🚩作为所有NAVM指令的入口 + let input = NALInput::Put(Cmd::parse(line)?); + Ok(input) + } + // 魔法注释/睡眠等待 + Rule::comment_sleep => { + // 取其中第一个`comment_raw`元素 | 一定只有唯一一个`comment_raw` + let duration_raw = pair.into_inner().next().unwrap().as_str().trim(); + // 尝试解析时间 + let duration = first! { + // 毫秒→微秒→纳秒→秒 | 对于「秒」分「整数」「浮点」两种 + duration_raw.ends_with("ms") => Duration::from_millis(duration_raw.strip_suffix("ms").unwrap().parse()?), + duration_raw.ends_with("μs") => Duration::from_micros(duration_raw.strip_suffix("μs").unwrap().parse()?), + duration_raw.ends_with("ns") => Duration::from_nanos(duration_raw.strip_suffix("ns").unwrap().parse()?), + duration_raw.ends_with('s') && duration_raw.contains('.') => Duration::try_from_secs_f64(duration_raw.strip_suffix('s').unwrap().parse()?)?, + duration_raw.ends_with('s') => Duration::from_secs(duration_raw.strip_suffix('s').unwrap().parse()?), + // 否则报错 + _ => return Err(anyhow::anyhow!("未知的睡眠时间参数 {duration_raw:?}")) + }; + // * 封装 + let input = NALInput::Sleep(duration); + Ok(input) + } + // 魔法注释/等待 + Rule::comment_await => { + // 取其中唯一一个「输出预期」 + let output_expectation = pair.into_inner().next().unwrap(); + let output_expectation = fold_pest_output_expectation(output_expectation)?; + Ok(NALInput::Await(output_expectation)) + } + // 魔法注释/输出包含 + Rule::comment_expect_contains => { + // 取其中唯一一个「输出预期」 + let output_expectation = pair.into_inner().next().unwrap(); + let output_expectation = fold_pest_output_expectation(output_expectation)?; + Ok(NALInput::ExpectContains(output_expectation)) + } + // 魔法注释/保存输出 + Rule::comment_save_outputs => { + // 取其中唯一一个「输出预期」 + let file_path = pair.into_inner().next().unwrap().as_str().into(); + Ok(NALInput::SaveOutputs(file_path)) + } + // 魔法注释/终止 + Rule::comment_terminate => { + // 预置默认值 + let mut if_not_user = false; + let mut result = StdOk(()); + + // 遍历其中的Pair + for inner in pair.into_inner() { + // 逐个匹配规则类型 + // * ✨comment_terminate_option: `if-not-user` + // * ✨comment_raw: Err(`message`) + match inner.as_rule() { + // 可选规则 + Rule::comment_terminate_option => { + if inner.as_str() == "if-no-user" { + if_not_user = true; + } + } + // 错误消息 + Rule::comment_raw => { + // 构造错误 | 仅取注释部分 + result = StdErr(inner.as_str().trim().into()) + } + // 其它 + _ => unreachable!("不该被匹配到的规则\tpair = {inner:?}"), + } + } + + // 构造&返回 + Ok(NALInput::Terminate { + if_not_user, + result, + }) + } + // 其它情况 + _ => unreachable!("不该被匹配到的规则\tpair = {pair:?}"), + } +} + +/// 解析其中的「输出预期」[`Pair`] +/// * 🚩在「遍历内部元素」时消耗[`Pair`]对象 +#[inline] +fn fold_pest_output_expectation(pair: Pair) -> Result { + // 构造一个(全空的)输出预期对象 + let mut result = OutputExpectation::default(); + // 开始遍历其中的元素 + for inner in pair.into_inner() { + // 逐个匹配规则类型 + // * 🚩【2024-04-01 00:18:23】目前只可能有三个 + // * ✨输出类型 + // * ✨Narsese + // * ✨NAVM操作 + match inner.as_rule() { + // 输出类型 + Rule::output_type => { + // 取其中唯一一个`output_type_name` + // ! 不能用`to_string`:后者只会显示其总体信息,而非捕获相应字符串切片 + let output_type = inner.as_str().into(); + // 添加到结果中 + result.output_type = Some(output_type); + } + // Narsese + Rule::narsese => { + // 取其中唯一一个`narsese` + let narsese = inner.as_str(); + // 解析Narsese + let narsese = FORMAT_ASCII.parse(narsese)?; + // 添加到结果中 + result.narsese = Some(narsese); + } + // NAVM操作 + Rule::output_operation => result.operation = Some(fold_pest_output_operation(inner)?), + // 其它情况 + _ => unreachable!("不该被匹配到的规则\tpair = {inner:?}"), + } + } + + // 返回 + Ok(result) +} + +/// 解析其中的「NAVM操作」[`Pair`] +/// * 其中[`Pair`]的`rule`属性必是`output_operation` +#[inline] +fn fold_pest_output_operation(pair: Pair) -> Result { + // 生成迭代器 + let mut pairs = pair.into_inner(); + // 取第一个子Pair当操作名 | 语法上保证一定有 + let operator_name = pairs.next().unwrap().as_str().to_owned(); + // 操作参数 + let mut params = vec![]; + // 消耗剩下的,填充参数 + for inner in pairs { + // 尝试作为Narsese词项解析 | 无法使用 *narsese.get_term()强制转换成词项 + let term = match FORMAT_ASCII.parse(inner.as_str())? { + Narsese::Term(term) + | Narsese::Sentence(Sentence { term, .. }) + | Narsese::Task(Task { + sentence: Sentence { term, .. }, + .. + }) => term, + }; + // 添加到参数中 + params.push(term); + } + // 返回 + Ok(Operation { + operator_name, + params, + }) +} + +/// 单元测试 +#[cfg(test)] +pub mod tests { + use super::*; + use util::{for_in_ifs, list}; + + pub const TESTSET: &str = "\ +' 用于测试CIN的「简单演绎推理」 +' * 📝利用现有`Narsese`语法 +' +' 输出预期 +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` + +'/VOL 0 + B>. + C>. + C>? +5 +''sleep: 1s +''expect-contains: ANSWER C>. + +A3. :|: +<(*, {SELF}, (*, P1, P2)) --> ^left>. :|: +G3. :|: +A3. :|: +G3! :|: +''sleep: 500ms +10 + +''expect-contains: EXE (^left, {SELF}, (*, P1, P2)) +''terminate(if-no-user)"; + + #[test] + fn test_parse() { + _test_parse(" B>."); + _test_parse("5"); + _test_parse("'这是一个注释"); + _test_parse("'/VOL 0"); + _test_parse("'''VOL 0"); + _test_parse("''await: OUT B>."); + _test_parse("''sleep: 500ms"); + _test_parse("''sleep: 5000μs"); + _test_parse("''sleep: 600ns"); + _test_parse("''terminate(if-no-user): 异常的退出消息!"); + _test_parse(TESTSET); + } + + fn _test_parse(input: &str) { + let results = parse(input); + let results = list![ + (r.expect("解析失败!")) + for r in (results) + ]; + for_in_ifs! { + {println!("{:?}", r);} + for r in (results) + } + } +} diff --git a/src/test_tools/nal_format/nal_grammar.pest b/src/test_tools/nal_format/nal_grammar.pest new file mode 100644 index 0000000..fcfe616 --- /dev/null +++ b/src/test_tools/nal_format/nal_grammar.pest @@ -0,0 +1,233 @@ +//! 「统一`.nal`格式」语法 +//! * 🎯从常见的`.nal`文件中解析出「NAVM测试语句」 +//! * 🎯在「ASCII CommonNarsese」之下,附加注释与「测试预期」语法 +//! * 📝原则:凡是无法对应到「解析结果」(此处是NALInput结构)的,都给「静默」unwrap + +/// 空白符 | 所有非换行的Unicode空白符,解析前忽略 +/// * 🚩【2024-03-31 23:40:41】现在将「行分割」交给Rust预处理 +/// * 📌此处对「换行符」的特殊处理,基本不会用到 +WHITESPACE = _{ !"\n" ~ WHITE_SPACE } + +/// 多个`.nal`输入 +/// * 🚩【2024-03-31 23:41:33】目前不启用,将「行分割」交给Rust预处理 +nal_inputs = !{ + "\n"* ~ nal_input ~ ("\n"+ ~ nal_input)* +} + +/// 入口/单个`.nal`输入(静默展开) +/// * 🚩以数字代替`CYC`指令,并兼容原`.nal`语法 +/// * 🚩以直接的Narsese代替`NSE`指令,并兼容原`.nal`语法 +/// * 🚩在注释中扩展新语法 +nal_input = _{ + (cyc_uint | comment | narsese) +} + +/// 直接对应CYC指令的用法 +/// * ✅`CYC`的语法糖 +cyc_uint = { ASCII_DIGIT+ } + +/// 注释(静默) +/// * 🚩包括「输出预期」等「魔法注释」 +comment = _{ + comment_head ~ (comment_navm_cmd | comment_sleep | comment_await | comment_expect_contains | comment_save_outputs | comment_terminate | comment_raw) +} + +/// 注释的头部字符(静默) +comment_head = _{ "'" } + +/// 有关「置入命令」的「魔法注释」 +/// * ✨允许构建并向NAVM置入指令 +/// * 📄用`'/VOL 0`代替非通用的`*volume=0` +/// * 📄亦可直接用三个`'`:`'''VOL 0` +/// * 🚩【2024-04-02 23:44:06】现在使用`$`要求「特殊前缀」紧挨内容,避免误认「被注释掉的注释」为指令 +/// * ✅使用正谓词`&LETTER`要求必须是`/文字`而非其它(如`//`) +comment_navm_cmd = ${ + // 特殊前缀`/`或`''` + ("/" | "''") ~ &LETTER ~ comment_raw +} + +/// 有关「睡眠等待」的「魔法注释」 +/// * ✨允许构建并向NAVM置入指令 +/// * 📄用`'/VOL 0`代替非通用的`*volume=0` +/// * 具体的「时间格式」留给Rust侧 +comment_sleep = !{ + // 额外的前缀 + "'sleep:" ~ WHITESPACE* ~ comment_raw +} + +/// 有关「输出等待」的「魔法注释」 +/// ✨阻塞主线程,等待NAVM的某个输出再继续 +comment_await = { + // 额外的前缀 + "'await:" ~ output_expectation +} + +/// 有关「输出预期(包含)」的「魔法注释」 +/// ✨检查NAVM的所有输出,返回「是否有符合预期的输出」的[`Result`] +comment_expect_contains = { + // 额外的前缀 + "'expect-contains:" ~ output_expectation +} + +/// 有关「保存输出」的「魔法注释」 +/// ✨存储缓存的所有输出到指定路径下的文件(阻塞主线程) +comment_save_outputs = { + // 额外的前缀 + "'save-outputs:" ~ output_expectation +} + +/// 有关「终止」的「魔法注释」 +/// ✨终止NAVM虚拟机 +/// * 📄参数:选项、理由 +/// * 选项:决定执行的条件 +/// * 📌无 ⇒ 无条件强制退出 +/// * 📌有 ⇒ 有条件,或其它副作用 +/// * 理由:决定返回是否「正常」 +/// * 📌无理由 ⇒ 虚拟机返回 `Ok` +/// * 📌有理由 ⇒ 虚拟机返回 `Err(终止理由)` +comment_terminate = { + // 额外的前缀 | 可选的「错误」参数 + "'terminate" ~ ("(" ~ comment_terminate_option ~ ")")? ~ (":" ~ comment_raw)? +} + +/// 虚拟机终止指令的选项 +/// * 🎯控制终止的前提条件:可以在「终止」后交由用户输入 +comment_terminate_option = @{ "if-no-user" } + +/// 原始注释语法:纯粹的行注释 +/// * ✅`REM`的语法糖 +comment_raw = @{ (!"\n" ~ ANY)* } + +/// 输出预期 +/// * 📌只描述「预期的内容」,与「具体的使用方式」无关 +/// * 🚩【2024-03-31 17:10:03】目前不包含对「原始内容」的预期:并非跨CIN通用 +output_expectation = { + output_type? ~ narsese? ~ output_operation? +} + +/// NAVM输出的「类型」 +/// * 🚩直接使用内容 +/// * 📝原子操作配合空格识别 +output_type = @{ (!WHITE_SPACE ~ ANY)* } + +/// NAVM输出中「操作」的一种表征形式 +/// * 🚩刻意与CommonNarsese语法不一致,以便省去「XX=」前缀进行识别 +output_operation = { + "(" ~ "^" ~ atom_content ~ "," ~ term_list? ~ ")" +} + +/// Narsese | 优先级:任务 > 语句 > 词项 +/// * 🚩不使用「静默规则」,让剩下的语法树作为「Narsese边界匹配」用 +/// * 🚩不能使用「原子规则」匹配整个字符串:会导致匹配失败 +/// * ✅`NSE`的语法糖 +narsese = { + task + | sentence + | term +} + +/// 任务:有预算的语句 +task = { + budget ~ sentence +} + +/// 预算值 | 不包括「空字串」隐含的「空预算」 +budget = { + "$" ~ budget_content ~ "$" +} + +/// 预算值内容 +budget_content = _{ + (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) + | "" // 空预算(但带括号) +} + +/// 通用于真值、预算值的项 | 用作内部数值,不约束取值范围 +truth_budget_term = @{ (ASCII_DIGIT | ".")+ } + +/// 语句 = 词项 标点 时间戳? 真值? +sentence = { + term ~ punctuation ~ stamp? ~ truth? +} + +/// 词项 = 陈述 | 复合 | 原子 +term = _{ + statement + | compound + | atom +} + +/// 陈述 = <词项 系词 词项> +statement = { + "<" ~ term ~ copula ~ term ~ ">" +} + +/// 陈述系词 +copula = @{ + (punct_sym ~ "-" ~ punct_sym) // 继承/相似/实例/属性/实例属性 + + | (punct_sym ~ "=" ~ punct_sym) // 蕴含/等价 + + | ("=" ~ punct_sym ~ ">") // 时序性蕴含 + + | ("<" ~ punct_sym ~ ">") // 时序性等价 +} + +/// 标点符号 | 用于「原子词项前缀」「复合词项连接词」和「陈述系词」 +punct_sym = { (PUNCTUATION | SYMBOL) } + +/// 复合 = (连接词, 词项...) | {外延集...} | [内涵集...] +compound = { + compound_common + | ext_set + | int_set +} + +/// 通用的复合词项 +compound_common = { ("(" ~ connecter ~ "," ~ term_list ~ ")") } + +/// 通用的「词项列表」 | 静默展开 +term_list = _{ term ~ ("," ~ term)* } + +/// 外延集 | 📌【2024-03-29 09:39:39】pest代码折叠中会丢掉所有「不被规则捕获的字符串信息」 +ext_set = { "{" ~ term_list ~ "}" } + +/// 内涵集 +int_set = { "[" ~ term_list ~ "]" } + +/// 复合词项连接词 +connecter = @{ punct_sym ~ (!"," ~ punct_sym)* } + +/// 原子 = 前缀(可选) 内容 +atom = { + placeholder // 占位符 + + | (atom_prefix ~ atom_content) // 变量/间隔/操作…… + + | atom_content // 词语 +} + +/// 占位符 = 纯下划线字符串 +placeholder = @{ "_"+ } + +/// 原子词项前缀 +atom_prefix = @{ (!"<" ~ !"(" ~ !"[" ~ !"{" ~ punct_sym)+ } + +/// 原子词项内容 | 已避免与「复合词项系词」相冲突 +atom_content = @{ atom_char ~ (!copula ~ atom_char)* } + +/// 能作为「原子词项内容」的字符 +atom_char = { LETTER | NUMBER | "_" | "-" } + +/// 标点 +punctuation = { (PUNCTUATION | SYMBOL) } + +/// 时间戳 | 空时间戳会直接在「语句」中缺省 +stamp = { + ":" ~ (!":" ~ ANY)+ ~ ":" +} + +/// 真值 | 空真值会直接在「语句」中缺省 +truth = { + "%" ~ (truth_budget_term ~ (";" ~ truth_budget_term)* ~ ";"*) ~ "%" +} diff --git a/src/test_tools/structs.rs b/src/test_tools/structs.rs new file mode 100644 index 0000000..1243232 --- /dev/null +++ b/src/test_tools/structs.rs @@ -0,0 +1,115 @@ +//! 有关「NAVM测试工具」的数据结构支持 +//! * 🎯构造在「NAVM指令」之上的超集,支持与测试有关的数据结构存储 +//! * ✨[`NALInput`]:在「直接对应CIN输入输出」的「NAVM指令」之上,引入「等待」「预期」等机制 +//! * ✨[`OutputExpectation`]:面向NAL测试,具体实现「预期」机制 + +use narsese::{conversion::string::impl_lexical::format_instances::FORMAT_ASCII, lexical::Narsese}; +use navm::{cmd::Cmd, output::Operation}; +use std::{fmt::Display, time::Duration}; +use thiserror::Error; + +/// NAVM测试中的「NAL输入」 +/// * 📌`.nal`文件中一行的超集 +/// * 🎯在原有NAVM指令下,扩展与测试有关的功能 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NALInput { + /// 置入 + /// * 🎯向CIN置入NAVM指令 + Put(Cmd), + + /// 睡眠 + /// * 📄语法示例:`''sleep 1s` + /// * 📌调用[`thread::sleep`]单纯等待一段时间(单位:[`Duration`]) + /// * 🚩语法中常用的是秒数,但这里不直接存储 + Sleep(Duration), + + /// 输出等待 + /// * 📄语法示例:`''await: IN B>.` + /// * 📌在CIN输出与指定[`Output`]符合后,再继续运行 + /// * 🎯用于结合`IN`等待CIN「回显」 + Await(OutputExpectation), + + /// 对「输出含有」的预期 + /// * 📄语法示例:`''expect-contains: ANSWER C>.` + /// * 🎯用于「在现有的输出中检查是否任一和指定的[`Output`]符合」 + /// * 📄对应OpenNARS中常有的`''outputMustContain('')` + ExpectContains(OutputExpectation), + + /// 保存「输出缓存」到指定文件 + /// * 📄语法示例:`''save-outputs: outputs.log` + /// * 🎯用于「将现有所有输出以『NAVM输出的JSON格式』存档至指定文件中」 + SaveOutputs(String), + + /// 终止虚拟机 + /// * 🎯用于「预加载NAL『测试』结束后,程序自动退出/交给用户输入」 + /// * 📄语法示例: + /// * `''terminate` + /// * `''terminate(if-no-user): 异常的退出消息!` + /// * 🔧可选的「子参数」 + /// * `if-no-user`:仅在「用户无法输入」时退出 + Terminate { + /// 仅在「用户无法输入」时退出 + /// * 🎯用于「测试完毕后交给用户输入」的测试 + if_not_user: bool, + + /// 退出的返回值 + /// * 🎯用于「测试完毕后向外部传递结果」的测试 + /// * 💭始终注意这只是个线性执行的指令,不要做得太复杂 + /// * 🚩【2024-04-02 23:56:34】目前不在此装载[`anyhow::Error`]类型:避免复杂 + result: std::result::Result<(), String>, + }, +} + +/// 输出预期 +/// * 📌对应语法中的`output_expectation`结构 +/// * 🎯用于统一表示对「NAVM输出」的预期 +/// * 🚩除了「原始内容」外,与[`Output`]类型一致 +/// * ✨可进行有关「检查范围」「严格性」等更细致的配置,而非仅仅是「文本包含」 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct OutputExpectation { + /// 预期的「输出类型」 + /// * 🚩可能没有:此时是「通配」情形 + /// * 对任何可能的输入都适用 + pub output_type: Option, + + /// 预期的「Narsese」字段 + /// * 🚩可能没有:此时是「通配」情形 + /// * 对任何可能的输入都适用 + /// * 🚩对内部[`Narsese`]会进行**递归匹配** + pub narsese: Option, + + /// 预期的「NAVM操作」字段 + /// * 🚩可能没有:此时是「通配」情形 + /// * 对任何可能的输入都适用 + pub operation: Option, +} + +impl Display for OutputExpectation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "OutputExpectation {{ {} {} {} }}", + self.output_type.as_deref().unwrap_or("*"), + match &self.narsese { + Some(narsese) => FORMAT_ASCII.format_narsese(narsese), + None => "*".to_string(), + }, + self.operation + .as_ref() + .map(|op| op.to_string()) + .unwrap_or("*".to_string()), + ) + } +} + +/// 预期错误 +/// * 🎯用于定义可被识别的「NAL预期失败/脱离预期」错误 +/// * 🚩使用[`thiserror`]快捷定义 +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum OutputExpectationError { + /// 输出未包含预期 + /// * 🎯对应[`NALInput::ExpectContains`] + /// * 📝此处`{0:?}`参照 + #[error("输出内容中不存在符合预期的输出:{0}")] + ExpectedNotExists(OutputExpectation), +} diff --git a/src/test_tools/vm_interact.rs b/src/test_tools/vm_interact.rs new file mode 100644 index 0000000..561f6ca --- /dev/null +++ b/src/test_tools/vm_interact.rs @@ -0,0 +1,312 @@ +//! 与NAVM虚拟机的交互逻辑 + +use std::{ops::ControlFlow, path::Path}; + +use crate::cli_support::error_handling_boost::error_anyhow; + +use super::{NALInput, OutputExpectation, OutputExpectationError}; +use anyhow::Result; +use nar_dev_utils::{if_return, ResultBoost}; +use navm::{output::Output, vm::VmRuntime}; + +/// * 🎯统一存放与「Narsese预期识别」有关的代码 +/// * 🚩【2024-04-02 22:49:12】从[`crate::runtimes::command_vm::runtime::tests`]中迁移而来 +mod narsese_expectation { + use nar_dev_utils::if_return; + use narsese::{ + api::{GetBudget, GetPunctuation, GetStamp, GetTerm, GetTruth}, + conversion::{ + inter_type::lexical_fold::TryFoldInto, + string::impl_enum::format_instances::FORMAT_ASCII as FORMAT_ASCII_ENUM, + }, + enum_narsese::{ + Budget as EnumBudget, Narsese as EnumNarsese, Sentence as EnumSentence, + Task as EnumTask, Term as EnumTerm, Truth as EnumTruth, + }, + lexical::Narsese, + }; + use navm::output::Operation; + + /// 判断「输出是否(在Narsese语义层面)符合预期」 + /// * 🎯词法Narsese⇒枚举Narsese,以便从语义上判断 + pub fn is_expected_narsese_lexical(expected: &Narsese, out: &Narsese) -> bool { + // 临时折叠预期 + let expected = (expected.clone().try_fold_into(&FORMAT_ASCII_ENUM)) + .expect("作为预期的词法Narsese无法折叠!"); + // 与预期一致 + (out.clone() // 必须复制:折叠消耗自身 + .try_fold_into(&FORMAT_ASCII_ENUM)) + .is_ok_and(|out| is_expected_narsese(&expected, &out)) + } + + /// 判断「输出是否(在Narsese层面)符合预期」 + /// * 🎯预期词项⇒只比较词项,语句⇒只比较语句,…… + pub fn is_expected_narsese(expected: &EnumNarsese, out: &EnumNarsese) -> bool { + match ((expected), (out)) { + // 词项⇒只比较词项 | 直接判等 + (EnumNarsese::Term(term), ..) => is_expected_term(term, out.get_term()), + // 语句⇒只比较语句 + // ! 仍然不能直接判等:真值/预算值 + ( + EnumNarsese::Sentence(s_exp), + EnumNarsese::Sentence(s_out) | EnumNarsese::Task(EnumTask(s_out, ..)), + ) => is_expected_sentence(s_exp, s_out), + // 任务⇒直接判断 + // ! 仍然不能直接判等:真值/预算值 + (EnumNarsese::Task(t_exp), EnumNarsese::Task(t_out)) => is_expected_task(t_exp, t_out), + // 所有其它情况⇒都是假 + (..) => false, + } + } + + /// 判断输出的任务是否与预期任务相同 + /// * 🎯用于细粒度判断「预算值」「语句」的预期 + pub fn is_expected_task(expected: &EnumTask, out: &EnumTask) -> bool { + // 预算 + is_expected_budget(expected.get_budget(), out.get_budget()) + // 语句 + && is_expected_sentence(expected.get_sentence(), out.get_sentence()) + } + + /// 判断输出的语句是否与预期语句相同 + /// * 🎯用于细粒度判断「真值」的预期 + pub fn is_expected_sentence(expected: &EnumSentence, out: &EnumSentence) -> bool { + // 词项 + (is_expected_term(expected.get_term(),out.get_term())) + // 标点相等 + && expected.get_punctuation() == out.get_punctuation() + // 时间戳相等 + && expected.get_stamp()== out.get_stamp() + // 真值兼容 | 需要考虑「没有真值可判断」的情况 + && match (expected.get_truth(),out.get_truth()) { + // 都有⇒判断「真值是否符合预期」 + (Some(t_e), Some(t_o)) => is_expected_truth(t_e, t_o), + // 都没⇒肯定真 + (None, None) => true, + // 有一个没有⇒肯定假 + _ => false, + } + } + + /// 判断输出的词项是否与预期词项相同 + /// * 🎯用于独立出「词项预期」功能 + /// * 🚩【2024-04-02 22:55:13】目前直接判等 + pub fn is_expected_term(expected: &EnumTerm, out: &EnumTerm) -> bool { + expected == out + } + + /// 判断「输出是否在真值层面符合预期」 + /// * 🎯空真值的语句,应该符合「固定真值的语句」的预期——相当于「通配符」 + pub fn is_expected_truth(expected: &EnumTruth, out: &EnumTruth) -> bool { + match (expected, out) { + // 预期空真值⇒通配 + (EnumTruth::Empty, ..) => true, + // 预期单真值 + (EnumTruth::Single(f_e), EnumTruth::Single(f_o) | EnumTruth::Double(f_o, ..)) => { + f_e == f_o + } + // 预期双真值 + (EnumTruth::Double(..), EnumTruth::Double(..)) => expected == out, + // 其它情况 + _ => false, + } + } + + /// 判断「输出是否在预算值层面符合预期」 + /// * 🎯空预算的语句,应该符合「固定预算值的语句」的预期——相当于「通配符」 + pub fn is_expected_budget(expected: &EnumBudget, out: &EnumBudget) -> bool { + match (expected, out) { + // 预期空预算⇒通配 + (EnumBudget::Empty, ..) => true, + // 预期单预算 + ( + EnumBudget::Single(p_e), + EnumBudget::Single(p_o) | EnumBudget::Double(p_o, ..) | EnumBudget::Triple(p_o, ..), + ) => p_e == p_o, + // 预期双预算 + ( + EnumBudget::Double(p_e, d_e), + EnumBudget::Double(p_o, d_o) | EnumBudget::Triple(p_o, d_o, ..), + ) => p_e == p_o && d_e == d_o, + // 预期三预算 + (EnumBudget::Triple(..), EnumBudget::Triple(..)) => expected == out, + // 其它情况 + _ => false, + } + } + + /// 判断「输出是否在操作层面符合预期」 + /// * 🎯仅有「操作符」的「NARS操作」应该能通配所有「NARS操作」 + pub fn is_expected_operation(expected: &Operation, out: &Operation) -> bool { + // 操作符名不同⇒直接pass + if_return! { expected.operator_name != out.operator_name => false } + + // 比对操作参数:先判空 + match (expected.no_params(), out.no_params()) { + // 预期无⇒通配 + (true, ..) => true, + // 预期有,输出无⇒直接pass + (false, true) => false, + // 预期有,输出有⇒判断参数是否相同 + (false, false) => expected.params == out.params, + } + } +} +pub use narsese_expectation::*; + +/// 实现/预期匹配功能 +impl OutputExpectation { + /// 判断一个「NAVM输出」是否与自身相符合 + pub fn matches(&self, output: &Output) -> bool { + // 输出类型 + if let Some(expected) = &self.output_type { + if_return! { expected != output.type_name() => false } + } + + // Narsese + match (&self.narsese, output.get_narsese()) { + // 预期有,输出无⇒直接pass + (Some(..), None) => return false, + // 预期输出都有⇒判断Narsese是否相同 + (Some(expected), Some(out)) => { + if_return! { !is_expected_narsese_lexical(expected, out) => false } + } + _ => (), + } + + // 操作 | 最后返回 + match (&self.operation, output.get_operation()) { + // 预期无⇒通配 + (None, ..) => true, + // 预期有,输出无⇒直接pass + (Some(_), None) => false, + // 预期有,输出有⇒判断操作是否相同 + (Some(expected), Some(out)) => is_expected_operation(expected, out), + } + } +} + +/// 输出缓存 +/// * 🎯为「使用『推送』功能,而不引入具体数据类型」设置 +/// * 📌基础功能:推送输出、遍历输出 +pub trait VmOutputCache { + /// 存入输出 + /// * 🎯统一的「打印输出」逻辑 + fn put(&mut self, output: Output) -> Result<()>; + + /// 遍历输出 + /// * 🚩不是返回迭代器,而是用闭包开始计算 + /// * 📝使用最新的「控制流」数据结构 + /// * 使用[`None`]代表「一路下来没`break`」 + fn for_each(&self, f: impl FnMut(&Output) -> ControlFlow) -> Result>; +} + +/// 向虚拟机置入[`NALInput`] +/// * 🎯除了「输入指令」之外,还附带其它逻辑 +/// * 🚩通过「输出缓存」参数,解决「缓存输出」问题 +/// * ❓需要迁移「符合预期」的逻辑 +pub fn put_nal( + vm: &mut impl VmRuntime, + input: NALInput, + output_cache: &mut impl VmOutputCache, + // 不能传入「启动配置」,就要传入「是否启用用户输入」状态变量 + enabled_user_input: bool, + nal_root_path: &Path, +) -> Result<()> { + match input { + // 置入NAVM指令 + NALInput::Put(cmd) => vm.input_cmd(cmd), + // 睡眠 + NALInput::Sleep(duration) => { + // 睡眠指定时间 + std::thread::sleep(duration); + // 返回`ok` + Ok(()) + } + // 等待一个符合预期的NAVM输出 + NALInput::Await(expectation) => loop { + let output = match vm.fetch_output() { + Ok(output) => { + // 加入缓存 + output_cache.put(output.clone())?; + // ! ❌【2024-04-03 01:19:06】无法再返回引用:不再能直接操作数组,MutexGuard也不允许返回引用 + // output_cache.last().unwrap() + output + } + Err(e) => { + println!("尝试拉取输出出错:{e}"); + continue; + } + }; + // 只有匹配了才返回 + if expectation.matches(&output) { + break Ok(()); + } + }, + // 检查是否有NAVM输出符合预期 + NALInput::ExpectContains(expectation) => { + // 先尝试拉取所有输出到「输出缓存」 + while let Some(output) = vm.try_fetch_output()? { + output_cache.put(output)?; + } + // 然后读取并匹配缓存 + let result = output_cache.for_each(|output| match expectation.matches(output) { + true => ControlFlow::Break(true), + false => ControlFlow::Continue(()), + })?; + match result { + // 只有匹配到了一个,才返回Ok + Some(true) => Ok(()), + // 否则返回Err + _ => Err(OutputExpectationError::ExpectedNotExists(expectation).into()), + } + // for output in output_cache.for_each() { + // // 只有匹配了才返回Ok + // if expectation.matches(output) { + // } + // } + } + // 保存(所有)输出 + // * 🚩输出到一个文本文件中 + // * ✨复合JSON「对象数组」格式 + NALInput::SaveOutputs(path_str) => { + // 先收集所有输出的字符串 + let mut file_str = "[".to_string(); + output_cache.for_each(|output| { + // 换行制表 + file_str += "\n\t"; + // 统一追加到字符串中 + file_str += &output.to_json_string(); + // 逗号 + file_str.push(','); + // 继续 + ControlFlow::<()>::Continue(()) + })?; + // 删去尾后逗号 + file_str.pop(); + // 换行,终止符 + file_str += "\n]"; + // 保存到文件中 | 使用基于`nal_root_path`的相对路径 + let path = nal_root_path.join(path_str.trim()); + std::fs::write(path, file_str)?; + // 提示 | ❌【2024-04-09 22:22:04】执行「NAL输入」时,应始终静默 + // println_cli!([Info] "已将所有NAVM输出保存到文件{path:?}"); + // 返回 + Ok(()) + } + // 终止虚拟机 + NALInput::Terminate { + if_not_user, + result, + } => { + // 检查前提条件 | 仅「非用户输入」&启用了用户输入 ⇒ 放弃终止 + if_return! { if_not_user && enabled_user_input => Ok(()) } + + // 终止虚拟机 + vm.terminate()?; + + // 返回 + result.transform_err(error_anyhow) + } + } +} diff --git a/src/tests/cli/config/_arg_parse_test.opennars.hjson b/src/tests/cli/config/_arg_parse_test.opennars.hjson new file mode 100644 index 0000000..b24ffc1 --- /dev/null +++ b/src/tests/cli/config/_arg_parse_test.opennars.hjson @@ -0,0 +1,23 @@ +#hjson +// * ⚠️仅作「读取配置」测试用 +// * 📌包含OpenNARS转译器 及其jar启动的命令配置 +// * 🎯用于测试「预加载NAL输入」,加载「简单演绎推理」 +{ + // 转译器 + translators: "opennars" + // 启动命令 + command: { + // 命令:启动java运行时 + cmd: "java" + // 传入的命令参数 + cmdArgs: [ + // 设置最大堆内存为1024M + "-Xmx1024m" + // 启动jar包 + -jar + nars.jar + ] + // 启动时的工作目录 | 仅测试「以配置自身为根」 + currentDir: ./../executables + } +} \ No newline at end of file diff --git a/src/tests/cli/config/cin_cxin_js.hjson b/src/tests/cli/config/cin_cxin_js.hjson new file mode 100644 index 0000000..3999356 --- /dev/null +++ b/src/tests/cli/config/cin_cxin_js.hjson @@ -0,0 +1,19 @@ +#hjson +// * 🎯用于测试CXinNARS(JS版本) +// * 📌使用Node.js启动 +{ + // 转译器支持单独指定「输入转译器」和「输出转译器」 + translators: cxin_js + command: { + // * ⚠️必须前缀`./`以指定是「启动当前工作目录下的exe文件」 + cmd: node + cmdArgs: [ + cxin-nars-shell.js + shell + ] + // * 🚩现在基于「固定位置的CIN程序包」运行测试 + // * 回溯路径:config(`./`) => cli => tests => src => BabelNAR.rs / executables + currentDir: ./../../../../executables + } + autoRestart: true +} \ No newline at end of file diff --git a/src/tests/cli/config/cin_native_il_1.hjson b/src/tests/cli/config/cin_native_il_1.hjson new file mode 100644 index 0000000..35597d4 --- /dev/null +++ b/src/tests/cli/config/cin_native_il_1.hjson @@ -0,0 +1,16 @@ +#hjson +// * 🎯用于测试原生「IL-1」运行时 +// * ✨基于NAVM,纯Rust编写 +{ + // 使用「原生」输入输出转译器 + translators: native + command: { + // * ⚠️必须前缀`./`以指定是「启动当前工作目录下的exe文件」 + cmd: ./native-IL-1.exe + cmdArgs: [] + // * 🚩现在基于「固定位置的CIN程序包」运行测试 + // * 回溯路径:config(`./`) => cli => tests => src => BabelNAR.rs / executables + currentDir: ./../../../../executables + } + autoRestart: true +} \ No newline at end of file diff --git a/src/tests/cli/config/cin_ona.hjson b/src/tests/cli/config/cin_ona.hjson new file mode 100644 index 0000000..d8ce203 --- /dev/null +++ b/src/tests/cli/config/cin_ona.hjson @@ -0,0 +1,22 @@ +#hjson +// * 🎯用于测试ONA +// * ⚠️启动需要cygwin +// * 🔗中文官网: http://www.cygwin.cn/ +{ + // 转译器支持单独指定「输入转译器」和「输出转译器」 + translators: { + in: ona + out: ona + } + command: { + // * ⚠️必须前缀`./`以指定是「启动当前工作目录下的exe文件」 + cmd: ./ONA.exe + cmdArgs: [ + shell + ] + // * 🚩现在基于「固定位置的CIN程序包」运行测试 + // * 回溯路径:config(`./`) => cli => tests => src => BabelNAR.rs / executables + currentDir: ./../../../../executables + } + autoRestart: true +} \ No newline at end of file diff --git a/src/tests/cli/config/cin_opennars.hjson b/src/tests/cli/config/cin_opennars.hjson new file mode 100644 index 0000000..cad8431 --- /dev/null +++ b/src/tests/cli/config/cin_opennars.hjson @@ -0,0 +1,24 @@ +#hjson +// * 📌包含OpenNARS转译器 及其jar启动的命令配置 +// * 🎯用于测试「预加载NAL输入」,加载「简单演绎推理」 +{ + // 转译器 + translators: "opennars" + // 启动命令 + command: { + // 命令:启动java运行时 + cmd: "java" + // 传入的命令参数 + cmdArgs: [ + // 设置最大堆内存为1024M + "-Xmx1024m" + // 启动jar包 + -jar + ./opennars-304-T-modified.jar + ] + // 启动时的工作目录 + // * 🚩现在基于「固定位置的CIN程序包」运行测试 + // * 回溯路径:config(`./`) => cli => tests => src => BabelNAR.rs / executables + currentDir: ./../../../../executables + } +} \ No newline at end of file diff --git a/src/tests/cli/config/cin_pynars.hjson b/src/tests/cli/config/cin_pynars.hjson new file mode 100644 index 0000000..a11d001 --- /dev/null +++ b/src/tests/cli/config/cin_pynars.hjson @@ -0,0 +1,15 @@ +// PyNARS的启动配置 +{ + translators: pynars + command: { + cmd: python + cmdArgs: [ + "-m" + // * 🚩【2024-04-07 14:41:20】使用扩展了「附加指令」的「高级控制台」 + pynars.ConsolePlus + ] + // * 🚩【2024-04-07 14:26:30】现在「CIN测试用运行时」路径已固定 + currentDir: ./../../../../executables/PyNARS + } + autoRestart: true +} \ No newline at end of file diff --git a/src/tests/cli/config/matriangle_server.hjson b/src/tests/cli/config/matriangle_server.hjson new file mode 100644 index 0000000..bac6e35 --- /dev/null +++ b/src/tests/cli/config/matriangle_server.hjson @@ -0,0 +1,18 @@ +#hjson +// * 🎯对接Matriangle服务器 +// * ✨兼容旧BabelNAR与Matriangle的Websocket交互逻辑 +{ + // Websocket服务端地址 + websocket: { + // * ❌【2024-04-07 23:05:21】不能是`localhost`,需要是`127.0.0.1`(Matriangle端要求) + host: 127.0.0.1 + port: 8765 + } + // 【2024-04-04 04:49:32】Matriangle环境目前以「NAVM指令」的形式传入 + inputMode: cmd + preludeNAL: { + // 预置的NAL指令 + // * 🔬【2024-04-08 15:43:27】控制程序输出:当产生大量输出时,将会发生线程死锁 + text: "'''VOL 0" + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_higher_deduction.hjson b/src/tests/cli/config/nal_higher_deduction.hjson new file mode 100644 index 0000000..453b251 --- /dev/null +++ b/src/tests/cli/config/nal_higher_deduction.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「高阶演绎推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_higher_deduction.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_i_var_elimination.hjson b/src/tests/cli/config/nal_i_var_elimination.hjson new file mode 100644 index 0000000..3df33eb --- /dev/null +++ b/src/tests/cli/config/nal_i_var_elimination.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「自变量消去推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_i_var_elimination.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_operation.hjson b/src/tests/cli/config/nal_operation.hjson new file mode 100644 index 0000000..2144a80 --- /dev/null +++ b/src/tests/cli/config/nal_operation.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「操作推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_operation.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_simple_deduction.hjson b/src/tests/cli/config/nal_simple_deduction.hjson new file mode 100644 index 0000000..d533c72 --- /dev/null +++ b/src/tests/cli/config/nal_simple_deduction.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「简单演绎推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_simple_deduction.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_simple_operation.hjson b/src/tests/cli/config/nal_simple_operation.hjson new file mode 100644 index 0000000..8b3f9c7 --- /dev/null +++ b/src/tests/cli/config/nal_simple_operation.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「简单操作推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_simple_operation.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/nal_temporal_induction.hjson b/src/tests/cli/config/nal_temporal_induction.hjson new file mode 100644 index 0000000..7b54610 --- /dev/null +++ b/src/tests/cli/config/nal_temporal_induction.hjson @@ -0,0 +1,10 @@ +#hjson +// * 🎯测试「时间归纳推理」 +// * ℹ️测试环境交由`prelude_test.hjson`加载 +// * 📌原则:每个配置文件中引用的相对路径,均基于「配置文件自身」的路径 +{ + preludeNAL: { + // 预置的NAL测试文件(相对配置文件自身) + file: ./../../nal/test_temporal_induction.nal + } +} \ No newline at end of file diff --git a/src/tests/cli/config/prelude_test.hjson b/src/tests/cli/config/prelude_test.hjson new file mode 100644 index 0000000..f075a3c --- /dev/null +++ b/src/tests/cli/config/prelude_test.hjson @@ -0,0 +1,14 @@ +#hjson +// * 🎯测试「预加载NAL输入」,并统一设置「测试环境」 +// * ⚠️不包括具体的「预引入NAL」文件定义 +{ + // preludeNAL: "" + // 禁止用户输入 + userInput: false + // 不自动重启 + autoRestart: false + // 开启严格模式 + // * 🎯用于自动化测试中捕获错误 + // * ✨可由此被外部脚本调用 + strictMode: true +} \ No newline at end of file diff --git a/src/tests/cli/config/websocket.hjson b/src/tests/cli/config/websocket.hjson new file mode 100644 index 0000000..1b09197 --- /dev/null +++ b/src/tests/cli/config/websocket.hjson @@ -0,0 +1,8 @@ +#hjson +// 用于测试可作补丁的Websocket配置 +{ + websocket: { + host: localhost + port: 8080 + } +} \ No newline at end of file diff --git a/src/tests/nal/test_higher_deduction.nal b/src/tests/nal/test_higher_deduction.nal new file mode 100644 index 0000000..a73a549 --- /dev/null +++ b/src/tests/nal/test_higher_deduction.nal @@ -0,0 +1,35 @@ +' 用于测试CIN的「高阶演绎推理」 +' * 📍所涉及NAL层级:NAL-5 +' * 📝在「文件表示」上利用现有`Narsese`语法 +' +' 输出等待 `expect-contains` +' * 📝统一的NAL等待语法:`''await: 【输入类别】 【其它内容】` +' * ⚠️可能会阻塞测试,慎用 +' * 🚩以下await已被注释失效,仅作语法演示 +' +' 输出预期 `expect-contains` +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` + +' 🚩降低音量,减少无关输出 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + +< B> ==> D>>. +' // ''await: IN < B> ==> D>>. + B>. +' // ''await: IN B>. + D>? +5 + +' 使用睡眠延时,给足输出呈现时间 +''sleep: 1s + +' 检验输出 +''expect-contains: ANSWER D>. + +' 用户无法输入时退出(正常退出) +''terminate(if-no-user) diff --git a/src/tests/nal/test_i_var_elimination.nal b/src/tests/nal/test_i_var_elimination.nal new file mode 100644 index 0000000..9552984 --- /dev/null +++ b/src/tests/nal/test_i_var_elimination.nal @@ -0,0 +1,28 @@ +' 用于测试「自变量消除」 +' * 📍所涉及NAL层级:NAL-5、NAL-6 +' +' 输出预期 +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` +' * 🚩【2024-04-03 02:10:19】有时对操作需要等待足够的时长,才能捕获到输出 + +' 🚩降低音量,减少无关输出 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + + B>. +< $1> ==> <$1 --> C>>. + C>? +100 + +' 使用睡眠延时,给足输出呈现时间 +''sleep: 1s + +' 检验输出 +''expect-contains: ANSWER C>. + +' 用户无法输入时退出(正常退出) +''terminate(if-no-user) diff --git a/src/tests/nal/test_operation.nal b/src/tests/nal/test_operation.nal new file mode 100644 index 0000000..dfb2cd5 --- /dev/null +++ b/src/tests/nal/test_operation.nal @@ -0,0 +1,67 @@ +' 用于测试CIN对「操作」的支持 +' * 📍所涉及NAL层级:NAL-7、NAL-8 +' ! ⚠️【2024-03-29 16:52:57】ONA不支持「有两个以上组分的乘积词项」,故使用(*, P1, P2)替代 +' * ONA特有「操作注册」语法:`*setopname 11 ^left` +' 输出预期 +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` +' * 🚩【2024-04-03 02:10:19】有时对操作需要等待足够的时长,才能捕获到输出 +' 日志存储 +' * ✨存储所有「NAVM输出」到指定文件(JSON格式):`''save-outputs: 【文件相对路径】` +' * 📌以`.nal`文件自身所在目录为根目录 + +' 降低音量,减少无关输出 +' * 📄【2024-04-03 11:51:12】目前输出过多会造成CLI轻微卡顿 +' * 📝【2024-04-07 14:18:43】观察到输出过多会导致「未能截取操作」的事情发生 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + +' ⚠️【2024-04-07 14:58:12】对ONA使用会造成失败 +' '/REG left + +A. :|: +<(*, {SELF}) --> ^left>. :|: +G. :|: +' ? 📝【2024-04-07 14:10:03】OpenNARS相比于ONA,需要进行「提问」以「提示」需要操作 +' ! ⚠️不加下边这条问句,OpenNARS将测试失败 +<(&/, A, <(*, {SELF}) --> ^left>) ==> G>? +A. :|: +G! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}) + +A2. :|: +<(*, {SELF}, P) --> ^left>. :|: +G2. :|: +' ? 📝【2024-04-07 14:10:03】OpenNARS相比于ONA,需要进行「提问」以「提示」需要操作 +' ! ⚠️不加下边这条问句,OpenNARS将测试失败 +<(&/, A2, <(*, {SELF}, P) --> ^left>) ==> G2>? +A2. :|: +G2! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}, P) + +A3. :|: +<(*, {SELF}, (*, P1, P2)) --> ^left>. :|: +G3. :|: +' ? 📝【2024-04-07 14:10:03】OpenNARS相比于ONA,需要进行「提问」以「提示」需要操作 +' ! ⚠️不加下边这条问句,OpenNARS将测试失败 +<(&/, A3, <(*, {SELF}, (*, P1, P2)) --> ^left>) ==> G3>? +A3. :|: +G3! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}, (*, P1, P2)) + +''sleep: 500ms + +' * 保存输出 +''save-outputs: nal_operation_outputs.log.json + +' * 终止CIN +''terminate(if-no-user) diff --git a/src/tests/nal/test_simple_deduction.nal b/src/tests/nal/test_simple_deduction.nal new file mode 100644 index 0000000..878ae4a --- /dev/null +++ b/src/tests/nal/test_simple_deduction.nal @@ -0,0 +1,35 @@ +' 用于测试CIN的「简单演绎推理」 +' * 📍所涉及NAL层级:NAL-1 +' * 📝在「文件表示」上利用现有`Narsese`语法 +' +' 输出等待 `expect-contains` +' * 📝统一的NAL等待语法:`''await: 【输入类别】 【其它内容】` +' * ⚠️可能会阻塞测试,慎用 +' * 🚩以下await已被注释失效,仅作语法演示 +' +' 输出预期 `expect-contains` +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` + +' 🚩降低音量,减少无关输出 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + + B>. +' // ''await: IN B>. + C>. +' // ''await: IN C>. + C>? +5 + +' 使用睡眠延时,给足输出呈现时间 +''sleep: 1s + +' 检验输出 +''expect-contains: ANSWER C>. + +' 用户无法输入时退出(正常退出) +''terminate(if-no-user) diff --git a/src/tests/nal/test_simple_operation.nal b/src/tests/nal/test_simple_operation.nal new file mode 100644 index 0000000..2999a2b --- /dev/null +++ b/src/tests/nal/test_simple_operation.nal @@ -0,0 +1,37 @@ +' 用于测试CIN对「简单操作」的支持 +' * 📍所涉及NAL层级:NAL-8 +' * 📝原理:可直接执行的操作 as 目标 ⇒ 直接执行 +' 输出预期 +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` +' * 🚩【2024-04-03 02:10:19】有时对操作需要等待足够的时长,才能捕获到输出 + +' 降低音量,减少无关输出 +' * 📄【2024-04-03 11:51:12】目前输出过多会造成CLI轻微卡顿 +' * 📝【2024-04-07 14:18:43】观察到输出过多会导致「未能截取操作」的事情发生 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + +' ⚠️【2024-04-07 14:58:12】对ONA使用会造成失败 +' '/REG left + +<(*, {SELF}) --> ^left>! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}) + +<(*, {SELF}, P) --> ^left>! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}, P) + +<(*, {SELF}, (*, P1, P2)) --> ^left>! :|: +10 +''sleep: 1s +''expect-contains: EXE (^left, {SELF}, (*, P1, P2)) + +''sleep: 500ms +''terminate(if-no-user) diff --git a/src/tests/nal/test_temporal_induction.nal b/src/tests/nal/test_temporal_induction.nal new file mode 100644 index 0000000..cdec7ee --- /dev/null +++ b/src/tests/nal/test_temporal_induction.nal @@ -0,0 +1,40 @@ +' 用于测试CIN的「时间归纳推理」 +' * 📍所涉及NAL层级:NAL-7 +' * 📝在「文件表示」上利用现有`Narsese`语法 +' +' 输出等待 `expect-contains` +' * 📝统一的NAL等待语法:`''await: 【输入类别】 【其它内容】` +' * ⚠️可能会阻塞测试,慎用 +' * 🚩以下await已被注释失效,仅作语法演示 +' +' 输出预期 `expect-contains` +' * 📝统一的NAL测试语法:`''expect-contains: 【输出类别】 【其它内容】` +' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】` +' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)` + +' 🚩降低音量,减少无关输出 +'/VOL 0 + +' 🚩【2024-04-07 14:22:28】兼容PyNARS:给启动留足时间 +''sleep: 0.5s + + B>. :|: +' // ''await: IN B>. + +5 + + D>. :|: +' // ''await: IN C>. + +< B> =/> D>>? + +5 + +' 使用睡眠延时,给足输出呈现时间 +''sleep: 1s + +' 检验输出 +''expect-contains: ANSWER < B> =/> D>>. + +' 用户无法输入时退出(正常退出) +''terminate(if-no-user)