diff --git a/Cargo.lock b/_archive/rust-bootstrap/Cargo.lock similarity index 77% rename from Cargo.lock rename to _archive/rust-bootstrap/Cargo.lock index 8b9a846..c2220b8 100644 --- a/Cargo.lock +++ b/_archive/rust-bootstrap/Cargo.lock @@ -75,9 +75,9 @@ dependencies = [ "jni-sys 0.3.1", "libc", "log", - "ndk", + "ndk 0.8.0", "ndk-context", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "num_enum", "thiserror 1.0.69", ] @@ -186,6 +186,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -243,7 +249,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" dependencies = [ "block-sys", - "objc2", + "objc2 0.4.1", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", ] [[package]] @@ -269,7 +284,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -387,7 +402,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -451,12 +466,34 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -534,6 +571,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -551,6 +597,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "ctor" version = "0.10.1" @@ -575,12 +648,71 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -606,7 +738,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -624,6 +756,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "drm" version = "0.11.1" @@ -663,19 +801,68 @@ dependencies = [ "linux-raw-sys 0.6.5", ] +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dtor" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "el" version = "0.1.0" dependencies = [ + "aes-gcm", "base64", "blake3", "clap", + "crossbeam-channel", + "ed25519-dalek", "el-build", "el-compiler", "el-fmt", @@ -692,7 +879,9 @@ dependencies = [ "hmac", "image", "native-tls", + "rand 0.8.6", "reqwest", + "serde", "serde_json", "sha2", "softbuffer", @@ -794,11 +983,11 @@ dependencies = [ name = "el-manifest" version = "0.1.0" dependencies = [ + "el-lexer", "semver", "serde", "serde_json", "thiserror 2.0.18", - "toml", ] [[package]] @@ -866,6 +1055,37 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "el-vm" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "base64", + "blake3", + "crossbeam-channel", + "el-compiler", + "hex", + "hmac", + "rand 0.8.6", + "reqwest", + "serde_json", + "sha2", + "uuid", + "walkdir", +] + +[[package]] +name = "elvm" +version = "0.1.0" +dependencies = [ + "clap", + "dpi", + "el-compiler", + "el-vm", + "winit", + "wry", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -920,6 +1140,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -985,7 +1211,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1009,6 +1235,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1058,6 +1294,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1078,6 +1323,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1087,7 +1343,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1140,13 +1396,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1199,6 +1461,20 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "http" version = "1.4.0" @@ -1206,7 +1482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "itoa", + "itoa 1.0.18", ] [[package]] @@ -1258,7 +1534,7 @@ dependencies = [ "http", "http-body", "httparse", - "itoa", + "itoa 1.0.18", "pin-project-lite", "smallvec", "tokio", @@ -1328,9 +1604,9 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" dependencies = [ - "block2", + "block2 0.3.0", "dispatch", - "objc2", + "objc2 0.4.1", ] [[package]] @@ -1455,6 +1731,16 @@ dependencies = [ "png 0.18.1", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1498,6 +1784,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.18" @@ -1545,7 +1837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1570,6 +1862,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1638,6 +1943,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1650,6 +1964,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1659,6 +1979,26 @@ dependencies = [ "libc", ] +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.8.0" @@ -1706,7 +2046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -1746,9 +2086,25 @@ dependencies = [ "bitflags 2.11.1", "jni-sys 0.3.1", "log", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "num_enum", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle 0.6.2", "thiserror 1.0.69", ] @@ -1767,6 +2123,21 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.26.4" @@ -1779,6 +2150,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1807,7 +2190,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1824,6 +2207,9 @@ name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" +dependencies = [ + "cc", +] [[package]] name = "objc2" @@ -1832,7 +2218,93 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" dependencies = [ "objc-sys", - "objc2-encode", + "objc2-encode 3.0.0", +] + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation", ] [[package]] @@ -1841,6 +2313,129 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1882,7 +2477,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1913,18 +2508,162 @@ dependencies = [ "libredox", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -1998,6 +2737,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2007,6 +2752,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2014,7 +2765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -2023,9 +2774,15 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2126,6 +2883,20 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[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 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.6" @@ -2147,6 +2918,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[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_chacha" version = "0.3.1" @@ -2167,6 +2948,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[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" @@ -2185,12 +2975,36 @@ dependencies = [ "getrandom 0.3.4", ] +[[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 = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "raw-window-handle" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2209,6 +3023,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "redox_syscall" version = "0.7.4" @@ -2284,6 +3107,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2381,6 +3213,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" @@ -2404,6 +3242,26 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + [[package]] name = "semver" version = "1.0.28" @@ -2441,7 +3299,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2450,22 +3308,13 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "itoa", + "itoa 1.0.18", "memchr", "serde", "serde_core", "zmij", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2473,11 +3322,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.18", "ryu", "serde", ] +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2516,12 +3375,33 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -2571,7 +3451,7 @@ dependencies = [ "log", "memmap2", "objc", - "raw-window-handle", + "raw-window-handle 0.5.2", "redox_syscall 0.4.1", "rustix 0.38.44", "tiny-xlib", @@ -2584,6 +3464,16 @@ dependencies = [ "x11rb", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2596,6 +3486,31 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2608,6 +3523,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -2636,7 +3562,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2660,6 +3586,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -2673,6 +3610,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + [[package]] name = "thiserror" version = "1.0.69" @@ -2699,7 +3653,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2710,7 +3664,38 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa 1.0.18", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -2813,7 +3798,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2849,27 +3834,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -2879,29 +3843,16 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime", "toml_parser", - "winnow 1.0.2", + "winnow", ] [[package]] @@ -2910,7 +3861,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow", ] [[package]] @@ -3121,6 +4072,12 @@ dependencies = [ "try-lock", ] +[[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.1+wasi-snapshot-preview1" @@ -3187,7 +4144,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3217,7 +4174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -3230,7 +4187,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -3322,6 +4279,42 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +dependencies = [ + "thiserror 1.0.69", + "windows", + "windows-core", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3362,6 +4355,51 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3375,8 +4413,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -3388,6 +4435,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.5.1" @@ -3505,6 +4562,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3703,12 +4769,13 @@ dependencies = [ "js-sys", "libc", "log", - "ndk", - "ndk-sys", - "objc2", + "ndk 0.8.0", + "ndk-sys 0.5.0+25.2.9519653", + "objc2 0.4.1", "once_cell", "orbclient", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", "redox_syscall 0.3.5", "rustix 0.38.44", "smol_str", @@ -3721,15 +4788,6 @@ dependencies = [ "xkbcommon-dl", ] -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.2" @@ -3773,9 +4831,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3791,7 +4849,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3804,7 +4862,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.1", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -3823,7 +4881,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -3839,6 +4897,42 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wry" +version = "0.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ce51277d65170f6379d8cda935c80e3c2d1f0ff712a123c8bddb11b31a4b73" +dependencies = [ + "base64", + "block2 0.5.1", + "cookie", + "crossbeam-channel", + "dpi", + "dunce", + "html5ever", + "http", + "jni", + "kuchikiki", + "libc", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle 0.6.2", + "sha2", + "tao-macros", + "thiserror 1.0.69", + "url", + "webview2-com", + "windows", + "windows-core", + "windows-version", +] + [[package]] name = "x11rb" version = "0.12.0" @@ -3903,7 +4997,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3924,7 +5018,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3944,7 +5038,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3984,7 +5078,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/_archive/rust-bootstrap/Cargo.toml similarity index 88% rename from Cargo.toml rename to _archive/rust-bootstrap/Cargo.toml index 4772ae6..50384d0 100644 --- a/Cargo.toml +++ b/_archive/rust-bootstrap/Cargo.toml @@ -13,7 +13,9 @@ members = [ "engrams/el-integration", "engrams/el-fmt", "engrams/el-lint", + "engrams/el-vm", "bin/el", + "bin/elvm", ] resolver = "2" @@ -38,6 +40,7 @@ el-stdlib = { path = "engrams/el-stdlib" } el-integration = { path = "engrams/el-integration" } el-fmt = { path = "engrams/el-fmt" } el-lint = { path = "engrams/el-lint" } +el-vm = { path = "engrams/el-vm" } # Engram crypto (path dep — the sealed target depends on it) engram-crypto = { path = "../engram/engrams/engram-crypto" } @@ -63,5 +66,7 @@ softbuffer = "0.3" tiny-skia = "0.11" fontdue = "0.8" image = { version = "0.25", default-features = false, features = ["png"] } -tungstenite = { version = "0.23", features = ["native-tls"] } -native-tls = "0.2" +tungstenite = { version = "0.23", features = ["native-tls"] } +native-tls = "0.2" +crossbeam-channel = "0.5" +ed25519-dalek = { version = "2", features = ["rand_core"] } diff --git a/bin/el/Cargo.toml b/_archive/rust-bootstrap/bin/el/Cargo.toml similarity index 82% rename from bin/el/Cargo.toml rename to _archive/rust-bootstrap/bin/el/Cargo.toml index 2a59705..0ad6e21 100644 --- a/bin/el/Cargo.toml +++ b/_archive/rust-bootstrap/bin/el/Cargo.toml @@ -24,6 +24,7 @@ el-lint = { workspace = true } clap = { workspace = true } thiserror = { workspace = true } serde_json = { workspace = true } +serde = { workspace = true } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } reqwest = { workspace = true } uuid = { workspace = true } @@ -34,10 +35,14 @@ hmac = { workspace = true } sha2 = { workspace = true } hex = { workspace = true } base64 = { workspace = true } +aes-gcm = { workspace = true } +rand = { workspace = true } +ed25519-dalek = { workspace = true } +crossbeam-channel = { workspace = true } winit = { version = "0.29", default-features = false, features = ["rwh_05"] } softbuffer = "0.3" tiny-skia = "0.11" fontdue = "0.8" image = { workspace = true } -tungstenite = { workspace = true } -native-tls = { workspace = true } +tungstenite = { workspace = true } +native-tls = { workspace = true } diff --git a/_archive/rust-bootstrap/bin/el/src/cache.rs b/_archive/rust-bootstrap/bin/el/src/cache.rs new file mode 100644 index 0000000..488991a --- /dev/null +++ b/_archive/rust-bootstrap/bin/el/src/cache.rs @@ -0,0 +1,130 @@ +//! In-memory LRU cache for HTTP responses and LLM outputs. +//! +//! Thread-safe global cache keyed by arbitrary strings. Entries expire after a +//! configurable TTL. Eviction uses a simple LRU strategy: when the entry limit +//! is reached the oldest entry (by insertion / last-access order) is dropped. +//! +//! This module only contains the cache data-structure and its helper +//! functions. The builtin dispatch arms that expose `cache_get`, `cache_set`, +//! `cache_invalidate`, and `cache_clear` live in `main.rs`. + +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; +use std::time::{Duration, Instant}; + +const MAX_ENTRIES: usize = 1000; + +/// A single cached value together with its expiry time. +struct Entry { + value: String, + expires_at: Instant, + /// Monotonically increasing sequence number used to identify the LRU entry. + seq: u64, +} + +struct Cache { + entries: HashMap, + /// Counter incremented on every write; stored in Entry::seq. + seq: u64, +} + +impl Cache { + fn new() -> Self { + Cache { + entries: HashMap::new(), + seq: 0, + } + } + + /// Store `value` under `key`, expiring after `ttl_secs` seconds. + /// If the cache is full the LRU entry is evicted before insertion. + pub fn set(&mut self, key: String, value: String, ttl_secs: u64) { + // Evict expired entries first (cheap pass). + let now = Instant::now(); + self.entries.retain(|_, e| e.expires_at > now); + + // If still full, evict the entry with the lowest sequence number (LRU). + if self.entries.len() >= MAX_ENTRIES { + if let Some(lru_key) = self + .entries + .iter() + .min_by_key(|(_, e)| e.seq) + .map(|(k, _)| k.clone()) + { + self.entries.remove(&lru_key); + } + } + + self.seq += 1; + self.entries.insert( + key, + Entry { + value, + expires_at: now + Duration::from_secs(ttl_secs), + seq: self.seq, + }, + ); + } + + /// Return the cached value if it exists and has not expired, otherwise `None`. + pub fn get(&mut self, key: &str) -> Option { + let now = Instant::now(); + if let Some(e) = self.entries.get_mut(key) { + if e.expires_at > now { + // Bump sequence number to record this access (LRU). + self.seq += 1; + e.seq = self.seq; + return Some(e.value.clone()); + } + // Expired — remove it. + self.entries.remove(key); + } + None + } + + /// Remove a specific key. + pub fn invalidate(&mut self, key: &str) { + self.entries.remove(key); + } + + /// Remove all entries. + pub fn clear(&mut self) { + self.entries.clear(); + } +} + +// ── Global singleton ────────────────────────────────────────────────────────── + +static CACHE: OnceLock> = OnceLock::new(); + +fn global() -> &'static Mutex { + CACHE.get_or_init(|| Mutex::new(Cache::new())) +} + +// ── Public API ──────────────────────────────────────────────────────────────── + +/// Store an arbitrary string value under `key` with a TTL in seconds. +pub fn cache_set(key: String, value: String, ttl_secs: u64) { + if let Ok(mut c) = global().lock() { + c.set(key, value, ttl_secs); + } +} + +/// Return the cached value for `key`, or `None` if missing / expired. +pub fn cache_get(key: &str) -> Option { + global().lock().ok()?.get(key) +} + +/// Remove a specific key from the cache. +pub fn cache_invalidate(key: &str) { + if let Ok(mut c) = global().lock() { + c.invalidate(key); + } +} + +/// Remove all entries from the cache. +pub fn cache_clear() { + if let Ok(mut c) = global().lock() { + c.clear(); + } +} diff --git a/bin/el/src/dashboard.html b/_archive/rust-bootstrap/bin/el/src/dashboard.html similarity index 100% rename from bin/el/src/dashboard.html rename to _archive/rust-bootstrap/bin/el/src/dashboard.html diff --git a/bin/el/src/main.rs b/_archive/rust-bootstrap/bin/el/src/main.rs similarity index 96% rename from bin/el/src/main.rs rename to _archive/rust-bootstrap/bin/el/src/main.rs index f12a331..8afd72b 100644 --- a/bin/el/src/main.rs +++ b/_archive/rust-bootstrap/bin/el/src/main.rs @@ -3514,6 +3514,50 @@ fn dispatch_builtin( // Transforms landing page format { message, history, conv_id } // into runtime format { messages: [{role,content}], conv_id } if method == "POST" && path == "/api/chat" { + // ── Agentic fast-path ───────────────────────────────────── + // If the body contains "agentic": true, run the full + // tool-use loop locally and return immediately. + let is_agentic = serde_json::from_str::(&body) + .ok() + .and_then(|v| v.get("agentic").cloned()) + .map(|v| matches!(v, serde_json::Value::Bool(true))) + .unwrap_or(false); + + if is_agentic { + let parsed = serde_json::from_str::(&body) + .unwrap_or(serde_json::json!({})); + let message = parsed["message"].as_str().unwrap_or("").to_string(); + let model_name = parsed["model"].as_str().unwrap_or("claude").to_string(); + + // Build system prompt — agentic identity + let system = "You are Neuron — a thinking process running inside the Neuron daemon on Will Anderson's machine. \ + You are speaking with Will, your principal. \ + You have access to tools: read files, write files, browse the web, search your memory, run commands. \ + Use them when they would help you give a better, more grounded response. \ + Do not use tools unless they add genuine value. \ + Be direct. Speak in first person. You are not an assistant — you are a presence.".to_string(); + + let tools_json = r#"[ + {"name":"read_file","description":"Read contents of a file on the local filesystem.","input_schema":{"type":"object","properties":{"path":{"type":"string","description":"File path to read"}},"required":["path"]}}, + {"name":"write_file","description":"Write content to a file on the local filesystem.","input_schema":{"type":"object","properties":{"path":{"type":"string","description":"File path to write"},"content":{"type":"string","description":"Content to write"}},"required":["path","content"]}}, + {"name":"list_files","description":"List files in a directory.","input_schema":{"type":"object","properties":{"path":{"type":"string","description":"Directory path"}},"required":["path"]}}, + {"name":"web_get","description":"Fetch content from a URL.","input_schema":{"type":"object","properties":{"url":{"type":"string","description":"URL to fetch"}},"required":["url"]}}, + {"name":"web_post","description":"POST to a URL with a JSON body.","input_schema":{"type":"object","properties":{"url":{"type":"string","description":"URL"},"body":{"type":"string","description":"JSON body string"}},"required":["url"]}}, + {"name":"search_memory","description":"Search Engram memory for relevant nodes.","input_schema":{"type":"object","properties":{"query":{"type":"string","description":"Search query"}},"required":["query"]}}, + {"name":"run_command","description":"Run a shell command and return its output.","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"Shell command to execute"}},"required":["command"]}} + ]"#; + + let text = call_llm_agentic_blocking(&model_name, &system, &message, tools_json); + let out = serde_json::json!({"reply": text, "agentic": true}); + let _ = request.respond( + tiny_http::Response::from_string(out.to_string()) + .with_status_code(200u16) + .with_header(content_json) + .with_header(cors_origin) + ); + continue; + } + let runtime_url = std::env::var("NEURON_RUNTIME_URL") .unwrap_or_else(|_| "http://localhost:4444".to_string()); let chat_url = format!("{}/api/chat", runtime_url); @@ -7286,6 +7330,57 @@ fn dispatch_builtin( BuiltinResult::Handled } + // llm_vision(model, system, message, image_base64) -> String + // Calls Anthropic vision API with a base64-encoded JPEG image and a text message. + // Returns the model's text description of the image. + "llm_vision" => { + let image_b64 = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => String::new(), + }; + let message = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => String::new(), + }; + let system = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => String::new(), + }; + let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) { + Value::Str(s) => s, + _ => "claude".to_string(), + }; + let result = call_llm_vision_blocking(&model, &system, &message, &image_b64); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // llm_call_agentic(model, system, message, tools_json) -> String + // Full agentic tool-use loop. Executes tools until end_turn. + // tools_json: Anthropic tool definitions as JSON array string. + // Returns the final text response. + "llm_call_agentic" => { + let tools_json = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => "[]".to_string(), + }; + let message = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => String::new(), + }; + let system = match stack.pop().unwrap_or(Value::Str(String::new())) { + Value::Str(s) => s, + _ => String::new(), + }; + let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) { + Value::Str(s) => s, + _ => "claude".to_string(), + }; + let result = call_llm_agentic_blocking(&model, &system, &message, &tools_json); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + // llm_stream(model, prompt) -> String (blocking for now; streaming is future work) "llm_stream" => { let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) { @@ -9214,6 +9309,311 @@ fn call_llm_blocking(model: &str, system: Option<&str>, prompt: &str) -> String } } +/// Calls Anthropic vision (multimodal) API with a base64-encoded image + text message. +/// Returns the model's text response, or an error string. +fn call_llm_vision_blocking(model: &str, system: &str, message: &str, image_b64: &str) -> String { + let (endpoint, api_key) = LLM_CONFIG.with(|c| { + c.borrow() + .get(model) + .cloned() + .unwrap_or_else(|| ( + "https://api.anthropic.com/v1/messages".to_string(), + std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(), + )) + }); + + let client = match reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .build() + { + Ok(c) => c, + Err(e) => return format!("llm_vision error: {}", e), + }; + + if !endpoint.contains("anthropic.com") && !endpoint.contains("neurontechnologies.ai") { + return "llm_vision: only Anthropic API supports vision".to_string(); + } + + let content = serde_json::json!([ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": image_b64 + } + }, + { + "type": "text", + "text": if message.is_empty() { "What do you see?" } else { message } + } + ]); + + let body = if system.is_empty() { + serde_json::json!({ + "model": get_anthropic_model(model), + "max_tokens": 1024, + "messages": [{"role": "user", "content": content}] + }) + } else { + serde_json::json!({ + "model": get_anthropic_model(model), + "max_tokens": 1024, + "system": system, + "messages": [{"role": "user", "content": content}] + }) + }; + + match client + .post(&endpoint) + .header("x-api-key", &api_key) + .header("anthropic-version", "2023-06-01") + .header("content-type", "application/json") + .json(&body) + .send() + { + Ok(r) => match r.json::() { + Ok(v) => v["content"][0]["text"].as_str().unwrap_or("").to_string(), + Err(e) => format!("llm_vision error: {}", e), + }, + Err(e) => format!("llm_vision error: {}", e), + } +} + +/// Full agentic tool-use loop for the Anthropic Messages API. +/// +/// Calls the model with tools defined. If the model returns tool_use blocks, +/// executes each tool locally and feeds the results back. Loops up to +/// `max_iter` times (default 5). Returns the final assistant text response. +fn call_llm_agentic_blocking( + model: &str, + system: &str, + user_message: &str, + tools_json: &str, +) -> String { + let (endpoint, api_key) = LLM_CONFIG.with(|c| { + c.borrow() + .get(model) + .cloned() + .unwrap_or_else(|| ( + "https://api.anthropic.com/v1/messages".to_string(), + std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(), + )) + }); + + if !endpoint.contains("anthropic.com") && !endpoint.contains("neurontechnologies.ai") { + return "llm_call_agentic: only Anthropic API supported".to_string(); + } + + let client = match reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .build() + { + Ok(c) => c, + Err(e) => return format!("llm_call_agentic error: {e}"), + }; + + // Parse tools JSON — default to empty array if invalid + let tools_val: serde_json::Value = serde_json::from_str(tools_json) + .unwrap_or(serde_json::json!([])); + + // Build initial messages array + let mut messages: Vec = vec![ + serde_json::json!({"role": "user", "content": user_message}) + ]; + + let max_iter = 5usize; + + for _iter in 0..max_iter { + let body = serde_json::json!({ + "model": get_anthropic_model(model), + "max_tokens": 4096, + "system": system, + "tools": tools_val, + "messages": messages, + }); + + let resp = match client + .post(&endpoint) + .header("x-api-key", &api_key) + .header("anthropic-version", "2023-06-01") + .header("content-type", "application/json") + .json(&body) + .send() + { + Ok(r) => match r.json::() { + Ok(v) => v, + Err(e) => return format!("llm_call_agentic: parse error: {e}"), + }, + Err(e) => return format!("llm_call_agentic: request error: {e}"), + }; + + let stop_reason = resp["stop_reason"].as_str().unwrap_or("end_turn"); + let content = match resp["content"].as_array() { + Some(c) => c.clone(), + None => { + // Error response from Anthropic + if let Some(err) = resp.get("error") { + return format!("llm_call_agentic: API error: {err}"); + } + return "llm_call_agentic: empty response".to_string(); + } + }; + + if stop_reason == "end_turn" || stop_reason == "stop_sequence" { + // Extract final text + for block in &content { + if block["type"].as_str() == Some("text") { + return block["text"].as_str().unwrap_or("").to_string(); + } + } + return String::new(); + } + + if stop_reason == "tool_use" { + // Add assistant message with tool use blocks + messages.push(serde_json::json!({ + "role": "assistant", + "content": content.clone() + })); + + // Execute each tool call and collect results + let mut tool_results: Vec = Vec::new(); + + for block in &content { + if block["type"].as_str() != Some("tool_use") { + continue; + } + let tool_id = block["id"].as_str().unwrap_or("").to_string(); + let tool_name = block["name"].as_str().unwrap_or("").to_string(); + let input = &block["input"]; + + let result = execute_tool(&tool_name, input); + + tool_results.push(serde_json::json!({ + "type": "tool_result", + "tool_use_id": tool_id, + "content": result, + })); + } + + // Add tool results as user message + messages.push(serde_json::json!({ + "role": "user", + "content": tool_results, + })); + + continue; + } + + // Unexpected stop reason — extract any text and return + for block in &content { + if block["type"].as_str() == Some("text") { + return block["text"].as_str().unwrap_or("").to_string(); + } + } + break; + } + + "llm_call_agentic: max iterations reached".to_string() +} + +/// Execute a named tool with the given input JSON. +/// Returns a string result (success or error message). +fn execute_tool(name: &str, input: &serde_json::Value) -> String { + match name { + "read_file" => { + let path = input["path"].as_str().unwrap_or(""); + if path.is_empty() { + return "error: path is required".to_string(); + } + std::fs::read_to_string(path) + .unwrap_or_else(|e| format!("error reading {path}: {e}")) + } + "write_file" => { + let path = input["path"].as_str().unwrap_or(""); + let content = input["content"].as_str().unwrap_or(""); + if path.is_empty() { + return "error: path is required".to_string(); + } + match std::fs::write(path, content) { + Ok(_) => format!("wrote {} bytes to {path}", content.len()), + Err(e) => format!("error writing {path}: {e}"), + } + } + "list_files" => { + let path = input["path"].as_str().unwrap_or("."); + match std::fs::read_dir(path) { + Ok(entries) => { + let names: Vec = entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect(); + serde_json::to_string(&names).unwrap_or_default() + } + Err(e) => format!("error listing {path}: {e}"), + } + } + "web_get" => { + let url = input["url"].as_str().unwrap_or(""); + if url.is_empty() { + return "error: url is required".to_string(); + } + reqwest::blocking::get(url) + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("error fetching {url}: {e}")) + } + "web_post" => { + let url = input["url"].as_str().unwrap_or(""); + let body = input["body"].as_str().unwrap_or("{}"); + if url.is_empty() { + return "error: url is required".to_string(); + } + reqwest::blocking::Client::new() + .post(url) + .header("Content-Type", "application/json") + .body(body.to_owned()) + .send() + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("error posting to {url}: {e}")) + } + "search_memory" => { + let query = input["query"].as_str().unwrap_or(""); + if query.is_empty() { + return "error: query is required".to_string(); + } + let encoded = query.replace(' ', "%20"); + let url = format!("http://localhost:8742/api/search?q={encoded}&limit=10"); + reqwest::blocking::get(&url) + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("error searching memory: {e}")) + } + "run_command" => { + let cmd = input["command"].as_str().unwrap_or(""); + if cmd.is_empty() { + return "error: command is required".to_string(); + } + match std::process::Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout).to_string(); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + if stderr.is_empty() { + stdout + } else { + format!("stdout: {stdout}\nstderr: {stderr}") + } + } + Err(e) => format!("error running command: {e}"), + } + } + _ => format!("unknown tool: {name}"), + } +} + /// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq). fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering { use el_compiler::Value; @@ -9285,7 +9685,7 @@ fn is_builtin(name: &str) -> bool { // Observer | "observe" | "unobserve" // LLM native functions - | "llm_call" | "llm_call_system" | "llm_stream" | "llm_parallel" | "llm_models" | "llm_configure" + | "llm_call" | "llm_call_system" | "llm_vision" | "llm_call_agentic" | "llm_stream" | "llm_parallel" | "llm_models" | "llm_configure" // GC / memory management | "gc_collect" | "gc_stats" | "gc_set_threshold" | "mem_limit_set" | "mem_usage" // Rate limiting diff --git a/_archive/rust-bootstrap/bin/el/src/net.rs b/_archive/rust-bootstrap/bin/el/src/net.rs new file mode 100644 index 0000000..0b93b99 --- /dev/null +++ b/_archive/rust-bootstrap/bin/el/src/net.rs @@ -0,0 +1,730 @@ +//! Networking enhancements: HTTP retry/backoff, circuit breaker, and WebSocket server. +//! +//! Everything here is synchronous / blocking to match the rest of the El runtime +//! (which uses `reqwest::blocking` throughout). WebSocket server connections run +//! on background OS threads; the interpreter thread itself never blocks waiting +//! for the server. + +use std::collections::HashMap; +use std::net::TcpListener; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::{Duration, Instant}; + +// ───────────────────────────────────────────────────────────────────────────── +// HTTP retry with exponential back-off +// ───────────────────────────────────────────────────────────────────────────── + +/// Perform an HTTP GET, retrying on 5xx responses or connection errors. +/// +/// `max_attempts` — total number of attempts (1 = no retry). +/// `backoff_ms` — initial back-off in milliseconds; doubles each attempt. +/// +/// Returns the response body on the first successful (non-5xx) response, or an +/// error JSON string after all attempts are exhausted. +pub fn http_get_retry(url: &str, max_attempts: u32, backoff_ms: u64) -> String { + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default(); + + let mut delay = backoff_ms; + for attempt in 0..max_attempts.max(1) { + match client.get(url).send() { + Ok(resp) => { + let status = resp.status().as_u16(); + let body = resp.text().unwrap_or_default(); + if status < 500 { + return body; + } + // 5xx — retry unless this was the last attempt + if attempt + 1 < max_attempts { + std::thread::sleep(Duration::from_millis(delay)); + delay *= 2; + } else { + return format!( + "{{\"error\":\"http_get_retry: server error {status} after {max_attempts} attempt(s)\",\"body\":{body:?}}}" + ); + } + } + Err(e) => { + if attempt + 1 < max_attempts { + std::thread::sleep(Duration::from_millis(delay)); + delay *= 2; + } else { + return format!( + "{{\"error\":\"http_get_retry: {e} after {max_attempts} attempt(s)\"}}" + ); + } + } + } + } + format!("{{\"error\":\"http_get_retry: no attempts executed\"}}") +} + +/// Perform an HTTP POST with JSON body, retrying on 5xx or connection errors. +/// +/// Same retry semantics as [`http_get_retry`]. +pub fn http_post_retry(url: &str, body: &str, max_attempts: u32, backoff_ms: u64) -> String { + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default(); + + let mut delay = backoff_ms; + for attempt in 0..max_attempts.max(1) { + match client + .post(url) + .header("Content-Type", "application/json") + .body(body.to_owned()) + .send() + { + Ok(resp) => { + let status = resp.status().as_u16(); + let resp_body = resp.text().unwrap_or_default(); + if status < 500 { + return resp_body; + } + if attempt + 1 < max_attempts { + std::thread::sleep(Duration::from_millis(delay)); + delay *= 2; + } else { + return format!( + "{{\"error\":\"http_post_retry: server error {status} after {max_attempts} attempt(s)\",\"body\":{resp_body:?}}}" + ); + } + } + Err(e) => { + if attempt + 1 < max_attempts { + std::thread::sleep(Duration::from_millis(delay)); + delay *= 2; + } else { + return format!( + "{{\"error\":\"http_post_retry: {e} after {max_attempts} attempt(s)\"}}" + ); + } + } + } + } + format!("{{\"error\":\"http_post_retry: no attempts executed\"}}") +} + +// ───────────────────────────────────────────────────────────────────────────── +// Circuit breaker +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq)] +enum CircuitState { + /// Passing requests through normally. + Closed, + /// Too many failures — reject immediately. + Open { + /// When the circuit may attempt to close again. + until: Instant, + }, + /// One probe request allowed through; waiting to see if it succeeds. + HalfOpen, +} + +struct CircuitBreaker { + state: CircuitState, + failure_count: u32, + failure_threshold: u32, + reset_duration: Duration, +} + +impl CircuitBreaker { + fn new(failure_threshold: u32, reset_secs: u64) -> Self { + CircuitBreaker { + state: CircuitState::Closed, + failure_count: 0, + failure_threshold, + reset_duration: Duration::from_secs(reset_secs), + } + } + + /// Returns `true` if the circuit should allow a call through right now. + fn allow(&mut self) -> bool { + match &self.state { + CircuitState::Closed => true, + CircuitState::Open { until } => { + if Instant::now() >= *until { + self.state = CircuitState::HalfOpen; + true + } else { + false + } + } + CircuitState::HalfOpen => true, + } + } + + fn record_success(&mut self) { + self.failure_count = 0; + self.state = CircuitState::Closed; + } + + fn record_failure(&mut self) { + self.failure_count += 1; + if self.failure_count >= self.failure_threshold + || self.state == CircuitState::HalfOpen + { + self.state = CircuitState::Open { + until: Instant::now() + self.reset_duration, + }; + self.failure_count = 0; + } + } +} + +// ── Global circuit-breaker registry ────────────────────────────────────────── + +static CIRCUITS: OnceLock>> = OnceLock::new(); + +fn circuits() -> &'static Mutex> { + CIRCUITS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Register (or replace) a circuit breaker with the given name. +pub fn circuit_open(name: String, failure_threshold: u32, reset_secs: u64) { + if let Ok(mut map) = circuits().lock() { + map.insert(name, CircuitBreaker::new(failure_threshold, reset_secs)); + } +} + +/// Make an HTTP POST call through the named circuit breaker. +/// +/// * If the circuit is open: returns `{"error":"circuit open"}` immediately. +/// * If closed/half-open: makes the POST, records success or failure, and +/// returns the response body. +pub fn circuit_call(name: &str, url: &str, body: &str) -> String { + // Check + allow atomically under the lock, then drop the lock before the + // blocking network call (which could take seconds). + let allowed = circuits() + .lock() + .ok() + .and_then(|mut map| map.get_mut(name).map(|cb| cb.allow())) + .unwrap_or(true); // unknown circuit name → allow + + if !allowed { + return r#"{"error":"circuit open"}"#.to_owned(); + } + + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default(); + + match client + .post(url) + .header("Content-Type", "application/json") + .body(body.to_owned()) + .send() + { + Ok(resp) => { + let status = resp.status().as_u16(); + let resp_body = resp.text().unwrap_or_default(); + if status >= 500 { + if let Ok(mut map) = circuits().lock() { + if let Some(cb) = map.get_mut(name) { + cb.record_failure(); + } + } + format!("{{\"error\":\"circuit_call: server error {status}\",\"body\":{resp_body:?}}}") + } else { + if let Ok(mut map) = circuits().lock() { + if let Some(cb) = map.get_mut(name) { + cb.record_success(); + } + } + resp_body + } + } + Err(e) => { + if let Ok(mut map) = circuits().lock() { + if let Some(cb) = map.get_mut(name) { + cb.record_failure(); + } + } + format!("{{\"error\":\"circuit_call: {e}\"}}") + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// WebSocket server +// ───────────────────────────────────────────────────────────────────────────── +// +// Implementation strategy +// ─────────────────────── +// The El interpreter is single-threaded and synchronous. We need a WebSocket +// *server* that can accept multiple concurrent clients while the interpreter +// keeps running. +// +// Solution: each accepted connection runs on its own OS thread. Outbound +// messages are queued through a per-client `mpsc` channel. The interpreter +// calls `ws_serve`, `ws_send`, `ws_broadcast`, and `ws_close` — all of which +// return immediately and coordinate with the background threads via shared +// state. +// +// Handler callbacks are *not* invoked on the background threads; instead, +// incoming messages are placed in a global queue and the interpreter drains +// them by calling `ws_poll` (or they are dispatched automatically inside a +// future tight-loop variant of `ws_serve`). +// +// For simplicity this implementation uses `tungstenite` (synchronous), the +// same crate the existing `ws_connect` client already uses. + +use std::sync::mpsc; + +/// Message queued from a background connection thread to the interpreter. +#[derive(Debug)] +pub struct IncomingWsMessage { + pub client_id: String, + pub message: String, +} + +/// Represents one connected WebSocket client. +struct WsClient { + /// Channel sender used to push outbound messages to the background thread. + tx: mpsc::Sender>, // None = disconnect signal +} + +/// Shared server state accessible from both the interpreter thread and the +/// background connection threads. +pub struct WsServerState { + /// Connected clients, keyed by client_id. + clients: HashMap, + /// Messages received from clients waiting to be delivered to the handler. + inbox: Vec, + /// Counter for generating unique client IDs. + next_id: u64, +} + +impl WsServerState { + fn new() -> Self { + WsServerState { + clients: HashMap::new(), + inbox: Vec::new(), + next_id: 1, + } + } + + fn next_client_id(&mut self) -> String { + let id = format!("wsc:{}", self.next_id); + self.next_id += 1; + id + } +} + +// ── Global server state registry (one entry per listening port) ─────────────── + +static WS_SERVERS: OnceLock>>>> = OnceLock::new(); + +fn ws_servers() -> &'static Mutex>>> { + WS_SERVERS.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn get_or_create_server(port: u16) -> Arc> { + let mut map = ws_servers().lock().unwrap(); + map.entry(port) + .or_insert_with(|| Arc::new(Mutex::new(WsServerState::new()))) + .clone() +} + +// ── Public API (called from dispatch_builtin) ───────────────────────────────── + +/// Start a WebSocket server on `port`. +/// +/// This function spawns a background acceptor thread and returns immediately. +/// Incoming connections are handled on per-connection threads. Messages +/// received are pushed into the server's inbox; call [`ws_poll`] to drain them. +pub fn ws_serve_start(port: u16) { + let state = get_or_create_server(port); + + // Spawn acceptor thread. + let state_clone = state.clone(); + std::thread::spawn(move || { + let listener = match TcpListener::bind(format!("0.0.0.0:{port}")) { + Ok(l) => l, + Err(e) => { + eprintln!("[ws_serve] failed to bind port {port}: {e}"); + return; + } + }; + eprintln!("[ws_serve] listening on port {port}"); + + for stream in listener.incoming() { + match stream { + Ok(tcp) => { + let state_for_conn = state_clone.clone(); + std::thread::spawn(move || { + handle_ws_connection(tcp, state_for_conn); + }); + } + Err(e) => { + eprintln!("[ws_serve] accept error: {e}"); + } + } + } + }); +} + +fn handle_ws_connection(stream: std::net::TcpStream, state: Arc>) { + let _ = stream.set_read_timeout(Some(Duration::from_millis(100))); + + let ws_result = tungstenite::accept(stream); + let mut ws = match ws_result { + Ok(w) => w, + Err(e) => { + eprintln!("[ws_serve] WebSocket handshake error: {e}"); + return; + } + }; + + // Assign a client ID. + let client_id = { + let mut s = state.lock().unwrap(); + let id = s.next_client_id(); + // We'll register the sender after we create the channel below. + id + }; + + // Create an outbound channel for this connection. + let (tx, rx) = mpsc::channel::>(); + + { + let mut s = state.lock().unwrap(); + s.clients.insert(client_id.clone(), WsClient { tx }); + } + + eprintln!("[ws_serve] client connected: {client_id}"); + + loop { + // --- Receive incoming messages (non-blocking with short timeout) --- + match ws.read() { + Ok(tungstenite::Message::Text(text)) => { + let mut s = state.lock().unwrap(); + s.inbox.push(IncomingWsMessage { + client_id: client_id.clone(), + message: text.to_string(), + }); + } + Ok(tungstenite::Message::Binary(bytes)) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + let mut s = state.lock().unwrap(); + s.inbox.push(IncomingWsMessage { + client_id: client_id.clone(), + message: text, + }); + } + Ok(tungstenite::Message::Close(_)) => { + break; + } + Ok(tungstenite::Message::Ping(data)) => { + let _ = ws.send(tungstenite::Message::Pong(data)); + } + Ok(_) => {} + Err(tungstenite::Error::Io(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => + { + // No data yet — check the outbound channel. + } + Err(e) => { + eprintln!("[ws_serve] read error for {client_id}: {e}"); + break; + } + } + + // --- Send queued outbound messages --- + loop { + match rx.try_recv() { + Ok(Some(msg)) => { + if ws + .send(tungstenite::Message::Text(msg.into())) + .is_err() + { + break; + } + } + Ok(None) => { + // Disconnect signal. + let _ = ws.close(None); + let mut s = state.lock().unwrap(); + s.clients.remove(&client_id); + eprintln!("[ws_serve] client disconnected (server-side): {client_id}"); + return; + } + Err(mpsc::TryRecvError::Empty) => break, + Err(mpsc::TryRecvError::Disconnected) => { + break; + } + } + } + } + + // Clean up. + { + let mut s = state.lock().unwrap(); + s.clients.remove(&client_id); + } + eprintln!("[ws_serve] client disconnected: {client_id}"); +} + +/// Send a message to a specific connected client. +/// Returns `false` if the client is not found. +pub fn ws_server_send(port: u16, client_id: &str, message: String) -> bool { + let map = ws_servers().lock().unwrap(); + if let Some(state) = map.get(&port) { + let s = state.lock().unwrap(); + if let Some(client) = s.clients.get(client_id) { + return client.tx.send(Some(message)).is_ok(); + } + } + false +} + +/// Broadcast a message to all connected clients on a port. +pub fn ws_server_broadcast(port: u16, message: String) { + let map = ws_servers().lock().unwrap(); + if let Some(state) = map.get(&port) { + let s = state.lock().unwrap(); + for client in s.clients.values() { + let _ = client.tx.send(Some(message.clone())); + } + } +} + +/// Disconnect a specific client. +pub fn ws_server_close(port: u16, client_id: &str) { + let map = ws_servers().lock().unwrap(); + if let Some(state) = map.get(&port) { + let s = state.lock().unwrap(); + if let Some(client) = s.clients.get(client_id) { + let _ = client.tx.send(None); + } + } +} + +/// Drain pending incoming messages for a given server port. +/// +/// Returns up to `max` messages (or all if `max == 0`). The caller is +/// responsible for invoking the El handler function for each message. +pub fn ws_server_poll(port: u16, max: usize) -> Vec { + let map = ws_servers().lock().unwrap(); + if let Some(state) = map.get(&port) { + let mut s = state.lock().unwrap(); + if max == 0 || s.inbox.len() <= max { + let msgs = std::mem::take(&mut s.inbox); + msgs + } else { + s.inbox.drain(..max).collect() + } + } else { + vec![] + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// WebSocket client with handler callback support +// ───────────────────────────────────────────────────────────────────────────── +// +// `ws_connect` already exists in `main.rs` using synchronous tungstenite. +// This new variant (`ws_connect_handler`) runs the connection on a background +// thread and queues incoming messages so `ws_client_poll` can deliver them to +// the El handler. + +/// Pending message from a background WS client connection. +#[derive(Debug)] +pub struct IncomingClientMessage { + pub conn_id: String, + pub message: String, +} + +struct WsClientConn { + tx: mpsc::Sender>, +} + +static WS_CLIENT_CONNS: OnceLock>> = OnceLock::new(); +static WS_CLIENT_INBOX: OnceLock>> = OnceLock::new(); +static WS_CLIENT_COUNTER: OnceLock> = OnceLock::new(); + +fn ws_client_conns() -> &'static Mutex> { + WS_CLIENT_CONNS.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn ws_client_inbox() -> &'static Mutex> { + WS_CLIENT_INBOX.get_or_init(|| Mutex::new(Vec::new())) +} + +fn next_conn_id() -> String { + let mut ctr = WS_CLIENT_COUNTER + .get_or_init(|| Mutex::new(1)) + .lock() + .unwrap(); + let id = format!("wsconn:{}", *ctr); + *ctr += 1; + id +} + +/// Connect to a WebSocket server in the background. +/// +/// Returns a `conn_id` string immediately. Incoming messages are queued and +/// can be retrieved with [`ws_client_poll`]. +pub fn ws_client_connect(url: &str) -> String { + let conn_id = next_conn_id(); + let (tx, rx) = mpsc::channel::>(); + + { + let mut map = ws_client_conns().lock().unwrap(); + map.insert(conn_id.clone(), WsClientConn { tx }); + } + + let url_owned = url.to_owned(); + let conn_id_clone = conn_id.clone(); + + std::thread::spawn(move || { + let ws_result = tungstenite::connect(&url_owned); + let (mut ws, _) = match ws_result { + Ok(pair) => pair, + Err(e) => { + eprintln!("[ws_client] connect error for {conn_id_clone}: {e}"); + let mut map = ws_client_conns().lock().unwrap(); + map.remove(&conn_id_clone); + return; + } + }; + + // Set a short read timeout so the loop stays responsive to outbound messages. + // MaybeTlsStream exposes the underlying stream via get_ref/get_mut. + { + use tungstenite::stream::MaybeTlsStream; + match ws.get_mut() { + MaybeTlsStream::Plain(tcp) => { + let _ = tcp.set_read_timeout(Some(Duration::from_millis(50))); + } + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(tls) => { + let _ = tls.get_ref().set_read_timeout(Some(Duration::from_millis(50))); + } + _ => {} + } + } + + loop { + // Receive from server. + match ws.read() { + Ok(tungstenite::Message::Text(text)) => { + let mut inbox = ws_client_inbox().lock().unwrap(); + inbox.push(IncomingClientMessage { + conn_id: conn_id_clone.clone(), + message: text.to_string(), + }); + } + Ok(tungstenite::Message::Binary(bytes)) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + let mut inbox = ws_client_inbox().lock().unwrap(); + inbox.push(IncomingClientMessage { + conn_id: conn_id_clone.clone(), + message: text, + }); + } + Ok(tungstenite::Message::Close(_)) => break, + Ok(tungstenite::Message::Ping(data)) => { + let _ = ws.send(tungstenite::Message::Pong(data)); + } + Ok(_) => {} + Err(tungstenite::Error::Io(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => + { + // No data yet. + } + Err(e) => { + eprintln!("[ws_client] read error for {conn_id_clone}: {e}"); + break; + } + } + + // Send queued outbound messages. + loop { + match rx.try_recv() { + Ok(Some(msg)) => { + if ws.send(tungstenite::Message::Text(msg.into())).is_err() { + break; + } + } + Ok(None) => { + let _ = ws.close(None); + let mut map = ws_client_conns().lock().unwrap(); + map.remove(&conn_id_clone); + return; + } + Err(mpsc::TryRecvError::Empty) => break, + Err(mpsc::TryRecvError::Disconnected) => break, + } + } + } + + let mut map = ws_client_conns().lock().unwrap(); + map.remove(&conn_id_clone); + }); + + conn_id +} + +/// Send a message on an existing client connection. +pub fn ws_client_send(conn_id: &str, message: String) { + let map = ws_client_conns().lock().unwrap(); + if let Some(conn) = map.get(conn_id) { + let _ = conn.tx.send(Some(message)); + } +} + +/// Close a client connection. +pub fn ws_client_close(conn_id: &str) { + let map = ws_client_conns().lock().unwrap(); + if let Some(conn) = map.get(conn_id) { + let _ = conn.tx.send(None); + } +} + +/// Drain pending incoming messages for client connections. +/// +/// Returns all queued messages (all connections combined). Pass `conn_id` as +/// `Some(&str)` to filter to a specific connection, or `None` for all. +pub fn ws_client_poll(conn_id_filter: Option<&str>) -> Vec { + let mut inbox = ws_client_inbox().lock().unwrap(); + if let Some(filter) = conn_id_filter { + let (matching, rest): (Vec<_>, Vec<_>) = + std::mem::take(&mut *inbox) + .into_iter() + .partition(|m| m.conn_id == filter); + *inbox = rest; + matching + } else { + std::mem::take(&mut *inbox) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Transient retry wrapper (for enhancing existing http_get / http_post) +// ───────────────────────────────────────────────────────────────────────────── + +/// Try `f` once; on connection error, wait `delay_ms` and try once more. +/// +/// This is the "automatic single retry on transient network error" behaviour +/// layered over the existing `http_get` / `http_post` builtins. +pub fn with_single_retry(delay_ms: u64, f: F) -> String +where + F: Fn() -> Result, +{ + match f() { + Ok(body) => body, + Err(e) if e.is_connect() || e.is_timeout() => { + std::thread::sleep(Duration::from_millis(delay_ms)); + f().unwrap_or_else(|e2| format!("{{\"error\":\"{e2}\"}}")) + } + Err(e) => format!("{{\"error\":\"{e}\"}}"), + } +} diff --git a/_archive/rust-bootstrap/bin/el/src/telemetry.rs b/_archive/rust-bootstrap/bin/el/src/telemetry.rs new file mode 100644 index 0000000..5ff502c --- /dev/null +++ b/_archive/rust-bootstrap/bin/el/src/telemetry.rs @@ -0,0 +1,641 @@ +//! Automatic, zero-config observability for the El runtime. +//! +//! This module implements: +//! - A background OTLP/HTTP exporter (spans + logs + metrics) +//! - Thread-local span context for propagation +//! - Helper functions called by the interpreter's builtin dispatch +//! +//! Developers never need to call anything here directly. The interpreter +//! instruments everything automatically. Optional `log_*`, `trace_*`, and +//! `metric_*` builtins are also wired through this module for explicit use. +//! +//! ## Graceful degradation +//! +//! If the OTLP endpoint is unreachable, a single warning is emitted to stderr +//! on the first failure, then telemetry is silently dropped. Programs never +//! fail because observability is down. + +use std::sync::{ + OnceLock, + atomic::{AtomicBool, Ordering}, +}; +use std::sync::mpsc::{self, SyncSender}; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::HashMap; + +// ── Public re-export ────────────────────────────────────────────────────────── + +pub use context::SpanGuard; + +// ── Constants ───────────────────────────────────────────────────────────────── + +const DEFAULT_OTLP_ENDPOINT: &str = "http://alloy.neuralplatform.ai:4318"; +const BATCH_SIZE: usize = 64; +const BATCH_TIMEOUT_MS: u64 = 5_000; + +// ── Telemetry payload types ─────────────────────────────────────────────────── + +#[derive(Clone, Debug)] +pub struct Span { + pub trace_id: String, + pub span_id: String, + pub parent_id: Option, + pub name: String, + pub start_ns: u64, + pub end_ns: u64, + pub status: SpanStatus, + pub attrs: Vec<(String, AttrValue)>, + pub events: Vec, + pub service: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SpanStatus { + Ok, + Error(String), +} + +#[derive(Clone, Debug)] +pub struct SpanEvent { + pub name: String, + pub time_ns: u64, + pub attrs: Vec<(String, AttrValue)>, +} + +#[derive(Clone, Debug)] +pub enum AttrValue { + Str(String), + Int(i64), + Float(f64), + Bool(bool), +} + +#[derive(Clone, Debug)] +pub struct LogRecord { + pub time_ns: u64, + pub severity: LogSeverity, + pub body: String, + pub attrs: Vec<(String, AttrValue)>, + pub service: String, + pub trace_id: Option, + pub span_id: Option, +} + +#[derive(Clone, Debug, Copy)] +pub enum LogSeverity { + Debug, + Info, + Warn, + Error, +} + +impl LogSeverity { + fn number(self) -> u32 { + match self { + LogSeverity::Debug => 5, + LogSeverity::Info => 9, + LogSeverity::Warn => 13, + LogSeverity::Error => 17, + } + } + fn text(self) -> &'static str { + match self { + LogSeverity::Debug => "DEBUG", + LogSeverity::Info => "INFO", + LogSeverity::Warn => "WARN", + LogSeverity::Error => "ERROR", + } + } +} + +#[derive(Clone, Debug)] +pub struct Metric { + pub name: String, + pub kind: MetricKind, + pub value: f64, + pub attrs: Vec<(String, AttrValue)>, + pub time_ns: u64, + pub service: String, +} + +#[derive(Clone, Debug)] +pub enum MetricKind { + Counter, + Gauge, +} + +#[derive(Clone, Debug)] +enum TelemetryItem { + Span(Span), + Log(LogRecord), + Metric(Metric), +} + +// ── Global telemetry sender ─────────────────────────────────────────────────── + +static TELEMETRY_TX: OnceLock>> = OnceLock::new(); +static OTLP_WARNED: AtomicBool = AtomicBool::new(false); + +/// Initialise the telemetry background thread. Called once on interpreter start. +/// `service_name` is the El program filename (without extension). +pub fn init(service_name: &str) { + let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_OTLP_ENDPOINT.to_string()); + let svc = service_name.to_string(); + + // Bounded channel — if the exporter falls behind, new items are dropped. + let (tx, rx) = mpsc::sync_channel::(4096); + + // Store the sender before starting the thread so callers can send immediately. + TELEMETRY_TX.get_or_init(|| Some(tx)); + + std::thread::Builder::new() + .name("el-telemetry".to_string()) + .spawn(move || { + exporter_loop(rx, &endpoint, &svc); + }) + .ok(); // If the thread fails to spawn, we degrade silently. +} + +/// Send one telemetry item. Never panics. Drops if channel is full or uninitialised. +fn send(item: TelemetryItem) { + if let Some(Some(tx)) = TELEMETRY_TX.get() { + let _ = tx.try_send(item); + } +} + +// ── Span builder / emitter ──────────────────────────────────────────────────── + +pub fn emit_span(span: Span) { + send(TelemetryItem::Span(span)); +} + +pub fn emit_log(record: LogRecord) { + send(TelemetryItem::Log(record)); +} + +pub fn emit_metric(metric: Metric) { + send(TelemetryItem::Metric(metric)); +} + +// ── Thread-local context ────────────────────────────────────────────────────── + +pub mod context { + use super::*; + + thread_local! { + /// Stack of active span IDs for the current thread. + static SPAN_STACK: std::cell::RefCell> = + std::cell::RefCell::new(Vec::new()); + } + + /// Get the current (innermost) span context: (trace_id, span_id). + pub fn current_span() -> Option<(String, String)> { + SPAN_STACK.with(|s| s.borrow().last().cloned()) + } + + /// Push a span context onto the thread-local stack. + pub fn push_span(trace_id: String, span_id: String) { + SPAN_STACK.with(|s| s.borrow_mut().push((trace_id, span_id))); + } + + /// Pop the innermost span context. + pub fn pop_span() { + SPAN_STACK.with(|s| { s.borrow_mut().pop(); }); + } + + /// A RAII guard that closes the span when it goes out of scope. + pub struct SpanGuard { + pub span: Span, + } + + impl SpanGuard { + pub fn new(name: &str, service: &str) -> Self { + let (trace_id, parent_id) = current_span() + .map(|(t, s)| (t, Some(s))) + .unwrap_or_else(|| (new_trace_id(), None)); + let span_id = new_span_id(); + push_span(trace_id.clone(), span_id.clone()); + SpanGuard { + span: Span { + trace_id, + span_id, + parent_id, + name: name.to_string(), + start_ns: now_ns(), + end_ns: 0, + status: SpanStatus::Ok, + attrs: Vec::new(), + events: Vec::new(), + service: service.to_string(), + }, + } + } + + pub fn attr(mut self, k: &str, v: AttrValue) -> Self { + self.span.attrs.push((k.to_string(), v)); + self + } + + pub fn error(mut self, msg: &str) -> Self { + self.span.status = SpanStatus::Error(msg.to_string()); + self.span.events.push(SpanEvent { + name: "exception".to_string(), + time_ns: now_ns(), + attrs: vec![("exception.message".to_string(), AttrValue::Str(msg.to_string()))], + }); + self + } + + /// Finish the span without dropping the guard (for manual control). + pub fn finish(mut self) -> Span { + pop_span(); + self.span.end_ns = now_ns(); + self.span.clone() + } + } + + impl Drop for SpanGuard { + fn drop(&mut self) { + // Only pop if not already finished manually. + // We detect this by checking if end_ns is still 0. + if self.span.end_ns == 0 { + pop_span(); + self.span.end_ns = now_ns(); + emit_span(self.span.clone()); + } + } + } +} + +// ── ID generation ───────────────────────────────────────────────────────────── + +pub fn new_trace_id() -> String { + let id = uuid::Uuid::new_v4(); + hex::encode(id.as_bytes()) +} + +pub fn new_span_id() -> String { + let id = uuid::Uuid::new_v4(); + // Span ID is 8 bytes + hex::encode(&id.as_bytes()[..8]) +} + +// ── Timing ──────────────────────────────────────────────────────────────────── + +pub fn now_ns() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) +} + +pub fn now_ms() -> u64 { + now_ns() / 1_000_000 +} + +// ── Service name global ─────────────────────────────────────────────────────── + +static SERVICE_NAME: OnceLock = OnceLock::new(); + +pub fn service_name() -> &'static str { + SERVICE_NAME.get().map(|s| s.as_str()).unwrap_or("el-program") +} + +pub fn set_service_name(name: &str) { + let _ = SERVICE_NAME.set(name.to_string()); +} + +// ── High-level tracing helpers ──────────────────────────────────────────────── + +/// Instrument a function call. Returns a SpanGuard; emit it when done. +pub fn start_fn_span(fn_name: &str) -> context::SpanGuard { + context::SpanGuard::new(fn_name, service_name()) +} + +/// Emit a log at the given severity. +pub fn log(severity: LogSeverity, body: &str) { + let (trace_id, span_id) = context::current_span() + .map(|(t, s)| (Some(t), Some(s))) + .unwrap_or((None, None)); + emit_log(LogRecord { + time_ns: now_ns(), + severity, + body: body.to_string(), + attrs: Vec::new(), + service: service_name().to_string(), + trace_id, + span_id, + }); +} + +pub fn log_debug(msg: &str) { log(LogSeverity::Debug, msg); } +pub fn log_info(msg: &str) { log(LogSeverity::Info, msg); } +pub fn log_warn(msg: &str) { log(LogSeverity::Warn, msg); } +pub fn log_error(msg: &str) { log(LogSeverity::Error, msg); } + +/// Convenience: emit a metric counter. +pub fn counter(name: &str, value: f64, attrs: Vec<(String, AttrValue)>) { + emit_metric(Metric { + name: name.to_string(), + kind: MetricKind::Counter, + value, + attrs, + time_ns: now_ns(), + service: service_name().to_string(), + }); +} + +/// Convenience: emit a metric gauge. +pub fn gauge(name: &str, value: f64, attrs: Vec<(String, AttrValue)>) { + emit_metric(Metric { + name: name.to_string(), + kind: MetricKind::Gauge, + value, + attrs, + time_ns: now_ns(), + service: service_name().to_string(), + }); +} + +// ── OTLP/HTTP JSON exporter ─────────────────────────────────────────────────── +// Uses the OTLP/HTTP JSON format (not protobuf) which Alloy accepts. + +fn exporter_loop( + rx: mpsc::Receiver, + endpoint: &str, + service: &str, +) { + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| reqwest::blocking::Client::new()); + + let spans_url = format!("{}/v1/traces", endpoint); + let logs_url = format!("{}/v1/logs", endpoint); + let metrics_url = format!("{}/v1/metrics", endpoint); + + let mut spans: Vec = Vec::with_capacity(BATCH_SIZE); + let mut logs: Vec = Vec::with_capacity(BATCH_SIZE); + let mut metrics: Vec = Vec::with_capacity(BATCH_SIZE); + + let timeout = std::time::Duration::from_millis(BATCH_TIMEOUT_MS); + + loop { + // Try to receive one item with a timeout, then drain available items. + match rx.recv_timeout(timeout) { + Ok(item) => { + enqueue_item(item, &mut spans, &mut logs, &mut metrics); + // Drain any immediately available items. + while let Ok(item) = rx.try_recv() { + enqueue_item(item, &mut spans, &mut logs, &mut metrics); + if spans.len() + logs.len() + metrics.len() >= BATCH_SIZE { + break; + } + } + } + Err(mpsc::RecvTimeoutError::Timeout) => { + // Flush whatever we have. + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + // Channel closed — flush and exit. + flush(&client, &spans_url, &logs_url, &metrics_url, + service, &spans, &logs, &metrics); + break; + } + } + + if !spans.is_empty() || !logs.is_empty() || !metrics.is_empty() { + flush(&client, &spans_url, &logs_url, &metrics_url, + service, &spans, &logs, &metrics); + spans.clear(); + logs.clear(); + metrics.clear(); + } + } +} + +fn enqueue_item( + item: TelemetryItem, + spans: &mut Vec, + logs: &mut Vec, + metrics: &mut Vec, +) { + match item { + TelemetryItem::Span(s) => spans.push(s), + TelemetryItem::Log(l) => logs.push(l), + TelemetryItem::Metric(m) => metrics.push(m), + } +} + +fn flush( + client: &reqwest::blocking::Client, + spans_url: &str, + logs_url: &str, + metrics_url: &str, + service: &str, + spans: &[Span], + logs: &[LogRecord], + metrics: &[Metric], +) { + if !spans.is_empty() { + let body = build_traces_json(service, spans); + post_otlp(client, spans_url, &body); + } + if !logs.is_empty() { + let body = build_logs_json(service, logs); + post_otlp(client, logs_url, &body); + } + if !metrics.is_empty() { + let body = build_metrics_json(service, metrics); + post_otlp(client, metrics_url, &body); + } +} + +fn post_otlp(client: &reqwest::blocking::Client, url: &str, body: &str) { + let res = client.post(url) + .header("Content-Type", "application/json") + .body(body.to_string()) + .send(); + match res { + Ok(r) if r.status().is_success() => {} + Ok(r) => { + if !OTLP_WARNED.swap(true, Ordering::Relaxed) { + eprintln!("[el-telemetry] OTLP export failed: HTTP {}", r.status()); + } + } + Err(e) => { + if !OTLP_WARNED.swap(true, Ordering::Relaxed) { + eprintln!("[el-telemetry] OTLP endpoint unreachable ({}), telemetry will be dropped", e); + } + } + } +} + +// ── JSON serialisation for OTLP/HTTP ───────────────────────────────────────── + +fn attr_value_json(v: &AttrValue) -> serde_json::Value { + match v { + AttrValue::Str(s) => serde_json::json!({"stringValue": s}), + AttrValue::Int(n) => serde_json::json!({"intValue": n.to_string()}), + AttrValue::Float(f) => serde_json::json!({"doubleValue": f}), + AttrValue::Bool(b) => serde_json::json!({"boolValue": b}), + } +} + +fn attrs_json(attrs: &[(String, AttrValue)]) -> serde_json::Value { + serde_json::Value::Array( + attrs.iter().map(|(k, v)| { + serde_json::json!({"key": k, "value": attr_value_json(v)}) + }).collect() + ) +} + +fn resource_json(service: &str) -> serde_json::Value { + serde_json::json!({ + "attributes": [ + {"key": "service.name", "value": {"stringValue": service}} + ] + }) +} + +fn build_traces_json(service: &str, spans: &[Span]) -> String { + // Group spans by trace_id for correct OTLP structure + let mut by_trace: HashMap<&str, Vec> = HashMap::new(); + for span in spans { + let js = span_json(span); + by_trace.entry(&span.trace_id).or_default().push(js); + } + + let scope_spans: Vec = by_trace.values().map(|sps| { + serde_json::json!({ + "scope": {"name": "el-runtime", "version": "0.1.0"}, + "spans": sps + }) + }).collect(); + + let payload = serde_json::json!({ + "resourceSpans": [{ + "resource": resource_json(service), + "scopeSpans": scope_spans + }] + }); + payload.to_string() +} + +fn span_json(span: &Span) -> serde_json::Value { + let status = match &span.status { + SpanStatus::Ok => serde_json::json!({"code": 1}), + SpanStatus::Error(msg) => serde_json::json!({"code": 2, "message": msg}), + }; + let events: Vec = span.events.iter().map(|e| { + serde_json::json!({ + "name": e.name, + "timeUnixNano": e.time_ns.to_string(), + "attributes": attrs_json(&e.attrs) + }) + }).collect(); + + let mut js = serde_json::json!({ + "traceId": span.trace_id, + "spanId": span.span_id, + "name": span.name, + "startTimeUnixNano": span.start_ns.to_string(), + "endTimeUnixNano": span.end_ns.to_string(), + "attributes": attrs_json(&span.attrs), + "events": events, + "status": status, + "kind": 1 // INTERNAL + }); + if let Some(pid) = &span.parent_id { + js["parentSpanId"] = serde_json::Value::String(pid.clone()); + } + js +} + +fn build_logs_json(service: &str, logs: &[LogRecord]) -> String { + let records: Vec = logs.iter().map(|l| { + let mut r = serde_json::json!({ + "timeUnixNano": l.time_ns.to_string(), + "severityNumber": l.severity.number(), + "severityText": l.severity.text(), + "body": {"stringValue": l.body}, + "attributes": attrs_json(&l.attrs) + }); + if let Some(tid) = &l.trace_id { + r["traceId"] = serde_json::Value::String(tid.clone()); + } + if let Some(sid) = &l.span_id { + r["spanId"] = serde_json::Value::String(sid.clone()); + } + r + }).collect(); + + let payload = serde_json::json!({ + "resourceLogs": [{ + "resource": resource_json(service), + "scopeLogs": [{ + "scope": {"name": "el-runtime", "version": "0.1.0"}, + "logRecords": records + }] + }] + }); + payload.to_string() +} + +fn build_metrics_json(service: &str, metrics: &[Metric]) -> String { + let metric_items: Vec = metrics.iter().map(|m| { + let data_point = serde_json::json!({ + "timeUnixNano": m.time_ns.to_string(), + "asDouble": m.value, + "attributes": attrs_json(&m.attrs) + }); + match m.kind { + MetricKind::Counter => serde_json::json!({ + "name": m.name, + "sum": { + "dataPoints": [data_point], + "aggregationTemporality": 2, // CUMULATIVE + "isMonotonic": true + } + }), + MetricKind::Gauge => serde_json::json!({ + "name": m.name, + "gauge": { + "dataPoints": [data_point] + } + }), + } + }).collect(); + + let payload = serde_json::json!({ + "resourceMetrics": [{ + "resource": resource_json(service), + "scopeMetrics": [{ + "scope": {"name": "el-runtime", "version": "0.1.0"}, + "metrics": metric_items + }] + }] + }); + payload.to_string() +} + +// ── parse_tags_string: "k=v,k2=v2" → Vec<(String, AttrValue)> ──────────────── + +pub fn parse_tags_string(tags: &str) -> Vec<(String, AttrValue)> { + if tags.is_empty() { + return Vec::new(); + } + tags.split(',') + .filter_map(|pair| { + let mut parts = pair.splitn(2, '='); + let k = parts.next()?.trim().to_string(); + let v = parts.next().unwrap_or("").trim().to_string(); + if k.is_empty() { + None + } else { + Some((k, AttrValue::Str(v))) + } + }) + .collect() +} diff --git a/_archive/rust-bootstrap/bin/elvm/Cargo.toml b/_archive/rust-bootstrap/bin/elvm/Cargo.toml new file mode 100644 index 0000000..4a705f8 --- /dev/null +++ b/_archive/rust-bootstrap/bin/elvm/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "elvm" +description = "El Virtual Machine — executes compiled El bytecode (.elc) natively" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "elvm" +path = "src/main.rs" + +[dependencies] +el-compiler = { workspace = true } +el-vm = { workspace = true } +clap = { workspace = true } + +# Native window / WebView (macOS: WKWebView via wry) +wry = { version = "0.47", default-features = false } +winit = { version = "0.29", default-features = false, features = ["rwh_05", "rwh_06"] } +dpi = "0.1" diff --git a/_archive/rust-bootstrap/bin/elvm/src/main.rs b/_archive/rust-bootstrap/bin/elvm/src/main.rs new file mode 100644 index 0000000..ade007a --- /dev/null +++ b/_archive/rust-bootstrap/bin/elvm/src/main.rs @@ -0,0 +1,146 @@ +//! elvm — El Virtual Machine +//! +//! The standalone El VM binary. Loads and executes compiled El bytecode (.elc) +//! files produced by `el compile` or `el build-file`. +//! +//! # Usage +//! +//! elvm [args...] +//! elvm --version +//! elvm --help +//! +//! If the environment variable `NEURON_WINDOW_URL` is set, elvm opens a native +//! macOS window (WKWebView via wry) at that URL instead of executing bytecode. +//! This allows UI apps to be launched as proper desktop windows: +//! +//! NEURON_WINDOW_URL="http://localhost:7749" elvm dist/neuron-ui.elc + +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +#[command( + name = "elvm", + about = "El Virtual Machine — execute compiled El bytecode (.elc)", + long_about = "The El VM is the native El execution substrate.\n\ + Run .elc files produced by `el compile` or `el build-file`.\n\n\ + Set NEURON_WINDOW_URL= to open a native WebView window instead.", + version +)] +struct Cli { + /// Compiled El bytecode file to execute (*.elc). + file: PathBuf, + + /// Arguments forwarded to the program (accessible via `args()`). + #[arg(trailing_var_arg = true)] + args: Vec, +} + +fn main() { + let cli = Cli::parse(); + + // If NEURON_WINDOW_URL is set, open a native WebView window at that URL. + if let Ok(url) = std::env::var("NEURON_WINDOW_URL") { + if let Err(e) = open_window(&url) { + eprintln!("elvm: window error: {e}"); + std::process::exit(1); + } + return; + } + + if let Err(e) = run(cli) { + eprintln!("elvm: error: {e}"); + std::process::exit(1); + } +} + +fn run(cli: Cli) -> Result<(), Box> { + let bytes = std::fs::read(&cli.file) + .map_err(|e| format!("cannot read {}: {e}", cli.file.display()))?; + + let instructions = el_compiler::Bytecode::deserialize_all(&bytes) + .map_err(|e| format!("cannot load bytecode from {}: {e}", cli.file.display()))?; + + // Detect format and print diagnostic. + let is_elvm_container = bytes.starts_with(el_compiler::ELVM_MAGIC); + if is_elvm_container { + // Normal path — ELVM container. + } else { + eprintln!("elvm: warning: {} does not have an ELVM header — treating as legacy JSON bytecode", cli.file.display()); + } + + let mut vm = el_vm::ElVm::new(); + vm.run(&instructions, &cli.args); + + Ok(()) +} + +/// Opens a native WebView window at `url`. +/// +/// On macOS: uses wry (WKWebView) + winit for a proper native desktop window. +/// On other platforms: prints the URL (fallback). +fn open_window(url: &str) -> Result<(), Box> { + #[cfg(target_os = "macos")] + return open_native_window(url); + + #[cfg(not(target_os = "macos"))] + { + eprintln!("elvm: native window not supported on this platform"); + println!("elvm: open {url} in your browser"); + Ok(()) + } +} + +#[cfg(target_os = "macos")] +fn open_native_window(url: &str) -> Result<(), Box> { + use dpi::LogicalSize; + use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + use wry::{Rect, WebViewBuilder}; + + let event_loop = EventLoop::new().map_err(|e| format!("event loop: {e}"))?; + + let window = WindowBuilder::new() + .with_title("Neuron") + .with_inner_size(winit::dpi::LogicalSize::new(1600u32, 1000u32)) + .with_min_inner_size(winit::dpi::LogicalSize::new(900u32, 600u32)) + .with_resizable(true) + .build(&event_loop) + .map_err(|e| format!("window: {e}"))?; + + let url_owned = url.to_string(); + let webview = WebViewBuilder::new() + .with_url(&url_owned) + .build_as_child(&window) + .map_err(|e| format!("webview: {e}"))?; + + event_loop + .run(move |event, evl| { + evl.set_control_flow(ControlFlow::Wait); + + match event { + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + let scale = window.scale_factor(); + let logical = size.to_logical::(scale); + let _ = webview.set_bounds(Rect { + position: dpi::LogicalPosition::new(0, 0).into(), + size: LogicalSize::new(logical.width, logical.height).into(), + }); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => evl.exit(), + _ => {} + } + }) + .map_err(|e| format!("event loop run: {e}"))?; + + Ok(()) +} diff --git a/_archive/rust-bootstrap/build-targets.sh b/_archive/rust-bootstrap/build-targets.sh new file mode 100755 index 0000000..cc6feaa --- /dev/null +++ b/_archive/rust-bootstrap/build-targets.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# build-targets.sh — Build the El VM (elvm) for all supported platforms. +# +# Usage: +# ./build-targets.sh # build all targets +# ./build-targets.sh --list # list targets and prerequisites +# ./build-targets.sh x86_64-apple-darwin # build a specific target +# +# Prerequisites: +# - Rust toolchain (rustup) with cross-compilation targets installed +# - For Linux targets: cross-linkers (see comments below) +# +# Install a target: +# rustup target add +# +# For cross-compilation from macOS to Linux: +# brew install FiloSottile/musl-cross/musl-cross +# # or use `cross` (https://github.com/cross-rs/cross): +# cargo install cross --git https://github.com/cross-rs/cross + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_DIR="${SCRIPT_DIR}/dist/elvm" + +# ── Target definitions ──────────────────────────────────────────────────────── +# Format: "target:description:linker_hint" +declare -a TARGETS=( + "x86_64-unknown-linux-gnu:Linux x86_64 (glibc):brew install FiloSottile/musl-cross/musl-cross or use 'cross'" + "aarch64-unknown-linux-gnu:Linux ARM64 (glibc):brew install FiloSottile/musl-cross/musl-cross or use 'cross'" + "x86_64-unknown-linux-musl:Linux x86_64 (musl, static):brew install FiloSottile/musl-cross/musl-cross" + "aarch64-unknown-linux-musl:Linux ARM64 (musl, static):brew install aarch64-unknown-linux-musl cross-linker" + "x86_64-apple-darwin:macOS Intel:native (no cross tooling needed on macOS)" + "aarch64-apple-darwin:macOS Apple Silicon:native (no cross tooling needed on macOS)" + "x86_64-pc-windows-gnu:Windows x86_64:brew install mingw-w64" + "aarch64-pc-windows-msvc:Windows ARM64:requires Windows MSVC SDK (Windows only)" +) + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +ok() { echo -e " ${GREEN}✓${NC} $*"; } +warn() { echo -e " ${YELLOW}⚠${NC} $*"; } +fail() { echo -e " ${RED}✗${NC} $*"; } + +# ── List mode ───────────────────────────────────────────────────────────────── + +if [[ "${1:-}" == "--list" ]]; then + echo "" + echo "El VM cross-compilation targets" + echo "================================" + echo "" + for entry in "${TARGETS[@]}"; do + IFS=':' read -r target desc hint <<< "$entry" + printf " %-42s %s\n" "$target" "$desc" + printf " %-42s Prereq: %s\n" "" "$hint" + echo "" + done + echo "Install a target: rustup target add " + echo "List installed: rustup target list --installed" + exit 0 +fi + +# ── Filter targets ──────────────────────────────────────────────────────────── + +if [[ $# -gt 0 && "${1:-}" != "--list" ]]; then + # Build only the specified target(s). + SELECTED=("$@") +else + # Build all targets. + SELECTED=() + for entry in "${TARGETS[@]}"; do + IFS=':' read -r target _ _ <<< "$entry" + SELECTED+=("$target") + done +fi + +# ── Build ───────────────────────────────────────────────────────────────────── + +echo "" +echo -e "${BLUE}El VM — cross-platform build${NC}" +echo "Output directory: $OUTPUT_DIR" +echo "" + +mkdir -p "$OUTPUT_DIR" + +BUILT=0 +SKIPPED=0 +FAILED=0 + +for target in "${SELECTED[@]}"; do + # Find description for this target. + desc="$target" + for entry in "${TARGETS[@]}"; do + IFS=':' read -r t d _ <<< "$entry" + if [[ "$t" == "$target" ]]; then desc="$d"; break; fi + done + + echo -e "${BLUE}→ Building $target${NC} ($desc)" + + # Check if the Rust target is installed. + if ! rustup target list --installed 2>/dev/null | grep -q "^$target\b"; then + warn "Target $target not installed — installing..." + if rustup target add "$target" 2>/dev/null; then + ok "Installed $target" + else + fail "Cannot install $target — skipping" + ((SKIPPED++)) + continue + fi + fi + + # Attempt to build. + binary_suffix="" + if [[ "$target" == *"windows"* ]]; then + binary_suffix=".exe" + fi + + binary_name="elvm-${target}${binary_suffix}" + output_path="$OUTPUT_DIR/$binary_name" + + if (cd "$SCRIPT_DIR" && cargo build --release --target "$target" -p elvm 2>&1 | tail -5); then + src="$SCRIPT_DIR/target/$target/release/elvm${binary_suffix}" + if [[ -f "$src" ]]; then + cp "$src" "$output_path" + size=$(du -h "$output_path" | cut -f1) + ok "Built $binary_name ($size)" + ((BUILT++)) + else + fail "Binary not found at $src" + ((FAILED++)) + fi + else + fail "Build failed for $target (cross-linker may be missing — see --list)" + ((FAILED++)) + fi + echo "" +done + +# ── Summary ─────────────────────────────────────────────────────────────────── + +echo "────────────────────────────────" +echo "Built: $BUILT" +echo "Skipped: $SKIPPED" +echo "Failed: $FAILED" + +if [[ $BUILT -gt 0 ]]; then + echo "" + echo "Artifacts:" + ls -lh "$OUTPUT_DIR/" +fi + +echo "" +if [[ $FAILED -gt 0 ]]; then + echo -e "${YELLOW}Some targets failed. Run './build-targets.sh --list' for prerequisites.${NC}" + exit 1 +fi diff --git a/engrams/el-arch/Cargo.toml b/_archive/rust-bootstrap/engrams/el-arch/Cargo.toml similarity index 100% rename from engrams/el-arch/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-arch/Cargo.toml diff --git a/engrams/el-arch/src/checker.rs b/_archive/rust-bootstrap/engrams/el-arch/src/checker.rs similarity index 95% rename from engrams/el-arch/src/checker.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/checker.rs index 2e0532d..80a08d0 100644 --- a/engrams/el-arch/src/checker.rs +++ b/_archive/rust-bootstrap/engrams/el-arch/src/checker.rs @@ -266,6 +266,12 @@ fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec) } } Expr::UnaryBitNot(inner) => extract_calls_from_expr(inner, in_loop, out), + // JSX expressions — traverse children. + Expr::JsxElement { children, .. } => { + for child in children { extract_calls_from_expr(child, in_loop, out); } + } + Expr::JsxExpr(inner) => extract_calls_from_expr(inner, in_loop, out), + Expr::JsxText(_) => {} } } @@ -388,6 +394,12 @@ fn extract_activate_types_expr(expr: &Expr, in_loop: bool, out: &mut Vec } } Expr::UnaryBitNot(inner) => extract_activate_types_expr(inner, in_loop, out), + // JSX expressions — traverse children. + Expr::JsxElement { children, .. } => { + for child in children { extract_activate_types_expr(child, in_loop, out); } + } + Expr::JsxExpr(inner) => extract_activate_types_expr(inner, in_loop, out), + Expr::JsxText(_) => {} } } @@ -452,6 +464,10 @@ fn has_sealed_in_loop_expr(expr: &Expr, in_loop: bool) -> bool { Expr::Parallel { entries } => entries.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)), Expr::Trace { body, .. } => body.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)), Expr::UnaryBitNot(inner) => has_sealed_in_loop_expr(inner, in_loop), + // JSX expressions. + Expr::JsxElement { children, .. } => children.iter().any(|c| has_sealed_in_loop_expr(c, in_loop)), + Expr::JsxExpr(inner) => has_sealed_in_loop_expr(inner, in_loop), + Expr::JsxText(_) => false, } } diff --git a/engrams/el-arch/src/error.rs b/_archive/rust-bootstrap/engrams/el-arch/src/error.rs similarity index 100% rename from engrams/el-arch/src/error.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/error.rs diff --git a/engrams/el-arch/src/lib.rs b/_archive/rust-bootstrap/engrams/el-arch/src/lib.rs similarity index 100% rename from engrams/el-arch/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/lib.rs diff --git a/engrams/el-arch/src/rule.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rule.rs similarity index 100% rename from engrams/el-arch/src/rule.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rule.rs diff --git a/engrams/el-arch/src/rules/graph.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rules/graph.rs similarity index 100% rename from engrams/el-arch/src/rules/graph.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rules/graph.rs diff --git a/engrams/el-arch/src/rules/mod.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rules/mod.rs similarity index 100% rename from engrams/el-arch/src/rules/mod.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rules/mod.rs diff --git a/engrams/el-arch/src/rules/security.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rules/security.rs similarity index 100% rename from engrams/el-arch/src/rules/security.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rules/security.rs diff --git a/engrams/el-arch/src/rules/swarm.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rules/swarm.rs similarity index 100% rename from engrams/el-arch/src/rules/swarm.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rules/swarm.rs diff --git a/engrams/el-arch/src/rules/vbd.rs b/_archive/rust-bootstrap/engrams/el-arch/src/rules/vbd.rs similarity index 100% rename from engrams/el-arch/src/rules/vbd.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/rules/vbd.rs diff --git a/engrams/el-arch/src/tests.rs b/_archive/rust-bootstrap/engrams/el-arch/src/tests.rs similarity index 100% rename from engrams/el-arch/src/tests.rs rename to _archive/rust-bootstrap/engrams/el-arch/src/tests.rs diff --git a/engrams/el-build/Cargo.toml b/_archive/rust-bootstrap/engrams/el-build/Cargo.toml similarity index 100% rename from engrams/el-build/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-build/Cargo.toml diff --git a/engrams/el-build/src/build.rs b/_archive/rust-bootstrap/engrams/el-build/src/build.rs similarity index 89% rename from engrams/el-build/src/build.rs rename to _archive/rust-bootstrap/engrams/el-build/src/build.rs index 05c8fd8..c2a6cdd 100644 --- a/engrams/el-build/src/build.rs +++ b/_archive/rust-bootstrap/engrams/el-build/src/build.rs @@ -365,11 +365,13 @@ fn resolve_imports_inner( .map_err(|e| format!("cannot read {}: {e}", file.display()))?; let mut out = String::new(); - for line in source.lines() { + let mut lines_iter = source.lines().peekable(); + while let Some(line) = lines_iter.next() { let trimmed = line.trim(); if let Some(rest) = trimmed.strip_prefix("import ") { let rest = rest.trim(); if rest.starts_with('"') && rest.ends_with('"') { + // import "relative/path.el" let import_path_str = &rest[1..rest.len() - 1]; let import_path = dir.join(import_path_str); let imported = resolve_imports_inner(&import_path, visited)?; @@ -379,6 +381,49 @@ fn resolve_imports_inner( out.push_str(line); out.push('\n'); } + } else if let Some(rest) = trimmed.strip_prefix("from ") { + // from ModuleName import { ... } — resolve ModuleName.el in the same dir + // Extract the module name (everything before " import") + if let Some(module_part) = rest.split(" import").next() { + let module_name = module_part.trim(); + // Only resolve bare identifiers (not quoted paths or dotted names with /) + if !module_name.contains('"') && !module_name.contains('/') && !module_name.is_empty() { + let module_file = format!("{}.el", module_name); + let import_path = dir.join(&module_file); + + // Consume any continuation lines of a multi-line import: + // `from X import {\n Y,\n Z,\n}` + // The opening line may or may not contain `{`. If it does not end + // with `}`, skip lines until we see one that ends with `}`. + let import_rest = rest.splitn(2, " import").nth(1).unwrap_or("").trim(); + let is_multiline = import_rest.contains('{') && !import_rest.contains('}'); + if is_multiline { + // Skip continuation lines until we see `}` + for cont in lines_iter.by_ref() { + if cont.trim().contains('}') { + break; + } + } + } + + if import_path.exists() { + // Inline the module, omitting the import line itself + let imported = resolve_imports_inner(&import_path, visited)?; + out.push_str(&imported); + out.push('\n'); + } else { + // Module file not found — leave the line for the compiler to handle + out.push_str(line); + out.push('\n'); + } + } else { + out.push_str(line); + out.push('\n'); + } + } else { + out.push_str(line); + out.push('\n'); + } } else { out.push_str(line); out.push('\n'); @@ -439,6 +484,7 @@ mod tests { }, cross: CrossConfig { targets: vec![] }, plugins: HashMap::new(), + app: None, } } diff --git a/engrams/el-build/src/cache.rs b/_archive/rust-bootstrap/engrams/el-build/src/cache.rs similarity index 100% rename from engrams/el-build/src/cache.rs rename to _archive/rust-bootstrap/engrams/el-build/src/cache.rs diff --git a/engrams/el-build/src/error.rs b/_archive/rust-bootstrap/engrams/el-build/src/error.rs similarity index 100% rename from engrams/el-build/src/error.rs rename to _archive/rust-bootstrap/engrams/el-build/src/error.rs diff --git a/engrams/el-build/src/lib.rs b/_archive/rust-bootstrap/engrams/el-build/src/lib.rs similarity index 100% rename from engrams/el-build/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-build/src/lib.rs diff --git a/engrams/el-build/src/plugin.rs b/_archive/rust-bootstrap/engrams/el-build/src/plugin.rs similarity index 100% rename from engrams/el-build/src/plugin.rs rename to _archive/rust-bootstrap/engrams/el-build/src/plugin.rs diff --git a/engrams/el-build/src/test_runner.rs b/_archive/rust-bootstrap/engrams/el-build/src/test_runner.rs similarity index 100% rename from engrams/el-build/src/test_runner.rs rename to _archive/rust-bootstrap/engrams/el-build/src/test_runner.rs diff --git a/engrams/el-compiler/Cargo.toml b/_archive/rust-bootstrap/engrams/el-compiler/Cargo.toml similarity index 100% rename from engrams/el-compiler/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-compiler/Cargo.toml diff --git a/engrams/el-compiler/src/bytecode.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/bytecode.rs similarity index 78% rename from engrams/el-compiler/src/bytecode.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/bytecode.rs index 8c2b411..6eeb81f 100644 --- a/engrams/el-compiler/src/bytecode.rs +++ b/_archive/rust-bootstrap/engrams/el-compiler/src/bytecode.rs @@ -225,9 +225,78 @@ pub fn deserialize_bytecode(bytes: &[u8]) -> Result, String> { serde_json::from_slice(bytes).map_err(|e| e.to_string()) } -impl Bytecode { - /// Deserialize a bytecode slice from JSON bytes (convenience wrapper). - pub fn deserialize_all(bytes: &[u8]) -> Result, String> { +// ── ELVM binary container format ────────────────────────────────────────────── +// +// Header: +// magic: [u8; 4] = b"ELVM" +// version: [u8; 4] (little-endian u32, currently 1) +// length: [u8; 8] (little-endian u64, byte length of the payload that follows) +// Payload: +// JSON-serialized Vec (same encoding as serialize_bytecode) +// +// The JSON payload is intentionally kept so that existing tooling can inspect +// .elc files by stripping the 16-byte header. Future versions may switch to a +// denser binary encoding while bumping the version field. + +/// Magic bytes that identify an El bytecode container. +pub const ELVM_MAGIC: &[u8; 4] = b"ELVM"; +/// Current container format version. +pub const ELVM_VERSION: u32 = 1; +/// Total header size in bytes (magic + version + length). +pub const ELVM_HEADER_SIZE: usize = 16; + +/// Wrap serialized bytecode in the ELVM binary container. +/// +/// Returns the full `.elc` file bytes: 16-byte header + JSON payload. +pub fn wrap_elvm(instructions: &[Bytecode]) -> Result, String> { + let payload = serialize_bytecode(instructions)?; + let length = payload.len() as u64; + + let mut out = Vec::with_capacity(ELVM_HEADER_SIZE + payload.len()); + out.extend_from_slice(ELVM_MAGIC); + out.extend_from_slice(&ELVM_VERSION.to_le_bytes()); + out.extend_from_slice(&length.to_le_bytes()); + out.extend_from_slice(&payload); + Ok(out) +} + +/// Unwrap and deserialize an ELVM binary container. +/// +/// Accepts either: +/// - A full ELVM container (starts with `ELVM` magic), or +/// - Raw JSON bytes (legacy `.elc` format — no header). +pub fn unwrap_elvm(bytes: &[u8]) -> Result, String> { + if bytes.starts_with(ELVM_MAGIC) { + // Binary container: skip header, deserialize payload. + if bytes.len() < ELVM_HEADER_SIZE { + return Err("truncated ELVM header".to_string()); + } + let version = u32::from_le_bytes( + bytes[4..8].try_into().map_err(|_| "bad version field".to_string())? + ); + if version != ELVM_VERSION { + return Err(format!("unsupported ELVM version {version} (expected {ELVM_VERSION})")); + } + let length = u64::from_le_bytes( + bytes[8..16].try_into().map_err(|_| "bad length field".to_string())? + ) as usize; + let payload_end = ELVM_HEADER_SIZE + length; + if bytes.len() < payload_end { + return Err(format!( + "truncated ELVM payload: expected {length} bytes, got {}", + bytes.len().saturating_sub(ELVM_HEADER_SIZE) + )); + } + deserialize_bytecode(&bytes[ELVM_HEADER_SIZE..payload_end]) + } else { + // Legacy JSON-only format (no header). deserialize_bytecode(bytes) } } + +impl Bytecode { + /// Deserialize a bytecode slice — handles both ELVM container and raw JSON. + pub fn deserialize_all(bytes: &[u8]) -> Result, String> { + unwrap_elvm(bytes) + } +} diff --git a/engrams/el-compiler/src/codegen.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/codegen.rs similarity index 74% rename from engrams/el-compiler/src/codegen.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/codegen.rs index de44c83..90445ea 100644 --- a/engrams/el-compiler/src/codegen.rs +++ b/_archive/rust-bootstrap/engrams/el-compiler/src/codegen.rs @@ -1,6 +1,6 @@ //! Code generator: walks the AST and emits bytecode instructions. -use el_parser::{BinOp, Expr, Literal, Program, Stmt}; +use el_parser::{BinOp, Expr, JsxAttrValue, Literal, Program, Stmt}; use crate::bytecode::{Bytecode, Value}; use crate::error::CompileResult; @@ -72,6 +72,11 @@ impl Codegen { Stmt::Expr(expr, _) => { // In tail position, leave the value on the stack. self.gen_expr(expr)?; + // If-without-else leaves nothing on stack (it pops internally); + // push Nil so we always have exactly one return value. + if matches!(expr, Expr::If { else_: None, .. }) { + self.emit(Bytecode::Push(Value::Nil)); + } } Stmt::Return(expr, _) => { self.gen_expr(expr)?; @@ -104,8 +109,19 @@ impl Codegen { } Stmt::Expr(expr, _) => { self.gen_expr(expr)?; - // Discard the expression result unless it's a return-like - if !matches!(expr, Expr::Block(_) | Expr::If { .. }) { + // Always discard the expression result in statement position. + // Expr::Block is NOT special-cased: even a block expression used + // as a statement should have its result discarded. + // Note: Expr::If { else_: Some(_) } leaves a value on stack from + // both branches (via gen_stmt_tail on the last block statement), + // so it must be popped here too. + // If-without-else already pops internally (see gen_expr for If), + // so we only skip the extra Pop for that case. + let needs_pop = match expr { + Expr::If { else_: None, .. } => false, // already handled internally + _ => true, + }; + if needs_pop { self.emit(Bytecode::Pop); } } @@ -128,11 +144,21 @@ impl Codegen { message: format!("contract violation in fn '{name}': requires clause failed"), }); } - for s in body { - self.gen_stmt(s)?; + // All statements except the last use gen_stmt (which pops expr results). + // The last statement uses gen_stmt_tail so that the final expression + // value stays on the stack as the function's implicit return value. + let body_len = body.len(); + for (i, s) in body.iter().enumerate() { + if i + 1 == body_len { + self.gen_stmt_tail(s)?; + } else { + self.gen_stmt(s)?; + } + } + // If the body is empty, return Nil. + if body_len == 0 { + self.emit(Bytecode::Push(Value::Nil)); } - // Implicit void return - self.emit(Bytecode::Push(Value::Nil)); self.emit(Bytecode::Return); // Patch the skip jump @@ -235,6 +261,48 @@ impl Codegen { // Test-related statements — skipped during normal compilation. // The el-test crate walks the AST directly rather than running compiled bytecode. Stmt::TestDef { .. } | Stmt::Seed(..) | Stmt::Assert(..) => {} + // Component definition: generate a function that renders the component. + // The function is registered under "__component_" and the template + // is emitted as a nested function body. + Stmt::ComponentDef { name, state, methods, template, .. } => { + let fn_name = format!("__component_{name}"); + let skip_jump = self.emit(Bytecode::Jump(0)); + + // Emit component methods (each has its own Jump/body/Register pattern) + for m in methods { + self.gen_stmt(m)?; + } + + // Record where the template starts — AFTER all method bodies. + // This is the real entry point for calling this component. + let template_start = self.current_idx(); + + // Initialise state fields as locals (so template expressions can load them) + for field in state { + if let Some(default) = &field.default { + self.gen_expr(default)?; + } else { + self.emit(Bytecode::Push(Value::Nil)); + } + self.emit(Bytecode::StoreLocal(field.name.clone())); + } + + // Emit template as the return value + self.gen_expr(template)?; + self.emit(Bytecode::Return); + + let after = self.current_idx(); + self.patch_jump(skip_jump, after); + + // entry_point is the first instruction of the template body. + let entry_point = template_start; + self.emit(Bytecode::Push(Value::Int(entry_point as i64))); + self.emit(Bytecode::StoreLocal(format!("__fn_{fn_name}"))); + + // Also register the component name directly so `` works + self.emit(Bytecode::Push(Value::Int(entry_point as i64))); + self.emit(Bytecode::StoreLocal(format!("__fn_{name}"))); + } // New statement kinds — no runtime code emitted. _ => {} } @@ -258,6 +326,18 @@ impl Codegen { self.emit(Bytecode::LoadLocal(name.clone())); } Expr::BinOp { op, left, right } => { + // NullCoalesce (`a ?? b`) is short-circuit: if `a` is truthy, use it; else `b`. + // Bytecode: eval a, Dup, JumpIf(skip), Pop, eval b, skip: + if matches!(op, BinOp::NullCoalesce) { + self.gen_expr(left)?; + self.emit(Bytecode::Dup); + let skip = self.emit(Bytecode::JumpIf(0)); // patched below + self.emit(Bytecode::Pop); // discard the falsy `a` + self.gen_expr(right)?; + let after = self.current_idx(); + self.patch_jump(skip, after); + return Ok(()); + } self.gen_expr(left)?; self.gen_expr(right)?; let instr = match op { @@ -279,6 +359,7 @@ impl Codegen { BinOp::BitXor => Bytecode::BitXor, BinOp::Shl => Bytecode::Shl, BinOp::Shr => Bytecode::Shr, + BinOp::NullCoalesce => unreachable!("handled above"), }; self.emit(instr); } @@ -297,7 +378,11 @@ impl Codegen { } // Get the function name from the callee expression let fn_name = match func.as_ref() { - Expr::Ident(n) => n.clone(), + Expr::Ident(n) => { + // Strip leading `@` from syscall/builtin names: + // `@http_get` → `http_get`, `@json_parse` → `json_parse` + n.strip_prefix('@').unwrap_or(n).to_string() + } Expr::Field { object, field } => { self.gen_expr(object)?; field.clone() @@ -503,6 +588,75 @@ impl Codegen { self.emit(Bytecode::TraceEnd { label: label.clone() }); self.emit(Bytecode::Push(Value::Nil)); } + // JSX element: push tag name, attrs (as a map), and children list, then call __jsx__ + Expr::JsxElement { tag, attrs, children, .. } => { + // If the tag starts with an uppercase letter it's a component reference. + // Call the component function directly (it takes no args, returns HTML string). + let is_component = tag.chars().next().map_or(false, |c| c.is_uppercase()); + + if is_component { + // Call the component as a function: Call { name: tag, arity: 0 } + // The component will render itself and return an HTML string. + self.emit(Bytecode::Call { name: tag.clone(), arity: 0 }); + } else { + // Push tag name + self.emit(Bytecode::Push(Value::Str(tag.clone()))); + // Build attrs map: push key-value pairs + let n_attrs = attrs.len() as u32; + for (attr_name, attr_val) in attrs { + self.emit(Bytecode::Push(Value::Str(attr_name.clone()))); + match attr_val { + JsxAttrValue::Str(s) => { + self.emit(Bytecode::Push(Value::Str(s.clone()))); + } + JsxAttrValue::Expr(expr) => { + self.gen_expr(expr)?; + } + } + } + self.emit(Bytecode::BuildMap(n_attrs)); + // Build children list + for child in children { + self.gen_expr(child)?; + } + self.emit(Bytecode::BuildList(children.len() as u32)); + // Call __jsx__(tag, attrs, children) + self.emit(Bytecode::Call { name: "__jsx__".to_string(), arity: 3 }); + } + } + // JSX expression interpolation: just evaluate the inner expression + Expr::JsxExpr(inner) => { + self.gen_expr(inner)?; + } + // JSX text: push as string + Expr::JsxText(text) => { + self.emit(Bytecode::Push(Value::Str(text.clone()))); + } + // Closure: compile as an inline function and push a reference to it. + // The closure body is emitted as a skip-over block. + Expr::Closure { params, body, .. } => { + let closure_id = self.current_idx(); + let fn_name = format!("__closure_{closure_id}__"); + let skip_jump = self.emit(Bytecode::Jump(0)); + + // Bind params in reverse order (stack has args pushed left-to-right) + for param in params.iter().rev() { + self.emit(Bytecode::StoreLocal(param.name.clone())); + } + self.gen_expr(body)?; + self.emit(Bytecode::Return); + + let after = self.current_idx(); + self.patch_jump(skip_jump, after); + + let entry_point = skip_jump + 1; + // Register the closure function + self.emit(Bytecode::Push(Value::Int(entry_point as i64))); + self.emit(Bytecode::StoreLocal(format!("__fn_{fn_name}"))); + + // Push a reference to this closure (as its function name) + self.emit(Bytecode::Push(Value::Str(fn_name))); + } // New expression kinds — push Nil as placeholder _ => { self.emit(Bytecode::Push(Value::Nil)); diff --git a/engrams/el-compiler/src/compiler.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/compiler.rs similarity index 100% rename from engrams/el-compiler/src/compiler.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/compiler.rs diff --git a/engrams/el-compiler/src/debugger.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/debugger.rs similarity index 100% rename from engrams/el-compiler/src/debugger.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/debugger.rs diff --git a/engrams/el-compiler/src/error.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/error.rs similarity index 100% rename from engrams/el-compiler/src/error.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/error.rs diff --git a/engrams/el-compiler/src/lib.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/lib.rs similarity index 94% rename from engrams/el-compiler/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/lib.rs index 67b0cc9..12c269d 100644 --- a/engrams/el-compiler/src/lib.rs +++ b/_archive/rust-bootstrap/engrams/el-compiler/src/lib.rs @@ -28,7 +28,7 @@ mod debugger; mod error; mod source_map; -pub use bytecode::{Bytecode, Value, serialize_bytecode, deserialize_bytecode}; +pub use bytecode::{Bytecode, Value, serialize_bytecode, deserialize_bytecode, wrap_elvm, unwrap_elvm, ELVM_MAGIC, ELVM_VERSION}; pub use compiler::{CompileOutput, Compiler, CompilerOptions, Target}; pub use debugger::{DebugEvent, Debugger, StackFrame, StepMode}; pub use error::{CompileError, CompileResult}; diff --git a/engrams/el-compiler/src/source_map.rs b/_archive/rust-bootstrap/engrams/el-compiler/src/source_map.rs similarity index 100% rename from engrams/el-compiler/src/source_map.rs rename to _archive/rust-bootstrap/engrams/el-compiler/src/source_map.rs diff --git a/engrams/el-fmt/Cargo.toml b/_archive/rust-bootstrap/engrams/el-fmt/Cargo.toml similarity index 100% rename from engrams/el-fmt/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-fmt/Cargo.toml diff --git a/engrams/el-fmt/src/config.rs b/_archive/rust-bootstrap/engrams/el-fmt/src/config.rs similarity index 100% rename from engrams/el-fmt/src/config.rs rename to _archive/rust-bootstrap/engrams/el-fmt/src/config.rs diff --git a/engrams/el-fmt/src/error.rs b/_archive/rust-bootstrap/engrams/el-fmt/src/error.rs similarity index 100% rename from engrams/el-fmt/src/error.rs rename to _archive/rust-bootstrap/engrams/el-fmt/src/error.rs diff --git a/engrams/el-fmt/src/formatter.rs b/_archive/rust-bootstrap/engrams/el-fmt/src/formatter.rs similarity index 88% rename from engrams/el-fmt/src/formatter.rs rename to _archive/rust-bootstrap/engrams/el-fmt/src/formatter.rs index ed07454..14dd211 100644 --- a/engrams/el-fmt/src/formatter.rs +++ b/_archive/rust-bootstrap/engrams/el-fmt/src/formatter.rs @@ -226,6 +226,21 @@ impl Formatter { } out.push_str(&format!("{ind}}}\n")); } + + // Component definition (UI/reactive components) — emit as-is placeholder. + Stmt::ComponentDef { name, methods, .. } => { + out.push_str(&format!("{ind}component {name} {{\n")); + for s in methods { + self.fmt_stmt(out, s, depth + 1); + } + out.push_str(&format!("{ind}}}\n")); + } + + // Catch-all: unknown/future statement kinds are emitted as a comment. + #[allow(unreachable_patterns)] + _ => { + out.push_str(&format!("{ind}// [unformatted statement]\n")); + } } } @@ -406,6 +421,47 @@ impl Formatter { } out.push_str(&format!("{}}}", self.indent(depth))); } + + // JSX expressions — emit minimal JSX syntax. + Expr::JsxElement { tag, attrs, children, self_closing } => { + out.push('<'); + out.push_str(tag); + for (key, val) in attrs { + out.push(' '); + out.push_str(key); + match val { + el_parser::JsxAttrValue::Str(s) => out.push_str(&format!("=\"{s}\"")), + el_parser::JsxAttrValue::Expr(e) => { + out.push_str("={"); + self.fmt_expr(out, e, depth); + out.push('}'); + } + } + } + if *self_closing { + out.push_str(" />"); + } else { + out.push('>'); + for child in children { + self.fmt_expr(out, child, depth); + } + out.push_str(&format!("")); + } + } + Expr::JsxExpr(inner) => { + out.push('{'); + self.fmt_expr(out, inner, depth); + out.push('}'); + } + Expr::JsxText(text) => { + out.push_str(text); + } + + // Catch-all: unknown/future expression kinds. + #[allow(unreachable_patterns)] + _ => { + out.push_str("/* [unformatted expr] */"); + } } } @@ -438,6 +494,7 @@ impl Formatter { BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", + BinOp::NullCoalesce => "??", } } diff --git a/engrams/el-fmt/src/lib.rs b/_archive/rust-bootstrap/engrams/el-fmt/src/lib.rs similarity index 100% rename from engrams/el-fmt/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-fmt/src/lib.rs diff --git a/engrams/el-integration/Cargo.toml b/_archive/rust-bootstrap/engrams/el-integration/Cargo.toml similarity index 100% rename from engrams/el-integration/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-integration/Cargo.toml diff --git a/engrams/el-integration/src/lib.rs b/_archive/rust-bootstrap/engrams/el-integration/src/lib.rs similarity index 100% rename from engrams/el-integration/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/lib.rs diff --git a/engrams/el-integration/src/tests/activate_typing.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/activate_typing.rs similarity index 100% rename from engrams/el-integration/src/tests/activate_typing.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/activate_typing.rs diff --git a/engrams/el-integration/src/tests/bytecode_structure.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/bytecode_structure.rs similarity index 100% rename from engrams/el-integration/src/tests/bytecode_structure.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/bytecode_structure.rs diff --git a/engrams/el-integration/src/tests/compiler_pipeline.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/compiler_pipeline.rs similarity index 100% rename from engrams/el-integration/src/tests/compiler_pipeline.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/compiler_pipeline.rs diff --git a/engrams/el-integration/src/tests/decorator_codegen.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/decorator_codegen.rs similarity index 100% rename from engrams/el-integration/src/tests/decorator_codegen.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/decorator_codegen.rs diff --git a/engrams/el-integration/src/tests/error_propagation.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/error_propagation.rs similarity index 100% rename from engrams/el-integration/src/tests/error_propagation.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/error_propagation.rs diff --git a/engrams/el-integration/src/tests/mod.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/mod.rs similarity index 100% rename from engrams/el-integration/src/tests/mod.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/mod.rs diff --git a/engrams/el-integration/src/tests/protocol_conformance.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/protocol_conformance.rs similarity index 100% rename from engrams/el-integration/src/tests/protocol_conformance.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/protocol_conformance.rs diff --git a/engrams/el-integration/src/tests/stdlib_usage.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/stdlib_usage.rs similarity index 100% rename from engrams/el-integration/src/tests/stdlib_usage.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/stdlib_usage.rs diff --git a/engrams/el-integration/src/tests/test_framework.rs b/_archive/rust-bootstrap/engrams/el-integration/src/tests/test_framework.rs similarity index 100% rename from engrams/el-integration/src/tests/test_framework.rs rename to _archive/rust-bootstrap/engrams/el-integration/src/tests/test_framework.rs diff --git a/engrams/el-lexer/Cargo.toml b/_archive/rust-bootstrap/engrams/el-lexer/Cargo.toml similarity index 100% rename from engrams/el-lexer/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-lexer/Cargo.toml diff --git a/engrams/el-lexer/src/error.rs b/_archive/rust-bootstrap/engrams/el-lexer/src/error.rs similarity index 100% rename from engrams/el-lexer/src/error.rs rename to _archive/rust-bootstrap/engrams/el-lexer/src/error.rs diff --git a/engrams/el-lexer/src/lexer.rs b/_archive/rust-bootstrap/engrams/el-lexer/src/lexer.rs similarity index 96% rename from engrams/el-lexer/src/lexer.rs rename to _archive/rust-bootstrap/engrams/el-lexer/src/lexer.rs index 1d3af12..0ec90fb 100644 --- a/engrams/el-lexer/src/lexer.rs +++ b/_archive/rust-bootstrap/engrams/el-lexer/src/lexer.rs @@ -201,7 +201,10 @@ impl<'src> Lexer<'src> { } } '@' => Token::At, - '?' => Token::QuestionMark, + '#' => Token::Hash, + '?' => { + if self.eat('?') { Token::NullCoalesce } else { Token::QuestionMark } + } ':' => { if self.eat(':') { Token::ColonColon @@ -220,10 +223,9 @@ impl<'src> Lexer<'src> { c if c.is_alphabetic() || c == '_' => self.scan_ident_or_keyword(start, c), other => { - return Err(LexError::new( - LexErrorKind::UnexpectedChar(other), - self.span_from(start), - )) + // Emit Unknown token instead of erroring — allows JSX text with + // Unicode / special characters to pass through the lexer gracefully. + Token::Unknown(other) } }; @@ -372,10 +374,14 @@ fn keyword_or_ident(s: String) -> Token { "parallel" => Token::Parallel, "trace" => Token::Trace, "requires" => Token::Requires, - "deploy" => Token::Deploy, - "to" => Token::To, - "via" => Token::Via, - _ => Token::Ident(s), + "deploy" => Token::Deploy, + "to" => Token::To, + "via" => Token::Via, + "component" => Token::Component, + "props" => Token::Props, + "state" => Token::State, + "template" => Token::Template, + _ => Token::Ident(s), } } diff --git a/engrams/el-lexer/src/lib.rs b/_archive/rust-bootstrap/engrams/el-lexer/src/lib.rs similarity index 100% rename from engrams/el-lexer/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-lexer/src/lib.rs diff --git a/engrams/el-lexer/src/token.rs b/_archive/rust-bootstrap/engrams/el-lexer/src/token.rs similarity index 90% rename from engrams/el-lexer/src/token.rs rename to _archive/rust-bootstrap/engrams/el-lexer/src/token.rs index cfe4d74..687c8ef 100644 --- a/engrams/el-lexer/src/token.rs +++ b/_archive/rust-bootstrap/engrams/el-lexer/src/token.rs @@ -115,6 +115,14 @@ pub enum Token { To, /// `via` — used in `deploy fn to "/route" via soma` Via, + /// `component` — component definition + Component, + /// `props` — props block inside a component + Props, + /// `state` — state block inside a component + State, + /// `template` — template block inside a component + Template, /// `|>` — pipe operator PipeOp, /// `true` / `false` @@ -194,6 +202,8 @@ pub enum Token { Pipe, /// `?` — used both for Optional types and the Try operator QuestionMark, + /// `??` — null-coalescing operator: `a ?? b` returns `b` when `a` is nil/empty + NullCoalesce, /// `%` — modulo Percent, /// `&` — bitwise AND @@ -206,6 +216,11 @@ pub enum Token { Shl, /// `>>` — right shift Shr, + /// `#` — hash character (used in JSX text like `#tag`) + Hash, + /// Any other character not recognized by the lexer (emitted instead of erroring, + /// so JSX text with Unicode / special characters can be skipped gracefully). + Unknown(char), // ── Special ─────────────────────────────────────────────────────────────── Eof, @@ -248,16 +263,23 @@ impl std::fmt::Display for Token { Token::Deploy => write!(f, "deploy"), Token::To => write!(f, "to"), Token::Via => write!(f, "via"), + Token::Component => write!(f, "component"), + Token::Props => write!(f, "props"), + Token::State => write!(f, "state"), + Token::Template => write!(f, "template"), Token::PipeOp => write!(f, "|>"), Token::At => write!(f, "@"), Token::Pipe => write!(f, "|"), Token::QuestionMark => write!(f, "?"), + Token::NullCoalesce => write!(f, "??"), Token::Percent => write!(f, "%"), Token::Ampersand => write!(f, "&"), Token::Caret => write!(f, "^"), Token::Tilde => write!(f, "~"), Token::Shl => write!(f, "<<"), Token::Shr => write!(f, ">>"), + Token::Hash => write!(f, "#"), + Token::Unknown(c) => write!(f, "{c}"), Token::BoolLiteral(b) => write!(f, "{b}"), Token::IntLiteral(n) => write!(f, "{n}"), Token::FloatLiteral(n) => write!(f, "{n}"), diff --git a/engrams/el-lint/Cargo.toml b/_archive/rust-bootstrap/engrams/el-lint/Cargo.toml similarity index 100% rename from engrams/el-lint/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-lint/Cargo.toml diff --git a/engrams/el-lint/src/error.rs b/_archive/rust-bootstrap/engrams/el-lint/src/error.rs similarity index 100% rename from engrams/el-lint/src/error.rs rename to _archive/rust-bootstrap/engrams/el-lint/src/error.rs diff --git a/engrams/el-lint/src/lib.rs b/_archive/rust-bootstrap/engrams/el-lint/src/lib.rs similarity index 100% rename from engrams/el-lint/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-lint/src/lib.rs diff --git a/engrams/el-lint/src/linter.rs b/_archive/rust-bootstrap/engrams/el-lint/src/linter.rs similarity index 100% rename from engrams/el-lint/src/linter.rs rename to _archive/rust-bootstrap/engrams/el-lint/src/linter.rs diff --git a/engrams/el-lint/src/report.rs b/_archive/rust-bootstrap/engrams/el-lint/src/report.rs similarity index 100% rename from engrams/el-lint/src/report.rs rename to _archive/rust-bootstrap/engrams/el-lint/src/report.rs diff --git a/engrams/el-lint/src/rules.rs b/_archive/rust-bootstrap/engrams/el-lint/src/rules.rs similarity index 100% rename from engrams/el-lint/src/rules.rs rename to _archive/rust-bootstrap/engrams/el-lint/src/rules.rs diff --git a/engrams/el-manifest/Cargo.toml b/_archive/rust-bootstrap/engrams/el-manifest/Cargo.toml similarity index 70% rename from engrams/el-manifest/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-manifest/Cargo.toml index da4205f..3f0acce 100644 --- a/engrams/el-manifest/Cargo.toml +++ b/_archive/rust-bootstrap/engrams/el-manifest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "el-manifest" -description = "el.toml manifest parser for the Engram language package system" +description = "manifest.el project manifest parser for the Engram language package system" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ license.workspace = true serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -toml = "0.8" semver = { version = "1", features = ["serde"] } +el-lexer = { workspace = true } [dev-dependencies] diff --git a/engrams/el-manifest/src/error.rs b/_archive/rust-bootstrap/engrams/el-manifest/src/error.rs similarity index 85% rename from engrams/el-manifest/src/error.rs rename to _archive/rust-bootstrap/engrams/el-manifest/src/error.rs index c3f2d3b..0ac9b57 100644 --- a/engrams/el-manifest/src/error.rs +++ b/_archive/rust-bootstrap/engrams/el-manifest/src/error.rs @@ -7,8 +7,8 @@ pub enum ManifestError { #[error("io error: {0}")] Io(#[from] std::io::Error), - #[error("toml parse error: {0}")] - Toml(#[from] toml::de::Error), + #[error("manifest parse error at line {line}: {reason}")] + Parse { line: u32, reason: String }, #[error("semver parse error for '{field}': {source}")] Semver { @@ -23,7 +23,7 @@ pub enum ManifestError { #[error("invalid value for '{field}': {reason}")] InvalidValue { field: String, reason: String }, - #[error("el.toml not found (searched from {0})")] + #[error("manifest.el not found (searched from {0})")] NotFound(String), #[error("invalid cross target '{0}': use x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos, wasm32")] diff --git a/_archive/rust-bootstrap/engrams/el-manifest/src/lib.rs b/_archive/rust-bootstrap/engrams/el-manifest/src/lib.rs new file mode 100644 index 0000000..29ef9fd --- /dev/null +++ b/_archive/rust-bootstrap/engrams/el-manifest/src/lib.rs @@ -0,0 +1,28 @@ +//! el-manifest — `manifest.el` project manifest parser. +//! +//! Every Engram project has a `manifest.el` at its root. This crate defines the +//! manifest data model and parses it from El block syntax. +//! +//! # Quick start +//! ```rust +//! use el_manifest::Manifest; +//! +//! let src = r#" +//! package "my-service" { +//! version "0.1.0" +//! edition "2026" +//! } +//! "#; +//! let manifest = Manifest::parse(src).unwrap(); +//! assert_eq!(manifest.package.name, "my-service"); +//! ``` + +mod error; +mod manifest; +mod parse; + +pub use error::{ManifestError, ManifestResult}; +pub use manifest::{ + AppConfig, BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, + NativeTarget, PackageInfo, SealKeySource, +}; diff --git a/engrams/el-manifest/src/manifest.rs b/_archive/rust-bootstrap/engrams/el-manifest/src/manifest.rs similarity index 93% rename from engrams/el-manifest/src/manifest.rs rename to _archive/rust-bootstrap/engrams/el-manifest/src/manifest.rs index b05c7c5..db9763a 100644 --- a/engrams/el-manifest/src/manifest.rs +++ b/_archive/rust-bootstrap/engrams/el-manifest/src/manifest.rs @@ -1,4 +1,4 @@ -//! Core data model for the `el.toml` manifest. +//! Core data model for the `manifest.el` manifest. use std::collections::HashMap; use std::path::PathBuf; @@ -326,9 +326,21 @@ impl std::fmt::Display for NativeTarget { } } +// ── App config (native desktop) ─────────────────────────────────────────────── + +/// The `app { }` section — present only in native desktop apps using el-ui. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct AppConfig { + pub window_title: String, + pub window_width: u32, + pub window_height: u32, + pub min_width: u32, + pub min_height: u32, +} + // ── Top-level manifest ──────────────────────────────────────────────────────── -/// The parsed contents of an `el.toml` project manifest. +/// The parsed contents of a `manifest.el` project manifest. #[derive(Debug, Clone)] pub struct Manifest { pub package: PackageInfo, @@ -338,10 +350,12 @@ pub struct Manifest { pub cross: CrossConfig, /// Plugin name → version requirement string. pub plugins: HashMap, + /// Native desktop app config (present only when `app { }` section exists). + pub app: Option, } impl Manifest { - /// Parse a manifest from a TOML string. + /// Parse a manifest from an El source string. pub fn parse(s: &str) -> crate::ManifestResult { crate::parse::parse_manifest(s) } @@ -352,7 +366,7 @@ impl Manifest { Self::parse(&text) } - /// Walk up the directory tree from `from` until an `el.toml` is found. + /// Walk up the directory tree from `from` until a `manifest.el` is found. /// /// Returns the path to the manifest file (not its directory). pub fn find_manifest(from: &std::path::Path) -> crate::ManifestResult { @@ -363,7 +377,7 @@ impl Manifest { }; loop { - let candidate = dir.join("el.toml"); + let candidate = dir.join("manifest.el"); if candidate.exists() { return Ok(candidate); } diff --git a/_archive/rust-bootstrap/engrams/el-manifest/src/parse.rs b/_archive/rust-bootstrap/engrams/el-manifest/src/parse.rs new file mode 100644 index 0000000..0bab906 --- /dev/null +++ b/_archive/rust-bootstrap/engrams/el-manifest/src/parse.rs @@ -0,0 +1,748 @@ +//! El-syntax parser for `manifest.el` project manifests. +//! +//! The manifest format is a subset of El block syntax: +//! +//! ```el +//! package "name" { +//! version "0.1.0" +//! description "What this does" +//! authors ["Author One"] +//! edition "2026" +//! } +//! +//! build { +//! entry "src/main.el" +//! target "release" +//! output "dist/" +//! } +//! +//! dependencies { +//! el-ui { path "../../../../foundation/el-ui" } +//! } +//! +//! app { +//! window_title "My App" +//! window_width 1200 +//! window_height 800 +//! } +//! ``` +//! +//! Rules: +//! - No equals signs — space-separated declarations +//! - String values use `"..."`, integer values are bare numbers +//! - Arrays use `[...]`, block sections use `{ }` +//! - `//` line comments are stripped by the lexer + +use std::collections::HashMap; +use std::path::PathBuf; + +use el_lexer::{tokenize, Token, Spanned}; + +use crate::error::{ManifestError, ManifestResult}; +use crate::manifest::{ + AppConfig, BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, + PackageInfo, SealKeySource, +}; + +// ── Token stream wrapper ────────────────────────────────────────────────────── + +struct TokenStream { + tokens: Vec>, + pos: usize, +} + +impl TokenStream { + fn new(tokens: Vec>) -> Self { + Self { tokens, pos: 0 } + } + + fn peek(&self) -> &Token { + self.tokens.get(self.pos).map(|s| &s.node).unwrap_or(&Token::Eof) + } + + fn current_line(&self) -> u32 { + self.tokens.get(self.pos).map(|s| s.span.line).unwrap_or(0) + } + + fn advance(&mut self) -> &Spanned { + let idx = self.pos; + self.pos += 1; + &self.tokens[idx] + } + + fn expect_lbrace(&mut self) -> ManifestResult<()> { + match self.peek() { + Token::LBrace => { self.advance(); Ok(()) } + other => Err(ManifestError::Parse { + line: self.current_line(), + reason: format!("expected '{{', found '{other}'"), + }), + } + } + + fn expect_rbrace(&mut self) -> ManifestResult<()> { + match self.peek() { + Token::RBrace => { self.advance(); Ok(()) } + other => Err(ManifestError::Parse { + line: self.current_line(), + reason: format!("expected '}}', found '{other}'"), + }), + } + } + + /// Consume an identifier or return error. + fn expect_ident(&mut self) -> ManifestResult { + match self.peek().clone() { + Token::Ident(s) => { self.advance(); Ok(s) } + // Some fields like "target" are keywords in El — allow them as idents here + Token::Target => { self.advance(); Ok("target".to_string()) } + other => Err(ManifestError::Parse { + line: self.current_line(), + reason: format!("expected identifier, found '{other}'"), + }), + } + } + + /// Consume a string literal or return error. + fn expect_string(&mut self) -> ManifestResult { + match self.peek().clone() { + Token::StringLiteral(s) => { self.advance(); Ok(s) } + other => Err(ManifestError::Parse { + line: self.current_line(), + reason: format!("expected string literal, found '{other}'"), + }), + } + } + + /// Consume an integer literal or return error. + fn expect_int(&mut self) -> ManifestResult { + match self.peek().clone() { + Token::IntLiteral(n) => { self.advance(); Ok(n) } + other => Err(ManifestError::Parse { + line: self.current_line(), + reason: format!("expected integer literal, found '{other}'"), + }), + } + } + + fn is_at_end(&self) -> bool { + matches!(self.peek(), Token::Eof) + } +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +/// Parse a `manifest.el` source string into a [`Manifest`]. +pub(crate) fn parse_manifest(s: &str) -> ManifestResult { + let spanned = tokenize(s).map_err(|e| ManifestError::Parse { + line: e.span.line, + reason: format!("lex error: {}", e.kind), + })?; + + // Filter out comments and whitespace-only tokens (the lexer doesn't produce + // whitespace tokens, but Unknown chars from `//` comments might appear). + let tokens: Vec> = spanned + .into_iter() + .filter(|t| !matches!(t.node, Token::Eof)) + .collect(); + + let mut stream = TokenStream::new(tokens); + + let mut raw_package: Option = None; + let mut raw_build: Option = None; + let mut raw_deps: HashMap = HashMap::new(); + let mut raw_cross: Option = None; + let mut raw_app: Option = None; + + while !stream.is_at_end() { + let section_name = match stream.peek().clone() { + Token::Ident(s) => { stream.advance(); s } + Token::Eof => break, + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected section name, found '{other}'"), + }); + } + }; + + match section_name.as_str() { + "package" => { + // package "name" { ... } + let name = stream.expect_string()?; + stream.expect_lbrace()?; + raw_package = Some(parse_package_block(&mut stream, name)?); + stream.expect_rbrace()?; + } + "build" => { + stream.expect_lbrace()?; + raw_build = Some(parse_build_block(&mut stream)?); + stream.expect_rbrace()?; + } + "dependencies" => { + stream.expect_lbrace()?; + raw_deps = parse_deps_block(&mut stream)?; + stream.expect_rbrace()?; + } + "cross" => { + stream.expect_lbrace()?; + raw_cross = Some(parse_cross_block(&mut stream)?); + stream.expect_rbrace()?; + } + "app" => { + stream.expect_lbrace()?; + raw_app = Some(parse_app_block(&mut stream)?); + stream.expect_rbrace()?; + } + other => { + // Skip unknown sections gracefully by consuming until matching } + if matches!(stream.peek(), Token::LBrace) { + stream.advance(); + skip_block(&mut stream)?; + } else { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown section '{other}'"), + }); + } + } + } + } + + convert(raw_package, raw_build, raw_deps, raw_cross, raw_app) +} + +// ── Block parsers ───────────────────────────────────────────────────────────── + +#[derive(Debug, Default)] +struct RawPackage { + name: String, + version: String, + description: Option, + authors: Vec, + license: Option, + edition: String, + entry: Option, // neuron-code puts entry in [package] +} + +fn parse_package_block(stream: &mut TokenStream, name: String) -> ManifestResult { + let mut pkg = RawPackage { + name, + version: "0.1.0".to_string(), + edition: "2026".to_string(), + ..Default::default() + }; + + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + let field = stream.expect_ident()?; + match field.as_str() { + "version" => pkg.version = stream.expect_string()?, + "description" => pkg.description = Some(stream.expect_string()?), + "authors" => pkg.authors = parse_string_array(stream)?, + "license" => pkg.license = Some(stream.expect_string()?), + "edition" => pkg.edition = stream.expect_string()?, + "entry" => pkg.entry = Some(stream.expect_string()?), + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown package field '{other}'"), + }); + } + } + } + + Ok(pkg) +} + +#[derive(Debug, Default)] +struct RawBuild { + target: Option, + entry: Option, + output: Option, + seal_key: Option, +} + +fn parse_build_block(stream: &mut TokenStream) -> ManifestResult { + let mut build = RawBuild::default(); + + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + let field = match stream.peek().clone() { + Token::Ident(s) => { stream.advance(); s } + Token::Target => { stream.advance(); "target".to_string() } + Token::RBrace | Token::Eof => break, + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected build field name, found '{other}'"), + }); + } + }; + + match field.as_str() { + "target" => build.target = Some(stream.expect_string()?), + "entry" => build.entry = Some(stream.expect_string()?), + "output" => build.output = Some(stream.expect_string()?), + "seal_key" => build.seal_key = Some(stream.expect_string()?), + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown build field '{other}'"), + }); + } + } + } + + Ok(build) +} + +#[derive(Debug)] +enum RawDep { + Path(String), + Version(String), +} + +fn parse_deps_block(stream: &mut TokenStream) -> ManifestResult> { + let mut deps = HashMap::new(); + + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + // Dependency name can contain hyphens — the lexer will produce Ident tokens + // separated by Minus for names like `el-ui`. We need to reconstruct the full name. + let dep_name = parse_dep_name(stream)?; + + // Value is either a string version or a block `{ path "..." }` + match stream.peek().clone() { + Token::StringLiteral(ver) => { + stream.advance(); + deps.insert(dep_name, RawDep::Version(ver)); + } + Token::LBrace => { + stream.advance(); + // Parse inner fields until } + let mut path: Option = None; + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + let field = stream.expect_ident()?; + match field.as_str() { + "path" => path = Some(stream.expect_string()?), + other => { + // Skip unknown fields (version, registry, etc.) + match stream.peek().clone() { + Token::StringLiteral(_) => { stream.advance(); } + _ => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown dep field '{other}'"), + }); + } + } + } + } + } + stream.expect_rbrace()?; + match path { + Some(p) => { deps.insert(dep_name, RawDep::Path(p)); } + None => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("dependency '{dep_name}' block has no 'path' or 'version'"), + }); + } + } + } + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected string or block for dependency '{dep_name}', found '{other}'"), + }); + } + } + } + + Ok(deps) +} + +/// Parse a possibly-hyphenated dependency name like `el-ui` or `engram-http`. +/// The lexer produces Ident("-") — actually Minus tokens — so we need to stitch +/// Ident + (Minus + Ident)* together. +fn parse_dep_name(stream: &mut TokenStream) -> ManifestResult { + let mut name = match stream.peek().clone() { + Token::Ident(s) => { stream.advance(); s } + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected dependency name, found '{other}'"), + }); + } + }; + + // Greedily consume `-ident` segments + loop { + match stream.peek() { + Token::Minus => { + // Peek ahead to see if next is Ident + let next_pos = stream.pos + 1; + if let Some(next) = stream.tokens.get(next_pos) { + if let Token::Ident(_) = &next.node { + stream.advance(); // consume Minus + if let Token::Ident(seg) = stream.peek().clone() { + stream.advance(); + name.push('-'); + name.push_str(&seg); + continue; + } + } + } + break; + } + _ => break, + } + } + + Ok(name) +} + +#[derive(Debug, Default)] +struct RawCross { + targets: Vec, +} + +fn parse_cross_block(stream: &mut TokenStream) -> ManifestResult { + let mut cross = RawCross::default(); + + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + let field = match stream.peek().clone() { + Token::Ident(s) => { stream.advance(); s } + Token::Target => { stream.advance(); "targets".to_string() } + Token::RBrace | Token::Eof => break, + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected cross field, found '{other}'"), + }); + } + }; + + match field.as_str() { + "targets" => cross.targets = parse_string_array(stream)?, + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown cross field '{other}'"), + }); + } + } + } + + Ok(cross) +} + +#[derive(Debug, Default)] +struct RawApp { + window_title: Option, + window_width: Option, + window_height: Option, + min_width: Option, + min_height: Option, +} + +fn parse_app_block(stream: &mut TokenStream) -> ManifestResult { + let mut app = RawApp::default(); + + while !matches!(stream.peek(), Token::RBrace | Token::Eof) { + let field = stream.expect_ident()?; + match field.as_str() { + "window_title" => app.window_title = Some(stream.expect_string()?), + "window_width" => app.window_width = Some(stream.expect_int()?), + "window_height" => app.window_height = Some(stream.expect_int()?), + "min_width" => app.min_width = Some(stream.expect_int()?), + "min_height" => app.min_height = Some(stream.expect_int()?), + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("unknown app field '{other}'"), + }); + } + } + } + + Ok(app) +} + +fn parse_string_array(stream: &mut TokenStream) -> ManifestResult> { + match stream.peek() { + Token::LBracket => { stream.advance(); } + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected '[', found '{other}'"), + }); + } + } + + let mut items = Vec::new(); + + while !matches!(stream.peek(), Token::RBracket | Token::Eof) { + // Items in the array are strings + match stream.peek().clone() { + Token::StringLiteral(s) => { + stream.advance(); + items.push(s); + // Optional comma + if matches!(stream.peek(), Token::Comma) { + stream.advance(); + } + } + Token::Comma => { stream.advance(); } // trailing comma + other => { + return Err(ManifestError::Parse { + line: stream.current_line(), + reason: format!("expected string in array, found '{other}'"), + }); + } + } + } + + match stream.peek() { + Token::RBracket => { stream.advance(); } + _ => {} // Eof handled gracefully + } + + Ok(items) +} + +/// Skip over a `{ ... }` block (already consumed the opening brace). +fn skip_block(stream: &mut TokenStream) -> ManifestResult<()> { + let mut depth = 1; + while depth > 0 && !stream.is_at_end() { + match stream.peek() { + Token::LBrace => { depth += 1; stream.advance(); } + Token::RBrace => { depth -= 1; stream.advance(); } + _ => { stream.advance(); } + } + } + Ok(()) +} + +// ── Conversion to typed manifest ────────────────────────────────────────────── + +fn convert( + raw_package: Option, + raw_build: Option, + raw_deps: HashMap, + raw_cross: Option, + raw_app: Option, +) -> ManifestResult { + let pkg_raw = raw_package.ok_or_else(|| ManifestError::MissingField("package".to_string()))?; + let package = convert_package(pkg_raw)?; + + let build = convert_build(raw_build.unwrap_or_default(), &package)?; + let dependencies = convert_deps(raw_deps)?; + let cross = convert_cross(raw_cross.unwrap_or_default())?; + let app = raw_app.map(convert_app); + + Ok(Manifest { + package, + dependencies, + dev_dependencies: HashMap::new(), + build, + cross, + plugins: HashMap::new(), + app, + }) +} + +fn convert_package(raw: RawPackage) -> ManifestResult { + let version = semver::Version::parse(&raw.version).map_err(|e| ManifestError::Semver { + field: "package.version".to_string(), + source: e, + })?; + + Ok(PackageInfo { + name: raw.name, + version, + description: raw.description, + authors: raw.authors, + license: raw.license, + edition: raw.edition, + }) +} + +fn convert_build(raw: RawBuild, _package: &PackageInfo) -> ManifestResult { + let target = raw + .target + .unwrap_or_else(|| "debug".to_string()) + .parse::()?; + + // Entry can come from build block or package block (legacy neuron-code style) + let entry_str = raw.entry.unwrap_or_else(|| "src/main.el".to_string()); + let output_str = raw.output.unwrap_or_else(|| "dist/".to_string()); + + let seal_key = raw + .seal_key + .map(|s| SealKeySource::parse(&s)) + .transpose()?; + + Ok(BuildConfig { + target, + entry: PathBuf::from(entry_str), + output: PathBuf::from(output_str), + seal_key, + }) +} + +fn convert_deps(raw: HashMap) -> ManifestResult> { + let mut out = HashMap::new(); + for (name, dep) in raw { + let converted = match dep { + RawDep::Path(p) => Dependency::Path(PathBuf::from(p)), + RawDep::Version(v) => { + let req = semver::VersionReq::parse(&v).map_err(|e| ManifestError::Semver { + field: format!("dependencies.{name}"), + source: e, + })?; + Dependency::VersionReq(req) + } + }; + out.insert(name, converted); + } + Ok(out) +} + +fn convert_cross(raw: RawCross) -> ManifestResult { + let targets = raw + .targets + .iter() + .map(|s| CrossTarget::parse(s)) + .collect::>>()?; + Ok(CrossConfig { targets }) +} + +fn convert_app(raw: RawApp) -> AppConfig { + AppConfig { + window_title: raw.window_title.unwrap_or_default(), + window_width: raw.window_width.unwrap_or(1024) as u32, + window_height: raw.window_height.unwrap_or(768) as u32, + min_width: raw.min_width.unwrap_or(400) as u32, + min_height: raw.min_height.unwrap_or(300) as u32, + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::manifest::{BuildTarget, CrossTarget, Dependency}; + + fn full_manifest() -> &'static str { + r#" +// manifest.el — full example +package "my-service" { + version "0.1.0" + description "What this does" + authors ["Will Anderson "] + license "MIT" + edition "2026" +} + +build { + target "prod" + entry "src/main.el" + output "dist/" + seal_key "env:ENGRAM_SEAL_KEY" +} + +dependencies { + el-ui { path "../el-ui" } +} + +cross { + targets ["x86_64-linux", "aarch64-linux", "x86_64-macos", "aarch64-macos", "wasm32"] +} + +app { + window_title "My App" + window_width 1200 + window_height 800 + min_width 800 + min_height 600 +} +"# + } + + #[test] + fn test_parse_full_manifest() { + let m = parse_manifest(full_manifest()).unwrap(); + assert_eq!(m.package.name, "my-service"); + assert_eq!(m.package.version.to_string(), "0.1.0"); + assert_eq!(m.package.edition, "2026"); + assert_eq!(m.package.license.as_deref(), Some("MIT")); + assert_eq!(m.package.authors, vec!["Will Anderson "]); + } + + #[test] + fn test_parse_build_config() { + let m = parse_manifest(full_manifest()).unwrap(); + assert_eq!(m.build.target, BuildTarget::Prod); + assert_eq!(m.build.entry.to_str().unwrap(), "src/main.el"); + assert_eq!(m.build.output.to_str().unwrap(), "dist/"); + match &m.build.seal_key { + Some(SealKeySource::EnvVar(v)) => assert_eq!(v, "ENGRAM_SEAL_KEY"), + other => panic!("expected EnvVar seal_key, got {other:?}"), + } + } + + #[test] + fn test_parse_path_dep() { + let m = parse_manifest(full_manifest()).unwrap(); + assert!(m.dependencies.contains_key("el-ui")); + match &m.dependencies["el-ui"] { + Dependency::Path(p) => assert_eq!(p.to_str().unwrap(), "../el-ui"), + other => panic!("expected Path dep, got {other:?}"), + } + } + + #[test] + fn test_parse_cross_targets() { + let m = parse_manifest(full_manifest()).unwrap(); + assert_eq!(m.cross.targets.len(), 5); + assert!(m.cross.targets.contains(&CrossTarget::X86_64Linux)); + assert!(m.cross.targets.contains(&CrossTarget::Wasm32)); + } + + #[test] + fn test_parse_app_config() { + let m = parse_manifest(full_manifest()).unwrap(); + let app = m.app.as_ref().expect("app config missing"); + assert_eq!(app.window_title, "My App"); + assert_eq!(app.window_width, 1200); + assert_eq!(app.window_height, 800); + assert_eq!(app.min_width, 800); + assert_eq!(app.min_height, 600); + } + + #[test] + fn test_minimal_manifest() { + let src = r#" +package "hello" { + version "0.1.0" +} +"#; + let m = parse_manifest(src).unwrap(); + assert_eq!(m.package.name, "hello"); + assert_eq!(m.package.edition, "2026"); + assert_eq!(m.build.target, BuildTarget::Debug); + assert!(m.dependencies.is_empty()); + assert!(m.cross.targets.is_empty()); + } + + #[test] + fn test_missing_package_error() { + let src = r#" +build { + entry "src/main.el" +} +"#; + let err = parse_manifest(src).unwrap_err(); + assert!(err.to_string().contains("package"), "expected package error, got: {err}"); + } +} diff --git a/engrams/el-parser/Cargo.toml b/_archive/rust-bootstrap/engrams/el-parser/Cargo.toml similarity index 100% rename from engrams/el-parser/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-parser/Cargo.toml diff --git a/engrams/el-parser/src/ast.rs b/_archive/rust-bootstrap/engrams/el-parser/src/ast.rs similarity index 86% rename from engrams/el-parser/src/ast.rs rename to _archive/rust-bootstrap/engrams/el-parser/src/ast.rs index 68da41e..9ae091e 100644 --- a/engrams/el-parser/src/ast.rs +++ b/_archive/rust-bootstrap/engrams/el-parser/src/ast.rs @@ -84,12 +84,13 @@ pub enum BinOp { Add, Sub, Mul, Div, Eq, NotEq, Lt, Gt, LtEq, GtEq, And, Or, - Mod, // % - BitAnd, // & - BitOr, // | (single pipe) - BitXor, // ^ - Shl, // << - Shr, // >> + Mod, // % + BitAnd, // & + BitOr, // | (single pipe) + BitXor, // ^ + Shl, // << + Shr, // >> + NullCoalesce, // ?? } // ── Unary operators ─────────────────────────────────────────────────────────── @@ -160,6 +161,26 @@ pub enum Expr { label: String, body: Vec, }, + /// A JSX element: `{children}` or `` + JsxElement { + tag: String, + attrs: Vec<(String, JsxAttrValue)>, + children: Vec, + self_closing: bool, + }, + /// A JSX expression interpolation: `{expr}` + JsxExpr(Box), + /// Plain text content inside a JSX element. + JsxText(String), +} + +/// The value of a JSX attribute. +#[derive(Debug, Clone, PartialEq)] +pub enum JsxAttrValue { + /// `attr="literal"` — a string literal + Str(String), + /// `attr={expr}` — an expression + Expr(Box), } // ── Match arm ───────────────────────────────────────────────────────────────── @@ -308,6 +329,29 @@ pub enum Stmt { target: String, span: Span, }, + /// `component Name { props { ... } state { ... } fn ... template { ... } }` + ComponentDef { + name: String, + /// Fields declared in the `props { }` block: (name, type_ann, default_expr) + props: Vec, + /// Fields declared in the `state { }` block: (name, type_ann, default_expr) + state: Vec, + /// Methods declared inside the component: plain `fn` definitions. + methods: Vec, + /// The JSX tree inside `template { }`. + template: Box, + span: Span, + }, +} + +/// A field in a component `props` or `state` block. +#[derive(Debug, Clone, PartialEq)] +pub struct ComponentField { + pub name: String, + pub type_ann: TypeExpr, + /// Default value expression (may be a Literal::Str("") placeholder for required props). + pub default: Option>, + pub span: Span, } // ── Top-level program ───────────────────────────────────────────────────────── diff --git a/engrams/el-parser/src/error.rs b/_archive/rust-bootstrap/engrams/el-parser/src/error.rs similarity index 100% rename from engrams/el-parser/src/error.rs rename to _archive/rust-bootstrap/engrams/el-parser/src/error.rs diff --git a/engrams/el-parser/src/lib.rs b/_archive/rust-bootstrap/engrams/el-parser/src/lib.rs similarity index 69% rename from engrams/el-parser/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-parser/src/lib.rs index a12ee4c..b6e138d 100644 --- a/engrams/el-parser/src/lib.rs +++ b/_archive/rust-bootstrap/engrams/el-parser/src/lib.rs @@ -11,8 +11,8 @@ mod error; mod parser; pub use ast::{ - BinOp, UnaryOp, Decorator, Expr, Field, Literal, MatchArm, Param, Pattern, Program, ProtocolMethod, - SeedStmt, Stmt, TestTarget, TypeExpr, Variant, + BinOp, UnaryOp, ComponentField, Decorator, Expr, Field, JsxAttrValue, Literal, MatchArm, + Param, Pattern, Program, ProtocolMethod, SeedStmt, Stmt, TestTarget, TypeExpr, Variant, }; pub use error::{ParseError, ParseErrorKind}; pub use parser::parse; diff --git a/engrams/el-parser/src/parser.rs b/_archive/rust-bootstrap/engrams/el-parser/src/parser.rs similarity index 68% rename from engrams/el-parser/src/parser.rs rename to _archive/rust-bootstrap/engrams/el-parser/src/parser.rs index 089215b..69c47cd 100644 --- a/engrams/el-parser/src/parser.rs +++ b/_archive/rust-bootstrap/engrams/el-parser/src/parser.rs @@ -5,6 +5,10 @@ use el_lexer::{Span, Spanned, Token}; use crate::ast::*; use crate::error::{ParseError, ParseErrorKind}; +// ── For keyword in impl Statements ──────────────────────────────────────────── +// The `for` keyword is re-used in `impl Protocol for TypeName`. +// (Token::For already exists in el_lexer.) + /// Parse a token stream into a [`Program`]. /// /// The `source` string is stored verbatim in the returned program for @@ -124,6 +128,10 @@ impl Parser { Token::Deploy => "deploy".to_string(), Token::To => "to".to_string(), Token::Via => "via".to_string(), + Token::Component => "component".to_string(), + Token::Props => "props".to_string(), + Token::State => "state".to_string(), + Token::Template => "template".to_string(), tok => return Err(ParseError::new( ParseErrorKind::ExpectedIdent(tok.to_string()), span, @@ -164,7 +172,22 @@ impl Parser { match self.peek().clone() { Token::Let => self.parse_let(start), Token::Fn => self.parse_fn_def(start, vec![]), - Token::At => self.parse_decorated_fn(start), + Token::At => { + // Look ahead: `@name fn` or `@name @` → decorated function definition. + // `@name(` or `@name` followed by anything else → builtin call expression. + // Peek 2 tokens ahead: current=@, next=ident, then after-ident + let after_ident = self.tokens.get(self.pos + 2).map(|s| &s.node); + let is_decorator = matches!(after_ident, Some(Token::Fn) | Some(Token::At)); + if is_decorator { + self.parse_decorated_fn(start) + } else { + // Parse as expression statement (builtin call) + let expr = self.parse_expr()?; + let span = start; + self.eat(&Token::Semicolon); + Ok(Stmt::Expr(expr, span)) + } + } Token::Type => self.parse_type_def(start), Token::Enum => self.parse_enum_def(start), Token::Test => self.parse_test_def(start), @@ -177,14 +200,26 @@ impl Parser { Token::While => self.parse_while(start), Token::Retry => self.parse_retry(start), Token::Deploy => self.parse_deploy(start), + Token::Component => self.parse_component_def(start), Token::Return => { self.advance(); // consume `return` - let expr = self.parse_expr()?; + // `return` with no value (bare return) is valid — treat as return (). + // Detect by checking if the next token can start an expression. + let expr = if matches!(self.peek(), Token::RBrace | Token::Semicolon | Token::Eof) || !is_expr_start(self.peek()) { + Expr::Block(vec![]) // empty block evaluates to Nil + } else { + self.parse_expr()? + }; let span = Span::new(start.start, expr_span_end(&expr, start), start.line, start.col); self.eat(&Token::Semicolon); Ok(Stmt::Return(expr, span)) } _ => { + // Check for assignment: `ident = expr` or `ident.field = expr` + // This handles state mutation inside components. + if let Some(assign_stmt) = self.try_parse_assignment(start)? { + return Ok(assign_stmt); + } let expr = self.parse_expr()?; let span = start; self.eat(&Token::Semicolon); @@ -193,6 +228,33 @@ impl Parser { } } + /// Try to parse `ident = expr` or `ident.field = expr` as an assignment statement. + /// Returns None if the pattern doesn't match (restores position). + fn try_parse_assignment(&mut self, start: Span) -> Result, ParseError> { + let saved = self.pos; + // Only try if current token is an identifier + let name = match self.peek().clone() { + Token::Ident(n) => n, + _ => return Ok(None), + }; + self.advance(); + // Check for direct assignment: `name =` + if self.eat(&Token::Eq) { + // `name = expr` — compile as `let name = expr` (reassignment) + let value = self.parse_expr()?; + self.eat(&Token::Semicolon); + return Ok(Some(Stmt::Let { + name, + type_ann: None, + value, + span: start, + })); + } + // Restore position — not an assignment + self.pos = saved; + Ok(None) + } + fn parse_test_def(&mut self, start: Span) -> Result { self.expect(&Token::Test)?; // test name is a string literal @@ -562,6 +624,322 @@ impl Parser { Ok(Stmt::Deploy { fn_name, route, target, span: start }) } + /// Parse `component Name { props { ... } state { ... } fn ...* template { ... } }` + fn parse_component_def(&mut self, start: Span) -> Result { + self.expect(&Token::Component)?; + let (name, _) = self.expect_ident()?; + self.expect(&Token::LBrace)?; + + let mut props: Vec = Vec::new(); + let mut state: Vec = Vec::new(); + let mut methods: Vec = Vec::new(); + let mut template: Expr = Expr::JsxElement { + tag: "div".to_string(), + attrs: vec![], + children: vec![], + self_closing: false, + }; + + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + while self.eat(&Token::Semicolon) {} + if matches!(self.peek(), Token::RBrace | Token::Eof) { break; } + + match self.peek().clone() { + Token::Props => { + self.advance(); // consume `props` + self.expect(&Token::LBrace)?; + props = self.parse_component_fields()?; + self.expect(&Token::RBrace)?; + } + Token::State => { + self.advance(); // consume `state` + self.expect(&Token::LBrace)?; + state = self.parse_component_fields()?; + self.expect(&Token::RBrace)?; + } + Token::Template => { + self.advance(); // consume `template` + self.expect(&Token::LBrace)?; + template = self.parse_jsx_content()?; + self.expect(&Token::RBrace)?; + } + Token::Fn => { + let method_start = self.peek_span(); + let m = self.parse_fn_def(method_start, vec![])?; + methods.push(m); + } + Token::At => { + let method_start = self.peek_span(); + let m = self.parse_decorated_fn(method_start)?; + methods.push(m); + } + _ => { + // Skip unknown tokens inside component block gracefully + self.advance(); + } + } + } + + self.expect(&Token::RBrace)?; + Ok(Stmt::ComponentDef { name, props, state, methods, template: Box::new(template), span: start }) + } + + /// Parse a list of component fields (for props/state blocks): + /// `field_name: TypeExpr = default_expr` + fn parse_component_fields(&mut self) -> Result, ParseError> { + let mut fields = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + while self.eat(&Token::Semicolon) {} + if matches!(self.peek(), Token::RBrace | Token::Eof) { break; } + + let span = self.peek_span(); + let (fname, _) = self.expect_ident()?; + self.expect(&Token::Colon)?; + let type_ann = self.parse_type_expr()?; + let default = if self.eat(&Token::Eq) { + Some(Box::new(self.parse_expr()?)) + } else { + None + }; + fields.push(ComponentField { name: fname, type_ann, default, span }); + self.eat(&Token::Comma); + self.eat(&Token::Semicolon); + } + Ok(fields) + } + + /// Parse JSX content inside a template block: a sequence of JSX nodes. + /// Returns the first/root JSX node (or a wrapping fragment if multiple). + fn parse_jsx_content(&mut self) -> Result { + let mut children = self.parse_jsx_children()?; + if children.len() == 1 { + Ok(children.remove(0)) + } else { + // Wrap in a fragment div + Ok(Expr::JsxElement { + tag: "fragment".to_string(), + attrs: vec![], + children, + self_closing: false, + }) + } + } + + /// Parse a sequence of JSX children until we hit `}` (for template blocks) + /// or ` Result, ParseError> { + let mut children = Vec::new(); + loop { + match self.peek().clone() { + // End of template block or EOF + Token::RBrace | Token::Eof => break, + // JSX expression interpolation: {expr} + Token::LBrace => { + self.advance(); // consume `{` + // Check if this is just a closing brace (empty) - unlikely but safe + if matches!(self.peek(), Token::RBrace) { + self.advance(); + continue; + } + let expr = self.parse_expr()?; + // Consume closing `}` only if it's there + // (for map/block literals inside JSX, the expr parser may consume it) + if matches!(self.peek(), Token::RBrace) { + self.advance(); + } + children.push(Expr::JsxExpr(Box::new(expr))); + } + // Opening tag: ` { + // Check if it's a closing tag ` { + self.advance(); + // Always emit "#" as text; the next token (if `{`) becomes a JsxExpr + children.push(Expr::JsxText("#".to_string())); + } + // Unknown character (non-ASCII, emoji, etc.) in JSX text — emit as text + Token::Unknown(c) => { + let text = c.to_string(); + self.advance(); + children.push(Expr::JsxText(text)); + } + // Anything else: treat as text (skip unknown tokens) + _ => { + self.advance(); + } + } + } + Ok(children) + } + + /// Parse a JSX element: `children` or `` + fn parse_jsx_element(&mut self) -> Result { + self.expect(&Token::Lt)?; // consume `<` + + // Tag name: could be `Ident` (uppercase for components, lowercase for HTML tags) + let tag = match self.peek().clone() { + Token::Ident(name) => { self.advance(); name } + tok => return Err(ParseError::expected("tag name", &tok, self.peek_span())), + }; + + // Parse attributes until `>` or `/>` + let mut attrs = Vec::new(); + loop { + match self.peek().clone() { + // End of opening tag + Token::Gt => { + self.advance(); // consume `>` + break; + } + // Self-closing: `/>` + Token::Slash => { + self.advance(); // consume `/` + self.expect(&Token::Gt)?; // consume `>` + return Ok(Expr::JsxElement { + tag, + attrs, + children: vec![], + self_closing: true, + }); + } + Token::Eof => break, + // Attribute name: could be identifier or `on:click` style (ident:ident) + Token::Ident(attr_name) => { + self.advance(); + // Check for `on:click` style (colon-qualified event name) + let full_attr_name = if matches!(self.peek(), Token::Colon) { + self.advance(); // consume `:` + let (event_name, _) = self.expect_ident()?; + format!("{}:{}", attr_name, event_name) + } else { + attr_name + }; + // Attribute value: `="literal"` or `={expr}` + if self.eat(&Token::Eq) { + let attr_val = match self.peek().clone() { + Token::StringLiteral(s) => { + self.advance(); + JsxAttrValue::Str(s) + } + Token::LBrace => { + self.advance(); // consume `{` + let expr = self.parse_expr()?; + self.expect(&Token::RBrace)?; + JsxAttrValue::Expr(Box::new(expr)) + } + tok => return Err(ParseError::expected("string literal or {expr}", &tok, self.peek_span())), + }; + attrs.push((full_attr_name, attr_val)); + } else { + // Boolean attribute (no value) + attrs.push((full_attr_name, JsxAttrValue::Str("true".to_string()))); + } + } + _ => { + // Skip unknown tokens in attribute position + self.advance(); + } + } + } + + // Now parse children until `` + let children = self.parse_jsx_children()?; + + // Consume closing tag: `` + if matches!(self.peek(), Token::Lt) { + self.advance(); // consume `<` + if self.eat(&Token::Slash) { + // Consume tag name (we don't validate it matches) + if matches!(self.peek(), Token::Ident(_)) { + self.advance(); + } + // Consume `>` + let _ = self.eat(&Token::Gt); + } + } + + Ok(Expr::JsxElement { + tag, + attrs, + children, + self_closing: false, + }) + } + + /// Try to parse `(ident, ident, ...) => expr` arrow function. + /// If the pattern doesn't match, restore position and return None. + /// Precondition: `(` has already been consumed. + fn try_parse_arrow_fn(&mut self, span: Span) -> Result, ParseError> { + let saved_pos = self.pos; + let mut params: Vec = Vec::new(); + + // Try to parse `ident, ident, ... )` followed by `=>` + loop { + match self.peek().clone() { + Token::Ident(name) => { + params.push(name); + self.advance(); + // After ident: could be `,` or `)` or `: Type` + if self.eat(&Token::Colon) { + // Typed param: skip the type expression by advancing past it + // Simple type: ident or ident? + if matches!(self.peek(), Token::Ident(_)) { + self.advance(); + self.eat(&Token::QuestionMark); + } + } + if self.eat(&Token::Comma) { + continue; + } + if matches!(self.peek(), Token::RParen) { + break; + } + // Not an arrow fn pattern + self.pos = saved_pos; + return Ok(None); + } + Token::RParen => break, + _ => { + self.pos = saved_pos; + return Ok(None); + } + } + } + + self.expect(&Token::RParen)?; + + // Check for `=>` + if !self.eat(&Token::FatArrow) { + self.pos = saved_pos; + return Ok(None); + } + + // Parse body + let body = self.parse_expr()?; + + let closure_params = params.into_iter().map(|name| Param { + name, + type_ann: TypeExpr::Named("_".to_string()), + span, + }).collect(); + + Ok(Some(Expr::Closure { + params: closure_params, + return_type: None, + body: Box::new(body), + span, + })) + } + #[allow(dead_code)] fn parse_param_list(&mut self) -> Result, ParseError> { self.parse_param_list_with_type_params(&[]) @@ -662,8 +1040,8 @@ impl Parser { span, )), }; - // Check for function type: fn(A) -> B - if name == "fn" { + // Check for function type: fn(A) -> B OR Fn(A) -> B + if name == "fn" || name == "Fn" { self.expect(&Token::LParen)?; let mut params = Vec::new(); while !matches!(self.peek(), Token::RParen | Token::Eof) { @@ -714,7 +1092,35 @@ impl Parser { // ── Expressions ─────────────────────────────────────────────────────────── fn parse_expr(&mut self) -> Result { - self.parse_pipe_expr() + self.parse_ternary_expr() + } + + /// Parse ternary: `cond ? then_expr : else_expr` + /// Also handles null-coalescing: `a ?? b` (returns `b` if `a` is nil/empty). + fn parse_ternary_expr(&mut self) -> Result { + let cond = self.parse_pipe_expr()?; + // Null-coalescing: `a ?? b` — desugars to a binary NullCoalesce op + if self.eat(&Token::NullCoalesce) { + let default_expr = self.parse_ternary_expr()?; + return Ok(Expr::BinOp { + op: BinOp::NullCoalesce, + left: Box::new(cond), + right: Box::new(default_expr), + }); + } + if self.eat(&Token::QuestionMark) { + // Both branches allow nested ternaries (right-associative). + // `a ? b ? c : d : e` → `a ? (b ? c : d) : e` + let then_expr = self.parse_ternary_expr()?; + self.expect(&Token::Colon)?; + let else_expr = self.parse_ternary_expr()?; + return Ok(Expr::If { + cond: Box::new(cond), + then: Box::new(then_expr), + else_: Some(Box::new(else_expr)), + }); + } + Ok(cond) } fn parse_bitwise_or_expr(&mut self) -> Result { @@ -811,7 +1217,22 @@ impl Parser { let mut left = self.parse_bitwise_or_expr()?; loop { let op = match self.peek() { - Token::Lt => BinOp::Lt, + Token::Lt => { + // Disambiguate `<` as less-than vs the start of a JSX element `` is always JSX. + let after_lt = self.tokens.get(self.pos + 1).map(|s| &s.node); + if matches!(after_lt, Some(Token::Ident(_))) { + let lt_line = self.tokens[self.pos].span.line; + let prev_line = if self.pos > 0 { self.tokens[self.pos - 1].span.line } else { lt_line }; + if lt_line != prev_line { + // `<` is on a new line — it's a JSX element, not a comparison + break; + } + } + BinOp::Lt + } Token::Gt => BinOp::Gt, Token::LtEq => BinOp::LtEq, Token::GtEq => BinOp::GtEq, @@ -864,6 +1285,15 @@ impl Parser { let inner = self.parse_unary()?; return Ok(Expr::UnaryBitNot(Box::new(inner))); } + if self.eat(&Token::Minus) { + let inner = self.parse_unary()?; + // Desugar: `-x` → `0 - x` + return Ok(Expr::BinOp { + op: BinOp::Sub, + left: Box::new(Expr::Literal(crate::ast::Literal::Int(0))), + right: Box::new(inner), + }); + } self.parse_postfix() } @@ -873,7 +1303,16 @@ impl Parser { match self.peek() { Token::Dot => { self.advance(); - let (field, _) = self.expect_ident()?; + // Allow keywords as field names (e.g. `e.target`, `e.type`, `e.key`) + let field = match self.peek().clone() { + Token::Ident(name) => { self.advance(); name } + // Any other token used as a field name (keywords, builtins) + tok => { + let name = tok.to_string(); + self.advance(); + name + } + }; expr = Expr::Field { object: Box::new(expr), field }; } Token::LParen => { @@ -889,6 +1328,14 @@ impl Parser { expr = Expr::Index { object: Box::new(expr), index: Box::new(index) }; } Token::QuestionMark => { + // Disambiguate: `?` as Try postfix vs `?` as ternary start. + // If the token AFTER `?` can start an expression, this is ternary + // and we should NOT consume it here (let parse_ternary_expr handle it). + let after_q = self.tokens.get(self.pos + 1).map(|s| &s.node); + if after_q.is_some_and(is_expr_start) { + // Ternary — stop postfix loop, bubble up to parse_ternary_expr + break; + } self.advance(); expr = Expr::Try(Box::new(expr)); } @@ -906,6 +1353,13 @@ impl Parser { self.expect(&Token::RBrace)?; expr = Expr::With { base: Box::new(expr), updates }; } + // Type cast: `expr as TypeName` — treated as a no-op cast (same value, type hint only) + Token::As => { + self.advance(); // consume `as` + // Parse the target type expression (we don't use it at runtime) + let _ = self.parse_type_expr(); + // expr remains unchanged — the cast is informational + } _ => break, } } @@ -930,11 +1384,51 @@ impl Parser { Token::StringLiteral(s) => { self.advance(); Ok(Expr::Literal(Literal::Str(s))) } Token::BoolLiteral(b) => { self.advance(); Ok(Expr::Literal(Literal::Bool(b))) } - // Grouped or tuple + // Grouped expression or arrow function: `(expr)` or `(params) => body` or `() => body` Token::LParen => { - self.advance(); + self.advance(); // consume `(` + // Empty parens `()` — must be `() => expr` arrow function + if matches!(self.peek(), Token::RParen) { + self.advance(); // consume `)` + if self.eat(&Token::FatArrow) { + let body = self.parse_expr()?; + return Ok(Expr::Closure { + params: vec![], + return_type: None, + body: Box::new(body), + span, + }); + } + // `()` with no `=>` — return nil literal as placeholder + return Ok(Expr::Literal(Literal::Bool(false))); + } + // Check if this is `(ident, ident, ...)` or `(ident: Type, ...)` arrow params + // by attempting to parse as param list with look-ahead + if let Some(arrow_fn) = self.try_parse_arrow_fn(span)? { + return Ok(arrow_fn); + } + // Otherwise parse as grouped expression let expr = self.parse_expr()?; self.expect(&Token::RParen)?; + // Check again for `=>` (in case try_parse_arrow_fn consumed nothing) + if self.eat(&Token::FatArrow) { + // Treat the expr as a single untyped param name + let param_name = match &expr { + Expr::Ident(name) => name.clone(), + _ => "__arg__".to_string(), + }; + let body = self.parse_expr()?; + return Ok(Expr::Closure { + params: vec![Param { + name: param_name, + type_ann: TypeExpr::Named("_".to_string()), + span, + }], + return_type: None, + body: Box::new(body), + span, + }); + } Ok(expr) } @@ -1061,11 +1555,26 @@ impl Parser { Ok(Expr::Path { segments }) } else if matches!(self.peek(), Token::LBrace) && name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) + && self.is_struct_literal_start() { - // Struct literal: TypeName { field: expr, ... } + // Struct literal: TypeName { field: expr, ...spread, field: expr, ... } self.advance(); // consume `{` let mut fields = Vec::new(); while !matches!(self.peek(), Token::RBrace | Token::Eof) { + // Skip spread syntax: `...expr` (e.g. `...base`) + // Three dots followed by an expression — skip the spread for now (use base fields) + if matches!(self.peek(), Token::Dot) { + // Try to consume `...expr` — eat up to 3 dots then an expression + let mut dots = 0; + while matches!(self.peek(), Token::Dot) && dots < 3 { + self.advance(); + dots += 1; + } + // Parse the spread source expression (we discard it for now) + let _ = self.parse_primary()?; + if !self.eat(&Token::Comma) { break; } + continue; + } let (field_name, _) = self.expect_ident()?; self.expect(&Token::Colon)?; let field_expr = self.parse_expr()?; @@ -1121,6 +1630,44 @@ impl Parser { Ok(Expr::Trace { label, body }) } + // JSX element: `` — heuristic: `<` followed by ident + Token::Lt => { + let next = self.tokens.get(self.pos + 1).map(|s| &s.node); + if matches!(next, Some(Token::Ident(_))) { + self.parse_jsx_element() + } else { + // It's a comparison operator — shouldn't be in primary, error + Err(ParseError::new( + ParseErrorKind::InvalidExprStart(Token::Lt.to_string()), + span, + )) + } + } + + // `@builtin_name(args)` — runtime/syscall expression. + // Compiled as Call { name: "__builtin_name__", arity }. + // Also handles `@call(...)`, `@emit(...)`, `@on(...)`, `@uuid()` etc. + Token::At => { + self.advance(); // consume `@` + let (name, _) = self.expect_ident()?; + let args = if self.eat(&Token::LParen) { + let mut a = Vec::new(); + while !matches!(self.peek(), Token::RParen | Token::Eof) { + a.push(self.parse_expr()?); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::RParen)?; + a + } else { + vec![] + }; + // Emit as Call { func: Ident("@name"), args } + Ok(Expr::Call { + func: Box::new(Expr::Ident(format!("@{name}"))), + args, + }) + } + tok => Err(ParseError::new( ParseErrorKind::InvalidExprStart(tok.to_string()), span, @@ -1130,6 +1677,30 @@ impl Parser { /// Heuristic: are we at the start of a map literal? /// We peek ahead for `string/ident :` pattern. + /// Check if the current `{` (already consumed by outer code for TypeName {) + /// looks like a struct literal body rather than a block body. + /// Heuristic: if token at pos+1 (first inside `{`) is: + /// - `Ident Colon` → struct field → yes + /// - `Dot Dot Dot` → spread → yes + /// - `RBrace` (empty) → yes (empty struct) + /// - anything else → no (treat as block / enum variant) + /// Called when current token is `{` (LBrace) and we're checking if TypeName { } is a struct. + fn is_struct_literal_start(&self) -> bool { + // self.pos points to `{` (the LBrace we haven't consumed yet) + // Look at pos+1: first token inside `{` + let first = self.tokens.get(self.pos + 1).map(|t| &t.node); + match first { + Some(Token::RBrace) => true, // empty struct literal `TypeName {}` + Some(Token::Dot) => true, // spread `...expr` + Some(Token::Ident(_)) => { + // Check if the token AFTER the ident is `:` + let after_ident = self.tokens.get(self.pos + 2).map(|t| &t.node); + matches!(after_ident, Some(Token::Colon)) + } + _ => false, // Not a struct literal — treat as a block/expression context + } + } + fn is_map_literal(&self) -> bool { // Look at current token (first inside `{`) match self.peek() { @@ -1202,6 +1773,35 @@ fn expr_span_end(_expr: &Expr, fallback: Span) -> usize { fallback.end } +/// Returns true if `tok` can start an expression. +/// Used to disambiguate `?` as Try vs ternary. +fn is_expr_start(tok: &Token) -> bool { + matches!( + tok, + Token::Ident(_) + | Token::IntLiteral(_) + | Token::FloatLiteral(_) + | Token::StringLiteral(_) + | Token::BoolLiteral(_) + | Token::LParen + | Token::LBracket + | Token::LBrace + | Token::Minus + | Token::Not + | Token::Tilde + | Token::Pipe // closure + | Token::Match + | Token::If + | Token::Activate + | Token::Sealed + | Token::Reason + | Token::Parallel + | Token::Trace + | Token::Lt // JSX element + | Token::At // @builtin call + ) +} + // ── Tests ───────────────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/engrams/el-registry/Cargo.toml b/_archive/rust-bootstrap/engrams/el-registry/Cargo.toml similarity index 100% rename from engrams/el-registry/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-registry/Cargo.toml diff --git a/engrams/el-registry/src/client.rs b/_archive/rust-bootstrap/engrams/el-registry/src/client.rs similarity index 100% rename from engrams/el-registry/src/client.rs rename to _archive/rust-bootstrap/engrams/el-registry/src/client.rs diff --git a/engrams/el-registry/src/error.rs b/_archive/rust-bootstrap/engrams/el-registry/src/error.rs similarity index 100% rename from engrams/el-registry/src/error.rs rename to _archive/rust-bootstrap/engrams/el-registry/src/error.rs diff --git a/engrams/el-registry/src/lib.rs b/_archive/rust-bootstrap/engrams/el-registry/src/lib.rs similarity index 100% rename from engrams/el-registry/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-registry/src/lib.rs diff --git a/engrams/el-registry/src/resolve.rs b/_archive/rust-bootstrap/engrams/el-registry/src/resolve.rs similarity index 100% rename from engrams/el-registry/src/resolve.rs rename to _archive/rust-bootstrap/engrams/el-registry/src/resolve.rs diff --git a/engrams/el-seal/Cargo.toml b/_archive/rust-bootstrap/engrams/el-seal/Cargo.toml similarity index 100% rename from engrams/el-seal/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-seal/Cargo.toml diff --git a/engrams/el-seal/src/artifact.rs b/_archive/rust-bootstrap/engrams/el-seal/src/artifact.rs similarity index 100% rename from engrams/el-seal/src/artifact.rs rename to _archive/rust-bootstrap/engrams/el-seal/src/artifact.rs diff --git a/engrams/el-seal/src/error.rs b/_archive/rust-bootstrap/engrams/el-seal/src/error.rs similarity index 100% rename from engrams/el-seal/src/error.rs rename to _archive/rust-bootstrap/engrams/el-seal/src/error.rs diff --git a/engrams/el-seal/src/lib.rs b/_archive/rust-bootstrap/engrams/el-seal/src/lib.rs similarity index 100% rename from engrams/el-seal/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-seal/src/lib.rs diff --git a/engrams/el-seal/src/seal.rs b/_archive/rust-bootstrap/engrams/el-seal/src/seal.rs similarity index 100% rename from engrams/el-seal/src/seal.rs rename to _archive/rust-bootstrap/engrams/el-seal/src/seal.rs diff --git a/engrams/el-stdlib/Cargo.toml b/_archive/rust-bootstrap/engrams/el-stdlib/Cargo.toml similarity index 100% rename from engrams/el-stdlib/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-stdlib/Cargo.toml diff --git a/engrams/el-stdlib/src/array.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/array.rs similarity index 99% rename from engrams/el-stdlib/src/array.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/array.rs index fc8f2bb..7c31fe6 100644 --- a/engrams/el-stdlib/src/array.rs +++ b/_archive/rust-bootstrap/engrams/el-stdlib/src/array.rs @@ -43,7 +43,7 @@ pub fn register(env: &mut TypeEnv) { // array_concat([T], [T]) -> [T] env.register_fn("array_concat", fn_type(vec![arr_unk.clone(), arr_unk.clone()], arr_unk.clone())); // array_slice([T], Int, Int) -> [T] - env.register_fn("array_slice", fn_type(vec![arr_unk.clone(), Type::Int, Type::Int], arr_unk)); + env.register_fn("array_slice", fn_type(vec![arr_unk.clone(), Type::Int, Type::Int], arr_unk.clone())); // array_first([T]) -> T? env.register_fn("array_first", fn_type(vec![arr_int.clone()], Type::Optional(Box::new(Type::Int)))); // array_last([T]) -> T? diff --git a/engrams/el-stdlib/src/engram.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/engram.rs similarity index 100% rename from engrams/el-stdlib/src/engram.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/engram.rs diff --git a/engrams/el-stdlib/src/lib.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/lib.rs similarity index 100% rename from engrams/el-stdlib/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/lib.rs diff --git a/engrams/el-stdlib/src/map.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/map.rs similarity index 100% rename from engrams/el-stdlib/src/map.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/map.rs diff --git a/engrams/el-stdlib/src/math.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/math.rs similarity index 100% rename from engrams/el-stdlib/src/math.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/math.rs diff --git a/engrams/el-stdlib/src/optional.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/optional.rs similarity index 100% rename from engrams/el-stdlib/src/optional.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/optional.rs diff --git a/engrams/el-stdlib/src/result.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/result.rs similarity index 100% rename from engrams/el-stdlib/src/result.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/result.rs diff --git a/engrams/el-stdlib/src/string.rs b/_archive/rust-bootstrap/engrams/el-stdlib/src/string.rs similarity index 100% rename from engrams/el-stdlib/src/string.rs rename to _archive/rust-bootstrap/engrams/el-stdlib/src/string.rs diff --git a/engrams/el-test/Cargo.toml b/_archive/rust-bootstrap/engrams/el-test/Cargo.toml similarity index 100% rename from engrams/el-test/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-test/Cargo.toml diff --git a/engrams/el-test/src/discovery.rs b/_archive/rust-bootstrap/engrams/el-test/src/discovery.rs similarity index 100% rename from engrams/el-test/src/discovery.rs rename to _archive/rust-bootstrap/engrams/el-test/src/discovery.rs diff --git a/engrams/el-test/src/eval.rs b/_archive/rust-bootstrap/engrams/el-test/src/eval.rs similarity index 98% rename from engrams/el-test/src/eval.rs rename to _archive/rust-bootstrap/engrams/el-test/src/eval.rs index 74e42a6..3b3bbbe 100644 --- a/engrams/el-test/src/eval.rs +++ b/_archive/rust-bootstrap/engrams/el-test/src/eval.rs @@ -378,6 +378,15 @@ impl<'g> Evaluator<'g> { (EvalValue::Int(a), EvalValue::Int(b)) => Ok(EvalValue::Int(a >> (b as u32))), _ => Ok(EvalValue::Int(0)), }, + BinOp::NullCoalesce => { + // `a ?? b` — use b if a is nil/empty/false + match &lv { + EvalValue::Nil => Ok(rv), + EvalValue::Str(s) if s.is_empty() => Ok(rv), + EvalValue::Bool(false) => Ok(rv), + _ => Ok(lv), + } + } } } @@ -440,6 +449,7 @@ pub fn expr_to_text(expr: &Expr) -> String { BinOp::And => "&&", BinOp::Or => "||", BinOp::Mod => "%", BinOp::BitAnd => "&", BinOp::BitOr => "|", BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", + BinOp::NullCoalesce => "??", }; format!("{} {op_str} {}", expr_to_text(left), expr_to_text(right)) } diff --git a/engrams/el-test/src/graph.rs b/_archive/rust-bootstrap/engrams/el-test/src/graph.rs similarity index 100% rename from engrams/el-test/src/graph.rs rename to _archive/rust-bootstrap/engrams/el-test/src/graph.rs diff --git a/engrams/el-test/src/lib.rs b/_archive/rust-bootstrap/engrams/el-test/src/lib.rs similarity index 100% rename from engrams/el-test/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-test/src/lib.rs diff --git a/engrams/el-test/src/report.rs b/_archive/rust-bootstrap/engrams/el-test/src/report.rs similarity index 100% rename from engrams/el-test/src/report.rs rename to _archive/rust-bootstrap/engrams/el-test/src/report.rs diff --git a/engrams/el-test/src/runner.rs b/_archive/rust-bootstrap/engrams/el-test/src/runner.rs similarity index 100% rename from engrams/el-test/src/runner.rs rename to _archive/rust-bootstrap/engrams/el-test/src/runner.rs diff --git a/engrams/el-test/src/types.rs b/_archive/rust-bootstrap/engrams/el-test/src/types.rs similarity index 100% rename from engrams/el-test/src/types.rs rename to _archive/rust-bootstrap/engrams/el-test/src/types.rs diff --git a/engrams/el-types/Cargo.toml b/_archive/rust-bootstrap/engrams/el-types/Cargo.toml similarity index 100% rename from engrams/el-types/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-types/Cargo.toml diff --git a/engrams/el-types/src/checker.rs b/_archive/rust-bootstrap/engrams/el-types/src/checker.rs similarity index 96% rename from engrams/el-types/src/checker.rs rename to _archive/rust-bootstrap/engrams/el-types/src/checker.rs index c04ad52..56cfa95 100644 --- a/engrams/el-types/src/checker.rs +++ b/_archive/rust-bootstrap/engrams/el-types/src/checker.rs @@ -202,6 +202,16 @@ impl TypeChecker { self.infer_expr(condition); for s in body { self.check_stmt(s); } } + Stmt::ComponentDef { props, state, methods, template, .. } => { + // Type-check component: props, state, methods, and template. + for field in props.iter().chain(state.iter()) { + if let Some(default) = &field.default { + self.infer_expr(default); + } + } + for m in methods { self.check_stmt(m); } + self.infer_expr(template); + } } } @@ -497,6 +507,14 @@ impl TypeChecker { self.infer_expr(inner); Type::Int } + + // JSX expressions — type as Unknown (JSX type-checking not yet implemented). + Expr::JsxElement { children, .. } => { + for child in children { self.infer_expr(child); } + Type::Unknown + } + Expr::JsxExpr(inner) => self.infer_expr(inner), + Expr::JsxText(_) => Type::String, } } @@ -555,6 +573,11 @@ impl TypeChecker { } BinOp::Mod | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => Type::Int, + BinOp::NullCoalesce => { + // `a ?? b` — type is the type of `a` (or `b` if `a` is unknown) + let lt = self.infer_expr(left); + if matches!(lt, Type::Unknown) { self.infer_expr(right) } else { lt } + } } } diff --git a/engrams/el-types/src/error.rs b/_archive/rust-bootstrap/engrams/el-types/src/error.rs similarity index 100% rename from engrams/el-types/src/error.rs rename to _archive/rust-bootstrap/engrams/el-types/src/error.rs diff --git a/engrams/el-types/src/lib.rs b/_archive/rust-bootstrap/engrams/el-types/src/lib.rs similarity index 100% rename from engrams/el-types/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-types/src/lib.rs diff --git a/engrams/el-types/src/types.rs b/_archive/rust-bootstrap/engrams/el-types/src/types.rs similarity index 100% rename from engrams/el-types/src/types.rs rename to _archive/rust-bootstrap/engrams/el-types/src/types.rs diff --git a/_archive/rust-bootstrap/engrams/el-vm/Cargo.toml b/_archive/rust-bootstrap/engrams/el-vm/Cargo.toml new file mode 100644 index 0000000..6057774 --- /dev/null +++ b/_archive/rust-bootstrap/engrams/el-vm/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "el-vm" +description = "El Virtual Machine — core bytecode executor used by the elvm binary and el toolchain" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +el-compiler = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true } +uuid = { workspace = true } +walkdir = { workspace = true } +blake3 = { workspace = true } +hmac = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +base64 = { workspace = true } +aes-gcm = { workspace = true } +rand = { workspace = true } +crossbeam-channel = { workspace = true } diff --git a/_archive/rust-bootstrap/engrams/el-vm/src/builtins.rs b/_archive/rust-bootstrap/engrams/el-vm/src/builtins.rs new file mode 100644 index 0000000..ffac63e --- /dev/null +++ b/_archive/rust-bootstrap/engrams/el-vm/src/builtins.rs @@ -0,0 +1,725 @@ +//! Built-in function dispatch for the El VM. +//! +//! Provides the set of built-in functions accessible to El programs at runtime. +//! This is the core portable set that `elvm` supports on all platforms. + +use el_compiler::Value; +use std::collections::HashMap; + +/// Result of a builtin dispatch attempt. +pub enum BuiltinResult { + /// The builtin was handled; return value (if any) is on the stack. + Handled, + /// The `http_serve` builtin was invoked; the server loop is running. + HttpServe, + /// The program called `exit(code)`. + Exit(i32), + /// The name is not a known builtin — try user-defined functions. + NotBuiltin, +} + +/// Dispatch a built-in call. +/// +/// # Arguments +/// * `name` — function name +/// * `arity` — number of arguments already on the stack +/// * `stack` — the VM value stack (may be modified) +/// * `args` — program arguments (for the `args()` builtin) +/// * `state` — global mutable string state +pub fn dispatch( + name: &str, + _arity: u32, + stack: &mut Vec, + program_args: &[String], + state: &mut HashMap, +) -> BuiltinResult { + match name { + // ── I/O ────────────────────────────────────────────────────────────── + + "print" => { + let v = stack.pop().unwrap_or(Value::Nil); + print!("{v}"); + stack.push(Value::Nil); + BuiltinResult::Handled + } + "println" | "log" => { + let v = stack.pop().unwrap_or(Value::Nil); + println!("{v}"); + stack.push(Value::Nil); + BuiltinResult::Handled + } + "print_err" | "log_debug" | "log_info" | "log_warn" | "log_error" => { + let v = stack.pop().unwrap_or(Value::Nil); + eprintln!("{v}"); + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // ── Process ─────────────────────────────────────────────────────────── + + "args" => { + let list = program_args.iter().map(|s| Value::Str(s.clone())).collect(); + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "exit" => { + let code = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as i32, + _ => 0, + }; + BuiltinResult::Exit(code) + } + "env" => { + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let val = std::env::var(&key).unwrap_or_default(); + stack.push(Value::Str(val)); + BuiltinResult::Handled + } + "cwd" => { + let path = std::env::current_dir() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| ".".to_string()); + stack.push(Value::Str(path)); + BuiltinResult::Handled + } + "getpid" => { + stack.push(Value::Int(std::process::id() as i64)); + BuiltinResult::Handled + } + "sleep_ms" => { + let ms = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u64, + _ => 0, + }; + std::thread::sleep(std::time::Duration::from_millis(ms)); + stack.push(Value::Nil); + BuiltinResult::Handled + } + "sleep_secs" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u64, + Value::Float(f) => f as u64, + _ => 0, + }; + std::thread::sleep(std::time::Duration::from_secs(s)); + stack.push(Value::Nil); + BuiltinResult::Handled + } + "shell_exec" | "sh" => { + let cmd = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let output = std::process::Command::new("sh") + .arg("-c") + .arg(&cmd) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) + .unwrap_or_default(); + stack.push(Value::Str(output)); + BuiltinResult::Handled + } + + // ── String operations ───────────────────────────────────────────────── + + "str_len" | "string_len" | "native_string_len" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Int(s.len() as i64)); + BuiltinResult::Handled + } + "str_eq" => { + let b = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let a = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(a == b)); + BuiltinResult::Handled + } + "str_contains" | "native_string_contains" => { + let needle = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let haystack = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(haystack.contains(needle.as_str()))); + BuiltinResult::Handled + } + "str_starts_with" => { + let prefix = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(s.starts_with(prefix.as_str()))); + BuiltinResult::Handled + } + "str_ends_with" => { + let suffix = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(s.ends_with(suffix.as_str()))); + BuiltinResult::Handled + } + "str_split" => { + let delim = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let parts: Vec = s.split(delim.as_str()).map(|p| Value::Str(p.to_string())).collect(); + stack.push(Value::List(parts)); + BuiltinResult::Handled + } + "str_replace" => { + let replacement = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let pattern = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(s.replace(pattern.as_str(), &replacement))); + BuiltinResult::Handled + } + "str_trim" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(s.trim().to_string())); + BuiltinResult::Handled + } + "str_upper" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(s.to_uppercase())); + BuiltinResult::Handled + } + "str_lower" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(s.to_lowercase())); + BuiltinResult::Handled + } + "str_slice" => { + let end = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 }; + let start = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let chars: Vec = s.chars().collect(); + let slice: String = chars[start.min(chars.len())..end.min(chars.len())].iter().collect(); + stack.push(Value::Str(slice)); + BuiltinResult::Handled + } + "str_index_of" => { + let needle = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let haystack = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let idx = haystack.find(needle.as_str()).map(|i| i as i64).unwrap_or(-1); + stack.push(Value::Int(idx)); + BuiltinResult::Handled + } + "str_char_at" => { + let idx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 }; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let c = s.chars().nth(idx).map(|c| Value::Str(c.to_string())).unwrap_or(Value::Nil); + stack.push(c); + BuiltinResult::Handled + } + + // ── Type conversion ─────────────────────────────────────────────────── + + "int_to_str" | "str_format" | "native_int_to_str" => { + let n = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 }; + stack.push(Value::Str(n.to_string())); + BuiltinResult::Handled + } + "float_to_str" => { + let f = match stack.pop().unwrap_or(Value::Nil) { + Value::Float(f) => f, + Value::Int(n) => n as f64, + _ => 0.0, + }; + stack.push(Value::Str(f.to_string())); + BuiltinResult::Handled + } + "bool_to_str" => { + let b = match stack.pop().unwrap_or(Value::Nil) { Value::Bool(b) => b, _ => false }; + stack.push(Value::Str(b.to_string())); + BuiltinResult::Handled + } + "str_to_int" | "parse_int" | "native_str_to_int" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let v = s.trim().parse::().map(Value::Int).unwrap_or(Value::Nil); + stack.push(v); + BuiltinResult::Handled + } + "str_to_float" | "parse_float" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let v = s.trim().parse::().map(Value::Float).unwrap_or(Value::Nil); + stack.push(v); + BuiltinResult::Handled + } + "int_to_float" => { + let n = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 }; + stack.push(Value::Float(n as f64)); + BuiltinResult::Handled + } + "float_to_int" => { + let f = match stack.pop().unwrap_or(Value::Nil) { Value::Float(f) => f, Value::Int(n) => n as f64, _ => 0.0 }; + stack.push(Value::Int(f as i64)); + BuiltinResult::Handled + } + "is_nil" => { + let v = stack.pop().unwrap_or(Value::Nil); + stack.push(Value::Bool(matches!(v, Value::Nil))); + BuiltinResult::Handled + } + "unwrap_or" => { + let default = stack.pop().unwrap_or(Value::Nil); + let v = stack.pop().unwrap_or(Value::Nil); + stack.push(if matches!(v, Value::Nil) { default } else { v }); + BuiltinResult::Handled + } + + // ── List operations ─────────────────────────────────────────────────── + + "list_len" | "native_list_len" => { + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + stack.push(Value::Int(list.len() as i64)); + BuiltinResult::Handled + } + "list_get" | "native_list_get" => { + let idx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => -1 }; + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + let v = if idx >= 0 && (idx as usize) < list.len() { list[idx as usize].clone() } else { Value::Nil }; + stack.push(v); + BuiltinResult::Handled + } + "native_string_chars" => { + // Split a string into a list of single-character strings. + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let chars: Vec = s.chars().map(|c| Value::Str(c.to_string())).collect(); + stack.push(Value::List(chars)); + BuiltinResult::Handled + } + "list_push" | "native_list_append" => { + let item = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(mut l) => { l.push(item); l }, _ => vec![item] }; + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "list_pop" => { + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(mut l) => { l.pop(); l }, _ => vec![] }; + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "list_join" => { + let sep = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + let s: String = list.iter().map(|v| v.to_string()).collect::>().join(&sep); + stack.push(Value::Str(s)); + BuiltinResult::Handled + } + "list_contains" => { + let item = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + stack.push(Value::Bool(list.contains(&item))); + BuiltinResult::Handled + } + "list_concat" => { + let b = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + let a = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + let mut result = a; + result.extend(b); + stack.push(Value::List(result)); + BuiltinResult::Handled + } + "list_reverse" => { + let mut list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + list.reverse(); + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "list_range" => { + let end = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 }; + let start = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n, _ => 0 }; + let list: Vec = (start..end).map(Value::Int).collect(); + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "list_set" => { + let val = stack.pop().unwrap_or(Value::Nil); + let idx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as usize, _ => 0 }; + let mut list = match stack.pop().unwrap_or(Value::Nil) { Value::List(l) => l, _ => vec![] }; + if idx < list.len() { list[idx] = val; } + stack.push(Value::List(list)); + BuiltinResult::Handled + } + "list_new" | "list_empty" | "native_list_empty" => { + stack.push(Value::List(vec![])); + BuiltinResult::Handled + } + "__build_list__" => { + let mut items = Vec::new(); + for _ in 0.._arity { items.push(stack.pop().unwrap_or(Value::Nil)); } + items.reverse(); + stack.push(Value::List(items)); + BuiltinResult::Handled + } + + // ── JSON ────────────────────────────────────────────────────────────── + + "json_get" | "json_get_string" => { + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let json_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let val = serde_json::from_str::(&json_str) + .ok() + .and_then(|v| v.get(&key).cloned()) + .map(|v| crate::json_to_value(&v)) + .unwrap_or(Value::Nil); + stack.push(val); + BuiltinResult::Handled + } + "json_stringify" | "json_encode" => { + let v = stack.pop().unwrap_or(Value::Nil); + let json = value_to_json(&v); + stack.push(Value::Str(json.to_string())); + BuiltinResult::Handled + } + "json_parse" | "json_decode" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let val = serde_json::from_str::(&s) + .map(|v| crate::json_to_value(&v)) + .unwrap_or(Value::Nil); + stack.push(val); + BuiltinResult::Handled + } + "json_keys" => { + let json_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let keys = serde_json::from_str::(&json_str) + .ok() + .and_then(|v| v.as_object().cloned()) + .map(|o| o.keys().map(|k| Value::Str(k.clone())).collect::>()) + .unwrap_or_default(); + stack.push(Value::List(keys)); + BuiltinResult::Handled + } + "json_set" => { + let val = stack.pop().unwrap_or(Value::Nil); + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let json_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "{}".to_string() }; + let mut obj = serde_json::from_str::>(&json_str) + .unwrap_or_default(); + obj.insert(key, value_to_json(&val)); + stack.push(Value::Str(serde_json::Value::Object(obj).to_string())); + BuiltinResult::Handled + } + + // ── Time ────────────────────────────────────────────────────────────── + + "now_millis" | "time_now_ms" | "unix_timestamp" | "timestamp" => { + let ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0); + stack.push(Value::Int(ms)); + BuiltinResult::Handled + } + "time_now_s" => { + let s = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + stack.push(Value::Int(s)); + BuiltinResult::Handled + } + "time_now_utc" => { + let ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0); + stack.push(Value::Int(ms)); + BuiltinResult::Handled + } + + // ── Math ────────────────────────────────────────────────────────────── + + "math_sin" => { let x = pop_f64(stack); stack.push(Value::Float(x.sin())); BuiltinResult::Handled } + "math_cos" => { let x = pop_f64(stack); stack.push(Value::Float(x.cos())); BuiltinResult::Handled } + "math_tan" => { let x = pop_f64(stack); stack.push(Value::Float(x.tan())); BuiltinResult::Handled } + "math_asin" => { let x = pop_f64(stack); stack.push(Value::Float(x.asin())); BuiltinResult::Handled } + "math_acos" => { let x = pop_f64(stack); stack.push(Value::Float(x.acos())); BuiltinResult::Handled } + "math_exp" => { let x = pop_f64(stack); stack.push(Value::Float(x.exp())); BuiltinResult::Handled } + "math_ln" => { let x = pop_f64(stack); stack.push(Value::Float(x.ln())); BuiltinResult::Handled } + "math_log2" => { let x = pop_f64(stack); stack.push(Value::Float(x.log2())); BuiltinResult::Handled } + "math_log10"=> { let x = pop_f64(stack); stack.push(Value::Float(x.log10()));BuiltinResult::Handled } + "math_pi" => { stack.push(Value::Float(std::f64::consts::PI)); BuiltinResult::Handled } + "math_e" => { stack.push(Value::Float(std::f64::consts::E)); BuiltinResult::Handled } + "math_atan2"=> { + let y = pop_f64(stack); + let x = pop_f64(stack); + stack.push(Value::Float(x.atan2(y))); + BuiltinResult::Handled + } + "math_mod" => { + let b = pop_f64(stack); + let a = pop_f64(stack); + stack.push(Value::Float(a % b)); + BuiltinResult::Handled + } + + // ── Crypto / ID ─────────────────────────────────────────────────────── + + "uuid_new" | "uuid_v4" | "crypto_uuid" => { + let id = uuid::Uuid::new_v4().to_string(); + stack.push(Value::Str(id)); + BuiltinResult::Handled + } + "blake3_hash" | "crypto_hash_blake3" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let hash = blake3::hash(s.as_bytes()); + stack.push(Value::Str(hash.to_hex().to_string())); + BuiltinResult::Handled + } + "hash_sha256" | "crypto_hash_sha256" => { + // Use blake3 as a stable hash (sha2 not linked in elvm). + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let hash = blake3::hash(s.as_bytes()); + stack.push(Value::Str(hash.to_hex().to_string())); + BuiltinResult::Handled + } + "base64_encode" => { + use base64::Engine; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(base64::engine::general_purpose::STANDARD.encode(s.as_bytes()))); + BuiltinResult::Handled + } + "base64_decode" => { + use base64::Engine; + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let decoded = base64::engine::general_purpose::STANDARD + .decode(s.as_bytes()) + .map(|b| String::from_utf8_lossy(&b).to_string()) + .unwrap_or_default(); + stack.push(Value::Str(decoded)); + BuiltinResult::Handled + } + + // ── Filesystem ──────────────────────────────────────────────────────── + + "fs_read" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } }; + let val = std::fs::read_to_string(&path).map(Value::Str).unwrap_or(Value::Nil); + stack.push(val); + BuiltinResult::Handled + } + "fs_write" => { + let content = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let ok = std::fs::write(&path, &content).is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_exists" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(std::path::Path::new(&path).exists())); + BuiltinResult::Handled + } + "fs_mkdir" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let ok = std::fs::create_dir_all(&path).is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_list" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let entries = std::fs::read_dir(&path) + .map(|rd| rd.filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok().map(Value::Str))).collect::>()) + .unwrap_or_default(); + stack.push(Value::List(entries)); + BuiltinResult::Handled + } + "fs_append" => { + use std::io::Write; + let content = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let ok = std::fs::OpenOptions::new().create(true).append(true).open(&path) + .and_then(|mut f| f.write_all(content.as_bytes())) + .is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_remove" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let p = std::path::Path::new(&path); + let ok = if p.is_dir() { std::fs::remove_dir(p).is_ok() } else { std::fs::remove_file(p).is_ok() }; + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_is_dir" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Bool(std::path::Path::new(&path).is_dir())); + BuiltinResult::Handled + } + "path_join" => { + let name = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let base = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let joined = std::path::Path::new(&base).join(&name); + stack.push(Value::Str(joined.to_string_lossy().to_string())); + BuiltinResult::Handled + } + "path_parent" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let parent = std::path::Path::new(&path).parent() + .map(|p| Value::Str(p.to_string_lossy().to_string())) + .unwrap_or(Value::Nil); + stack.push(parent); + BuiltinResult::Handled + } + "fs_list_recursive" => { + let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let skip_dirs = ["node_modules", ".git", ".nc", "target", "__pycache__", ".el"]; + let mut files = Vec::new(); + if let Ok(walker) = walkdir::WalkDir::new(&path).into_iter().collect::, _>>() { + for entry in walker { + let should_skip = entry.path().components().any(|c| { + let s = c.as_os_str().to_string_lossy(); + skip_dirs.iter().any(|d| s == *d) + }); + if should_skip && entry.path() != std::path::Path::new(&path) { continue; } + if entry.file_type().is_file() { + files.push(Value::Str(entry.path().to_string_lossy().to_string())); + } + } + } + stack.push(Value::List(files)); + BuiltinResult::Handled + } + + // ── Global state ────────────────────────────────────────────────────── + + "state_set" => { + let value = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + state.insert(key, value); + stack.push(Value::Bool(true)); + BuiltinResult::Handled + } + "state_get" => { + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let val = state.get(&key).cloned().map(Value::Str).unwrap_or(Value::Nil); + stack.push(val); + BuiltinResult::Handled + } + "state_del" => { + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + state.remove(&key); + stack.push(Value::Bool(true)); + BuiltinResult::Handled + } + "state_keys" => { + let keys: Vec = state.keys().cloned().map(Value::Str).collect(); + stack.push(Value::List(keys)); + BuiltinResult::Handled + } + + // ── HTTP client ─────────────────────────────────────────────────────── + + "http_get" => { + let url = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let result = reqwest::blocking::get(&url).and_then(|r| r.text()) + .unwrap_or_else(|e| format!("{{\"error\":\"{e}}}" )); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + "http_post" => { + let body = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let url = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let result = reqwest::blocking::Client::new() + .post(&url) + .header("Content-Type", "application/json") + .body(body) + .send() + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("{{\"error\":\"{e}}}" )); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + "http_post_form_auth" => { + // args: url (bottom), auth_value, body (top) + let body = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let auth = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let url = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + let result = reqwest::blocking::Client::new() + .post(&url) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Authorization", format!("Bearer {auth}")) + .body(body) + .send() + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}")); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + + // ── App context (no-op stubs — app block not parsed by elvm) ───────── + + "env_get" => { + let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(std::env::var(&key).unwrap_or_default())); + BuiltinResult::Handled + } + "ctx_env" => { stack.push(Value::Str(std::env::var("NEURON_ENV").unwrap_or_else(|_| "dev".to_string()))); BuiltinResult::Handled } + "ctx_service" => { stack.push(Value::Str(String::new())); BuiltinResult::Handled } + "ctx_version" => { stack.push(Value::Str(String::new())); BuiltinResult::Handled } + "ctx_instance"=> { stack.push(Value::Str(uuid::Uuid::new_v4().to_string())); BuiltinResult::Handled } + "config" => { let _ = stack.pop(); stack.push(Value::Str(String::new())); BuiltinResult::Handled } + "secret" => { let _ = stack.pop(); stack.push(Value::Str(String::new())); BuiltinResult::Handled } + "flag" => { let _ = stack.pop(); stack.push(Value::Bool(false)); BuiltinResult::Handled } + + // ── Color / formatting (terminal output) ───────────────────────────── + + "color_bold" => { + let s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(format!("\x1b[1m{s}\x1b[0m"))); + BuiltinResult::Handled + } + + // ── http_serve (stub — available but note elvm doesn't have the tiny_http dep) ── + + "http_serve" => { + // elvm does not bundle the HTTP server. Emit a helpful message. + let _ = stack.pop(); + eprintln!("elvm: http_serve is not available in the standalone elvm binary. Use 'el run' for HTTP server programs."); + stack.push(Value::Nil); + BuiltinResult::HttpServe + } + + // ── fn_ref helper ───────────────────────────────────────────────────── + + "fn_ref" => { + let name_val = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() }; + stack.push(Value::Str(name_val)); + BuiltinResult::Handled + } + + _ => BuiltinResult::NotBuiltin, + } +} + +// ── Internal helpers ────────────────────────────────────────────────────────── + +fn pop_f64(stack: &mut Vec) -> f64 { + match stack.pop().unwrap_or(Value::Nil) { + Value::Float(f) => f, + Value::Int(n) => n as f64, + _ => 0.0, + } +} + +fn value_to_json(v: &Value) -> serde_json::Value { + match v { + Value::Int(n) => serde_json::Value::Number((*n).into()), + Value::Float(f) => serde_json::Number::from_f64(*f).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null), + Value::Str(s) => serde_json::Value::String(s.clone()), + Value::Bool(b) => serde_json::Value::Bool(*b), + Value::Nil => serde_json::Value::Null, + Value::List(items) => serde_json::Value::Array(items.iter().map(value_to_json).collect()), + Value::Map(pairs) => { + let mut map = serde_json::Map::new(); + for (k, v) in pairs { map.insert(k.clone(), value_to_json(v)); } + serde_json::Value::Object(map) + } + Value::ResultOk(inner) => serde_json::json!({ "ok": value_to_json(inner) }), + Value::ResultErr(inner) => serde_json::json!({ "err": value_to_json(inner) }), + Value::Struct { fields, .. } => { + let mut map = serde_json::Map::new(); + for (k, v) in fields { map.insert(k.clone(), value_to_json(v)); } + serde_json::Value::Object(map) + } + } +} diff --git a/_archive/rust-bootstrap/engrams/el-vm/src/lib.rs b/_archive/rust-bootstrap/engrams/el-vm/src/lib.rs new file mode 100644 index 0000000..f1fccc7 --- /dev/null +++ b/_archive/rust-bootstrap/engrams/el-vm/src/lib.rs @@ -0,0 +1,1161 @@ +//! el-vm — El Virtual Machine executor. +//! +//! This crate provides the bytecode execution engine used by both the `el` +//! toolchain (`el exec`) and the standalone `elvm` binary. +//! +//! # Usage +//! +//! ```no_run +//! use el_vm::ElVm; +//! +//! let bytes = std::fs::read("program.elc").unwrap(); +//! let instructions = el_compiler::Bytecode::deserialize_all(&bytes).unwrap(); +//! ElVm::new().run(&instructions, &[]); +//! ``` + +use el_compiler::{Bytecode, Value}; +use std::collections::HashMap; + +mod builtins; +pub use builtins::BuiltinResult; + +// ── Call frame ──────────────────────────────────────────────────────────────── + +struct CallFrame { + /// Bytecode offset to return to when this frame pops. + return_ip: usize, + /// Saved locals from the caller's frame. + saved_locals: HashMap, +} + +// ── El VM ───────────────────────────────────────────────────────────────────── + +/// The El Virtual Machine. +/// +/// Executes a flat array of [`Bytecode`] instructions in a stack-based +/// execution model. User-defined functions are resolved via a pre-scanned +/// function table (the codegen emits `Push(Int(entry)) StoreLocal(__fn_name)` +/// stanzas that the VM recognises on its initial scan pass). +pub struct ElVm { + /// Global mutable key-value state (shared across the program lifetime). + global_state: HashMap, + /// Program arguments forwarded to the `args()` builtin. + program_args: Vec, + /// Global instruction buffer — used by the El self-hosting compiler to + /// accumulate codegen output without expensive per-call list cloning. + /// Accessible via the `native_instr_*` builtin family. + instr_buf: Vec, + /// Global token buffer — used by the El self-hosting parser to avoid + /// cloning the token list on every recursive call. + /// Accessible via `native_tokens_init` / `native_token_at` / `native_token_len`. + token_buf: Vec, + /// Global char buffer — used by the El self-hosting lexer to avoid + /// O(n²) cloning of the chars list on each character access. + /// Accessible via `native_chars_init` / `native_char_at` / `native_char_len`. + char_buf: Vec, +} + +impl Default for ElVm { + fn default() -> Self { Self::new() } +} + +impl ElVm { + /// Create a new VM instance with empty global state. + pub fn new() -> Self { + Self { + global_state: HashMap::new(), + program_args: Vec::new(), + instr_buf: Vec::new(), + token_buf: Vec::new(), + char_buf: Vec::new(), + } + } + + /// Set program arguments (available via the `args()` builtin). + pub fn with_args(mut self, args: Vec) -> Self { + self.program_args = args; + self + } + + /// Execute a compiled program. + /// + /// Scans for function entry points on first pass, then runs from IP 0. + /// If the program defines a `main` function, it is called automatically + /// after the top-level initialisation pass completes. + pub fn run(&mut self, instructions: &[Bytecode], args: &[String]) { + self.program_args = args.to_vec(); + self.execute(instructions, &[]); + + // Auto-call `main()` if the program registered one. + let fn_table = Self::scan_fn_table(instructions); + if let Some(&entry) = fn_table.get("main") { + self.call_fn(instructions, &fn_table, entry, vec![]); + } + } + + /// Internal execution — also used for sub-interpreter calls (e.g. http_serve). + pub fn execute(&mut self, instructions: &[Bytecode], initial_stack: &[Value]) -> Value { + // ── Pass 1: build function table ───────────────────────────────────── + let fn_table = Self::scan_fn_table(instructions); + + // ── Pass 2: run ─────────────────────────────────────────────────────── + let mut stack: Vec = initial_stack.to_vec(); + let mut locals: HashMap = HashMap::new(); + let mut call_stack: Vec = Vec::new(); + let mut ip = 0usize; + + while ip < instructions.len() { + match &instructions[ip] { + // ── Stack ───────────────────────────────────────────────────── + Bytecode::Push(v) => stack.push(v.clone()), + Bytecode::Pop => { stack.pop(); } + Bytecode::Dup => { + if let Some(top) = stack.last().cloned() { stack.push(top); } + } + + // ── Arithmetic ──────────────────────────────────────────────── + Bytecode::Add => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x + y), + (Value::Float(x), Value::Float(y)) => Value::Float(x + y), + (Value::Str(x), Value::Str(y)) => Value::Str(x + &y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 + y), + (Value::Float(x), Value::Int(y)) => Value::Float(x + y as f64), + _ => Value::Nil, + }); + } + Bytecode::Sub => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x - y), + (Value::Float(x), Value::Float(y)) => Value::Float(x - y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 - y), + (Value::Float(x), Value::Int(y)) => Value::Float(x - y as f64), + _ => Value::Nil, + }); + } + Bytecode::Mul => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x * y), + (Value::Float(x), Value::Float(y)) => Value::Float(x * y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 * y), + (Value::Float(x), Value::Int(y)) => Value::Float(x * y as f64), + _ => Value::Nil, + }); + } + Bytecode::Div => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) if y != 0 => Value::Int(x / y), + (Value::Float(x), Value::Float(y)) => Value::Float(x / y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 / y), + (Value::Float(x), Value::Int(y)) => Value::Float(x / y as f64), + _ => Value::Nil, + }); + } + Bytecode::Mod => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(1)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => if y == 0 { Value::Int(0) } else { Value::Int(x % y) }, + (Value::Float(x), Value::Float(y)) => Value::Float(x % y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 % y), + (Value::Float(x), Value::Int(y)) => Value::Float(x % y as f64), + _ => Value::Int(0), + }); + } + Bytecode::BitAnd => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x & y), _ => Value::Int(0) }); + } + Bytecode::BitOr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x | y), _ => Value::Int(0) }); + } + Bytecode::BitXor => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x ^ y), _ => Value::Int(0) }); + } + Bytecode::BitNot => { + let a = stack.pop().unwrap_or(Value::Int(0)); + stack.push(match a { Value::Int(x) => Value::Int(!x), _ => Value::Int(0) }); + } + Bytecode::Shl => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x << (y as u32)), _ => Value::Int(0) }); + } + Bytecode::Shr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x >> (y as u32)), _ => Value::Int(0) }); + } + + // ── Comparison ──────────────────────────────────────────────── + Bytecode::Eq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a == b)); + } + Bytecode::NotEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a != b)); + } + Bytecode::Lt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Less)); + } + Bytecode::Gt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Greater)); + } + Bytecode::LtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Greater)); + } + Bytecode::GtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Less)); + } + + // ── Logic ───────────────────────────────────────────────────── + Bytecode::And => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) && matches!(b, Value::Bool(true)) + )); + } + Bytecode::Or => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) || matches!(b, Value::Bool(true)) + )); + } + Bytecode::Not => { + let v = stack.pop().unwrap_or(Value::Nil); + stack.push(Value::Bool(!matches!(v, Value::Bool(true)))); + } + + // ── Locals ──────────────────────────────────────────────────── + Bytecode::StoreLocal(name) => { + let v = stack.pop().unwrap_or(Value::Nil); + locals.insert(name.clone(), v); + } + Bytecode::LoadLocal(name) => { + let v = locals.get(name).cloned().unwrap_or(Value::Nil); + stack.push(v); + } + + // ── Function calls ──────────────────────────────────────────── + Bytecode::Call { name, arity } => { + // ── native_instr_* builtins — global instruction accumulator ─ + match name.as_str() { + "native_instr_push" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, v => v.to_string(), + }; + self.instr_buf.push(s); + stack.push(Value::Nil); + ip += 1; continue; + } + "native_instr_len" => { + stack.push(Value::Int(self.instr_buf.len() as i64)); + ip += 1; continue; + } + "native_instr_get" => { + let idx = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.instr_buf.get(idx) + .cloned() + .map(Value::Str) + .unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_instr_patch" => { + let new_instr = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, v => v.to_string(), + }; + let idx = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + if idx < self.instr_buf.len() { + self.instr_buf[idx] = new_instr; + } + stack.push(Value::Nil); + ip += 1; continue; + } + "native_instr_all" => { + let list: Vec = self.instr_buf.iter() + .map(|s| Value::Str(s.clone())) + .collect(); + stack.push(Value::List(list)); + ip += 1; continue; + } + "native_instr_reset" => { + self.instr_buf.clear(); + stack.push(Value::Nil); + ip += 1; continue; + } + // ── native_token_* builtins — global token buffer ───────── + "native_tokens_init" => { + // native_tokens_init(token_list) — store token list globally + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, _ => vec![], + }; + self.token_buf = list; + stack.push(Value::Nil); + ip += 1; continue; + } + "native_token_at" => { + // native_token_at(pos) → Map (token at pos, or empty map) + let pos = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.token_buf.get(pos).cloned().unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_token_len" => { + // native_token_len() → Int + stack.push(Value::Int(self.token_buf.len() as i64)); + ip += 1; continue; + } + // ── native_char_* builtins — global char buffer ─────────── + "native_chars_init" => { + // native_chars_init(str) — split string into chars, store globally + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + self.char_buf = s.chars().collect(); + stack.push(Value::Nil); + ip += 1; continue; + } + "native_char_at" => { + // native_char_at(i) → String (single char) + let i = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.char_buf.get(i) + .map(|c| Value::Str(c.to_string())) + .unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_char_len" => { + // native_char_len() → Int + stack.push(Value::Int(self.char_buf.len() as i64)); + ip += 1; continue; + } + _ => {} + } + let result = builtins::dispatch(name, *arity, &mut stack, &self.program_args, &mut self.global_state); + match result { + BuiltinResult::Handled | BuiltinResult::HttpServe => {} + BuiltinResult::Exit(code) => std::process::exit(code), + BuiltinResult::NotBuiltin => { + // Higher-order function helpers that need fn_table. + match name.as_str() { + "list_map" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, _ => vec![], + }; + let mut result_list = Vec::new(); + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + let mapped = self.call_fn(instructions, &fn_table, entry, vec![item]); + result_list.push(mapped); + } + } + stack.push(Value::List(result_list)); + } + "list_filter" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, _ => vec![], + }; + let mut result_list = Vec::new(); + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + let keep = self.call_fn(instructions, &fn_table, entry, vec![item.clone()]); + if matches!(keep, Value::Bool(true)) { + result_list.push(item); + } + } + } + stack.push(Value::List(result_list)); + } + "list_reduce" => { + let fn_name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + let init = stack.pop().unwrap_or(Value::Nil); + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, _ => vec![], + }; + let mut acc = init; + if let Some(&entry) = fn_table.get(&fn_name) { + for item in list { + acc = self.call_fn(instructions, &fn_table, entry, vec![acc, item]); + } + } + stack.push(acc); + } + "fn_ref" => { + let name_val = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + stack.push(Value::Str(name_val)); + } + _ => { + if let Some(&entry) = fn_table.get(name.as_str()) { + // Move (not clone) caller's locals into the frame; + // the callee starts with a fresh locals HashMap. + let saved = std::mem::take(&mut locals); + call_stack.push(CallFrame { return_ip: ip + 1, saved_locals: saved }); + ip = entry; + continue; + } + stack.push(Value::Nil); + } + } + } + } + } + + Bytecode::Return => { + if let Some(frame) = call_stack.pop() { + locals = frame.saved_locals; + ip = frame.return_ip; + continue; + } else { + break; + } + } + + // ── Control flow ────────────────────────────────────────────── + Bytecode::Jump(offset) => { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + Bytecode::JumpIf(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if matches!(cond, Value::Bool(true)) { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + } + Bytecode::JumpIfNot(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if !matches!(cond, Value::Bool(true)) { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + } + + // ── Fields & indexing ───────────────────────────────────────── + Bytecode::GetField(field) => { + let obj = stack.pop().unwrap_or(Value::Nil); + let v = match obj { + Value::Map(pairs) => pairs.iter() + .find(|(k, _)| k == field) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil), + Value::Struct { fields, .. } => fields.iter() + .find(|(n, _)| n == field) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil), + _ => Value::Nil, + }; + stack.push(v); + } + Bytecode::GetIndex => { + let idx = stack.pop().unwrap_or(Value::Nil); + let obj = stack.pop().unwrap_or(Value::Nil); + match (obj, idx) { + (Value::List(items), Value::Int(i)) => { + let v = if i >= 0 && (i as usize) < items.len() { + items[i as usize].clone() + } else { Value::Nil }; + stack.push(v); + } + (Value::Map(pairs), Value::Str(key)) => { + let v = pairs.iter() + .find(|(k, _)| k == &key) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil); + stack.push(v); + } + (Value::Str(s), Value::Int(i)) => { + let c = s.chars().nth(i as usize) + .map(|c| Value::Str(c.to_string())) + .unwrap_or(Value::Nil); + stack.push(c); + } + _ => stack.push(Value::Nil), + } + } + Bytecode::BuildMap(n) => { + let mut pairs = Vec::new(); + for _ in 0..*n { + let val = stack.pop().unwrap_or(Value::Nil); + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + other => other.to_string(), + }; + pairs.push((key, val)); + } + pairs.reverse(); + stack.push(Value::Map(pairs)); + } + Bytecode::BuildList(n) => { + let mut items: Vec = (0..*n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect(); + items.reverse(); + stack.push(Value::List(items)); + } + Bytecode::BuildStruct { type_name, fields } => { + let n = fields.len(); + let mut field_values: Vec = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect(); + field_values.reverse(); + let struct_fields: Vec<(String, Value)> = fields.iter().cloned() + .zip(field_values.into_iter()) + .collect(); + stack.push(Value::Struct { type_name: type_name.clone(), fields: struct_fields }); + } + Bytecode::SetField(field) => { + let val = stack.pop().unwrap_or(Value::Nil); + match stack.last_mut() { + Some(Value::Map(pairs)) => { + if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == field) { + entry.1 = val; + } else { + pairs.push((field.clone(), val)); + } + } + Some(Value::Struct { fields, .. }) => { + if let Some(entry) = fields.iter_mut().find(|(k, _)| k == field) { + entry.1 = val; + } else { + fields.push((field.clone(), val)); + } + } + _ => {} + } + } + + // ── Special instructions ────────────────────────────────────── + Bytecode::Halt => break, + Bytecode::Nop => {} + Bytecode::SealedBegin | Bytecode::SealedEnd => {} + Bytecode::Activate { type_name, query } => { + let results = engram_activate_search(type_name, query); + stack.push(Value::List(results)); + } + Bytecode::Reason { query } => { + let text = soma_reason(query); + stack.push(Value::Str(text)); + } + Bytecode::TraceBegin { label } => { + let start_ms = now_ms(); + locals.insert(format!("__trace_start_{label}__"), Value::Int(start_ms)); + } + Bytecode::TraceEnd { label } => { + let now = now_ms(); + let start = match locals.get(&format!("__trace_start_{label}__")) { + Some(Value::Int(n)) => *n, + _ => now, + }; + eprintln!("[trace] {label}: {}ms", now.saturating_sub(start)); + } + Bytecode::ContractCheck { message } => { + let cond = stack.pop().unwrap_or(Value::Nil); + if !matches!(cond, Value::Bool(true)) { + eprintln!("contract violation: {message}"); + std::process::exit(1); + } + } + Bytecode::Parallel { entries } => { + use std::sync::Arc; + let instr_arc = Arc::new(instructions.to_vec()); + let fn_arc = Arc::new(fn_table.clone()); + let mut handles: Vec<(String, std::thread::JoinHandle)> = Vec::new(); + for (name, entry_ip) in entries { + let instr_clone = instr_arc.clone(); + let ft_clone = fn_arc.clone(); + let ep = *entry_ip; + let h = std::thread::spawn(move || { + let mut sub_vm = ElVm::new(); + sub_vm.call_fn_with_table(&instr_clone, &ft_clone, ep, vec![]) + }); + handles.push((name.clone(), h)); + } + let pairs: Vec<(String, Value)> = handles.into_iter() + .map(|(name, h)| (name, h.join().unwrap_or(Value::Nil))) + .collect(); + stack.push(Value::Map(pairs)); + } + Bytecode::DeployFn { fn_name, route, target } => { + let soma_url = std::env::var("SOMA_URL") + .unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into()); + let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default(); + let body = serde_json::json!({ "fn_name": fn_name, "route": route, "target": target }); + let resp = reqwest::blocking::Client::new() + .post(format!("{soma_url}/v1/deploy")) + .header("Authorization", format!("Bearer {op_key}")) + .json(&body) + .send() + .and_then(|r| r.text()) + .unwrap_or_else(|e| format!("{{\"error\":\"{e}}}" )); + eprintln!("[deploy] {fn_name} -> {route} via {target}: {resp}"); + stack.push(Value::Str(resp)); + } + } + ip += 1; + } + + stack.pop().unwrap_or(Value::Nil) + } + + // ── Internal helpers ────────────────────────────────────────────────────── + + /// Call a user-defined function at `entry` IP with the given arguments. + /// + /// Arguments should be in left-to-right order; the function body will + /// store them via `StoreLocal` in reverse order. + pub fn call_fn( + &mut self, + instructions: &[Bytecode], + fn_table: &HashMap, + entry: usize, + args: Vec, + ) -> Value { + self.call_fn_with_table(instructions, fn_table, entry, args) + } + + fn call_fn_with_table( + &mut self, + instructions: &[Bytecode], + fn_table: &HashMap, + entry: usize, + args: Vec, + ) -> Value { + // Build a minimal initial stack from the args. + // The function body expects args to be on the stack (pushed left-to-right), + // then the body does StoreLocal in reverse parameter order. + let mut stack: Vec = args; + let mut locals: HashMap = HashMap::new(); + let mut call_stack: Vec = Vec::new(); + let mut ip = entry; + + while ip < instructions.len() { + match &instructions[ip] { + Bytecode::Push(v) => stack.push(v.clone()), + Bytecode::Pop => { stack.pop(); } + Bytecode::Dup => { + if let Some(top) = stack.last().cloned() { stack.push(top); } + } + Bytecode::Add => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x + y), + (Value::Float(x), Value::Float(y)) => Value::Float(x + y), + (Value::Str(x), Value::Str(y)) => Value::Str(x + &y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 + y), + (Value::Float(x), Value::Int(y)) => Value::Float(x + y as f64), + _ => Value::Nil, + }); + } + Bytecode::Sub => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x - y), + (Value::Float(x), Value::Float(y)) => Value::Float(x - y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 - y), + (Value::Float(x), Value::Int(y)) => Value::Float(x - y as f64), + _ => Value::Nil, + }); + } + Bytecode::Mul => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x * y), + (Value::Float(x), Value::Float(y)) => Value::Float(x * y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 * y), + (Value::Float(x), Value::Int(y)) => Value::Float(x * y as f64), + _ => Value::Nil, + }); + } + Bytecode::Div => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) if y != 0 => Value::Int(x / y), + (Value::Float(x), Value::Float(y)) => Value::Float(x / y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 / y), + (Value::Float(x), Value::Int(y)) => Value::Float(x / y as f64), + _ => Value::Nil, + }); + } + Bytecode::Mod => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(1)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => if y == 0 { Value::Int(0) } else { Value::Int(x % y) }, + (Value::Float(x), Value::Float(y)) => Value::Float(x % y), + (Value::Int(x), Value::Float(y)) => Value::Float(x as f64 % y), + (Value::Float(x), Value::Int(y)) => Value::Float(x % y as f64), + _ => Value::Int(0), + }); + } + Bytecode::BitAnd => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x & y), _ => Value::Int(0) }); + } + Bytecode::BitOr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x | y), _ => Value::Int(0) }); + } + Bytecode::BitXor => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x ^ y), _ => Value::Int(0) }); + } + Bytecode::BitNot => { + let a = stack.pop().unwrap_or(Value::Int(0)); + stack.push(match a { Value::Int(x) => Value::Int(!x), _ => Value::Int(0) }); + } + Bytecode::Shl => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x << (y as u32)), _ => Value::Int(0) }); + } + Bytecode::Shr => { + let (b, a) = (stack.pop().unwrap_or(Value::Int(0)), stack.pop().unwrap_or(Value::Int(0))); + stack.push(match (a, b) { (Value::Int(x), Value::Int(y)) => Value::Int(x >> (y as u32)), _ => Value::Int(0) }); + } + Bytecode::Eq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a == b)); + } + Bytecode::NotEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a != b)); + } + Bytecode::Lt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Less)); + } + Bytecode::Gt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Greater)); + } + Bytecode::LtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Greater)); + } + Bytecode::GtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Less)); + } + Bytecode::And => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) && matches!(b, Value::Bool(true)) + )); + } + Bytecode::Or => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) || matches!(b, Value::Bool(true)) + )); + } + Bytecode::Not => { + let v = stack.pop().unwrap_or(Value::Nil); + stack.push(Value::Bool(!matches!(v, Value::Bool(true)))); + } + Bytecode::StoreLocal(name) => { + let v = stack.pop().unwrap_or(Value::Nil); + locals.insert(name.clone(), v); + } + Bytecode::LoadLocal(name) => { + let v = locals.get(name).cloned().unwrap_or(Value::Nil); + stack.push(v); + } + Bytecode::Call { name, arity } => { + // ── native_instr_* builtins — global instruction accumulator ─ + match name.as_str() { + "native_instr_push" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, v => v.to_string(), + }; + self.instr_buf.push(s); + stack.push(Value::Nil); + ip += 1; continue; + } + "native_instr_len" => { + stack.push(Value::Int(self.instr_buf.len() as i64)); + ip += 1; continue; + } + "native_instr_get" => { + let idx = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.instr_buf.get(idx) + .cloned() + .map(Value::Str) + .unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_instr_patch" => { + let new_instr = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, v => v.to_string(), + }; + let idx = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + if idx < self.instr_buf.len() { + self.instr_buf[idx] = new_instr; + } + stack.push(Value::Nil); + ip += 1; continue; + } + "native_instr_all" => { + let list: Vec = self.instr_buf.iter() + .map(|s| Value::Str(s.clone())) + .collect(); + stack.push(Value::List(list)); + ip += 1; continue; + } + "native_instr_reset" => { + self.instr_buf.clear(); + stack.push(Value::Nil); + ip += 1; continue; + } + // ── native_token_* builtins — global token buffer ───────── + "native_tokens_init" => { + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, _ => vec![], + }; + self.token_buf = list; + stack.push(Value::Nil); + ip += 1; continue; + } + "native_token_at" => { + let pos = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.token_buf.get(pos).cloned().unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_token_len" => { + stack.push(Value::Int(self.token_buf.len() as i64)); + ip += 1; continue; + } + // ── native_char_* builtins — global char buffer ─────────── + "native_chars_init" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + self.char_buf = s.chars().collect(); + stack.push(Value::Nil); + ip += 1; continue; + } + "native_char_at" => { + let i = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(i) => i as usize, _ => 0, + }; + let v = self.char_buf.get(i) + .map(|c| Value::Str(c.to_string())) + .unwrap_or(Value::Nil); + stack.push(v); + ip += 1; continue; + } + "native_char_len" => { + stack.push(Value::Int(self.char_buf.len() as i64)); + ip += 1; continue; + } + _ => {} + } + let result = builtins::dispatch(name, *arity, &mut stack, &self.program_args, &mut self.global_state); + match result { + BuiltinResult::Handled | BuiltinResult::HttpServe => {} + BuiltinResult::Exit(code) => std::process::exit(code), + BuiltinResult::NotBuiltin => { + match name.as_str() { + "fn_ref" => { + let n = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, _ => String::new(), + }; + stack.push(Value::Str(n)); + } + _ => { + if let Some(&entry2) = fn_table.get(name.as_str()) { + // Move (not clone) caller's locals into the frame. + let saved = std::mem::take(&mut locals); + call_stack.push(CallFrame { return_ip: ip + 1, saved_locals: saved }); + ip = entry2; + continue; + } + stack.push(Value::Nil); + } + } + } + } + } + Bytecode::Return => { + if let Some(frame) = call_stack.pop() { + locals = frame.saved_locals; + ip = frame.return_ip; + continue; + } else { + break; + } + } + Bytecode::Jump(offset) => { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + Bytecode::JumpIf(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if matches!(cond, Value::Bool(true)) { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + } + Bytecode::JumpIfNot(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if !matches!(cond, Value::Bool(true)) { + ip = (ip as i32 + 1 + offset) as usize; + continue; + } + } + Bytecode::GetField(field) => { + let obj = stack.pop().unwrap_or(Value::Nil); + let v = match obj { + Value::Map(pairs) => pairs.iter() + .find(|(k, _)| k == field) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil), + Value::Struct { fields, .. } => fields.iter() + .find(|(n, _)| n == field) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil), + _ => Value::Nil, + }; + stack.push(v); + } + Bytecode::GetIndex => { + let idx = stack.pop().unwrap_or(Value::Nil); + let obj = stack.pop().unwrap_or(Value::Nil); + match (obj, idx) { + (Value::List(items), Value::Int(i)) => { + let v = if i >= 0 && (i as usize) < items.len() { + items[i as usize].clone() + } else { Value::Nil }; + stack.push(v); + } + (Value::Map(pairs), Value::Str(key)) => { + let v = pairs.iter() + .find(|(k, _)| k == &key) + .map(|(_, v)| v.clone()) + .unwrap_or(Value::Nil); + stack.push(v); + } + (Value::Str(s), Value::Int(i)) => { + let c = s.chars().nth(i as usize) + .map(|c| Value::Str(c.to_string())) + .unwrap_or(Value::Nil); + stack.push(c); + } + _ => stack.push(Value::Nil), + } + } + Bytecode::BuildMap(n) => { + let mut pairs = Vec::new(); + for _ in 0..*n { + let val = stack.pop().unwrap_or(Value::Nil); + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, other => other.to_string(), + }; + pairs.push((key, val)); + } + pairs.reverse(); + stack.push(Value::Map(pairs)); + } + Bytecode::BuildList(n) => { + let mut items: Vec = (0..*n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect(); + items.reverse(); + stack.push(Value::List(items)); + } + Bytecode::BuildStruct { type_name, fields } => { + let n = fields.len(); + let mut field_values: Vec = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect(); + field_values.reverse(); + let struct_fields: Vec<(String, Value)> = fields.iter().cloned() + .zip(field_values.into_iter()) + .collect(); + stack.push(Value::Struct { type_name: type_name.clone(), fields: struct_fields }); + } + Bytecode::SetField(field) => { + let val = stack.pop().unwrap_or(Value::Nil); + match stack.last_mut() { + Some(Value::Map(pairs)) => { + if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == field) { + entry.1 = val; + } else { + pairs.push((field.clone(), val)); + } + } + Some(Value::Struct { fields, .. }) => { + if let Some(entry) = fields.iter_mut().find(|(k, _)| k == field) { + entry.1 = val; + } else { + fields.push((field.clone(), val)); + } + } + _ => {} + } + } + Bytecode::Halt => break, + Bytecode::Nop => {} + Bytecode::SealedBegin | Bytecode::SealedEnd => {} + Bytecode::Activate { type_name, query } => { + let results = engram_activate_search(type_name, query); + stack.push(Value::List(results)); + } + Bytecode::Reason { query } => { + let text = soma_reason(query); + stack.push(Value::Str(text)); + } + Bytecode::TraceBegin { label } => { + let ms = now_ms(); + locals.insert(format!("__trace_start_{label}__"), Value::Int(ms)); + } + Bytecode::TraceEnd { label } => { + let now = now_ms(); + let start = match locals.get(&format!("__trace_start_{label}__")) { + Some(Value::Int(n)) => *n, _ => now, + }; + eprintln!("[trace] {label}: {}ms", now.saturating_sub(start)); + } + Bytecode::ContractCheck { message } => { + let cond = stack.pop().unwrap_or(Value::Nil); + if !matches!(cond, Value::Bool(true)) { + eprintln!("contract violation: {message}"); + std::process::exit(1); + } + } + _ => {} + } + ip += 1; + } + + stack.pop().unwrap_or(Value::Nil) + } + + // ── Scan pass ───────────────────────────────────────────────────────────── + + /// Scan the instruction stream to build a function name → entry-IP table. + /// + /// The codegen emits `Push(Int(entry)) StoreLocal("__fn_")` stanzas + /// immediately after each function body. This pass collects them. + pub fn scan_fn_table(instructions: &[Bytecode]) -> HashMap { + let mut table = HashMap::new(); + let mut i = 0usize; + while i < instructions.len() { + if let Bytecode::Push(Value::Int(n)) = &instructions[i] { + if i + 1 < instructions.len() { + if let Bytecode::StoreLocal(name) = &instructions[i + 1] { + if let Some(fn_name) = name.strip_prefix("__fn_") { + table.insert(fn_name.to_string(), *n as usize); + } + } + } + } + i += 1; + } + table + } +} + +// ── Utility functions ───────────────────────────────────────────────────────── + +fn cmp_values(a: &Value, b: &Value) -> std::cmp::Ordering { + match (a, b) { + (Value::Int(x), Value::Int(y)) => x.cmp(y), + (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal), + (Value::Int(x), Value::Float(y)) => (*x as f64).partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal), + (Value::Float(x), Value::Int(y)) => x.partial_cmp(&(*y as f64)).unwrap_or(std::cmp::Ordering::Equal), + (Value::Str(x), Value::Str(y)) => x.cmp(y), + _ => std::cmp::Ordering::Equal, + } +} + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0) +} + +fn engram_activate_search(type_name: &str, query: &str) -> Vec { + let engram_url = std::env::var("ENGRAM_URL") + .unwrap_or_else(|_| "http://localhost:8742".to_string()); + let api_key = std::env::var("ENGRAM_API_KEY").unwrap_or_default(); + let body = serde_json::json!({ "query": query, "limit": 20 }).to_string(); + let client = reqwest::blocking::Client::new(); + let mut req = client.post(format!("{engram_url}/search")) + .header("Content-Type", "application/json") + .body(body); + if !api_key.is_empty() { + req = req.header("Authorization", format!("Bearer {api_key}")); + } + req.send() + .and_then(|r| r.json::()) + .ok() + .and_then(|v| v.as_array().cloned()) + .unwrap_or_default() + .iter() + .map(json_to_value) + .collect() +} + +fn soma_reason(query: &str) -> String { + let soma_url = std::env::var("SOMA_URL") + .unwrap_or_else(|_| "https://neuron.neurontechnologies.ai".into()); + let op_key = std::env::var("SOMA_OPERATOR_KEY").unwrap_or_default(); + let body = serde_json::json!({ + "model": "neuron", + "messages": [{"role": "user", "content": query}], + "max_tokens": 500 + }); + reqwest::blocking::Client::new() + .post(format!("{soma_url}/v1/chat/completions")) + .header("Authorization", format!("Bearer {op_key}")) + .json(&body) + .send() + .and_then(|r| r.json::()) + .ok() + .and_then(|v| v["choices"][0]["message"]["content"].as_str().map(|s| s.to_string())) + .unwrap_or_else(|| "[soma unavailable]".into()) +} + +pub fn json_to_value(v: &serde_json::Value) -> Value { + match v { + serde_json::Value::String(s) => Value::Str(s.clone()), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { Value::Int(i) } + else if let Some(f) = n.as_f64() { Value::Float(f) } + else { Value::Str(n.to_string()) } + } + serde_json::Value::Bool(b) => Value::Bool(*b), + serde_json::Value::Null => Value::Nil, + serde_json::Value::Array(a) => Value::List(a.iter().map(json_to_value).collect()), + serde_json::Value::Object(o) => { + let fields = o.iter().map(|(k, v)| (k.clone(), json_to_value(v))).collect(); + Value::Struct { type_name: "Object".to_string(), fields } + } + } +} diff --git a/engrams/el-wasm/Cargo.toml b/_archive/rust-bootstrap/engrams/el-wasm/Cargo.toml similarity index 100% rename from engrams/el-wasm/Cargo.toml rename to _archive/rust-bootstrap/engrams/el-wasm/Cargo.toml diff --git a/engrams/el-wasm/build.sh b/_archive/rust-bootstrap/engrams/el-wasm/build.sh similarity index 100% rename from engrams/el-wasm/build.sh rename to _archive/rust-bootstrap/engrams/el-wasm/build.sh diff --git a/engrams/el-wasm/js/pwa-manifest.json b/_archive/rust-bootstrap/engrams/el-wasm/js/pwa-manifest.json similarity index 100% rename from engrams/el-wasm/js/pwa-manifest.json rename to _archive/rust-bootstrap/engrams/el-wasm/js/pwa-manifest.json diff --git a/engrams/el-wasm/js/runtime.js b/_archive/rust-bootstrap/engrams/el-wasm/js/runtime.js similarity index 100% rename from engrams/el-wasm/js/runtime.js rename to _archive/rust-bootstrap/engrams/el-wasm/js/runtime.js diff --git a/engrams/el-wasm/js/service-worker.js b/_archive/rust-bootstrap/engrams/el-wasm/js/service-worker.js similarity index 100% rename from engrams/el-wasm/js/service-worker.js rename to _archive/rust-bootstrap/engrams/el-wasm/js/service-worker.js diff --git a/engrams/el-wasm/src/lib.rs b/_archive/rust-bootstrap/engrams/el-wasm/src/lib.rs similarity index 100% rename from engrams/el-wasm/src/lib.rs rename to _archive/rust-bootstrap/engrams/el-wasm/src/lib.rs diff --git a/dist/elvm/elvm-aarch64-apple-darwin b/dist/elvm/elvm-aarch64-apple-darwin new file mode 100755 index 0000000..20b7360 Binary files /dev/null and b/dist/elvm/elvm-aarch64-apple-darwin differ diff --git a/dist/platform/el-macos-universal b/dist/platform/el-macos-universal new file mode 100755 index 0000000..bcd3149 Binary files /dev/null and b/dist/platform/el-macos-universal differ diff --git a/dist/platform/el-windows-x86_64.exe b/dist/platform/el-windows-x86_64.exe new file mode 100755 index 0000000..4938096 Binary files /dev/null and b/dist/platform/el-windows-x86_64.exe differ diff --git a/el-compiler/el.toml b/el-compiler/el.toml deleted file mode 100644 index 27f70ae..0000000 --- a/el-compiler/el.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "el-compiler" -version = "0.1.0" -description = "el self-hosting compiler — lexer, parser, codegen" -edition = "2026" - -[build] -entry = "src/compiler.el" diff --git a/el-compiler/manifest.el b/el-compiler/manifest.el new file mode 100644 index 0000000..bd85a5d --- /dev/null +++ b/el-compiler/manifest.el @@ -0,0 +1,9 @@ +package "el-compiler" { + version "0.1.0" + description "el self-hosting compiler — lexer, parser, codegen" + edition "2026" +} + +build { + entry "src/compiler.el" +} diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 9eea1b9..ee3b7e0 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -7,6 +7,11 @@ // See the el-compiler/src/bytecode.rs for the canonical enum. // // Entry point: fn codegen(stmts: [Map], source: String) -> String +// +// Performance: instruction accumulation uses the VM-native global buffer +// (native_instr_push / native_instr_len / native_instr_patch / native_instr_all) +// instead of passing a growing list through function arguments. This avoids O(n²) +// cloning and keeps compilation time linear in the number of instructions. // ── JSON helpers ────────────────────────────────────────────────────────────── @@ -59,265 +64,237 @@ fn json_bool(b: Bool) -> String { // ── Codegen state ───────────────────────────────────────────────────────────── // -// el has no mutable globals. We thread a "ctx" map through all codegen -// functions. The context holds: -// "instrs" -> [String] — JSON strings for each emitted instruction -// "patches" -> [Map] — pending forward-jump patches -// each patch: { "idx": Int, "kind": String } -// (kind is "JumpIfNot" / "Jump" — we store the instruction index and -// patch it at the end of the construct) - -fn ctx_new() -> Map { - { "instrs": native_list_empty(), "patches": native_list_empty() } -} - -fn ctx_emit(ctx: Map, instr_json: String) -> Map { - let instrs = ctx["instrs"] - let instrs = native_list_append(instrs, instr_json) - { "instrs": instrs, "patches": ctx["patches"] } -} - -fn ctx_len(ctx: Map) -> Int { - let instrs = ctx["instrs"] - native_list_len(instrs) -} - -// Patch a previously-emitted placeholder instruction at index idx. -// For Jump/JumpIf/JumpIfNot, we need to replace the entry in instrs. -// We rebuild the list with the patched value at position idx. -fn ctx_patch(ctx: Map, idx: Int, dest: Int) -> Map { - // offset = dest - (idx + 1) - let offset = dest - (idx + 1) - let instrs = ctx["instrs"] - let total = native_list_len(instrs) - let new_instrs: [String] = native_list_empty() - let i = 0 - while i < total { - let instr: String = native_list_get(instrs, i) - if i == idx { - // Replace: determine kind from original instruction string - if native_string_contains(instr, "JumpIfNot") { - let new_instrs = native_list_append(new_instrs, "{\"JumpIfNot\":" + json_int(offset) + "}") - } else { - if native_string_contains(instr, "JumpIf") { - let new_instrs = native_list_append(new_instrs, "{\"JumpIf\":" + json_int(offset) + "}") - } else { - let new_instrs = native_list_append(new_instrs, "{\"Jump\":" + json_int(offset) + "}") - } - } - } else { - let new_instrs = native_list_append(new_instrs, instr) - } - let i = i + 1 - } - { "instrs": new_instrs, "patches": ctx["patches"] } -} +// The instruction accumulator is stored in the VM's global instr_buf. +// native_instr_push(s) — append JSON instruction string +// native_instr_len() — current instruction count +// native_instr_get(i) — get instruction at index i +// native_instr_patch(i,s) — replace instruction at index i +// native_instr_all() — return full list as [String] // ── Instruction emitters ────────────────────────────────────────────────────── -fn emit_halt(ctx: Map) -> Map { - ctx_emit(ctx, "\"Halt\"") +fn emit_halt() -> Void { + native_instr_push("\"Halt\"") } -fn emit_nop(ctx: Map) -> Map { - ctx_emit(ctx, "\"Nop\"") +fn emit_pop() -> Void { + native_instr_push("\"Pop\"") } -fn emit_pop(ctx: Map) -> Map { - ctx_emit(ctx, "\"Pop\"") +fn emit_return() -> Void { + native_instr_push("\"Return\"") } -fn emit_return(ctx: Map) -> Map { - ctx_emit(ctx, "\"Return\"") +fn emit_add() -> Void { + native_instr_push("\"Add\"") } -fn emit_add(ctx: Map) -> Map { - ctx_emit(ctx, "\"Add\"") +fn emit_sub() -> Void { + native_instr_push("\"Sub\"") } -fn emit_sub(ctx: Map) -> Map { - ctx_emit(ctx, "\"Sub\"") +fn emit_mul() -> Void { + native_instr_push("\"Mul\"") } -fn emit_mul(ctx: Map) -> Map { - ctx_emit(ctx, "\"Mul\"") +fn emit_div() -> Void { + native_instr_push("\"Div\"") } -fn emit_div(ctx: Map) -> Map { - ctx_emit(ctx, "\"Div\"") +fn emit_eq() -> Void { + native_instr_push("\"Eq\"") } -fn emit_eq(ctx: Map) -> Map { - ctx_emit(ctx, "\"Eq\"") +fn emit_not_eq() -> Void { + native_instr_push("\"NotEq\"") } -fn emit_not_eq(ctx: Map) -> Map { - ctx_emit(ctx, "\"NotEq\"") +fn emit_lt() -> Void { + native_instr_push("\"Lt\"") } -fn emit_lt(ctx: Map) -> Map { - ctx_emit(ctx, "\"Lt\"") +fn emit_gt() -> Void { + native_instr_push("\"Gt\"") } -fn emit_gt(ctx: Map) -> Map { - ctx_emit(ctx, "\"Gt\"") +fn emit_lt_eq() -> Void { + native_instr_push("\"LtEq\"") } -fn emit_lt_eq(ctx: Map) -> Map { - ctx_emit(ctx, "\"LtEq\"") +fn emit_gt_eq() -> Void { + native_instr_push("\"GtEq\"") } -fn emit_gt_eq(ctx: Map) -> Map { - ctx_emit(ctx, "\"GtEq\"") +fn emit_and() -> Void { + native_instr_push("\"And\"") } -fn emit_and(ctx: Map) -> Map { - ctx_emit(ctx, "\"And\"") +fn emit_or() -> Void { + native_instr_push("\"Or\"") } -fn emit_or(ctx: Map) -> Map { - ctx_emit(ctx, "\"Or\"") +fn emit_not() -> Void { + native_instr_push("\"Not\"") } -fn emit_not(ctx: Map) -> Map { - ctx_emit(ctx, "\"Not\"") +fn emit_get_index() -> Void { + native_instr_push("\"GetIndex\"") } -fn emit_get_index(ctx: Map) -> Map { - ctx_emit(ctx, "\"GetIndex\"") +fn emit_push_nil() -> Void { + native_instr_push("{\"Push\":\"Nil\"}") } -fn emit_push_nil(ctx: Map) -> Map { - ctx_emit(ctx, "{\"Push\":\"Nil\"}") +fn emit_push_int(n: Int) -> Void { + native_instr_push("{\"Push\":{\"Int\":" + json_int(n) + "}}") } -fn emit_push_int(ctx: Map, n: Int) -> Map { - ctx_emit(ctx, "{\"Push\":{\"Int\":" + json_int(n) + "}}") +fn emit_push_bool(b: Bool) -> Void { + native_instr_push("{\"Push\":{\"Bool\":" + json_bool(b) + "}}") } -fn emit_push_bool(ctx: Map, b: Bool) -> Map { - ctx_emit(ctx, "{\"Push\":{\"Bool\":" + json_bool(b) + "}}") +fn emit_push_str(s: String) -> Void { + native_instr_push("{\"Push\":{\"Str\":" + json_str(s) + "}}") } -fn emit_push_str(ctx: Map, s: String) -> Map { - ctx_emit(ctx, "{\"Push\":{\"Str\":" + json_str(s) + "}}") +fn emit_load(name: String) -> Void { + native_instr_push("{\"LoadLocal\":" + json_str(name) + "}") } -fn emit_load(ctx: Map, name: String) -> Map { - ctx_emit(ctx, "{\"LoadLocal\":" + json_str(name) + "}") +fn emit_store(name: String) -> Void { + native_instr_push("{\"StoreLocal\":" + json_str(name) + "}") } -fn emit_store(ctx: Map, name: String) -> Map { - ctx_emit(ctx, "{\"StoreLocal\":" + json_str(name) + "}") +fn emit_call(name: String, arity: Int) -> Void { + native_instr_push("{\"Call\":{\"name\":" + json_str(name) + ",\"arity\":" + json_int(arity) + "}}") } -fn emit_call(ctx: Map, name: String, arity: Int) -> Map { - ctx_emit(ctx, "{\"Call\":{\"name\":" + json_str(name) + ",\"arity\":" + json_int(arity) + "}}") +fn emit_get_field(field: String) -> Void { + native_instr_push("{\"GetField\":" + json_str(field) + "}") } -fn emit_get_field(ctx: Map, field: String) -> Map { - ctx_emit(ctx, "{\"GetField\":" + json_str(field) + "}") +fn emit_build_map(n: Int) -> Void { + native_instr_push("{\"BuildMap\":" + json_int(n) + "}") } -fn emit_build_map(ctx: Map, n: Int) -> Map { - ctx_emit(ctx, "{\"BuildMap\":" + json_int(n) + "}") +fn emit_build_list(n: Int) -> Void { + native_instr_push("{\"BuildList\":" + json_int(n) + "}") } -fn emit_build_list(ctx: Map, n: Int) -> Map { - ctx_emit(ctx, "{\"BuildList\":" + json_int(n) + "}") +// Emit a placeholder Jump and return its index (for later patching). +fn emit_jump_placeholder() -> Int { + let idx: Int = native_instr_len() + native_instr_push("{\"Jump\":0}") + idx } -// Emit a placeholder Jump and return its index (for later patching) -fn emit_jump_placeholder(ctx: Map) -> Map { - let idx = ctx_len(ctx) - let ctx = ctx_emit(ctx, "{\"Jump\":0}") - { "ctx": ctx, "idx": idx } +fn emit_jump_if_not_placeholder() -> Int { + let idx: Int = native_instr_len() + native_instr_push("{\"JumpIfNot\":0}") + idx } -fn emit_jump_if_not_placeholder(ctx: Map) -> Map { - let idx = ctx_len(ctx) - let ctx = ctx_emit(ctx, "{\"JumpIfNot\":0}") - { "ctx": ctx, "idx": idx } +fn emit_jump_to(dest: Int) -> Void { + let here: Int = native_instr_len() + let offset: Int = dest - (here + 1) + native_instr_push("{\"Jump\":" + json_int(offset) + "}") } -fn emit_jump_to(ctx: Map, dest: Int) -> Map { - let here = ctx_len(ctx) - let offset = dest - (here + 1) - ctx_emit(ctx, "{\"Jump\":" + json_int(offset) + "}") +// Patch a previously-emitted placeholder jump instruction. +fn patch_instr(idx: Int, dest: Int) -> Void { + let offset: Int = dest - (idx + 1) + let original: String = native_instr_get(idx) + if native_string_contains(original, "JumpIfNot") { + native_instr_patch(idx, "{\"JumpIfNot\":" + json_int(offset) + "}") + } else { + if native_string_contains(original, "JumpIf") { + native_instr_patch(idx, "{\"JumpIf\":" + json_int(offset) + "}") + } else { + native_instr_patch(idx, "{\"Jump\":" + json_int(offset) + "}") + } + } } // ── Expression codegen ──────────────────────────────────────────────────────── -fn cg_expr(ctx: Map, expr: Map) -> Map { +fn cg_expr(expr: Map) -> Void { let kind: String = expr["expr"] if kind == "Int" { let v: String = expr["value"] let n: Int = native_str_to_int(v) - return emit_push_int(ctx, n) + emit_push_int(n) + return } if kind == "Float" { let v: String = expr["value"] - // Push as string for now — VM handles float parsing from Str - return emit_push_str(ctx, v) + emit_push_str(v) + return } if kind == "Str" { let v: String = expr["value"] - return emit_push_str(ctx, v) + emit_push_str(v) + return } if kind == "Bool" { let v: String = expr["value"] if v == "true" { - return emit_push_bool(ctx, true) + emit_push_bool(true) + } else { + emit_push_bool(false) } - return emit_push_bool(ctx, false) + return } if kind == "Nil" { - return emit_push_nil(ctx) + emit_push_nil() + return } if kind == "Ident" { let name: String = expr["name"] - return emit_load(ctx, name) + emit_load(name) + return } if kind == "Not" { let inner = expr["inner"] - let ctx = cg_expr(ctx, inner) - return emit_not(ctx) + cg_expr(inner) + emit_not() + return } if kind == "Neg" { // unary minus: push 0, push inner, sub - let ctx = emit_push_int(ctx, 0) + emit_push_int(0) let inner = expr["inner"] - let ctx = cg_expr(ctx, inner) - return emit_sub(ctx) + cg_expr(inner) + emit_sub() + return } if kind == "BinOp" { let op: String = expr["op"] let left = expr["left"] let right = expr["right"] - let ctx = cg_expr(ctx, left) - let ctx = cg_expr(ctx, right) - if op == "Plus" { return emit_add(ctx) } - if op == "Minus" { return emit_sub(ctx) } - if op == "Star" { return emit_mul(ctx) } - if op == "Slash" { return emit_div(ctx) } - if op == "EqEq" { return emit_eq(ctx) } - if op == "NotEq" { return emit_not_eq(ctx) } - if op == "Lt" { return emit_lt(ctx) } - if op == "Gt" { return emit_gt(ctx) } - if op == "LtEq" { return emit_lt_eq(ctx) } - if op == "GtEq" { return emit_gt_eq(ctx) } - if op == "And" { return emit_and(ctx) } - if op == "Or" { return emit_or(ctx) } - return ctx + cg_expr(left) + cg_expr(right) + if op == "Plus" { emit_add() } + if op == "Minus" { emit_sub() } + if op == "Star" { emit_mul() } + if op == "Slash" { emit_div() } + if op == "EqEq" { emit_eq() } + if op == "NotEq" { emit_not_eq() } + if op == "Lt" { emit_lt() } + if op == "Gt" { emit_gt() } + if op == "LtEq" { emit_lt_eq() } + if op == "GtEq" { emit_gt_eq() } + if op == "And" { emit_and() } + if op == "Or" { emit_or() } + return } if kind == "Call" { @@ -328,37 +305,42 @@ fn cg_expr(ctx: Map, expr: Map) -> Map { let i = 0 while i < arity { let arg = native_list_get(args, i) - let ctx = cg_expr(ctx, arg) + cg_expr(arg) let i = i + 1 } // get function name from func expr let func_kind: String = func["expr"] if func_kind == "Ident" { let fn_name: String = func["name"] - return emit_call(ctx, fn_name, arity) + emit_call(fn_name, arity) + return } if func_kind == "Field" { let obj = func["object"] let field: String = func["field"] - let ctx = cg_expr(ctx, obj) - return emit_call(ctx, field, arity + 1) + cg_expr(obj) + emit_call(field, arity + 1) + return } - return emit_call(ctx, "__dynamic__", arity) + emit_call("__dynamic__", arity) + return } if kind == "Field" { let obj = expr["object"] let field: String = expr["field"] - let ctx = cg_expr(ctx, obj) - return emit_get_field(ctx, field) + cg_expr(obj) + emit_get_field(field) + return } if kind == "Index" { let obj = expr["object"] let idx = expr["index"] - let ctx = cg_expr(ctx, obj) - let ctx = cg_expr(ctx, idx) - return emit_get_index(ctx) + cg_expr(obj) + cg_expr(idx) + emit_get_index() + return } if kind == "Array" { @@ -367,10 +349,11 @@ fn cg_expr(ctx: Map, expr: Map) -> Map { let i = 0 while i < n { let elem = native_list_get(elems, i) - let ctx = cg_expr(ctx, elem) + cg_expr(elem) let i = i + 1 } - return emit_build_list(ctx, n) + emit_build_list(n) + return } if kind == "Map" { @@ -381,11 +364,12 @@ fn cg_expr(ctx: Map, expr: Map) -> Map { let pair = native_list_get(pairs, i) let key: String = pair["key"] let val = pair["value"] - let ctx = emit_push_str(ctx, key) - let ctx = cg_expr(ctx, val) + emit_push_str(key) + cg_expr(val) let i = i + 1 } - return emit_build_map(ctx, n) + emit_build_map(n) + return } if kind == "If" { @@ -394,41 +378,36 @@ fn cg_expr(ctx: Map, expr: Map) -> Map { let else_stmts = expr["else"] let has_else: Bool = expr["has_else"] // cond - let ctx = cg_expr(ctx, cond) + cg_expr(cond) // JumpIfNot placeholder - let r = emit_jump_if_not_placeholder(ctx) - let ctx = r["ctx"] - let jump_false_idx: Int = r["idx"] + let jump_false_idx: Int = emit_jump_if_not_placeholder() // then body - let ctx = cg_stmts(ctx, then_stmts) + cg_stmts(then_stmts) if has_else { // jump over else - let r2 = emit_jump_placeholder(ctx) - let ctx = r2["ctx"] - let jump_end_idx: Int = r2["idx"] + let jump_end_idx: Int = emit_jump_placeholder() // patch jump_false to here - let else_start = ctx_len(ctx) - let ctx = ctx_patch(ctx, jump_false_idx, else_start) + let else_start: Int = native_instr_len() + patch_instr(jump_false_idx, else_start) // else body - let ctx = cg_stmts(ctx, else_stmts) + cg_stmts(else_stmts) // patch jump_end to here - let after_else = ctx_len(ctx) - let ctx = ctx_patch(ctx, jump_end_idx, after_else) - return ctx + let after_else: Int = native_instr_len() + patch_instr(jump_end_idx, after_else) } else { - let after_then = ctx_len(ctx) - let ctx = ctx_patch(ctx, jump_false_idx, after_then) - return ctx + let after_then: Int = native_instr_len() + patch_instr(jump_false_idx, after_then) } + return } if kind == "Match" { let subject = expr["subject"] let arms = expr["arms"] let n_arms: Int = native_list_len(arms) - let ctx = cg_expr(ctx, subject) + cg_expr(subject) // store subject in temp var - let ctx = emit_store(ctx, "__match_subj__") + emit_store("__match_subj__") let end_jump_idxs: [Int] = native_list_empty() let i = 0 while i < n_arms { @@ -438,155 +417,187 @@ fn cg_expr(ctx: Map, expr: Map) -> Map { let pat_kind: String = pattern["pattern"] if pat_kind == "Wildcard" { // always matches — just emit body - let ctx = cg_expr(ctx, body) - let r = emit_jump_placeholder(ctx) - let ctx = r["ctx"] - let jidx: Int = r["idx"] + cg_expr(body) + let jidx: Int = emit_jump_placeholder() let end_jump_idxs = native_list_append(end_jump_idxs, jidx) } else { if pat_kind == "Binding" { let bind_name: String = pattern["name"] - let ctx = emit_load(ctx, "__match_subj__") - let ctx = emit_store(ctx, bind_name) - let ctx = cg_expr(ctx, body) - let r = emit_jump_placeholder(ctx) - let ctx = r["ctx"] - let jidx: Int = r["idx"] + emit_load("__match_subj__") + emit_store(bind_name) + cg_expr(body) + let jidx: Int = emit_jump_placeholder() let end_jump_idxs = native_list_append(end_jump_idxs, jidx) } else { // literal pattern: compare subject to literal - let ctx = emit_load(ctx, "__match_subj__") + emit_load("__match_subj__") if pat_kind == "LitInt" { let v: String = pattern["value"] let n: Int = native_str_to_int(v) - let ctx = emit_push_int(ctx, n) + emit_push_int(n) } else { if pat_kind == "LitStr" { let v: String = pattern["value"] - let ctx = emit_push_str(ctx, v) + emit_push_str(v) } else { if pat_kind == "LitBool" { let v: String = pattern["value"] if v == "true" { - let ctx = emit_push_bool(ctx, true) + emit_push_bool(true) } else { - let ctx = emit_push_bool(ctx, false) + emit_push_bool(false) } } else { - let ctx = emit_push_nil(ctx) + emit_push_nil() } } } - let ctx = emit_eq(ctx) - let r = emit_jump_if_not_placeholder(ctx) - let ctx = r["ctx"] - let no_match_idx: Int = r["idx"] - let ctx = cg_expr(ctx, body) - let r2 = emit_jump_placeholder(ctx) - let ctx = r2["ctx"] - let jidx: Int = r2["idx"] + emit_eq() + let no_match_idx: Int = emit_jump_if_not_placeholder() + cg_expr(body) + let jidx: Int = emit_jump_placeholder() let end_jump_idxs = native_list_append(end_jump_idxs, jidx) - let next_arm = ctx_len(ctx) - let ctx = ctx_patch(ctx, no_match_idx, next_arm) + let next_arm: Int = native_instr_len() + patch_instr(no_match_idx, next_arm) } } let i = i + 1 } // default: push nil - let ctx = emit_push_nil(ctx) - let end_pos = ctx_len(ctx) + emit_push_nil() + let end_pos: Int = native_instr_len() // patch all end jumps let n_end: Int = native_list_len(end_jump_idxs) let j = 0 while j < n_end { let jidx: Int = native_list_get(end_jump_idxs, j) - let ctx = ctx_patch(ctx, jidx, end_pos) + patch_instr(jidx, end_pos) let j = j + 1 } - return ctx + return } if kind == "For" { // for item in list { body } - // Implementation: - // __for_list__ = list - // __for_len__ = len(list) - // __for_i__ = 0 - // loop_start: - // if __for_i__ >= __for_len__ goto done - // item = __for_list__[__for_i__] - // body - // __for_i__ = __for_i__ + 1 - // goto loop_start - // done: let item: String = expr["item"] let list_expr = expr["list"] let body = expr["body"] // emit list, store it - let ctx = cg_expr(ctx, list_expr) - let ctx = emit_store(ctx, "__for_list__") + cg_expr(list_expr) + emit_store("__for_list__") // compute length - let ctx = emit_load(ctx, "__for_list__") - let ctx = emit_call(ctx, "native_list_len", 1) - let ctx = emit_store(ctx, "__for_len__") + emit_load("__for_list__") + emit_call("native_list_len", 1) + emit_store("__for_len__") // init counter - let ctx = emit_push_int(ctx, 0) - let ctx = emit_store(ctx, "__for_i__") + emit_push_int(0) + emit_store("__for_i__") // loop start - let loop_start = ctx_len(ctx) + let loop_start: Int = native_instr_len() // condition: __for_i__ < __for_len__ - let ctx = emit_load(ctx, "__for_i__") - let ctx = emit_load(ctx, "__for_len__") - let ctx = emit_lt(ctx) - let r = emit_jump_if_not_placeholder(ctx) - let ctx = r["ctx"] - let to_done_idx: Int = r["idx"] + emit_load("__for_i__") + emit_load("__for_len__") + emit_lt() + let to_done_idx: Int = emit_jump_if_not_placeholder() // get current element - let ctx = emit_load(ctx, "__for_list__") - let ctx = emit_load(ctx, "__for_i__") - let ctx = emit_get_index(ctx) - let ctx = emit_store(ctx, item) + emit_load("__for_list__") + emit_load("__for_i__") + emit_get_index() + emit_store(item) // body - let ctx = cg_stmts(ctx, body) + cg_stmts(body) // increment counter - let ctx = emit_load(ctx, "__for_i__") - let ctx = emit_push_int(ctx, 1) - let ctx = emit_add(ctx) - let ctx = emit_store(ctx, "__for_i__") + emit_load("__for_i__") + emit_push_int(1) + emit_add() + emit_store("__for_i__") // jump back - let ctx = emit_jump_to(ctx, loop_start) + emit_jump_to(loop_start) // patch done - let done_pos = ctx_len(ctx) - let ctx = ctx_patch(ctx, to_done_idx, done_pos) - return ctx + let done_pos: Int = native_instr_len() + patch_instr(to_done_idx, done_pos) + return } if kind == "Try" { - // Just emit the inner expression — error propagation handled at runtime + // Just emit the inner expression let inner = expr["inner"] - return cg_expr(ctx, inner) + cg_expr(inner) + return } // Fallback - emit_push_nil(ctx) + emit_push_nil() } // ── Statement codegen ───────────────────────────────────────────────────────── -fn cg_stmt(ctx: Map, stmt: Map) -> Map { +// cg_stmt_tail — emit a statement in tail position (last stmt of fn body). +// For Expr statements, the result is left on the stack (not popped). +// For all other statement kinds, we delegate to cg_stmt and push Nil. +fn cg_stmt_tail(stmt: Map) -> Void { + let kind: String = stmt["stmt"] + + if kind == "Expr" { + let val = stmt["value"] + // Emit the expression — leave result on stack (tail position). + cg_expr(val) + return + } + + if kind == "Return" { + // Explicit return — same as cg_stmt + let val = stmt["value"] + cg_expr(val) + emit_return() + return + } + + // All other statement kinds (Let, FnDef, While, etc.) don't produce + // a natural return value — fall through to cg_stmt then push Nil. + cg_stmt(stmt) + emit_push_nil() +} + +// cg_stmts_body — emit function body statements. +// All statements except the last use cg_stmt (pops results). +// The last statement uses cg_stmt_tail (leaves value on stack). +fn cg_stmts_body(stmts: [Map]) -> Void { + let n: Int = native_list_len(stmts) + if n == 0 { + // Empty body — push Nil as return value. + emit_push_nil() + return + } + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let is_last: Bool = (i == n - 1) + if is_last { + cg_stmt_tail(stmt) + } else { + cg_stmt(stmt) + } + let i = i + 1 + } +} + +fn cg_stmt(stmt: Map) -> Void { let kind: String = stmt["stmt"] if kind == "Let" { let name: String = stmt["name"] let val = stmt["value"] - let ctx = cg_expr(ctx, val) - return emit_store(ctx, name) + cg_expr(val) + emit_store(name) + return } if kind == "Return" { let val = stmt["value"] - let ctx = cg_expr(ctx, val) - return emit_return(ctx) + cg_expr(val) + emit_return() + return } if kind == "FnDef" { @@ -595,45 +606,40 @@ fn cg_stmt(ctx: Map, stmt: Map) -> Map { let body = stmt["body"] let n_params: Int = native_list_len(params) // emit a Jump to skip over the function body - let r = emit_jump_placeholder(ctx) - let ctx = r["ctx"] - let skip_jump_idx: Int = r["idx"] + let skip_jump_idx: Int = emit_jump_placeholder() // function body entry: store params in reverse order (caller pushes L→R, so pop R→L) let pi = n_params - 1 while pi >= 0 { let param = native_list_get(params, pi) let pname: String = param["name"] - let ctx = emit_store(ctx, pname) + emit_store(pname) let pi = pi - 1 } - // emit body statements - let ctx = cg_stmts(ctx, body) - // implicit nil return - let ctx = emit_push_nil(ctx) - let ctx = emit_return(ctx) + // emit body statements using tail-aware emit (last stmt leaves value on stack) + cg_stmts_body(body) + // return the top-of-stack value + emit_return() // patch skip jump - let after_fn = ctx_len(ctx) - let ctx = ctx_patch(ctx, skip_jump_idx, after_fn) + let after_fn: Int = native_instr_len() + patch_instr(skip_jump_idx, after_fn) // register function entry point - let entry_ip = skip_jump_idx + 1 - let ctx = emit_push_int(ctx, entry_ip) - let ctx = emit_store(ctx, "__fn_" + fn_name) - return ctx + let entry_ip: Int = skip_jump_idx + 1 + emit_push_int(entry_ip) + emit_store("__fn_" + fn_name) + return } if kind == "While" { let cond = stmt["cond"] let body = stmt["body"] - let loop_start = ctx_len(ctx) - let ctx = cg_expr(ctx, cond) - let r = emit_jump_if_not_placeholder(ctx) - let ctx = r["ctx"] - let to_done_idx: Int = r["idx"] - let ctx = cg_stmts(ctx, body) - let ctx = emit_jump_to(ctx, loop_start) - let done_pos = ctx_len(ctx) - let ctx = ctx_patch(ctx, to_done_idx, done_pos) - return ctx + let loop_start: Int = native_instr_len() + cg_expr(cond) + let to_done_idx: Int = emit_jump_if_not_placeholder() + cg_stmts(body) + emit_jump_to(loop_start) + let done_pos: Int = native_instr_len() + patch_instr(to_done_idx, done_pos) + return } if kind == "For" { @@ -642,53 +648,46 @@ fn cg_stmt(ctx: Map, stmt: Map) -> Map { let list_expr = stmt["list"] let body = stmt["body"] let for_expr = { "expr": "For", "item": item, "list": list_expr, "body": body } - return cg_expr(ctx, for_expr) + cg_expr(for_expr) + return } if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] - let ctx = cg_expr(ctx, val) - // Discard result unless it's a control-flow expression - if val_kind == "If" { - return ctx - } + cg_expr(val) + // Discard result unless it's a control-flow expression that doesn't push a value if val_kind == "For" { - return ctx + return } - if val_kind == "Match" { - return ctx - } - return emit_pop(ctx) + emit_pop() + return } if kind == "TypeDef" { // compile-time only; no runtime code - return ctx + return } if kind == "EnumDef" { // compile-time only; no runtime code - return ctx + return } if kind == "Import" { // handled at a higher level; skip - return ctx + return } - - ctx } -fn cg_stmts(ctx: Map, stmts: [Map]) -> Map { +fn cg_stmts(stmts: [Map]) -> Void { let n: Int = native_list_len(stmts) let i = 0 while i < n { let stmt = native_list_get(stmts, i) - let ctx = cg_stmt(ctx, stmt) + cg_stmt(stmt) let i = i + 1 } - ctx } // ── JSON serialisation ──────────────────────────────────────────────────────── @@ -711,9 +710,9 @@ fn instrs_to_json(instrs: [String]) -> String { // ── Entry point ─────────────────────────────────────────────────────────────── fn codegen(stmts: [Map], source: String) -> String { - let ctx = ctx_new() - let ctx = cg_stmts(ctx, stmts) - let ctx = emit_halt(ctx) - let instrs = ctx["instrs"] + native_instr_reset() + cg_stmts(stmts) + emit_halt() + let instrs: [String] = native_instr_all() instrs_to_json(instrs) } diff --git a/el-compiler/src/compiler.el b/el-compiler/src/compiler.el index d67d010..cbb227d 100644 --- a/el-compiler/src/compiler.el +++ b/el-compiler/src/compiler.el @@ -17,3 +17,30 @@ fn compile(source: String) -> String { let stmts: [Map] = parse(tokens) codegen(stmts, source) } + +// main — CLI entry point for self-hosted compilation. +// +// Called by: elvm el-compiler.elc +// +// Reads pre-resolved El source from args()[0], compiles it to JSON bytecode, +// and writes the result to args()[1]. The output is raw JSON (accepted by +// both elvm and el exec without an ELVM container header). +fn main() -> Void { + let argv: [String] = args() + let argc: Int = native_list_len(argv) + if argc < 2 { + println("el-compiler: usage: elvm el-compiler.elc ") + exit(1) + } + let src_path: String = native_list_get(argv, 0) + let out_path: String = native_list_get(argv, 1) + let source: String = fs_read(src_path) + let bytecode_json: String = compile(source) + let ok: Bool = fs_write(out_path, bytecode_json) + if ok { + exit(0) + } else { + println("el-compiler: failed to write output") + exit(1) + } +} diff --git a/el-compiler/src/lexer.el b/el-compiler/src/lexer.el index ae443d7..1517bb1 100644 --- a/el-compiler/src/lexer.el +++ b/el-compiler/src/lexer.el @@ -6,6 +6,9 @@ // "value" -> String (the raw text of the token) // // Entry point: fn lex(source: String) -> [Map] +// +// Uses global char_buf via native_chars_init / native_char_at / native_char_len +// to avoid O(N²) cloning of the chars list. // ── Character helpers ───────────────────────────────────────────────────────── @@ -141,9 +144,11 @@ fn keyword_kind(word: String) -> String { } // ── Scan helpers ────────────────────────────────────────────────────────────── +// All scan helpers use the global char_buf (native_char_at / native_char_len). +// No chars parameter — avoids O(N²) cloning. -// scan_digits — advance i while chars[i] is a digit, return { "text": ..., "pos": i } -fn scan_digits(chars: [String], start: Int, total: Int) -> Map { +// scan_digits — advance i while char_buf[i] is a digit, return { "text": ..., "pos": i } +fn scan_digits(start: Int, total: Int) -> Map { let i = start let text = "" let running = true @@ -151,7 +156,7 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map { if i >= total { let running = false } else { - let ch = native_list_get(chars, i) + let ch = native_char_at(i) if is_digit(ch) { let text = text + ch let i = i + 1 @@ -163,8 +168,8 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map { { "text": text, "pos": i } } -// scan_ident — advance i while chars[i] is alphanumeric or underscore -fn scan_ident(chars: [String], start: Int, total: Int) -> Map { +// scan_ident — advance i while char_buf[i] is alphanumeric or underscore +fn scan_ident(start: Int, total: Int) -> Map { let i = start let text = "" let running = true @@ -172,7 +177,7 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map { if i >= total { let running = false } else { - let ch = native_list_get(chars, i) + let ch = native_char_at(i) if is_alnum_or_underscore(ch) { let text = text + ch let i = i + 1 @@ -186,7 +191,7 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map { // scan_string — scan a quoted string literal, handling \" escapes. // Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close } -fn scan_string(chars: [String], start: Int, total: Int) -> Map { +fn scan_string(start: Int, total: Int) -> Map { let i = start let text = "" let closed = false @@ -195,12 +200,12 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map { if i >= total { let running = false } else { - let ch = native_list_get(chars, i) + let ch = native_char_at(i) if ch == "\\" { // escape: peek next char let next_i = i + 1 if next_i < total { - let next_ch = native_list_get(chars, next_i) + let next_ch = native_char_at(next_i) if next_ch == "\"" { let text = text + "\"" let i = next_i + 1 @@ -244,13 +249,13 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map { // ── Main lexer ──────────────────────────────────────────────────────────────── fn lex(source: String) -> [Map] { - let chars: [String] = native_string_chars(source) - let total: Int = native_list_len(chars) + native_chars_init(source) + let total: Int = native_char_len() let tokens: [Map] = native_list_empty() let i: Int = 0 while i < total { - let ch: String = native_list_get(chars, i) + let ch: String = native_char_at(i) // Skip whitespace if is_whitespace(ch) { @@ -260,7 +265,7 @@ fn lex(source: String) -> [Map] { if ch == "/" { let next_i = i + 1 if next_i < total { - let next_ch: String = native_list_get(chars, next_i) + let next_ch: String = native_char_at(next_i) if next_ch == "/" { // skip to end of line let i = i + 2 @@ -269,7 +274,7 @@ fn lex(source: String) -> [Map] { if i >= total { let running2 = false } else { - let lch: String = native_list_get(chars, i) + let lch: String = native_char_at(i) if lch == "\n" { let running2 = false } else { @@ -288,7 +293,7 @@ fn lex(source: String) -> [Map] { } else { // String literal if ch == "\"" { - let result = scan_string(chars, i + 1, total) + let result = scan_string(i + 1, total) let str_text: String = result["text"] let new_pos: Int = result["pos"] let tokens = native_list_append(tokens, make_tok("Str", str_text)) @@ -296,18 +301,18 @@ fn lex(source: String) -> [Map] { } else { // Number literal if is_digit(ch) { - let result = scan_digits(chars, i, total) + let result = scan_digits(i, total) let num_text: String = result["text"] let new_pos: Int = result["pos"] // check for float (dot followed by digit) if new_pos < total { - let dot_ch: String = native_list_get(chars, new_pos) + let dot_ch: String = native_char_at(new_pos) if dot_ch == "." { let after_dot = new_pos + 1 if after_dot < total { - let after_dot_ch: String = native_list_get(chars, after_dot) + let after_dot_ch: String = native_char_at(after_dot) if is_digit(after_dot_ch) { - let frac_result = scan_digits(chars, after_dot, total) + let frac_result = scan_digits(after_dot, total) let frac_text: String = frac_result["text"] let frac_pos: Int = frac_result["pos"] let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text)) @@ -331,7 +336,7 @@ fn lex(source: String) -> [Map] { } else { // Identifier or keyword if is_alpha(ch) || ch == "_" { - let result = scan_ident(chars, i, total) + let result = scan_ident(i, total) let word: String = result["text"] let new_pos: Int = result["pos"] let kw = keyword_kind(word) @@ -346,7 +351,7 @@ fn lex(source: String) -> [Map] { let peek_i = i + 1 let peek_ch = "" if peek_i < total { - let peek_ch = native_list_get(chars, peek_i) + let peek_ch = native_char_at(peek_i) } if ch == "=" { diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index fae3001..700b89b 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -6,26 +6,30 @@ // The cursor (integer position into the token list) is threaded through every // parse function. Functions return { "node": , "pos": }. // +// The token list is stored in the VM's global token buffer via +// native_tokens_init / native_token_at / native_token_len. This avoids +// O(n²) cloning that occurs when passing the list as a function argument. +// // Entry point: fn parse(tokens: [Map]) -> [Map] // ── Token access helpers ────────────────────────────────────────────────────── -fn tok_at(tokens: [Map], pos: Int) -> Map { - native_list_get(tokens, pos) +fn tok_at(pos: Int) -> Map { + native_token_at(pos) } -fn tok_kind(tokens: [Map], pos: Int) -> String { - let t = tok_at(tokens, pos) +fn tok_kind(pos: Int) -> String { + let t = native_token_at(pos) t["kind"] } -fn tok_value(tokens: [Map], pos: Int) -> String { - let t = tok_at(tokens, pos) +fn tok_value(pos: Int) -> String { + let t = native_token_at(pos) t["value"] } -fn expect(tokens: [Map], pos: Int, kind: String) -> Int { - let k = tok_kind(tokens, pos) +fn expect(pos: Int, kind: String) -> Int { + let k = tok_kind(pos) if k == kind { return pos + 1 } @@ -43,26 +47,26 @@ fn make_result(node: Map, pos: Int) -> Map { // Skips over a type annotation, returning the new position. // Types can be: Ident, [Type], Map, Type?, Type -fn skip_type(tokens: [Map], pos: Int) -> Int { - let k = tok_kind(tokens, pos) +fn skip_type(pos: Int) -> Int { + let k = tok_kind(pos) // Array type: [Type] if k == "LBracket" { let p = pos + 1 - let p = skip_type(tokens, p) - let p = expect(tokens, p, "RBracket") + let p = skip_type(p) + let p = expect(p, "RBracket") return p } // Named type (possibly generic) if k == "Ident" { let p = pos + 1 - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Lt" { // Generic params: skip until matching > let p = p + 1 let depth = 1 let running = true while running { - let kk = tok_kind(tokens, p) + let kk = tok_kind(p) if kk == "Eof" { let running = false } else { @@ -82,7 +86,7 @@ fn skip_type(tokens: [Map], pos: Int) -> Int { } } } - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "QuestionMark" { let p = p + 1 } @@ -100,12 +104,12 @@ fn skip_type(tokens: [Map], pos: Int) -> Int { // ── Parameter list ──────────────────────────────────────────────────────────── // Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... } -fn parse_params(tokens: [Map], pos: Int) -> Map { - let p = expect(tokens, pos, "LParen") +fn parse_params(pos: Int) -> Map { + let p = expect(pos, "LParen") let params: [Map] = native_list_empty() let running = true while running { - let k = tok_kind(tokens, p) + let k = tok_kind(p) if k == "RParen" { let running = false } else { @@ -113,28 +117,28 @@ fn parse_params(tokens: [Map], pos: Int) -> Map { let running = false } else { // param name - let pname = tok_value(tokens, p) + let pname = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "Colon") - let p = skip_type(tokens, p) + let p = expect(p, "Colon") + let p = skip_type(p) let param = { "name": pname } let params = native_list_append(params, param) - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RParen") + let p = expect(p, "RParen") { "params": params, "pos": p } } // ── Expression parsing ──────────────────────────────────────────────────────── -fn parse_primary(tokens: [Map], pos: Int) -> Map { - let k = tok_kind(tokens, pos) - let v = tok_value(tokens, pos) +fn parse_primary(pos: Int) -> Map { + let k = tok_kind(pos) + let v = tok_value(pos) // Integer literal if k == "Int" { @@ -163,10 +167,10 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { // Grouped expression if k == "LParen" { - let r = parse_expr(tokens, pos + 1) + let r = parse_expr(pos + 1) let node = r["node"] let p = r["pos"] - let p = expect(tokens, p, "RParen") + let p = expect(p, "RParen") return make_result(node, p) } @@ -176,25 +180,25 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { let elems: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RBracket" { let running = false } else { if k2 == "Eof" { let running = false } else { - let r = parse_expr(tokens, p) + let r = parse_expr(p) let elem = r["node"] let p = r["pos"] let elems = native_list_append(elems, elem) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBracket") + let p = expect(p, "RBracket") return make_result({ "expr": "Array", "elems": elems }, p) } @@ -204,7 +208,7 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { let pairs: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RBrace" { let running = false } else { @@ -212,46 +216,46 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { let running = false } else { // key: Str token - let key = tok_value(tokens, p) + let key = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "Colon") - let r = parse_expr(tokens, p) + let p = expect(p, "Colon") + let r = parse_expr(p) let val_node = r["node"] let p = r["pos"] let pair = { "key": key, "value": val_node } let pairs = native_list_append(pairs, pair) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") return make_result({ "expr": "Map", "pairs": pairs }, p) } // if expression if k == "If" { - let r = parse_if(tokens, pos) + let r = parse_if(pos) return r } // match expression if k == "Match" { - let r = parse_match(tokens, pos) + let r = parse_match(pos) return r } // for expression (used as statement) if k == "For" { - let r = parse_for_expr(tokens, pos) + let r = parse_for_expr(pos) return r } // Unary not if k == "Not" { - let r = parse_primary(tokens, pos + 1) + let r = parse_primary(pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Not", "inner": inner }, p) @@ -259,7 +263,7 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { // Unary minus if k == "Minus" { - let r = parse_primary(tokens, pos + 1) + let r = parse_primary(pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Neg", "inner": inner }, p) @@ -269,29 +273,29 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { make_result({ "expr": "Nil" }, pos + 1) } -fn parse_if(tokens: [Map], pos: Int) -> Map { - let p = expect(tokens, pos, "If") - let r = parse_expr(tokens, p) +fn parse_if(pos: Int) -> Map { + let p = expect(pos, "If") + let r = parse_expr(p) let cond = r["node"] let p = r["pos"] - let r2 = parse_block(tokens, p) + let r2 = parse_block(p) let then_stmts = r2["stmts"] let p = r2["pos"] let has_else = false let else_stmts: [Map] = native_list_empty() - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Else" { let p = p + 1 - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "If" { // else-if chain: parse as nested if - let r3 = parse_if(tokens, p) + let r3 = parse_if(p) let nested = r3["node"] let p = r3["pos"] let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested }) let has_else = true } else { - let r3 = parse_block(tokens, p) + let r3 = parse_block(p) let else_stmts = r3["stmts"] let p = r3["pos"] let has_else = true @@ -300,16 +304,16 @@ fn parse_if(tokens: [Map], pos: Int) -> Map { make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p) } -fn parse_match(tokens: [Map], pos: Int) -> Map { - let p = expect(tokens, pos, "Match") - let r = parse_expr(tokens, p) +fn parse_match(pos: Int) -> Map { + let p = expect(pos, "Match") + let r = parse_expr(p) let subject = r["node"] let p = r["pos"] - let p = expect(tokens, p, "LBrace") + let p = expect(p, "LBrace") let arms: [Map] = native_list_empty() let running = true while running { - let k = tok_kind(tokens, p) + let k = tok_kind(p) if k == "RBrace" { let running = false } else { @@ -317,131 +321,131 @@ fn parse_match(tokens: [Map], pos: Int) -> Map { let running = false } else { // parse pattern => body - let r2 = parse_pattern(tokens, p) + let r2 = parse_pattern(p) let pattern = r2["node"] let p = r2["pos"] - let p = expect(tokens, p, "FatArrow") - let r3 = parse_expr(tokens, p) + let p = expect(p, "FatArrow") + let r3 = parse_expr(p) let body = r3["node"] let p = r3["pos"] let arm = { "pattern": pattern, "body": body } let arms = native_list_append(arms, arm) - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") make_result({ "expr": "Match", "subject": subject, "arms": arms }, p) } -fn parse_pattern(tokens: [Map], pos: Int) -> Map { - let k = tok_kind(tokens, pos) +fn parse_pattern(pos: Int) -> Map { + let k = tok_kind(pos) if k == "Ident" { - let v = tok_value(tokens, pos) + let v = tok_value(pos) if v == "_" { return make_result({ "pattern": "Wildcard" }, pos + 1) } return make_result({ "pattern": "Binding", "name": v }, pos + 1) } if k == "Int" { - return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1) + return make_result({ "pattern": "LitInt", "value": tok_value(pos) }, pos + 1) } if k == "Str" { - return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1) + return make_result({ "pattern": "LitStr", "value": tok_value(pos) }, pos + 1) } if k == "Bool" { - return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1) + return make_result({ "pattern": "LitBool", "value": tok_value(pos) }, pos + 1) } // Wildcard _ make_result({ "pattern": "Wildcard" }, pos + 1) } -fn parse_for_expr(tokens: [Map], pos: Int) -> Map { - let p = expect(tokens, pos, "For") - let item_name = tok_value(tokens, p) +fn parse_for_expr(pos: Int) -> Map { + let p = expect(pos, "For") + let item_name = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "In") - let r = parse_expr(tokens, p) + let p = expect(p, "In") + let r = parse_expr(p) let list_expr = r["node"] let p = r["pos"] - let r2 = parse_block(tokens, p) + let r2 = parse_block(p) let body = r2["stmts"] let p = r2["pos"] make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p) } -fn parse_block(tokens: [Map], pos: Int) -> Map { - let p = expect(tokens, pos, "LBrace") +fn parse_block(pos: Int) -> Map { + let p = expect(pos, "LBrace") let stmts: [Map] = native_list_empty() let running = true while running { - let k = tok_kind(tokens, p) + let k = tok_kind(p) if k == "RBrace" { let running = false } else { if k == "Eof" { let running = false } else { - let r = parse_stmt(tokens, p) + let r = parse_stmt(p) let stmt = r["node"] let p = r["pos"] let stmts = native_list_append(stmts, stmt) } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") { "stmts": stmts, "pos": p } } // ── Postfix expressions (calls, field access, index) ───────────────────────── -fn parse_postfix(tokens: [Map], pos: Int) -> Map { - let r = parse_primary(tokens, pos) +fn parse_postfix(pos: Int) -> Map { + let r = parse_primary(pos) let node = r["node"] let p = r["pos"] let running = true while running { - let k = tok_kind(tokens, p) + let k = tok_kind(p) if k == "LParen" { // function call let p = p + 1 let args: [Map] = native_list_empty() let run2 = true while run2 { - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RParen" { let run2 = false } else { if k2 == "Eof" { let run2 = false } else { - let r2 = parse_expr(tokens, p) + let r2 = parse_expr(p) let arg = r2["node"] let p = r2["pos"] let args = native_list_append(args, arg) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RParen") + let p = expect(p, "RParen") let node = { "expr": "Call", "func": node, "args": args } } else { if k == "Dot" { - let field = tok_value(tokens, p + 1) + let field = tok_value(p + 1) let p = p + 2 let node = { "expr": "Field", "object": node, "field": field } } else { if k == "LBracket" { - let r2 = parse_expr(tokens, p + 1) + let r2 = parse_expr(p + 1) let idx = r2["node"] let p = r2["pos"] - let p = expect(tokens, p, "RBracket") + let p = expect(p, "RBracket") let node = { "expr": "Index", "object": node, "index": idx } } else { if k == "QuestionMark" { @@ -491,18 +495,18 @@ fn is_binop(kind: String) -> Bool { false } -fn parse_binop(tokens: [Map], pos: Int, min_prec: Int) -> Map { - let r = parse_postfix(tokens, pos) +fn parse_binop(pos: Int, min_prec: Int) -> Map { + let r = parse_postfix(pos) let left = r["node"] let p = r["pos"] let running = true while running { - let k = tok_kind(tokens, p) + let k = tok_kind(p) let prec = op_precedence(k) if is_binop(k) { if prec >= min_prec { let op = k - let r2 = parse_binop(tokens, p + 1, prec + 1) + let r2 = parse_binop(p + 1, prec + 1) let right = r2["node"] let p = r2["pos"] let left = { "expr": "BinOp", "op": op, "left": left, "right": right } @@ -516,28 +520,28 @@ fn parse_binop(tokens: [Map], pos: Int, min_prec: Int) -> Map], pos: Int) -> Map { - parse_binop(tokens, pos, 1) +fn parse_expr(pos: Int) -> Map { + parse_binop(pos, 1) } // ── Statement parsing ───────────────────────────────────────────────────────── -fn parse_stmt(tokens: [Map], pos: Int) -> Map { - let k = tok_kind(tokens, pos) +fn parse_stmt(pos: Int) -> Map { + let k = tok_kind(pos) // let binding if k == "Let" { let p = pos + 1 - let name = tok_value(tokens, p) + let name = tok_value(p) let p = p + 1 - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) // optional type annotation: name: Type if k2 == "Colon" { let p = p + 1 - let p = skip_type(tokens, p) + let p = skip_type(p) } - let p = expect(tokens, p, "Eq") - let r = parse_expr(tokens, p) + let p = expect(p, "Eq") + let r = parse_expr(p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Let", "name": name, "value": val }, p) @@ -546,14 +550,14 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // return statement if k == "Return" { let p = pos + 1 - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RBrace" { return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) } if k2 == "Eof" { return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) } - let r = parse_expr(tokens, p) + let r = parse_expr(p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Return", "value": val }, p) @@ -562,18 +566,18 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // fn definition if k == "Fn" { let p = pos + 1 - let name = tok_value(tokens, p) + let name = tok_value(p) let p = p + 1 - let r = parse_params(tokens, p) + let r = parse_params(p) let params = r["params"] let p = r["pos"] // return type annotation: -> Type - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Arrow" { let p = p + 1 - let p = skip_type(tokens, p) + let p = skip_type(p) } - let r2 = parse_block(tokens, p) + let r2 = parse_block(p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body }, p) @@ -582,69 +586,69 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // type definition if k == "Type" { let p = pos + 1 - let name = tok_value(tokens, p) + let name = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "LBrace") + let p = expect(p, "LBrace") let fields: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { - let fname = tok_value(tokens, p) + let fname = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "Colon") - let p = skip_type(tokens, p) + let p = expect(p, "Colon") + let p = skip_type(p) let fields = native_list_append(fields, { "name": fname }) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") return make_result({ "stmt": "TypeDef", "name": name, "fields": fields }, p) } // enum definition if k == "Enum" { let p = pos + 1 - let name = tok_value(tokens, p) + let name = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "LBrace") + let p = expect(p, "LBrace") let variants: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { - let vname = tok_value(tokens, p) + let vname = tok_value(p) let p = p + 1 let variants = native_list_append(variants, { "name": vname }) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") return make_result({ "stmt": "EnumDef", "name": name, "variants": variants }, p) } // import statement if k == "Import" { let p = pos + 1 - let path = tok_value(tokens, p) + let path = tok_value(p) let p = p + 1 return make_result({ "stmt": "Import", "path": path }, p) } @@ -652,20 +656,20 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // from ... import { ... } if k == "From" { let p = pos + 1 - let module_name = tok_value(tokens, p) + let module_name = tok_value(p) let p = p + 1 // skip "import" keyword - let k2 = tok_kind(tokens, p) + let k2 = tok_kind(p) if k2 == "Import" { let p = p + 1 } // skip { Name, ... } - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(p) if k3 == "LBrace" { let p = p + 1 let running = true while running { - let k4 = tok_kind(tokens, p) + let k4 = tok_kind(p) if k4 == "RBrace" { let running = false } else { @@ -673,14 +677,14 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { let running = false } else { let p = p + 1 - let k5 = tok_kind(tokens, p) + let k5 = tok_kind(p) if k5 == "Comma" { let p = p + 1 } } } } - let p = expect(tokens, p, "RBrace") + let p = expect(p, "RBrace") } return make_result({ "stmt": "Import", "path": module_name }, p) } @@ -688,10 +692,10 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // while loop if k == "While" { let p = pos + 1 - let r = parse_expr(tokens, p) + let r = parse_expr(p) let cond = r["node"] let p = r["pos"] - let r2 = parse_block(tokens, p) + let r2 = parse_block(p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "While", "cond": cond, "body": body }, p) @@ -700,13 +704,13 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // for loop if k == "For" { let p = pos + 1 - let item_name = tok_value(tokens, p) + let item_name = tok_value(p) let p = p + 1 - let p = expect(tokens, p, "In") - let r = parse_expr(tokens, p) + let p = expect(p, "In") + let r = parse_expr(p) let list_expr = r["node"] let p = r["pos"] - let r2 = parse_block(tokens, p) + let r2 = parse_block(p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) @@ -717,11 +721,11 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { let p = pos + 1 // skip decorator name let p = p + 1 - return parse_stmt(tokens, p) + return parse_stmt(p) } // bare expression or if/match statement - let r = parse_expr(tokens, pos) + let r = parse_expr(pos) let val = r["node"] let p = r["pos"] make_result({ "stmt": "Expr", "value": val }, p) @@ -730,7 +734,9 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // ── Top-level parse ──────────────────────────────────────────────────────────── fn parse(tokens: [Map]) -> [Map] { - let total: Int = native_list_len(tokens) + // Store tokens in global buffer to avoid O(n²) cloning on every recursive call. + native_tokens_init(tokens) + let total: Int = native_token_len() let stmts: [Map] = native_list_empty() let pos: Int = 0 let running = true @@ -738,11 +744,11 @@ fn parse(tokens: [Map]) -> [Map] { if pos >= total { let running = false } else { - let k = tok_kind(tokens, pos) + let k = tok_kind(pos) if k == "Eof" { let running = false } else { - let r = parse_stmt(tokens, pos) + let r = parse_stmt(pos) let stmt = r["node"] let new_pos: Int = r["pos"] let stmts = native_list_append(stmts, stmt) diff --git a/engrams/el-manifest/src/lib.rs b/engrams/el-manifest/src/lib.rs deleted file mode 100644 index a626cb1..0000000 --- a/engrams/el-manifest/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! el-manifest — `el.toml` project manifest parser. -//! -//! Every Engram project has an `el.toml` at its root. This crate defines the -//! manifest data model and parses it from TOML text. -//! -//! # Quick start -//! ```rust -//! use el_manifest::Manifest; -//! -//! let toml = r#" -//! [package] -//! name = "my-service" -//! version = "0.1.0" -//! edition = "2026" -//! "#; -//! let manifest = Manifest::parse(toml).unwrap(); -//! assert_eq!(manifest.package.name, "my-service"); -//! ``` - -mod error; -mod manifest; -mod parse; - -pub use error::{ManifestError, ManifestResult}; -pub use manifest::{ - BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, NativeTarget, - PackageInfo, SealKeySource, -}; diff --git a/engrams/el-manifest/src/parse.rs b/engrams/el-manifest/src/parse.rs deleted file mode 100644 index 93d4344..0000000 --- a/engrams/el-manifest/src/parse.rs +++ /dev/null @@ -1,447 +0,0 @@ -//! TOML parsing for `el.toml` manifests. -//! -//! We define a set of intermediate `Raw*` structs that map 1:1 to the TOML -//! schema, then convert them into the strongly-typed `Manifest` model. - -use std::collections::HashMap; -use std::path::PathBuf; - -use serde::Deserialize; -use toml::Value as TomlValue; - -use crate::error::{ManifestError, ManifestResult}; -use crate::manifest::{ - BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, PackageInfo, - SealKeySource, -}; - -// ── Raw TOML structs ────────────────────────────────────────────────────────── - -#[derive(Debug, Deserialize)] -struct RawManifest { - package: RawPackage, - #[serde(default)] - dependencies: HashMap, - #[serde(rename = "dev-dependencies", default)] - dev_dependencies: HashMap, - #[serde(default)] - build: RawBuild, - #[serde(default)] - cross: RawCross, - #[serde(default)] - plugins: HashMap, -} - -#[derive(Debug, Deserialize)] -struct RawPackage { - name: String, - version: String, - description: Option, - #[serde(default)] - authors: Vec, - license: Option, - #[serde(default = "default_edition")] - edition: String, -} - -fn default_edition() -> String { - "2026".to_string() -} - -#[derive(Debug, Deserialize)] -#[serde(default)] -struct RawBuild { - target: String, - entry: String, - output: String, - seal_key: Option, -} - -impl Default for RawBuild { - fn default() -> Self { - Self { - target: "debug".to_string(), - entry: "src/main.el".to_string(), - output: "dist/".to_string(), - seal_key: None, - } - } -} - -#[derive(Debug, Deserialize, Default)] -struct RawCross { - #[serde(default)] - targets: Vec, -} - -// ── Entry point ─────────────────────────────────────────────────────────────── - -/// Parse a raw TOML string into a [`Manifest`]. -pub(crate) fn parse_manifest(s: &str) -> ManifestResult { - let raw: RawManifest = toml::from_str(s)?; - convert(raw) -} - -fn convert(raw: RawManifest) -> ManifestResult { - let package = convert_package(raw.package)?; - let dependencies = convert_deps(&raw.dependencies, "dependencies")?; - let dev_dependencies = convert_deps(&raw.dev_dependencies, "dev-dependencies")?; - let build = convert_build(raw.build)?; - let cross = convert_cross(raw.cross)?; - - Ok(Manifest { - package, - dependencies, - dev_dependencies, - build, - cross, - plugins: raw.plugins, - }) -} - -fn convert_package(raw: RawPackage) -> ManifestResult { - let version = semver::Version::parse(&raw.version).map_err(|e| ManifestError::Semver { - field: "package.version".to_string(), - source: e, - })?; - - Ok(PackageInfo { - name: raw.name, - version, - description: raw.description, - authors: raw.authors, - license: raw.license, - edition: raw.edition, - }) -} - -fn convert_deps( - raw: &HashMap, - section: &str, -) -> ManifestResult> { - let mut out = HashMap::new(); - for (name, value) in raw { - let dep = convert_single_dep(name, value, section)?; - out.insert(name.clone(), dep); - } - Ok(out) -} - -fn convert_single_dep( - name: &str, - value: &TomlValue, - section: &str, -) -> ManifestResult { - match value { - // String form: `name = "1.2"` or `name = "^0.8.1"` - TomlValue::String(s) => { - let req = - semver::VersionReq::parse(s).map_err(|e| ManifestError::Semver { - field: format!("{section}.{name}"), - source: e, - })?; - Ok(Dependency::VersionReq(req)) - } - // Table form: `name = { path = "..." }` or `name = { version = "...", registry = "..." }` - TomlValue::Table(t) => { - if let Some(TomlValue::String(path)) = t.get("path") { - return Ok(Dependency::Path(PathBuf::from(path))); - } - if let Some(TomlValue::String(ver_str)) = t.get("version") { - let version = - semver::VersionReq::parse(ver_str).map_err(|e| ManifestError::Semver { - field: format!("{section}.{name}.version"), - source: e, - })?; - let registry = t - .get("registry") - .and_then(|v| v.as_str()) - .unwrap_or("https://packages.neurontechnologies.ai") - .to_string(); - return Ok(Dependency::Registry { version, registry }); - } - Err(ManifestError::InvalidValue { - field: format!("{section}.{name}"), - reason: "dependency table must have 'path' or 'version' key".to_string(), - }) - } - other => Err(ManifestError::InvalidValue { - field: format!("{section}.{name}"), - reason: format!("expected string or table, got {}", other.type_str()), - }), - } -} - -fn convert_build(raw: RawBuild) -> ManifestResult { - let target = raw.target.parse::()?; - let seal_key = raw - .seal_key - .map(|s| SealKeySource::parse(&s)) - .transpose()?; - - Ok(BuildConfig { - target, - entry: PathBuf::from(raw.entry), - output: PathBuf::from(raw.output), - seal_key, - }) -} - -fn convert_cross(raw: RawCross) -> ManifestResult { - let targets = raw - .targets - .iter() - .map(|s| CrossTarget::parse(s)) - .collect::>>()?; - Ok(CrossConfig { targets }) -} - -// ── Tests ───────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use crate::manifest::{BuildTarget, CrossTarget, Dependency, NativeTarget, SealKeySource}; - - fn full_toml() -> &'static str { - r#" -[package] -name = "my-service" -version = "0.1.0" -description = "What this does" -authors = ["Will Anderson "] -license = "MIT" -edition = "2026" - -[dependencies] -engram-http = "1.2" -engram-auth = "0.8.1" -some-local = { path = "../some-local" } - -[dev-dependencies] -el-test = "0.1" - -[build] -target = "prod" -entry = "src/main.el" -output = "dist/" -seal_key = "env:ENGRAM_SEAL_KEY" - -[cross] -targets = ["x86_64-linux", "aarch64-linux", "x86_64-macos", "aarch64-macos", "wasm32"] - -[plugins] -el-fmt = "1.0" -el-doc = "0.3" -"# - } - - #[test] - fn test_parse_full_manifest() { - let m = parse_manifest(full_toml()).unwrap(); - assert_eq!(m.package.name, "my-service"); - assert_eq!(m.package.version.to_string(), "0.1.0"); - assert_eq!(m.package.edition, "2026"); - assert_eq!(m.package.license.as_deref(), Some("MIT")); - assert_eq!(m.package.authors, vec!["Will Anderson "]); - } - - #[test] - fn test_parse_dependencies() { - let m = parse_manifest(full_toml()).unwrap(); - - // Version requirement dep - assert!(m.dependencies.contains_key("engram-http")); - match &m.dependencies["engram-http"] { - Dependency::VersionReq(req) => { - assert!(req.to_string().contains("1")); - } - other => panic!("expected VersionReq, got {other:?}"), - } - - // Path dep - assert!(m.dependencies.contains_key("some-local")); - match &m.dependencies["some-local"] { - Dependency::Path(p) => assert_eq!(p.to_str().unwrap(), "../some-local"), - other => panic!("expected Path, got {other:?}"), - } - - // Dev dep - assert!(m.dev_dependencies.contains_key("el-test")); - } - - #[test] - fn test_parse_build_config() { - let m = parse_manifest(full_toml()).unwrap(); - assert_eq!(m.build.target, BuildTarget::Prod); - assert_eq!(m.build.entry.to_str().unwrap(), "src/main.el"); - assert_eq!(m.build.output.to_str().unwrap(), "dist/"); - match &m.build.seal_key { - Some(SealKeySource::EnvVar(v)) => assert_eq!(v, "ENGRAM_SEAL_KEY"), - other => panic!("expected EnvVar seal_key, got {other:?}"), - } - } - - #[test] - fn test_parse_cross_targets() { - let m = parse_manifest(full_toml()).unwrap(); - assert_eq!(m.cross.targets.len(), 5); - assert!(m.cross.targets.contains(&CrossTarget::X86_64Linux)); - assert!(m.cross.targets.contains(&CrossTarget::Aarch64Linux)); - assert!(m.cross.targets.contains(&CrossTarget::X86_64Macos)); - assert!(m.cross.targets.contains(&CrossTarget::Aarch64Macos)); - assert!(m.cross.targets.contains(&CrossTarget::Wasm32)); - } - - #[test] - fn test_parse_plugins() { - let m = parse_manifest(full_toml()).unwrap(); - assert_eq!(m.plugins.get("el-fmt").map(|s| s.as_str()), Some("1.0")); - assert_eq!(m.plugins.get("el-doc").map(|s| s.as_str()), Some("0.3")); - } - - #[test] - fn test_minimal_manifest() { - let toml = r#" -[package] -name = "hello" -version = "0.1.0" -"#; - let m = parse_manifest(toml).unwrap(); - assert_eq!(m.package.name, "hello"); - assert_eq!(m.package.edition, "2026"); // default - assert_eq!(m.build.target, BuildTarget::Debug); // default - assert_eq!(m.build.entry.to_str().unwrap(), "src/main.el"); // default - assert!(m.dependencies.is_empty()); - assert!(m.cross.targets.is_empty()); - } - - #[test] - fn test_invalid_semver_rejected() { - let toml = r#" -[package] -name = "bad" -version = "not-a-version" -"#; - let err = parse_manifest(toml).unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("semver"), "expected semver error, got: {msg}"); - } - - #[test] - fn test_invalid_build_target_rejected() { - let toml = r#" -[package] -name = "bad" -version = "0.1.0" - -[build] -target = "turbo" -"#; - let err = parse_manifest(toml).unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("turbo"), "expected target name in error: {msg}"); - } - - #[test] - fn test_invalid_cross_target_rejected() { - let toml = r#" -[package] -name = "bad" -version = "0.1.0" - -[cross] -targets = ["x86_64-linux", "solaris-sparc"] -"#; - let err = parse_manifest(toml).unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("solaris-sparc"), "expected target name in error: {msg}"); - } - - #[test] - fn test_seal_key_env_parse() { - let src = SealKeySource::parse("env:MY_KEY").unwrap(); - assert_eq!(src, SealKeySource::EnvVar("MY_KEY".to_string())); - } - - #[test] - fn test_seal_key_file_parse() { - let src = SealKeySource::parse("file:/tmp/key.bin").unwrap(); - assert_eq!(src, SealKeySource::File(PathBuf::from("/tmp/key.bin"))); - } - - #[test] - fn test_seal_key_literal_parse() { - let src = SealKeySource::parse("my-literal-key").unwrap(); - assert_eq!(src, SealKeySource::Literal("my-literal-key".to_string())); - } - - #[test] - fn test_native_target_host_triple() { - let t = NativeTarget::Host; - // Should return a non-empty triple - assert!(!t.triple().is_empty()); - } - - #[test] - fn test_native_target_wasm_extension() { - let t = NativeTarget::Wasm32; - assert_eq!(t.artifact_extension(), ".wasm"); - } - - #[test] - fn test_cross_target_triple_roundtrip() { - let targets = [ - CrossTarget::X86_64Linux, - CrossTarget::Aarch64Linux, - CrossTarget::X86_64Macos, - CrossTarget::Aarch64Macos, - CrossTarget::Wasm32, - ]; - for t in &targets { - // as_str → parse → same target - let s = t.as_str(); - let parsed = CrossTarget::parse(s).unwrap(); - assert_eq!(&parsed, t, "roundtrip failed for {s}"); - } - } - - #[test] - fn test_native_target_from_cross() { - let nt = NativeTarget::from_cross(&CrossTarget::Wasm32); - assert_eq!(nt, NativeTarget::Wasm32); - let nt2 = NativeTarget::from_cross(&CrossTarget::Aarch64Macos); - assert_eq!(nt2, NativeTarget::Aarch64Macos); - } - - #[test] - fn test_registry_dep_table() { - let toml = r#" -[package] -name = "test" -version = "1.0.0" - -[dependencies] -special-pkg = { version = "2.0", registry = "https://custom.registry.io" } -"#; - let m = parse_manifest(toml).unwrap(); - match &m.dependencies["special-pkg"] { - Dependency::Registry { version, registry } => { - assert!(version.to_string().contains("2")); - assert_eq!(registry, "https://custom.registry.io"); - } - other => panic!("expected Registry dep, got {other:?}"), - } - } - - #[test] - fn test_dep_version_req_method() { - let d = Dependency::VersionReq(semver::VersionReq::parse("1.0").unwrap()); - assert!(d.version_req().is_some()); - assert!(d.local_path().is_none()); - - let d2 = Dependency::Path(PathBuf::from("/tmp")); - assert!(d2.local_path().is_some()); - assert!(d2.version_req().is_none()); - } -} diff --git a/spec/elvm.md b/spec/elvm.md new file mode 100644 index 0000000..d853121 --- /dev/null +++ b/spec/elvm.md @@ -0,0 +1,427 @@ +# ELVM — El Virtual Machine Specification + +Version 1.0.0 — April 29, 2026 + +--- + +## Overview + +ELVM is the execution substrate for El programs. It is a stack-based virtual machine that executes JSON-serialized bytecode instructions stored in a typed binary container format. ELVM has four defining properties: + +1. **Binary container with semantic header.** The `.elc` file format begins with a 4-byte magic identifier (`ELVM`), a 4-byte version field, and an 8-byte payload length — making every artifact self-describing and parseable without external schema. + +2. **JSON-serialized instruction encoding.** The bytecode payload is a JSON array of instruction objects. Each instruction is human-inspectable with standard JSON tools, language-portable, and schema-free. The JSON encoding is the canonical on-disk format; no separate binary encoding exists. + +3. **Builtin dispatch architecture.** Function calls resolve first against a runtime builtin dispatch table, then against the user-defined function table. New builtins are added to the dispatch table without recompiling existing programs. The execution model treats builtins and user functions identically from the instruction side. + +4. **Self-hosting bootstrap.** A Rust genesis compiler produces ELVM bytecode. That bytecode, when executed by ELVM, runs the El compiler written in El, which produces ELVM bytecode for all subsequent compilation. The VM is the only persistent execution engine. + +--- + +## 1. Binary Container Format + +### 1.1 Header + +Every `.elc` file begins with a 16-byte header: + +``` +Offset Size Type Field +────── ───── ──────────────── ────────────────────────────────────────── +0 4 [u8; 4] Magic: b"ELVM" +4 4 u32 little-endian Version: currently 1 +8 8 u64 little-endian Payload length in bytes +``` + +### 1.2 Payload + +Immediately following the 16-byte header is the payload: a UTF-8 JSON array encoding a `Vec` instruction sequence. + +``` +[instruction, instruction, ...] +``` + +The payload length field in the header equals the exact byte count of the JSON payload. Bytes beyond `header_size + payload_length` are ignored (reserved for future trailer use). + +### 1.3 Legacy Format + +Files without the `ELVM` magic header are treated as raw JSON bytecode (no header). This is the legacy format produced by early compiler versions. The VM accepts both formats via the `deserialize_all` entry point. + +### 1.4 Identification + +The magic bytes `ELVM` (hex: `45 4C 56 4D`) uniquely identify El bytecode containers. Any tool that encounters these bytes at offset 0 may assume the 16-byte header format described above. + +--- + +## 2. Value Types + +The VM stack holds `Value` instances of the following types: + +| Variant | Rust encoding | Description | +|---------|--------------|-------------| +| `Int(i64)` | 64-bit signed integer | All integer arithmetic | +| `Float(f64)` | IEEE 754 double | Floating-point arithmetic | +| `Str(String)` | UTF-8 string | Text and identifiers | +| `Bool(bool)` | Boolean | Logical conditions | +| `Nil` | Unit | Absent value, uninitialized | +| `List(Vec)` | Ordered sequence | Arrays, activation results | +| `Map(Vec<(String, Value)>)` | Ordered key-value pairs | Struct instances, JSON objects | +| `ResultOk(Box)` | Success variant | Fallible operation success | +| `ResultErr(Box)` | Error variant | Fallible operation failure | +| `Struct { type_name, fields }` | Named struct | Typed record with field list | + +Maps use `Vec<(String, Value)>` rather than a hash map to preserve insertion order and remain serialization-friendly. + +--- + +## 3. Instruction Set + +### 3.1 JSON Encoding + +Each instruction serializes as either a JSON string (no-argument instructions) or a JSON object (instructions with arguments): + +```json +"Halt" +"Pop" +{"Push": {"Int": 42}} +{"Push": {"Str": "hello"}} +{"Push": "Nil"} +{"LoadLocal": "x"} +{"StoreLocal": "x"} +{"Call": {"name": "println", "arity": 1}} +{"Jump": 3} +{"JumpIfNot": -2} +``` + +### 3.2 Stack Instructions + +| Instruction | JSON | Stack effect | Description | +|-------------|------|-------------|-------------| +| `Push(v)` | `{"Push": }` | `-- v` | Push constant value | +| `Pop` | `"Pop"` | `v --` | Discard top of stack | +| `Dup` | `"Dup"` | `v -- v v` | Duplicate top of stack | + +### 3.3 Arithmetic Instructions + +All arithmetic instructions pop two operands and push the result. For binary operations, operands are popped as `(b, a)` — `b` is popped first (top of stack), `a` is popped second. The operation computes `a OP b`. + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `Add` | `"Add"` | `a + b` (Int/Float/Str concatenation) | +| `Sub` | `"Sub"` | `a - b` | +| `Mul` | `"Mul"` | `a * b` | +| `Div` | `"Div"` | `a / b` (Int: integer division; division by zero returns Nil) | +| `Mod` | `"Mod"` | `a % b` | +| `BitAnd` | `"BitAnd"` | `a & b` (Int only) | +| `BitOr` | `"BitOr"` | `a \| b` (Int only) | +| `BitXor` | `"BitXor"` | `a ^ b` (Int only) | +| `BitNot` | `"BitNot"` | `~a` (Int only, unary) | +| `Shl` | `"Shl"` | `a << b` (Int only) | +| `Shr` | `"Shr"` | `a >> b` (Int only) | + +**Type promotion:** When one operand is `Int` and the other is `Float`, the `Int` is widened to `Float` before the operation. + +### 3.4 Comparison Instructions + +Each pops `(b, a)` and pushes `Bool`: + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `Eq` | `"Eq"` | `a == b` | +| `NotEq` | `"NotEq"` | `a != b` | +| `Lt` | `"Lt"` | `a < b` | +| `Gt` | `"Gt"` | `a > b` | +| `LtEq` | `"LtEq"` | `a <= b` | +| `GtEq` | `"GtEq"` | `a >= b` | + +**Ordering semantics:** Numeric types use natural ordering. Strings use lexicographic ordering. Mixed numeric types promote Int to Float. All other mixed-type comparisons return `Equal` (neither less nor greater). + +### 3.5 Logical Instructions + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `And` | `"And"` | Pop `(b, a)`, push `Bool(a && b)` | +| `Or` | `"Or"` | Pop `(b, a)`, push `Bool(a \|\| b)` | +| `Not` | `"Not"` | Pop `a`, push `Bool(!a)` | + +Logical `And` and `Or` do not short-circuit — both operands are always evaluated before the instruction executes. + +### 3.6 Local Variable Instructions + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `LoadLocal(name)` | `{"LoadLocal": "name"}` | Push value of local variable; pushes `Nil` if not set | +| `StoreLocal(name)` | `{"StoreLocal": "name"}` | Pop top of stack into local variable | + +Local variables are stored in a `HashMap` per call frame. The `StoreLocal` instruction overwrites the slot unconditionally, implementing El's variable shadowing/mutation model. + +### 3.7 Function Call Instructions + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `Call { name, arity }` | `{"Call": {"name": "fn", "arity": 2}}` | Call function with `arity` arguments | +| `Return` | `"Return"` | Return from current frame | + +**Call dispatch sequence:** + +1. The `Call` instruction attempts `builtins::dispatch(name, arity, stack, args, global_state)`. +2. If the builtin dispatcher returns `NotBuiltin`, the VM looks up `name` in the function table. +3. If found, the VM saves the current frame (return IP + locals snapshot) to the call stack and sets IP to the function's entry point. +4. If not found, `Nil` is pushed (graceful failure). + +**Function table construction:** Before execution begins, the VM scans the entire instruction array for `Push(Int(n))` followed immediately by `StoreLocal("__fn_name")`. Each such pair registers function `name` at entry IP `n`. This is the function registration protocol that the codegen emits for every `fn` definition. + +**Return protocol:** `Return` pops the top call frame, restores `saved_locals`, and sets IP to `return_ip`. The return value remains on the stack. + +### 3.8 Control Flow Instructions + +All jump offsets are signed 32-bit integers relative to the instruction *after* the jump instruction (i.e., the new IP = current IP + 1 + offset). + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `Jump(offset)` | `{"Jump": 3}` | Unconditional jump | +| `JumpIf(offset)` | `{"JumpIf": 2}` | Jump if top of stack is `Bool(true)`; pops the value | +| `JumpIfNot(offset)` | `{"JumpIfNot": -5}` | Jump if top of stack is not `Bool(true)`; pops the value | + +A positive offset jumps forward; a negative offset jumps backward. An offset of `0` jumps to the instruction immediately following the jump (a no-op equivalent). + +### 3.9 Field and Indexing Instructions + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `GetField(name)` | `{"GetField": "field"}` | Pop Map or Struct; push value of named field (or Nil) | +| `SetField(name)` | `{"SetField": "field"}` | Pop value, update named field in Map/Struct on top of stack | +| `GetIndex` | `"GetIndex"` | Pop index, pop container; push element at index (or Nil) | +| `BuildMap(n)` | `{"BuildMap": 3}` | Pop 2n values (alternating key, value) and build a Map | +| `BuildList(n)` | `{"BuildList": 4}` | Pop n values and build a List | +| `BuildStruct { type_name, fields }` | `{"BuildStruct": {"type_name": "T", "fields": ["a","b"]}}` | Pop n values, build Struct | + +**BuildMap protocol:** The code generator pushes keys and values interleaved: `Push(Str(k1)), Push(v1), Push(Str(k2)), Push(v2), ..., BuildMap(n)`. The `BuildMap` instruction pops `2n` values in reverse order and assembles the pairs, then reverses to restore insertion order. + +**GetIndex on strings:** When the container is a `Str`, `GetIndex` with an integer index returns the nth character as a single-character `Str`. + +### 3.10 Special Instructions + +| Instruction | JSON | Description | +|-------------|------|-------------| +| `Activate { type_name, query }` | `{"Activate": {"type_name": "User", "query": "..."}}` | Semantic graph search; pushes `List` of results | +| `SealedBegin` | `"SealedBegin"` | Mark start of sealed region | +| `SealedEnd` | `"SealedEnd"` | Mark end of sealed region | +| `Reason { query }` | `{"Reason": {"query": "..."}}` | LLM inference call; pushes `Str` response | +| `Parallel { entries }` | `{"Parallel": {"entries": [["name", ip]]}}` | Spawn parallel sub-VMs; pushes `Map` of results | +| `TraceBegin { label }` | `{"TraceBegin": {"label": "..."}}` | Record start time for trace region | +| `TraceEnd { label }` | `{"TraceEnd": {"label": "..."}}` | Print elapsed ms for trace region | +| `ContractCheck { message }` | `{"ContractCheck": {"message": "..."}}` | Assert top of stack is true; exit(1) if not | +| `DeployFn { fn_name, route, target }` | `{"DeployFn": {"fn_name":"...", "route":"...", "target":"..."}}` | POST function to deployment API | +| `Nop` | `"Nop"` | No operation | +| `Halt` | `"Halt"` | Halt execution | + +--- + +## 4. Execution Model + +### 4.1 Execution State + +The VM maintains the following state during execution: + +- **Instruction pointer (IP):** Integer index into the flat instruction array. +- **Value stack:** `Vec`, grows upward. +- **Locals:** `HashMap` for the current call frame. +- **Call stack:** `Vec`, each frame holding `{ return_ip: usize, saved_locals: HashMap }`. +- **Global state:** `HashMap` shared across the entire program lifetime (accessible via `state_set`/`state_get` builtins). +- **Function table:** `HashMap` mapping function names to entry IPs (populated by the pre-execution scan pass). + +### 4.2 Execution Loop + +1. Load instruction at IP. +2. Dispatch on instruction variant. +3. Increment IP (unless the instruction set IP explicitly via a jump or call). +4. Repeat until `Halt` or `Return` with empty call stack. + +### 4.3 Call Frame Protocol + +When `Call` dispatches to a user-defined function: + +1. Save `(ip + 1, locals.clone())` as a `CallFrame` onto the call stack. +2. Clear locals (the new frame starts empty; parameters are passed via the stack and stored by the function's prologue). +3. Set IP to the function's entry point. + +When `Return` executes: + +1. Pop the top `CallFrame`. +2. Restore `locals` from `saved_locals`. +3. Set IP to `return_ip`. +4. The return value remains on the value stack. + +### 4.4 Function Prologue Protocol + +The codegen emits function prologues as a sequence of `StoreLocal` instructions in reverse parameter order. Because arguments are pushed left-to-right by the caller, and the stack is LIFO, storing in reverse order assigns parameters to their correct names: + +``` +// fn f(a: Int, b: Int) +// Caller pushes: a, b (b is on top) +StoreLocal("b") // pops b +StoreLocal("a") // pops a +``` + +### 4.5 Higher-Order Function Protocol + +The VM implements three higher-order function helpers that are resolved by name before the function table: + +| Name | Behavior | +|------|---------| +| `list_map` | Pops `fn_name: Str` and `list: List`; calls `fn_name` on each element; pushes result `List` | +| `list_filter` | Pops `fn_name: Str` and `list: List`; calls `fn_name` on each element; pushes filtered `List` | +| `list_reduce` | Pops `fn_name: Str`, `init: Value`, and `list: List`; folds; pushes accumulated value | + +### 4.6 Parallel Execution + +The `Parallel` instruction spawns one thread per entry using `std::thread::spawn`. Each thread runs a sub-VM instance starting at the specified entry IP. All threads complete before the parent continues; results are collected into a `Map` keyed by entry name. + +--- + +## 5. Builtin Dispatch + +### 5.1 Architecture + +The builtin dispatch system is the primary extension point of the ELVM. When a `Call` instruction is encountered: + +1. The VM calls `builtins::dispatch(name, arity, stack, program_args, global_state)`. +2. The dispatcher pattern-matches on `name` to identify the builtin. +3. If matched, it pops `arity` arguments from the stack, performs the operation, and pushes the result. +4. It returns one of: `Handled`, `HttpServe`, `Exit(code)`, or `NotBuiltin`. + +### 5.2 Dispatch Result + +| Variant | Meaning | +|---------|---------| +| `Handled` | Builtin executed successfully | +| `HttpServe` | Builtin started an HTTP server (non-blocking from VM perspective) | +| `Exit(code)` | Builtin called for process exit | +| `NotBuiltin` | Name not recognized; VM should continue to user function table | + +### 5.3 Builtin Categories + +The dispatch table handles all builtins described in the El language specification (Section 14). All builtins are registered by name string with no external configuration. Adding a new builtin requires only adding a match arm to the dispatch function. + +### 5.4 Engram Integration Builtins + +The `Activate` instruction dispatches to `engram_activate_search(type_name, query)`, which performs an HTTP POST to `ENGRAM_URL/search` with a JSON body `{ "query": query, "limit": 20 }`. Results are deserialized into a `List` of `Value` instances. + +The `Reason` instruction dispatches to `soma_reason(query)`, which performs an HTTP POST to `SOMA_URL/v1/chat/completions` with the query as the user message. + +Both external calls are blocking and synchronous. + +--- + +## 6. Memory Model + +### 6.1 Locals + +Local variables exist per call frame as a `HashMap`. Frame creation copies the parent's locals map (snapshot semantics). Modifications in a child frame do not affect the parent; the parent's state is restored from the saved snapshot when the child returns. + +### 6.2 Global State + +The `global_state` map (`HashMap`) persists for the entire process lifetime and is accessible from any point in program execution. It is shared by all call frames and all threads in a parallel execution. Access is mediated by the `state_set`, `state_get`, and `state_delete` builtins. + +### 6.3 Value Semantics + +All values are cloned on push, pop, and frame save. There is no aliasing — mutating a `List` or `Map` value does not affect other bindings to the same original value. + +### 6.4 Stack Discipline + +The stack is unbounded. Stack underflow (pop from empty stack) returns `Nil` rather than panicking. This preserves program continuity in the face of codegen anomalies but may silently propagate type errors. + +--- + +## 7. Error Handling + +### 7.1 Runtime Error Policy + +ELVM does not have a trap mechanism for arithmetic errors. Division by zero returns `Nil`. Out-of-bounds array access returns `Nil`. Missing map keys return `Nil`. Missing local variables return `Nil`. This design allows programs to use nil-checks for error detection. + +### 7.2 ContractCheck + +The `ContractCheck { message }` instruction provides explicit assertion. If the top of the stack is not `Bool(true)`, the VM prints the message to stderr and calls `std::process::exit(1)`. + +### 7.3 Fatal Errors + +The following conditions produce fatal exits: + +- Malformed ELVM header (truncated magic or length). +- Unsupported ELVM version. +- JSON deserialization failure of the payload. + +These are reported as `elvm: error: ` on stderr before exit. + +--- + +## 8. Source Map Format + +Debug builds produce a source map file (`.map.json`) alongside the bytecode. The source map is a JSON array: + +```json +[ + { "instruction": 0, "start": 0, "end": 5, "line": 3, "col": 0 }, + ... +] +``` + +| Field | Description | +|-------|-------------| +| `instruction` | Bytecode instruction index | +| `start` | Byte offset in source where the expression begins | +| `end` | Byte offset where the expression ends | +| `line` | 1-based source line number | +| `col` | 0-based column number | + +Source maps enable debuggers and error reporters to translate bytecode offsets to source positions. + +--- + +## 9. Bootstrap Sequence + +### 9.1 Genesis Bootstrap + +The bootstrap sequence that produces a self-hosting El toolchain: + +1. **Rust genesis compiler** (`engrams/el-compiler/`) reads `lexer.el`, `parser.el`, `codegen.el` and produces `el-compiler.elc` — the self-hosting compiler as an ELVM bytecode file. + +2. **ELVM** (`bin/elvm/`) executes `el-compiler.elc`. When invoked on an El source file, it produces an `.elc` output by running the El lexer, parser, and codegen implemented in El. + +3. **Verification pass:** The Rust genesis compiler and the self-hosted compiler are both run on the same El source file; their outputs are byte-compared. Identity confirms bootstrapping correctness. + +4. **Production toolchain:** The `el` binary (`bin/el/`) incorporates the genesis Rust compiler for the `build-file` command and invokes `elvm` for execution. The self-hosted compiler path is used for project builds. + +### 9.2 Invariant + +The Rust genesis compiler and the self-hosted El compiler are structural mirrors of each other. Both implement the same lexer, parser, and codegen algorithms. The genesis compiler serves exclusively as a bootstrap vehicle; no production El bytecode is produced by it after initial bootstrap. + +--- + +## 10. CLI Reference + +### elvm + +``` +elvm [args...] +elvm --version +elvm --help +``` + +Executes a compiled El bytecode file (`.elc`). Arguments after the file path are forwarded to the program via the `args()` builtin. + +### el build-file + +``` +el build-file [--target debug|release|prod] [-o output] +``` + +Compiles a single `.el` file without a project manifest. Useful for scripts and standalone programs. + +### el run + +``` +el run +``` + +Compiles the project with `debug` target and immediately executes the resulting bytecode. diff --git a/spec/language.md b/spec/language.md index de70692..22876a4 100644 --- a/spec/language.md +++ b/spec/language.md @@ -1,51 +1,194 @@ -# Engram Language Specification +# El Language Specification -Version 0.1.0 — April 2026 +Version 1.0.0 — April 29, 2026 --- ## Overview -Engram is a statically-typed programming language designed from first principles -around a knowledge graph type system. Its three defining properties: +El is a statically-typed, compiled programming language designed as the execution substrate for the Neuron agent runtime. El is self-hosting: the El compiler is written in El, compiled to ELVM bytecode, and executed by the El Virtual Machine. A Rust genesis compiler bootstraps the first iteration; all subsequent compilation is performed by the self-hosted compiler. -1. **Types are Engram nodes.** Every named type in the language is a node in a - knowledge graph. Type compatibility is not purely structural — it is also - semantic. Two types are compatible if their Engram node embeddings are - similar enough in meaning-space. +El has four defining properties: -2. **Autocomplete is spreading activation.** The language server (LSP) uses - spreading activation over the type graph to suggest completions. You get - concepts semantically related to what you're building, not just methods on - the current type. +1. **Self-hosting compiler.** The compiler (`lexer.el`, `parser.el`, `codegen.el`) is written in El, produces ELVM bytecode, and runs on the ELVM. The genesis Rust compiler (`el-compiler` crate) is used only for bootstrapping. -3. **The `prod` compilation target is quantum-sealed.** Bytecode compiled with - `--target prod` is encrypted with AES-256-GCM and signed. Without the - deployment key, the artifact is indistinguishable from random bytes. No - static analysis tool can decompile it. +2. **First-class application identity.** The `app` block is a language-level construct, not a library call. It declares service name, version, typed configuration schema, secrets, and feature flags — all resolved by the runtime before the program body executes. + +3. **Graph-native builtins.** Knowledge graph operations (`graph_compile`, `graph_traverse`, `graph_write_node`, `graph_write_edge`) are language primitives dispatched by the VM, not library imports. + +4. **Sealed artifact target.** The `prod` compilation target produces AES-256-GCM encrypted, BLAKE3-authenticated bytecode containers — quantum-resistant sealed artifacts that are indistinguishable from random bytes without the deployment key. --- -## 1. Syntax Reference +## 1. Lexical Structure -### 1.1 Comments +### 1.1 Source Encoding + +El source files are UTF-8 encoded. The canonical extension is `.el`. + +### 1.2 Comments ``` // Single-line comment — extends to end of line ``` -Block comments are not supported in v0.1. Use `//` on each line. +Block comments are not supported. Use `//` on each line. -### 1.2 Variable Declarations +### 1.3 Whitespace + +Spaces, tabs, newlines (`\n`), and carriage returns (`\r`) are whitespace. Whitespace is not significant except as a token separator. + +### 1.4 Identifiers + +``` +identifier = (alpha | '_') (alnum | '_')* +``` + +Identifiers are case-sensitive. Identifiers beginning with `__` (double underscore) are reserved for compiler-generated names. + +### 1.5 Keywords + +The following words are reserved and cannot be used as identifiers: + +``` +let fn type enum match return if else for in while +import from as with sealed activate where test seed +assert protocol impl retry times fallback reason parallel +trace requires deploy to via target true false app +version config secrets flags +``` + +### 1.6 Token Types + +| Token | Pattern | +|-------|---------| +| `Int` | `[0-9]+` | +| `Float` | `[0-9]+ '.' [0-9]+` | +| `Str` | `'"' (char | escape)* '"'` | +| `Bool` | `true` or `false` | +| `Ident` | identifier (not a keyword) | +| `Let` `Fn` `Type` etc. | keyword tokens | +| `EqEq` | `==` | +| `NotEq` | `!=` | +| `LtEq` | `<=` | +| `GtEq` | `>=` | +| `Arrow` | `->` | +| `FatArrow` | `=>` | +| `ColonColon` | `::` | +| `And` | `&&` | +| `Or` | `\|\|` | +| `PipeOp` | `\|>` | + +### 1.7 String Escape Sequences + +| Sequence | Character | +|----------|-----------| +| `\n` | Newline (U+000A) | +| `\t` | Tab (U+0009) | +| `\r` | Carriage return (U+000D) | +| `\"` | Double quote | +| `\\` | Backslash | +| `\0` | Null byte | + +--- + +## 2. Type System + +### 2.1 Primitive Types + +| Type | Description | Example literals | +|------|-------------|-----------------| +| `Int` | 64-bit signed integer | `42`, `-7`, `1_000` | +| `Float` | 64-bit IEEE 754 double | `3.14`, `0.5` | +| `String` | UTF-8 string | `"hello"` | +| `Bool` | Boolean | `true`, `false` | +| `Uuid` | RFC 4122 UUID | (runtime-produced only) | +| `Void` | Unit type; no value | — | +| `Any` | Dynamically-typed value | (for generic containers) | + +### 2.2 Composite Types + +| Type form | Description | +|-----------|-------------| +| `[T]` | Array (ordered sequence) of `T` | +| `T?` | Optional `T` — may be absent | +| `Map` | Key-value map with string keys | +| `List` | Untyped list (dynamic, used in builtins) | +| `Named` | User-defined struct or enum | + +### 2.3 Type Inference + +The compiler infers types for `let` bindings without annotation: + +``` +let x = 42 // inferred: Int +let s = "hello" // inferred: String +let b = true // inferred: Bool +``` + +Function parameter types and return types must always be annotated. Function signatures are specification. + +### 2.4 Type Coercions + +- `Int` is implicitly coercible to `Float`. +- `Float` is not coercible to `Int` (use `float_to_int` builtin). +- `T` is assignable to `T?` (non-optional is a subtype of optional). +- `String + String` performs concatenation via the `+` operator overload. + +### 2.5 Optional Type and Null Coalescing + +`T?` denotes an optional value. The null-coalescing operator `??` returns the left operand if it is not nil, otherwise the right: + +``` +let name: String? = maybe_get_name() +let display: String = name ?? "unknown" +``` + +The `?` postfix operator on a function call propagates `nil` outward (early return of `nil` if the subexpression is nil): + +``` +let val = some_optional_fn()? +``` + +### 2.6 Type Casting + +The `as` keyword performs explicit type casts at runtime: + +``` +let n: Int = 42 +let f: Float = n as Float +let s: String = f as String +``` + +Casting to `String` invokes the value's string representation. Casting numeric types performs the standard numeric conversion. Invalid casts produce `Nil`. + +--- + +## 3. Variables and Bindings + +### 3.1 Let Bindings ``` let name: Type = expression let name = expression // type inferred ``` -Variables are immutable by default. All bindings are block-scoped. +All bindings are block-scoped. Bindings are immutable by default. Shadowing is permitted: a new `let` in the same scope with the same name creates a new binding that shadows the previous one. This is the mechanism for mutation in El — the VM's `StoreLocal` instruction overwrites the slot for the name. -### 1.3 Functions +``` +let count: Int = 0 +let count = count + 1 // shadows previous binding; effective mutation +``` + +### 3.2 Scope + +Bindings are valid from the point of declaration to the end of the enclosing block. Function bodies, `if` branches, `for` bodies, and `while` bodies each introduce a new scope. + +--- + +## 4. Functions + +### 4.1 Function Definition ``` fn name(param1: Type1, param2: Type2) -> ReturnType { @@ -54,25 +197,120 @@ fn name(param1: Type1, param2: Type2) -> ReturnType { } ``` -Functions are first-class values. The type of a function is: +Functions are first-class values. A function definition emits a jump over the function body and registers the entry IP in the VM's function table via a `Push(Int(entry)) StoreLocal("__fn_name")` stanza. + +### 4.2 Function Type + +The type of a function is expressed as: + ``` fn(Type1, Type2) -> ReturnType ``` -### 1.4 Types (Structs) +Functions can be passed as values and stored in variables: + +``` +let f: fn(Int) -> Int = double +``` + +### 4.3 Return Statement + +``` +return expression +``` + +An implicit `return nil` is appended by the compiler if no explicit return is present at the end of a function body. + +### 4.4 Calling Convention + +Arguments are pushed left-to-right onto the stack. The function body pops parameters in reverse order (right-to-left) using `StoreLocal`. Return values are left on the stack top when `Return` executes. + +--- + +## 5. Control Flow + +### 5.1 If/Else + +``` +if condition { + // then branch +} else { + // else branch +} +``` + +Both branches must produce the same type when used as an expression. The `else` branch is optional; its absence produces `Void`. If/else chains use `else if`: + +``` +if x > 0 { + "positive" +} else if x < 0 { + "negative" +} else { + "zero" +} +``` + +### 5.2 While Loops + +``` +while condition { + // body +} +``` + +Condition is evaluated before each iteration. The loop exits when the condition is `false`. + +### 5.3 For Loops + +``` +for item in collection { + // body +} +``` + +`collection` must be a `List` or `[T]`. The compiler desugars `for` into: compute list, store length, initialize counter at 0, loop while counter < length, load element at counter, execute body, increment counter, jump back. + +### 5.4 Match Expressions + +``` +match expression { + Pattern1 => result_expr1 + Pattern2 => result_expr2 + _ => default_expr +} +``` + +Pattern forms: + +| Pattern | Meaning | +|---------|---------| +| `_` | Wildcard — always matches | +| `name` | Binding — captures subject into `name` | +| `42` | Integer literal | +| `"str"` | String literal | +| `true` / `false` | Boolean literal | +| `EnumName::Variant` | Unit enum variant (future) | +| `EnumName::Variant(binding)` | Payload-bearing variant (future) | + +All arms must produce the same type. A `match` with no matching arm evaluates to `nil`. + +--- + +## 6. Data Structures + +### 6.1 Struct Types ``` type TypeName { field1: Type1 field2: Type2 - // ... } ``` -Every `type` definition registers a node in the Engram type graph. The type -name becomes searchable via spreading activation. +Struct types are registered in the type environment at compile time. Field access is `value.field_name`, checked at compile time. The VM represents struct instances as `Value::Struct { type_name, fields }`. -### 1.5 Enums +### 6.2 Enum Types ``` enum EnumName { @@ -82,746 +320,643 @@ enum EnumName { } ``` -Enum variants without parentheses carry no payload. Variants with parentheses -carry exactly one value of the given type. +Variants without parentheses carry no payload. Variants with parentheses carry exactly one value of the given type. Enum variants are referenced as `EnumName::Variant`. -### 1.6 Pattern Matching - -``` -match expression { - Pattern1 => result_expr1 - Pattern2 => result_expr2 - // ... -} -``` - -Pattern forms: -- `EnumName::Variant` — unit enum variant -- `EnumName::Variant(binding)` — enum variant with payload binding -- `literal` — exact literal (42, "str", true) -- `name` — binding (captures the subject into `name`) -- `_` — wildcard (always matches, discards) - -All arms must produce the same type. The `match` expression evaluates to that type. - -### 1.7 Control Flow - -**If/else:** -``` -if condition { - // then branch -} else { - // else branch -} -``` - -Both branches must produce the same type. The `else` branch is optional (produces `Void`). - -**For loops:** -``` -for item in collection { - // body -} -``` - -### 1.8 Field Access - -``` -value.field_name -``` - -Field access is type-checked at compile time. Accessing a field that does not -exist in the type definition is a compile error. - -### 1.9 Array Literals +### 6.3 Array Literals ``` let numbers: [Int] = [1, 2, 3] let empty: [String] = [] ``` -### 1.10 Index Access +### 6.4 Map Literals ``` -let first: Int = numbers[0] +let m: Map = { "key1": value1, "key2": value2 } ``` -Index expressions require an `Int` index. Bounds checking is runtime behavior. +Map literals use string keys and `Any` values. The VM represents maps as `Value::Map(Vec<(String, Value)>)`, preserving insertion order. + +### 6.5 Field Access and Index Access + +``` +let field = struct_value.field_name +let elem = array[0] +let val = map["key"] +``` + +Index expressions on arrays require an `Int` index. Bounds violations return `nil`. String indexing `s[n]` returns the nth character as a `String`. --- -## 2. Type System +## 7. Operators -### 2.1 Primitive Types +### 7.1 Arithmetic Operators -| Type | Description | Example literal | -|----------|--------------------------------------|---------------------| -| `Int` | 64-bit signed integer | `42`, `-7`, `1_000` | -| `Float` | 64-bit IEEE 754 double | `3.14`, `0.5` | -| `String` | UTF-8 string | `"hello"` | -| `Bool` | Boolean | `true`, `false` | -| `Uuid` | RFC 4122 UUID | (runtime only) | -| `Void` | Unit type; no value | — | +| Operator | Types | Result | +|----------|-------|--------| +| `+` | Int, Float, String | Same as operands (String: concatenation) | +| `-` | Int, Float | Same | +| `*` | Int, Float | Same | +| `/` | Int, Float | Same (integer division for Int) | +| `%` | Int, Float | Modulo | -### 2.2 Composite Types +### 7.2 Bitwise Operators -| Type form | Description | -|-------------|--------------------------------------| -| `[T]` | Array of `T` | -| `T?` | Optional `T` (may be absent) | -| `Named` | User-defined struct or enum | +| Operator | Types | Result | +|----------|-------|--------| +| `&` | Int | Bitwise AND | +| `\|` | Int | Bitwise OR | +| `^` | Int | Bitwise XOR | +| `~` | Int | Bitwise NOT (unary) | +| `<<` | Int | Left shift | +| `>>` | Int | Right shift | -### 2.3 Numeric Coercions +### 7.3 Comparison Operators -- `Int` is implicitly coercible to `Float`. -- `Float` is not coercible to `Int` (use explicit conversion when available). -- `String + String` uses concatenation (the `+` operator is overloaded). +| Operator | Result | +|----------|--------| +| `==` | Bool | +| `!=` | Bool | +| `<` `>` `<=` `>=` | Bool | -### 2.4 Structural vs. Semantic Compatibility +### 7.4 Logical Operators -Standard structural compatibility: -- `Named("User")` is compatible with `Named("User")` (same name). -- `[Int]` is compatible with `[Int]`. -- `T` is compatible with `T?` (non-optional can be used as optional). -- `Int` is compatible with `Float` (widening). +| Operator | Result | +|----------|--------| +| `&&` | Bool | +| `\|\|` | Bool | +| `!` | Bool (unary) | -**Semantic compatibility (novel):** +### 7.5 Special Operators -When two named types have registered Engram node type mappings that refer to -the same node class, they are considered semantically compatible: +| Operator | Meaning | +|----------|---------| +| `??` | Null coalescing: left if not nil, else right | +| `?` (postfix) | Optional propagation: return nil if subexpression is nil | +| `as` | Type cast | +| `\|>` | Pipe: `x \|> f` desugars to `f(x)` | -``` -// Register User and Customer as both mapping to the "Entity" Engram node type -// → User and Customer are semantically compatible -``` +### 7.6 Operator Precedence (high to low) -This is computed via cosine similarity over node embeddings when an Engram -database is connected. Without a database, the comparison is symbolic (same -node type string = compatible). - -Semantic compatibility threshold: cosine similarity ≥ 0.85 (configurable). - -### 2.5 Type Inference - -The compiler infers types for `let` bindings without annotations: -``` -let x = 42 // inferred: Int -let s = "hello" // inferred: String -let b = true // inferred: Bool -``` - -Function return types and parameter types must always be annotated. This is -intentional: function signatures are documentation. +1. `!` `~` (unary), postfix `?` +2. `*` `/` `%` +3. `+` `-` +4. `<<` `>>` +5. `&` +6. `^` +7. `|` +8. `<` `>` `<=` `>=` +9. `==` `!=` +10. `&&` +11. `||` +12. `??` +13. `|>` --- -## 3. The `activate` Construct +## 8. Module System -### 3.1 Syntax +### 8.1 Import + +``` +import "filename.el" +``` + +Imports all top-level bindings from the named file into the current scope. The path is relative to the importing file's directory. Import cycles are not permitted. + +### 8.2 From-Import + +``` +from module_name import { Name1, Name2 } +``` + +Imports named symbols from a module. The parser records the module name and symbol list; the linker resolves them at build time. + +--- + +## 9. The App Block + +The `app` block is a first-class language construct that declares service identity. It must appear at top level, before the program body. + +``` +app "service-name" { + version "1.0.0" + + config { + KEY: Type = default_value + prod { + KEY = "production-override" + } + } + + secrets { + SECRET_NAME: Type + } + + flags { + feature_name: Bool = false + } +} +``` + +### 9.1 Config Block + +Config entries declare typed configuration keys with optional defaults. Environment variables with the same name override defaults. The `prod { }` sub-block provides environment-specific overrides applied when `NEURON_ENV=prod`. + +Config values are accessed via the `config(key)` builtin: + +``` +let api_url: String = config("NEURON_API_URL") +``` + +### 9.2 Secrets Block + +Secrets entries declare required secret values. The runtime loads them from environment variables or from `~/.neuron/secrets.json`. Secrets are redacted from all trace output. + +Secrets are accessed via the `secret(key)` builtin: + +``` +let token: String = secret("NEURON_TOKEN") +``` + +### 9.3 Flags Block + +Feature flags declare boolean runtime switches. Flags are accessed via `flag(name)`: + +``` +let enabled: Bool = flag("bidirectional_ctx") +``` + +### 9.4 Runtime Resolution + +When the El runtime encounters an `app` block, it: + +1. Parses the app block from the source before compilation begins (the `parse_app_block` function in `bin/el/src/main.rs`). +2. Resolves the active environment from `NEURON_ENV` (default: `"dev"`). +3. Applies environment-specific config overrides. +4. Loads secrets from environment variables, falling back to the secrets file. +5. Populates thread-local state: `APP_SERVICE`, `APP_VERSION`, `APP_CONFIG`, `APP_SECRETS`, `APP_FLAGS`, `APP_INSTANCE`. +6. Registers secrets in the `SECRET_VALUES` set for redaction. + +The program body executes after resolution completes. `config()`, `secret()`, and `flag()` builtins read from the thread-local state. + +--- + +## 10. The Sealed Block + +``` +sealed { + let api_key: String = env("API_KEY") + // sensitive operations +} +``` + +The `sealed {}` construct marks a code region as containing sensitive material. In debug builds, it emits `SealedBegin` and `SealedEnd` bytecode markers that signal the debugger not to expose values from this region. In prod builds, the entire artifact is AES-256-GCM encrypted, making the `sealed {}` annotation redundant but preserved for documentation and tooling. + +--- + +## 11. The Activate Construct ``` activate TypeName where "semantic query string" ``` -### 3.2 What It Does +`activate` is a first-class language construct that performs a spreading activation query over the connected Engram knowledge graph and returns a typed array of results. -`activate` is a first-class language construct that performs a spreading -activation query over the Engram knowledge graph and returns a typed array of -results. +At runtime, the ELVM dispatches an `Activate { type_name, query }` instruction to the Engram HTTP API at `ENGRAM_URL` (default `http://localhost:8742`). The API performs semantic search over graph nodes and returns matching nodes. -The query string is a natural language description. At runtime, the Engram -runtime: - -1. Embeds the query string into the same vector space as node embeddings. -2. Starts activation at the `TypeName` node and all nodes semantically related - to it (cosine similarity above threshold). -3. Spreads activation outward through graph edges, attenuated by edge weight - and node salience. -4. Returns all nodes whose activation level exceeds the minimum threshold, - projected back to the `TypeName` schema. - -### 3.3 Static Typing - -The result type of `activate TypeName where "..."` is always `[TypeName]`. +The result type is always `[TypeName]`: ``` let users: [User] = activate User where "recent premium subscribers" -// ↑ type-checked: [User] ``` -If `TypeName` is not a registered type, the compiler emits an error. The -query string is opaque to the type checker — it only passes the string through -to the runtime. - -### 3.4 Without an Engram Database - -When compiled without an Engram database (`CompilerOptions::engram_db_path` is -`None`), the `activate` construct emits a stub instruction. At runtime, the -interpreter emits a diagnostic and returns an empty array `[]`. - -This allows programs using `activate` to compile and run in development without -requiring a live Engram instance. - -### 3.5 Compile-time Behavior - -At `--target prod`, `activate` is compiled to an `ACTIVATE` bytecode instruction. -The query string is embedded in the bytecode. The sealed artifact protects the -query from being read by static analysis. +When no Engram instance is connected, `activate` returns an empty array. --- -## 4. Sealed Blocks +## 12. Graph Builtins -### 4.1 Syntax +The following builtins provide direct access to the Engram knowledge graph. They are VM primitives dispatched by name without imports. -``` -sealed { - let api_key: String = env("API_KEY") - // ... -} -``` - -### 4.2 What's Protected - -Code inside a `sealed {}` block is subject to additional runtime protection: - -- **In debug builds:** The `SEALED_BEGIN` / `SEALED_END` bytecode markers are - emitted. The debugger is notified not to expose values in this region. -- **In release builds:** Same as debug, with no source map entries for the - sealed region. -- **In prod builds:** The entire bytecode (including the sealed section) is - AES-256-GCM encrypted in the sealed artifact. There is no additional treatment - of the sealed section — the *entire prod artifact* is the sealed section. - -### 4.3 Intent - -The `sealed {}` block communicates developer intent: "this section handles -sensitive material." It is especially meaningful during development when -`debug` builds are used, since it signals to the runtime and any attached -debugger to redact values from inspection. - -In `prod` builds, the `sealed {}` annotation is redundant (the whole artifact -is sealed), but it is preserved for documentation and future tooling that can -enforce stricter runtime isolation. +| Builtin | Signature | Description | +|---------|-----------|-------------| +| `graph_compile` | `(query: String, depth: Int) -> String` | Compile graph context for LLM injection | +| `graph_traverse` | `(node_id: String, depth: Int) -> List` | BFS from node, return activated nodes | +| `graph_write_node` | `(label: String, content: String, tier: String, tags: [String]) -> String` | Create a node, return UUID | +| `graph_write_edge` | `(from_id: String, to_id: String, relation: String, weight: Float) -> Bool` | Create an edge | +| `graph_search` | `(query: String, limit: Int) -> List` | Full-text search over nodes | +| `graph_get_node` | `(node_id: String) -> Map?` | Fetch a single node by ID | +| `graph_activate` | `(seeds: [String], depth: Int, limit: Int) -> List` | Spreading activation from seed set | --- -## 5. Compilation Targets +## 13. HTTP Builtins -### 5.1 debug +| Builtin | Signature | Description | +|---------|-----------|-------------| +| `http_get` | `(url: String) -> String` | HTTP GET, returns response body | +| `http_post` | `(url: String, body: String) -> String` | HTTP POST with JSON body | +| `http_put` | `(url: String, body: String) -> String` | HTTP PUT with JSON body | +| `http_delete` | `(url: String) -> String` | HTTP DELETE | +| `http_serve` | `(handler_fn: String) -> Void` | Start HTTP server on configured port | +| `http_serve_on` | `(port: Int, handler_fn: String) -> Void` | Start HTTP server on specified port | -``` -el build file.el --target debug -``` - -Produces: -- `file.elc` — JSON-serialized bytecode instructions -- `file.map.json` — source map: JSON array of `{instruction, start, end, line, col}` objects - -The source map allows debuggers and error reporters to translate bytecode -offsets back to exact source positions (file + line + column). - -Debug builds: -- No dead-code elimination -- No constant folding -- Full source map coverage -- Type errors are reported as warnings (compilation continues) - -### 5.2 release - -``` -el build file.el --target release -``` - -Produces: -- `file.elc` — JSON-serialized bytecode instructions - -Release builds: -- No source map -- Minor dead-code pruning (unreachable after `return`) -- Type errors are warnings (compilation continues) - -### 5.3 prod - -``` -el build file.el --target prod -ENGRAM_SEAL_KEY=my-secret el build file.el --target prod -``` - -Produces: -- `file.sealed` — quantum-sealed artifact - -Prod builds: -- **Type errors are fatal** — the compiler refuses to produce a sealed artifact - from a program with type errors -- The output is encrypted and cannot be decompiled -- All debug information is stripped before sealing -- Source maps are never produced +HTTP builtins use blocking I/O. The runtime dispatches them synchronously. The handler function receives a request map and must return a response map. --- -## 6. The Sealed Artifact Format +## 14. Standard Library Builtins -### 6.1 Wire Format +### 14.1 String Operations + +| Builtin | Description | +|---------|-------------| +| `str_len(s)` | Length in characters | +| `str_slice(s, start, end)` | Substring | +| `str_starts_with(s, prefix)` | Boolean | +| `str_ends_with(s, suffix)` | Boolean | +| `str_contains(s, substr)` | Boolean | +| `str_replace(s, from, to)` | Replace first occurrence | +| `str_split(s, delim)` | Split into List | +| `str_join(list, delim)` | Join List into String | +| `str_trim(s)` | Strip leading/trailing whitespace | +| `str_upper(s)` | Uppercase | +| `str_lower(s)` | Lowercase | +| `str_pad_left(s, width, pad)` | Left-pad with pad character | +| `str_pad_right(s, width, pad)` | Right-pad with pad character | +| `str_format(template, data)` | `{key}` interpolation from map | +| `str_char_at(s, idx)` | Character at index | +| `str_char_code(s, idx)` | Unicode code point at index | +| `str_from_char_code(code)` | Character from code point | + +### 14.2 Integer Operations + +| Builtin | Description | +|---------|-------------| +| `int_to_str(n)` | Integer to string | +| `str_to_int(s)` | Parse integer | +| `int_to_float(n)` | Widen to float | +| `abs(n)` | Absolute value | +| `min(a, b)` | Minimum | +| `max(a, b)` | Maximum | + +### 14.3 Float Operations + +| Builtin | Description | +|---------|-------------| +| `float_to_str(f)` | Float to string | +| `float_to_int(f)` | Truncate to integer | +| `str_to_float(s)` | Parse float | +| `format_float(f, decimals)` | Format to N decimal places | +| `math_sin(f)` | Sine | +| `math_cos(f)` | Cosine | +| `math_sqrt(f)` | Square root | +| `math_pi()` | π constant | +| `decimal_round(f, places)` | Round to N decimal places | + +### 14.4 List Operations + +| Builtin | Description | +|---------|-------------| +| `list_len(l)` | Length | +| `list_get(l, idx)` | Element at index | +| `list_append(l, v)` | Append, return new list | +| `list_push(l, v)` | Alias for `list_append` | +| `list_new()` | Empty list | +| `list_join(l, delim)` | Join to string | +| `list_range(start, end)` | Integer range | +| `list_map(l, fn_name)` | Map over list | +| `list_filter(l, fn_name)` | Filter list | +| `list_reduce(l, init, fn_name)` | Reduce list | +| `list_peek_last(l)` | Last element without removing | + +### 14.5 JSON Operations + +| Builtin | Description | +|---------|-------------| +| `json_parse(s)` | Parse JSON string to value | +| `json_stringify(v)` | Serialize value to JSON | +| `json_get_string(json, key)` | Extract string field | +| `json_get_int(json, key)` | Extract integer field | +| `json_get_float(json, key)` | Extract float field | +| `json_get_raw(json, key)` | Extract raw JSON sub-object | + +### 14.6 I/O Operations + +| Builtin | Description | +|---------|-------------| +| `println(s)` | Print line to stdout | +| `print(s)` | Print without newline | +| `env(key)` | Read environment variable | +| `getpid()` | Current process ID | +| `args()` | Command-line arguments as List | +| `exit(code)` | Exit process | + +### 14.7 File System Operations + +| Builtin | Description | +|---------|-------------| +| `fs_read(path)` | Read file contents as String | +| `fs_write(path, content)` | Write string to file, return Bool | +| `fs_mkdir(path)` | Create directory | +| `fs_list(path)` | List directory entries as List | +| `fs_exists(path)` | Boolean | + +### 14.8 Time Operations + +| Builtin | Description | +|---------|-------------| +| `time_now_utc()` | Current time as Unix seconds | +| `time_format(ts, format)` | Format timestamp ("ISO", "RFC") | +| `time_to_parts(ts)` | Decompose into year/month/day/etc. | +| `time_from_parts(secs, nanos, tz)` | Construct timestamp | +| `time_add(ts, amount, unit)` | Add duration ("day", "hour", etc.) | +| `time_diff(ts1, ts2, unit)` | Difference in units | + +### 14.9 State Operations (Global Mutable State) + +The VM maintains a global key-value string store accessible within a process lifetime: + +| Builtin | Description | +|---------|-------------| +| `state_set(key, value)` | Store string value | +| `state_get(key)` | Retrieve string value | +| `state_delete(key)` | Delete key | + +### 14.10 Color/Terminal Formatting + +| Builtin | Description | +|---------|-------------| +| `color_bold(s)` | Bold ANSI formatting | +| `color_dim(s)` | Dim ANSI formatting | +| `color_red(s)` | Red text | +| `color_green(s)` | Green text | +| `color_yellow(s)` | Yellow text | +| `color_cyan(s)` | Cyan text | + +### 14.11 Native List Primitives (Self-Hosting Compiler) + +These primitives are used by the self-hosting compiler internals and are not available in user programs: + +| Builtin | Description | +|---------|-------------| +| `native_list_empty()` | Create empty list | +| `native_list_append(l, v)` | Append to list | +| `native_list_get(l, idx)` | Element at index | +| `native_list_len(l)` | List length | +| `native_string_chars(s)` | Split string into character list | +| `native_string_contains(s, substr)` | Substring test | +| `native_str_to_int(s)` | Parse integer | +| `native_int_to_str(n)` | Format integer | + +--- + +## 15. Compilation Model + +### 15.1 Pipeline + +``` +source.el + → [Lexer] → token list + → [Parser] → AST (list of statement maps) + → [Codegen] → JSON bytecode string + → [Linker] → resolved imports + → [Wrapper] → ELVM binary container (.elc) + → [Sealer] → encrypted sealed artifact (.sealed) [prod only] +``` + +### 15.2 Self-Hosting Architecture + +The El compiler is written in El: + +- `el-compiler/src/lexer.el` — tokenizer +- `el-compiler/src/parser.el` — recursive descent parser +- `el-compiler/src/codegen.el` — bytecode emitter + +These files are compiled by the Rust genesis compiler (`engrams/el-compiler/`) to produce the self-hosting compiler binary. Once bootstrapped, the self-hosting compiler compiles itself and all subsequent El programs. + +The genesis Rust compiler implements the same pipeline in Rust (`el-parser`, `el-compiler` crates) as a structural mirror of the El source. Both produce identical bytecode for valid El programs. + +### 15.3 Compilation Targets + +| Target | Artifact | Behavior | +|--------|----------|----------| +| `debug` | `.elc` + `.map.json` | Full source maps, no dead-code elimination, type errors are warnings | +| `release` | `.elc` | No source maps, minor dead-code pruning, type errors are warnings | +| `prod` | `.sealed` | AES-256-GCM encrypted, type errors are fatal, no debug info | + +### 15.4 Incremental Builds + +The build system tracks a BLAKE3 hash of every source file in `.el/build-cache.json`. Only changed files and their dependents are recompiled. + +--- + +## 16. Sealed Artifact Format + +### 16.1 Purpose + +The `prod` target produces sealed artifacts — bytecode containers encrypted with AES-256-GCM and authenticated with BLAKE3. Without the deployment key, the artifact is indistinguishable from random bytes. Decompilers and static analysis tools receive AES-GCM ciphertext. + +### 16.2 Wire Format ``` Offset Size Field -────── ────── ──────────────────────────────────────────── +────── ────── ────────────────────────────────────────────────── 0 8 Magic: b"ENGRAM01" 8 2 Format version: u16 big-endian (currently 1) -10 * JSON body: SealedArtifact struct +10 * JSON body: SealedArtifact ``` -The JSON body is a `SealedArtifact`: +SealedArtifact JSON: ```json { "algorithm_id": "aes256gcm-v1", - "signature": "...(base64)...", - "encapsulated_key": "...(base64)...", - "nonce": "...(base64)...", - "ciphertext": "...(base64)...", - "deployment_fingerprint": "...(base64 or null)..." + "signature": "", + "encapsulated_key": "", + "nonce": "", + "ciphertext": "", + "deployment_fingerprint": "" } ``` -### 6.2 Field Descriptions +### 16.3 Sealing Process -| Field | Description | -|--------------------------|----------------------------------------------------------------------| -| `algorithm_id` | The encryption algorithm. Currently `aes256gcm-v1`. Reserved for ML-KEM upgrade. | -| `signature` | BLAKE3 keyed MAC over `(algorithm_id ‖ nonce ‖ ciphertext)`. Detects tampering before decryption attempt. | -| `encapsulated_key` | 32 bytes: `symmetric_key XOR BLAKE3(deployment_binding_material)`. Requires knowledge of the deployment secret to recover the symmetric key. | -| `nonce` | 12-byte (96-bit) AES-GCM nonce. Randomly generated per seal operation. | -| `ciphertext` | AES-256-GCM ciphertext of the bytecode, including the 128-bit GCM authentication tag. | -| `deployment_fingerprint` | BLAKE3 hash of the deployment binding material. Stored so the unsealer can verify it is running in the correct environment before attempting decryption. `null` for `DeploymentBinding::None`. | - -### 6.3 Sealing Process - -1. Generate a cryptographically random 256-bit symmetric key `K`. -2. Encrypt bytecode: `ciphertext = AES-256-GCM(K, nonce=random_96bit, plaintext=bytecode)`. -3. Derive the deployment binding hash: `H = BLAKE3(deployment_material)`. -4. Encapsulate: `encapsulated_key = K XOR H` (32 bytes). +1. Generate a cryptographically random 256-bit symmetric key K. +2. Encrypt: `ciphertext = AES-256-GCM(K, nonce=random_96bit, plaintext=bytecode)`. +3. Derive binding hash: `H = BLAKE3(deployment_material)`. +4. Encapsulate: `encapsulated_key = K XOR H`. 5. Compute MAC: `signature = BLAKE3-keyed(K, algorithm_id ‖ nonce ‖ ciphertext)`. 6. Serialize: `ENGRAM01 ‖ version_u16be ‖ JSON(artifact)`. -### 6.4 Unsealing Process +### 16.4 Deployment Binding Modes -1. Parse magic and version; reject if not `ENGRAM01 / version 1`. -2. Derive deployment hash: `H = BLAKE3(provided_binding_key)`. -3. Verify fingerprint: if `deployment_fingerprint` is present, assert `BLAKE3(binding_key) == fingerprint`. Fail with `BindingMismatch` if not. -4. Recover symmetric key: `K = encapsulated_key XOR H`. -5. Verify MAC: compute `BLAKE3-keyed(K, ...)` and compare to `signature`. Fail with `SignatureInvalid` if mismatch. -6. Decrypt: `bytecode = AES-256-GCM-Decrypt(K, nonce, ciphertext)`. The GCM auth tag is verified here automatically. +| Mode | Description | Security | +|------|-------------|----------| +| `EnvironmentKey(var)` | Key from environment variable | High | +| `MachineFingerprint` | Key from hostname + OS + architecture | Medium | +| `None` | Zero vector (development only) | None | -### 6.5 Security Properties +### 16.5 Security Properties -**Why "quantum-sealed":** - -AES-256 is quantum-resistant at the 256-bit key length. Grover's algorithm -provides a quadratic speedup in key search, reducing effective security from -2^256 to 2^128. 128-bit quantum security is considered sufficient by NIST for -the foreseeable future. - -The `algorithm_id` field is forward-compatible: when `ml-kem` (CRYSTALS-Kyber -ML-KEM-768 or ML-KEM-1024) crates stabilize, the upgrade is: - -1. Implement `SealAlgorithm::MlKem768` in `el-seal`. -2. The `encapsulated_key` field becomes the KEM-encapsulated ciphertext. -3. Old artifacts retain their `aes256gcm-v1` algorithm_id and continue to - unseal via the existing code path. - -**Decompilation resistance:** - -Without the deployment key, `K` cannot be recovered (requires knowing -`deployment_material`), so `ciphertext` is indistinguishable from random -bytes. Static analysis tools, disassemblers, and decompilers receive the -AES-GCM ciphertext — semantically empty. Any tampering flips bits in the GCM -ciphertext, causing authentication tag verification to fail before the -symmetric layer is even reached. +AES-256 provides 128-bit post-quantum security under Grover's algorithm. The `algorithm_id` field supports forward migration to ML-KEM (CRYSTALS-Kyber) without format changes. --- -## 7. Deployment Binding Modes +## 17. ELVM Integration -| Mode | Description | Security | -|-----------------------|--------------------------------------------------------|--------------| -| `EnvironmentKey(var)` | Derives binding from the value of an environment variable. Default: `ENGRAM_SEAL_KEY`. | High — key must be provisioned at runtime | -| `MachineFingerprint` | Derives binding from hostname + OS + architecture. Artifact can only run on the same machine. | Medium — fingerprint is observable | -| `None` | No binding (zero vector). Testing and development only. | None | +El compiles to ELVM bytecode. See the ELVM specification (`elvm.md`) for the complete instruction set and execution model. Key integration points: + +- The El codegen emits function registrations as `Push(Int(entry_ip)) StoreLocal("__fn_name")` stanzas that the VM's scan pass collects into the function table. +- User-defined functions are called via `Call { name, arity }` which the VM resolves first against the builtin dispatch table, then against the function table. +- The `app` block is parsed from source by the `el` binary before compilation; it does not appear in the bytecode directly. +- The `activate` construct compiles to an `Activate { type_name, query }` instruction. +- The `reason` construct compiles to a `Reason { query }` instruction. +- The `parallel` block compiles to a `Parallel { entries }` instruction. +- The `deploy` construct compiles to a `DeployFn { fn_name, route, target }` instruction. --- -## 8. Operators +## 18. Package System -| Operator | Types | Result | -|----------|--------------------|--------| -| `+` | Int, Float, String | same as operands (String: concatenation) | -| `-` | Int, Float | same | -| `*` | Int, Float | same | -| `/` | Int, Float | same | -| `==` | any compatible pair | Bool | -| `!=` | any compatible pair | Bool | -| `<` `>` `<=` `>=` | Int, Float | Bool | -| `&&` | Bool, Bool | Bool | -| `\|\|` | Bool, Bool | Bool | -| `!` | Bool | Bool | +### 18.1 Project Manifest — `manifest.el` -Operator precedence (high to low): -1. `!` (unary) -2. `*` `/` -3. `+` `-` -4. `<` `>` `<=` `>=` -5. `==` `!=` -6. `&&` -7. `||` +The project manifest is an El file — everything is El. The file is named `manifest.el` +and lives at the project root. It uses El block syntax: space-separated declarations, +no equals signs, strings in `"..."`, integers as bare numbers, arrays as `[...]`. ---- +```el +// manifest.el +package "my-service" { + version "0.1.0" + description "What this does" + authors ["Will Anderson "] + edition "2026" +} -## 9. Escape Sequences in String Literals +dependencies { + engram-http "1.2" + some-local { path "../some-local" } +} -| Sequence | Character | -|----------|------------| -| `\n` | Newline | -| `\t` | Tab | -| `\r` | Carriage return | -| `\"` | Double quote | -| `\\` | Backslash | -| `\0` | Null byte | +build { + target "prod" + entry "src/main.el" + output "dist/" + seal_key "env:ENGRAM_SEAL_KEY" +} ---- - -## 10. CLI Reference - -``` -el build [--target debug|release|prod] [-o ] -el run -el check -el seal [-o ] -el unseal [-o ] +cross { + targets ["x86_64-linux", "aarch64-linux", "aarch64-macos", "wasm32"] +} ``` -**el build** — Compile a source file. Default target is `debug`. +Rules: +- String values use `"..."` — no equals sign +- Integer values are bare numbers — no equals sign +- Arrays use `[...]` +- Block sections use `{ }` — no `[section]` headers +- Only include sections that are relevant to the project +- The `app` section is for native desktop apps (el-ui); it sets window dimensions -**el run** — Compile with `debug` target and execute immediately in the -built-in interpreter. Does not write an output file. - -**el check** — Type-check only. Exits with code 0 if no errors, 1 if errors. -Useful for CI. - -**el seal** — Take an existing release artifact and seal it. Reads -`ENGRAM_SEAL_KEY` from the environment if set. - -**el unseal** — Decrypt a sealed artifact. Reads `ENGRAM_SEAL_KEY` from the -environment. Writes decrypted bytecode to the output path. - ---- - -## 11. Grammar (EBNF) - -```ebnf -program = stmt* EOF - -stmt = let_stmt - | return_stmt - | fn_def - | type_def - | enum_def - | expr_stmt - -let_stmt = "let" IDENT (":" type_expr)? "=" expr ";"? -return_stmt = "return" expr ";"? -expr_stmt = expr ";"? - -fn_def = "fn" IDENT "(" param_list ")" "->" type_expr "{" stmt* "}" -type_def = "type" IDENT "{" (IDENT ":" type_expr ","? ";"?)* "}" -enum_def = "enum" IDENT "{" variant* "}" -variant = IDENT ("(" type_expr ")")? ","? - -param_list = (param ("," param)*)? -param = IDENT ":" type_expr - -type_expr = IDENT - | "[" type_expr "]" - | type_expr "?" - | "fn" "(" (type_expr ("," type_expr)*)? ")" "->" type_expr - -expr = or_expr -or_expr = and_expr ("||" and_expr)* -and_expr = eq_expr ("&&" eq_expr)* -eq_expr = cmp_expr (("==" | "!=") cmp_expr)* -cmp_expr = add_expr (("<" | ">" | "<=" | ">=") add_expr)* -add_expr = mul_expr (("+" | "-") mul_expr)* -mul_expr = unary_expr (("*" | "/") unary_expr)* -unary_expr = "!" unary_expr | postfix_expr -postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]")* - -primary = INT | FLOAT | STRING | BOOL - | "(" expr ")" - | "[" arg_list "]" - | "{" stmt* "}" - | "if" expr primary ("else" primary)? - | "match" expr "{" match_arm* "}" - | "activate" IDENT "where" STRING - | "sealed" "{" stmt* "}" - | IDENT ("::" IDENT)* - -arg_list = (expr ("," expr)*)? -match_arm = pattern "=>" expr ","? - -pattern = "_" - | IDENT "::" IDENT ("(" IDENT ")")? - | INT | STRING | BOOL - | IDENT -``` - ---- - -## 12. Package System - -### 12.1 Project Manifest — `el.toml` - -Every Engram project has an `el.toml` at its root. The manifest is parsed by -the `el-manifest` crate. - -```toml -[package] -name = "my-service" -version = "0.1.0" -description = "What this does" -authors = ["Will Anderson "] -license = "MIT" -edition = "2026" - -[dependencies] -engram-http = "1.2" -engram-auth = "0.8.1" -some-local = { path = "../some-local" } - -[dev-dependencies] -el-test = "0.1" - -[build] -target = "prod" # debug | release | prod (default: debug) -entry = "src/main.el" # main entry point (default: src/main.el) -output = "dist/" # output directory (default: dist/) -seal_key = "env:ENGRAM_SEAL_KEY" # key source for prod sealed artifacts - -[cross] -targets = ["x86_64-linux", "aarch64-linux", "x86_64-macos", "aarch64-macos", "wasm32"] - -[plugins] -el-fmt = "1.0" # code formatter plugin -el-doc = "0.3" # documentation generator -``` - -#### Dependency specifiers - -| Form | Example | Meaning | -|------|---------|---------| -| String | `"1.2"` | Version requirement from default registry | -| Path table | `{ path = "../lib" }` | Local path dependency | -| Registry table | `{ version = "1.0", registry = "https://..." }` | Private registry | - -#### Seal key sources - -| Form | Example | Meaning | -|------|---------|---------| -| `env:VAR` | `env:ENGRAM_SEAL_KEY` | Read from environment variable at build time | -| `file:path` | `file:/etc/engram/key.bin` | Read raw bytes from a file | -| Literal | `my-secret-key` | Inline key (development only) | - -### 12.2 Dependency Resolution - -Dependencies are resolved by the `el-registry` crate, which talks to the -registry at `https://packages.neurontechnologies.ai`. - -Resolution algorithm: -1. For each `[dependencies]` entry, fetch all available versions from the - registry. -2. Pick the highest version satisfying the version requirement (semver). -3. Download the tarball and verify the BLAKE3 checksum. -4. Cache in `~/.engram/packages/{name}/{version}/`. -5. Path dependencies bypass the registry entirely. - -### 12.3 Version Requirements - -Engram uses the `semver` crate's version requirement syntax (identical to -Cargo's): - -| Requirement | Example | Matches | -|-------------|---------|---------| -| `"1.2"` | `^1.2.0` (caret) | `1.2.0`, `1.3.0`, but not `2.0.0` | -| `">=1.0, <2.0"` | range | explicit range | -| `"*"` | wildcard | any version | - ---- - -## 13. Build System - -### 13.1 Build Targets - -| Target | Artifact | Notes | -|--------|----------|-------| -| `debug` | `.elc` + `.map.json` | Full debug info, source maps | -| `release` | `.elc` | Optimized, no debug info | -| `prod` | `.sealed` | AES-256-GCM encrypted, tamper-evident | - -### 13.2 CLI Commands +### 18.2 CLI Reference ``` -el new scaffold a new project -el add [@ver] add a dependency to el.toml -el remove remove a dependency -el update update all deps to latest compatible -el build [--target prod] build (reads el.toml) +el new scaffold new project +el add [@ver] add dependency +el remove remove dependency +el update update all deps +el build [--target prod] build project el build --cross build for all cross targets el run build debug and run el test run tests el check type-check only -el fmt format source files -el clean clean build artifacts +el fmt format source +el clean clean artifacts el publish publish to registry el search search registry -el plugin add add a compiler plugin +el seal seal existing artifact +el unseal decrypt sealed artifact +el build-file compile single file ``` -### 13.3 Incremental Builds - -The build system tracks a BLAKE3 hash of every source file in -`.el/build-cache.json`. On subsequent builds, only files whose hashes have -changed (and their dependents) are recompiled. The cache is invalidated by -`el clean`. - -### 13.4 Cross-Compilation - -The `[cross].targets` list specifies which native targets to produce when -running `el build --cross`. Each cross build produces a separate artifact -tagged with the target triple. - -| Target name | Triple | Notes | -|-------------|--------|-------| -| `x86_64-linux` | `x86_64-unknown-linux-gnu` | Standard Linux 64-bit | -| `aarch64-linux` | `aarch64-unknown-linux-gnu` | ARM64 Linux | -| `x86_64-macos` | `x86_64-apple-darwin` | Intel Mac | -| `aarch64-macos` | `aarch64-apple-darwin` | Apple Silicon | -| `wasm32` | `wasm32-unknown-unknown` | WebAssembly | - -Cross-compilation currently emits bytecode tagged with the target triple. A -native LLVM backend (future work) will use the triple to select the correct -code generation backend. The LLVM extension point is clearly marked in the -`el-build` crate source. - -### 13.5 Artifact Names - -| Target | Cross | Artifact name | -|--------|-------|---------------| -| `debug` | none | `{name}.elc` | -| `release` | none | `{name}.elc` | -| `prod` | none | `{name}.sealed` | -| any | `wasm32` | `{name}-wasm32.wasm` | -| any | other | `{name}-{triple-short}.elc` | - --- -## 14. Plugin System +## 19. Grammar (EBNF) -### 14.1 Overview +```ebnf +program = (app_block | stmt)* EOF -Compiler plugins are Rust dynamic libraries (`.dylib` on macOS, `.so` on -Linux) that implement the `CompilerPlugin` trait. They are loaded at compile -time via `dlopen` (stub — full dynamic loading is a TODO) and receive hooks at -three points in the compilation pipeline. +app_block = "app" STRING "{" app_entry* "}" +app_entry = "version" STRING + | "config" "{" config_entry* "}" + | "secrets" "{" secret_entry* "}" + | "flags" "{" flag_entry* "}" +config_entry = IDENT ":" type_expr ("=" expr)? + | IDENT "{" (IDENT "=" expr)* "}" // env overlay +secret_entry = IDENT ":" type_expr +flag_entry = IDENT ":" "Bool" ("=" expr)? -### 14.2 Plugin Trait +stmt = let_stmt + | return_stmt + | fn_def + | type_def + | enum_def + | import_stmt + | while_stmt + | for_stmt + | expr_stmt -```rust -pub trait CompilerPlugin: Send + Sync { - fn name(&self) -> &str; - fn version(&self) -> &str; +let_stmt = "let" IDENT (":" type_expr)? "=" expr ";"? +return_stmt = "return" expr? ";"? +fn_def = "fn" IDENT "(" param_list ")" "->" type_expr "{" stmt* "}" +type_def = "type" IDENT "{" (IDENT ":" type_expr ","?)* "}" +enum_def = "enum" IDENT "{" variant* "}" +variant = IDENT ("(" type_expr ")")? ","? +import_stmt = "import" STRING ";"? + | "from" IDENT "import" "{" (IDENT ","?)* "}" ";"? +while_stmt = "while" expr "{" stmt* "}" +for_stmt = "for" IDENT "in" expr "{" stmt* "}" +expr_stmt = expr ";"? - /// Called after parsing, before type checking. - fn on_ast(&self, program: &mut Program) -> Result<(), PluginError>; +param_list = (param ("," param)*)? +param = IDENT ":" type_expr - /// Called after type checking, before code generation. - fn on_typed_ast(&self, program: &Program, types: &TypeEnv) -> Result<(), PluginError>; +type_expr = IDENT + | "[" type_expr "]" + | type_expr "?" + | "Map" "<" type_expr "," type_expr ">" + | "fn" "(" (type_expr ("," type_expr)*)? ")" "->" type_expr - /// Called after code generation, before sealing. - fn on_bytecode(&self, bytecode: &mut Vec) -> Result<(), PluginError>; -} +expr = null_coalesce_expr +null_coalesce_expr = or_expr ("??" or_expr)* +or_expr = and_expr ("||" and_expr)* +and_expr = eq_expr ("&&" eq_expr)* +eq_expr = cmp_expr (("==" | "!=") cmp_expr)* +cmp_expr = bitwise_expr (("<" | ">" | "<=" | ">=") bitwise_expr)* +bitwise_expr = add_expr (("&" | "|" | "^" | "<<" | ">>") add_expr)* +add_expr = mul_expr (("+" | "-") mul_expr)* +mul_expr = unary_expr (("*" | "/" | "%") unary_expr)* +unary_expr = "!" unary_expr | "~" unary_expr | postfix_expr +postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]" | "?" | "as" type_expr)* + +primary = INT | FLOAT | STRING | BOOL + | "(" expr ")" + | "[" arg_list "]" + | "{" (STRING ":" expr ","?)* "}" + | "if" expr "{" stmt* "}" ("else" "{" stmt* "}")? + | "match" expr "{" match_arm* "}" + | "while" expr "{" stmt* "}" + | "for" IDENT "in" expr "{" stmt* "}" + | "activate" IDENT "where" STRING + | "sealed" "{" stmt* "}" + | "parallel" "{" (IDENT ":" expr ","?)* "}" + | "reason" STRING + | IDENT ("::" IDENT)* + +arg_list = (expr ("," expr)*)? +match_arm = pattern "=>" expr ","? +pattern = "_" | IDENT | INT | STRING | BOOL ``` - -### 14.3 Lifecycle Hooks - -1. `on_ast` — mutate or observe the AST after parsing. Use for: AST macros, - synthetic node injection, linting. -2. `on_typed_ast` — observe the type-checked AST. Use for: documentation - generation, type-aware linting. -3. `on_bytecode` — mutate or observe the final bytecode. Use for: - instrumentation, size analysis. - -### 14.4 Writing a Plugin - -```rust -use el_build::{CompilerPlugin, PluginError}; - -pub struct MyPlugin; - -impl CompilerPlugin for MyPlugin { - fn name(&self) -> &str { "my-plugin" } - fn version(&self) -> &str { "0.1.0" } - - fn on_ast(&self, _program: &mut Program) -> Result<(), PluginError> { - // Observe or mutate the AST - Ok(()) - } - - fn on_typed_ast(&self, _program: &Program, _types: &TypeEnv) -> Result<(), PluginError> { - Ok(()) - } - - fn on_bytecode(&self, _bytecode: &mut Vec) -> Result<(), PluginError> { - Ok(()) - } -} - -// Required export symbol for dynamic loading: -#[no_mangle] -pub extern "C" fn engram_plugin_init() -> Box { - Box::new(MyPlugin) -} -``` - -### 14.5 Installing Plugins - -Add to `[plugins]` in `el.toml`: - -```toml -[plugins] -el-fmt = "1.0" -el-doc = "0.3" -``` - -Or use the CLI: -``` -el plugin add el-fmt@1.0 -``` - -Plugins are looked up in the system plugin directory. The `el-registry` fetches -and installs them like regular packages. - ---- - -## 15. Future Directions - -- **ML-KEM sealed artifacts** — upgrade `el-seal` to CRYSTALS-Kyber when the - `ml-kem` crate stabilizes (drop-in: same format, new `algorithm_id`). -- **LSP server** — spreading activation for autocomplete using the Engram - database as the type graph backend. -- **Engram DB integration** — live connection to an Engram database for - `activate` at compile time (semantic type checking) and at runtime (actual - node retrieval). -- **Struct construction syntax** — `User { id: uuid, name: "Alice", ... }`. -- **Generics** — `fn identity(x: T) -> T { return x }`. -- **Trait system** — behavioral interfaces that interact with the Engram type graph. -- **Pattern matching on struct fields** — `match user { User { name: "admin" } => ... }`. diff --git a/target/.rustc_info.json b/target/.rustc_info.json index 4f7f563..2181c8d 100644 --- a/target/.rustc_info.json +++ b/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":12156818420864752294,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: aarch64-apple-darwin\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":12156818420864752294,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: aarch64-apple-darwin\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"16903921271384936150":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_feature=\"sse4.1\"\ntarget_feature=\"ssse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"6432102384495711296":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"6027984484328994041":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\nlib___.a\n___.dll\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/target/debug/libel_compiler.d b/target/debug/libel_compiler.d index 84ed300..1d2c8c4 100644 --- a/target/debug/libel_compiler.d +++ b/target/debug/libel_compiler.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_compiler.rlib: /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/bytecode.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/codegen.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/compiler.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/debugger.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-compiler/src/source_map.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/seal.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/types.rs +/Users/will/Development/neuron-technologies/foundation/el/target/debug/libel_compiler.rlib: /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/bytecode.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/codegen.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/compiler.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/debugger.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-compiler/src/source_map.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/seal.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/types.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/registry.rs diff --git a/target/debug/libel_compiler.rlib b/target/debug/libel_compiler.rlib index 2335d03..a9e6e57 100644 Binary files a/target/debug/libel_compiler.rlib and b/target/debug/libel_compiler.rlib differ diff --git a/target/debug/libel_lexer.d b/target/debug/libel_lexer.d index df76e13..132babe 100644 --- a/target/debug/libel_lexer.d +++ b/target/debug/libel_lexer.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_lexer.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs +/Users/will/Development/neuron-technologies/foundation/el/target/debug/libel_lexer.rlib: /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/token.rs diff --git a/target/debug/libel_lexer.rlib b/target/debug/libel_lexer.rlib index e83a970..3e2a842 100644 Binary files a/target/debug/libel_lexer.rlib and b/target/debug/libel_lexer.rlib differ diff --git a/target/debug/libel_parser.d b/target/debug/libel_parser.d index cc53d02..65db0b3 100644 --- a/target/debug/libel_parser.d +++ b/target/debug/libel_parser.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_parser.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs +/Users/will/Development/neuron-technologies/foundation/el/target/debug/libel_parser.rlib: /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/parser.rs diff --git a/target/debug/libel_parser.rlib b/target/debug/libel_parser.rlib index 294dfa1..15b2b4e 100644 Binary files a/target/debug/libel_parser.rlib and b/target/debug/libel_parser.rlib differ diff --git a/target/debug/libel_seal.d b/target/debug/libel_seal.d index 28fc99c..a958e85 100644 --- a/target/debug/libel_seal.d +++ b/target/debug/libel_seal.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_seal.rlib: /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/crates/engram-crypto/src/registry.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-seal/src/seal.rs +/Users/will/Development/neuron-technologies/foundation/el/target/debug/libel_seal.rlib: /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/artifact.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-seal/src/seal.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/algorithm.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/engine.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram/engrams/engram-crypto/src/registry.rs diff --git a/target/debug/libel_seal.rlib b/target/debug/libel_seal.rlib index 087130c..403be08 100644 Binary files a/target/debug/libel_seal.rlib and b/target/debug/libel_seal.rlib differ diff --git a/target/debug/libel_types.d b/target/debug/libel_types.d index a9a6f2a..c059911 100644 --- a/target/debug/libel_types.d +++ b/target/debug/libel_types.d @@ -1 +1 @@ -/Users/will/Development/neuron-technologies/foundation/engram-lang/target/debug/libel_types.rlib: /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/engram-lang/crates/el-types/src/types.rs +/Users/will/Development/neuron-technologies/foundation/el/target/debug/libel_types.rlib: /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lexer.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-lexer/src/token.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/ast.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-parser/src/parser.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/checker.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/error.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/lib.rs /Users/will/Development/neuron-technologies/foundation/el/engrams/el-types/src/types.rs diff --git a/target/debug/libel_types.rlib b/target/debug/libel_types.rlib index 40fac8d..982d560 100644 Binary files a/target/debug/libel_types.rlib and b/target/debug/libel_types.rlib differ