diff --git a/.gitignore b/.gitignore index dfa48c3..9b4f27b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,13 @@ CMakeUserPresets.jsonc target/ debug/ + + +# Tap specific ignores +.tap_history +tap_history +*/.tap_history + + +# AOC specific +input* diff --git a/Cargo.lock b/Cargo.lock index 193ba27..786605d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,63 @@ version = 4 [[package]] -name = "addr2line" -version = "0.25.1" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "gimli", + "libc", ] [[package]] -name = "adler2" -version = "2.0.1" +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] [[package]] name = "atty" @@ -35,31 +79,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backtrace" -version = "0.3.76" +name = "bitflags" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", + "serde_core", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "bumpalo" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] -name = "bitflags" -version = "2.10.0" +name = "cc" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" @@ -68,127 +110,176 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "clipboard-win" -version = "4.5.0" +name = "chrono" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "error-code", - "str-buf", - "winapi", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", ] [[package]] -name = "color-eyre" -version = "0.6.5" +name = "clap" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", + "clap_builder", + "clap_derive", ] [[package]] -name = "color-spantrace" -version = "0.3.0" +name = "clap_builder" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "clap_derive" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "cfg-if", - "dirs-sys-next", + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "clap_lex" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ - "libc", - "redox_users", + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "serde", + "signal-hook", + "signal-hook-mio", "winapi", ] [[package]] -name = "endian-type" -version = "0.1.2" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] [[package]] -name = "errno" -version = "0.3.14" +name = "derive_more" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "libc", - "windows-sys 0.61.2", + "derive_more-impl", ] [[package]] -name = "error-code" -version = "2.3.1" +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "libc", - "str-buf", + "convert_case", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "eyre" -version = "0.6.12" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "indenter", - "once_cell", + "litrs", ] [[package]] -name = "fd-lock" -version = "3.0.13" +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "cfg-if", - "rustix", - "windows-sys 0.48.0", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "fd-lock" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "libc", - "wasi", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -200,38 +291,80 @@ dependencies = [ ] [[package]] -name = "indenter" -version = "0.3.4" +name = "iana-time-zone" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] [[package]] -name = "libc" -version = "0.2.177" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "libredox" -version = "0.1.10" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ - "bitflags 2.10.0", - "libc", + "either", ] +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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" @@ -246,42 +379,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "mio" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ - "adler2", + "libc", + "log", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "nibble_vec" -version = "0.1.0" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "smallvec", + "windows-sys 0.61.2", ] [[package]] -name = "nix" -version = "0.25.1" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "bitflags 1.3.2", - "cfg-if", - "libc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", ] [[package]] @@ -291,16 +415,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "owo-colors" -version = "4.2.3" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +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", + "smallvec", + "windows-link", +] [[package]] name = "proc-macro2" @@ -321,67 +462,53 @@ dependencies = [ ] [[package]] -name = "radix_trie" -version = "0.2.1" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "endian-type", - "nibble_vec", + "bitflags", ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "reedline" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "0fb6657e89284163d4c081738148bd3c2c7487586898b38a004c3ea432c1a6f3" dependencies = [ - "getrandom", - "libredox", - "thiserror 1.0.69", + "chrono", + "crossterm", + "fd-lock", + "itertools", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicase", + "unicode-segmentation", + "unicode-width", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "rustyline" -version = "10.1.1" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" @@ -390,137 +517,159 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "lazy_static", + "serde_core", + "serde_derive", ] [[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "str-buf" -version = "1.0.6" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] [[package]] -name = "syn" -version = "2.0.110" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] -name = "tap" -version = "0.1.0" +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ - "atty", - "color-eyre", - "eyre", - "rustyline", - "thiserror 2.0.17", + "libc", + "signal-hook-registry", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "signal-hook-mio" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ - "thiserror-impl 1.0.69", + "libc", + "mio", + "signal-hook", ] [[package]] -name = "thiserror" -version = "2.0.17" +name = "signal-hook-registry" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ - "thiserror-impl 2.0.17", + "libc", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" dependencies = [ - "proc-macro2", - "quote", - "syn", + "vte", ] [[package]] -name = "thiserror-impl" -version = "2.0.17" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ + "heck", "proc-macro2", "quote", + "rustversion", "syn", ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "syn" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ - "cfg-if", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +name = "tap" +version = "0.1.0" dependencies = [ - "pin-project-lite", - "tracing-core", + "atty", + "chrono", + "clap", + "nu-ansi-term", + "reedline", + "thiserror", ] [[package]] -name = "tracing-core" -version = "0.1.34" +name = "thiserror" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "once_cell", - "valuable", + "thiserror-impl", ] [[package]] -name = "tracing-error" -version = "0.2.1" +name = "thiserror-impl" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ - "tracing", - "tracing-subscriber", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "tracing-subscriber" -version = "0.3.20" +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -536,9 +685,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "utf8parse" @@ -547,10 +696,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "valuable" -version = "0.1.1" +name = "vte" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] [[package]] name = "wasi" @@ -558,6 +710,51 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -580,6 +777,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -587,12 +819,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.48.5", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -601,7 +842,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -613,67 +854,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -686,48 +894,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 8a922ab..195a88e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,14 @@ name = "tap" version = "0.1.0" edition = "2024" +[[bin]] +name = "tap" +path = "src/main.rs" + [dependencies] atty = "0.2.14" -color-eyre = "0.6.5" -eyre = "0.6.12" -rustyline = "10.0" +chrono = "0.4.42" +clap = { version="4.5.53", features = ["derive"] } +nu-ansi-term = "0.50.3" +reedline = "0.44.0" thiserror = "2.0.17" diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 3cf5f07..b86d3c9 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -1,306 +1,43 @@ -When running `cargo t`, several tests are failing. -I want you to systemically fix them one by one. For each test that you've attempted a fix for, -create a separate Markdown file explaining the issue and how you fixed it. -DO NOT Change the tests themselves. When you think a test is wrong, CONFIRM with the user first and foremost. +In the top-level grammar.ebnf file, you will find a EBNF description of a grammar for the +Tap programming language. -Current state of `cargo t`: -``` +# Main instructions -running 37 tests -test test_array_access_expression ... ok -test test_array_mutation ... ok -test test_fib ... ok -test test_enum_decl ... ok -test test_dodaj_polish ... ok -test test_conditional_as_value ... FAILED -test test_complex_boolean_logic ... FAILED -test test_early_return_in_loop ... FAILED -test test_float_arithmetic ... ok -test test_fib_lambda ... ok -test test_float_comparison ... ok -test test_function_returning_lambda ... FAILED -test test_hello ... ok -test test_hi_func ... ok -test test_inline_lambda ... ok -test test_lambda_declaration ... ok -test test_iterative_fib ... FAILED -test test_lambdas2 ... FAILED -test test_list_sum ... FAILED -test test_logic_short_circuit_and ... FAILED -test test_logic_short_circuit_or ... FAILED -test test_modulo ... ok -test test_match ... FAILED -test test_mutual_recursion ... FAILED -test test_nested_loops_continue ... FAILED -test test_precedence_order ... ok -test test_option_type ... FAILED -test test_nested_loops_break ... FAILED -test test_nested_if_expression ... FAILED -test test_silnia_polish ... ok -test test_struct_access_nested ... FAILED -test test_string_concatenation ... FAILED -test test_structs ... FAILED -test test_unit_return ... FAILED -test test_unary_operators ... FAILED -test test_polish_if_else ... FAILED -test test_polish_while_loop has been running for over 60 seconds -^C -``` -I have disabled the test that was timing out and here is the test results: -``` -failures: +Your MAIN, PRIMARY task right now: write the tests as per TESTS.md. Don't worry about whether they pass or not. +Some of the currently written tests may be wrong. You will have to fix them as you go along. But first & primarily: write the new tests and make sure they compile, +and not necessarily that they pass. +Remember that for grammar reference you can refer to grammar.ebnf, which is the SINGLE SOURCE OF TRUTH on the grammar. +For syntax reference, refer to the README.md which provides quite a few useful syntax example constructs. +NO mocking please. Write real tests that run the real lexer, parser, & interpreter. ----- test_early_return_in_loop stdout ---- +For now, implement more parser tests. Once you have a good number of parser tests, get back to me for further instructions. +There are some parser tests already written, but they are not enough. You will have to write more. -thread 'test_early_return_in_loop' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement +Periodically refer back to this file to recall your top-level goals. -Caused by: - 0: Failed to parse function definition - 1: body of 'find_match' - 2: Unexpected token in block: expected '{', but found '->' at line 2 +--- -Location: - src/parser.rs:734:13 +### Additional context that was used previously ----- test_conditional_as_value stdout ---- +The lexer should handle Unicode correctly (use .chars()) as we will have to handle Polish language syntax eventually. +Tokens should have spans (start/end char offsets) for better error reporting. -thread 'test_conditional_as_value' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement +As for the parser: it will be a recursive-descent, context-aware parser. Every non-terminal becomes a method. Each method should have a helpful doxy comment, +including a snippet of the relevant EBNF production. +Hard context-sensitive rules (e.g., forbidding assignment to non-lvalues, checking pattern validity, enforcing keyword vs identifier distinctions) must be enforced during parsing. +All productions must be written in a style that is readable, correct, and testable. +Produce a well-typed AST with enums and structs. Errors should be helpful and provide context +(what production were we trying to parse?). Each -Caused by: - 0: Failed to parse declaration statement - 1: Failed to parse expression or assignment statement - 2: Unexpected token in expression statement: expected ';', but found '}' at line 7 +As for the interpreter: A tree-walking interpreter. +Lexically scoped environments. Braces are closures. -Location: - src/parser.rs:734:13 -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +First-class functions + closures (lexical capture). Strong runtime error diagnostics with spans. ----- test_complex_boolean_logic stdout ---- +Start by generating tests for the language. Do good Test Driver Development. Any ambiguities in grammar should be resolved +by referencing the grammar.ebnf file. It is the single source of truth on the grammar. For a basic source of tests look into the +top-level TESTS.md file. You WILL HAVE to update this file, checking off implemented tests as you implement more tests. -thread 'test_complex_boolean_logic' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement +--- -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in group: expected ')', but found '||' at line 7 - -Location: - src/parser.rs:734:13 - ----- test_function_returning_lambda stdout ---- - -thread 'test_function_returning_lambda' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse function definition - 1: body of 'make_adder' - 2: Unexpected token in block: expected '{', but found '->' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_lambdas2 stdout ---- - -thread 'test_lambdas2' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse function definition - 1: body of 'transform' - 2: Unexpected token in block: expected '{', but found '->' at line 3 - -Location: - src/parser.rs:734:13 - ----- test_list_sum stdout ---- - -thread 'test_list_sum' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse for loop - 1: Unexpected token in field: expected ':', but found '=' at line 6 - -Location: - src/parser.rs:734:13 - ----- test_iterative_fib stdout ---- - -thread 'test_iterative_fib' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse function definition - 1: body of 'fib_iter' - 2: Unexpected token in block: expected '{', but found '->' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_logic_short_circuit_and stdout ---- - -thread 'test_logic_short_circuit_and' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found '&&' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_logic_short_circuit_or stdout ---- - -thread 'test_logic_short_circuit_or' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found '||' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_mutual_recursion stdout ---- - -thread 'test_mutual_recursion' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse function definition - 1: body of 'is_even' - 2: Unexpected token in block: expected '{', but found '->' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_match stdout ---- - -thread 'test_match' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found 'Light' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_nested_loops_break stdout ---- - -thread 'test_nested_loops_break' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse while loop - 1: Failed to parse while loop - 2: Failed to parse if-statement - 3: Failed to parse expression or assignment statement - 4: Unexpected token in expression: expected literal, identifier, or '(', but found 'break' at line 10 - -Location: - src/parser.rs:734:13 - ----- test_option_type stdout ---- - -thread 'test_option_type' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found 'MaybeInt' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_polish_if_else stdout ---- - -thread 'test_polish_if_else' panicked at tests/integration.rs:524:5: -assertion `left == right` failed - left: Some(Integer(0)) - right: Some(Integer(2)) - ----- test_nested_if_expression stdout ---- - -thread 'test_nested_if_expression' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse declaration statement - 1: Failed to parse if-statement - 2: Failed to parse expression or assignment statement - 3: Unexpected token in expression statement: expected ';', but found '}' at line 8 - -Location: - src/parser.rs:734:13 - ----- test_nested_loops_continue stdout ---- - -thread 'test_nested_loops_continue' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse while loop - 1: Failed to parse if-statement - 2: Failed to parse expression or assignment statement - 3: Unexpected token in expression: expected literal, identifier, or '(', but found 'continue' at line 7 - -Location: - src/parser.rs:734:13 - ----- test_string_concatenation stdout ---- - -thread 'test_string_concatenation' panicked at tests/integration.rs:18:42: -Runtime error: TypeError("Type mismatch in binary op") - ----- test_unit_return stdout ---- - -thread 'test_unit_return' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression: expected literal, identifier, or '(', but found '{' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_struct_access_nested stdout ---- - -thread 'test_struct_access_nested' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found 'Point' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_structs stdout ---- - -thread 'test_structs' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found 'Rect' at line 2 - -Location: - src/parser.rs:734:13 - ----- test_unary_operators stdout ---- - -thread 'test_unary_operators' panicked at tests/integration.rs:13:42: -Parser error: Failed to parse top-level statement - -Caused by: - 0: Failed to parse expression or assignment statement - 1: Unexpected token in expression statement: expected ';', but found '&&' at line 4 - -Location: - src/parser.rs:734:13 -``` +_Remember to follow the main instructions above carefully_ diff --git a/README.md b/README.md index 911631e..5a37054 100644 --- a/README.md +++ b/README.md @@ -1,200 +1,244 @@ # Tap -An unremarkable but deliberately _Polished_ interpreted programming language. Originally built -as a personal project to learn C++20 (CMake, Gtest, and other ritual sacrifices), -it has since been rewritten in Rust (I like the convenience of match statements compared to std::visit (bleh)). -Currently in infancy. The short term goal is to solve all Advent of Code problems using Tap. There is no long term goal. +An unremarkable but deliberately _Polished_ interpreted programming language. +Originally built as a personal project to learn C++20 (CMake, Gtest, and other +ritual sacrifices), it has since been rewritten. Currently in its infancy. + +The short term goal is to solve all Advent of Code problems using Tap. There is +no long term goal. The purpose of this project was and remains education and fun. ## Current state of affairs: -- Rust / C++ inspired syntax -- tree walking interpreter +- **bilingual keywords** (English + Polish aliases for all keywords) 🇵🇱 +- a slow tree walking interpreter - rudimentary runtime type checking - hand rolled lexer & parser -- basic closure & lexical scoping -- **bilingual keywords** (English + Polish aliases for all keywords) 🇵🇱 +- basic closures & lexical scoping ## but... why? Because I can. Also it's fun. Lexing and parsing are already solved problems (see the excellent [logos](https://docs.rs/logos/latest/logos/) crate for creating lexers, or [nom](https://docs.rs/nom/latest/nom/) creating parsers. People older to the trade are surely familiar with the [GNU Bison](https://www.gnu.org/software/bison/) parser _generator_). - ## Planned features -- floating point numbers (lol) -- match statements - type inference -- byte code compilation -- VM -- potentially experimenting into JIT compilation (but nothing too serious, I have a life) - -### Syntax examples +- emitting byte code + a VM implementation +- potentially looking into JIT compilation (but nothing too serious, I have a life) +- a faster, state machine based lexer (DFA) + - might require edits to the Tap grammar + - perfect hashing for keywords? + - cache friendly token storage + +### Tap syntax examples +[Check out the WIP EBNF Grammar](grammar.ebnf) ``` -### Variable assignment ### -x = 5; +// Type Declarations + +// Sum type with variants +type Option = Some(int) | None; + +// Sum type with multiple variants +type Result = Ok(int) | Error(string); + +// Record type +type Point = { x: int, y: int }; -### Immutable variables by default ### -a = 10; # const -mut b = 10; # mutable -b += 5; +// Generic type usage +type IntList = [int]; -### Optional in-line type annotations ### -y: int = 20; # Type annotations can be added in-line... -b = 3.456; # ...or deduced (b : float64) -# Lists -z: [str] = ["John", "Smith"]; +// Variable Bindings + +// Immutable variable +x: int = 42; + +// Immutable variable with type inference +name = "Alice"; + +// Mutable variable +mut counter: int = 0; + + +// Function Definitions + +// Function with parameters and return type +add(a: int, b: int): int = { + a + b +} -# Functions (TODO: How to disambiguate between function call and declaration?) -fib(n: int) : int = { - if (n < 2) { - n +// Function with block body and statements +factorial(n: int): int = { + mut result = 1; + mut i = 1; + while (i <= n) { + result *= i; + i += 1; + } + result +} + +// Function using records +distance(p1: Point, p2: Point): float = { + dx = p1.x - p2.x; + dy = p1.y - p2.y; + (dx * dx + dy * dy) +} + + +// Control Flow + +// If expression +max(a: int, b: int): int = { + if (a > b) { + a } else { - fib(n - 1) + fib(n - 2) # implicit return of last expression + b } } -# Closures (braces define lexical scope) -gensym() : string = { - mut count : int = 0; +// While loop +print_numbers(n: int): int = { + mut i = 0; + while (i < n) { + i += 1; + } + i +} + +// For loop +for i in [1, 2, 3, 4, 5] { + print(i); +} + +for fruit in fruits { + print(fruit); // "apple" "banana" "cherry" +} + +// Pattern Matching - next() = { - count += 1 +// Match expression with patterns +unwrap_or(opt: Option, default: int): int = { + match (opt) { + | Some(value) => value, + | None => default } +} - "symbol_" + next().to_string() +// Match with multiple patterns +handle_result(res: Result): string = { + match (res) { + | Ok(val) => "Success", + | Error(msg) => msg + } } -# Algebraic data types (ADTs) -# sum types -type Option[T] = - | Some(T) - | None +// Lambda Expressions -# product types -type Point = { - x : float, - y : float +// Lambda with type annotation +map_fn = (f: int -> int, lst: [int]): [int] => { + lst } +// Lambda without type annotation +simple_lambda = (x: int) => x + 1; + + +// Operators and Expressions + +compute(): int = { + // Arithmetic operators + a = 10 + 5; + b = 20 - 3; + c = 4 * 7; + d = 15 / 3; + + // Comparison operators + is_equal = (a == 15); + is_not_equal = (b != 10); + is_less = (c < 30); + is_greater = (d > 2); + is_less_equal = (a <= 15); + is_greater_equal = (b >= 17); + + // Logical operators + both_true = is_equal && is_not_equal; + either_true = is_less || is_greater; -# Match statements -match opt { - Some(x) => x + 1, - None => 0 -}; + // Unary operators + negated = -a; + positive = +b; + not_true = !is_equal; -# While loops -while x > 0 { - x = x - 1; + // Compound assignment + mut x = 10; + x += 5; + x -= 2; + x *= 3; + x /= 4; + + x } -# For loops -for i in [1, 2, 3] { - println(i); + +// Data Structures + +// Record literal +origin: Point = { x: 0, y: 0 }; + +// List literal +numbers: [int] = [1, 2, 3, 4, 5]; +fruits: [string] = ["apple", "banana", "cherry"]; + +// Empty list +empty = []; + + +// Accessing Data + +access_demo(): int = { + // Field access + p = { x: 10, y: 20 }; + x_val = p.x; + + // Array indexing + arr = [1, 2, 3]; + first = arr[0]; + + // Function call + result = add(x_val, first); + + result } -``` -### EBNF Grammar (WIP) -```ebnf -program = {statement} . - -statement = assignment ";" - | var_decl ";" - | expression ";" - | return_stmt - | function_def - | struct_decl - | enum_decl - | if_stmt - | match_stmt - | while_loop - | for_loop . - -var_decl = ident ":" type-annotation . -assignment = ident [ ":" type-annotation ] "=" expression . -return_stmt = "return" expression ";" . - -struct_decl = ident ":" struct_type ";" . -enum_decl = ident ":" enum_type ";" . - -if_stmt = "if" expression block [ "else" block ] . -block = "{" {statement} "}" . - -match_stmt = "match" expression "{" match_arm { match_arm } "}" . -match_arm = pattern "=>" expression ";" . -pattern = ident - | literal - | list_pattern - | struct_pattern - | variant_pattern . - -list_pattern = "[" [ pattern { "," pattern } ] "]" . -struct_pattern = "struct" "{" field_pattern { "," field_pattern } "}" . -field_pattern = ident ":" pattern . -variant_pattern = ident "(" pattern { "," pattern } ")" . - -while_loop = "while" expression block . -for_loop = "for" ident "in" expression block . - -expression = term { ("+" | "-") term } . -term = factor { ("*" | "/") factor } . -factor = literal - | ident - | list - | lambda - | if_stmt // TODO: Make if_stmt an if_expression instead and remove from expressions? - | function_call - | list_access - | "(" expression ")" . - -literal = integer | float | string | "true" | "false" . - -list = "[" [ expression { "," expression } ] "]" . -lambda = "\"" ident { ident } "." expression . -function_call = ident "(" arglist ")" . -list_access = ident "[" expression "]" . - -function_def = "func" ident "(" typed-arglist ")" [ ":" type-annotation ] "{" {statement} "}" . - -arglist = [ expression { "," expression } ] . -typed-arglist = [ typed_param { "," typed_param } ] . -typed_param = ident [ ":" type-annotation ] . - -type-annotation = function_type - | array_type - | struct_type - | enum_type - | type-ident - | "(" type-annotation ")" . - -function_type = type-ident "->" type-annotation . -array_type = "[" type-annotation "]" . -struct_type = "struct" "{" field { "," field } "}" . -enum_type = "enum" "{" variant { "," variant } "}" . -field = ident ":" type-annotation . -variant = ident [ "(" type-annotation { "," type-annotation } ")" ] . - -type-ident = primitive_type | ident . -primitive_type = "int" | "str" | "float" | "bool" | "unit" . - -ident = letter { letter | digit | "_" } . -letter = "a"..."z" | "A"..."Z" . -digit = "0"..."9" . - -integer = digit {digit} . -float = digit {digit} "." digit {digit} [ exponent ] . -exponent = ("e" | "E") ["+" | "-"] digit {digit} . - -string = '"' { char } '"' . -char = ? any character except ", \, and newline ? | escape_seq . -escape_seq = "\" ( '"' | '\\' | "n" | "t" | "r" ) . - -comment = "#" { ? any character except newline ? } "\n" . -whitespace = " " | "\t" | "\n" | "\r" . +// Variant Construction + +// Creating sum type values +some_value: Option = Some(42); +no_value: Option = None; + +ok_result: Result = Ok(100); +error_result: Result = Error("Something went wrong"); + + +// Nested Expressions + +complex_expr(): int = { + if (true && (10 > 5)) { + match (Some(42)) { + | Some(x) => { + y = x + 10; + y * 2 + }, + | _ => 0 + } + } else { + -1 + } +} ``` + ### Dependencies - Rust 1.70+ - Cargo diff --git a/TESTS.md b/TESTS.md new file mode 100644 index 0000000..642514a --- /dev/null +++ b/TESTS.md @@ -0,0 +1,114 @@ +# 100 Tests for the Tap Language + +## Lexer + +- [ ] Test that the lexer correctly handles all single-character tokens. +- [ ] Test that the lexer correctly handles all multi-character tokens. +- [ ] Test that the lexer correctly handles all keywords. +- [ ] Test that the lexer correctly handles integer literals. +- [ ] Test that the lexer correctly handles float literals. +- [ ] Test that the lexer correctly handles string literals. +- [ ] Test that the lexer correctly handles identifiers. +- [ ] Test that the lexer correctly handles comments. +- [ ] Test that the lexer correctly handles whitespace. +- [ ] Test that the lexer correctly handles a mix of all token types. + +## Parser + +- [x] Test that the parser correctly parses a simple let statement. +- [x] Test that the parser correctly parses a let statement with a type annotation. +- [x] Test that the parser correctly parses a mutable let statement. +- [x] Test that the parser correctly parses a function definition. +- [x] Test that the parser correctly parses a function definition with parameters. +- [x] Test that the parser correctly parses a function definition with a return type. +- [x] Test that the parser correctly parses a struct definition. +- [x] Test that the parser correctly parses a struct definition with fields. +- [x] Test that the parser correctly parses an enum definition. +- [x] Test that the parser correctly parses an enum definition with variants. +- [x] Test that the parser correctly parses an if expression. +- [x] Test that the parser correctly parses an if-else expression. +- [x] Test that the parser correctly parses a while expression. +- [x] Test that the parser correctly parses a for expression. +- [x] Test that the parser correctly parses a match expression. +- [x] Test that the parser correctly parses a block expression. +- [x] Test that the parser correctly parses a unary expression. +- [x] Test that the parser correctly parses a binary expression. +- [x] Test that the parser correctly parses a postfix expression. +- [x] Test that the parser correctly parses a primary expression. +- [x] Test that the parser correctly parses a record literal expression. +- [x] Test that the parser correctly parses a field access expression. +- [x] Test that the parser correctly parses a path resolution expression. + +## Interpreter + +- [ ] Test that the interpreter correctly evaluates an integer literal. +- [ ] Test that the interpreter correctly evaluates a float literal. +- [ ] Test that the interpreter correctly evaluates a string literal. +- [ ] Test that the interpreter correctly evaluates a boolean literal. +- [ ] Test that the interpreter correctly evaluates a unit literal. +- [ ] Test that the interpreter correctly evaluates a list literal. +- [ ] Test that the interpreter correctly evaluates a struct literal. +- [ ] Test that the interpreter correctly evaluates an enum literal. +- [ ] Test that the interpreter correctly evaluates a unary plus expression. +- [ ] Test that the interpreter correctly evaluates a unary minus expression. +- [ ] Test that the interpreter correctly evaluates a unary not expression. +- [ ] Test that the interpreter correctly evaluates an addition expression. +- [ ] Test that the interpreter correctly evaluates a subtraction expression. +- [ ] Test that the interpreter correctly evaluates a multiplication expression. +- [ ] Test that the interpreter correctly evaluates a division expression. +- [ ] Test that the interpreter correctly evaluates an equality expression. +- [ ] Test that the interpreter correctly evaluates an inequality expression. +- [ ] Test that the interpreter correctly evaluates a less than expression. +- [ ] Test that the interpreter correctly evaluates a less than or equal to expression. +- [ ] Test that the interpreter correctly evaluates a greater than expression. +- [ ] Test that the interpreter correctly evaluates a greater than or equal to expression. +- [ ] Test that the interpreter correctly evaluates a logical and expression. +- [ ] Test that the interpreter correctly evaluates a logical or expression. +- [ ] Test that the interpreter correctly evaluates a let statement. +- [ ] Test that the interpreter correctly evaluates an identifier expression. +- [ ] Test that the interpreter correctly evaluates a function call expression. +- [ ] Test that the interpreter correctly evaluates a struct field access expression. +- [ ] Test that the interpreter correctly evaluates an enum variant access expression. +- [ ] Test that the interpreter correctly evaluates a list element access expression. +- [ ] Test that the interpreter correctly evaluates an if expression. +- [ ] Test that the interpreter correctly evaluates an if-else expression. +- [ ] Test that the interpreter correctly evaluates a while expression. +- [ ] Test that the interpreter correctly evaluates a for expression. +- [ ] Test that the interpreter correctly evaluates a match expression. +- [ ] Test that the interpreter correctly evaluates a block expression. +- [ ] Test that the interpreter correctly handles a return statement. +- [ ] Test that the interpreter correctly handles a break statement. +- [ ] Test that the interpreter correctly handles a continue statement. +- [ ] Test that the interpreter correctly handles a recursive function call. +- [ ] Test that the interpreter correctly handles a closure. +- [ ] Test that the interpreter correctly handles a lambda. +- [ ] Test that the interpreter correctly handles a struct with methods. +- [ ] Test that the interpreter correctly handles an enum with methods. +- [ ] Test that the interpreter correctly handles a list with methods. +- [ ] Test that the interpreter correctly handles a string with methods. +- [ ] Test that the interpreter correctly handles a integer with methods. +- [ ] Test that the interpreter correctly handles a float with methods. +- [ ] Test that the interpreter correctly handles a boolean with methods. +- [ ] Test that the interpreter correctly handles a unit with methods. +- [ ] Test that the interpreter correctly handles a function as an argument. +- [ ] Test that the interpreter correctly handles a function as a return value. +- [ ] Test that the interpreter correctly handles a closure as an argument. +- [ ] Test that the interpreter correctly handles a closure as a return value. +- [ ] Test that the interpreter correctly handles a lambda as an argument. +- [ ] Test that the interpreter correctly handles a lambda as a return value. +- [ ] Test that the interpreter correctly handles a struct as an argument. +- [ ] Test that the interpreter correctly handles a struct as a return value. +- [ ] Test that the interpreter correctly handles an enum as an argument. +- [ ] Test that the interpreter correctly handles an enum as a return value. +- [ ] Test that the interpreter correctly handles a list as an argument. +- [ ] Test that the interpreter correctly handles a list as a return value. +- [ ] Test that the interpreter correctly handles a string as an argument. +- [ ] Test that the interpreter correctly handles a string as a return value. +- [ ] Test that the interpreter correctly handles a integer as an argument. +- [ ] Test that the interpreter correctly handles a integer as a return value. +- [ ] Test that the interpreter correctly handles a float as an argument. +- [ ] Test that the interpreter correctly handles a float as a return value. +- [ ] Test that the interpreter correctly handles a boolean as an argument. +- [ ] Test that the interpreter correctly handles a boolean as a return value. +- [ ] Test that the interpreter correctly handles a unit as an argument. +- [ ] Test that the interpreter correctly handles a unit as a return value. diff --git a/aoc/day1_1.tap b/aoc/day1_1.tap new file mode 100644 index 0000000..da9dc4f --- /dev/null +++ b/aoc/day1_1.tap @@ -0,0 +1,62 @@ +// Example input file: "L68\nL30\nR48\nL5\nR60\nL55\nL1\nL99\nR14\nL82" +get_file_content(): string = { + // `args` is a built-in injected into global env by interpreter runtime + file = open(args.get(0), "r"); + content: string = file.read(); + file.close(); + content +} + +// Parse a line like "R60" or "L30" into a turn value +// R becomes positive, L becomes negative +parse_turn(line: string): int = { + direction = line.char_at(0); + len = line.length(); + value_str = line.substring(1, len - 1); + value = value_str.parse_int(); + + if (direction == "L") { + -value + } else { + value + } +} + +// Parse all lines into a list of turns +get_turns(content: string): [int] = { + lines = content.split("\n"); + mut turns: [int] = []; + + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + turn = parse_turn(trimmed); + turns = turns.push(turn); + } + } + + turns +} + +solve(): int = { + content = get_file_content(); + turns = get_turns(content); + + mut res = 0; + mut dial = 50; + + for turn in turns { + dial = dial + turn; + dial = dial % 100; + + // Check if dial reached zero + if (dial == 0) { + res = res + 1; + } + } + + // Return the count of times dial reached zero + print(res); +} + +solve(); diff --git a/aoc/day1_2.pl.tap b/aoc/day1_2.pl.tap new file mode 100644 index 0000000..b48262a --- /dev/null +++ b/aoc/day1_2.pl.tap @@ -0,0 +1,84 @@ +// Przykładowy plik wejściowy: "L68\nL30\nR48\nL5\nR60\nL55\nL1\nL99\nR14\nL82" +wczytaj_zawartość_pliku(): string = { + // `args` to wbudowana zmienna dodana do globalnego środowiska przez interpreter + plik_danych = open(args.get(0), "r"); + zawartość: string = plik_danych.read(); + plik_danych.close(); + zwróć zawartość; +} + +// Konwertuje linię taką jak "R60" lub "L30" na wartość obrotu (liczba całkowita). +// 'R' oznacza wartość dodatnią, 'L' - ujemną. +przetwarzaj_obrót(linia: string): int = { + długość_linii = linia.length(); + jeżeli (długość_linii == 0) { + zwróć 0; + } + kierunek = linia.char_at(0); + // Wartość liczbową pobieramy od drugiego znaku do końca linii + wartość_tekstowa = linia.substring(1, długość_linii); + wartość_obrotu: int = wartość_tekstowa.parse_int(); + + jeżeli (kierunek == "L") { + -wartość_obrotu + } albo { + +wartość_obrotu + } +} + +// Przetwarza wszystkie linie na listę wartości obrotów. +wczytaj_listę_obrotów(zawartosc: string): [int] = { + linie = zawartosc.split("\n"); + zmienna lista_obrotów: [int] = []; + + dla pojedyncza_linia in linie { + linia_przycięta = pojedyncza_linia.trim(); + jeżeli (linia_przycięta.length() > 0) { + obrót = przetwarzaj_obrót(linia_przycięta); + lista_obrotów = lista_obrotów.push(obrót); + } + } + lista_obrotów +} + +rozwiąż_problem() = { + zawartosc_pliku = wczytaj_zawartość_pliku(); + obroty = wczytaj_listę_obrotów(zawartosc_pliku); + + zmienna całkowity_wynik = 0; + zmienna aktualna_tarcza = 50; // Początkowa pozycja tarczy [0-99] + + dla pojedynczy_obrót in obroty { + zmienna pokonana_odległość = pojedynczy_obrót; + zmienna znak_kierunku = 1; // 1 dla prawo, -1 dla lewo + jeżeli (pojedynczy_obrót < 0) { + pokonana_odległość = -pojedynczy_obrót; + znak_kierunku = -1; + } + + // Dodaj wszystkie przejścia przez punkt "zero" wynikające z pełnych obrotów (360 stopnii) + całkowity_wynik += pokonana_odległość / 100; + + // Sprawdź, czy nastąpiło dodatkowe przejście z powodu pozostałej części obrotu + reszta_obrotu = pokonana_odległość % 100; + jeżeli (pojedynczy_obrót > 0) { // Obrót w prawo + jeżeli (aktualna_tarcza + reszta_obrotu >= 100) { + całkowity_wynik += 1; + } + } albo { // Obrót w lewo + // Sprawdzamy, czy tarcza faktycznie przekroczy zero + jeżeli (aktualna_tarcza > 0 && aktualna_tarcza - reszta_obrotu <= 0) { + całkowity_wynik += 1; + } + } + + // Zaktualizuj pozycję tarczy do jej końcowego położenia + // (możemy pominąć pełne obroty i użyć tylko reszty, zachowując zakres [0, 99]) + aktualna_tarcza = aktualna_tarcza + (reszta_obrotu * znak_kierunku); + aktualna_tarcza = (aktualna_tarcza % 100 + 100) % 100; // Normalizacja do zakresu [0, 99] + } + + print(całkowity_wynik); +} + +rozwiąż_problem(); diff --git a/aoc/day1_2.tap b/aoc/day1_2.tap new file mode 100644 index 0000000..22074e2 --- /dev/null +++ b/aoc/day1_2.tap @@ -0,0 +1,82 @@ +// Example input file: "L68\nL30\nR48\nL5\nR60\nL55\nL1\nL99\nR14\nL82" +get_file_content(): string = { + // `args` is a built-in injected into global env by interpreter runtime + file = open(args.get(0), "r"); + content: string = file.read(); + file.close(); + content +} + +// Parse a line like "R60" or "L30" into a turn value (int) +// R becomes positive, L becomes negative +parse_turn(line: string): int = { + len = line.length(); + if (len == 0) { + return 0; + } + direction = line.char_at(0); + value_str = line.substring(1, len - 1); + value = value_str.parse_int(); + + if (direction == "L") { + -value + } else { + value + } +} + +// Parse all lines into a list of turns +get_turns(content: string): [int] = { + lines = content.split("\n"); + mut turns: [int] = []; + + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + turn = parse_turn(line); + turns = turns.push(turn); + } + } + turns +} + +solve(): int = { + content = get_file_content(); + turns = get_turns(content); + + mut res = 0; + mut dial = 50; + + for turn in turns { + mut distance = turn; + mut sign = 1; + if (turn < 0) { + distance = -turn; + sign = -1; + } + + // Add all the crosses due to full 360deg rotations + res += distance / 100; + + // Check for a cross due to remaining part of a full turn + remainder = distance % 100; + if (turn > 0) { // Right turn + if (dial + remainder >= 100) { + res += 1; + } + } else { // Left turn + if (dial > 0 && dial - remainder <= 0) { + res += 1; + } + } + + // Update dial's position to its end position + // (we can skip full turns and only use the remainder) + dial = dial + (remainder * sign); + dial = (dial % 100 + 100) % 100; // Keep dial in [0, 99] + } + + print(res); +} + +solve(); diff --git a/aoc/day2_1.tap b/aoc/day2_1.tap new file mode 100644 index 0000000..ecae19e --- /dev/null +++ b/aoc/day2_1.tap @@ -0,0 +1,91 @@ +get_file_content(): string = { + // `args` is a built-in injected into global env by interpreter runtime + file = open(args.get(0), "r"); + content: string = file.read(); + file.close(); + content +} + +type Range = { + start: int, + end: int +}; + +parse_range(range: string) : Range = { + parts = range.split("-"); + if (parts.length() != 2) { + return None; + } + start = parts[0].parse_int(); + end = parts[1].parse_int(); + range = {start: start, end: end}; + range +} + +// Parse input into a list of Ranges +get_ranges(content: string): [Range] = { + range_strs: [string] = content.split(","); + mut ranges: [Range] = []; + + for range_str in range_strs { + range = parse_range(range_str); + ranges = ranges.push(range); + } + ranges +} + +find_invalid(r: Range) : [int] = { + // Prune ranges where no invalid IDs can exist + if (r.start.to_string().length() % 2 != 0 && r.end.to_string().length() % 2 != 0) { + if (r.start.to_string().length() == r.end.to_string().length()) { + return []; + } + } + mut res: [int] = []; + + mut i = 0; + while (r.start + i <= r.end) { + id = r.start + i; + + id_str = id.to_string(); + n: int = id_str.length(); + if (n % 2 != 0) { + i += 1; + continue; + } + + half = n / 2; + left_substr = id_str.substring(0, half); + right_substr = id_str.substring(half, n - 1); + if (left_substr == right_substr) { + res = res.push(id); + } + + i += 1; + } + res +} + +solve(): int = { + content = get_file_content(); + ranges: [Range] = get_ranges(content); + + mut res : [int] = []; + for range in ranges { + invalid = find_invalid(range); + res += invalid; + print("Found invalid IDs in range "); + print(range); + print(invalid.length()); + print(" "); + } + print(res); + + mut sum = 0; + for id in res { + sum += id; + } + print(sum) +} + +solve(); diff --git a/aoc/day2_2.tap b/aoc/day2_2.tap new file mode 100644 index 0000000..8c431d2 --- /dev/null +++ b/aoc/day2_2.tap @@ -0,0 +1,105 @@ +get_file_content(): string = { + // `args` is a built-in injected into global env by interpreter runtime + file = open(args.get(0), "r"); + content: string = file.read(); + file.close(); + content +} + +type Range = { + start: int, + end: int +}; + +parse_range(range: string) : Range = { + parts = range.split("-"); + if (parts.length() != 2) { + return None; + } + start = parts[0].parse_int(); + end = parts[1].parse_int(); + range = {start: start, end: end}; + range +} + +// Parse input into a list of Ranges +get_ranges(content: string): [Range] = { + range_strs: [string] = content.split(","); + mut ranges: [Range] = []; + + for range_str in range_strs { + range = parse_range(range_str); + ranges = ranges.push(range); + } + ranges +} + + +is_id_invalid(id: int): bool = { + id_str = id.to_string(); + n: int = id_str.length(); + + // Iterate through all possible lengths (L) of the repeating pattern. + // The pattern must repeat at least twice, so L must be a divisor of n, + // and L must be less or equal to half of n (i.e., L <= n / 2). + mut L = 1; + while (L <= n / 2) { + if (n % L == 0) { // divisor of N, hence could be a repeating pattern + pattern = id_str.substring(0, L); // Extract the potential repeating pattern + + // Reconstruct the string by repeating the pattern n/L times + mut reconstructed_str = ""; + mut k = 0; + while (k < n / L) { // k represents the number of repetitions + reconstructed_str = reconstructed_str + pattern; + k += 1; + } + + // If the reconstructed string matches the original id_str, then it's an invalid ID + if (reconstructed_str == id_str) { + return true; // Found a repeating pattern, no need to check further L's + } + } + L += 1; + } + return false; // No repeating pattern found that meets the criteria +} + + +find_invalid(r: Range) : [int] = { + mut res: [int] = []; + + // Iterate through each ID in the given range + mut current_id = r.start; + while (current_id <= r.end) { + if (is_id_invalid(current_id)) { + res = res.push(current_id); + } + current_id += 1; + } + res +} + +solve(): int = { + content = get_file_content(); + ranges: [Range] = get_ranges(content); + + mut res : [int] = []; + for range in ranges { + invalid = find_invalid(range); + res += invalid; + print("Found invalid IDs in range "); + print(range); + print(invalid.length()); + print(" "); + } + print(res); + + mut sum = 0; + for id in res { + sum += id; + } + print(sum) +} + +solve(); diff --git a/aoc/day3_1.tap b/aoc/day3_1.tap new file mode 100644 index 0000000..d361701 --- /dev/null +++ b/aoc/day3_1.tap @@ -0,0 +1,97 @@ +get_file_content(): string = { + // `args` is a built-in injected into global env by interpreter runtime + file = open(args.get(0), "r"); + content: string = file.read(); + file.close(); + content +} + +// Parse a line like "987654321111111" into a list of integers +parse_bank(line: string): [int] = { + mut digits: [int] = []; + chars = line.split(""); + for ch in chars { + if (ch != "") { + digit = ch.parse_int(); + digits = digits.push(digit); + } + } + digits +} + +// Parse all lines into a list of battery banks +get_banks(content: string): [int] = { + lines = content.split("\n"); + mut banks: [[int]] = []; + + + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + bank = parse_bank(trimmed); + banks = banks.push(bank); + } + } + + banks +} + +solve(): int = { + content = get_file_content(); + banks = get_banks(content); + + // For every index position in each line, we build a + // prefix max list + + // after that, we iterate through each bank, + // at each battery treating it as the leftmost one, + // and querying the prefix max list for the max to the right + + // at each step we get a number (left battery * 10 + right battery). + // if this number is larger than the current max, we update the max + + mut max_joltages: [int] = []; + + // OR: for bank in banks { + // but wanted to test my Range implementation for today + for bidx in 0.. Rust? + bank = banks[bidx]; + + max_to_right: [int] = []; + + max: int = -1; + for j in 1..=bank.length() { + idx = bank.length() - j; + batt: int = bank[idx]; + if (batt > max) { + max = batt; + } + max_to_right = max_to_right.push(max); + } + max_to_right = max_to_right.reverse(); + print(max_to_right); + + max_joltages = max_joltages.push(-1); // TODO: really need to make this in-place + for i in 0..<(bank.length()-1) { + left_batt: int = bank[i]; + right_batt: int = max_to_right[i + 1]; + + joltage: int = left_batt * 10 + right_batt; + if (joltage > max_joltages[bidx]) { + max_joltages[bidx] = joltage; + } + } + print(max_joltages); + } + + + + mut res = 0; + for i in 0.. 0) { + bank = parse_bank(trimmed); + banks = banks.push(bank); + } + } + + banks +} + +solve(): int = { + content = get_file_content(); + banks = get_banks(content); + + // For every index position in each line, we build a + // prefix max list + + // after that, we iterate through each bank, + // at each battery treating it as the leftmost one, + // and querying the prefix max list for the max to the right + + // at each step we get a number (left battery * 10 + right battery). + // if this number is larger than the current max, we update the max + + mut max_joltages: [int] = []; + + // OR: for bank in banks { + // but wanted to test my Range implementation for today + for bidx in 0.. Rust? + bank = banks[bidx]; + + mut curr: [int] = []; + mut last_chosen_idx: int = -1; // index of last battery picked + + digits: int = 12; + for i in 0.. max_digit_in_window) { + max_digit_in_window = batt; + max_digit_idx = j; + } + } + + // Pick that battery + curr = curr.push(max_digit_in_window); + last_chosen_idx = max_digit_idx; + } + + // Convert [int] list of digits to number + joltage : int = 0; + // TODO: assert(curr.length() == digits); + for k in 0.. 0) { + line = parse_line(trimmed); + result.push(line); // TODO: fix in-place push() + } + } + + result +} + +is_valid(x: int, y: int, m: int, n: int): bool = { + return x >= 0 && x < m && y >= 0 && y < n; +} + +can_access(x: int, y: int, positions: [[int]]): bool = { + m = positions.length(); + n = positions[0].length(); + if (!is_valid(x, y, m, n)) { + return false; + } + + mut count = 0; + for dx in [-1, 0, 1] { + for dy in [-1, 0, 1] { + if (dx == 0 && dy == 0) { + continue; + } + nx = x + dx; + ny = y + dy; + if (!is_valid(nx, ny, m, n)) { + continue; + } + if (is_valid(nx, ny, m, n) && positions[nx][ny] == 1) { // TODO: Fix and short-circuiting + count += 1; + if (count >= 4) { + return false; + } + } + } + } + true +} + +solve(): int = { + content = get_file_content(); + lines = get_lines(content); + + mut res = 0; + m = lines.length(); + n = lines[0].length(); + + for i in 0.. 0) { + line = parse_line(trimmed); + result.push(line); // TODO: fix in-place push() + } + } + + result +} + +is_valid(x: int, y: int, m: int, n: int): bool = { + return x >= 0 && x < m && y >= 0 && y < n; +} + +can_access(x: int, y: int, positions: [[int]]): bool = { + m = positions.length(); + n = positions[0].length(); + if (!is_valid(x, y, m, n)) { + return false; + } + + mut count = 0; + for dx in [-1, 0, 1] { + for dy in [-1, 0, 1] { + if (dx == 0 && dy == 0) { + continue; + } + nx = x + dx; + ny = y + dy; + if (!is_valid(nx, ny, m, n)) { + continue; + } + if (is_valid(nx, ny, m, n) && positions[nx][ny] == 1) { // TODO: Fix and short-circuiting + count += 1; + if (count >= 4) { + return false; + } + } + } + } + true +} + +solve(): int = { + content = get_file_content(); + mut lines = get_lines(content); + + mut res = 0; + + pass(grid: [[int]]): [[int]] = { + mut to_remove = []; + m = grid.length(); + n = grid[0].length(); + for i in 0.. ::= ()* - ::= ";" - | + ::= ";" + | + | | ::= ";" + ::= ";" | E + ::= "type" "=" ::= - ::= | + ::= | | ::= ( "|" )* ::= "(" ")" | - ::= ";" | + ::= | - ::= ":" "=" + ::= ( ":" )? "=" ::= ( ":" )? "=" ";" @@ -27,6 +30,7 @@ ::= | + | | | | @@ -37,14 +41,29 @@ ::= ()* - ::= | ";" + ::= + | + | + | + | + | ";" + + ::= | | | + + ::= "return" ? ";" + + ::= "break" ";" + + ::= "continue" ";" ::= | E - ::= "if" "(" ")" ( "else" )? + ::= "if" "(" ")" ( "else" ( | ) )? ::= "while" "(" ")" + ::= "for" "in" + ::= "match" "(" ")" "{" "}" ::= ()* @@ -52,9 +71,10 @@ ::= "," | E - ::= "_" + ::= "_" | | "(" ? ")" + | ::= ("," )* @@ -69,15 +89,16 @@ ::= ()* - ::= "(" ? ")" - | "." - | "::" + ::= "(" ? ")" + | "." + | "::" | "[" "]" ::= ("," )* ::= | + | "this" | "(" ")" | | @@ -93,21 +114,27 @@ ::= ( "->" )? - ::= - | - | + ::= ("," )* + + ::= + | + | | "[" "]" - ::= "[" "]" + ::= "[" "]" - ::= "{" ( ("," )*)? "}" + ::= "{" ( ("," )* ","?)? "}" + + ::= | ::= ":" - ::= "+" | "-" | "*" | "/" - | "==" | "!=" - | "<" | "<=" | ">" | ">=" - | "&&" | "||" + ::= ":" "=" + + ::= "+" | "-" | "*" | "/" | "%" + | "==" | "!=" + | "<" | "<=" | ">" | ">=" + | "&&" | "||" | "+=" | "-=" | "*=" | "/=" ::= | | | "true" | "false" | "None" diff --git a/src/ast.rs b/src/ast.rs index 142bd47..7fea716 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,174 +1,453 @@ use std::fmt; -/// Represents a program, which is a collection of statements. +/// Represents a span of code in the source file, from `start` to `end` character offset. +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Span { + pub fn new(start: usize, end: usize) -> Self { + Span { start, end } + } +} + +/// Represents a program, which is a collection of top-level statements. #[derive(Debug, Clone, PartialEq)] pub struct Program { + pub statements: Vec, + pub span: Span, +} + +impl Program { + pub fn new(statements: Vec, span: Span) -> Self { + Program { statements, span } + } +} + +/// Represents a top-level statement in the language. +#[derive(Debug, Clone, PartialEq)] +pub enum TopStatement { + TypeDecl(TypeDeclaration), + LetStmt(LetStatement), + Expression(ExpressionStatement), +} + +impl TopStatement { + pub fn span(&self) -> Span { + match self { + TopStatement::TypeDecl(decl) => decl.span, + TopStatement::LetStmt(stmt) => stmt.span(), + TopStatement::Expression(expr_stmt) => expr_stmt.span, + } + } +} + +/// Represents a type declaration: `type = ` +#[derive(Debug, Clone, PartialEq)] +pub struct TypeDeclaration { + pub name: String, + pub constructor: TypeConstructor, + pub span: Span, +} + +/// Represents the right-hand side of a type declaration. +#[derive(Debug, Clone, PartialEq)] +pub enum TypeConstructor { + Sum(SumConstructor), + Record(RecordType), + Alias(Type), // Added: for simple type aliases like `type A = B;` +} + +impl TypeConstructor { + pub fn span(&self) -> Span { + match self { + TypeConstructor::Sum(sum) => sum.span, + TypeConstructor::Record(record) => record.span, + TypeConstructor::Alias(ty) => ty.span(), // Added + } + } +} + +/// Represents a sum type constructor: ` ( "|" )*` +#[derive(Debug, Clone, PartialEq)] +pub struct SumConstructor { + pub variants: Vec, + pub span: Span, +} + +/// Represents a variant in a sum type: ` "(" ")" | ` +#[derive(Debug, Clone, PartialEq)] +pub struct Variant { + pub name: String, + pub ty: Option, // Optional type for variants with data + pub span: Span, +} + +/// Represents a let statement, which can be a function binding or a variable binding. +#[derive(Debug, Clone, PartialEq)] +pub enum LetStatement { + Function(FunctionBinding), + Variable(VariableBinding), +} + +impl LetStatement { + pub fn span(&self) -> Span { + match self { + LetStatement::Function(func) => func.span, + LetStatement::Variable(var) => var.span, + } + } +} + +/// Represents a function binding: ` ":" "=" ` +#[derive(Debug, Clone, PartialEq)] +pub struct FunctionBinding { + pub mutable: bool, + pub name: String, + pub params: Vec, + pub return_type: Type, + pub body: Block, + pub span: Span, +} + +/// Represents a variable binding: ` ( ":" )? "=" ";"` +#[derive(Debug, Clone, PartialEq)] +pub struct VariableBinding { + pub mutable: bool, + pub name: String, + pub type_annotation: Option, + pub value: Expression, + pub span: Span, +} + +/// Represents an expression statement: ` ";"` +#[derive(Debug, Clone, PartialEq)] +pub struct ExpressionStatement { + pub expression: Expression, + pub span: Span, +} + +/// Represents a block of statements and an optional final expression. +#[derive(Debug, Clone, PartialEq)] +pub struct Block { pub statements: Vec, + pub final_expression: Option>, + pub span: Span, +} + +/// Represents a statement within a block. +#[derive(Debug, Clone, PartialEq)] +pub enum Statement { + Let(LetStatement), + Expression(ExpressionStatement), + Return(Option, Span), + Break(Span), + Continue(Span), +} + +impl Statement { + pub fn span(&self) -> Span { + match self { + Statement::Let(let_stmt) => let_stmt.span(), + Statement::Expression(expr_stmt) => expr_stmt.span, + Statement::Return(_, span) => *span, + Statement::Break(span) => *span, + Statement::Continue(span) => *span, + } + } } -/// Represents a type annotation in the language. +/// Represents a parameter in a function or lambda definition. #[derive(Debug, Clone, PartialEq)] -pub enum TypeAnnotation { - Int, - Str, - Bool, - Unit, +pub struct Parameter { + pub name: String, + pub ty: Type, + pub span: Span, +} + +/// Represents a type in the language. +#[derive(Debug, Clone, PartialEq)] +pub enum Type { Function { - from: Box, - to: Box, - }, - Array(Box), - Struct { - name: String, - fields: Vec<(String, TypeAnnotation)>, + params: Vec, // Types of parameters + return_type: Box, + span: Span, }, - Enum { - name: String, - }, - UserDefined(String), + Primary(TypePrimary), +} + +impl Type { + pub fn span(&self) -> Span { + match self { + Type::Function { span, .. } => *span, + Type::Primary(primary) => primary.span(), + } + } } -/// Represents a single statement in the language. #[derive(Debug, Clone, PartialEq)] -pub enum Statement { - Expression(Expression), - Assignment { - name: String, - value: Expression, - }, - PropertyAssignment { - object: Expression, - property: String, - value: Expression, - }, - // Supports arr[i] = value - ArrayAssignment { - array: Expression, - index: Expression, - value: Expression, - }, - VarDecl { +pub enum TypePrimary { + Named(String, Span), // e.g., "Int", "String", "MyStruct" + Generic { name: String, - type_annotation: TypeAnnotation, - value: Option, - }, - FunctionDef(FunctionDef), - StructDecl(StructDecl), - EnumDecl(EnumDecl), - // Control Flow - While { - condition: Box, - body: Vec, - }, - For { - iterator: String, - iterable: Box, - body: Vec, - }, - // Return is optional (void returns) - Return(Option), - Break, - Continue, + args: Vec, + span: Span, + }, // e.g., "Option[Int, String]" + Record(RecordType), + List(Box, Span), // e.g., "[Int]" +} + +impl TypePrimary { + pub fn span(&self) -> Span { + match self { + TypePrimary::Named(_, span) => *span, + TypePrimary::Generic { span, .. } => *span, + TypePrimary::Record(record) => record.span, + TypePrimary::List(_, span) => *span, + } + } +} + +/// Represents a record (struct) type definition. +#[derive(Debug, Clone, PartialEq)] +pub struct RecordType { + pub fields: Vec, + pub span: Span, } -// --- Match Specific Structures --- +/// Represents a field declaration within a record type. +#[derive(Debug, Clone, PartialEq)] +pub struct FieldDeclaration { + pub name: String, + pub ty: Type, + pub span: Span, +} + +/// Represents a range expression: ` "..=" ` or ` "..<" ` +#[derive(Debug, Clone, PartialEq)] +pub struct RangeExpression { + pub start: Box, + pub end: Box, + pub inclusive: bool, + pub span: Span, +} + +/// Represents an expression in the language. +#[derive(Debug, Clone, PartialEq)] +pub enum Expression { + If(IfExpression), + While(WhileExpression), + For(ForExpression), + Match(MatchExpression), + Lambda(LambdaExpression), + Binary(BinaryExpression), + Unary(UnaryExpression), + Range(RangeExpression), + Postfix(PostfixExpression), + Primary(PrimaryExpression), + Block(Block), // A block can be an expression if it returns a value. +} + +impl Expression { + pub fn span(&self) -> Span { + match self { + Expression::If(expr) => expr.span, + Expression::While(expr) => expr.span, + Expression::For(expr) => expr.span, + Expression::Match(expr) => expr.span, + Expression::Lambda(expr) => expr.span, + Expression::Binary(expr) => expr.span, + Expression::Unary(expr) => expr.span, + Expression::Range(expr) => expr.span, + Expression::Postfix(expr) => expr.span, + Expression::Primary(expr) => expr.span(), + Expression::Block(block) => block.span, + } + } +} + +/// Represents an if expression: `"if" "(" ")" ( "else" ( | ) )?` +#[derive(Debug, Clone, PartialEq)] +pub struct IfExpression { + pub condition: Box, + pub then_branch: Block, + pub else_branch: Option>, // Changed: can be another if or a block + pub span: Span, +} + +/// Represents a while expression: `"while" "(" ")" ` +#[derive(Debug, Clone, PartialEq)] +pub struct WhileExpression { + pub condition: Box, + pub body: Block, + pub span: Span, +} + +/// Represents a for expression: `"for" "in" ` +#[derive(Debug, Clone, PartialEq)] +pub struct ForExpression { + pub pattern: Pattern, + pub iterable: Box, + pub body: Block, + pub span: Span, +} + +/// Represents a match expression: `"match" "(" ")" "{" "}"` +#[derive(Debug, Clone, PartialEq)] +pub struct MatchExpression { + pub value: Box, + pub arms: Vec, + pub span: Span, +} +/// Represents a match arm: `"|" "=>" ` #[derive(Debug, Clone, PartialEq)] pub struct MatchArm { pub pattern: Pattern, - pub body: Expression, + pub body: ExpressionOrBlock, + pub span: Span, +} + +/// Represents either an expression or a block as a body of a match arm. +#[derive(Debug, Clone, PartialEq)] +pub enum ExpressionOrBlock { + Expression(Box), + Block(Block), +} + +impl ExpressionOrBlock { + pub fn span(&self) -> Span { + match self { + ExpressionOrBlock::Expression(expr) => expr.span(), + ExpressionOrBlock::Block(block) => block.span, + } + } } +/// Represents a pattern in a match arm. #[derive(Debug, Clone, PartialEq)] pub enum Pattern { - Wildcard, // The '_' pattern - Literal(LiteralValue), - Identifier(String), - EnumVariant { - enum_name: String, - variant: String, - vars: Vec, // Recursive patterns for Option::Some(x) + Wildcard(Span), // "_" + Identifier(String, Span), // e.g., "x" + Variant { + name: String, + patterns: Option>, // For variants with data, e.g., Some(x) + span: Span, }, + Literal(LiteralValue, Span), // For literal patterns: int, float, string, bool, None +} + +impl Pattern { + pub fn span(&self) -> Span { + match self { + Pattern::Wildcard(span) => *span, + Pattern::Identifier(_, span) => *span, + Pattern::Variant { span, .. } => *span, + Pattern::Literal(_, span) => *span, + } + } } +/// Represents a lambda expression: ` ( ":" )? "=>" ` #[derive(Debug, Clone, PartialEq)] -pub struct EnumVariant { - pub name: String, - pub types: Vec, +pub struct LambdaExpression { + pub params: Vec, + pub return_type_annotation: Option, + pub body: ExpressionOrBlock, + pub span: Span, } +/// Represents a binary expression: ` ( )*` #[derive(Debug, Clone, PartialEq)] -pub struct StructDecl { - pub name: String, - pub fields: Vec<(String, TypeAnnotation)>, +pub struct BinaryExpression { + pub left: Box, + pub operator: BinaryOperator, + pub right: Box, + pub span: Span, } +/// Represents a unary expression: `( "+" | "-" | "!" )? ` #[derive(Debug, Clone, PartialEq)] -pub struct EnumDecl { - pub name: String, - pub variants: Vec, +pub struct UnaryExpression { + pub operator: UnaryOperator, + pub right: Box, + pub span: Span, } +/// Represents a postfix expression: ` ()*` #[derive(Debug, Clone, PartialEq)] -pub struct FunctionDef { - pub name: String, - pub args: Vec, - pub body: Vec, +pub struct PostfixExpression { + pub primary: Box, // Changed from PrimaryExpression to Expression + pub operators: Vec, + pub span: Span, } -/// Represents a single expression in the language. +/// Represents a postfix operator. #[derive(Debug, Clone, PartialEq)] -pub enum Expression { - Literal(LiteralValue), - Identifier(String), - List(Vec), - Lambda { - args: Vec, - body: Box, - }, - FunctionCall { - callee: Box, - args: Vec, - }, - Binary { - left: Box, - op: Operator, - right: Box, - }, - // Unary Operations (!true, -5) - Unary { - op: Operator, - right: Box, - }, - StructInstantiation { - name: String, - fields: Vec<(String, Expression)>, - }, - Get { - object: Box, - name: String, - }, - Path { - parts: Vec, - }, - If { - condition: Box, - then_branch: Vec, - else_branch: Option>, - }, - EnumVariant { - enum_name: String, - variant_name: String, - }, - ArrayAccess { - array: Box, - index: Box, - }, - Block(Vec), - Match { - value: Box, - arms: Vec, - }, +pub enum PostfixOperator { + Call { args: Vec, span: Span }, // "(" ? ")" + FieldAccess { name: String, span: Span }, // "." + TypePath { name: String, span: Span }, // "::" + ListAccess { index: Box, span: Span }, // "[" "]" +} + +impl PostfixOperator { + pub fn span(&self) -> Span { + match self { + PostfixOperator::Call { span, .. } => *span, + PostfixOperator::FieldAccess { span, .. } => *span, + PostfixOperator::TypePath { span, .. } => *span, + PostfixOperator::ListAccess { span, .. } => *span, + } + } +} + +/// Represents a primary expression. +#[derive(Debug, Clone, PartialEq)] +pub enum PrimaryExpression { + Literal(LiteralValue, Span), + Identifier(String, Span), + This(Span), + Parenthesized(Box, Span), // "(" ")" + List(ListLiteral), + Record(RecordLiteral), +} + +impl PrimaryExpression { + pub fn span(&self) -> Span { + match self { + PrimaryExpression::Literal(_, span) => *span, + PrimaryExpression::Identifier(_, span) => *span, + PrimaryExpression::This(span) => *span, + PrimaryExpression::Parenthesized(_, span) => *span, + PrimaryExpression::List(list) => list.span, + PrimaryExpression::Record(record) => record.span, + } + } +} + +/// Represents a list literal: "[" ( ("," )*)? "]" +#[derive(Debug, Clone, PartialEq)] +pub struct ListLiteral { + pub elements: Vec, + pub span: Span, +} + +/// Represents a record literal: "{" ("," )* "}" +#[derive(Debug, Clone, PartialEq)] +pub struct RecordLiteral { + pub fields: Vec, + pub span: Span, +} + +/// Represents a field initializer in a record literal: ` ":" ` +#[derive(Debug, Clone, PartialEq)] +pub struct FieldInitializer { + pub name: String, + pub value: Expression, + pub span: Span, } /// Represents a literal value in the language. @@ -178,14 +457,13 @@ pub enum LiteralValue { Float(f64), String(String), Boolean(bool), - Unit, - Array(Vec), + None, // "None" keyword } -/// Represents an operator in a binary or unary expression. +/// Represents a binary operator. #[derive(Debug, Clone, Copy, PartialEq)] -pub enum Operator { - // Binary Arithmetic +pub enum BinaryOperator { + // Arithmetic Add, Subtract, Multiply, @@ -200,15 +478,31 @@ pub enum Operator { LessThan, LessThanEqual, - // Logic + // Logical And, Or, - // Unary + // Assignment + Assign, + + // Assignment with operation + AddAssign, + SubtractAssign, + MultiplyAssign, + DivideAssign, + ModuloAssign, +} + +/// Represents a unary operator. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UnaryOperator { + Plus, + Minus, Not, - // Negate uses 'Subtract' usually, or you can add specific 'Negate' } +// Display implementations for easier debugging and printing + impl fmt::Display for LiteralValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -216,17 +510,43 @@ impl fmt::Display for LiteralValue { LiteralValue::Float(fl) => write!(f, "{}", fl), LiteralValue::String(s) => write!(f, "\"{}\"", s), LiteralValue::Boolean(b) => write!(f, "{}", b), - LiteralValue::Unit => write!(f, "()"), - LiteralValue::Array(elements) => { - let elems: Vec = elements.iter().map(|e| format!("{}", e)).collect(); - write!(f, "[{}]", elems.join(", ")) - } + LiteralValue::None => write!(f, "None"), } } } -impl fmt::Display for FunctionDef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fn {}({}) {{ ... }}", self.name, self.args.join(", ")) +impl fmt::Display for BinaryOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BinaryOperator::Add => write!(f, "+"), + BinaryOperator::Subtract => write!(f, "-"), + BinaryOperator::Multiply => write!(f, "*"), + BinaryOperator::Divide => write!(f, "/"), + BinaryOperator::Modulo => write!(f, "%"), + BinaryOperator::Equal => write!(f, "=="), + BinaryOperator::NotEqual => write!(f, "!="), + BinaryOperator::GreaterThan => write!(f, ">"), + BinaryOperator::GreaterThanEqual => write!(f, ">="), + BinaryOperator::LessThan => write!(f, "<"), + BinaryOperator::LessThanEqual => write!(f, "<="), + BinaryOperator::And => write!(f, "&&"), + BinaryOperator::Or => write!(f, "||"), + BinaryOperator::Assign => write!(f, "="), + BinaryOperator::AddAssign => write!(f, "+="), + BinaryOperator::SubtractAssign => write!(f, "-="), + BinaryOperator::MultiplyAssign => write!(f, "*="), + BinaryOperator::DivideAssign => write!(f, "/="), + BinaryOperator::ModuloAssign => write!(f, "%="), + } + } +} + +impl fmt::Display for UnaryOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnaryOperator::Plus => write!(f, "+"), + UnaryOperator::Minus => write!(f, "-"), + UnaryOperator::Not => write!(f, "!"), + } } } diff --git a/src/builtins.rs b/src/builtins.rs new file mode 100644 index 0000000..2379280 --- /dev/null +++ b/src/builtins.rs @@ -0,0 +1,596 @@ +use crate::interpreter::{Interpreter, MapKey, RuntimeError, Value}; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Read, Write}; + +pub fn eval_method( + interp: &mut Interpreter, + receiver: Value, + method: &str, + args: Vec, + var_name: Option<&str>, +) -> Result { + // Helper to enforce argument counts + let check_arg_count = |expected: usize| -> Result<(), RuntimeError> { + if args.len() != expected { + Err(RuntimeError::Type(format!( + "{} expects {} arguments", + method, expected + ))) + } else { + Ok(()) + } + }; + + // Helper macro to handle mutation: update env and return the mutated structure + macro_rules! mutate_and_return { + ($new_val:expr) => {{ + let val = $new_val; + if let Some(name) = var_name { + interp.env.set(name, val.clone()); + } + Ok(val) + }}; + } + + // Helper macro for side-effects (pop/remove) that return an Item but modify the Collection in env + macro_rules! mutate_side_effect { + ($new_collection:expr, $return_item:expr) => {{ + if let Some(name) = var_name { + interp.env.set(name, $new_collection); + } + Ok($return_item) + }}; + } + + match receiver { + // ==================== MAP METHODS ==================== + Value::Map(mut map) => match method { + "insert" => { + check_arg_count(2)?; + let key = MapKey::from_value(&args[0])?; + map.insert(key, args[1].clone()); + mutate_and_return!(Value::Map(map)) + } + "get" => { + check_arg_count(1)?; + let key = MapKey::from_value(&args[0])?; + map.get(&key) + .cloned() + .ok_or_else(|| RuntimeError::Type(format!("Key {:?} not found in map", key))) + } + "has" | "contains" => { + check_arg_count(1)?; + let key = MapKey::from_value(&args[0])?; + Ok(Value::Boolean(map.contains_key(&key))) + } + "remove" => { + check_arg_count(1)?; + let key = MapKey::from_value(&args[0])?; + let removed = map + .remove(&key) + .ok_or_else(|| RuntimeError::Type(format!("Key {:?} not found in map", key)))?; + mutate_side_effect!(Value::Map(map), removed) + } + "length" | "size" => Ok(Value::Integer(map.len() as i64)), + "is_empty" => Ok(Value::Boolean(map.is_empty())), + "clear" => { + mutate_and_return!(Value::Map(HashMap::new())) + } + "keys" => { + let keys: Vec = map.keys().map(|k| k.to_value()).collect(); + Ok(Value::List(keys)) + } + "values" => { + let values: Vec = map.values().cloned().collect(); + Ok(Value::List(values)) + } + "entries" => { + let entries: Vec = map + .iter() + .map(|(k, v)| { + let mut fields = HashMap::new(); + fields.insert("key".to_string(), k.to_value()); + fields.insert("value".to_string(), v.clone()); + Value::Record(fields) + }) + .collect(); + Ok(Value::List(entries)) + } + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for Map", + method + ))), + }, + + // ==================== LIST METHODS ==================== + Value::List(mut list) => match method { + "push" | "append" => { + check_arg_count(1)?; + list.push(args[0].clone()); + mutate_and_return!(Value::List(list)) + } + "pop" => { + if list.is_empty() { + return Err(RuntimeError::Type("Cannot pop from empty list".into())); + } + let popped = list.pop().unwrap(); + mutate_side_effect!(Value::List(list), popped) + } + "remove" => { + check_arg_count(1)?; + if let Value::Integer(idx) = args[0] { + if idx < 0 || idx as usize >= list.len() { + return Err(RuntimeError::Type(format!("Index {} out of bounds", idx))); + } + let removed = list.remove(idx as usize); + mutate_side_effect!(Value::List(list), removed) + } else { + Err(RuntimeError::Type("remove index must be integer".into())) + } + } + "insert" => { + check_arg_count(2)?; + if let Value::Integer(idx) = args[0] { + if idx < 0 || idx as usize > list.len() { + return Err(RuntimeError::Type(format!("Index {} out of bounds", idx))); + } + list.insert(idx as usize, args[1].clone()); + mutate_and_return!(Value::List(list)) + } else { + Err(RuntimeError::Type("insert index must be integer".into())) + } + } + "reverse" => { + list.reverse(); + mutate_and_return!(Value::List(list)) + } + "sort" => { + // Sorting logic + if list.iter().all(|v| matches!(v, Value::Integer(_))) { + list.sort_by(|a, b| { + if let (Value::Integer(x), Value::Integer(y)) = (a, b) { + x.cmp(y) + } else { + std::cmp::Ordering::Equal + } + }); + } else if list.iter().all(|v| matches!(v, Value::Float(_))) { + list.sort_by(|a, b| { + if let (Value::Float(x), Value::Float(y)) = (a, b) { + x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal) + } else { + std::cmp::Ordering::Equal + } + }); + } else if list.iter().all(|v| matches!(v, Value::String(_))) { + list.sort_by(|a, b| { + if let (Value::String(x), Value::String(y)) = (a, b) { + x.cmp(y) + } else { + std::cmp::Ordering::Equal + } + }); + } else { + return Err(RuntimeError::Type( + "Cannot sort list with mixed or unsortable types".into(), + )); + } + mutate_and_return!(Value::List(list)) + } + "length" => Ok(Value::Integer(list.len() as i64)), + "contains" => { + check_arg_count(1)?; + Ok(Value::Boolean(list.contains(&args[0]))) + } + "index_of" => { + check_arg_count(1)?; + match list.iter().position(|v| v == &args[0]) { + Some(idx) => Ok(Value::Integer(idx as i64)), + None => Ok(Value::Integer(-1)), + } + } + "slice" => { + check_arg_count(2)?; + match (&args[0], &args[1]) { + (Value::Integer(s), Value::Integer(e)) => { + let start = (*s).max(0) as usize; + let end = ((*e).max(0) as usize).min(list.len()); + if start <= end && start <= list.len() { + Ok(Value::List(list[start..end].to_vec())) + } else { + Err(RuntimeError::Type(format!( + "Invalid slice range {}..{}", + s, e + ))) + } + } + _ => Err(RuntimeError::Type( + "slice arguments must be integers".into(), + )), + } + } + "join" => { + check_arg_count(1)?; + if let Value::String(sep) = &args[0] { + let strings: Result, _> = list + .iter() + .map(|v| { + if let Value::String(s) = v { + Ok(s.clone()) + } else { + Err(RuntimeError::Type("join requires list of strings".into())) + } + }) + .collect(); + match strings { + Ok(strs) => Ok(Value::String(strs.join(sep))), + Err(e) => Err(e), + } + } else { + Err(RuntimeError::Type("join separator must be string".into())) + } + } + "map" => { + check_arg_count(1)?; + let func = args[0].clone(); + let mut results = Vec::new(); + for elem in list { + // Call back into Interpreter to evaluate closure! + let result = interp.eval_function_call_value(func.clone(), &[elem])?; + results.push(result); + } + Ok(Value::List(results)) + } + "filter" => { + check_arg_count(1)?; + let func = args[0].clone(); + let mut results = Vec::new(); + for elem in list { + let keep = interp.eval_function_call_value(func.clone(), &[elem.clone()])?; + if let Value::Boolean(true) = keep { + results.push(elem); + } else if !matches!(keep, Value::Boolean(_)) { + return Err(RuntimeError::Type( + "filter predicate must return boolean".into(), + )); + } + } + Ok(Value::List(results)) + } + "first" => list + .first() + .cloned() + .ok_or_else(|| RuntimeError::Type("Cannot get first of empty list".into())), + "last" => list + .last() + .cloned() + .ok_or_else(|| RuntimeError::Type("Cannot get last of empty list".into())), + "is_empty" => Ok(Value::Boolean(list.is_empty())), + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for List", + method + ))), + }, + + // ==================== STRING METHODS ==================== + Value::String(s) => match method { + "length" => Ok(Value::Integer(s.len() as i64)), + "split" => { + check_arg_count(1)?; + if let Value::String(d) = &args[0] { + let parts: Vec = s + .split(d.as_str()) + .map(|p| Value::String(p.to_string())) + .collect(); + Ok(Value::List(parts)) + } else { + Err(RuntimeError::Type("split delimiter must be string".into())) + } + } + "parse_int" => s + .trim() + .parse::() + .map(Value::Integer) + .map_err(|_| RuntimeError::Type(format!("Cannot parse '{}' as integer", s))), + "parse_float" => s + .trim() + .parse::() + .map(Value::Float) + .map_err(|_| RuntimeError::Type(format!("Cannot parse '{}' as float", s))), + "trim" => Ok(Value::String(s.trim().to_string())), + "trim_start" => Ok(Value::String(s.trim_start().to_string())), + "trim_end" => Ok(Value::String(s.trim_end().to_string())), + "contains" => { + check_arg_count(1)?; + if let Value::String(n) = &args[0] { + Ok(Value::Boolean(s.contains(n.as_str()))) + } else { + Err(RuntimeError::Type( + "contains argument must be string".into(), + )) + } + } + "starts_with" => { + check_arg_count(1)?; + if let Value::String(p) = &args[0] { + Ok(Value::Boolean(s.starts_with(p.as_str()))) + } else { + Err(RuntimeError::Type( + "starts_with argument must be string".into(), + )) + } + } + "ends_with" => { + check_arg_count(1)?; + if let Value::String(suf) = &args[0] { + Ok(Value::Boolean(s.ends_with(suf.as_str()))) + } else { + Err(RuntimeError::Type( + "ends_with argument must be string".into(), + )) + } + } + "replace" => { + check_arg_count(2)?; + if let (Value::String(f), Value::String(t)) = (&args[0], &args[1]) { + Ok(Value::String(s.replace(f.as_str(), t.as_str()))) + } else { + Err(RuntimeError::Type( + "replace arguments must be strings".into(), + )) + } + } + "to_lower" => Ok(Value::String(s.to_lowercase())), + "to_upper" => Ok(Value::String(s.to_uppercase())), + "char_at" => { + check_arg_count(1)?; + if let Value::Integer(i) = args[0] { + if i < 0 || i as usize >= s.len() { + return Err(RuntimeError::Type(format!("Index {} out of bounds", i))); + } + let ch = s.chars().nth(i as usize).unwrap(); + Ok(Value::String(ch.to_string())) + } else { + Err(RuntimeError::Type("char_at index must be integer".into())) + } + } + "chars" => { + let chars: Vec = s.chars().map(|c| Value::String(c.to_string())).collect(); + Ok(Value::List(chars)) + } + "index_of" => { + check_arg_count(1)?; + if let Value::String(n) = &args[0] { + match s.find(n.as_str()) { + Some(idx) => Ok(Value::Integer(idx as i64)), + None => Ok(Value::Integer(-1)), + } + } else { + Err(RuntimeError::Type( + "index_of argument must be string".into(), + )) + } + } + "substring" => { + check_arg_count(2)?; + match (&args[0], &args[1]) { + (Value::Integer(start), Value::Integer(len)) => { + let start = *start as usize; + let len = *len as usize; + let end = (start + len).min(s.len()); + if start <= s.len() { + Ok(Value::String(s[start..end].to_string())) + } else { + Err(RuntimeError::Type(format!( + "Start index {} out of bounds", + start + ))) + } + } + _ => Err(RuntimeError::Type( + "substring arguments must be integers".into(), + )), + } + } + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for String", + method + ))), + }, + + // ==================== INTEGER METHODS ==================== + Value::Integer(i) => match method { + "to_float" => Ok(Value::Float(i as f64)), + "to_string" => Ok(Value::String(i.to_string())), + "abs" => Ok(Value::Integer(i.abs())), + "pow" => { + check_arg_count(1)?; + if let Value::Integer(e) = args[0] { + if e < 0 { + return Err(RuntimeError::Type( + "pow exponent must be non-negative".into(), + )); + } + Ok(Value::Integer(i.pow(e as u32))) + } else { + Err(RuntimeError::Type("pow exponent must be integer".into())) + } + } + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for Integer", + method + ))), + }, + + // ==================== FLOAT METHODS ==================== + Value::Float(f) => match method { + "to_string" => Ok(Value::String(f.to_string())), + "to_int" => Ok(Value::Integer(f as i64)), + "abs" => Ok(Value::Float(f.abs())), + "floor" => Ok(Value::Float(f.floor())), + "ceil" => Ok(Value::Float(f.ceil())), + "round" => Ok(Value::Float(f.round())), + "sqrt" => Ok(Value::Float(f.sqrt())), + "pow" => { + check_arg_count(1)?; + match args[0] { + Value::Float(e) => Ok(Value::Float(f.powf(e))), + Value::Integer(e) => Ok(Value::Float(f.powi(e as i32))), + _ => Err(RuntimeError::Type("pow exponent must be number".into())), + } + } + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for Float", + method + ))), + }, + + // ==================== BOOLEAN METHODS ==================== + Value::Boolean(b) => match method { + "to_string" => Ok(Value::String(b.to_string())), + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for Boolean", + method + ))), + }, + + // ==================== FILE METHODS ==================== + Value::File { id, closed, .. } => match method { + "read" => { + if closed { + return Err(RuntimeError::Type("Cannot read from closed file".into())); + } + if id >= interp.files.len() || interp.files[id].is_none() { + return Err(RuntimeError::Type("Invalid file descriptor".into())); + } + let mut content = String::new(); + if let Some(file) = &mut interp.files[id] { + file.read_to_string(&mut content) + .map_err(|e| RuntimeError::Type(format!("Failed to read file: {}", e)))?; + } + Ok(Value::String(content)) + } + "read_lines" => { + if closed { + return Err(RuntimeError::Type("Cannot read from closed file".into())); + } + if id >= interp.files.len() || interp.files[id].is_none() { + return Err(RuntimeError::Type("Invalid file descriptor".into())); + } + let lines: Vec = if let Some(file) = &interp.files[id] { + BufReader::new(file) + .lines() + .collect::, _>>() + .map_err(|e| RuntimeError::Type(format!("Failed to read lines: {}", e)))? + .into_iter() + .map(Value::String) + .collect() + } else { + Vec::new() + }; + Ok(Value::List(lines)) + } + "write" => { + check_arg_count(1)?; + if closed { + return Err(RuntimeError::Type("Cannot write to closed file".into())); + } + if id >= interp.files.len() || interp.files[id].is_none() { + return Err(RuntimeError::Type("Invalid file descriptor".into())); + } + let text_data = interp.value_to_display_string(&args[0]); + if let Some(file) = &mut interp.files[id] { + file.write_all(text_data.as_bytes()).map_err(|e| { + RuntimeError::Type(format!("Failed to write to file: {}", e)) + })?; + } + Ok(Value::Unit) + } + "write_line" => { + check_arg_count(1)?; + if closed { + return Err(RuntimeError::Type("Cannot write to closed file".into())); + } + if id >= interp.files.len() || interp.files[id].is_none() { + return Err(RuntimeError::Type("Invalid file descriptor".into())); + } + let text_data = format!("{}\n", interp.value_to_display_string(&args[0])); + if let Some(file) = &mut interp.files[id] { + file.write_all(text_data.as_bytes()).map_err(|e| { + RuntimeError::Type(format!("Failed to write to file: {}", e)) + })?; + } + Ok(Value::Unit) + } + "close" => { + if id >= interp.files.len() { + return Err(RuntimeError::Type("Invalid file descriptor".into())); + } + interp.files[id] = None; + Ok(Value::Unit) + } + "is_closed" => Ok(Value::Boolean(closed)), + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for File", + method + ))), + }, + + // ==================== ARGS METHODS ==================== + Value::Args(args_obj) => match method { + "program" => Ok(Value::String(args_obj.program.clone())), + "values" => Ok(Value::List( + args_obj + .values + .iter() + .map(|s| Value::String(s.clone())) + .collect(), + )), + "length" => Ok(Value::Integer(args_obj.values.len() as i64)), + "get" => { + check_arg_count(1)?; + if let Value::Integer(i) = args[0] { + if i < 0 || i as usize >= args_obj.values.len() { + return Ok(Value::Unit); + } + Ok(Value::String(args_obj.values[i as usize].clone())) + } else { + Err(RuntimeError::Type("args.get index must be integer".into())) + } + } + "has" => { + check_arg_count(1)?; + if let Value::String(f) = &args[0] { + Ok(Value::Boolean(args_obj.flags.contains_key(f))) + } else { + Err(RuntimeError::Type( + "args.has argument must be string".into(), + )) + } + } + "get_option" => { + check_arg_count(1)?; + if let Value::String(k) = &args[0] { + match args_obj.options.get(k) { + Some(v) => Ok(Value::String(v.clone())), + None => Ok(Value::Unit), + } + } else { + Err(RuntimeError::Type( + "args.get_option argument must be string".into(), + )) + } + } + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for Args", + method + ))), + }, + + _ => Err(RuntimeError::Type(format!( + "Unknown method '{}' for type {:?}", + method, + std::mem::discriminant(&receiver) + ))), + } +} diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 6274dd9..d68bd6c 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -1,160 +1,123 @@ -use crate::parser::ParseError; -use std::fmt::Write; +use crate::ast::Span; + +/// Represents the severity of a diagnostic message. +#[derive(Debug, Clone, PartialEq)] +pub enum DiagnosticKind { + Error, + Warning, + // Info, // Potentially add info or hint +} -/// Convert byte offset to (line, column) -pub fn byte_to_line_col(source: &str, byte_offset: usize) -> (usize, usize) { - let mut line = 1; - let mut col = 1; +/// Represents a single diagnostic message (error, warning, etc.). +#[derive(Debug, Clone, PartialEq)] +pub struct Diagnostic { + pub kind: DiagnosticKind, + pub message: String, + pub span: Span, + pub context: Option, +} - for (idx, ch) in source.char_indices() { - if idx >= byte_offset { - break; - } - if ch == '\n' { - line += 1; - col = 1; - } else { - col += 1; +impl Diagnostic { + pub fn new(kind: DiagnosticKind, message: String, span: Span) -> Self { + Diagnostic { + kind, + message, + span, + context: None, } } - (line, col) + pub fn with_context(mut self, context: String) -> Self { + self.context = Some(context); + self + } } -/// Extract a specific line from source -fn get_line(source: &str, line_num: usize) -> Option<&str> { - source.lines().nth(line_num - 1) +/// Manages and collects diagnostic messages during lexing, parsing, and other phases. +pub struct Reporter { + pub diagnostics: Vec, + pub has_errors: bool, } -/// Format a diagnostic error with pretty-printing -pub fn format_diagnostic(error: &ParseError, source: &str, filename: &str) -> String { - let mut output = String::new(); +impl Reporter { + pub fn new() -> Self { + Reporter { + diagnostics: Vec::new(), + has_errors: false, + } + } - match error { - ParseError::UnexpectedToken { - context, - expected, - found, - span, - .. - } => { - let (line, col) = byte_to_line_col(source, span.lo); - - // Header - writeln!( - &mut output, - "\x1b[1;31merror\x1b[0m: Unexpected token in {}", - context - ) - .unwrap(); - writeln!( - &mut output, - " \x1b[1;34m-->\x1b[0m {}:{}:{}", - filename, line, col - ) - .unwrap(); - - // Context lines - if let Some(line_content) = get_line(source, line) { - writeln!(&mut output, "\x1b[1;34m{:>4} |\x1b[0m", line).unwrap(); - writeln!(&mut output, "\x1b[1;34m |\x1b[0m {}", line_content).unwrap(); - - // Underline with carets - let underline_start = col - 1; - let underline_len = span.len().max(1); - let spaces = " ".repeat(underline_start); - let carets = "\x1b[1;31m".to_string() + &"^".repeat(underline_len) + "\x1b[0m"; - - writeln!( - &mut output, - "\x1b[1;34m |\x1b[0m {}{} expected {}, found '{}'", - spaces, carets, expected, found - ) - .unwrap(); - } + pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) { + if diagnostic.kind == DiagnosticKind::Error { + self.has_errors = true; } + self.diagnostics.push(diagnostic); + } + + pub fn has_errors(&self) -> bool { + self.has_errors + } - ParseError::UnexpectedEof { context, span } => { - let (line, col) = byte_to_line_col(source, span.lo); - - writeln!( - &mut output, - "\x1b[1;31merror\x1b[0m: Unexpected end of file" - ) - .unwrap(); - writeln!( - &mut output, - " \x1b[1;34m-->\x1b[0m {}:{}:{}", - filename, line, col - ) - .unwrap(); - writeln!(&mut output, "\x1b[1;34m |\x1b[0m").unwrap(); - writeln!( - &mut output, - "\x1b[1;34m |\x1b[0m \x1b[1;31m^\x1b[0m unexpected EOF while parsing {}", - context - ) - .unwrap(); + pub fn format_diagnostics(&self, source: &str) -> String { + let mut output = String::new(); + for diagnostic in &self.diagnostics { + let (line, col) = byte_to_line_col(source, diagnostic.span.start); + let kind_str = match diagnostic.kind { + DiagnosticKind::Error => "Error", + DiagnosticKind::Warning => "Warning", + }; + + output.push_str(&format!("{} at line {}, column {}", kind_str, line, col)); + if let Some(context) = &diagnostic.context { + output.push_str(&format!(" ({})", context)); + } + output.push_str(&format!(": {}\n", diagnostic.message)); + + // Show the line with the error + if let Some(line_text) = get_line(source, line) { + output.push_str(&format!(" | {}\n", line_text)); + output.push_str(&format!(" | {}^\n", " ".repeat(col.saturating_sub(1)))); + } } + output } - output + /// Reports all collected diagnostics to stderr. + /// In a real compiler, this would involve pretty-printing with source context. + pub fn emit_diagnostics(&self, _source: &str) { + for diagnostic in &self.diagnostics { + // For now, simple printing. This will be expanded later for pretty-printing. + eprintln!( + "{:?} at {:?} (Context: {:?}): {}", + diagnostic.kind, diagnostic.span, diagnostic.context, diagnostic.message + ); + } + } } -/// Check if ANSI colors should be disabled -pub fn should_use_colors() -> bool { - // Check if output is a TTY and TERM is set - std::env::var("NO_COLOR").is_err() && atty::is(atty::Stream::Stderr) -} +// --- Utility functions for rich error reporting (to be used by format_diagnostic later) --- -/// Format diagnostic without colors -pub fn format_diagnostic_plain(error: &ParseError, source: &str, filename: &str) -> String { - // Similar to above but without ANSI codes - let mut output = String::new(); +/// Convert byte offset to (line, column) +pub fn byte_to_line_col(source: &str, byte_offset: usize) -> (usize, usize) { + let mut line = 1; + let mut col = 1; - match error { - ParseError::UnexpectedToken { - context, - expected, - found, - span, - .. - } => { - let (line, col) = byte_to_line_col(source, span.lo); - writeln!(&mut output, "error: Unexpected token in {}", context).unwrap(); - writeln!(&mut output, " --> {}:{}:{}", filename, line, col).unwrap(); - - if let Some(line_content) = get_line(source, line) { - writeln!(&mut output, "{:>4} |", line).unwrap(); - writeln!(&mut output, " | {}", line_content).unwrap(); - - let underline_start = col - 1; - let underline_len = span.len().max(1); - let spaces = " ".repeat(underline_start); - let carets = "^".repeat(underline_len); - - writeln!( - &mut output, - " | {}{} expected {}, found '{}'", - spaces, carets, expected, found - ) - .unwrap(); - } + for (idx, ch) in source.char_indices() { + if idx >= byte_offset { + break; } - - ParseError::UnexpectedEof { context, span } => { - let (line, col) = byte_to_line_col(source, span.lo); - writeln!(&mut output, "error: Unexpected end of file").unwrap(); - writeln!(&mut output, " --> {}:{}:{}", filename, line, col).unwrap(); - writeln!(&mut output, " |").unwrap(); - writeln!( - &mut output, - " | ^ unexpected EOF while parsing {}", - context - ) - .unwrap(); + if ch == '\n' { + line += 1; + col = 1; + } else { + col += 1; } } - output + (line, col) +} + +/// Extract a specific line from source +pub fn get_line(source: &str, line_num: usize) -> Option<&str> { + source.lines().nth(line_num - 1) } diff --git a/src/environment.rs b/src/environment.rs index 6472be9..a40a307 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -3,60 +3,70 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -/// Represents a runtime environment, which stores variables and functions. +#[derive(Debug, Clone, PartialEq)] +struct Scope { + values: HashMap, + enclosing: Option, +} + #[derive(Debug, Clone, PartialEq)] pub struct Environment { - store: HashMap, - parent: Option>>, + state: Rc>, } impl Environment { - /// Creates a new, empty `Environment`. pub fn new() -> Self { Environment { - store: HashMap::new(), - parent: None, + state: Rc::new(RefCell::new(Scope { + values: HashMap::new(), + enclosing: None, + })), } } - /// Creates a new `Environment` that is enclosed by another `Environment`. - pub fn new_enclosed(parent: Rc>) -> Self { + pub fn enclose(&self) -> Self { Environment { - store: HashMap::new(), - parent: Some(parent), + state: Rc::new(RefCell::new(Scope { + values: HashMap::new(), + enclosing: Some(self.clone()), + })), } } - /// Gets a value from the environment. + pub fn define(&mut self, name: String, value: Value) { + self.state.borrow_mut().values.insert(name, value); + } + pub fn get(&self, name: &str) -> Option { - if let Some(value) = self.store.get(name) { - Some(value.clone()) - } else if let Some(parent_rc) = &self.parent { - let parent = parent_rc.borrow(); - parent.get(name) - } else { - None + let state = self.state.borrow(); + if let Some(val) = state.values.get(name) { + return Some(val.clone()); } - } - pub fn define(&mut self, name: String, value: Value) { - self.store.insert(name, value); + if let Some(enclosing) = &state.enclosing { + return enclosing.get(name); + } + + None } - /// Sets a value in the environment, traversing up to parent scopes. - pub fn set(&mut self, name: String, value: Value) { - if self.store.contains_key(&name) { - self.store.insert(name, value); - return; + fn update_if_exists(&self, name: &str, value: Value) -> bool { + let mut state = self.state.borrow_mut(); + if state.values.contains_key(name) { + state.values.insert(name.to_string(), value); + return true; + } + if let Some(enclosing) = &state.enclosing { + return enclosing.update_if_exists(name, value); } + false + } - if let Some(parent_rc) = &self.parent { - parent_rc.borrow_mut().set(name, value); - } else { - // If it doesn't exist in any scope, define it in the current one. - // This is wrong for assignment, but the parser should prevent this. - // For now, we will allow it to create a global. - self.store.insert(name, value); + pub fn set(&mut self, name: &str, value: Value) -> bool { + if self.update_if_exists(name, value.clone()) { + return true; } + self.define(name.to_string(), value); + true } } diff --git a/src/interpreter.rs b/src/interpreter.rs index 08bb317..eca8bb7 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1,772 +1,1329 @@ -use crate::ast::{Expression, FunctionDef, LiteralValue, Operator, Pattern, Program, Statement}; +use crate::ast::*; +use crate::builtins::eval_method; use crate::environment::Environment; -use crate::utils::gensym; -use std::cell::RefCell; use std::collections::HashMap; -use std::fmt; -use std::rc::Rc; use thiserror::Error; #[derive(Debug, Clone, PartialEq)] -pub struct Lambda { - pub args: Vec, - pub body: Box, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Struct { - pub name: String, - pub fields: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct StructInstance { - pub name: String, - pub fields: HashMap, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Enum { - pub name: String, - pub variants: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct EnumVariant { - pub enum_name: String, - pub variant_name: String, - pub values: Vec, +pub enum Value { + Integer(i64), + Float(f64), + String(String), + Boolean(bool), + List(Vec), + Record(HashMap), + Function { + name: Option, + params: Vec, + body: Block, + env: Environment, + }, + BuiltInMethod { + receiver: Box, + method: String, + }, + File { + id: usize, // index into interpreter's file table + path: String, // path to file + mode: FileMode, // R/W/Append + closed: bool, + }, + Args(Args), + Variant { + name: String, + data: Option>, + }, + Range { + start: i64, + end: i64, + inclusive: bool, + }, + Map(HashMap), + Unit, } #[derive(Debug, Clone, PartialEq)] -pub struct Closure { - pub definition: Rc, - pub env: Rc>, +pub enum FileMode { + Read, + Write, + Append, } -#[derive(Debug, Clone, PartialEq)] -pub enum Value { +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum MapKey { Integer(i64), - Float(f64), String(String), - Function(FunctionDef), - Closure(Closure), - Lambda(Lambda), - Struct(Struct), - StructInstance(StructInstance), - Enum(Enum), - EnumVariant(EnumVariant), Boolean(bool), - List(Vec), - Unit, - Null, } -impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl MapKey { + pub fn from_value(value: &Value) -> Result { + match value { + Value::Integer(i) => Ok(MapKey::Integer(*i)), + Value::String(s) => Ok(MapKey::String(s.clone())), + Value::Boolean(b) => Ok(MapKey::Boolean(*b)), + _ => Err(RuntimeError::Type( + "Map keys must be int, string, or bool".to_string(), + )), + } + } + + pub fn to_value(&self) -> Value { match self { - Value::Integer(val) => write!(f, "{}", val), - Value::Float(val) => write!(f, "{}", val), - Value::String(val) => write!(f, "{}", val), - Value::Boolean(b) => write!(f, "{}", b), - Value::List(els) => { - let elements: Vec = els.iter().map(|e| format!("{}", e)).collect(); - write!(f, "[{}]", elements.join(", ")) - } - Value::Unit => write!(f, "()"), - Value::Null => write!(f, "null"), - _ => write!(f, "<{}>", self.type_name()), + MapKey::Integer(i) => Value::Integer(*i), + MapKey::String(s) => Value::String(s.clone()), + MapKey::Boolean(b) => Value::Boolean(*b), } } } -impl Value { - pub fn type_name(&self) -> &'static str { - match self { - Value::Integer(_) => "int", - Value::Float(_) => "float", - Value::String(_) => "str", - Value::Boolean(_) => "bool", - Value::List(_) => "list", - Value::Unit => "unit", - Value::Null => "null", - Value::Function(_) | Value::Closure(_) | Value::Lambda(_) => "function", - Value::Struct(_) => "struct_def", - Value::StructInstance(_) => "struct_instance", - Value::Enum(_) => "enum_def", - Value::EnumVariant(_) => "enum_variant", +#[derive(Debug, Clone, PartialEq)] +pub struct Args { + pub(crate) program: String, // argv[0] + pub(crate) values: Vec, // positional args + pub(crate) flags: HashMap, // --flag, -f + pub(crate) options: HashMap, // --key=value, --key value +} + +impl Args { + pub fn empty() -> Self { + Args { + program: String::new(), + values: Vec::new(), + flags: HashMap::new(), + options: HashMap::new(), + } + } + pub fn parse(args: Vec) -> Self { + let mut parsed = Args::empty(); + if args.is_empty() { + return parsed; + } + parsed.program = args[0].clone(); + let mut i = 1; + while i < args.len() { + let arg = &args[i]; + if arg.starts_with("--") { + if let Some(eq_pos) = arg.find('=') { + let key = arg[2..eq_pos].to_string(); + let value = arg[eq_pos + 1..].to_string(); + parsed.options.insert(key, value); + } else { + let key = arg[2..].to_string(); + if i + 1 < args.len() && !args[i + 1].starts_with('-') { + parsed.options.insert(key, args[i + 1].clone()); + i += 1; + } else { + parsed.flags.insert(key, true); + } + } + } else if arg.starts_with('-') && arg.len() > 1 { + for ch in arg[1..].chars() { + parsed.flags.insert(ch.to_string(), true); + } + } else { + parsed.values.push(arg.clone()); + } + i += 1; } + parsed } } #[derive(Error, Debug, Clone, PartialEq)] pub enum RuntimeError { - #[error("Undefined variable: {0}")] - UndefinedVariable(String), - #[error("Invalid access: {0}")] - InvalidAccess(String), #[error("Type error: {0}")] - TypeError(String), + Type(String), #[error("Division by zero")] DivisionByZero, - #[error("No match found for value")] - MatchError, -} - -// Internal enum to handle control flow + values -enum StatementResult { - Normal(Value), + #[error("Return: {0:?}")] Return(Value), + #[error("Break")] Break, + #[error("Continue")] Continue, } -pub struct Interpreter; +pub struct Interpreter { + // Interpreter state visible to built-ins + pub(crate) env: Environment, + pub(crate) files: Vec>, // fd table + pub(crate) next_fd: usize, // next available fd + pub(crate) args: Args, // parsed cmdline +} impl Interpreter { pub fn new() -> Self { - Interpreter + let mut interp = Interpreter { + env: Environment::new(), + files: vec![None, None, None], // 0,1,2 for stdin, stdout, stderr + next_fd: 3, + args: Args::empty(), + }; + interp.inject_builtins(); + interp } - pub fn interpret( - &self, - program: &Program, - env: Rc>, - ) -> Result, RuntimeError> { - let mut last_val = Value::Unit; + pub fn new_with_args(args: Vec) -> Self { + let mut interp = Self::new(); + interp.args = Args::parse(args); + interp.inject_globals(); + interp + } - for statement in &program.statements { - match self.evaluate_statement(statement, &env)? { - StatementResult::Return(val) => return Ok(Some(val)), - StatementResult::Normal(val) => last_val = val, - StatementResult::Break | StatementResult::Continue => { - // Top-level break/continue is usually invalid, but we'll ignore or error. - // For now, treating as normal no-op. - } - } + fn inject_builtins(&mut self) { + // Inject built-in functions as special function values + let builtins = vec!["print", "eprint", "open", "input", "Map"]; + for name in builtins { + self.env.define( + name.to_string(), + Value::Function { + name: Some(name.to_string()), + params: vec![], + body: Block { + statements: vec![], + final_expression: None, + span: Span { start: 0, end: 0 }, + }, + env: Environment::new(), + }, + ); } + } - // If the last statement produced a value (e.g. `5;`), we return it. - // This supports the test cases expecting implicit returns from scripts. - if matches!(last_val, Value::Unit) { - Ok(None) - } else { - Ok(Some(last_val)) + fn inject_globals(&mut self) { + self.env + .define("args".to_string(), Value::Args(self.args.clone())); + } + + pub fn interpret(&mut self, program: &Program) -> Result, RuntimeError> { + let mut last_val = None; + for statement in &program.statements { + last_val = Some(self.eval_top_statement(statement)?); } + Ok(last_val) } - fn evaluate_statement( - &self, - statement: &Statement, - env: &Rc>, - ) -> Result { + fn eval_top_statement(&mut self, statement: &TopStatement) -> Result { match statement { - Statement::Expression(expr) => self.evaluate_expression(expr, env), - Statement::Assignment { name, value } => { - let res = self.evaluate_expression(value, env)?; - let val = match res { - StatementResult::Normal(v) => v, - _ => return Ok(res), - }; - env.borrow_mut().set(name.clone(), val); - Ok(StatementResult::Normal(Value::Unit)) - } - Statement::VarDecl { name, value, .. } => { - let initial_value = match value { - Some(expr) => { - let res = self.evaluate_expression(expr, env)?; - match res { - StatementResult::Normal(v) => v, - _ => return Ok(res), + TopStatement::Expression(expr_stmt) => self.eval_expr(&expr_stmt.expression), + TopStatement::LetStmt(let_stmt) => self.eval_let_statement(let_stmt), + TopStatement::TypeDecl(type_decl) => { + if let TypeConstructor::Sum(sum_ctor) = &type_decl.constructor { + for variant in &sum_ctor.variants { + if variant.ty.is_some() { + // Variant with data - create a constructor function + let variant_name = variant.name.clone(); + let constructor = Value::Function { + name: Some(format!("{}Constructor", variant_name)), + params: vec![Parameter { + name: "value".to_string(), + ty: Type::Primary(TypePrimary::Named( + "any".to_string(), + variant.span, + )), + span: variant.span, + }], + body: Block { + statements: vec![], + final_expression: Some(Box::new(Expression::Primary( + PrimaryExpression::Identifier( + "value".to_string(), + variant.span, + ), + ))), + span: variant.span, + }, + env: self.env.clone(), + }; + self.env.define(variant_name.clone(), constructor); + } else { + // Unit variant - just a marker + self.env.define( + variant.name.clone(), + Value::Variant { + name: variant.name.clone(), + data: None, + }, + ); } } - None => Value::Null, + } + Ok(Value::Unit) + } + } + } + + fn eval_let_statement(&mut self, let_stmt: &LetStatement) -> Result { + match let_stmt { + LetStatement::Variable(var_binding) => { + let value = self.eval_expr(&var_binding.value)?; + // Check if variable exists first + if self.env.get(&var_binding.name).is_some() { + // Update existing variable + self.env.set(&var_binding.name, value); + } else { + // Create new variable + self.env.define(var_binding.name.clone(), value); + } + Ok(Value::Unit) + } + LetStatement::Function(func_binding) => { + let func_value = Value::Function { + name: Some(func_binding.name.clone()), + params: func_binding.params.clone(), + body: func_binding.body.clone(), + env: self.env.clone(), }; - env.borrow_mut().define(name.clone(), initial_value); - Ok(StatementResult::Normal(Value::Unit)) + self.env.define(func_binding.name.clone(), func_value); + Ok(Value::Unit) } - Statement::FunctionDef(def) => { - let closure = Closure { - definition: Rc::new(def.clone()), - env: Rc::clone(env), + } + } + + fn eval_statement(&mut self, stmt: &Statement) -> Result { + match stmt { + Statement::Let(let_stmt) => self.eval_let_statement(let_stmt), + Statement::Expression(expr_stmt) => self.eval_expr(&expr_stmt.expression), + Statement::Return(expr_opt, _) => { + let value = if let Some(expr) = expr_opt { + self.eval_expr(expr)? + } else { + Value::Unit }; - env.borrow_mut() - .define(def.name.clone(), Value::Closure(closure)); - Ok(StatementResult::Normal(Value::Unit)) - } - Statement::StructDecl(decl) => { - let struct_val = Value::Struct(Struct { - name: decl.name.clone(), - fields: decl.fields.iter().map(|(n, _)| n.clone()).collect(), - }); - env.borrow_mut().set(decl.name.clone(), struct_val); - Ok(StatementResult::Normal(Value::Unit)) + Err(RuntimeError::Return(value)) } - Statement::EnumDecl(decl) => { - let enum_val = Value::Enum(Enum { - name: decl.name.clone(), - variants: decl.variants.iter().map(|v| v.name.clone()).collect(), - }); - env.borrow_mut().set(decl.name.clone(), enum_val); - Ok(StatementResult::Normal(Value::Unit)) - } - Statement::While { condition, body } => { - let mut last_loop_val = Value::Unit; - while { - let cond_res = self.evaluate_expression(condition, env)?; - let cond_val = match cond_res { - StatementResult::Normal(v) => v, - _ => return Ok(cond_res), - }; - self.is_truthy(&cond_val) - } { - match self.execute_block(body, env)? { - StatementResult::Return(v) => return Ok(StatementResult::Return(v)), - StatementResult::Break => break, - StatementResult::Continue => continue, - StatementResult::Normal(v) => last_loop_val = v, + Statement::Break(_) => Err(RuntimeError::Break), + Statement::Continue(_) => Err(RuntimeError::Continue), + } + } + + pub fn eval_expr(&mut self, expr: &Expression) -> Result { + match expr { + Expression::Primary(primary) => self.eval_primary(primary), + Expression::Binary(binary_expr) => { + // Handle assignment operators + if matches!( + binary_expr.operator, + BinaryOperator::Assign + | BinaryOperator::AddAssign + | BinaryOperator::SubtractAssign + | BinaryOperator::MultiplyAssign + | BinaryOperator::DivideAssign + | BinaryOperator::ModuloAssign + ) { + if let Expression::Primary(PrimaryExpression::Identifier(name, _)) = + &*binary_expr.left + { + // Evaluate and set + if binary_expr.operator == BinaryOperator::Assign { + let value = self.eval_expr(&binary_expr.right)?; + self.env.set(name, value); + return Ok(Value::Unit); + } + + let current_val = self + .env + .get(name) + .ok_or(RuntimeError::Type(format!("Undefined variable: {}", name)))?; + let right_val = self.eval_expr(&binary_expr.right)?; + + let base_op = match binary_expr.operator { + BinaryOperator::AddAssign => BinaryOperator::Add, + BinaryOperator::SubtractAssign => BinaryOperator::Subtract, + BinaryOperator::MultiplyAssign => BinaryOperator::Multiply, + BinaryOperator::DivideAssign => BinaryOperator::Divide, + BinaryOperator::ModuloAssign => BinaryOperator::Modulo, + _ => unreachable!(), + }; + + let new_val = self.apply_binary_op(current_val, base_op, right_val)?; + self.env.set(name, new_val); + return Ok(Value::Unit); } - } - Ok(StatementResult::Normal(last_loop_val)) - } - Statement::For { - iterator, - iterable, - body, - } => { - let iter_res = self.evaluate_expression(iterable, env)?; - let iter_val = match iter_res { - StatementResult::Normal(v) => v, - _ => return Ok(iter_res), - }; - let mut last_loop_val = Value::Unit; - - if let Value::List(elements) = iter_val { - for element in elements { - let loop_env = - Rc::new(RefCell::new(Environment::new_enclosed(Rc::clone(env)))); - loop_env.borrow_mut().define(iterator.clone(), element); - - match self.execute_block_with_env(body, &loop_env)? { - StatementResult::Return(v) => return Ok(StatementResult::Return(v)), - StatementResult::Break => break, - StatementResult::Continue => continue, - StatementResult::Normal(v) => last_loop_val = v, + + // Handle field assignment (record.field = value) + if let Expression::Postfix(postfix_expr) = &*binary_expr.left { + if let Expression::Primary(PrimaryExpression::Identifier(var_name, _)) = + &*postfix_expr.primary + { + // Handle nested list/field assignment + if !postfix_expr.operators.is_empty() { + let value = self.eval_expr(&binary_expr.right)?; + let mut root = self.env.get(var_name).ok_or(RuntimeError::Type( + format!("Undefined variable: {}", var_name), + ))?; + + root = + self.update_nested_value(root, &postfix_expr.operators, value)?; + self.env.set(var_name, root); + return Ok(Value::Unit); + } } } - Ok(StatementResult::Normal(last_loop_val)) - } else { - Err(RuntimeError::TypeError("For loop expects a list".into())) } + + let left = self.eval_expr(&binary_expr.left)?; + let right = self.eval_expr(&binary_expr.right)?; + self.apply_binary_op(left, binary_expr.operator, right) } - Statement::Return(expr) => { - let value = match expr { - Some(e) => { - let res = self.evaluate_expression(e, env)?; - match res { - StatementResult::Normal(v) => v, - _ => return Ok(res), + Expression::Unary(unary_expr) => { + let right = self.eval_expr(&unary_expr.right)?; + self.apply_unary_op(unary_expr.operator, right) + } + Expression::If(if_expr) => self.eval_if_expression(if_expr), + Expression::While(while_expr) => self.eval_while_expression(while_expr), + Expression::For(for_expr) => self.eval_for_expression(for_expr), + Expression::Match(match_expr) => self.eval_match_expression(match_expr), + Expression::Lambda(lambda_expr) => self.eval_lambda_expression(lambda_expr), + Expression::Block(block) => self.eval_block(block), + Expression::Postfix(postfix_expr) => self.eval_postfix_expression(postfix_expr), + Expression::Range(range_expr) => self.eval_range_expression(range_expr), + } + } + + fn update_nested_value( + &mut self, + current: Value, + operators: &[PostfixOperator], + new_value: Value, + ) -> Result { + if operators.is_empty() { + return Ok(new_value); + } + + match &operators[0] { + PostfixOperator::ListAccess { index, .. } => { + if let Value::List(mut elements) = current { + let idx_val = self.eval_expr(index)?; + if let Value::Integer(idx) = idx_val { + if idx < 0 || idx as usize >= elements.len() { + return Err(RuntimeError::Type(format!("Index {} out of bounds", idx))); } + + let idx = idx as usize; + // Recursively update nested value + elements[idx] = self.update_nested_value( + elements[idx].clone(), + &operators[1..], + new_value, + )?; + + return Ok(Value::List(elements)); } - None => Value::Unit, - }; - Ok(StatementResult::Return(value)) - } - Statement::PropertyAssignment { - object, - property, - value, - } => { - let object_name = if let Expression::Identifier(name) = object { - name + Err(RuntimeError::Type("List index must be integer".into())) } else { - return Err(RuntimeError::TypeError( - "Cannot assign to non-variable".into(), - )); - }; - let res = self.evaluate_expression(value, env)?; - let new_value = match res { - StatementResult::Normal(v) => v, - _ => return Ok(res), - }; - let object_val = env - .borrow() - .get(object_name) - .ok_or(RuntimeError::UndefinedVariable(object_name.clone()))?; - - if let Value::StructInstance(mut instance) = object_val { - if instance.fields.contains_key(property) { - instance.fields.insert(property.clone(), new_value); - env.borrow_mut() - .set(object_name.clone(), Value::StructInstance(instance)); - Ok(StatementResult::Normal(Value::Unit)) - } else { - Err(RuntimeError::InvalidAccess(format!( - "Property {} not found", - property - ))) - } + Err(RuntimeError::Type("Cannot index non-list value".into())) + } + } + PostfixOperator::FieldAccess { name, .. } => { + if let Value::Record(mut fields) = current { + // Recursively update nested value + let old_value = fields.get(name).cloned().unwrap_or(Value::Unit); + fields.insert( + name.clone(), + self.update_nested_value(old_value, &operators[1..], new_value)?, + ); + Ok(Value::Record(fields)) } else { - Err(RuntimeError::TypeError( - "Target is not a struct instance".into(), + Err(RuntimeError::Type( + "Cannot access field on non-record".into(), )) } } - Statement::ArrayAssignment { - array, - index, - value, - } => { - let array_expr = match array { - Expression::Identifier(name) => name, - _ => { - return Err(RuntimeError::TypeError( - "Array assignment only works on variables currently".into(), - )); - } - }; + _ => Err(RuntimeError::Type("Unsupported nested assignment".into())), + } + } - let array_val = env - .borrow() - .get(array_expr) - .ok_or(RuntimeError::UndefinedVariable(array_expr.clone()))?; - - let idx_res = self.evaluate_expression(index, env)?; - let idx_val = match idx_res { - StatementResult::Normal(v) => v, - _ => return Ok(idx_res), - }; - - let val_res = self.evaluate_expression(value, env)?; - let new_val = match val_res { - StatementResult::Normal(v) => v, - _ => return Ok(val_res), - }; + fn eval_range_expression( + &mut self, + range_expr: &RangeExpression, + ) -> Result { + let start_val = self.eval_expr(&range_expr.start)?; + let end_val = self.eval_expr(&range_expr.end)?; - if let (Value::List(mut list), Value::Integer(idx)) = (array_val, idx_val) { - let i = idx as usize; - if i < list.len() { - list[i] = new_val; - env.borrow_mut().set(array_expr.clone(), Value::List(list)); - Ok(StatementResult::Normal(Value::Unit)) - } else { - Err(RuntimeError::InvalidAccess("Index out of bounds".into())) - } - } else { - Err(RuntimeError::TypeError("Invalid array assignment".into())) + match (start_val, end_val) { + (Value::Integer(start), Value::Integer(end)) => Ok(Value::Range { + start, + end, + inclusive: range_expr.inclusive, + }), + (Value::Integer(_), _) => Err(RuntimeError::Type( + "Range end bound must be an integer".to_string(), + )), + (_, Value::Integer(_)) => Err(RuntimeError::Type( + "Range start bound must be an integer".to_string(), + )), + _ => Err(RuntimeError::Type( + "Range bounds must be integers".to_string(), + )), + } + } + + fn eval_primary(&mut self, primary: &PrimaryExpression) -> Result { + match primary { + PrimaryExpression::Literal(literal, _) => self.eval_literal(literal), + PrimaryExpression::Identifier(name, _) => self + .env + .get(name) + .ok_or(RuntimeError::Type(format!("Undefined variable: {}", name))), + PrimaryExpression::Parenthesized(expr, _) => self.eval_expr(expr), + PrimaryExpression::List(list_literal) => { + let mut elements = Vec::new(); + for elem_expr in &list_literal.elements { + elements.push(self.eval_expr(elem_expr)?); } + Ok(Value::List(elements)) } - Statement::Break => Ok(StatementResult::Break), - Statement::Continue => Ok(StatementResult::Continue), + PrimaryExpression::Record(record_literal) => self.eval_record_literal(record_literal), + PrimaryExpression::This(_) => self.env.get("this").ok_or(RuntimeError::Type( + "Cannot use 'this' outside of a method".to_string(), + )), } } - fn execute_block( - &self, - statements: &[Statement], - env: &Rc>, - ) -> Result { - let block_env = Rc::new(RefCell::new(Environment::new_enclosed(Rc::clone(env)))); - self.execute_block_with_env(statements, &block_env) + fn eval_if_expression(&mut self, if_expr: &IfExpression) -> Result { + let condition = self.eval_expr(&if_expr.condition)?; + if self.is_truthy(&condition) { + // Scope handling is now inside eval_block + self.eval_block(&if_expr.then_branch) + } else if let Some(else_branch) = &if_expr.else_branch { + self.eval_expr(else_branch) + } else { + Ok(Value::Unit) + } } - fn execute_block_with_env( - &self, - statements: &[Statement], - env: &Rc>, - ) -> Result { - let mut last_val = Value::Unit; - for stmt in statements { - match self.evaluate_statement(stmt, env)? { - StatementResult::Return(v) => return Ok(StatementResult::Return(v)), - StatementResult::Break => return Ok(StatementResult::Break), - StatementResult::Continue => return Ok(StatementResult::Continue), - StatementResult::Normal(v) => last_val = v, + fn eval_while_expression( + &mut self, + while_expr: &WhileExpression, + ) -> Result { + loop { + let condition = self.eval_expr(&while_expr.condition)?; + if !self.is_truthy(&condition) { + break; + } + // eval_block manages its own scope + match self.eval_block(&while_expr.body) { + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + Ok(_) => {} } } - Ok(StatementResult::Normal(last_val)) + Ok(Value::Unit) } - fn evaluate_expression( - &self, - expr: &Expression, - env: &Rc>, - ) -> Result { - match expr { - Expression::Literal(literal) => Ok(StatementResult::Normal(self.evaluate_literal(literal))), - Expression::Identifier(name) => env - .borrow() - .get(name) - .map(|v| StatementResult::Normal(v)) - .ok_or(RuntimeError::UndefinedVariable(name.clone())), - Expression::Binary { left, op, right } => { - let left_res = self.evaluate_expression(left, env)?; - let left_val = match left_res { - StatementResult::Normal(v) => v, - // Propagate control flow signals - _ => return Ok(left_res), - }; + fn eval_for_expression(&mut self, for_expr: &ForExpression) -> Result { + let iterable = self.eval_expr(&for_expr.iterable)?; + match iterable { + Value::Range { + start, + end, + inclusive, + } => { + let actual_end = if inclusive { end + 1 } else { end }; - match op { - Operator::Or => { - if self.is_truthy(&left_val) { - return Ok(StatementResult::Normal(Value::Boolean(true))); - } - let right_res = self.evaluate_expression(right, env)?; - return Ok(StatementResult::Normal(Value::Boolean( - self.is_truthy(&match right_res { - StatementResult::Normal(v) => v, - _ => return Ok(right_res), - }), - ))); + for i in start..actual_end { + // For loop binds variable in current scope (or we could make a new scope) + // Existing logic bound in current. To be safe/clean for loops, + // we usually want a scope per iteration, or at least a scope for the loop. + // But eval_block creates a scope. + // So we bind in the *outer* scope (surrounding the block). + self.bind_pattern(&for_expr.pattern, Value::Integer(i))?; + match self.eval_block(&for_expr.body) { + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + Ok(_) => {} } - Operator::And => { - if !self.is_truthy(&left_val) { - return Ok(StatementResult::Normal(Value::Boolean(false))); - } - let right_res = self.evaluate_expression(right, env)?; - return Ok(StatementResult::Normal(Value::Boolean( - self.is_truthy(&match right_res { - StatementResult::Normal(v) => v, - _ => return Ok(right_res), - }), - ))); + } + Ok(Value::Unit) + } + Value::List(elements) => { + for element in elements { + self.bind_pattern(&for_expr.pattern, element)?; + match self.eval_block(&for_expr.body) { + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + Ok(_) => {} } - _ => {} } - let right_res = self.evaluate_expression(right, env)?; - let right_val = match right_res { - StatementResult::Normal(v) => v, - _ => return Ok(right_res), - }; - let result = self.apply_binary_op(left_val, *op, right_val)?; - Ok(StatementResult::Normal(result)) - } - Expression::Unary { op, right } => { - let right_res = self.evaluate_expression(right, env)?; - let val = match right_res { - StatementResult::Normal(v) => v, - _ => return Ok(right_res), - }; - let result = match op { - Operator::Not => Value::Boolean(!self.is_truthy(&val)), - Operator::Subtract => match val { - Value::Integer(i) => Value::Integer(-i), - Value::Float(f) => Value::Float(-f), - _ => return Err(RuntimeError::TypeError("Negation requires number".into())), - }, - _ => return Err(RuntimeError::TypeError("Invalid unary operator".into())), - }; - Ok(StatementResult::Normal(result)) + Ok(Value::Unit) } - Expression::FunctionCall { callee, args } => { - let func_res = self.evaluate_expression(callee, env)?; - let func = match func_res { - StatementResult::Normal(v) => v, - _ => return Ok(func_res), - }; + _ => Err(RuntimeError::Type( + "For loop requires an iterable value".to_string(), + )), + } + } - let mut arg_vals = Vec::new(); - for arg in args { - let arg_res = self.evaluate_expression(arg, env)?; - let arg_val = match arg_res { - StatementResult::Normal(v) => v, - _ => return Ok(arg_res), - }; - arg_vals.push(arg_val); - } + fn eval_match_expression( + &mut self, + match_expr: &MatchExpression, + ) -> Result { + let value = self.eval_expr(&match_expr.value)?; - let result = match func { - Value::Closure(closure) => self.call_function(closure, arg_vals)?, - Value::Function(def) => { - let closure = Closure { - definition: Rc::new(def), - env: Rc::new(RefCell::new(Environment::new())), - }; - self.call_function(closure, arg_vals)? - } - Value::EnumVariant(ev) => { - // This is an enum variant instantiation - Value::EnumVariant(EnumVariant { - enum_name: ev.enum_name, - variant_name: ev.variant_name, - values: arg_vals, - }) + for arm in &match_expr.arms { + if self.pattern_matches(&arm.pattern, &value)? { + // Match arm creates a scope + let previous = self.env.clone(); + self.env = self.env.enclose(); + + self.bind_pattern(&arm.pattern, value.clone())?; + + let result = match &arm.body { + ExpressionOrBlock::Expression(expr) => self.eval_expr(expr), + ExpressionOrBlock::Block(block) => { + // eval_block creates ANOTHER scope. That's fine. + // But we need to use a helper that DOESN'T create a scope + // if we want the match bindings to be visible in the block without a double-layer. + // Actually, double layer is fine. + self.eval_block(block) } - _ => return Err(RuntimeError::TypeError("Not a function".into())), }; - Ok(StatementResult::Normal(result)) + + self.env = previous; + return result; } - Expression::Lambda { args, body } => { - let func_def = FunctionDef { - name: gensym("lambda"), - args: args.clone(), - body: vec![Statement::Return(Some(*body.clone()))], - }; - Ok(StatementResult::Normal(Value::Closure(Closure { - definition: Rc::new(func_def), - env: Rc::clone(env), - }))) - } - Expression::List(elements) => { - let mut vals = Vec::new(); - for el in elements { - let el_res = self.evaluate_expression(el, env)?; - let el_val = match el_res { - StatementResult::Normal(v) => v, - _ => return Ok(el_res), - }; - vals.push(el_val); - } - Ok(StatementResult::Normal(Value::List(vals))) - } - Expression::StructInstantiation { name, fields } => { - let mut field_vals = HashMap::new(); - for (k, v_expr) in fields { - let v_res = self.evaluate_expression(v_expr, env)?; - let v = match v_res { - StatementResult::Normal(val) => val, - _ => return Ok(v_res), - }; - field_vals.insert(k.clone(), v); - } - Ok(StatementResult::Normal(Value::StructInstance( - StructInstance { - name: name.clone(), - fields: field_vals, - }, - ))) + } + + Err(RuntimeError::Type( + "No matching pattern in match expression".to_string(), + )) + } + + fn eval_lambda_expression( + &mut self, + lambda_expr: &LambdaExpression, + ) -> Result { + let body = match &lambda_expr.body { + ExpressionOrBlock::Block(block) => block.clone(), + ExpressionOrBlock::Expression(expr) => Block { + statements: vec![], + final_expression: Some(expr.clone()), + span: expr.span(), + }, + }; + + Ok(Value::Function { + name: None, + params: lambda_expr.params.clone(), + body, + env: self.env.clone(), + }) + } + + fn eval_block(&mut self, block: &Block) -> Result { + let previous = self.env.clone(); + self.env = self.env.enclose(); + + // Use a closure to easily handle environment restoration + let result = (|| { + for stmt in &block.statements { + self.eval_statement(stmt)?; } - Expression::Get { object, name } => { - let obj_res = self.evaluate_expression(object, env)?; - let obj = match obj_res { - StatementResult::Normal(v) => v, - _ => return Ok(obj_res), - }; - if let Value::StructInstance(inst) = obj { - inst.fields - .get(name) - .cloned() - .map(|v| StatementResult::Normal(v)) - .ok_or(RuntimeError::InvalidAccess(format!("Field {}", name))) - } else { - Err(RuntimeError::TypeError("Not a struct".into())) - } + + if let Some(final_expr) = &block.final_expression { + self.eval_expr(final_expr) + } else { + Ok(Value::Unit) } - Expression::ArrayAccess { array, index } => { - let arr_res = self.evaluate_expression(array, env)?; - let arr = match arr_res { - StatementResult::Normal(v) => v, - _ => return Ok(arr_res), - }; - let idx_res = self.evaluate_expression(index, env)?; - let idx = match idx_res { - StatementResult::Normal(v) => v, - _ => return Ok(idx_res), - }; + })(); - if let (Value::List(list), Value::Integer(i)) = (arr, idx) { - list.get(i as usize) - .cloned() - .map(|v| StatementResult::Normal(v)) - .ok_or(RuntimeError::InvalidAccess("Index bounds".into())) - } else { - Err(RuntimeError::TypeError("Invalid array access".into())) + self.env = previous; + result + } + + fn eval_postfix_expression( + &mut self, + postfix_expr: &PostfixExpression, + ) -> Result { + let mut value = self.eval_expr(&postfix_expr.primary)?; + + // Track if the primary is a simple identifier for mutation tracking + let root_var_name = if let Expression::Primary(PrimaryExpression::Identifier(name, _)) = + &*postfix_expr.primary + { + Some(name.clone()) + } else { + None + }; + + for op in postfix_expr.operators.iter() { + // Pass var_name to ALL operations for chaining support + let var_name_for_mutation = root_var_name.as_deref(); + + value = match op { + PostfixOperator::Call { args, .. } => { + self.eval_function_call(value, args, var_name_for_mutation)? } - } - Expression::If { - condition, - then_branch, - else_branch, - } => { - let cond_res = self.evaluate_expression(condition, env)?; - let cond = match cond_res { - StatementResult::Normal(v) => v, - _ => return Ok(cond_res), - }; + PostfixOperator::FieldAccess { name, .. } => self.eval_field_access(value, name)?, + PostfixOperator::ListAccess { index, .. } => self.eval_list_access(value, index)?, + PostfixOperator::TypePath { .. } => unimplemented!(), + }; + } - if self.is_truthy(&cond) { - self.execute_block(then_branch, env) - } else if let Some(else_stmt) = else_branch { - self.execute_block(else_stmt, env) - } else { - Ok(StatementResult::Normal(Value::Unit)) + Ok(value) + } + + fn eval_function_call( + &mut self, + func_value: Value, + args: &[Expression], + var_name: Option<&str>, + ) -> Result { + // Evaluate arguments from AST to Values + let mut arg_values = Vec::new(); + for arg in args { + arg_values.push(self.eval_expr(arg)?); + } + + if let Value::Function { + name: Some(name), .. + } = &func_value + { + match name.as_str() { + "print" => { + if arg_values.len() != 1 { + return Err(RuntimeError::Type("print expects 1 argument".into())); + } + println!("{}", self.value_to_display_string(&arg_values[0])); + return Ok(Value::Unit); } - } - Expression::EnumVariant { - enum_name, - variant_name, - } => { - // Just return the value directly, assumes checks pass or done loosely - Ok(StatementResult::Normal(Value::EnumVariant(EnumVariant { - enum_name: enum_name.clone(), - variant_name: variant_name.clone(), - values: vec![], - }))) - } - Expression::Path { parts } => { - // Quick path implementation - if parts.len() == 2 { - Ok(StatementResult::Normal(Value::EnumVariant(EnumVariant { - enum_name: parts[0].clone(), - variant_name: parts[1].clone(), - values: vec![], - }))) - } else { - Err(RuntimeError::TypeError("Invalid path".into())) + "eprint" => { + if arg_values.len() != 1 { + return Err(RuntimeError::Type("eprint expects 1 argument".into())); + } + eprintln!("{}", self.value_to_display_string(&arg_values[0])); + return Ok(Value::Unit); } - } - Expression::Block(statements) => self.execute_block(&statements, env), - Expression::Match { value, arms } => { - let val_res = self.evaluate_expression(value, env)?; - let val = match val_res { - StatementResult::Normal(v) => v, - _ => return Ok(val_res), - }; - for arm in arms { - let match_env = - Rc::new(RefCell::new(Environment::new_enclosed(Rc::clone(env)))); - if self.match_pattern(&val, &arm.pattern, &match_env) { - return self.evaluate_expression(&arm.body, &match_env); + "open" => { + if arg_values.len() != 2 { + return Err(RuntimeError::Type("open expects 2 arguments".into())); + } + if let (Value::String(p), Value::String(m)) = (&arg_values[0], &arg_values[1]) { + return self.open_file(p.clone(), m.clone()); + } + return Err(RuntimeError::Type("open arguments must be strings".into())); + } + "input" => { + use std::io::{self, Write}; + if arg_values.len() > 1 { + return Err(RuntimeError::Type("input expects 0 or 1 argument".into())); + } + if arg_values.len() == 1 { + print!("{}", self.value_to_display_string(&arg_values[0])); + io::stdout().flush().ok(); } + let mut line = String::new(); + io::stdin() + .read_line(&mut line) + .map_err(|_| RuntimeError::Type("Failed to read from stdin".into()))?; + return Ok(Value::String(line.trim_end_matches('\n').to_string())); } - Err(RuntimeError::MatchError) + "Map" => { + if !arg_values.is_empty() { + return Err(RuntimeError::Type("Map() takes no arguments".into())); + } + return Ok(Value::Map(HashMap::new())); + } + _ => {} // Continue to normal call } } + + if let Value::BuiltInMethod { receiver, method } = func_value { + // For methods, we need to pass back to builtins module + // But wait, eval_builtin_method expects AST expressions in the old code? + return eval_method(self, *receiver, &method, arg_values, var_name); + } + + // It's a standard function call + self.eval_function_call_value(func_value, &arg_values) } - fn call_function(&self, closure: Closure, args: Vec) -> Result { - if args.len() != closure.definition.args.len() { - return Err(RuntimeError::TypeError("Arg count mismatch".into())); + // Evaluate a function call + // public for use in built-in methods (.map, .filter, etc.) which need to call user functions + pub(crate) fn eval_function_call_value( + &mut self, + func_value: Value, + arg_values: &[Value], + ) -> Result { + if let Value::Function { + name, + params, + body, + env, + } = func_value + { + if let Some(func_name) = &name { + if func_name.ends_with("Constructor") { + let variant_name = func_name.trim_end_matches("Constructor"); + // Constructors for variants with data always take 1 argument + if arg_values.len() == 1 { + return Ok(Value::Variant { + name: variant_name.to_string(), + data: Some(Box::new(arg_values[0].clone())), + }); + } + } + } + if params.len() != arg_values.len() { + return Err(RuntimeError::Type(format!( + "Function expects {} arguments, got {}", + params.len(), + arg_values.len() + ))); + } + + let mut call_env = env.enclose(); + + if let Some(func_name) = &name { + call_env.define( + func_name.clone(), + Value::Function { + name: name.clone(), + params: params.clone(), + body: body.clone(), + env: env.clone(), + }, + ); + } + + for (param, arg_value) in params.iter().zip(arg_values.iter()) { + call_env.define(param.name.clone(), arg_value.clone()); + } + + let previous = self.env.clone(); + self.env = call_env; + + let result = match self.eval_block(&body) { + Ok(val) => Ok(val), + Err(RuntimeError::Return(val)) => Ok(val), + Err(e) => Err(e), + }; + + self.env = previous; + result + } else { + Err(RuntimeError::Type("Cannot call non-function value".into())) } - let call_env = Rc::new(RefCell::new(Environment::new_enclosed(closure.env))); - for (name, val) in closure.definition.args.iter().zip(args) { - call_env.borrow_mut().define(name.clone(), val); + } + + fn eval_field_access(&mut self, value: Value, field_name: &str) -> Result { + // Check for built-in methods + match &value { + Value::Map(_) + if matches!( + field_name, + "insert" + | "get" + | "has" + | "contains" + | "remove" + | "delete" + | "length" + | "size" + | "is_empty" + | "clear" + | "keys" + | "values" + | "entries" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::List(_) + if matches!( + field_name, + "length" + | "push" + | "append" + | "pop" + | "remove" + | "insert" + | "reverse" + | "sort" + | "contains" + | "index_of" + | "slice" + | "join" + | "map" + | "filter" + | "first" + | "last" + | "is_empty" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::String(_) + if matches!( + field_name, + "length" + | "substring" + | "split" + | "parse_int" + | "parse_float" + | "trim" + | "trim_start" + | "trim_end" + | "contains" + | "starts_with" + | "ends_with" + | "replace" + | "to_lower" + | "to_upper" + | "char_at" + | "chars" + | "index_of" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::Integer(_) if matches!(field_name, "to_float" | "to_string" | "abs" | "pow") => { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::Float(_) + if matches!( + field_name, + "to_string" | "to_int" | "abs" | "floor" | "ceil" | "round" | "sqrt" | "pow" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::Boolean(_) if field_name == "to_string" => { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::File { .. } + if matches!( + field_name, + "read" | "read_lines" | "write" | "write_line" | "close" | "is_closed" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + Value::Args(_) + if matches!( + field_name, + "program" | "values" | "length" | "get" | "has" | "get_option" + ) => + { + return Ok(Value::BuiltInMethod { + receiver: Box::new(value), + method: field_name.to_string(), + }); + } + _ => {} } - match self.execute_block_with_env(&closure.definition.body, &call_env)? { - StatementResult::Return(val) => Ok(val), - StatementResult::Normal(val) => Ok(val), // Implicit return of last value - _ => Ok(Value::Unit), + // Handle record fields + match value { + Value::Record(fields) => { + fields + .get(field_name) + .cloned() + .ok_or(RuntimeError::Type(format!( + "Field '{}' not found", + field_name + ))) + } + _ => Err(RuntimeError::Type(format!( + "Cannot access field '{}' on non-record value {}", + field_name, + self.value_to_display_string(&value) + ))), } } - fn evaluate_literal(&self, literal: &LiteralValue) -> Value { - match literal { - LiteralValue::Integer(i) => Value::Integer(*i), - LiteralValue::Float(f) => Value::Float(*f), - LiteralValue::String(s) => Value::String(s.clone()), - LiteralValue::Boolean(b) => Value::Boolean(*b), - LiteralValue::Unit => Value::Unit, - LiteralValue::Array(elements) => { - let vals = elements - .iter() - .map(|el| self.evaluate_literal(el)) - .collect(); - Value::List(vals) + fn eval_list_access( + &mut self, + value: Value, + index_expr: &Expression, + ) -> Result { + match value { + Value::List(elements) => { + let index_value = self.eval_expr(index_expr)?; + match index_value { + Value::Integer(idx) => { + if idx < 0 { + return Err(RuntimeError::Type(format!("Index {} out of bounds", idx))); + } + let idx = idx as usize; + elements + .get(idx) + .cloned() + .ok_or(RuntimeError::Type(format!("Index {} out of bounds", idx))) + } + _ => Err(RuntimeError::Type( + "List index must be an integer".to_string(), + )), + } } + Value::String(s) => { + let index_value = self.eval_expr(index_expr)?; + match index_value { + Value::Integer(idx) => { + if idx < 0 || idx as usize >= s.len() { + return Err(RuntimeError::Type(format!("Index {} out of bounds", idx))); + } + let ch = s.chars().nth(idx as usize).unwrap(); + Ok(Value::String(ch.to_string())) + } + _ => Err(RuntimeError::Type( + "String index must be an integer".to_string(), + )), + } + } + _ => Err(RuntimeError::Type( + "Cannot index non-list value".to_string(), + )), } } - fn is_truthy(&self, value: &Value) -> bool { - match value { - Value::Boolean(b) => *b, - Value::Null => false, - Value::Unit => false, - _ => true, + fn eval_record_literal( + &mut self, + record_literal: &RecordLiteral, + ) -> Result { + let mut fields = HashMap::new(); + for field_init in &record_literal.fields { + let value = self.eval_expr(&field_init.value)?; + fields.insert(field_init.name.clone(), value); } + Ok(Value::Record(fields)) } - fn match_pattern( - &self, - value: &Value, - pattern: &Pattern, - env: &Rc>, - ) -> bool { + fn pattern_matches(&self, pattern: &Pattern, value: &Value) -> Result { match pattern { - Pattern::Wildcard => true, - Pattern::Literal(lit) => self.evaluate_literal(lit) == *value, - Pattern::Identifier(p_name) => { - if let Value::EnumVariant(v_val) = value { - // When matching an enum, an identifier pattern is treated as a variant name - p_name == &v_val.variant_name - } else { - // Otherwise, it's a variable binding - env.borrow_mut().set(p_name.clone(), value.clone()); - true + Pattern::Wildcard(_) => Ok(true), + Pattern::Identifier(name, _) => { + // Check if it's a variant + if let Value::Variant { name: v_name, data } = value { + if v_name == name && data.is_none() { + return Ok(true); + } } + // Otherwise it's a binding variable + Ok(true) + } + Pattern::Literal(lit, _) => { + let pattern_value = match lit { + LiteralValue::Integer(i) => Value::Integer(*i), + LiteralValue::Float(f) => Value::Float(*f), + LiteralValue::String(s) => Value::String(s.clone()), + LiteralValue::Boolean(b) => Value::Boolean(*b), + LiteralValue::None => Value::Unit, + }; + Ok(&pattern_value == value) } - Pattern::EnumVariant { variant, vars, .. } => { - if let Value::EnumVariant(ev) = value { - if ev.variant_name == *variant && ev.values.len() == vars.len() { - for (i, sub_pattern) in vars.iter().enumerate() { - if !self.match_pattern(&ev.values[i], sub_pattern, env) { - return false; // a sub-pattern failed to match + Pattern::Variant { name, patterns, .. } => { + if let Value::Variant { name: v_name, data } = value { + if v_name != name { + return Ok(false); + } + if let Some(inner_patterns) = patterns { + if let Some(variant_data) = data { + if inner_patterns.len() == 1 { + return self.pattern_matches(&inner_patterns[0], variant_data); } } - true // all sub-patterns matched - } else { - false + return Ok(false); } + Ok(data.is_none()) } else { - false + Ok(false) } } } } + fn bind_pattern(&mut self, pattern: &Pattern, value: Value) -> Result<(), RuntimeError> { + match pattern { + Pattern::Wildcard(_) => Ok(()), + Pattern::Identifier(name, _) => { + // Don't bind if it's a variant match + if let Value::Variant { name: v_name, data } = &value { + if v_name == name && data.is_none() { + return Ok(()); + } + } + self.env.define(name.clone(), value); + Ok(()) + } + Pattern::Literal(_, _) => Ok(()), + Pattern::Variant { patterns, .. } => { + if let Some(inner_patterns) = patterns { + if let Value::Variant { + data: Some(variant_data), + .. + } = value + { + if inner_patterns.len() == 1 { + self.bind_pattern(&inner_patterns[0], *variant_data)?; + } + } + } + Ok(()) + } + } + } + + fn apply_unary_op(&self, op: UnaryOperator, right: Value) -> Result { + match op { + UnaryOperator::Minus => match right { + Value::Integer(i) => Ok(Value::Integer(-i)), + Value::Float(f) => Ok(Value::Float(-f)), + _ => Err(RuntimeError::Type( + "Unary minus can only be applied to integers and floats".into(), + )), + }, + UnaryOperator::Not => Ok(Value::Boolean(!self.is_truthy(&right))), + UnaryOperator::Plus => match right { + Value::Integer(i) => Ok(Value::Integer(i)), + Value::Float(f) => Ok(Value::Float(f)), + _ => Err(RuntimeError::Type( + "Unary plus can only be applied to integers and floats".into(), + )), + }, + } + } + + fn is_truthy(&self, value: &Value) -> bool { + match value { + Value::Boolean(b) => *b, + Value::Unit => false, + _ => true, + } + } + fn apply_binary_op( - &self, + &mut self, left: Value, - op: Operator, + op: BinaryOperator, right: Value, ) -> Result { + if matches!(op, BinaryOperator::And | BinaryOperator::Or) { + return match (&left, &right) { + (Value::Boolean(l), Value::Boolean(r)) => match op { + BinaryOperator::And => Ok(Value::Boolean(*l && *r)), + BinaryOperator::Or => Ok(Value::Boolean(*l || *r)), + _ => unreachable!(), + }, + _ => Err(RuntimeError::Type( + "Type mismatch in binary operation".into(), + )), + }; + } + match (left, right) { - (Value::Boolean(l), Value::Boolean(r)) => match op { - Operator::Equal => Ok(Value::Boolean(l == r)), - Operator::NotEqual => Ok(Value::Boolean(l != r)), - _ => Err(RuntimeError::TypeError("Invalid boolean operator".into())), - }, (Value::Integer(l), Value::Integer(r)) => match op { - Operator::Add => Ok(Value::Integer(l + r)), - Operator::Subtract => Ok(Value::Integer(l - r)), - Operator::Multiply => Ok(Value::Integer(l * r)), - Operator::Divide => { + BinaryOperator::Add => Ok(Value::Integer(l.wrapping_add(r))), + BinaryOperator::Subtract => Ok(Value::Integer(l.wrapping_sub(r))), + BinaryOperator::Multiply => Ok(Value::Integer(l.wrapping_mul(r))), + BinaryOperator::Divide => { if r == 0 { return Err(RuntimeError::DivisionByZero); } Ok(Value::Integer(l / r)) } - Operator::Modulo => { + BinaryOperator::Modulo => { if r == 0 { return Err(RuntimeError::DivisionByZero); } - Ok(Value::Integer(l % r)) + Ok(Value::Integer(l.rem_euclid(r))) } - Operator::Equal => Ok(Value::Boolean(l == r)), - Operator::NotEqual => Ok(Value::Boolean(l != r)), - Operator::GreaterThan => Ok(Value::Boolean(l > r)), - Operator::LessThan => Ok(Value::Boolean(l < r)), - Operator::GreaterThanEqual => Ok(Value::Boolean(l >= r)), - Operator::LessThanEqual => Ok(Value::Boolean(l <= r)), - _ => Err(RuntimeError::TypeError("Invalid integer operator".into())), + BinaryOperator::Equal => Ok(Value::Boolean(l == r)), + BinaryOperator::NotEqual => Ok(Value::Boolean(l != r)), + BinaryOperator::LessThan => Ok(Value::Boolean(l < r)), + BinaryOperator::LessThanEqual => Ok(Value::Boolean(l <= r)), + BinaryOperator::GreaterThan => Ok(Value::Boolean(l > r)), + BinaryOperator::GreaterThanEqual => Ok(Value::Boolean(l >= r)), + _ => Err(RuntimeError::Type(format!( + "Invalid integer operator: {:?}", + op + ))), + }, + (Value::Float(l), Value::Float(r)) => match op { + BinaryOperator::Add => Ok(Value::Float(l + r)), + BinaryOperator::Subtract => Ok(Value::Float(l - r)), + BinaryOperator::Multiply => Ok(Value::Float(l * r)), + BinaryOperator::Divide => Ok(Value::Float(l / r)), + BinaryOperator::Modulo => Ok(Value::Float(l % r)), + BinaryOperator::Equal => Ok(Value::Boolean((l - r).abs() < f64::EPSILON)), + BinaryOperator::NotEqual => Ok(Value::Boolean((l - r).abs() >= f64::EPSILON)), + BinaryOperator::LessThan => Ok(Value::Boolean(l < r)), + BinaryOperator::LessThanEqual => Ok(Value::Boolean(l <= r)), + BinaryOperator::GreaterThan => Ok(Value::Boolean(l > r)), + BinaryOperator::GreaterThanEqual => Ok(Value::Boolean(l >= r)), + _ => Err(RuntimeError::Type("Invalid float operator".into())), + }, + (Value::Float(l), Value::Integer(r)) => match op { + BinaryOperator::Divide => Ok(Value::Float(l / r as f64)), + BinaryOperator::Modulo => Ok(Value::Float(l % r as f64)), + _ => Err(RuntimeError::Type("Invalid float operator".into())), }, (Value::String(l), Value::String(r)) => match op { - Operator::Add => Ok(Value::String(format!("{}{}", l, r))), - Operator::Equal => Ok(Value::Boolean(l == r)), - Operator::NotEqual => Ok(Value::Boolean(l != r)), - _ => Err(RuntimeError::TypeError("Invalid string operator".into())), + BinaryOperator::Add => Ok(Value::String(format!("{}{}", l, r))), + BinaryOperator::Equal => Ok(Value::Boolean(l == r)), + BinaryOperator::NotEqual => Ok(Value::Boolean(l != r)), + _ => Err(RuntimeError::Type("Invalid string operator".into())), }, - (Value::Float(l), Value::Float(r)) => match op { - Operator::Add => Ok(Value::Float(l + r)), - Operator::Subtract => Ok(Value::Float(l - r)), - Operator::Multiply => Ok(Value::Float(l * r)), - Operator::Divide => Ok(Value::Float(l / r)), - Operator::Equal => Ok(Value::Boolean(l == r)), - Operator::LessThan => Ok(Value::Boolean(l < r)), - _ => Err(RuntimeError::TypeError("Invalid float operator".into())), + (Value::List(l), Value::List(r)) => match op { + BinaryOperator::Add => { + let mut new_list = l.clone(); + new_list.extend(r); + Ok(Value::List(new_list)) + } + BinaryOperator::Equal => Ok(Value::Boolean(l == r)), + BinaryOperator::NotEqual => Ok(Value::Boolean(l != r)), + _ => Err(RuntimeError::Type("Invalid list operator".into())), }, - _ => Err(RuntimeError::TypeError("Type mismatch in binary op".into())), + _ => Err(RuntimeError::Type( + "Type mismatch in binary operation".into(), + )), + } + } + + fn eval_literal(&self, literal: &LiteralValue) -> Result { + match literal { + LiteralValue::Integer(i) => Ok(Value::Integer(*i)), + LiteralValue::Float(f) => Ok(Value::Float(*f)), + LiteralValue::String(s) => Ok(Value::String(s.clone())), + LiteralValue::Boolean(b) => Ok(Value::Boolean(*b)), + LiteralValue::None => Ok(Value::Unit), + } + } + + pub fn value_to_display_string(&self, value: &Value) -> String { + match value { + Value::Integer(i) => i.to_string(), + Value::Float(f) => f.to_string(), + Value::String(s) => s.clone(), + Value::Boolean(b) => b.to_string(), + Value::Unit => "()".to_string(), + Value::List(items) => { + let items_str: Vec = items + .iter() + .map(|v| self.value_to_display_string(v)) + .collect(); + format!("[{}]", items_str.join(", ")) + } + Value::Record(fields) => { + let fields_str: Vec = fields + .iter() + .map(|(k, v)| format!("{}: {}", k, self.value_to_display_string(v))) + .collect(); + format!("{{{}}}", fields_str.join(", ")) + } + Value::Function { name, .. } => { + format!( + "", + name.as_ref().unwrap_or(&"anonymous".to_string()) + ) + } + Value::File { path, .. } => format!("", path), + Value::Args { .. } => format!("CLI arguments: {:?}", self.args).to_string(), + Value::Variant { name, data } => { + if let Some(d) = data { + format!("{}({})", name, self.value_to_display_string(d)) + } else { + name.clone() + } + } + Value::Range { + start, + end, + inclusive, + } => { + if *inclusive { + format!("{}..={}", start, end) + } else { + format!("{}..{}", start, end) + } + } + Value::Map(map) => { + let entries: Vec = map + .iter() + .map(|(k, v)| { + format!( + "{}: {}", + self.value_to_display_string(&k.to_value()), + self.value_to_display_string(v) + ) + }) + .collect(); + format!("{{{}}}", entries.join(", ")) + } + _ => format!("{:?}", value), + } + } + + fn open_file(&mut self, path: String, mode_str: String) -> Result { + use std::fs::OpenOptions; + + let mode = match mode_str.as_str() { + "r" => FileMode::Read, + "w" => FileMode::Write, + "a" => FileMode::Append, + _ => { + return Err(RuntimeError::Type(format!( + "Invalid file mode: {}", + mode_str + ))); + } + }; + + let file = match mode { + FileMode::Read => OpenOptions::new().read(true).open(&path), + FileMode::Write => OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&path), + FileMode::Append => OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(&path), + }; + + match file { + Ok(f) => { + let id = self.next_fd; + self.files.push(Some(f)); + self.next_fd += 1; + Ok(Value::File { + id, + path: path.clone(), + mode, + closed: false, + }) + } + Err(e) => Err(RuntimeError::Type(format!( + "Failed to open file '{}': {}", + path, e + ))), } } } diff --git a/src/lexer.rs b/src/lexer.rs index 3c34164..14141bd 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,99 +1,104 @@ use std::fmt; +use crate::ast::Span; +use crate::diagnostics::{Diagnostic, DiagnosticKind, Reporter}; + #[derive(Debug, Clone, PartialEq)] pub enum TokenType { - Identifier, + // Single-character tokens + Semicolon, // ; + Colon, // : + Comma, // , + Dot, // . + OpenParen, // ( + CloseParen, // ) + OpenBrace, // { + CloseBrace, // } + OpenBracket, // [ + CloseBracket, // ] + Bang, // ! + + // Operators + Assign, // = + Equal, // == + NotEqual, // != + LessThan, // < + LessThanEqual, // <= + GreaterThan, // > + GreaterThanEqual, // >= + Plus, // + + Minus, // - + Star, // * + Slash, // / + Percent, // % + AmpAmp, // && + Pipe, // | + PipePipe, // || + + // Compound assignment operators + PlusEqual, // += + MinusEqual, // -= + StarEqual, // *= + SlashEqual, // /= + PercentEqual, // %= + + // Fat arrow for lambdas and match arms + Arrow, // -> + FatArrow, // => + + // Double colon for type paths + DoubleColon, // :: + + // Ranges + DotDot, // .. + DotDotEqual, // ..= + DotDotLess, // ..< + + // Literals + Identifier(String), Integer(i64), Float(f64), String(String), - Semicolon, - Assign, - AddAssign, - SubAssign, - MulAssign, - DivAssign, - ModAssign, - XorAssign, - OrAssign, - AndAssign, - NegAssign, - Colon, - ColonColon, - OpPlus, - OpMinus, - OpMult, - OpDivide, - OpFloorDiv, - OpMod, // Renamed from OpModulo to match Parser - OpXor, - OpOr, // Bitwise Or - OpAnd, // Bitwise And - OpNeg, - OpLor, // Logical || - OpLand, // Logical && - Bang, // Renamed from OpLneg (!) to match Parser - OpIncrement, - OpDecrement, - OpExponent, - Arrow, // -> - ArrowFat, // => - Equal, - NotEqual, - GreaterThan, - GreaterThanEqual, - LessThan, - LessThanEqual, - OpenParen, - CloseParen, - OpenBrace, - CloseBrace, - OpenBracket, - CloseBracket, - Comma, - Period, - Lambda, // \ - Underscore, // _ - KeywordInt, - KeywordStr, - KeywordFunc, - KeywordReturn, - KeywordStruct, - KeywordMatch, - KeywordEnum, - KeywordFor, - KeywordIn, - KeywordWhile, - KeywordContinue, - KeywordBreak, - KeywordIf, - KeywordElse, - KeywordTrue, - KeywordFalse, - KeywordUnit, - KeywordType, - Unknown, - EndOfFile, -} -impl fmt::Display for TokenType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} + // Keywords + KeywordType, // type + KeywordMut, // mut + KeywordIf, // if + KeywordElse, // else + KeywordWhile, // while + KeywordFor, // for + KeywordIn, // in + KeywordMatch, // match + KeywordTrue, // true + KeywordFalse, // false + KeywordNone, // None + KeywordThis, // this + KeywordContinue, // continue + KeywordBreak, // break + KeywordReturn, // return + KeywordUnderscore, // _ (used in patterns) -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Span { - pub lo: usize, - pub hi: usize, + // End of File + EndOfFile, } -impl Span { - pub fn new(lo: usize, hi: usize) -> Self { - Self { lo, hi } +impl TokenType { + pub fn is_identifier(&self) -> bool { + matches!(self, TokenType::Identifier(_)) } +} - pub fn len(self) -> usize { - self.hi - self.lo +impl fmt::Display for TokenType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TokenType::Identifier(s) => write!(f, "IDENTIFIER({})", s), + TokenType::Integer(i) => write!(f, "INTEGER({})", i), + TokenType::Float(fl) => write!(f, "FLOAT({})", fl), + TokenType::String(s) => write!(f, "STRING(\"{}\")", s), + TokenType::Percent => write!(f, "PERCENT"), + TokenType::PercentEqual => write!(f, "PERCENT_EQUAL"), + _ => write!(f, "{:?}", self), + } } } @@ -101,265 +106,369 @@ impl Span { pub struct Token { pub token_type: TokenType, pub lexeme: String, - pub line: usize, pub span: Span, } +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} ({}:{})", + self.token_type, self.span.start, self.span.end + ) + } +} + impl Token { - pub fn new(token_type: TokenType, lexeme: String, line: usize, span: Span) -> Self { + pub fn new(token_type: TokenType, lexeme: String, span: Span) -> Self { Token { token_type, lexeme, - line, span, } } } -pub struct Lexer { +pub struct Lexer<'a> { chars: Vec, tokens: Vec, start: usize, current: usize, line: usize, + reporter: &'a mut Reporter, } -impl Lexer { - pub fn new(source: &str) -> Self { +impl<'a> Lexer<'a> { + pub fn new(source: &str, reporter: &'a mut Reporter) -> Self { Lexer { chars: source.chars().collect(), tokens: Vec::new(), start: 0, current: 0, - line: 1, + line: 1, // Lines are 1-indexed + reporter, } } - pub fn tokenize(mut self) -> Result, String> { + pub fn tokenize(mut self) -> Result, Diagnostic> { while !self.is_at_end() { self.start = self.current; - self.scan_token()?; + self.scan_token(); } + self.tokens.push(Token::new( TokenType::EndOfFile, "".to_string(), - self.line, Span::new(self.current, self.current), )); - Ok(self.tokens) + + if self.reporter.has_errors() { + Err(self.reporter.diagnostics.remove(0)) // Return first diagnostic if any + } else { + Ok(self.tokens) + } } fn is_at_end(&self) -> bool { self.current >= self.chars.len() } - fn scan_token(&mut self) -> Result<(), String> { - let c = self.advance(); + fn advance(&mut self) -> char { + let c = self.chars[self.current]; + self.current += 1; + c + } + + fn add_token(&mut self, token_type: TokenType) { + let text: String = self.chars[self.start..self.current].iter().collect(); + let span = Span::new(self.start, self.current); + self.tokens.push(Token::new(token_type, text, span)); + } + + fn match_char(&mut self, expected: char) -> bool { + if self.is_at_end() || self.chars[self.current] != expected { + return false; + } + self.current += 1; + true + } + + fn peek(&self) -> char { + if self.is_at_end() { + return '\0'; + } + self.chars[self.current] + } + + fn peek_next(&self) -> char { + if self.current + 1 >= self.chars.len() { + return '\0'; + } + self.chars[self.current + 1] + } + + fn scan_token(&mut self) { + let c = self.peek(); match c { - ';' => self.add_token(TokenType::Semicolon), - ':' => { - if self.match_char(':') { - self.add_token(TokenType::ColonColon) + ';' => { + self.advance(); + self.add_token(TokenType::Semicolon) + } + ',' => { + self.advance(); + self.add_token(TokenType::Comma) + } + '(' => { + self.advance(); + self.add_token(TokenType::OpenParen) + } + ')' => { + self.advance(); + self.add_token(TokenType::CloseParen) + } + '{' => { + self.advance(); + self.add_token(TokenType::OpenBrace) + } + '}' => { + self.advance(); + self.add_token(TokenType::CloseBrace) + } + '[' => { + self.advance(); + self.add_token(TokenType::OpenBracket) + } + ']' => { + self.advance(); + self.add_token(TokenType::CloseBracket) + } + '.' => { + self.advance(); + if self.peek() == '.' { + self.advance(); + if self.match_char('=') { + self.add_token(TokenType::DotDotEqual) + } else if self.match_char('<') { + self.add_token(TokenType::DotDotLess); + } else { + self.add_token(TokenType::Dot); + } } else { - self.add_token(TokenType::Colon) + self.add_token(TokenType::Dot) } } - '(' => self.add_token(TokenType::OpenParen), - ')' => self.add_token(TokenType::CloseParen), - '{' => self.add_token(TokenType::OpenBrace), - '}' => self.add_token(TokenType::CloseBrace), - '[' => self.add_token(TokenType::OpenBracket), - ']' => self.add_token(TokenType::CloseBracket), - ',' => self.add_token(TokenType::Comma), - '.' => self.add_token(TokenType::Period), - '\\' => self.add_token(TokenType::Lambda), - '_' => self.add_token(TokenType::Underscore), - '=' => { + '!' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::Equal) - } else if self.match_char('>') { - self.add_token(TokenType::ArrowFat) + self.add_token(TokenType::NotEqual); } else { - self.add_token(TokenType::Assign) + self.add_token(TokenType::Bang); } } - '+' => { + '=' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::AddAssign) - } else if self.match_char('+') { - self.add_token(TokenType::OpIncrement) + self.add_token(TokenType::Equal); + } else if self.match_char('>') { + self.add_token(TokenType::FatArrow); } else { - self.add_token(TokenType::OpPlus) + self.add_token(TokenType::Assign); } } - '-' => { - if self.match_char('>') { - self.add_token(TokenType::Arrow) - } else if self.match_char('=') { - self.add_token(TokenType::SubAssign) - } else if self.match_char('-') { - self.add_token(TokenType::OpDecrement) + '<' => { + self.advance(); + if self.match_char('=') { + self.add_token(TokenType::LessThanEqual); } else { - self.add_token(TokenType::OpMinus) + self.add_token(TokenType::LessThan); } } - '*' => { + '>' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::MulAssign) - } else if self.match_char('*') { - self.add_token(TokenType::OpExponent) + self.add_token(TokenType::GreaterThanEqual); } else { - self.add_token(TokenType::OpMult) + self.add_token(TokenType::GreaterThan); } } - '/' => { + '+' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::DivAssign) - } else if self.match_char('/') { - self.add_token(TokenType::OpFloorDiv) + self.add_token(TokenType::PlusEqual); } else { - self.add_token(TokenType::OpDivide) + self.add_token(TokenType::Plus); } } - '%' => { + '-' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::ModAssign) + self.add_token(TokenType::MinusEqual); + } else if self.match_char('>') { + self.add_token(TokenType::Arrow); } else { - self.add_token(TokenType::OpMod) + self.add_token(TokenType::Minus); } } - '^' => { + '*' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::XorAssign) + self.add_token(TokenType::StarEqual); } else { - self.add_token(TokenType::OpXor) + self.add_token(TokenType::Star); } } - '|' => { - if self.match_char('=') { - self.add_token(TokenType::OrAssign) - } else if self.match_char('|') { - self.add_token(TokenType::OpLor) + '/' => { + self.advance(); + if self.match_char('/') { + while self.peek() != '\n' && !self.is_at_end() { + self.advance(); + } + } else if self.match_char('=') { + self.add_token(TokenType::SlashEqual); } else { - self.add_token(TokenType::OpOr) + self.add_token(TokenType::Slash); } } - '&' => { + '%' => { + self.advance(); if self.match_char('=') { - self.add_token(TokenType::AndAssign) - } else if self.match_char('&') { - self.add_token(TokenType::OpLand) + self.add_token(TokenType::PercentEqual); } else { - self.add_token(TokenType::OpAnd) + self.add_token(TokenType::Percent); } } - '!' => { - if self.match_char('=') { - self.add_token(TokenType::NotEqual) + '&' => { + self.advance(); + if self.match_char('&') { + self.add_token(TokenType::AmpAmp); } else { - self.add_token(TokenType::Bang) // Changed from OpLneg + self.error(self.current - 1, "Unexpected character '&'."); } } - '>' => { - if self.match_char('=') { - self.add_token(TokenType::GreaterThanEqual) + '|' => { + self.advance(); + if self.match_char('|') { + self.add_token(TokenType::PipePipe); } else { - self.add_token(TokenType::GreaterThan) + self.add_token(TokenType::Pipe); } } - '<' => { - if self.match_char('=') { - self.add_token(TokenType::LessThanEqual) + ':' => { + self.advance(); + if self.match_char(':') { + self.add_token(TokenType::DoubleColon); } else { - self.add_token(TokenType::LessThan) + self.add_token(TokenType::Colon); } } - '#' => { - while self.peek() != '\n' && !self.is_at_end() { - self.advance(); - } + // Whitespace + ' ' | '\r' | '\t' => { + self.advance(); + } // Ignore whitespace + '\n' => { + self.advance(); + self.line += 1 + } + + // Literals + '"' => self.string(), + _ if c.is_ascii_digit() => self.number(), + _ if c.is_alphabetic() || c == '_' => self.identifier(), + + _ => { + self.error(self.current, &format!("Unexpected character: {}", c)); + self.advance(); } - ' ' | '\r' | '\t' => {} - '\n' => self.line += 1, - '"' => self.string()?, - c if c.is_digit(10) => self.number(), - c if c.is_alphabetic() || c == '_' => self.identifier(), - _ => self.add_token(TokenType::Unknown), } - Ok(()) } - fn advance(&mut self) -> char { - let c = self.chars[self.current]; - self.current += 1; - c - } + fn string(&mut self) { + // consume opening quote + self.advance(); - fn add_token(&mut self, token_type: TokenType) { - let text: String = self.chars[self.start..self.current].iter().collect(); - let span = Span::new(self.start, self.current); - self.tokens - .push(Token::new(token_type, text, self.line, span)); - } + let mut value = String::new(); - fn match_char(&mut self, expected: char) -> bool { - if self.is_at_end() || self.chars[self.current] != expected { - return false; - } - self.current += 1; - true - } + while !self.is_at_end() { + let c = self.advance(); - fn peek(&self) -> char { - if self.is_at_end() { - return '\0'; - } - self.chars[self.current] - } + match c { + '"' => { + // closing quote terminates string + self.add_token(TokenType::String(value)); + return; + } - fn peek_next(&self) -> char { - if self.current + 1 >= self.chars.len() { - return '\0'; - } - self.chars[self.current + 1] - } + '\\' => { + // escaped character + if self.is_at_end() { + self.error(self.current - 1, "Unterminated string escape."); + return; + } + + let esc = self.advance(); + let decoded = match esc { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + '\\' => '\\', + '"' => '"', + '0' => '\0', + _ => { + self.error( + self.current - 1, + &format!("Invalid escape sequence: \\{}", esc), + ); + continue; + } + }; + value.push(decoded); + } - fn string(&mut self) -> Result<(), String> { - while self.peek() != '"' && !self.is_at_end() { - if self.peek() == '\n' { - self.line += 1; + '\n' => { + // raw newline inside string: treat as error + self.error( + self.current - 1, + "Unterminated string (newline inside literal).", + ); + self.line += 1; + return; + } + + ch => value.push(ch), } - self.advance(); } - if self.is_at_end() { - return Err("Unterminated string.".to_string()); - } - self.advance(); - let value = self.chars[self.start + 1..self.current - 1] - .iter() - .collect(); - self.add_token(TokenType::String(value)); - Ok(()) + + // EOF reached before closing quote + self.error(self.start, "Unterminated string."); } fn number(&mut self) { - while self.peek().is_digit(10) { + while self.peek().is_ascii_digit() { self.advance(); } + let mut is_float = false; // Look for a fractional part. - if self.peek() == '.' && self.peek_next().is_digit(10) { + if self.peek() == '.' && self.peek_next().is_ascii_digit() { + is_float = true; self.advance(); // Consume the "." - while self.peek().is_digit(10) { + while self.peek().is_ascii_digit() { self.advance(); } - let value_str: String = self.chars[self.start..self.current].iter().collect(); - let value: f64 = value_str.parse().unwrap(); - self.add_token(TokenType::Float(value)); + } + + let value_str: String = self.chars[self.start..self.current].iter().collect(); + + if is_float { + match value_str.parse::() { + Ok(value) => self.add_token(TokenType::Float(value)), + Err(_) => self.error(self.start, "Invalid float literal."), + } } else { - let value_str: String = self.chars[self.start..self.current].iter().collect(); - let value: i64 = value_str.parse().unwrap(); - self.add_token(TokenType::Integer(value)); + match value_str.parse::() { + Ok(value) => self.add_token(TokenType::Integer(value)), + Err(_) => self.error(self.start, "Invalid integer literal."), + } } } @@ -367,28 +476,38 @@ impl Lexer { while self.peek().is_alphanumeric() || self.peek() == '_' { self.advance(); } + let text: String = self.chars[self.start..self.current].iter().collect(); let token_type = match text.as_str() { - "int" | "całkowita" => TokenType::KeywordInt, - "str" | "tekst" => TokenType::KeywordStr, - "unit" | "pusty" => TokenType::KeywordUnit, - "func" | "funkcja" => TokenType::KeywordFunc, - "return" | "zwróć" => TokenType::KeywordReturn, - "struct" | "struktura" => TokenType::KeywordStruct, - "match" | "dopasuj" => TokenType::KeywordMatch, - "enum" | "wyliczenie" => TokenType::KeywordEnum, - "if" | "jeśli" => TokenType::KeywordIf, - "else" | "inaczej" => TokenType::KeywordElse, + "type" | "typ" => TokenType::KeywordType, + "mut" | "zmienna" => TokenType::KeywordMut, + "if" | "jeżeli" | "jeśli" => TokenType::KeywordIf, + "else" | "albo" | "lub" | "w_innym_razie" => TokenType::KeywordElse, + "while" | "dopóki" => TokenType::KeywordWhile, "for" | "dla" => TokenType::KeywordFor, "in" | "w" => TokenType::KeywordIn, - "while" | "dopóki" => TokenType::KeywordWhile, - "continue" | "kontynuuj" => TokenType::KeywordContinue, - "break" | "przerwij" => TokenType::KeywordBreak, + "match" | "dopasuj" => TokenType::KeywordMatch, "true" | "prawda" => TokenType::KeywordTrue, "false" | "fałsz" => TokenType::KeywordFalse, - "type" | "typ" => TokenType::KeywordType, - _ => TokenType::Identifier, + "None" | "Nic" => TokenType::KeywordNone, + "this" | "ten" | "ta" | "to" => TokenType::KeywordThis, + "continue" | "kontynuuj" | "dalej" => TokenType::KeywordContinue, + "break" | "przerwij" | "koniec" => TokenType::KeywordBreak, + "return" | "zwróć" => TokenType::KeywordReturn, + "_" => TokenType::KeywordUnderscore, // Explicit keyword for '_' pattern + _ => TokenType::Identifier(text.clone()), }; self.add_token(token_type); } + + fn error(&mut self, at: usize, message: &str) { + self.reporter.add_diagnostic( + Diagnostic::new( + DiagnosticKind::Error, + message.to_string(), + Span::new(at, at + 1), // Single character error span + ) + .with_context(format!("Error on line {}", self.line)), + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 1effff2..7156988 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ pub mod ast; +pub mod builtins; pub mod diagnostics; pub mod environment; pub mod interpreter; pub mod lexer; pub mod parser; +pub mod prompt; pub mod utils; diff --git a/src/main.rs b/src/main.rs index 8aa8ad4..e4884d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,128 +1,334 @@ -use tap::*; - -use environment::Environment; -use interpreter::{Interpreter, Value}; -use lexer::Lexer; -use parser::Parser; -use rustyline::Editor; -use rustyline::error::ReadlineError; -use std::cell::RefCell; -use std::env; +use clap::{Parser as CLAParser, Subcommand}; +use nu_ansi_term::Color; +use reedline::{FileBackedHistory, Reedline, Signal}; use std::fs; -use std::rc::Rc; +use std::path::PathBuf; +use tap::{ + diagnostics::Reporter, interpreter::Interpreter, lexer::Lexer, parser::Parser, prompt::Prompt, +}; -const VERSION: &str = "0.1.0"; -const AUTHOR: &str = "Michał Kurek"; +#[derive(CLAParser)] +#[command(name = "tap")] +#[command(about = "Tap Programming Language", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, -// ASCII logo banner -fn print_banner() { - println!( - r#"████████╗ █████╗ ██████╗ -╚══██╔══╝██╔══██╗██╔══██╗ - ██║ ███████║██████╔╝ - ██║ ██╔══██║██╔═══╝ - ██║ ██║ ██║██║ - ╚═╝ ╚═╝ ╚═╝╚═╝ - -Tap Language REPL -Version: {version} -Author: {author} -Type 'exit' to quit. -"#, - version = VERSION, - author = AUTHOR, - ); + /// Source file to execute + file: Option, + + /// Arguments to pass to the program + #[arg(trailing_var_arg = true)] + args: Vec, } -fn main() { - let args: Vec = env::args().collect(); - if args.len() > 2 { - println!("Usage: tap [script]"); - return; - } +#[derive(Subcommand)] +enum Commands { + /// Start an interactive REPL + Repl, - color_eyre::install().unwrap_or(eprintln!( - "Failed to install color_eyre for enhanced error reporting." - )); + /// Run a Tap source file + Run { + /// Path to the source file + file: PathBuf, - if args.len() == 2 { - run_file(&args[1]); - } else { - run_prompt(); - } + /// Arguments to pass to the program + #[arg(trailing_var_arg = true)] + args: Vec, + }, } -fn run_file(path: &str) { - let source = fs::read_to_string(path).expect("Failed to read file"); - run(&source); +fn main() { + let cli = Cli::parse(); + + match cli.command { + Some(Commands::Repl) => run_repl(), + Some(Commands::Run { file, args }) => { + if let Err(e) = run_file(&file, args) { + eprintln!("{}", Color::Red.paint(format!("Error: {}", e))); + std::process::exit(1); + } + } + None => { + if let Some(file) = cli.file { + // Execute file directly + if let Err(e) = run_file(&file, cli.args) { + eprintln!("{}", Color::Red.paint(format!("Error: {}", e))); + std::process::exit(1); + } + } else { + // No file provided, start REPL + run_repl(); + } + } + } } -fn run_prompt() { - print_banner(); // ← print the logo once here +fn run_repl() { + println!("{}", Color::Cyan.bold().paint("Tap REPL v0.1.0")); + println!( + "Type {} for help, {} to exit\n", + Color::Yellow.paint(".help"), + Color::Yellow.paint(".exit") + ); + + let history = Box::new( + FileBackedHistory::with_file(100, "tap_history.txt".into()) + .expect("Failed to create history file"), + ); + + let mut line_editor = Reedline::create().with_history(history); - let mut rl = Editor::<()>::new().unwrap(); - let interpreter = Interpreter::new(); - let env = Rc::new(RefCell::new(Environment::new())); + let mut interpreter = Interpreter::new(); + let mut line_num = 1; loop { - let readline = rl.readline(">> "); - match readline { - Ok(line) => { - rl.add_history_entry(line.as_str()); - if line.trim() == "exit" { - break; + let sig = line_editor.read_line(&Prompt); + + match sig { + Ok(Signal::Success(buffer)) => { + let input = buffer.trim(); + + // Handle REPL commands + if input.starts_with('.') { + match input { + ".help" => print_help(), + ".exit" | ".quit" => { + println!("{}", Color::Green.paint("Goodbye!")); + break; + } + ".clear" => { + print!("\x1B[2J\x1B[1;1H"); + continue; + } + ".reset" => { + interpreter = Interpreter::new(); + println!("{}", Color::Green.paint("Interpreter state reset")); + continue; + } + _ => { + eprintln!( + "{}", + Color::Red.paint(format!("Unknown command: {}", input)) + ); + continue; + } + } + continue; } - run_with_interpreter(&line, &interpreter, Rc::clone(&env)); - } - Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); - break; + + if input.is_empty() { + continue; + } + + // Execute the input + match execute_repl_line(&mut interpreter, input, line_num) { + Ok(Some(result)) => { + println!( + "{} {}", + Color::Green.paint("=>"), + Color::White.bold().paint(result) + ); + } + Ok(None) => {} + Err(e) => { + eprintln!("{}", Color::Red.paint(e)); + } + } + + line_num += 1; } - Err(ReadlineError::Eof) => { - println!("CTRL-D"); + Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => { + println!("\n{}", Color::Green.paint("Goodbye!")); break; } Err(err) => { - println!("Error: {:?}", err); + eprintln!("{}", Color::Red.paint(format!("Error: {}", err))); break; } } } } -fn run(source: &str) { - let interpreter = Interpreter::new(); - let env = Rc::new(RefCell::new(Environment::new())); - run_with_interpreter(source, &interpreter, env); -} +fn execute_repl_line( + interpreter: &mut Interpreter, + input: &str, + line_num: usize, +) -> Result, String> { + let mut reporter = Reporter::new(); -fn run_with_interpreter(source: &str, interpreter: &Interpreter, env: Rc>) { - let tokens = match Lexer::new(source).tokenize() { + // Lex + let lexer = Lexer::new(input, &mut reporter); + let tokens = match lexer.tokenize() { Ok(tokens) => tokens, - Err(err) => { - eprintln!("Lexer Error: {}", err); - return; + Err(e) => { + return Err(format!("Lexer error: {:?}", e)); } }; - let mut parser = Parser::new(&tokens, source); + if reporter.has_errors() { + return Err(format!( + "Lexer errors:\n{}", + reporter.format_diagnostics(input) + )); + } + + // Parse + let mut parser = Parser::new(&tokens, &mut reporter); let program = match parser.parse_program() { - Ok(program) => program, - Err(err) => { - eprintln!("Parser Error: {:?}", err); - return; + Ok(prog) => prog, + Err(e) => { + if reporter.has_errors() { + return Err(format!( + "Parse errors:\n{}", + reporter.format_diagnostics(input) + )); + } + return Err(format!("Parse error: {}", e.message)); } }; - match interpreter.interpret(&program, env) { + if reporter.has_errors() { + return Err(format!( + "Parse errors:\n{}", + reporter.format_diagnostics(input) + )); + } + + // Interpret + match interpreter.interpret(&program) { Ok(Some(value)) => { - if value != Value::Null { - println!("{}", value); - } + // let display = interpreter_value_to_string(&value); + let display = interpreter.value_to_display_string(&value); + Ok(Some(display)) } - Ok(None) => { /* no value to print */ } - Err(err) => { - eprintln!("Runtime Error: {}", err); + Ok(None) => Ok(None), + Err(e) => Err(format!("Runtime error at line {}: {}", line_num, e)), + } +} + +fn run_file(path: &PathBuf, args: Vec) -> Result<(), String> { + let source = fs::read_to_string(path) + .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?; + + let mut reporter = Reporter::new(); + + // Lex + let lexer = Lexer::new(&source, &mut reporter); + let tokens = match lexer.tokenize() { + Ok(tokens) => tokens, + Err(e) => { + return Err(format!("Lexer error: {:?}", e)); } + }; + + if reporter.has_errors() { + return Err(format!( + "Lexer errors:\n{}", + reporter.format_diagnostics(&source) + )); + } + + // Parse + let mut parser = Parser::new(&tokens, &mut reporter); + let program = match parser.parse_program() { + Ok(prog) => prog, + Err(e) => { + if reporter.has_errors() { + return Err(format!( + "Parse errors:\n{}", + reporter.format_diagnostics(&source) + )); + } + return Err(format!("Parse error: {}", e.message)); + } + }; + + if reporter.has_errors() { + return Err(format!( + "Parse errors:\n{}", + reporter.format_diagnostics(&source) + )); + } + + // Prepare arguments + let mut full_args = vec![path.to_string_lossy().to_string()]; + full_args.extend(args); + + // Interpret + let mut interpreter = Interpreter::new_with_args(full_args); + + match interpreter.interpret(&program) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Runtime error: {}", e)), } } + +// fn interpreter_value_to_string(value: &tap::interpreter::Value) -> String { +// use tap::interpreter::Value; + +// match value { +// Value::Integer(i) => i.to_string(), +// Value::Float(f) => f.to_string(), +// Value::String(s) => format!("\"{}\"", s), +// Value::Boolean(b) => b.to_string(), +// Value::Unit => "()".to_string(), +// Value::List(items) => { +// let items_str: Vec = items.iter().map(interpreter_value_to_string).collect(); +// format!("[{}]", items_str.join(", ")) +// } +// Value::Record(fields) => { +// let fields_str: Vec = fields +// .iter() +// .map(|(k, v)| format!("{}: {}", k, interpreter_value_to_string(v))) +// .collect(); +// format!("{{{}}}", fields_str.join(", ")) +// } +// Value::Function { name, .. } => { +// format!( +// "", +// name.as_ref().unwrap_or(&"anonymous".to_string()) +// ) +// } +// Value::File { path, .. } => format!("", path), +// Value::Args { .. } => "".to_string(), +// Value::Variant { name, data } => { +// if let Some(d) = data { +// format!("{}({})", name, interpreter_value_to_string(d)) +// } else { +// name.clone() +// } +// } +// _ => format!("{:?}", value), +// } +// } + +fn print_help() { + println!("{}", Color::Cyan.bold().paint("Tap REPL Commands:")); + println!( + " {} - Show this help message", + Color::Yellow.paint(".help") + ); + println!(" {} - Exit the REPL", Color::Yellow.paint(".exit")); + println!(" {} - Clear the screen", Color::Yellow.paint(".clear")); + println!( + " {} - Reset interpreter state", + Color::Yellow.paint(".reset") + ); + println!(); + println!("{}", Color::Cyan.bold().paint("Tips:")); + println!( + " - Press {} or {} to exit", + Color::Yellow.paint("Ctrl+D"), + Color::Yellow.paint("Ctrl+C") + ); + println!( + " - Use {} and {} to navigate history", + Color::Yellow.paint("↑"), + Color::Yellow.paint("↓") + ); + println!( + " - History is saved to {}", + Color::Green.paint("tap_history.txt") + ); +} diff --git a/src/parser.rs b/src/parser.rs index a70bcf7..5491daa 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,854 +1,1823 @@ -use crate::ast::{ - Expression, LiteralValue, MatchArm, Operator, Pattern, Program, Statement, TypeAnnotation, -}; -use crate::lexer::{Span, Token, TokenType}; -use color_eyre::eyre::{self, WrapErr, eyre}; -use std::collections::HashSet; -use thiserror::Error; - -/// The parser, responsible for turning a vector of tokens into an Abstract Syntax Tree (AST). +use crate::ast::*; +use crate::diagnostics::{Diagnostic, DiagnosticKind, Reporter}; +use crate::lexer::{Token, TokenType}; +use std::cell::RefCell; +use std::rc::Rc; + +/// Structured parse error used as the error type in parser `Result`s. +#[derive(Debug, Clone)] +pub struct ParseError { + pub message: String, + pub span: Span, + pub context: Option, +} + +impl ParseError { + fn new(message: String, span: Span, context: Option) -> Self { + ParseError { + message, + span, + context, + } + } +} + +/// RAII guard for parse contexts - automatically pops context on drop +pub struct ContextGuard { + stack: Rc>>, +} + +impl ContextGuard { + fn new(stack: Rc>>, context: &str) -> Self { + stack.borrow_mut().push(context.to_string()); + ContextGuard { stack } + } +} + +impl Drop for ContextGuard { + fn drop(&mut self) { + self.stack.borrow_mut().pop(); + } +} + pub struct Parser<'a> { tokens: &'a [Token], current: usize, - type_context: HashSet, - source: &'a str, -} - -#[derive(Error, Debug, Clone, PartialEq)] -pub enum ParseError { - #[error( - "Unexpected token in {context}: expected {expected}, but found '{found}' at line {line}" - )] - UnexpectedToken { - context: String, - expected: String, - found: String, - found_type: TokenType, - line: usize, - span: Span, - }, - - #[error("Unexpected end of file while parsing {context}")] - UnexpectedEof { context: String, span: Span }, + reporter: &'a mut Reporter, + parse_stack: Rc>>, } impl<'a> Parser<'a> { - pub fn new(tokens: &'a [Token], source: &'a str) -> Self { + pub fn new(tokens: &'a [Token], reporter: &'a mut Reporter) -> Self { Parser { tokens, current: 0, - type_context: HashSet::new(), - source, + reporter, + parse_stack: Rc::new(RefCell::new(Vec::new())), } } - pub fn parse_program(&mut self) -> eyre::Result { - let mut statements = Vec::new(); - while !self.is_at_end() { - if self.match_token(&TokenType::Semicolon) { - continue; - } - statements.push( - self.parse_statement() - .wrap_err("Failed to parse top-level statement")?, - ); + /// Create an RAII context guard + fn context(&self, context: &str) -> ContextGuard { + ContextGuard::new(Rc::clone(&self.parse_stack), context) + } + + /// Get the current parsing context chain as a string + fn current_context_chain(&self) -> String { + let stack = self.parse_stack.borrow(); + if stack.is_empty() { + "top level".to_string() + } else { + stack.join(" -> ") } - Ok(Program { statements }) } - // --- Statement Parsing --- + fn error(&mut self, span: Span, message: String, _context: Option<&str>) -> ParseError { + let full_context = self.current_context_chain(); + let diagnostic = Diagnostic::new(DiagnosticKind::Error, message.clone(), span) + .with_context(full_context.clone()); - fn parse_statement(&mut self) -> eyre::Result { - if self.match_token(&TokenType::KeywordType) { - return self - .parse_type_declaration() - .wrap_err("Failed to parse type declaration"); - } - if self.match_token(&TokenType::KeywordBreak) { - self.consume(&TokenType::Semicolon, "break statement", "';'")?; - return Ok(Statement::Break); - } - if self.match_token(&TokenType::KeywordContinue) { - self.consume(&TokenType::Semicolon, "continue statement", "';'")?; - return Ok(Statement::Continue); - } - if self.check(&TokenType::KeywordIf) { - let if_expr = self.parse_if_expression()?; - return Ok(Statement::Expression(if_expr)); - } - if self.check(&TokenType::KeywordMatch) { - let match_expr = self.parse_match_expression()?; - return Ok(Statement::Expression(match_expr)); - } - if self.match_token(&TokenType::KeywordFunc) { - return self - .parse_function_def() - .wrap_err("Failed to parse function definition"); - } - if self.match_token(&TokenType::KeywordReturn) { - return self - .parse_return_statement() - .wrap_err("Failed to parse return statement"); - } - if self.match_token(&TokenType::KeywordWhile) { - return self - .parse_while_loop() - .wrap_err("Failed to parse while loop"); - } - if self.match_token(&TokenType::KeywordFor) { - return self.parse_for_loop().wrap_err("Failed to parse for loop"); - } + self.reporter.add_diagnostic(diagnostic); + ParseError::new(message, span, Some(full_context)) + } - // Declaration vs Assignment/Expression - if self.check(&TokenType::Identifier) && self.peek_next_is(&TokenType::Colon) { - self.parse_declaration_statement() - .wrap_err("Failed to parse declaration statement") + fn consume( + &mut self, + expected: TokenType, + message: &str, + _context: Option<&str>, + ) -> Result<&Token, ParseError> { + if self.check(expected.clone()) { + Ok(self.advance()) } else { - self.parse_expression_or_assignment_statement() - .wrap_err("Failed to parse expression or assignment statement") + let found = self.peek().clone(); + let full_message = format!("{} Found {:?} instead.", message, found.token_type); + Err(self.error(found.span, full_message, None)) } } - fn parse_type_declaration(&mut self) -> eyre::Result { - let name = self - .consume(&TokenType::Identifier, "type declaration", "type name")? - .lexeme - .clone(); - self.consume(&TokenType::Assign, "type declaration", "'='")?; + pub fn parse_program(&mut self) -> Result { + let _ctx = self.context("program"); - if self.match_token(&TokenType::KeywordStruct) { - self.parse_struct_declaration_after_name(name) - } else if self.match_token(&TokenType::KeywordEnum) { - self.parse_enum_declaration_after_name(name) - } else { - Err(self.unexpected_token("type declaration", "'struct' or 'enum'")) + let mut statements = Vec::new(); + let start_span = self.peek().span; + + while !self.is_at_end() { + let stmt = self.parse_top_statement()?; + statements.push(stmt); } + + let end_span = self.previous().span; + let program_span = Span::new(start_span.start, end_span.end); + + Ok(Program::new(statements, program_span)) } - fn parse_declaration_statement(&mut self) -> eyre::Result { - let name = self - .consume(&TokenType::Identifier, "declaration", "identifier")? - .lexeme - .clone(); - self.consume(&TokenType::Colon, "declaration", "':' after identifier")?; + fn parse_top_statement(&mut self) -> Result { + let _ctx = self.context("top-level statement"); - if self.match_token(&TokenType::KeywordStruct) { - return self - .parse_struct_declaration_after_name(name.clone()) - .wrap_err_with(|| format!("In declaration of struct '{}'", name)); + // Handle type declarations + if self.check(TokenType::KeywordType) { + return self.parse_type_declaration().map(TopStatement::TypeDecl); } - if self.match_token(&TokenType::KeywordEnum) { - return self - .parse_enum_declaration_after_name(name.clone()) - .wrap_err_with(|| format!("In declaration of enum '{}'", name)); + + // Handle while loops + if self.check(TokenType::KeywordWhile) { + let expr = self.parse_while_statement()?; + self.maybe_consume(&[TokenType::Semicolon]); + let span = Span::new(expr.span().start, self.previous().span.end); + return Ok(TopStatement::Expression(ExpressionStatement { + expression: expr, + span, + })); } - let type_annotation = self.parse_type_annotation()?; + // Handle for loops (contextual keyword) + if self.check(TokenType::KeywordFor) { + let expr = self.parse_for_statement()?; + self.maybe_consume(&[TokenType::Semicolon]); + let span = Span::new(expr.span().start, self.previous().span.end); + return Ok(TopStatement::Expression(ExpressionStatement { + expression: expr, + span, + })); + } - if self.match_token(&TokenType::Assign) { - let value = self.parse_expression()?; - // Semicolon is optional after an expression in a declaration - self.match_token(&TokenType::Semicolon); - Ok(Statement::VarDecl { - name, - type_annotation, - value: Some(value), - }) - } else { - self.consume( - &TokenType::Semicolon, - "variable declaration", - "';' after type", - )?; - Ok(Statement::VarDecl { - name, - type_annotation, - value: None, - }) + // Handle if expressions + if self.check(TokenType::KeywordIf) { + let expr = self.parse_if_expression()?; + self.maybe_consume(&[TokenType::Semicolon]); + let span = Span::new(expr.span().start, self.previous().span.end); + return Ok(TopStatement::Expression(ExpressionStatement { + expression: expr, + span, + })); } - } - fn parse_expression_or_assignment_statement(&mut self) -> eyre::Result { - let expr = self.parse_expression()?; + // Handle match expressions + if self.check(TokenType::KeywordMatch) { + let expr = self.parse_match_expression()?; + self.maybe_consume(&[TokenType::Semicolon]); + let span = Span::new(expr.span().start, self.previous().span.end); + return Ok(TopStatement::Expression(ExpressionStatement { + expression: expr, + span, + })); + } - if self.match_token(&TokenType::Assign) { - let value = self.parse_expression()?; - self.consume(&TokenType::Semicolon, "assignment", "';' after value")?; - - // Check L-Value validity - if let Expression::Get { object, name } = expr { - return Ok(Statement::PropertyAssignment { - object: *object, - property: name, - value, - }); - } else if let Expression::Identifier(name) = expr { - return Ok(Statement::Assignment { name, value }); - } else if let Expression::ArrayAccess { array, index } = expr { - return Ok(Statement::ArrayAssignment { - array: *array, - index: *index, - value, - }); - } else { - return Err(eyre!("Invalid assignment target.")); + // Disambiguate function definition vs function call/variable binding + if self.peek().token_type.is_identifier() + && self.peek_next().token_type == TokenType::OpenParen + { + if self.looks_like_function_definition() { + let func_stmt = self.parse_function_statement()?; + self.maybe_consume(&[TokenType::Semicolon]); + return Ok(TopStatement::LetStmt(func_stmt)); } } - if self.check(&TokenType::CloseBrace) { - return Ok(Statement::Expression(expr)); + // Handle let/mut + if self.peek().token_type == TokenType::KeywordMut { + return self.parse_let_statement().map(TopStatement::LetStmt); } - if self.is_at_end() { - return Ok(Statement::Expression(expr)); + // Handle variable binding (identifier with : or =) + if self.peek().token_type.is_identifier() + && (self.peek_next().token_type == TokenType::Colon + || self.peek_next().token_type == TokenType::Assign) + { + return self.parse_let_statement().map(TopStatement::LetStmt); } - self.consume(&TokenType::Semicolon, "expression statement", "';'")?; - Ok(Statement::Expression(expr)) + self.parse_expression_statement() + .map(TopStatement::Expression) } - // --- Control Flow Implementations --- + fn looks_like_function_definition(&self) -> bool { + let mut idx = self.current + 2; // Skip identifier and OpenParen - fn parse_while_loop(&mut self) -> eyre::Result { - // "while" already consumed - let condition = self.parse_expression()?; - let body = self.parse_block()?; - Ok(Statement::While { - condition: Box::new(condition), - body, - }) - } + // Empty params: `()` - check if followed by `:` or `=` + if idx < self.tokens.len() && self.tokens[idx].token_type == TokenType::CloseParen { + if idx + 1 < self.tokens.len() { + let next = &self.tokens[idx + 1].token_type; + return matches!(next, TokenType::Colon | TokenType::Assign); + } + return false; + } - fn parse_for_loop(&mut self) -> eyre::Result { - // "for" already consumed - let iterator = self - .consume(&TokenType::Identifier, "for loop", "iterator name")? - .lexeme - .clone(); - self.consume(&TokenType::KeywordIn, "for loop", "'in'")?; - let iterable = self.parse_expression()?; - let body = self.parse_block()?; - Ok(Statement::For { - iterator, - iterable: Box::new(iterable), - body, - }) + // First token after ( should be an identifier or 'this' for a function definition + if idx < self.tokens.len() { + if !matches!( + &self.tokens[idx].token_type, + TokenType::Identifier(_) | TokenType::KeywordThis + ) { + // Not a function definition - might be a call with a literal argument + return false; + } + } + + // Scan through parameters looking for type annotations or return type + let mut depth = 1; // We're inside the opening paren + while idx < self.tokens.len() && depth > 0 { + match &self.tokens[idx].token_type { + TokenType::OpenParen => depth += 1, + TokenType::CloseParen => { + depth -= 1; + if depth == 0 { + // Found matching close paren, check what follows + if idx + 1 < self.tokens.len() { + let next = &self.tokens[idx + 1].token_type; + // Function def has `:` (return type) or `=` (body) + return matches!(next, TokenType::Colon | TokenType::Assign); + } + return false; + } + } + TokenType::Colon if depth == 1 => { + // Found a type annotation or return type + return true; + } + _ => {} + } + idx += 1; + } + + false } - fn parse_match_expression(&mut self) -> eyre::Result { - self.consume(&TokenType::KeywordMatch, "match statement", "'match'")?; - let value = self.parse_expression()?; - self.consume(&TokenType::OpenBrace, "match statement", "'{'")?; + fn parse_type_declaration(&mut self) -> Result { + let _ctx = self.context("type declaration"); - let mut arms = Vec::new(); - while !self.check(&TokenType::CloseBrace) && !self.is_at_end() { - let pattern = self.parse_pattern()?; - self.consume(&TokenType::ArrowFat, "match arm", "'=>'")?; // Ensure Token::ArrowFat (=>) exists - let expr = self.parse_expression()?; + self.consume(TokenType::KeywordType, "Expected 'type' keyword.", None)?; - // Optional comma after expression - self.match_token(&TokenType::Comma); + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!("Expected type name, but found {:?}.", name_token.token_type); + return Err(self.error(name_token.span, msg, None)); + }; - arms.push(MatchArm { - pattern, - body: expr, - }); - } - self.consume(&TokenType::CloseBrace, "match statement", "'}'")?; + self.consume(TokenType::Assign, "Expected '=' after type name.", None)?; - Ok(Expression::Match { - value: Box::new(value), - arms, + let constructor = self.parse_type_constructor()?; + self.maybe_consume(&[TokenType::Semicolon]); // optional after type declaration + let span = Span::new(name_token.span.start, self.previous().span.end); + Ok(TypeDeclaration { + name, + constructor, + span, }) } - fn parse_pattern(&mut self) -> eyre::Result { - // Very basic pattern matching support - if self.match_token(&TokenType::Underscore) { - return Ok(Pattern::Wildcard); + fn parse_type_constructor(&mut self) -> Result { + let _ctx = self.context("type constructor"); + + // Case 1: RecordType (starts with '{') + if self.check(TokenType::OpenBrace) { + let record = self.parse_record_type()?; + return Ok(TypeConstructor::Record(record)); } - if let Some(token) = self.peek() { - match token.token_type { - TokenType::Integer(i) => { - self.advance(); - Ok(Pattern::Literal(LiteralValue::Integer(i))) - } - TokenType::String(ref s) => { - let s = s.clone(); - self.advance(); - Ok(Pattern::Literal(LiteralValue::String(s))) + + // Case 2: List type or other type alias starting with '[' + if self.check(TokenType::OpenBracket) { + let alias_type = self.parse_type()?; + return Ok(TypeConstructor::Alias(alias_type)); + } + + // Case 3: Could be a sum type or type alias + let saved_pos = self.current; + + if self.peek().token_type.is_identifier() + || self.peek().token_type == TokenType::KeywordNone + { + let next = self.peek_next().token_type.clone(); + + if next == TokenType::Pipe { + match self.parse_variant() { + Ok(first_variant) => { + let mut variants = vec![first_variant.clone()]; + while self.maybe_consume(&[TokenType::Pipe]) { + variants.push(self.parse_variant()?); + } + let end_span = variants + .last() + .map(|v| v.span) + .unwrap_or(first_variant.span); + let sum_span = Span::new(first_variant.span.start, end_span.end); + return Ok(TypeConstructor::Sum(SumConstructor { + variants, + span: sum_span, + })); + } + Err(e) => return Err(e), } - TokenType::Identifier => { - let name = self.advance().lexeme.clone(); - // Check for Enum variant pattern: MyEnum::Variant(x) or Some(x) - if self.match_token(&TokenType::OpenParen) { - let mut sub_patterns = Vec::new(); - if !self.check(&TokenType::CloseParen) { - loop { - sub_patterns.push(self.parse_pattern()?); - if !self.match_token(&TokenType::Comma) { - break; - } + } else if next == TokenType::OpenParen { + match self.parse_variant() { + Ok(first_variant) => { + if self.check(TokenType::Pipe) { + let mut variants = vec![first_variant.clone()]; + while self.maybe_consume(&[TokenType::Pipe]) { + variants.push(self.parse_variant()?); } + let end_span = variants + .last() + .map(|v| v.span) + .unwrap_or(first_variant.span); + let sum_span = Span::new(first_variant.span.start, end_span.end); + return Ok(TypeConstructor::Sum(SumConstructor { + variants, + span: sum_span, + })); + } else { + let span = first_variant.span; + return Ok(TypeConstructor::Sum(SumConstructor { + variants: vec![first_variant], + span, + })); } - self.consume(&TokenType::CloseParen, "pattern", "')'")?; - Ok(Pattern::EnumVariant { - enum_name: "".to_string(), // enum name is unknown here - variant: name, - vars: sub_patterns, - }) - } else if self.match_token(&TokenType::ColonColon) { - let variant = self - .consume(&TokenType::Identifier, "pattern", "variant name")? - .lexeme - .clone(); - let mut sub_patterns = Vec::new(); - if self.match_token(&TokenType::OpenParen) { - if !self.check(&TokenType::CloseParen) { - loop { - sub_patterns.push(self.parse_pattern()?); - if !self.match_token(&TokenType::Comma) { - break; - } - } + } + Err(_) => { + self.current = saved_pos; + let alias_type = self.parse_type()?; + return Ok(TypeConstructor::Alias(alias_type)); + } + } + } else if next == TokenType::OpenBracket { + let alias_type = self.parse_type()?; + return Ok(TypeConstructor::Alias(alias_type)); + } else { + match self.parse_variant() { + Ok(variant) => { + if self.check(TokenType::Pipe) { + let mut variants = vec![variant.clone()]; + while self.maybe_consume(&[TokenType::Pipe]) { + variants.push(self.parse_variant()?); } - self.consume(&TokenType::CloseParen, "pattern", "')'")?; + let end_span = variants.last().map(|v| v.span).unwrap_or(variant.span); + let sum_span = Span::new(variant.span.start, end_span.end); + return Ok(TypeConstructor::Sum(SumConstructor { + variants, + span: sum_span, + })); + } else { + let span = variant.span; + return Ok(TypeConstructor::Sum(SumConstructor { + variants: vec![variant], + span, + })); } - Ok(Pattern::EnumVariant { - enum_name: name, - variant, - vars: sub_patterns, - }) - } else { - // Just a variable capture - Ok(Pattern::Identifier(name)) + } + Err(_) => { + self.current = saved_pos; + let alias_type = self.parse_type()?; + return Ok(TypeConstructor::Alias(alias_type)); } } - _ => Err(self.unexpected_token("pattern", "literal, identifier, or '_'")), } - } else { - Err(self.unexpected_token("pattern", "literal, identifier, or '_'")) } + + let alias_type = self.parse_type()?; + Ok(TypeConstructor::Alias(alias_type)) } - fn parse_return_statement(&mut self) -> eyre::Result { - let value = if !self.check(&TokenType::Semicolon) { - Some(self.parse_expression()?) + fn parse_variant(&mut self) -> Result { + let _ctx = self.context("variant"); + + let name_token = self.peek().clone(); + let name = match &name_token.token_type { + TokenType::Identifier(s) => { + self.advance(); + s.clone() + } + TokenType::KeywordNone => { + self.advance(); + "None".to_string() + } + _ => { + let msg = format!( + "Expected variant name, but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + } + }; + + let ty = if self.check(TokenType::OpenParen) { + self.advance(); + let inner_type = self.parse_type()?; + self.consume( + TokenType::CloseParen, + "Expected ')' after variant payload type.", + None, + )?; + Some(inner_type) } else { None }; - self.consume(&TokenType::Semicolon, "return statement", "';'")?; - Ok(Statement::Return(value)) + + let end_span = self.previous().span; + let span = Span::new(name_token.span.start, end_span.end); + + Ok(Variant { name, ty, span }) } - fn parse_block(&mut self) -> eyre::Result> { - self.consume(&TokenType::OpenBrace, "block", "'{'")?; - let mut statements = Vec::new(); - while !self.check(&TokenType::CloseBrace) && !self.is_at_end() { - statements.push(self.parse_statement()?); + fn parse_statement(&mut self) -> Result { + let _ctx = self.context("statement"); + + // Handle return statements + if self.check(TokenType::KeywordReturn) { + self.advance(); + let start = self.previous().span.start; + let expr = if self.check(TokenType::Semicolon) { + None + } else { + Some(self.parse_expression()?) + }; + self.consume( + TokenType::Semicolon, + "Expected ';' after return statement.", + None, + )?; + let span = Span::new(start, self.previous().span.end); + return Ok(Statement::Return(expr, span)); } - self.consume(&TokenType::CloseBrace, "block", "'}'")?; - Ok(statements) - } - // --- Expression Parsing (Stratified) --- + // Handle break statements + if self.check(TokenType::KeywordBreak) { + let start = self.advance().span.start; + self.consume(TokenType::Semicolon, "Expected ';' after break.", None)?; + let span = Span::new(start, self.previous().span.end); + return Ok(Statement::Break(span)); + } - pub fn parse_expression(&mut self) -> eyre::Result { - self.parse_logic_or() - } + // Handle continue statements + if self.check(TokenType::KeywordContinue) { + let start = self.advance().span.start; + self.consume(TokenType::Semicolon, "Expected ';' after continue.", None)?; + let span = Span::new(start, self.previous().span.end); + return Ok(Statement::Continue(span)); + } - fn parse_logic_or(&mut self) -> eyre::Result { - self.parse_binary_expr(Self::parse_logic_and, &[TokenType::OpLor]) - } + // // Handle while loops + // if self.check(TokenType::KeywordWhile) { + // let expr = self.parse_while_statement()?; + // self.match_token(&[TokenType::Semicolon]); + // let span = expr.span(); + // return Ok(Statement::Expression(ExpressionStatement { + // expression: expr, + // span, + // })); + // } + + // // Handle for loops + // if self.is_contextual_keyword("for") { + // let expr = self.parse_for_statement()?; + // self.match_token(&[TokenType::Semicolon]); + // let span = expr.span(); + // return Ok(Statement::Expression(ExpressionStatement { + // expression: expr, + // span, + // })); + // } + + // Handle if expressions + // if self.check(TokenType::KeywordIf) { + // let expr = self.parse_if_expression()?; + // self.match_token(&[TokenType::Semicolon]); + // let span = expr.span(); + // return Ok(Statement::Expression(ExpressionStatement { + // expression: expr, + // span, + // })); + // } + + // Handle match expressions + // if self.check(TokenType::KeywordMatch) { + // let expr = self.parse_match_expression()?; + // self.match_token(&[TokenType::Semicolon]); + // let span = expr.span(); + // return Ok(Statement::Expression(ExpressionStatement { + // expression: expr, + // span, + // })); + // } + + // Function definitions + if self.peek().token_type.is_identifier() + && self.peek_next().token_type == TokenType::OpenParen + && self.looks_like_function_definition() + { + return self.parse_function_statement().map(Statement::Let); + } - fn parse_logic_and(&mut self) -> eyre::Result { - self.parse_binary_expr(Self::parse_equality, &[TokenType::OpLand]) - } + // Let/mut bindings + if self.peek().token_type == TokenType::KeywordMut { + return self.parse_let_statement().map(Statement::Let); + } - fn parse_equality(&mut self) -> eyre::Result { - self.parse_binary_expr( - Self::parse_comparison, - &[TokenType::Equal, TokenType::NotEqual], - ) - } + // Variable binding with type annotation or assignment + if self.peek().token_type.is_identifier() { + let next = self.peek_next().token_type.clone(); + if next == TokenType::Colon { + return self.parse_let_statement().map(Statement::Let); + } else if next == TokenType::Assign { + let saved = self.current; + if let Ok(let_stmt) = self.parse_let_statement() { + return Ok(Statement::Let(let_stmt)); + } + self.current = saved; + } + } - fn parse_comparison(&mut self) -> eyre::Result { - self.parse_binary_expr( - Self::parse_term, - &[ - TokenType::GreaterThan, - TokenType::GreaterThanEqual, - TokenType::LessThan, - TokenType::LessThanEqual, - ], - ) + self.parse_expression_statement().map(Statement::Expression) } - fn parse_term(&mut self) -> eyre::Result { - self.parse_binary_expr(Self::parse_factor, &[TokenType::OpPlus, TokenType::OpMinus]) - } + fn parse_let_statement(&mut self) -> Result { + let _ctx = self.context("let statement"); + + let mutable = self.maybe_consume(&[TokenType::KeywordMut]); + + let name = if let TokenType::Identifier(s) = self.peek().token_type.clone() { + self.advance(); + s + } else { + let token = self.peek().clone(); + let msg = format!( + "Expected identifier for variable name in let statement, but found {:?}.", + token.token_type + ); + return Err(self.error(token.span, msg, None)); + }; + + let type_annotation = if self.maybe_consume(&[TokenType::Colon]) { + Some(self.parse_type()?) + } else { + None + }; + + self.consume( + TokenType::Assign, + "Expected '=' after identifier in let statement.", + None, + )?; - fn parse_factor(&mut self) -> eyre::Result { - self.parse_binary_expr( - Self::parse_unary, - &[TokenType::OpMult, TokenType::OpDivide, TokenType::OpMod], - ) + let value = self.parse_expression()?; + + self.consume( + TokenType::Semicolon, + "Expected ';' after expression in let statement.", + None, + )?; + + let span = Span::new( + if mutable { + self.tokens[self.current - (if type_annotation.is_some() { 6 } else { 4 })] + .span + .start + } else { + self.tokens[self.current - (if type_annotation.is_some() { 5 } else { 3 })] + .span + .start + }, + self.previous().span.end, + ); + + Ok(LetStatement::Variable(VariableBinding { + mutable, + name, + type_annotation, + value, + span, + })) } - fn parse_unary(&mut self) -> eyre::Result { - if self.match_tokens(&[TokenType::OpMinus, TokenType::Bang]) { - let op = Operator::try_from(&self.previous().token_type).unwrap(); - let right = self.parse_unary()?; - return Ok(Expression::Unary { - op, - right: Box::new(right), - }); - } - self.parse_call_member() + fn parse_function_statement(&mut self) -> Result { + let _ctx = self.context("function declaration"); + + let mutable = self.maybe_consume(&[TokenType::KeywordMut]); + + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!( + "Expected identifier for function name, but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + }; + + let params = self.parse_parameters()?; + + let return_type = if self.maybe_consume(&[TokenType::Colon]) { + self.parse_type()? + } else { + Type::Primary(TypePrimary::Named( + "unit".to_string(), + Span { start: 0, end: 0 }, + )) + }; + + self.consume( + TokenType::Assign, + "Expected '=' after function signature.", + None, + )?; + + let body = self.parse_block()?; + let span = Span::new(name_token.span.start, body.span.end); + + Ok(LetStatement::Function(FunctionBinding { + mutable, + name, + params, + return_type, + body, + span, + })) } - // The "Loop" for postfix operations (call, index, dot) - fn parse_call_member(&mut self) -> eyre::Result { - let mut expr = self.parse_primary()?; + fn parse_type(&mut self) -> Result { + let _ctx = self.context("type annotation"); - loop { - if self.match_token(&TokenType::OpenParen) { - expr = self.parse_call_expression(expr)?; - } else if self.match_token(&TokenType::Period) { - let name = self - .consume(&TokenType::Identifier, "property access", "property name")? - .lexeme - .clone(); - expr = Expression::Get { - object: Box::new(expr), - name, - }; - } else if self.match_token(&TokenType::OpenBracket) { - let index_expr = self.parse_expression()?; - self.consume(&TokenType::CloseBracket, "array access", "']'")?; - expr = Expression::ArrayAccess { - array: Box::new(expr), - index: Box::new(index_expr), - }; - } else { - break; - } + let primary = self.parse_type_primary()?; + + if self.maybe_consume(&[TokenType::Arrow]) { + let return_type = self.parse_type()?; + let span = Span::new(primary.span().start, return_type.span().end); + return Ok(Type::Function { + params: vec![Type::Primary(primary)], + return_type: Box::new(return_type), + span, + }); } - Ok(expr) + + Ok(Type::Primary(primary)) } - // Primary atoms (no recursion into operators on the left) - fn parse_primary(&mut self) -> eyre::Result { - if self.is_at_end() { - return Err(eyre!(ParseError::UnexpectedEof { - context: "primary expression".to_string(), - span: Span::new(0, 0), // Simplify span logic for EOF - })); - } + fn parse_type_primary(&mut self) -> Result { + let token = self.peek().clone(); + let span = token.span; - let token = self.peek().unwrap(); - match token.token_type { - TokenType::Integer(val) => { - self.advance(); - Ok(Expression::Literal(LiteralValue::Integer(val))) - } - TokenType::Float(val) => { - self.advance(); - Ok(Expression::Literal(LiteralValue::Float(val))) - } - TokenType::String(ref val) => { - let val = val.clone(); - self.advance(); - Ok(Expression::Literal(LiteralValue::String(val))) - } - TokenType::KeywordTrue => { - self.advance(); - Ok(Expression::Literal(LiteralValue::Boolean(true))) - } - TokenType::KeywordFalse => { - self.advance(); - Ok(Expression::Literal(LiteralValue::Boolean(false))) - } - TokenType::KeywordUnit => { + match &token.token_type { + TokenType::Identifier(name) => { + let name = name.clone(); self.advance(); - Ok(Expression::Literal(LiteralValue::Unit)) - } - TokenType::Identifier => { - // Lookahead for Struct Instantiation or Enum::Variant - let token = self.peek().unwrap(); - if self.peek_next_is(&TokenType::OpenBrace) - && self.type_context.contains(&token.lexeme) - { - self.parse_struct_instantiation() - } else if self.peek_next_is(&TokenType::ColonColon) { - let enum_name = self.advance().lexeme.clone(); - self.advance(); // consume :: - let variant_name = self - .consume(&TokenType::Identifier, "enum", "variant")? - .lexeme - .clone(); - Ok(Expression::EnumVariant { - enum_name, - variant_name, + if self.check(TokenType::OpenBracket) { + let _ctx = self.context("generic type"); + self.advance(); + let mut args = Vec::new(); + while !self.check(TokenType::CloseBracket) && !self.is_at_end() { + args.push(self.parse_type()?); + if !self.maybe_consume(&[TokenType::Comma]) { + break; + } + } + self.consume( + TokenType::CloseBracket, + "Expected ']' after generic type arguments.", + None, + )?; + Ok(TypePrimary::Generic { + name, + args, + span: Span::new(span.start, self.previous().span.end), }) } else { - self.advance(); - Ok(Expression::Identifier(self.previous().lexeme.clone())) + Ok(TypePrimary::Named(name, span)) } } - TokenType::OpenParen => { + TokenType::OpenBracket => { + let _ctx = self.context("list type"); self.advance(); - let expr = self.parse_expression()?; - self.consume(&TokenType::CloseParen, "group", "')'")?; - Ok(expr) + let inner_type = self.parse_type()?; + self.consume( + TokenType::CloseBracket, + "Expected ']' after list type.", + None, + )?; + Ok(TypePrimary::List( + Box::new(inner_type), + Span::new(span.start, self.previous().span.end), + )) + } + TokenType::OpenBrace => { + let record_type = self.parse_record_type()?; + Ok(TypePrimary::Record(record_type)) + } + _ => { + let msg = format!("Expected type, but found {:?}.", token.token_type); + Err(self.error(span, msg, None)) } - TokenType::OpenBracket => self.parse_list_literal(), - TokenType::Lambda => self.parse_lambda_expression(), - TokenType::KeywordIf => self.parse_if_expression(), - TokenType::KeywordMatch => self.parse_match_expression(), - TokenType::OpenBrace => self.parse_block_expression(), - _ => Err(self.unexpected_token("expression", "literal, identifier, or '('")), } } - // --- Helpers for complex expressions --- + fn parse_record_type(&mut self) -> Result { + let _ctx = self.context("record type"); - fn parse_block_expression(&mut self) -> eyre::Result { - let statements = self.parse_block()?; - Ok(Expression::Block(statements)) - } + let start_span = self + .consume( + TokenType::OpenBrace, + "Expected '{' to start a record type.", + None, + )? + .span; - fn parse_binary_expr( - &mut self, - next_parser: F, - types: &[TokenType], - ) -> eyre::Result - where - F: Fn(&mut Self) -> eyre::Result, - { - let mut expr = next_parser(self)?; - while self.match_tokens(types) { - let op = Operator::try_from(&self.previous().token_type).unwrap(); - let right = next_parser(self)?; - expr = Expression::Binary { - left: Box::new(expr), - op, - right: Box::new(right), + let mut fields = Vec::new(); + + while !self.check(TokenType::CloseBrace) && !self.is_at_end() { + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!( + "Expected identifier for field name in record type, but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); }; - } - Ok(expr) - } - fn parse_call_expression(&mut self, callee: Expression) -> eyre::Result { - let mut args = Vec::new(); - if !self.check(&TokenType::CloseParen) { - loop { - args.push(self.parse_expression()?); - if !self.match_token(&TokenType::Comma) { - break; - } + let name_span = self.previous().span; + + if self.check(TokenType::OpenParen) { + let _ctx = self.context("record method"); + let params = self.parse_parameters()?; + + self.consume( + TokenType::Colon, + "Expected ':' after method parameters.", + None, + )?; + + let return_type = self.parse_type()?; + + self.consume( + TokenType::Assign, + "Expected '=' after method signature.", + None, + )?; + + let body = self.parse_block()?; + let method_span = Span::new(name_span.start, body.span.end); + + fields.push(FieldDeclaration { + name: name.clone(), + ty: Type::Function { + params: params + .iter() + .map(|p| { + Type::Primary(TypePrimary::Named(format!("{}", p.name), p.span)) + }) + .collect(), + return_type: Box::new(return_type), + span: method_span, + }, + span: method_span, + }); + } else { + self.consume( + TokenType::Colon, + "Expected ':' after field name in record type.", + None, + )?; + + let type_ = self.parse_type()?; + let field_span = Span::new(name_span.start, type_.span().end); + + fields.push(FieldDeclaration { + name, + ty: type_, + span: field_span, + }); } - } - self.consume(&TokenType::CloseParen, "function call", "')'")?; - Ok(Expression::FunctionCall { - callee: Box::new(callee), - args, - }) - } - fn parse_lambda_expression(&mut self) -> eyre::Result { - self.consume(&TokenType::Lambda, "lambda", "'\\'")?; - let mut args = Vec::new(); - // Parse args until we hit a dot - if !self.check(&TokenType::Period) { - loop { - args.push( - self.consume(&TokenType::Identifier, "lambda param", "name")? - .lexeme - .clone(), - ); - // Optional comma for lambda args? syntax said "ident {ident} ." - // Assuming spaces/logic separator, but let's support optional comma - if self.check(&TokenType::Period) { + if !self.maybe_consume(&[TokenType::Comma]) { + self.maybe_consume(&[TokenType::Semicolon]); + if !self.check(TokenType::CloseBrace) { break; } - self.match_token(&TokenType::Comma); } } - self.consume(&TokenType::Period, "lambda", "'.' after parameters")?; - let body = self.parse_expression()?; - Ok(Expression::Lambda { - args, - body: Box::new(body), - }) + + let end_span = self + .consume( + TokenType::CloseBrace, + "Expected '}' to end a record type.", + None, + )? + .span; + + let span = Span::new(start_span.start, end_span.end); + + Ok(RecordType { fields, span }) } - fn parse_list_literal(&mut self) -> eyre::Result { - self.consume(&TokenType::OpenBracket, "list", "'['")?; - let mut elements = Vec::new(); - if !self.check(&TokenType::CloseBracket) { - loop { - elements.push(self.parse_expression()?); - if !self.match_token(&TokenType::Comma) { - break; + fn parse_expression_statement(&mut self) -> Result { + let _ctx = self.context("expression statement"); + + let expr = self.parse_expression()?; + + if !self.is_at_end() { + self.consume(TokenType::Semicolon, "Expected ';' after expression.", None)?; + } else { + // Optional semicolon at EOF + self.maybe_consume(&[TokenType::Semicolon]); + } + + let span = Span::new(expr.span().start, self.previous().span.end); + Ok(ExpressionStatement { + expression: expr, + span, + }) + } + + fn parse_expression(&mut self) -> Result { + let _ctx = self.context("expression"); + + // Check for lambda expression + if self.check(TokenType::OpenParen) { + if self.peek_next().token_type == TokenType::CloseParen { + if self + .tokens + .get(self.current + 2) + .map_or(false, |t| t.token_type == TokenType::FatArrow) + { + return self.parse_function_expression(); + } + } else if self.peek_next().token_type.is_identifier() { + if self + .tokens + .get(self.current + 2) + .map_or(false, |t| t.token_type == TokenType::Colon) + { + return self.parse_function_expression(); + } + } + } + + self.parse_assignment_expression() + } + + fn parse_assignment_expression(&mut self) -> Result { + let expr = self.parse_range_expression()?; + + if self.maybe_consume(&[ + TokenType::Assign, + TokenType::PlusEqual, + TokenType::MinusEqual, + TokenType::StarEqual, + TokenType::SlashEqual, + TokenType::PercentEqual, + ]) { + let op_token = self.previous().clone(); + let right = self.parse_assignment_expression()?; + let span = Span::new(expr.span().start, right.span().end); + let operator = match op_token.token_type { + TokenType::Assign => BinaryOperator::Assign, + TokenType::PlusEqual => BinaryOperator::AddAssign, + TokenType::MinusEqual => BinaryOperator::SubtractAssign, + TokenType::StarEqual => BinaryOperator::MultiplyAssign, + TokenType::SlashEqual => BinaryOperator::DivideAssign, + TokenType::PercentEqual => BinaryOperator::ModuloAssign, + _ => unreachable!(), + }; + return Ok(Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator, + right: Box::new(right), + span, + })); + } + + Ok(expr) + } + + fn parse_range_expression(&mut self) -> Result { + let expr = self.parse_logical_or_expression()?; + + if self.maybe_consume(&[ + TokenType::DotDot, + TokenType::DotDotLess, + TokenType::DotDotEqual, + ]) { + let operator_token = self.previous().clone(); + let inclusive = operator_token.token_type == TokenType::DotDotEqual; + let end = self.parse_logical_or_expression()?; + let span = Span::new(expr.span().start, end.span().end); + + return Ok(Expression::Range(RangeExpression { + start: Box::new(expr), + end: Box::new(end), + inclusive, + span, + })); + } + + Ok(expr) + } + + fn parse_logical_or_expression(&mut self) -> Result { + let mut expr = self.parse_logical_and_expression()?; + while self.maybe_consume(&[TokenType::PipePipe]) { + let right = self.parse_logical_and_expression()?; + let span = Span::new(expr.span().start, right.span().end); + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator: BinaryOperator::Or, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_logical_and_expression(&mut self) -> Result { + let mut expr = self.parse_equality_expression()?; + while self.maybe_consume(&[TokenType::AmpAmp]) { + let right = self.parse_equality_expression()?; + let span = Span::new(expr.span().start, right.span().end); + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator: BinaryOperator::And, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_equality_expression(&mut self) -> Result { + let mut expr = self.parse_comparison_expression()?; + while self.maybe_consume(&[TokenType::Equal, TokenType::NotEqual]) { + let operator_token = self.previous().clone(); + let right = self.parse_comparison_expression()?; + let span = Span::new(expr.span().start, right.span().end); + let operator = match operator_token.token_type { + TokenType::Equal => BinaryOperator::Equal, + TokenType::NotEqual => BinaryOperator::NotEqual, + _ => unreachable!(), + }; + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_comparison_expression(&mut self) -> Result { + let mut expr = self.parse_additive_expression()?; + while self.maybe_consume(&[ + TokenType::LessThan, + TokenType::LessThanEqual, + TokenType::GreaterThan, + TokenType::GreaterThanEqual, + ]) { + let operator_token = self.previous().clone(); + let right = self.parse_additive_expression()?; + let span = Span::new(expr.span().start, right.span().end); + let operator = match operator_token.token_type { + TokenType::LessThan => BinaryOperator::LessThan, + TokenType::LessThanEqual => BinaryOperator::LessThanEqual, + TokenType::GreaterThan => BinaryOperator::GreaterThan, + TokenType::GreaterThanEqual => BinaryOperator::GreaterThanEqual, + _ => unreachable!(), + }; + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_additive_expression(&mut self) -> Result { + let mut expr = self.parse_multiplicative_expression()?; + while self.maybe_consume(&[TokenType::Plus, TokenType::Minus]) { + let operator_token = self.previous().clone(); + let right = self.parse_multiplicative_expression()?; + let span = Span::new(expr.span().start, right.span().end); + let operator = match operator_token.token_type { + TokenType::Plus => BinaryOperator::Add, + TokenType::Minus => BinaryOperator::Subtract, + _ => unreachable!(), + }; + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_multiplicative_expression(&mut self) -> Result { + let mut expr = self.parse_unary_expression()?; + while self.maybe_consume(&[TokenType::Star, TokenType::Slash, TokenType::Percent]) { + let operator_token = self.previous().clone(); + let right = self.parse_unary_expression()?; + let span = Span::new(expr.span().start, right.span().end); + let operator = match operator_token.token_type { + TokenType::Star => BinaryOperator::Multiply, + TokenType::Slash => BinaryOperator::Divide, + TokenType::Percent => BinaryOperator::Modulo, + _ => unreachable!(), + }; + expr = Expression::Binary(BinaryExpression { + left: Box::new(expr), + operator, + right: Box::new(right), + span, + }); + } + Ok(expr) + } + + fn parse_unary_expression(&mut self) -> Result { + if self.maybe_consume(&[TokenType::Bang, TokenType::Minus, TokenType::Plus]) { + let operator_token = self.previous().clone(); + let right = self.parse_unary_expression()?; + let span = Span::new(operator_token.span.start, right.span().end); + let operator = match operator_token.token_type { + TokenType::Bang => UnaryOperator::Not, + TokenType::Minus => UnaryOperator::Minus, + TokenType::Plus => UnaryOperator::Plus, + _ => unreachable!(), + }; + return Ok(Expression::Unary(UnaryExpression { + operator, + right: Box::new(right), + span, + })); + } + self.parse_postfix_expression() + } + + fn parse_postfix_expression(&mut self) -> Result { + let expr = self.parse_primary_expression()?; + + let mut operators = Vec::new(); + while self.maybe_consume(&[ + TokenType::Dot, + TokenType::DoubleColon, + TokenType::OpenParen, + TokenType::OpenBracket, + ]) { + let operator_token = self.previous().clone(); + let operator = match operator_token.token_type { + TokenType::Dot => { + let _ctx = self.context("field access"); + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!( + "Expected identifier after '.', but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + }; + let span = Span::new(operator_token.span.start, self.previous().span.end); + PostfixOperator::FieldAccess { name, span } + } + TokenType::DoubleColon => { + let _ctx = self.context("type path"); + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!( + "Expected identifier after '::', but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + }; + let span = Span::new(operator_token.span.start, self.previous().span.end); + PostfixOperator::TypePath { name, span } + } + TokenType::OpenParen => { + let _ctx = self.context("function call"); + let mut args = Vec::new(); + while !self.check(TokenType::CloseParen) && !self.is_at_end() { + args.push(self.parse_expression()?); + if !self.maybe_consume(&[TokenType::Comma]) { + break; + } + } + self.consume(TokenType::CloseParen, "Expected ')' after arguments.", None)?; + let span = Span::new(operator_token.span.start, self.previous().span.end); + PostfixOperator::Call { args, span } + } + TokenType::OpenBracket => { + let _ctx = self.context("list index"); + let index = self.parse_expression()?; + self.consume(TokenType::CloseBracket, "Expected ']' after index.", None)?; + let span = Span::new(operator_token.span.start, self.previous().span.end); + PostfixOperator::ListAccess { + index: Box::new(index), + span, + } } + _ => unreachable!(), + }; + operators.push(operator); + } + + if operators.is_empty() { + Ok(expr) + } else { + let span = Span::new(expr.span().start, self.previous().span.end); + Ok(Expression::Postfix(crate::ast::PostfixExpression { + primary: Box::new(expr), + operators, + span, + })) + } + } + + fn parse_primary_expression(&mut self) -> Result { + let token = self.peek().clone(); + let span = token.span; + + match &token.token_type { + TokenType::KeywordIf => self.parse_if_expression(), + TokenType::KeywordMatch => self.parse_match_expression(), + TokenType::KeywordWhile => self.parse_while_statement(), + TokenType::KeywordFor => self.parse_for_statement(), + TokenType::Integer(i) => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(*i), + span, + ))) + } + TokenType::Float(f) => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Float(*f), + span, + ))) + } + TokenType::String(s) => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::String(s.clone()), + span, + ))) + } + TokenType::KeywordTrue => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Boolean(true), + span, + ))) + } + TokenType::KeywordFalse => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Boolean(false), + span, + ))) + } + TokenType::KeywordNone => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Literal( + LiteralValue::None, + span, + ))) + } + TokenType::KeywordThis => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::This(span))) + } + TokenType::OpenParen => { + let _ctx = self.context("parenthesized expression"); + self.advance(); + let expr = self.parse_expression()?; + self.consume( + TokenType::CloseParen, + "Expected ')' after expression.", + None, + )?; + let end_span = self.previous().span; + let full_span = Span::new(span.start, end_span.end); + Ok(Expression::Primary(PrimaryExpression::Parenthesized( + Box::new(expr), + full_span, + ))) + } + TokenType::Identifier(name) => { + self.advance(); + Ok(Expression::Primary(PrimaryExpression::Identifier( + name.clone(), + span, + ))) + } + TokenType::OpenBrace => self.parse_brace_expression(), + TokenType::OpenBracket => self.parse_list_literal(), + _ => { + let msg = format!("Expected expression, but found {:?}.", token.token_type); + Err(self.error(span, msg, None)) } } - self.consume(&TokenType::CloseBracket, "list", "']'")?; - Ok(Expression::List(elements)) } - fn parse_if_expression(&mut self) -> eyre::Result { - self.consume(&TokenType::KeywordIf, "if expr", "'if'")?; + fn parse_brace_expression(&mut self) -> Result { + let saved_pos = self.current; + self.advance(); + + if self.check(TokenType::CloseBrace) { + self.current = saved_pos; + return self.parse_block_expression(); + } + + if self.peek().token_type.is_identifier() && self.peek_next().token_type == TokenType::Colon + { + self.current = saved_pos; + self.parse_record_literal() + } else { + self.current = saved_pos; + self.parse_block_expression() + } + } + + fn parse_block_expression(&mut self) -> Result { + let block = self.parse_block()?; + Ok(Expression::Block(block)) + } + + fn parse_list_literal(&mut self) -> Result { + let _ctx = self.context("list literal"); + + let start_span = self + .consume( + TokenType::OpenBracket, + "Expected '[' to start list literal.", + None, + )? + .span; + + let mut elements = Vec::new(); + + while !self.check(TokenType::CloseBracket) && !self.is_at_end() { + elements.push(self.parse_expression()?); + if !self.maybe_consume(&[TokenType::Comma]) { + break; + } + } + + let end_span = self + .consume( + TokenType::CloseBracket, + "Expected ']' to end list literal.", + None, + )? + .span; + + let span = Span::new(start_span.start, end_span.end); + + Ok(Expression::Primary(PrimaryExpression::List(ListLiteral { + elements, + span, + }))) + } + + fn parse_if_expression(&mut self) -> Result { + let _ctx = self.context("if expression"); + + let start_span = self + .consume(TokenType::KeywordIf, "Expected 'if' keyword.", None)? + .span; + + let left_paren = self.maybe_consume(&[TokenType::OpenParen]); + let condition = self.parse_expression()?; + + if left_paren { + // If a left parenthesis was consumed, we expect a right parenthesis + self.consume( + TokenType::CloseParen, + "Expected ')' after if condition.", + None, + )?; + } else { + // If no left parenthesis, we should NOT have a right parenthesis + if self.maybe_consume(&[TokenType::CloseParen]) { + let msg = "Unexpected ')' after if condition.".to_string(); + return Err(self.error(self.previous().span, msg, None)); + } + } + let then_branch = self.parse_block()?; - let else_branch = if self.match_token(&TokenType::KeywordElse) { - Some(if self.check(&TokenType::KeywordIf) { - // Handle "else if" by wrapping it in a block or expression - // For simplicity here, we treat it as a block containing one expression/statement - vec![Statement::Expression(self.parse_if_expression()?)] + + let else_branch = if self.maybe_consume(&[TokenType::KeywordElse]) { + if self.check(TokenType::KeywordIf) { + let else_if_expr = self.parse_if_expression()?; + Some(Box::new(else_if_expr)) } else { - self.parse_block()? - }) + Some(Box::new(Expression::Block(self.parse_block()?))) + } } else { None }; - Ok(Expression::If { + + let end_span = else_branch + .as_ref() + .map(|e| e.span().end) + .unwrap_or(then_branch.span.end); + let span = Span::new(start_span.start, end_span); + + Ok(Expression::If(IfExpression { condition: Box::new(condition), then_branch, else_branch, - }) + span, + })) } - fn parse_struct_instantiation(&mut self) -> eyre::Result { - let name = self - .consume(&TokenType::Identifier, "struct", "name")? - .lexeme - .clone(); - self.consume(&TokenType::OpenBrace, "struct", "'{'")?; - let mut fields = Vec::new(); - if !self.check(&TokenType::CloseBrace) { - loop { - let field_name = self - .consume(&TokenType::Identifier, "field", "name")? - .lexeme - .clone(); - self.consume(&TokenType::Colon, "field", "':'")?; - let val = self.parse_expression()?; - fields.push((field_name, val)); - if !self.match_token(&TokenType::Comma) { - break; - } + fn parse_match_expression(&mut self) -> Result { + let _ctx = self.context("match expression"); + + let start_span = self + .consume(TokenType::KeywordMatch, "Expected 'match' keyword.", None)? + .span; + + self.consume(TokenType::OpenParen, "Expected '(' after 'match'.", None)?; + + let value = self.parse_expression()?; + + self.consume( + TokenType::CloseParen, + "Expected ')' after match scrutinee.", + None, + )?; + + self.consume( + TokenType::OpenBrace, + "Expected '{' to start match arms.", + None, + )?; + + let mut arms = Vec::new(); + + while !self.check(TokenType::CloseBrace) && !self.is_at_end() { + let _ctx = self.context("match arm"); + + // TODO: playing around with the grammar + // self.consume(TokenType::Pipe, "Expected '|' before match arm.", None)?; + // FOR NOW, make the '|' optional + self.maybe_consume(&[TokenType::Pipe]); + + let pattern = self.parse_pattern()?; + + self.consume(TokenType::FatArrow, "Expected '=>' after pattern.", None)?; + + let body_expr = self.parse_expression()?; + let body_span = body_expr.span(); + let body = ExpressionOrBlock::Expression(Box::new(body_expr)); + + arms.push(MatchArm { + pattern: pattern.clone(), + body, + span: Span::new(pattern.span().start, body_span.end), + }); + + if !self.maybe_consume(&[TokenType::Comma]) { + break; } } - self.consume(&TokenType::CloseBrace, "struct", "'}'")?; - Ok(Expression::StructInstantiation { name, fields }) - } - - // --- Type & Definition Parsing --- - - fn parse_function_def(&mut self) -> eyre::Result { - let name = self - .consume(&TokenType::Identifier, "func", "name")? - .lexeme - .clone(); - self.consume(&TokenType::OpenParen, "func", "'('")?; - let mut args = Vec::new(); - if !self.check(&TokenType::CloseParen) { - loop { - let arg_name = self - .consume(&TokenType::Identifier, "param", "name")? - .lexeme - .clone(); - // Optional type annotation for args - if self.match_token(&TokenType::Colon) { - self.parse_type_annotation()?; // Just consume it for now, or store it if AST supports it - } - args.push(arg_name); - if !self.match_token(&TokenType::Comma) { - break; + + let end_span = self + .consume( + TokenType::CloseBrace, + "Expected '}' to end match expression.", + None, + )? + .span; + + let span = Span::new(start_span.start, end_span.end); + + Ok(Expression::Match(MatchExpression { + value: Box::new(value), + arms, + span, + })) + } + + fn parse_pattern(&mut self) -> Result { + let _ctx = self.context("pattern"); + + let token = self.peek().clone(); + + if token.token_type == TokenType::KeywordUnderscore { + self.advance(); + return Ok(Pattern::Wildcard(token.span)); + } + + if token.token_type == TokenType::KeywordNone { + self.advance(); + return Ok(Pattern::Identifier("None".to_string(), token.span)); + } + + match &token.token_type { + TokenType::Integer(i) => { + self.advance(); + return Ok(Pattern::Literal(LiteralValue::Integer(*i), token.span)); + } + TokenType::Float(f) => { + self.advance(); + return Ok(Pattern::Literal(LiteralValue::Float(*f), token.span)); + } + TokenType::String(s) => { + self.advance(); + return Ok(Pattern::Literal( + LiteralValue::String(s.clone()), + token.span, + )); + } + TokenType::KeywordTrue => { + self.advance(); + return Ok(Pattern::Literal(LiteralValue::Boolean(true), token.span)); + } + TokenType::KeywordFalse => { + self.advance(); + return Ok(Pattern::Literal(LiteralValue::Boolean(false), token.span)); + } + TokenType::KeywordNone => { + self.advance(); + return Ok(Pattern::Literal(LiteralValue::None, token.span)); + } + TokenType::Identifier(name) => { + self.advance(); + + if self.check(TokenType::OpenParen) { + self.advance(); + let mut patterns = Vec::new(); + while !self.check(TokenType::CloseParen) && !self.is_at_end() { + patterns.push(self.parse_pattern()?); + if !self.maybe_consume(&[TokenType::Comma]) { + break; + } + } + self.consume(TokenType::CloseParen, "Expected ')' after pattern.", None)?; + let end_span = self.previous().span; + let span = Span::new(token.span.start, end_span.end); + return Ok(Pattern::Variant { + name: name.clone(), + patterns: Some(patterns), + span, + }); + } else { + return Ok(Pattern::Identifier(name.clone(), token.span)); } } + _ => {} } - self.consume(&TokenType::CloseParen, "func", "')'")?; - // Optional return type - if self.match_token(&TokenType::Arrow) { - self.parse_type_annotation()?; + let msg = format!("Expected pattern, but found {:?}.", token.token_type); + Err(self.error(token.span, msg, None)) + } + + fn parse_while_statement(&mut self) -> Result { + let _ctx = self.context("while loop"); + + let start_token = self.advance().clone(); + + self.consume(TokenType::OpenParen, "Expected '(' after 'while'.", None)?; + + let condition = self.parse_expression()?; + + self.consume( + TokenType::CloseParen, + "Expected ')' after while condition.", + None, + )?; + + let body = self.parse_block()?; + let span = Span::new(start_token.span.start, body.span.end); + + Ok(Expression::While(WhileExpression { + condition: Box::new(condition), + body, + span, + })) + } + + fn parse_for_statement(&mut self) -> Result { + let _ctx = self.context("for loop"); + + let start_token = self.advance().clone(); + + let pattern = self.parse_pattern()?; + + if !self.check(TokenType::KeywordIn) { + let token = self.peek().clone(); + let msg = format!( + "Expected 'in' after loop variable, but found {:?}.", + token.token_type + ); + return Err(self.error(token.span, msg, None)); } + self.advance(); - let body = self - .parse_block() - .wrap_err_with(|| format!("body of '{}'", name))?; - Ok(Statement::FunctionDef(crate::ast::FunctionDef { - name, - args, + let iterable = self.parse_expression()?; + let body = self.parse_block()?; + let span = Span::new(start_token.span.start, body.span.end); + + Ok(Expression::For(ForExpression { + pattern, + iterable: Box::new(iterable), body, + span, })) } - fn parse_struct_declaration_after_name(&mut self, name: String) -> eyre::Result { - self.consume(&TokenType::OpenBrace, "struct", "'{'")?; + fn parse_record_literal(&mut self) -> Result { + let _ctx = self.context("record literal"); + + let start_span = self + .consume( + TokenType::OpenBrace, + "Expected '{' to start a record literal.", + None, + )? + .span; + let mut fields = Vec::new(); - if !self.check(&TokenType::CloseBrace) { - loop { - let field_name = self - .consume(&TokenType::Identifier, "field", "name")? - .lexeme - .clone(); - self.consume(&TokenType::Colon, "field", "':'")?; - let type_ann = self.parse_type_annotation()?; - fields.push((field_name, type_ann)); - if !self.match_token(&TokenType::Comma) { - break; - } + + while !self.check(TokenType::CloseBrace) && !self.is_at_end() { + let name_token = self.peek().clone(); + let name = if let TokenType::Identifier(s) = name_token.token_type.clone() { + self.advance(); + s + } else { + let msg = format!( + "Expected identifier for field name in record literal, but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + }; + + let name_span = self.previous().span; + + self.consume( + TokenType::Colon, + "Expected ':' after field name in record literal.", + None, + )?; + + let value = self.parse_expression()?; + let field_span = Span::new(name_span.start, value.span().end); + + fields.push(FieldInitializer { + name, + value, + span: field_span, + }); + + if !self.maybe_consume(&[TokenType::Comma]) { + break; } } - self.consume(&TokenType::CloseBrace, "struct", "'}'")?; - self.consume(&TokenType::Semicolon, "struct", "';'")?; - self.type_context.insert(name.clone()); - Ok(Statement::StructDecl(crate::ast::StructDecl { - name, - fields, + + let end_span = self + .consume( + TokenType::CloseBrace, + "Expected '}' to end a record literal.", + None, + )? + .span; + + let span = Span::new(start_span.start, end_span.end); + + Ok(Expression::Primary(PrimaryExpression::Record( + RecordLiteral { fields, span }, + ))) + } + + fn parse_function_expression(&mut self) -> Result { + let _ctx = self.context("lambda expression"); + + let params = self.parse_parameters()?; + let return_type = if self.maybe_consume(&[TokenType::Colon]) { + self.parse_type()? + } else { + Type::Primary(TypePrimary::Named( + "unit".to_string(), + Span { start: 0, end: 0 }, + )) + }; + + self.consume( + TokenType::FatArrow, + "Expected '=>' for lambda expression body.", + None, + )?; + + let body = if self.check(TokenType::OpenBrace) { + ExpressionOrBlock::Block(self.parse_block()?) + } else { + ExpressionOrBlock::Expression(Box::new(self.parse_expression()?)) + }; + + let span = Span::new(self.previous().span.start, self.previous().span.end); + + Ok(Expression::Lambda(LambdaExpression { + params, + return_type_annotation: Some(return_type), + body, + span, })) } - fn parse_enum_declaration_after_name(&mut self, name: String) -> eyre::Result { - self.consume(&TokenType::OpenBrace, "enum", "'{'")?; - let mut variants = Vec::new(); - if !self.check(&TokenType::CloseBrace) { - loop { - let variant_name = self - .consume(&TokenType::Identifier, "variant", "name")? - .lexeme - .clone(); - - let mut types = Vec::new(); - if self.match_token(&TokenType::OpenParen) { - loop { - types.push(self.parse_type_annotation()?); - if !self.match_token(&TokenType::Comma) { - break; - } - } - self.consume(&TokenType::CloseParen, "enum variant", "')'")?; - } + fn parse_parameters(&mut self) -> Result, ParseError> { + let _ctx = self.context("parameter list"); - variants.push(crate::ast::EnumVariant { - name: variant_name, - types, - }); + self.consume( + TokenType::OpenParen, + "Expected '(' to start a parameter list.", + None, + )?; - if !self.match_token(&TokenType::Comma) { - break; + let mut params = Vec::new(); + + while !self.check(TokenType::CloseParen) && !self.is_at_end() { + let name_token = self.peek().clone(); + let name = match &name_token.token_type { + TokenType::Identifier(s) => { + self.advance(); + s.clone() } - } - } - self.consume(&TokenType::CloseBrace, "enum", "'}'")?; - self.consume(&TokenType::Semicolon, "enum", "';'")?; - self.type_context.insert(name.clone()); - Ok(Statement::EnumDecl(crate::ast::EnumDecl { name, variants })) - } + TokenType::KeywordThis => { + // Allow 'this' as a parameter name + self.advance(); + "this".to_string() + } + _ => { + let msg = format!( + "Expected identifier in parameter list, but found {:?}.", + name_token.token_type + ); + return Err(self.error(name_token.span, msg, None)); + } + }; - fn parse_type_annotation(&mut self) -> eyre::Result { - let mut type_ann = self.parse_primary_type()?; - while self.match_token(&TokenType::Arrow) { - let right = self.parse_type_annotation()?; - type_ann = TypeAnnotation::Function { - from: Box::new(type_ann), - to: Box::new(right), + // Type annotation is optional + let type_ = if self.maybe_consume(&[TokenType::Colon]) { + self.parse_type()? + } else { + // No type annotation - use a placeholder or inferred type + Type::Primary(TypePrimary::Named("inferred".to_string(), name_token.span)) }; + + let span = Span::new(name_token.span.start, type_.span().end); + + params.push(Parameter { + name, + ty: type_, + span, + }); + + if !self.maybe_consume(&[TokenType::Comma]) { + break; + } } - Ok(type_ann) - } - - fn parse_primary_type(&mut self) -> eyre::Result { - if self.match_token(&TokenType::KeywordInt) { - Ok(TypeAnnotation::Int) - } else if self.match_token(&TokenType::KeywordStr) { - Ok(TypeAnnotation::Str) - } else if self.match_token(&TokenType::OpenBracket) { - let inner = self.parse_type_annotation()?; - self.consume(&TokenType::CloseBracket, "type", "']'")?; - Ok(TypeAnnotation::Array(Box::new(inner))) - } else if self.check(&TokenType::Identifier) { - let name = self.advance().lexeme.clone(); - // In a real compiler, we might validate `name` exists in `type_context` - Ok(TypeAnnotation::UserDefined(name)) - } else { - Err(self.unexpected_token("type", "int, str, [T], or identifier")) - } + + self.consume( + TokenType::CloseParen, + "Expected ')' after parameters.", + None, + )?; + + Ok(params) } - // --- Utility --- + fn parse_block(&mut self) -> Result { + let _ctx = self.context("block"); - fn consume( - &mut self, - token_type: &TokenType, - context: &str, - expected: &str, - ) -> eyre::Result<&Token> { - if self.check(token_type) { - return Ok(self.advance()); - } - Err(self.unexpected_token(context, expected)) - } - - fn unexpected_token(&self, context: &str, expected: &str) -> eyre::Report { - let peeked = self.peek(); - if let Some(token) = peeked { - eyre!(ParseError::UnexpectedToken { - context: context.to_string(), - expected: expected.to_string(), - found: token.lexeme.clone(), - found_type: token.token_type.clone(), - line: token.line, - span: token.span, - }) - } else { - eyre!(ParseError::UnexpectedEof { - context: context.to_string(), - span: if self.tokens.is_empty() { - Span::new(0, 0) - } else { - let l = self.tokens.last().unwrap(); - Span::new(l.span.hi, l.span.hi) + let start_span = self + .consume(TokenType::OpenBrace, "Expected '{' to start a block.", None)? + .span; + + let mut statements = Vec::new(); + let mut final_expression = None; + + while !self.check(TokenType::CloseBrace) && !self.is_at_end() { + // Only these MUST be statements (cannot be final expressions) + let must_be_statement = self.peek().token_type == TokenType::KeywordReturn + || self.check(TokenType::KeywordBreak) + || self.check(TokenType::KeywordContinue); + + if must_be_statement { + statements.push(self.parse_statement()?); + continue; + } + + // Special case: 'mut' keyword starts a let statement + if self.peek().token_type == TokenType::KeywordMut { + statements.push(self.parse_statement()?); + continue; + } + + // Special case: identifier with : or identifier with ( that looks like function def + if self.peek().token_type.is_identifier() { + let next_token = self.peek_next().token_type.clone(); + if next_token == TokenType::Colon + || (next_token == TokenType::OpenParen && self.looks_like_function_definition()) + { + statements.push(self.parse_statement()?); + continue; } - }) + } + + // Try to parse as expression + let expr = self.parse_expression()?; + + // Check if expression ends with a closing brace (control flow constructs) + let expr_ends_with_brace = matches!( + expr, + Expression::If(_) + | Expression::While(_) + | Expression::For(_) + | Expression::Match(_) + | Expression::Block(_) + ); + + // Decide if it's a final expression or a statement + if self.check(TokenType::CloseBrace) { + // At end of block - this is the final expression + final_expression = Some(Box::new(expr)); + break; + } else if self.maybe_consume(&[TokenType::Semicolon]) { + // Has semicolon - it's a statement + let span = Span::new(expr.span().start, self.previous().span.end); + statements.push(Statement::Expression(ExpressionStatement { + expression: expr, + span, + })); + } else if expr_ends_with_brace { + // Expression ends with } and is not at end of block + // Treat as statement (allows while/for/if/match to be followed by more code) + let span = expr.span(); + statements.push(Statement::Expression(ExpressionStatement { + expression: expr, + span, + })); + } else { + // No semicolon, not at end, doesn't end with brace - error + let found = self.peek().clone(); + return Err(self.error( + found.span, + format!("Expected ';' or '}}', but found {:?}", found.token_type), + None, + )); + } } + + let end_span = self + .consume(TokenType::CloseBrace, "Expected '}' to end a block.", None)? + .span; + + let span = Span::new(start_span.start, end_span.end); + + Ok(Block { + statements, + final_expression, + span, + }) } - fn match_token(&mut self, token_type: &TokenType) -> bool { - if self.check(token_type) { - self.advance(); - true - } else { - false - } + // --- Helper methods --- + + fn peek(&self) -> &Token { + &self.tokens[self.current] } - fn match_tokens(&mut self, types: &[TokenType]) -> bool { - for t in types { - if self.check(t) { - self.advance(); - return true; - } + fn peek_next(&self) -> &Token { + if self.current + 1 >= self.tokens.len() { + &self.tokens[self.tokens.len() - 1] + } else { + &self.tokens[self.current + 1] } - false } - fn check(&self, token_type: &TokenType) -> bool { - if self.is_at_end() { - return false; - } - std::mem::discriminant(&self.peek().unwrap().token_type) - == std::mem::discriminant(token_type) + fn previous(&self) -> &Token { + &self.tokens[self.current - 1] } - fn peek_next_is(&self, token_type: &TokenType) -> bool { - self.tokens.get(self.current + 1).map_or(false, |t| { - std::mem::discriminant(&t.token_type) == std::mem::discriminant(token_type) - }) + fn is_at_end(&self) -> bool { + self.peek().token_type == TokenType::EndOfFile } fn advance(&mut self) -> &Token { @@ -858,38 +1827,22 @@ impl<'a> Parser<'a> { self.previous() } - fn is_at_end(&self) -> bool { - self.peek() - .map_or(true, |t| t.token_type == TokenType::EndOfFile) - } - - fn peek(&self) -> Option<&Token> { - self.tokens.get(self.current) - } - fn previous(&self) -> &Token { - &self.tokens[self.current - 1] + // Return true if the current token matches the given type + fn check(&self, token_type: TokenType) -> bool { + if self.is_at_end() { + return false; + } + self.peek().token_type == token_type } -} -impl TryFrom<&TokenType> for Operator { - type Error = eyre::Report; - fn try_from(value: &TokenType) -> Result { - match value { - TokenType::OpPlus => Ok(Operator::Add), - TokenType::OpMinus => Ok(Operator::Subtract), - TokenType::OpMult => Ok(Operator::Multiply), - TokenType::OpDivide => Ok(Operator::Divide), - TokenType::OpMod => Ok(Operator::Modulo), - TokenType::Equal => Ok(Operator::Equal), - TokenType::NotEqual => Ok(Operator::NotEqual), - TokenType::GreaterThan => Ok(Operator::GreaterThan), - TokenType::GreaterThanEqual => Ok(Operator::GreaterThanEqual), - TokenType::LessThan => Ok(Operator::LessThan), - TokenType::LessThanEqual => Ok(Operator::LessThanEqual), - TokenType::OpLand => Ok(Operator::And), - TokenType::OpLor => Ok(Operator::Or), - TokenType::Bang => Ok(Operator::Not), - _ => Err(eyre!("Cannot convert {:?} to a binary operator", value)), + // Return true if any of the given token types are matched and consumed + fn maybe_consume(&mut self, types: &[TokenType]) -> bool { + for token_type in types { + if self.check(token_type.clone()) { + self.advance(); + return true; + } } + false } } diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..5d53ffc --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,33 @@ +use chrono::Utc; +use reedline::{Prompt as ReedlinePrompt, PromptEditMode, PromptHistorySearch}; +use std::borrow::Cow; + +#[derive(Clone)] +pub struct Prompt; + +impl ReedlinePrompt for Prompt { + fn render_prompt_left(&self) -> std::borrow::Cow<'_, str> { + Cow::Borrowed("tap") + } + + fn render_prompt_right(&self) -> std::borrow::Cow<'_, str> { + // Render current time + let time_str = Utc::now().format("%H:%M:%S").to_string(); + Cow::Owned(time_str) + } + + fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow<'_, str> { + Cow::Owned(String::from(" >> ")) + } + + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + Cow::Owned(String::from(" multiline >> ")) + } + + fn render_prompt_history_search_indicator( + &self, + _history_search: PromptHistorySearch, + ) -> Cow<'_, str> { + Cow::Owned(String::from(" search history >> ")) + } +} diff --git a/src/utils.rs b/src/utils.rs index 97a858d..90f9b48 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,9 @@ -pub fn gensym(prefix: &str) -> String { - use std::sync::atomic::{AtomicUsize, Ordering}; - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let id = COUNTER.fetch_add(1, Ordering::Relaxed); - format!("{}_{}", prefix, id) +use crate::lexer::Token; + +pub fn pretty_print_tokens(tokens: &[Token]) -> String { + tokens + .iter() + .map(|t| format!("{}", t)) + .collect::>() + .join(", ") } diff --git a/tap_history.txt b/tap_history.txt new file mode 100644 index 0000000..8523efb --- /dev/null +++ b/tap_history.txt @@ -0,0 +1,24 @@ +.help +clear +.help +1 + 2 ; +"Siema" + "Byku"; +jeżeli ("życie daje ci cytryny") { "sprzedaj je i kup wódkę" } lub { "kup wódkę" } +.exit +hello there +3 + 5; +3 + 4; +scores = [1, 2, 3]; +scores; +scores.push(4); +scores; +a = [[1]]; +a[0]; +a[0][0]; +a[0] = 2; +a; +a[0]; +a[0] = [3]; +a[0]; +a[0][0] = 4; +a; diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index 55308e2..0000000 --- a/tests/integration.rs +++ /dev/null @@ -1,609 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; -use tap::environment::Environment; -use tap::interpreter::{Interpreter, Value}; -use tap::lexer::Lexer; -use tap::parser::Parser; - -/// Helper function to run source code and return the final value. -fn run_tap(source: &str) -> Option { - let tokens = Lexer::new(source).tokenize().expect("Lexer error"); - - let mut parser = Parser::new(&tokens, source); - let program = parser.parse_program().expect("Parser error"); - - let interpreter = Interpreter::new(); - let env = Rc::new(RefCell::new(Environment::new())); - - interpreter.interpret(&program, env).expect("Runtime error") -} - -#[test] -fn test_structs() { - // file: structs.tap - let source = r#" - type Rect = struct { - width: int, - height: int - }; - - func area(r: Rect) -> int { - r.width * r.height; - } - - my_rect : Rect = Rect { width: 10, height: 5 }; - - # Modify a property - my_rect.width = 20; - - area(my_rect); # Should return 100 - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(100))); -} - -#[test] -fn test_option_type() { - // file: option_type.tap - let source = r#" - type MaybeInt = enum { Some(int), None }; - - func safe_div(a: int, b: int) -> MaybeInt { - if b == 0 { - MaybeInt::None - } else { - MaybeInt::Some(a / b) - } - } - - # Pattern match to unwrap the value - match safe_div(10, 2) { - Some(val) => val, - None => 0 - }; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(5))); -} - -#[test] -fn test_iterative_fib() { - // file: iterative_fib.tap - let source = r#" - func fib_iter(n: int) -> int { - a : int = 0; - b : int = 1; - count : int = 0; - - while count < n { - temp : int = a; - a = b; - b = temp + b; - count = count + 1; - } - a; - } - - fib_iter(10); # Should return 55 - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(55))); -} - -#[test] -fn test_conditional_as_value() { - // file: conditional_as_value.tap - let source = r#" - age : int = 20; - - # Assigning the result of an if-block to a variable - status : str = if age >= 18 { - "Adult" - } else { - "Minor" - }; - - status == "Adult" - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_fib_lambda() { - // file: fib_lambda.tap - let source = r#" - # comment tokens get removed at lexing time, up until the new line - - # inline function definition of fibonacci - fib : int -> int = - \x. ( - if x < 2 { - x; - } else { - fib(x - 1) + fib(x - 2); - } - ); - - fib(5) == 5; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_lambdas2() { - // file: lambdas2.tap - let source = r#" - # A function that takes an int and a function(int -> int) - func transform(val: int, op: int -> int) -> int { - op(val); - } - - # Pass a lambda that squares the input - transform(5, \x. x * x); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(25))); -} - -#[test] -fn test_enum_decl() { - // file: enum.tap - let source = r#" - Color : enum { Red, Green, Blue }; - "#; - let result = run_tap(source); - // Declarations return Unit, which interpreter converts to None - assert_eq!(result, None); -} - -#[test] -fn test_inline_lambda() { - // file: inline_lambda.tap - let source = r#" - res = (\y. y + 10)(10); # Expected output: 20 - res == 20; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_list_sum() { - // file: list_sum.tap - let source = r#" - numbers : [int] = [10, 20, 30, 40, 50]; - total : int = 0; - - for n in numbers { - total = total + n; - } - - total; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(150))); -} - -#[test] -fn test_fib() { - // file: fib.tap - let source = r#" - func fib(n) { - if n < 2 { - return n; - } - return fib(n - 1) + fib(n - 2); - } - - fib(5) == 5; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_lambda_declaration() { - // file: lambda.tap - let source = r#" - inc: int -> int = \x. x + 1; - - inc(5) == 6; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_hello() { - // file: hello.tap - let source = r#" - func main() { - "Hello, World!"; - } - - main(); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::String("Hello, World!".to_string()))); -} - -#[test] -fn test_array_mutation() { - // file: array_mutation.tap - let source = r#" - matrix : [int] = [0, 0, 0]; - - matrix[0] = 1; - matrix[1] = 2; - matrix[2] = 3; - - # Should return [1, 2, 3] - matrix; - "#; - let result = run_tap(source); - assert_eq!( - result, - Some(Value::List(vec![ - Value::Integer(1), - Value::Integer(2), - Value::Integer(3) - ])) - ); -} - -#[test] -fn test_match() { - // file: match.tap - let source = r#" - type Light = enum { Red, Yellow, Green }; - - func action(l: Light) -> str { - match l { - Red => "Stop", - Yellow => "Caution", - Green => "Go" - } - } - - action(Light::Yellow); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::String("Caution".to_string()))); -} - -#[test] -fn test_dodaj_polish() { - // file: dodaj.tap - let source = r#" - funkcja dodaj(a, b) { - zwróć a + b; - } - - dodaj(4, 5) == 9; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_hi_func() { - // file: hi_func.tap - let source = r#" - func multiply(a, b) { - return a * b; - } - - double: int -> int; - double = \x. multiply(x, 2); - double(6) == 12; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_silnia_polish() { - // file: silnia.tap - let source = r#" - funkcja silnia(n) { - jeśli n == 0 { - zwróć 1; - } - zwróć n * silnia(n - 1); - } - - silnia(5) == 120; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_float_arithmetic() { - let source = r#" - a : float = 10.5; - b : float = 2.5; - a + b; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Float(13.0))); -} - -#[test] -fn test_float_comparison() { - let source = r#" - 1.5 < 2.5; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_string_concatenation() { - let source = r#" - "Hello" + " " + "World"; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::String("Hello World".to_string()))); -} - -#[test] -fn test_logic_short_circuit_and() { - // If short-circuiting works, the division by zero won't happen - let source = r#" - false && (1 / 0 == 0); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(false))); -} - -#[test] -fn test_logic_short_circuit_or() { - // If short-circuiting works, the division by zero won't happen - let source = r#" - true || (1 / 0 == 0); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_unary_operators() { - let source = r#" - x : int = 10; - flag : bool = true; - (-x == -10) && (!flag == false); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_modulo() { - let source = r#" - 10 % 3; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(1))); -} - -#[test] -fn test_precedence_order() { - let source = r#" - # Should be 2 + (3 * 4) = 14, not (2 + 3) * 4 = 20 - 2 + 3 * 4; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(14))); -} - -#[test] -fn test_nested_loops_break() { - let source = r#" - outer : int = 0; - inner : int = 0; - sum : int = 0; - - while outer < 3 { - inner = 0; - while inner < 3 { - if inner == 1 { - break; - } - sum = sum + 1; - inner = inner + 1; - } - outer = outer + 1; - } - sum; - "#; - // Outer runs 3 times. Inner runs: 0 (sum+1), 1 (break). - // So sum increments once per outer loop. Total 3. - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(3))); -} - -#[test] -fn test_nested_loops_continue() { - let source = r#" - i : int = 0; - sum : int = 0; - while i < 5 { - i = i + 1; - if i == 3 { - continue; - } - sum = sum + i; - } - sum; - "#; - // i=1, sum=1 - // i=2, sum=3 - // i=3, continue - // i=4, sum=7 - // i=5, sum=12 - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(12))); -} - -#[test] -fn test_mutual_recursion() { - let source = r#" - func is_even(n: int) -> bool { - if n == 0 { - true - } else { - is_odd(n - 1) - } - } - - func is_odd(n: int) -> bool { - if n == 0 { - false - } else { - is_even(n - 1) - } - } - - is_even(4) && is_odd(3); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} - -#[test] -fn test_nested_if_expression() { - let source = r#" - x : int = 10; - y : int = 20; - - res : str = if x > 5 { - if y < 10 { - "A" - } else { - "B" - } - } else { - "C" - }; - res; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::String("B".to_string()))); -} - -#[test] -#[ignore] -fn test_polish_while_loop() { - let source = r#" - licznik : int = 0; - dopóki licznik < 5 { - licznik = licznik + 1; - } - licznik; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(5))); -} - -#[test] -fn test_polish_if_else() { - let source = r#" - x : int = 10; - wynik : int = 0; - jeśli x > 100 { - wynik = 1; - } inaczej { - wynik = 2; - } - wynik; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(2))); -} - -#[test] -fn test_array_access_expression() { - let source = r#" - [10, 20, 30][1]; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(20))); -} - -#[test] -fn test_function_returning_lambda() { - let source = r#" - func make_adder(n: int) -> int -> int { - \x. x + n; - } - - add5 = make_adder(5); - add5(10); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(15))); -} - -#[test] -fn test_struct_access_nested() { - let source = r#" - type Point = struct { x: int, y: int }; - type Line = struct { start: Point, end: Point }; - - l : Line = Line { - start: Point { x: 0, y: 0 }, - end: Point { x: 10, y: 20 } - }; - - l.end.y; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(20))); -} - -#[test] -fn test_early_return_in_loop() { - let source = r#" - func find_match(target: int, list: [int]) -> int { - for x in list { - if x == target { - return x; - } - } - return -1; - } - - find_match(30, [10, 20, 30, 40]); - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Integer(30))); -} - -#[test] -fn test_unit_return() { - // Empty block returns Unit (None in run_tap if implicit return) - // But we can verify assignment of Unit - let source = r#" - x = {}; - x; - "#; - let result = run_tap(source); - assert_eq!(result, None); -} - -#[test] -fn test_complex_boolean_logic() { - let source = r#" - a : bool = true; - b : bool = false; - c : bool = true; - - # (T || F) && T -> T && T -> T - (a || b) && c; - "#; - let result = run_tap(source); - assert_eq!(result, Some(Value::Boolean(true))); -} diff --git a/tests/interpreter.rs b/tests/interpreter.rs index 23e747a..8b01562 100644 --- a/tests/interpreter.rs +++ b/tests/interpreter.rs @@ -1,326 +1,1780 @@ -use std::cell::RefCell; -use std::rc::Rc; -use tap::environment::Environment; -use tap::interpreter::{Interpreter, Value}; +use tap::diagnostics::Reporter; +use tap::interpreter::{Interpreter, RuntimeError, Value}; use tap::lexer::Lexer; use tap::parser::Parser; +use tap::utils::pretty_print_tokens; -// --- TEST HELPERS --- - -/// Helper for tests that are expected to succeed. -/// It lexes, parses, and interprets the source, panicking with a rich error if any step fails. -/// Returns the final value of the last expression, if any. -fn eval_source(source: &str) -> Option { - let tokens = Lexer::new(source).tokenize().unwrap_or_else(|e| { - panic!( - "Lexing failed for source:\n{}\nError: {:?}", - source.trim(), - e - ); - }); - - let mut parser = Parser::new(&tokens, source); - let program = parser.parse_program().unwrap_or_else(|e| { - panic!( - "Parsing failed for source:\n{}\nError Report:\n{:?}", - source.trim(), - e +type AstProgram = tap::ast::Program; + +struct InterpretOutput { + pub result: Result, RuntimeError>, + pub ast: Option, + pub source: String, +} + +fn interpret_source_with_ast(source: &str) -> InterpretOutput { + let mut reporter = Reporter::new(); + let tokens = Lexer::new(source, &mut reporter) + .tokenize() + .unwrap_or_else(|e| { + eprintln!("Lexing failed for source:\n\"{}\"\nError: {:?}", source, e); + panic!("Lexing failed."); + }); + + if reporter.has_errors() { + eprintln!( + "Lexing failed with reporter errors for source:\n\"{}\"\nTokens: {}\nLexer Errors: {:?}", + source, + pretty_print_tokens(&tokens), + reporter.diagnostics ); - }); - - let interpreter = Interpreter::new(); - let env = Rc::new(RefCell::new(Environment::new())); - interpreter.interpret(&program, env).unwrap_or_else(|e| { - panic!( - "Interpretation failed for source:\n{}\nRuntime Error: {}", - source.trim(), - e + panic!("Lexing failed with reporter errors."); + } + + let mut parser = Parser::new(&tokens, &mut reporter); + let program_result = parser.parse_program(); + + if reporter.has_errors() { + eprintln!( + "Parsing failed with reporter errors for source:\n\"{}\"\nTokens: {}\nParser Errors: {:?}\nAST: {:#?}", + source, + pretty_print_tokens(&tokens), + reporter.diagnostics, + program_result ); - }) -} + panic!("Parsing failed with reporter errors."); + } -/// Helper for tests that are expected to fail at runtime. -/// It will panic if parsing fails or if the interpreter *succeeds*. -fn expect_runtime_error(source: &str) { - let tokens = Lexer::new(source).tokenize().unwrap(); - let mut parser = Parser::new(&tokens, source); - let program = parser.parse_program().unwrap(); - let interpreter = Interpreter::new(); - let env = Rc::new(RefCell::new(Environment::new())); - let result = interpreter.interpret(&program, env); - assert!( - result.is_err(), - "Expected a runtime error, but execution succeeded for source:\n{}", - source - ); -} + let program = program_result.expect("Parser failed unexpectedly but no errors reported."); + let mut interpreter = Interpreter::new(); + let interpretation_result = interpreter.interpret(&program); -// --- EXPRESSION & OPERATOR TESTS --- - -#[test] -fn test_literal_evaluation() { - assert_eq!(eval_source("42;").unwrap(), Value::Integer(42)); - assert_eq!( - eval_source("\"hello\";").unwrap(), - Value::String("hello".to_string()) - ); - assert_eq!(eval_source("true;").unwrap(), Value::Boolean(true)); - assert_eq!(eval_source("false;").unwrap(), Value::Boolean(false)); + InterpretOutput { + result: interpretation_result, + ast: Some(program), + source: source.to_string(), + } } -#[test] -fn test_arithmetic_expressions() { - assert_eq!(eval_source("10 + 5;").unwrap(), Value::Integer(15)); - assert_eq!(eval_source("10 - 5;").unwrap(), Value::Integer(5)); - assert_eq!(eval_source("10 * 5;").unwrap(), Value::Integer(50)); - assert_eq!(eval_source("10 / 5;").unwrap(), Value::Integer(2)); -} +#[cfg(test)] +mod interpreter_tests { + use super::*; -#[test] -fn test_operator_precedence() { - assert_eq!(eval_source("5 + 2 * 10;").unwrap(), Value::Integer(25)); - assert_eq!(eval_source("(5 + 2) * 10;").unwrap(), Value::Integer(70)); -} + // A new helper macro to reduce boilerplate in tests + macro_rules! assert_interpret_output_and_dump_ast { + ($source:expr, $expected:expr) => {{ + let output = interpret_source_with_ast($source); + // Explicitly type the expected value to help the compiler infer generic parameters for Result + let expected_val: Result, RuntimeError> = $expected; + if output.result != expected_val { + eprintln!("\n--- Test Assertion Failed ---"); + eprintln!("Source:\n```tap\n{}\n```", output.source); + // if let Some(ast) = output.ast { + // eprintln!("AST:\n{:#?}", ast); + // } else { + // eprintln!("AST: Not available due to parsing error."); + // } + eprintln!("Expected: {:?}", expected_val); // Use the explicitly typed variable here + eprintln!("Actual: {:?}", output.result); + eprintln!("--- End Test Assertion Failed ---"); + panic!("Assertion failed: Interpreter output did not match expected value. See above for details and AST dump."); + } + }}; + } -#[test] -fn test_comparison_expressions() { - assert_eq!(eval_source("10 > 5;").unwrap(), Value::Boolean(true)); - assert_eq!(eval_source("10 < 5;").unwrap(), Value::Boolean(false)); - assert_eq!(eval_source("10 == 10;").unwrap(), Value::Boolean(true)); - assert_eq!(eval_source("10 != 5;").unwrap(), Value::Boolean(true)); -} + // --- Small Snippets (Core Functionality) --- -#[test] -fn test_division_by_zero_error() { - expect_runtime_error("10 / 0;"); -} + #[test] + fn test_interpret_integer_literal() { + assert_interpret_output_and_dump_ast!("123;", Ok(Some(Value::Integer(123)))); + } -// --- VARIABLE & SCOPING TESTS --- + #[test] + fn test_interpret_float_literal() { + assert_interpret_output_and_dump_ast!("3.14;", Ok(Some(Value::Float(3.14)))); + } -#[test] -fn test_variable_assignment_and_retrieval() { - let source = "x = 42; x;"; - assert_eq!(eval_source(source).unwrap(), Value::Integer(42)); -} + #[test] + fn test_interpret_string_literal() { + assert_interpret_output_and_dump_ast!( + "\"hello\";", + Ok(Some(Value::String("hello".to_string()))) + ); + } -// --- CONTROL FLOW TESTS --- + #[test] + fn test_interpret_boolean_literal_true() { + assert_interpret_output_and_dump_ast!("true;", Ok(Some(Value::Boolean(true)))); + } -#[test] -fn test_if_statement() { - assert_eq!(eval_source("if true { 1; }").unwrap(), Value::Integer(1)); - assert_eq!(eval_source("if false { 1; }"), None); -} + #[test] + fn test_interpret_boolean_literal_false() { + assert_interpret_output_and_dump_ast!("false;", Ok(Some(Value::Boolean(false)))); + } -#[test] -fn test_if_else_statement() { - assert_eq!( - eval_source("if true { 1; } else { 2; }").unwrap(), - Value::Integer(1) - ); - assert_eq!( - eval_source("if false { 1; } else { 2; }").unwrap(), - Value::Integer(2) - ); -} + #[test] + fn test_interpret_none_literal() { + assert_interpret_output_and_dump_ast!("None;", Ok(Some(Value::Unit))); + } -#[test] -fn test_if_as_expression() { - let source = "x = if 1 > 0 { 100; } else { 200; }; x;"; - assert_eq!(eval_source(source).unwrap(), Value::Integer(100)); -} + #[test] + fn test_interpret_integer_addition() { + assert_interpret_output_and_dump_ast!("1 + 2;", Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_interpret_float_multiplication() { + assert_interpret_output_and_dump_ast!("2.5 * 2.0;", Ok(Some(Value::Float(5.0)))); + } -// --- FUNCTION AND CLOSURE TESTS --- + #[test] + fn test_interpret_integer_division() { + assert_interpret_output_and_dump_ast!("10 / 2;", Ok(Some(Value::Integer(5)))); + } -#[test] -fn test_basic_function_invocation() { - let source = r#" - func multiply(a, b) { - return a * b; - } - multiply(3, 4); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(12)); -} + #[test] + fn test_interpret_division_by_zero() { + assert_interpret_output_and_dump_ast!("10 / 0;", Err(RuntimeError::DivisionByZero)); + } + + #[test] + fn test_interpret_unary_minus_integer() { + assert_interpret_output_and_dump_ast!("-5;", Ok(Some(Value::Integer(-5)))); + } + + #[test] + fn test_interpret_unary_not_boolean() { + assert_interpret_output_and_dump_ast!("!true;", Ok(Some(Value::Boolean(false)))); + assert_interpret_output_and_dump_ast!("!false;", Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_interpret_unary_not_truthiness() { + assert_interpret_output_and_dump_ast!("!10;", Ok(Some(Value::Boolean(false)))); + assert_interpret_output_and_dump_ast!("!\"hello\";", Ok(Some(Value::Boolean(false)))); + assert_interpret_output_and_dump_ast!("!None;", Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_interpret_variable_declaration_no_type() { + let source = "x = 10; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_variable_declaration_with_type() { + let source = "x: int = 10; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_mutable_variable_declaration() { + let source = "mut x: int = 10; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_variable_in_expression() { + let source = "x = 5; x + 3;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(8)))); + } + + #[test] + fn test_interpret_multiple_variable_declarations() { + let source = "a = 1; b = 2; a * b;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_interpret_undefined_variable() { + let source = "x;"; + assert_interpret_output_and_dump_ast!( + source, + Err(RuntimeError::Type("Undefined variable: x".to_string())) + ); + } + + #[test] + fn test_interpret_binary_comparison_equal() { + assert_interpret_output_and_dump_ast!("1 == 1;", Ok(Some(Value::Boolean(true)))); + assert_interpret_output_and_dump_ast!("1 == 2;", Ok(Some(Value::Boolean(false)))); + } + + #[test] + fn test_interpret_binary_logical_and_error() { + assert_interpret_output_and_dump_ast!( + "1 && 0;", + Err(RuntimeError::Type( + "Type mismatch in binary operation".into() + )) + ); + } + + #[test] + fn test_interpret_mixed_types_arithmetic_error() { + let source = "10 + 3.5;"; + assert_interpret_output_and_dump_ast!( + source, + Err(RuntimeError::Type( + "Type mismatch in binary operation".to_string() + )) + ); + } + + #[test] + fn test_interpret_parenthesized_expression() { + assert_interpret_output_and_dump_ast!("(2 + 3) * 4;", Ok(Some(Value::Integer(20)))); + } + + // --- Tests for implemented features --- + + #[test] + fn test_interpret_type_declaration() { + assert_interpret_output_and_dump_ast!( + "type Option = Some(int) | None;", + Ok(Some(Value::Unit)) + ); + } + + #[test] + fn test_interpret_function_definition() { + let source = "add(a: int, b: int): int = { a + b }; add(2, 3);"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(5)))); + } + + #[test] + fn test_interpret_if_expression() { + assert_interpret_output_and_dump_ast!( + "if (true) { 1 } else { 0 };", + Ok(Some(Value::Integer(1))) + ); + } + + #[test] + fn test_interpret_while_expression() { + let source = "mut i = 0; while (i < 3) { i = i + 1; }; i;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_interpret_for_expression() { + let source = "mut sum = 0; for i in [1, 2] { sum = sum + i; }; sum;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_interpret_match_expression() { + assert_interpret_output_and_dump_ast!( + "match (1) { | _ => 1 };", + Ok(Some(Value::Integer(1))) + ); + } + + #[test] + fn test_interpret_lambda_expression() { + let source = "f = (x: int) => x + 1; f(5);"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(6)))); + } + + #[test] + fn test_interpret_record_literal() { + let source = "p = { x: 1, y: 2 }; p.x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_list_literal() { + let source = "numbers = [1, 2, 3]; numbers[0];"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_field_access() { + let source = "p = { x: 10, y: 20 }; p.x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_list_access() { + let source = "arr = [1, 2, 3]; arr[0];"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_function_call() { + let source = "my_func(): int = { 0 }; my_func();"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(0)))); + } + + #[test] + fn test_interpret_compound_assignment() { + let source = "mut x = 10; x += 5; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(15)))); + } + + #[test] + fn test_interpret_compound_assignment_add() { + let source = "mut x = 10; x += 5; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(15)))); + } + + #[test] + fn test_interpret_compound_assignment_subtract() { + let source = "mut x = 10; x -= 5; x;"; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(5)))); + } + + // --- More Full-Fledged Examples --- + + #[test] + fn test_interpret_complex_arithmetic_with_variables() { + let source = " + x = 10; + y = 5; + result = (x + y) * 2 / 3; + result; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_complex_nested_expression() { + let source = " + complex_expr(): int = { + if (true) { + match (None) { + | _ => { 1 } + } + } else { + -1 + } + }; + complex_expr(); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_multiple_statements() { + let source = " + val1 = 1; + val2 = 2; + val3 = val1 + val2; + val3; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_interpret_complex_precedence() { + assert_interpret_output_and_dump_ast!("1 + 2 * 3 - 4 / 2;", Ok(Some(Value::Integer(5)))); + } + + #[test] + fn test_interpret_nested_parentheses() { + assert_interpret_output_and_dump_ast!("((1 + 1) * 2) - 3;", Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_unary_minus_in_expression() { + assert_interpret_output_and_dump_ast!("10 + (-5);", Ok(Some(Value::Integer(5)))); + } + + #[test] + fn test_interpret_unary_plus() { + assert_interpret_output_and_dump_ast!("+10;", Ok(Some(Value::Integer(10)))); + assert_interpret_output_and_dump_ast!("+(2.5);", Ok(Some(Value::Float(2.5)))); + } + + #[test] + fn test_interpret_chain_variable_assignment_then_access() { + let source = " + a = 5; + b = a; + b; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(5)))); + } + + #[test] + fn test_interpret_variable_shadowing_not_supported_yet() { + let source = " + x = 10; + x = 20; + x; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(20)))); + } + + // --- Additional Tests for Implemented Features --- + + #[test] + fn test_interpret_record_type_declaration() { + assert_interpret_output_and_dump_ast!( + "type Point = { x: int, y: int };", + Ok(Some(Value::Unit)) + ); + } + + #[test] + fn test_interpret_generic_type_declaration() { + assert_interpret_output_and_dump_ast!("type IntList = [int];", Ok(Some(Value::Unit))); + } + + #[test] + fn test_interpret_if_else_if_expression() { + let source = " + val = if (false) { 1 } else if (true) { 2 } else { 3 }; + val; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_interpret_while_loop_with_block() { + let source = " + mut i = 0; + while (i < 2) { + i = i + 1; + }; + i; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_interpret_for_loop_identifier_pattern() { + let source = " + mut sum = 0; + for i in [1, 2, 3] { + sum = sum + i; + }; + sum; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(6)))); + } + + #[test] + fn test_interpret_for_loop_wildcard_pattern() { + let source = " + mut count = 0; + for _ in [1, 2] { + count = count + 1; + }; + count; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_interpret_match_expression_with_variant() { + let source = " + type Option = Some(int) | None; + opt_val = Some(10); + match (opt_val) { + | Some(x) => x, + | None => 0 + }; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_match_expression_with_multiple_arms() { + let source = " + type Result = Ok(string) | Error(int); + res_val = Ok(\"success\"); + match (res_val) { + | Ok(s) => s, + | Error(e) => \"failure\" + }; + "; + assert_interpret_output_and_dump_ast!( + source, + Ok(Some(Value::String("success".to_string()))) + ); + } + + #[test] + fn test_interpret_lambda_assignment() { + let source = " + my_lambda = (x: int) => x * 2; + my_lambda(5); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_record_literal_and_access() { + let source = " + p = { x: 10, y: 20 }; + p.x; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + #[ignore] + fn test_method_call_on_record_member() { + let source = " + type Circle = { + radius: float, + area(this): float = { 3.14 * this.radius * this.radius }, + } + mut c: Circle = { radius: 5.0 }; + c.radius = 10.0; + c.area(); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Float(314.0)))); + } + + #[test] + fn test_interpret_list_literal_and_access() { + let source = " + arr = [1, 2, 3]; + arr[0]; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_interpret_factorial_function() { + let source = " + factorial(n: int): int = { + mut result = 1; + mut i = 1; + while (i <= n) { + result = result * i; + i = i + 1; + } + result + }; + factorial(5); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(120)))); + } + + #[test] + fn test_interpret_access_demo() { + let source = " + add(a: int, b: int): int = { a + b } + access_demo(): int = { + p = { x: 10, y: 20 }; + x_val = p.x; + arr = [1, 2, 3]; + first = arr[0]; + result = add(x_val, first); + result + }; + access_demo(); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(11)))); + } + + #[test] + fn test_interpret_variant_construction() { + let source = " + type Option = Some(int) | None; + some_value = Some(42); + no_value = None; + some_value; + "; + // Should construct a variant value + // assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Variant("Some".to_string(), Box::new(Value::Integer(42)))))); + // The original test just checked `is_ok()`, which this macro will handle if the output matches. + let output = interpret_source_with_ast(source); + assert!(output.result.is_ok()); + } + + #[test] + fn test_interpret_block_as_final_expression_in_function() { + let source = " + my_func(): int = { + { + a = 1; + a + 1 + } + }; + my_func(); + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_interpret_arithmetic_with_multiple_variables() { + let source = " + v1 = 10; + v2 = 5; + v3 = 2; + result = v1 + v2 * v3; + result; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(20)))); + } + + #[test] + fn test_interpret_boolean_expressions_with_variables_error() { + let source = " + is_valid = true; + count = 10; + check = is_valid && count; + check; + "; + assert_interpret_output_and_dump_ast!( + source, + Err(RuntimeError::Type( + "Type mismatch in binary operation".to_string() + )) + ); + } + + #[test] + fn test_basic_range_inclusive() { + let source = " + mut sum = 0; + for i in 0..=5 { + sum += i; + } + sum; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(15)))); + } + + #[test] + fn test_basic_range_exclusive() { + let source = " + mut sum = 0; + for i in 0..<5 { + sum += i; + } + sum; + "; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_interpret_dfs_example() { + let dfs_source = r#" + dfs_visit_and_count(graph_adj: [[int]], u: int, visited_status: [bool], num_nodes: int): int = { + if (u < 0 || u >= num_nodes) { + return 0; + }; + + if (visited_status[u]) { + return 0; + }; + + visited_status[u] = true; + + mut count = 1; + + for v in graph_adj[u] { + count = count + dfs_visit_and_count(graph_adj, v, visited_status, num_nodes); + }; + + count + }; + + run_dfs(graph: [[int]], start_node: int, graph_size: int): int = { + mut visited_nodes: [bool] = [false, false, false, false]; + dfs_visit_and_count(graph, start_node, visited_nodes, graph_size) + }; -#[test] -fn test_implicit_return_from_function() { - let source = r#" - func add(a, b) { - a + b; + example_graph = [[1, 2], [0, 3], [0], [1]]; + num_example_nodes = 4; + + run_dfs(example_graph, 0, num_example_nodes); + "#; + + assert_interpret_output_and_dump_ast!(dfs_source, Ok(Some(Value::Integer(4)))); + } + + #[test] + fn test_interpret_recursive_fibonacci() { + let fib_source = r#" + fib(n: int): int = { + if (n <= 1) { + n + } else { + fib(n - 1) + fib(n - 2) + } + } + + fib(6); + "#; + + assert_interpret_output_and_dump_ast!(fib_source, Ok(Some(Value::Integer(8)))); + } + + #[test] + fn test_interpret_iterative_binary_search() { + let binary_search_source = r#" + binary_search(list: [int], target: int): int = { + mut low = 0; + mut high = list.length() - 1; + + while (low <= high) { + mut mid = low + (high - low) / 2; + + if (list[mid] == target) { + return mid; + } else if (list[mid] < target) { + low = mid + 1; + } else { + high = mid - 1; + } + }; + + return -1; + }; + + sorted_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + target_value = 7; + binary_search(sorted_list, target_value); + "#; + + assert_interpret_output_and_dump_ast!(binary_search_source, Ok(Some(Value::Integer(6)))); + } + + #[test] + #[ignore] + fn test_interpret_bubble_sort() { + let bubble_sort_source = r#" + bubble_sort(list_to_sort: [int]) = { + mut n = list_to_sort.length(); + mut i = 0; + while (i < n - 1) { + mut j = 0; + while (j < n - i - 1) { + if (list_to_sort[j] > list_to_sort[j+1]) { + mut temp = list_to_sort[j]; + list_to_sort[j] = list_to_sort[j+1]; + list_to_sort[j+1] = temp; + }; + j = j + 1; + }; + i = i + 1; + }; + }; + + mut unsorted = [5, 1, 4, 2, 8]; + bubble_sort(unsorted); + unsorted == [1, 2, 4, 5, 8]; + "#; + + assert_interpret_output_and_dump_ast!(bubble_sort_source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_interpret_list_mapping_with_lambda() { + let map_lambda_source = r#" + map(f: int -> int, lst: [int]): [int] = { + mut result_list: [int] = []; + for element in lst { + result_list = result_list.push(f(element)); + }; + return result_list; + }; + + double = (x: int) => x * 2; + + input_list = [1, 2, 3]; + mapped_list = map(double, input_list); + mapped_list == [2, 4, 6]; + "#; + + assert_interpret_output_and_dump_ast!(map_lambda_source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_interpret_record_factory_and_access() { + let record_source = r#" + type Point = { x: int, y: int }; + + make_point(x_val: int, y_val: int): Point = { + return { x: x_val, y: y_val }; + } + + origin = make_point(0, 0); + my_point = make_point(10, 20); + + my_point.x; + "#; + + assert_interpret_output_and_dump_ast!(record_source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_snippet_fizzbuzz_last_element() { + let source = r#" + fizzbuzz_last(n: int): string = { + mut results: [string] = []; + mut i = 1; + while (i <= n) { + if (i % 15 == 0) { + results = results.push("FizzBuzz"); + } else if (i % 3 == 0) { + results = results.push("Fizz"); + } else if (i % 5 == 0) { + results = results.push("Buzz"); + } else { + results = results.push(i.to_string()); + }; + i = i + 1; + } + results[n - 1] + } + fizzbuzz_last(5); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::String("Buzz".to_string())))); + } + + #[test] + fn test_snippet_iterative_factorial() { + let source = r#" + factorial(n: int): int = { + mut result = 1; + mut i = 1; + while (i <= n) { + result = result * i; + i = i + 1; + } + result + } + factorial(5); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(120)))); + } + + #[test] + fn test_snippet_prime_number_checker() { + let source = r#" + is_prime(n: int): bool = { + if (n <= 1) { + return false; + } + mut i = 2; + while (i * i <= n) { + if (n % i == 0) { + return false; + } + i = i + 1; + } + return true; + } + is_prime(7); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_snippet_gcd_euclidean_algorithm() { + let source = r#" + gcd(a: int, b: int): int = { + mut x = a; + mut y = b; + while (y != 0) { + mut temp = y; + y = x % y; + x = temp; + } + return x; + } + gcd(48, 18); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(6)))); + } + + #[test] + fn test_snippet_list_reversal_first_element() { + let source = r#" + reverse_list(lst: [int]): [int] = { + mut reversed: [int] = []; + mut i = lst.length() - 1; + while (i >= 0) { + reversed = reversed.append(lst[i]); + i = i - 1; + } + reversed + } + reverse_list([1, 2, 3])[0]; + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_snippet_count_occurrences_in_list() { + let source = r#" + count_occurrences(lst: [int], target: int): int = { + mut count = 0; + for element in lst { + if (element == target) { + count = count + 1; + } + } + count + }; + count_occurrences([1, 2, 1, 3, 1], 1); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_snippet_string_palindrome_checker() { + let source = r#" + is_palindrome(s: string): bool = { + mut len = s.length(); + if (len <= 1) { + return true; + } + mut i = 0; + while (i < len / 2) { + if (s.substring(i, 1) != s.substring(len - 1 - i, 1)) { + return false; + } + i = i + 1; + } + return true; + } + is_palindrome("madam"); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_snippet_sum_type_state_machine() { + let source = r#" + type State = Start | Running | Paused | End; + + get_next_state_name(current: State): string = { + match (current) { + | Start => "Running", + | Running => "Paused", + | Paused => "End", + | End => "Start" + } + } + get_next_state_name(Start); + "#; + assert_interpret_output_and_dump_ast!( + source, + Ok(Some(Value::String("Running".to_string()))) + ); + } + + #[test] + fn test_snippet_list_filtering_lambda_predicate() { + let source = r#" + filter_list(predicate: int -> bool, lst: [int]): [int] = { + mut filtered: [int] = []; + for element in lst { + if (predicate(element)) { + filtered = filtered.push(element); + } + }; + return filtered; + } + + is_even = (x: int) => x % 2 == 0; + input_list = [1, 2, 3, 4, 5]; + filter_list(is_even, input_list)[0]; + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); + } + + #[test] + fn test_snippet_find_maximum_element_in_list() { + let source = r#" + find_max(lst: [int]): int = { + if (lst.length() == 0) { + return -1; + }; + mut max_val = lst[0]; + mut i = 1; + while (i < lst.length()) { + if (lst[i] > max_val) { + max_val = lst[i]; + }; + i = i + 1; + } + return max_val; + } + find_max([10, 5, 99, 23, 7]); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(99)))); + } + + #[test] + fn test_snippet_average_of_list_of_numbers() { + let source = r#" + average(lst: [int]): float = { + if (lst.length() == 0) { + return 0.0; + } + mut sum_val = 0; + for element in lst { + sum_val = sum_val + element; + } + return sum_val.to_float() / lst.length(); + } + average([1, 2, 3, 4, 5]); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Float(3.0)))); + } + + #[test] + fn test_snippet_calculate_distance_squared_between_points() { + let source = r#" + type Point = { x: int, y: int }; + + distance_squared(p1: Point, p2: Point): float = { + mut dx = p1.x - p2.x; + mut dy = p1.y - p2.y; + return (dx * dx + dy * dy).to_float(); + }; + distance_squared({x:0, y:0}, {x:3, y:4}); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Float(25.0)))); + } + + #[test] + fn test_snippet_simple_vowel_counter() { + let source = r#" + count_vowels(s: string): int = { + mut count = 0; + mut i = 0; + while (i < s.length()) { + mut char_str = s.substring(i, 1); + if (char_str == "a" || char_str == "e" || char_str == "i" || char_str == "o" || char_str == "u") { + count = count + 1; + }; + i = i + 1; + }; + return count; + }; + count_vowels("hello world"); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_snippet_collatz_conjecture_step_function() { + let source = r#" + collatz_step(n: int): int = { + if (n % 2 == 0) { + n / 2 + } else { + return n * 3 + 1; + } + }; + collatz_step(10); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(5)))); + } + + #[test] + fn test_snippet_convert_celsius_to_fahrenheit() { + let source = r#" + celsius_to_fahrenheit(celsius: float): float = { + celsius * 9.0 / 5.0 + 32.0 + }; + celsius_to_fahrenheit(0.0); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Float(32.0)))); + } + + #[test] + fn test_snippet_find_unique_elements_first_element() { + let source = r#" + contains(lst: [int], target: int): bool = { + for element in lst { + if (element == target) { + return true; + } + }; + return false; + } + + unique_elements(lst: [int]): [int] = { + mut uniques: [int] = []; + for element in lst { + if (!contains(uniques, element)) { + uniques = uniques.append(element); + }; + }; + return uniques; + }; + unique_elements([1, 2, 2, 3, 1])[0]; + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_snippet_sum_until_five_or_max() { + let source = r#" + sum_until_five_or_max(max_val: int): int = { + mut sum = 0; + mut i = 1; + while (i <= max_val) { + if (i == 5) { + break; + } else { + sum = sum + i; + i = i + 1; + }; + }; + return sum; + }; + sum_until_five_or_max(10); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(10)))); + } + + #[test] + fn test_snippet_match_day_name_default() { + let source = r#" + get_day_name(day_num: int): string = { + match (day_num) { + | 1 => "Monday", + | 2 => "Tuesday", + | 3 => "Wednesday", + | 4 => "Thursday", + | 5 => "Friday", + | 6 => "Saturday", + | _ => "Sunday" + } + }; + get_day_name(3); + "#; + assert_interpret_output_and_dump_ast!( + source, + Ok(Some(Value::String("Wednesday".to_string()))) + ); + } + + #[test] + fn test_snippet_match_day_name_no_pipe() { + let source = r#" + get_day_name(day_num: int): string = { + match (day_num) { + 1 => "Monday", + 2 => "Tuesday", + 3 => "Wednesday", + 4 => "Thursday", + 5 => "Friday", + 6 => "Saturday", + _ => "Sunday" + } + }; + get_day_name(7); + "#; + assert_interpret_output_and_dump_ast!( + source, + Ok(Some(Value::String("Sunday".to_string()))) + ); + } + + #[test] + fn test_snippet_calculate_nth_power_iterative() { + let source = r#" + power(base, exponent: int) = { + if (exponent < 0) { + return 0; + }; + if (exponent == 0) { + return 1; + }; + mut result = 1; + mut i = 0; + while (i < exponent) { + result = result * base; + i = i + 1; + }; + return result; + }; + power(5, 3); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(125)))); + } + + #[test] + fn test_snippet_map_records_to_list_of_fields_first_element() { + let source = r#" + type Point = { x: int, y: int }; + + map_points_to_x(points: [Point]): [int] = { + mut x_coords: [int] = []; + for p in points { + x_coords = x_coords.push(p.x); + } + return x_coords; + }; + + list_of_points = [{x:1, y:10}, {x:3, y:30}, {x:5, y:50}]; + map_points_to_x(list_of_points)[1]; + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_snippet_aoc_2025_day1_1() { + let source = r#" + solve(): int = { + // Hardcoded example turns (until we implement I/O) + turns = [-68, -30, 48, -5, 60, -55, -1, -99, 14, -82]; + + mut res = 0; + mut dial = 50; + + for turn in turns { + dial = dial + turn; + dial = dial % 100; + + if (dial == 0) { + res = res + 1; + } + } + + res + }; + + solve(); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_snippet_aoc_2025_day1_1_input_parsing() { + let source = r#" + get_file_content(): string = { + "L68\nL30\nR48\nL5\nR60\nL55\nL1\nL99\nR14\nL82" + } + + // Parse a line like "R60" or "L30" into a turn value + // R becomes positive, L becomes negative + parse_turn(line: string): int = { + direction = line.char_at(0); + len = line.length(); + value_str = line.substring(1, len - 1); + value = value_str.parse_int(); + + if (direction == "L") { + -value + } else { + value + } + } + + // Parse all lines into a list of turns + get_turns(content: string): [int] = { + lines = content.split("\n"); + mut turns: [int] = []; + + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + turn = parse_turn(trimmed); + turns = turns.push(turn); + } + } + + turns + } + + solve(): int = { + content = get_file_content(); + turns = get_turns(content); + + mut res = 0; + mut dial = 50; + + for turn in turns { + dial = dial + turn; + dial = dial % 100; + + // Check if dial reached zero + if (dial == 0) { + res = res + 1; + } + } + + // Return the count of times dial reached zero + res + } + + solve(); + "#; + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(3)))); + } + + #[test] + fn test_file_processing_line_by_line() { + let source = r#" + // Create a file with numbers + file = open("/tmp/test_numbers.txt", "w"); + file.write_line("10"); + file.write_line("20"); + file.write_line("30"); + file.close(); + + // Read and sum the numbers + file2 = open("/tmp/test_numbers.txt", "r"); + lines = file2.read_lines(); + file2.close(); + + mut sum = 0; + for line in lines { + num = line.trim().parse_int(); + sum = sum + num; + }; + + sum; + "#; + + // Cleanup before test + std::fs::remove_file("/tmp/test_numbers.txt").ok(); + + let result = interpret_source_with_ast(source).result; + + // Cleanup after test + std::fs::remove_file("/tmp/test_numbers.txt").ok(); + + assert_eq!(result, Ok(Some(Value::Integer(60)))); + } + #[test] + fn test_file_error_invalid_mode() { + let source = r#" + file = open("test.txt", "invalid"); + "#; + + let result = interpret_source_with_ast(source).result; + assert!(matches!(result, Err(RuntimeError::Type(_)))); + } + + #[test] + fn test_args_out_of_bounds() { + let source = r#" + args.get(100); + "#; + + let mut reporter = Reporter::new(); + let tokens = Lexer::new(source, &mut reporter).tokenize().unwrap(); + let mut parser = Parser::new(&tokens, &mut reporter); + let program = parser.parse_program().unwrap(); + + let mut interpreter = Interpreter::new_with_args(vec!["program".to_string()]); + let result = interpreter.interpret(&program); + + assert_eq!(result, Ok(Some(Value::Unit))); + } + + #[test] + fn test_args_missing_option() { + let source = r#" + args.get_option("--missing"); + "#; + + let mut reporter = Reporter::new(); + let tokens = Lexer::new(source, &mut reporter).tokenize().unwrap(); + let mut parser = Parser::new(&tokens, &mut reporter); + let program = parser.parse_program().unwrap(); + + let mut interpreter = Interpreter::new_with_args(vec!["program".to_string()]); + let result = interpreter.interpret(&program); + + assert_eq!(result, Ok(Some(Value::Unit))); + } + + #[test] + fn test_hashmap_has_key() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100); + scores.insert("Bob", 85); + + res: int = if (scores.has("Alice")) { + 1 + } else { + 0 + }; + res + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(1)))); + } + + #[test] + fn test_hashmap_get() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100); + scores.insert("Bob", 85); + + scores.get("Alice"); + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(100)))); + } + + #[test] + fn test_hashmap_entries() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100); + scores.insert("Bob", 85); + + res: int = 0; + for entry in scores.entries() { + if (entry.key == "Alice") { + res += entry.value; + } + if (entry.key == "Bob") { + res += entry.value; + } + if (entry.key == "Mallory") { + res += 12345; + } + } + res; + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(185)))); + } + + #[test] + fn test_hashmap_keys() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100); + scores.insert("Bob", 85); + + res: int = 0; + keys: [string] = scores.keys(); + keys.length() == 2 && keys.contains("Alice") && keys.contains("Bob"); + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_hashmap_values() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100); + scores.insert("Bob", 85); + + res: int = 0; + values: [int] = scores.values(); + values.contains(100) && values.contains(85); + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Boolean(true)))); + } + + #[test] + fn test_hashmap_method_chaining() { + let source = r#" + mut scores = Map(); + scores.insert("Alice", 100).insert("Bob", 85); + + scores.get("Bob") + scores.get("Alice"); + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(185)))); + } + + #[test] + fn test_aoc_2025_day1_part1() { + let source = r#" + get_file_content(): string = { + "R50\nR10\nL150\nR20\nL25\nR100\nL55" } - add(7, 8); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(15)); -} -#[test] -fn test_recursive_function_factorial() { - let source = r#" - func factorial(n) { - if n == 0 { - return 1; + // Parse a line like "R60" or "L30" into a turn value + // R becomes positive, L becomes negative + parse_turn(line: string): int = { + direction = line.char_at(0); + len = line.length(); + value_str = line.substring(1, len - 1); + value = value_str.parse_int(); + + if (direction == "L") { + -value + } else { + value } - return n * factorial(n - 1); } - factorial(5); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(120)); -} -#[test] -fn test_lambda_evaluation() { - let source = r"(\x. x + 1)(5);"; - assert_eq!(eval_source(source).unwrap(), Value::Integer(6)); -} + // Parse all lines into a list of turns + get_turns(content: string): [int] = { + lines = content.split("\n"); + mut turns: [int] = []; -#[test] -fn test_closure_captures_environment() { - let source = r#" - x = 10; - f = \y. x + y; - f(5); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(15)); -} + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + turn = parse_turn(trimmed); + turns.push(turn); + } + } -#[test] -fn test_nested_lambdas_and_currying() { - let source = r#" - add_x = \x. \y. x + y; - add_five = add_x(5); - add_five(3); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(8)); -} + turns + } -// --- DATA STRUCTURE TESTS --- + solve(): int = { + content = get_file_content(); + turns = get_turns(content); -#[test] -fn test_struct_instantiation_and_access() { - let source = r#" - Point : struct { x: int, y: int }; - p = Point { x: 1, y: 2 }; - p.x; - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(1)); -} + mut res = 0; + mut dial = 50; -#[test] -fn test_struct_field_modification() { - let source = r#" - Point : struct { x: int, y: int }; - p = Point { x: 1, y: 2 }; - p.y = 99; - p.y; - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(99)); -} + for turn in turns { + dial += turn; + dial %= 100; + + // Check if dial reached zero + if (dial == 0) { + res = res + 1; + } + } -#[test] -fn test_function_with_struct_arg() { - let source = r#" - Point : struct { x: int, y: int }; - func get_x(p) { - return p.x; + // Return the count of times dial reached zero + res } - p = Point { x: 10, y: 20 }; - get_x(p); - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(10)); -} -#[test] -fn test_enum_variant_creation() { - let source = r#" - Color : enum { Red, Green, Blue }; - c = Color::Red; - c; - "#; - let result = eval_source(source).unwrap(); - if let Value::EnumVariant(variant) = result { - assert_eq!(variant.enum_name, "Color"); - assert_eq!(variant.variant_name, "Red"); - } else { - panic!("Expected an enum variant, got {:?}", result); + solve() + "#; + // The dial is set to 0 after first move, and then again after the last move + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(2)))); } -} -#[test] -fn test_struct_with_enum_field() { - let source = r#" - Status : enum { Active, Inactive }; - User : struct { name: str, status: Status }; - u = User { name: "Alice", status: Status::Active }; - u.status; - "#; - let result = eval_source(source).unwrap(); - if let Value::EnumVariant(variant) = result { - assert_eq!(variant.enum_name, "Status"); - assert_eq!(variant.variant_name, "Active"); - } else { - panic!("Expected an enum variant, got {:?}", result); + #[test] + fn test_aoc_2025_day1_part2() { + let source = r#" + get_file_content(): string = { + "R50\nR10\nL150\nR20\nL25\nR100" + } + + // Parse a line like "R60" or "L30" into a turn value (int) + // R becomes positive, L becomes negative + parse_turn(line: string): int = { + len = line.length(); + if (len == 0) { + return 0; + } + direction = line.char_at(0); + value_str = line.substring(1, len - 1); + value = value_str.parse_int(); + + if (direction == "L") { + -value + } else { + value + } + } + + // Parse all lines into a list of turns + get_turns(content: string): [int] = { + lines = content.split("\n"); + mut turns: [int] = []; + + for line in lines { + trimmed = line.trim(); + if (trimmed.length() > 0) { + turn = parse_turn(line); + turns.push(turn); + } + } + turns + } + + solve(): int = { + content = get_file_content(); + turns = get_turns(content); + + mut res = 0; + mut dial = 50; + + for turn in turns { + mut distance = turn; + mut sign = 1; + if (turn < 0) { + distance = -turn; + sign = -1; + } + + // Add all the crosses due to full 360deg rotations + res += distance / 100; + + // Check for a cross due to remaining part of a full turn + remainder = distance % 100; + if (turn > 0) { // Right turn + if (dial + remainder >= 100) { + res += 1; + } + } else { // Left turn + if (dial > 0 && dial - remainder <= 0) { + res += 1; + } + } + + // Update dial's position to its end position + // (we can skip full turns and only use the remainder) + dial += (remainder * sign); + dial = (dial % 100 + 100) % 100; // Keep dial in [0, 99] + } + + res + } + + solve(); + "#; + + assert_interpret_output_and_dump_ast!(source, Ok(Some(Value::Integer(4)))); } -} -// --- LIST TESTS (assuming list support is partial) --- + #[test] + fn test_aoc_2025_day4_part2() { + // Test that closures capture variables correctly (by reference) + let source = r#" + get_file_content(): string = { + "@.@.@@@.@.\n.@@@@@@@@.\n@.@@@.@@@@\n.@.@.@.@@@\n.@@@@@@@.@\n@@.@@@@.@@\n@.@@@@..@.\n@@@@@.@.@@\n@@@.@.@.@@\n..@@.@@@@.\n" + } -#[test] -fn test_list_creation_and_access() { - let source = r#" - my_list = [10, 20, 30]; - my_list[1]; - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(20)); -} + parse_line(line: string): [int] = { + mut digits: [int] = []; + chars = line.split(""); + for ch in chars { + if ch == "" { + continue; + } + if ch == "." { + digits.push(0); + } else { + digits.push(1); + } + } + digits + } -#[test] -fn test_list_access_out_of_bounds() { - let source = r#" - my_list = [10, 20, 30]; - my_list[99]; - "#; - expect_runtime_error(source); -} + // Parse all lines into a list of battery banks + get_lines(content: string) = { + mut lines = []; + lines = content.split("\n"); -#[test] -#[ignore] // Enable when list modification is implemented -fn test_list_modification() { - let source = r#" - my_list = [10, 20, 30]; - my_list[1] = 25; - my_list[1]; - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(25)); -} + mut result = []; + for line in lines { + trimmed = line.trim(); + if trimmed.length() > 0 { + line = parse_line(trimmed); + result.push(line); + } + } -// --- FUTURE FEATURE TESTS (IGNORED) --- - -#[test] -#[ignore] -fn test_match_statement_with_enums() { - let source = r#" - Status : enum { Active, Inactive }; - s = Status::Active; - result = match s { - Status::Active => 1, - Status::Inactive => 0 - }; - result; - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(1)); -} + result + } -#[test] -#[ignore] -fn test_while_loop() { - let source = r#" - x = 5; - y = 0; - while x > 0 { - y = y + x; - x = x - 1; - } - y; // 5 + 4 + 3 + 2 + 1 = 15 - "#; - assert_eq!(eval_source(source).unwrap(), Value::Integer(15)); + is_valid(x: int, y: int, m: int, n: int): bool = { + return x >= 0 && x < m && y >= 0 && y < n; + } + + can_access(x: int, y: int, positions: [[int]]): bool = { + m = positions.length(); + n = positions[0].length(); + if (!is_valid(x, y, m, n)) { + return false; + } + + mut count = 0; + for dx in [-1, 0, 1] { + for dy in [-1, 0, 1] { + if dx == 0 && dy == 0 { + continue; + } + nx = x + dx; + ny = y + dy; + if !is_valid(nx, ny, m, n) { + continue; + } + if is_valid(nx, ny, m, n) && positions[nx][ny] == 1 { // TODO: Fix `&&` short-circuiting + count += 1; + if (count >= 4) { + return false; + } + } + } + } + true + } + + solve(): int = { + content = get_file_content(); + mut lines = get_lines(content); + + mut res = 0; + + pass(grid: [[int]]): [[int]] = { + mut to_remove = []; + m = grid.length(); + n = grid[0].length(); + for i in 0..= 18 { +status : string = if age >= 18 { "Adult" } else { "Minor" diff --git a/tests/programs/dodaj.tap b/tests/legacy_programs_in_old_syntax/dodaj.tap similarity index 64% rename from tests/programs/dodaj.tap rename to tests/legacy_programs_in_old_syntax/dodaj.tap index a29049e..feca948 100644 --- a/tests/programs/dodaj.tap +++ b/tests/legacy_programs_in_old_syntax/dodaj.tap @@ -1,4 +1,4 @@ -funkcja dodaj(a, b) { +dodaj(a, b) = { zwróć a + b; } diff --git a/tests/programs/enum.tap b/tests/legacy_programs_in_old_syntax/enum.tap similarity index 100% rename from tests/programs/enum.tap rename to tests/legacy_programs_in_old_syntax/enum.tap diff --git a/tests/programs/fib.tap b/tests/legacy_programs_in_old_syntax/fib.tap similarity index 100% rename from tests/programs/fib.tap rename to tests/legacy_programs_in_old_syntax/fib.tap diff --git a/tests/programs/fib_lambda.tap b/tests/legacy_programs_in_old_syntax/fib_lambda.tap similarity index 100% rename from tests/programs/fib_lambda.tap rename to tests/legacy_programs_in_old_syntax/fib_lambda.tap diff --git a/tests/programs/hello.tap b/tests/legacy_programs_in_old_syntax/hello.tap similarity index 100% rename from tests/programs/hello.tap rename to tests/legacy_programs_in_old_syntax/hello.tap diff --git a/tests/programs/hi_func.tap b/tests/legacy_programs_in_old_syntax/hi_func.tap similarity index 100% rename from tests/programs/hi_func.tap rename to tests/legacy_programs_in_old_syntax/hi_func.tap diff --git a/tests/programs/inline_lambda.tap b/tests/legacy_programs_in_old_syntax/inline_lambda.tap similarity index 100% rename from tests/programs/inline_lambda.tap rename to tests/legacy_programs_in_old_syntax/inline_lambda.tap diff --git a/tests/programs/iterative_fib.tap b/tests/legacy_programs_in_old_syntax/iterative_fib.tap similarity index 100% rename from tests/programs/iterative_fib.tap rename to tests/legacy_programs_in_old_syntax/iterative_fib.tap diff --git a/tests/programs/lambda.tap b/tests/legacy_programs_in_old_syntax/lambda.tap similarity index 100% rename from tests/programs/lambda.tap rename to tests/legacy_programs_in_old_syntax/lambda.tap diff --git a/tests/programs/lambdas2.tap b/tests/legacy_programs_in_old_syntax/lambdas2.tap similarity index 100% rename from tests/programs/lambdas2.tap rename to tests/legacy_programs_in_old_syntax/lambdas2.tap diff --git a/tests/programs/list_sum.tap b/tests/legacy_programs_in_old_syntax/list_sum.tap similarity index 100% rename from tests/programs/list_sum.tap rename to tests/legacy_programs_in_old_syntax/list_sum.tap diff --git a/tests/programs/match.tap b/tests/legacy_programs_in_old_syntax/match.tap similarity index 100% rename from tests/programs/match.tap rename to tests/legacy_programs_in_old_syntax/match.tap diff --git a/tests/programs/option_type.tap b/tests/legacy_programs_in_old_syntax/option_type.tap similarity index 100% rename from tests/programs/option_type.tap rename to tests/legacy_programs_in_old_syntax/option_type.tap diff --git a/tests/programs/silnia.tap b/tests/legacy_programs_in_old_syntax/silnia.tap similarity index 100% rename from tests/programs/silnia.tap rename to tests/legacy_programs_in_old_syntax/silnia.tap diff --git a/tests/programs/structs.tap b/tests/legacy_programs_in_old_syntax/structs.tap similarity index 100% rename from tests/programs/structs.tap rename to tests/legacy_programs_in_old_syntax/structs.tap diff --git a/tests/lexer_tests.rs b/tests/lexer_tests.rs new file mode 100644 index 0000000..c0d4938 --- /dev/null +++ b/tests/lexer_tests.rs @@ -0,0 +1,354 @@ +use tap::ast::Span; +use tap::diagnostics::Reporter; +use tap::lexer::{Lexer, Token, TokenType}; + +#[test] +fn test_single_char_tokens() { + let source = "+-*/;(){}[],.:"; // Added colon for completeness + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::Plus, "+".to_string(), Span::new(0, 1)), + Token::new(TokenType::Minus, "-".to_string(), Span::new(1, 2)), + Token::new(TokenType::Star, "*".to_string(), Span::new(2, 3)), + Token::new(TokenType::Slash, "/".to_string(), Span::new(3, 4)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(4, 5)), + Token::new(TokenType::OpenParen, "(".to_string(), Span::new(5, 6)), + Token::new(TokenType::CloseParen, ")".to_string(), Span::new(6, 7)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(7, 8)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(8, 9)), + Token::new(TokenType::OpenBracket, "[".to_string(), Span::new(9, 10)), + Token::new(TokenType::CloseBracket, "]".to_string(), Span::new(10, 11)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(11, 12)), + Token::new(TokenType::Dot, ".".to_string(), Span::new(12, 13)), + Token::new(TokenType::Colon, ":".to_string(), Span::new(13, 14)), // Added colon + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(14, 14)), + ]; + + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_multi_char_tokens() { + let source = "== != <= >= && || += -= *= /= => :: !"; // Added '!' for Bang + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::Equal, "==".to_string(), Span::new(0, 2)), + Token::new(TokenType::NotEqual, "!=".to_string(), Span::new(3, 5)), + Token::new(TokenType::LessThanEqual, "<=".to_string(), Span::new(6, 8)), + Token::new( + TokenType::GreaterThanEqual, + ">=".to_string(), + Span::new(9, 11), + ), + Token::new(TokenType::AmpAmp, "&&".to_string(), Span::new(12, 14)), + Token::new(TokenType::PipePipe, "||".to_string(), Span::new(15, 17)), + Token::new(TokenType::PlusEqual, "+=".to_string(), Span::new(18, 20)), + Token::new(TokenType::MinusEqual, "-=".to_string(), Span::new(21, 23)), + Token::new(TokenType::StarEqual, "*=".to_string(), Span::new(24, 26)), + Token::new(TokenType::SlashEqual, "/=".to_string(), Span::new(27, 29)), + Token::new(TokenType::FatArrow, "=>".to_string(), Span::new(30, 32)), + Token::new(TokenType::DoubleColon, "::".to_string(), Span::new(33, 35)), + Token::new(TokenType::Bang, "!".to_string(), Span::new(36, 37)), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(37, 37)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_keywords() { + let source = "type mut if else while match true false None _"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::KeywordType, "type".to_string(), Span::new(0, 4)), + Token::new(TokenType::KeywordMut, "mut".to_string(), Span::new(5, 8)), + Token::new(TokenType::KeywordIf, "if".to_string(), Span::new(9, 11)), + Token::new( + TokenType::KeywordElse, + "else".to_string(), + Span::new(12, 16), + ), + Token::new( + TokenType::KeywordWhile, + "while".to_string(), + Span::new(17, 22), + ), + Token::new( + TokenType::KeywordMatch, + "match".to_string(), + Span::new(23, 28), + ), + Token::new( + TokenType::KeywordTrue, + "true".to_string(), + Span::new(29, 33), + ), + Token::new( + TokenType::KeywordFalse, + "false".to_string(), + Span::new(34, 39), + ), + Token::new( + TokenType::KeywordNone, + "None".to_string(), + Span::new(40, 44), + ), + Token::new( + TokenType::KeywordUnderscore, + "_".to_string(), + Span::new(45, 46), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(46, 46)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_integer_literals() { + let source = "123 0 4567890"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::Integer(123), "123".to_string(), Span::new(0, 3)), + Token::new(TokenType::Integer(0), "0".to_string(), Span::new(4, 5)), + Token::new( + TokenType::Integer(4567890), + "4567890".to_string(), + Span::new(6, 13), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(13, 13)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_float_literals() { + let source = "123.45 0.0 987.654"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new( + TokenType::Float(123.45), + "123.45".to_string(), + Span::new(0, 6), + ), + Token::new(TokenType::Float(0.0), "0.0".to_string(), Span::new(7, 10)), + Token::new( + TokenType::Float(987.654), + "987.654".to_string(), + Span::new(11, 18), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(18, 18)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_string_literals() { + let source = r#""hello" "world 123" """#; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::String("hello".to_string()), "\"hello\"".to_string(), Span::new(0, 7)), + Token::new(TokenType::String("world 123".to_string()), "\"world 123\"".to_string(), Span::new(8, 19)), + Token::new(TokenType::String("".to_string()), "\"\"".to_string(), Span::new(20, 22)), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(22, 22)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_identifiers() { + let source = "myVar another_var _underscore VAR123"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new( + TokenType::Identifier("myVar".to_string()), + "myVar".to_string(), + Span::new(0, 5), + ), + Token::new( + TokenType::Identifier("another_var".to_string()), + "another_var".to_string(), + Span::new(6, 17), + ), + Token::new( + TokenType::Identifier("_underscore".to_string()), + "_underscore".to_string(), + Span::new(18, 29), + ), + Token::new( + TokenType::Identifier("VAR123".to_string()), + "VAR123".to_string(), + Span::new(30, 36), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(36, 36)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_comments() { + let source = "// This is a comment\n123 // Another comment\n456"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new( + TokenType::Integer(123), + "123".to_string(), + Span::new(21, 24), + ), + Token::new( + TokenType::Integer(456), + "456".to_string(), + Span::new(44, 47), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(47, 47)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_whitespace() { + let source = " \t123 +\n 456\r\n"; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::Integer(123), "123".to_string(), Span::new(3, 6)), + Token::new(TokenType::Plus, "+".to_string(), Span::new(9, 10)), + Token::new( + TokenType::Integer(456), + "456".to_string(), + Span::new(14, 17), + ), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(19, 19)), + ]; + assert_eq!(tokens, expected_tokens); +} + +#[test] +fn test_mixed_tokens() { + let source = r#" +type Point = { x: f64, y: f64 }; // Define a struct +mut counter = 0; +increment(c: int): int = { + c = c + 1; +} +if counter < 10 && !false { + increment(counter); +} else { + // nothing +} +match counter { + 0 => "zero", + _ => "not zero", +}; +my_list = [1, 2, 3]; +"hello world" +"#; + let mut reporter = Reporter::new(); + let lexer = Lexer::new(source, &mut reporter); + let tokens = lexer.tokenize().expect("Lexing failed"); + + let expected_tokens = vec![ + Token::new(TokenType::KeywordType, "type".to_string(), Span::new(1, 5)), + Token::new(TokenType::Identifier("Point".to_string()), "Point".to_string(), Span::new(6, 11)), + Token::new(TokenType::Assign, "=".to_string(), Span::new(12, 13)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(14, 15)), + Token::new(TokenType::Identifier("x".to_string()), "x".to_string(), Span::new(16, 17)), + Token::new(TokenType::Colon, ":".to_string(), Span::new(17, 18)), + Token::new(TokenType::Identifier("f64".to_string()), "f64".to_string(), Span::new(19, 22)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(22, 23)), + Token::new(TokenType::Identifier("y".to_string()), "y".to_string(), Span::new(24, 25)), + Token::new(TokenType::Colon, ":".to_string(), Span::new(25, 26)), + Token::new(TokenType::Identifier("f64".to_string()), "f64".to_string(), Span::new(27, 30)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(31, 32)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(32, 33)), + Token::new(TokenType::KeywordMut, "mut".to_string(), Span::new(53, 56)), + Token::new(TokenType::Identifier("counter".to_string()), "counter".to_string(), Span::new(57, 64)), + Token::new(TokenType::Assign, "=".to_string(), Span::new(65, 66)), + Token::new(TokenType::Integer(0), "0".to_string(), Span::new(67, 68)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(68, 69)), + Token::new(TokenType::Identifier("increment".to_string()), "increment".to_string(), Span::new(70, 79)), + Token::new(TokenType::OpenParen, "(".to_string(), Span::new(79, 80)), + Token::new(TokenType::Identifier("c".to_string()), "c".to_string(), Span::new(80, 81)), + Token::new(TokenType::Colon, ":".to_string(), Span::new(81, 82)), + Token::new(TokenType::Identifier("int".to_string()), "int".to_string(), Span::new(83, 86)), + Token::new(TokenType::CloseParen, ")".to_string(), Span::new(86, 87)), + Token::new(TokenType::Colon, ":".to_string(), Span::new(87, 88)), + Token::new(TokenType::Identifier("int".to_string()), "int".to_string(), Span::new(89, 92)), + Token::new(TokenType::Assign, "=".to_string(), Span::new(93, 94)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(95, 96)), + Token::new(TokenType::Identifier("c".to_string()), "c".to_string(), Span::new(101, 102)), + Token::new(TokenType::Assign, "=".to_string(), Span::new(103, 104)), + Token::new(TokenType::Identifier("c".to_string()), "c".to_string(), Span::new(105, 106)), + Token::new(TokenType::Plus, "+".to_string(), Span::new(107, 108)), + Token::new(TokenType::Integer(1), "1".to_string(), Span::new(109, 110)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(110, 111)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(112, 113)), + Token::new(TokenType::KeywordIf, "if".to_string(), Span::new(114, 116)), + Token::new(TokenType::Identifier("counter".to_string()), "counter".to_string(), Span::new(117, 124)), + Token::new(TokenType::LessThan, "<".to_string(), Span::new(125, 126)), + Token::new(TokenType::Integer(10), "10".to_string(), Span::new(127, 129)), + Token::new(TokenType::AmpAmp, "&&".to_string(), Span::new(130, 132)), + Token::new(TokenType::Bang, "!".to_string(), Span::new(133, 134)), + Token::new(TokenType::KeywordFalse, "false".to_string(), Span::new(134, 139)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(140, 141)), + Token::new(TokenType::Identifier("increment".to_string()), "increment".to_string(), Span::new(146, 155)), + Token::new(TokenType::OpenParen, "(".to_string(), Span::new(155, 156)), + Token::new(TokenType::Identifier("counter".to_string()), "counter".to_string(), Span::new(156, 163)), + Token::new(TokenType::CloseParen, ")".to_string(), Span::new(163, 164)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(164, 165)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(166, 167)), + Token::new(TokenType::KeywordElse, "else".to_string(), Span::new(168, 172)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(173, 174)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(190, 191)), + Token::new(TokenType::KeywordMatch, "match".to_string(), Span::new(192, 197)), + Token::new(TokenType::Identifier("counter".to_string()), "counter".to_string(), Span::new(198, 205)), + Token::new(TokenType::OpenBrace, "{".to_string(), Span::new(206, 207)), + Token::new(TokenType::Integer(0), "0".to_string(), Span::new(212, 213)), + Token::new(TokenType::FatArrow, "=>".to_string(), Span::new(214, 216)), + Token::new(TokenType::String("zero".to_string()), "\"zero\"".to_string(), Span::new(217, 223)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(223, 224)), + Token::new(TokenType::KeywordUnderscore, "_".to_string(), Span::new(229, 230)), + Token::new(TokenType::FatArrow, "=>".to_string(), Span::new(231, 233)), + Token::new(TokenType::String("not zero".to_string()), "\"not zero\"".to_string(), Span::new(234, 244)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(244, 245)), + Token::new(TokenType::CloseBrace, "}".to_string(), Span::new(246, 247)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(247, 248)), + Token::new(TokenType::Identifier("my_list".to_string()), "my_list".to_string(), Span::new(249, 256)), + Token::new(TokenType::Assign, "=".to_string(), Span::new(257, 258)), + Token::new(TokenType::OpenBracket, "[".to_string(), Span::new(259, 260)), + Token::new(TokenType::Integer(1), "1".to_string(), Span::new(260, 261)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(261, 262)), + Token::new(TokenType::Integer(2), "2".to_string(), Span::new(263, 264)), + Token::new(TokenType::Comma, ",".to_string(), Span::new(264, 265)), + Token::new(TokenType::Integer(3), "3".to_string(), Span::new(266, 267)), + Token::new(TokenType::CloseBracket, "]".to_string(), Span::new(267, 268)), + Token::new(TokenType::Semicolon, ";".to_string(), Span::new(268, 269)), + Token::new(TokenType::String("hello world".to_string()), "\"hello world\"".to_string(), Span::new(270, 283)), + Token::new(TokenType::EndOfFile, "".to_string(), Span::new(284, 284)), + ]; + assert_eq!(tokens, expected_tokens); +} diff --git a/tests/parser.rs b/tests/parser.rs index 8e75567..3daf365 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -1,363 +1,1456 @@ -use tap::ast::{ - EnumDecl, Expression, FunctionDef, LiteralValue, Operator, Program, Statement, StructDecl, - TypeAnnotation, -}; +// This file will contain tests for the new parser based on the updated grammar and AST. + +use tap::ast::*; +use tap::diagnostics::Reporter; use tap::lexer::Lexer; use tap::parser::Parser; // --- TEST HELPER --- -// This helper function reduces boilerplate in all tests. -// It handles lexing and parsing, and provides a rich error report if parsing fails. + +fn assert_parses(source: &str) -> Program { + let mut reporter = Reporter::new(); + let tokens = match Lexer::new(source, &mut reporter).tokenize() { + Ok(tokens) => tokens, + Err(_) => { + panic!( + "Lexing failed for source: \"{}\"\n\nLexer Errors:\n{:?}", + source.trim(), + reporter.diagnostics + ); + } + }; + + let mut parser = Parser::new(&tokens, &mut reporter); + match parser.parse_program() { + Ok(program) => { + if reporter.has_errors() { + panic!( + "Parsing failed with reporter errors for source: \"{}\"\n\nTokens:\n{:?}\n\nParser Errors:\n{:?}", + source.trim(), + tokens, + reporter.diagnostics + ); + } + program + } + Err(e) => { + panic!( + "Parsing failed for source: \"{}\"\n\nTokens:\n{:?}\n\nError Report:\n{:?}", + source.trim(), + tokens, + e + ); + } + } +} + fn parse_test_source(source: &str) -> Program { - let tokens = Lexer::new(source) - .tokenize() - .unwrap_or_else(|_| panic!("Lexing failed for source: {}", source)); + assert_parses(source) +} + +#[test] +fn test_parse_simple_expression_statement() { + let source = "1 + 2;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + let expected_span = Span::new(0, 6); // "1 + 2;" + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + assert_eq!(expr_stmt.span, expected_span); + match &expr_stmt.expression { + Expression::Binary(BinaryExpression { + left, + operator, + right, + span, + }) => { + assert_eq!(*span, Span::new(0, 5)); // "1 + 2" + + match &**left { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + lit_span, + )) => { + assert_eq!(*val, 1); + assert_eq!(*lit_span, Span::new(0, 1)); + } + _ => panic!("Expected integer literal for left operand"), + } + + assert_eq!(*operator, BinaryOperator::Add); + + match &**right { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + lit_span, + )) => { + assert_eq!(*val, 2); + assert_eq!(*lit_span, Span::new(4, 5)); + } + _ => panic!("Expected integer literal for right operand"), + } + } + _ => panic!("Expected binary expression"), + } + } + _ => panic!("Expected an expression statement"), + } +} + +#[test] +fn test_parse_function_definition() { + // Note: No semicolon required for function definitions + let source = "my_function(): int = { 1 + 2 }"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Function(func_binding)) => { + assert_eq!(func_binding.name, "my_function"); + assert!(func_binding.params.is_empty()); + + if let Type::Primary(TypePrimary::Named(name, _)) = &func_binding.return_type { + assert_eq!(name, "int"); + } else { + panic!("Expected named type for return type"); + } + + if let Some(expr) = &func_binding.body.final_expression { + if let Expression::Binary(BinaryExpression { left, .. }) = &**expr { + if let Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) = &**left + { + assert_eq!(*val, 1); + } + } + } else { + panic!("Expected final expression in function body"); + } + } + _ => panic!("Expected a function definition statement"), + } +} + +#[test] +fn test_parse_function_definition_with_parameters() { + let source = "add(a: int, b: int): int = { a + b }"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Function(func_binding)) => { + assert_eq!(func_binding.name, "add"); + assert_eq!(func_binding.params.len(), 2); + + assert_eq!(func_binding.params[0].name, "a"); + if let Type::Primary(TypePrimary::Named(name, _)) = &func_binding.params[0].ty { + assert_eq!(name, "int"); + } else { + panic!("Expected named type for parameter a"); + } - let mut parser = Parser::new(&tokens, source); - parser.parse_program().unwrap_or_else(|e| { - panic!( - "Parsing failed for source: \"{}\"\n\nError Report:\n{:?}", - source.trim(), - e - ) - }) + assert_eq!(func_binding.params[1].name, "b"); + if let Type::Primary(TypePrimary::Named(name, _)) = &func_binding.params[1].ty { + assert_eq!(name, "int"); + } else { + panic!("Expected named type for parameter b"); + } + + if let Type::Primary(TypePrimary::Named(name, _)) = &func_binding.return_type { + assert_eq!(name, "int"); + } else { + panic!("Expected named type for return type"); + } + } + _ => panic!("Expected a function definition statement"), + } } -// --- EXPRESSION PARSING TESTS --- +#[test] +fn test_parse_variable_bindings() { + let source = " + x: int = 5; + mut y = 10; + name = \"Alice\"; + "; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 3); + + // 1. x: int = 5; + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "x"); + assert_eq!(bind.mutable, false); + assert!(bind.type_annotation.is_some()); + } + _ => panic!("Expected variable binding for x"), + } + + // 2. mut y = 10; + match &program.statements[1] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "y"); + assert_eq!(bind.mutable, true); + assert!(bind.type_annotation.is_none()); + } + _ => panic!("Expected variable binding for y"), + } + + // 3. name = "Alice"; + match &program.statements[2] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "name"); + assert_eq!(bind.mutable, false); + } + _ => panic!("Expected variable binding for name"), + } +} #[test] -fn test_parse_simple_binary_expression() { - let program = parse_test_source("1 + 2;"); - let stmt = &program.statements[0]; - match stmt { - Statement::Expression(Expression::Binary { left, op, right }) => { - assert_eq!(**left, Expression::Literal(LiteralValue::Integer(1))); - assert_eq!(*op, Operator::Add); - assert_eq!(**right, Expression::Literal(LiteralValue::Integer(2))); +fn test_parse_struct_definition() { + let source = "type EmptyStruct = {};"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "EmptyStruct"); + match &decl.constructor { + TypeConstructor::Record(record_type) => { + assert!(record_type.fields.is_empty()); + } + _ => panic!("Expected record constructor"), + } } - _ => panic!("Expected a binary expression statement."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_operator_precedence() { - let program = parse_test_source("1 + 2 * 3;"); - let stmt = &program.statements[0]; - // Should parse as 1 + (2 * 3) - match stmt { - Statement::Expression(Expression::Binary { left, op, right }) => { - assert_eq!(*op, Operator::Add); - assert_eq!(**left, Expression::Literal(LiteralValue::Integer(1))); - // The right side should be another binary expression - match &**right { - Expression::Binary { - left: inner_left, - op: inner_op, - right: inner_right, - } => { - assert_eq!(**inner_left, Expression::Literal(LiteralValue::Integer(2))); - assert_eq!(*inner_op, Operator::Multiply); - assert_eq!(**inner_right, Expression::Literal(LiteralValue::Integer(3))); +fn test_parse_struct_definition_with_fields() { + let source = "type Point = { x: int, y: int };"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "Point"); + match &decl.constructor { + TypeConstructor::Record(record_type) => { + assert_eq!(record_type.fields.len(), 2); + assert_eq!(record_type.fields[0].name, "x"); + assert_eq!(record_type.fields[1].name, "y"); } - _ => panic!("Expected nested binary expression for precedence."), + _ => panic!("Expected record constructor"), } } - _ => panic!("Expected a binary expression statement."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_parenthesized_expression() { - let program = parse_test_source("(1 + 2) * 3;"); - let stmt = &program.statements[0]; - // Should parse as (1 + 2) * 3 - match stmt { - Statement::Expression(Expression::Binary { left, op, right }) => { - assert_eq!(*op, Operator::Multiply); - assert_eq!(**right, Expression::Literal(LiteralValue::Integer(3))); - match &**left { - Expression::Binary { .. } => { /* Correct */ } - _ => panic!("Expected left side to be a binary expression."), - } - } - _ => panic!("Expected a binary expression statement."), - } -} - -#[test] -fn test_parse_function_call() { - let program = parse_test_source("my_function(1, \"hello\");"); - let stmt = &program.statements[0]; - match stmt { - Statement::Expression(Expression::FunctionCall { callee, args }) => { - assert_eq!(**callee, Expression::Identifier("my_function".to_string())); - assert_eq!(args.len(), 2); - assert_eq!(args[0], Expression::Literal(LiteralValue::Integer(1))); - assert_eq!( - args[1], - Expression::Literal(LiteralValue::String("hello".to_string())) - ); +fn test_parse_enum_definition() { + let source = "type Color = Red | Green | Blue;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "Color"); + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + assert_eq!(sum_type.variants.len(), 3); + assert_eq!(sum_type.variants[0].name, "Red"); + assert!(sum_type.variants[0].ty.is_none()); + assert_eq!(sum_type.variants[1].name, "Green"); + assert_eq!(sum_type.variants[2].name, "Blue"); + } + _ => panic!("Expected sum constructor"), + } } - _ => panic!("Expected a function call expression."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_nested_function_call() { - let program = parse_test_source("func_a(func_b(1));"); - let stmt = &program.statements[0]; - match stmt { - Statement::Expression(Expression::FunctionCall { - callee, - args: outer_args, - }) => { - assert_eq!(**callee, Expression::Identifier("func_a".to_string())); - assert_eq!(outer_args.len(), 1); - match &outer_args[0] { - Expression::FunctionCall { - callee: inner_callee, - args: inner_args, - } => { - assert_eq!(**inner_callee, Expression::Identifier("func_b".to_string())); - assert_eq!(inner_args.len(), 1); +fn test_parse_sum_type_with_payloads() { + // Tests: "(" ")" + let source = "type Result = Ok(int) | Err(string);"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "Result"); + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + assert_eq!(sum_type.variants.len(), 2); + + // Check "Ok(int)" + assert_eq!(sum_type.variants[0].name, "Ok"); + assert!(sum_type.variants[0].ty.is_some()); + // Verify the inner type is int (assuming you have a Type enum) + // matches!(sum_type.variants[0].ty, Some(Type::Primary(name)) if name == "int") + + // Check "Err(string)" + assert_eq!(sum_type.variants[1].name, "Err"); + assert!(sum_type.variants[1].ty.is_some()); } - _ => panic!("Expected nested function call."), + _ => panic!("Expected sum constructor"), } } - _ => panic!("Expected a function call expression."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_lambda_expression() { - let program = parse_test_source("\\x. x + 1;"); +fn test_parse_sum_type_with_nested_record() { + // Tests: holding a + // type Action = Move({x: int, y: int}) | Quit; + let source = "type Action = Move({x: int, y: int}) | Quit;"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + let move_variant = &sum_type.variants[0]; + assert_eq!(move_variant.name, "Move"); + + // Verify the payload is a Record Type + match &move_variant.ty { + Some(Type::Primary(TypePrimary::Record(record_type))) => { + assert_eq!(record_type.fields.len(), 2); + assert_eq!(record_type.fields[0].name, "x"); + assert_eq!(record_type.fields[1].name, "y"); + } + _ => panic!("Expected Record type inside Move variant"), + } + } + _ => panic!("Expected sum constructor"), + } + } + _ => panic!("Expected type declaration"), + } +} + +#[test] +fn test_parse_mixed_sum_type() { + // Tests mixing: | "(" ")" + let source = "type OptionInt = Some(int) | None;"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "OptionInt"); + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + assert_eq!(sum_type.variants.len(), 2); + + // Some(int) + assert_eq!(sum_type.variants[0].name, "Some"); + assert!(sum_type.variants[0].ty.is_some()); + + // None + assert_eq!(sum_type.variants[1].name, "None"); + assert!(sum_type.variants[1].ty.is_none()); // Should be None + } + _ => panic!("Expected sum constructor"), + } + } + _ => panic!("Expected type declaration"), + } +} + +#[test] +fn test_parse_simple_enum_definition() { + let source = "type A = B;"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::Expression(Expression::Lambda { args, body }) => { - assert_eq!(args, &["x"]); - match &**body { - Expression::Binary { .. } => { /* Correct */ } - _ => panic!("Expected binary expression in lambda body"), + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "A"); + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + assert_eq!(sum_type.variants.len(), 1); + assert_eq!(sum_type.variants[0].name, "B"); + assert!(sum_type.variants[0].ty.is_none()); + } + _ => panic!("Expected sum constructor"), } } - _ => panic!("Expected lambda expression"), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_struct_instantiation() { - let program = parse_test_source("type Point = struct {x:int, y:int}; Point { x: 10, y: 20 };"); - let stmt = &program.statements[1]; - match stmt { - Statement::Expression(Expression::StructInstantiation { name, fields }) => { - assert_eq!(name, "Point"); - assert_eq!(fields.len(), 2); - assert_eq!(fields[0].0, "x"); - assert_eq!(fields[0].1, Expression::Literal(LiteralValue::Integer(10))); - assert_eq!(fields[1].0, "y"); - assert_eq!(fields[1].1, Expression::Literal(LiteralValue::Integer(20))); +fn test_parse_enum_definition_with_variants() { + let source = "type MaybeInt = Some(int) | None;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "MaybeInt"); + match &decl.constructor { + TypeConstructor::Sum(sum_type) => { + assert_eq!(sum_type.variants.len(), 2); + + // Some(int) + assert_eq!(sum_type.variants[0].name, "Some"); + assert!(sum_type.variants[0].ty.is_some()); + + // None + assert_eq!(sum_type.variants[1].name, "None"); + assert!(sum_type.variants[1].ty.is_none()); + } + _ => panic!("Expected sum constructor"), + } } - _ => panic!("Expected struct instantiation expression."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_property_access() { - let program = parse_test_source("my_car.make;"); - let stmt = &program.statements[0]; - match stmt { - Statement::Expression(Expression::Get { object, name }) => { - assert_eq!(**object, Expression::Identifier("my_car".to_string())); - assert_eq!(name, "make"); +fn test_parse_control_flow_if() { + let source = " + val = if (x > 0) { + true + } else { + false + }; + "; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + match &bind.value { + Expression::If(if_expr) => { + // Check condition + match &*if_expr.condition { + Expression::Binary(_) => {} + _ => panic!("Expected binary expression in condition"), + } + // Check then block + assert!(if_expr.then_branch.final_expression.is_some()); + // Check else block + assert!(if_expr.else_branch.is_some()); + } + _ => panic!("Expected if expression"), + } } - _ => panic!("Expected property access expression."), + _ => panic!("Expected variable binding"), } } #[test] -fn test_parse_chained_property_access() { - let program = parse_test_source("my_car.owner.name;"); - let stmt = &program.statements[0]; - match stmt { - Statement::Expression(Expression::Get { object, name }) => { - assert_eq!(name, "name"); - match &**object { - Expression::Get { - object: inner_object, - name: inner_name, - } => { - assert_eq!(**inner_object, Expression::Identifier("my_car".to_string())); - assert_eq!(inner_name, "owner"); +fn test_parse_control_flow_simple_if() { + let source = " + if (x > 0) { + true + }; + "; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::If(if_expr) => { + // Check condition + match &*if_expr.condition { + Expression::Binary(_) => {} + _ => panic!("Expected binary expression in condition"), + } + // Check then block + assert!(if_expr.then_branch.final_expression.is_some()); + // Ensure else block is absent + assert!(if_expr.else_branch.is_none()); } - _ => panic!("Expected nested Get expression."), + _ => panic!("Expected if expression"), } } - _ => panic!("Expected property access expression."), + _ => panic!("Expected expression statement"), } } -// --- STATEMENT PARSING TESTS --- +#[test] +fn test_parse_control_flow_while() { + let source = "while (i < 10) { i += 1; }"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::While(while_expr) => { + match &*while_expr.condition { + Expression::Binary(_) => {} + _ => panic!("Expected binary condition"), + } + assert!(!while_expr.body.statements.is_empty()); + } + _ => panic!("Expected while expression"), + }, + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_for_expression() { + let source = "mut sum = 0; for i in [1, 2, 3] { sum += i; }; sum;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 3); + + match &program.statements[1] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::For(for_expr) => { + match &for_expr.pattern { + Pattern::Identifier(name, _) => assert_eq!(name, "i"), + _ => panic!("Expected identifier pattern for iterator"), + } + match &*for_expr.iterable { + Expression::Primary(PrimaryExpression::List(list_lit)) => { + assert_eq!(list_lit.elements.len(), 3); + } + _ => panic!("Expected list literal for iterable"), + } + assert_eq!(for_expr.body.statements.len(), 1); + } + _ => panic!("Expected for expression"), + }, + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_match_expression() { + let source = " + match (val) { + | Some(x) => x, + | None => 0, + | _ => -1 + }; + "; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Match(match_expr) => { + assert_eq!(match_expr.arms.len(), 3); + + // Check first arm: | Some(x) => x + let arm1 = &match_expr.arms[0]; + match &arm1.pattern { + // Use struct variant syntax + Pattern::Variant { name, patterns, .. } => { + assert_eq!(name, "Some"); + if let Some(pats) = patterns { + assert_eq!(pats.len(), 1); + } else { + panic!("Expected parameters for Some variant"); + } + } + _ => panic!("Expected variant pattern"), + } + + // Check third arm: | _ => -1 + let arm3 = &match_expr.arms[2]; + match &arm3.pattern { + Pattern::Wildcard(_) => {} + _ => panic!("Expected wildcard pattern"), + } + } + _ => panic!("Expected match expression"), + } + } + _ => panic!("Expected expression statement"), + } +} #[test] -fn test_parse_if_statement() { - let program = parse_test_source("if true { 1; }"); +fn test_parse_lists_and_indexing() { + let source = "[1, 2, 3][0];"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: [1, 2, 3] + // postfix.primary is Box + match &*postfix.primary { + Expression::Primary(primary) => { + match primary { + PrimaryExpression::List(list_lit) => { + // ListLiteral contains `elements` + assert_eq!(list_lit.elements.len(), 3); + } + _ => panic!("Expected List primary expression"), + } + } + _ => panic!("Expected Primary Expression inside Postfix"), + } + + // Check the operation: [0] + assert_eq!(postfix.operators.len(), 1); + } + _ => panic!("Expected postfix expression"), + } + } + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_lambda() { + let source = "f = (x: int) => x + 1;"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + match &bind.value { + Expression::Lambda(lambda) => { + assert_eq!(lambda.params.len(), 1); + assert_eq!(lambda.params[0].name, "x"); + // Body expression + match &lambda.body { + ExpressionOrBlock::Expression(expr) => { + if let Expression::Binary(_) = **expr { + // OK + } else { + panic!("Expected binary expression in lambda body"); + } + } + _ => panic!("Expected expression body for lambda"), + } + } + _ => panic!("Expected lambda expression"), + } + } + _ => panic!("Expected variable binding"), + } +} + +#[test] +fn test_operator_precedence() { + // Multiplication should bind tighter than addition + let source = "1 + 2 * 3;"; + let program = parse_test_source(source); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Binary(bin_expr) => { + // The top-level operation should be Add + assert_eq!(bin_expr.operator, BinaryOperator::Add); + + // Left should be 1 + match &*bin_expr.left { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(1), + _, + )) => {} + _ => panic!("Left side should be 1"), + } + + // Right should be a Binary Expression (2 * 3) + match &*bin_expr.right { + Expression::Binary(inner_bin) => { + assert_eq!(inner_bin.operator, BinaryOperator::Multiply); + } + _ => panic!("Right side should be a multiplication expression"), + } + } + _ => panic!("Expected binary expression"), + } + } + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_complex_struct_definition() { + let source = " + type User = { + id: int, + username: string, + is_active: bool, + }; + "; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::Expression(Expression::If { - condition, - then_branch, - else_branch, - }) => { - assert_eq!( - **condition, - Expression::Literal(LiteralValue::Boolean(true)) - ); - assert_eq!(then_branch.len(), 1); - assert!(else_branch.is_none()); + TopStatement::TypeDecl(decl) => { + assert_eq!(decl.name, "User"); + match &decl.constructor { + TypeConstructor::Record(record_type) => { + assert_eq!(record_type.fields.len(), 3); + assert_eq!(record_type.fields[0].name, "id"); + if let Type::Primary(TypePrimary::Named(name, _)) = &record_type.fields[0].ty { + assert_eq!(name, "int"); + } else { + panic!("Expected named type for field 'id'"); + } + assert_eq!(record_type.fields[1].name, "username"); + if let Type::Primary(TypePrimary::Named(name, _)) = &record_type.fields[1].ty { + assert_eq!(name, "string"); + } else { + panic!("Expected named type for field 'username'"); + } + assert_eq!(record_type.fields[2].name, "is_active"); + if let Type::Primary(TypePrimary::Named(name, _)) = &record_type.fields[2].ty { + assert_eq!(name, "bool"); + } else { + panic!("Expected named type for field 'is_active'"); + } + } + _ => panic!("Expected record constructor"), + } } - _ => panic!("Expected an if statement."), + _ => panic!("Expected type declaration"), } } #[test] -fn test_parse_if_else_statement() { - let program = parse_test_source("if x < 10 { 1; } else { 2; }"); +fn test_parse_block_expression() { + let source = " + x = { + a = 1; + b = 2; + a + b + }; + "; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::Expression(Expression::If { else_branch, .. }) => { - assert!(else_branch.is_some()); - assert_eq!(else_branch.as_ref().unwrap().len(), 1); + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "x"); + match &bind.value { + Expression::Block(block) => { + assert_eq!(block.statements.len(), 2); + assert!(block.final_expression.is_some()); + } + _ => panic!("Expected block expression"), + } } - _ => panic!("Expected an if-else statement."), + _ => panic!("Expected variable binding"), } } #[test] -fn test_parse_return_statement() { - let program = parse_test_source("return 42;"); +fn test_parse_nested_if_expression() { + let source = " + result = if (x > 0) { + if (y > 0) { + 1 + } else { + -1 + } + } else { + 0 + }; + "; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::Return(value) => { - assert_eq!(*value, Some(Expression::Literal(LiteralValue::Integer(42)))); + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "result"); + match &bind.value { + Expression::If(if_expr) => { + assert!(if_expr.else_branch.is_some()); + match &if_expr.then_branch.final_expression { + Some(expr) => match &**expr { + Expression::If(_) => { + // Nested if expression is present. + } + _ => panic!("Expected nested if expression"), + }, + None => panic!("Expected final expression in then branch"), + } + } + _ => panic!("Expected if expression"), + } } - _ => panic!("Expected a return statement."), + _ => panic!("Expected variable binding"), } } #[test] -fn test_parse_variable_declaration_and_assignment() { - let program = parse_test_source("x : int = 10;"); +fn test_parse_nested_if_expression_no_parenthesis() { + let source = " + result = if x > 0 { + if y > 0 { + 1 + } else { + -1 + } + } else { + 0 + }; + "; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::VarDecl { - name, - type_annotation, - value, - } => { - assert_eq!(name, "x"); - assert_eq!(*type_annotation, TypeAnnotation::Int); - assert_eq!( - *value, - Some(Expression::Literal(LiteralValue::Integer(10))) - ); + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "result"); + match &bind.value { + Expression::If(if_expr) => { + assert!(if_expr.else_branch.is_some()); + match &if_expr.then_branch.final_expression { + Some(expr) => match &**expr { + Expression::If(_) => { + // Nested if expression is present. + } + _ => panic!("Expected nested if expression"), + }, + None => panic!("Expected final expression in then branch"), + } + } + _ => panic!("Expected if expression"), + } } - _ => panic!("Expected an assignment statement, parser should handle this form."), + _ => panic!("Expected variable binding"), } } #[test] -fn test_parse_function_definition() { - let source = "func fib(n) { return n; }"; +fn test_parse_function_call_with_arguments() { + let source = "add(1, 2);"; let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::FunctionDef(FunctionDef { name, args, body }) => { - assert_eq!(name, "fib"); - assert_eq!(args, &["n".to_string()]); - assert_eq!(body.len(), 1); + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: add + match &*postfix.primary { + Expression::Primary(PrimaryExpression::Identifier(ident, _)) => { + assert_eq!(ident, "add"); + } + _ => panic!("Expected identifier 'add' for primary expression"), + } + + assert_eq!(postfix.operators.len(), 1); + match &postfix.operators[0] { + PostfixOperator::Call { args, .. } => { + assert_eq!(args.len(), 2); + match &args[0] { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 1); + } + _ => panic!("Expected integer literal '1' for first argument"), + } + match &args[1] { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 2); + } + _ => panic!("Expected integer literal '2' for second argument"), + } + } + _ => panic!("Expected Call postfix operator"), + } + } + _ => panic!("Expected postfix expression"), + } } - _ => panic!("Expected a function definition."), + _ => panic!("Expected expression statement"), } } -// --- TYPE DECLARATION TESTS --- +#[test] +fn test_parse_unary_expression() { + let source = "-1;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::Unary(unary_expr) => { + assert_eq!(unary_expr.operator, UnaryOperator::Minus); + match &*unary_expr.right { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 1); + } + _ => panic!("Expected integer literal '1' for unary expression"), + } + } + _ => panic!("Expected unary expression"), + }, + _ => panic!("Expected expression statement"), + } +} #[test] -fn test_parse_struct_declaration() { - let program = parse_test_source("Point : struct { x: int, y: int };"); +fn test_parse_parenthesized_expression() { + let source = "(1 + 2) * 3;"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Binary(bin_expr) => { + assert_eq!(bin_expr.operator, tap::ast::BinaryOperator::Multiply); + match &*bin_expr.left { + Expression::Primary(PrimaryExpression::Parenthesized(expr, _)) => { + match &**expr { + Expression::Binary(_) => { + // Correctly parsed as a binary expression inside parentheses. + } + _ => panic!("Expected binary expression inside parentheses"), + } + } + _ => panic!("Expected parenthesized expression on the left side"), + } + } + _ => panic!("Expected binary expression"), + } + } + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_boolean_expression() { + let source = "true == false;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::Binary(bin_expr) => { + assert_eq!(bin_expr.operator, tap::ast::BinaryOperator::Equal); + match &*bin_expr.left { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Boolean(val), + _, + )) => { + assert_eq!(*val, true); + } + _ => panic!("Expected boolean literal 'true' on the left side"), + } + match &*bin_expr.right { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Boolean(val), + _, + )) => { + assert_eq!(*val, false); + } + _ => panic!("Expected boolean literal 'false' on the right side"), + } + } + _ => panic!("Expected binary expression"), + }, + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_string_literal_expression() { + let source = "\"hello world\";"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::Primary(PrimaryExpression::Literal(LiteralValue::String(val), _)) => { + assert_eq!(*val, "hello world"); + } + _ => panic!("Expected string literal expression"), + }, + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_float_literal_expression() { + let source = "3.14;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => match &expr_stmt.expression { + Expression::Primary(PrimaryExpression::Literal(LiteralValue::Float(val), _)) => { + assert_eq!(*val, 3.14); + } + _ => panic!("Expected float literal expression"), + }, + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_none_literal_expression() { + let source = "None;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::StructDecl(StructDecl { name, fields }) => { - assert_eq!(name, "Point"); - assert_eq!(fields.len(), 2); - assert_eq!(fields[0], ("x".to_string(), TypeAnnotation::Int)); - assert_eq!(fields[1], ("y".to_string(), TypeAnnotation::Int)); + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Primary(PrimaryExpression::Literal(LiteralValue::None, _)) => { + // Successfully parsed None literal + } + _ => panic!("Expected None literal expression"), + } } - _ => panic!("Expected struct declaration"), + _ => panic!("Expected expression statement"), } } #[test] -fn test_parse_enum_declaration() { - let program = parse_test_source("Color : enum { Red, Green, Blue };"); +fn test_parse_record_literal() { + let source = "point = { x: 1, y: 2 };"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); + match &program.statements[0] { - Statement::EnumDecl(EnumDecl { name, variants }) => { - assert_eq!(name, "Color"); - let variant_names: Vec = variants.iter().map(|v| v.name.clone()).collect(); - assert_eq!(variant_names, &["Red", "Green", "Blue"]); + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "point"); + match &bind.value { + Expression::Primary(PrimaryExpression::Record(record_lit)) => { + assert_eq!(record_lit.fields.len(), 2); + assert_eq!(record_lit.fields[0].name, "x"); + match &record_lit.fields[0].value { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 1); + } + _ => panic!("Expected integer literal for field 'x'"), + } + assert_eq!(record_lit.fields[1].name, "y"); + match &record_lit.fields[1].value { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 2); + } + _ => panic!("Expected integer literal for field 'y'"), + } + } + _ => panic!("Expected record literal expression"), + } } - _ => panic!("Expected enum declaration"), + _ => panic!("Expected variable binding"), } } -// --- FUTURE FEATURE TESTS (IGNORED) --- +#[test] +fn test_parse_field_access() { + let source = "point.x;"; + let program = parse_test_source(source); + + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: point + match &*postfix.primary { + Expression::Primary(PrimaryExpression::Identifier(ident, _)) => { + assert_eq!(ident, "point"); + } + _ => panic!("Expected identifier 'point' for primary expression"), + } + + assert_eq!(postfix.operators.len(), 1); + match &postfix.operators[0] { + PostfixOperator::FieldAccess { name, .. } => { + assert_eq!(name, "x"); + } + _ => panic!("Expected Field access postfix operator"), + } + } + _ => panic!("Expected postfix expression"), + } + } + _ => panic!("Expected expression statement"), + } +} #[test] -#[ignore] -fn test_parse_while_loop() { - let program = parse_test_source("while x > 0 { x = x - 1; }"); +fn test_parse_path_resolution() { + let source = "Option::Some;"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); - // Add assertions here when the parser is updated. + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: Option + match &*postfix.primary { + Expression::Primary(PrimaryExpression::Identifier(ident, _)) => { + assert_eq!(ident, "Option"); + } + _ => panic!("Expected identifier 'Option' for primary expression"), + } + + assert_eq!(postfix.operators.len(), 1); + match &postfix.operators[0] { + PostfixOperator::TypePath { name, .. } => { + assert_eq!(name, "Some"); + } + _ => panic!("Expected TypePath resolution postfix operator"), + } + } + _ => panic!("Expected postfix expression"), + } + } + _ => panic!("Expected expression statement"), + } } #[test] -fn test_parse_for_loop() { - let program = parse_test_source("for i in [1, 2, 3] { print(i); }"); +fn test_parse_simple_method_invocation() { + let source = "circle.radius();"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); - // Add assertions here when the parser is updated. + match &program.statements[0] { - Statement::For { - iterator, - iterable, - body, - } => { - assert_eq!(iterator, "i"); - match &**iterable { - Expression::List(elements) => { - assert_eq!(elements.len(), 3); + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: circle + match &*postfix.primary { + Expression::Primary(PrimaryExpression::Identifier(ident, _)) => { + assert_eq!(ident, "circle"); + } + _ => panic!("Expected identifier 'circle' for primary expression"), + } + + assert_eq!(postfix.operators.len(), 2); + match &postfix.operators[0] { + PostfixOperator::FieldAccess { name, .. } => { + assert_eq!(name, "radius"); + } + _ => panic!("Expected Field access postfix operator"), + } + match &postfix.operators[1] { + PostfixOperator::Call { args, .. } => { + assert!(args.is_empty()); + } + _ => panic!("Expected Call postfix operator"), + } } - _ => panic!("Expected array literal as iterable."), + _ => panic!("Expected postfix expression"), } - assert_eq!(body.len(), 1); } - _ => panic!("Expected a for loop statement."), + _ => panic!("Expected expression statement"), } } #[test] -#[ignore] -fn test_parse_array_access_assignment() { - let program = parse_test_source("arr[0] += 1; arr[1] -= 2; arr[2] *= 3; arr[3] /= 4;"); +fn test_parse_method_invocation_with_arguments() { + let source = "rect.resize(10, 20);"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); - // Add assertions here when the parser is updated. + + match &program.statements[0] { + TopStatement::Expression(expr_stmt) => { + match &expr_stmt.expression { + Expression::Postfix(postfix) => { + // Check the primary: rect + match &*postfix.primary { + Expression::Primary(PrimaryExpression::Identifier(ident, _)) => { + assert_eq!(ident, "rect"); + } + _ => panic!("Expected identifier 'rect' for primary expression"), + } + + assert_eq!(postfix.operators.len(), 2); + match &postfix.operators[0] { + PostfixOperator::FieldAccess { name, .. } => { + assert_eq!(name, "resize"); + } + _ => panic!("Expected Field access postfix operator"), + } + match &postfix.operators[1] { + PostfixOperator::Call { args, .. } => { + assert_eq!(args.len(), 2); + match &args[0] { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 10); + } + _ => panic!("Expected integer literal for first argument"), + } + match &args[1] { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 20); + } + _ => panic!("Expected integer literal for second argument"), + } + } + _ => panic!("Expected Call postfix operator"), + } + } + _ => panic!("Expected postfix expression"), + } + } + _ => panic!("Expected expression statement"), + } } #[test] -#[ignore] -fn test_parse_match_statement() { - let source = "match opt { Some(x) => x + 1, None => 0 };"; + +fn test_parse_method_definition() { + let source = "c = { r: 5, area: () => this.r * this.r * 3.14 };"; + let program = parse_test_source(source); + assert_eq!(program.statements.len(), 1); - // Add assertions here when the parser is updated. + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Variable(bind)) => { + assert_eq!(bind.name, "c"); + + match &bind.value { + Expression::Primary(PrimaryExpression::Record(record_lit)) => { + assert_eq!(record_lit.fields.len(), 2); + + assert_eq!(record_lit.fields[0].name, "r"); + + match &record_lit.fields[1].value { + Expression::Lambda(lambda) => { + assert!(lambda.params.is_empty()); + + match &lambda.body { + ExpressionOrBlock::Expression(expr) => { + if let Expression::Binary(bin_expr) = &**expr { + // this.r * this.r * 3.14 + + assert_eq!(bin_expr.operator, BinaryOperator::Multiply); + } else { + panic!("Expected binary expression in lambda body"); + } + } + + _ => panic!("Expected expression body for lambda"), + } + } + + _ => panic!("Expected lambda expression for field 'area'"), + } + } + + _ => panic!("Expected record literal expression"), + } + } + + _ => panic!("Expected variable binding"), + } +} + +#[test] +fn test_parse_return_statement() { + let source = r#" + foo(): int = { + return 42; + } + "#; + let program = assert_parses(source); + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::LetStmt(LetStatement::Function(func)) => { + assert_eq!(func.name, "foo"); + assert_eq!(func.params.len(), 0); + match &func.return_type { + Type::Primary(TypePrimary::Named(name, _)) => assert_eq!(name, "int"), + _ => panic!("Expected return type 'int'"), + } + // Check block contains a single statement: return 42; + assert_eq!(func.body.statements.len(), 1); + match &func.body.statements[0] { + Statement::Return(Some(expr), _) => match expr { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Integer(val), + _, + )) => { + assert_eq!(*val, 42); + } + _ => panic!("Expected integer literal in return"), + }, + Statement::Return(None, _) => { + panic!("Expected return with value, got return without value"); + } + Statement::Let(_) => panic!("Expected return statement, got let"), + Statement::Expression(_) => panic!("Expected return statement, got expression"), + Statement::Break(_) => panic!("Unexpected break statement"), + Statement::Continue(_) => panic!("Unexpected continue statement"), + } + assert!(func.body.final_expression.is_none()); + } + _ => panic!("Expected function binding"), + } +} + +#[test] +fn test_parse_break_and_continue_statements() { + let source = r#" + while (true) { + break; + continue; + } + "#; + let program = assert_parses(source); + assert_eq!(program.statements.len(), 1); + + match &program.statements[0] { + TopStatement::Expression(ExpressionStatement { expression, .. }) => { + match expression { + Expression::While(while_expr) => { + // Condition should be 'true' + match &*while_expr.condition { + Expression::Primary(PrimaryExpression::Literal( + LiteralValue::Boolean(true), + _, + )) => {} + _ => panic!("Expected 'true' condition in while"), + } + // Block should contain break and continue + assert_eq!(while_expr.body.statements.len(), 2); + match &while_expr.body.statements[0] { + Statement::Break(_) => {} + _ => panic!("Expected break statement"), + } + match &while_expr.body.statements[1] { + Statement::Continue(_) => {} + _ => panic!("Expected continue statement"), + } + } + _ => panic!("Expected while expression"), + } + } + _ => panic!("Expected expression statement"), + } +} + +#[test] +fn test_parse_generic_type_list() { + let source = r#" + type Map = Map[string, int]; + type Pair = Pair[int, float]; + "#; + let program = assert_parses(source); + assert_eq!(program.statements.len(), 2); + + // First: type Map = Map[string, int]; + match &program.statements[0] { + TopStatement::TypeDecl(TypeDeclaration { + name, constructor, .. + }) => { + assert_eq!(name, "Map"); + match constructor { + TypeConstructor::Alias(Type::Primary(TypePrimary::Generic { + name: generic_name, + args, + .. + })) => { + assert_eq!(generic_name, "Map"); + assert_eq!(args.len(), 2); + match &args[0] { + Type::Primary(TypePrimary::Named(type_name, _)) => { + assert_eq!(type_name, "string") + } + _ => panic!("Expected first generic arg to be 'string'"), + } + match &args[1] { + Type::Primary(TypePrimary::Named(type_name, _)) => { + assert_eq!(type_name, "int") + } + _ => panic!("Expected second generic arg to be 'int'"), + } + } + _ => panic!("Expected generic type alias for Map"), + } + } + _ => panic!("Expected type declaration for Map"), + } + + // Second: type Pair = Pair[int, float]; + match &program.statements[1] { + TopStatement::TypeDecl(TypeDeclaration { + name, constructor, .. + }) => { + assert_eq!(name, "Pair"); + match constructor { + TypeConstructor::Alias(Type::Primary(TypePrimary::Generic { + name: generic_name, + args, + .. + })) => { + assert_eq!(generic_name, "Pair"); + assert_eq!(args.len(), 2); + match &args[0] { + Type::Primary(TypePrimary::Named(type_name, _)) => { + assert_eq!(type_name, "int") + } + _ => panic!("Expected first generic arg to be 'int'"), + } + match &args[1] { + Type::Primary(TypePrimary::Named(type_name, _)) => { + assert_eq!(type_name, "float") + } + _ => panic!("Expected second generic arg to be 'float'"), + } + } + _ => panic!("Expected generic type alias for Pair"), + } + } + _ => panic!("Expected type declaration for Pair"), + } } diff --git a/tests/tap_tests.rs b/tests/tap_tests.rs new file mode 100644 index 0000000..e69de29