Compare commits
818 Commits
loops/mini
...
architectu
| Author | SHA1 | Date | |
|---|---|---|---|
| a76d072d3f | |||
| 97c800a36b | |||
| 0526f796f4 | |||
| e5d751c5fb | |||
| 8525165594 | |||
| f62df8d64e | |||
| ca8e6f4da3 | |||
| 885943c5ae | |||
| 87f503f54b | |||
| 90cd0f8f6f | |||
| 818e68a2f8 | |||
| 22411f7f80 | |||
| 26112f1003 | |||
| 680cdf62aa | |||
| 7e795f95fc | |||
| f927fb6515 | |||
| e200935698 | |||
| 342e1a2ccf | |||
| 9a7ca54902 | |||
| eb14a7576b | |||
| a90f56e3f3 | |||
| 55c376f559 | |||
| e3e5d3e888 | |||
| c560f3d70d | |||
| 5e7d431f15 | |||
| 88c7ce4068 | |||
| c19bcc51cb | |||
| 129f11fdbc | |||
| cf933f0ece | |||
| 0fccd1b353 | |||
| 23a53a2ccb | |||
| e222e8b0aa | |||
| c919d9a0d7 | |||
| a75b4cbc57 | |||
| 4fd376a348 | |||
| a7665a7b25 | |||
| 95c2d0b64a | |||
| cfbab3b2f9 | |||
| 4d92eafb36 | |||
| 4db1f85fe8 | |||
| 4563a7ae97 | |||
| 2981a479e8 | |||
| 54a890db71 | |||
| 480462646d | |||
| decaf818fa | |||
| 03d4e350d7 | |||
| 4504b8ae5e | |||
| 9efbf4ad38 | |||
| 4e904a2782 | |||
| dea2a6e390 | |||
| c27db9b78f | |||
| 39381fda92 | |||
| 2e7e3141d4 | |||
| edfc37636f | |||
| 58f019bc14 | |||
| 1f466186f9 | |||
| 24d8e362d5 | |||
| 29ef89d473 | |||
| f7bd3a6bf1 | |||
| d5d77a3611 | |||
| 40dff449ef | |||
| 67449f5b0c | |||
| 6d8f11e093 | |||
| 78dab5b28c | |||
| 1fb852ef64 | |||
| b80871ac4f | |||
| 9ff5d1b464 | |||
| 5fa6c6ecc1 | |||
| a4a7753314 | |||
| f12c19eaa3 | |||
| af8d10a717 | |||
| c21eb9d5ad | |||
| d896685555 | |||
| bf7ec55e92 | |||
| 45789520ce | |||
| b91d8cf72e | |||
| 6e997e9382 | |||
| 0df5e92c46 | |||
| fadcdbd6a9 | |||
| ce98d97728 | |||
| 82dfa20e82 | |||
| 66aa003461 | |||
| 6bae94bae1 | |||
| 7a94a47e26 | |||
| 917ffe5ccc | |||
| ba60db2eef | |||
| 00881f84eb | |||
| 9e380fd96e | |||
| c6f646607e | |||
| 0da39de68a | |||
| 285cd530eb | |||
| dcae125955 | |||
| 9a16f27075 | |||
| 154e2297fe | |||
| 0231bb46a6 | |||
| fed07059a3 | |||
| c8327823ee | |||
| fad81e0b0c | |||
| 3ccce58e0a | |||
| 8ab2f80615 | |||
| 230f803abb | |||
| b240408a4c | |||
| 67ece98ba1 | |||
| 33be068c01 | |||
| bf468e5ec3 | |||
| 90ba37ecc8 | |||
| 3f00e62577 | |||
| 97a29c6bac | |||
| 73efd229be | |||
| 6d89da9380 | |||
| d3340107e6 | |||
| aaa6020037 | |||
| 8ef24847d3 | |||
| b3ee88e9bb | |||
| 2c7a1bfc47 | |||
| 047ea62d43 | |||
| 2726ed9b8a | |||
| 6d7df11224 | |||
| 8a80bd3923 | |||
| 609205b551 | |||
| f9371e7d22 | |||
| 7f310a4da7 | |||
| 6780acd0af | |||
| b771ea306c | |||
| 6c77dec495 | |||
| 0a3f02d636 | |||
| 800dca67ca | |||
| fd1f94f292 | |||
| 1d1c35a438 | |||
| ca34cede88 | |||
| cb626fc402 | |||
| 175a77fba5 | |||
| 3fe3b7b66f | |||
| 689438d12e | |||
| d1a4616ac4 | |||
| 32f6c4ee0c | |||
| 62712accdd | |||
| c69a7694c8 | |||
| 5384ff6c42 | |||
| bcb7db2ea4 | |||
| 5eed0dd5f5 | |||
| 3ea8967571 | |||
| e057d9f18f | |||
| 4761d41a0d | |||
| a9e4eea334 | |||
| 3a1ecaa362 | |||
| 69a53ece43 | |||
| 96c9e90743 | |||
| 5bcda5c88c | |||
| 4b5e75dc3e | |||
| 2a1d8eeab2 | |||
| 2c8c1f75b3 | |||
| 7e57e0b215 | |||
| cbba642d7f | |||
| 4510e7e475 | |||
| 0fbfce949b | |||
| 7c229eb321 | |||
| 01d0e97706 | |||
| a8596bd090 | |||
| 9d364a0c20 | |||
| dfb660073e | |||
| 7f5b77415f | |||
| 29a3fb4bc2 | |||
| 019a0c6105 | |||
| 1e29bba1be | |||
| 0142d69212 | |||
| e93e1eeab1 | |||
| 551c24c5a0 | |||
| 85414df868 | |||
| 237ea5ce84 | |||
| df4aa8eb0a | |||
| 5bb65d8315 | |||
| bed374c9e1 | |||
| fb8bb9f105 | |||
| b4571f0f9f | |||
| 0ef26b20f3 | |||
| 19d0ef0f38 | |||
| 769559bae7 | |||
| 1dd350d592 | |||
| 4fdf6980da | |||
| cccef832d9 | |||
| 836b31a5b6 | |||
| 526ffbb5f0 | |||
| 99f321f532 | |||
| dfd89d998e | |||
| 74d8ade089 | |||
| d7cc6d1b39 | |||
| 872302ede1 | |||
| 57a63826e3 | |||
| 7a67637826 | |||
| 42a506faff | |||
| df5e36aa5e | |||
| 713d506bb8 | |||
| bcaa41d1ae | |||
| edbb03e205 | |||
| 8a06c2d72b | |||
| 551ed44f7f | |||
| 76de0a20f8 | |||
| 353dcb67d6 | |||
| 36e02c906a | |||
| 058dcd5600 | |||
| 5c1b4349aa | |||
| e23aa9c273 | |||
| da54c3ea53 | |||
| 1a34cc4456 | |||
| 63901931c4 | |||
| e77a2d3a81 | |||
| 836e01dbb4 | |||
| fb0e83d3a1 | |||
| ad897122d7 | |||
| 0b79d4d4b4 | |||
| 58ea001f12 | |||
| da96a79104 | |||
| ed8aaf8af7 | |||
| ce067e32a4 | |||
| 37f7405dcf | |||
| 4e6a345342 | |||
| 25b30788b4 | |||
| 21dbd195d5 | |||
| 87f9a84365 | |||
| 46e49dc947 | |||
| f15a8d8fef | |||
| ea7120751d | |||
| 89a807a1ed | |||
| 391a2d0c4f | |||
| b4f7f814be | |||
| 5959989324 | |||
| 320d78a993 | |||
| dedb82565b | |||
| 2a01758f28 | |||
| 533be5b36b | |||
| 853504642f | |||
| 7d575cb1fe | |||
| 00ffba9306 | |||
| cecde8733a | |||
| c16a8f2d53 | |||
| 793eccfce2 | |||
| d4eb57fa07 | |||
| 73917745a0 | |||
| c8206e718a | |||
| ada7a147e5 | |||
| 288c0f8c3e | |||
| 2c7246e11d | |||
| 65f3b6fcc0 | |||
| 4840a9f660 | |||
| 53968c2480 | |||
| 3759aad7a6 | |||
| f256132eb3 | |||
| 14575a9cd7 | |||
| be13f2daba | |||
| 810f61a1c1 | |||
| d4be87166b | |||
| 37a514d566 | |||
| 7e838bb62b | |||
| b2ff367c6b | |||
| 0655b942a5 | |||
| 17a7a91d73 | |||
| df6efeb68e | |||
| 60e3ce1c96 | |||
| eb621240d7 | |||
| 1fef6ec94d | |||
| e8a0c86de0 | |||
| 4eeb7e59b4 | |||
| f1df5b1b72 | |||
| 87bf3711c4 | |||
| 254ef0daff | |||
| b6e723fc3e | |||
| 2e84492d96 | |||
| 8ae7187c55 | |||
| 1bde4e834f | |||
| 554ef48c63 | |||
| b7b841821c | |||
| 3d821d1290 | |||
| 2129e04bfd | |||
| 89726ed6c2 | |||
| 5d71be364e | |||
| ce013fa138 | |||
| d1482482ff | |||
| 07de86365e | |||
| 5b38f4d499 | |||
| a3a93c20b8 | |||
| 72be94c900 | |||
| 30b237a891 | |||
| 667dfcfd7c | |||
| 7f8bf5f455 | |||
| 7fc37abe02 | |||
| a98d683e60 | |||
| a2f3c533b8 | |||
| 0f2eb45f5c | |||
| 802544fdc6 | |||
| 1c40fec8fa | |||
| b94a47a9a9 | |||
| 699b30ed1b | |||
| 7de014cd75 | |||
| 0eef5bc8e6 | |||
| d437727f1d | |||
| 16e21ef6fa | |||
| ef0a24f0db | |||
| 50981a2a9b | |||
| 05487b497d | |||
| af38d98583 | |||
| cd014cdb29 | |||
| f5122a9a5d | |||
| 097c7f4590 | |||
| 5c587c0f61 | |||
| adc4cb89c6 | |||
| acc8b01ddb | |||
| 027678f31e | |||
| cca3a28206 | |||
| b8dfc080dd | |||
| 4481f5f98b | |||
| ac19b7aced | |||
| aa0a7fa1a2 | |||
| bafa2410e4 | |||
| b59f08a1b8 | |||
| a91ff62730 | |||
| 073ea44fdb | |||
| aee7226b9c | |||
| 3e8aae77d5 | |||
| b3d5da5361 | |||
| da6d8e39c9 | |||
| 32aba1823d | |||
| d145532afe | |||
| 3be2dc6e78 | |||
| b0cbdaf713 | |||
| aaaf054441 | |||
| 86f7a351fb | |||
| 70b9b4f6cf | |||
| 095bb62ef9 | |||
| e4c92a19d4 | |||
| 13fb1bd7a9 | |||
| 39f4c7a9a8 | |||
| 1a828d5b9f | |||
| 21d0be58ec | |||
| 5c70747ac7 | |||
| c272b1ea04 | |||
| 9a8bbff5b2 | |||
| 5632830118 | |||
| 75a1adbbd5 | |||
| 90418c120b | |||
| e42ff3b1f6 | |||
| dcde14a471 | |||
| 97a8c06690 | |||
| 0c3b5d21fa | |||
| 98ba772acd | |||
| cb272317bc | |||
| 4d32c80a99 | |||
| ddd1e40d00 | |||
| 7ca5bfbb70 | |||
| 2d519461c4 | |||
| 013ce15357 | |||
| 24416f8cef | |||
| ec12b721e8 | |||
| 76d6528c51 | |||
| 5d33f8f20b | |||
| 7773c40337 | |||
| 7c40506571 | |||
| 41dbac55b8 | |||
| 82ffc695a5 | |||
| b526d81a4c | |||
| 64f4f10c32 | |||
| 9bf4bd6180 | |||
| 8ca3ef342d | |||
| 41190c6d23 | |||
| 141795449a | |||
| dab8718289 | |||
| 7e64695a74 | |||
| a6793fa656 | |||
| cb14a07413 | |||
| 8188a82a58 | |||
| a0e8b64f5c | |||
| e5709c5aec | |||
| 55fe1e4468 | |||
| f68ea63e46 | |||
| a66b262267 | |||
| 073588812a | |||
| 0b7d88bbe1 | |||
| 1ed3216ba6 | |||
| 5618dd1ef5 | |||
| 19497c9fba | |||
| b57f40db63 | |||
| a34cfe69dc | |||
| 8af3630625 | |||
| 34d518d555 | |||
| 9907c1c58c | |||
| c8ab505c32 | |||
| 207dfc60ad | |||
| 1b38f89055 | |||
| 14b52cfaa7 | |||
| 7c63fd8a7f | |||
| bd2cd8aad1 | |||
| 0234ae329e | |||
| f895a118fb | |||
| 30a7dd2108 | |||
| b9d63112e6 | |||
| eeb530eb85 | |||
| c45a2b34a0 | |||
| bc4f4a5477 | |||
| 36e1519613 | |||
| aa620b767f | |||
| 20997d3360 | |||
| 57a84b372d | |||
| d1a491e530 | |||
| a4ef271459 | |||
| 416546cc07 | |||
| f0c0a5e19f | |||
| 55ecdf24bb | |||
| 015ecb8bc8 | |||
| 50b69bcbd0 | |||
| a074ea9e98 | |||
| 14986d787d | |||
| ef53232314 | |||
| 23afc9dde3 | |||
| 8cdebbe305 | |||
| 9dd9fb9c37 | |||
| e8246340fc | |||
| a1030dce5d | |||
| 982e9680fe | |||
| 6dc535dde3 | |||
| 0d9c45176b | |||
| 0530120bc7 | |||
| 6d9ac1e55a | |||
| a4ef9a8ec9 | |||
| d8b8de6195 | |||
| ce75bd6848 | |||
| c7d8b7dd62 | |||
| 029c1783f4 | |||
| b92a98fb45 | |||
| ecae58316f | |||
| 8fab20c8bc | |||
| de8b1dd681 | |||
| ce81ce2e95 | |||
| 1bff28e99e | |||
| 8c7ad62b44 | |||
| fff8fe2dc8 | |||
| 360a3ed51f | |||
| 5b501f7937 | |||
| 50a219b688 | |||
| d9979eaf6c | |||
| 66da0e5b84 | |||
| 0d99b5dfe8 | |||
| f070bddb0e | |||
| 0858986877 | |||
| d8f1882b50 | |||
| 0bc6dbd233 | |||
| cabf5dc9c3 | |||
| 4909ebe2ad | |||
| a8d0dfb38a | |||
| f05d405bac | |||
| ffa74399fd | |||
| ecdd90345e | |||
| 2f271fa6a6 | |||
| dbe3c6c203 | |||
| 404c908a9a | |||
| ee422f3d15 | |||
| b297c83b1d | |||
| 85867e329b | |||
| cd93b11328 | |||
| 4bca2cacff | |||
| d61ee088c5 | |||
| f40dfbbeb5 | |||
| f0dffd275d | |||
| 9f05e24c52 | |||
| 86343345dc | |||
| ad252088c3 | |||
| 76ccbfbab6 | |||
| 98049d5458 | |||
| 92619301e2 | |||
| 0cf5c8f219 | |||
| 47e68454ad | |||
| 62a5a29d5b | |||
| 17d6f58cc5 | |||
| e981368dcf | |||
| 59bec68dcc | |||
| 4a7cff2f6b | |||
| 21c541bd1b | |||
| e9d4d107a6 | |||
| 0985dc6386 | |||
| f12edc8fd9 | |||
| 92f6f187b7 | |||
| c361946974 | |||
| 9f539ab392 | |||
| 986b15c0e5 | |||
| 0b4f5e1df9 | |||
| ee002f2e02 | |||
| 16df48ff74 | |||
| dac9cf124f | |||
| 46d0eb258e | |||
| de7be332c8 | |||
| 4ab79f5758 | |||
| 756d5fba64 | |||
| 5bc7895ce0 | |||
| 81247eb6ea | |||
| d2bf0c0d00 | |||
| 202ea9cf5f | |||
| 812aa75d43 | |||
| 6d7197182e | |||
| b7627b4102 | |||
| a0abdcf520 | |||
| 88c02c7c73 | |||
| 9edccb8f33 | |||
| bc557a5ad2 | |||
| 8e508bc90f | |||
| d8f6250962 | |||
| 5f4defe99e | |||
| d20df7aa8c | |||
| 851e0585cf | |||
| d51ae65bbb | |||
| e97bdc4602 | |||
| f03aa3056d | |||
| 96f66d3596 | |||
| 254052a43b | |||
| ec7e4dd5c4 | |||
| 370df5b8e5 | |||
| a648247ae4 | |||
| 5a3db1a458 | |||
| 549cb5ea84 | |||
| 30880927f2 | |||
| e0c7de1a1c | |||
| de734b27b8 | |||
| 4c11c4e1b9 | |||
| 7a64be22d8 | |||
| 9695d31dab | |||
| fc6979a371 | |||
| 43fa31375d | |||
| 4a643a5c52 | |||
| ce8fed6b22 | |||
| 5100c5d5a6 | |||
| 9c5a697e45 | |||
| 282a3d3d06 | |||
| 57a1dbb232 | |||
| a53e47b415 | |||
| a080ce656c | |||
| 2a01d8ac91 | |||
| 71b73bd87e | |||
| 88b3db2e9f | |||
| e2c149e60a | |||
| d66ddc614b | |||
| f33a8d69f5 | |||
| 148c3f2068 | |||
| 18fb54a8c5 | |||
| cf634ad2b1 | |||
| 62da10030b | |||
| 0e30cf1af6 | |||
| 21028c4fb0 | |||
| b3c9d9eb3a | |||
| 7415dd020e | |||
| 380580af17 | |||
| cc64ec5cf2 | |||
| 7fb65cd26a | |||
| 9473911cf3 | |||
| 74b80e6b0e | |||
| c7315f5877 | |||
| 9054fe983d | |||
| 082749f0a9 | |||
| 408fc27366 | |||
| b95d8c5a63 | |||
| c8bfd22786 | |||
| a63d67247a | |||
| d09ed83fa1 | |||
| 55286cc5bc | |||
| 26863242a0 | |||
| 5a1dc4392f | |||
| 4c6790046c | |||
| f4c155c9c5 | |||
| 790c17dfc1 | |||
| 19f1cad11d | |||
| de302fc236 | |||
| 5603ecc3a6 | |||
| 7a898567e4 | |||
| 3cc760082c | |||
| d45e653a87 | |||
| ce603e9879 | |||
| 317f93b2af | |||
| 0528a5cfa7 | |||
| 6d04cf7bf2 | |||
| 2fa0bb4df1 | |||
| caec05eb27 | |||
| 6a1f63f0d1 | |||
| 937342bbf0 | |||
| d964f58c48 | |||
| 9b8b0b4325 | |||
| a11f3c33b6 | |||
| 9b833a9442 | |||
| 4dca583ee3 | |||
| a6ab944c39 | |||
| 58c6ec27f3 | |||
| 9102e57d89 | |||
| fa43aa6711 | |||
| 9648dac88d | |||
| 0d2eede5fb | |||
| a9eb821cce | |||
| 1b7bb5ad1f | |||
| d0b358eca2 | |||
| badb428100 | |||
| bfec2a4320 | |||
| b1023f11d9 | |||
| 16f7a14506 | |||
| 0cfaeb9136 | |||
| 8d9ce7838d | |||
| fb0ca374a3 | |||
| d676bcb6b7 | |||
| 9b07f97341 | |||
| 0df2b1c7b2 | |||
| 24a67fae97 | |||
| b9dc69a3c1 | |||
| c8f9b8be06 | |||
| e83c01cdcc | |||
| 82100603f0 | |||
| 7ce723f732 | |||
| 69078a59a9 | |||
| 982b9d6be6 | |||
| 6457eb668c | |||
| 9bc70fd2a9 | |||
| 8046df7ce5 | |||
| 5c1807c832 | |||
| 9a090c6e42 | |||
| f5d3b1df19 | |||
| 9bd6bbb7e7 | |||
| 85b7fed4fc | |||
| 06a5b5b07c | |||
| bf782d9c49 | |||
| 2490c901bf | |||
| bcdd137d6f | |||
| 27bfceb1aa | |||
| 0b3610a63a | |||
| 96a7541d70 | |||
| 42cce5e3fc | |||
| 544e79f533 | |||
| 2b8c1a506c | |||
| 2d475f95d1 | |||
| 197c073308 | |||
| 203f81004d | |||
| 04b0e61a33 | |||
| 1eb9d0f8d2 | |||
| f182d04e6a | |||
| ab2c40c14c | |||
| d3c34b46b9 | |||
| 80dac0051d | |||
| 11612a511b | |||
| b661318a45 | |||
| 47d9d07f2e | |||
| d75c61d408 | |||
| f1fea0f2f1 | |||
| 21e6351657 | |||
| 5f97e78d5f | |||
| a677585639 | |||
| c04f38a1ba | |||
| 0b4b7c9dbc | |||
| f4b0ebf353 | |||
| b13819c50c | |||
| f26f25f146 | |||
| d9cf00f287 | |||
| 0c0ed0605a | |||
| 63c1e17c75 | |||
| a4fd57cff1 | |||
| 95fb5ef8ef | |||
| 76d141737a | |||
| 9307437679 | |||
| 843c3a7e5e | |||
| b89e321007 | |||
| cf0ba8a02a | |||
| ca9e12fc57 | |||
| f0e1d2d615 | |||
| 2adbc101fa | |||
| 4e554113a9 | |||
| 4205989aee | |||
| 49252eaa5c | |||
| c81e3f3705 | |||
| ebbf0fc10c | |||
| 8dfb3f6387 | |||
| 66f13c95d5 | |||
| 5a8c25bec7 | |||
| c821e21f94 | |||
| 081f934cad | |||
| 5605fe1cc2 | |||
| 379bb93f14 | |||
| 7ce0c797f3 | |||
| 34513908df | |||
| 208953667b | |||
| e6d6273265 | |||
| e95ca4624b | |||
| e1a020dc90 | |||
| b0974b58c0 | |||
| 6620c0ac06 | |||
| 95cf653ba9 | |||
| 12de24e3a0 | |||
| 180b9009bf | |||
| 9b0f42defb | |||
| a29bb6feca | |||
| d2638170db | |||
| a5c41d2573 | |||
| 882815e612 | |||
| e27daee4a8 | |||
| ef33e9a43a | |||
| 89f1c0ccbe | |||
| 1b7bd86b43 | |||
| e5fe9ad2d4 | |||
| 2d373da06b | |||
| 25cf832998 | |||
| 29542ba9d2 | |||
| c2de220cce | |||
| d523df30c2 | |||
| 1b844f6a19 | |||
| 5f758d27c1 | |||
| 51f57aa2fa | |||
| 31308602ca | |||
| 788e8682f5 | |||
| bb134b88e3 | |||
| d8dec07df3 | |||
| 39c7baa44c | |||
| ee74a396c5 | |||
| a8997ab452 | |||
| 54b7a6aed0 | |||
| 80d6507e57 | |||
| 685fcd11d5 | |||
| 066ddcd6e1 | |||
| f6efba410a | |||
| 4a35998469 | |||
| f93b13e861 | |||
| 6fa0cdeedc | |||
| 394d4d69c4 | |||
| aad178aa0f | |||
| 32a8ed8ef0 | |||
| 91611f9179 | |||
| 97180b4aa3 | |||
| 055cd14cc0 | |||
| ea63b6d9bb | |||
| 5d7f931cf1 | |||
| 79f3e1ada2 | |||
| 4d00250233 | |||
| 80c21cbabb | |||
| 70f91ef3d8 | |||
| 5f38e49ba4 | |||
| 0f9d361a92 | |||
| 11315d91cc | |||
| f16e1b69c0 | |||
| ae86579ae8 | |||
| 8ca5c8052d | |||
| 55f3024743 | |||
| 0d6d0bf439 | |||
| f6e333dd19 | |||
| c28333adb3 | |||
| 1b2935828c | |||
| 64af162b5d | |||
| 8ca2fe3564 | |||
| b1a7852045 | |||
| dd47fa8a0b | |||
| fad44ca097 | |||
| 702e7c8eac | |||
| 89a879799a | |||
| 73694a3a84 | |||
| b9b875f399 | |||
| f620be096b | |||
| 1b34d41b33 | |||
| fd32bcf547 | |||
| 47f66ad1be | |||
| d170d5fbae | |||
| abc98b7665 | |||
| c726a9e0fe | |||
| 77f20b713d | |||
| 0491f061c4 | |||
| 2a4a4531b9 | |||
| b6810e90ab | |||
| f89e50aa4d | |||
| e670e914e7 | |||
| bd0377b6a3 | |||
| 3ec52d4556 | |||
| 3ab01b271d | |||
| fb18629916 | |||
| d8be6b8230 | |||
| 8e1466032a | |||
| e105edee01 | |||
| 27425a3173 | |||
| bac3471a1f | |||
| 68b0a279f8 | |||
| b1bed8e0e5 | |||
| 9560145228 | |||
| 9435fab790 | |||
| fc2baee9c7 | |||
| 387a6e7f5d | |||
| 12b02d5691 | |||
| 57516ce18e | |||
| 46741a9643 | |||
| acf9c273a2 | |||
| 1d3a93b0ca | |||
| f0a4dfbea8 | |||
| 54d7fcf436 | |||
| 35ce18eb97 | |||
| d361d83402 | |||
| 0b0d704f1e | |||
| 5ea81fe4e0 | |||
| 781bd36eeb | |||
| 1c975f229d | |||
| 743e0bae87 | |||
| cf4d19fb94 | |||
| 24fde8aa2f | |||
| 582894121d | |||
| 0e509af0a2 | |||
| c6b7e19892 | |||
| 40439cf0e1 | |||
| 6dfef34a4b | |||
| 8c25527205 | |||
| a5947e1295 | |||
| a47b3e5420 | |||
| 0934c4bd28 | |||
| e224fb2db0 | |||
| e066e14267 | |||
| 43c13c4eb1 | |||
| 4815db461b | |||
| 3ab8474e78 | |||
| bb16477fd4 | |||
| d925be4768 | |||
| 418a0dc120 | |||
| fe0fafe8e9 | |||
| 2b448d99bc | |||
| b2939c1922 | |||
| 8bfeff8623 |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"sessionId":"31c80255-eb92-43e4-8997-84ad84e27326","pid":90960,"procStart":"564684","acquiredAt":1777049890282}
|
||||||
@@ -1279,7 +1279,7 @@ let run_foundation_tests () =
|
|||||||
assert_true "sx_truthy \"\"" (Bool (sx_truthy (String "")));
|
assert_true "sx_truthy \"\"" (Bool (sx_truthy (String "")));
|
||||||
assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil));
|
assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil));
|
||||||
assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false)));
|
assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false)));
|
||||||
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None } in
|
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None; l_call_count = 0; l_uid = Sx_types.next_lambda_uid () } in
|
||||||
assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l)));
|
assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l)));
|
||||||
ignore (Sx_types.set_lambda_name (Lambda l) "my-fn");
|
ignore (Sx_types.set_lambda_name (Lambda l) "my-fn");
|
||||||
assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l))
|
assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l))
|
||||||
|
|||||||
@@ -676,7 +676,11 @@ let () =
|
|||||||
let rec deep_equal a b =
|
let rec deep_equal a b =
|
||||||
match a, b with
|
match a, b with
|
||||||
| Nil, Nil -> true | Bool a, Bool b -> a = b
|
| Nil, Nil -> true | Bool a, Bool b -> a = b
|
||||||
| Number a, Number b -> a = b | String a, String b -> a = b
|
| Integer a, Integer b -> a = b
|
||||||
|
| Number a, Number b -> a = b
|
||||||
|
| Integer a, Number b -> float_of_int a = b
|
||||||
|
| Number a, Integer b -> a = float_of_int b
|
||||||
|
| String a, String b -> a = b
|
||||||
| Symbol a, Symbol b -> a = b | Keyword a, Keyword b -> a = b
|
| Symbol a, Symbol b -> a = b | Keyword a, Keyword b -> a = b
|
||||||
| (List a | ListRef { contents = a }), (List b | ListRef { contents = b }) ->
|
| (List a | ListRef { contents = a }), (List b | ListRef { contents = b }) ->
|
||||||
List.length a = List.length b && List.for_all2 deep_equal a b
|
List.length a = List.length b && List.for_all2 deep_equal a b
|
||||||
|
|||||||
@@ -528,6 +528,183 @@ let () =
|
|||||||
| [Rational (_, d)] -> Integer d
|
| [Rational (_, d)] -> Integer d
|
||||||
| [Integer _] -> Integer 1
|
| [Integer _] -> Integer 1
|
||||||
| _ -> raise (Eval_error "denominator: expected rational or integer"));
|
| _ -> raise (Eval_error "denominator: expected rational or integer"));
|
||||||
|
(* printf-spec: apply one Tcl/printf format spec to one arg.
|
||||||
|
spec is like "%5.2f", "%-10s", "%x", "%c", "%d". Always starts with %
|
||||||
|
and ends with the conversion char. Supports d i u x X o c s f e g.
|
||||||
|
Coerces arg to the right type per conversion. *)
|
||||||
|
register "printf-spec" (fun args ->
|
||||||
|
let spec_str, arg = match args with
|
||||||
|
| [String s; v] -> (s, v)
|
||||||
|
| _ -> raise (Eval_error "printf-spec: (spec arg)")
|
||||||
|
in
|
||||||
|
let n = String.length spec_str in
|
||||||
|
if n < 2 || spec_str.[0] <> '%' then
|
||||||
|
raise (Eval_error ("printf-spec: invalid spec " ^ spec_str));
|
||||||
|
let type_char = spec_str.[n - 1] in
|
||||||
|
let to_int v = match v with
|
||||||
|
| Integer i -> i
|
||||||
|
| Number f -> int_of_float f
|
||||||
|
| String s ->
|
||||||
|
let s = String.trim s in
|
||||||
|
(try int_of_string s
|
||||||
|
with _ ->
|
||||||
|
try int_of_float (float_of_string s)
|
||||||
|
with _ -> 0)
|
||||||
|
| Bool true -> 1 | Bool false -> 0
|
||||||
|
| _ -> 0
|
||||||
|
in
|
||||||
|
let to_float v = match v with
|
||||||
|
| Number f -> f
|
||||||
|
| Integer i -> float_of_int i
|
||||||
|
| String s ->
|
||||||
|
let s = String.trim s in
|
||||||
|
(try float_of_string s with _ -> 0.0)
|
||||||
|
| _ -> 0.0
|
||||||
|
in
|
||||||
|
let to_string v = match v with
|
||||||
|
| String s -> s
|
||||||
|
| Integer i -> string_of_int i
|
||||||
|
| Number f -> Sx_types.format_number f
|
||||||
|
| Bool true -> "1" | Bool false -> "0"
|
||||||
|
| Nil -> ""
|
||||||
|
| _ -> Sx_types.inspect v
|
||||||
|
in
|
||||||
|
try
|
||||||
|
match type_char with
|
||||||
|
| 'd' | 'i' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%d" in
|
||||||
|
String (Printf.sprintf fmt (to_int arg))
|
||||||
|
| 'u' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%u" in
|
||||||
|
String (Printf.sprintf fmt (to_int arg))
|
||||||
|
| 'x' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%x" in
|
||||||
|
String (Printf.sprintf fmt (to_int arg))
|
||||||
|
| 'X' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%X" in
|
||||||
|
String (Printf.sprintf fmt (to_int arg))
|
||||||
|
| 'o' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%o" in
|
||||||
|
String (Printf.sprintf fmt (to_int arg))
|
||||||
|
| 'c' ->
|
||||||
|
let n_val = to_int arg in
|
||||||
|
let body = String.sub spec_str 0 (n - 1) in
|
||||||
|
let fmt = Scanf.format_from_string (body ^ "s") "%s" in
|
||||||
|
String (Printf.sprintf fmt (String.make 1 (Char.chr (n_val land 0xff))))
|
||||||
|
| 's' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%s" in
|
||||||
|
String (Printf.sprintf fmt (to_string arg))
|
||||||
|
| 'f' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%f" in
|
||||||
|
String (Printf.sprintf fmt (to_float arg))
|
||||||
|
| 'e' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%e" in
|
||||||
|
String (Printf.sprintf fmt (to_float arg))
|
||||||
|
| 'E' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%E" in
|
||||||
|
String (Printf.sprintf fmt (to_float arg))
|
||||||
|
| 'g' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%g" in
|
||||||
|
String (Printf.sprintf fmt (to_float arg))
|
||||||
|
| 'G' ->
|
||||||
|
let fmt = Scanf.format_from_string spec_str "%G" in
|
||||||
|
String (Printf.sprintf fmt (to_float arg))
|
||||||
|
| _ -> raise (Eval_error ("printf-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||||
|
with
|
||||||
|
| Eval_error _ as e -> raise e
|
||||||
|
| _ -> raise (Eval_error ("printf-spec: invalid format " ^ spec_str)));
|
||||||
|
|
||||||
|
(* scan-spec: apply one Tcl/scanf format spec to a string.
|
||||||
|
Returns (consumed-count . parsed-value), or nil on failure. *)
|
||||||
|
register "scan-spec" (fun args ->
|
||||||
|
let spec_str, str = match args with
|
||||||
|
| [String s; String input] -> (s, input)
|
||||||
|
| _ -> raise (Eval_error "scan-spec: (spec input)")
|
||||||
|
in
|
||||||
|
let n = String.length spec_str in
|
||||||
|
if n < 2 || spec_str.[0] <> '%' then
|
||||||
|
raise (Eval_error ("scan-spec: invalid spec " ^ spec_str));
|
||||||
|
let type_char = spec_str.[n - 1] in
|
||||||
|
let len = String.length str in
|
||||||
|
(* skip leading whitespace for non-%c/%s conversions *)
|
||||||
|
let i = ref 0 in
|
||||||
|
if type_char <> 'c' then
|
||||||
|
while !i < len && (str.[!i] = ' ' || str.[!i] = '\t' || str.[!i] = '\n') do incr i done;
|
||||||
|
let start = !i in
|
||||||
|
try
|
||||||
|
match type_char with
|
||||||
|
| 'd' | 'i' ->
|
||||||
|
let j = ref !i in
|
||||||
|
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||||
|
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done;
|
||||||
|
if !j > start && (str.[start] >= '0' && str.[start] <= '9'
|
||||||
|
|| (!j > start + 1 && (str.[start] = '-' || str.[start] = '+'))) then
|
||||||
|
let n_val = int_of_string (String.sub str start (!j - start)) in
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (Integer n_val);
|
||||||
|
Hashtbl.replace d "consumed" (Integer !j);
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| 'x' | 'X' ->
|
||||||
|
let j = ref !i in
|
||||||
|
while !j < len &&
|
||||||
|
((str.[!j] >= '0' && str.[!j] <= '9') ||
|
||||||
|
(str.[!j] >= 'a' && str.[!j] <= 'f') ||
|
||||||
|
(str.[!j] >= 'A' && str.[!j] <= 'F')) do incr j done;
|
||||||
|
if !j > start then
|
||||||
|
let n_val = int_of_string ("0x" ^ String.sub str start (!j - start)) in
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (Integer n_val);
|
||||||
|
Hashtbl.replace d "consumed" (Integer !j);
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| 'o' ->
|
||||||
|
let j = ref !i in
|
||||||
|
while !j < len && str.[!j] >= '0' && str.[!j] <= '7' do incr j done;
|
||||||
|
if !j > start then
|
||||||
|
let n_val = int_of_string ("0o" ^ String.sub str start (!j - start)) in
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (Integer n_val);
|
||||||
|
Hashtbl.replace d "consumed" (Integer !j);
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| 'f' | 'e' | 'g' ->
|
||||||
|
let j = ref !i in
|
||||||
|
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||||
|
while !j < len && ((str.[!j] >= '0' && str.[!j] <= '9') || str.[!j] = '.') do incr j done;
|
||||||
|
if !j < len && (str.[!j] = 'e' || str.[!j] = 'E') then begin
|
||||||
|
incr j;
|
||||||
|
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||||
|
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done
|
||||||
|
end;
|
||||||
|
if !j > start then
|
||||||
|
let f_val = float_of_string (String.sub str start (!j - start)) in
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (Number f_val);
|
||||||
|
Hashtbl.replace d "consumed" (Integer !j);
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| 's' ->
|
||||||
|
let j = ref !i in
|
||||||
|
while !j < len && str.[!j] <> ' ' && str.[!j] <> '\t' && str.[!j] <> '\n' do incr j done;
|
||||||
|
if !j > start then
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (String (String.sub str start (!j - start)));
|
||||||
|
Hashtbl.replace d "consumed" (Integer !j);
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| 'c' ->
|
||||||
|
if !i < len then
|
||||||
|
let d = Hashtbl.create 2 in
|
||||||
|
Hashtbl.replace d "value" (Integer (Char.code str.[!i]));
|
||||||
|
Hashtbl.replace d "consumed" (Integer (!i + 1));
|
||||||
|
Dict d
|
||||||
|
else Nil
|
||||||
|
| _ -> raise (Eval_error ("scan-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||||
|
with
|
||||||
|
| Eval_error _ as e -> raise e
|
||||||
|
| _ -> Nil);
|
||||||
|
|
||||||
register "parse-int" (fun args ->
|
register "parse-int" (fun args ->
|
||||||
let parse_leading_int s =
|
let parse_leading_int s =
|
||||||
let len = String.length s in
|
let len = String.length s in
|
||||||
@@ -582,11 +759,22 @@ let () =
|
|||||||
(List lb | ListRef { contents = lb }) ->
|
(List lb | ListRef { contents = lb }) ->
|
||||||
List.length la = List.length lb &&
|
List.length la = List.length lb &&
|
||||||
List.for_all2 safe_eq la lb
|
List.for_all2 safe_eq la lb
|
||||||
(* Dict: check __host_handle for DOM node identity *)
|
(* Dict: __host_handle identity for DOM-wrapped dicts; otherwise
|
||||||
|
structural equality over keys + values. *)
|
||||||
| Dict a, Dict b ->
|
| Dict a, Dict b ->
|
||||||
(match Hashtbl.find_opt a "__host_handle", Hashtbl.find_opt b "__host_handle" with
|
(match Hashtbl.find_opt a "__host_handle", Hashtbl.find_opt b "__host_handle" with
|
||||||
| Some (Number ha), Some (Number hb) -> ha = hb
|
| Some (Number ha), Some (Number hb) -> ha = hb
|
||||||
| _ -> false)
|
| Some _, _ | _, Some _ -> false
|
||||||
|
| None, None ->
|
||||||
|
Hashtbl.length a = Hashtbl.length b &&
|
||||||
|
(let eq = ref true in
|
||||||
|
Hashtbl.iter (fun k v ->
|
||||||
|
if !eq then
|
||||||
|
match Hashtbl.find_opt b k with
|
||||||
|
| Some v' -> if not (safe_eq v v') then eq := false
|
||||||
|
| None -> eq := false
|
||||||
|
) a;
|
||||||
|
!eq))
|
||||||
(* Records: same type + structurally equal fields *)
|
(* Records: same type + structurally equal fields *)
|
||||||
| Record a, Record b ->
|
| Record a, Record b ->
|
||||||
a.r_type.rt_uid = b.r_type.rt_uid &&
|
a.r_type.rt_uid = b.r_type.rt_uid &&
|
||||||
@@ -3399,6 +3587,204 @@ let () =
|
|||||||
Nil
|
Nil
|
||||||
| _ -> raise (Eval_error "channel-set-blocking!: (channel bool)"));
|
| _ -> raise (Eval_error "channel-set-blocking!: (channel bool)"));
|
||||||
|
|
||||||
|
(* === Exec === run an external process; capture stdout *)
|
||||||
|
register "exec-process" (fun args ->
|
||||||
|
let items = match args with
|
||||||
|
| [List xs] | [ListRef { contents = xs }] -> xs
|
||||||
|
| _ -> raise (Eval_error "exec-process: (cmd-list)")
|
||||||
|
in
|
||||||
|
let argv = Array.of_list (List.map (function
|
||||||
|
| String s -> s
|
||||||
|
| v -> Sx_types.inspect v
|
||||||
|
) items) in
|
||||||
|
if Array.length argv = 0 then raise (Eval_error "exec: empty command");
|
||||||
|
let (out_r, out_w) = Unix.pipe () in
|
||||||
|
let (err_r, err_w) = Unix.pipe () in
|
||||||
|
let pid =
|
||||||
|
try Unix.create_process argv.(0) argv Unix.stdin out_w err_w
|
||||||
|
with Unix.Unix_error (e, _, _) ->
|
||||||
|
Unix.close out_r; Unix.close out_w;
|
||||||
|
Unix.close err_r; Unix.close err_w;
|
||||||
|
raise (Eval_error ("exec: " ^ Unix.error_message e))
|
||||||
|
in
|
||||||
|
Unix.close out_w;
|
||||||
|
Unix.close err_w;
|
||||||
|
let buf = Buffer.create 256 in
|
||||||
|
let errbuf = Buffer.create 64 in
|
||||||
|
let chunk = Bytes.create 4096 in
|
||||||
|
let read_all fd target =
|
||||||
|
try
|
||||||
|
let stop = ref false in
|
||||||
|
while not !stop do
|
||||||
|
let n = Unix.read fd chunk 0 (Bytes.length chunk) in
|
||||||
|
if n = 0 then stop := true
|
||||||
|
else Buffer.add_subbytes target chunk 0 n
|
||||||
|
done
|
||||||
|
with _ -> ()
|
||||||
|
in
|
||||||
|
read_all out_r buf;
|
||||||
|
read_all err_r errbuf;
|
||||||
|
Unix.close out_r;
|
||||||
|
Unix.close err_r;
|
||||||
|
let (_, status) = Unix.waitpid [] pid in
|
||||||
|
let exit_code = match status with
|
||||||
|
| Unix.WEXITED n -> n
|
||||||
|
| Unix.WSIGNALED _ | Unix.WSTOPPED _ -> 1
|
||||||
|
in
|
||||||
|
let s = Buffer.contents buf in
|
||||||
|
let trimmed =
|
||||||
|
if String.length s > 0 && s.[String.length s - 1] = '\n'
|
||||||
|
then String.sub s 0 (String.length s - 1) else s
|
||||||
|
in
|
||||||
|
if exit_code <> 0 then
|
||||||
|
raise (Eval_error ("exec: child exited " ^ string_of_int exit_code
|
||||||
|
^ (if Buffer.length errbuf > 0
|
||||||
|
then ": " ^ Buffer.contents errbuf
|
||||||
|
else "")))
|
||||||
|
else String trimmed);
|
||||||
|
|
||||||
|
(* exec-pipeline: takes a list of words like Tcl `exec` would receive.
|
||||||
|
Recognizes `|` as a stage separator and `> file`, `>> file`, `< file`,
|
||||||
|
`2>@1` (stderr→stdout), `2> file`. Returns trimmed stdout of the last
|
||||||
|
stage; raises Eval_error if the last stage exits non-zero. *)
|
||||||
|
register "exec-pipeline" (fun args ->
|
||||||
|
let items = match args with
|
||||||
|
| [List xs] | [ListRef { contents = xs }] -> xs
|
||||||
|
| _ -> raise (Eval_error "exec-pipeline: (word-list)")
|
||||||
|
in
|
||||||
|
let words = List.map (function
|
||||||
|
| String s -> s
|
||||||
|
| v -> Sx_types.inspect v
|
||||||
|
) items in
|
||||||
|
if words = [] then raise (Eval_error "exec: empty command");
|
||||||
|
let split_stages ws =
|
||||||
|
let rec loop acc cur = function
|
||||||
|
| [] -> List.rev (List.rev cur :: acc)
|
||||||
|
| "|" :: rest -> loop (List.rev cur :: acc) [] rest
|
||||||
|
| w :: rest -> loop acc (w :: cur) rest
|
||||||
|
in
|
||||||
|
loop [] [] ws
|
||||||
|
in
|
||||||
|
let extract_redirs ws =
|
||||||
|
let in_path = ref None in
|
||||||
|
let out_path = ref None in
|
||||||
|
let out_append = ref false in
|
||||||
|
let err_path = ref None in
|
||||||
|
let merge_err = ref false in
|
||||||
|
let cleaned = ref [] in
|
||||||
|
let rec loop = function
|
||||||
|
| [] -> ()
|
||||||
|
| "<" :: p :: rest -> in_path := Some p; loop rest
|
||||||
|
| ">" :: p :: rest -> out_path := Some p; out_append := false; loop rest
|
||||||
|
| ">>" :: p :: rest -> out_path := Some p; out_append := true; loop rest
|
||||||
|
| "2>@1" :: rest -> merge_err := true; loop rest
|
||||||
|
| "2>" :: p :: rest -> err_path := Some p; loop rest
|
||||||
|
| w :: rest -> cleaned := w :: !cleaned; loop rest
|
||||||
|
in
|
||||||
|
loop ws;
|
||||||
|
(List.rev !cleaned, !in_path, !out_path, !out_append, !err_path, !merge_err)
|
||||||
|
in
|
||||||
|
let stages = List.map extract_redirs (split_stages words) in
|
||||||
|
if stages = [] then raise (Eval_error "exec: no stages");
|
||||||
|
let n = List.length stages in
|
||||||
|
let pipes = Array.init (max 0 (n - 1)) (fun _ -> Unix.pipe ()) in
|
||||||
|
let (final_r, final_w) = Unix.pipe () in
|
||||||
|
let (errstash_r, errstash_w) = Unix.pipe () in
|
||||||
|
let pids = ref [] in
|
||||||
|
let close_safe fd = try Unix.close fd with _ -> () in
|
||||||
|
let open_in_redir = function
|
||||||
|
| None -> Unix.stdin
|
||||||
|
| Some path ->
|
||||||
|
(try Unix.openfile path [Unix.O_RDONLY] 0o644
|
||||||
|
with Unix.Unix_error (e, _, _) ->
|
||||||
|
raise (Eval_error ("exec: open <" ^ path ^ ": " ^ Unix.error_message e)))
|
||||||
|
in
|
||||||
|
let open_out_redir path append =
|
||||||
|
let flags = Unix.O_WRONLY :: Unix.O_CREAT :: (if append then [Unix.O_APPEND] else [Unix.O_TRUNC]) in
|
||||||
|
try Unix.openfile path flags 0o644
|
||||||
|
with Unix.Unix_error (e, _, _) ->
|
||||||
|
raise (Eval_error ("exec: open >" ^ path ^ ": " ^ Unix.error_message e))
|
||||||
|
in
|
||||||
|
let stages_arr = Array.of_list stages in
|
||||||
|
(try
|
||||||
|
Array.iteri (fun i (cleaned, ip, op, app, ep, merge) ->
|
||||||
|
if cleaned = [] then raise (Eval_error "exec: empty stage in pipeline");
|
||||||
|
let argv = Array.of_list cleaned in
|
||||||
|
let stdin_fd =
|
||||||
|
if i = 0 then open_in_redir ip
|
||||||
|
else fst pipes.(i - 1)
|
||||||
|
in
|
||||||
|
let stdout_fd =
|
||||||
|
if i = n - 1 then
|
||||||
|
(match op with
|
||||||
|
| None -> final_w
|
||||||
|
| Some path -> open_out_redir path app)
|
||||||
|
else snd pipes.(i)
|
||||||
|
in
|
||||||
|
let stderr_fd =
|
||||||
|
if merge then stdout_fd
|
||||||
|
else (match ep with
|
||||||
|
| None -> if i = n - 1 then errstash_w else Unix.stderr
|
||||||
|
| Some path -> open_out_redir path false)
|
||||||
|
in
|
||||||
|
let pid =
|
||||||
|
try Unix.create_process argv.(0) argv stdin_fd stdout_fd stderr_fd
|
||||||
|
with Unix.Unix_error (e, _, _) ->
|
||||||
|
raise (Eval_error ("exec: " ^ argv.(0) ^ ": " ^ Unix.error_message e))
|
||||||
|
in
|
||||||
|
pids := pid :: !pids;
|
||||||
|
if i > 0 then close_safe (fst pipes.(i - 1));
|
||||||
|
if i < n - 1 then close_safe (snd pipes.(i));
|
||||||
|
if i = 0 && ip <> None then close_safe stdin_fd;
|
||||||
|
if i = n - 1 && op <> None then close_safe stdout_fd;
|
||||||
|
if not merge && ep <> None then close_safe stderr_fd
|
||||||
|
) stages_arr
|
||||||
|
with e ->
|
||||||
|
close_safe final_r; close_safe final_w;
|
||||||
|
close_safe errstash_r; close_safe errstash_w;
|
||||||
|
Array.iter (fun (a,b) -> close_safe a; close_safe b) pipes;
|
||||||
|
raise e);
|
||||||
|
close_safe final_w;
|
||||||
|
close_safe errstash_w;
|
||||||
|
let buf = Buffer.create 256 in
|
||||||
|
let errbuf = Buffer.create 64 in
|
||||||
|
let chunk = Bytes.create 4096 in
|
||||||
|
let read_all fd target =
|
||||||
|
try
|
||||||
|
let stop = ref false in
|
||||||
|
while not !stop do
|
||||||
|
let r = Unix.read fd chunk 0 (Bytes.length chunk) in
|
||||||
|
if r = 0 then stop := true
|
||||||
|
else Buffer.add_subbytes target chunk 0 r
|
||||||
|
done
|
||||||
|
with _ -> ()
|
||||||
|
in
|
||||||
|
read_all final_r buf;
|
||||||
|
read_all errstash_r errbuf;
|
||||||
|
close_safe final_r;
|
||||||
|
close_safe errstash_r;
|
||||||
|
let exit_codes = List.rev_map (fun pid ->
|
||||||
|
let (_, st) = Unix.waitpid [] pid in
|
||||||
|
match st with
|
||||||
|
| Unix.WEXITED c -> c
|
||||||
|
| _ -> 1
|
||||||
|
) !pids in
|
||||||
|
let final_code = match List.rev exit_codes with
|
||||||
|
| [] -> 0
|
||||||
|
| last :: _ -> last
|
||||||
|
in
|
||||||
|
let s = Buffer.contents buf in
|
||||||
|
let trimmed =
|
||||||
|
if String.length s > 0 && s.[String.length s - 1] = '\n'
|
||||||
|
then String.sub s 0 (String.length s - 1) else s
|
||||||
|
in
|
||||||
|
if final_code <> 0 then
|
||||||
|
raise (Eval_error ("exec: pipeline last stage exited " ^ string_of_int final_code
|
||||||
|
^ (if Buffer.length errbuf > 0
|
||||||
|
then ": " ^ Buffer.contents errbuf
|
||||||
|
else "")))
|
||||||
|
else String trimmed);
|
||||||
|
|
||||||
(* === Sockets === wrapping Unix.socket/connect/bind/listen/accept *)
|
(* === Sockets === wrapping Unix.socket/connect/bind/listen/accept *)
|
||||||
let resolve_inet_addr host =
|
let resolve_inet_addr host =
|
||||||
if host = "" || host = "0.0.0.0" then Unix.inet_addr_any
|
if host = "" || host = "0.0.0.0" then Unix.inet_addr_any
|
||||||
@@ -3734,4 +4120,42 @@ let () =
|
|||||||
| k :: v :: rest -> ignore (env_bind child (value_to_string k) v); add_bindings rest
|
| k :: v :: rest -> ignore (env_bind child (value_to_string k) v); add_bindings rest
|
||||||
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
||||||
add_bindings pairs;
|
add_bindings pairs;
|
||||||
Env child)
|
Env child);
|
||||||
|
|
||||||
|
(* JIT cache control & observability — backed by refs in sx_types.ml to
|
||||||
|
avoid creating a sx_primitives → sx_vm dependency cycle. sx_vm reads
|
||||||
|
these refs to decide when to JIT. *)
|
||||||
|
register "jit-stats" (fun _args ->
|
||||||
|
let d = Hashtbl.create 8 in
|
||||||
|
Hashtbl.replace d "threshold" (Number (float_of_int !Sx_types.jit_threshold));
|
||||||
|
Hashtbl.replace d "budget" (Number (float_of_int !Sx_types.jit_budget));
|
||||||
|
Hashtbl.replace d "cache-size" (Number (float_of_int (Sx_types.jit_cache_size ())));
|
||||||
|
Hashtbl.replace d "compiled" (Number (float_of_int !Sx_types.jit_compiled_count));
|
||||||
|
Hashtbl.replace d "compile-failed" (Number (float_of_int !Sx_types.jit_skipped_count));
|
||||||
|
Hashtbl.replace d "below-threshold" (Number (float_of_int !Sx_types.jit_threshold_skipped_count));
|
||||||
|
Hashtbl.replace d "evicted" (Number (float_of_int !Sx_types.jit_evicted_count));
|
||||||
|
Dict d);
|
||||||
|
register "jit-set-threshold!" (fun args ->
|
||||||
|
match args with
|
||||||
|
| [Number n] -> Sx_types.jit_threshold := int_of_float n; Nil
|
||||||
|
| [Integer n] -> Sx_types.jit_threshold := n; Nil
|
||||||
|
| _ -> raise (Eval_error "jit-set-threshold!: (n) where n is integer"));
|
||||||
|
register "jit-set-budget!" (fun args ->
|
||||||
|
match args with
|
||||||
|
| [Number n] -> Sx_types.jit_budget := int_of_float n; Nil
|
||||||
|
| [Integer n] -> Sx_types.jit_budget := n; Nil
|
||||||
|
| _ -> raise (Eval_error "jit-set-budget!: (n) where n is integer"));
|
||||||
|
register "jit-reset-cache!" (fun _args ->
|
||||||
|
(* Phase 3 manual cache reset — clear all compiled VmClosures.
|
||||||
|
Hot paths will re-JIT on next call (after re-hitting threshold). *)
|
||||||
|
Queue.iter (fun (_, v) ->
|
||||||
|
match v with Lambda l -> l.l_compiled <- None | _ -> ()
|
||||||
|
) Sx_types.jit_cache_queue;
|
||||||
|
Queue.clear Sx_types.jit_cache_queue;
|
||||||
|
Nil);
|
||||||
|
register "jit-reset-counters!" (fun _args ->
|
||||||
|
Sx_types.jit_compiled_count := 0;
|
||||||
|
Sx_types.jit_skipped_count := 0;
|
||||||
|
Sx_types.jit_threshold_skipped_count := 0;
|
||||||
|
Sx_types.jit_evicted_count := 0;
|
||||||
|
Nil)
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ and lambda = {
|
|||||||
l_closure : env;
|
l_closure : env;
|
||||||
mutable l_name : string option;
|
mutable l_name : string option;
|
||||||
mutable l_compiled : vm_closure option; (** Lazy JIT cache *)
|
mutable l_compiled : vm_closure option; (** Lazy JIT cache *)
|
||||||
|
mutable l_call_count : int; (** Tiered-compilation counter — JIT after threshold calls *)
|
||||||
|
l_uid : int; (** Unique identity for LRU cache tracking *)
|
||||||
}
|
}
|
||||||
|
|
||||||
and component = {
|
and component = {
|
||||||
@@ -444,12 +446,60 @@ let unwrap_env_val = function
|
|||||||
| Env e -> e
|
| Env e -> e
|
||||||
| _ -> raise (Eval_error "make_lambda: expected env for closure")
|
| _ -> raise (Eval_error "make_lambda: expected env for closure")
|
||||||
|
|
||||||
|
(* Lambda UID — minted on construction, used as LRU cache key (Phase 2). *)
|
||||||
|
let lambda_uid_counter = ref 0
|
||||||
|
let next_lambda_uid () = incr lambda_uid_counter; !lambda_uid_counter
|
||||||
|
|
||||||
let make_lambda params body closure =
|
let make_lambda params body closure =
|
||||||
let ps = match params with
|
let ps = match params with
|
||||||
| List items -> List.map value_to_string items
|
| List items -> List.map value_to_string items
|
||||||
| _ -> value_to_string_list params
|
| _ -> value_to_string_list params
|
||||||
in
|
in
|
||||||
Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None; l_compiled = None }
|
Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None; l_compiled = None; l_call_count = 0; l_uid = next_lambda_uid () }
|
||||||
|
|
||||||
|
(** {1 JIT cache control}
|
||||||
|
|
||||||
|
Tiered compilation: only JIT a lambda after it's been called [jit_threshold]
|
||||||
|
times. This filters out one-shot lambdas (test harness, dynamic eval, REPLs)
|
||||||
|
so they never enter the JIT cache. Counters are exposed to SX as [(jit-stats)].
|
||||||
|
|
||||||
|
These live here (in sx_types) rather than sx_vm so [sx_primitives] can read
|
||||||
|
them without creating a sx_primitives → sx_vm dependency cycle. *)
|
||||||
|
let jit_threshold = ref 4
|
||||||
|
let jit_compiled_count = ref 0
|
||||||
|
let jit_skipped_count = ref 0
|
||||||
|
let jit_threshold_skipped_count = ref 0
|
||||||
|
|
||||||
|
(** {2 JIT cache LRU eviction — Phase 2}
|
||||||
|
|
||||||
|
Once a lambda crosses the threshold, its [l_compiled] slot is filled.
|
||||||
|
To bound memory under unbounded compilation pressure, track all live
|
||||||
|
compiled lambdas in FIFO order, and evict from the head when the count
|
||||||
|
exceeds [jit_budget].
|
||||||
|
|
||||||
|
[lambda_uid_counter] mints unique identities on lambda creation; the
|
||||||
|
LRU queue holds these IDs paired with a back-reference to the lambda
|
||||||
|
so we can clear its [l_compiled] slot on eviction.
|
||||||
|
|
||||||
|
Budget of 0 = no cache (disable JIT entirely).
|
||||||
|
Budget of [max_int] = unbounded (legacy behaviour). Default 5000 is
|
||||||
|
a generous ceiling for any realistic page; the test harness compiles
|
||||||
|
~3000 distinct one-shot lambdas in a full run but tiered compilation
|
||||||
|
(Phase 1) means most never enter the cache, so steady-state count
|
||||||
|
stays small.
|
||||||
|
|
||||||
|
[lambda_uid_counter] and [next_lambda_uid] are defined above
|
||||||
|
[make_lambda] (which uses them on construction). *)
|
||||||
|
let jit_budget = ref 5000
|
||||||
|
let jit_evicted_count = ref 0
|
||||||
|
|
||||||
|
(** Live compiled lambdas in FIFO order — front is oldest, back is newest.
|
||||||
|
Each entry is (uid, lambda); on eviction we clear lambda.l_compiled and
|
||||||
|
drop from the queue. Using a mutable Queue rather than a hand-rolled
|
||||||
|
linked list because eviction is amortised O(1) at the head and inserts
|
||||||
|
are O(1) at the tail. *)
|
||||||
|
let jit_cache_queue : (int * value) Queue.t = Queue.create ()
|
||||||
|
let jit_cache_size () = Queue.length jit_cache_queue
|
||||||
|
|
||||||
let make_component name params has_children body closure affinity =
|
let make_component name params has_children body closure affinity =
|
||||||
let n = value_to_string name in
|
let n = value_to_string name in
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ let () = Sx_types._convert_vm_suspension := (fun exn ->
|
|||||||
let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option) ref =
|
let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option) ref =
|
||||||
ref (fun _ _ -> None)
|
ref (fun _ _ -> None)
|
||||||
|
|
||||||
|
(* JIT threshold and counters live in Sx_types so primitives can read them
|
||||||
|
without creating a sx_primitives → sx_vm dependency cycle. *)
|
||||||
|
|
||||||
(** Sentinel closure indicating JIT compilation was attempted and failed.
|
(** Sentinel closure indicating JIT compilation was attempted and failed.
|
||||||
Prevents retrying compilation on every call. *)
|
Prevents retrying compilation on every call. *)
|
||||||
let jit_failed_sentinel = {
|
let jit_failed_sentinel = {
|
||||||
@@ -364,13 +367,29 @@ and vm_call vm f args =
|
|||||||
| None ->
|
| None ->
|
||||||
if l.l_name <> None
|
if l.l_name <> None
|
||||||
then begin
|
then begin
|
||||||
|
l.l_call_count <- l.l_call_count + 1;
|
||||||
|
if l.l_call_count >= !Sx_types.jit_threshold && !Sx_types.jit_budget > 0 then begin
|
||||||
l.l_compiled <- Some jit_failed_sentinel;
|
l.l_compiled <- Some jit_failed_sentinel;
|
||||||
match !jit_compile_ref l vm.globals with
|
match !jit_compile_ref l vm.globals with
|
||||||
| Some cl ->
|
| Some cl ->
|
||||||
|
incr Sx_types.jit_compiled_count;
|
||||||
l.l_compiled <- Some cl;
|
l.l_compiled <- Some cl;
|
||||||
|
(* Phase 2 LRU: track this compiled lambda; if cache exceeds budget,
|
||||||
|
evict the oldest by clearing its l_compiled slot. *)
|
||||||
|
Queue.add (l.l_uid, Lambda l) Sx_types.jit_cache_queue;
|
||||||
|
while Queue.length Sx_types.jit_cache_queue > !Sx_types.jit_budget do
|
||||||
|
(match Queue.pop Sx_types.jit_cache_queue with
|
||||||
|
| (_, Lambda ev_l) -> ev_l.l_compiled <- None; incr Sx_types.jit_evicted_count
|
||||||
|
| _ -> ())
|
||||||
|
done;
|
||||||
push_closure_frame vm cl args
|
push_closure_frame vm cl args
|
||||||
| None ->
|
| None ->
|
||||||
|
incr Sx_types.jit_skipped_count;
|
||||||
push vm (cek_call_or_suspend vm f (List args))
|
push vm (cek_call_or_suspend vm f (List args))
|
||||||
|
end else begin
|
||||||
|
incr Sx_types.jit_threshold_skipped_count;
|
||||||
|
push vm (cek_call_or_suspend vm f (List args))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
push vm (cek_call_or_suspend vm f (List args)))
|
push vm (cek_call_or_suspend vm f (List args)))
|
||||||
|
|||||||
@@ -25,8 +25,9 @@
|
|||||||
; Glyph classification sets
|
; Glyph classification sets
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
(define apl-parse-op-glyphs
|
(define
|
||||||
(list "/" "\\" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@"))
|
apl-parse-op-glyphs
|
||||||
|
(list "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@"))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
apl-parse-fn-glyphs
|
apl-parse-fn-glyphs
|
||||||
@@ -82,22 +83,48 @@
|
|||||||
"⍎"
|
"⍎"
|
||||||
"⍕"))
|
"⍕"))
|
||||||
|
|
||||||
(define apl-quad-fn-names (list "⎕FMT"))
|
(define apl-quad-fn-names (list "⎕FMT" "⎕←"))
|
||||||
|
|
||||||
(define
|
(define apl-known-fn-names (list))
|
||||||
apl-parse-op-glyph?
|
|
||||||
(fn (v) (some (fn (g) (= g v)) apl-parse-op-glyphs)))
|
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Token accessors
|
; Token accessors
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-collect-fn-bindings
|
||||||
|
(fn
|
||||||
|
(stmt-groups)
|
||||||
|
(set! apl-known-fn-names (list))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(toks)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(>= (len toks) 3)
|
||||||
|
(= (tok-type (nth toks 0)) :name)
|
||||||
|
(= (tok-type (nth toks 1)) :assign)
|
||||||
|
(= (tok-type (nth toks 2)) :lbrace))
|
||||||
|
(set!
|
||||||
|
apl-known-fn-names
|
||||||
|
(cons (tok-val (nth toks 0)) apl-known-fn-names))))
|
||||||
|
stmt-groups)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-parse-op-glyph?
|
||||||
|
(fn (v) (some (fn (g) (= g v)) apl-parse-op-glyphs)))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
apl-parse-fn-glyph?
|
apl-parse-fn-glyph?
|
||||||
(fn (v) (some (fn (g) (= g v)) apl-parse-fn-glyphs)))
|
(fn (v) (some (fn (g) (= g v)) apl-parse-fn-glyphs)))
|
||||||
|
|
||||||
(define tok-type (fn (tok) (get tok :type)))
|
(define tok-type (fn (tok) (get tok :type)))
|
||||||
|
|
||||||
|
; ============================================================
|
||||||
|
; Collect trailing operators starting at index i
|
||||||
|
; Returns {:ops (op ...) :end new-i}
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
(define tok-val (fn (tok) (get tok :value)))
|
(define tok-val (fn (tok) (get tok :value)))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -107,8 +134,8 @@
|
|||||||
(and (= (tok-type tok) :glyph) (apl-parse-op-glyph? (tok-val tok)))))
|
(and (= (tok-type tok) :glyph) (apl-parse-op-glyph? (tok-val tok)))))
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Collect trailing operators starting at index i
|
; Build a derived-fn node by chaining operators left-to-right
|
||||||
; Returns {:ops (op ...) :end new-i}
|
; (+/¨ → (:derived-fn "¨" (:derived-fn "/" (:fn-glyph "+"))))
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -119,15 +146,17 @@
|
|||||||
(and (= (tok-type tok) :glyph) (apl-parse-fn-glyph? (tok-val tok)))
|
(and (= (tok-type tok) :glyph) (apl-parse-fn-glyph? (tok-val tok)))
|
||||||
(and
|
(and
|
||||||
(= (tok-type tok) :name)
|
(= (tok-type tok) :name)
|
||||||
(some (fn (q) (= q (tok-val tok))) apl-quad-fn-names)))))
|
(or
|
||||||
|
(some (fn (q) (= q (tok-val tok))) apl-quad-fn-names)
|
||||||
|
(some (fn (q) (= q (tok-val tok))) apl-known-fn-names))))))
|
||||||
|
|
||||||
|
; ============================================================
|
||||||
|
; Find matching close bracket/paren/brace
|
||||||
|
; Returns the index of the matching close token
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
(define collect-ops (fn (tokens i) (collect-ops-loop tokens i (list))))
|
(define collect-ops (fn (tokens i) (collect-ops-loop tokens i (list))))
|
||||||
|
|
||||||
; ============================================================
|
|
||||||
; Build a derived-fn node by chaining operators left-to-right
|
|
||||||
; (+/¨ → (:derived-fn "¨" (:derived-fn "/" (:fn-glyph "+"))))
|
|
||||||
; ============================================================
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
collect-ops-loop
|
collect-ops-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -143,8 +172,10 @@
|
|||||||
{:end i :ops acc})))))
|
{:end i :ops acc})))))
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Find matching close bracket/paren/brace
|
; Segment collection: scan tokens left-to-right, building
|
||||||
; Returns the index of the matching close token
|
; a list of {:kind "val"/"fn" :node ast} segments.
|
||||||
|
; Operators following function glyphs are merged into
|
||||||
|
; derived-fn nodes during this pass.
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -163,12 +194,20 @@
|
|||||||
(find-matching-close-loop tokens start open-type close-type 1)))
|
(find-matching-close-loop tokens start open-type close-type 1)))
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Segment collection: scan tokens left-to-right, building
|
; Build tree from segment list
|
||||||
; a list of {:kind "val"/"fn" :node ast} segments.
|
;
|
||||||
; Operators following function glyphs are merged into
|
; The segments are in left-to-right order.
|
||||||
; derived-fn nodes during this pass.
|
; APL evaluates right-to-left, so the LEFTMOST function is
|
||||||
|
; the outermost (last-evaluated) node.
|
||||||
|
;
|
||||||
|
; Patterns:
|
||||||
|
; [val] → val node
|
||||||
|
; [fn val ...] → (:monad fn (build-tree rest))
|
||||||
|
; [val fn val ...] → (:dyad fn val (build-tree rest))
|
||||||
|
; [val val ...] → (:vec val1 val2 ...) — strand
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
|
; Find the index of the first function segment (returns -1 if none)
|
||||||
(define
|
(define
|
||||||
find-matching-close-loop
|
find-matching-close-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -208,21 +247,9 @@
|
|||||||
collect-segments
|
collect-segments
|
||||||
(fn (tokens) (collect-segments-loop tokens 0 (list))))
|
(fn (tokens) (collect-segments-loop tokens 0 (list))))
|
||||||
|
|
||||||
; ============================================================
|
; Build an array node from 0..n value segments
|
||||||
; Build tree from segment list
|
; If n=1 → return that segment's node
|
||||||
;
|
; If n>1 → return (:vec node1 node2 ...)
|
||||||
; The segments are in left-to-right order.
|
|
||||||
; APL evaluates right-to-left, so the LEFTMOST function is
|
|
||||||
; the outermost (last-evaluated) node.
|
|
||||||
;
|
|
||||||
; Patterns:
|
|
||||||
; [val] → val node
|
|
||||||
; [fn val ...] → (:monad fn (build-tree rest))
|
|
||||||
; [val fn val ...] → (:dyad fn val (build-tree rest))
|
|
||||||
; [val val ...] → (:vec val1 val2 ...) — strand
|
|
||||||
; ============================================================
|
|
||||||
|
|
||||||
; Find the index of the first function segment (returns -1 if none)
|
|
||||||
(define
|
(define
|
||||||
collect-segments-loop
|
collect-segments-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -242,36 +269,71 @@
|
|||||||
((= tt :str)
|
((= tt :str)
|
||||||
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :str tv)})))
|
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :str tv)})))
|
||||||
((= tt :name)
|
((= tt :name)
|
||||||
(if
|
(cond
|
||||||
(some (fn (q) (= q tv)) apl-quad-fn-names)
|
((and (< (+ i 1) (len tokens)) (= (tok-type (nth tokens (+ i 1))) :assign))
|
||||||
|
(let
|
||||||
|
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||||
|
(let
|
||||||
|
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||||
|
(collect-segments-loop
|
||||||
|
tokens
|
||||||
|
(len tokens)
|
||||||
|
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)})))))
|
||||||
|
((some (fn (q) (= q tv)) apl-quad-fn-names)
|
||||||
(let
|
(let
|
||||||
((op-result (collect-ops tokens (+ i 1))))
|
((op-result (collect-ops tokens (+ i 1))))
|
||||||
(let
|
(let
|
||||||
((ops (get op-result :ops)) (ni (get op-result :end)))
|
((ops (get op-result :ops))
|
||||||
|
(ni (get op-result :end)))
|
||||||
(let
|
(let
|
||||||
((fn-node (build-derived-fn (list :fn-glyph tv) ops)))
|
((fn-node (build-derived-fn (list :fn-glyph tv) ops)))
|
||||||
(collect-segments-loop
|
(collect-segments-loop
|
||||||
tokens
|
tokens
|
||||||
ni
|
ni
|
||||||
(append acc {:kind "fn" :node fn-node})))))
|
(append acc {:kind "fn" :node fn-node}))))))
|
||||||
|
((some (fn (q) (= q tv)) apl-known-fn-names)
|
||||||
|
(let
|
||||||
|
((op-result (collect-ops tokens (+ i 1))))
|
||||||
|
(let
|
||||||
|
((ops (get op-result :ops))
|
||||||
|
(ni (get op-result :end)))
|
||||||
|
(let
|
||||||
|
((fn-node (build-derived-fn (list :fn-name tv) ops)))
|
||||||
|
(collect-segments-loop
|
||||||
|
tokens
|
||||||
|
ni
|
||||||
|
(append acc {:kind "fn" :node fn-node}))))))
|
||||||
|
(else
|
||||||
(let
|
(let
|
||||||
((br (maybe-bracket (list :name tv) tokens (+ i 1))))
|
((br (maybe-bracket (list :name tv) tokens (+ i 1))))
|
||||||
(collect-segments-loop
|
(collect-segments-loop
|
||||||
tokens
|
tokens
|
||||||
(nth br 1)
|
(nth br 1)
|
||||||
(append acc {:kind "val" :node (nth br 0)})))))
|
(append acc {:kind "val" :node (nth br 0)}))))))
|
||||||
((= tt :lparen)
|
((= tt :lparen)
|
||||||
(let
|
(let
|
||||||
((end (find-matching-close tokens (+ i 1) :lparen :rparen)))
|
((end (find-matching-close tokens (+ i 1) :lparen :rparen)))
|
||||||
(let
|
(let
|
||||||
((inner-tokens (slice tokens (+ i 1) end))
|
((inner-tokens (slice tokens (+ i 1) end))
|
||||||
(after (+ end 1)))
|
(after (+ end 1)))
|
||||||
|
(let
|
||||||
|
((inner-segs (collect-segments inner-tokens)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(>= (len inner-segs) 2)
|
||||||
|
(every? (fn (s) (= (get s :kind) "fn")) inner-segs))
|
||||||
|
(let
|
||||||
|
((train-node (cons :train (map (fn (s) (get s :node)) inner-segs))))
|
||||||
|
(collect-segments-loop
|
||||||
|
tokens
|
||||||
|
after
|
||||||
|
(append acc {:kind "fn" :node train-node})))
|
||||||
(let
|
(let
|
||||||
((br (maybe-bracket (parse-apl-expr inner-tokens) tokens after)))
|
((br (maybe-bracket (parse-apl-expr inner-tokens) tokens after)))
|
||||||
(collect-segments-loop
|
(collect-segments-loop
|
||||||
tokens
|
tokens
|
||||||
(nth br 1)
|
(nth br 1)
|
||||||
(append acc {:kind "val" :node (nth br 0)}))))))
|
(append acc {:kind "val" :node (nth br 0)}))))))))
|
||||||
((= tt :lbrace)
|
((= tt :lbrace)
|
||||||
(let
|
(let
|
||||||
((end (find-matching-close tokens (+ i 1) :lbrace :rbrace)))
|
((end (find-matching-close tokens (+ i 1) :lbrace :rbrace)))
|
||||||
@@ -282,10 +344,22 @@
|
|||||||
((= tt :glyph)
|
((= tt :glyph)
|
||||||
(cond
|
(cond
|
||||||
((or (= tv "⍺") (= tv "⍵"))
|
((or (= tv "⍺") (= tv "⍵"))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(< (+ i 1) (len tokens))
|
||||||
|
(= (tok-type (nth tokens (+ i 1))) :assign))
|
||||||
|
(let
|
||||||
|
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||||
|
(let
|
||||||
|
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||||
|
(collect-segments-loop
|
||||||
|
tokens
|
||||||
|
(len tokens)
|
||||||
|
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)}))))
|
||||||
(collect-segments-loop
|
(collect-segments-loop
|
||||||
tokens
|
tokens
|
||||||
(+ i 1)
|
(+ i 1)
|
||||||
(append acc {:kind "val" :node (list :name tv)})))
|
(append acc {:kind "val" :node (list :name tv)}))))
|
||||||
((= tv "∇")
|
((= tv "∇")
|
||||||
(collect-segments-loop
|
(collect-segments-loop
|
||||||
tokens
|
tokens
|
||||||
@@ -340,15 +414,34 @@
|
|||||||
ni
|
ni
|
||||||
(append acc {:kind "fn" :node fn-node})))))))
|
(append acc {:kind "fn" :node fn-node})))))))
|
||||||
((apl-parse-op-glyph? tv)
|
((apl-parse-op-glyph? tv)
|
||||||
(collect-segments-loop tokens (+ i 1) acc))
|
(if
|
||||||
|
(or (= tv "/") (= tv "⌿") (= tv "\\") (= tv "⍀"))
|
||||||
|
(let
|
||||||
|
((next-i (+ i 1)))
|
||||||
|
(let
|
||||||
|
((next-tok (if (< next-i n) (nth tokens next-i) nil)))
|
||||||
|
(let
|
||||||
|
((mod (if (and next-tok (= (tok-type next-tok) :glyph) (or (= (get next-tok :value) "⍨") (= (get next-tok :value) "¨"))) (get next-tok :value) nil))
|
||||||
|
(base-fn-node (list :fn-glyph tv)))
|
||||||
|
(let
|
||||||
|
((node (if mod (list :derived-fn mod base-fn-node) base-fn-node))
|
||||||
|
(advance (if mod 2 1)))
|
||||||
|
(collect-segments-loop
|
||||||
|
tokens
|
||||||
|
(+ i advance)
|
||||||
|
(append acc {:kind "fn" :node node}))))))
|
||||||
|
(collect-segments-loop tokens (+ i 1) acc)))
|
||||||
(true (collect-segments-loop tokens (+ i 1) acc))))
|
(true (collect-segments-loop tokens (+ i 1) acc))))
|
||||||
(true (collect-segments-loop tokens (+ i 1) acc))))))))
|
(true (collect-segments-loop tokens (+ i 1) acc))))))))
|
||||||
|
|
||||||
(define find-first-fn (fn (segs) (find-first-fn-loop segs 0)))
|
(define find-first-fn (fn (segs) (find-first-fn-loop segs 0)))
|
||||||
|
|
||||||
; Build an array node from 0..n value segments
|
|
||||||
; If n=1 → return that segment's node
|
; ============================================================
|
||||||
; If n>1 → return (:vec node1 node2 ...)
|
; Split token list on statement separators (diamond / newline)
|
||||||
|
; Only splits at depth 0 (ignores separators inside { } or ( ) )
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
find-first-fn-loop
|
find-first-fn-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -370,10 +463,9 @@
|
|||||||
(get (first segs) :node)
|
(get (first segs) :node)
|
||||||
(cons :vec (map (fn (s) (get s :node)) segs)))))
|
(cons :vec (map (fn (s) (get s :node)) segs)))))
|
||||||
|
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Split token list on statement separators (diamond / newline)
|
; Parse a dfn body (tokens between { and })
|
||||||
; Only splits at depth 0 (ignores separators inside { } or ( ) )
|
; Handles guard expressions: cond : expr
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -408,11 +500,6 @@
|
|||||||
split-statements
|
split-statements
|
||||||
(fn (tokens) (split-statements-loop tokens (list) (list) 0)))
|
(fn (tokens) (split-statements-loop tokens (list) (list) 0)))
|
||||||
|
|
||||||
; ============================================================
|
|
||||||
; Parse a dfn body (tokens between { and })
|
|
||||||
; Handles guard expressions: cond : expr
|
|
||||||
; ============================================================
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
split-statements-loop
|
split-statements-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -467,6 +554,10 @@
|
|||||||
((stmt-groups (split-statements tokens)))
|
((stmt-groups (split-statements tokens)))
|
||||||
(let ((stmts (map parse-dfn-stmt stmt-groups))) (cons :dfn stmts)))))
|
(let ((stmts (map parse-dfn-stmt stmt-groups))) (cons :dfn stmts)))))
|
||||||
|
|
||||||
|
; ============================================================
|
||||||
|
; Parse a single statement (assignment or expression)
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
parse-dfn-stmt
|
parse-dfn-stmt
|
||||||
(fn
|
(fn
|
||||||
@@ -483,12 +574,17 @@
|
|||||||
(parse-apl-expr body-tokens)))
|
(parse-apl-expr body-tokens)))
|
||||||
(parse-stmt tokens)))))
|
(parse-stmt tokens)))))
|
||||||
|
|
||||||
|
; ============================================================
|
||||||
|
; Parse an expression from a flat token list
|
||||||
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
find-top-level-colon
|
find-top-level-colon
|
||||||
(fn (tokens i) (find-top-level-colon-loop tokens i 0)))
|
(fn (tokens i) (find-top-level-colon-loop tokens i 0)))
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Parse a single statement (assignment or expression)
|
; Main entry point
|
||||||
|
; parse-apl: string → AST
|
||||||
; ============================================================
|
; ============================================================
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -508,10 +604,6 @@
|
|||||||
((and (= tt :colon) (= depth 0)) i)
|
((and (= tt :colon) (= depth 0)) i)
|
||||||
(true (find-top-level-colon-loop tokens (+ i 1) depth)))))))
|
(true (find-top-level-colon-loop tokens (+ i 1) depth)))))))
|
||||||
|
|
||||||
; ============================================================
|
|
||||||
; Parse an expression from a flat token list
|
|
||||||
; ============================================================
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
parse-stmt
|
parse-stmt
|
||||||
(fn
|
(fn
|
||||||
@@ -526,11 +618,6 @@
|
|||||||
(parse-apl-expr (slice tokens 2)))
|
(parse-apl-expr (slice tokens 2)))
|
||||||
(parse-apl-expr tokens))))
|
(parse-apl-expr tokens))))
|
||||||
|
|
||||||
; ============================================================
|
|
||||||
; Main entry point
|
|
||||||
; parse-apl: string → AST
|
|
||||||
; ============================================================
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
parse-apl-expr
|
parse-apl-expr
|
||||||
(fn
|
(fn
|
||||||
@@ -547,13 +634,52 @@
|
|||||||
((tokens (apl-tokenize src)))
|
((tokens (apl-tokenize src)))
|
||||||
(let
|
(let
|
||||||
((stmt-groups (split-statements tokens)))
|
((stmt-groups (split-statements tokens)))
|
||||||
|
(begin
|
||||||
|
(apl-collect-fn-bindings stmt-groups)
|
||||||
(if
|
(if
|
||||||
(= (len stmt-groups) 0)
|
(= (len stmt-groups) 0)
|
||||||
nil
|
nil
|
||||||
(if
|
(if
|
||||||
(= (len stmt-groups) 1)
|
(= (len stmt-groups) 1)
|
||||||
(parse-stmt (first stmt-groups))
|
(parse-stmt (first stmt-groups))
|
||||||
(cons :program (map parse-stmt stmt-groups))))))))
|
(cons :program (map parse-stmt stmt-groups)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
split-bracket-loop
|
||||||
|
(fn
|
||||||
|
(tokens current acc depth)
|
||||||
|
(if
|
||||||
|
(= (len tokens) 0)
|
||||||
|
(append acc (list current))
|
||||||
|
(let
|
||||||
|
((tok (first tokens)) (more (rest tokens)))
|
||||||
|
(let
|
||||||
|
((tt (tok-type tok)))
|
||||||
|
(cond
|
||||||
|
((or (= tt :lparen) (= tt :lbrace) (= tt :lbracket))
|
||||||
|
(split-bracket-loop
|
||||||
|
more
|
||||||
|
(append current (list tok))
|
||||||
|
acc
|
||||||
|
(+ depth 1)))
|
||||||
|
((or (= tt :rparen) (= tt :rbrace) (= tt :rbracket))
|
||||||
|
(split-bracket-loop
|
||||||
|
more
|
||||||
|
(append current (list tok))
|
||||||
|
acc
|
||||||
|
(- depth 1)))
|
||||||
|
((and (= tt :semi) (= depth 0))
|
||||||
|
(split-bracket-loop
|
||||||
|
more
|
||||||
|
(list)
|
||||||
|
(append acc (list current))
|
||||||
|
depth))
|
||||||
|
(else
|
||||||
|
(split-bracket-loop more (append current (list tok)) acc depth))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
split-bracket-content
|
||||||
|
(fn (tokens) (split-bracket-loop tokens (list) (list) 0)))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
maybe-bracket
|
maybe-bracket
|
||||||
@@ -568,9 +694,18 @@
|
|||||||
(let
|
(let
|
||||||
((inner-tokens (slice tokens (+ after 1) end))
|
((inner-tokens (slice tokens (+ after 1) end))
|
||||||
(next-after (+ end 1)))
|
(next-after (+ end 1)))
|
||||||
|
(let
|
||||||
|
((sections (split-bracket-content inner-tokens)))
|
||||||
|
(if
|
||||||
|
(= (len sections) 1)
|
||||||
(let
|
(let
|
||||||
((idx-expr (parse-apl-expr inner-tokens)))
|
((idx-expr (parse-apl-expr inner-tokens)))
|
||||||
(let
|
(let
|
||||||
((indexed (list :dyad (list :fn-glyph "⌷") idx-expr val-node)))
|
((indexed (list :dyad (list :fn-glyph "⌷") idx-expr val-node)))
|
||||||
(maybe-bracket indexed tokens next-after)))))
|
(maybe-bracket indexed tokens next-after)))
|
||||||
|
(let
|
||||||
|
((axis-exprs (map (fn (toks) (if (= (len toks) 0) :all (parse-apl-expr toks))) sections)))
|
||||||
|
(let
|
||||||
|
((indexed (cons :bracket (cons val-node axis-exprs))))
|
||||||
|
(maybe-bracket indexed tokens next-after)))))))
|
||||||
(list val-node after))))
|
(list val-node after))))
|
||||||
|
|||||||
@@ -65,10 +65,30 @@
|
|||||||
(get a :shape)
|
(get a :shape)
|
||||||
(map (fn (x) (f x sv)) (get a :ravel)))))
|
(map (fn (x) (f x sv)) (get a :ravel)))))
|
||||||
(else
|
(else
|
||||||
(if
|
(let
|
||||||
(equal? (get a :shape) (get b :shape))
|
((a-shape (get a :shape)) (b-shape (get b :shape)))
|
||||||
(make-array (get a :shape) (map f (get a :ravel) (get b :ravel)))
|
(cond
|
||||||
(error "length error: shape mismatch"))))))
|
((equal? a-shape b-shape)
|
||||||
|
(make-array a-shape (map f (get a :ravel) (get b :ravel))))
|
||||||
|
((and (= (len a-shape) 1) (> (len b-shape) 1))
|
||||||
|
(make-array
|
||||||
|
(append a-shape b-shape)
|
||||||
|
(flatten
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(x)
|
||||||
|
(get (broadcast-dyadic f (apl-scalar x) b) :ravel))
|
||||||
|
(get a :ravel)))))
|
||||||
|
((and (= (len b-shape) 1) (> (len a-shape) 1))
|
||||||
|
(make-array
|
||||||
|
(append a-shape b-shape)
|
||||||
|
(flatten
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(acell)
|
||||||
|
(get (broadcast-dyadic f (apl-scalar acell) b) :ravel))
|
||||||
|
(get a :ravel)))))
|
||||||
|
(else (error "length error: shape mismatch"))))))))
|
||||||
|
|
||||||
; ============================================================
|
; ============================================================
|
||||||
; Arithmetic primitives
|
; Arithmetic primitives
|
||||||
@@ -808,6 +828,125 @@
|
|||||||
((picked (map (fn (i) (nth arr-ravel i)) kept)))
|
((picked (map (fn (i) (nth arr-ravel i)) kept)))
|
||||||
(make-array (list (len picked)) picked))))))
|
(make-array (list (len picked)) picked))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-compress-first
|
||||||
|
(fn
|
||||||
|
(mask arr)
|
||||||
|
(let
|
||||||
|
((mask-ravel (get mask :ravel))
|
||||||
|
(shape (get arr :shape))
|
||||||
|
(ravel (get arr :ravel)))
|
||||||
|
(if
|
||||||
|
(< (len shape) 2)
|
||||||
|
(apl-compress mask arr)
|
||||||
|
(let
|
||||||
|
((rows (first shape)) (cols (last shape)))
|
||||||
|
(let
|
||||||
|
((kept-rows (filter (fn (i) (not (= 0 (nth mask-ravel i)))) (range 0 rows))))
|
||||||
|
(let
|
||||||
|
((new-ravel (reduce (fn (acc r) (append acc (map (fn (j) (nth ravel (+ (* r cols) j))) (range 0 cols)))) (list) kept-rows)))
|
||||||
|
(make-array (cons (len kept-rows) (rest shape)) new-ravel))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-where
|
||||||
|
(fn
|
||||||
|
(arr)
|
||||||
|
(let
|
||||||
|
((ravel (get arr :ravel)) (io (disclose (apl-quad-io))))
|
||||||
|
(let
|
||||||
|
((indices (filter (fn (i) (not (= (nth ravel i) 0))) (range 0 (len ravel)))))
|
||||||
|
(apl-vector (map (fn (i) (+ i io)) indices))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-interval-index
|
||||||
|
(fn
|
||||||
|
(breaks vals)
|
||||||
|
(let
|
||||||
|
((b-ravel (get breaks :ravel))
|
||||||
|
(v-ravel
|
||||||
|
(if (scalar? vals) (list (disclose vals)) (get vals :ravel))))
|
||||||
|
(let
|
||||||
|
((result (map (fn (y) (len (filter (fn (b) (<= b y)) b-ravel))) v-ravel)))
|
||||||
|
(if
|
||||||
|
(scalar? vals)
|
||||||
|
(apl-scalar (first result))
|
||||||
|
(make-array (get vals :shape) result))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-unique
|
||||||
|
(fn
|
||||||
|
(arr)
|
||||||
|
(let
|
||||||
|
((ravel (if (scalar? arr) (list (disclose arr)) (get arr :ravel))))
|
||||||
|
(let
|
||||||
|
((dedup (reduce (fn (acc x) (if (index-of acc x) acc (append acc (list x)))) (list) ravel)))
|
||||||
|
(apl-vector dedup)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-union
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(let
|
||||||
|
((a-ravel (if (scalar? a) (list (disclose a)) (get a :ravel)))
|
||||||
|
(b-ravel (if (scalar? b) (list (disclose b)) (get b :ravel))))
|
||||||
|
(let
|
||||||
|
((a-dedup (reduce (fn (acc x) (if (index-of acc x) acc (append acc (list x)))) (list) a-ravel)))
|
||||||
|
(let
|
||||||
|
((b-extra (filter (fn (x) (not (index-of a-dedup x))) b-ravel)))
|
||||||
|
(let
|
||||||
|
((b-extra-dedup (reduce (fn (acc x) (if (index-of acc x) acc (append acc (list x)))) (list) b-extra)))
|
||||||
|
(apl-vector (append a-dedup b-extra-dedup))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-intersect
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(let
|
||||||
|
((a-ravel (if (scalar? a) (list (disclose a)) (get a :ravel)))
|
||||||
|
(b-ravel (if (scalar? b) (list (disclose b)) (get b :ravel))))
|
||||||
|
(apl-vector (filter (fn (x) (index-of b-ravel x)) a-ravel)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-decode
|
||||||
|
(fn
|
||||||
|
(base digits)
|
||||||
|
(let
|
||||||
|
((d-ravel (if (scalar? digits) (list (disclose digits)) (get digits :ravel))))
|
||||||
|
(let
|
||||||
|
((d-len (len d-ravel)))
|
||||||
|
(let
|
||||||
|
((b-ravel (if (scalar? base) (let ((b (disclose base))) (map (fn (i) b) (range 0 d-len))) (get base :ravel))))
|
||||||
|
(let
|
||||||
|
((result (reduce (fn (acc i) (if (= i 0) (nth d-ravel 0) (+ (* acc (nth b-ravel i)) (nth d-ravel i)))) 0 (range 0 d-len))))
|
||||||
|
(apl-scalar result)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-encode
|
||||||
|
(fn
|
||||||
|
(base val)
|
||||||
|
(let
|
||||||
|
((b-ravel (if (scalar? base) (list (disclose base)) (get base :ravel)))
|
||||||
|
(n (if (scalar? val) (disclose val) (first (get val :ravel)))))
|
||||||
|
(let
|
||||||
|
((b-len (len b-ravel)))
|
||||||
|
(let
|
||||||
|
((result (reduce (fn (acc-and-n i) (let ((acc (first acc-and-n)) (rem (nth acc-and-n 1))) (let ((b (nth b-ravel (- (- b-len 1) i)))) (if (= b 0) (list (cons rem acc) 0) (list (cons (modulo rem b) acc) (floor (/ rem b))))))) (list (list) n) (range 0 b-len))))
|
||||||
|
(apl-vector (first result)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-partition
|
||||||
|
(fn
|
||||||
|
(mask val)
|
||||||
|
(let
|
||||||
|
((m-ravel (if (scalar? mask) (list (disclose mask)) (get mask :ravel)))
|
||||||
|
(v-ravel
|
||||||
|
(if (scalar? val) (list (disclose val)) (get val :ravel))))
|
||||||
|
(let
|
||||||
|
((n (len m-ravel)))
|
||||||
|
(let
|
||||||
|
((built (reduce (fn (acc-and-prev i) (let ((acc (first acc-and-prev)) (prev (nth acc-and-prev 1))) (let ((mi (nth m-ravel i)) (vi (nth v-ravel i))) (cond ((= mi 0) (list acc 0)) ((> mi prev) (list (append acc (list (list vi))) mi)) (else (let ((idx (- (len acc) 1))) (list (append (slice acc 0 idx) (list (append (nth acc idx) (list vi)))) mi))))))) (list (list) 0) (range 0 n))))
|
||||||
|
(apl-vector (map (fn (part) (apl-vector part)) (first built))))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
apl-primes
|
apl-primes
|
||||||
(fn
|
(fn
|
||||||
@@ -883,7 +1022,7 @@
|
|||||||
(let
|
(let
|
||||||
((sub (apl-permutations (- n 1))))
|
((sub (apl-permutations (- n 1))))
|
||||||
(reduce
|
(reduce
|
||||||
(fn (acc p) (append acc (apl-insert-everywhere n p)))
|
(fn (acc p) (append (apl-insert-everywhere n p) acc))
|
||||||
(list)
|
(list)
|
||||||
sub)))))
|
sub)))))
|
||||||
|
|
||||||
@@ -985,6 +1124,60 @@
|
|||||||
(some (fn (c) (= c 0)) codes)
|
(some (fn (c) (= c 0)) codes)
|
||||||
(some (fn (c) (= c (nth e 1))) codes)))))
|
(some (fn (c) (= c (nth e 1))) codes)))))
|
||||||
|
|
||||||
|
(define apl-rng-state 12345)
|
||||||
|
|
||||||
|
(define apl-rng-seed! (fn (s) (set! apl-rng-state s)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-rng-next!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(begin
|
||||||
|
(set!
|
||||||
|
apl-rng-state
|
||||||
|
(mod (+ (* apl-rng-state 1103515245) 12345) 2147483648))
|
||||||
|
apl-rng-state)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-roll
|
||||||
|
(fn
|
||||||
|
(arr)
|
||||||
|
(let
|
||||||
|
((n (if (scalar? arr) (first (get arr :ravel)) (first (get arr :ravel)))))
|
||||||
|
(apl-scalar (+ apl-io (mod (apl-rng-next!) n))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-cartesian
|
||||||
|
(fn
|
||||||
|
(lists)
|
||||||
|
(if
|
||||||
|
(= (len lists) 0)
|
||||||
|
(list (list))
|
||||||
|
(let
|
||||||
|
((rest-prods (apl-cartesian (rest lists))))
|
||||||
|
(reduce
|
||||||
|
(fn (acc x) (append acc (map (fn (p) (cons x p)) rest-prods)))
|
||||||
|
(list)
|
||||||
|
(first lists))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-bracket-multi
|
||||||
|
(fn
|
||||||
|
(axes arr)
|
||||||
|
(let
|
||||||
|
((shape (get arr :shape)) (ravel (get arr :ravel)))
|
||||||
|
(let
|
||||||
|
((rank (len shape)) (strides (apl-strides shape)))
|
||||||
|
(let
|
||||||
|
((axis-info (map (fn (i) (let ((a (nth axes i))) (cond ((= a nil) {:idxs (range 0 (nth shape i)) :scalar? false}) ((= (len (get a :shape)) 0) {:idxs (list (- (first (get a :ravel)) apl-io)) :scalar? true}) (else {:idxs (map (fn (x) (- x apl-io)) (get a :ravel)) :scalar? false})))) (range 0 rank))))
|
||||||
|
(let
|
||||||
|
((cells (apl-cartesian (map (fn (a) (get a :idxs)) axis-info))))
|
||||||
|
(let
|
||||||
|
((result-ravel (map (fn (cell) (let ((flat (reduce + 0 (map (fn (i) (* (nth cell i) (nth strides i))) (range 0 rank))))) (nth ravel flat))) cells)))
|
||||||
|
(let
|
||||||
|
((result-shape (filter (fn (x) (>= x 0)) (map (fn (i) (let ((a (nth axis-info i))) (if (get a :scalar?) -1 (len (get a :idxs))))) (range 0 rank)))))
|
||||||
|
(make-array result-shape result-ravel)))))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
apl-reduce
|
apl-reduce
|
||||||
(fn
|
(fn
|
||||||
@@ -1001,11 +1194,9 @@
|
|||||||
(if
|
(if
|
||||||
(= n 0)
|
(= n 0)
|
||||||
(apl-scalar 0)
|
(apl-scalar 0)
|
||||||
(apl-scalar
|
(let
|
||||||
(reduce
|
((rr (reduce (fn (a b) (let ((wa (if (= (type-of a) "dict") a (apl-scalar a))) (wb (if (= (type-of b) "dict") b (apl-scalar b)))) (let ((r (f wa wb))) (if (scalar? r) (disclose r) r)))) (first ravel) (rest ravel))))
|
||||||
(fn (a b) (disclose (f (apl-scalar a) (apl-scalar b))))
|
(if (= (type-of rr) "dict") rr (apl-scalar rr)))))
|
||||||
(first ravel)
|
|
||||||
(rest ravel)))))
|
|
||||||
(let
|
(let
|
||||||
((last-dim (last shape))
|
((last-dim (last shape))
|
||||||
(pre-shape (take shape (- (len shape) 1)))
|
(pre-shape (take shape (- (len shape) 1)))
|
||||||
@@ -1027,7 +1218,13 @@
|
|||||||
(reduce
|
(reduce
|
||||||
(fn
|
(fn
|
||||||
(a b)
|
(a b)
|
||||||
(disclose (f (apl-scalar a) (apl-scalar b))))
|
(let
|
||||||
|
((wa (if (= (type-of a) "dict") a (apl-scalar a)))
|
||||||
|
(wb
|
||||||
|
(if (= (type-of b) "dict") b (apl-scalar b))))
|
||||||
|
(let
|
||||||
|
((r (f wa wb)))
|
||||||
|
(if (scalar? r) (disclose r) r))))
|
||||||
(first elems)
|
(first elems)
|
||||||
(rest elems)))))
|
(rest elems)))))
|
||||||
(range 0 pre-size)))))))))
|
(range 0 pre-size)))))))))
|
||||||
@@ -1168,13 +1365,29 @@
|
|||||||
(cond
|
(cond
|
||||||
((and (scalar? a) (scalar? b)) (apl-scalar (disclose (f a b))))
|
((and (scalar? a) (scalar? b)) (apl-scalar (disclose (f a b))))
|
||||||
((scalar? a)
|
((scalar? a)
|
||||||
|
(let
|
||||||
|
((a-eff (let ((d (disclose a))) (if (= (type-of d) "dict") d a))))
|
||||||
(make-array
|
(make-array
|
||||||
(get b :shape)
|
(get b :shape)
|
||||||
(map (fn (x) (disclose (f a (apl-scalar x)))) (get b :ravel))))
|
(map
|
||||||
|
(fn
|
||||||
|
(x)
|
||||||
|
(let
|
||||||
|
((r (f a-eff (apl-scalar x))))
|
||||||
|
(if (scalar? r) (disclose r) r)))
|
||||||
|
(get b :ravel)))))
|
||||||
((scalar? b)
|
((scalar? b)
|
||||||
|
(let
|
||||||
|
((b-eff (let ((d (disclose b))) (if (= (type-of d) "dict") d b))))
|
||||||
(make-array
|
(make-array
|
||||||
(get a :shape)
|
(get a :shape)
|
||||||
(map (fn (x) (disclose (f (apl-scalar x) b))) (get a :ravel))))
|
(map
|
||||||
|
(fn
|
||||||
|
(x)
|
||||||
|
(let
|
||||||
|
((r (f (apl-scalar x) b-eff)))
|
||||||
|
(if (scalar? r) (disclose r) r)))
|
||||||
|
(get a :ravel)))))
|
||||||
(else
|
(else
|
||||||
(if
|
(if
|
||||||
(equal? (get a :shape) (get b :shape))
|
(equal? (get a :shape) (get b :shape))
|
||||||
@@ -1195,6 +1408,8 @@
|
|||||||
(b-shape (get b :shape))
|
(b-shape (get b :shape))
|
||||||
(a-ravel (get a :ravel))
|
(a-ravel (get a :ravel))
|
||||||
(b-ravel (get b :ravel)))
|
(b-ravel (get b :ravel)))
|
||||||
|
(let
|
||||||
|
((wrap (fn (x) (if (= (type-of x) "dict") x (apl-scalar x)))))
|
||||||
(make-array
|
(make-array
|
||||||
(append a-shape b-shape)
|
(append a-shape b-shape)
|
||||||
(flatten
|
(flatten
|
||||||
@@ -1202,9 +1417,13 @@
|
|||||||
(fn
|
(fn
|
||||||
(x)
|
(x)
|
||||||
(map
|
(map
|
||||||
(fn (y) (disclose (f (apl-scalar x) (apl-scalar y))))
|
(fn
|
||||||
|
(y)
|
||||||
|
(let
|
||||||
|
((r (f (wrap x) (wrap y))))
|
||||||
|
(if (scalar? r) (disclose r) r)))
|
||||||
b-ravel))
|
b-ravel))
|
||||||
a-ravel))))))
|
a-ravel)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
apl-inner
|
apl-inner
|
||||||
@@ -1228,25 +1447,12 @@
|
|||||||
((a-pre-size (reduce * 1 a-pre))
|
((a-pre-size (reduce * 1 a-pre))
|
||||||
(b-post-size (reduce * 1 b-post))
|
(b-post-size (reduce * 1 b-post))
|
||||||
(new-shape (append a-pre b-post)))
|
(new-shape (append a-pre b-post)))
|
||||||
(make-array
|
|
||||||
new-shape
|
|
||||||
(flatten
|
|
||||||
(map
|
|
||||||
(fn
|
|
||||||
(i)
|
|
||||||
(map
|
|
||||||
(fn
|
|
||||||
(j)
|
|
||||||
(let
|
(let
|
||||||
((pairs (map (fn (k) (disclose (g (apl-scalar (nth a-ravel (+ (* i inner-dim) k))) (apl-scalar (nth b-ravel (+ (* k b-post-size) j)))))) (range 0 inner-dim))))
|
((result (make-array new-shape (flatten (map (fn (i) (map (fn (j) (let ((pairs (map (fn (k) (let ((a-elem (nth a-ravel (+ (* i inner-dim) k))) (b-elem (nth b-ravel (+ (* k b-post-size) j)))) (let ((a-cell (if (= (type-of a-elem) "dict") (nth (get a-elem :ravel) j) a-elem)) (b-cell (if (= (type-of b-elem) "dict") (nth (get b-elem :ravel) 0) b-elem))) (disclose (g (apl-scalar a-cell) (apl-scalar b-cell)))))) (range 0 inner-dim)))) (reduce (fn (x y) (let ((wx (if (= (type-of x) "dict") x (apl-scalar x))) (wy (if (= (type-of y) "dict") y (apl-scalar y)))) (let ((r (f wx wy))) (if (scalar? r) (disclose r) r)))) (first pairs) (rest pairs)))) (range 0 b-post-size))) (range 0 a-pre-size))))))
|
||||||
(reduce
|
(if
|
||||||
(fn
|
(some (fn (x) (= (type-of x) "dict")) a-ravel)
|
||||||
(x y)
|
(enclose result)
|
||||||
(disclose (f (apl-scalar x) (apl-scalar y))))
|
result)))))))))
|
||||||
(first pairs)
|
|
||||||
(rest pairs))))
|
|
||||||
(range 0 b-post-size)))
|
|
||||||
(range 0 a-pre-size)))))))))))
|
|
||||||
|
|
||||||
(define apl-commute (fn (f x) (f x x)))
|
(define apl-commute (fn (f x) (f x x)))
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ cat > "$TMPFILE" << 'EPOCHS'
|
|||||||
(load "lib/apl/tests/idioms.sx")
|
(load "lib/apl/tests/idioms.sx")
|
||||||
(load "lib/apl/tests/eval-ops.sx")
|
(load "lib/apl/tests/eval-ops.sx")
|
||||||
(load "lib/apl/tests/pipeline.sx")
|
(load "lib/apl/tests/pipeline.sx")
|
||||||
|
(load "lib/apl/tests/programs-e2e.sx")
|
||||||
(epoch 4)
|
(epoch 4)
|
||||||
(eval "(list apl-test-pass apl-test-fail)")
|
(eval "(list apl-test-pass apl-test-fail)")
|
||||||
EPOCHS
|
EPOCHS
|
||||||
|
|||||||
@@ -178,3 +178,510 @@
|
|||||||
"apl-run \"(⍳5)[3] × 7\" → 21"
|
"apl-run \"(⍳5)[3] × 7\" → 21"
|
||||||
(mkrv (apl-run "(⍳5)[3] × 7"))
|
(mkrv (apl-run "(⍳5)[3] × 7"))
|
||||||
(list 21))
|
(list 21))
|
||||||
|
|
||||||
|
(apl-test "decimal: 3.7 → 3.7" (mkrv (apl-run "3.7")) (list 3.7))
|
||||||
|
|
||||||
|
(apl-test "decimal: ¯2.5 → -2.5" (mkrv (apl-run "¯2.5")) (list -2.5))
|
||||||
|
|
||||||
|
(apl-test "decimal: 1.5 + 2.5 → 4" (mkrv (apl-run "1.5 + 2.5")) (list 4))
|
||||||
|
|
||||||
|
(apl-test "decimal: ⌊3.7 → 3" (mkrv (apl-run "⌊ 3.7")) (list 3))
|
||||||
|
|
||||||
|
(apl-test "decimal: ⌈3.7 → 4" (mkrv (apl-run "⌈ 3.7")) (list 4))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"⎕← scalar passthrough"
|
||||||
|
(mkrv (apl-run "⎕← 42"))
|
||||||
|
(list 42))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"⎕← vector passthrough"
|
||||||
|
(mkrv (apl-run "⎕← 1 2 3"))
|
||||||
|
(list 1 2 3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"string: 'abc' → 3-char vector"
|
||||||
|
(mkrv (apl-run "'abc'"))
|
||||||
|
(list "a" "b" "c"))
|
||||||
|
|
||||||
|
(apl-test "string: 'a' is rank-0 scalar" (mksh (apl-run "'a'")) (list))
|
||||||
|
|
||||||
|
(apl-test "string: 'hello' shape (5)" (mksh (apl-run "'hello'")) (list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn: f ← {⍺+⍵} ⋄ 3 f 4 → 7"
|
||||||
|
(mkrv (apl-run "f ← {⍺+⍵} ⋄ 3 f 4"))
|
||||||
|
(list 7))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn monadic: sq ← {⍵×⍵} ⋄ sq 7 → 49"
|
||||||
|
(mkrv (apl-run "sq ← {⍵×⍵} ⋄ sq 7"))
|
||||||
|
(list 49))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn dyadic: hyp ← {((⍺×⍺)+⍵×⍵)} ⋄ 3 hyp 4 → 25"
|
||||||
|
(mkrv (apl-run "hyp ← {((⍺×⍺)+⍵×⍵)} ⋄ 3 hyp 4"))
|
||||||
|
(list 25))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn: dbl ← {⍵+⍵} ⋄ dbl ⍳5"
|
||||||
|
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ dbl ⍳5"))
|
||||||
|
(list 2 4 6 8 10))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn factorial via ∇ recursion"
|
||||||
|
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 5"))
|
||||||
|
(list 120))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn used twice in expr: dbl ← {⍵+⍵} ⋄ (dbl 3) + dbl 4"
|
||||||
|
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ (dbl 3) + dbl 4"))
|
||||||
|
(list 14))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"named-fn with vector arg: neg ← {-⍵} ⋄ neg 1 2 3"
|
||||||
|
(mkrv (apl-run "neg ← {-⍵} ⋄ neg 1 2 3"))
|
||||||
|
(list -1 -2 -3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[2;2] → center"
|
||||||
|
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[2;2]"))
|
||||||
|
(list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[1;] → first row"
|
||||||
|
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[1;]"))
|
||||||
|
(list 1 2 3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[;2] → second column"
|
||||||
|
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[;2]"))
|
||||||
|
(list 2 5 8))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[1 2;1 2] → 2x2 block"
|
||||||
|
(mkrv (apl-run "M ← (2 3) ⍴ ⍳6 ⋄ M[1 2;1 2]"))
|
||||||
|
(list 1 2 4 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[1 2;1 2] shape (2 2)"
|
||||||
|
(mksh (apl-run "M ← (2 3) ⍴ ⍳6 ⋄ M[1 2;1 2]"))
|
||||||
|
(list 2 2))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[;] full matrix"
|
||||||
|
(mkrv (apl-run "M ← (2 2) ⍴ 10 20 30 40 ⋄ M[;]"))
|
||||||
|
(list 10 20 30 40))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: M[1;] shape collapsed"
|
||||||
|
(mksh (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[1;]"))
|
||||||
|
(list 3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"multi-axis: select all rows of column 3"
|
||||||
|
(mkrv (apl-run "M ← (4 3) ⍴ 1 2 3 4 5 6 7 8 9 10 11 12 ⋄ M[;3]"))
|
||||||
|
(list 3 6 9 12))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train: mean = (+/÷≢) on 1..5"
|
||||||
|
(mkrv (apl-run "(+/÷≢) 1 2 3 4 5"))
|
||||||
|
(list 3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train: mean of 2 4 6 8 10"
|
||||||
|
(mkrv (apl-run "(+/÷≢) 2 4 6 8 10"))
|
||||||
|
(list 6))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train 2-atop: (- ⌊) 5 → -5"
|
||||||
|
(mkrv (apl-run "(- ⌊) 5"))
|
||||||
|
(list -5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train 3-fork dyadic: 2(+×-)5 → -21"
|
||||||
|
(mkrv (apl-run "2 (+ × -) 5"))
|
||||||
|
(list -21))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train: range = (⌈/-⌊/) on vector"
|
||||||
|
(mkrv (apl-run "(⌈/-⌊/) 3 1 4 1 5 9 2 6"))
|
||||||
|
(list 8))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"train: mean of ⍳10 has shape ()"
|
||||||
|
(mksh (apl-run "(+/÷≢) ⍳10"))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"compress: 1 0 1 0 1 / 10 20 30 40 50"
|
||||||
|
(mkrv (apl-run "1 0 1 0 1 / 10 20 30 40 50"))
|
||||||
|
(list 10 30 50))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"compress: empty mask → empty"
|
||||||
|
(mkrv (apl-run "0 0 0 / 1 2 3"))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes via classic idiom (multi-stmt)"
|
||||||
|
(mkrv (apl-run "P ← ⍳ 30 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||||
|
(list 2 3 5 7 11 13 17 19 23 29))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes via classic idiom (n=20)"
|
||||||
|
(mkrv (apl-run "P ← ⍳ 20 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||||
|
(list 2 3 5 7 11 13 17 19))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"compress: filter even values"
|
||||||
|
(mkrv (apl-run "(0 = 2 | 1 2 3 4 5 6) / 1 2 3 4 5 6"))
|
||||||
|
(list 2 4 6))
|
||||||
|
|
||||||
|
(apl-test "inline-assign: x ← 5" (mkrv (apl-run "x ← 5")) (list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"inline-assign: (2×x) + x←10 → 30"
|
||||||
|
(mkrv (apl-run "(2 × x) + x ← 10"))
|
||||||
|
(list 30))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"inline-assign primes one-liner: (2=+⌿0=a∘.|a)/a←⍳30"
|
||||||
|
(mkrv (apl-run "(2 = +⌿ 0 = a ∘.| a) / a ← ⍳ 30"))
|
||||||
|
(list 2 3 5 7 11 13 17 19 23 29))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"inline-assign: x is reusable — x + x ← 7 → 14"
|
||||||
|
(mkrv (apl-run "x + x ← 7"))
|
||||||
|
(list 14))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"inline-assign in dfn: f ← {x + x ← ⍵} ⋄ f 8 → 16"
|
||||||
|
(mkrv (apl-run "f ← {x + x ← ⍵} ⋄ f 8"))
|
||||||
|
(list 16))
|
||||||
|
|
||||||
|
(begin (apl-rng-seed! 42) nil)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"?10 with seed 42 → 8 (deterministic)"
|
||||||
|
(mkrv (apl-run "?10"))
|
||||||
|
(list 8))
|
||||||
|
|
||||||
|
(apl-test "?10 next call → 5" (mkrv (apl-run "?10")) (list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"?100 stays in range"
|
||||||
|
(let ((v (first (mkrv (apl-run "?100"))))) (and (>= v 1) (<= v 100)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(begin (apl-rng-seed! 42) nil)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"?10 with re-seed 42 → 8 (reproducible)"
|
||||||
|
(mkrv (apl-run "?10"))
|
||||||
|
(list 8))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"apl-run-file: load primes.apl returns dfn AST"
|
||||||
|
(first (apl-run-file "lib/apl/tests/programs/primes.apl"))
|
||||||
|
:dfn)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"apl-run-file: life.apl parses without error"
|
||||||
|
(first (apl-run-file "lib/apl/tests/programs/life.apl"))
|
||||||
|
:dfn)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"apl-run-file: quicksort.apl parses without error"
|
||||||
|
(first (apl-run-file "lib/apl/tests/programs/quicksort.apl"))
|
||||||
|
:dfn)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"apl-run-file: source-then-call returns primes count"
|
||||||
|
(mksh
|
||||||
|
(apl-run
|
||||||
|
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 30")))
|
||||||
|
(list 10))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes one-liner with ⍵-rebind: primes 30"
|
||||||
|
(mkrv
|
||||||
|
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 30"))
|
||||||
|
(list 2 3 5 7 11 13 17 19 23 29))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes one-liner: primes 50"
|
||||||
|
(mkrv
|
||||||
|
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 50"))
|
||||||
|
(list 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes.apl loaded + called via apl-run-file"
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 20")))
|
||||||
|
(list 2 3 5 7 11 13 17 19))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"primes.apl loaded — count of primes ≤ 100"
|
||||||
|
(first
|
||||||
|
(mksh
|
||||||
|
(apl-run
|
||||||
|
(str
|
||||||
|
(file-read "lib/apl/tests/programs/primes.apl")
|
||||||
|
" ⋄ primes 100"))))
|
||||||
|
25)
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"⍉ monadic transpose 2x3 → 3x2"
|
||||||
|
(mkrv (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||||
|
(list 1 4 2 5 3 6))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"⍉ transpose shape (3 2)"
|
||||||
|
(mksh (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||||
|
(list 3 2))
|
||||||
|
|
||||||
|
(apl-test "⊢ monadic identity" (mkrv (apl-run "⊢ 1 2 3")) (list 1 2 3))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"5 ⊣ 1 2 3 → 5 (left)"
|
||||||
|
(mkrv (apl-run "5 ⊣ 1 2 3"))
|
||||||
|
(list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"5 ⊢ 1 2 3 → 1 2 3 (right)"
|
||||||
|
(mkrv (apl-run "5 ⊢ 1 2 3"))
|
||||||
|
(list 1 2 3))
|
||||||
|
|
||||||
|
(apl-test "⍕ 42 → \"42\" (alias for ⎕FMT)" (apl-run "⍕ 42") "42")
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"⍸ where: indices of truthy cells"
|
||||||
|
(mkrv (apl-run "⍸ 0 1 0 1 1"))
|
||||||
|
(list 2 4 5))
|
||||||
|
(apl-test
|
||||||
|
"⍸ where: leading truthy"
|
||||||
|
(mkrv (apl-run "⍸ 1 0 0 1 1"))
|
||||||
|
(list 1 4 5))
|
||||||
|
(apl-test
|
||||||
|
"⍸ where: all-zero → empty"
|
||||||
|
(mkrv (apl-run "⍸ 0 0 0"))
|
||||||
|
(list))
|
||||||
|
(apl-test
|
||||||
|
"⍸ where: all-truthy"
|
||||||
|
(mkrv (apl-run "⍸ 1 1 1"))
|
||||||
|
(list 1 2 3))
|
||||||
|
(apl-test
|
||||||
|
"⍸ where: ⎕IO=1 (1-based)"
|
||||||
|
(mkrv (apl-run "⍸ (⍳5)=3"))
|
||||||
|
(list 3))
|
||||||
|
(apl-test
|
||||||
|
"⍸ interval-index: 2 4 6 ⍸ 5 → 2"
|
||||||
|
(mkrv (apl-run "2 4 6 ⍸ 5"))
|
||||||
|
(list 2))
|
||||||
|
(apl-test
|
||||||
|
"⍸ interval-index: 2 4 6 ⍸ 1 3 5 6 7 → 0 1 2 3 3"
|
||||||
|
(mkrv (apl-run "2 4 6 ⍸ 1 3 5 6 7"))
|
||||||
|
(list 0 1 2 3 3))
|
||||||
|
(apl-test
|
||||||
|
"⍸ interval-index: ⍳5 ⍸ 3 → 3"
|
||||||
|
(mkrv (apl-run "(⍳5) ⍸ 3"))
|
||||||
|
(list 3))
|
||||||
|
(apl-test
|
||||||
|
"⍸ interval-index: y below all → 0"
|
||||||
|
(mkrv (apl-run "10 20 30 ⍸ 5"))
|
||||||
|
(list 0))
|
||||||
|
(apl-test
|
||||||
|
"⍸ interval-index: y above all → len breaks"
|
||||||
|
(mkrv (apl-run "10 20 30 ⍸ 100"))
|
||||||
|
(list 3)))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"∪ unique: dedup keeps first-occurrence order"
|
||||||
|
(mkrv (apl-run "∪ 1 2 1 3 2 1 4"))
|
||||||
|
(list 1 2 3 4))
|
||||||
|
(apl-test
|
||||||
|
"∪ unique: already-unique unchanged"
|
||||||
|
(mkrv (apl-run "∪ 5 4 3 2 1"))
|
||||||
|
(list 5 4 3 2 1))
|
||||||
|
(apl-test "∪ unique: scalar" (mkrv (apl-run "∪ 7")) (list 7))
|
||||||
|
(apl-test
|
||||||
|
"∪ unique: string mississippi → misp"
|
||||||
|
(mkrv (apl-run "∪ 'mississippi'"))
|
||||||
|
(list "m" "i" "s" "p"))
|
||||||
|
(apl-test
|
||||||
|
"∪ union: 1 2 3 ∪ 3 4 5 → 1 2 3 4 5"
|
||||||
|
(mkrv (apl-run "1 2 3 ∪ 3 4 5"))
|
||||||
|
(list 1 2 3 4 5))
|
||||||
|
(apl-test
|
||||||
|
"∪ union: dedups left side too"
|
||||||
|
(mkrv (apl-run "1 2 1 ∪ 1 3 2"))
|
||||||
|
(list 1 2 3))
|
||||||
|
(apl-test
|
||||||
|
"∪ union: disjoint → catenated"
|
||||||
|
(mkrv (apl-run "1 2 ∪ 3 4"))
|
||||||
|
(list 1 2 3 4))
|
||||||
|
(apl-test
|
||||||
|
"∩ intersection: 1 2 3 4 ∩ 2 4 6 → 2 4"
|
||||||
|
(mkrv (apl-run "1 2 3 4 ∩ 2 4 6"))
|
||||||
|
(list 2 4))
|
||||||
|
(apl-test
|
||||||
|
"∩ intersection: disjoint → empty"
|
||||||
|
(mkrv (apl-run "1 2 3 ∩ 4 5 6"))
|
||||||
|
(list))
|
||||||
|
(apl-test
|
||||||
|
"∩ intersection: preserves left order"
|
||||||
|
(mkrv (apl-run "(⍳5) ∩ 5 3 1"))
|
||||||
|
(list 1 3 5))
|
||||||
|
(apl-test
|
||||||
|
"∩ intersection: identical"
|
||||||
|
(mkrv (apl-run "1 2 3 ∩ 1 2 3"))
|
||||||
|
(list 1 2 3))
|
||||||
|
(apl-test
|
||||||
|
"∪/∩ identity: A ∪ A = ∪A"
|
||||||
|
(mkrv (apl-run "1 2 1 ∪ 1 2 1"))
|
||||||
|
(list 1 2)))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: 2 2 2 ⊥ 1 0 1 → 5"
|
||||||
|
(mkrv (apl-run "2 2 2 ⊥ 1 0 1"))
|
||||||
|
(list 5))
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: 10 10 10 ⊥ 1 2 3 → 123"
|
||||||
|
(mkrv (apl-run "10 10 10 ⊥ 1 2 3"))
|
||||||
|
(list 123))
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: 24 60 60 ⊥ 2 3 4 → 7384 (mixed-radix HMS)"
|
||||||
|
(mkrv (apl-run "24 60 60 ⊥ 2 3 4"))
|
||||||
|
(list 7384))
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: scalar base 2 ⊥ 1 0 1 0 → 10"
|
||||||
|
(mkrv (apl-run "2 ⊥ 1 0 1 0"))
|
||||||
|
(list 10))
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: 16 16 ⊥ 15 15 → 255"
|
||||||
|
(mkrv (apl-run "16 16 ⊥ 15 15"))
|
||||||
|
(list 255))
|
||||||
|
(apl-test
|
||||||
|
"⊤ encode: 2 2 2 ⊤ 5 → 1 0 1"
|
||||||
|
(mkrv (apl-run "2 2 2 ⊤ 5"))
|
||||||
|
(list 1 0 1))
|
||||||
|
(apl-test
|
||||||
|
"⊤ encode: 24 60 60 ⊤ 7384 → 2 3 4 (HMS)"
|
||||||
|
(mkrv (apl-run "24 60 60 ⊤ 7384"))
|
||||||
|
(list 2 3 4))
|
||||||
|
(apl-test
|
||||||
|
"⊤ encode: 2 2 2 2 ⊤ 13 → 1 1 0 1"
|
||||||
|
(mkrv (apl-run "2 2 2 2 ⊤ 13"))
|
||||||
|
(list 1 1 0 1))
|
||||||
|
(apl-test
|
||||||
|
"⊤ encode: 10 10 ⊤ 42 → 4 2"
|
||||||
|
(mkrv (apl-run "10 10 ⊤ 42"))
|
||||||
|
(list 4 2))
|
||||||
|
(apl-test
|
||||||
|
"⊤ encode: round-trip B⊥(B⊤N) = N"
|
||||||
|
(mkrv (apl-run "24 60 60 ⊥ 24 60 60 ⊤ 7384"))
|
||||||
|
(list 7384))
|
||||||
|
(apl-test
|
||||||
|
"⊥ decode: round-trip B⊤(B⊥V) = V"
|
||||||
|
(mkrv (apl-run "2 2 2 ⊤ 2 2 2 ⊥ 1 0 1"))
|
||||||
|
(list 1 0 1)))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(define
|
||||||
|
mk-parts
|
||||||
|
(fn (s) (map (fn (p) (get p :ravel)) (get (apl-run s) :ravel))))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: 1 1 0 1 1 ⊆ 'abcde' → ('ab' 'de')"
|
||||||
|
(mk-parts "1 1 0 1 1 ⊆ 'abcde'")
|
||||||
|
(list (list "a" "b") (list "d" "e")))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: 1 0 0 1 1 ⊆ ⍳5 → ((1) (4 5))"
|
||||||
|
(mk-parts "1 0 0 1 1 ⊆ ⍳5")
|
||||||
|
(list (list 1) (list 4 5)))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: all-zero mask → empty"
|
||||||
|
(len (get (apl-run "0 0 0 ⊆ 1 2 3") :ravel))
|
||||||
|
0)
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: all-one mask → single partition"
|
||||||
|
(mk-parts "1 1 1 ⊆ 7 8 9")
|
||||||
|
(list (list 7 8 9)))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: strict increase 1 2 starts new"
|
||||||
|
(mk-parts "1 2 ⊆ 10 20")
|
||||||
|
(list (list 10) (list 20)))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: same level continues 2 2 → one partition"
|
||||||
|
(mk-parts "2 2 ⊆ 10 20")
|
||||||
|
(list (list 10 20)))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: 0 separates"
|
||||||
|
(mk-parts "1 1 0 0 1 ⊆ 1 2 3 4 5")
|
||||||
|
(list (list 1 2) (list 5)))
|
||||||
|
(apl-test
|
||||||
|
"⊆ partition: outer length matches partition count"
|
||||||
|
(len (get (apl-run "1 0 1 0 1 ⊆ ⍳5") :ravel))
|
||||||
|
3))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: ⍎ '1 + 2' → 3"
|
||||||
|
(mkrv (apl-run "⍎ '1 + 2'"))
|
||||||
|
(list 3))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: ⍎ '+/⍳10' → 55"
|
||||||
|
(mkrv (apl-run "⍎ '+/⍳10'"))
|
||||||
|
(list 55))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: ⍎ '⌈/ 1 3 9 5 7' → 9"
|
||||||
|
(mkrv (apl-run "⍎ '⌈/ 1 3 9 5 7'"))
|
||||||
|
(list 9))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: ⍎ '⍳5' → 1..5"
|
||||||
|
(mkrv (apl-run "⍎ '⍳5'"))
|
||||||
|
(list 1 2 3 4 5))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: ⍎ '×/⍳5' → 120"
|
||||||
|
(mkrv (apl-run "⍎ '×/⍳5'"))
|
||||||
|
(list 120))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: round-trip ⍎ ⎕FMT 42 → 42"
|
||||||
|
(mkrv (apl-run "⍎ ⎕FMT 42"))
|
||||||
|
(list 42))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: nested ⍎ ⍎"
|
||||||
|
(mkrv (apl-run "⍎ '⍎ ''2 × 3'''"))
|
||||||
|
(list 6))
|
||||||
|
(apl-test
|
||||||
|
"⍎ execute: with assignment side-effect"
|
||||||
|
(mkrv (apl-run "⍎ 'q ← 99 ⋄ q + 1'"))
|
||||||
|
(list 100)))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"het-inner: 1 ⍵ ∨.∧ X — result is enclosed (5 5)"
|
||||||
|
(let
|
||||||
|
((r (apl-run "B ← 5 5 ⍴ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 ⋄ X ← 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂B ⋄ 1 B ∨.∧ X")))
|
||||||
|
(list
|
||||||
|
(len (get r :shape))
|
||||||
|
(= (type-of (first (get r :ravel))) "dict")))
|
||||||
|
(list 0 true))
|
||||||
|
(apl-test
|
||||||
|
"het-inner: ⊃ unwraps to (5 5) board"
|
||||||
|
(mksh
|
||||||
|
(apl-run
|
||||||
|
"B ← 5 5 ⍴ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 ⋄ X ← 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂B ⋄ ⊃ 1 B ∨.∧ X"))
|
||||||
|
(list 5 5))
|
||||||
|
(apl-test
|
||||||
|
"het-inner: homogeneous inner product unaffected"
|
||||||
|
(mkrv (apl-run "1 2 3 +.× 4 5 6"))
|
||||||
|
(list 32))
|
||||||
|
(apl-test
|
||||||
|
"het-inner: matrix inner product unaffected"
|
||||||
|
(mkrv (apl-run "(2 2 ⍴ 1 2 3 4) +.× 2 2 ⍴ 5 6 7 8"))
|
||||||
|
(list 19 22 43 50)))
|
||||||
|
|||||||
189
lib/apl/tests/programs-e2e.sx
Normal file
189
lib/apl/tests/programs-e2e.sx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
; End-to-end tests of the classic-program archetypes — running APL
|
||||||
|
; source through the full pipeline (tokenize → parse → eval-ast → runtime).
|
||||||
|
;
|
||||||
|
; These mirror the algorithms documented in lib/apl/tests/programs/*.apl
|
||||||
|
; but use forms our pipeline supports today (named functions instead of
|
||||||
|
; the inline ⍵← rebinding idiom; multi-stmt over single one-liners).
|
||||||
|
|
||||||
|
(define mkrv (fn (arr) (get arr :ravel)))
|
||||||
|
(define mksh (fn (arr) (get arr :shape)))
|
||||||
|
|
||||||
|
; ---------- factorial via ∇ recursion (cf. n-queens style) ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: factorial 5! = 120"
|
||||||
|
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 5"))
|
||||||
|
(list 120))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: factorial 7! = 5040"
|
||||||
|
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 7"))
|
||||||
|
(list 5040))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: factorial via ×/⍳N (no recursion)"
|
||||||
|
(mkrv (apl-run "fact ← {×/⍳⍵} ⋄ fact 6"))
|
||||||
|
(list 720))
|
||||||
|
|
||||||
|
; ---------- sum / triangular numbers (sum-1..N) ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: triangular(10) = 55"
|
||||||
|
(mkrv (apl-run "tri ← {+/⍳⍵} ⋄ tri 10"))
|
||||||
|
(list 55))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: triangular(100) = 5050"
|
||||||
|
(mkrv (apl-run "tri ← {+/⍳⍵} ⋄ tri 100"))
|
||||||
|
(list 5050))
|
||||||
|
|
||||||
|
; ---------- sum of squares ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: sum-of-squares 1..5 = 55"
|
||||||
|
(mkrv (apl-run "ss ← {+/⍵×⍵} ⋄ ss ⍳5"))
|
||||||
|
(list 55))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: sum-of-squares 1..10 = 385"
|
||||||
|
(mkrv (apl-run "ss ← {+/⍵×⍵} ⋄ ss ⍳10"))
|
||||||
|
(list 385))
|
||||||
|
|
||||||
|
; ---------- divisor-counting (prime-sieve building blocks) ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: divisor counts 1..5 via outer mod"
|
||||||
|
(mkrv (apl-run "P ← ⍳ 5 ⋄ +⌿ 0 = P ∘.| P"))
|
||||||
|
(list 1 2 2 3 2))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: divisor counts 1..10"
|
||||||
|
(mkrv (apl-run "P ← ⍳ 10 ⋄ +⌿ 0 = P ∘.| P"))
|
||||||
|
(list 1 2 2 3 2 4 2 4 3 4))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: prime-mask 1..10 (count==2)"
|
||||||
|
(mkrv (apl-run "P ← ⍳ 10 ⋄ 2 = +⌿ 0 = P ∘.| P"))
|
||||||
|
(list 0 1 1 0 1 0 1 0 0 0))
|
||||||
|
|
||||||
|
; ---------- monadic primitives chained ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: sum of |abs| = 15"
|
||||||
|
(mkrv (apl-run "+/|¯1 ¯2 ¯3 ¯4 ¯5"))
|
||||||
|
(list 15))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: max of squares 1..6"
|
||||||
|
(mkrv (apl-run "⌈/(⍳6)×⍳6"))
|
||||||
|
(list 36))
|
||||||
|
|
||||||
|
; ---------- nested named functions ----------
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: compose dbl and sq via two named fns"
|
||||||
|
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ sq ← {⍵×⍵} ⋄ sq dbl 3"))
|
||||||
|
(list 36))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: max-of-two as named dyadic fn"
|
||||||
|
(mkrv (apl-run "mx ← {⍺⌈⍵} ⋄ 5 mx 3"))
|
||||||
|
(list 5))
|
||||||
|
|
||||||
|
(apl-test
|
||||||
|
"e2e: sqrt-via-newton 1 step from 1 → 2.5"
|
||||||
|
(mkrv (apl-run "step ← {(⍵+⍺÷⍵)÷2} ⋄ 4 step 1"))
|
||||||
|
(list 2.5))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"life.apl: blinker 5×5 → vertical blinker"
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵} ⋄ life 5 5 ⍴ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0"))
|
||||||
|
(list 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0))
|
||||||
|
(apl-test
|
||||||
|
"life.apl: blinker oscillates (period 2)"
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵} ⋄ life life 5 5 ⍴ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0"))
|
||||||
|
(list 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0))
|
||||||
|
(apl-test
|
||||||
|
"life.apl: 2×2 block stable"
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵} ⋄ life 4 4 ⍴ 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 0"))
|
||||||
|
(list 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 0))
|
||||||
|
(apl-test
|
||||||
|
"life.apl: empty grid stays empty"
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵} ⋄ life 5 5 ⍴ 0"))
|
||||||
|
(list 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))
|
||||||
|
(apl-test
|
||||||
|
"life.apl: source-file as-written runs"
|
||||||
|
(let
|
||||||
|
((dfn (apl-run-file "lib/apl/tests/programs/life.apl"))
|
||||||
|
(board
|
||||||
|
(apl-run "5 5 ⍴ 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0")))
|
||||||
|
(get (apl-call-dfn-m dfn board) :ravel))
|
||||||
|
(list 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0)))
|
||||||
|
|
||||||
|
(begin
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: 11-element with duplicates"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort 3 1 4 1 5 9 2 6 5 3 5")))
|
||||||
|
(list 1 1 2 3 3 4 5 5 5 6 9))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: already sorted"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort 1 2 3 4 5")))
|
||||||
|
(list 1 2 3 4 5))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: reverse sorted"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort 5 4 3 2 1")))
|
||||||
|
(list 1 2 3 4 5))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: all equal"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort 7 7 7 7")))
|
||||||
|
(list 7 7 7 7))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: single element"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort ,42")))
|
||||||
|
(list 42))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: matches grade-up"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(mkrv
|
||||||
|
(apl-run
|
||||||
|
"V ← 8 3 1 9 2 7 5 6 4 ⋄ quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p} ⋄ quicksort V")))
|
||||||
|
(list 1 2 3 4 5 6 7 8 9))
|
||||||
|
(apl-test
|
||||||
|
"quicksort.apl: source-file as-written runs"
|
||||||
|
(begin
|
||||||
|
(apl-rng-seed! 42)
|
||||||
|
(let
|
||||||
|
((dfn (apl-run-file "lib/apl/tests/programs/quicksort.apl"))
|
||||||
|
(vec (apl-run "5 2 8 1 9 3 7 4 6")))
|
||||||
|
(get (apl-call-dfn-m dfn vec) :ravel)))
|
||||||
|
(list 1 2 3 4 5 6 7 8 9)))
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
⍝ ¯1 0 1 ⌽¨ ⊂⍵ : produce 3 horizontally-shifted copies
|
⍝ ¯1 0 1 ⌽¨ ⊂⍵ : produce 3 horizontally-shifted copies
|
||||||
⍝ ¯1 0 1 ∘.⊖ … : outer-product with vertical shifts → 3×3 = 9 shifts
|
⍝ ¯1 0 1 ∘.⊖ … : outer-product with vertical shifts → 3×3 = 9 shifts
|
||||||
⍝ +/ +/ … : sum the 9 boards element-wise → neighbor-count + self
|
⍝ +/ +/ … : sum the 9 boards element-wise → neighbor-count + self
|
||||||
⍝ 3 4 = … : boolean — count is exactly 3 or exactly 4
|
⍝ 3 4 = … : leading-axis-extended boolean — count is 3 (born) or 4 (survive)
|
||||||
⍝ 1 ⍵ ∨.∧ … : "alive next" iff (count=3) or (alive AND count=4)
|
⍝ 1 ⍵ ∨.∧ … : "alive next" iff (count=3) or (alive AND count=4)
|
||||||
⍝ ⊃ … : disclose back to a 2D board
|
⍝ ⊃ … : disclose the enclosed result back to a 2D board
|
||||||
⍝
|
⍝
|
||||||
⍝ Rules in plain language:
|
⍝ Rules in plain language:
|
||||||
⍝ - dead cell + 3 live neighbors → born
|
⍝ - dead cell + 3 live neighbors → born
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
(list "+" "-" "×" "÷" "*" "⍟" "⌈" "⌊" "|" "!" "?" "○" "~" "<" "≤" "=" "≥" ">" "≠"
|
(list "+" "-" "×" "÷" "*" "⍟" "⌈" "⌊" "|" "!" "?" "○" "~" "<" "≤" "=" "≥" ">" "≠"
|
||||||
"≢" "≡" "∊" "∧" "∨" "⍱" "⍲" "," "⍪" "⍴" "⌽" "⊖" "⍉" "↑" "↓" "⊂" "⊃" "⊆"
|
"≢" "≡" "∊" "∧" "∨" "⍱" "⍲" "," "⍪" "⍴" "⌽" "⊖" "⍉" "↑" "↓" "⊂" "⊃" "⊆"
|
||||||
"∪" "∩" "⍳" "⍸" "⌷" "⍋" "⍒" "⊥" "⊤" "⊣" "⊢" "⍎" "⍕"
|
"∪" "∩" "⍳" "⍸" "⌷" "⍋" "⍒" "⊥" "⊤" "⊣" "⊢" "⍎" "⍕"
|
||||||
"⍺" "⍵" "∇" "/" "\\" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@" "¯"))
|
"⍺" "⍵" "∇" "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@" "¯"))
|
||||||
|
|
||||||
(define apl-glyph?
|
(define apl-glyph?
|
||||||
(fn (ch)
|
(fn (ch)
|
||||||
@@ -19,91 +19,87 @@
|
|||||||
(and (>= ch "A") (<= ch "Z"))
|
(and (>= ch "A") (<= ch "Z"))
|
||||||
(= ch "_")))))
|
(= ch "_")))))
|
||||||
|
|
||||||
(define apl-tokenize
|
(define
|
||||||
(fn (source)
|
apl-tokenize
|
||||||
(let ((pos 0)
|
(fn
|
||||||
(src-len (len source))
|
(source)
|
||||||
(tokens (list)))
|
(let
|
||||||
|
((pos 0) (src-len (len source)) (tokens (list)))
|
||||||
(define tok-push!
|
(define tok-push! (fn (type value) (append! tokens {:value value :type type})))
|
||||||
(fn (type value)
|
(define
|
||||||
(append! tokens {:type type :value value})))
|
cur-sw?
|
||||||
|
(fn
|
||||||
(define cur-sw?
|
(ch)
|
||||||
(fn (ch)
|
|
||||||
(and (< pos src-len) (starts-with? (slice source pos) ch))))
|
(and (< pos src-len) (starts-with? (slice source pos) ch))))
|
||||||
|
(define cur-byte (fn () (if (< pos src-len) (nth source pos) nil)))
|
||||||
(define cur-byte
|
(define advance! (fn () (set! pos (+ pos 1))))
|
||||||
(fn ()
|
(define consume! (fn (ch) (set! pos (+ pos (len ch)))))
|
||||||
(if (< pos src-len) (nth source pos) nil)))
|
(define
|
||||||
|
find-glyph
|
||||||
(define advance!
|
(fn
|
||||||
(fn ()
|
()
|
||||||
(set! pos (+ pos 1))))
|
(let
|
||||||
|
((rem (slice source pos)))
|
||||||
(define consume!
|
(let
|
||||||
(fn (ch)
|
((matches (filter (fn (g) (starts-with? rem g)) apl-glyph-set)))
|
||||||
(set! pos (+ pos (len ch)))))
|
|
||||||
|
|
||||||
(define find-glyph
|
|
||||||
(fn ()
|
|
||||||
(let ((rem (slice source pos)))
|
|
||||||
(let ((matches (filter (fn (g) (starts-with? rem g)) apl-glyph-set)))
|
|
||||||
(if (> (len matches) 0) (first matches) nil)))))
|
(if (> (len matches) 0) (first matches) nil)))))
|
||||||
|
(define
|
||||||
(define read-digits!
|
read-digits!
|
||||||
(fn (acc)
|
(fn
|
||||||
(if (and (< pos src-len) (apl-digit? (cur-byte)))
|
(acc)
|
||||||
(let ((ch (cur-byte)))
|
(if
|
||||||
(begin
|
(and (< pos src-len) (apl-digit? (cur-byte)))
|
||||||
(advance!)
|
(let
|
||||||
(read-digits! (str acc ch))))
|
((ch (cur-byte)))
|
||||||
|
(begin (advance!) (read-digits! (str acc ch))))
|
||||||
acc)))
|
acc)))
|
||||||
|
(define
|
||||||
(define read-ident-cont!
|
read-ident-cont!
|
||||||
(fn ()
|
(fn
|
||||||
(when (and (< pos src-len)
|
()
|
||||||
(let ((ch (cur-byte)))
|
(when
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur-byte)))
|
||||||
(or (apl-alpha? ch) (apl-digit? ch))))
|
(or (apl-alpha? ch) (apl-digit? ch))))
|
||||||
(begin
|
(begin (advance!) (read-ident-cont!)))))
|
||||||
(advance!)
|
(define
|
||||||
(read-ident-cont!)))))
|
read-string!
|
||||||
|
(fn
|
||||||
(define read-string!
|
(acc)
|
||||||
(fn (acc)
|
|
||||||
(cond
|
(cond
|
||||||
((>= pos src-len) acc)
|
((>= pos src-len) acc)
|
||||||
((cur-sw? "'")
|
((cur-sw? "'")
|
||||||
(if (and (< (+ pos 1) src-len) (cur-sw? "'"))
|
(if
|
||||||
(begin
|
(and (< (+ pos 1) src-len) (cur-sw? "'"))
|
||||||
(advance!)
|
(begin (advance!) (advance!) (read-string! (str acc "'")))
|
||||||
(advance!)
|
|
||||||
(read-string! (str acc "'")))
|
|
||||||
(begin (advance!) acc)))
|
(begin (advance!) acc)))
|
||||||
(true
|
(true
|
||||||
(let ((ch (cur-byte)))
|
(let
|
||||||
(begin
|
((ch (cur-byte)))
|
||||||
(advance!)
|
(begin (advance!) (read-string! (str acc ch))))))))
|
||||||
(read-string! (str acc ch))))))))
|
(define
|
||||||
|
skip-line!
|
||||||
(define skip-line!
|
(fn
|
||||||
(fn ()
|
()
|
||||||
(when (and (< pos src-len) (not (cur-sw? "\n")))
|
(when
|
||||||
(begin
|
(and (< pos src-len) (not (cur-sw? "\n")))
|
||||||
(advance!)
|
(begin (advance!) (skip-line!)))))
|
||||||
(skip-line!)))))
|
(define
|
||||||
|
scan!
|
||||||
(define scan!
|
(fn
|
||||||
(fn ()
|
()
|
||||||
(when (< pos src-len)
|
(when
|
||||||
(let ((ch (cur-byte)))
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur-byte)))
|
||||||
(cond
|
(cond
|
||||||
((or (= ch " ") (= ch "\t") (= ch "\r"))
|
((or (= ch " ") (= ch "\t") (= ch "\r"))
|
||||||
(begin (advance!) (scan!)))
|
(begin (advance!) (scan!)))
|
||||||
((= ch "\n")
|
((= ch "\n")
|
||||||
(begin (advance!) (tok-push! :newline nil) (scan!)))
|
(begin (advance!) (tok-push! :newline nil) (scan!)))
|
||||||
((cur-sw? "⍝")
|
((cur-sw? "⍝") (begin (skip-line!) (scan!)))
|
||||||
(begin (skip-line!) (scan!)))
|
|
||||||
((cur-sw? "⋄")
|
((cur-sw? "⋄")
|
||||||
(begin (consume! "⋄") (tok-push! :diamond nil) (scan!)))
|
(begin (consume! "⋄") (tok-push! :diamond nil) (scan!)))
|
||||||
((= ch "(")
|
((= ch "(")
|
||||||
@@ -123,46 +119,80 @@
|
|||||||
((cur-sw? "←")
|
((cur-sw? "←")
|
||||||
(begin (consume! "←") (tok-push! :assign nil) (scan!)))
|
(begin (consume! "←") (tok-push! :assign nil) (scan!)))
|
||||||
((= ch ":")
|
((= ch ":")
|
||||||
(let ((start pos))
|
(let
|
||||||
|
((start pos))
|
||||||
(begin
|
(begin
|
||||||
(advance!)
|
(advance!)
|
||||||
(if (and (< pos src-len) (apl-alpha? (cur-byte)))
|
(if
|
||||||
|
(and (< pos src-len) (apl-alpha? (cur-byte)))
|
||||||
(begin
|
(begin
|
||||||
(read-ident-cont!)
|
(read-ident-cont!)
|
||||||
(tok-push! :keyword (slice source start pos)))
|
(tok-push! :keyword (slice source start pos)))
|
||||||
(tok-push! :colon nil))
|
(tok-push! :colon nil))
|
||||||
(scan!))))
|
(scan!))))
|
||||||
((and (cur-sw? "¯")
|
((and (cur-sw? "¯") (< (+ pos (len "¯")) src-len) (apl-digit? (nth source (+ pos (len "¯")))))
|
||||||
(< (+ pos (len "¯")) src-len)
|
|
||||||
(apl-digit? (nth source (+ pos (len "¯")))))
|
|
||||||
(begin
|
(begin
|
||||||
(consume! "¯")
|
(consume! "¯")
|
||||||
(let ((digits (read-digits! "")))
|
(let
|
||||||
(tok-push! :num (- 0 (parse-int digits 0))))
|
((digits (read-digits! "")))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(= (cur-byte) ".")
|
||||||
|
(< (+ pos 1) src-len)
|
||||||
|
(apl-digit? (nth source (+ pos 1))))
|
||||||
|
(begin
|
||||||
|
(advance!)
|
||||||
|
(let
|
||||||
|
((frac (read-digits! "")))
|
||||||
|
(tok-push!
|
||||||
|
:num (- 0 (string->number (str digits "." frac))))))
|
||||||
|
(tok-push! :num (- 0 (parse-int digits 0)))))
|
||||||
(scan!)))
|
(scan!)))
|
||||||
((apl-digit? ch)
|
((apl-digit? ch)
|
||||||
(begin
|
(begin
|
||||||
(let ((digits (read-digits! "")))
|
(let
|
||||||
(tok-push! :num (parse-int digits 0)))
|
((digits (read-digits! "")))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(= (cur-byte) ".")
|
||||||
|
(< (+ pos 1) src-len)
|
||||||
|
(apl-digit? (nth source (+ pos 1))))
|
||||||
|
(begin
|
||||||
|
(advance!)
|
||||||
|
(let
|
||||||
|
((frac (read-digits! "")))
|
||||||
|
(tok-push!
|
||||||
|
:num (string->number (str digits "." frac)))))
|
||||||
|
(tok-push! :num (parse-int digits 0))))
|
||||||
(scan!)))
|
(scan!)))
|
||||||
((= ch "'")
|
((= ch "'")
|
||||||
(begin
|
(begin
|
||||||
(advance!)
|
(advance!)
|
||||||
(let ((s (read-string! "")))
|
(let ((s (read-string! ""))) (tok-push! :str s))
|
||||||
(tok-push! :str s))
|
|
||||||
(scan!)))
|
(scan!)))
|
||||||
((or (apl-alpha? ch) (cur-sw? "⎕"))
|
((or (apl-alpha? ch) (cur-sw? "⎕"))
|
||||||
(let ((start pos))
|
(let
|
||||||
|
((start pos))
|
||||||
(begin
|
(begin
|
||||||
(if (cur-sw? "⎕") (consume! "⎕") (advance!))
|
(if
|
||||||
(read-ident-cont!)
|
(cur-sw? "⎕")
|
||||||
|
(begin
|
||||||
|
(consume! "⎕")
|
||||||
|
(if
|
||||||
|
(and (< pos src-len) (cur-sw? "←"))
|
||||||
|
(consume! "←")
|
||||||
|
(read-ident-cont!)))
|
||||||
|
(begin (advance!) (read-ident-cont!)))
|
||||||
(tok-push! :name (slice source start pos))
|
(tok-push! :name (slice source start pos))
|
||||||
(scan!))))
|
(scan!))))
|
||||||
(true
|
(true
|
||||||
(let ((g (find-glyph)))
|
(let
|
||||||
(if g
|
((g (find-glyph)))
|
||||||
|
(if
|
||||||
|
g
|
||||||
(begin (consume! g) (tok-push! :glyph g) (scan!))
|
(begin (consume! g) (tok-push! :glyph g) (scan!))
|
||||||
(begin (advance!) (scan!))))))))))
|
(begin (advance!) (scan!))))))))))
|
||||||
|
|
||||||
(scan!)
|
(scan!)
|
||||||
tokens)))
|
tokens)))
|
||||||
|
|||||||
@@ -39,7 +39,16 @@
|
|||||||
((= g "⊖") apl-reverse-first)
|
((= g "⊖") apl-reverse-first)
|
||||||
((= g "⍋") apl-grade-up)
|
((= g "⍋") apl-grade-up)
|
||||||
((= g "⍒") apl-grade-down)
|
((= g "⍒") apl-grade-down)
|
||||||
|
((= g "?") apl-roll)
|
||||||
|
((= g "⍉") apl-transpose)
|
||||||
|
((= g "⊢") (fn (a) a))
|
||||||
|
((= g "⊣") (fn (a) a))
|
||||||
|
((= g "⍕") apl-quad-fmt)
|
||||||
((= g "⎕FMT") apl-quad-fmt)
|
((= g "⎕FMT") apl-quad-fmt)
|
||||||
|
((= g "⎕←") apl-quad-print)
|
||||||
|
((= g "⍸") apl-where)
|
||||||
|
((= g "∪") apl-unique)
|
||||||
|
((= g "⍎") apl-execute)
|
||||||
(else (error "no monadic fn for glyph")))))
|
(else (error "no monadic fn for glyph")))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -79,6 +88,17 @@
|
|||||||
((= g "∊") apl-member)
|
((= g "∊") apl-member)
|
||||||
((= g "⍳") apl-index-of)
|
((= g "⍳") apl-index-of)
|
||||||
((= g "~") apl-without)
|
((= g "~") apl-without)
|
||||||
|
((= g "/") apl-compress)
|
||||||
|
((= g "⌿") apl-compress-first)
|
||||||
|
((= g "⍉") apl-transpose-dyadic)
|
||||||
|
((= g "⊢") (fn (a b) b))
|
||||||
|
((= g "⊣") (fn (a b) a))
|
||||||
|
((= g "⍸") apl-interval-index)
|
||||||
|
((= g "∪") apl-union)
|
||||||
|
((= g "∩") apl-intersect)
|
||||||
|
((= g "⊥") apl-decode)
|
||||||
|
((= g "⊤") apl-encode)
|
||||||
|
((= g "⊆") apl-partition)
|
||||||
(else (error "no dyadic fn for glyph")))))
|
(else (error "no dyadic fn for glyph")))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -97,6 +117,15 @@
|
|||||||
((tag (first node)))
|
((tag (first node)))
|
||||||
(cond
|
(cond
|
||||||
((= tag :num) (apl-scalar (nth node 1)))
|
((= tag :num) (apl-scalar (nth node 1)))
|
||||||
|
((= tag :str)
|
||||||
|
(let
|
||||||
|
((s (nth node 1)))
|
||||||
|
(if
|
||||||
|
(= (len s) 1)
|
||||||
|
(apl-scalar s)
|
||||||
|
(make-array
|
||||||
|
(list (len s))
|
||||||
|
(map (fn (i) (slice s i (+ i 1))) (range 0 (len s)))))))
|
||||||
((= tag :vec)
|
((= tag :vec)
|
||||||
(let
|
(let
|
||||||
((items (rest node)))
|
((items (rest node)))
|
||||||
@@ -104,13 +133,26 @@
|
|||||||
((vals (map (fn (n) (apl-eval-ast n env)) items)))
|
((vals (map (fn (n) (apl-eval-ast n env)) items)))
|
||||||
(make-array
|
(make-array
|
||||||
(list (len vals))
|
(list (len vals))
|
||||||
(map (fn (v) (first (get v :ravel))) vals)))))
|
(map
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(if
|
||||||
|
(= (len (get v :shape)) 0)
|
||||||
|
(first (get v :ravel))
|
||||||
|
v))
|
||||||
|
vals)))))
|
||||||
((= tag :name)
|
((= tag :name)
|
||||||
(let
|
(let
|
||||||
((nm (nth node 1)))
|
((nm (nth node 1)))
|
||||||
(cond
|
(cond
|
||||||
((= nm "⍺") (get env "alpha"))
|
((= nm "⍺")
|
||||||
((= nm "⍵") (get env "omega"))
|
(let
|
||||||
|
((v (get env "⍺")))
|
||||||
|
(if (= v nil) (get env "alpha") v)))
|
||||||
|
((= nm "⍵")
|
||||||
|
(let
|
||||||
|
((v (get env "⍵")))
|
||||||
|
(if (= v nil) (get env "omega") v)))
|
||||||
((= nm "⎕IO") (apl-quad-io))
|
((= nm "⎕IO") (apl-quad-io))
|
||||||
((= nm "⎕ML") (apl-quad-ml))
|
((= nm "⎕ML") (apl-quad-ml))
|
||||||
((= nm "⎕FR") (apl-quad-fr))
|
((= nm "⎕FR") (apl-quad-fr))
|
||||||
@@ -122,7 +164,11 @@
|
|||||||
(if
|
(if
|
||||||
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
||||||
(apl-call-dfn-m (get env "nabla") (apl-eval-ast arg env))
|
(apl-call-dfn-m (get env "nabla") (apl-eval-ast arg env))
|
||||||
((apl-resolve-monadic fn-node env) (apl-eval-ast arg env)))))
|
(let
|
||||||
|
((arg-val (apl-eval-ast arg env)))
|
||||||
|
(let
|
||||||
|
((new-env (if (and (list? arg) (> (len arg) 0) (= (first arg) :assign-expr)) (assoc env (nth arg 1) arg-val) env)))
|
||||||
|
((apl-resolve-monadic fn-node new-env) arg-val))))))
|
||||||
((= tag :dyad)
|
((= tag :dyad)
|
||||||
(let
|
(let
|
||||||
((fn-node (nth node 1))
|
((fn-node (nth node 1))
|
||||||
@@ -134,11 +180,27 @@
|
|||||||
(get env "nabla")
|
(get env "nabla")
|
||||||
(apl-eval-ast lhs env)
|
(apl-eval-ast lhs env)
|
||||||
(apl-eval-ast rhs env))
|
(apl-eval-ast rhs env))
|
||||||
((apl-resolve-dyadic fn-node env)
|
(let
|
||||||
(apl-eval-ast lhs env)
|
((rhs-val (apl-eval-ast rhs env)))
|
||||||
(apl-eval-ast rhs env)))))
|
(let
|
||||||
|
((new-env (if (and (list? rhs) (> (len rhs) 0) (= (first rhs) :assign-expr)) (assoc env (nth rhs 1) rhs-val) env)))
|
||||||
|
((apl-resolve-dyadic fn-node new-env)
|
||||||
|
(apl-eval-ast lhs new-env)
|
||||||
|
rhs-val))))))
|
||||||
((= tag :program) (apl-eval-stmts (rest node) env))
|
((= tag :program) (apl-eval-stmts (rest node) env))
|
||||||
((= tag :dfn) node)
|
((= tag :dfn) node)
|
||||||
|
((= tag :bracket)
|
||||||
|
(let
|
||||||
|
((arr-expr (nth node 1)) (axis-exprs (rest (rest node))))
|
||||||
|
(let
|
||||||
|
((arr (apl-eval-ast arr-expr env))
|
||||||
|
(axes
|
||||||
|
(map
|
||||||
|
(fn (a) (if (= a :all) nil (apl-eval-ast a env)))
|
||||||
|
axis-exprs)))
|
||||||
|
(apl-bracket-multi axes arr))))
|
||||||
|
((= tag :assign-expr) (apl-eval-ast (nth node 2) env))
|
||||||
|
((= tag :assign) (apl-eval-ast (nth node 2) env))
|
||||||
(else (error (list "apl-eval-ast: unknown node tag" tag node)))))))
|
(else (error (list "apl-eval-ast: unknown node tag" tag node)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -419,6 +481,36 @@
|
|||||||
((f (apl-resolve-dyadic inner env)))
|
((f (apl-resolve-dyadic inner env)))
|
||||||
(fn (arr) (apl-commute f arr))))
|
(fn (arr) (apl-commute f arr))))
|
||||||
(else (error "apl-resolve-monadic: unsupported op")))))
|
(else (error "apl-resolve-monadic: unsupported op")))))
|
||||||
|
((= tag :fn-name)
|
||||||
|
(let
|
||||||
|
((nm (nth fn-node 1)))
|
||||||
|
(let
|
||||||
|
((bound (get env nm)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? bound)
|
||||||
|
(> (len bound) 0)
|
||||||
|
(= (first bound) :dfn))
|
||||||
|
(fn (arg) (apl-call-dfn-m bound arg))
|
||||||
|
(error "apl-resolve-monadic: name not bound to dfn")))))
|
||||||
|
((= tag :train)
|
||||||
|
(let
|
||||||
|
((fns (rest fn-node)))
|
||||||
|
(let
|
||||||
|
((n (len fns)))
|
||||||
|
(cond
|
||||||
|
((= n 2)
|
||||||
|
(let
|
||||||
|
((g (apl-resolve-monadic (nth fns 0) env))
|
||||||
|
(h (apl-resolve-monadic (nth fns 1) env)))
|
||||||
|
(fn (arg) (g (h arg)))))
|
||||||
|
((= n 3)
|
||||||
|
(let
|
||||||
|
((f (apl-resolve-monadic (nth fns 0) env))
|
||||||
|
(g (apl-resolve-dyadic (nth fns 1) env))
|
||||||
|
(h (apl-resolve-monadic (nth fns 2) env)))
|
||||||
|
(fn (arg) (g (f arg) (h arg)))))
|
||||||
|
(else (error "monadic train arity not 2 or 3"))))))
|
||||||
(else (error "apl-resolve-monadic: unknown fn-node tag"))))))
|
(else (error "apl-resolve-monadic: unknown fn-node tag"))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -442,6 +534,18 @@
|
|||||||
((f (apl-resolve-dyadic inner env)))
|
((f (apl-resolve-dyadic inner env)))
|
||||||
(fn (a b) (apl-commute-dyadic f a b))))
|
(fn (a b) (apl-commute-dyadic f a b))))
|
||||||
(else (error "apl-resolve-dyadic: unsupported op")))))
|
(else (error "apl-resolve-dyadic: unsupported op")))))
|
||||||
|
((= tag :fn-name)
|
||||||
|
(let
|
||||||
|
((nm (nth fn-node 1)))
|
||||||
|
(let
|
||||||
|
((bound (get env nm)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? bound)
|
||||||
|
(> (len bound) 0)
|
||||||
|
(= (first bound) :dfn))
|
||||||
|
(fn (a b) (apl-call-dfn bound a b))
|
||||||
|
(error "apl-resolve-dyadic: name not bound to dfn")))))
|
||||||
((= tag :outer)
|
((= tag :outer)
|
||||||
(let
|
(let
|
||||||
((inner (nth fn-node 2)))
|
((inner (nth fn-node 2)))
|
||||||
@@ -455,6 +559,34 @@
|
|||||||
((f (apl-resolve-dyadic f-node env))
|
((f (apl-resolve-dyadic f-node env))
|
||||||
(g (apl-resolve-dyadic g-node env)))
|
(g (apl-resolve-dyadic g-node env)))
|
||||||
(fn (a b) (apl-inner f g a b)))))
|
(fn (a b) (apl-inner f g a b)))))
|
||||||
|
((= tag :train)
|
||||||
|
(let
|
||||||
|
((fns (rest fn-node)))
|
||||||
|
(let
|
||||||
|
((n (len fns)))
|
||||||
|
(cond
|
||||||
|
((= n 2)
|
||||||
|
(let
|
||||||
|
((g (apl-resolve-monadic (nth fns 0) env))
|
||||||
|
(h (apl-resolve-dyadic (nth fns 1) env)))
|
||||||
|
(fn (a b) (g (h a b)))))
|
||||||
|
((= n 3)
|
||||||
|
(let
|
||||||
|
((f (apl-resolve-dyadic (nth fns 0) env))
|
||||||
|
(g (apl-resolve-dyadic (nth fns 1) env))
|
||||||
|
(h (apl-resolve-dyadic (nth fns 2) env)))
|
||||||
|
(fn (a b) (g (f a b) (h a b)))))
|
||||||
|
(else (error "dyadic train arity not 2 or 3"))))))
|
||||||
(else (error "apl-resolve-dyadic: unknown fn-node tag"))))))
|
(else (error "apl-resolve-dyadic: unknown fn-node tag"))))))
|
||||||
|
|
||||||
(define apl-run (fn (src) (apl-eval-ast (parse-apl src) {})))
|
(define apl-run (fn (src) (apl-eval-ast (parse-apl src) {})))
|
||||||
|
|
||||||
|
(define apl-run-file (fn (path) (apl-run (file-read path))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
apl-execute
|
||||||
|
(fn
|
||||||
|
(arr)
|
||||||
|
(let
|
||||||
|
((src (cond ((string? arr) arr) ((scalar? arr) (disclose arr)) (else (reduce str "" (get arr :ravel))))))
|
||||||
|
(apl-run src))))
|
||||||
|
|||||||
@@ -330,37 +330,22 @@
|
|||||||
false))))))
|
false))))))
|
||||||
(check-all 0)))))
|
(check-all 0)))))
|
||||||
|
|
||||||
;; Precedence distance: how far class-name is from spec-name up the hierarchy.
|
;; CLOS-side adapter for lib/guest/reflective/class-chain.sx. Classes
|
||||||
(define
|
;; live in clos-class-registry; :parents is a list of parent class
|
||||||
clos-specificity
|
;; names (CLOS supports multiple inheritance).
|
||||||
(let
|
(define clos-class-cfg
|
||||||
((registry clos-class-registry))
|
{:parents-of (fn (cn)
|
||||||
(fn
|
(let ((rec (clos-find-class cn)))
|
||||||
(class-name spec-name)
|
(cond ((nil? rec) (list))
|
||||||
(define
|
(:else (or (get rec "parents") (list))))))
|
||||||
walk
|
:class? (fn (n) (not (nil? (clos-find-class n))))})
|
||||||
(fn
|
|
||||||
(cn depth)
|
;; Precedence distance: how far class-name is from spec-name up the
|
||||||
(if
|
;; hierarchy. Delegates to refl-class-chain-depth-with which handles
|
||||||
(= cn spec-name)
|
;; the multi-parent DFS with min-depth selection.
|
||||||
depth
|
(define clos-specificity
|
||||||
(let
|
(fn (class-name spec-name)
|
||||||
((rec (get registry cn)))
|
(refl-class-chain-depth-with clos-class-cfg class-name spec-name)))
|
||||||
(if
|
|
||||||
(nil? rec)
|
|
||||||
nil
|
|
||||||
(let
|
|
||||||
((results (map (fn (p) (walk p (+ depth 1))) (get rec "parents"))))
|
|
||||||
(let
|
|
||||||
((non-nil (filter (fn (x) (not (nil? x))) results)))
|
|
||||||
(if
|
|
||||||
(empty? non-nil)
|
|
||||||
nil
|
|
||||||
(reduce
|
|
||||||
(fn (a b) (if (< a b) a b))
|
|
||||||
(first non-nil)
|
|
||||||
(rest non-nil))))))))))
|
|
||||||
(walk class-name 0))))
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
clos-method-more-specific?
|
clos-method-more-specific?
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ run_program_suite \
|
|||||||
|
|
||||||
# ── Phase 4: CLOS unit tests ─────────────────────────────────────────────────
|
# ── Phase 4: CLOS unit tests ─────────────────────────────────────────────────
|
||||||
CLOS_FILE=$(mktemp); trap "rm -f $CLOS_FILE" EXIT
|
CLOS_FILE=$(mktemp); trap "rm -f $CLOS_FILE" EXIT
|
||||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "lib/common-lisp/tests/clos.sx")\n(epoch 5)\n(eval "passed")\n(epoch 6)\n(eval "failed")\n(epoch 7)\n(eval "failures")\n' > "$CLOS_FILE"
|
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/guest/reflective/class-chain.sx")\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "lib/common-lisp/tests/clos.sx")\n(epoch 5)\n(eval "passed")\n(epoch 6)\n(eval "failed")\n(epoch 7)\n(eval "failures")\n' > "$CLOS_FILE"
|
||||||
CLOS_OUT=$(timeout 30 "$SX_SERVER" < "$CLOS_FILE" 2>/dev/null)
|
CLOS_OUT=$(timeout 30 "$SX_SERVER" < "$CLOS_FILE" 2>/dev/null)
|
||||||
rm -f "$CLOS_FILE"
|
rm -f "$CLOS_FILE"
|
||||||
CLOS_PASSED=$(echo "$CLOS_OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
CLOS_PASSED=$(echo "$CLOS_OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
||||||
@@ -389,7 +389,7 @@ fi
|
|||||||
run_clos_suite() {
|
run_clos_suite() {
|
||||||
local prog="$1" pass_var="$2" fail_var="$3" failures_var="$4"
|
local prog="$1" pass_var="$2" fail_var="$3" failures_var="$4"
|
||||||
local PROG_FILE=$(mktemp)
|
local PROG_FILE=$(mktemp)
|
||||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "%s")\n(epoch 5)\n(eval "%s")\n(epoch 6)\n(eval "%s")\n(epoch 7)\n(eval "%s")\n' \
|
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/guest/reflective/class-chain.sx")\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "%s")\n(epoch 5)\n(eval "%s")\n(epoch 6)\n(eval "%s")\n(epoch 7)\n(eval "%s")\n' \
|
||||||
"$prog" "$pass_var" "$fail_var" "$failures_var" > "$PROG_FILE"
|
"$prog" "$pass_var" "$fail_var" "$failures_var" > "$PROG_FILE"
|
||||||
local OUT; OUT=$(timeout 20 "$SX_SERVER" < "$PROG_FILE" 2>/dev/null)
|
local OUT; OUT=$(timeout 20 "$SX_SERVER" < "$PROG_FILE" 2>/dev/null)
|
||||||
rm -f "$PROG_FILE"
|
rm -f "$PROG_FILE"
|
||||||
|
|||||||
157
lib/datalog/aggregates.sx
Normal file
157
lib/datalog/aggregates.sx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
;; lib/datalog/aggregates.sx — count / sum / min / max / findall.
|
||||||
|
;;
|
||||||
|
;; Surface form (always 3-arg after the relation name):
|
||||||
|
;;
|
||||||
|
;; (count Result Var GoalLit)
|
||||||
|
;; (sum Result Var GoalLit)
|
||||||
|
;; (min Result Var GoalLit)
|
||||||
|
;; (max Result Var GoalLit)
|
||||||
|
;; (findall List Var GoalLit)
|
||||||
|
;;
|
||||||
|
;; Parsed naturally because arg-position compounds are already allowed
|
||||||
|
;; (Phase 4 needs them for arithmetic). At evaluation time the aggregator
|
||||||
|
;; runs `dl-find-bindings` on `GoalLit` under the current subst, collects
|
||||||
|
;; the distinct values of `Var`, and binds `Result`.
|
||||||
|
;;
|
||||||
|
;; Aggregation is non-monotonic — `count(C, X, p(X))` shrinks as p loses
|
||||||
|
;; tuples. The stratifier (lib/datalog/strata.sx) treats every aggregate's
|
||||||
|
;; goal relation as a negation-like edge so the inner relation is fully
|
||||||
|
;; derived before the aggregate fires.
|
||||||
|
;;
|
||||||
|
;; Empty input: count → 0, sum → 0, min/max → no binding (rule fails).
|
||||||
|
|
||||||
|
(define dl-aggregate-rels (list "count" "sum" "min" "max" "findall"))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-aggregate?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(>= (len lit) 4)
|
||||||
|
(let ((rel (dl-rel-name lit)))
|
||||||
|
(cond
|
||||||
|
((nil? rel) false)
|
||||||
|
(else (dl-member-string? rel dl-aggregate-rels)))))))
|
||||||
|
|
||||||
|
;; Apply aggregation operator to a list of (already-distinct) numeric or
|
||||||
|
;; symbolic values. Returns the aggregated value, or :empty if min/max
|
||||||
|
;; has no input.
|
||||||
|
(define
|
||||||
|
dl-do-aggregate
|
||||||
|
(fn
|
||||||
|
(op vals)
|
||||||
|
(cond
|
||||||
|
((= op "count") (len vals))
|
||||||
|
((= op "sum") (dl-sum-vals vals 0))
|
||||||
|
((= op "findall") vals)
|
||||||
|
((= op "min")
|
||||||
|
(cond
|
||||||
|
((= (len vals) 0) :empty)
|
||||||
|
(else (dl-min-vals vals 1 (first vals)))))
|
||||||
|
((= op "max")
|
||||||
|
(cond
|
||||||
|
((= (len vals) 0) :empty)
|
||||||
|
(else (dl-max-vals vals 1 (first vals)))))
|
||||||
|
(else (error (str "datalog: unknown aggregate " op))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-sum-vals
|
||||||
|
(fn
|
||||||
|
(vals acc)
|
||||||
|
(cond
|
||||||
|
((= (len vals) 0) acc)
|
||||||
|
(else (dl-sum-vals (rest vals) (+ acc (first vals)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-min-vals
|
||||||
|
(fn
|
||||||
|
(vals i cur)
|
||||||
|
(cond
|
||||||
|
((>= i (len vals)) cur)
|
||||||
|
(else
|
||||||
|
(let ((v (nth vals i)))
|
||||||
|
(dl-min-vals vals (+ i 1) (if (< v cur) v cur)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-max-vals
|
||||||
|
(fn
|
||||||
|
(vals i cur)
|
||||||
|
(cond
|
||||||
|
((>= i (len vals)) cur)
|
||||||
|
(else
|
||||||
|
(let ((v (nth vals i)))
|
||||||
|
(dl-max-vals vals (+ i 1) (if (> v cur) v cur)))))))
|
||||||
|
|
||||||
|
;; Membership check by deep equality (so 30 == 30.0 etc).
|
||||||
|
(define
|
||||||
|
dl-val-member?
|
||||||
|
(fn
|
||||||
|
(v xs)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-tuple-equal? v (first xs)) true)
|
||||||
|
(else (dl-val-member? v (rest xs))))))
|
||||||
|
|
||||||
|
;; Evaluate an aggregate body lit under `subst`. Returns the list of
|
||||||
|
;; extended substitutions (0 or 1 element).
|
||||||
|
(define
|
||||||
|
dl-eval-aggregate
|
||||||
|
(fn
|
||||||
|
(lit db subst)
|
||||||
|
(let
|
||||||
|
((op (dl-rel-name lit))
|
||||||
|
(result-var (nth lit 1))
|
||||||
|
(agg-var (nth lit 2))
|
||||||
|
(goal (nth lit 3)))
|
||||||
|
(cond
|
||||||
|
((not (dl-var? agg-var))
|
||||||
|
(error (str "datalog aggregate (" op
|
||||||
|
"): second arg must be a variable, got " agg-var)))
|
||||||
|
((not (and (list? goal) (> (len goal) 0)
|
||||||
|
(symbol? (first goal))))
|
||||||
|
(error (str "datalog aggregate (" op
|
||||||
|
"): third arg must be a positive literal, got "
|
||||||
|
goal)))
|
||||||
|
((not (dl-member-string?
|
||||||
|
(symbol->string agg-var)
|
||||||
|
(dl-vars-of goal)))
|
||||||
|
(error (str "datalog aggregate (" op
|
||||||
|
"): aggregation variable " agg-var
|
||||||
|
" does not appear in the goal " goal
|
||||||
|
" — without it every match contributes the same "
|
||||||
|
"(unbound) value and the result is meaningless")))
|
||||||
|
(else
|
||||||
|
(let ((vals (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let ((v (dl-apply-subst agg-var s)))
|
||||||
|
(when (not (dl-val-member? v vals))
|
||||||
|
(append! vals v))))
|
||||||
|
(dl-find-bindings (list goal) db subst))
|
||||||
|
(let ((agg-val (dl-do-aggregate op vals)))
|
||||||
|
(cond
|
||||||
|
((= agg-val :empty) (list))
|
||||||
|
(else
|
||||||
|
(let ((s2 (dl-unify result-var agg-val subst)))
|
||||||
|
(if (nil? s2) (list) (list s2)))))))))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; Stratification edges from aggregates: like negation, the goal's
|
||||||
|
;; relation must be in a strictly lower stratum so that the aggregate
|
||||||
|
;; fires only after the underlying tuples are settled.
|
||||||
|
(define
|
||||||
|
dl-aggregate-dep-edge
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((dl-aggregate? lit)
|
||||||
|
(let ((goal (nth lit 3)))
|
||||||
|
(cond
|
||||||
|
((and (list? goal) (> (len goal) 0))
|
||||||
|
(let ((rel (dl-rel-name goal)))
|
||||||
|
(if (nil? rel) nil {:rel rel :neg true})))
|
||||||
|
(else nil))))
|
||||||
|
(else nil))))
|
||||||
303
lib/datalog/api.sx
Normal file
303
lib/datalog/api.sx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
;; lib/datalog/api.sx — SX-data embedding API.
|
||||||
|
;;
|
||||||
|
;; Where Phase 1's `dl-program` takes a Datalog source string,
|
||||||
|
;; this module exposes a parser-free API that consumes SX data
|
||||||
|
;; directly. Two rule shapes are accepted:
|
||||||
|
;;
|
||||||
|
;; - dict: {:head <literal> :body (<literal> ...)}
|
||||||
|
;; - list: (<head-elements...> <- <body-literal> ...)
|
||||||
|
;; — `<-` is an SX symbol used as the rule arrow.
|
||||||
|
;;
|
||||||
|
;; Examples:
|
||||||
|
;;
|
||||||
|
;; (dl-program-data
|
||||||
|
;; '((parent tom bob) (parent tom liz) (parent bob ann))
|
||||||
|
;; '((ancestor X Y <- (parent X Y))
|
||||||
|
;; (ancestor X Z <- (parent X Y) (ancestor Y Z))))
|
||||||
|
;;
|
||||||
|
;; (dl-query db '(ancestor tom X)) ; same query API as before
|
||||||
|
;;
|
||||||
|
;; Variables follow the parser convention: SX symbols whose first
|
||||||
|
;; character is uppercase or `_` are variables.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rule
|
||||||
|
(fn (head body) {:head head :body body}))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rule-arrow?
|
||||||
|
(fn
|
||||||
|
(x)
|
||||||
|
(and (symbol? x) (= (symbol->string x) "<-"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-find-arrow
|
||||||
|
(fn
|
||||||
|
(rl i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) nil)
|
||||||
|
((dl-rule-arrow? (nth rl i)) i)
|
||||||
|
(else (dl-find-arrow rl (+ i 1) n)))))
|
||||||
|
|
||||||
|
;; Given a list of the form (head-elt ... <- body-lit ...) returns
|
||||||
|
;; {:head (head-elt ...) :body (body-lit ...)}. If no arrow is
|
||||||
|
;; present, the whole list is treated as the head and the body is
|
||||||
|
;; empty (i.e. a fact written rule-style).
|
||||||
|
(define
|
||||||
|
dl-rule-from-list
|
||||||
|
(fn
|
||||||
|
(rl)
|
||||||
|
(let ((n (len rl)))
|
||||||
|
(let ((idx (dl-find-arrow rl 0 n)))
|
||||||
|
(cond
|
||||||
|
((nil? idx) {:head rl :body (list)})
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((head (slice rl 0 idx))
|
||||||
|
(body (slice rl (+ idx 1) n)))
|
||||||
|
{:head head :body body})))))))
|
||||||
|
|
||||||
|
;; Coerce a rule given as either a dict or a list-with-arrow to a dict.
|
||||||
|
(define
|
||||||
|
dl-coerce-rule
|
||||||
|
(fn
|
||||||
|
(r)
|
||||||
|
(cond
|
||||||
|
((dict? r) r)
|
||||||
|
((list? r) (dl-rule-from-list r))
|
||||||
|
(else (error (str "dl-coerce-rule: expected dict or list, got " r))))))
|
||||||
|
|
||||||
|
;; Build a db from SX data lists.
|
||||||
|
(define
|
||||||
|
dl-program-data
|
||||||
|
(fn
|
||||||
|
(facts rules)
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(for-each (fn (lit) (dl-add-fact! db lit)) facts)
|
||||||
|
(for-each
|
||||||
|
(fn (r) (dl-add-rule! db (dl-coerce-rule r)))
|
||||||
|
rules)
|
||||||
|
db))))
|
||||||
|
|
||||||
|
;; Add a single fact at runtime, then re-saturate the db so derived
|
||||||
|
;; tuples reflect the change. Returns the db.
|
||||||
|
(define
|
||||||
|
dl-assert!
|
||||||
|
(fn
|
||||||
|
(db lit)
|
||||||
|
(do
|
||||||
|
(dl-add-fact! db lit)
|
||||||
|
(dl-saturate! db)
|
||||||
|
db)))
|
||||||
|
|
||||||
|
;; Remove a fact and re-saturate. Mixed relations (which have BOTH
|
||||||
|
;; user-asserted facts AND rules) are supported via :edb-keys provenance
|
||||||
|
;; — explicit facts are marked at dl-add-fact! time, the saturator uses
|
||||||
|
;; dl-add-derived! which doesn't mark them, so the retract pass can
|
||||||
|
;; safely wipe IDB-derived tuples while preserving the user's EDB.
|
||||||
|
;;
|
||||||
|
;; Effect:
|
||||||
|
;; - remove tuples matching `lit` from :facts and :edb-keys
|
||||||
|
;; - for every relation that has a rule (i.e. potentially IDB or
|
||||||
|
;; mixed), drop the IDB-derived portion (anything not in :edb-keys)
|
||||||
|
;; so the saturator can re-derive cleanly
|
||||||
|
;; - re-saturate
|
||||||
|
(define
|
||||||
|
dl-retract!
|
||||||
|
(fn
|
||||||
|
(db lit)
|
||||||
|
(let
|
||||||
|
((rel-key (dl-rel-name lit)))
|
||||||
|
(do
|
||||||
|
;; Drop the matching tuple from its relation list, its facts-keys,
|
||||||
|
;; its first-arg index, AND from :edb-keys (if present).
|
||||||
|
(when
|
||||||
|
(has-key? (get db :facts) rel-key)
|
||||||
|
(let
|
||||||
|
((existing (get (get db :facts) rel-key))
|
||||||
|
(kept (list))
|
||||||
|
(kept-keys {})
|
||||||
|
(kept-index {})
|
||||||
|
(edb-rel (cond
|
||||||
|
((has-key? (get db :edb-keys) rel-key)
|
||||||
|
(get (get db :edb-keys) rel-key))
|
||||||
|
(else nil)))
|
||||||
|
(kept-edb {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(t)
|
||||||
|
(when
|
||||||
|
(not (dl-tuple-equal? t lit))
|
||||||
|
(do
|
||||||
|
(append! kept t)
|
||||||
|
(let ((tk (dl-tuple-key t)))
|
||||||
|
(do
|
||||||
|
(dict-set! kept-keys tk true)
|
||||||
|
(when
|
||||||
|
(and (not (nil? edb-rel))
|
||||||
|
(has-key? edb-rel tk))
|
||||||
|
(dict-set! kept-edb tk true))))
|
||||||
|
(when
|
||||||
|
(>= (len t) 2)
|
||||||
|
(let ((k (dl-arg-key (nth t 1))))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? kept-index k))
|
||||||
|
(dict-set! kept-index k (list)))
|
||||||
|
(append! (get kept-index k) t)))))))
|
||||||
|
existing)
|
||||||
|
(dict-set! (get db :facts) rel-key kept)
|
||||||
|
(dict-set! (get db :facts-keys) rel-key kept-keys)
|
||||||
|
(dict-set! (get db :facts-index) rel-key kept-index)
|
||||||
|
(when
|
||||||
|
(not (nil? edb-rel))
|
||||||
|
(dict-set! (get db :edb-keys) rel-key kept-edb)))))
|
||||||
|
;; For each rule-head relation, strip the IDB-derived tuples
|
||||||
|
;; (anything not marked in :edb-keys) so the saturator can
|
||||||
|
;; cleanly re-derive without leaving stale tuples that depended
|
||||||
|
;; on the now-removed fact.
|
||||||
|
(let ((rule-heads (dl-rule-head-rels db)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(when
|
||||||
|
(has-key? (get db :facts) k)
|
||||||
|
(let
|
||||||
|
((existing (get (get db :facts) k))
|
||||||
|
(kept (list))
|
||||||
|
(kept-keys {})
|
||||||
|
(kept-index {})
|
||||||
|
(edb-rel (cond
|
||||||
|
((has-key? (get db :edb-keys) k)
|
||||||
|
(get (get db :edb-keys) k))
|
||||||
|
(else {}))))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(t)
|
||||||
|
(let ((tk (dl-tuple-key t)))
|
||||||
|
(when
|
||||||
|
(has-key? edb-rel tk)
|
||||||
|
(do
|
||||||
|
(append! kept t)
|
||||||
|
(dict-set! kept-keys tk true)
|
||||||
|
(when
|
||||||
|
(>= (len t) 2)
|
||||||
|
(let ((kk (dl-arg-key (nth t 1))))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? kept-index kk))
|
||||||
|
(dict-set! kept-index kk (list)))
|
||||||
|
(append! (get kept-index kk) t))))))))
|
||||||
|
existing)
|
||||||
|
(dict-set! (get db :facts) k kept)
|
||||||
|
(dict-set! (get db :facts-keys) k kept-keys)
|
||||||
|
(dict-set! (get db :facts-index) k kept-index)))))
|
||||||
|
rule-heads))
|
||||||
|
(dl-saturate! db)
|
||||||
|
db))))
|
||||||
|
|
||||||
|
;; ── Convenience: single-call source + query ───────────────────
|
||||||
|
;; (dl-eval source query-source) parses both, builds a db, saturates,
|
||||||
|
;; runs the query, returns the substitution list. The query source
|
||||||
|
;; should be `?- goal[, goal ...].` — the parser produces a clause
|
||||||
|
;; with :query containing a list of literals which is fed straight
|
||||||
|
;; to dl-query.
|
||||||
|
(define
|
||||||
|
dl-eval
|
||||||
|
(fn
|
||||||
|
(source query-source)
|
||||||
|
(let
|
||||||
|
((db (dl-program source))
|
||||||
|
(queries (dl-parse query-source)))
|
||||||
|
(cond
|
||||||
|
((= (len queries) 0) (error "dl-eval: query string is empty"))
|
||||||
|
((not (has-key? (first queries) :query))
|
||||||
|
(error "dl-eval: second arg must be a `?- ...` query clause"))
|
||||||
|
(else
|
||||||
|
(dl-query db (get (first queries) :query)))))))
|
||||||
|
|
||||||
|
;; (dl-eval-magic source query-source) — like dl-eval but routes a
|
||||||
|
;; single-positive-literal query through `dl-magic-query` for goal-
|
||||||
|
;; directed evaluation. Multi-literal query bodies fall back to the
|
||||||
|
;; standard dl-query path (magic-sets is currently only wired for
|
||||||
|
;; single-positive goals). The caller's source is parsed afresh
|
||||||
|
;; each call so successive invocations are independent.
|
||||||
|
(define
|
||||||
|
dl-eval-magic
|
||||||
|
(fn
|
||||||
|
(source query-source)
|
||||||
|
(let
|
||||||
|
((db (dl-program source))
|
||||||
|
(queries (dl-parse query-source)))
|
||||||
|
(cond
|
||||||
|
((= (len queries) 0) (error "dl-eval-magic: query string is empty"))
|
||||||
|
((not (has-key? (first queries) :query))
|
||||||
|
(error
|
||||||
|
"dl-eval-magic: second arg must be a `?- ...` query clause"))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((qbody (get (first queries) :query)))
|
||||||
|
(cond
|
||||||
|
((and (= (len qbody) 1)
|
||||||
|
(list? (first qbody))
|
||||||
|
(> (len (first qbody)) 0)
|
||||||
|
(symbol? (first (first qbody))))
|
||||||
|
(dl-magic-query db (first qbody)))
|
||||||
|
(else (dl-query db qbody)))))))))
|
||||||
|
|
||||||
|
;; List rules whose head's relation matches `rel-name`. Useful for
|
||||||
|
;; inspection ("show me how this relation is derived") without
|
||||||
|
;; exposing the internal `:rules` list.
|
||||||
|
(define
|
||||||
|
dl-rules-of
|
||||||
|
(fn
|
||||||
|
(db rel-name)
|
||||||
|
(let ((out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(when
|
||||||
|
(= (dl-rel-name (get rule :head)) rel-name)
|
||||||
|
(append! out rule)))
|
||||||
|
(dl-rules db))
|
||||||
|
out))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rule-head-rels
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let ((seen (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let ((h (dl-rel-name (get rule :head))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? h)) (not (dl-member-string? h seen)))
|
||||||
|
(append! seen h))))
|
||||||
|
(dl-rules db))
|
||||||
|
seen))))
|
||||||
|
|
||||||
|
;; Wipe every relation that has at least one rule (i.e. every IDB
|
||||||
|
;; relation) — leaves EDB facts and rule definitions intact. Useful
|
||||||
|
;; before a follow-up `dl-saturate!` if you want a clean restart, or
|
||||||
|
;; for inspection of the EDB-only baseline.
|
||||||
|
(define
|
||||||
|
dl-clear-idb!
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let ((rule-heads (dl-rule-head-rels db)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(do
|
||||||
|
(dict-set! (get db :facts) k (list))
|
||||||
|
(dict-set! (get db :facts-keys) k {})
|
||||||
|
(dict-set! (get db :facts-index) k {})))
|
||||||
|
rule-heads)
|
||||||
|
db))))
|
||||||
406
lib/datalog/builtins.sx
Normal file
406
lib/datalog/builtins.sx
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
;; lib/datalog/builtins.sx — comparison + arithmetic body literals.
|
||||||
|
;;
|
||||||
|
;; Built-in predicates filter / extend candidate substitutions during
|
||||||
|
;; rule evaluation. They are not stored facts and do not participate in
|
||||||
|
;; the Herbrand base.
|
||||||
|
;;
|
||||||
|
;; (< a b) (<= a b) (> a b) (>= a b) ; numeric (or string) compare
|
||||||
|
;; (= a b) ; unify (binds vars)
|
||||||
|
;; (!= a b) ; ground-only inequality
|
||||||
|
;; (is X expr) ; bind X to expr's value
|
||||||
|
;;
|
||||||
|
;; Arithmetic expressions are SX-list compounds:
|
||||||
|
;; (+ a b) (- a b) (* a b) (/ a b)
|
||||||
|
;; or numbers / variables (must be bound at evaluation time).
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-comparison?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)))
|
||||||
|
(cond
|
||||||
|
((nil? rel) false)
|
||||||
|
(else (dl-member-string? rel (list "<" "<=" ">" ">=" "!="))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eq?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(let ((rel (dl-rel-name lit))) (and (not (nil? rel)) (= rel "="))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-is?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)))
|
||||||
|
(and (not (nil? rel)) (= rel "is"))))))
|
||||||
|
|
||||||
|
;; Evaluate an arithmetic expression under subst. Returns the numeric
|
||||||
|
;; result, or raises if any operand is unbound or non-numeric.
|
||||||
|
(define
|
||||||
|
dl-eval-arith
|
||||||
|
(fn
|
||||||
|
(expr subst)
|
||||||
|
(let
|
||||||
|
((w (dl-walk expr subst)))
|
||||||
|
(cond
|
||||||
|
((number? w) w)
|
||||||
|
((dl-var? w)
|
||||||
|
(error (str "datalog arith: unbound variable " (symbol->string w))))
|
||||||
|
((list? w)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name w)) (args (rest w)))
|
||||||
|
(cond
|
||||||
|
((not (= (len args) 2))
|
||||||
|
(error (str "datalog arith: need 2 args, got " w)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((a (dl-eval-arith (first args) subst))
|
||||||
|
(b (dl-eval-arith (nth args 1) subst)))
|
||||||
|
(cond
|
||||||
|
((= rel "+") (+ a b))
|
||||||
|
((= rel "-") (- a b))
|
||||||
|
((= rel "*") (* a b))
|
||||||
|
((= rel "/")
|
||||||
|
(cond
|
||||||
|
((= b 0)
|
||||||
|
(error
|
||||||
|
(str "datalog arith: division by zero in "
|
||||||
|
w)))
|
||||||
|
(else (/ a b))))
|
||||||
|
(else (error (str "datalog arith: unknown op " rel)))))))))
|
||||||
|
(else (error (str "datalog arith: not a number — " w)))))))
|
||||||
|
|
||||||
|
;; Comparable types — both operands must be the same primitive type
|
||||||
|
;; (both numbers, both strings). `!=` is the exception: it's defined
|
||||||
|
;; for any pair (returns true iff not equal) since dl-tuple-equal?
|
||||||
|
;; handles type-mixed comparisons.
|
||||||
|
(define
|
||||||
|
dl-compare-typeok?
|
||||||
|
(fn
|
||||||
|
(rel a b)
|
||||||
|
(cond
|
||||||
|
((= rel "!=") true)
|
||||||
|
((and (number? a) (number? b)) true)
|
||||||
|
((and (string? a) (string? b)) true)
|
||||||
|
(else false))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eval-compare
|
||||||
|
(fn
|
||||||
|
(lit subst)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit))
|
||||||
|
(a (dl-walk (nth lit 1) subst))
|
||||||
|
(b (dl-walk (nth lit 2) subst)))
|
||||||
|
(cond
|
||||||
|
((or (dl-var? a) (dl-var? b))
|
||||||
|
(error
|
||||||
|
(str
|
||||||
|
"datalog: comparison "
|
||||||
|
rel
|
||||||
|
" has unbound argument; "
|
||||||
|
"ensure prior body literal binds the variable")))
|
||||||
|
((not (dl-compare-typeok? rel a b))
|
||||||
|
(error
|
||||||
|
(str "datalog: comparison " rel " requires same-type "
|
||||||
|
"operands (both numbers or both strings), got "
|
||||||
|
a " and " b)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((ok (cond ((= rel "<") (< a b)) ((= rel "<=") (<= a b)) ((= rel ">") (> a b)) ((= rel ">=") (>= a b)) ((= rel "!=") (not (dl-tuple-equal? a b))) (else (error (str "datalog: unknown compare " rel))))))
|
||||||
|
(if ok subst nil)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eval-eq
|
||||||
|
(fn
|
||||||
|
(lit subst)
|
||||||
|
(dl-unify (nth lit 1) (nth lit 2) subst)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eval-is
|
||||||
|
(fn
|
||||||
|
(lit subst)
|
||||||
|
(let
|
||||||
|
((target (nth lit 1)) (expr (nth lit 2)))
|
||||||
|
(let
|
||||||
|
((value (dl-eval-arith expr subst)))
|
||||||
|
(dl-unify target value subst)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eval-builtin
|
||||||
|
(fn
|
||||||
|
(lit subst)
|
||||||
|
(cond
|
||||||
|
((dl-comparison? lit) (dl-eval-compare lit subst))
|
||||||
|
((dl-eq? lit) (dl-eval-eq lit subst))
|
||||||
|
((dl-is? lit) (dl-eval-is lit subst))
|
||||||
|
(else (error (str "dl-eval-builtin: not a built-in: " lit))))))
|
||||||
|
|
||||||
|
;; ── Safety analysis ──────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; Walks body literals left-to-right tracking a "bound" set. The check
|
||||||
|
;; understands these literal kinds:
|
||||||
|
;;
|
||||||
|
;; positive non-built-in → adds its vars to bound
|
||||||
|
;; (is X expr) → vars(expr) ⊆ bound, then add X (if var)
|
||||||
|
;; <,<=,>,>=,!= → all vars ⊆ bound (no binding)
|
||||||
|
;; (= a b) where:
|
||||||
|
;; both non-vars → constraint check, no binding
|
||||||
|
;; a var, b not → bind a
|
||||||
|
;; b var, a not → bind b
|
||||||
|
;; both vars → at least one in bound; bind the other
|
||||||
|
;; {:neg lit} → all vars ⊆ bound (Phase 7 enforces fully)
|
||||||
|
;;
|
||||||
|
;; At end, head vars (minus `_`) must be ⊆ bound.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-vars-not-in
|
||||||
|
(fn
|
||||||
|
(vs bound)
|
||||||
|
(let
|
||||||
|
((out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when (not (dl-member-string? v bound)) (append! out v)))
|
||||||
|
vs)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
;; Filter a list of variable-name strings to exclude anonymous-renamed
|
||||||
|
;; vars (`_` in source → `_anon*` by dl-rename-anon-term). Used by
|
||||||
|
;; the negation safety check, where anonymous vars are existential
|
||||||
|
;; within the negated literal.
|
||||||
|
(define
|
||||||
|
dl-non-anon-vars
|
||||||
|
(fn
|
||||||
|
(vs)
|
||||||
|
(let
|
||||||
|
((out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(not (and (>= (len v) 5)
|
||||||
|
(= (slice v 0 5) "_anon")))
|
||||||
|
(append! out v)))
|
||||||
|
vs)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rule-check-safety
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let
|
||||||
|
((head (get rule :head))
|
||||||
|
(body (get rule :body))
|
||||||
|
(bound (list))
|
||||||
|
(err nil))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-add-bound!
|
||||||
|
(fn
|
||||||
|
(vs)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when (not (dl-member-string? v bound)) (append! bound v)))
|
||||||
|
vs)))
|
||||||
|
(define
|
||||||
|
dl-process-eq!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((a (nth lit 1)) (b (nth lit 2)))
|
||||||
|
(let
|
||||||
|
((va (dl-var? a)) (vb (dl-var? b)))
|
||||||
|
(cond
|
||||||
|
((and (not va) (not vb)) nil)
|
||||||
|
((and va (not vb))
|
||||||
|
(dl-add-bound! (list (symbol->string a))))
|
||||||
|
((and (not va) vb)
|
||||||
|
(dl-add-bound! (list (symbol->string b))))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((sa (symbol->string a)) (sb (symbol->string b)))
|
||||||
|
(cond
|
||||||
|
((dl-member-string? sa bound)
|
||||||
|
(dl-add-bound! (list sb)))
|
||||||
|
((dl-member-string? sb bound)
|
||||||
|
(dl-add-bound! (list sa)))
|
||||||
|
(else
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str
|
||||||
|
"= between two unbound variables "
|
||||||
|
(list sa sb)
|
||||||
|
" — at least one must be bound by an "
|
||||||
|
"earlier positive body literal")))))))))))
|
||||||
|
(define
|
||||||
|
dl-process-cmp!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((needed (dl-vars-of (list (nth lit 1) (nth lit 2)))))
|
||||||
|
(let
|
||||||
|
((missing (dl-vars-not-in needed bound)))
|
||||||
|
(when
|
||||||
|
(> (len missing) 0)
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str
|
||||||
|
"comparison "
|
||||||
|
(dl-rel-name lit)
|
||||||
|
" requires bound variable(s) "
|
||||||
|
missing
|
||||||
|
" (must be bound by an earlier positive "
|
||||||
|
"body literal)")))))))
|
||||||
|
(define
|
||||||
|
dl-process-is!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((tgt (nth lit 1)) (expr (nth lit 2)))
|
||||||
|
(let
|
||||||
|
((needed (dl-vars-of expr)))
|
||||||
|
(let
|
||||||
|
((missing (dl-vars-not-in needed bound)))
|
||||||
|
(cond
|
||||||
|
((> (len missing) 0)
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str
|
||||||
|
"is RHS uses unbound variable(s) "
|
||||||
|
missing
|
||||||
|
" — bind them via a prior positive body "
|
||||||
|
"literal")))
|
||||||
|
(else
|
||||||
|
(when
|
||||||
|
(dl-var? tgt)
|
||||||
|
(dl-add-bound! (list (symbol->string tgt)))))))))))
|
||||||
|
(define
|
||||||
|
dl-process-neg!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((inner (get lit :neg)))
|
||||||
|
(let
|
||||||
|
((inner-rn
|
||||||
|
(cond
|
||||||
|
((and (list? inner) (> (len inner) 0))
|
||||||
|
(dl-rel-name inner))
|
||||||
|
(else nil)))
|
||||||
|
;; Anonymous variables (`_` in source → `_anon*` after
|
||||||
|
;; renaming) are existentially quantified within the
|
||||||
|
;; negated literal — they don't need to be bound by
|
||||||
|
;; an earlier body lit, since `not p(X, _)` is a
|
||||||
|
;; valid idiom for "no Y exists s.t. p(X, Y)". Filter
|
||||||
|
;; them out of the safety check.
|
||||||
|
(needed (dl-non-anon-vars (dl-vars-of inner)))
|
||||||
|
(missing (dl-vars-not-in needed bound)))
|
||||||
|
(cond
|
||||||
|
((and (not (nil? inner-rn)) (dl-reserved-rel? inner-rn))
|
||||||
|
(set! err
|
||||||
|
(str "negated literal uses reserved name '"
|
||||||
|
inner-rn
|
||||||
|
"' — nested `not(...)` / negated built-ins are "
|
||||||
|
"not supported; introduce an intermediate "
|
||||||
|
"relation and negate that")))
|
||||||
|
((> (len missing) 0)
|
||||||
|
(set! err
|
||||||
|
(str "negation refers to unbound variable(s) "
|
||||||
|
missing
|
||||||
|
" — they must be bound by an earlier "
|
||||||
|
"positive body literal"))))))))
|
||||||
|
(define
|
||||||
|
dl-process-agg!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((result-var (nth lit 1)))
|
||||||
|
;; Aggregate goal vars are existentially quantified within
|
||||||
|
;; the aggregate; nothing required from outer context. The
|
||||||
|
;; result var becomes bound after the aggregate fires.
|
||||||
|
(when
|
||||||
|
(dl-var? result-var)
|
||||||
|
(dl-add-bound! (list (symbol->string result-var)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-process-lit!
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(when
|
||||||
|
(nil? err)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-process-neg! lit))
|
||||||
|
;; A bare dict that is not a recognised negation is
|
||||||
|
;; almost certainly a typo (e.g. `{:negs ...}` instead
|
||||||
|
;; of `{:neg ...}`). Without this guard the dict would
|
||||||
|
;; silently fall through every clause; the head safety
|
||||||
|
;; check would then flag the head variables as unbound
|
||||||
|
;; even though the real bug is the malformed body lit.
|
||||||
|
((dict? lit)
|
||||||
|
(set! err
|
||||||
|
(str "body literal is a dict but lacks :neg — "
|
||||||
|
"the only dict-shaped body lit recognised is "
|
||||||
|
"{:neg <positive-lit>} for stratified "
|
||||||
|
"negation, got " lit)))
|
||||||
|
((dl-aggregate? lit) (dl-process-agg! lit))
|
||||||
|
((dl-eq? lit) (dl-process-eq! lit))
|
||||||
|
((dl-is? lit) (dl-process-is! lit))
|
||||||
|
((dl-comparison? lit) (dl-process-cmp! lit))
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(let ((rn (dl-rel-name lit)))
|
||||||
|
(cond
|
||||||
|
((and (not (nil? rn)) (dl-reserved-rel? rn))
|
||||||
|
(set! err
|
||||||
|
(str "body literal uses reserved name '" rn
|
||||||
|
"' — built-ins / aggregates have their own "
|
||||||
|
"syntax; nested `not(...)` is not supported "
|
||||||
|
"(use stratified negation via an "
|
||||||
|
"intermediate relation)")))
|
||||||
|
(else (dl-add-bound! (dl-vars-of lit))))))
|
||||||
|
(else
|
||||||
|
;; Anything that's not a dict, not a list, or an
|
||||||
|
;; empty list. Numbers / strings / symbols as body
|
||||||
|
;; lits don't make sense — surface the type.
|
||||||
|
(set! err
|
||||||
|
(str "body literal must be a positive lit, "
|
||||||
|
"built-in, aggregate, or {:neg ...} dict, "
|
||||||
|
"got " lit)))))))
|
||||||
|
(for-each dl-process-lit! body)
|
||||||
|
(when
|
||||||
|
(nil? err)
|
||||||
|
(let
|
||||||
|
((head-vars (dl-vars-of head)) (missing (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(and (not (dl-member-string? v bound)) (not (= v "_")))
|
||||||
|
(append! missing v)))
|
||||||
|
head-vars)
|
||||||
|
(when
|
||||||
|
(> (len missing) 0)
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str
|
||||||
|
"head variable(s) "
|
||||||
|
missing
|
||||||
|
" do not appear in any positive body literal"))))))
|
||||||
|
err))))
|
||||||
32
lib/datalog/conformance.conf
Normal file
32
lib/datalog/conformance.conf
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Datalog conformance config — sourced by lib/guest/conformance.sh.
|
||||||
|
|
||||||
|
LANG_NAME=datalog
|
||||||
|
MODE=dict
|
||||||
|
|
||||||
|
PRELOADS=(
|
||||||
|
lib/datalog/tokenizer.sx
|
||||||
|
lib/datalog/parser.sx
|
||||||
|
lib/datalog/unify.sx
|
||||||
|
lib/datalog/db.sx
|
||||||
|
lib/datalog/builtins.sx
|
||||||
|
lib/datalog/aggregates.sx
|
||||||
|
lib/datalog/strata.sx
|
||||||
|
lib/datalog/eval.sx
|
||||||
|
lib/datalog/api.sx
|
||||||
|
lib/datalog/magic.sx
|
||||||
|
lib/datalog/demo.sx
|
||||||
|
)
|
||||||
|
|
||||||
|
SUITES=(
|
||||||
|
"tokenize:lib/datalog/tests/tokenize.sx:(dl-tokenize-tests-run!)"
|
||||||
|
"parse:lib/datalog/tests/parse.sx:(dl-parse-tests-run!)"
|
||||||
|
"unify:lib/datalog/tests/unify.sx:(dl-unify-tests-run!)"
|
||||||
|
"eval:lib/datalog/tests/eval.sx:(dl-eval-tests-run!)"
|
||||||
|
"builtins:lib/datalog/tests/builtins.sx:(dl-builtins-tests-run!)"
|
||||||
|
"semi_naive:lib/datalog/tests/semi_naive.sx:(dl-semi-naive-tests-run!)"
|
||||||
|
"negation:lib/datalog/tests/negation.sx:(dl-negation-tests-run!)"
|
||||||
|
"aggregates:lib/datalog/tests/aggregates.sx:(dl-aggregates-tests-run!)"
|
||||||
|
"api:lib/datalog/tests/api.sx:(dl-api-tests-run!)"
|
||||||
|
"magic:lib/datalog/tests/magic.sx:(dl-magic-tests-run!)"
|
||||||
|
"demo:lib/datalog/tests/demo.sx:(dl-demo-tests-run!)"
|
||||||
|
)
|
||||||
3
lib/datalog/conformance.sh
Executable file
3
lib/datalog/conformance.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Thin wrapper — see lib/guest/conformance.sh and lib/datalog/conformance.conf.
|
||||||
|
exec bash "$(dirname "$0")/../guest/conformance.sh" "$(dirname "$0")/conformance.conf" "$@"
|
||||||
97
lib/datalog/datalog.sx
Normal file
97
lib/datalog/datalog.sx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
;; lib/datalog/datalog.sx — public API documentation index.
|
||||||
|
;;
|
||||||
|
;; This file is reference-only — `load` is an epoch-protocol command,
|
||||||
|
;; not an SX function, so it cannot reload a list of files from inside
|
||||||
|
;; another `.sx` file. To set up a fresh sx_server session with all
|
||||||
|
;; modules in scope, issue these loads in order:
|
||||||
|
;;
|
||||||
|
;; (load "lib/datalog/tokenizer.sx")
|
||||||
|
;; (load "lib/datalog/parser.sx")
|
||||||
|
;; (load "lib/datalog/unify.sx")
|
||||||
|
;; (load "lib/datalog/db.sx")
|
||||||
|
;; (load "lib/datalog/builtins.sx")
|
||||||
|
;; (load "lib/datalog/aggregates.sx")
|
||||||
|
;; (load "lib/datalog/strata.sx")
|
||||||
|
;; (load "lib/datalog/eval.sx")
|
||||||
|
;; (load "lib/datalog/api.sx")
|
||||||
|
;; (load "lib/datalog/magic.sx")
|
||||||
|
;; (load "lib/datalog/demo.sx")
|
||||||
|
;;
|
||||||
|
;; (lib/datalog/conformance.sh runs this load list automatically.)
|
||||||
|
;;
|
||||||
|
;; ── Public API surface ─────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; Source / data:
|
||||||
|
;; (dl-tokenize "src") → token list
|
||||||
|
;; (dl-parse "src") → parsed clauses
|
||||||
|
;; (dl-program "src") → db built from a source string
|
||||||
|
;; (dl-program-data facts rules) → db from SX data lists; rules
|
||||||
|
;; accept either dict form or
|
||||||
|
;; list form with `<-` arrow
|
||||||
|
;;
|
||||||
|
;; Construction (mutates db):
|
||||||
|
;; (dl-make-db) empty db
|
||||||
|
;; (dl-add-fact! db lit) rejects non-ground
|
||||||
|
;; (dl-add-rule! db rule) rejects unsafe rules
|
||||||
|
;; (dl-rule head body) dict-rule constructor
|
||||||
|
;; (dl-add-clause! db clause) parser output → fact or rule
|
||||||
|
;; (dl-load-program! db src) string source
|
||||||
|
;; (dl-set-strategy! db strategy) :semi-naive default; :magic
|
||||||
|
;; is informational, use
|
||||||
|
;; dl-magic-query for actual
|
||||||
|
;; magic-sets evaluation
|
||||||
|
;;
|
||||||
|
;; Mutation:
|
||||||
|
;; (dl-assert! db lit) add + re-saturate
|
||||||
|
;; (dl-retract! db lit) drop EDB, wipe IDB, re-saturate
|
||||||
|
;; (dl-clear-idb! db) wipe rule-headed relations
|
||||||
|
;;
|
||||||
|
;; Query / inspection:
|
||||||
|
;; (dl-saturate! db) stratified semi-naive default
|
||||||
|
;; (dl-saturate-naive! db) reference (slow on chains)
|
||||||
|
;; (dl-saturate-rules! db rules) per-rule-set semi-naive worker
|
||||||
|
;; (dl-query db goal) list of substitution dicts
|
||||||
|
;; (dl-relation db rel-name) tuple list for a relation
|
||||||
|
;; (dl-rules db) rule list
|
||||||
|
;; (dl-fact-count db) total ground tuples
|
||||||
|
;; (dl-summary db) {<rel>: count} for inspection
|
||||||
|
;;
|
||||||
|
;; Single-call convenience:
|
||||||
|
;; (dl-eval source query-source) parse, run, return substs
|
||||||
|
;; (dl-eval-magic source query-source) single-goal → magic-sets
|
||||||
|
;;
|
||||||
|
;; Magic-sets (lib/datalog/magic.sx):
|
||||||
|
;; (dl-adorn-goal goal) "b/f" adornment string
|
||||||
|
;; (dl-rule-sips rule head-adn) SIPS analysis per body lit
|
||||||
|
;; (dl-magic-rewrite rules rel adn args)
|
||||||
|
;; rewritten rule list + seed
|
||||||
|
;; (dl-magic-query db query-goal) end-to-end magic-sets query
|
||||||
|
;;
|
||||||
|
;; ── Body literal kinds ─────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; Positive (rel arg ... arg)
|
||||||
|
;; Negation {:neg (rel arg ...)}
|
||||||
|
;; Comparison (< X Y), (<= X Y), (> X Y), (>= X Y),
|
||||||
|
;; (= X Y), (!= X Y)
|
||||||
|
;; Arithmetic (is Z (+ X Y)) and (- * /)
|
||||||
|
;; Aggregation (count R V Goal), (sum R V Goal),
|
||||||
|
;; (min R V Goal), (max R V Goal),
|
||||||
|
;; (findall L V Goal)
|
||||||
|
;;
|
||||||
|
;; ── Variable conventions ───────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; Variables: SX symbols whose first char is uppercase A–Z or '_'.
|
||||||
|
;; Anonymous '_' is renamed to a fresh _anon<N> per occurrence at
|
||||||
|
;; rule/query load time so multiple '_' don't unify.
|
||||||
|
;;
|
||||||
|
;; ── Demo programs ──────────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; See lib/datalog/demo.sx — federation, content, permissions, and
|
||||||
|
;; the canonical "cooking posts by people I follow (transitively)"
|
||||||
|
;; example.
|
||||||
|
;;
|
||||||
|
;; ── Status ─────────────────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; See plans/datalog-on-sx.md — phase-by-phase progress log and
|
||||||
|
;; roadmap. Run `bash lib/datalog/conformance.sh` to refresh
|
||||||
|
;; `lib/datalog/scoreboard.{json,md}`.
|
||||||
575
lib/datalog/db.sx
Normal file
575
lib/datalog/db.sx
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
;; lib/datalog/db.sx — Datalog database (EDB + IDB + rules) + safety hook.
|
||||||
|
;;
|
||||||
|
;; A db is a mutable dict:
|
||||||
|
;; {:facts {<rel-name-string> -> (literal ...)}
|
||||||
|
;; :rules ({:head literal :body (literal ...)} ...)}
|
||||||
|
;;
|
||||||
|
;; Facts are stored as full literals `(rel arg ... arg)` so they unify
|
||||||
|
;; directly against rule body literals. Each relation's tuple list is
|
||||||
|
;; deduplicated on insert.
|
||||||
|
;;
|
||||||
|
;; Phase 3 introduced safety analysis for head variables; Phase 4 (in
|
||||||
|
;; lib/datalog/builtins.sx) swaps in the real `dl-rule-check-safety`,
|
||||||
|
;; which is order-aware and understands built-in predicates.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-make-db
|
||||||
|
(fn ()
|
||||||
|
{:facts {}
|
||||||
|
:facts-keys {}
|
||||||
|
:facts-index {}
|
||||||
|
:edb-keys {}
|
||||||
|
:rules (list)
|
||||||
|
:strategy :semi-naive}))
|
||||||
|
|
||||||
|
;; Record (rel-key, tuple-key) as user-asserted EDB. dl-add-fact! calls
|
||||||
|
;; this when an explicit fact is added; the saturator (which uses
|
||||||
|
;; dl-add-derived!) does NOT, so derived tuples never appear here.
|
||||||
|
;; dl-retract! consults :edb-keys to know which tuples must survive
|
||||||
|
;; the wipe-and-resaturate round-trip.
|
||||||
|
(define
|
||||||
|
dl-mark-edb!
|
||||||
|
(fn
|
||||||
|
(db rel-key tk)
|
||||||
|
(let
|
||||||
|
((edb (get db :edb-keys)))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? edb rel-key))
|
||||||
|
(dict-set! edb rel-key {}))
|
||||||
|
(dict-set! (get edb rel-key) tk true)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-edb-fact?
|
||||||
|
(fn
|
||||||
|
(db rel-key tk)
|
||||||
|
(let
|
||||||
|
((edb (get db :edb-keys)))
|
||||||
|
(and (has-key? edb rel-key)
|
||||||
|
(has-key? (get edb rel-key) tk)))))
|
||||||
|
|
||||||
|
;; Evaluation strategy. Default :semi-naive (used by dl-saturate!).
|
||||||
|
;; :naive selects dl-saturate-naive! (slower but easier to reason
|
||||||
|
;; about). :magic is a marker — goal-directed magic-sets evaluation
|
||||||
|
;; is invoked separately via `dl-magic-query`; setting :magic here
|
||||||
|
;; is purely informational. Any other value is rejected so typos
|
||||||
|
;; don't silently fall back to the default.
|
||||||
|
(define
|
||||||
|
dl-strategy-values
|
||||||
|
(list :semi-naive :naive :magic))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-set-strategy!
|
||||||
|
(fn
|
||||||
|
(db strategy)
|
||||||
|
(cond
|
||||||
|
((not (dl-keyword-member? strategy dl-strategy-values))
|
||||||
|
(error (str "dl-set-strategy!: unknown strategy " strategy
|
||||||
|
" — must be one of " dl-strategy-values)))
|
||||||
|
(else
|
||||||
|
(do
|
||||||
|
(dict-set! db :strategy strategy)
|
||||||
|
db)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-keyword-member?
|
||||||
|
(fn
|
||||||
|
(k xs)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((= k (first xs)) true)
|
||||||
|
(else (dl-keyword-member? k (rest xs))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-get-strategy
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(if (has-key? db :strategy) (get db :strategy) :semi-naive)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rel-name
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg)) (dl-rel-name (get lit :neg)))
|
||||||
|
((and (list? lit) (> (len lit) 0) (symbol? (first lit)))
|
||||||
|
(symbol->string (first lit)))
|
||||||
|
(else nil))))
|
||||||
|
|
||||||
|
(define dl-builtin-rels (list "<" "<=" ">" ">=" "=" "!=" "is"))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-member-string?
|
||||||
|
(fn
|
||||||
|
(s xs)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((= (first xs) s) true)
|
||||||
|
(else (dl-member-string? s (rest xs))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-builtin?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)))
|
||||||
|
(cond
|
||||||
|
((nil? rel) false)
|
||||||
|
(else (dl-member-string? rel dl-builtin-rels)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-positive-lit?
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg)) false)
|
||||||
|
((dl-builtin? lit) false)
|
||||||
|
((and (list? lit) (> (len lit) 0)) true)
|
||||||
|
(else false))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tuple-equal?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-tuple-equal-list? a b 0)))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tuple-equal-list?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-tuple-equal? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-tuple-equal-list? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tuple-member?
|
||||||
|
(fn
|
||||||
|
(lit lits)
|
||||||
|
(dl-tuple-member-aux? lit lits 0 (len lits))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tuple-member-aux?
|
||||||
|
(fn
|
||||||
|
(lit lits i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) false)
|
||||||
|
((dl-tuple-equal? lit (nth lits i)) true)
|
||||||
|
(else (dl-tuple-member-aux? lit lits (+ i 1) n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ensure-rel!
|
||||||
|
(fn
|
||||||
|
(db rel-key)
|
||||||
|
(let
|
||||||
|
((facts (get db :facts))
|
||||||
|
(fk (get db :facts-keys))
|
||||||
|
(fi (get db :facts-index)))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? facts rel-key))
|
||||||
|
(dict-set! facts rel-key (list)))
|
||||||
|
(when
|
||||||
|
(not (has-key? fk rel-key))
|
||||||
|
(dict-set! fk rel-key {}))
|
||||||
|
(when
|
||||||
|
(not (has-key? fi rel-key))
|
||||||
|
(dict-set! fi rel-key {}))
|
||||||
|
(get facts rel-key)))))
|
||||||
|
|
||||||
|
;; First-arg index helpers. Tuples are keyed by their first-after-rel
|
||||||
|
;; arg's `(str ...)`; when that arg is a constant, dl-match-positive
|
||||||
|
;; uses the index instead of scanning the full relation.
|
||||||
|
(define
|
||||||
|
dl-arg-key
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(str v)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-index-add!
|
||||||
|
(fn
|
||||||
|
(db rel-key lit)
|
||||||
|
(let
|
||||||
|
((idx (get db :facts-index))
|
||||||
|
(n (len lit)))
|
||||||
|
(when
|
||||||
|
(and (>= n 2) (has-key? idx rel-key))
|
||||||
|
(let
|
||||||
|
((rel-idx (get idx rel-key))
|
||||||
|
(k (dl-arg-key (nth lit 1))))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? rel-idx k))
|
||||||
|
(dict-set! rel-idx k (list)))
|
||||||
|
(append! (get rel-idx k) lit)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-index-lookup
|
||||||
|
(fn
|
||||||
|
(db rel-key arg-val)
|
||||||
|
(let
|
||||||
|
((idx (get db :facts-index)))
|
||||||
|
(cond
|
||||||
|
((not (has-key? idx rel-key)) (list))
|
||||||
|
(else
|
||||||
|
(let ((rel-idx (get idx rel-key))
|
||||||
|
(k (dl-arg-key arg-val)))
|
||||||
|
(if (has-key? rel-idx k) (get rel-idx k) (list))))))))
|
||||||
|
|
||||||
|
(define dl-tuple-key (fn (lit) (str lit)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rel-tuples
|
||||||
|
(fn
|
||||||
|
(db rel-key)
|
||||||
|
(let
|
||||||
|
((facts (get db :facts)))
|
||||||
|
(if (has-key? facts rel-key) (get facts rel-key) (list)))))
|
||||||
|
|
||||||
|
;; Reserved relation names: built-in / aggregate / negation / arrow.
|
||||||
|
;; Rules and facts may not have these as their head's relation, since
|
||||||
|
;; the saturator treats them specially or they are not relation names
|
||||||
|
;; at all.
|
||||||
|
(define
|
||||||
|
dl-reserved-rel-names
|
||||||
|
(list "not" "count" "sum" "min" "max" "findall" "is"
|
||||||
|
"<" "<=" ">" ">=" "=" "!=" "+" "-" "*" "/" ":-" "?-"))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-reserved-rel?
|
||||||
|
(fn
|
||||||
|
(name) (dl-member-string? name dl-reserved-rel-names)))
|
||||||
|
|
||||||
|
;; Internal: append a derived tuple to :facts without the public
|
||||||
|
;; validation pass and without marking :edb-keys. Used by the saturator
|
||||||
|
;; (eval.sx) and magic-sets (magic.sx). Returns true if the tuple was
|
||||||
|
;; new, false if already present.
|
||||||
|
(define
|
||||||
|
dl-add-derived!
|
||||||
|
(fn
|
||||||
|
(db lit)
|
||||||
|
(let
|
||||||
|
((rel-key (dl-rel-name lit)))
|
||||||
|
(let
|
||||||
|
((tuples (dl-ensure-rel! db rel-key))
|
||||||
|
(key-dict (get (get db :facts-keys) rel-key))
|
||||||
|
(tk (dl-tuple-key lit)))
|
||||||
|
(cond
|
||||||
|
((has-key? key-dict tk) false)
|
||||||
|
(else
|
||||||
|
(do
|
||||||
|
(dict-set! key-dict tk true)
|
||||||
|
(append! tuples lit)
|
||||||
|
(dl-index-add! db rel-key lit)
|
||||||
|
true)))))))
|
||||||
|
|
||||||
|
;; A simple term — number, string, or symbol — i.e. anything legal
|
||||||
|
;; as an EDB fact arg. Compound (list) args belong only in body
|
||||||
|
;; literals where they encode arithmetic / aggregate sub-goals.
|
||||||
|
(define
|
||||||
|
dl-simple-term?
|
||||||
|
(fn
|
||||||
|
(term)
|
||||||
|
(or (number? term) (string? term) (symbol? term))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-args-simple?
|
||||||
|
(fn
|
||||||
|
(lit i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) true)
|
||||||
|
((not (dl-simple-term? (nth lit i))) false)
|
||||||
|
(else (dl-args-simple? lit (+ i 1) n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-add-fact!
|
||||||
|
(fn
|
||||||
|
(db lit)
|
||||||
|
(cond
|
||||||
|
((not (and (list? lit) (> (len lit) 0)))
|
||||||
|
(error (str "dl-add-fact!: expected literal list, got " lit)))
|
||||||
|
((dl-reserved-rel? (dl-rel-name lit))
|
||||||
|
(error (str "dl-add-fact!: '" (dl-rel-name lit)
|
||||||
|
"' is a reserved name (built-in / aggregate / negation)")))
|
||||||
|
((not (dl-args-simple? lit 1 (len lit)))
|
||||||
|
(error (str "dl-add-fact!: fact args must be numbers, strings, "
|
||||||
|
"or symbols — compound args (e.g. arithmetic "
|
||||||
|
"expressions) are body-only and aren't evaluated "
|
||||||
|
"in fact position. got " lit)))
|
||||||
|
((not (dl-ground? lit (dl-empty-subst)))
|
||||||
|
(error (str "dl-add-fact!: expected ground literal, got " lit)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((rel-key (dl-rel-name lit)) (tk (dl-tuple-key lit)))
|
||||||
|
(do
|
||||||
|
;; Always mark EDB origin — even if the tuple key was already
|
||||||
|
;; present (e.g. previously derived), so an explicit assert
|
||||||
|
;; promotes it to EDB and protects it from the IDB wipe.
|
||||||
|
(dl-mark-edb! db rel-key tk)
|
||||||
|
(dl-add-derived! db lit)))))))
|
||||||
|
|
||||||
|
;; The full safety check lives in builtins.sx (it has to know which
|
||||||
|
;; predicates are built-ins). dl-add-rule! calls it via forward
|
||||||
|
;; reference; load builtins.sx alongside db.sx in any setup that
|
||||||
|
;; adds rules. The fallback below is used if builtins.sx isn't loaded.
|
||||||
|
(define
|
||||||
|
dl-rule-check-safety
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let
|
||||||
|
((head-vars (dl-vars-of (get rule :head))) (body-vars (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(not (and (dict? lit) (has-key? lit :neg))))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(not (dl-member-string? v body-vars))
|
||||||
|
(append! body-vars v)))
|
||||||
|
(dl-vars-of lit))))
|
||||||
|
(get rule :body))
|
||||||
|
(let
|
||||||
|
((missing (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(not (dl-member-string? v body-vars))
|
||||||
|
(not (= v "_")))
|
||||||
|
(append! missing v)))
|
||||||
|
head-vars)
|
||||||
|
(cond
|
||||||
|
((> (len missing) 0)
|
||||||
|
(str
|
||||||
|
"head variable(s) "
|
||||||
|
missing
|
||||||
|
" do not appear in any body literal"))
|
||||||
|
(else nil))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rename-anon-term
|
||||||
|
(fn
|
||||||
|
(term next-name)
|
||||||
|
(cond
|
||||||
|
((and (symbol? term) (= (symbol->string term) "_"))
|
||||||
|
(next-name))
|
||||||
|
((list? term)
|
||||||
|
(map (fn (x) (dl-rename-anon-term x next-name)) term))
|
||||||
|
(else term))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rename-anon-lit
|
||||||
|
(fn
|
||||||
|
(lit next-name)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
{:neg (dl-rename-anon-term (get lit :neg) next-name)})
|
||||||
|
((list? lit) (dl-rename-anon-term lit next-name))
|
||||||
|
(else lit))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-make-anon-renamer
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(let ((counter start))
|
||||||
|
(fn () (do (set! counter (+ counter 1))
|
||||||
|
(string->symbol (str "_anon" counter)))))))
|
||||||
|
|
||||||
|
;; Scan a rule for variables already named `_anon<N>` (which would
|
||||||
|
;; otherwise collide with the renamer's output). Returns the max N
|
||||||
|
;; seen, or 0 if none. The renamer then starts at that max + 1, so
|
||||||
|
;; freshly-introduced anonymous names can't shadow a user-written
|
||||||
|
;; `_anon<N>` symbol.
|
||||||
|
(define
|
||||||
|
dl-max-anon-num
|
||||||
|
(fn
|
||||||
|
(term acc)
|
||||||
|
(cond
|
||||||
|
((symbol? term)
|
||||||
|
(let ((s (symbol->string term)))
|
||||||
|
(cond
|
||||||
|
((and (>= (len s) 6) (= (slice s 0 5) "_anon"))
|
||||||
|
(let ((n (dl-try-parse-int (slice s 5 (len s)))))
|
||||||
|
(cond
|
||||||
|
((nil? n) acc)
|
||||||
|
((> n acc) n)
|
||||||
|
(else acc))))
|
||||||
|
(else acc))))
|
||||||
|
((dict? term)
|
||||||
|
(cond
|
||||||
|
((has-key? term :neg)
|
||||||
|
(dl-max-anon-num (get term :neg) acc))
|
||||||
|
(else acc)))
|
||||||
|
((list? term) (dl-max-anon-num-list term acc 0))
|
||||||
|
(else acc))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-max-anon-num-list
|
||||||
|
(fn
|
||||||
|
(xs acc i)
|
||||||
|
(cond
|
||||||
|
((>= i (len xs)) acc)
|
||||||
|
(else
|
||||||
|
(dl-max-anon-num-list xs (dl-max-anon-num (nth xs i) acc) (+ i 1))))))
|
||||||
|
|
||||||
|
;; Cheap "is this string a decimal int" check. Returns the number or
|
||||||
|
;; nil. Avoids relying on host parse-number, which on non-int strings
|
||||||
|
;; might raise rather than return nil.
|
||||||
|
(define
|
||||||
|
dl-try-parse-int
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(cond
|
||||||
|
((= (len s) 0) nil)
|
||||||
|
((not (dl-all-digits? s 0 (len s))) nil)
|
||||||
|
(else (parse-number s)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-all-digits?
|
||||||
|
(fn
|
||||||
|
(s i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) true)
|
||||||
|
((let ((c (slice s i (+ i 1))))
|
||||||
|
(not (and (>= c "0") (<= c "9"))))
|
||||||
|
false)
|
||||||
|
(else (dl-all-digits? s (+ i 1) n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rename-anon-rule
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let
|
||||||
|
((start (dl-max-anon-num (get rule :head)
|
||||||
|
(dl-max-anon-num-list (get rule :body) 0 0))))
|
||||||
|
(let ((next-name (dl-make-anon-renamer start)))
|
||||||
|
{:head (dl-rename-anon-term (get rule :head) next-name)
|
||||||
|
:body (map (fn (lit) (dl-rename-anon-lit lit next-name))
|
||||||
|
(get rule :body))}))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-add-rule!
|
||||||
|
(fn
|
||||||
|
(db rule)
|
||||||
|
(cond
|
||||||
|
((not (dict? rule))
|
||||||
|
(error (str "dl-add-rule!: expected rule dict, got " rule)))
|
||||||
|
((not (has-key? rule :head))
|
||||||
|
(error (str "dl-add-rule!: rule missing :head, got " rule)))
|
||||||
|
((not (and (list? (get rule :head))
|
||||||
|
(> (len (get rule :head)) 0)
|
||||||
|
(symbol? (first (get rule :head)))))
|
||||||
|
(error (str "dl-add-rule!: head must be a non-empty list "
|
||||||
|
"starting with a relation-name symbol, got "
|
||||||
|
(get rule :head))))
|
||||||
|
((not (dl-args-simple? (get rule :head) 1 (len (get rule :head))))
|
||||||
|
(error (str "dl-add-rule!: rule head args must be variables or "
|
||||||
|
"constants — compound terms (e.g. `(*(X, 2))`) are "
|
||||||
|
"not legal in head position; introduce an `is`-bound "
|
||||||
|
"intermediate in the body. got " (get rule :head))))
|
||||||
|
((not (list? (if (has-key? rule :body) (get rule :body) (list))))
|
||||||
|
(error (str "dl-add-rule!: body must be a list of literals, got "
|
||||||
|
(get rule :body))))
|
||||||
|
((dl-reserved-rel? (dl-rel-name (get rule :head)))
|
||||||
|
(error (str "dl-add-rule!: '" (dl-rel-name (get rule :head))
|
||||||
|
"' is a reserved name (built-in / aggregate / negation)")))
|
||||||
|
(else
|
||||||
|
(let ((rule (dl-rename-anon-rule rule)))
|
||||||
|
(let
|
||||||
|
((err (dl-rule-check-safety rule)))
|
||||||
|
(cond
|
||||||
|
((not (nil? err)) (error (str "dl-add-rule!: " err)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((rules (get db :rules)))
|
||||||
|
(do (append! rules rule) true))))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-add-clause!
|
||||||
|
(fn
|
||||||
|
(db clause)
|
||||||
|
(cond
|
||||||
|
((has-key? clause :query) false)
|
||||||
|
((and (has-key? clause :body) (= (len (get clause :body)) 0))
|
||||||
|
(dl-add-fact! db (get clause :head)))
|
||||||
|
(else (dl-add-rule! db clause)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-load-program!
|
||||||
|
(fn
|
||||||
|
(db source)
|
||||||
|
(let
|
||||||
|
((clauses (dl-parse source)))
|
||||||
|
(do (for-each (fn (c) (dl-add-clause! db c)) clauses) db))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-program
|
||||||
|
(fn (source) (let ((db (dl-make-db))) (dl-load-program! db source))))
|
||||||
|
|
||||||
|
(define dl-rules (fn (db) (get db :rules)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-fact-count
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((facts (get db :facts)) (total 0))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn (k) (set! total (+ total (len (get facts k)))))
|
||||||
|
(keys facts))
|
||||||
|
total))))
|
||||||
|
|
||||||
|
;; Returns {<rel-name>: tuple-count} for debugging. Includes
|
||||||
|
;; relations with any tuples plus all rule-head relations (so empty
|
||||||
|
;; IDB shows as 0). Skips empty EDB-only entries that are placeholders
|
||||||
|
;; from internal `dl-ensure-rel!` calls.
|
||||||
|
(define
|
||||||
|
dl-summary
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((facts (get db :facts))
|
||||||
|
(out {})
|
||||||
|
(rule-heads (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let ((h (dl-rel-name (get rule :head))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? h)) (not (dl-member-string? h rule-heads)))
|
||||||
|
(append! rule-heads h))))
|
||||||
|
(dl-rules db))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(let ((c (len (get facts k))))
|
||||||
|
(when
|
||||||
|
(or (> c 0) (dl-member-string? k rule-heads))
|
||||||
|
(dict-set! out k c))))
|
||||||
|
(keys facts))
|
||||||
|
;; Add rule heads that have no facts (yet).
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(when (not (has-key? out k)) (dict-set! out k 0)))
|
||||||
|
rule-heads)
|
||||||
|
out))))
|
||||||
162
lib/datalog/demo.sx
Normal file
162
lib/datalog/demo.sx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
;; lib/datalog/demo.sx — example programs over rose-ash-shaped data.
|
||||||
|
;;
|
||||||
|
;; Phase 10 prototypes Datalog as a rose-ash query language. Wiring
|
||||||
|
;; the EDB to actual PostgreSQL is out of scope for this loop (it
|
||||||
|
;; would touch service code outside lib/datalog/), but the programs
|
||||||
|
;; below show the shape of queries we want, and the test suite runs
|
||||||
|
;; them against synthetic in-memory tuples loaded via dl-program-data.
|
||||||
|
;;
|
||||||
|
;; Seven thematic demos:
|
||||||
|
;;
|
||||||
|
;; 1. Federation — follow graph, transitive reach, mutuals, FOAF.
|
||||||
|
;; 2. Content — posts, tags, likes, popularity, "for you" feed.
|
||||||
|
;; 3. Permissions — group membership and resource access.
|
||||||
|
;; 4. Cooking-posts — canonical "posts about cooking by people I
|
||||||
|
;; follow (transitively)" multi-domain query.
|
||||||
|
;; 5. Tag co-occurrence — distinct (T1, T2) pairs with counts.
|
||||||
|
;; 6. Shortest path — weighted-DAG path enumeration + min agg.
|
||||||
|
;; 7. Org chart — transitive subordinate + headcount per mgr.
|
||||||
|
|
||||||
|
;; ── Demo 1: federation follow graph ─────────────────────────────
|
||||||
|
;; EDB: (follows ACTOR-A ACTOR-B) — A follows B.
|
||||||
|
;; IDB:
|
||||||
|
;; (mutual A B) — A follows B and B follows A
|
||||||
|
;; (reachable A B) — transitive follow closure
|
||||||
|
;; (foaf A C) — friend of a friend (mutual filter)
|
||||||
|
(define
|
||||||
|
dl-demo-federation-rules
|
||||||
|
(quote
|
||||||
|
((mutual A B <- (follows A B) (follows B A))
|
||||||
|
(reachable A B <- (follows A B))
|
||||||
|
(reachable A C <- (follows A B) (reachable B C))
|
||||||
|
(foaf A C <- (follows A B) (follows B C) (!= A C)))))
|
||||||
|
|
||||||
|
;; ── Demo 2: content recommendation ──────────────────────────────
|
||||||
|
;; EDB:
|
||||||
|
;; (authored ACTOR POST)
|
||||||
|
;; (tagged POST TAG)
|
||||||
|
;; (liked ACTOR POST)
|
||||||
|
;; IDB:
|
||||||
|
;; (post-likes POST N) — count of likes per post
|
||||||
|
;; (popular POST) — posts with >= 3 likes
|
||||||
|
;; (tagged-by-mutual ACTOR POST) — post tagged TOPIC by someone
|
||||||
|
;; A's mutuals follow.
|
||||||
|
(define
|
||||||
|
dl-demo-content-rules
|
||||||
|
(quote
|
||||||
|
((post-likes P N <- (authored Author P) (count N L (liked L P)))
|
||||||
|
(popular P <- (authored Author P) (post-likes P N) (>= N 3))
|
||||||
|
(interesting Me P
|
||||||
|
<-
|
||||||
|
(follows Me Buddy)
|
||||||
|
(authored Buddy P)
|
||||||
|
(popular P)))))
|
||||||
|
|
||||||
|
;; ── Demo 3: role-based permissions ──────────────────────────────
|
||||||
|
;; EDB:
|
||||||
|
;; (member ACTOR GROUP)
|
||||||
|
;; (subgroup CHILD PARENT)
|
||||||
|
;; (allowed GROUP RESOURCE)
|
||||||
|
;; IDB:
|
||||||
|
;; (in-group ACTOR GROUP) — direct or via subgroup chain
|
||||||
|
;; (can-access ACTOR RESOURCE) — actor inherits group permission
|
||||||
|
(define
|
||||||
|
dl-demo-perm-rules
|
||||||
|
(quote
|
||||||
|
((in-group A G <- (member A G))
|
||||||
|
(in-group A G <- (member A H) (subgroup-trans H G))
|
||||||
|
(subgroup-trans X Y <- (subgroup X Y))
|
||||||
|
(subgroup-trans X Z <- (subgroup X Y) (subgroup-trans Y Z))
|
||||||
|
(can-access A R <- (in-group A G) (allowed G R)))))
|
||||||
|
|
||||||
|
;; ── Demo 4: cooking-posts (the canonical Phase 10 query) ────────
|
||||||
|
;; "Posts about cooking by people I follow (transitively)."
|
||||||
|
;; Combines federation (follows + transitive reach), authoring,
|
||||||
|
;; tagging — the rose-ash multi-domain join.
|
||||||
|
;;
|
||||||
|
;; EDB:
|
||||||
|
;; (follows ACTOR-A ACTOR-B)
|
||||||
|
;; (authored ACTOR POST)
|
||||||
|
;; (tagged POST TAG)
|
||||||
|
(define
|
||||||
|
dl-demo-cooking-rules
|
||||||
|
(quote
|
||||||
|
((reach Me Them <- (follows Me Them))
|
||||||
|
(reach Me Them <- (follows Me X) (reach X Them))
|
||||||
|
(cooking-post-by-network Me P
|
||||||
|
<-
|
||||||
|
(reach Me Author)
|
||||||
|
(authored Author P)
|
||||||
|
(tagged P cooking)))))
|
||||||
|
|
||||||
|
;; ── Demo 5: tag co-occurrence ───────────────────────────────────
|
||||||
|
;; "Posts tagged with both T1 AND T2." Useful for narrowed-down
|
||||||
|
;; recommendations like "vegetarian cooking" posts.
|
||||||
|
;;
|
||||||
|
;; EDB:
|
||||||
|
;; (tagged POST TAG)
|
||||||
|
;; IDB:
|
||||||
|
;; (cotagged POST T1 T2) — post has both T1 and T2 (T1 != T2)
|
||||||
|
;; (popular-pair T1 T2 N) — count of posts cotagged (T1, T2)
|
||||||
|
(define
|
||||||
|
dl-demo-tag-cooccur-rules
|
||||||
|
(quote
|
||||||
|
((cotagged P T1 T2 <- (tagged P T1) (tagged P T2) (!= T1 T2))
|
||||||
|
;; Distinct (T1, T2) pairs that occur somewhere.
|
||||||
|
(tag-pair T1 T2 <- (cotagged P T1 T2))
|
||||||
|
(tag-pair-count T1 T2 N
|
||||||
|
<-
|
||||||
|
(tag-pair T1 T2)
|
||||||
|
(count N P (cotagged P T1 T2))))))
|
||||||
|
|
||||||
|
;; ── Demo 6: weighted-DAG shortest path ─────────────────────────
|
||||||
|
;; "What's the cheapest way from X to Y?" Edge weights with `is`
|
||||||
|
;; arithmetic to sum costs, then `min` aggregation to pick the
|
||||||
|
;; shortest. Termination requires the graph to be a DAG (cycles
|
||||||
|
;; would produce infinite distances without a bound; programs
|
||||||
|
;; built on this should add a depth filter `(<, D, MAX)` if cycles
|
||||||
|
;; are possible).
|
||||||
|
;;
|
||||||
|
;; EDB:
|
||||||
|
;; (edge FROM TO COST)
|
||||||
|
;; IDB:
|
||||||
|
;; (path FROM TO COST) — any path
|
||||||
|
;; (shortest FROM TO COST) — minimum cost path
|
||||||
|
(define
|
||||||
|
dl-demo-shortest-path-rules
|
||||||
|
(quote
|
||||||
|
((path X Y W <- (edge X Y W))
|
||||||
|
(path X Z W
|
||||||
|
<-
|
||||||
|
(edge X Y W1)
|
||||||
|
(path Y Z W2)
|
||||||
|
(is W (+ W1 W2)))
|
||||||
|
(shortest X Y W <- (path X Y _) (min W C (path X Y C))))))
|
||||||
|
|
||||||
|
;; ── Demo 7: org chart + transitive headcount ───────────────────
|
||||||
|
;; Manager graph: each employee has a single manager. Compute the
|
||||||
|
;; transitive subordinate set and headcount per manager.
|
||||||
|
;;
|
||||||
|
;; EDB:
|
||||||
|
;; (manager EMP MGR) — EMP reports directly to MGR
|
||||||
|
;; IDB:
|
||||||
|
;; (subordinate MGR EMP) — EMP is in MGR's subtree
|
||||||
|
;; (headcount MGR N) — number of subordinates under MGR
|
||||||
|
(define
|
||||||
|
dl-demo-org-rules
|
||||||
|
(quote
|
||||||
|
((subordinate Mgr Emp <- (manager Emp Mgr))
|
||||||
|
(subordinate Mgr Emp
|
||||||
|
<- (manager Mid Mgr) (subordinate Mid Emp))
|
||||||
|
(headcount Mgr N
|
||||||
|
<- (subordinate Mgr Anyone) (count N E (subordinate Mgr E))))))
|
||||||
|
|
||||||
|
;; ── Loader stub ──────────────────────────────────────────────────
|
||||||
|
;; Wiring to PostgreSQL would replace these helpers with calls into
|
||||||
|
;; rose-ash's internal HTTP RPC (fetch_data → /internal/data/...).
|
||||||
|
;; The shape returned by dl-load-from-edb! is the same in either case.
|
||||||
|
(define
|
||||||
|
dl-demo-make
|
||||||
|
(fn
|
||||||
|
(facts rules)
|
||||||
|
(dl-program-data facts rules)))
|
||||||
512
lib/datalog/eval.sx
Normal file
512
lib/datalog/eval.sx
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
;; lib/datalog/eval.sx — fixpoint evaluator (naive + semi-naive).
|
||||||
|
;;
|
||||||
|
;; Two saturators are exposed:
|
||||||
|
;; `dl-saturate-naive!` — re-joins each rule against the full DB every
|
||||||
|
;; iteration. Reference implementation; useful for differential tests.
|
||||||
|
;; `dl-saturate!` — semi-naive default. Tracks per-relation delta
|
||||||
|
;; sets and substitutes one positive body literal per rule with the
|
||||||
|
;; delta of its relation, joining the rest against the previous-
|
||||||
|
;; iteration DB. Same fixpoint, dramatically less work on recursive
|
||||||
|
;; rules.
|
||||||
|
;;
|
||||||
|
;; Body literal kinds:
|
||||||
|
;; positive (rel arg ... arg) → match against EDB+IDB tuples
|
||||||
|
;; built-in (< X Y), (is X e) → constraint via dl-eval-builtin
|
||||||
|
;; negation {:neg lit} → Phase 7
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-match-positive
|
||||||
|
(fn
|
||||||
|
(lit db subst)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)) (results (list)))
|
||||||
|
(cond
|
||||||
|
((nil? rel) (error (str "dl-match-positive: bad literal " lit)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
;; If the first argument walks to a non-variable (constant
|
||||||
|
;; or already-bound var), use the first-arg index for
|
||||||
|
;; this relation. Otherwise scan the full tuple list.
|
||||||
|
((tuples
|
||||||
|
(cond
|
||||||
|
((>= (len lit) 2)
|
||||||
|
(let ((walked (dl-walk (nth lit 1) subst)))
|
||||||
|
(cond
|
||||||
|
((dl-var? walked) (dl-rel-tuples db rel))
|
||||||
|
(else (dl-index-lookup db rel walked)))))
|
||||||
|
(else (dl-rel-tuples db rel)))))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(tuple)
|
||||||
|
(let
|
||||||
|
((s (dl-unify lit tuple subst)))
|
||||||
|
(when (not (nil? s)) (append! results s))))
|
||||||
|
tuples)
|
||||||
|
results)))))))
|
||||||
|
|
||||||
|
;; Match a positive literal against the delta set for its relation only.
|
||||||
|
(define
|
||||||
|
dl-match-positive-delta
|
||||||
|
(fn
|
||||||
|
(lit delta subst)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)) (results (list)))
|
||||||
|
(let
|
||||||
|
((tuples (if (has-key? delta rel) (get delta rel) (list))))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(tuple)
|
||||||
|
(let
|
||||||
|
((s (dl-unify lit tuple subst)))
|
||||||
|
(when (not (nil? s)) (append! results s))))
|
||||||
|
tuples)
|
||||||
|
results)))))
|
||||||
|
|
||||||
|
;; Naive matcher (for dl-saturate-naive! and dl-query post-saturation).
|
||||||
|
(define
|
||||||
|
dl-match-negation
|
||||||
|
(fn
|
||||||
|
(inner db subst)
|
||||||
|
(let
|
||||||
|
((walked (dl-apply-subst inner subst))
|
||||||
|
(matches (dl-match-positive inner db subst)))
|
||||||
|
(cond
|
||||||
|
((= (len matches) 0) (list subst))
|
||||||
|
(else (list))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-match-lit
|
||||||
|
(fn
|
||||||
|
(lit db subst)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-match-negation (get lit :neg) db subst))
|
||||||
|
((dl-aggregate? lit) (dl-eval-aggregate lit db subst))
|
||||||
|
((dl-builtin? lit)
|
||||||
|
(let
|
||||||
|
((s (dl-eval-builtin lit subst)))
|
||||||
|
(if (nil? s) (list) (list s))))
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(dl-match-positive lit db subst))
|
||||||
|
(else (error (str "datalog: unknown body-literal shape: " lit))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-find-bindings
|
||||||
|
(fn (lits db subst) (dl-fb-aux lits db subst 0 (len lits))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-fb-aux
|
||||||
|
(fn
|
||||||
|
(lits db subst i n)
|
||||||
|
(cond
|
||||||
|
((nil? subst) (list))
|
||||||
|
((>= i n) (list subst))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((options (dl-match-lit (nth lits i) db subst))
|
||||||
|
(results (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(for-each
|
||||||
|
(fn (s2) (append! results s2))
|
||||||
|
(dl-fb-aux lits db s (+ i 1) n)))
|
||||||
|
options)
|
||||||
|
results))))))
|
||||||
|
|
||||||
|
;; Naive: apply each rule against full DB until no new tuples.
|
||||||
|
(define
|
||||||
|
dl-apply-rule!
|
||||||
|
(fn
|
||||||
|
(db rule)
|
||||||
|
(let
|
||||||
|
((head (get rule :head)) (body (get rule :body)) (new? false))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((derived (dl-apply-subst head s)))
|
||||||
|
(when (dl-add-derived! db derived) (set! new? true))))
|
||||||
|
(dl-find-bindings body db (dl-empty-subst)))
|
||||||
|
new?))))
|
||||||
|
|
||||||
|
;; Returns true iff one more saturation step would derive no new
|
||||||
|
;; tuples (i.e. the db is at fixpoint). Useful in tests that want
|
||||||
|
;; to assert "no work left" after a saturation call. Works under
|
||||||
|
;; either saturator since both compute the same fixpoint.
|
||||||
|
(define
|
||||||
|
dl-saturated?
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let ((any-new false))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(when (not any-new)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let ((derived (dl-apply-subst (get rule :head) s)))
|
||||||
|
(when
|
||||||
|
(and (not any-new)
|
||||||
|
(not (dl-tuple-member?
|
||||||
|
derived
|
||||||
|
(dl-rel-tuples
|
||||||
|
db (dl-rel-name derived)))))
|
||||||
|
(set! any-new true))))
|
||||||
|
(dl-find-bindings (get rule :body) db (dl-empty-subst)))))
|
||||||
|
(dl-rules db))
|
||||||
|
(not any-new)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-saturate-naive!
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((changed true))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-snloop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
changed
|
||||||
|
(do
|
||||||
|
(set! changed false)
|
||||||
|
(for-each
|
||||||
|
(fn (r) (when (dl-apply-rule! db r) (set! changed true)))
|
||||||
|
(dl-rules db))
|
||||||
|
(dl-snloop)))))
|
||||||
|
(dl-snloop)
|
||||||
|
db))))
|
||||||
|
|
||||||
|
;; ── Semi-naive ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Take a snapshot dict {rel -> tuples} of every relation currently in
|
||||||
|
;; the DB. Used as initial delta for the first iteration.
|
||||||
|
(define
|
||||||
|
dl-snapshot-facts
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((facts (get db :facts)) (out {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn (k) (dict-set! out k (dl-copy-list (get facts k))))
|
||||||
|
(keys facts))
|
||||||
|
out))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-copy-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(let
|
||||||
|
((out (list)))
|
||||||
|
(do (for-each (fn (x) (append! out x)) xs) out))))
|
||||||
|
|
||||||
|
;; Does any relation in `delta` have ≥1 tuple?
|
||||||
|
(define
|
||||||
|
dl-delta-empty?
|
||||||
|
(fn
|
||||||
|
(delta)
|
||||||
|
(let
|
||||||
|
((ks (keys delta)) (any-non-empty false))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(when
|
||||||
|
(> (len (get delta k)) 0)
|
||||||
|
(set! any-non-empty true)))
|
||||||
|
ks)
|
||||||
|
(not any-non-empty)))))
|
||||||
|
|
||||||
|
;; Find substitutions such that `lits` are all satisfied AND `delta-idx`
|
||||||
|
;; is matched against the per-relation delta only. The other positive
|
||||||
|
;; literals match against the snapshot DB (db.facts read at iteration
|
||||||
|
;; start). Built-ins and negations behave as in `dl-match-lit`.
|
||||||
|
(define
|
||||||
|
dl-find-bindings-semi
|
||||||
|
(fn
|
||||||
|
(lits db delta delta-idx subst)
|
||||||
|
(dl-fbs-aux lits db delta delta-idx 0 subst)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-fbs-aux
|
||||||
|
(fn
|
||||||
|
(lits db delta delta-idx i subst)
|
||||||
|
(cond
|
||||||
|
((nil? subst) (list))
|
||||||
|
((>= i (len lits)) (list subst))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((lit (nth lits i))
|
||||||
|
(options
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-match-negation (get lit :neg) db subst))
|
||||||
|
((dl-aggregate? lit) (dl-eval-aggregate lit db subst))
|
||||||
|
((dl-builtin? lit)
|
||||||
|
(let
|
||||||
|
((s (dl-eval-builtin lit subst)))
|
||||||
|
(if (nil? s) (list) (list s))))
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(if
|
||||||
|
(= i delta-idx)
|
||||||
|
(dl-match-positive-delta lit delta subst)
|
||||||
|
(dl-match-positive lit db subst)))
|
||||||
|
(else (error (str "datalog: unknown body-lit: " lit)))))
|
||||||
|
(results (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(for-each
|
||||||
|
(fn (s2) (append! results s2))
|
||||||
|
(dl-fbs-aux lits db delta delta-idx (+ i 1) s)))
|
||||||
|
options)
|
||||||
|
results))))))
|
||||||
|
|
||||||
|
;; Collect candidate head tuples from a rule using delta. Walks every
|
||||||
|
;; positive body position and unions the resulting heads. For rules
|
||||||
|
;; with no positive body literal, falls back to a naive single-pass
|
||||||
|
;; (so static facts like `(p X) :- (= X 5).` derive on iteration 1).
|
||||||
|
(define
|
||||||
|
dl-collect-rule-candidates
|
||||||
|
(fn
|
||||||
|
(rule db delta)
|
||||||
|
(let
|
||||||
|
((head (get rule :head))
|
||||||
|
(body (get rule :body))
|
||||||
|
(out (list))
|
||||||
|
(saw-pos false))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-cri
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i (len body))
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((lit (nth body i)))
|
||||||
|
(when
|
||||||
|
(dl-positive-lit? lit)
|
||||||
|
(do
|
||||||
|
(set! saw-pos true)
|
||||||
|
(for-each
|
||||||
|
(fn (s) (append! out (dl-apply-subst head s)))
|
||||||
|
(dl-find-bindings-semi
|
||||||
|
body
|
||||||
|
db
|
||||||
|
delta
|
||||||
|
i
|
||||||
|
(dl-empty-subst))))))
|
||||||
|
(dl-cri (+ i 1))))))
|
||||||
|
(dl-cri 0)
|
||||||
|
(when
|
||||||
|
(not saw-pos)
|
||||||
|
(for-each
|
||||||
|
(fn (s) (append! out (dl-apply-subst head s)))
|
||||||
|
(dl-find-bindings body db (dl-empty-subst))))
|
||||||
|
out))))
|
||||||
|
|
||||||
|
;; Add a list of candidate tuples to db; collect newly-added ones into
|
||||||
|
;; the new-delta dict (keyed by relation name).
|
||||||
|
(define
|
||||||
|
dl-commit-candidates!
|
||||||
|
(fn
|
||||||
|
(db candidates new-delta)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(when
|
||||||
|
(dl-add-derived! db lit)
|
||||||
|
(let
|
||||||
|
((rel (dl-rel-name lit)))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? new-delta rel))
|
||||||
|
(dict-set! new-delta rel (list)))
|
||||||
|
(append! (get new-delta rel) lit)))))
|
||||||
|
candidates)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-saturate-rules!
|
||||||
|
(fn
|
||||||
|
(db rules)
|
||||||
|
(let
|
||||||
|
((delta (dl-snapshot-facts db)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-sr-step
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((pending (list)) (new-delta {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(for-each
|
||||||
|
(fn (cand) (append! pending cand))
|
||||||
|
(dl-collect-rule-candidates rule db delta)))
|
||||||
|
rules)
|
||||||
|
(dl-commit-candidates! db pending new-delta)
|
||||||
|
(cond
|
||||||
|
((dl-delta-empty? new-delta) nil)
|
||||||
|
(else (do (set! delta new-delta) (dl-sr-step))))))))
|
||||||
|
(dl-sr-step)
|
||||||
|
db))))
|
||||||
|
|
||||||
|
;; Stratified driver: rejects non-stratifiable programs at saturation
|
||||||
|
;; time, then iterates strata in increasing order, running semi-naive on
|
||||||
|
;; the rules whose head sits in that stratum.
|
||||||
|
(define
|
||||||
|
dl-saturate!
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((err (dl-check-stratifiable db)))
|
||||||
|
(cond
|
||||||
|
((not (nil? err)) (error (str "dl-saturate!: " err)))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((strata (dl-compute-strata db)))
|
||||||
|
(let
|
||||||
|
((grouped (dl-group-rules-by-stratum db strata)))
|
||||||
|
(let
|
||||||
|
((groups (get grouped :groups))
|
||||||
|
(max-s (get grouped :max)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-strat-loop
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(when
|
||||||
|
(<= s max-s)
|
||||||
|
(let
|
||||||
|
((sk (str s)))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(has-key? groups sk)
|
||||||
|
(dl-saturate-rules! db (get groups sk)))
|
||||||
|
(dl-strat-loop (+ s 1)))))))
|
||||||
|
(dl-strat-loop 0)
|
||||||
|
db)))))))))
|
||||||
|
|
||||||
|
;; ── Querying ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Coerce a query argument to a list of body literals. A single literal
|
||||||
|
;; like `(p X)` (positive — head is a symbol) or `{:neg ...}` becomes
|
||||||
|
;; `((p X))`. A list of literals like `((p X) (q X))` is returned as-is.
|
||||||
|
(define
|
||||||
|
dl-query-coerce
|
||||||
|
(fn
|
||||||
|
(goal)
|
||||||
|
(cond
|
||||||
|
((and (dict? goal) (has-key? goal :neg)) (list goal))
|
||||||
|
((and (list? goal) (> (len goal) 0) (symbol? (first goal)))
|
||||||
|
(list goal))
|
||||||
|
((list? goal) goal)
|
||||||
|
(else (error (str "dl-query: unrecognised goal shape: " goal))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-query
|
||||||
|
(fn
|
||||||
|
(db goal)
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
;; Rename anonymous '_' vars in each goal literal so multiple
|
||||||
|
;; occurrences do not unify together. Keep the user-facing var
|
||||||
|
;; list (taken before renaming) so projected results retain user
|
||||||
|
;; names.
|
||||||
|
(let
|
||||||
|
((goals (dl-query-coerce goal))
|
||||||
|
;; Start the renamer past any `_anon<N>` symbols the user
|
||||||
|
;; may have written in the query — avoids collision.
|
||||||
|
(renamer
|
||||||
|
(dl-make-anon-renamer (dl-max-anon-num-list goal 0 0))))
|
||||||
|
(let
|
||||||
|
((user-vars (dl-query-user-vars goals))
|
||||||
|
(renamed (map (fn (g) (dl-rename-anon-lit g renamer)) goals)))
|
||||||
|
(let
|
||||||
|
((substs (dl-find-bindings renamed db (dl-empty-subst)))
|
||||||
|
(results (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((proj (dl-project-subst s user-vars)))
|
||||||
|
(when
|
||||||
|
(not (dl-tuple-member? proj results))
|
||||||
|
(append! results proj))))
|
||||||
|
substs)
|
||||||
|
results)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-query-user-vars
|
||||||
|
(fn
|
||||||
|
(goals)
|
||||||
|
(let ((seen (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(g)
|
||||||
|
(cond
|
||||||
|
((and (dict? g) (has-key? g :neg))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(and (not (= v "_")) (not (dl-member-string? v seen)))
|
||||||
|
(append! seen v)))
|
||||||
|
(dl-vars-of (get g :neg))))
|
||||||
|
((dl-aggregate? g)
|
||||||
|
;; Only the result var (first arg of the aggregate
|
||||||
|
;; literal) is user-facing. The aggregated var and
|
||||||
|
;; any vars in the inner goal are internal.
|
||||||
|
(let ((r (nth g 1)))
|
||||||
|
(when
|
||||||
|
(dl-var? r)
|
||||||
|
(let ((rn (symbol->string r)))
|
||||||
|
(when
|
||||||
|
(and (not (= rn "_"))
|
||||||
|
(not (dl-member-string? rn seen)))
|
||||||
|
(append! seen rn))))))
|
||||||
|
(else
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(when
|
||||||
|
(and (not (= v "_")) (not (dl-member-string? v seen)))
|
||||||
|
(append! seen v)))
|
||||||
|
(dl-vars-of g)))))
|
||||||
|
goals)
|
||||||
|
seen))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-project-subst
|
||||||
|
(fn
|
||||||
|
(subst names)
|
||||||
|
(let
|
||||||
|
((out {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(n)
|
||||||
|
(let
|
||||||
|
((sym (string->symbol n)))
|
||||||
|
(let
|
||||||
|
((v (dl-walk sym subst)))
|
||||||
|
(dict-set! out n (dl-apply-subst v subst)))))
|
||||||
|
names)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
(define dl-relation (fn (db name) (dl-rel-tuples db name)))
|
||||||
464
lib/datalog/magic.sx
Normal file
464
lib/datalog/magic.sx
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
;; lib/datalog/magic.sx — adornment analysis + sideways info passing.
|
||||||
|
;;
|
||||||
|
;; First step of the magic-sets transformation (Phase 6). Right now
|
||||||
|
;; the saturator does not consume these — they are introspection
|
||||||
|
;; helpers that future magic-set rewriting will build on top of.
|
||||||
|
;;
|
||||||
|
;; Definitions:
|
||||||
|
;; - An *adornment* of an n-ary literal is an n-character string
|
||||||
|
;; of "b" (bound — value already known at the call site) and
|
||||||
|
;; "f" (free — to be derived).
|
||||||
|
;; - SIPS (Sideways Information Passing Strategy) walks the body
|
||||||
|
;; of an adorned rule left-to-right tracking which variables
|
||||||
|
;; have been bound so far, computing each body literal's
|
||||||
|
;; adornment in turn.
|
||||||
|
;;
|
||||||
|
;; Usage:
|
||||||
|
;;
|
||||||
|
;; (dl-adorn-goal '(ancestor tom X))
|
||||||
|
;; => "bf"
|
||||||
|
;;
|
||||||
|
;; (dl-rule-sips
|
||||||
|
;; {:head (ancestor X Z)
|
||||||
|
;; :body ((parent X Y) (ancestor Y Z))}
|
||||||
|
;; "bf")
|
||||||
|
;; => ({:lit (parent X Y) :adornment "bf"}
|
||||||
|
;; {:lit (ancestor Y Z) :adornment "bf"})
|
||||||
|
|
||||||
|
;; Per-arg adornment under the current bound-var name set.
|
||||||
|
(define
|
||||||
|
dl-adorn-arg
|
||||||
|
(fn
|
||||||
|
(arg bound)
|
||||||
|
(cond
|
||||||
|
((dl-var? arg)
|
||||||
|
(if (dl-member-string? (symbol->string arg) bound) "b" "f"))
|
||||||
|
(else "b"))))
|
||||||
|
|
||||||
|
;; Adornment for the args of a literal (after the relation name).
|
||||||
|
(define
|
||||||
|
dl-adorn-args
|
||||||
|
(fn
|
||||||
|
(args bound)
|
||||||
|
(cond
|
||||||
|
((= (len args) 0) "")
|
||||||
|
(else
|
||||||
|
(str
|
||||||
|
(dl-adorn-arg (first args) bound)
|
||||||
|
(dl-adorn-args (rest args) bound))))))
|
||||||
|
|
||||||
|
;; Adornment of a top-level goal under the empty bound-var set.
|
||||||
|
(define
|
||||||
|
dl-adorn-goal
|
||||||
|
(fn (goal) (dl-adorn-args (rest goal) (list))))
|
||||||
|
|
||||||
|
;; Adornment of a literal under an explicit bound set.
|
||||||
|
(define
|
||||||
|
dl-adorn-lit
|
||||||
|
(fn (lit bound) (dl-adorn-args (rest lit) bound)))
|
||||||
|
|
||||||
|
;; The set of variable names made bound by walking a positive
|
||||||
|
;; literal whose adornment is known. Free positions add their
|
||||||
|
;; vars to the bound set.
|
||||||
|
(define
|
||||||
|
dl-vars-bound-by-lit
|
||||||
|
(fn
|
||||||
|
(lit bound)
|
||||||
|
(let ((args (rest lit)) (out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn (a)
|
||||||
|
(when
|
||||||
|
(and (dl-var? a)
|
||||||
|
(not (dl-member-string? (symbol->string a) bound))
|
||||||
|
(not (dl-member-string? (symbol->string a) out)))
|
||||||
|
(append! out (symbol->string a))))
|
||||||
|
args)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
;; Walk the rule body left-to-right tracking bound vars seeded by the
|
||||||
|
;; head adornment. Returns a list of {:lit :adornment} entries.
|
||||||
|
;;
|
||||||
|
;; Negation, comparison, and built-ins are passed through with their
|
||||||
|
;; adornment computed from the current bound set; they don't add new
|
||||||
|
;; bindings (except `is`, which binds its left arg if a var). Aggregates
|
||||||
|
;; are treated like is — the result var becomes bound.
|
||||||
|
(define
|
||||||
|
dl-init-head-bound
|
||||||
|
(fn
|
||||||
|
(head adornment)
|
||||||
|
(let ((args (rest head)) (out (list)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-ihb-loop
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i (len args))
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((c (slice adornment i (+ i 1)))
|
||||||
|
(a (nth args i)))
|
||||||
|
(when
|
||||||
|
(and (= c "b") (dl-var? a))
|
||||||
|
(let ((n (symbol->string a)))
|
||||||
|
(when
|
||||||
|
(not (dl-member-string? n out))
|
||||||
|
(append! out n)))))
|
||||||
|
(dl-ihb-loop (+ i 1))))))
|
||||||
|
(dl-ihb-loop 0)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rule-sips
|
||||||
|
(fn
|
||||||
|
(rule head-adornment)
|
||||||
|
(let
|
||||||
|
((bound (dl-init-head-bound (get rule :head) head-adornment))
|
||||||
|
(out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(let ((target (get lit :neg)))
|
||||||
|
(append!
|
||||||
|
out
|
||||||
|
{:lit lit :adornment (dl-adorn-lit target bound)})))
|
||||||
|
((dl-builtin? lit)
|
||||||
|
(let ((adn (dl-adorn-lit lit bound)))
|
||||||
|
(do
|
||||||
|
(append! out {:lit lit :adornment adn})
|
||||||
|
;; `is` binds its left arg (if var) once RHS is ground.
|
||||||
|
(when
|
||||||
|
(and (= (dl-rel-name lit) "is") (dl-var? (nth lit 1)))
|
||||||
|
(let ((n (symbol->string (nth lit 1))))
|
||||||
|
(when
|
||||||
|
(not (dl-member-string? n bound))
|
||||||
|
(append! bound n)))))))
|
||||||
|
((and (list? lit) (dl-aggregate? lit))
|
||||||
|
(let ((adn (dl-adorn-lit lit bound)))
|
||||||
|
(do
|
||||||
|
(append! out {:lit lit :adornment adn})
|
||||||
|
;; Result var (first arg) becomes bound.
|
||||||
|
(when (dl-var? (nth lit 1))
|
||||||
|
(let ((n (symbol->string (nth lit 1))))
|
||||||
|
(when
|
||||||
|
(not (dl-member-string? n bound))
|
||||||
|
(append! bound n)))))))
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(let ((adn (dl-adorn-lit lit bound)))
|
||||||
|
(do
|
||||||
|
(append! out {:lit lit :adornment adn})
|
||||||
|
(for-each
|
||||||
|
(fn (n)
|
||||||
|
(when (not (dl-member-string? n bound))
|
||||||
|
(append! bound n)))
|
||||||
|
(dl-vars-bound-by-lit lit bound)))))))
|
||||||
|
(get rule :body))
|
||||||
|
out))))
|
||||||
|
|
||||||
|
;; ── Magic predicate naming + bound-args extraction ─────────────
|
||||||
|
;; These are building blocks for the magic-sets *transformation*
|
||||||
|
;; itself. The transformation (which generates rewritten rules
|
||||||
|
;; with magic_<rel>^<adornment> filters) is future work — for now
|
||||||
|
;; these helpers can be used to inspect what such a transformation
|
||||||
|
;; would produce.
|
||||||
|
|
||||||
|
;; "magic_p^bf" given relation "p" and adornment "bf".
|
||||||
|
(define
|
||||||
|
dl-magic-rel-name
|
||||||
|
(fn (rel adornment) (str "magic_" rel "^" adornment)))
|
||||||
|
|
||||||
|
;; A magic predicate literal:
|
||||||
|
;; (magic_<rel>^<adornment> arg1 arg2 ...)
|
||||||
|
(define
|
||||||
|
dl-magic-lit
|
||||||
|
(fn
|
||||||
|
(rel adornment bound-args)
|
||||||
|
(cons (string->symbol (dl-magic-rel-name rel adornment)) bound-args)))
|
||||||
|
|
||||||
|
;; Extract bound args (those at "b" positions in `adornment`) from a
|
||||||
|
;; literal `(rel arg1 arg2 ... argN)`. Returns the list of arg values.
|
||||||
|
(define
|
||||||
|
dl-bound-args
|
||||||
|
(fn
|
||||||
|
(lit adornment)
|
||||||
|
(let ((args (rest lit)) (out (list)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-ba-loop
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i (len args))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(= (slice adornment i (+ i 1)) "b")
|
||||||
|
(append! out (nth args i)))
|
||||||
|
(dl-ba-loop (+ i 1))))))
|
||||||
|
(dl-ba-loop 0)
|
||||||
|
out))))
|
||||||
|
|
||||||
|
;; ── Magic-sets rewriter ─────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; Given the original rule list and a query (rel, adornment) pair,
|
||||||
|
;; generates the magic-rewritten program: a list of rules that
|
||||||
|
;; (a) gate each original rule with a `magic_<rel>^<adn>` filter and
|
||||||
|
;; (b) propagate the magic relation through SIPS so that only
|
||||||
|
;; query-relevant tuples are derived. Seed facts are returned
|
||||||
|
;; separately and must be added to the db at evaluation time.
|
||||||
|
;;
|
||||||
|
;; Output: {:rules <rewritten-rules> :seed <magic-seed-literal>}
|
||||||
|
;;
|
||||||
|
;; The rewriter only rewrites IDB rules; EDB facts pass through.
|
||||||
|
;; Built-in predicates and negation in body literals are kept in
|
||||||
|
;; place but do not generate propagation rules of their own.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-magic-pair-key
|
||||||
|
(fn (rel adornment) (str rel "^" adornment)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-magic-rewrite
|
||||||
|
(fn
|
||||||
|
(rules query-rel query-adornment query-args)
|
||||||
|
(let
|
||||||
|
((seen (list))
|
||||||
|
(queue (list))
|
||||||
|
(out (list)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-mq-mark!
|
||||||
|
(fn
|
||||||
|
(rel adornment)
|
||||||
|
(let ((k (dl-magic-pair-key rel adornment)))
|
||||||
|
(when
|
||||||
|
(not (dl-member-string? k seen))
|
||||||
|
(do
|
||||||
|
(append! seen k)
|
||||||
|
(append! queue {:rel rel :adn adornment}))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mq-rewrite-rule!
|
||||||
|
(fn
|
||||||
|
(rule adn)
|
||||||
|
(let
|
||||||
|
((head (get rule :head))
|
||||||
|
(body (get rule :body))
|
||||||
|
(sips (dl-rule-sips rule adn)))
|
||||||
|
(let
|
||||||
|
((magic-filter
|
||||||
|
(dl-magic-lit
|
||||||
|
(dl-rel-name head)
|
||||||
|
adn
|
||||||
|
(dl-bound-args head adn))))
|
||||||
|
(do
|
||||||
|
;; Adorned rule: head :- magic-filter, body...
|
||||||
|
(let ((new-body (list)))
|
||||||
|
(do
|
||||||
|
(append! new-body magic-filter)
|
||||||
|
(for-each
|
||||||
|
(fn (lit) (append! new-body lit))
|
||||||
|
body)
|
||||||
|
(append! out {:head head :body new-body})))
|
||||||
|
;; Propagation rules for each positive non-builtin
|
||||||
|
;; body literal at position i.
|
||||||
|
(define
|
||||||
|
dl-mq-prop-loop
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i (len body))
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((lit (nth body i))
|
||||||
|
(sip-entry (nth sips i)))
|
||||||
|
(when
|
||||||
|
(and (list? lit)
|
||||||
|
(> (len lit) 0)
|
||||||
|
(not (and (dict? lit) (has-key? lit :neg)))
|
||||||
|
(not (dl-builtin? lit))
|
||||||
|
(not (dl-aggregate? lit)))
|
||||||
|
(let
|
||||||
|
((lit-adn (get sip-entry :adornment))
|
||||||
|
(lit-rel (dl-rel-name lit)))
|
||||||
|
(let
|
||||||
|
((prop-head
|
||||||
|
(dl-magic-lit
|
||||||
|
lit-rel
|
||||||
|
lit-adn
|
||||||
|
(dl-bound-args lit lit-adn))))
|
||||||
|
(let ((prop-body (list)))
|
||||||
|
(do
|
||||||
|
(append! prop-body magic-filter)
|
||||||
|
(define
|
||||||
|
dl-mq-prefix-loop
|
||||||
|
(fn
|
||||||
|
(j)
|
||||||
|
(when
|
||||||
|
(< j i)
|
||||||
|
(do
|
||||||
|
(append!
|
||||||
|
prop-body
|
||||||
|
(nth body j))
|
||||||
|
(dl-mq-prefix-loop (+ j 1))))))
|
||||||
|
(dl-mq-prefix-loop 0)
|
||||||
|
(append!
|
||||||
|
out
|
||||||
|
{:head prop-head :body prop-body})
|
||||||
|
(dl-mq-mark! lit-rel lit-adn)))))))
|
||||||
|
(dl-mq-prop-loop (+ i 1))))))
|
||||||
|
(dl-mq-prop-loop 0))))))
|
||||||
|
|
||||||
|
(dl-mq-mark! query-rel query-adornment)
|
||||||
|
|
||||||
|
(let ((idx 0))
|
||||||
|
(define
|
||||||
|
dl-mq-process
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(< idx (len queue))
|
||||||
|
(let ((item (nth queue idx)))
|
||||||
|
(do
|
||||||
|
(set! idx (+ idx 1))
|
||||||
|
(let
|
||||||
|
((rel (get item :rel)) (adn (get item :adn)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(when
|
||||||
|
(= (dl-rel-name (get rule :head)) rel)
|
||||||
|
(dl-mq-rewrite-rule! rule adn)))
|
||||||
|
rules))
|
||||||
|
(dl-mq-process))))))
|
||||||
|
(dl-mq-process))
|
||||||
|
|
||||||
|
{:rules out
|
||||||
|
:seed
|
||||||
|
(dl-magic-lit
|
||||||
|
query-rel
|
||||||
|
query-adornment
|
||||||
|
query-args)}))))
|
||||||
|
|
||||||
|
;; ── Top-level magic-sets driver ─────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; (dl-magic-query db query-goal) — run `query-goal` under magic-sets
|
||||||
|
;; evaluation. Builds a fresh internal db with:
|
||||||
|
;; - the caller's EDB facts (relations not headed by any rule),
|
||||||
|
;; - the magic seed fact, and
|
||||||
|
;; - the rewritten rules.
|
||||||
|
;; Saturates and queries, returning the substitution list. The
|
||||||
|
;; caller's db is untouched.
|
||||||
|
;;
|
||||||
|
;; Useful primarily as a perf alternative for queries that only
|
||||||
|
;; need a small slice of a recursive relation. Equivalent to
|
||||||
|
;; dl-query for any single fully-stratifiable program.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-magic-rule-heads
|
||||||
|
(fn
|
||||||
|
(rules)
|
||||||
|
(let ((seen (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(r)
|
||||||
|
(let ((h (dl-rel-name (get r :head))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? h)) (not (dl-member-string? h seen)))
|
||||||
|
(append! seen h))))
|
||||||
|
rules)
|
||||||
|
seen))))
|
||||||
|
|
||||||
|
;; True iff any rule's body contains a literal kind that the magic
|
||||||
|
;; rewriter doesn't propagate magic to — i.e. an aggregate or a
|
||||||
|
;; negation. Used by dl-magic-query to decide whether to pre-saturate
|
||||||
|
;; the source db (for correctness on stratified programs) or skip
|
||||||
|
;; that step (preserving full magic-sets efficiency for pure
|
||||||
|
;; positive programs).
|
||||||
|
(define
|
||||||
|
dl-rule-has-nonprop-lit?
|
||||||
|
(fn
|
||||||
|
(body i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) false)
|
||||||
|
((let ((lit (nth body i)))
|
||||||
|
(or (and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-aggregate? lit)))
|
||||||
|
true)
|
||||||
|
(else (dl-rule-has-nonprop-lit? body (+ i 1) n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-rules-need-presaturation?
|
||||||
|
(fn
|
||||||
|
(rules)
|
||||||
|
(cond
|
||||||
|
((= (len rules) 0) false)
|
||||||
|
((let ((body (get (first rules) :body)))
|
||||||
|
(dl-rule-has-nonprop-lit? body 0 (len body)))
|
||||||
|
true)
|
||||||
|
(else (dl-rules-need-presaturation? (rest rules))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-magic-query
|
||||||
|
(fn
|
||||||
|
(db query-goal)
|
||||||
|
;; Magic-sets only applies to positive non-builtin / non-aggregate
|
||||||
|
;; literals against rule-defined relations. For other goal shapes
|
||||||
|
;; (built-ins, aggregates, EDB-only relations) the seed is either
|
||||||
|
;; non-ground or unused; fall back to dl-query.
|
||||||
|
(cond
|
||||||
|
((not (and (list? query-goal)
|
||||||
|
(> (len query-goal) 0)
|
||||||
|
(symbol? (first query-goal))))
|
||||||
|
(error (str "dl-magic-query: goal must be a positive literal "
|
||||||
|
"(non-empty list with a symbol head), got " query-goal)))
|
||||||
|
((or (dl-builtin? query-goal)
|
||||||
|
(dl-aggregate? query-goal)
|
||||||
|
(and (dict? query-goal) (has-key? query-goal :neg)))
|
||||||
|
(dl-query db query-goal))
|
||||||
|
(else
|
||||||
|
(do
|
||||||
|
;; If the rule set has aggregates or negation, pre-saturate
|
||||||
|
;; the source db before copying facts. The magic rewriter
|
||||||
|
;; passes aggregate body lits and negated lits through
|
||||||
|
;; unchanged (no magic propagation generated for them) — so
|
||||||
|
;; if their inner-goal relation is IDB, it would be empty in
|
||||||
|
;; the magic db. Pre-saturating ensures equivalence with
|
||||||
|
;; `dl-query` for every stratified program. Pure positive
|
||||||
|
;; programs skip this and keep the full magic-sets perf win
|
||||||
|
;; from goal-directed re-derivation.
|
||||||
|
(when
|
||||||
|
(dl-rules-need-presaturation? (dl-rules db))
|
||||||
|
(dl-saturate! db))
|
||||||
|
(let
|
||||||
|
((query-rel (dl-rel-name query-goal))
|
||||||
|
(query-adn (dl-adorn-goal query-goal)))
|
||||||
|
(let
|
||||||
|
((query-args (dl-bound-args query-goal query-adn))
|
||||||
|
(rules (dl-rules db)))
|
||||||
|
(let
|
||||||
|
((rewritten (dl-magic-rewrite rules query-rel query-adn query-args))
|
||||||
|
(mdb (dl-make-db))
|
||||||
|
(rule-heads (dl-magic-rule-heads rules)))
|
||||||
|
(do
|
||||||
|
;; Copy ALL existing facts. EDB-only relations bring their
|
||||||
|
;; tuples; mixed EDB+IDB relations bring both their EDB
|
||||||
|
;; portion and any pre-saturated IDB tuples (which the
|
||||||
|
;; rewritten rules would re-derive anyway). Skipping facts
|
||||||
|
;; for rule-headed relations would leave the magic run
|
||||||
|
;; without the EDB portion of mixed relations.
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rel)
|
||||||
|
(for-each
|
||||||
|
(fn (t) (dl-add-fact! mdb t))
|
||||||
|
(dl-rel-tuples db rel)))
|
||||||
|
(keys (get db :facts)))
|
||||||
|
;; Seed + rewritten rules.
|
||||||
|
(dl-add-fact! mdb (get rewritten :seed))
|
||||||
|
(for-each (fn (r) (dl-add-rule! mdb r)) (get rewritten :rules))
|
||||||
|
(dl-query mdb query-goal))))))))))
|
||||||
252
lib/datalog/parser.sx
Normal file
252
lib/datalog/parser.sx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
;; lib/datalog/parser.sx — Datalog tokens → AST
|
||||||
|
;;
|
||||||
|
;; Output shapes:
|
||||||
|
;; Literal (positive) := (relname arg ... arg) — SX list
|
||||||
|
;; Literal (negative) := {:neg (relname arg ... arg)} — dict
|
||||||
|
;; Argument := var-symbol | atom-symbol | number | string
|
||||||
|
;; | (op-name arg ... arg) — arithmetic compound
|
||||||
|
;; Fact := {:head literal :body ()}
|
||||||
|
;; Rule := {:head literal :body (lit ... lit)}
|
||||||
|
;; Query := {:query (lit ... lit)}
|
||||||
|
;; Program := list of facts / rules / queries
|
||||||
|
;;
|
||||||
|
;; Variables and constants are both SX symbols; the evaluator dispatches
|
||||||
|
;; on first-char case ('A'..'Z' or '_' = variable, otherwise constant).
|
||||||
|
;;
|
||||||
|
;; The parser permits nested compounds in arg position to support
|
||||||
|
;; arithmetic (e.g. (is Z (+ X Y))). Safety analysis at rule-load time
|
||||||
|
;; rejects compounds whose head is not an arithmetic operator.
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-peek
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((i (get st :idx)) (tokens (get st :tokens)))
|
||||||
|
(if (< i (len tokens)) (nth tokens i) {:type "eof" :value nil :pos 0}))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-peek2
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((i (+ (get st :idx) 1)) (tokens (get st :tokens)))
|
||||||
|
(if (< i (len tokens)) (nth tokens i) {:type "eof" :value nil :pos 0}))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-advance!
|
||||||
|
(fn (st) (dict-set! st :idx (+ (get st :idx) 1))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-at?
|
||||||
|
(fn
|
||||||
|
(st type value)
|
||||||
|
(let
|
||||||
|
((t (dl-pp-peek st)))
|
||||||
|
(and
|
||||||
|
(= (get t :type) type)
|
||||||
|
(or (= value nil) (= (get t :value) value))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-error
|
||||||
|
(fn
|
||||||
|
(st msg)
|
||||||
|
(let
|
||||||
|
((t (dl-pp-peek st)))
|
||||||
|
(error
|
||||||
|
(str
|
||||||
|
"Parse error at pos "
|
||||||
|
(get t :pos)
|
||||||
|
": "
|
||||||
|
msg
|
||||||
|
" (got "
|
||||||
|
(get t :type)
|
||||||
|
" '"
|
||||||
|
(if (= (get t :value) nil) "" (get t :value))
|
||||||
|
"')")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pp-expect!
|
||||||
|
(fn
|
||||||
|
(st type value)
|
||||||
|
(let
|
||||||
|
((t (dl-pp-peek st)))
|
||||||
|
(if
|
||||||
|
(dl-pp-at? st type value)
|
||||||
|
(do (dl-pp-advance! st) t)
|
||||||
|
(dl-pp-error
|
||||||
|
st
|
||||||
|
(str "expected " type (if (= value nil) "" (str " '" value "'"))))))))
|
||||||
|
|
||||||
|
;; Argument: variable, atom, number, string, or compound (relname/op + parens).
|
||||||
|
(define
|
||||||
|
dl-pp-parse-arg
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((t (dl-pp-peek st)))
|
||||||
|
(let
|
||||||
|
((ty (get t :type)) (vv (get t :value)))
|
||||||
|
(cond
|
||||||
|
((= ty "number") (do (dl-pp-advance! st) vv))
|
||||||
|
((= ty "string") (do (dl-pp-advance! st) vv))
|
||||||
|
((= ty "var") (do (dl-pp-advance! st) (string->symbol vv)))
|
||||||
|
;; Negative numeric literal: `-` op directly followed by a
|
||||||
|
;; number (no `(`) is parsed as a single negative number.
|
||||||
|
;; This keeps `(-X Y)` (compound) and `-N` (literal) distinct.
|
||||||
|
((and (= ty "op") (= vv "-")
|
||||||
|
(= (get (dl-pp-peek2 st) :type) "number"))
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((n (get (dl-pp-peek st) :value)))
|
||||||
|
(do (dl-pp-advance! st) (- 0 n)))))
|
||||||
|
((or (= ty "atom") (= ty "op"))
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(if
|
||||||
|
(dl-pp-at? st "punct" "(")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((args (dl-pp-parse-arg-list st)))
|
||||||
|
(do
|
||||||
|
(dl-pp-expect! st "punct" ")")
|
||||||
|
(cons (string->symbol vv) args))))
|
||||||
|
(string->symbol vv))))
|
||||||
|
(else (dl-pp-error st "expected term")))))))
|
||||||
|
|
||||||
|
;; Comma-separated args inside parens.
|
||||||
|
(define
|
||||||
|
dl-pp-parse-arg-list
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((args (list)))
|
||||||
|
(do
|
||||||
|
(append! args (dl-pp-parse-arg st))
|
||||||
|
(define
|
||||||
|
dl-pp-arg-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(dl-pp-at? st "punct" ",")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(append! args (dl-pp-parse-arg st))
|
||||||
|
(dl-pp-arg-loop)))))
|
||||||
|
(dl-pp-arg-loop)
|
||||||
|
args))))
|
||||||
|
|
||||||
|
;; A positive literal: relname (atom or op) followed by optional (args).
|
||||||
|
(define
|
||||||
|
dl-pp-parse-positive
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((t (dl-pp-peek st)))
|
||||||
|
(let
|
||||||
|
((ty (get t :type)) (vv (get t :value)))
|
||||||
|
(if
|
||||||
|
(or (= ty "atom") (= ty "op"))
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(if
|
||||||
|
(dl-pp-at? st "punct" "(")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((args (dl-pp-parse-arg-list st)))
|
||||||
|
(do
|
||||||
|
(dl-pp-expect! st "punct" ")")
|
||||||
|
(cons (string->symbol vv) args))))
|
||||||
|
(list (string->symbol vv))))
|
||||||
|
(dl-pp-error st "expected literal head"))))))
|
||||||
|
|
||||||
|
;; A body literal: positive, or not(positive).
|
||||||
|
(define
|
||||||
|
dl-pp-parse-body-lit
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((t1 (dl-pp-peek st)) (t2 (dl-pp-peek2 st)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(= (get t1 :type) "atom")
|
||||||
|
(= (get t1 :value) "not")
|
||||||
|
(= (get t2 :type) "punct")
|
||||||
|
(= (get t2 :value) "("))
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((inner (dl-pp-parse-positive st)))
|
||||||
|
(do (dl-pp-expect! st "punct" ")") {:neg inner})))
|
||||||
|
(dl-pp-parse-positive st)))))
|
||||||
|
|
||||||
|
;; Comma-separated body literals.
|
||||||
|
(define
|
||||||
|
dl-pp-parse-body
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(let
|
||||||
|
((lits (list)))
|
||||||
|
(do
|
||||||
|
(append! lits (dl-pp-parse-body-lit st))
|
||||||
|
(define
|
||||||
|
dl-pp-body-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(dl-pp-at? st "punct" ",")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(append! lits (dl-pp-parse-body-lit st))
|
||||||
|
(dl-pp-body-loop)))))
|
||||||
|
(dl-pp-body-loop)
|
||||||
|
lits))))
|
||||||
|
|
||||||
|
;; Single clause: fact, rule, or query. Consumes trailing dot.
|
||||||
|
(define
|
||||||
|
dl-pp-parse-clause
|
||||||
|
(fn
|
||||||
|
(st)
|
||||||
|
(cond
|
||||||
|
((dl-pp-at? st "op" "?-")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((body (dl-pp-parse-body st)))
|
||||||
|
(do (dl-pp-expect! st "punct" ".") {:query body}))))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((head (dl-pp-parse-positive st)))
|
||||||
|
(cond
|
||||||
|
((dl-pp-at? st "op" ":-")
|
||||||
|
(do
|
||||||
|
(dl-pp-advance! st)
|
||||||
|
(let
|
||||||
|
((body (dl-pp-parse-body st)))
|
||||||
|
(do (dl-pp-expect! st "punct" ".") {:body body :head head}))))
|
||||||
|
(else (do (dl-pp-expect! st "punct" ".") {:body (list) :head head}))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-parse-program
|
||||||
|
(fn
|
||||||
|
(tokens)
|
||||||
|
(let
|
||||||
|
((st {:tokens tokens :idx 0}) (clauses (list)))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-pp-prog-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(not (dl-pp-at? st "eof" nil))
|
||||||
|
(do
|
||||||
|
(append! clauses (dl-pp-parse-clause st))
|
||||||
|
(dl-pp-prog-loop)))))
|
||||||
|
(dl-pp-prog-loop)
|
||||||
|
clauses))))
|
||||||
|
|
||||||
|
(define dl-parse (fn (src) (dl-parse-program (dl-tokenize src))))
|
||||||
20
lib/datalog/scoreboard.json
Normal file
20
lib/datalog/scoreboard.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"lang": "datalog",
|
||||||
|
"total_passed": 276,
|
||||||
|
"total_failed": 0,
|
||||||
|
"total": 276,
|
||||||
|
"suites": [
|
||||||
|
{"name":"tokenize","passed":31,"failed":0,"total":31},
|
||||||
|
{"name":"parse","passed":23,"failed":0,"total":23},
|
||||||
|
{"name":"unify","passed":29,"failed":0,"total":29},
|
||||||
|
{"name":"eval","passed":44,"failed":0,"total":44},
|
||||||
|
{"name":"builtins","passed":26,"failed":0,"total":26},
|
||||||
|
{"name":"semi_naive","passed":8,"failed":0,"total":8},
|
||||||
|
{"name":"negation","passed":12,"failed":0,"total":12},
|
||||||
|
{"name":"aggregates","passed":23,"failed":0,"total":23},
|
||||||
|
{"name":"api","passed":22,"failed":0,"total":22},
|
||||||
|
{"name":"magic","passed":37,"failed":0,"total":37},
|
||||||
|
{"name":"demo","passed":21,"failed":0,"total":21}
|
||||||
|
],
|
||||||
|
"generated": "2026-05-11T09:40:12+00:00"
|
||||||
|
}
|
||||||
17
lib/datalog/scoreboard.md
Normal file
17
lib/datalog/scoreboard.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# datalog scoreboard
|
||||||
|
|
||||||
|
**276 / 276 passing** (0 failure(s)).
|
||||||
|
|
||||||
|
| Suite | Passed | Total | Status |
|
||||||
|
|-------|--------|-------|--------|
|
||||||
|
| tokenize | 31 | 31 | ok |
|
||||||
|
| parse | 23 | 23 | ok |
|
||||||
|
| unify | 29 | 29 | ok |
|
||||||
|
| eval | 44 | 44 | ok |
|
||||||
|
| builtins | 26 | 26 | ok |
|
||||||
|
| semi_naive | 8 | 8 | ok |
|
||||||
|
| negation | 12 | 12 | ok |
|
||||||
|
| aggregates | 23 | 23 | ok |
|
||||||
|
| api | 22 | 22 | ok |
|
||||||
|
| magic | 37 | 37 | ok |
|
||||||
|
| demo | 21 | 21 | ok |
|
||||||
323
lib/datalog/strata.sx
Normal file
323
lib/datalog/strata.sx
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
;; lib/datalog/strata.sx — dependency graph, SCC analysis, stratum assignment.
|
||||||
|
;;
|
||||||
|
;; A program is stratifiable iff no cycle in its dependency graph passes
|
||||||
|
;; through a negative edge. The stratum of relation R is the depth at which
|
||||||
|
;; R can first be evaluated:
|
||||||
|
;;
|
||||||
|
;; stratum(R) = max over edges (R → S) of:
|
||||||
|
;; stratum(S) if the edge is positive
|
||||||
|
;; stratum(S) + 1 if the edge is negative
|
||||||
|
;;
|
||||||
|
;; All relations in the same SCC share a stratum (and the SCC must have only
|
||||||
|
;; positive internal edges, else the program is non-stratifiable).
|
||||||
|
|
||||||
|
;; Build dep graph: dict {head-rel-name -> ({:rel str :neg bool} ...)}.
|
||||||
|
(define
|
||||||
|
dl-build-dep-graph
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let ((g {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let
|
||||||
|
((head-rel (dl-rel-name (get rule :head))))
|
||||||
|
(when
|
||||||
|
(not (nil? head-rel))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? g head-rel))
|
||||||
|
(dict-set! g head-rel (list)))
|
||||||
|
(let ((existing (get g head-rel)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((dl-aggregate? lit)
|
||||||
|
(let
|
||||||
|
((edge (dl-aggregate-dep-edge lit)))
|
||||||
|
(when
|
||||||
|
(not (nil? edge))
|
||||||
|
(append! existing edge))))
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((target
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-rel-name (get lit :neg)))
|
||||||
|
((dl-builtin? lit) nil)
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(dl-rel-name lit))
|
||||||
|
(else nil)))
|
||||||
|
(neg?
|
||||||
|
(and (dict? lit) (has-key? lit :neg))))
|
||||||
|
(when
|
||||||
|
(not (nil? target))
|
||||||
|
(append!
|
||||||
|
existing
|
||||||
|
{:rel target :neg neg?}))))))
|
||||||
|
(get rule :body)))))))
|
||||||
|
(dl-rules db))
|
||||||
|
g))))
|
||||||
|
|
||||||
|
;; All relations referenced — heads of rules + EDB names + body relations.
|
||||||
|
(define
|
||||||
|
dl-all-relations
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let ((seen (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(when (not (dl-member-string? k seen)) (append! seen k)))
|
||||||
|
(keys (get db :facts)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(do
|
||||||
|
(let ((h (dl-rel-name (get rule :head))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? h)) (not (dl-member-string? h seen)))
|
||||||
|
(append! seen h)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(let
|
||||||
|
((t
|
||||||
|
(cond
|
||||||
|
((dl-aggregate? lit)
|
||||||
|
(let ((edge (dl-aggregate-dep-edge lit)))
|
||||||
|
(if (nil? edge) nil (get edge :rel))))
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(dl-rel-name (get lit :neg)))
|
||||||
|
((dl-builtin? lit) nil)
|
||||||
|
((and (list? lit) (> (len lit) 0))
|
||||||
|
(dl-rel-name lit))
|
||||||
|
(else nil))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? t)) (not (dl-member-string? t seen)))
|
||||||
|
(append! seen t))))
|
||||||
|
(get rule :body))))
|
||||||
|
(dl-rules db))
|
||||||
|
seen))))
|
||||||
|
|
||||||
|
;; reach: dict {from: dict {to: edge-info}} where edge-info is
|
||||||
|
;; {:any bool :neg bool}
|
||||||
|
;; meaning "any path from `from` to `to`" and "exists a negative-passing
|
||||||
|
;; path from `from` to `to`".
|
||||||
|
;;
|
||||||
|
;; Floyd-Warshall over the dep graph. The 'neg' flag propagates through
|
||||||
|
;; concatenation: if any edge along the path is negative, the path's
|
||||||
|
;; flag is true.
|
||||||
|
(define
|
||||||
|
dl-build-reach
|
||||||
|
(fn
|
||||||
|
(graph nodes)
|
||||||
|
(let ((reach {}))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn (n) (dict-set! reach n {}))
|
||||||
|
nodes)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(head)
|
||||||
|
(when
|
||||||
|
(has-key? graph head)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(edge)
|
||||||
|
(let
|
||||||
|
((target (get edge :rel)) (n (get edge :neg)))
|
||||||
|
(let ((row (get reach head)))
|
||||||
|
(cond
|
||||||
|
((has-key? row target)
|
||||||
|
(let ((cur (get row target)))
|
||||||
|
(dict-set!
|
||||||
|
row
|
||||||
|
target
|
||||||
|
{:any true :neg (or n (get cur :neg))})))
|
||||||
|
(else
|
||||||
|
(dict-set! row target {:any true :neg n}))))))
|
||||||
|
(get graph head))))
|
||||||
|
nodes)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(let ((row-i (get reach i)))
|
||||||
|
(when
|
||||||
|
(has-key? row-i k)
|
||||||
|
(let ((ik (get row-i k)) (row-k (get reach k)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(j)
|
||||||
|
(when
|
||||||
|
(has-key? row-k j)
|
||||||
|
(let ((kj (get row-k j)))
|
||||||
|
(let
|
||||||
|
((combined-neg (or (get ik :neg) (get kj :neg))))
|
||||||
|
(cond
|
||||||
|
((has-key? row-i j)
|
||||||
|
(let ((cur (get row-i j)))
|
||||||
|
(dict-set!
|
||||||
|
row-i
|
||||||
|
j
|
||||||
|
{:any true
|
||||||
|
:neg (or combined-neg (get cur :neg))})))
|
||||||
|
(else
|
||||||
|
(dict-set!
|
||||||
|
row-i
|
||||||
|
j
|
||||||
|
{:any true :neg combined-neg})))))))
|
||||||
|
nodes)))))
|
||||||
|
nodes))
|
||||||
|
nodes)
|
||||||
|
reach))))
|
||||||
|
|
||||||
|
;; Returns nil on success, or error message string on failure.
|
||||||
|
(define
|
||||||
|
dl-check-stratifiable
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((graph (dl-build-dep-graph db))
|
||||||
|
(nodes (dl-all-relations db)))
|
||||||
|
(let ((reach (dl-build-reach graph nodes)) (err nil))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(when
|
||||||
|
(nil? err)
|
||||||
|
(let ((head-rel (dl-rel-name (get rule :head))))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(lit)
|
||||||
|
(cond
|
||||||
|
((and (dict? lit) (has-key? lit :neg))
|
||||||
|
(let ((tgt (dl-rel-name (get lit :neg))))
|
||||||
|
(when
|
||||||
|
(and (not (nil? tgt))
|
||||||
|
(dl-reach-cycle? reach head-rel tgt))
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str "non-stratifiable: relation " head-rel
|
||||||
|
" transitively depends through negation on "
|
||||||
|
tgt
|
||||||
|
" which depends back on " head-rel)))))
|
||||||
|
((dl-aggregate? lit)
|
||||||
|
(let ((edge (dl-aggregate-dep-edge lit)))
|
||||||
|
(when
|
||||||
|
(not (nil? edge))
|
||||||
|
(let ((tgt (get edge :rel)))
|
||||||
|
(when
|
||||||
|
(and (not (nil? tgt))
|
||||||
|
(dl-reach-cycle? reach head-rel tgt))
|
||||||
|
(set!
|
||||||
|
err
|
||||||
|
(str "non-stratifiable: relation "
|
||||||
|
head-rel
|
||||||
|
" aggregates over " tgt
|
||||||
|
" which depends back on "
|
||||||
|
head-rel)))))))))
|
||||||
|
(get rule :body)))))
|
||||||
|
(dl-rules db))
|
||||||
|
err)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-reach-cycle?
|
||||||
|
(fn
|
||||||
|
(reach a b)
|
||||||
|
(and
|
||||||
|
(dl-reach-row-has? reach b a)
|
||||||
|
(dl-reach-row-has? reach a b))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-reach-row-has?
|
||||||
|
(fn
|
||||||
|
(reach from to)
|
||||||
|
(let ((row (get reach from)))
|
||||||
|
(and (not (nil? row)) (has-key? row to)))))
|
||||||
|
|
||||||
|
;; Compute stratum per relation. Iteratively propagate from EDB roots.
|
||||||
|
;; Uses the per-relation max-stratum-of-deps formula. Stops when stable.
|
||||||
|
(define
|
||||||
|
dl-compute-strata
|
||||||
|
(fn
|
||||||
|
(db)
|
||||||
|
(let
|
||||||
|
((graph (dl-build-dep-graph db))
|
||||||
|
(nodes (dl-all-relations db))
|
||||||
|
(strata {}))
|
||||||
|
(do
|
||||||
|
(for-each (fn (n) (dict-set! strata n 0)) nodes)
|
||||||
|
(let ((changed true))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-cs-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
changed
|
||||||
|
(do
|
||||||
|
(set! changed false)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(head)
|
||||||
|
(when
|
||||||
|
(has-key? graph head)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(edge)
|
||||||
|
(let
|
||||||
|
((tgt (get edge :rel))
|
||||||
|
(n (get edge :neg)))
|
||||||
|
(let
|
||||||
|
((tgt-strat
|
||||||
|
(if (has-key? strata tgt)
|
||||||
|
(get strata tgt) 0))
|
||||||
|
(cur (get strata head)))
|
||||||
|
(let
|
||||||
|
((needed
|
||||||
|
(if n (+ tgt-strat 1) tgt-strat)))
|
||||||
|
(when
|
||||||
|
(> needed cur)
|
||||||
|
(do
|
||||||
|
(dict-set! strata head needed)
|
||||||
|
(set! changed true)))))))
|
||||||
|
(get graph head))))
|
||||||
|
nodes)
|
||||||
|
(dl-cs-loop)))))
|
||||||
|
(dl-cs-loop)))
|
||||||
|
strata))))
|
||||||
|
|
||||||
|
;; Group rules by their head's stratum. Returns dict {stratum-int -> rules}.
|
||||||
|
(define
|
||||||
|
dl-group-rules-by-stratum
|
||||||
|
(fn
|
||||||
|
(db strata)
|
||||||
|
(let ((groups {}) (max-s 0))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(rule)
|
||||||
|
(let
|
||||||
|
((head-rel (dl-rel-name (get rule :head))))
|
||||||
|
(let
|
||||||
|
((s (if (has-key? strata head-rel)
|
||||||
|
(get strata head-rel) 0)))
|
||||||
|
(do
|
||||||
|
(when (> s max-s) (set! max-s s))
|
||||||
|
(let
|
||||||
|
((sk (str s)))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(not (has-key? groups sk))
|
||||||
|
(dict-set! groups sk (list)))
|
||||||
|
(append! (get groups sk) rule)))))))
|
||||||
|
(dl-rules db))
|
||||||
|
{:groups groups :max max-s}))))
|
||||||
357
lib/datalog/tests/aggregates.sx
Normal file
357
lib/datalog/tests/aggregates.sx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
;; lib/datalog/tests/aggregates.sx — count / sum / min / max.
|
||||||
|
|
||||||
|
(define dl-at-pass 0)
|
||||||
|
(define dl-at-fail 0)
|
||||||
|
(define dl-at-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-at-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let ((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-at-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-at-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-at-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i)))
|
||||||
|
(not (dl-at-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-at-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and
|
||||||
|
(= (len a) (len b))
|
||||||
|
(dl-at-subset? a b)
|
||||||
|
(dl-at-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-at-contains? ys (first xs))) false)
|
||||||
|
(else (dl-at-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-at-deep=? (first xs) target) true)
|
||||||
|
(else (dl-at-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-at-deep=? got expected)
|
||||||
|
(set! dl-at-pass (+ dl-at-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-at-fail (+ dl-at-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-at-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected: " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-at-set=? got expected)
|
||||||
|
(set! dl-at-pass (+ dl-at-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-at-fail (+ dl-at-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-at-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-throws?
|
||||||
|
(fn
|
||||||
|
(thunk)
|
||||||
|
(let
|
||||||
|
((threw false))
|
||||||
|
(do
|
||||||
|
(guard
|
||||||
|
(e (#t (set! threw true)))
|
||||||
|
(thunk))
|
||||||
|
threw))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-at-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
;; count
|
||||||
|
(dl-at-test-set! "count siblings"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(p, bob). parent(p, alice). parent(p, charlie).
|
||||||
|
sibling(X, Y) :- parent(P, X), parent(P, Y), !=(X, Y).
|
||||||
|
sib_count(N) :- count(N, S, sibling(bob, S)).")
|
||||||
|
(list (quote sib_count) (quote N)))
|
||||||
|
(list {:N 2}))
|
||||||
|
|
||||||
|
;; sum
|
||||||
|
(dl-at-test-set! "sum prices"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"price(apple, 5). price(pear, 7). price(plum, 3).
|
||||||
|
total(T) :- sum(T, X, price(F, X)).")
|
||||||
|
(list (quote total) (quote T)))
|
||||||
|
(list {:T 15}))
|
||||||
|
|
||||||
|
;; min
|
||||||
|
(dl-at-test-set! "min score"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"score(alice, 80). score(bob, 65). score(carol, 92).
|
||||||
|
lo(M) :- min(M, S, score(P, S)).")
|
||||||
|
(list (quote lo) (quote M)))
|
||||||
|
(list {:M 65}))
|
||||||
|
|
||||||
|
;; max
|
||||||
|
(dl-at-test-set! "max score"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"score(alice, 80). score(bob, 65). score(carol, 92).
|
||||||
|
hi(M) :- max(M, S, score(P, S)).")
|
||||||
|
(list (quote hi) (quote M)))
|
||||||
|
(list {:M 92}))
|
||||||
|
|
||||||
|
;; count over derived relation (stratification needed).
|
||||||
|
(dl-at-test-set! "count over derived"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(a, b). parent(a, c). parent(b, d). parent(c, e).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
|
||||||
|
num_ancestors(N) :- count(N, X, ancestor(a, X)).")
|
||||||
|
(list (quote num_ancestors) (quote N)))
|
||||||
|
(list {:N 4}))
|
||||||
|
|
||||||
|
;; count with no matches → 0.
|
||||||
|
(dl-at-test-set! "count empty"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2).
|
||||||
|
zero(N) :- count(N, X, q(X)).")
|
||||||
|
(list (quote zero) (quote N)))
|
||||||
|
(list {:N 0}))
|
||||||
|
|
||||||
|
;; sum with no matches → 0.
|
||||||
|
(dl-at-test-set! "sum empty"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2).
|
||||||
|
total(T) :- sum(T, X, q(X)).")
|
||||||
|
(list (quote total) (quote T)))
|
||||||
|
(list {:T 0}))
|
||||||
|
|
||||||
|
;; min with no matches → rule does not fire.
|
||||||
|
(dl-at-test-set! "min empty"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2).
|
||||||
|
lo(M) :- min(M, X, q(X)).")
|
||||||
|
(list (quote lo) (quote M)))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; Aggregate with comparison filter on result.
|
||||||
|
(dl-at-test-set! "popularity threshold"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"post(p1). post(p2).
|
||||||
|
liked(u1, p1). liked(u2, p1). liked(u3, p1).
|
||||||
|
liked(u1, p2). liked(u2, p2).
|
||||||
|
popular(P) :- post(P), count(N, U, liked(U, P)), >=(N, 3).")
|
||||||
|
(list (quote popular) (quote P)))
|
||||||
|
(list {:P (quote p1)}))
|
||||||
|
|
||||||
|
;; findall: collect distinct values into a list.
|
||||||
|
(dl-at-test-set! "findall over EDB"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(a). p(b). p(c).
|
||||||
|
all_p(L) :- findall(L, X, p(X)).")
|
||||||
|
(list (quote all_p) (quote L)))
|
||||||
|
(list {:L (list (quote a) (quote b) (quote c))}))
|
||||||
|
|
||||||
|
(dl-at-test-set! "findall over derived"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(a, b). parent(b, c). parent(c, d).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
|
||||||
|
desc(L) :- findall(L, X, ancestor(a, X)).")
|
||||||
|
(list (quote desc) (quote L)))
|
||||||
|
(list {:L (list (quote b) (quote c) (quote d))}))
|
||||||
|
|
||||||
|
(dl-at-test-set! "findall empty"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1).
|
||||||
|
all_q(L) :- findall(L, X, q(X)).")
|
||||||
|
(list (quote all_q) (quote L)))
|
||||||
|
(list {:L (list)}))
|
||||||
|
|
||||||
|
;; Aggregate vs single distinct.
|
||||||
|
;; Group-by via aggregate-in-rule-body. Per-user friend count
|
||||||
|
;; over a friends relation. The U var is bound by the prior
|
||||||
|
;; positive lit u(U) so the aggregate counts only U-rooted
|
||||||
|
;; friends per group.
|
||||||
|
(dl-at-test-set! "group-by per-user friend count"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"u(alice). u(bob). u(carol).
|
||||||
|
f(alice, x). f(alice, y). f(bob, x).
|
||||||
|
counts(U, N) :- u(U), count(N, X, f(U, X)).")
|
||||||
|
(list (quote counts) (quote U) (quote N)))
|
||||||
|
(list
|
||||||
|
{:U (quote alice) :N 2}
|
||||||
|
{:U (quote bob) :N 1}
|
||||||
|
{:U (quote carol) :N 0}))
|
||||||
|
|
||||||
|
;; Stratification: recursion through aggregation is rejected.
|
||||||
|
;; Aggregate validates that second arg is a variable.
|
||||||
|
(dl-at-test! "agg second arg must be var"
|
||||||
|
(dl-at-throws?
|
||||||
|
(fn () (dl-eval "p(1). q(N) :- count(N, 5, p(X))." "?- q(N).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Aggregate validates that third arg is a positive literal.
|
||||||
|
(dl-at-test! "agg third arg must be a literal"
|
||||||
|
(dl-at-throws?
|
||||||
|
(fn () (dl-eval "p(1). q(N) :- count(N, X, 42)." "?- q(N).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Aggregate validates that the agg-var (2nd arg) appears in the
|
||||||
|
;; goal. Without it every match contributes the same unbound
|
||||||
|
;; symbol — count silently returns 1, sum raises a confusing
|
||||||
|
;; "expected number" error, etc. Catch the mistake at safety
|
||||||
|
;; check time instead.
|
||||||
|
(dl-at-test! "agg-var must appear in goal"
|
||||||
|
(dl-at-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-eval
|
||||||
|
"p(1). p(2). c(N) :- count(N, Y, p(X))."
|
||||||
|
"?- c(N).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Indirect recursion through aggregation also rejected.
|
||||||
|
;; q -> r (via positive lit), r -> q (via aggregate body).
|
||||||
|
;; The aggregate edge counts as negation for stratification.
|
||||||
|
(dl-at-test! "indirect agg cycle rejected"
|
||||||
|
(dl-at-throws?
|
||||||
|
(fn ()
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(dl-add-rule! db
|
||||||
|
{:head (list (quote q) (quote N))
|
||||||
|
:body (list (list (quote r) (quote N)))})
|
||||||
|
(dl-add-rule! db
|
||||||
|
{:head (list (quote r) (quote N))
|
||||||
|
:body (list (list (quote count) (quote N) (quote X)
|
||||||
|
(list (quote q) (quote X))))})
|
||||||
|
(dl-saturate! db)))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-at-test! "agg recursion rejected"
|
||||||
|
(dl-at-throws?
|
||||||
|
(fn ()
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(dl-add-rule! db
|
||||||
|
{:head (list (quote q) (quote N))
|
||||||
|
:body (list (list (quote count) (quote N) (quote X)
|
||||||
|
(list (quote q) (quote X))))})
|
||||||
|
(dl-saturate! db)))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Negation + aggregation in the same body — different strata.
|
||||||
|
(dl-at-test-set! "neg + agg coexist"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"u(a). u(b). u(c). banned(b).
|
||||||
|
active(X) :- u(X), not(banned(X)).
|
||||||
|
cnt(N) :- count(N, X, active(X)).")
|
||||||
|
(list (quote cnt) (quote N)))
|
||||||
|
(list {:N 2}))
|
||||||
|
|
||||||
|
;; Min over a derived empty relation: no result.
|
||||||
|
(dl-at-test-set! "min over empty derived"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"s(50). s(60).
|
||||||
|
score(N) :- s(N), >(N, 100).
|
||||||
|
low(M) :- min(M, X, score(X)).")
|
||||||
|
(list (quote low) (quote M)))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; Aggregates as the top-level query goal (regression for
|
||||||
|
;; dl-match-lit aggregate dispatch and projection cleanup).
|
||||||
|
(dl-at-test-set! "count as query goal"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(1). p(2). p(3). p(4).")
|
||||||
|
(list (quote count) (quote N) (quote X) (list (quote p) (quote X))))
|
||||||
|
(list {:N 4}))
|
||||||
|
|
||||||
|
(dl-at-test-set! "findall as query goal"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(1). p(2). p(3).")
|
||||||
|
(list (quote findall) (quote L) (quote X)
|
||||||
|
(list (quote p) (quote X))))
|
||||||
|
(list {:L (list 1 2 3)}))
|
||||||
|
|
||||||
|
(dl-at-test-set! "distinct counted once"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"rated(alice, x). rated(alice, y). rated(bob, x).
|
||||||
|
rater_count(N) :- count(N, U, rated(U, F)).")
|
||||||
|
(list (quote rater_count) (quote N)))
|
||||||
|
(list {:N 2})))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-aggregates-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-at-pass 0)
|
||||||
|
(set! dl-at-fail 0)
|
||||||
|
(set! dl-at-failures (list))
|
||||||
|
(dl-at-run-all!)
|
||||||
|
{:passed dl-at-pass
|
||||||
|
:failed dl-at-fail
|
||||||
|
:total (+ dl-at-pass dl-at-fail)
|
||||||
|
:failures dl-at-failures})))
|
||||||
350
lib/datalog/tests/api.sx
Normal file
350
lib/datalog/tests/api.sx
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
;; lib/datalog/tests/api.sx — SX-data embedding API.
|
||||||
|
|
||||||
|
(define dl-api-pass 0)
|
||||||
|
(define dl-api-fail 0)
|
||||||
|
(define dl-api-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-api-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let ((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-api-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-api-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-api-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i)))
|
||||||
|
(not (dl-api-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-api-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and
|
||||||
|
(= (len a) (len b))
|
||||||
|
(dl-api-subset? a b)
|
||||||
|
(dl-api-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-api-contains? ys (first xs))) false)
|
||||||
|
(else (dl-api-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-api-deep=? (first xs) target) true)
|
||||||
|
(else (dl-api-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-api-deep=? got expected)
|
||||||
|
(set! dl-api-pass (+ dl-api-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-api-fail (+ dl-api-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-api-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected: " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-api-set=? got expected)
|
||||||
|
(set! dl-api-pass (+ dl-api-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-api-fail (+ dl-api-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-api-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
;; dl-program-data with arrow form.
|
||||||
|
(dl-api-test-set! "data API ancestor closure"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((parent tom bob) (parent bob ann) (parent ann pat)))
|
||||||
|
(quote
|
||||||
|
((ancestor X Y <- (parent X Y))
|
||||||
|
(ancestor X Z <- (parent X Y) (ancestor Y Z)))))
|
||||||
|
(quote (ancestor tom X)))
|
||||||
|
(list {:X (quote bob)} {:X (quote ann)} {:X (quote pat)}))
|
||||||
|
|
||||||
|
;; dl-program-data with dict rules.
|
||||||
|
(dl-api-test-set! "data API with dict rules"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((p a) (p b) (p c)))
|
||||||
|
(list
|
||||||
|
{:head (quote (q X)) :body (quote ((p X)))}))
|
||||||
|
(quote (q X)))
|
||||||
|
(list {:X (quote a)} {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; dl-rule helper.
|
||||||
|
(dl-api-test-set! "dl-rule constructor"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((p 1) (p 2)))
|
||||||
|
(list (dl-rule (quote (q X)) (quote ((p X))))))
|
||||||
|
(quote (q X)))
|
||||||
|
(list {:X 1} {:X 2}))
|
||||||
|
|
||||||
|
;; dl-assert! adds and re-derives.
|
||||||
|
(dl-api-test-set! "dl-assert! incremental"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((parent tom bob) (parent bob ann)))
|
||||||
|
(quote
|
||||||
|
((ancestor X Y <- (parent X Y))
|
||||||
|
(ancestor X Z <- (parent X Y) (ancestor Y Z)))))))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-assert! db (quote (parent ann pat)))
|
||||||
|
(dl-query db (quote (ancestor tom X)))))
|
||||||
|
(list {:X (quote bob)} {:X (quote ann)} {:X (quote pat)}))
|
||||||
|
|
||||||
|
;; dl-retract! removes a fact and recomputes IDB.
|
||||||
|
(dl-api-test-set! "dl-retract! removes derived"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((parent tom bob) (parent bob ann) (parent ann pat)))
|
||||||
|
(quote
|
||||||
|
((ancestor X Y <- (parent X Y))
|
||||||
|
(ancestor X Z <- (parent X Y) (ancestor Y Z)))))))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-retract! db (quote (parent bob ann)))
|
||||||
|
(dl-query db (quote (ancestor tom X)))))
|
||||||
|
(list {:X (quote bob)}))
|
||||||
|
|
||||||
|
;; dl-retract! on a relation with BOTH explicit facts AND a rule
|
||||||
|
;; (a "mixed" relation) used to wipe the EDB portion when the IDB
|
||||||
|
;; was re-derived, even when the retract didn't match anything.
|
||||||
|
;; :edb-keys provenance now preserves user-asserted facts.
|
||||||
|
(dl-api-test-set! "dl-retract! preserves EDB in mixed relation"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((p a) (p b) (q c)))
|
||||||
|
(quote ((p X <- (q X)))))))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
;; Retract a non-existent tuple — should be a no-op.
|
||||||
|
(dl-retract! db (quote (p z)))
|
||||||
|
(dl-query db (quote (p X)))))
|
||||||
|
(list {:X (quote a)} {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; And retracting an actual EDB fact in a mixed relation drops
|
||||||
|
;; only that fact; the derived portion stays.
|
||||||
|
(dl-api-test-set! "dl-retract! mixed: drop EDB, keep IDB"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((p a) (p b) (q c)))
|
||||||
|
(quote ((p X <- (q X)))))))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-retract! db (quote (p a)))
|
||||||
|
(dl-query db (quote (p X)))))
|
||||||
|
(list {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; dl-program-data + dl-query with constants in head.
|
||||||
|
(dl-api-test-set! "constant-in-head data"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((edge a b) (edge b c) (edge c a)))
|
||||||
|
(quote
|
||||||
|
((reach X Y <- (edge X Y))
|
||||||
|
(reach X Z <- (edge X Y) (reach Y Z)))))
|
||||||
|
(quote (reach a X)))
|
||||||
|
(list {:X (quote a)} {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; Assert into empty db.
|
||||||
|
(dl-api-test-set! "assert into empty"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data (list) (list))))
|
||||||
|
(do
|
||||||
|
(dl-assert! db (quote (p 1)))
|
||||||
|
(dl-assert! db (quote (p 2)))
|
||||||
|
(dl-query db (quote (p X)))))
|
||||||
|
(list {:X 1} {:X 2}))
|
||||||
|
|
||||||
|
;; Multi-goal query: pass list of literals.
|
||||||
|
(dl-api-test-set! "multi-goal query"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((p 1) (p 2) (p 3) (q 2) (q 3)))
|
||||||
|
(list))
|
||||||
|
(list (quote (p X)) (quote (q X))))
|
||||||
|
(list {:X 2} {:X 3}))
|
||||||
|
|
||||||
|
;; Multi-goal with comparison.
|
||||||
|
(dl-api-test-set! "multi-goal with comparison"
|
||||||
|
(dl-query
|
||||||
|
(dl-program-data
|
||||||
|
(quote ((n 1) (n 2) (n 3) (n 4) (n 5)))
|
||||||
|
(list))
|
||||||
|
(list (quote (n X)) (list (string->symbol ">") (quote X) 2)))
|
||||||
|
(list {:X 3} {:X 4} {:X 5}))
|
||||||
|
|
||||||
|
;; dl-eval: single-call source + query.
|
||||||
|
(dl-api-test-set! "dl-eval ancestor"
|
||||||
|
(dl-eval
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."
|
||||||
|
"?- ancestor(a, X).")
|
||||||
|
(list {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
(dl-api-test-set! "dl-eval multi-goal"
|
||||||
|
(dl-eval
|
||||||
|
"p(1). p(2). p(3). q(2). q(3)."
|
||||||
|
"?- p(X), q(X).")
|
||||||
|
(list {:X 2} {:X 3}))
|
||||||
|
|
||||||
|
;; dl-rules-of: rules with head matching a relation name.
|
||||||
|
(dl-api-test! "dl-rules-of count"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"p(1). q(X) :- p(X). r(X) :- p(X). q(2).")))
|
||||||
|
(len (dl-rules-of db "q")))
|
||||||
|
1)
|
||||||
|
|
||||||
|
(dl-api-test! "dl-rules-of empty"
|
||||||
|
(let
|
||||||
|
((db (dl-program "p(1). p(2).")))
|
||||||
|
(len (dl-rules-of db "q")))
|
||||||
|
0)
|
||||||
|
|
||||||
|
;; dl-clear-idb!: wipe rule-headed relations.
|
||||||
|
(dl-api-test! "dl-clear-idb! wipes IDB"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-clear-idb! db)
|
||||||
|
(len (dl-relation db "ancestor"))))
|
||||||
|
0)
|
||||||
|
|
||||||
|
(dl-api-test! "dl-clear-idb! preserves EDB"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).")))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-clear-idb! db)
|
||||||
|
(len (dl-relation db "parent"))))
|
||||||
|
2)
|
||||||
|
|
||||||
|
;; dl-eval-magic — routes single-goal queries through
|
||||||
|
;; magic-sets evaluation.
|
||||||
|
(dl-api-test-set! "dl-eval-magic ancestor"
|
||||||
|
(dl-eval-magic
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."
|
||||||
|
"?- ancestor(a, X).")
|
||||||
|
(list {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; Equivalence: dl-eval and dl-eval-magic produce the same
|
||||||
|
;; answers for any well-formed query (magic-sets is a perf
|
||||||
|
;; alternative, not a semantic change).
|
||||||
|
(dl-api-test! "dl-eval ≡ dl-eval-magic on ancestor"
|
||||||
|
(let
|
||||||
|
((source "parent(a, b). parent(b, c). parent(c, d).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))
|
||||||
|
(let
|
||||||
|
((semi (dl-eval source "?- ancestor(a, X)."))
|
||||||
|
(magic (dl-eval-magic source "?- ancestor(a, X).")))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Comprehensive integration: recursion + stratified negation
|
||||||
|
;; + aggregation + comparison composed in a single program.
|
||||||
|
;; (Uses _Anything as a regular var instead of `_` so the
|
||||||
|
;; outer rule binds via the reach lit.)
|
||||||
|
(dl-api-test-set! "integration"
|
||||||
|
(dl-eval
|
||||||
|
(str
|
||||||
|
"edge(a, b). edge(b, c). edge(c, d). edge(a, d). "
|
||||||
|
"banned(c). "
|
||||||
|
"reach(X, Y) :- edge(X, Y). "
|
||||||
|
"reach(X, Z) :- edge(X, Y), reach(Y, Z). "
|
||||||
|
"safe(X, Y) :- reach(X, Y), not(banned(Y)). "
|
||||||
|
"reach_count(X, N) :- reach(X, Z), count(N, Y, safe(X, Y)). "
|
||||||
|
"popular(X) :- reach_count(X, N), >=(N, 2).")
|
||||||
|
"?- popular(X).")
|
||||||
|
(list {:X (quote a)}))
|
||||||
|
|
||||||
|
;; dl-rule-from-list with no arrow → fact-style.
|
||||||
|
(dl-api-test-set! "no arrow → fact-like rule"
|
||||||
|
(let
|
||||||
|
((rule (dl-rule-from-list (quote (foo X Y)))))
|
||||||
|
(list rule))
|
||||||
|
(list {:head (quote (foo X Y)) :body (list)}))
|
||||||
|
|
||||||
|
;; dl-coerce-rule on dict passes through.
|
||||||
|
(dl-api-test-set! "coerce dict rule"
|
||||||
|
(let
|
||||||
|
((d {:head (quote (h X)) :body (quote ((b X)))}))
|
||||||
|
(list (dl-coerce-rule d)))
|
||||||
|
(list {:head (quote (h X)) :body (quote ((b X)))})))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-api-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-api-pass 0)
|
||||||
|
(set! dl-api-fail 0)
|
||||||
|
(set! dl-api-failures (list))
|
||||||
|
(dl-api-run-all!)
|
||||||
|
{:passed dl-api-pass
|
||||||
|
:failed dl-api-fail
|
||||||
|
:total (+ dl-api-pass dl-api-fail)
|
||||||
|
:failures dl-api-failures})))
|
||||||
285
lib/datalog/tests/builtins.sx
Normal file
285
lib/datalog/tests/builtins.sx
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
;; lib/datalog/tests/builtins.sx — comparison + arithmetic body literals.
|
||||||
|
|
||||||
|
(define dl-bt-pass 0)
|
||||||
|
(define dl-bt-fail 0)
|
||||||
|
(define dl-bt-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-bt-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let
|
||||||
|
((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-bt-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-bt-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-bt-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i))) (not (dl-bt-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-bt-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and (= (len a) (len b)) (dl-bt-subset? a b) (dl-bt-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-bt-contains? ys (first xs))) false)
|
||||||
|
(else (dl-bt-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-bt-deep=? (first xs) target) true)
|
||||||
|
(else (dl-bt-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-bt-set=? got expected)
|
||||||
|
(set! dl-bt-pass (+ dl-bt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-bt-fail (+ dl-bt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-bt-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): "
|
||||||
|
expected
|
||||||
|
"\n got: "
|
||||||
|
got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-bt-deep=? got expected)
|
||||||
|
(set! dl-bt-pass (+ dl-bt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-bt-fail (+ dl-bt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-bt-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-throws?
|
||||||
|
(fn
|
||||||
|
(thunk)
|
||||||
|
(let
|
||||||
|
((threw false))
|
||||||
|
(do (guard (e (#t (set! threw true))) (thunk)) threw))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-bt-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"less than filter"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"age(alice, 30). age(bob, 17). age(carol, 22).\n adult(X) :- age(X, A), >=(A, 18).")
|
||||||
|
(list (quote adult) (quote X)))
|
||||||
|
(list {:X (quote alice)} {:X (quote carol)}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"less-equal filter"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1). n(2). n(3). n(4). n(5).\n small(X) :- n(X), <=(X, 3).")
|
||||||
|
(list (quote small) (quote X)))
|
||||||
|
(list {:X 1} {:X 2} {:X 3}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"not-equal filter"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1, 2). p(2, 2). p(3, 4).\n diff(X, Y) :- p(X, Y), !=(X, Y).")
|
||||||
|
(list (quote diff) (quote X) (quote Y)))
|
||||||
|
(list {:X 1 :Y 2} {:X 3 :Y 4}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"is plus"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1). n(2). n(3).\n succ(X, Y) :- n(X), is(Y, +(X, 1)).")
|
||||||
|
(list (quote succ) (quote X) (quote Y)))
|
||||||
|
(list {:X 1 :Y 2} {:X 2 :Y 3} {:X 3 :Y 4}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"is multiply"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(2). n(3). n(4).\n square(X, Y) :- n(X), is(Y, *(X, X)).")
|
||||||
|
(list (quote square) (quote X) (quote Y)))
|
||||||
|
(list {:X 2 :Y 4} {:X 3 :Y 9} {:X 4 :Y 16}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"is nested expr"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1). n(2). n(3).\n f(X, Y) :- n(X), is(Y, *(+(X, 1), 2)).")
|
||||||
|
(list (quote f) (quote X) (quote Y)))
|
||||||
|
(list {:X 1 :Y 4} {:X 2 :Y 6} {:X 3 :Y 8}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"is bound LHS — equality"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1, 2). n(2, 5). n(3, 4).\n succ(X, Y) :- n(X, Y), is(Y, +(X, 1)).")
|
||||||
|
(list (quote succ) (quote X) (quote Y)))
|
||||||
|
(list {:X 1 :Y 2} {:X 3 :Y 4}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"triple via is"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1). n(2). n(3).\n triple(X, Y) :- n(X), is(Y, *(X, 3)).")
|
||||||
|
(list (quote triple) (quote X) (quote Y)))
|
||||||
|
(list {:X 1 :Y 3} {:X 2 :Y 6} {:X 3 :Y 9}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"= unifies var with constant"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(a). p(b).\n qual(X) :- p(X), =(X, a).")
|
||||||
|
(list (quote qual) (quote X)))
|
||||||
|
(list {:X (quote a)}))
|
||||||
|
(dl-bt-test-set!
|
||||||
|
"= unifies two vars (one bound)"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(a). p(b).\n twin(X, Y) :- p(X), =(Y, X).")
|
||||||
|
(list (quote twin) (quote X) (quote Y)))
|
||||||
|
(list {:X (quote a) :Y (quote a)} {:X (quote b) :Y (quote b)}))
|
||||||
|
(dl-bt-test!
|
||||||
|
"big count"
|
||||||
|
(let
|
||||||
|
((db (dl-program "n(0). n(1). n(2). n(3). n(4). n(5). n(6). n(7). n(8). n(9).\n big(X) :- n(X), >=(X, 5).")))
|
||||||
|
(do (dl-saturate! db) (len (dl-relation db "big"))))
|
||||||
|
5)
|
||||||
|
;; Built-in / arithmetic literals work as standalone query goals
|
||||||
|
;; (without needing a wrapper rule).
|
||||||
|
(dl-bt-test-set! "comparison-only goal true"
|
||||||
|
(dl-eval "" "?- <(1, 2).")
|
||||||
|
(list {}))
|
||||||
|
|
||||||
|
(dl-bt-test-set! "comparison-only goal false"
|
||||||
|
(dl-eval "" "?- <(2, 1).")
|
||||||
|
(list))
|
||||||
|
|
||||||
|
(dl-bt-test-set! "is goal binds"
|
||||||
|
(dl-eval "" "?- is(N, +(2, 3)).")
|
||||||
|
(list {:N 5}))
|
||||||
|
|
||||||
|
;; Bounded successor: a recursive rule with a comparison
|
||||||
|
;; guard terminates because the Herbrand base is effectively
|
||||||
|
;; bounded.
|
||||||
|
(dl-bt-test-set! "bounded successor"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"nat(0).
|
||||||
|
nat(Y) :- nat(X), is(Y, +(X, 1)), <(Y, 5).")
|
||||||
|
(list (quote nat) (quote X)))
|
||||||
|
(list {:X 0} {:X 1} {:X 2} {:X 3} {:X 4}))
|
||||||
|
|
||||||
|
(dl-bt-test!
|
||||||
|
"unsafe — comparison without binder"
|
||||||
|
(dl-bt-throws? (fn () (dl-program "p(X) :- <(X, 5).")))
|
||||||
|
true)
|
||||||
|
(dl-bt-test!
|
||||||
|
"unsafe — comparison both unbound"
|
||||||
|
(dl-bt-throws? (fn () (dl-program "p(X, Y) :- <(X, Y), q(X).")))
|
||||||
|
true)
|
||||||
|
(dl-bt-test!
|
||||||
|
"unsafe — is uses unbound RHS var"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn () (dl-program "p(X, Y) :- q(X), is(Y, +(X, Z)).")))
|
||||||
|
true)
|
||||||
|
(dl-bt-test!
|
||||||
|
"unsafe — is on its own"
|
||||||
|
(dl-bt-throws? (fn () (dl-program "p(Y) :- is(Y, +(X, 1)).")))
|
||||||
|
true)
|
||||||
|
(dl-bt-test!
|
||||||
|
"unsafe — = between two unbound"
|
||||||
|
(dl-bt-throws? (fn () (dl-program "p(X, Y) :- =(X, Y).")))
|
||||||
|
true)
|
||||||
|
(dl-bt-test!
|
||||||
|
"safe — is binds head var"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn () (dl-program "n(1). p(Y) :- n(X), is(Y, +(X, 1)).")))
|
||||||
|
false)
|
||||||
|
(dl-bt-test!
|
||||||
|
"safe — comparison after binder"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn () (dl-program "n(1). big(X) :- n(X), >=(X, 0).")))
|
||||||
|
false)
|
||||||
|
(dl-bt-test!
|
||||||
|
"safe — = binds head var"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn () (dl-program "p(a). p(b). x(Y) :- p(X), =(Y, X).")))
|
||||||
|
false)
|
||||||
|
|
||||||
|
;; Division by zero raises with a clear error. Without this guard
|
||||||
|
;; SX's `/` returned IEEE infinity, which then silently flowed
|
||||||
|
;; through comparisons and aggregations.
|
||||||
|
(dl-bt-test!
|
||||||
|
"is — division by zero raises"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-eval "p(10). q(R) :- p(X), is(R, /(X, 0))." "?- q(R).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Comparison ops `<`, `<=`, `>`, `>=` require both operands to
|
||||||
|
;; have the same primitive type. Cross-type comparisons used to
|
||||||
|
;; silently return false (for some pairs) or raise a confusing
|
||||||
|
;; host-level error (for others) — now they all raise with a
|
||||||
|
;; message that names the offending values.
|
||||||
|
(dl-bt-test!
|
||||||
|
"comparison — string vs number raises"
|
||||||
|
(dl-bt-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-eval "p(\"hello\"). q(X) :- p(X), <(X, 5)." "?- q(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; `!=` is the exception — it's a polymorphic inequality test
|
||||||
|
;; (uses dl-tuple-equal? underneath) so cross-type pairs are
|
||||||
|
;; legitimate (and trivially unequal).
|
||||||
|
(dl-bt-test-set! "!= works across types"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(\"1\"). q(X) :- p(X), !=(X, 1).")
|
||||||
|
(quote (q X)))
|
||||||
|
(list {:X "1"})))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-builtins-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-bt-pass 0)
|
||||||
|
(set! dl-bt-fail 0)
|
||||||
|
(set! dl-bt-failures (list))
|
||||||
|
(dl-bt-run-all!)
|
||||||
|
{:failures dl-bt-failures :total (+ dl-bt-pass dl-bt-fail) :passed dl-bt-pass :failed dl-bt-fail})))
|
||||||
321
lib/datalog/tests/demo.sx
Normal file
321
lib/datalog/tests/demo.sx
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
;; lib/datalog/tests/demo.sx — Phase 10 demo programs.
|
||||||
|
|
||||||
|
(define dl-demo-pass 0)
|
||||||
|
(define dl-demo-fail 0)
|
||||||
|
(define dl-demo-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-demo-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let ((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-demo-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-demo-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-demo-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i)))
|
||||||
|
(not (dl-demo-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-demo-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and
|
||||||
|
(= (len a) (len b))
|
||||||
|
(dl-demo-subset? a b)
|
||||||
|
(dl-demo-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-demo-contains? ys (first xs))) false)
|
||||||
|
(else (dl-demo-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-demo-deep=? (first xs) target) true)
|
||||||
|
(else (dl-demo-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-demo-set=? got expected)
|
||||||
|
(set! dl-demo-pass (+ dl-demo-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-demo-fail (+ dl-demo-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-demo-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
;; ── Federation ──────────────────────────────────────────
|
||||||
|
(dl-demo-test-set! "mutuals"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((follows alice bob) (follows bob alice)
|
||||||
|
(follows bob carol) (follows carol dave)))
|
||||||
|
dl-demo-federation-rules)
|
||||||
|
(quote (mutual alice X)))
|
||||||
|
(list {:X (quote bob)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "reachable transitive"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((follows alice bob) (follows bob carol) (follows carol dave)))
|
||||||
|
dl-demo-federation-rules)
|
||||||
|
(quote (reachable alice X)))
|
||||||
|
(list {:X (quote bob)} {:X (quote carol)} {:X (quote dave)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "foaf"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((follows alice bob) (follows bob carol) (follows alice dave)))
|
||||||
|
dl-demo-federation-rules)
|
||||||
|
(quote (foaf alice X)))
|
||||||
|
(list {:X (quote carol)}))
|
||||||
|
|
||||||
|
;; ── Content ─────────────────────────────────────────────
|
||||||
|
(dl-demo-test-set! "popular posts"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((authored alice p1) (authored bob p2) (authored carol p3)
|
||||||
|
(liked u1 p1) (liked u2 p1) (liked u3 p1)
|
||||||
|
(liked u1 p2)))
|
||||||
|
dl-demo-content-rules)
|
||||||
|
(quote (popular P)))
|
||||||
|
(list {:P (quote p1)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "interesting feed"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((follows me alice) (follows me bob)
|
||||||
|
(authored alice p1) (authored bob p2)
|
||||||
|
(liked u1 p1) (liked u2 p1) (liked u3 p1)
|
||||||
|
(liked u4 p2)))
|
||||||
|
dl-demo-content-rules)
|
||||||
|
(quote (interesting me P)))
|
||||||
|
(list {:P (quote p1)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "post likes count"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((authored alice p1)
|
||||||
|
(liked u1 p1) (liked u2 p1) (liked u3 p1)))
|
||||||
|
dl-demo-content-rules)
|
||||||
|
(quote (post-likes p1 N)))
|
||||||
|
(list {:N 3}))
|
||||||
|
|
||||||
|
;; ── Permissions ─────────────────────────────────────────
|
||||||
|
(dl-demo-test-set! "direct group access"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((member alice editors)
|
||||||
|
(allowed editors blog)))
|
||||||
|
dl-demo-perm-rules)
|
||||||
|
(quote (can-access X blog)))
|
||||||
|
(list {:X (quote alice)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "subgroup access"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((member bob writers)
|
||||||
|
(subgroup writers editors)
|
||||||
|
(allowed editors blog)))
|
||||||
|
dl-demo-perm-rules)
|
||||||
|
(quote (can-access X blog)))
|
||||||
|
(list {:X (quote bob)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "transitive subgroup"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((member carol drafters)
|
||||||
|
(subgroup drafters writers)
|
||||||
|
(subgroup writers editors)
|
||||||
|
(allowed editors blog)))
|
||||||
|
dl-demo-perm-rules)
|
||||||
|
(quote (can-access X blog)))
|
||||||
|
(list {:X (quote carol)}))
|
||||||
|
|
||||||
|
;; ── Cooking posts (canonical Phase 10 example) ─────────
|
||||||
|
(dl-demo-test-set! "cooking posts by network"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((follows me alice) (follows alice bob) (follows alice carol)
|
||||||
|
(authored alice p1) (authored bob p2)
|
||||||
|
(authored carol p3) (authored carol p4)
|
||||||
|
(tagged p1 travel) (tagged p2 cooking)
|
||||||
|
(tagged p3 cooking) (tagged p4 books)))
|
||||||
|
dl-demo-cooking-rules)
|
||||||
|
(quote (cooking-post-by-network me P)))
|
||||||
|
(list {:P (quote p2)} {:P (quote p3)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "cooking — direct follow only"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((follows me bob)
|
||||||
|
(authored bob p1) (authored bob p2)
|
||||||
|
(tagged p1 cooking) (tagged p2 books)))
|
||||||
|
dl-demo-cooking-rules)
|
||||||
|
(quote (cooking-post-by-network me P)))
|
||||||
|
(list {:P (quote p1)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "cooking — none in network"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((follows me bob)
|
||||||
|
(authored bob p1) (tagged p1 books)))
|
||||||
|
dl-demo-cooking-rules)
|
||||||
|
(quote (cooking-post-by-network me P)))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; ── Tag co-occurrence ──────────────────────────────────
|
||||||
|
(dl-demo-test-set! "cotagged posts"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((tagged p1 cooking) (tagged p1 vegetarian)
|
||||||
|
(tagged p2 cooking) (tagged p2 quick)
|
||||||
|
(tagged p3 vegetarian)))
|
||||||
|
dl-demo-tag-cooccur-rules)
|
||||||
|
(quote (cotagged P cooking vegetarian)))
|
||||||
|
(list {:P (quote p1)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "tag pair count"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((tagged p1 cooking) (tagged p1 vegetarian)
|
||||||
|
(tagged p2 cooking) (tagged p2 quick)
|
||||||
|
(tagged p3 cooking) (tagged p3 vegetarian)))
|
||||||
|
dl-demo-tag-cooccur-rules)
|
||||||
|
(quote (tag-pair-count cooking vegetarian N)))
|
||||||
|
(list {:N 2}))
|
||||||
|
|
||||||
|
;; ── Shortest path on a weighted DAG ──────────────────
|
||||||
|
(dl-demo-test-set! "shortest a→d via DAG"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((edge a b 5) (edge b c 3) (edge a c 10) (edge c d 2)))
|
||||||
|
dl-demo-shortest-path-rules)
|
||||||
|
(quote (shortest a d W)))
|
||||||
|
(list {:W 10}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "shortest a→c picks 2-hop"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((edge a b 5) (edge b c 3) (edge a c 10)))
|
||||||
|
dl-demo-shortest-path-rules)
|
||||||
|
(quote (shortest a c W)))
|
||||||
|
(list {:W 8}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "shortest unreachable empty"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((edge a b 5) (edge b c 3)))
|
||||||
|
dl-demo-shortest-path-rules)
|
||||||
|
(quote (shortest a d W)))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; ── Org chart + headcount ─────────────────────────────
|
||||||
|
(dl-demo-test-set! "ceo subordinate transitive"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((manager ic1 mgr1) (manager ic2 mgr1)
|
||||||
|
(manager mgr1 vp1) (manager ic3 vp1)
|
||||||
|
(manager vp1 ceo)))
|
||||||
|
dl-demo-org-rules)
|
||||||
|
(quote (subordinate ceo X)))
|
||||||
|
(list
|
||||||
|
{:X (quote vp1)} {:X (quote mgr1)} {:X (quote ic1)}
|
||||||
|
{:X (quote ic2)} {:X (quote ic3)}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "ceo headcount = 5"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((manager ic1 mgr1) (manager ic2 mgr1)
|
||||||
|
(manager mgr1 vp1) (manager ic3 vp1)
|
||||||
|
(manager vp1 ceo)))
|
||||||
|
dl-demo-org-rules)
|
||||||
|
(quote (headcount ceo N)))
|
||||||
|
(list {:N 5}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "mgr1 headcount = 2"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote
|
||||||
|
((manager ic1 mgr1) (manager ic2 mgr1)
|
||||||
|
(manager mgr1 vp1) (manager ic3 vp1)
|
||||||
|
(manager vp1 ceo)))
|
||||||
|
dl-demo-org-rules)
|
||||||
|
(quote (headcount mgr1 N)))
|
||||||
|
(list {:N 2}))
|
||||||
|
|
||||||
|
(dl-demo-test-set! "no access without grant"
|
||||||
|
(dl-query
|
||||||
|
(dl-demo-make
|
||||||
|
(quote ((member dave outsiders) (allowed editors blog)))
|
||||||
|
dl-demo-perm-rules)
|
||||||
|
(quote (can-access X blog)))
|
||||||
|
(list)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-demo-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-demo-pass 0)
|
||||||
|
(set! dl-demo-fail 0)
|
||||||
|
(set! dl-demo-failures (list))
|
||||||
|
(dl-demo-run-all!)
|
||||||
|
{:passed dl-demo-pass
|
||||||
|
:failed dl-demo-fail
|
||||||
|
:total (+ dl-demo-pass dl-demo-fail)
|
||||||
|
:failures dl-demo-failures})))
|
||||||
463
lib/datalog/tests/eval.sx
Normal file
463
lib/datalog/tests/eval.sx
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
;; lib/datalog/tests/eval.sx — naive evaluation + safety analysis tests.
|
||||||
|
|
||||||
|
(define dl-et-pass 0)
|
||||||
|
(define dl-et-fail 0)
|
||||||
|
(define dl-et-failures (list))
|
||||||
|
|
||||||
|
;; Same deep-equal helper used in other suites.
|
||||||
|
(define
|
||||||
|
dl-et-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-et-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let
|
||||||
|
((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-et-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-et-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-et-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i))) (not (dl-et-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-et-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
;; Set-equality on lists (order-independent, uses dl-et-deep=?).
|
||||||
|
(define
|
||||||
|
dl-et-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and (= (len a) (len b)) (dl-et-subset? a b) (dl-et-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-et-contains? ys (first xs))) false)
|
||||||
|
(else (dl-et-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-et-deep=? (first xs) target) true)
|
||||||
|
(else (dl-et-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-et-deep=? got expected)
|
||||||
|
(set! dl-et-pass (+ dl-et-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-et-fail (+ dl-et-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-et-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-et-set=? got expected)
|
||||||
|
(set! dl-et-pass (+ dl-et-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-et-fail (+ dl-et-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-et-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): "
|
||||||
|
expected
|
||||||
|
"\n got: "
|
||||||
|
got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-throws?
|
||||||
|
(fn
|
||||||
|
(thunk)
|
||||||
|
(let
|
||||||
|
((threw false))
|
||||||
|
(do (guard (e (#t (set! threw true))) (thunk)) threw))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-et-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-et-test-set!
|
||||||
|
"fact lookup any"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "parent(tom, bob). parent(bob, ann).")
|
||||||
|
(list (quote parent) (quote X) (quote Y)))
|
||||||
|
(list {:X (quote tom) :Y (quote bob)} {:X (quote bob) :Y (quote ann)}))
|
||||||
|
(dl-et-test-set!
|
||||||
|
"fact lookup constant arg"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "parent(tom, bob). parent(tom, liz). parent(bob, ann).")
|
||||||
|
(list (quote parent) (quote tom) (quote Y)))
|
||||||
|
(list {:Y (quote bob)} {:Y (quote liz)}))
|
||||||
|
(dl-et-test-set!
|
||||||
|
"no match"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "parent(tom, bob).")
|
||||||
|
(list (quote parent) (quote nobody) (quote X)))
|
||||||
|
(list))
|
||||||
|
(dl-et-test-set!
|
||||||
|
"ancestor closure"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(tom, bob). parent(bob, ann). parent(ann, pat).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")
|
||||||
|
(list (quote ancestor) (quote tom) (quote X)))
|
||||||
|
(list {:X (quote bob)} {:X (quote ann)} {:X (quote pat)}))
|
||||||
|
(dl-et-test-set!
|
||||||
|
"sibling"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(tom, bob). parent(tom, liz). parent(jane, bob). parent(jane, liz).\n sibling(X, Y) :- parent(P, X), parent(P, Y).")
|
||||||
|
(list (quote sibling) (quote bob) (quote Y)))
|
||||||
|
(list {:Y (quote bob)} {:Y (quote liz)}))
|
||||||
|
(dl-et-test-set!
|
||||||
|
"same-generation"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(liz, joe).\n person(tom). person(bob). person(liz). person(ann). person(joe).\n sg(X, X) :- person(X).\n sg(X, Y) :- parent(P1, X), sg(P1, P2), parent(P2, Y).")
|
||||||
|
(list (quote sg) (quote ann) (quote X)))
|
||||||
|
(list {:X (quote ann)} {:X (quote joe)}))
|
||||||
|
(dl-et-test!
|
||||||
|
"ancestor count"
|
||||||
|
(let
|
||||||
|
((db (dl-program "parent(a, b). parent(b, c). parent(c, d).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(do (dl-saturate! db) (len (dl-relation db "ancestor"))))
|
||||||
|
6)
|
||||||
|
(dl-et-test-set!
|
||||||
|
"grandparent"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"parent(a, b). parent(b, c). parent(c, d).\n grandparent(X, Z) :- parent(X, Y), parent(Y, Z).")
|
||||||
|
(list (quote grandparent) (quote X) (quote Y)))
|
||||||
|
(list {:X (quote a) :Y (quote c)} {:X (quote b) :Y (quote d)}))
|
||||||
|
(dl-et-test!
|
||||||
|
"no recursion infinite loop"
|
||||||
|
(let
|
||||||
|
((db (dl-program "edge(1, 2). edge(2, 3). edge(3, 1).\n reach(X, Y) :- edge(X, Y).\n reach(X, Z) :- edge(X, Y), reach(Y, Z).")))
|
||||||
|
(do (dl-saturate! db) (len (dl-relation db "reach"))))
|
||||||
|
9)
|
||||||
|
;; Rule-shape sanity: empty-list head and non-list body raise
|
||||||
|
;; clear errors rather than crashing inside the saturator.
|
||||||
|
(dl-et-test! "empty head rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-add-rule! (dl-make-db)
|
||||||
|
{:head (list) :body (list)})))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-et-test! "non-list body rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-add-rule! (dl-make-db)
|
||||||
|
{:head (list (quote p) (quote X)) :body 42})))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Reserved relation names rejected as rule/fact heads.
|
||||||
|
(dl-et-test!
|
||||||
|
"reserved name `not` as head rejected"
|
||||||
|
(dl-et-throws? (fn () (dl-program "not(X) :- p(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-et-test!
|
||||||
|
"reserved name `count` as head rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn () (dl-program "count(N, X, p(X)) :- p(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-et-test!
|
||||||
|
"reserved name `<` as head rejected"
|
||||||
|
(dl-et-throws? (fn () (dl-program "<(X, 5) :- p(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-et-test!
|
||||||
|
"reserved name `is` as head rejected"
|
||||||
|
(dl-et-throws? (fn () (dl-program "is(N, +(1, 2)) :- p(N).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Body literal with a reserved-name positive head is rejected.
|
||||||
|
;; The parser only treats outer-level `not(P)` as negation; nested
|
||||||
|
;; `not(not(P))` would otherwise silently parse as a positive call
|
||||||
|
;; to a relation named `not` and succeed vacuously. The safety
|
||||||
|
;; checker now flags this so the user gets a clear error.
|
||||||
|
;; Body literal with a reserved-name positive head is rejected.
|
||||||
|
;; The parser only treats outer-level `not(P)` as negation; nested
|
||||||
|
;; `not(not(P))` would otherwise silently parse as a positive call
|
||||||
|
;; to a relation named `not` and succeed vacuously — so the safety
|
||||||
|
;; checker now flags this to give the user a clear error.
|
||||||
|
(dl-et-test!
|
||||||
|
"nested not(not(...)) rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-program
|
||||||
|
"banned(a). u(a). vip(X) :- u(X), not(not(banned(X))).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; A dict body literal that isn't `{:neg ...}` is almost always a
|
||||||
|
;; typo — it would otherwise silently fall through to a confusing
|
||||||
|
;; head-var-unbound safety error. Now caught with a clear message.
|
||||||
|
(dl-et-test!
|
||||||
|
"dict body lit without :neg rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn ()
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(dl-add-rule! db
|
||||||
|
{:head (list (quote p) (quote X))
|
||||||
|
:body (list {:weird "stuff"})}))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Facts may only have simple-term args (number / string / symbol).
|
||||||
|
;; A compound arg like `+(1, 2)` would otherwise be silently
|
||||||
|
;; stored as the unreduced expression `(+ 1 2)` because dl-ground?
|
||||||
|
;; sees no free variables.
|
||||||
|
(dl-et-test!
|
||||||
|
"compound arg in fact rejected"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(+(1, 2)).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Rule heads may only have variable or constant args — no
|
||||||
|
;; compounds. Compound heads would be saturated as unreduced
|
||||||
|
;; tuples rather than the arithmetic result the user expected.
|
||||||
|
(dl-et-test!
|
||||||
|
"compound arg in rule head rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn () (dl-program "n(3). double(*(X, 2)) :- n(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; The anonymous-variable renamer used to start at `_anon1`
|
||||||
|
;; unconditionally; a rule that wrote `q(_anon1) :- p(_anon1, _)`
|
||||||
|
;; (the user picking the same name the renamer would generate)
|
||||||
|
;; would see the `_` renamed to `_anon1` too, collapsing the
|
||||||
|
;; two positions in `p(_anon1, _)` to a single var. Now the
|
||||||
|
;; renamer scans the rule for the max `_anon<N>` and starts past
|
||||||
|
;; it, so user-written names of that form are preserved.
|
||||||
|
(dl-et-test-set! "anonymous-rename avoids user `_anon` collision"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(a, b). p(c, d). q(_anon1) :- p(_anon1, _).")
|
||||||
|
(quote (q X)))
|
||||||
|
(list {:X (quote a)} {:X (quote c)}))
|
||||||
|
|
||||||
|
(dl-et-test!
|
||||||
|
"unsafe head var"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X, Y) :- q(X).")))
|
||||||
|
true)
|
||||||
|
(dl-et-test!
|
||||||
|
"unsafe — empty body"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X) :- .")))
|
||||||
|
true)
|
||||||
|
;; Underscore in head is unsafe — it's a fresh existential per
|
||||||
|
;; occurrence after Phase 5d's anonymous-var renaming, and there's
|
||||||
|
;; nothing in the body to bind it. (Old behavior accepted this by
|
||||||
|
;; treating '_' as a literal name to skip; the renaming made it an
|
||||||
|
;; ordinary unbound variable.)
|
||||||
|
(dl-et-test!
|
||||||
|
"underscore in head — unsafe"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X, _) :- q(X).")))
|
||||||
|
true)
|
||||||
|
(dl-et-test!
|
||||||
|
"underscore in body only — safe"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X) :- q(X, _).")))
|
||||||
|
false)
|
||||||
|
(dl-et-test!
|
||||||
|
"var only in head — unsafe"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X, Y) :- q(Z).")))
|
||||||
|
true)
|
||||||
|
(dl-et-test!
|
||||||
|
"head var bound by body"
|
||||||
|
(dl-et-throws? (fn () (dl-program "p(X) :- q(X).")))
|
||||||
|
false)
|
||||||
|
(dl-et-test!
|
||||||
|
"head subset of body"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(dl-program
|
||||||
|
"edge(a,b). edge(b,c). reach(X, Z) :- edge(X, Y), edge(Y, Z).")))
|
||||||
|
false)
|
||||||
|
|
||||||
|
;; Anonymous variables: each occurrence must be independent.
|
||||||
|
(dl-et-test-set! "anon vars in rule are independent"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(a, b). p(c, d). q(X) :- p(X, _), p(_, Y).")
|
||||||
|
(list (quote q) (quote X)))
|
||||||
|
(list {:X (quote a)} {:X (quote c)}))
|
||||||
|
|
||||||
|
(dl-et-test-set! "anon vars in goal are independent"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(1, 2, 3). p(4, 5, 6).")
|
||||||
|
(list (quote p) (quote _) (quote X) (quote _)))
|
||||||
|
(list {:X 2} {:X 5}))
|
||||||
|
|
||||||
|
;; dl-summary: relation -> tuple-count for inspection.
|
||||||
|
(dl-et-test! "dl-summary basic"
|
||||||
|
(dl-summary
|
||||||
|
(let
|
||||||
|
((db (dl-program "p(1). p(2). q(3).")))
|
||||||
|
(do (dl-saturate! db) db)))
|
||||||
|
{:p 2 :q 1})
|
||||||
|
|
||||||
|
(dl-et-test! "dl-summary empty IDB shown"
|
||||||
|
(dl-summary
|
||||||
|
(let
|
||||||
|
((db (dl-program "r(X) :- s(X).")))
|
||||||
|
(do (dl-saturate! db) db)))
|
||||||
|
{:r 0})
|
||||||
|
|
||||||
|
(dl-et-test! "dl-summary mixed EDB and IDB"
|
||||||
|
(dl-summary
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(do (dl-saturate! db) db)))
|
||||||
|
{:parent 1 :ancestor 1})
|
||||||
|
|
||||||
|
(dl-et-test! "dl-summary empty db"
|
||||||
|
(dl-summary (dl-make-db))
|
||||||
|
{})
|
||||||
|
|
||||||
|
;; Strategy hook: default semi-naive; :magic accepted but
|
||||||
|
;; falls back to semi-naive (the transformation itself is
|
||||||
|
;; deferred — Phase 6 in plan).
|
||||||
|
(dl-et-test! "default strategy"
|
||||||
|
(dl-get-strategy (dl-make-db))
|
||||||
|
:semi-naive)
|
||||||
|
|
||||||
|
(dl-et-test! "set strategy"
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(do (dl-set-strategy! db :magic) (dl-get-strategy db)))
|
||||||
|
:magic)
|
||||||
|
|
||||||
|
;; Unknown strategy values are rejected so typos don't silently
|
||||||
|
;; fall back to the default.
|
||||||
|
(dl-et-test!
|
||||||
|
"unknown strategy rejected"
|
||||||
|
(dl-et-throws?
|
||||||
|
(fn ()
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(dl-set-strategy! db :semi_naive))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; dl-saturated?: no-work-left predicate.
|
||||||
|
(dl-et-test! "saturated? after saturation"
|
||||||
|
(let ((db (dl-program
|
||||||
|
"parent(a, b).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).")))
|
||||||
|
(do (dl-saturate! db) (dl-saturated? db)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-et-test! "saturated? before saturation"
|
||||||
|
(let ((db (dl-program
|
||||||
|
"parent(a, b).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).")))
|
||||||
|
(dl-saturated? db))
|
||||||
|
false)
|
||||||
|
|
||||||
|
;; Disjunction via multiple rules — Datalog has no `;` in
|
||||||
|
;; body, so disjunction is expressed as separate rules with
|
||||||
|
;; the same head. Here plant_based(X) is satisfied by either
|
||||||
|
;; vegan(X) or vegetarian(X).
|
||||||
|
(dl-et-test-set! "disjunction via multiple rules"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"vegan(alice). vegetarian(bob). meat_eater(carol).
|
||||||
|
plant_based(X) :- vegan(X).
|
||||||
|
plant_based(X) :- vegetarian(X).")
|
||||||
|
(list (quote plant_based) (quote X)))
|
||||||
|
(list {:X (quote alice)} {:X (quote bob)}))
|
||||||
|
|
||||||
|
;; Bipartite-style join: pair-of-friends who share a hobby.
|
||||||
|
;; Three-relation join exercising the planner's join order.
|
||||||
|
(dl-et-test-set! "bipartite friends-with-hobby"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"hobby(alice, climb). hobby(bob, paint).
|
||||||
|
hobby(carol, climb).
|
||||||
|
friend(alice, carol). friend(bob, alice).
|
||||||
|
match(A, B, H) :- friend(A, B), hobby(A, H), hobby(B, H).")
|
||||||
|
(list (quote match) (quote A) (quote B) (quote H)))
|
||||||
|
(list {:A (quote alice) :B (quote carol) :H (quote climb)}))
|
||||||
|
|
||||||
|
;; Repeated variable (diagonal): p(X, X) only matches tuples
|
||||||
|
;; whose two args are equal. The unifier handles this via the
|
||||||
|
;; subst chain — first occurrence binds X, second occurrence
|
||||||
|
;; checks against the binding.
|
||||||
|
(dl-et-test-set! "diagonal query"
|
||||||
|
(dl-query
|
||||||
|
(dl-program "p(1, 1). p(2, 3). p(4, 4). p(5, 5).")
|
||||||
|
(list (quote p) (quote X) (quote X)))
|
||||||
|
(list {:X 1} {:X 4} {:X 5}))
|
||||||
|
|
||||||
|
;; A relation can be both EDB-seeded and rule-derived;
|
||||||
|
;; saturate combines facts + derivations.
|
||||||
|
(dl-et-test-set! "mixed EDB + IDB same relation"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"link(a, b). link(c, d). link(e, c).
|
||||||
|
via(a, e).
|
||||||
|
link(X, Y) :- via(X, M), link(M, Y).")
|
||||||
|
(list (quote link) (quote a) (quote X)))
|
||||||
|
(list {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
(dl-et-test! "saturated? after assert"
|
||||||
|
(let ((db (dl-program
|
||||||
|
"parent(a, b).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).")))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(dl-add-fact! db (list (quote parent) (quote b) (quote c)))
|
||||||
|
(dl-saturated? db)))
|
||||||
|
false)
|
||||||
|
|
||||||
|
(dl-et-test-set! "magic-set still derives correctly"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(do
|
||||||
|
(dl-set-strategy! db :magic)
|
||||||
|
(dl-query db (list (quote ancestor) (quote a) (quote X)))))
|
||||||
|
(list {:X (quote b)} {:X (quote c)})))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-eval-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-et-pass 0)
|
||||||
|
(set! dl-et-fail 0)
|
||||||
|
(set! dl-et-failures (list))
|
||||||
|
(dl-et-run-all!)
|
||||||
|
{:failures dl-et-failures :total (+ dl-et-pass dl-et-fail) :passed dl-et-pass :failed dl-et-fail})))
|
||||||
528
lib/datalog/tests/magic.sx
Normal file
528
lib/datalog/tests/magic.sx
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
;; lib/datalog/tests/magic.sx — adornment + SIPS analysis tests.
|
||||||
|
|
||||||
|
(define dl-mt-pass 0)
|
||||||
|
(define dl-mt-fail 0)
|
||||||
|
(define dl-mt-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mt-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-mt-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let ((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-mt-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mt-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-mt-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-mt-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mt-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i)))
|
||||||
|
(not (dl-mt-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-mt-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mt-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-mt-deep=? got expected)
|
||||||
|
(set! dl-mt-pass (+ dl-mt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-mt-fail (+ dl-mt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-mt-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected: " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-mt-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
;; Goal adornment.
|
||||||
|
(dl-mt-test! "adorn 0-ary"
|
||||||
|
(dl-adorn-goal (list (quote ready)))
|
||||||
|
"")
|
||||||
|
(dl-mt-test! "adorn all bound"
|
||||||
|
(dl-adorn-goal (list (quote p) 1 2 3))
|
||||||
|
"bbb")
|
||||||
|
(dl-mt-test! "adorn all free"
|
||||||
|
(dl-adorn-goal (list (quote p) (quote X) (quote Y)))
|
||||||
|
"ff")
|
||||||
|
(dl-mt-test! "adorn mixed"
|
||||||
|
(dl-adorn-goal (list (quote ancestor) (quote tom) (quote X)))
|
||||||
|
"bf")
|
||||||
|
(dl-mt-test! "adorn const var const"
|
||||||
|
(dl-adorn-goal (list (quote p) (quote a) (quote X) (quote b)))
|
||||||
|
"bfb")
|
||||||
|
|
||||||
|
;; dl-adorn-lit with explicit bound set.
|
||||||
|
(dl-mt-test! "adorn lit with bound"
|
||||||
|
(dl-adorn-lit (list (quote p) (quote X) (quote Y)) (list "X"))
|
||||||
|
"bf")
|
||||||
|
|
||||||
|
;; Rule SIPS — chain ancestor.
|
||||||
|
(dl-mt-test! "sips chain ancestor bf"
|
||||||
|
(dl-rule-sips
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Z))
|
||||||
|
:body (list (list (quote parent) (quote X) (quote Y))
|
||||||
|
(list (quote ancestor) (quote Y) (quote Z)))}
|
||||||
|
"bf")
|
||||||
|
(list
|
||||||
|
{:lit (list (quote parent) (quote X) (quote Y)) :adornment "bf"}
|
||||||
|
{:lit (list (quote ancestor) (quote Y) (quote Z)) :adornment "bf"}))
|
||||||
|
|
||||||
|
;; SIPS — head fully bound.
|
||||||
|
(dl-mt-test! "sips head bb"
|
||||||
|
(dl-rule-sips
|
||||||
|
{:head (list (quote q) (quote X) (quote Y))
|
||||||
|
:body (list (list (quote p) (quote X) (quote Z))
|
||||||
|
(list (quote r) (quote Z) (quote Y)))}
|
||||||
|
"bb")
|
||||||
|
(list
|
||||||
|
{:lit (list (quote p) (quote X) (quote Z)) :adornment "bf"}
|
||||||
|
{:lit (list (quote r) (quote Z) (quote Y)) :adornment "bb"}))
|
||||||
|
|
||||||
|
;; SIPS — comparison; vars must be bound by prior body lit.
|
||||||
|
(dl-mt-test! "sips with comparison"
|
||||||
|
(dl-rule-sips
|
||||||
|
{:head (list (quote q) (quote X))
|
||||||
|
:body (list (list (quote p) (quote X))
|
||||||
|
(list (string->symbol "<") (quote X) 5))}
|
||||||
|
"f")
|
||||||
|
(list
|
||||||
|
{:lit (list (quote p) (quote X)) :adornment "f"}
|
||||||
|
{:lit (list (string->symbol "<") (quote X) 5) :adornment "bb"}))
|
||||||
|
|
||||||
|
;; SIPS — `is` binds its left arg.
|
||||||
|
(dl-mt-test! "sips with is"
|
||||||
|
(dl-rule-sips
|
||||||
|
{:head (list (quote q) (quote X) (quote Y))
|
||||||
|
:body (list (list (quote p) (quote X))
|
||||||
|
(list (quote is) (quote Y) (list (string->symbol "+") (quote X) 1)))}
|
||||||
|
"ff")
|
||||||
|
(list
|
||||||
|
{:lit (list (quote p) (quote X)) :adornment "f"}
|
||||||
|
{:lit (list (quote is) (quote Y)
|
||||||
|
(list (string->symbol "+") (quote X) 1))
|
||||||
|
:adornment "fb"}))
|
||||||
|
|
||||||
|
;; Magic predicate naming.
|
||||||
|
(dl-mt-test! "magic-rel-name"
|
||||||
|
(dl-magic-rel-name "ancestor" "bf")
|
||||||
|
"magic_ancestor^bf")
|
||||||
|
|
||||||
|
;; Bound-args extraction.
|
||||||
|
(dl-mt-test! "bound-args bf"
|
||||||
|
(dl-bound-args (list (quote ancestor) (quote tom) (quote X)) "bf")
|
||||||
|
(list (quote tom)))
|
||||||
|
|
||||||
|
(dl-mt-test! "bound-args mixed"
|
||||||
|
(dl-bound-args (list (quote p) 1 (quote Y) 3) "bfb")
|
||||||
|
(list 1 3))
|
||||||
|
|
||||||
|
(dl-mt-test! "bound-args all-free"
|
||||||
|
(dl-bound-args (list (quote p) (quote X) (quote Y)) "ff")
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; Magic literal construction.
|
||||||
|
(dl-mt-test! "magic-lit"
|
||||||
|
(dl-magic-lit "ancestor" "bf" (list (quote tom)))
|
||||||
|
(list (string->symbol "magic_ancestor^bf") (quote tom)))
|
||||||
|
|
||||||
|
;; Magic-sets rewriter: structural sanity.
|
||||||
|
(dl-mt-test! "rewrite ancestor produces seed"
|
||||||
|
(let
|
||||||
|
((rules
|
||||||
|
(list
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Y))
|
||||||
|
:body (list (list (quote parent) (quote X) (quote Y)))}
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Z))
|
||||||
|
:body
|
||||||
|
(list (list (quote parent) (quote X) (quote Y))
|
||||||
|
(list (quote ancestor) (quote Y) (quote Z)))})))
|
||||||
|
(get
|
||||||
|
(dl-magic-rewrite rules "ancestor" "bf" (list (quote a)))
|
||||||
|
:seed))
|
||||||
|
(list (string->symbol "magic_ancestor^bf") (quote a)))
|
||||||
|
|
||||||
|
;; Equivalence: rewritten program derives same ancestor tuples.
|
||||||
|
;; In a chain a→b→c→d, magic-rewritten run still derives all
|
||||||
|
;; ancestor pairs reachable from any node a/b/c/d propagated via
|
||||||
|
;; magic_ancestor^bf — i.e. the full closure (6 tuples). Magic
|
||||||
|
;; saves work only when the EDB has irrelevant nodes outside
|
||||||
|
;; the seed's transitive cone.
|
||||||
|
(dl-mt-test! "magic-rewritten ancestor count"
|
||||||
|
(let
|
||||||
|
((rules
|
||||||
|
(list
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Y))
|
||||||
|
:body (list (list (quote parent) (quote X) (quote Y)))}
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Z))
|
||||||
|
:body
|
||||||
|
(list (list (quote parent) (quote X) (quote Y))
|
||||||
|
(list (quote ancestor) (quote Y) (quote Z)))}))
|
||||||
|
(edb (list
|
||||||
|
(list (quote parent) (quote a) (quote b))
|
||||||
|
(list (quote parent) (quote b) (quote c))
|
||||||
|
(list (quote parent) (quote c) (quote d)))))
|
||||||
|
(let
|
||||||
|
((rewritten (dl-magic-rewrite rules "ancestor" "bf" (list (quote a))))
|
||||||
|
(db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(for-each (fn (f) (dl-add-fact! db f)) edb)
|
||||||
|
(dl-add-fact! db (get rewritten :seed))
|
||||||
|
(for-each (fn (r) (dl-add-rule! db r)) (get rewritten :rules))
|
||||||
|
(dl-saturate! db)
|
||||||
|
(len (dl-relation db "ancestor")))))
|
||||||
|
6)
|
||||||
|
|
||||||
|
;; dl-magic-query: end-to-end driver, doesn't mutate caller's db.
|
||||||
|
;; Magic over a rule with negated body literal — propagation
|
||||||
|
;; rules generated only for positive lits; negated lits pass
|
||||||
|
;; through unchanged.
|
||||||
|
(dl-mt-test! "magic over rule with negation"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"u(a). u(b). u(c). banned(b).
|
||||||
|
active(X) :- u(X), not(banned(X)).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote active) (quote X))))
|
||||||
|
(magic (dl-magic-query db (list (quote active) (quote X)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; All-bound query (existence check) generates an "bb"
|
||||||
|
;; adornment chain. Verifies the rewriter walks multiple
|
||||||
|
;; (rel, adn) pairs through the worklist.
|
||||||
|
(dl-mt-test! "magic existence check via bb"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c). parent(c, d).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(let
|
||||||
|
((found (dl-magic-query
|
||||||
|
db (list (quote ancestor) (quote a) (quote c))))
|
||||||
|
(missing (dl-magic-query
|
||||||
|
db (list (quote ancestor) (quote a) (quote z)))))
|
||||||
|
(and (= (len found) 1) (= (len missing) 0))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Magic equivalence on the federation demo.
|
||||||
|
(dl-mt-test! "magic ≡ semi on foaf demo"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((follows alice bob)
|
||||||
|
(follows bob carol)
|
||||||
|
(follows alice dave)))
|
||||||
|
dl-demo-federation-rules)))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (quote (foaf alice X))))
|
||||||
|
(magic (dl-magic-query db (quote (foaf alice X)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Shape validation: dl-magic-query rejects non-list / non-
|
||||||
|
;; dict goal shapes cleanly rather than crashing in `rest`.
|
||||||
|
(dl-mt-test! "magic rejects string goal"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-magic-query (dl-make-db) "foo"))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-mt-test! "magic rejects bare dict goal"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-magic-query (dl-make-db) {:foo "bar"}))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; 3-stratum program under magic — distinct rule heads at
|
||||||
|
;; strata 0/1/2 must all rewrite via the worklist.
|
||||||
|
(dl-mt-test! "magic 3-stratum program"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"a(1). a(2). a(3). b(2).
|
||||||
|
c(X) :- a(X), not(b(X)).
|
||||||
|
d(X) :- c(X), not(banned(X)).
|
||||||
|
banned(3).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote d) (quote X))))
|
||||||
|
(magic (dl-magic-query db (list (quote d) (quote X)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Aggregate -> derived -> threshold chain via magic.
|
||||||
|
(dl-mt-test! "magic aggregate-derived chain"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"src(1). src(2). src(3).
|
||||||
|
cnt(N) :- count(N, X, src(X)).
|
||||||
|
active(N) :- cnt(N), >=(N, 2).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote active) (quote N))))
|
||||||
|
(magic (dl-magic-query db (list (quote active) (quote N)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Multi-relation rewrite chain: query r4 → propagate to r3,
|
||||||
|
;; r2, r1, a. The worklist must process all of them; an
|
||||||
|
;; earlier bug stopped after only the head pair.
|
||||||
|
(dl-mt-test! "magic chain through 4 rule levels"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"a(1). a(2). r1(X) :- a(X). r2(X) :- r1(X).
|
||||||
|
r3(X) :- r2(X). r4(X) :- r3(X).")))
|
||||||
|
(= 2 (len (dl-magic-query db (list (quote r4) (quote X))))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Shortest-path demo via magic — exercises the rewriter
|
||||||
|
;; against rules that mix recursive positive lits with an
|
||||||
|
;; aggregate body literal.
|
||||||
|
(dl-mt-test! "magic on shortest-path demo"
|
||||||
|
(let
|
||||||
|
((db (dl-program-data
|
||||||
|
(quote ((edge a b 5) (edge b c 3) (edge a c 10)))
|
||||||
|
dl-demo-shortest-path-rules)))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (quote (shortest a c W))))
|
||||||
|
(magic (dl-magic-query db (quote (shortest a c W)))))
|
||||||
|
(and (= (len semi) (len magic))
|
||||||
|
(= (len semi) 1))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Same relation called with different adornment patterns
|
||||||
|
;; in different rules. The worklist must enqueue and process
|
||||||
|
;; each (rel, adornment) pair.
|
||||||
|
(dl-mt-test! "magic with multi-adornment same relation"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(p1, alice). parent(p2, bob).
|
||||||
|
parent(g, p1). parent(g, p2).
|
||||||
|
sibling(P1, P2) :- parent(G, P1), parent(G, P2),
|
||||||
|
!=(P1, P2).
|
||||||
|
cousin(X, Y) :- parent(P1, X), parent(P2, Y),
|
||||||
|
sibling(P1, P2).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote cousin) (quote alice) (quote Y))))
|
||||||
|
(magic (dl-magic-query db (list (quote cousin) (quote alice) (quote Y)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Magic over a rule whose body contains an aggregate.
|
||||||
|
;; The rewriter passes aggregate body lits through unchanged
|
||||||
|
;; (no propagation generated for them), so semi-naive's count
|
||||||
|
;; logic still fires correctly under the rewritten program.
|
||||||
|
(dl-mt-test! "magic over rule with aggregate body"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"post(p1). post(p2). post(p3).
|
||||||
|
liked(u1, p1). liked(u2, p1). liked(u3, p1).
|
||||||
|
liked(u1, p2).
|
||||||
|
rich(P) :- post(P), count(N, U, liked(U, P)),
|
||||||
|
>=(N, 2).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote rich) (quote P))))
|
||||||
|
(magic (dl-magic-query db (list (quote rich) (quote P)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Mixed EDB + IDB: a relation can be both EDB-seeded and
|
||||||
|
;; rule-derived. dl-magic-query must include the EDB portion
|
||||||
|
;; even though the relation has rules.
|
||||||
|
(dl-mt-test! "magic mixed EDB+IDB"
|
||||||
|
(len
|
||||||
|
(dl-magic-query
|
||||||
|
(dl-program
|
||||||
|
"link(a, b). link(c, d). link(e, c).
|
||||||
|
via(a, e).
|
||||||
|
link(X, Y) :- via(X, M), link(M, Y).")
|
||||||
|
(list (quote link) (quote a) (quote X))))
|
||||||
|
2)
|
||||||
|
|
||||||
|
;; dl-magic-query falls back to dl-query for built-in,
|
||||||
|
;; aggregate, and negation goals (the magic seed would
|
||||||
|
;; otherwise be non-ground).
|
||||||
|
(dl-mt-test! "magic-query falls back on aggregate"
|
||||||
|
(let
|
||||||
|
((r (dl-magic-query
|
||||||
|
(dl-program "p(1). p(2). p(3).")
|
||||||
|
(list (quote count) (quote N) (quote X)
|
||||||
|
(list (quote p) (quote X))))))
|
||||||
|
(and (= (len r) 1) (= (get (first r) "N") 3)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-mt-test! "magic-query equivalent to dl-query"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c). parent(c, d).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(let
|
||||||
|
((semi (dl-query db (list (quote ancestor) (quote a) (quote X))))
|
||||||
|
(magic (dl-magic-query
|
||||||
|
db (list (quote ancestor) (quote a) (quote X)))))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; The magic rewriter passes aggregate body lits through
|
||||||
|
;; unchanged, so an aggregate over an IDB relation would see an
|
||||||
|
;; empty inner-goal in the magic db unless the IDB is already
|
||||||
|
;; materialised. dl-magic-query now pre-saturates the source db
|
||||||
|
;; to guarantee equivalence with dl-query for every stratified
|
||||||
|
;; program. Previously this returned `({:N 0})` because `active`
|
||||||
|
;; (IDB, derived through negation) was never derived in the
|
||||||
|
;; magic db.
|
||||||
|
(dl-mt-test! "magic over aggregate-of-IDB matches vanilla"
|
||||||
|
(let
|
||||||
|
((src
|
||||||
|
"u(a). u(b). u(c). u(d). banned(b). banned(d).
|
||||||
|
active(X) :- u(X), not(banned(X)).
|
||||||
|
n(N) :- count(N, X, active(X))."))
|
||||||
|
(let
|
||||||
|
((vanilla (dl-eval src "?- n(N)."))
|
||||||
|
(magic (dl-eval-magic src "?- n(N).")))
|
||||||
|
(and (= (len vanilla) 1)
|
||||||
|
(= (len magic) 1)
|
||||||
|
(= (get (first vanilla) "N")
|
||||||
|
(get (first magic) "N")))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; magic-query doesn't mutate caller db.
|
||||||
|
(dl-mt-test! "magic-query preserves caller db"
|
||||||
|
(let
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(let
|
||||||
|
((rules-before (len (dl-rules db))))
|
||||||
|
(do
|
||||||
|
(dl-magic-query db (list (quote ancestor) (quote a) (quote X)))
|
||||||
|
(= rules-before (len (dl-rules db))))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Magic-sets benefit: query touches only one cluster of a
|
||||||
|
;; multi-component graph. Semi-naive derives the full closure
|
||||||
|
;; over both clusters; magic only the seeded one.
|
||||||
|
;; Magic-vs-semi work shape: chain of 12. Semi-naive
|
||||||
|
;; derives the full closure (78 = 12·13/2). A magic query
|
||||||
|
;; rooted at node 0 returns the 12 descendants only —
|
||||||
|
;; demonstrating that magic limits derivation to the
|
||||||
|
;; query's transitive cone.
|
||||||
|
(dl-mt-test! "magic vs semi work-shape on chain-12"
|
||||||
|
(let
|
||||||
|
((source (str
|
||||||
|
"parent(0, 1). parent(1, 2). parent(2, 3). "
|
||||||
|
"parent(3, 4). parent(4, 5). parent(5, 6). "
|
||||||
|
"parent(6, 7). parent(7, 8). parent(8, 9). "
|
||||||
|
"parent(9, 10). parent(10, 11). parent(11, 12). "
|
||||||
|
"ancestor(X, Y) :- parent(X, Y). "
|
||||||
|
"ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(let
|
||||||
|
((db1 (dl-make-db)) (db2 (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(dl-load-program! db1 source)
|
||||||
|
(dl-saturate! db1)
|
||||||
|
(dl-load-program! db2 source)
|
||||||
|
(let
|
||||||
|
((semi-count (len (dl-relation db1 "ancestor")))
|
||||||
|
(magic-count
|
||||||
|
(len (dl-magic-query
|
||||||
|
db2 (list (quote ancestor) 0 (quote X))))))
|
||||||
|
;; Magic returns only descendants of 0 (12 of them).
|
||||||
|
(and (= semi-count 78) (= magic-count 12))))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Magic + arithmetic: rules with `is` clauses pass through
|
||||||
|
;; the rewriter unchanged (built-ins aren't propagated).
|
||||||
|
(dl-mt-test! "magic preserves arithmetic"
|
||||||
|
(let
|
||||||
|
((source "n(1). n(2). n(3).
|
||||||
|
doubled(X, Y) :- n(X), is(Y, *(X, 2))."))
|
||||||
|
(let
|
||||||
|
((semi (dl-eval source "?- doubled(2, Y)."))
|
||||||
|
(magic (dl-eval-magic source "?- doubled(2, Y).")))
|
||||||
|
(= (len semi) (len magic))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-mt-test! "magic skips irrelevant clusters"
|
||||||
|
(let
|
||||||
|
;; Two disjoint chains. Query is rooted in cluster 1.
|
||||||
|
((db (dl-program
|
||||||
|
"parent(a, b). parent(b, c).
|
||||||
|
parent(x, y). parent(y, z).
|
||||||
|
ancestor(X, Y) :- parent(X, Y).
|
||||||
|
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db)
|
||||||
|
(let
|
||||||
|
((semi-count (len (dl-relation db "ancestor")))
|
||||||
|
(magic-results
|
||||||
|
(dl-magic-query
|
||||||
|
db (list (quote ancestor) (quote a) (quote X)))))
|
||||||
|
;; Semi-naive derives 6 (3 in each cluster). Magic
|
||||||
|
;; gives 3 query results (a's reachable: b, c).
|
||||||
|
(and (= semi-count 6) (= (len magic-results) 2)))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-mt-test! "magic-rewritten finds same answers"
|
||||||
|
(let
|
||||||
|
((rules
|
||||||
|
(list
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Y))
|
||||||
|
:body (list (list (quote parent) (quote X) (quote Y)))}
|
||||||
|
{:head (list (quote ancestor) (quote X) (quote Z))
|
||||||
|
:body
|
||||||
|
(list (list (quote parent) (quote X) (quote Y))
|
||||||
|
(list (quote ancestor) (quote Y) (quote Z)))}))
|
||||||
|
(edb (list
|
||||||
|
(list (quote parent) (quote a) (quote b))
|
||||||
|
(list (quote parent) (quote b) (quote c)))))
|
||||||
|
(let
|
||||||
|
((rewritten (dl-magic-rewrite rules "ancestor" "bf" (list (quote a))))
|
||||||
|
(db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(for-each (fn (f) (dl-add-fact! db f)) edb)
|
||||||
|
(dl-add-fact! db (get rewritten :seed))
|
||||||
|
(for-each (fn (r) (dl-add-rule! db r)) (get rewritten :rules))
|
||||||
|
(dl-saturate! db)
|
||||||
|
(len (dl-query db (list (quote ancestor) (quote a) (quote X)))))))
|
||||||
|
2))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-magic-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-mt-pass 0)
|
||||||
|
(set! dl-mt-fail 0)
|
||||||
|
(set! dl-mt-failures (list))
|
||||||
|
(dl-mt-run-all!)
|
||||||
|
{:passed dl-mt-pass
|
||||||
|
:failed dl-mt-fail
|
||||||
|
:total (+ dl-mt-pass dl-mt-fail)
|
||||||
|
:failures dl-mt-failures})))
|
||||||
252
lib/datalog/tests/negation.sx
Normal file
252
lib/datalog/tests/negation.sx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
;; lib/datalog/tests/negation.sx — stratified negation tests.
|
||||||
|
|
||||||
|
(define dl-nt-pass 0)
|
||||||
|
(define dl-nt-fail 0)
|
||||||
|
(define dl-nt-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-deep=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-nt-deq-l? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let ((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-nt-deq-d? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-deq-l?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-nt-deep=? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-nt-deq-l? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-deq-d?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i)))
|
||||||
|
(not (dl-nt-deep=? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-nt-deq-d? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-set=?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(and
|
||||||
|
(= (len a) (len b))
|
||||||
|
(dl-nt-subset? a b)
|
||||||
|
(dl-nt-subset? b a))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-subset?
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) true)
|
||||||
|
((not (dl-nt-contains? ys (first xs))) false)
|
||||||
|
(else (dl-nt-subset? (rest xs) ys)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-contains?
|
||||||
|
(fn
|
||||||
|
(xs target)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((dl-nt-deep=? (first xs) target) true)
|
||||||
|
(else (dl-nt-contains? (rest xs) target)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-nt-deep=? got expected)
|
||||||
|
(set! dl-nt-pass (+ dl-nt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-nt-fail (+ dl-nt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-nt-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected: " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-test-set!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-nt-set=? got expected)
|
||||||
|
(set! dl-nt-pass (+ dl-nt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-nt-fail (+ dl-nt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-nt-failures
|
||||||
|
(str
|
||||||
|
name
|
||||||
|
"\n expected (set): " expected
|
||||||
|
"\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-throws?
|
||||||
|
(fn
|
||||||
|
(thunk)
|
||||||
|
(let
|
||||||
|
((threw false))
|
||||||
|
(do
|
||||||
|
(guard
|
||||||
|
(e (#t (set! threw true)))
|
||||||
|
(thunk))
|
||||||
|
threw))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-nt-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
;; Negation against EDB-only relation.
|
||||||
|
(dl-nt-test-set! "not against EDB"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2). p(3). r(2).
|
||||||
|
q(X) :- p(X), not(r(X)).")
|
||||||
|
(list (quote q) (quote X)))
|
||||||
|
(list {:X 1} {:X 3}))
|
||||||
|
|
||||||
|
;; Negation against derived relation — needs stratification.
|
||||||
|
(dl-nt-test-set! "not against derived rel"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2). p(3). s(2).
|
||||||
|
r(X) :- s(X).
|
||||||
|
q(X) :- p(X), not(r(X)).")
|
||||||
|
(list (quote q) (quote X)))
|
||||||
|
(list {:X 1} {:X 3}))
|
||||||
|
|
||||||
|
;; Two-step strata: r derives via s; q derives via not r.
|
||||||
|
(dl-nt-test-set! "two-step strata"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"node(a). node(b). node(c). node(d).
|
||||||
|
edge(a, b). edge(b, c). edge(c, a).
|
||||||
|
reach(X, Y) :- edge(X, Y).
|
||||||
|
reach(X, Z) :- edge(X, Y), reach(Y, Z).
|
||||||
|
unreachable(X) :- node(X), not(reach(a, X)).")
|
||||||
|
(list (quote unreachable) (quote X)))
|
||||||
|
(list {:X (quote d)}))
|
||||||
|
|
||||||
|
;; Combine negation with arithmetic and comparison.
|
||||||
|
(dl-nt-test-set! "negation with arithmetic"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"n(1). n(2). n(3). n(4). n(5). odd(1). odd(3). odd(5).
|
||||||
|
even(X) :- n(X), not(odd(X)).")
|
||||||
|
(list (quote even) (quote X)))
|
||||||
|
(list {:X 2} {:X 4}))
|
||||||
|
|
||||||
|
;; Empty negation result.
|
||||||
|
(dl-nt-test-set! "negation always succeeds"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2). q(X) :- p(X), not(r(X)).")
|
||||||
|
(list (quote q) (quote X)))
|
||||||
|
(list {:X 1} {:X 2}))
|
||||||
|
|
||||||
|
;; Negation always fails.
|
||||||
|
(dl-nt-test-set! "negation always fails"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2). r(1). r(2). q(X) :- p(X), not(r(X)).")
|
||||||
|
(list (quote q) (quote X)))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
;; Anonymous `_` in a negated literal is existentially quantified
|
||||||
|
;; — it doesn't need to be bound by an earlier body lit. Without
|
||||||
|
;; this exemption the safety check would reject the common idiom
|
||||||
|
;; `orphan(X) :- person(X), not(parent(X, _))`.
|
||||||
|
(dl-nt-test-set! "negation with anonymous var — orphan idiom"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"person(a). person(b). person(c). parent(a, b).
|
||||||
|
orphan(X) :- person(X), not(parent(X, _)).")
|
||||||
|
(list (quote orphan) (quote X)))
|
||||||
|
(list {:X (quote b)} {:X (quote c)}))
|
||||||
|
|
||||||
|
;; Multiple anonymous vars are each independently existential.
|
||||||
|
(dl-nt-test-set! "negation with multiple anonymous vars"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"u(a). u(b). u(c). edge(a, x). edge(b, y).
|
||||||
|
solo(X) :- u(X), not(edge(X, _)).")
|
||||||
|
(list (quote solo) (quote X)))
|
||||||
|
(list {:X (quote c)}))
|
||||||
|
|
||||||
|
;; Stratifiability checks.
|
||||||
|
(dl-nt-test! "non-stratifiable rejected"
|
||||||
|
(dl-nt-throws?
|
||||||
|
(fn ()
|
||||||
|
(let ((db (dl-make-db)))
|
||||||
|
(do
|
||||||
|
(dl-add-rule!
|
||||||
|
db
|
||||||
|
{:head (list (quote p) (quote X))
|
||||||
|
:body (list (list (quote q) (quote X))
|
||||||
|
{:neg (list (quote r) (quote X))})})
|
||||||
|
(dl-add-rule!
|
||||||
|
db
|
||||||
|
{:head (list (quote r) (quote X))
|
||||||
|
:body (list (list (quote p) (quote X)))})
|
||||||
|
(dl-add-fact! db (list (quote q) 1))
|
||||||
|
(dl-saturate! db)))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-nt-test! "stratifiable accepted"
|
||||||
|
(dl-nt-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-program
|
||||||
|
"p(1). p(2). r(2).
|
||||||
|
q(X) :- p(X), not(r(X)).")))
|
||||||
|
false)
|
||||||
|
|
||||||
|
;; Multi-stratum chain.
|
||||||
|
(dl-nt-test-set! "three-level strata"
|
||||||
|
(dl-query
|
||||||
|
(dl-program
|
||||||
|
"a(1). a(2). a(3). a(4).
|
||||||
|
b(X) :- a(X), not(c(X)).
|
||||||
|
c(X) :- d(X).
|
||||||
|
d(2).
|
||||||
|
d(4).")
|
||||||
|
(list (quote b) (quote X)))
|
||||||
|
(list {:X 1} {:X 3}))
|
||||||
|
|
||||||
|
;; Safety violation: negation refers to unbound var.
|
||||||
|
(dl-nt-test! "negation safety violation"
|
||||||
|
(dl-nt-throws?
|
||||||
|
(fn ()
|
||||||
|
(dl-program
|
||||||
|
"p(1). q(X) :- p(X), not(r(Y)).")))
|
||||||
|
true))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-negation-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-nt-pass 0)
|
||||||
|
(set! dl-nt-fail 0)
|
||||||
|
(set! dl-nt-failures (list))
|
||||||
|
(dl-nt-run-all!)
|
||||||
|
{:passed dl-nt-pass
|
||||||
|
:failed dl-nt-fail
|
||||||
|
:total (+ dl-nt-pass dl-nt-fail)
|
||||||
|
:failures dl-nt-failures})))
|
||||||
179
lib/datalog/tests/parse.sx
Normal file
179
lib/datalog/tests/parse.sx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
;; lib/datalog/tests/parse.sx — parser unit tests
|
||||||
|
;;
|
||||||
|
;; Run via: bash lib/datalog/conformance.sh
|
||||||
|
;; Or: (load "lib/datalog/tokenizer.sx") (load "lib/datalog/parser.sx")
|
||||||
|
;; (load "lib/datalog/tests/parse.sx") (dl-parse-tests-run!)
|
||||||
|
|
||||||
|
(define dl-pt-pass 0)
|
||||||
|
(define dl-pt-fail 0)
|
||||||
|
(define dl-pt-failures (list))
|
||||||
|
|
||||||
|
;; Order-independent structural equality. Lists compared positionally,
|
||||||
|
;; dicts as sets of (key, value) pairs. Numbers via = (so 30.0 = 30).
|
||||||
|
(define
|
||||||
|
dl-deep-equal?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-deep-equal-list? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let
|
||||||
|
((ka (keys a)) (kb (keys b)))
|
||||||
|
(and
|
||||||
|
(= (len ka) (len kb))
|
||||||
|
(dl-deep-equal-dict? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-deep-equal-list?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-deep-equal? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-deep-equal-list? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-deep-equal-dict?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i))) (not (dl-deep-equal? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-deep-equal-dict? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pt-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-deep-equal? got expected)
|
||||||
|
(set! dl-pt-pass (+ dl-pt-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-pt-fail (+ dl-pt-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-pt-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pt-throws?
|
||||||
|
(fn
|
||||||
|
(thunk)
|
||||||
|
(let
|
||||||
|
((threw false))
|
||||||
|
(do (guard (e (#t (set! threw true))) (thunk)) threw))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-pt-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-pt-test! "empty program" (dl-parse "") (list))
|
||||||
|
(dl-pt-test! "fact" (dl-parse "parent(tom, bob).") (list {:body (list) :head (list (quote parent) (quote tom) (quote bob))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"two facts"
|
||||||
|
(dl-parse "parent(tom, bob). parent(bob, ann).")
|
||||||
|
(list {:body (list) :head (list (quote parent) (quote tom) (quote bob))} {:body (list) :head (list (quote parent) (quote bob) (quote ann))}))
|
||||||
|
(dl-pt-test! "zero-ary fact" (dl-parse "ready.") (list {:body (list) :head (list (quote ready))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"rule one body lit"
|
||||||
|
(dl-parse "ancestor(X, Y) :- parent(X, Y).")
|
||||||
|
(list {:body (list (list (quote parent) (quote X) (quote Y))) :head (list (quote ancestor) (quote X) (quote Y))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"recursive rule"
|
||||||
|
(dl-parse "ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")
|
||||||
|
(list {:body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z))) :head (list (quote ancestor) (quote X) (quote Z))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"query single"
|
||||||
|
(dl-parse "?- ancestor(tom, X).")
|
||||||
|
(list {:query (list (list (quote ancestor) (quote tom) (quote X)))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"query multi"
|
||||||
|
(dl-parse "?- p(X), q(X).")
|
||||||
|
(list {:query (list (list (quote p) (quote X)) (list (quote q) (quote X)))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"negation"
|
||||||
|
(dl-parse "safe(X) :- person(X), not(parent(X, _)).")
|
||||||
|
(list {:body (list (list (quote person) (quote X)) {:neg (list (quote parent) (quote X) (quote _))}) :head (list (quote safe) (quote X))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"number arg"
|
||||||
|
(dl-parse "age(alice, 30).")
|
||||||
|
(list {:body (list) :head (list (quote age) (quote alice) 30)}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"string arg"
|
||||||
|
(dl-parse "label(x, \"hi\").")
|
||||||
|
(list {:body (list) :head (list (quote label) (quote x) "hi")}))
|
||||||
|
;; Quoted 'atoms' parse as strings — a uppercase-starting name
|
||||||
|
;; in quotes used to misclassify as a variable and reject the
|
||||||
|
;; fact as non-ground.
|
||||||
|
(dl-pt-test!
|
||||||
|
"quoted atom arg parses as string"
|
||||||
|
(dl-parse "p('Hello World').")
|
||||||
|
(list {:body (list) :head (list (quote p) "Hello World")}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"comparison literal"
|
||||||
|
(dl-parse "p(X) :- <(X, 5).")
|
||||||
|
(list {:body (list (list (string->symbol "<") (quote X) 5)) :head (list (quote p) (quote X))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"is with arith"
|
||||||
|
(dl-parse "succ(X, Y) :- nat(X), is(Y, +(X, 1)).")
|
||||||
|
(list {:body (list (list (quote nat) (quote X)) (list (quote is) (quote Y) (list (string->symbol "+") (quote X) 1))) :head (list (quote succ) (quote X) (quote Y))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"mixed program"
|
||||||
|
(dl-parse "p(a). p(b). q(X) :- p(X). ?- q(Y).")
|
||||||
|
(list {:body (list) :head (list (quote p) (quote a))} {:body (list) :head (list (quote p) (quote b))} {:body (list (list (quote p) (quote X))) :head (list (quote q) (quote X))} {:query (list (list (quote q) (quote Y)))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"comments skipped"
|
||||||
|
(dl-parse "% comment\nfoo(a).\n/* block */ bar(b).")
|
||||||
|
(list {:body (list) :head (list (quote foo) (quote a))} {:body (list) :head (list (quote bar) (quote b))}))
|
||||||
|
(dl-pt-test!
|
||||||
|
"underscore var"
|
||||||
|
(dl-parse "p(X) :- q(X, _).")
|
||||||
|
(list {:body (list (list (quote q) (quote X) (quote _))) :head (list (quote p) (quote X))}))
|
||||||
|
;; Negative number literals parse as one negative number,
|
||||||
|
;; while subtraction (`-(X, Y)`) compound is preserved.
|
||||||
|
(dl-pt-test!
|
||||||
|
"negative integer literal"
|
||||||
|
(dl-parse "n(-3).")
|
||||||
|
(list {:head (list (quote n) -3) :body (list)}))
|
||||||
|
|
||||||
|
(dl-pt-test!
|
||||||
|
"subtraction compound preserved"
|
||||||
|
(dl-parse "r(X) :- is(X, -(10, 3)).")
|
||||||
|
(list
|
||||||
|
{:head (list (quote r) (quote X))
|
||||||
|
:body (list (list (quote is) (quote X)
|
||||||
|
(list (string->symbol "-") 10 3)))}))
|
||||||
|
|
||||||
|
(dl-pt-test!
|
||||||
|
"number as relation name raises"
|
||||||
|
(dl-pt-throws? (fn () (dl-parse "1(X) :- p(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-pt-test!
|
||||||
|
"var as relation name raises"
|
||||||
|
(dl-pt-throws? (fn () (dl-parse "P(X).")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-pt-test!
|
||||||
|
"missing dot raises"
|
||||||
|
(dl-pt-throws? (fn () (dl-parse "p(a)")))
|
||||||
|
true)
|
||||||
|
(dl-pt-test!
|
||||||
|
"trailing comma raises"
|
||||||
|
(dl-pt-throws? (fn () (dl-parse "p(a,).")))
|
||||||
|
true))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-parse-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-pt-pass 0)
|
||||||
|
(set! dl-pt-fail 0)
|
||||||
|
(set! dl-pt-failures (list))
|
||||||
|
(dl-pt-run-all!)
|
||||||
|
{:failures dl-pt-failures :total (+ dl-pt-pass dl-pt-fail) :passed dl-pt-pass :failed dl-pt-fail})))
|
||||||
153
lib/datalog/tests/semi_naive.sx
Normal file
153
lib/datalog/tests/semi_naive.sx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
;; lib/datalog/tests/semi_naive.sx — semi-naive correctness vs naive.
|
||||||
|
;;
|
||||||
|
;; Strategy: differential — run both saturators on each program and
|
||||||
|
;; compare the resulting per-relation tuple counts. Counting (not
|
||||||
|
;; element-wise set equality) keeps the suite fast under the bundled
|
||||||
|
;; conformance session; correctness on the inhabitants is covered by
|
||||||
|
;; eval.sx and builtins.sx (which use dl-saturate! by default — the
|
||||||
|
;; semi-naive saturator).
|
||||||
|
|
||||||
|
(define dl-sn-pass 0)
|
||||||
|
(define dl-sn-fail 0)
|
||||||
|
(define dl-sn-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-sn-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(equal? got expected)
|
||||||
|
(set! dl-sn-pass (+ dl-sn-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-sn-fail (+ dl-sn-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-sn-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
;; Load `source` into both a semi-naive and a naive db and return a
|
||||||
|
;; list of (rel-name semi-count naive-count) triples. Both sets must
|
||||||
|
;; have the same union of relation names.
|
||||||
|
(define
|
||||||
|
dl-sn-counts
|
||||||
|
(fn
|
||||||
|
(source)
|
||||||
|
(let
|
||||||
|
((db-s (dl-program source)) (db-n (dl-program source)))
|
||||||
|
(do
|
||||||
|
(dl-saturate! db-s)
|
||||||
|
(dl-saturate-naive! db-n)
|
||||||
|
(let
|
||||||
|
((out (list)))
|
||||||
|
(do
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(append!
|
||||||
|
out
|
||||||
|
(list
|
||||||
|
k
|
||||||
|
(len (dl-relation db-s k))
|
||||||
|
(len (dl-relation db-n k)))))
|
||||||
|
(keys (get db-s :facts)))
|
||||||
|
out))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-sn-counts-agree?
|
||||||
|
(fn
|
||||||
|
(counts)
|
||||||
|
(cond
|
||||||
|
((= (len counts) 0) true)
|
||||||
|
(else
|
||||||
|
(let
|
||||||
|
((row (first counts)))
|
||||||
|
(and
|
||||||
|
(= (nth row 1) (nth row 2))
|
||||||
|
(dl-sn-counts-agree? (rest counts))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-sn-chain-source
|
||||||
|
(fn
|
||||||
|
(n)
|
||||||
|
(let
|
||||||
|
((parts (list "")))
|
||||||
|
(do
|
||||||
|
(define
|
||||||
|
dl-sn-loop
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(do
|
||||||
|
(append! parts (str "parent(" i ", " (+ i 1) "). "))
|
||||||
|
(dl-sn-loop (+ i 1))))))
|
||||||
|
(dl-sn-loop 0)
|
||||||
|
(str
|
||||||
|
(join "" parts)
|
||||||
|
"ancestor(X, Y) :- parent(X, Y). "
|
||||||
|
"ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-sn-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-sn-test!
|
||||||
|
"ancestor closure counts match"
|
||||||
|
(dl-sn-counts-agree?
|
||||||
|
(dl-sn-counts
|
||||||
|
"parent(a, b). parent(b, c). parent(c, d).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))
|
||||||
|
true)
|
||||||
|
(dl-sn-test!
|
||||||
|
"cyclic reach counts match"
|
||||||
|
(dl-sn-counts-agree?
|
||||||
|
(dl-sn-counts
|
||||||
|
"edge(1, 2). edge(2, 3). edge(3, 1). edge(3, 4).\n reach(X, Y) :- edge(X, Y).\n reach(X, Z) :- edge(X, Y), reach(Y, Z)."))
|
||||||
|
true)
|
||||||
|
(dl-sn-test!
|
||||||
|
"same-gen counts match"
|
||||||
|
(dl-sn-counts-agree?
|
||||||
|
(dl-sn-counts
|
||||||
|
"parent(a, b). parent(a, c). parent(b, d). parent(c, e).\n person(a). person(b). person(c). person(d). person(e).\n sg(X, X) :- person(X).\n sg(X, Y) :- parent(P1, X), sg(P1, P2), parent(P2, Y)."))
|
||||||
|
true)
|
||||||
|
(dl-sn-test!
|
||||||
|
"rules with builtins counts match"
|
||||||
|
(dl-sn-counts-agree?
|
||||||
|
(dl-sn-counts
|
||||||
|
"n(1). n(2). n(3). n(4). n(5).\n small(X) :- n(X), <(X, 5).\n succ(X, Y) :- n(X), <(X, 5), is(Y, +(X, 1))."))
|
||||||
|
true)
|
||||||
|
(dl-sn-test!
|
||||||
|
"static rule fires under semi-naive"
|
||||||
|
(dl-sn-counts-agree?
|
||||||
|
(dl-sn-counts "p(a). p(b). q(X) :- p(X), =(X, a)."))
|
||||||
|
true)
|
||||||
|
;; Chain length 12 — multiple semi-naive iterations against
|
||||||
|
;; the recursive ancestor rule (differential vs naive).
|
||||||
|
(dl-sn-test!
|
||||||
|
"chain-12 ancestor counts match"
|
||||||
|
(dl-sn-counts-agree? (dl-sn-counts (dl-sn-chain-source 12)))
|
||||||
|
true)
|
||||||
|
;; Chain length 25 — semi-naive only — first-arg index makes
|
||||||
|
;; this tractable in conformance budget.
|
||||||
|
(dl-sn-test!
|
||||||
|
"chain-25 ancestor count value (semi only)"
|
||||||
|
(let
|
||||||
|
((db (dl-program (dl-sn-chain-source 25))))
|
||||||
|
(do (dl-saturate! db) (len (dl-relation db "ancestor"))))
|
||||||
|
325)
|
||||||
|
(dl-sn-test!
|
||||||
|
"query through semi saturate"
|
||||||
|
(let
|
||||||
|
((db (dl-program "parent(a, b). parent(b, c).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
|
||||||
|
(len (dl-query db (list (quote ancestor) (quote a) (quote X)))))
|
||||||
|
2))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-semi-naive-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-sn-pass 0)
|
||||||
|
(set! dl-sn-fail 0)
|
||||||
|
(set! dl-sn-failures (list))
|
||||||
|
(dl-sn-run-all!)
|
||||||
|
{:failures dl-sn-failures :total (+ dl-sn-pass dl-sn-fail) :passed dl-sn-pass :failed dl-sn-fail})))
|
||||||
189
lib/datalog/tests/tokenize.sx
Normal file
189
lib/datalog/tests/tokenize.sx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
;; lib/datalog/tests/tokenize.sx — tokenizer unit tests
|
||||||
|
;;
|
||||||
|
;; Run via: bash lib/datalog/conformance.sh
|
||||||
|
;; Or: (load "lib/datalog/tokenizer.sx") (load "lib/datalog/tests/tokenize.sx")
|
||||||
|
;; (dl-tokenize-tests-run!)
|
||||||
|
|
||||||
|
(define dl-tk-pass 0)
|
||||||
|
(define dl-tk-fail 0)
|
||||||
|
(define dl-tk-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tk-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(= got expected)
|
||||||
|
(set! dl-tk-pass (+ dl-tk-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-tk-fail (+ dl-tk-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-tk-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
(define dl-tk-types (fn (toks) (map (fn (t) (get t :type)) toks)))
|
||||||
|
(define dl-tk-values (fn (toks) (map (fn (t) (get t :value)) toks)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tk-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-tk-test! "empty" (dl-tk-types (dl-tokenize "")) (list "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"atom dot"
|
||||||
|
(dl-tk-types (dl-tokenize "foo."))
|
||||||
|
(list "atom" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"atom dot value"
|
||||||
|
(dl-tk-values (dl-tokenize "foo."))
|
||||||
|
(list "foo" "." nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"var"
|
||||||
|
(dl-tk-types (dl-tokenize "X."))
|
||||||
|
(list "var" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"underscore var"
|
||||||
|
(dl-tk-types (dl-tokenize "_x."))
|
||||||
|
(list "var" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"integer"
|
||||||
|
(dl-tk-values (dl-tokenize "42"))
|
||||||
|
(list 42 nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"decimal"
|
||||||
|
(dl-tk-values (dl-tokenize "3.14"))
|
||||||
|
(list 3.14 nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"string"
|
||||||
|
(dl-tk-values (dl-tokenize "\"hello\""))
|
||||||
|
(list "hello" nil))
|
||||||
|
;; Quoted 'atoms' tokenize as strings — see the type-table
|
||||||
|
;; comment in lib/datalog/tokenizer.sx for the rationale.
|
||||||
|
(dl-tk-test!
|
||||||
|
"quoted atom as string"
|
||||||
|
(dl-tk-types (dl-tokenize "'two words'"))
|
||||||
|
(list "string" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"quoted atom value"
|
||||||
|
(dl-tk-values (dl-tokenize "'two words'"))
|
||||||
|
(list "two words" nil))
|
||||||
|
;; A quoted atom whose name would otherwise be a variable
|
||||||
|
;; (uppercase / leading underscore) is now safely a string —
|
||||||
|
;; this was the bug that motivated the type change.
|
||||||
|
(dl-tk-test!
|
||||||
|
"quoted Uppercase as string"
|
||||||
|
(dl-tk-types (dl-tokenize "'Hello'"))
|
||||||
|
(list "string" "eof"))
|
||||||
|
(dl-tk-test! ":-" (dl-tk-values (dl-tokenize ":-")) (list ":-" nil))
|
||||||
|
(dl-tk-test! "?-" (dl-tk-values (dl-tokenize "?-")) (list "?-" nil))
|
||||||
|
(dl-tk-test! "<=" (dl-tk-values (dl-tokenize "<=")) (list "<=" nil))
|
||||||
|
(dl-tk-test! ">=" (dl-tk-values (dl-tokenize ">=")) (list ">=" nil))
|
||||||
|
(dl-tk-test! "!=" (dl-tk-values (dl-tokenize "!=")) (list "!=" nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"single op values"
|
||||||
|
(dl-tk-values (dl-tokenize "< > = + - * /"))
|
||||||
|
(list "<" ">" "=" "+" "-" "*" "/" nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"single op types"
|
||||||
|
(dl-tk-types (dl-tokenize "< > = + - * /"))
|
||||||
|
(list "op" "op" "op" "op" "op" "op" "op" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"punct"
|
||||||
|
(dl-tk-values (dl-tokenize "( ) , ."))
|
||||||
|
(list "(" ")" "," "." nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"fact tokens"
|
||||||
|
(dl-tk-types (dl-tokenize "parent(tom, bob)."))
|
||||||
|
(list "atom" "punct" "atom" "punct" "atom" "punct" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"rule shape"
|
||||||
|
(dl-tk-types (dl-tokenize "p(X) :- q(X)."))
|
||||||
|
(list
|
||||||
|
"atom"
|
||||||
|
"punct"
|
||||||
|
"var"
|
||||||
|
"punct"
|
||||||
|
"op"
|
||||||
|
"atom"
|
||||||
|
"punct"
|
||||||
|
"var"
|
||||||
|
"punct"
|
||||||
|
"punct"
|
||||||
|
"eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"comparison literal"
|
||||||
|
(dl-tk-values (dl-tokenize "<(X, 5)"))
|
||||||
|
(list "<" "(" "X" "," 5 ")" nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"is form"
|
||||||
|
(dl-tk-values (dl-tokenize "is(Y, +(X, 1))"))
|
||||||
|
(list "is" "(" "Y" "," "+" "(" "X" "," 1 ")" ")" nil))
|
||||||
|
(dl-tk-test!
|
||||||
|
"line comment"
|
||||||
|
(dl-tk-types (dl-tokenize "% comment line\nfoo."))
|
||||||
|
(list "atom" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"block comment"
|
||||||
|
(dl-tk-types (dl-tokenize "/* a\nb */ x."))
|
||||||
|
(list "atom" "punct" "eof"))
|
||||||
|
;; Unexpected characters surface at tokenize time rather
|
||||||
|
;; than being silently consumed (previously `?(X)` parsed as
|
||||||
|
;; if the leading `?` weren't there).
|
||||||
|
(dl-tk-test!
|
||||||
|
"unexpected char raises"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-tokenize "?(X)"))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Unterminated string / quoted-atom must raise.
|
||||||
|
(dl-tk-test!
|
||||||
|
"unterminated string raises"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-tokenize "\"unclosed"))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(dl-tk-test!
|
||||||
|
"unterminated quoted atom raises"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-tokenize "'unclosed"))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Unterminated block comment must raise — previously it was
|
||||||
|
;; silently consumed to EOF.
|
||||||
|
(dl-tk-test!
|
||||||
|
"unterminated block comment raises"
|
||||||
|
(let ((threw false))
|
||||||
|
(do
|
||||||
|
(guard (e (#t (set! threw true)))
|
||||||
|
(dl-tokenize "/* unclosed comment"))
|
||||||
|
threw))
|
||||||
|
true)
|
||||||
|
(dl-tk-test!
|
||||||
|
"whitespace"
|
||||||
|
(dl-tk-types (dl-tokenize " foo ,\t bar ."))
|
||||||
|
(list "atom" "punct" "atom" "punct" "eof"))
|
||||||
|
(dl-tk-test!
|
||||||
|
"positions"
|
||||||
|
(map (fn (t) (get t :pos)) (dl-tokenize "foo bar"))
|
||||||
|
(list 0 4 7)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tokenize-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-tk-pass 0)
|
||||||
|
(set! dl-tk-fail 0)
|
||||||
|
(set! dl-tk-failures (list))
|
||||||
|
(dl-tk-run-all!)
|
||||||
|
{:failures dl-tk-failures :total (+ dl-tk-pass dl-tk-fail) :passed dl-tk-pass :failed dl-tk-fail})))
|
||||||
194
lib/datalog/tests/unify.sx
Normal file
194
lib/datalog/tests/unify.sx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
;; lib/datalog/tests/unify.sx — unification + substitution tests.
|
||||||
|
|
||||||
|
(define dl-ut-pass 0)
|
||||||
|
(define dl-ut-fail 0)
|
||||||
|
(define dl-ut-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ut-deep-equal?
|
||||||
|
(fn
|
||||||
|
(a b)
|
||||||
|
(cond
|
||||||
|
((and (list? a) (list? b))
|
||||||
|
(and (= (len a) (len b)) (dl-ut-deq-list? a b 0)))
|
||||||
|
((and (dict? a) (dict? b))
|
||||||
|
(let
|
||||||
|
((ka (keys a)) (kb (keys b)))
|
||||||
|
(and (= (len ka) (len kb)) (dl-ut-deq-dict? a b ka 0))))
|
||||||
|
((and (number? a) (number? b)) (= a b))
|
||||||
|
(else (equal? a b)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ut-deq-list?
|
||||||
|
(fn
|
||||||
|
(a b i)
|
||||||
|
(cond
|
||||||
|
((>= i (len a)) true)
|
||||||
|
((not (dl-ut-deep-equal? (nth a i) (nth b i))) false)
|
||||||
|
(else (dl-ut-deq-list? a b (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ut-deq-dict?
|
||||||
|
(fn
|
||||||
|
(a b ka i)
|
||||||
|
(cond
|
||||||
|
((>= i (len ka)) true)
|
||||||
|
((let ((k (nth ka i))) (not (dl-ut-deep-equal? (get a k) (get b k))))
|
||||||
|
false)
|
||||||
|
(else (dl-ut-deq-dict? a b ka (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ut-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(if
|
||||||
|
(dl-ut-deep-equal? got expected)
|
||||||
|
(set! dl-ut-pass (+ dl-ut-pass 1))
|
||||||
|
(do
|
||||||
|
(set! dl-ut-fail (+ dl-ut-fail 1))
|
||||||
|
(append!
|
||||||
|
dl-ut-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ut-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(dl-ut-test! "var? uppercase" (dl-var? (quote X)) true)
|
||||||
|
(dl-ut-test! "var? underscore" (dl-var? (quote _foo)) true)
|
||||||
|
(dl-ut-test! "var? lowercase" (dl-var? (quote tom)) false)
|
||||||
|
(dl-ut-test! "var? number" (dl-var? 5) false)
|
||||||
|
(dl-ut-test! "var? string" (dl-var? "hi") false)
|
||||||
|
(dl-ut-test! "var? list" (dl-var? (list 1)) false)
|
||||||
|
(dl-ut-test!
|
||||||
|
"atom-atom match"
|
||||||
|
(dl-unify (quote tom) (quote tom) (dl-empty-subst))
|
||||||
|
{})
|
||||||
|
(dl-ut-test!
|
||||||
|
"atom-atom fail"
|
||||||
|
(dl-unify (quote tom) (quote bob) (dl-empty-subst))
|
||||||
|
nil)
|
||||||
|
(dl-ut-test!
|
||||||
|
"num-num match"
|
||||||
|
(dl-unify 5 5 (dl-empty-subst))
|
||||||
|
{})
|
||||||
|
(dl-ut-test!
|
||||||
|
"num-num fail"
|
||||||
|
(dl-unify 5 6 (dl-empty-subst))
|
||||||
|
nil)
|
||||||
|
(dl-ut-test!
|
||||||
|
"string match"
|
||||||
|
(dl-unify "hi" "hi" (dl-empty-subst))
|
||||||
|
{})
|
||||||
|
(dl-ut-test! "string fail" (dl-unify "hi" "bye" (dl-empty-subst)) nil)
|
||||||
|
(dl-ut-test!
|
||||||
|
"var-atom binds"
|
||||||
|
(dl-unify (quote X) (quote tom) (dl-empty-subst))
|
||||||
|
{:X (quote tom)})
|
||||||
|
(dl-ut-test!
|
||||||
|
"atom-var binds"
|
||||||
|
(dl-unify (quote tom) (quote X) (dl-empty-subst))
|
||||||
|
{:X (quote tom)})
|
||||||
|
(dl-ut-test!
|
||||||
|
"var-var same"
|
||||||
|
(dl-unify (quote X) (quote X) (dl-empty-subst))
|
||||||
|
{})
|
||||||
|
(dl-ut-test!
|
||||||
|
"var-var bind"
|
||||||
|
(let
|
||||||
|
((s (dl-unify (quote X) (quote Y) (dl-empty-subst))))
|
||||||
|
(dl-walk (quote X) s))
|
||||||
|
(quote Y))
|
||||||
|
(dl-ut-test!
|
||||||
|
"tuple match"
|
||||||
|
(dl-unify
|
||||||
|
(list (quote parent) (quote X) (quote bob))
|
||||||
|
(list (quote parent) (quote tom) (quote Y))
|
||||||
|
(dl-empty-subst))
|
||||||
|
{:X (quote tom) :Y (quote bob)})
|
||||||
|
(dl-ut-test!
|
||||||
|
"tuple arity mismatch"
|
||||||
|
(dl-unify
|
||||||
|
(list (quote p) (quote X))
|
||||||
|
(list (quote p) (quote a) (quote b))
|
||||||
|
(dl-empty-subst))
|
||||||
|
nil)
|
||||||
|
(dl-ut-test!
|
||||||
|
"tuple head mismatch"
|
||||||
|
(dl-unify
|
||||||
|
(list (quote p) (quote X))
|
||||||
|
(list (quote q) (quote X))
|
||||||
|
(dl-empty-subst))
|
||||||
|
nil)
|
||||||
|
(dl-ut-test!
|
||||||
|
"walk chain"
|
||||||
|
(let
|
||||||
|
((s1 (dl-unify (quote X) (quote Y) (dl-empty-subst))))
|
||||||
|
(let
|
||||||
|
((s2 (dl-unify (quote Y) (quote tom) s1)))
|
||||||
|
(dl-walk (quote X) s2)))
|
||||||
|
(quote tom))
|
||||||
|
|
||||||
|
;; Walk with circular substitution must not infinite-loop.
|
||||||
|
;; Cycles return the current term unchanged.
|
||||||
|
(dl-ut-test!
|
||||||
|
"walk circular subst no hang"
|
||||||
|
(let ((s (dl-bind (quote B) (quote A)
|
||||||
|
(dl-bind (quote A) (quote B) (dl-empty-subst)))))
|
||||||
|
(dl-walk (quote A) s))
|
||||||
|
(quote A))
|
||||||
|
(dl-ut-test!
|
||||||
|
"apply subst on tuple"
|
||||||
|
(let
|
||||||
|
((s (dl-bind (quote X) (quote tom) (dl-empty-subst))))
|
||||||
|
(dl-apply-subst (list (quote parent) (quote X) (quote Y)) s))
|
||||||
|
(list (quote parent) (quote tom) (quote Y)))
|
||||||
|
(dl-ut-test!
|
||||||
|
"ground? all const"
|
||||||
|
(dl-ground?
|
||||||
|
(list (quote p) (quote tom) 5)
|
||||||
|
(dl-empty-subst))
|
||||||
|
true)
|
||||||
|
(dl-ut-test!
|
||||||
|
"ground? unbound var"
|
||||||
|
(dl-ground? (list (quote p) (quote X)) (dl-empty-subst))
|
||||||
|
false)
|
||||||
|
(dl-ut-test!
|
||||||
|
"ground? bound var"
|
||||||
|
(let
|
||||||
|
((s (dl-bind (quote X) (quote tom) (dl-empty-subst))))
|
||||||
|
(dl-ground? (list (quote p) (quote X)) s))
|
||||||
|
true)
|
||||||
|
(dl-ut-test!
|
||||||
|
"ground? bare var"
|
||||||
|
(dl-ground? (quote X) (dl-empty-subst))
|
||||||
|
false)
|
||||||
|
(dl-ut-test!
|
||||||
|
"vars-of basic"
|
||||||
|
(dl-vars-of
|
||||||
|
(list (quote p) (quote X) (quote tom) (quote Y) (quote X)))
|
||||||
|
(list "X" "Y"))
|
||||||
|
(dl-ut-test!
|
||||||
|
"vars-of ground"
|
||||||
|
(dl-vars-of (list (quote p) (quote tom) (quote bob)))
|
||||||
|
(list))
|
||||||
|
(dl-ut-test!
|
||||||
|
"vars-of nested compound"
|
||||||
|
(dl-vars-of
|
||||||
|
(list
|
||||||
|
(quote is)
|
||||||
|
(quote Z)
|
||||||
|
(list (string->symbol "+") (quote X) 1)))
|
||||||
|
(list "Z" "X")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-unify-tests-run!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(set! dl-ut-pass 0)
|
||||||
|
(set! dl-ut-fail 0)
|
||||||
|
(set! dl-ut-failures (list))
|
||||||
|
(dl-ut-run-all!)
|
||||||
|
{:failures dl-ut-failures :total (+ dl-ut-pass dl-ut-fail) :passed dl-ut-pass :failed dl-ut-fail})))
|
||||||
269
lib/datalog/tokenizer.sx
Normal file
269
lib/datalog/tokenizer.sx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
;; lib/datalog/tokenizer.sx — Datalog source → token stream
|
||||||
|
;;
|
||||||
|
;; Tokens: {:type T :value V :pos P}
|
||||||
|
;; Types:
|
||||||
|
;; "atom" — lowercase-start bare identifier
|
||||||
|
;; "var" — uppercase-start or _-start ident (value is the name)
|
||||||
|
;; "number" — numeric literal (decoded to number)
|
||||||
|
;; "string" — "..." string literal OR quoted 'atom' (treated as a
|
||||||
|
;; string value to avoid the var-vs-atom ambiguity that
|
||||||
|
;; would arise from a quoted atom whose name starts with
|
||||||
|
;; an uppercase letter or underscore)
|
||||||
|
;; "punct" — ( ) , .
|
||||||
|
;; "op" — :- ?- <= >= != < > = + - * /
|
||||||
|
;; "eof"
|
||||||
|
;;
|
||||||
|
;; Datalog has no function symbols in arg position; the parser still
|
||||||
|
;; accepts nested compounds for arithmetic ((is X (+ A B))) but safety
|
||||||
|
;; analysis rejects non-arithmetic nesting at rule-load time.
|
||||||
|
|
||||||
|
(define dl-make-token (fn (type value pos) {:type type :value value :pos pos}))
|
||||||
|
|
||||||
|
(define dl-digit? (fn (c) (and (>= c "0") (<= c "9"))))
|
||||||
|
(define dl-lower? (fn (c) (and (>= c "a") (<= c "z"))))
|
||||||
|
(define dl-upper? (fn (c) (and (>= c "A") (<= c "Z"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ident-char?
|
||||||
|
(fn (c) (or (dl-lower? c) (dl-upper? c) (dl-digit? c) (= c "_"))))
|
||||||
|
|
||||||
|
(define dl-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-tokenize
|
||||||
|
(fn
|
||||||
|
(src)
|
||||||
|
(let
|
||||||
|
((tokens (list)) (pos 0) (src-len (len src)))
|
||||||
|
(define
|
||||||
|
dl-peek
|
||||||
|
(fn
|
||||||
|
(offset)
|
||||||
|
(if (< (+ pos offset) src-len) (nth src (+ pos offset)) nil)))
|
||||||
|
(define cur (fn () (dl-peek 0)))
|
||||||
|
(define advance! (fn (n) (set! pos (+ pos n))))
|
||||||
|
(define
|
||||||
|
at?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((sl (len s)))
|
||||||
|
(and (<= (+ pos sl) src-len) (= (slice src pos (+ pos sl)) s)))))
|
||||||
|
(define
|
||||||
|
dl-emit!
|
||||||
|
(fn
|
||||||
|
(type value start)
|
||||||
|
(append! tokens (dl-make-token type value start))))
|
||||||
|
(define
|
||||||
|
skip-line-comment!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (not (= (cur) "\n")))
|
||||||
|
(do (advance! 1) (skip-line-comment!)))))
|
||||||
|
(define
|
||||||
|
skip-block-comment!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len)
|
||||||
|
(error (str "Tokenizer: unterminated block comment "
|
||||||
|
"(started at position " pos ")")))
|
||||||
|
((and (= (cur) "*") (< (+ pos 1) src-len) (= (dl-peek 1) "/"))
|
||||||
|
(advance! 2))
|
||||||
|
(else (do (advance! 1) (skip-block-comment!))))))
|
||||||
|
(define
|
||||||
|
skip-ws!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((dl-ws? (cur)) (do (advance! 1) (skip-ws!)))
|
||||||
|
((= (cur) "%")
|
||||||
|
(do (advance! 1) (skip-line-comment!) (skip-ws!)))
|
||||||
|
((and (= (cur) "/") (< (+ pos 1) src-len) (= (dl-peek 1) "*"))
|
||||||
|
(do (advance! 2) (skip-block-comment!) (skip-ws!)))
|
||||||
|
(else nil))))
|
||||||
|
(define
|
||||||
|
read-ident
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (dl-ident-char? (cur)))
|
||||||
|
(do (advance! 1) (read-ident start)))
|
||||||
|
(slice src start pos))))
|
||||||
|
(define
|
||||||
|
read-decimal-digits!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (dl-digit? (cur)))
|
||||||
|
(do (advance! 1) (read-decimal-digits!)))))
|
||||||
|
(define
|
||||||
|
read-number
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(do
|
||||||
|
(read-decimal-digits!)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(= (cur) ".")
|
||||||
|
(< (+ pos 1) src-len)
|
||||||
|
(dl-digit? (dl-peek 1)))
|
||||||
|
(do (advance! 1) (read-decimal-digits!)))
|
||||||
|
(parse-number (slice src start pos)))))
|
||||||
|
(define
|
||||||
|
read-quoted
|
||||||
|
(fn
|
||||||
|
(quote-char)
|
||||||
|
(let
|
||||||
|
((chars (list)))
|
||||||
|
(advance! 1)
|
||||||
|
(define
|
||||||
|
loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len)
|
||||||
|
(error
|
||||||
|
(str "Tokenizer: unterminated "
|
||||||
|
(if (= quote-char "'") "quoted atom" "string")
|
||||||
|
" (started near position " pos ")")))
|
||||||
|
((= (cur) "\\")
|
||||||
|
(do
|
||||||
|
(advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur)))
|
||||||
|
(do
|
||||||
|
(cond
|
||||||
|
((= ch "n") (append! chars "\n"))
|
||||||
|
((= ch "t") (append! chars "\t"))
|
||||||
|
((= ch "r") (append! chars "\r"))
|
||||||
|
((= ch "\\") (append! chars "\\"))
|
||||||
|
((= ch "'") (append! chars "'"))
|
||||||
|
((= ch "\"") (append! chars "\""))
|
||||||
|
(else (append! chars ch)))
|
||||||
|
(advance! 1))))
|
||||||
|
(loop)))
|
||||||
|
((= (cur) quote-char) (advance! 1))
|
||||||
|
(else
|
||||||
|
(do (append! chars (cur)) (advance! 1) (loop))))))
|
||||||
|
(loop)
|
||||||
|
(join "" chars))))
|
||||||
|
(define
|
||||||
|
scan!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(skip-ws!)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur)) (start pos))
|
||||||
|
(cond
|
||||||
|
((at? ":-")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" ":-" start)
|
||||||
|
(advance! 2)
|
||||||
|
(scan!)))
|
||||||
|
((at? "?-")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "?-" start)
|
||||||
|
(advance! 2)
|
||||||
|
(scan!)))
|
||||||
|
((at? "<=")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "<=" start)
|
||||||
|
(advance! 2)
|
||||||
|
(scan!)))
|
||||||
|
((at? ">=")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" ">=" start)
|
||||||
|
(advance! 2)
|
||||||
|
(scan!)))
|
||||||
|
((at? "!=")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "!=" start)
|
||||||
|
(advance! 2)
|
||||||
|
(scan!)))
|
||||||
|
((dl-digit? ch)
|
||||||
|
(do
|
||||||
|
(dl-emit! "number" (read-number start) start)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "'")
|
||||||
|
;; Quoted 'atoms' tokenize as strings so a name
|
||||||
|
;; like 'Hello World' doesn't get misclassified
|
||||||
|
;; as a variable by dl-var? (which inspects the
|
||||||
|
;; symbol's first character).
|
||||||
|
(do (dl-emit! "string" (read-quoted "'") start) (scan!)))
|
||||||
|
((= ch "\"")
|
||||||
|
(do (dl-emit! "string" (read-quoted "\"") start) (scan!)))
|
||||||
|
((dl-lower? ch)
|
||||||
|
(do (dl-emit! "atom" (read-ident start) start) (scan!)))
|
||||||
|
((or (dl-upper? ch) (= ch "_"))
|
||||||
|
(do (dl-emit! "var" (read-ident start) start) (scan!)))
|
||||||
|
((= ch "(")
|
||||||
|
(do
|
||||||
|
(dl-emit! "punct" "(" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch ")")
|
||||||
|
(do
|
||||||
|
(dl-emit! "punct" ")" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch ",")
|
||||||
|
(do
|
||||||
|
(dl-emit! "punct" "," start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch ".")
|
||||||
|
(do
|
||||||
|
(dl-emit! "punct" "." start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "<")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "<" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch ">")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" ">" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "=")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "=" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "+")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "+" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "-")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "-" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "*")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "*" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
((= ch "/")
|
||||||
|
(do
|
||||||
|
(dl-emit! "op" "/" start)
|
||||||
|
(advance! 1)
|
||||||
|
(scan!)))
|
||||||
|
(else (error
|
||||||
|
(str "Tokenizer: unexpected character '" ch
|
||||||
|
"' at position " start)))))))))
|
||||||
|
(scan!)
|
||||||
|
(dl-emit! "eof" nil pos)
|
||||||
|
tokens)))
|
||||||
171
lib/datalog/unify.sx
Normal file
171
lib/datalog/unify.sx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
;; lib/datalog/unify.sx — unification + substitution for Datalog terms.
|
||||||
|
;;
|
||||||
|
;; Term taxonomy (after parsing):
|
||||||
|
;; variable — SX symbol whose first char is uppercase A–Z or '_'.
|
||||||
|
;; constant — SX symbol whose first char is lowercase a–z (atom name).
|
||||||
|
;; number — numeric literal.
|
||||||
|
;; string — string literal.
|
||||||
|
;; compound — SX list (functor arg ... arg). In core Datalog these
|
||||||
|
;; only appear as arithmetic expressions (see Phase 4
|
||||||
|
;; safety analysis); compound-against-compound unification
|
||||||
|
;; is supported anyway for completeness.
|
||||||
|
;;
|
||||||
|
;; Substitutions are immutable dicts keyed by variable name (string).
|
||||||
|
;; A failed unification returns nil; success returns the extended subst.
|
||||||
|
|
||||||
|
(define dl-empty-subst (fn () {}))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-var?
|
||||||
|
(fn
|
||||||
|
(term)
|
||||||
|
(and
|
||||||
|
(symbol? term)
|
||||||
|
(let
|
||||||
|
((name (symbol->string term)))
|
||||||
|
(and
|
||||||
|
(> (len name) 0)
|
||||||
|
(let
|
||||||
|
((c (slice name 0 1)))
|
||||||
|
(or (and (>= c "A") (<= c "Z")) (= c "_"))))))))
|
||||||
|
|
||||||
|
;; Walk: chase variable bindings until we hit a non-variable or an unbound
|
||||||
|
;; variable. The result is either a non-variable term or an unbound var.
|
||||||
|
(define
|
||||||
|
dl-walk
|
||||||
|
(fn (term subst) (dl-walk-aux term subst (list))))
|
||||||
|
|
||||||
|
;; Internal: walk with a visited-var set so circular substitutions
|
||||||
|
;; (from raw dl-bind misuse) don't infinite-loop. Cycles return the
|
||||||
|
;; current term unchanged.
|
||||||
|
(define
|
||||||
|
dl-walk-aux
|
||||||
|
(fn
|
||||||
|
(term subst visited)
|
||||||
|
(if
|
||||||
|
(dl-var? term)
|
||||||
|
(let
|
||||||
|
((name (symbol->string term)))
|
||||||
|
(cond
|
||||||
|
((dl-member? name visited) term)
|
||||||
|
((and (dict? subst) (has-key? subst name))
|
||||||
|
(let ((seen (list)))
|
||||||
|
(do
|
||||||
|
(for-each (fn (v) (append! seen v)) visited)
|
||||||
|
(append! seen name)
|
||||||
|
(dl-walk-aux (get subst name) subst seen))))
|
||||||
|
(else term)))
|
||||||
|
term)))
|
||||||
|
|
||||||
|
;; Bind a variable symbol to a value in subst, returning a new subst.
|
||||||
|
(define
|
||||||
|
dl-bind
|
||||||
|
(fn (var-sym value subst) (assoc subst (symbol->string var-sym) value)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-unify
|
||||||
|
(fn
|
||||||
|
(t1 t2 subst)
|
||||||
|
(if
|
||||||
|
(nil? subst)
|
||||||
|
nil
|
||||||
|
(let
|
||||||
|
((u1 (dl-walk t1 subst)) (u2 (dl-walk t2 subst)))
|
||||||
|
(cond
|
||||||
|
((dl-var? u1)
|
||||||
|
(cond
|
||||||
|
((and (dl-var? u2) (= (symbol->string u1) (symbol->string u2)))
|
||||||
|
subst)
|
||||||
|
(else (dl-bind u1 u2 subst))))
|
||||||
|
((dl-var? u2) (dl-bind u2 u1 subst))
|
||||||
|
((and (list? u1) (list? u2))
|
||||||
|
(if
|
||||||
|
(= (len u1) (len u2))
|
||||||
|
(dl-unify-list u1 u2 subst 0)
|
||||||
|
nil))
|
||||||
|
((and (number? u1) (number? u2)) (if (= u1 u2) subst nil))
|
||||||
|
((and (string? u1) (string? u2)) (if (= u1 u2) subst nil))
|
||||||
|
((and (symbol? u1) (symbol? u2))
|
||||||
|
(if (= (symbol->string u1) (symbol->string u2)) subst nil))
|
||||||
|
(else nil))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-unify-list
|
||||||
|
(fn
|
||||||
|
(a b subst i)
|
||||||
|
(cond
|
||||||
|
((nil? subst) nil)
|
||||||
|
((>= i (len a)) subst)
|
||||||
|
(else
|
||||||
|
(dl-unify-list
|
||||||
|
a
|
||||||
|
b
|
||||||
|
(dl-unify (nth a i) (nth b i) subst)
|
||||||
|
(+ i 1))))))
|
||||||
|
|
||||||
|
;; Apply substitution: walk the term and recurse into lists.
|
||||||
|
(define
|
||||||
|
dl-apply-subst
|
||||||
|
(fn
|
||||||
|
(term subst)
|
||||||
|
(let
|
||||||
|
((w (dl-walk term subst)))
|
||||||
|
(if (list? w) (map (fn (x) (dl-apply-subst x subst)) w) w))))
|
||||||
|
|
||||||
|
;; Ground? — true iff no free variables remain after walking.
|
||||||
|
(define
|
||||||
|
dl-ground?
|
||||||
|
(fn
|
||||||
|
(term subst)
|
||||||
|
(let
|
||||||
|
((w (dl-walk term subst)))
|
||||||
|
(cond
|
||||||
|
((dl-var? w) false)
|
||||||
|
((list? w) (dl-ground-list? w subst 0))
|
||||||
|
(else true)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-ground-list?
|
||||||
|
(fn
|
||||||
|
(xs subst i)
|
||||||
|
(cond
|
||||||
|
((>= i (len xs)) true)
|
||||||
|
((not (dl-ground? (nth xs i) subst)) false)
|
||||||
|
(else (dl-ground-list? xs subst (+ i 1))))))
|
||||||
|
|
||||||
|
;; Return the list of variable names appearing in a term (deduped, in
|
||||||
|
;; left-to-right order). Useful for safety analysis later.
|
||||||
|
(define
|
||||||
|
dl-vars-of
|
||||||
|
(fn (term) (let ((seen (list))) (do (dl-vars-of-aux term seen) seen))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-vars-of-aux
|
||||||
|
(fn
|
||||||
|
(term acc)
|
||||||
|
(cond
|
||||||
|
((dl-var? term)
|
||||||
|
(let
|
||||||
|
((name (symbol->string term)))
|
||||||
|
(when (not (dl-member? name acc)) (append! acc name))))
|
||||||
|
((list? term) (dl-vars-of-list term acc 0))
|
||||||
|
(else nil))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-vars-of-list
|
||||||
|
(fn
|
||||||
|
(xs acc i)
|
||||||
|
(when
|
||||||
|
(< i (len xs))
|
||||||
|
(do
|
||||||
|
(dl-vars-of-aux (nth xs i) acc)
|
||||||
|
(dl-vars-of-list xs acc (+ i 1))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
dl-member?
|
||||||
|
(fn
|
||||||
|
(x xs)
|
||||||
|
(cond
|
||||||
|
((= (len xs) 0) false)
|
||||||
|
((= (first xs) x) true)
|
||||||
|
(else (dl-member? x (rest xs))))))
|
||||||
@@ -76,7 +76,7 @@ cat > "$TMPFILE" << 'EPOCHS'
|
|||||||
(eval "(list er-fib-test-pass er-fib-test-count)")
|
(eval "(list er-fib-test-pass er-fib-test-count)")
|
||||||
EPOCHS
|
EPOCHS
|
||||||
|
|
||||||
timeout 120 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||||
|
|
||||||
# Parse "(N M)" from the line after each "(ok-len <epoch> ...)" marker.
|
# Parse "(N M)" from the line after each "(ok-len <epoch> ...)" marker.
|
||||||
parse_pair() {
|
parse_pair() {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"language": "erlang",
|
"language": "erlang",
|
||||||
"total_pass": 0,
|
"total_pass": 530,
|
||||||
"total": 0,
|
"total": 530,
|
||||||
"suites": [
|
"suites": [
|
||||||
{"name":"tokenize","pass":0,"total":0,"status":"ok"},
|
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||||
{"name":"parse","pass":0,"total":0,"status":"ok"},
|
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||||
{"name":"eval","pass":0,"total":0,"status":"ok"},
|
{"name":"eval","pass":346,"total":346,"status":"ok"},
|
||||||
{"name":"runtime","pass":0,"total":0,"status":"ok"},
|
{"name":"runtime","pass":39,"total":39,"status":"ok"},
|
||||||
{"name":"ring","pass":0,"total":0,"status":"ok"},
|
{"name":"ring","pass":4,"total":4,"status":"ok"},
|
||||||
{"name":"ping-pong","pass":0,"total":0,"status":"ok"},
|
{"name":"ping-pong","pass":4,"total":4,"status":"ok"},
|
||||||
{"name":"bank","pass":0,"total":0,"status":"ok"},
|
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
||||||
{"name":"echo","pass":0,"total":0,"status":"ok"},
|
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
||||||
{"name":"fib","pass":0,"total":0,"status":"ok"}
|
{"name":"fib","pass":8,"total":8,"status":"ok"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# Erlang-on-SX Scoreboard
|
# Erlang-on-SX Scoreboard
|
||||||
|
|
||||||
**Total: 0 / 0 tests passing**
|
**Total: 530 / 530 tests passing**
|
||||||
|
|
||||||
| | Suite | Pass | Total |
|
| | Suite | Pass | Total |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ✅ | tokenize | 0 | 0 |
|
| ✅ | tokenize | 62 | 62 |
|
||||||
| ✅ | parse | 0 | 0 |
|
| ✅ | parse | 52 | 52 |
|
||||||
| ✅ | eval | 0 | 0 |
|
| ✅ | eval | 346 | 346 |
|
||||||
| ✅ | runtime | 0 | 0 |
|
| ✅ | runtime | 39 | 39 |
|
||||||
| ✅ | ring | 0 | 0 |
|
| ✅ | ring | 4 | 4 |
|
||||||
| ✅ | ping-pong | 0 | 0 |
|
| ✅ | ping-pong | 4 | 4 |
|
||||||
| ✅ | bank | 0 | 0 |
|
| ✅ | bank | 8 | 8 |
|
||||||
| ✅ | echo | 0 | 0 |
|
| ✅ | echo | 7 | 7 |
|
||||||
| ✅ | fib | 0 | 0 |
|
| ✅ | fib | 8 | 8 |
|
||||||
|
|
||||||
|
|
||||||
Generated by `lib/erlang/conformance.sh`.
|
Generated by `lib/erlang/conformance.sh`.
|
||||||
|
|||||||
14
lib/forth/ans-tests/README.md
Normal file
14
lib/forth/ans-tests/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
ANS Forth conformance tests — vendored from
|
||||||
|
https://github.com/gerryjackson/forth2012-test-suite (master, commit-locked
|
||||||
|
on first fetch: 2026-04-24).
|
||||||
|
|
||||||
|
Files in this directory are pristine copies of upstream — do not edit them.
|
||||||
|
They are consumed by the conformance runner in `lib/forth/conformance.sh`.
|
||||||
|
|
||||||
|
- `tester.fr` — John Hayes' test harness (`T{ ... -> ... }T`). (C) 1995
|
||||||
|
Johns Hopkins APL, distributable under its notice.
|
||||||
|
- `core.fr` — Core word set tests (Hayes, ~1000 lines).
|
||||||
|
- `coreexttest.fth` — Core Extension tests (Gerry Jackson).
|
||||||
|
|
||||||
|
Only `core.fr` is expected to run green end-to-end for Phase 3; the others
|
||||||
|
stay parked until later phases.
|
||||||
1009
lib/forth/ans-tests/core.fr
Normal file
1009
lib/forth/ans-tests/core.fr
Normal file
File diff suppressed because it is too large
Load Diff
775
lib/forth/ans-tests/coreexttest.fth
Normal file
775
lib/forth/ans-tests/coreexttest.fth
Normal file
@@ -0,0 +1,775 @@
|
|||||||
|
\ To test the ANS Forth Core Extension word set
|
||||||
|
|
||||||
|
\ This program was written by Gerry Jackson in 2006, with contributions from
|
||||||
|
\ others where indicated, and is in the public domain - it can be distributed
|
||||||
|
\ and/or modified in any way but please retain this notice.
|
||||||
|
|
||||||
|
\ This program is distributed in the hope that it will be useful,
|
||||||
|
\ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
\ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
\ The tests are not claimed to be comprehensive or correct
|
||||||
|
|
||||||
|
\ ------------------------------------------------------------------------------
|
||||||
|
\ Version 0.15 1 August 2025 Added two tests to VALUE
|
||||||
|
\ 0.14 21 July 2022 Updated first line of BUFFER: test as recommended
|
||||||
|
\ in issue 32
|
||||||
|
\ 0.13 28 October 2015
|
||||||
|
\ Replace <FALSE> and <TRUE> with FALSE and TRUE to avoid
|
||||||
|
\ dependence on Core tests
|
||||||
|
\ Moved SAVE-INPUT and RESTORE-INPUT tests in a file to filetest.fth
|
||||||
|
\ Use of 2VARIABLE (from optional wordset) replaced with CREATE.
|
||||||
|
\ Minor lower to upper case conversions.
|
||||||
|
\ Calls to COMPARE replaced by S= (in utilities.fth) to avoid use
|
||||||
|
\ of a word from an optional word set.
|
||||||
|
\ UNUSED tests revised as UNUSED UNUSED = may return FALSE when an
|
||||||
|
\ implementation has the data stack sharing unused dataspace.
|
||||||
|
\ Double number input dependency removed from the HOLDS tests.
|
||||||
|
\ Minor case sensitivities removed in definition names.
|
||||||
|
\ 0.11 25 April 2015
|
||||||
|
\ Added tests for PARSE-NAME HOLDS BUFFER:
|
||||||
|
\ S\" tests added
|
||||||
|
\ DEFER IS ACTION-OF DEFER! DEFER@ tests added
|
||||||
|
\ Empty CASE statement test added
|
||||||
|
\ [COMPILE] tests removed because it is obsolescent in Forth 2012
|
||||||
|
\ 0.10 1 August 2014
|
||||||
|
\ Added tests contributed by James Bowman for:
|
||||||
|
\ <> U> 0<> 0> NIP TUCK ROLL PICK 2>R 2R@ 2R>
|
||||||
|
\ HEX WITHIN UNUSED AGAIN MARKER
|
||||||
|
\ Added tests for:
|
||||||
|
\ .R U.R ERASE PAD REFILL SOURCE-ID
|
||||||
|
\ Removed ABORT from NeverExecuted to enable Win32
|
||||||
|
\ to continue after failure of RESTORE-INPUT.
|
||||||
|
\ Removed max-intx which is no longer used.
|
||||||
|
\ 0.7 6 June 2012 Extra CASE test added
|
||||||
|
\ 0.6 1 April 2012 Tests placed in the public domain.
|
||||||
|
\ SAVE-INPUT & RESTORE-INPUT tests, position
|
||||||
|
\ of T{ moved so that tests work with ttester.fs
|
||||||
|
\ CONVERT test deleted - obsolete word removed from Forth 200X
|
||||||
|
\ IMMEDIATE VALUEs tested
|
||||||
|
\ RECURSE with :NONAME tested
|
||||||
|
\ PARSE and .( tested
|
||||||
|
\ Parsing behaviour of C" added
|
||||||
|
\ 0.5 14 September 2011 Removed the double [ELSE] from the
|
||||||
|
\ initial SAVE-INPUT & RESTORE-INPUT test
|
||||||
|
\ 0.4 30 November 2009 max-int replaced with max-intx to
|
||||||
|
\ avoid redefinition warnings.
|
||||||
|
\ 0.3 6 March 2009 { and } replaced with T{ and }T
|
||||||
|
\ CONVERT test now independent of cell size
|
||||||
|
\ 0.2 20 April 2007 ANS Forth words changed to upper case
|
||||||
|
\ Tests qd3 to qd6 by Reinhold Straub
|
||||||
|
\ 0.1 Oct 2006 First version released
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
\ The tests are based on John Hayes test program for the core word set
|
||||||
|
|
||||||
|
\ Words tested in this file are:
|
||||||
|
\ .( .R 0<> 0> 2>R 2R> 2R@ :NONAME <> ?DO AGAIN C" CASE COMPILE, ENDCASE
|
||||||
|
\ ENDOF ERASE FALSE HEX MARKER NIP OF PAD PARSE PICK REFILL
|
||||||
|
\ RESTORE-INPUT ROLL SAVE-INPUT SOURCE-ID TO TRUE TUCK U.R U> UNUSED
|
||||||
|
\ VALUE WITHIN [COMPILE]
|
||||||
|
|
||||||
|
\ Words not tested or partially tested:
|
||||||
|
\ \ because it has been extensively used already and is, hence, unnecessary
|
||||||
|
\ REFILL and SOURCE-ID from the user input device which are not possible
|
||||||
|
\ when testing from a file such as this one
|
||||||
|
\ UNUSED (partially tested) as the value returned is system dependent
|
||||||
|
\ Obsolescent words #TIB CONVERT EXPECT QUERY SPAN TIB as they have been
|
||||||
|
\ removed from the Forth 2012 standard
|
||||||
|
|
||||||
|
\ Results from words that output to the user output device have to visually
|
||||||
|
\ checked for correctness. These are .R U.R .(
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
\ Assumptions & dependencies:
|
||||||
|
\ - tester.fr (or ttester.fs), errorreport.fth and utilities.fth have been
|
||||||
|
\ included prior to this file
|
||||||
|
\ - the Core word set available
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING Core Extension words
|
||||||
|
|
||||||
|
DECIMAL
|
||||||
|
|
||||||
|
TESTING TRUE FALSE
|
||||||
|
|
||||||
|
T{ TRUE -> 0 INVERT }T
|
||||||
|
T{ FALSE -> 0 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING <> U> (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ 0 0 <> -> FALSE }T
|
||||||
|
T{ 1 1 <> -> FALSE }T
|
||||||
|
T{ -1 -1 <> -> FALSE }T
|
||||||
|
T{ 1 0 <> -> TRUE }T
|
||||||
|
T{ -1 0 <> -> TRUE }T
|
||||||
|
T{ 0 1 <> -> TRUE }T
|
||||||
|
T{ 0 -1 <> -> TRUE }T
|
||||||
|
|
||||||
|
T{ 0 1 U> -> FALSE }T
|
||||||
|
T{ 1 2 U> -> FALSE }T
|
||||||
|
T{ 0 MID-UINT U> -> FALSE }T
|
||||||
|
T{ 0 MAX-UINT U> -> FALSE }T
|
||||||
|
T{ MID-UINT MAX-UINT U> -> FALSE }T
|
||||||
|
T{ 0 0 U> -> FALSE }T
|
||||||
|
T{ 1 1 U> -> FALSE }T
|
||||||
|
T{ 1 0 U> -> TRUE }T
|
||||||
|
T{ 2 1 U> -> TRUE }T
|
||||||
|
T{ MID-UINT 0 U> -> TRUE }T
|
||||||
|
T{ MAX-UINT 0 U> -> TRUE }T
|
||||||
|
T{ MAX-UINT MID-UINT U> -> TRUE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING 0<> 0> (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ 0 0<> -> FALSE }T
|
||||||
|
T{ 1 0<> -> TRUE }T
|
||||||
|
T{ 2 0<> -> TRUE }T
|
||||||
|
T{ -1 0<> -> TRUE }T
|
||||||
|
T{ MAX-UINT 0<> -> TRUE }T
|
||||||
|
T{ MIN-INT 0<> -> TRUE }T
|
||||||
|
T{ MAX-INT 0<> -> TRUE }T
|
||||||
|
|
||||||
|
T{ 0 0> -> FALSE }T
|
||||||
|
T{ -1 0> -> FALSE }T
|
||||||
|
T{ MIN-INT 0> -> FALSE }T
|
||||||
|
T{ 1 0> -> TRUE }T
|
||||||
|
T{ MAX-INT 0> -> TRUE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING NIP TUCK ROLL PICK (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ 1 2 NIP -> 2 }T
|
||||||
|
T{ 1 2 3 NIP -> 1 3 }T
|
||||||
|
|
||||||
|
T{ 1 2 TUCK -> 2 1 2 }T
|
||||||
|
T{ 1 2 3 TUCK -> 1 3 2 3 }T
|
||||||
|
|
||||||
|
T{ : RO5 100 200 300 400 500 ; -> }T
|
||||||
|
T{ RO5 3 ROLL -> 100 300 400 500 200 }T
|
||||||
|
T{ RO5 2 ROLL -> RO5 ROT }T
|
||||||
|
T{ RO5 1 ROLL -> RO5 SWAP }T
|
||||||
|
T{ RO5 0 ROLL -> RO5 }T
|
||||||
|
|
||||||
|
T{ RO5 2 PICK -> 100 200 300 400 500 300 }T
|
||||||
|
T{ RO5 1 PICK -> RO5 OVER }T
|
||||||
|
T{ RO5 0 PICK -> RO5 DUP }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING 2>R 2R@ 2R> (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ : RR0 2>R 100 R> R> ; -> }T
|
||||||
|
T{ 300 400 RR0 -> 100 400 300 }T
|
||||||
|
T{ 200 300 400 RR0 -> 200 100 400 300 }T
|
||||||
|
|
||||||
|
T{ : RR1 2>R 100 2R@ R> R> ; -> }T
|
||||||
|
T{ 300 400 RR1 -> 100 300 400 400 300 }T
|
||||||
|
T{ 200 300 400 RR1 -> 200 100 300 400 400 300 }T
|
||||||
|
|
||||||
|
T{ : RR2 2>R 100 2R> ; -> }T
|
||||||
|
T{ 300 400 RR2 -> 100 300 400 }T
|
||||||
|
T{ 200 300 400 RR2 -> 200 100 300 400 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING HEX (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ BASE @ HEX BASE @ DECIMAL BASE @ - SWAP BASE ! -> 6 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING WITHIN (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ 0 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 0 MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ 0 0 MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ 0 0 MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ 0 MID-UINT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT+1 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ 0 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MAX-UINT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MAX-UINT MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ 0 MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ 0 MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT 0 MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT 0 MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT 0 MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT MID-UINT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MID-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT MID-UINT MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT MID-UINT+1 0 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MID-UINT+1 MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MAX-UINT 0 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MAX-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 0 MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 0 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 0 MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT+1 0 WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MID-UINT+1 MAX-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MID-UINT+1 MAX-UINT 0 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MAX-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MAX-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MID-UINT+1 MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT 0 MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT 0 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT 0 MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MID-UINT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MID-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MID-UINT+1 0 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-UINT MAX-UINT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MAX-UINT MID-UINT WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||||
|
|
||||||
|
T{ MIN-INT MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT MIN-INT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT MIN-INT 1 WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT 0 MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 0 1 WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 0 MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 1 MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 1 0 WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT 1 1 WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT 1 MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ MIN-INT MAX-INT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT MAX-INT 1 WITHIN -> TRUE }T
|
||||||
|
T{ MIN-INT MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MIN-INT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MIN-INT 1 WITHIN -> TRUE }T
|
||||||
|
T{ 0 MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ 0 0 MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ 0 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 0 1 WITHIN -> TRUE }T
|
||||||
|
T{ 0 0 MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ 0 1 MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ 0 1 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 1 1 WITHIN -> FALSE }T
|
||||||
|
T{ 0 1 MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ 0 MAX-INT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 0 MAX-INT 1 WITHIN -> TRUE }T
|
||||||
|
T{ 0 MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ 1 MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ 1 MIN-INT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 1 MIN-INT 1 WITHIN -> FALSE }T
|
||||||
|
T{ 1 MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ 1 0 MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ 1 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ 1 0 1 WITHIN -> FALSE }T
|
||||||
|
T{ 1 0 MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ 1 1 MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ 1 1 0 WITHIN -> TRUE }T
|
||||||
|
T{ 1 1 1 WITHIN -> FALSE }T
|
||||||
|
T{ 1 1 MAX-INT WITHIN -> TRUE }T
|
||||||
|
T{ 1 MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ 1 MAX-INT 0 WITHIN -> FALSE }T
|
||||||
|
T{ 1 MAX-INT 1 WITHIN -> FALSE }T
|
||||||
|
T{ 1 MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT MIN-INT 0 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT MIN-INT 1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT MIN-INT MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT 0 MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT 0 0 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT 0 1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT 0 MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT 1 MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT 1 0 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT 1 1 WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT 1 MAX-INT WITHIN -> FALSE }T
|
||||||
|
T{ MAX-INT MAX-INT MIN-INT WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT MAX-INT 0 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT MAX-INT 1 WITHIN -> TRUE }T
|
||||||
|
T{ MAX-INT MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING UNUSED (contributed by James Bowman & Peter Knaggs)
|
||||||
|
|
||||||
|
VARIABLE UNUSED0
|
||||||
|
T{ UNUSED DROP -> }T
|
||||||
|
T{ ALIGN UNUSED UNUSED0 ! 0 , UNUSED CELL+ UNUSED0 @ = -> TRUE }T
|
||||||
|
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ =
|
||||||
|
-> TRUE }T \ aligned -> unaligned
|
||||||
|
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ = -> TRUE }T \ unaligned -> ?
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING AGAIN (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ : AG0 701 BEGIN DUP 7 MOD 0= IF EXIT THEN 1+ AGAIN ; -> }T
|
||||||
|
T{ AG0 -> 707 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING MARKER (contributed by James Bowman)
|
||||||
|
|
||||||
|
T{ : MA? BL WORD FIND NIP 0<> ; -> }T
|
||||||
|
T{ MARKER MA0 -> }T
|
||||||
|
T{ : MA1 111 ; -> }T
|
||||||
|
T{ MARKER MA2 -> }T
|
||||||
|
T{ : MA1 222 ; -> }T
|
||||||
|
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE TRUE }T
|
||||||
|
T{ MA1 MA2 MA1 -> 222 111 }T
|
||||||
|
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE FALSE }T
|
||||||
|
T{ MA0 -> }T
|
||||||
|
T{ MA? MA0 MA? MA1 MA? MA2 -> FALSE FALSE FALSE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING ?DO
|
||||||
|
|
||||||
|
: QD ?DO I LOOP ;
|
||||||
|
T{ 789 789 QD -> }T
|
||||||
|
T{ -9876 -9876 QD -> }T
|
||||||
|
T{ 5 0 QD -> 0 1 2 3 4 }T
|
||||||
|
|
||||||
|
: QD1 ?DO I 10 +LOOP ;
|
||||||
|
T{ 50 1 QD1 -> 1 11 21 31 41 }T
|
||||||
|
T{ 50 0 QD1 -> 0 10 20 30 40 }T
|
||||||
|
|
||||||
|
: QD2 ?DO I 3 > IF LEAVE ELSE I THEN LOOP ;
|
||||||
|
T{ 5 -1 QD2 -> -1 0 1 2 3 }T
|
||||||
|
|
||||||
|
: QD3 ?DO I 1 +LOOP ;
|
||||||
|
T{ 4 4 QD3 -> }T
|
||||||
|
T{ 4 1 QD3 -> 1 2 3 }T
|
||||||
|
T{ 2 -1 QD3 -> -1 0 1 }T
|
||||||
|
|
||||||
|
: QD4 ?DO I -1 +LOOP ;
|
||||||
|
T{ 4 4 QD4 -> }T
|
||||||
|
T{ 1 4 QD4 -> 4 3 2 1 }T
|
||||||
|
T{ -1 2 QD4 -> 2 1 0 -1 }T
|
||||||
|
|
||||||
|
: QD5 ?DO I -10 +LOOP ;
|
||||||
|
T{ 1 50 QD5 -> 50 40 30 20 10 }T
|
||||||
|
T{ 0 50 QD5 -> 50 40 30 20 10 0 }T
|
||||||
|
T{ -25 10 QD5 -> 10 0 -10 -20 }T
|
||||||
|
|
||||||
|
VARIABLE ITERS
|
||||||
|
VARIABLE INCRMNT
|
||||||
|
|
||||||
|
: QD6 ( limit start increment -- )
|
||||||
|
INCRMNT !
|
||||||
|
0 ITERS !
|
||||||
|
?DO
|
||||||
|
1 ITERS +!
|
||||||
|
I
|
||||||
|
ITERS @ 6 = IF LEAVE THEN
|
||||||
|
INCRMNT @
|
||||||
|
+LOOP ITERS @
|
||||||
|
;
|
||||||
|
|
||||||
|
T{ 4 4 -1 QD6 -> 0 }T
|
||||||
|
T{ 1 4 -1 QD6 -> 4 3 2 1 4 }T
|
||||||
|
T{ 4 1 -1 QD6 -> 1 0 -1 -2 -3 -4 6 }T
|
||||||
|
T{ 4 1 0 QD6 -> 1 1 1 1 1 1 6 }T
|
||||||
|
T{ 0 0 0 QD6 -> 0 }T
|
||||||
|
T{ 1 4 0 QD6 -> 4 4 4 4 4 4 6 }T
|
||||||
|
T{ 1 4 1 QD6 -> 4 5 6 7 8 9 6 }T
|
||||||
|
T{ 4 1 1 QD6 -> 1 2 3 3 }T
|
||||||
|
T{ 4 4 1 QD6 -> 0 }T
|
||||||
|
T{ 2 -1 -1 QD6 -> -1 -2 -3 -4 -5 -6 6 }T
|
||||||
|
T{ -1 2 -1 QD6 -> 2 1 0 -1 4 }T
|
||||||
|
T{ 2 -1 0 QD6 -> -1 -1 -1 -1 -1 -1 6 }T
|
||||||
|
T{ -1 2 0 QD6 -> 2 2 2 2 2 2 6 }T
|
||||||
|
T{ -1 2 1 QD6 -> 2 3 4 5 6 7 6 }T
|
||||||
|
T{ 2 -1 1 QD6 -> -1 0 1 3 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING BUFFER:
|
||||||
|
|
||||||
|
T{ 2 CELLS BUFFER: BUF:TEST -> }T
|
||||||
|
T{ BUF:TEST DUP ALIGNED = -> TRUE }T
|
||||||
|
T{ 111 BUF:TEST ! 222 BUF:TEST CELL+ ! -> }T
|
||||||
|
T{ BUF:TEST @ BUF:TEST CELL+ @ -> 111 222 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING VALUE TO
|
||||||
|
|
||||||
|
T{ 111 VALUE VAL1 -999 VALUE VAL2 -> }T
|
||||||
|
T{ VAL1 -> 111 }T
|
||||||
|
T{ VAL2 -> -999 }T
|
||||||
|
T{ 222 TO VAL1 -> }T
|
||||||
|
T{ VAL1 -> 222 }T
|
||||||
|
T{ : VD1 VAL1 ; -> }T
|
||||||
|
T{ VD1 -> 222 }T
|
||||||
|
T{ : VD2 TO VAL2 ; -> }T
|
||||||
|
T{ VAL2 -> -999 }T
|
||||||
|
T{ -333 VD2 -> }T
|
||||||
|
T{ VAL2 -> -333 }T
|
||||||
|
T{ VAL1 -> 222 }T
|
||||||
|
T{ 444 TO VAL1 -> }T
|
||||||
|
T{ VD1 -> 444 }T
|
||||||
|
T{ 123 VALUE VAL3 IMMEDIATE VAL3 -> 123 }T
|
||||||
|
T{ : VD3 VAL3 LITERAL ; VD3 -> 123 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING CASE OF ENDOF ENDCASE
|
||||||
|
|
||||||
|
: CS1 CASE 1 OF 111 ENDOF
|
||||||
|
2 OF 222 ENDOF
|
||||||
|
3 OF 333 ENDOF
|
||||||
|
>R 999 R>
|
||||||
|
ENDCASE
|
||||||
|
;
|
||||||
|
|
||||||
|
T{ 1 CS1 -> 111 }T
|
||||||
|
T{ 2 CS1 -> 222 }T
|
||||||
|
T{ 3 CS1 -> 333 }T
|
||||||
|
T{ 4 CS1 -> 999 }T
|
||||||
|
|
||||||
|
\ Nested CASE's
|
||||||
|
|
||||||
|
: CS2 >R CASE -1 OF CASE R@ 1 OF 100 ENDOF
|
||||||
|
2 OF 200 ENDOF
|
||||||
|
>R -300 R>
|
||||||
|
ENDCASE
|
||||||
|
ENDOF
|
||||||
|
-2 OF CASE R@ 1 OF -99 ENDOF
|
||||||
|
>R -199 R>
|
||||||
|
ENDCASE
|
||||||
|
ENDOF
|
||||||
|
>R 299 R>
|
||||||
|
ENDCASE R> DROP
|
||||||
|
;
|
||||||
|
|
||||||
|
T{ -1 1 CS2 -> 100 }T
|
||||||
|
T{ -1 2 CS2 -> 200 }T
|
||||||
|
T{ -1 3 CS2 -> -300 }T
|
||||||
|
T{ -2 1 CS2 -> -99 }T
|
||||||
|
T{ -2 2 CS2 -> -199 }T
|
||||||
|
T{ 0 2 CS2 -> 299 }T
|
||||||
|
|
||||||
|
\ Boolean short circuiting using CASE
|
||||||
|
|
||||||
|
: CS3 ( N1 -- N2 )
|
||||||
|
CASE 1- FALSE OF 11 ENDOF
|
||||||
|
1- FALSE OF 22 ENDOF
|
||||||
|
1- FALSE OF 33 ENDOF
|
||||||
|
44 SWAP
|
||||||
|
ENDCASE
|
||||||
|
;
|
||||||
|
|
||||||
|
T{ 1 CS3 -> 11 }T
|
||||||
|
T{ 2 CS3 -> 22 }T
|
||||||
|
T{ 3 CS3 -> 33 }T
|
||||||
|
T{ 9 CS3 -> 44 }T
|
||||||
|
|
||||||
|
\ Empty CASE statements with/without default
|
||||||
|
|
||||||
|
T{ : CS4 CASE ENDCASE ; 1 CS4 -> }T
|
||||||
|
T{ : CS5 CASE 2 SWAP ENDCASE ; 1 CS5 -> 2 }T
|
||||||
|
T{ : CS6 CASE 1 OF ENDOF 2 ENDCASE ; 1 CS6 -> }T
|
||||||
|
T{ : CS7 CASE 3 OF ENDOF 2 ENDCASE ; 1 CS7 -> 1 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING :NONAME RECURSE
|
||||||
|
|
||||||
|
VARIABLE NN1
|
||||||
|
VARIABLE NN2
|
||||||
|
:NONAME 1234 ; NN1 !
|
||||||
|
:NONAME 9876 ; NN2 !
|
||||||
|
T{ NN1 @ EXECUTE -> 1234 }T
|
||||||
|
T{ NN2 @ EXECUTE -> 9876 }T
|
||||||
|
|
||||||
|
T{ :NONAME ( n -- 0,1,..n ) DUP IF DUP >R 1- RECURSE R> THEN ;
|
||||||
|
CONSTANT RN1 -> }T
|
||||||
|
T{ 0 RN1 EXECUTE -> 0 }T
|
||||||
|
T{ 4 RN1 EXECUTE -> 0 1 2 3 4 }T
|
||||||
|
|
||||||
|
:NONAME ( n -- n1 ) \ Multiple RECURSEs in one definition
|
||||||
|
1- DUP
|
||||||
|
CASE 0 OF EXIT ENDOF
|
||||||
|
1 OF 11 SWAP RECURSE ENDOF
|
||||||
|
2 OF 22 SWAP RECURSE ENDOF
|
||||||
|
3 OF 33 SWAP RECURSE ENDOF
|
||||||
|
DROP ABS RECURSE EXIT
|
||||||
|
ENDCASE
|
||||||
|
; CONSTANT RN2
|
||||||
|
|
||||||
|
T{ 1 RN2 EXECUTE -> 0 }T
|
||||||
|
T{ 2 RN2 EXECUTE -> 11 0 }T
|
||||||
|
T{ 4 RN2 EXECUTE -> 33 22 11 0 }T
|
||||||
|
T{ 25 RN2 EXECUTE -> 33 22 11 0 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING C"
|
||||||
|
|
||||||
|
T{ : CQ1 C" 123" ; -> }T
|
||||||
|
T{ CQ1 COUNT EVALUATE -> 123 }T
|
||||||
|
T{ : CQ2 C" " ; -> }T
|
||||||
|
T{ CQ2 COUNT EVALUATE -> }T
|
||||||
|
T{ : CQ3 C" 2345"COUNT EVALUATE ; CQ3 -> 2345 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING COMPILE,
|
||||||
|
|
||||||
|
:NONAME DUP + ; CONSTANT DUP+
|
||||||
|
T{ : Q DUP+ COMPILE, ; -> }T
|
||||||
|
T{ : AS1 [ Q ] ; -> }T
|
||||||
|
T{ 123 AS1 -> 246 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
\ Cannot automatically test SAVE-INPUT and RESTORE-INPUT from a console source
|
||||||
|
|
||||||
|
TESTING SAVE-INPUT and RESTORE-INPUT with a string source
|
||||||
|
|
||||||
|
VARIABLE SI_INC 0 SI_INC !
|
||||||
|
|
||||||
|
: SI1
|
||||||
|
SI_INC @ >IN +!
|
||||||
|
15 SI_INC !
|
||||||
|
;
|
||||||
|
|
||||||
|
: S$ S" SAVE-INPUT SI1 RESTORE-INPUT 12345" ;
|
||||||
|
|
||||||
|
T{ S$ EVALUATE SI_INC @ -> 0 2345 15 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING .(
|
||||||
|
|
||||||
|
CR CR .( Output from .()
|
||||||
|
T{ CR .( You should see -9876: ) -9876 . -> }T
|
||||||
|
T{ CR .( and again: ).( -9876)CR -> }T
|
||||||
|
|
||||||
|
CR CR .( On the next 2 lines you should see First then Second messages:)
|
||||||
|
T{ : DOTP CR ." Second message via ." [CHAR] " EMIT \ Check .( is immediate
|
||||||
|
[ CR ] .( First message via .( ) ; DOTP -> }T
|
||||||
|
CR CR
|
||||||
|
T{ : IMM? BL WORD FIND NIP ; IMM? .( -> 1 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING .R and U.R - has to handle different cell sizes
|
||||||
|
|
||||||
|
\ Create some large integers just below/above MAX and Min INTs
|
||||||
|
MAX-INT 73 79 */ CONSTANT LI1
|
||||||
|
MIN-INT 71 73 */ CONSTANT LI2
|
||||||
|
|
||||||
|
LI1 0 <# #S #> NIP CONSTANT LENLI1
|
||||||
|
|
||||||
|
: (.R&U.R) ( u1 u2 -- ) \ u1 <= string length, u2 is required indentation
|
||||||
|
TUCK + >R
|
||||||
|
LI1 OVER SPACES . CR R@ LI1 SWAP .R CR
|
||||||
|
LI2 OVER SPACES . CR R@ 1+ LI2 SWAP .R CR
|
||||||
|
LI1 OVER SPACES U. CR R@ LI1 SWAP U.R CR
|
||||||
|
LI2 SWAP SPACES U. CR R> LI2 SWAP U.R CR
|
||||||
|
;
|
||||||
|
|
||||||
|
: .R&U.R ( -- )
|
||||||
|
CR ." You should see lines duplicated:" CR
|
||||||
|
." indented by 0 spaces" CR 0 0 (.R&U.R) CR
|
||||||
|
." indented by 0 spaces" CR LENLI1 0 (.R&U.R) CR \ Just fits required width
|
||||||
|
." indented by 5 spaces" CR LENLI1 5 (.R&U.R) CR
|
||||||
|
;
|
||||||
|
|
||||||
|
CR CR .( Output from .R and U.R)
|
||||||
|
T{ .R&U.R -> }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING PAD ERASE
|
||||||
|
\ Must handle different size characters i.e. 1 CHARS >= 1
|
||||||
|
|
||||||
|
84 CONSTANT CHARS/PAD \ Minimum size of PAD in chars
|
||||||
|
CHARS/PAD CHARS CONSTANT AUS/PAD
|
||||||
|
: CHECKPAD ( caddr u ch -- f ) \ f = TRUE if u chars = ch
|
||||||
|
SWAP 0
|
||||||
|
?DO
|
||||||
|
OVER I CHARS + C@ OVER <>
|
||||||
|
IF 2DROP UNLOOP FALSE EXIT THEN
|
||||||
|
LOOP
|
||||||
|
2DROP TRUE
|
||||||
|
;
|
||||||
|
|
||||||
|
T{ PAD DROP -> }T
|
||||||
|
T{ 0 INVERT PAD C! -> }T
|
||||||
|
T{ PAD C@ CONSTANT MAXCHAR -> }T
|
||||||
|
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL MAXCHAR CHECKPAD -> TRUE }T
|
||||||
|
T{ PAD CHARS/PAD 2DUP CHARS ERASE 0 CHECKPAD -> TRUE }T
|
||||||
|
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL PAD 0 ERASE MAXCHAR CHECKPAD -> TRUE }T
|
||||||
|
T{ PAD 43 CHARS + 9 CHARS ERASE -> }T
|
||||||
|
T{ PAD 43 MAXCHAR CHECKPAD -> TRUE }T
|
||||||
|
T{ PAD 43 CHARS + 9 0 CHECKPAD -> TRUE }T
|
||||||
|
T{ PAD 52 CHARS + CHARS/PAD 52 - MAXCHAR CHECKPAD -> TRUE }T
|
||||||
|
|
||||||
|
\ Check that use of WORD and pictured numeric output do not corrupt PAD
|
||||||
|
\ Minimum size of buffers for these are 33 chars and (2*n)+2 chars respectively
|
||||||
|
\ where n is number of bits per cell
|
||||||
|
|
||||||
|
PAD CHARS/PAD ERASE
|
||||||
|
2 BASE !
|
||||||
|
MAX-UINT MAX-UINT <# #S CHAR 1 DUP HOLD HOLD #> 2DROP
|
||||||
|
DECIMAL
|
||||||
|
BL WORD 12345678123456781234567812345678 DROP
|
||||||
|
T{ PAD CHARS/PAD 0 CHECKPAD -> TRUE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING PARSE
|
||||||
|
|
||||||
|
T{ CHAR | PARSE 1234| DUP ROT ROT EVALUATE -> 4 1234 }T
|
||||||
|
T{ CHAR ^ PARSE 23 45 ^ DUP ROT ROT EVALUATE -> 7 23 45 }T
|
||||||
|
: PA1 [CHAR] $ PARSE DUP >R PAD SWAP CHARS MOVE PAD R> ;
|
||||||
|
T{ PA1 3456
|
||||||
|
DUP ROT ROT EVALUATE -> 4 3456 }T
|
||||||
|
T{ CHAR A PARSE A SWAP DROP -> 0 }T
|
||||||
|
T{ CHAR Z PARSE
|
||||||
|
SWAP DROP -> 0 }T
|
||||||
|
T{ CHAR " PARSE 4567 "DUP ROT ROT EVALUATE -> 5 4567 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING PARSE-NAME (Forth 2012)
|
||||||
|
\ Adapted from the PARSE-NAME RfD tests
|
||||||
|
|
||||||
|
T{ PARSE-NAME abcd STR1 S= -> TRUE }T \ No leading spaces
|
||||||
|
T{ PARSE-NAME abcde STR2 S= -> TRUE }T \ Leading spaces
|
||||||
|
|
||||||
|
\ Test empty parse area, new lines are necessary
|
||||||
|
T{ PARSE-NAME
|
||||||
|
NIP -> 0 }T
|
||||||
|
\ Empty parse area with spaces after PARSE-NAME
|
||||||
|
T{ PARSE-NAME
|
||||||
|
NIP -> 0 }T
|
||||||
|
|
||||||
|
T{ : PARSE-NAME-TEST ( "name1" "name2" -- n )
|
||||||
|
PARSE-NAME PARSE-NAME S= ; -> }T
|
||||||
|
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T
|
||||||
|
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T \ Leading spaces
|
||||||
|
T{ PARSE-NAME-TEST abcde abcdf -> FALSE }T
|
||||||
|
T{ PARSE-NAME-TEST abcdf abcde -> FALSE }T
|
||||||
|
T{ PARSE-NAME-TEST abcde abcde
|
||||||
|
-> TRUE }T \ Parse to end of line
|
||||||
|
T{ PARSE-NAME-TEST abcde abcde
|
||||||
|
-> TRUE }T \ Leading and trailing spaces
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING DEFER DEFER@ DEFER! IS ACTION-OF (Forth 2012)
|
||||||
|
\ Adapted from the Forth 200X RfD tests
|
||||||
|
|
||||||
|
T{ DEFER DEFER1 -> }T
|
||||||
|
T{ : MY-DEFER DEFER ; -> }T
|
||||||
|
T{ : IS-DEFER1 IS DEFER1 ; -> }T
|
||||||
|
T{ : ACTION-DEFER1 ACTION-OF DEFER1 ; -> }T
|
||||||
|
T{ : DEF! DEFER! ; -> }T
|
||||||
|
T{ : DEF@ DEFER@ ; -> }T
|
||||||
|
|
||||||
|
T{ ' * ' DEFER1 DEFER! -> }T
|
||||||
|
T{ 2 3 DEFER1 -> 6 }T
|
||||||
|
T{ ' DEFER1 DEFER@ -> ' * }T
|
||||||
|
T{ ' DEFER1 DEF@ -> ' * }T
|
||||||
|
T{ ACTION-OF DEFER1 -> ' * }T
|
||||||
|
T{ ACTION-DEFER1 -> ' * }T
|
||||||
|
T{ ' + IS DEFER1 -> }T
|
||||||
|
T{ 1 2 DEFER1 -> 3 }T
|
||||||
|
T{ ' DEFER1 DEFER@ -> ' + }T
|
||||||
|
T{ ' DEFER1 DEF@ -> ' + }T
|
||||||
|
T{ ACTION-OF DEFER1 -> ' + }T
|
||||||
|
T{ ACTION-DEFER1 -> ' + }T
|
||||||
|
T{ ' - IS-DEFER1 -> }T
|
||||||
|
T{ 1 2 DEFER1 -> -1 }T
|
||||||
|
T{ ' DEFER1 DEFER@ -> ' - }T
|
||||||
|
T{ ' DEFER1 DEF@ -> ' - }T
|
||||||
|
T{ ACTION-OF DEFER1 -> ' - }T
|
||||||
|
T{ ACTION-DEFER1 -> ' - }T
|
||||||
|
|
||||||
|
T{ MY-DEFER DEFER2 -> }T
|
||||||
|
T{ ' DUP IS DEFER2 -> }T
|
||||||
|
T{ 1 DEFER2 -> 1 1 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING HOLDS (Forth 2012)
|
||||||
|
|
||||||
|
: HTEST S" Testing HOLDS" ;
|
||||||
|
: HTEST2 S" works" ;
|
||||||
|
: HTEST3 S" Testing HOLDS works 123" ;
|
||||||
|
T{ 0 0 <# HTEST HOLDS #> HTEST S= -> TRUE }T
|
||||||
|
T{ 123 0 <# #S BL HOLD HTEST2 HOLDS BL HOLD HTEST HOLDS #>
|
||||||
|
HTEST3 S= -> TRUE }T
|
||||||
|
T{ : HLD HOLDS ; -> }T
|
||||||
|
T{ 0 0 <# HTEST HLD #> HTEST S= -> TRUE }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
TESTING REFILL SOURCE-ID
|
||||||
|
\ REFILL and SOURCE-ID from the user input device can't be tested from a file,
|
||||||
|
\ can only be tested from a string via EVALUATE
|
||||||
|
|
||||||
|
T{ : RF1 S" REFILL" EVALUATE ; RF1 -> FALSE }T
|
||||||
|
T{ : SID1 S" SOURCE-ID" EVALUATE ; SID1 -> -1 }T
|
||||||
|
|
||||||
|
\ ------------------------------------------------------------------------------
|
||||||
|
TESTING S\" (Forth 2012 compilation mode)
|
||||||
|
\ Extended the Forth 200X RfD tests
|
||||||
|
\ Note this tests the Core Ext definition of S\" which has unedfined
|
||||||
|
\ interpretation semantics. S\" in interpretation mode is tested in the tests on
|
||||||
|
\ the File-Access word set
|
||||||
|
|
||||||
|
T{ : SSQ1 S\" abc" S" abc" S= ; -> }T \ No escapes
|
||||||
|
T{ SSQ1 -> TRUE }T
|
||||||
|
T{ : SSQ2 S\" " ; SSQ2 SWAP DROP -> 0 }T \ Empty string
|
||||||
|
|
||||||
|
T{ : SSQ3 S\" \a\b\e\f\l\m\q\r\t\v\x0F0\x1Fa\xaBx\z\"\\" ; -> }T
|
||||||
|
T{ SSQ3 SWAP DROP -> 20 }T \ String length
|
||||||
|
T{ SSQ3 DROP C@ -> 7 }T \ \a BEL Bell
|
||||||
|
T{ SSQ3 DROP 1 CHARS + C@ -> 8 }T \ \b BS Backspace
|
||||||
|
T{ SSQ3 DROP 2 CHARS + C@ -> 27 }T \ \e ESC Escape
|
||||||
|
T{ SSQ3 DROP 3 CHARS + C@ -> 12 }T \ \f FF Form feed
|
||||||
|
T{ SSQ3 DROP 4 CHARS + C@ -> 10 }T \ \l LF Line feed
|
||||||
|
T{ SSQ3 DROP 5 CHARS + C@ -> 13 }T \ \m CR of CR/LF pair
|
||||||
|
T{ SSQ3 DROP 6 CHARS + C@ -> 10 }T \ LF of CR/LF pair
|
||||||
|
T{ SSQ3 DROP 7 CHARS + C@ -> 34 }T \ \q " Double Quote
|
||||||
|
T{ SSQ3 DROP 8 CHARS + C@ -> 13 }T \ \r CR Carriage Return
|
||||||
|
T{ SSQ3 DROP 9 CHARS + C@ -> 9 }T \ \t TAB Horizontal Tab
|
||||||
|
T{ SSQ3 DROP 10 CHARS + C@ -> 11 }T \ \v VT Vertical Tab
|
||||||
|
T{ SSQ3 DROP 11 CHARS + C@ -> 15 }T \ \x0F Given Char
|
||||||
|
T{ SSQ3 DROP 12 CHARS + C@ -> 48 }T \ 0 0 Digit follow on
|
||||||
|
T{ SSQ3 DROP 13 CHARS + C@ -> 31 }T \ \x1F Given Char
|
||||||
|
T{ SSQ3 DROP 14 CHARS + C@ -> 97 }T \ a a Hex follow on
|
||||||
|
T{ SSQ3 DROP 15 CHARS + C@ -> 171 }T \ \xaB Insensitive Given Char
|
||||||
|
T{ SSQ3 DROP 16 CHARS + C@ -> 120 }T \ x x Non hex follow on
|
||||||
|
T{ SSQ3 DROP 17 CHARS + C@ -> 0 }T \ \z NUL No Character
|
||||||
|
T{ SSQ3 DROP 18 CHARS + C@ -> 34 }T \ \" " Double Quote
|
||||||
|
T{ SSQ3 DROP 19 CHARS + C@ -> 92 }T \ \\ \ Back Slash
|
||||||
|
|
||||||
|
\ The above does not test \n as this is a system dependent value.
|
||||||
|
\ Check it displays a new line
|
||||||
|
CR .( The next test should display:)
|
||||||
|
CR .( One line...)
|
||||||
|
CR .( another line)
|
||||||
|
T{ : SSQ4 S\" \nOne line...\nanotherLine\n" TYPE ; SSQ4 -> }T
|
||||||
|
|
||||||
|
\ Test bare escapable characters appear as themselves
|
||||||
|
T{ : SSQ5 S\" abeflmnqrtvxz" S" abeflmnqrtvxz" S= ; SSQ5 -> TRUE }T
|
||||||
|
|
||||||
|
T{ : SSQ6 S\" a\""2DROP 1111 ; SSQ6 -> 1111 }T \ Parsing behaviour
|
||||||
|
|
||||||
|
T{ : SSQ7 S\" 111 : SSQ8 S\\\" 222\" EVALUATE ; SSQ8 333" EVALUATE ; -> }T
|
||||||
|
T{ SSQ7 -> 111 222 333 }T
|
||||||
|
T{ : SSQ9 S\" 11 : SSQ10 S\\\" \\x32\\x32\" EVALUATE ; SSQ10 33" EVALUATE ; -> }T
|
||||||
|
T{ SSQ9 -> 11 22 33 }T
|
||||||
|
|
||||||
|
\ -----------------------------------------------------------------------------
|
||||||
|
CORE-EXT-ERRORS SET-ERROR-COUNT
|
||||||
|
|
||||||
|
CR .( End of Core Extension word tests) CR
|
||||||
|
|
||||||
|
|
||||||
66
lib/forth/ans-tests/tester.fr
Normal file
66
lib/forth/ans-tests/tester.fr
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
\ From: John Hayes S1I
|
||||||
|
\ Subject: tester.fr
|
||||||
|
\ Date: Mon, 27 Nov 95 13:10:09 PST
|
||||||
|
|
||||||
|
\ (C) 1995 JOHNS HOPKINS UNIVERSITY / APPLIED PHYSICS LABORATORY
|
||||||
|
\ MAY BE DISTRIBUTED FREELY AS LONG AS THIS COPYRIGHT NOTICE REMAINS.
|
||||||
|
\ VERSION 1.2
|
||||||
|
|
||||||
|
\ 24/11/2015 Replaced Core Ext word <> with = 0=
|
||||||
|
\ 31/3/2015 Variable #ERRORS added and incremented for each error reported.
|
||||||
|
\ 22/1/09 The words { and } have been changed to T{ and }T respectively to
|
||||||
|
\ agree with the Forth 200X file ttester.fs. This avoids clashes with
|
||||||
|
\ locals using { ... } and the FSL use of }
|
||||||
|
|
||||||
|
HEX
|
||||||
|
|
||||||
|
\ SET THE FOLLOWING FLAG TO TRUE FOR MORE VERBOSE OUTPUT; THIS MAY
|
||||||
|
\ ALLOW YOU TO TELL WHICH TEST CAUSED YOUR SYSTEM TO HANG.
|
||||||
|
VARIABLE VERBOSE
|
||||||
|
FALSE VERBOSE !
|
||||||
|
\ TRUE VERBOSE !
|
||||||
|
|
||||||
|
: EMPTY-STACK \ ( ... -- ) EMPTY STACK: HANDLES UNDERFLOWED STACK TOO.
|
||||||
|
DEPTH ?DUP IF DUP 0< IF NEGATE 0 DO 0 LOOP ELSE 0 DO DROP LOOP THEN THEN ;
|
||||||
|
|
||||||
|
VARIABLE #ERRORS 0 #ERRORS !
|
||||||
|
|
||||||
|
: ERROR \ ( C-ADDR U -- ) DISPLAY AN ERROR MESSAGE FOLLOWED BY
|
||||||
|
\ THE LINE THAT HAD THE ERROR.
|
||||||
|
CR TYPE SOURCE TYPE \ DISPLAY LINE CORRESPONDING TO ERROR
|
||||||
|
EMPTY-STACK \ THROW AWAY EVERY THING ELSE
|
||||||
|
#ERRORS @ 1 + #ERRORS !
|
||||||
|
\ QUIT \ *** Uncomment this line to QUIT on an error
|
||||||
|
;
|
||||||
|
|
||||||
|
VARIABLE ACTUAL-DEPTH \ STACK RECORD
|
||||||
|
CREATE ACTUAL-RESULTS 20 CELLS ALLOT
|
||||||
|
|
||||||
|
: T{ \ ( -- ) SYNTACTIC SUGAR.
|
||||||
|
;
|
||||||
|
|
||||||
|
: -> \ ( ... -- ) RECORD DEPTH AND CONTENT OF STACK.
|
||||||
|
DEPTH DUP ACTUAL-DEPTH ! \ RECORD DEPTH
|
||||||
|
?DUP IF \ IF THERE IS SOMETHING ON STACK
|
||||||
|
0 DO ACTUAL-RESULTS I CELLS + ! LOOP \ SAVE THEM
|
||||||
|
THEN ;
|
||||||
|
|
||||||
|
: }T \ ( ... -- ) COMPARE STACK (EXPECTED) CONTENTS WITH SAVED
|
||||||
|
\ (ACTUAL) CONTENTS.
|
||||||
|
DEPTH ACTUAL-DEPTH @ = IF \ IF DEPTHS MATCH
|
||||||
|
DEPTH ?DUP IF \ IF THERE IS SOMETHING ON THE STACK
|
||||||
|
0 DO \ FOR EACH STACK ITEM
|
||||||
|
ACTUAL-RESULTS I CELLS + @ \ COMPARE ACTUAL WITH EXPECTED
|
||||||
|
= 0= IF S" INCORRECT RESULT: " ERROR LEAVE THEN
|
||||||
|
LOOP
|
||||||
|
THEN
|
||||||
|
ELSE \ DEPTH MISMATCH
|
||||||
|
S" WRONG NUMBER OF RESULTS: " ERROR
|
||||||
|
THEN ;
|
||||||
|
|
||||||
|
: TESTING \ ( -- ) TALKING COMMENT.
|
||||||
|
SOURCE VERBOSE @
|
||||||
|
IF DUP >R TYPE CR R> >IN !
|
||||||
|
ELSE >IN ! DROP [CHAR] * EMIT
|
||||||
|
THEN ;
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
170
lib/forth/conformance.sh
Executable file
170
lib/forth/conformance.sh
Executable file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run the Hayes/Gerry-Jackson Core conformance suite against our Forth
|
||||||
|
# interpreter and emit scoreboard.json + scoreboard.md.
|
||||||
|
#
|
||||||
|
# Method:
|
||||||
|
# 1. Preprocess lib/forth/ans-tests/core.fr — strip \ comments, ( ... )
|
||||||
|
# comments, and TESTING … metadata lines.
|
||||||
|
# 2. Split into chunks ending at each `}T` so an error in one test
|
||||||
|
# chunk doesn't abort the run.
|
||||||
|
# 3. Emit an SX file that exposes those chunks as a list.
|
||||||
|
# 4. Run our Forth + hayes-runner under sx_server; record pass/fail/error.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
FORTH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$FORTH_DIR/../.." && pwd)"
|
||||||
|
SX_SERVER="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||||
|
SOURCE="$FORTH_DIR/ans-tests/core.fr"
|
||||||
|
OUT_JSON="$FORTH_DIR/scoreboard.json"
|
||||||
|
OUT_MD="$FORTH_DIR/scoreboard.md"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
PREPROC="$TMP/preproc.forth"
|
||||||
|
CHUNKS_SX="$TMP/chunks.sx"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# 1. preprocess
|
||||||
|
awk '
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
# protect POSTPONE \ so the comment-strip below leaves the literal \ alone
|
||||||
|
gsub(/POSTPONE[ \t]+\\/, "POSTPONE @@BS@@", line)
|
||||||
|
# strip leading/embedded \ line comments (must be \ followed by space or EOL)
|
||||||
|
gsub(/(^|[ \t])\\([ \t].*|$)/, " ", line)
|
||||||
|
# strip ( ... ) block comments that sit on one line
|
||||||
|
gsub(/\([^)]*\)/, " ", line)
|
||||||
|
# strip TESTING … metadata lines (rest of line, incl. bare TESTING)
|
||||||
|
sub(/TESTING([ \t].*)?$/, " ", line)
|
||||||
|
# restore the protected backslash
|
||||||
|
gsub(/@@BS@@/, "\\", line)
|
||||||
|
print line
|
||||||
|
}' "$SOURCE" > "$PREPROC"
|
||||||
|
|
||||||
|
# 2 + 3: split into chunks at each `}T` and emit as a SX file
|
||||||
|
#
|
||||||
|
# Cap chunks via MAX_CHUNKS env (default 638 = full Hayes Core). Lower
|
||||||
|
# it temporarily if later tests regress into an infinite loop while you
|
||||||
|
# are iterating on primitives.
|
||||||
|
MAX_CHUNKS="${MAX_CHUNKS:-638}"
|
||||||
|
|
||||||
|
MAX_CHUNKS="$MAX_CHUNKS" python3 - "$PREPROC" "$CHUNKS_SX" <<'PY'
|
||||||
|
import os, re, sys
|
||||||
|
preproc_path, out_path = sys.argv[1], sys.argv[2]
|
||||||
|
max_chunks = int(os.environ.get("MAX_CHUNKS", "590"))
|
||||||
|
text = open(preproc_path).read()
|
||||||
|
# keep the `}T` attached to the preceding chunk
|
||||||
|
parts = re.split(r'(\}T)', text)
|
||||||
|
chunks = []
|
||||||
|
buf = ""
|
||||||
|
for p in parts:
|
||||||
|
buf += p
|
||||||
|
if p == "}T":
|
||||||
|
s = buf.strip()
|
||||||
|
if s:
|
||||||
|
chunks.append(s)
|
||||||
|
buf = ""
|
||||||
|
if buf.strip():
|
||||||
|
chunks.append(buf.strip())
|
||||||
|
chunks = chunks[:max_chunks]
|
||||||
|
|
||||||
|
def esc(s):
|
||||||
|
s = s.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
s = s.replace('\r', ' ').replace('\n', ' ')
|
||||||
|
s = re.sub(r'\s+', ' ', s).strip()
|
||||||
|
return s
|
||||||
|
|
||||||
|
with open(out_path, "w") as f:
|
||||||
|
f.write("(define hayes-chunks (list\n")
|
||||||
|
for c in chunks:
|
||||||
|
f.write(' "' + esc(c) + '"\n')
|
||||||
|
f.write("))\n\n")
|
||||||
|
f.write("(define\n")
|
||||||
|
f.write(" hayes-run-all\n")
|
||||||
|
f.write(" (fn\n")
|
||||||
|
f.write(" ()\n")
|
||||||
|
f.write(" (hayes-reset!)\n")
|
||||||
|
f.write(" (let ((s (hayes-boot)))\n")
|
||||||
|
f.write(" (for-each (fn (c) (hayes-run-chunk s c)) hayes-chunks))\n")
|
||||||
|
f.write(" (hayes-summary)))\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
# 4. run it
|
||||||
|
OUT=$(printf '(epoch 1)\n(load "lib/forth/runtime.sx")\n(epoch 2)\n(load "lib/forth/reader.sx")\n(epoch 3)\n(load "lib/forth/interpreter.sx")\n(epoch 4)\n(load "lib/forth/compiler.sx")\n(epoch 5)\n(load "lib/forth/hayes-runner.sx")\n(epoch 6)\n(load "%s")\n(epoch 7)\n(eval "(hayes-run-all)")\n' "$CHUNKS_SX" \
|
||||||
|
| timeout 180 "$SX_SERVER" 2>&1)
|
||||||
|
STATUS=$?
|
||||||
|
|
||||||
|
SUMMARY=$(printf '%s\n' "$OUT" | awk '/^\{:pass / {print; exit}')
|
||||||
|
PASS=$(printf '%s' "$SUMMARY" | sed -n 's/.*:pass \([0-9-]*\).*/\1/p')
|
||||||
|
FAIL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:fail \([0-9-]*\).*/\1/p')
|
||||||
|
ERR=$(printf '%s' "$SUMMARY" | sed -n 's/.*:error \([0-9-]*\).*/\1/p')
|
||||||
|
TOTAL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:total \([0-9-]*\).*/\1/p')
|
||||||
|
CHUNK_COUNT=$(grep -c '^ "' "$CHUNKS_SX" || echo 0)
|
||||||
|
TOTAL_AVAILABLE=$(grep -c '}T' "$PREPROC" || echo 0)
|
||||||
|
|
||||||
|
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
if [ -z "$PASS" ]; then
|
||||||
|
PASS=0; FAIL=0; ERR=0; TOTAL=0
|
||||||
|
NOTE="runner halted before completing (timeout or SX error)"
|
||||||
|
else
|
||||||
|
NOTE="completed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PCT=0
|
||||||
|
if [ "$TOTAL" -gt 0 ]; then
|
||||||
|
PCT=$((PASS * 100 / TOTAL))
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$OUT_JSON" <<JSON
|
||||||
|
{
|
||||||
|
"source": "gerryjackson/forth2012-test-suite src/core.fr",
|
||||||
|
"generated_at": "$NOW",
|
||||||
|
"chunks_available": $TOTAL_AVAILABLE,
|
||||||
|
"chunks_fed": $CHUNK_COUNT,
|
||||||
|
"total": $TOTAL,
|
||||||
|
"pass": $PASS,
|
||||||
|
"fail": $FAIL,
|
||||||
|
"error": $ERR,
|
||||||
|
"percent": $PCT,
|
||||||
|
"note": "$NOTE"
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
cat > "$OUT_MD" <<MD
|
||||||
|
# Forth Hayes Core scoreboard
|
||||||
|
|
||||||
|
| metric | value |
|
||||||
|
| ----------------- | ----: |
|
||||||
|
| chunks available | $TOTAL_AVAILABLE |
|
||||||
|
| chunks fed | $CHUNK_COUNT |
|
||||||
|
| total | $TOTAL |
|
||||||
|
| pass | $PASS |
|
||||||
|
| fail | $FAIL |
|
||||||
|
| error | $ERR |
|
||||||
|
| percent | ${PCT}% |
|
||||||
|
|
||||||
|
- **Source**: \`gerryjackson/forth2012-test-suite\` \`src/core.fr\`
|
||||||
|
- **Generated**: $NOW
|
||||||
|
- **Note**: $NOTE
|
||||||
|
|
||||||
|
A "chunk" is any preprocessed segment ending at a \`}T\` (every Hayes test
|
||||||
|
is one chunk, plus the small declaration blocks between tests).
|
||||||
|
The runner catches raised errors at chunk boundaries so one bad chunk
|
||||||
|
does not abort the rest. \`error\` covers chunks that raised; \`fail\`
|
||||||
|
covers tests whose \`->\` / \`}T\` comparison mismatched.
|
||||||
|
|
||||||
|
### Chunk cap
|
||||||
|
|
||||||
|
\`conformance.sh\` processes the first \`\$MAX_CHUNKS\` chunks (default
|
||||||
|
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
|
||||||
|
while iterating on primitives if a regression re-opens an infinite
|
||||||
|
loop in later tests.
|
||||||
|
MD
|
||||||
|
|
||||||
|
echo "$SUMMARY"
|
||||||
|
echo "Scoreboard: $OUT_JSON"
|
||||||
|
echo " $OUT_MD"
|
||||||
|
|
||||||
|
if [ "$STATUS" -ne 0 ] && [ "$TOTAL" -eq 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
158
lib/forth/hayes-runner.sx
Normal file
158
lib/forth/hayes-runner.sx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
;; Hayes conformance test runner.
|
||||||
|
;; Installs T{ -> }T as Forth primitives that snapshot and compare dstack,
|
||||||
|
;; plus stub TESTING / HEX / DECIMAL so the Hayes Core file can stream
|
||||||
|
;; through the interpreter without halting on unsupported metadata words.
|
||||||
|
|
||||||
|
(define hayes-pass 0)
|
||||||
|
(define hayes-fail 0)
|
||||||
|
(define hayes-error 0)
|
||||||
|
(define hayes-start-depth 0)
|
||||||
|
(define hayes-actual (list))
|
||||||
|
(define hayes-actual-set false)
|
||||||
|
(define hayes-failures (list))
|
||||||
|
(define hayes-first-error "")
|
||||||
|
(define hayes-error-hist (dict))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-reset!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(set! hayes-pass 0)
|
||||||
|
(set! hayes-fail 0)
|
||||||
|
(set! hayes-error 0)
|
||||||
|
(set! hayes-start-depth 0)
|
||||||
|
(set! hayes-actual (list))
|
||||||
|
(set! hayes-actual-set false)
|
||||||
|
(set! hayes-failures (list))
|
||||||
|
(set! hayes-first-error "")
|
||||||
|
(set! hayes-error-hist (dict))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-slice
|
||||||
|
(fn
|
||||||
|
(state base)
|
||||||
|
(let
|
||||||
|
((n (- (forth-depth state) base)))
|
||||||
|
(if (<= n 0) (list) (take (get state "dstack") n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-truncate!
|
||||||
|
(fn
|
||||||
|
(state base)
|
||||||
|
(let
|
||||||
|
((n (- (forth-depth state) base)))
|
||||||
|
(when (> n 0) (dict-set! state "dstack" (drop (get state "dstack") n))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-install!
|
||||||
|
(fn
|
||||||
|
(state)
|
||||||
|
(forth-def-prim!
|
||||||
|
state
|
||||||
|
"T{"
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(set! hayes-start-depth (forth-depth s))
|
||||||
|
(set! hayes-actual-set false)
|
||||||
|
(set! hayes-actual (list))))
|
||||||
|
(forth-def-prim!
|
||||||
|
state
|
||||||
|
"->"
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(set! hayes-actual (hayes-slice s hayes-start-depth))
|
||||||
|
(set! hayes-actual-set true)
|
||||||
|
(hayes-truncate! s hayes-start-depth)))
|
||||||
|
(forth-def-prim!
|
||||||
|
state
|
||||||
|
"}T"
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((expected (hayes-slice s hayes-start-depth)))
|
||||||
|
(hayes-truncate! s hayes-start-depth)
|
||||||
|
(if
|
||||||
|
(and hayes-actual-set (= expected hayes-actual))
|
||||||
|
(set! hayes-pass (+ hayes-pass 1))
|
||||||
|
(begin
|
||||||
|
(set! hayes-fail (+ hayes-fail 1))
|
||||||
|
(set!
|
||||||
|
hayes-failures
|
||||||
|
(concat
|
||||||
|
hayes-failures
|
||||||
|
(list
|
||||||
|
(dict
|
||||||
|
"kind"
|
||||||
|
"fail"
|
||||||
|
"expected"
|
||||||
|
(str expected)
|
||||||
|
"actual"
|
||||||
|
(str hayes-actual))))))))))
|
||||||
|
(forth-def-prim! state "TESTING" (fn (s) nil))
|
||||||
|
;; HEX/DECIMAL are real primitives now (runtime.sx) — no stub needed.
|
||||||
|
state))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-boot
|
||||||
|
(fn () (let ((s (forth-boot))) (hayes-install! s) (hayes-reset!) s)))
|
||||||
|
|
||||||
|
;; Run a single preprocessed chunk (string of Forth source) on the shared
|
||||||
|
;; state. Catch any raised error and move on — the chunk boundary is a
|
||||||
|
;; safe resume point.
|
||||||
|
(define
|
||||||
|
hayes-bump-error-key!
|
||||||
|
(fn
|
||||||
|
(err)
|
||||||
|
(let
|
||||||
|
((msg (str err)))
|
||||||
|
(let
|
||||||
|
((space-idx (index-of msg " ")))
|
||||||
|
(let
|
||||||
|
((key
|
||||||
|
(if
|
||||||
|
(> space-idx 0)
|
||||||
|
(substr msg 0 space-idx)
|
||||||
|
msg)))
|
||||||
|
(dict-set!
|
||||||
|
hayes-error-hist
|
||||||
|
key
|
||||||
|
(+ 1 (or (get hayes-error-hist key) 0))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-run-chunk
|
||||||
|
(fn
|
||||||
|
(state src)
|
||||||
|
(guard
|
||||||
|
(err
|
||||||
|
((= 1 1)
|
||||||
|
(begin
|
||||||
|
(set! hayes-error (+ hayes-error 1))
|
||||||
|
(when
|
||||||
|
(= (len hayes-first-error) 0)
|
||||||
|
(set! hayes-first-error (str err)))
|
||||||
|
(hayes-bump-error-key! err)
|
||||||
|
(dict-set! state "dstack" (list))
|
||||||
|
(dict-set! state "rstack" (list))
|
||||||
|
(dict-set! state "compiling" false)
|
||||||
|
(dict-set! state "current-def" nil)
|
||||||
|
(dict-set! state "cstack" (list))
|
||||||
|
(dict-set! state "input" (list)))))
|
||||||
|
(forth-interpret state src))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hayes-summary
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(dict
|
||||||
|
"pass"
|
||||||
|
hayes-pass
|
||||||
|
"fail"
|
||||||
|
hayes-fail
|
||||||
|
"error"
|
||||||
|
hayes-error
|
||||||
|
"total"
|
||||||
|
(+ (+ hayes-pass hayes-fail) hayes-error)
|
||||||
|
"first-error"
|
||||||
|
hayes-first-error
|
||||||
|
"error-hist"
|
||||||
|
hayes-error-hist)))
|
||||||
@@ -5,7 +5,39 @@
|
|||||||
|
|
||||||
(define
|
(define
|
||||||
forth-execute-word
|
forth-execute-word
|
||||||
(fn (state word) (let ((body (get word "body"))) (body state))))
|
(fn
|
||||||
|
(state word)
|
||||||
|
(dict-set! word "call-count" (+ 1 (or (get word "call-count") 0)))
|
||||||
|
(let ((body (get word "body"))) (body state))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-hot-words
|
||||||
|
(fn
|
||||||
|
(state threshold)
|
||||||
|
(forth-hot-walk
|
||||||
|
(keys (get state "dict"))
|
||||||
|
(get state "dict")
|
||||||
|
threshold
|
||||||
|
(list))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-hot-walk
|
||||||
|
(fn
|
||||||
|
(names dict threshold acc)
|
||||||
|
(if
|
||||||
|
(= (len names) 0)
|
||||||
|
acc
|
||||||
|
(let
|
||||||
|
((n (first names)))
|
||||||
|
(let
|
||||||
|
((w (get dict n)))
|
||||||
|
(let
|
||||||
|
((c (or (get w "call-count") 0)))
|
||||||
|
(forth-hot-walk
|
||||||
|
(rest names)
|
||||||
|
dict
|
||||||
|
threshold
|
||||||
|
(if (>= c threshold) (cons (list n c) acc) acc))))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
forth-interpret-token
|
forth-interpret-token
|
||||||
@@ -17,7 +49,7 @@
|
|||||||
(not (nil? w))
|
(not (nil? w))
|
||||||
(forth-execute-word state w)
|
(forth-execute-word state w)
|
||||||
(let
|
(let
|
||||||
((n (forth-parse-number tok (get state "base"))))
|
((n (forth-parse-number tok (get (get state "vars") "base"))))
|
||||||
(if
|
(if
|
||||||
(not (nil? n))
|
(not (nil? n))
|
||||||
(forth-push state n)
|
(forth-push state n)
|
||||||
|
|||||||
1555
lib/forth/runtime.sx
1555
lib/forth/runtime.sx
File diff suppressed because it is too large
Load Diff
12
lib/forth/scoreboard.json
Normal file
12
lib/forth/scoreboard.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"source": "gerryjackson/forth2012-test-suite src/core.fr",
|
||||||
|
"generated_at": "2026-05-05T21:30:21Z",
|
||||||
|
"chunks_available": 638,
|
||||||
|
"chunks_fed": 638,
|
||||||
|
"total": 638,
|
||||||
|
"pass": 632,
|
||||||
|
"fail": 6,
|
||||||
|
"error": 0,
|
||||||
|
"percent": 99,
|
||||||
|
"note": "completed"
|
||||||
|
}
|
||||||
28
lib/forth/scoreboard.md
Normal file
28
lib/forth/scoreboard.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Forth Hayes Core scoreboard
|
||||||
|
|
||||||
|
| metric | value |
|
||||||
|
| ----------------- | ----: |
|
||||||
|
| chunks available | 638 |
|
||||||
|
| chunks fed | 638 |
|
||||||
|
| total | 638 |
|
||||||
|
| pass | 632 |
|
||||||
|
| fail | 6 |
|
||||||
|
| error | 0 |
|
||||||
|
| percent | 99% |
|
||||||
|
|
||||||
|
- **Source**: `gerryjackson/forth2012-test-suite` `src/core.fr`
|
||||||
|
- **Generated**: 2026-05-05T21:30:21Z
|
||||||
|
- **Note**: completed
|
||||||
|
|
||||||
|
A "chunk" is any preprocessed segment ending at a `}T` (every Hayes test
|
||||||
|
is one chunk, plus the small declaration blocks between tests).
|
||||||
|
The runner catches raised errors at chunk boundaries so one bad chunk
|
||||||
|
does not abort the rest. `error` covers chunks that raised; `fail`
|
||||||
|
covers tests whose `->` / `}T` comparison mismatched.
|
||||||
|
|
||||||
|
### Chunk cap
|
||||||
|
|
||||||
|
`conformance.sh` processes the first `$MAX_CHUNKS` chunks (default
|
||||||
|
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
|
||||||
|
while iterating on primitives if a regression re-opens an infinite
|
||||||
|
loop in later tests.
|
||||||
239
lib/forth/tests/test-phase3.sx
Normal file
239
lib/forth/tests/test-phase3.sx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
;; Phase 3 — control flow (IF/ELSE/THEN, BEGIN/UNTIL/WHILE/REPEAT/AGAIN,
|
||||||
|
;; DO/LOOP, return stack). Grows as each control construct lands.
|
||||||
|
|
||||||
|
(define forth-p3-passed 0)
|
||||||
|
(define forth-p3-failed 0)
|
||||||
|
(define forth-p3-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-assert
|
||||||
|
(fn
|
||||||
|
(label expected actual)
|
||||||
|
(if
|
||||||
|
(= expected actual)
|
||||||
|
(set! forth-p3-passed (+ forth-p3-passed 1))
|
||||||
|
(begin
|
||||||
|
(set! forth-p3-failed (+ forth-p3-failed 1))
|
||||||
|
(set!
|
||||||
|
forth-p3-failures
|
||||||
|
(concat
|
||||||
|
forth-p3-failures
|
||||||
|
(list
|
||||||
|
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-check-stack
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let ((r (forth-run src))) (forth-p3-assert label expected (nth r 2)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-if-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF taken (-1)"
|
||||||
|
": Q -1 IF 10 THEN ; Q"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF not taken (0)"
|
||||||
|
": Q 0 IF 10 THEN ; Q"
|
||||||
|
(list))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF with non-zero truthy"
|
||||||
|
": Q 42 IF 10 THEN ; Q"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF ELSE — true branch"
|
||||||
|
": Q -1 IF 10 ELSE 20 THEN ; Q"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF ELSE — false branch"
|
||||||
|
": Q 0 IF 10 ELSE 20 THEN ; Q"
|
||||||
|
(list 20))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF consumes flag"
|
||||||
|
": Q IF 1 ELSE 2 THEN ; 0 Q"
|
||||||
|
(list 2))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"absolute value via IF"
|
||||||
|
": ABS2 DUP 0 < IF NEGATE THEN ; -7 ABS2"
|
||||||
|
(list 7))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"abs leaves positive alone"
|
||||||
|
": ABS2 DUP 0 < IF NEGATE THEN ; 7 ABS2"
|
||||||
|
(list 7))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"sign: negative"
|
||||||
|
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; -3 SIGN"
|
||||||
|
(list -1))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"sign: positive"
|
||||||
|
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; 3 SIGN"
|
||||||
|
(list 1))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"nested IF (both true)"
|
||||||
|
": Q 1 IF 1 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"nested IF (inner false)"
|
||||||
|
": Q 1 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||||
|
(list 20))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"nested IF (outer false)"
|
||||||
|
": Q 0 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||||
|
(list 30))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF before other ops"
|
||||||
|
": Q 1 IF 5 ELSE 6 THEN 2 * ; Q"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"IF in chained def"
|
||||||
|
": POS? 0 > ;
|
||||||
|
: DOUBLE-IF-POS DUP POS? IF 2 * THEN ;
|
||||||
|
3 DOUBLE-IF-POS"
|
||||||
|
(list 6))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"empty then branch"
|
||||||
|
": Q 1 IF THEN 99 ; Q"
|
||||||
|
(list 99))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"empty else branch"
|
||||||
|
": Q 0 IF 99 ELSE THEN ; Q"
|
||||||
|
(list))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"sequential IF blocks"
|
||||||
|
": Q -1 IF 1 THEN -1 IF 2 THEN ; Q"
|
||||||
|
(list 1 2))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-loop-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN UNTIL (countdown to zero)"
|
||||||
|
": CD BEGIN 1- DUP 0 = UNTIL ; 3 CD"
|
||||||
|
(list 0))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN UNTIL — single pass (UNTIL true immediately)"
|
||||||
|
": Q BEGIN -1 UNTIL 42 ; Q"
|
||||||
|
(list 42))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN UNTIL — accumulate sum 1+2+3"
|
||||||
|
": SUM3 0 3 BEGIN TUCK + SWAP 1- DUP 0 = UNTIL DROP ; SUM3"
|
||||||
|
(list 6))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN WHILE REPEAT — triangular sum 5"
|
||||||
|
": TRI 0 5 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||||
|
(list 15))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN WHILE REPEAT — zero iterations"
|
||||||
|
": TRI 0 0 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||||
|
(list 0))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN WHILE REPEAT — one iteration"
|
||||||
|
": TRI 0 1 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||||
|
(list 1))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"nested BEGIN UNTIL"
|
||||||
|
": INNER BEGIN 1- DUP 0 = UNTIL DROP ;
|
||||||
|
: OUTER BEGIN 3 INNER 1- DUP 0 = UNTIL ;
|
||||||
|
2 OUTER"
|
||||||
|
(list 0))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"BEGIN UNTIL after colon prefix"
|
||||||
|
": TEN 10 ;
|
||||||
|
: CD TEN BEGIN 1- DUP 0 = UNTIL ;
|
||||||
|
CD"
|
||||||
|
(list 0))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"WHILE inside IF branch"
|
||||||
|
": Q 1 IF 0 3 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ELSE 99 THEN ; Q"
|
||||||
|
(list 6))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-do-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — simple sum 0..4"
|
||||||
|
": SUM 0 5 0 DO I + LOOP ; SUM"
|
||||||
|
(list 10))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — 10..14 sum using I"
|
||||||
|
": SUM 0 15 10 DO I + LOOP ; SUM"
|
||||||
|
(list 60))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — limit = start runs one pass"
|
||||||
|
": SUM 0 5 5 DO I + LOOP ; SUM"
|
||||||
|
(list 5))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — count iterations"
|
||||||
|
": COUNT 0 4 0 DO 1+ LOOP ; COUNT"
|
||||||
|
(list 4))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — nested, I inner / J outer"
|
||||||
|
": MATRIX 0 3 0 DO 3 0 DO I J + + LOOP LOOP ; MATRIX"
|
||||||
|
(list 18))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP — I used in arithmetic"
|
||||||
|
": DBL 0 5 1 DO I 2 * + LOOP ; DBL"
|
||||||
|
(list 20))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"+LOOP — count by 2"
|
||||||
|
": Q 0 10 0 DO I + 2 +LOOP ; Q"
|
||||||
|
(list 20))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"+LOOP — count by 3"
|
||||||
|
": Q 0 10 0 DO I + 3 +LOOP ; Q"
|
||||||
|
(list 18))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"+LOOP — negative step"
|
||||||
|
": Q 0 0 10 DO I + -1 +LOOP ; Q"
|
||||||
|
(list 55))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"LEAVE — early exit at I=3"
|
||||||
|
": Q 0 10 0 DO I 3 = IF LEAVE THEN I + LOOP ; Q"
|
||||||
|
(list 3))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"LEAVE — in nested loop exits only inner"
|
||||||
|
": Q 0 3 0 DO 5 0 DO I 2 = IF LEAVE THEN I + LOOP LOOP ; Q"
|
||||||
|
(list 3))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"DO LOOP preserves outer stack"
|
||||||
|
": Q 99 5 0 DO I + LOOP ; Q"
|
||||||
|
(list 109))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
">R R>"
|
||||||
|
": Q 7 >R 11 R> ; Q"
|
||||||
|
(list 11 7))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
">R R@ R>"
|
||||||
|
": Q 7 >R R@ R> ; Q"
|
||||||
|
(list 7 7))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"2>R 2R>"
|
||||||
|
": Q 1 2 2>R 99 2R> ; Q"
|
||||||
|
(list 99 1 2))
|
||||||
|
(forth-p3-check-stack
|
||||||
|
"2>R 2R@ 2R>"
|
||||||
|
": Q 3 4 2>R 2R@ 2R> ; Q"
|
||||||
|
(list 3 4 3 4))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p3-run-all
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(set! forth-p3-passed 0)
|
||||||
|
(set! forth-p3-failed 0)
|
||||||
|
(set! forth-p3-failures (list))
|
||||||
|
(forth-p3-if-tests)
|
||||||
|
(forth-p3-loop-tests)
|
||||||
|
(forth-p3-do-tests)
|
||||||
|
(dict
|
||||||
|
"passed"
|
||||||
|
forth-p3-passed
|
||||||
|
"failed"
|
||||||
|
forth-p3-failed
|
||||||
|
"failures"
|
||||||
|
forth-p3-failures)))
|
||||||
268
lib/forth/tests/test-phase4.sx
Normal file
268
lib/forth/tests/test-phase4.sx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
;; Phase 4 — strings + more Core.
|
||||||
|
;; Uses the byte-memory model on state ("mem" dict + "here" cursor).
|
||||||
|
|
||||||
|
(define forth-p4-passed 0)
|
||||||
|
(define forth-p4-failed 0)
|
||||||
|
(define forth-p4-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-assert
|
||||||
|
(fn
|
||||||
|
(label expected actual)
|
||||||
|
(if
|
||||||
|
(= expected actual)
|
||||||
|
(set! forth-p4-passed (+ forth-p4-passed 1))
|
||||||
|
(begin
|
||||||
|
(set! forth-p4-failed (+ forth-p4-failed 1))
|
||||||
|
(set!
|
||||||
|
forth-p4-failures
|
||||||
|
(concat
|
||||||
|
forth-p4-failures
|
||||||
|
(list
|
||||||
|
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-check-output
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let ((r (forth-run src))) (forth-p4-assert label expected (nth r 1)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-check-stack-size
|
||||||
|
(fn
|
||||||
|
(label src expected-n)
|
||||||
|
(let
|
||||||
|
((r (forth-run src)))
|
||||||
|
(forth-p4-assert label expected-n (len (nth r 2))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-check-top
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let
|
||||||
|
((r (forth-run src)))
|
||||||
|
(let
|
||||||
|
((stk (nth r 2)))
|
||||||
|
(forth-p4-assert label expected (nth stk (- (len stk) 1)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-check-typed
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(forth-p4-check-output label (str src " TYPE") expected)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-string-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"S\" + TYPE — hello"
|
||||||
|
"S\" HELLO\""
|
||||||
|
"HELLO")
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"S\" + TYPE — two words"
|
||||||
|
"S\" HELLO WORLD\""
|
||||||
|
"HELLO WORLD")
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"S\" + TYPE — empty"
|
||||||
|
"S\" \""
|
||||||
|
"")
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"S\" + TYPE — single char"
|
||||||
|
"S\" X\""
|
||||||
|
"X")
|
||||||
|
(forth-p4-check-stack-size
|
||||||
|
"S\" pushes (addr len)"
|
||||||
|
"S\" HI\""
|
||||||
|
2)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"S\" length is correct"
|
||||||
|
"S\" HELLO\""
|
||||||
|
5)
|
||||||
|
(forth-p4-check-output
|
||||||
|
".\" prints at interpret time"
|
||||||
|
".\" HELLO\""
|
||||||
|
"HELLO")
|
||||||
|
(forth-p4-check-output
|
||||||
|
".\" in colon def"
|
||||||
|
": GREET .\" HI \" ; GREET GREET"
|
||||||
|
"HI HI ")))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-count-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"C\" + COUNT + TYPE"
|
||||||
|
"C\" ABC\" COUNT"
|
||||||
|
"ABC")
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"C\" then COUNT leaves right len"
|
||||||
|
"C\" HI THERE\" COUNT"
|
||||||
|
"HI THERE")))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-fill-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"FILL overwrites prefix bytes"
|
||||||
|
"S\" ABCDE\" 2DUP DROP 3 65 FILL"
|
||||||
|
"AAADE")
|
||||||
|
(forth-p4-check-typed
|
||||||
|
"BLANK sets spaces"
|
||||||
|
"S\" XYZAB\" 2DUP DROP 3 BLANK"
|
||||||
|
" AB")))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-cmove-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-output
|
||||||
|
"CMOVE copies HELLO forward"
|
||||||
|
": MKH 72 0 C! 69 1 C! 76 2 C! 76 3 C! 79 4 C! ;
|
||||||
|
: T MKH 0 10 5 CMOVE 10 5 TYPE ; T"
|
||||||
|
"HELLO")
|
||||||
|
(forth-p4-check-output
|
||||||
|
"CMOVE> copies overlapping backward"
|
||||||
|
": MKA 65 0 C! 66 1 C! 67 2 C! ;
|
||||||
|
: T MKA 0 1 2 CMOVE> 0 3 TYPE ; T"
|
||||||
|
"AAB")
|
||||||
|
(forth-p4-check-output
|
||||||
|
"MOVE picks direction for overlap"
|
||||||
|
": MKA 65 0 C! 66 1 C! 67 2 C! ;
|
||||||
|
: T MKA 0 1 2 MOVE 0 3 TYPE ; T"
|
||||||
|
"AAB")))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-charplus-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-top
|
||||||
|
"CHAR+ increments"
|
||||||
|
"5 CHAR+"
|
||||||
|
6)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-char-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-top "CHAR A -> 65" "CHAR A" 65)
|
||||||
|
(forth-p4-check-top "CHAR x -> 120" "CHAR x" 120)
|
||||||
|
(forth-p4-check-top "CHAR takes only first char" "CHAR HELLO" 72)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"[CHAR] compiles literal"
|
||||||
|
": AA [CHAR] A ; AA"
|
||||||
|
65)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"[CHAR] reads past IMMEDIATE"
|
||||||
|
": ZZ [CHAR] Z ; ZZ"
|
||||||
|
90)
|
||||||
|
(forth-p4-check-stack-size
|
||||||
|
"[CHAR] doesn't leak at compile time"
|
||||||
|
": FOO [CHAR] A ; "
|
||||||
|
0)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-key-accept-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((r (forth-run "1000 2 ACCEPT")))
|
||||||
|
(let ((stk (nth r 2))) (forth-p4-assert "ACCEPT empty buf -> 0" (list 0) stk)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-shift-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-top "1 0 LSHIFT" "1 0 LSHIFT" 1)
|
||||||
|
(forth-p4-check-top "1 1 LSHIFT" "1 1 LSHIFT" 2)
|
||||||
|
(forth-p4-check-top "1 2 LSHIFT" "1 2 LSHIFT" 4)
|
||||||
|
(forth-p4-check-top "1 15 LSHIFT" "1 15 LSHIFT" 32768)
|
||||||
|
(forth-p4-check-top "1 31 LSHIFT" "1 31 LSHIFT" -2147483648)
|
||||||
|
(forth-p4-check-top "1 0 RSHIFT" "1 0 RSHIFT" 1)
|
||||||
|
(forth-p4-check-top "1 1 RSHIFT" "1 1 RSHIFT" 0)
|
||||||
|
(forth-p4-check-top "2 1 RSHIFT" "2 1 RSHIFT" 1)
|
||||||
|
(forth-p4-check-top "4 2 RSHIFT" "4 2 RSHIFT" 1)
|
||||||
|
(forth-p4-check-top "-1 1 RSHIFT (logical, not arithmetic)" "-1 1 RSHIFT" 2147483647)
|
||||||
|
(forth-p4-check-top "MSB via 1S 1 RSHIFT INVERT" "0 INVERT 1 RSHIFT INVERT" -2147483648)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-sp-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-top "SP@ returns depth (0)" "SP@" 0)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"SP@ after pushes"
|
||||||
|
"1 2 3 SP@ SWAP DROP SWAP DROP SWAP DROP"
|
||||||
|
3)
|
||||||
|
(forth-p4-check-stack-size
|
||||||
|
"SP! truncates"
|
||||||
|
"1 2 3 4 5 2 SP!"
|
||||||
|
2)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"SP! leaves base items intact"
|
||||||
|
"1 2 3 4 5 2 SP!"
|
||||||
|
2)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-base-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-top
|
||||||
|
"BASE default is 10"
|
||||||
|
"BASE @"
|
||||||
|
10)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"HEX switches base to 16"
|
||||||
|
"HEX BASE @"
|
||||||
|
16)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"DECIMAL resets to 10"
|
||||||
|
"HEX DECIMAL BASE @"
|
||||||
|
10)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"HEX parses 10 as 16"
|
||||||
|
"HEX 10"
|
||||||
|
16)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"HEX parses FF as 255"
|
||||||
|
"HEX FF"
|
||||||
|
255)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"DECIMAL parses 10 as 10"
|
||||||
|
"HEX DECIMAL 10"
|
||||||
|
10)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"OCTAL parses 17 as 15"
|
||||||
|
"OCTAL 17"
|
||||||
|
15)
|
||||||
|
(forth-p4-check-top
|
||||||
|
"BASE @ ; 16 BASE ! ; BASE @"
|
||||||
|
"BASE @ 16 BASE ! BASE @ SWAP DROP"
|
||||||
|
16)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-run-all
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(set! forth-p4-passed 0)
|
||||||
|
(set! forth-p4-failed 0)
|
||||||
|
(set! forth-p4-failures (list))
|
||||||
|
(forth-p4-string-tests)
|
||||||
|
(forth-p4-count-tests)
|
||||||
|
(forth-p4-fill-tests)
|
||||||
|
(forth-p4-cmove-tests)
|
||||||
|
(forth-p4-charplus-tests)
|
||||||
|
(forth-p4-char-tests)
|
||||||
|
(forth-p4-key-accept-tests)
|
||||||
|
(forth-p4-base-tests)
|
||||||
|
(forth-p4-shift-tests)
|
||||||
|
(forth-p4-sp-tests)
|
||||||
|
(dict
|
||||||
|
"passed"
|
||||||
|
forth-p4-passed
|
||||||
|
"failed"
|
||||||
|
forth-p4-failed
|
||||||
|
"failures"
|
||||||
|
forth-p4-failures)))
|
||||||
333
lib/forth/tests/test-phase5.sx
Normal file
333
lib/forth/tests/test-phase5.sx
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
;; Phase 5 — Core Extension + memory primitives.
|
||||||
|
|
||||||
|
(define forth-p5-passed 0)
|
||||||
|
(define forth-p5-failed 0)
|
||||||
|
(define forth-p5-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-assert
|
||||||
|
(fn
|
||||||
|
(label expected actual)
|
||||||
|
(if
|
||||||
|
(= expected actual)
|
||||||
|
(set! forth-p5-passed (+ forth-p5-passed 1))
|
||||||
|
(begin
|
||||||
|
(set! forth-p5-failed (+ forth-p5-failed 1))
|
||||||
|
(set!
|
||||||
|
forth-p5-failures
|
||||||
|
(concat
|
||||||
|
forth-p5-failures
|
||||||
|
(list
|
||||||
|
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-check-stack
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 2)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-check-top
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let
|
||||||
|
((r (forth-run src)))
|
||||||
|
(let
|
||||||
|
((stk (nth r 2)))
|
||||||
|
(forth-p5-assert label expected (nth stk (- (len stk) 1)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-create-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top
|
||||||
|
"CREATE pushes HERE-at-creation"
|
||||||
|
"HERE CREATE FOO FOO ="
|
||||||
|
-1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"CREATE + ALLOT advances HERE"
|
||||||
|
"HERE 5 ALLOT HERE SWAP -"
|
||||||
|
5)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"CREATE + , stores cell"
|
||||||
|
"CREATE FOO 42 , FOO @"
|
||||||
|
42)
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"CREATE multiple ,"
|
||||||
|
"CREATE TBL 1 , 2 , 3 , TBL @ TBL CELL+ @ TBL CELL+ CELL+ @"
|
||||||
|
(list 1 2 3))
|
||||||
|
(forth-p5-check-top
|
||||||
|
"C, stores byte"
|
||||||
|
"CREATE B 65 C, 66 C, B C@"
|
||||||
|
65)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-unsigned-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top "1 2 U<" "1 2 U<" -1)
|
||||||
|
(forth-p5-check-top "2 1 U<" "2 1 U<" 0)
|
||||||
|
(forth-p5-check-top "0 1 U<" "0 1 U<" -1)
|
||||||
|
(forth-p5-check-top "-1 1 U< (since -1 unsigned is huge)" "-1 1 U<" 0)
|
||||||
|
(forth-p5-check-top "1 -1 U<" "1 -1 U<" -1)
|
||||||
|
(forth-p5-check-top "1 2 U>" "1 2 U>" 0)
|
||||||
|
(forth-p5-check-top "-1 1 U>" "-1 1 U>" -1)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-2bang-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"2! / 2@"
|
||||||
|
"CREATE X 0 , 0 , 11 22 X 2! X 2@"
|
||||||
|
(list 11 22))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-mixed-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-stack "S>D positive" "5 S>D" (list 5 0))
|
||||||
|
(forth-p5-check-stack "S>D negative" "-5 S>D" (list -5 -1))
|
||||||
|
(forth-p5-check-stack "S>D zero" "0 S>D" (list 0 0))
|
||||||
|
(forth-p5-check-top "D>S keeps low" "5 0 D>S" 5)
|
||||||
|
(forth-p5-check-stack "M* small positive" "3 4 M*" (list 12 0))
|
||||||
|
(forth-p5-check-stack "M* negative" "-3 4 M*" (list -12 -1))
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"M* negative * negative"
|
||||||
|
"-3 -4 M*"
|
||||||
|
(list 12 0))
|
||||||
|
(forth-p5-check-stack "UM* small" "3 4 UM*" (list 12 0))
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"UM/MOD: 100 0 / 5"
|
||||||
|
"100 0 5 UM/MOD"
|
||||||
|
(list 0 20))
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"FM/MOD: -7 / 2 floored"
|
||||||
|
"-7 -1 2 FM/MOD"
|
||||||
|
(list 1 -4))
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"SM/REM: -7 / 2 truncated"
|
||||||
|
"-7 -1 2 SM/REM"
|
||||||
|
(list -1 -3))
|
||||||
|
(forth-p5-check-top "*/ truncated" "7 11 13 */" 5)
|
||||||
|
(forth-p5-check-stack "*/MOD" "7 11 13 */MOD" (list 12 5))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-double-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-stack "D+ small" "5 0 7 0 D+" (list 12 0))
|
||||||
|
(forth-p5-check-stack "D+ negative" "-5 -1 -3 -1 D+" (list -8 -1))
|
||||||
|
(forth-p5-check-stack "D- small" "10 0 3 0 D-" (list 7 0))
|
||||||
|
(forth-p5-check-stack "DNEGATE positive" "5 0 DNEGATE" (list -5 -1))
|
||||||
|
(forth-p5-check-stack "DNEGATE negative" "-5 -1 DNEGATE" (list 5 0))
|
||||||
|
(forth-p5-check-stack "DABS negative" "-7 -1 DABS" (list 7 0))
|
||||||
|
(forth-p5-check-stack "DABS positive" "7 0 DABS" (list 7 0))
|
||||||
|
(forth-p5-check-top "D= equal" "5 0 5 0 D=" -1)
|
||||||
|
(forth-p5-check-top "D= unequal lo" "5 0 7 0 D=" 0)
|
||||||
|
(forth-p5-check-top "D= unequal hi" "5 0 5 1 D=" 0)
|
||||||
|
(forth-p5-check-top "D< lt" "5 0 7 0 D<" -1)
|
||||||
|
(forth-p5-check-top "D< gt" "7 0 5 0 D<" 0)
|
||||||
|
(forth-p5-check-top "D0= zero" "0 0 D0=" -1)
|
||||||
|
(forth-p5-check-top "D0= nonzero" "5 0 D0=" 0)
|
||||||
|
(forth-p5-check-top "D0< neg" "-5 -1 D0<" -1)
|
||||||
|
(forth-p5-check-top "D0< pos" "5 0 D0<" 0)
|
||||||
|
(forth-p5-check-stack "DMAX" "5 0 7 0 DMAX" (list 7 0))
|
||||||
|
(forth-p5-check-stack "DMIN" "5 0 7 0 DMIN" (list 5 0))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-format-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"U. prints with trailing space"
|
||||||
|
"123 U."
|
||||||
|
"123 ")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"<# #S #> TYPE — decimal"
|
||||||
|
"123 0 <# #S #> TYPE"
|
||||||
|
"123")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"<# #S #> TYPE — hex"
|
||||||
|
"255 HEX 0 <# #S #> TYPE"
|
||||||
|
"FF")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"<# # # #> partial"
|
||||||
|
"1234 0 <# # # #> TYPE"
|
||||||
|
"34")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"SIGN holds minus"
|
||||||
|
"<# -1 SIGN -1 SIGN 0 0 #> TYPE"
|
||||||
|
"--")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
".R right-justifies"
|
||||||
|
"42 5 .R"
|
||||||
|
" 42")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
".R negative"
|
||||||
|
"-42 5 .R"
|
||||||
|
" -42")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"U.R"
|
||||||
|
"42 5 U.R"
|
||||||
|
" 42")
|
||||||
|
(forth-p4-check-output-passthrough
|
||||||
|
"HOLD char"
|
||||||
|
"<# 0 0 65 HOLD #> TYPE"
|
||||||
|
"A")))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-dict-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top
|
||||||
|
"EXECUTE via tick"
|
||||||
|
": INC 1+ ; 9 ' INC EXECUTE"
|
||||||
|
10)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"['] inside def"
|
||||||
|
": DUB 2* ; : APPLY ['] DUB EXECUTE ; 5 APPLY"
|
||||||
|
10)
|
||||||
|
(forth-p5-check-top
|
||||||
|
">BODY of CREATE word"
|
||||||
|
"CREATE C 99 , ' C >BODY @"
|
||||||
|
99)
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"WORD parses next token to counted-string"
|
||||||
|
": A 5 ; BL WORD A COUNT TYPE"
|
||||||
|
(list))
|
||||||
|
(forth-p5-check-top
|
||||||
|
"FIND on known word -> non-zero"
|
||||||
|
": A 5 ; BL WORD A FIND SWAP DROP"
|
||||||
|
-1)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-state-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top
|
||||||
|
"STATE @ in interpret mode"
|
||||||
|
"STATE @"
|
||||||
|
0)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"STATE @ via IMMEDIATE inside compile"
|
||||||
|
": GT8 STATE @ ; IMMEDIATE : T GT8 LITERAL ; T"
|
||||||
|
-1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"[ ] LITERAL captures"
|
||||||
|
": SEVEN [ 7 ] LITERAL ; SEVEN"
|
||||||
|
7)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"EVALUATE in interpret mode"
|
||||||
|
"S\" 5 7 +\" EVALUATE"
|
||||||
|
12)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"EVALUATE inside def"
|
||||||
|
": A 100 ; : B S\" A\" EVALUATE ; B"
|
||||||
|
100)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-misc-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top "WITHIN inclusive lower" "3 2 10 WITHIN" -1)
|
||||||
|
(forth-p5-check-top "WITHIN exclusive upper" "10 2 10 WITHIN" 0)
|
||||||
|
(forth-p5-check-top "WITHIN below range" "1 2 10 WITHIN" 0)
|
||||||
|
(forth-p5-check-top "WITHIN at lower" "2 2 10 WITHIN" -1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"EXIT leaves colon-def early"
|
||||||
|
": F 5 EXIT 99 ; F"
|
||||||
|
5)
|
||||||
|
(forth-p5-check-stack
|
||||||
|
"EXIT in IF branch"
|
||||||
|
": F 5 0 IF DROP 99 EXIT THEN ; F"
|
||||||
|
(list 5))
|
||||||
|
(forth-p5-check-top
|
||||||
|
"UNLOOP + EXIT in DO"
|
||||||
|
": SUM 0 10 0 DO I 5 = IF I UNLOOP EXIT THEN LOOP ; SUM"
|
||||||
|
5)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-fa-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top
|
||||||
|
"R/O R/W W/O constants"
|
||||||
|
"R/O R/W W/O + +"
|
||||||
|
3)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"CREATE-FILE returns ior=0"
|
||||||
|
"CREATE PAD 50 ALLOT PAD S\" /tmp/test.fxf\" ROT SWAP CMOVE S\" /tmp/test.fxf\" R/W CREATE-FILE SWAP DROP"
|
||||||
|
0)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"WRITE-FILE then CLOSE"
|
||||||
|
"S\" /tmp/t2.fxf\" R/W CREATE-FILE DROP >R S\" HI\" R@ WRITE-FILE R> CLOSE-FILE +"
|
||||||
|
0)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"OPEN-FILE on unknown path returns ior!=0"
|
||||||
|
"S\" /tmp/nope.fxf\" R/O OPEN-FILE SWAP DROP 0 ="
|
||||||
|
0)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-string-tests
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(forth-p5-check-top "COMPARE equal" "S\" ABC\" S\" ABC\" COMPARE" 0)
|
||||||
|
(forth-p5-check-top "COMPARE less" "S\" ABC\" S\" ABD\" COMPARE" -1)
|
||||||
|
(forth-p5-check-top "COMPARE greater" "S\" ABD\" S\" ABC\" COMPARE" 1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"COMPARE prefix less"
|
||||||
|
"S\" AB\" S\" ABC\" COMPARE"
|
||||||
|
-1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"COMPARE prefix greater"
|
||||||
|
"S\" ABC\" S\" AB\" COMPARE"
|
||||||
|
1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"SEARCH found flag"
|
||||||
|
"S\" HELLO WORLD\" S\" WORLD\" SEARCH"
|
||||||
|
-1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"SEARCH not found flag"
|
||||||
|
"S\" HELLO\" S\" XYZ\" SEARCH"
|
||||||
|
0)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"SEARCH empty needle flag"
|
||||||
|
"S\" HELLO\" S\" \" SEARCH"
|
||||||
|
-1)
|
||||||
|
(forth-p5-check-top
|
||||||
|
"SLITERAL via [ S\" ... \" ]"
|
||||||
|
": A [ S\" HI\" ] SLITERAL ; A SWAP DROP"
|
||||||
|
2)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p4-check-output-passthrough
|
||||||
|
(fn
|
||||||
|
(label src expected)
|
||||||
|
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 1)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
forth-p5-run-all
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(set! forth-p5-passed 0)
|
||||||
|
(set! forth-p5-failed 0)
|
||||||
|
(set! forth-p5-failures (list))
|
||||||
|
(forth-p5-create-tests)
|
||||||
|
(forth-p5-unsigned-tests)
|
||||||
|
(forth-p5-2bang-tests)
|
||||||
|
(forth-p5-mixed-tests)
|
||||||
|
(forth-p5-double-tests)
|
||||||
|
(forth-p5-format-tests)
|
||||||
|
(forth-p5-dict-tests)
|
||||||
|
(forth-p5-state-tests)
|
||||||
|
(forth-p5-misc-tests)
|
||||||
|
(forth-p5-fa-tests)
|
||||||
|
(forth-p5-string-tests)
|
||||||
|
(dict
|
||||||
|
"passed"
|
||||||
|
forth-p5-passed
|
||||||
|
"failed"
|
||||||
|
forth-p5-failed
|
||||||
|
"failures"
|
||||||
|
forth-p5-failures)))
|
||||||
180
lib/guest/hm.sx
Normal file
180
lib/guest/hm.sx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
;; lib/guest/hm.sx — Hindley-Milner type-inference foundations.
|
||||||
|
;;
|
||||||
|
;; Builds on lib/guest/match.sx (terms + unify) and ast.sx (canonical
|
||||||
|
;; AST shapes). This file ships the ALGEBRA — types, schemes, free
|
||||||
|
;; type-vars, generalize / instantiate, substitution composition — so a
|
||||||
|
;; full Algorithm W (or J) can be assembled on top either inside this
|
||||||
|
;; file or in a host-specific consumer (haskell/infer.sx,
|
||||||
|
;; lib/ocaml/types.sx, …).
|
||||||
|
;;
|
||||||
|
;; Per the brief the second consumer for this step is OCaml-on-SX
|
||||||
|
;; Phase 5 (paired sequencing). Until that lands, the algebra is the
|
||||||
|
;; deliverable; the host-flavoured assembly (lambda / app / let
|
||||||
|
;; inference rules with substitution threading) lives in the host.
|
||||||
|
;;
|
||||||
|
;; Types
|
||||||
|
;; -----
|
||||||
|
;; A type is a canonical match.sx term — type variables use mk-var,
|
||||||
|
;; type constructors use mk-ctor:
|
||||||
|
;; (hm-tv NAME) type variable
|
||||||
|
;; (hm-arrow A B) A -> B
|
||||||
|
;; (hm-con NAME ARGS) named n-ary constructor
|
||||||
|
;; (hm-int) / (hm-bool) / (hm-string) primitive constructors
|
||||||
|
;;
|
||||||
|
;; Schemes
|
||||||
|
;; -------
|
||||||
|
;; (hm-scheme VARS TYPE) ∀ VARS . TYPE
|
||||||
|
;; (hm-monotype TYPE) empty quantifier
|
||||||
|
;; (hm-scheme? S) (hm-scheme-vars S) (hm-scheme-type S)
|
||||||
|
;;
|
||||||
|
;; Free type variables
|
||||||
|
;; -------------------
|
||||||
|
;; (hm-ftv TYPE) names occurring in TYPE
|
||||||
|
;; (hm-ftv-scheme S) free names (minus quantifiers)
|
||||||
|
;; (hm-ftv-env ENV) free across an env (name -> scheme)
|
||||||
|
;;
|
||||||
|
;; Substitution
|
||||||
|
;; ------------
|
||||||
|
;; (hm-apply SUBST TYPE) substitute through a type
|
||||||
|
;; (hm-apply-scheme SUBST S) leaves bound vars alone
|
||||||
|
;; (hm-apply-env SUBST ENV)
|
||||||
|
;; (hm-compose S2 S1) apply S1 then S2
|
||||||
|
;;
|
||||||
|
;; Generalize / Instantiate
|
||||||
|
;; ------------------------
|
||||||
|
;; (hm-generalize TYPE ENV) → scheme over ftv(t) - ftv(env)
|
||||||
|
;; (hm-instantiate SCHEME COUNTER) → fresh-var instance
|
||||||
|
;; (hm-fresh-tv COUNTER) → (:var "tN"), bumps COUNTER
|
||||||
|
;;
|
||||||
|
;; Inference (literal only — the rest of Algorithm W lives in the host)
|
||||||
|
;; --------------------------------------------------------------------
|
||||||
|
;; (hm-infer-literal EXPR) → {:subst {} :type T}
|
||||||
|
;;
|
||||||
|
;; A complete Algorithm W consumes this kit by assembling lambda / app
|
||||||
|
;; / let rules in the host language file.
|
||||||
|
|
||||||
|
(define hm-tv (fn (name) (list :var name)))
|
||||||
|
(define hm-con (fn (name args) (list :ctor name args)))
|
||||||
|
(define hm-arrow (fn (a b) (hm-con "->" (list a b))))
|
||||||
|
(define hm-int (fn () (hm-con "Int" (list))))
|
||||||
|
(define hm-bool (fn () (hm-con "Bool" (list))))
|
||||||
|
(define hm-string (fn () (hm-con "String" (list))))
|
||||||
|
|
||||||
|
(define hm-scheme (fn (vars t) (list :scheme vars t)))
|
||||||
|
(define hm-monotype (fn (t) (hm-scheme (list) t)))
|
||||||
|
(define hm-scheme? (fn (s) (and (list? s) (not (empty? s)) (= (first s) :scheme))))
|
||||||
|
(define hm-scheme-vars (fn (s) (nth s 1)))
|
||||||
|
(define hm-scheme-type (fn (s) (nth s 2)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-fresh-tv
|
||||||
|
(fn (counter)
|
||||||
|
(let ((n (first counter)))
|
||||||
|
(begin
|
||||||
|
(set-nth! counter 0 (+ n 1))
|
||||||
|
(hm-tv (str "t" (+ n 1)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-ftv-acc
|
||||||
|
(fn (t acc)
|
||||||
|
(cond
|
||||||
|
((is-var? t)
|
||||||
|
(if (some (fn (n) (= n (var-name t))) acc) acc (cons (var-name t) acc)))
|
||||||
|
((is-ctor? t)
|
||||||
|
(let ((a acc))
|
||||||
|
(begin
|
||||||
|
(for-each (fn (x) (set! a (hm-ftv-acc x a))) (ctor-args t))
|
||||||
|
a)))
|
||||||
|
(:else acc))))
|
||||||
|
|
||||||
|
(define hm-ftv (fn (t) (hm-ftv-acc t (list))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-ftv-scheme
|
||||||
|
(fn (s)
|
||||||
|
(let ((qs (hm-scheme-vars s))
|
||||||
|
(all (hm-ftv (hm-scheme-type s))))
|
||||||
|
(filter (fn (n) (not (some (fn (q) (= q n)) qs))) all))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-ftv-env
|
||||||
|
(fn (env)
|
||||||
|
(let ((acc (list)))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn (k)
|
||||||
|
(for-each
|
||||||
|
(fn (n)
|
||||||
|
(when (not (some (fn (m) (= m n)) acc))
|
||||||
|
(set! acc (cons n acc))))
|
||||||
|
(hm-ftv-scheme (get env k))))
|
||||||
|
(keys env))
|
||||||
|
acc))))
|
||||||
|
|
||||||
|
(define hm-apply (fn (subst t) (walk* t subst)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-apply-scheme
|
||||||
|
(fn (subst s)
|
||||||
|
(let ((qs (hm-scheme-vars s))
|
||||||
|
(d {}))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn (k)
|
||||||
|
(when (not (some (fn (q) (= q k)) qs))
|
||||||
|
(dict-set! d k (get subst k))))
|
||||||
|
(keys subst))
|
||||||
|
(hm-scheme qs (walk* (hm-scheme-type s) d))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-apply-env
|
||||||
|
(fn (subst env)
|
||||||
|
(let ((d {}))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn (k) (dict-set! d k (hm-apply-scheme subst (get env k))))
|
||||||
|
(keys env))
|
||||||
|
d))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-compose
|
||||||
|
(fn (s2 s1)
|
||||||
|
(let ((d {}))
|
||||||
|
(begin
|
||||||
|
(for-each (fn (k) (dict-set! d k (walk* (get s1 k) s2))) (keys s1))
|
||||||
|
(for-each
|
||||||
|
(fn (k) (when (not (has-key? d k)) (dict-set! d k (get s2 k))))
|
||||||
|
(keys s2))
|
||||||
|
d))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-generalize
|
||||||
|
(fn (t env)
|
||||||
|
(let ((tvars (hm-ftv t))
|
||||||
|
(evars (hm-ftv-env env)))
|
||||||
|
(let ((qs (filter (fn (n) (not (some (fn (m) (= m n)) evars))) tvars)))
|
||||||
|
(hm-scheme qs t)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hm-instantiate
|
||||||
|
(fn (s counter)
|
||||||
|
(let ((qs (hm-scheme-vars s))
|
||||||
|
(subst {}))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn (q) (set! subst (assoc subst q (hm-fresh-tv counter))))
|
||||||
|
qs)
|
||||||
|
(walk* (hm-scheme-type s) subst)))))
|
||||||
|
|
||||||
|
;; Literal inference — the only AST kind whose typing rule is closed
|
||||||
|
;; in the kit. Lambda / app / let live in host code so the host's own
|
||||||
|
;; AST conventions stay untouched.
|
||||||
|
(define
|
||||||
|
hm-infer-literal
|
||||||
|
(fn (expr)
|
||||||
|
(let ((v (ast-literal-value expr)))
|
||||||
|
(cond
|
||||||
|
((number? v) {:subst {} :type (hm-int)})
|
||||||
|
((string? v) {:subst {} :type (hm-string)})
|
||||||
|
((boolean? v) {:subst {} :type (hm-bool)})
|
||||||
|
(:else (error (str "hm-infer-literal: unknown kind: " v)))))))
|
||||||
145
lib/guest/layout.sx
Normal file
145
lib/guest/layout.sx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
;; lib/guest/layout.sx — configurable off-side / layout-sensitive lexer.
|
||||||
|
;;
|
||||||
|
;; Inserts virtual open / close / separator tokens based on indentation.
|
||||||
|
;; Configurable enough to encode either the Haskell 98 layout rule (let /
|
||||||
|
;; where / do / of opens a virtual brace at the next token's column) or
|
||||||
|
;; a Python-ish indent / dedent rule (a colon at the end of a line opens
|
||||||
|
;; a block at the next non-blank line's column).
|
||||||
|
;;
|
||||||
|
;; Token shape (input + output)
|
||||||
|
;; ----------------------------
|
||||||
|
;; Each token is a dict {:type :value :line :col …}. The kit reads
|
||||||
|
;; only :type / :value / :line / :col and passes everything else
|
||||||
|
;; through. The input stream MUST be free of newline filler tokens
|
||||||
|
;; (preprocess them away with your tokenizer) — line breaks are detected
|
||||||
|
;; by comparing :line of consecutive tokens.
|
||||||
|
;;
|
||||||
|
;; Config
|
||||||
|
;; ------
|
||||||
|
;; :open-keywords list of strings; a token whose :value matches
|
||||||
|
;; opens a new layout block at the next token's
|
||||||
|
;; column (Haskell: let/where/do/of).
|
||||||
|
;; :open-trailing-fn (fn (tok) -> bool) — alternative trigger that
|
||||||
|
;; fires AFTER the token is emitted. Use for
|
||||||
|
;; Python-style trailing `:`.
|
||||||
|
;; :open-token / :close-token / :sep-token
|
||||||
|
;; templates {:type :value} merged with :line and
|
||||||
|
;; :col when virtual tokens are emitted.
|
||||||
|
;; :explicit-open? (fn (tok) -> bool) — if the next token after a
|
||||||
|
;; trigger satisfies this, suppress virtual layout
|
||||||
|
;; for that block (Haskell: `{`).
|
||||||
|
;; :module-prelude? if true, wrap whole input in an implicit block
|
||||||
|
;; at the first token's column (Haskell yes,
|
||||||
|
;; Python no).
|
||||||
|
;;
|
||||||
|
;; Public entry
|
||||||
|
;; ------------
|
||||||
|
;; (layout-pass cfg tokens) -> tokens with virtual layout inserted.
|
||||||
|
|
||||||
|
(define
|
||||||
|
layout-mk-virtual
|
||||||
|
(fn (template line col)
|
||||||
|
(assoc (assoc template :line line) :col col)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
layout-is-open-kw?
|
||||||
|
(fn (tok open-kws)
|
||||||
|
(and (= (get tok :type) "reserved")
|
||||||
|
(some (fn (k) (= k (get tok :value))) open-kws))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
layout-pass
|
||||||
|
(fn (cfg tokens)
|
||||||
|
(let ((open-kws (get cfg :open-keywords))
|
||||||
|
(trailing-fn (get cfg :open-trailing-fn))
|
||||||
|
(open-tmpl (get cfg :open-token))
|
||||||
|
(close-tmpl (get cfg :close-token))
|
||||||
|
(sep-tmpl (get cfg :sep-token))
|
||||||
|
(mod-prelude? (get cfg :module-prelude?))
|
||||||
|
(expl?-fn (get cfg :explicit-open?))
|
||||||
|
(out (list))
|
||||||
|
(stack (list))
|
||||||
|
(n (len tokens))
|
||||||
|
(i 0)
|
||||||
|
(prev-line -1)
|
||||||
|
(pending-open false)
|
||||||
|
(just-opened false))
|
||||||
|
(define
|
||||||
|
emit-closes-while-greater
|
||||||
|
(fn (col line)
|
||||||
|
(when (and (not (empty? stack)) (> (first stack) col))
|
||||||
|
(do
|
||||||
|
(append! out (layout-mk-virtual close-tmpl line col))
|
||||||
|
(set! stack (rest stack))
|
||||||
|
(emit-closes-while-greater col line)))))
|
||||||
|
(define
|
||||||
|
emit-pending-open
|
||||||
|
(fn (line col)
|
||||||
|
(do
|
||||||
|
(append! out (layout-mk-virtual open-tmpl line col))
|
||||||
|
(set! stack (cons col stack))
|
||||||
|
(set! pending-open false)
|
||||||
|
(set! just-opened true))))
|
||||||
|
(define
|
||||||
|
layout-step
|
||||||
|
(fn ()
|
||||||
|
(when (< i n)
|
||||||
|
(let ((tok (nth tokens i)))
|
||||||
|
(let ((line (get tok :line)) (col (get tok :col)))
|
||||||
|
(cond
|
||||||
|
(pending-open
|
||||||
|
(cond
|
||||||
|
((and (not (= expl?-fn nil)) (expl?-fn tok))
|
||||||
|
(do
|
||||||
|
(set! pending-open false)
|
||||||
|
(append! out tok)
|
||||||
|
(set! prev-line line)
|
||||||
|
(set! i (+ i 1))
|
||||||
|
(layout-step)))
|
||||||
|
(:else
|
||||||
|
(do
|
||||||
|
(emit-pending-open line col)
|
||||||
|
(layout-step)))))
|
||||||
|
(:else
|
||||||
|
(let ((on-fresh-line? (and (> prev-line 0) (> line prev-line))))
|
||||||
|
(do
|
||||||
|
(when on-fresh-line?
|
||||||
|
(let ((stack-before stack))
|
||||||
|
(begin
|
||||||
|
(emit-closes-while-greater col line)
|
||||||
|
(when (and (not (empty? stack))
|
||||||
|
(= (first stack) col)
|
||||||
|
(not just-opened)
|
||||||
|
;; suppress separator if a dedent fired
|
||||||
|
;; — the dedent is itself the separator
|
||||||
|
(= (len stack) (len stack-before)))
|
||||||
|
(append! out (layout-mk-virtual sep-tmpl line col))))))
|
||||||
|
(set! just-opened false)
|
||||||
|
(append! out tok)
|
||||||
|
(set! prev-line line)
|
||||||
|
(set! i (+ i 1))
|
||||||
|
(cond
|
||||||
|
((layout-is-open-kw? tok open-kws)
|
||||||
|
(set! pending-open true))
|
||||||
|
((and (not (= trailing-fn nil)) (trailing-fn tok))
|
||||||
|
(set! pending-open true)))
|
||||||
|
(layout-step))))))))))
|
||||||
|
(begin
|
||||||
|
;; Module prelude: implicit layout block at the first token's column.
|
||||||
|
(when (and mod-prelude? (> n 0))
|
||||||
|
(let ((tok (nth tokens 0)))
|
||||||
|
(do
|
||||||
|
(append! out (layout-mk-virtual open-tmpl (get tok :line) (get tok :col)))
|
||||||
|
(set! stack (cons (get tok :col) stack))
|
||||||
|
(set! just-opened true))))
|
||||||
|
(layout-step)
|
||||||
|
;; EOF: close every remaining block.
|
||||||
|
(define close-rest
|
||||||
|
(fn ()
|
||||||
|
(when (not (empty? stack))
|
||||||
|
(do
|
||||||
|
(append! out (layout-mk-virtual close-tmpl 0 0))
|
||||||
|
(set! stack (rest stack))
|
||||||
|
(close-rest)))))
|
||||||
|
(close-rest)
|
||||||
|
out))))
|
||||||
129
lib/guest/reflective/class-chain.sx
Normal file
129
lib/guest/reflective/class-chain.sx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
;; lib/guest/reflective/class-chain.sx — class inheritance walker.
|
||||||
|
;;
|
||||||
|
;; Extracted from Smalltalk's `st-method-lookup-walk` (single-parent
|
||||||
|
;; class chain for message-send dispatch) and CLOS's `clos-specificity`
|
||||||
|
;; (multi-parent class graph for method-precedence distance). Both walk
|
||||||
|
;; a class-name → parent-name(s) graph applying a probe at each node;
|
||||||
|
;; the cfg adapter normalises single-parent and multi-parent classes
|
||||||
|
;; into a uniform `:parents-of` callback that returns a (possibly
|
||||||
|
;; empty) list of parent class names.
|
||||||
|
;;
|
||||||
|
;; Adapter cfg
|
||||||
|
;; -----------
|
||||||
|
;; :parents-of — fn (class-name) → list of parent class names.
|
||||||
|
;; Empty list = no parents (root). Single-parent guests
|
||||||
|
;; return a 1-element list; multi-parent guests (CLOS)
|
||||||
|
;; may return any number.
|
||||||
|
;; :class? — fn (name) → bool. False short-circuits the walk —
|
||||||
|
;; used to skip non-existent class names mid-chain.
|
||||||
|
;;
|
||||||
|
;; Public API
|
||||||
|
;; (refl-class-chain-find-with CFG CLASS-NAME PROBE)
|
||||||
|
;; Depth-first walk from CLASS-NAME up its parent chain. At each
|
||||||
|
;; class, calls `(probe class-name)`. Returns the first non-nil
|
||||||
|
;; probe result, or nil if no class produces one. Probes evaluate
|
||||||
|
;; left-to-right across siblings in multi-parent guests.
|
||||||
|
;;
|
||||||
|
;; (refl-class-chain-depth-with CFG CLASS-NAME ANCESTOR-NAME)
|
||||||
|
;; Minimum hop count from CLASS-NAME to ANCESTOR-NAME along any
|
||||||
|
;; parent path. CLASS-NAME itself counts as depth 0. Returns nil
|
||||||
|
;; if ANCESTOR-NAME is unreachable.
|
||||||
|
;;
|
||||||
|
;; (refl-class-chain-ancestors-with CFG CLASS-NAME)
|
||||||
|
;; Flat list of all reachable ancestor names in DFS order (no
|
||||||
|
;; dedup; multi-parent guests may want to dedup themselves).
|
||||||
|
;;
|
||||||
|
;; Consumer migrations
|
||||||
|
;; -------------------
|
||||||
|
;; - Smalltalk: see `lib/smalltalk/runtime.sx` — `st-method-lookup-walk`
|
||||||
|
;; becomes a one-line probe through `refl-class-chain-find-with`.
|
||||||
|
;; - CLOS: see `lib/common-lisp/clos.sx` — `clos-specificity` becomes a
|
||||||
|
;; thin wrapper around `refl-class-chain-depth-with`.
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-find-in-parents-with
|
||||||
|
(fn
|
||||||
|
(cfg parents probe)
|
||||||
|
(cond
|
||||||
|
((or (nil? parents) (= (length parents) 0)) nil)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((hit (refl-class-chain-find-with cfg (first parents) probe)))
|
||||||
|
(cond
|
||||||
|
((not (nil? hit)) hit)
|
||||||
|
(:else (refl-find-in-parents-with cfg (rest parents) probe))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-class-chain-find-with
|
||||||
|
(fn
|
||||||
|
(cfg class-name probe)
|
||||||
|
(cond
|
||||||
|
((nil? class-name) nil)
|
||||||
|
((not ((get cfg :class?) class-name)) nil)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((hit (probe class-name)))
|
||||||
|
(cond
|
||||||
|
((not (nil? hit)) hit)
|
||||||
|
(:else
|
||||||
|
(refl-find-in-parents-with
|
||||||
|
cfg
|
||||||
|
((get cfg :parents-of) class-name)
|
||||||
|
probe))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-class-chain-depth-walk
|
||||||
|
(fn
|
||||||
|
(cfg cur target depth)
|
||||||
|
(cond
|
||||||
|
((= cur target) depth)
|
||||||
|
((nil? cur) nil)
|
||||||
|
((not ((get cfg :class?) cur)) nil)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((parents ((get cfg :parents-of) cur)))
|
||||||
|
(let
|
||||||
|
((results (map (fn (p) (refl-class-chain-depth-walk cfg p target (+ depth 1))) parents)))
|
||||||
|
(let
|
||||||
|
((non-nil (filter (fn (x) (not (nil? x))) results)))
|
||||||
|
(cond
|
||||||
|
((or (nil? non-nil) (= (length non-nil) 0)) nil)
|
||||||
|
(:else
|
||||||
|
(reduce
|
||||||
|
(fn (a b) (if (< a b) a b))
|
||||||
|
(first non-nil)
|
||||||
|
(rest non-nil)))))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-class-chain-depth-with
|
||||||
|
(fn
|
||||||
|
(cfg class-name ancestor-name)
|
||||||
|
(refl-class-chain-depth-walk cfg class-name ancestor-name 0)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-class-chain-ancestors-with
|
||||||
|
(fn (cfg class-name) (refl-ancestors-walk cfg class-name (list))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-ancestors-walk
|
||||||
|
(fn
|
||||||
|
(cfg cn acc)
|
||||||
|
(cond
|
||||||
|
((nil? cn) acc)
|
||||||
|
((not ((get cfg :class?) cn)) acc)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((parents ((get cfg :parents-of) cn)))
|
||||||
|
(refl-ancestors-walk-list cfg parents (append acc (list cn))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-ancestors-walk-list
|
||||||
|
(fn
|
||||||
|
(cfg parents acc)
|
||||||
|
(cond
|
||||||
|
((or (nil? parents) (= (length parents) 0)) acc)
|
||||||
|
(:else
|
||||||
|
(refl-ancestors-walk-list
|
||||||
|
cfg
|
||||||
|
(rest parents)
|
||||||
|
(refl-ancestors-walk cfg (first parents) acc))))))
|
||||||
159
lib/guest/reflective/env.sx
Normal file
159
lib/guest/reflective/env.sx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
;; lib/guest/reflective/env.sx — first-class environment kit.
|
||||||
|
;;
|
||||||
|
;; Extracted from Kernel-on-SX (lib/kernel/eval.sx) when Tcl's
|
||||||
|
;; uplevel/upvar machinery (lib/tcl/runtime.sx) materialised as a
|
||||||
|
;; second consumer needing the same scope-chain semantics.
|
||||||
|
;;
|
||||||
|
;; Canonical wire shape
|
||||||
|
;; --------------------
|
||||||
|
;; {:refl-tag :env :bindings DICT :parent ENV-OR-NIL}
|
||||||
|
;;
|
||||||
|
;; - :bindings is a mutable SX dict keyed by symbol name.
|
||||||
|
;; - :parent is either another env or nil (root).
|
||||||
|
;; - Lookup walks the parent chain until a hit or nil.
|
||||||
|
;; - Default cfg uses dict-set! to mutate bindings in place.
|
||||||
|
;;
|
||||||
|
;; Consumers with their own shape (e.g., Tcl's {:level :locals :parent})
|
||||||
|
;; pass an adapter cfg dict — same trick as lib/guest/match.sx's cfg
|
||||||
|
;; for unification over guest-specific term shapes.
|
||||||
|
;;
|
||||||
|
;; Adapter cfg keys
|
||||||
|
;; ----------------
|
||||||
|
;; :bindings-of — fn (scope) → DICT
|
||||||
|
;; :parent-of — fn (scope) → SCOPE-OR-NIL
|
||||||
|
;; :extend — fn (scope) → SCOPE (push a fresh child)
|
||||||
|
;; :bind! — fn (scope name val) → scope (functional or mutable)
|
||||||
|
;; :env? — fn (v) → bool (predicate; cheap shape check)
|
||||||
|
;;
|
||||||
|
;; Public API — canonical shape, mutable, raises on miss
|
||||||
|
;;
|
||||||
|
;; (refl-make-env)
|
||||||
|
;; (refl-extend-env PARENT)
|
||||||
|
;; (refl-env? V)
|
||||||
|
;; (refl-env-bind! ENV NAME VAL)
|
||||||
|
;; (refl-env-has? ENV NAME)
|
||||||
|
;; (refl-env-lookup ENV NAME)
|
||||||
|
;; (refl-env-lookup-or-nil ENV NAME)
|
||||||
|
;;
|
||||||
|
;; Public API — adapter-cfg, any shape
|
||||||
|
;;
|
||||||
|
;; (refl-env-extend-with CFG SCOPE)
|
||||||
|
;; (refl-env-bind!-with CFG SCOPE NAME VAL)
|
||||||
|
;; (refl-env-has?-with CFG SCOPE NAME)
|
||||||
|
;; (refl-env-lookup-with CFG SCOPE NAME)
|
||||||
|
;; (refl-env-lookup-or-nil-with CFG SCOPE NAME)
|
||||||
|
;; (refl-env-find-frame-with CFG SCOPE NAME)
|
||||||
|
;; — returns the scope in the chain that contains NAME (or nil).
|
||||||
|
;; Consumers needing source-frame mutation use this.
|
||||||
|
;;
|
||||||
|
;; (refl-canonical-cfg) — the default cfg, exposed so consumers
|
||||||
|
;; can compare or extend it.
|
||||||
|
|
||||||
|
;; ── Canonical-shape predicates and constructors ─────────────────
|
||||||
|
|
||||||
|
(define refl-env? (fn (v) (and (dict? v) (= (get v :refl-tag) :env))))
|
||||||
|
|
||||||
|
(define refl-make-env (fn () {:parent nil :refl-tag :env :bindings {}}))
|
||||||
|
|
||||||
|
(define refl-extend-env (fn (parent) {:parent parent :refl-tag :env :bindings {}}))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-bind!
|
||||||
|
(fn (env name val) (dict-set! (get env :bindings) name val) env))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-has?
|
||||||
|
(fn
|
||||||
|
(env name)
|
||||||
|
(cond
|
||||||
|
((nil? env) false)
|
||||||
|
((not (refl-env? env)) false)
|
||||||
|
((dict-has? (get env :bindings) name) true)
|
||||||
|
(:else (refl-env-has? (get env :parent) name)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-lookup
|
||||||
|
(fn
|
||||||
|
(env name)
|
||||||
|
(cond
|
||||||
|
((nil? env) (error (str "refl-env-lookup: unbound symbol: " name)))
|
||||||
|
((not (refl-env? env))
|
||||||
|
(error (str "refl-env-lookup: corrupt env: " env)))
|
||||||
|
((dict-has? (get env :bindings) name) (get (get env :bindings) name))
|
||||||
|
(:else (refl-env-lookup (get env :parent) name)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-lookup-or-nil
|
||||||
|
(fn
|
||||||
|
(env name)
|
||||||
|
(cond
|
||||||
|
((nil? env) nil)
|
||||||
|
((not (refl-env? env)) nil)
|
||||||
|
((dict-has? (get env :bindings) name) (get (get env :bindings) name))
|
||||||
|
(:else (refl-env-lookup-or-nil (get env :parent) name)))))
|
||||||
|
|
||||||
|
;; ── Adapter-cfg variants — any wire shape ───────────────────────
|
||||||
|
|
||||||
|
(define refl-env-extend-with (fn (cfg scope) ((get cfg :extend) scope)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-bind!-with
|
||||||
|
(fn (cfg scope name val) ((get cfg :bind!) scope name val)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-has?-with
|
||||||
|
(fn
|
||||||
|
(cfg scope name)
|
||||||
|
(cond
|
||||||
|
((nil? scope) false)
|
||||||
|
((not ((get cfg :env?) scope)) false)
|
||||||
|
((dict-has? ((get cfg :bindings-of) scope) name) true)
|
||||||
|
(:else (refl-env-has?-with cfg ((get cfg :parent-of) scope) name)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-lookup-with
|
||||||
|
(fn
|
||||||
|
(cfg scope name)
|
||||||
|
(cond
|
||||||
|
((nil? scope) (error (str "refl-env-lookup: unbound symbol: " name)))
|
||||||
|
((not ((get cfg :env?) scope))
|
||||||
|
(error (str "refl-env-lookup: corrupt scope: " scope)))
|
||||||
|
((dict-has? ((get cfg :bindings-of) scope) name)
|
||||||
|
(get ((get cfg :bindings-of) scope) name))
|
||||||
|
(:else (refl-env-lookup-with cfg ((get cfg :parent-of) scope) name)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-env-lookup-or-nil-with
|
||||||
|
(fn
|
||||||
|
(cfg scope name)
|
||||||
|
(cond
|
||||||
|
((nil? scope) nil)
|
||||||
|
((not ((get cfg :env?) scope)) nil)
|
||||||
|
((dict-has? ((get cfg :bindings-of) scope) name)
|
||||||
|
(get ((get cfg :bindings-of) scope) name))
|
||||||
|
(:else
|
||||||
|
(refl-env-lookup-or-nil-with cfg ((get cfg :parent-of) scope) name)))))
|
||||||
|
|
||||||
|
;; Returns the SCOPE in the chain that contains NAME, or nil if no
|
||||||
|
;; scope binds it. Consumers (e.g. Smalltalk) use this to mutate the
|
||||||
|
;; binding at its source frame rather than introducing a new shadow
|
||||||
|
;; binding at the current frame. Pairs with `refl-env-lookup-with`
|
||||||
|
;; for callers that need both the value and the defining scope.
|
||||||
|
|
||||||
|
(define refl-env-find-frame-with
|
||||||
|
(fn (cfg scope name)
|
||||||
|
(cond
|
||||||
|
((nil? scope) nil)
|
||||||
|
((not ((get cfg :env?) scope)) nil)
|
||||||
|
((dict-has? ((get cfg :bindings-of) scope) name) scope)
|
||||||
|
(:else
|
||||||
|
(refl-env-find-frame-with cfg ((get cfg :parent-of) scope) name)))))
|
||||||
|
|
||||||
|
(define refl-env-find-frame
|
||||||
|
(fn (env name) (refl-env-find-frame-with refl-canonical-cfg env name)))
|
||||||
|
|
||||||
|
;; ── Default canonical cfg ───────────────────────────────────────
|
||||||
|
;; Exposed so consumers can use it explicitly, compose with it, or
|
||||||
|
;; check adapter-correctness against the canonical implementation.
|
||||||
|
|
||||||
|
(define refl-canonical-cfg {:bind! (fn (e n v) (refl-env-bind! e n v)) :parent-of (fn (e) (get e :parent)) :env? (fn (v) (refl-env? v)) :bindings-of (fn (e) (get e :bindings)) :extend (fn (e) (refl-extend-env e))})
|
||||||
77
lib/guest/reflective/quoting.sx
Normal file
77
lib/guest/reflective/quoting.sx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
;; lib/guest/reflective/quoting.sx — quasiquote walker.
|
||||||
|
;;
|
||||||
|
;; Extracted from Kernel's `knl-quasi-walk` and Scheme's `scm-quasi-walk`,
|
||||||
|
;; which differ only in:
|
||||||
|
;; - the unquote keyword name (Kernel: "$unquote" / "$unquote-splicing";
|
||||||
|
;; Scheme: "unquote" / "unquote-splicing")
|
||||||
|
;; - the host evaluator function (`kernel-eval` vs `scheme-eval`)
|
||||||
|
;;
|
||||||
|
;; Algorithm is identical. Adapter cfg parameterises the two
|
||||||
|
;; language-specific knobs.
|
||||||
|
;;
|
||||||
|
;; Adapter cfg keys
|
||||||
|
;; ----------------
|
||||||
|
;; :unquote-name — string, name of the unquote keyword
|
||||||
|
;; :unquote-splicing-name — string, name of the splice keyword
|
||||||
|
;; :eval — fn (form env) → value
|
||||||
|
;;
|
||||||
|
;; Public API
|
||||||
|
;; (refl-quasi-walk-with CFG FORM ENV)
|
||||||
|
;; Top-level walker. Returns FORM with unquotes evaluated in ENV.
|
||||||
|
;;
|
||||||
|
;; (refl-quasi-walk-list-with CFG FORMS ENV)
|
||||||
|
;; Walks a list of forms, splicing unquote-splicing results inline.
|
||||||
|
;;
|
||||||
|
;; (refl-quasi-list-concat XS YS)
|
||||||
|
;; Pure-SX list append (no host append/append! needed).
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-quasi-list-concat
|
||||||
|
(fn
|
||||||
|
(xs ys)
|
||||||
|
(cond
|
||||||
|
((or (nil? xs) (= (length xs) 0)) ys)
|
||||||
|
(:else (cons (first xs) (refl-quasi-list-concat (rest xs) ys))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-quasi-walk-with
|
||||||
|
(fn
|
||||||
|
(cfg form env)
|
||||||
|
(cond
|
||||||
|
((not (list? form)) form)
|
||||||
|
((= (length form) 0) form)
|
||||||
|
((and (string? (first form)) (= (first form) (get cfg :unquote-name)))
|
||||||
|
(cond
|
||||||
|
((not (= (length form) 2))
|
||||||
|
(error
|
||||||
|
(str (get cfg :unquote-name) ": expects exactly 1 argument")))
|
||||||
|
(:else ((get cfg :eval) (nth form 1) env))))
|
||||||
|
(:else (refl-quasi-walk-list-with cfg form env)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-quasi-walk-list-with
|
||||||
|
(fn
|
||||||
|
(cfg forms env)
|
||||||
|
(cond
|
||||||
|
((or (nil? forms) (= (length forms) 0)) (list))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((head (first forms)))
|
||||||
|
(cond
|
||||||
|
((and (list? head) (= (length head) 2) (string? (first head)) (= (first head) (get cfg :unquote-splicing-name)))
|
||||||
|
(let
|
||||||
|
((spliced ((get cfg :eval) (nth head 1) env)))
|
||||||
|
(cond
|
||||||
|
((not (list? spliced))
|
||||||
|
(error
|
||||||
|
(str
|
||||||
|
(get cfg :unquote-splicing-name)
|
||||||
|
": value must be a list")))
|
||||||
|
(:else
|
||||||
|
(refl-quasi-list-concat
|
||||||
|
spliced
|
||||||
|
(refl-quasi-walk-list-with cfg (rest forms) env))))))
|
||||||
|
(:else
|
||||||
|
(cons
|
||||||
|
(refl-quasi-walk-with cfg head env)
|
||||||
|
(refl-quasi-walk-list-with cfg (rest forms) env)))))))))
|
||||||
50
lib/guest/test-runner.sx
Normal file
50
lib/guest/test-runner.sx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
;; lib/guest/test-runner.sx — per-suite test harness for guest test files.
|
||||||
|
;;
|
||||||
|
;; Across the codebase 142+ test files implement the identical four-form
|
||||||
|
;; boilerplate: `<X>-test-pass`, `<X>-test-fail`, `<X>-test-fails`, and
|
||||||
|
;; an `<X>-test` recording function. Only the prefix differs. This kit
|
||||||
|
;; collapses the boilerplate to a per-suite mutable dict + a recording
|
||||||
|
;; helper, so each test file goes from ~12 lines of harness to ~3:
|
||||||
|
;;
|
||||||
|
;; (define ke-suite (refl-make-test-suite))
|
||||||
|
;; (define ke-test (fn (n a e) (refl-test ke-suite n a e)))
|
||||||
|
;; (define ke-tests-run! (fn () (refl-test-report ke-suite)))
|
||||||
|
;;
|
||||||
|
;; The suite is a mutable dict `{:pass N :fail N :fails LIST}`. Each
|
||||||
|
;; failed assertion appends `{:name NAME :expected EXPECTED :actual ACT}`
|
||||||
|
;; to :fails — same shape every existing harness already produces.
|
||||||
|
;;
|
||||||
|
;; The `:fails` list is mutated in place via `append!`, so callers who
|
||||||
|
;; have a reference to it see the same updates. (Same semantic the
|
||||||
|
;; existing per-suite globals had — just held in the suite dict now.)
|
||||||
|
;;
|
||||||
|
;; Public API
|
||||||
|
;; (refl-make-test-suite) — fresh suite
|
||||||
|
;; (refl-test SUITE NAME ACT EXP) — record one assertion
|
||||||
|
;; (refl-test-report SUITE) — return {:total :passed :failed :fails}
|
||||||
|
;; (refl-test-pass? SUITE) — convenience: all green?
|
||||||
|
;; (refl-test-suite? V) — predicate
|
||||||
|
|
||||||
|
(define refl-make-test-suite (fn () {:fail 0 :pass 0 :fails (list)}))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-test-suite?
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(and (dict? v) (number? (get v :pass)) (number? (get v :fail)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
refl-test
|
||||||
|
(fn
|
||||||
|
(suite name actual expected)
|
||||||
|
(cond
|
||||||
|
((= actual expected)
|
||||||
|
(dict-set! suite :pass (+ (get suite :pass) 1)))
|
||||||
|
(:else
|
||||||
|
(begin
|
||||||
|
(dict-set! suite :fail (+ (get suite :fail) 1))
|
||||||
|
(append! (get suite :fails) {:name name :actual actual :expected expected}))))))
|
||||||
|
|
||||||
|
(define refl-test-report (fn (suite) {:total (+ (get suite :pass) (get suite :fail)) :passed (get suite :pass) :failed (get suite :fail) :fails (get suite :fails)}))
|
||||||
|
|
||||||
|
(define refl-test-pass? (fn (suite) (= (get suite :fail) 0)))
|
||||||
89
lib/guest/tests/hm.sx
Normal file
89
lib/guest/tests/hm.sx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
;; lib/guest/tests/hm.sx — exercises lib/guest/hm.sx algebra.
|
||||||
|
|
||||||
|
(define ghm-test-pass 0)
|
||||||
|
(define ghm-test-fail 0)
|
||||||
|
(define ghm-test-fails (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
ghm-test
|
||||||
|
(fn (name actual expected)
|
||||||
|
(if (= actual expected)
|
||||||
|
(set! ghm-test-pass (+ ghm-test-pass 1))
|
||||||
|
(begin
|
||||||
|
(set! ghm-test-fail (+ ghm-test-fail 1))
|
||||||
|
(append! ghm-test-fails {:name name :expected expected :actual actual})))))
|
||||||
|
|
||||||
|
;; ── Type constructors ─────────────────────────────────────────────
|
||||||
|
(ghm-test "tv" (hm-tv "a") (list :var "a"))
|
||||||
|
(ghm-test "int" (hm-int) (list :ctor "Int" (list)))
|
||||||
|
(ghm-test "arrow" (ctor-head (hm-arrow (hm-int) (hm-bool))) "->")
|
||||||
|
(ghm-test "arrow-args-len" (len (ctor-args (hm-arrow (hm-int) (hm-bool)))) 2)
|
||||||
|
|
||||||
|
;; ── Schemes ───────────────────────────────────────────────────────
|
||||||
|
(ghm-test "scheme-vars" (hm-scheme-vars (hm-scheme (list "a") (hm-tv "a"))) (list "a"))
|
||||||
|
(ghm-test "monotype-vars" (hm-scheme-vars (hm-monotype (hm-int))) (list))
|
||||||
|
(ghm-test "scheme?-yes" (hm-scheme? (hm-monotype (hm-int))) true)
|
||||||
|
(ghm-test "scheme?-no" (hm-scheme? (hm-int)) false)
|
||||||
|
|
||||||
|
;; ── Fresh tyvars ──────────────────────────────────────────────────
|
||||||
|
(ghm-test "fresh-1"
|
||||||
|
(let ((c (list 0))) (var-name (hm-fresh-tv c))) "t1")
|
||||||
|
(ghm-test "fresh-bumps"
|
||||||
|
(let ((c (list 5))) (begin (hm-fresh-tv c) (first c))) 6)
|
||||||
|
|
||||||
|
;; ── Free type variables ──────────────────────────────────────────
|
||||||
|
(ghm-test "ftv-int" (hm-ftv (hm-int)) (list))
|
||||||
|
(ghm-test "ftv-tv" (hm-ftv (hm-tv "a")) (list "a"))
|
||||||
|
(ghm-test "ftv-arrow"
|
||||||
|
(len (hm-ftv (hm-arrow (hm-tv "a") (hm-arrow (hm-tv "b") (hm-tv "a"))))) 2)
|
||||||
|
(ghm-test "ftv-scheme-quantified"
|
||||||
|
(hm-ftv-scheme (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "b")))) (list "b"))
|
||||||
|
(ghm-test "ftv-env"
|
||||||
|
(let ((env (assoc {} "f" (hm-monotype (hm-arrow (hm-tv "x") (hm-tv "y"))))))
|
||||||
|
(len (hm-ftv-env env))) 2)
|
||||||
|
|
||||||
|
;; ── Substitution / apply / compose ───────────────────────────────
|
||||||
|
(ghm-test "apply-tv"
|
||||||
|
(hm-apply (assoc {} "a" (hm-int)) (hm-tv "a")) (hm-int))
|
||||||
|
(ghm-test "apply-arrow"
|
||||||
|
(ctor-head
|
||||||
|
(hm-apply (assoc {} "a" (hm-int))
|
||||||
|
(hm-arrow (hm-tv "a") (hm-tv "b")))) "->")
|
||||||
|
(ghm-test "compose-1-then-2"
|
||||||
|
(var-name
|
||||||
|
(hm-apply
|
||||||
|
(hm-compose (assoc {} "b" (hm-tv "c")) (assoc {} "a" (hm-tv "b")))
|
||||||
|
(hm-tv "a"))) "c")
|
||||||
|
|
||||||
|
;; ── Generalize / Instantiate ─────────────────────────────────────
|
||||||
|
;; forall a. a -> a instantiated twice yields fresh vars each time
|
||||||
|
(ghm-test "generalize-id"
|
||||||
|
(len (hm-scheme-vars (hm-generalize (hm-arrow (hm-tv "a") (hm-tv "a")) {}))) 1)
|
||||||
|
|
||||||
|
(ghm-test "generalize-skips-env"
|
||||||
|
;; ftv(t)={a,b}, ftv(env)={a}, qs={b}
|
||||||
|
(let ((env (assoc {} "x" (hm-monotype (hm-tv "a")))))
|
||||||
|
(len (hm-scheme-vars
|
||||||
|
(hm-generalize (hm-arrow (hm-tv "a") (hm-tv "b")) env)))) 1)
|
||||||
|
|
||||||
|
(ghm-test "instantiate-fresh"
|
||||||
|
(let ((s (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "a"))))
|
||||||
|
(c (list 0)))
|
||||||
|
(let ((t1 (hm-instantiate s c)) (t2 (hm-instantiate s c)))
|
||||||
|
(not (= (var-name (first (ctor-args t1)))
|
||||||
|
(var-name (first (ctor-args t2)))))))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; ── Inference (literal only) ─────────────────────────────────────
|
||||||
|
(ghm-test "infer-int"
|
||||||
|
(ctor-head (get (hm-infer-literal (ast-literal 42)) :type)) "Int")
|
||||||
|
(ghm-test "infer-string"
|
||||||
|
(ctor-head (get (hm-infer-literal (ast-literal "hi")) :type)) "String")
|
||||||
|
(ghm-test "infer-bool"
|
||||||
|
(ctor-head (get (hm-infer-literal (ast-literal true)) :type)) "Bool")
|
||||||
|
|
||||||
|
(define ghm-tests-run!
|
||||||
|
(fn ()
|
||||||
|
{:passed ghm-test-pass
|
||||||
|
:failed ghm-test-fail
|
||||||
|
:total (+ ghm-test-pass ghm-test-fail)}))
|
||||||
180
lib/guest/tests/layout.sx
Normal file
180
lib/guest/tests/layout.sx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
;; lib/guest/tests/layout.sx — synthetic Python-ish off-side fixture.
|
||||||
|
;;
|
||||||
|
;; Exercises lib/guest/layout.sx with a config different from Haskell's
|
||||||
|
;; (no module-prelude, layout opens via trailing `:` not via reserved
|
||||||
|
;; keyword) to prove the kit isn't Haskell-shaped.
|
||||||
|
|
||||||
|
(define glayout-test-pass 0)
|
||||||
|
(define glayout-test-fail 0)
|
||||||
|
(define glayout-test-fails (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
glayout-test
|
||||||
|
(fn (name actual expected)
|
||||||
|
(if (= actual expected)
|
||||||
|
(set! glayout-test-pass (+ glayout-test-pass 1))
|
||||||
|
(begin
|
||||||
|
(set! glayout-test-fail (+ glayout-test-fail 1))
|
||||||
|
(append! glayout-test-fails {:name name :expected expected :actual actual})))))
|
||||||
|
|
||||||
|
;; Convenience: build a token from {type value line col}.
|
||||||
|
(define
|
||||||
|
glayout-tok
|
||||||
|
(fn (ty val line col)
|
||||||
|
{:type ty :value val :line line :col col}))
|
||||||
|
|
||||||
|
;; Project a token list to ((type value) ...) for compact comparison.
|
||||||
|
(define
|
||||||
|
glayout-shape
|
||||||
|
(fn (toks)
|
||||||
|
(map (fn (t) (list (get t :type) (get t :value))) toks)))
|
||||||
|
|
||||||
|
;; ── Haskell-flavour: keyword opens block ─────────────────────────
|
||||||
|
(define
|
||||||
|
glayout-haskell-cfg
|
||||||
|
{:open-keywords (list "let" "where" "do" "of")
|
||||||
|
:open-trailing-fn nil
|
||||||
|
:open-token {:type "vlbrace" :value "{"}
|
||||||
|
:close-token {:type "vrbrace" :value "}"}
|
||||||
|
:sep-token {:type "vsemi" :value ";"}
|
||||||
|
:module-prelude? false
|
||||||
|
:explicit-open? (fn (tok) (= (get tok :type) "lbrace"))})
|
||||||
|
|
||||||
|
;; do
|
||||||
|
;; a
|
||||||
|
;; b
|
||||||
|
;; c ← outside the do-block
|
||||||
|
(glayout-test "haskell-do-block"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
glayout-haskell-cfg
|
||||||
|
(list (glayout-tok "reserved" "do" 1 1)
|
||||||
|
(glayout-tok "ident" "a" 2 3)
|
||||||
|
(glayout-tok "ident" "b" 3 3)
|
||||||
|
(glayout-tok "ident" "c" 4 1))))
|
||||||
|
(list (list "reserved" "do")
|
||||||
|
(list "vlbrace" "{")
|
||||||
|
(list "ident" "a")
|
||||||
|
(list "vsemi" ";")
|
||||||
|
(list "ident" "b")
|
||||||
|
(list "vrbrace" "}")
|
||||||
|
(list "ident" "c")))
|
||||||
|
|
||||||
|
;; Explicit `{` after `do` suppresses virtual layout.
|
||||||
|
(glayout-test "haskell-explicit-brace"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
glayout-haskell-cfg
|
||||||
|
(list (glayout-tok "reserved" "do" 1 1)
|
||||||
|
(glayout-tok "lbrace" "{" 1 4)
|
||||||
|
(glayout-tok "ident" "a" 1 6)
|
||||||
|
(glayout-tok "rbrace" "}" 1 8))))
|
||||||
|
(list (list "reserved" "do")
|
||||||
|
(list "lbrace" "{")
|
||||||
|
(list "ident" "a")
|
||||||
|
(list "rbrace" "}")))
|
||||||
|
|
||||||
|
;; Single-statement do-block on the same line.
|
||||||
|
(glayout-test "haskell-do-inline"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
glayout-haskell-cfg
|
||||||
|
(list (glayout-tok "reserved" "do" 1 1)
|
||||||
|
(glayout-tok "ident" "a" 1 4))))
|
||||||
|
(list (list "reserved" "do")
|
||||||
|
(list "vlbrace" "{")
|
||||||
|
(list "ident" "a")
|
||||||
|
(list "vrbrace" "}")))
|
||||||
|
|
||||||
|
;; Module-prelude: wrap whole input in implicit layout block at first
|
||||||
|
;; tok's column.
|
||||||
|
(glayout-test "haskell-module-prelude"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
(assoc glayout-haskell-cfg :module-prelude? true)
|
||||||
|
(list (glayout-tok "ident" "x" 1 1)
|
||||||
|
(glayout-tok "ident" "y" 2 1)
|
||||||
|
(glayout-tok "ident" "z" 3 1))))
|
||||||
|
(list (list "vlbrace" "{")
|
||||||
|
(list "ident" "x")
|
||||||
|
(list "vsemi" ";")
|
||||||
|
(list "ident" "y")
|
||||||
|
(list "vsemi" ";")
|
||||||
|
(list "ident" "z")
|
||||||
|
(list "vrbrace" "}")))
|
||||||
|
|
||||||
|
;; ── Python-flavour: trailing `:` opens block ─────────────────────
|
||||||
|
(define
|
||||||
|
glayout-python-cfg
|
||||||
|
{:open-keywords (list)
|
||||||
|
:open-trailing-fn (fn (tok) (and (= (get tok :type) "punct")
|
||||||
|
(= (get tok :value) ":")))
|
||||||
|
:open-token {:type "indent" :value "INDENT"}
|
||||||
|
:close-token {:type "dedent" :value "DEDENT"}
|
||||||
|
:sep-token {:type "newline" :value "NEWLINE"}
|
||||||
|
:module-prelude? false
|
||||||
|
:explicit-open? nil})
|
||||||
|
|
||||||
|
;; if x:
|
||||||
|
;; a
|
||||||
|
;; b
|
||||||
|
;; c
|
||||||
|
(glayout-test "python-if-block"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
glayout-python-cfg
|
||||||
|
(list (glayout-tok "reserved" "if" 1 1)
|
||||||
|
(glayout-tok "ident" "x" 1 4)
|
||||||
|
(glayout-tok "punct" ":" 1 5)
|
||||||
|
(glayout-tok "ident" "a" 2 5)
|
||||||
|
(glayout-tok "ident" "b" 3 5)
|
||||||
|
(glayout-tok "ident" "c" 4 1))))
|
||||||
|
(list (list "reserved" "if")
|
||||||
|
(list "ident" "x")
|
||||||
|
(list "punct" ":")
|
||||||
|
(list "indent" "INDENT")
|
||||||
|
(list "ident" "a")
|
||||||
|
(list "newline" "NEWLINE")
|
||||||
|
(list "ident" "b")
|
||||||
|
(list "dedent" "DEDENT")
|
||||||
|
(list "ident" "c")))
|
||||||
|
|
||||||
|
;; Nested Python-style blocks.
|
||||||
|
;; def f():
|
||||||
|
;; if x:
|
||||||
|
;; a
|
||||||
|
;; b
|
||||||
|
(glayout-test "python-nested"
|
||||||
|
(glayout-shape
|
||||||
|
(layout-pass
|
||||||
|
glayout-python-cfg
|
||||||
|
(list (glayout-tok "reserved" "def" 1 1)
|
||||||
|
(glayout-tok "ident" "f" 1 5)
|
||||||
|
(glayout-tok "punct" "(" 1 6)
|
||||||
|
(glayout-tok "punct" ")" 1 7)
|
||||||
|
(glayout-tok "punct" ":" 1 8)
|
||||||
|
(glayout-tok "reserved" "if" 2 5)
|
||||||
|
(glayout-tok "ident" "x" 2 8)
|
||||||
|
(glayout-tok "punct" ":" 2 9)
|
||||||
|
(glayout-tok "ident" "a" 3 9)
|
||||||
|
(glayout-tok "ident" "b" 4 5))))
|
||||||
|
(list (list "reserved" "def")
|
||||||
|
(list "ident" "f")
|
||||||
|
(list "punct" "(")
|
||||||
|
(list "punct" ")")
|
||||||
|
(list "punct" ":")
|
||||||
|
(list "indent" "INDENT")
|
||||||
|
(list "reserved" "if")
|
||||||
|
(list "ident" "x")
|
||||||
|
(list "punct" ":")
|
||||||
|
(list "indent" "INDENT")
|
||||||
|
(list "ident" "a")
|
||||||
|
(list "dedent" "DEDENT")
|
||||||
|
(list "ident" "b")
|
||||||
|
(list "dedent" "DEDENT")))
|
||||||
|
|
||||||
|
(define glayout-tests-run!
|
||||||
|
(fn ()
|
||||||
|
{:passed glayout-test-pass
|
||||||
|
:failed glayout-test-fail
|
||||||
|
:total (+ glayout-test-pass glayout-test-fail)}))
|
||||||
@@ -14,6 +14,8 @@ PRELOADS=(
|
|||||||
lib/haskell/runtime.sx
|
lib/haskell/runtime.sx
|
||||||
lib/haskell/match.sx
|
lib/haskell/match.sx
|
||||||
lib/haskell/eval.sx
|
lib/haskell/eval.sx
|
||||||
|
lib/haskell/map.sx
|
||||||
|
lib/haskell/set.sx
|
||||||
lib/haskell/testlib.sx
|
lib/haskell/testlib.sx
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +38,24 @@ SUITES=(
|
|||||||
"matrix:lib/haskell/tests/program-matrix.sx"
|
"matrix:lib/haskell/tests/program-matrix.sx"
|
||||||
"wordcount:lib/haskell/tests/program-wordcount.sx"
|
"wordcount:lib/haskell/tests/program-wordcount.sx"
|
||||||
"powers:lib/haskell/tests/program-powers.sx"
|
"powers:lib/haskell/tests/program-powers.sx"
|
||||||
|
"caesar:lib/haskell/tests/program-caesar.sx"
|
||||||
|
"runlength-str:lib/haskell/tests/program-runlength-str.sx"
|
||||||
|
"showadt:lib/haskell/tests/program-showadt.sx"
|
||||||
|
"showio:lib/haskell/tests/program-showio.sx"
|
||||||
|
"partial:lib/haskell/tests/program-partial.sx"
|
||||||
|
"statistics:lib/haskell/tests/program-statistics.sx"
|
||||||
|
"newton:lib/haskell/tests/program-newton.sx"
|
||||||
|
"wordfreq:lib/haskell/tests/program-wordfreq.sx"
|
||||||
|
"mapgraph:lib/haskell/tests/program-mapgraph.sx"
|
||||||
|
"uniquewords:lib/haskell/tests/program-uniquewords.sx"
|
||||||
|
"setops:lib/haskell/tests/program-setops.sx"
|
||||||
|
"shapes:lib/haskell/tests/program-shapes.sx"
|
||||||
|
"person:lib/haskell/tests/program-person.sx"
|
||||||
|
"config:lib/haskell/tests/program-config.sx"
|
||||||
|
"counter:lib/haskell/tests/program-counter.sx"
|
||||||
|
"accumulate:lib/haskell/tests/program-accumulate.sx"
|
||||||
|
"safediv:lib/haskell/tests/program-safediv.sx"
|
||||||
|
"trycatch:lib/haskell/tests/program-trycatch.sx"
|
||||||
)
|
)
|
||||||
|
|
||||||
emit_scoreboard_json() {
|
emit_scoreboard_json() {
|
||||||
|
|||||||
@@ -131,119 +131,281 @@
|
|||||||
(let
|
(let
|
||||||
((tag (first node)))
|
((tag (first node)))
|
||||||
(cond
|
(cond
|
||||||
;; Transformations
|
|
||||||
((= tag "where")
|
((= tag "where")
|
||||||
(list
|
(list
|
||||||
:let
|
:let (map hk-desugar (nth node 2))
|
||||||
(map hk-desugar (nth node 2))
|
|
||||||
(hk-desugar (nth node 1))))
|
(hk-desugar (nth node 1))))
|
||||||
((= tag "guarded") (hk-guards-to-if (nth node 1)))
|
((= tag "guarded") (hk-guards-to-if (nth node 1)))
|
||||||
((= tag "list-comp")
|
((= tag "list-comp")
|
||||||
(hk-lc-desugar
|
(hk-lc-desugar (hk-desugar (nth node 1)) (nth node 2)))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(nth node 2)))
|
|
||||||
|
|
||||||
;; Expression nodes
|
|
||||||
((= tag "app")
|
((= tag "app")
|
||||||
(list
|
(list
|
||||||
:app
|
:app (hk-desugar (nth node 1))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(hk-desugar (nth node 2))))
|
(hk-desugar (nth node 2))))
|
||||||
|
((= tag "p-rec")
|
||||||
|
(let
|
||||||
|
((cname (nth node 1))
|
||||||
|
(field-pats (nth node 2))
|
||||||
|
(field-order (hk-record-field-names cname)))
|
||||||
|
(cond
|
||||||
|
((nil? field-order)
|
||||||
|
(raise (str "p-rec: no record info for " cname)))
|
||||||
|
(:else
|
||||||
|
(list
|
||||||
|
:p-con
|
||||||
|
cname
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(fname)
|
||||||
|
(let
|
||||||
|
((p (hk-find-rec-pair field-pats fname)))
|
||||||
|
(cond
|
||||||
|
((nil? p) (list :p-wild))
|
||||||
|
(:else (hk-desugar (nth p 1))))))
|
||||||
|
field-order))))))
|
||||||
|
((= tag "rec-update")
|
||||||
|
(list
|
||||||
|
:rec-update
|
||||||
|
(hk-desugar (nth node 1))
|
||||||
|
(map
|
||||||
|
(fn (p) (list (first p) (hk-desugar (nth p 1))))
|
||||||
|
(nth node 2))))
|
||||||
|
((= tag "rec-create")
|
||||||
|
(let
|
||||||
|
((cname (nth node 1))
|
||||||
|
(field-pairs (nth node 2))
|
||||||
|
(field-order (hk-record-field-names cname)))
|
||||||
|
(cond
|
||||||
|
((nil? field-order)
|
||||||
|
(raise (str "rec-create: no record info for " cname)))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((acc (list :con cname)))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(fname)
|
||||||
|
(let
|
||||||
|
((pair
|
||||||
|
(hk-find-rec-pair field-pairs fname)))
|
||||||
|
(cond
|
||||||
|
((nil? pair)
|
||||||
|
(raise
|
||||||
|
(str
|
||||||
|
"rec-create: missing field "
|
||||||
|
fname
|
||||||
|
" for "
|
||||||
|
cname)))
|
||||||
|
(:else
|
||||||
|
(set!
|
||||||
|
acc
|
||||||
|
(list
|
||||||
|
:app
|
||||||
|
acc
|
||||||
|
(hk-desugar (nth pair 1))))))))
|
||||||
|
field-order)
|
||||||
|
acc))))))
|
||||||
((= tag "op")
|
((= tag "op")
|
||||||
(list
|
(list
|
||||||
:op
|
:op (nth node 1)
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))
|
(hk-desugar (nth node 2))
|
||||||
(hk-desugar (nth node 3))))
|
(hk-desugar (nth node 3))))
|
||||||
|
((= tag "type-ann") (hk-desugar (nth node 1)))
|
||||||
((= tag "neg") (list :neg (hk-desugar (nth node 1))))
|
((= tag "neg") (list :neg (hk-desugar (nth node 1))))
|
||||||
((= tag "if")
|
((= tag "if")
|
||||||
(list
|
(list
|
||||||
:if
|
:if (hk-desugar (nth node 1))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(hk-desugar (nth node 2))
|
(hk-desugar (nth node 2))
|
||||||
(hk-desugar (nth node 3))))
|
(hk-desugar (nth node 3))))
|
||||||
((= tag "tuple")
|
((= tag "tuple") (list :tuple (map hk-desugar (nth node 1))))
|
||||||
(list :tuple (map hk-desugar (nth node 1))))
|
((= tag "list") (list :list (map hk-desugar (nth node 1))))
|
||||||
((= tag "list")
|
|
||||||
(list :list (map hk-desugar (nth node 1))))
|
|
||||||
((= tag "range")
|
((= tag "range")
|
||||||
(list
|
(list
|
||||||
:range
|
:range (hk-desugar (nth node 1))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(hk-desugar (nth node 2))))
|
(hk-desugar (nth node 2))))
|
||||||
((= tag "range-step")
|
((= tag "range-step")
|
||||||
(list
|
(list
|
||||||
:range-step
|
:range-step (hk-desugar (nth node 1))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(hk-desugar (nth node 2))
|
(hk-desugar (nth node 2))
|
||||||
(hk-desugar (nth node 3))))
|
(hk-desugar (nth node 3))))
|
||||||
((= tag "lambda")
|
((= tag "lambda")
|
||||||
(list
|
(list :lambda (nth node 1) (hk-desugar (nth node 2))))
|
||||||
:lambda
|
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))))
|
|
||||||
((= tag "let")
|
((= tag "let")
|
||||||
(list
|
(list
|
||||||
:let
|
:let (map hk-desugar (nth node 1))
|
||||||
(map hk-desugar (nth node 1))
|
|
||||||
(hk-desugar (nth node 2))))
|
(hk-desugar (nth node 2))))
|
||||||
((= tag "case")
|
((= tag "case")
|
||||||
(list
|
(list
|
||||||
:case
|
:case (hk-desugar (nth node 1))
|
||||||
(hk-desugar (nth node 1))
|
|
||||||
(map hk-desugar (nth node 2))))
|
(map hk-desugar (nth node 2))))
|
||||||
((= tag "alt")
|
((= tag "alt")
|
||||||
(list :alt (nth node 1) (hk-desugar (nth node 2))))
|
(list :alt (hk-desugar (nth node 1)) (hk-desugar (nth node 2))))
|
||||||
((= tag "do") (hk-desugar-do (nth node 1)))
|
((= tag "do") (hk-desugar-do (nth node 1)))
|
||||||
((= tag "sect-left")
|
((= tag "sect-left")
|
||||||
(list
|
(list :sect-left (nth node 1) (hk-desugar (nth node 2))))
|
||||||
:sect-left
|
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))))
|
|
||||||
((= tag "sect-right")
|
((= tag "sect-right")
|
||||||
(list
|
(list :sect-right (nth node 1) (hk-desugar (nth node 2))))
|
||||||
:sect-right
|
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))))
|
|
||||||
|
|
||||||
;; Top-level
|
|
||||||
((= tag "program")
|
((= tag "program")
|
||||||
(list :program (map hk-desugar (nth node 1))))
|
(list :program (map hk-desugar (hk-expand-records (nth node 1)))))
|
||||||
((= tag "module")
|
((= tag "module")
|
||||||
(list
|
(list
|
||||||
:module
|
:module (nth node 1)
|
||||||
(nth node 1)
|
|
||||||
(nth node 2)
|
(nth node 2)
|
||||||
(nth node 3)
|
(nth node 3)
|
||||||
(map hk-desugar (nth node 4))))
|
(map hk-desugar (hk-expand-records (nth node 4)))))
|
||||||
|
|
||||||
;; Decls carrying a body
|
|
||||||
((= tag "fun-clause")
|
((= tag "fun-clause")
|
||||||
(list
|
(list
|
||||||
:fun-clause
|
:fun-clause (nth node 1)
|
||||||
(nth node 1)
|
(map hk-desugar (nth node 2))
|
||||||
(nth node 2)
|
|
||||||
(hk-desugar (nth node 3))))
|
(hk-desugar (nth node 3))))
|
||||||
|
((= tag "instance-decl")
|
||||||
|
(list
|
||||||
|
:instance-decl (nth node 1)
|
||||||
|
(nth node 2)
|
||||||
|
(map hk-desugar (nth node 3))))
|
||||||
((= tag "pat-bind")
|
((= tag "pat-bind")
|
||||||
(list
|
(list :pat-bind (nth node 1) (hk-desugar (nth node 2))))
|
||||||
:pat-bind
|
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))))
|
|
||||||
((= tag "bind")
|
((= tag "bind")
|
||||||
(list
|
(list :bind (nth node 1) (hk-desugar (nth node 2))))
|
||||||
:bind
|
|
||||||
(nth node 1)
|
|
||||||
(hk-desugar (nth node 2))))
|
|
||||||
|
|
||||||
;; Everything else: leaf literals, vars, cons, patterns,
|
|
||||||
;; types, imports, type-sigs, data / newtype / fixity, …
|
|
||||||
(:else node)))))))
|
(:else node)))))))
|
||||||
|
|
||||||
;; Convenience — tokenize + layout + parse + desugar.
|
;; Convenience — tokenize + layout + parse + desugar.
|
||||||
(define
|
(define hk-record-fields (dict))
|
||||||
hk-core
|
|
||||||
(fn (src) (hk-desugar (hk-parse-top src))))
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hk-core-expr
|
hk-register-record-fields!
|
||||||
(fn (src) (hk-desugar (hk-parse src))))
|
(fn (cname fields) (dict-set! hk-record-fields cname fields)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-record-field-names
|
||||||
|
(fn
|
||||||
|
(cname)
|
||||||
|
(if (has-key? hk-record-fields cname) (get hk-record-fields cname) nil)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-record-field-index
|
||||||
|
(fn
|
||||||
|
(cname fname)
|
||||||
|
(let
|
||||||
|
((fields (hk-record-field-names cname)))
|
||||||
|
(cond
|
||||||
|
((nil? fields) -1)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((i 0) (idx -1))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(f)
|
||||||
|
(begin (when (= f fname) (set! idx i)) (set! i (+ i 1))))
|
||||||
|
fields)
|
||||||
|
idx)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-find-rec-pair
|
||||||
|
(fn
|
||||||
|
(pairs name)
|
||||||
|
(cond
|
||||||
|
((empty? pairs) nil)
|
||||||
|
((= (first (first pairs)) name) (first pairs))
|
||||||
|
(:else (hk-find-rec-pair (rest pairs) name)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-record-accessors
|
||||||
|
(fn
|
||||||
|
(cname rec-fields)
|
||||||
|
(let
|
||||||
|
((n (len rec-fields)) (i 0) (out (list)))
|
||||||
|
(define
|
||||||
|
hk-ra-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(let
|
||||||
|
((field (nth rec-fields i)))
|
||||||
|
(let
|
||||||
|
((fname (first field)) (j 0) (pats (list)))
|
||||||
|
(define
|
||||||
|
hk-pat-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(< j n)
|
||||||
|
(begin
|
||||||
|
(append!
|
||||||
|
pats
|
||||||
|
(if
|
||||||
|
(= j i)
|
||||||
|
(list "p-var" "__rec_field")
|
||||||
|
(list "p-wild")))
|
||||||
|
(set! j (+ j 1))
|
||||||
|
(hk-pat-loop)))))
|
||||||
|
(hk-pat-loop)
|
||||||
|
(append!
|
||||||
|
out
|
||||||
|
(list
|
||||||
|
"fun-clause"
|
||||||
|
fname
|
||||||
|
(list (list "p-con" cname pats))
|
||||||
|
(list "var" "__rec_field")))
|
||||||
|
(set! i (+ i 1))
|
||||||
|
(hk-ra-loop))))))
|
||||||
|
(hk-ra-loop)
|
||||||
|
out)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-expand-records
|
||||||
|
(fn
|
||||||
|
(decls)
|
||||||
|
(let
|
||||||
|
((out (list)))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(d)
|
||||||
|
(cond
|
||||||
|
((and (list? d) (= (first d) "data"))
|
||||||
|
(let
|
||||||
|
((dname (nth d 1))
|
||||||
|
(tvars (nth d 2))
|
||||||
|
(cons-list (nth d 3))
|
||||||
|
(deriving (if (> (len d) 4) (nth d 4) (list)))
|
||||||
|
(new-cons (list))
|
||||||
|
(accessors (list)))
|
||||||
|
(begin
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(c)
|
||||||
|
(cond
|
||||||
|
((= (first c) "con-rec")
|
||||||
|
(let
|
||||||
|
((cname (nth c 1)) (rec-fields (nth c 2)))
|
||||||
|
(begin
|
||||||
|
(hk-register-record-fields!
|
||||||
|
cname
|
||||||
|
(map (fn (f) (first f)) rec-fields))
|
||||||
|
(append!
|
||||||
|
new-cons
|
||||||
|
(list
|
||||||
|
"con-def"
|
||||||
|
cname
|
||||||
|
(map (fn (f) (nth f 1)) rec-fields)))
|
||||||
|
(for-each
|
||||||
|
(fn (a) (append! accessors a))
|
||||||
|
(hk-record-accessors cname rec-fields)))))
|
||||||
|
(:else (append! new-cons c))))
|
||||||
|
cons-list)
|
||||||
|
(append!
|
||||||
|
out
|
||||||
|
(if
|
||||||
|
(empty? deriving)
|
||||||
|
(list "data" dname tvars new-cons)
|
||||||
|
(list "data" dname tvars new-cons deriving)))
|
||||||
|
(for-each (fn (a) (append! out a)) accessors))))
|
||||||
|
(:else (append! out d))))
|
||||||
|
decls)
|
||||||
|
out)))
|
||||||
|
|
||||||
|
(define hk-core (fn (src) (hk-desugar (hk-parse-top src))))
|
||||||
|
|
||||||
|
(define hk-core-expr (fn (src) (hk-desugar (hk-parse src))))
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
520
lib/haskell/map.sx
Normal file
520
lib/haskell/map.sx
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
;; map.sx — Phase 11 Data.Map: weight-balanced BST in pure SX.
|
||||||
|
;;
|
||||||
|
;; Algorithm: Adams's weight-balanced tree (the same family as Haskell's
|
||||||
|
;; Data.Map). Each node tracks its size; rotations maintain the invariant
|
||||||
|
;;
|
||||||
|
;; size(small-side) * delta >= size(large-side) (delta = 3)
|
||||||
|
;;
|
||||||
|
;; with single or double rotations chosen by the gamma ratio (gamma = 2).
|
||||||
|
;; The size field is an Int and is included so `size`, `lookup`, etc. are
|
||||||
|
;; O(log n) on both extremes of the tree.
|
||||||
|
;;
|
||||||
|
;; Representation:
|
||||||
|
;; Empty → ("Map-Empty")
|
||||||
|
;; Node → ("Map-Node" key val left right size)
|
||||||
|
;;
|
||||||
|
;; All operations are pure SX — no mutation of nodes once constructed.
|
||||||
|
;; The user-facing Haskell layer (Phase 11 next iteration) wraps these
|
||||||
|
;; for `import Data.Map as Map`.
|
||||||
|
|
||||||
|
;; ── Constructors ────────────────────────────────────────────
|
||||||
|
(define hk-map-empty (list "Map-Empty"))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-node
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(list "Map-Node" k v l r (+ 1 (+ (hk-map-size l) (hk-map-size r))))))
|
||||||
|
|
||||||
|
;; ── Predicates and accessors ────────────────────────────────
|
||||||
|
(define hk-map-empty? (fn (m) (and (list? m) (= (first m) "Map-Empty"))))
|
||||||
|
|
||||||
|
(define hk-map-node? (fn (m) (and (list? m) (= (first m) "Map-Node"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-size
|
||||||
|
(fn (m) (cond ((hk-map-empty? m) 0) (:else (nth m 5)))))
|
||||||
|
|
||||||
|
(define hk-map-key (fn (m) (nth m 1)))
|
||||||
|
(define hk-map-val (fn (m) (nth m 2)))
|
||||||
|
(define hk-map-left (fn (m) (nth m 3)))
|
||||||
|
(define hk-map-right (fn (m) (nth m 4)))
|
||||||
|
|
||||||
|
;; ── Weight-balanced rotations ───────────────────────────────
|
||||||
|
;; delta and gamma per Adams 1992 / Haskell Data.Map.
|
||||||
|
|
||||||
|
(define hk-map-delta 3)
|
||||||
|
(define hk-map-gamma 2)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-single-l
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(let
|
||||||
|
((rk (hk-map-key r))
|
||||||
|
(rv (hk-map-val r))
|
||||||
|
(rl (hk-map-left r))
|
||||||
|
(rr (hk-map-right r)))
|
||||||
|
(hk-map-node rk rv (hk-map-node k v l rl) rr))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-single-r
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(let
|
||||||
|
((lk (hk-map-key l))
|
||||||
|
(lv (hk-map-val l))
|
||||||
|
(ll (hk-map-left l))
|
||||||
|
(lr (hk-map-right l)))
|
||||||
|
(hk-map-node lk lv ll (hk-map-node k v lr r)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-double-l
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(let
|
||||||
|
((rk (hk-map-key r))
|
||||||
|
(rv (hk-map-val r))
|
||||||
|
(rl (hk-map-left r))
|
||||||
|
(rr (hk-map-right r))
|
||||||
|
(rlk (hk-map-key (hk-map-left r)))
|
||||||
|
(rlv (hk-map-val (hk-map-left r)))
|
||||||
|
(rll (hk-map-left (hk-map-left r)))
|
||||||
|
(rlr (hk-map-right (hk-map-left r))))
|
||||||
|
(hk-map-node
|
||||||
|
rlk
|
||||||
|
rlv
|
||||||
|
(hk-map-node k v l rll)
|
||||||
|
(hk-map-node rk rv rlr rr)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-double-r
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(let
|
||||||
|
((lk (hk-map-key l))
|
||||||
|
(lv (hk-map-val l))
|
||||||
|
(ll (hk-map-left l))
|
||||||
|
(lr (hk-map-right l))
|
||||||
|
(lrk (hk-map-key (hk-map-right l)))
|
||||||
|
(lrv (hk-map-val (hk-map-right l)))
|
||||||
|
(lrl (hk-map-left (hk-map-right l)))
|
||||||
|
(lrr (hk-map-right (hk-map-right l))))
|
||||||
|
(hk-map-node
|
||||||
|
lrk
|
||||||
|
lrv
|
||||||
|
(hk-map-node lk lv ll lrl)
|
||||||
|
(hk-map-node k v lrr r)))))
|
||||||
|
|
||||||
|
;; ── Balanced node constructor ──────────────────────────────
|
||||||
|
;; Use this in place of hk-map-node when one side may have grown
|
||||||
|
;; or shrunk by one and we need to restore the weight invariant.
|
||||||
|
(define
|
||||||
|
hk-map-balance
|
||||||
|
(fn
|
||||||
|
(k v l r)
|
||||||
|
(let
|
||||||
|
((sl (hk-map-size l)) (sr (hk-map-size r)))
|
||||||
|
(cond
|
||||||
|
((<= (+ sl sr) 1) (hk-map-node k v l r))
|
||||||
|
((> sr (* hk-map-delta sl))
|
||||||
|
(let
|
||||||
|
((rl (hk-map-left r)) (rr (hk-map-right r)))
|
||||||
|
(cond
|
||||||
|
((< (hk-map-size rl) (* hk-map-gamma (hk-map-size rr)))
|
||||||
|
(hk-map-single-l k v l r))
|
||||||
|
(:else (hk-map-double-l k v l r)))))
|
||||||
|
((> sl (* hk-map-delta sr))
|
||||||
|
(let
|
||||||
|
((ll (hk-map-left l)) (lr (hk-map-right l)))
|
||||||
|
(cond
|
||||||
|
((< (hk-map-size lr) (* hk-map-gamma (hk-map-size ll)))
|
||||||
|
(hk-map-single-r k v l r))
|
||||||
|
(:else (hk-map-double-r k v l r)))))
|
||||||
|
(:else (hk-map-node k v l r))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-singleton
|
||||||
|
(fn (k v) (hk-map-node k v hk-map-empty hk-map-empty)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-insert
|
||||||
|
(fn
|
||||||
|
(k v m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (hk-map-singleton k v))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-insert k v (hk-map-left m))
|
||||||
|
(hk-map-right m)))
|
||||||
|
((> k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-insert k v (hk-map-right m))))
|
||||||
|
(:else (hk-map-node k v (hk-map-left m) (hk-map-right m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-lookup
|
||||||
|
(fn
|
||||||
|
(k m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (list "Nothing"))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk) (hk-map-lookup k (hk-map-left m)))
|
||||||
|
((> k mk) (hk-map-lookup k (hk-map-right m)))
|
||||||
|
(:else (list "Just" (hk-map-val m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-member
|
||||||
|
(fn
|
||||||
|
(k m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) false)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk) (hk-map-member k (hk-map-left m)))
|
||||||
|
((> k mk) (hk-map-member k (hk-map-right m)))
|
||||||
|
(:else true)))))))
|
||||||
|
|
||||||
|
(define hk-map-null hk-map-empty?)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-find-min
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? (hk-map-left m))
|
||||||
|
(list (hk-map-key m) (hk-map-val m)))
|
||||||
|
(:else (hk-map-find-min (hk-map-left m))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-delete-min
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? (hk-map-left m)) (hk-map-right m))
|
||||||
|
(:else
|
||||||
|
(hk-map-balance
|
||||||
|
(hk-map-key m)
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-delete-min (hk-map-left m))
|
||||||
|
(hk-map-right m))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-find-max
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? (hk-map-right m))
|
||||||
|
(list (hk-map-key m) (hk-map-val m)))
|
||||||
|
(:else (hk-map-find-max (hk-map-right m))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-delete-max
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? (hk-map-right m)) (hk-map-left m))
|
||||||
|
(:else
|
||||||
|
(hk-map-balance
|
||||||
|
(hk-map-key m)
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-delete-max (hk-map-right m)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-glue
|
||||||
|
(fn
|
||||||
|
(l r)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? l) r)
|
||||||
|
((hk-map-empty? r) l)
|
||||||
|
((> (hk-map-size l) (hk-map-size r))
|
||||||
|
(let
|
||||||
|
((mp (hk-map-find-max l)))
|
||||||
|
(hk-map-balance (first mp) (nth mp 1) (hk-map-delete-max l) r)))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mp (hk-map-find-min r)))
|
||||||
|
(hk-map-balance (first mp) (nth mp 1) l (hk-map-delete-min r)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-delete
|
||||||
|
(fn
|
||||||
|
(k m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) m)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-delete k (hk-map-left m))
|
||||||
|
(hk-map-right m)))
|
||||||
|
((> k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-delete k (hk-map-right m))))
|
||||||
|
(:else (hk-map-glue (hk-map-left m) (hk-map-right m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-from-list
|
||||||
|
(fn
|
||||||
|
(pairs)
|
||||||
|
(reduce
|
||||||
|
(fn (acc p) (hk-map-insert (first p) (nth p 1) acc))
|
||||||
|
hk-map-empty
|
||||||
|
pairs)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-to-asc-list
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (list))
|
||||||
|
(:else
|
||||||
|
(append
|
||||||
|
(hk-map-to-asc-list (hk-map-left m))
|
||||||
|
(cons
|
||||||
|
(list (hk-map-key m) (hk-map-val m))
|
||||||
|
(hk-map-to-asc-list (hk-map-right m))))))))
|
||||||
|
|
||||||
|
(define hk-map-to-list hk-map-to-asc-list)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-keys
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (list))
|
||||||
|
(:else
|
||||||
|
(append
|
||||||
|
(hk-map-keys (hk-map-left m))
|
||||||
|
(cons (hk-map-key m) (hk-map-keys (hk-map-right m))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-elems
|
||||||
|
(fn
|
||||||
|
(m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (list))
|
||||||
|
(:else
|
||||||
|
(append
|
||||||
|
(hk-map-elems (hk-map-left m))
|
||||||
|
(cons (hk-map-val m) (hk-map-elems (hk-map-right m))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-union-with
|
||||||
|
(fn
|
||||||
|
(f m1 m2)
|
||||||
|
(reduce
|
||||||
|
(fn
|
||||||
|
(acc p)
|
||||||
|
(let
|
||||||
|
((k (first p)) (v (nth p 1)))
|
||||||
|
(let
|
||||||
|
((look (hk-map-lookup k acc)))
|
||||||
|
(cond
|
||||||
|
((= (first look) "Just")
|
||||||
|
(hk-map-insert k (f (nth look 1) v) acc))
|
||||||
|
(:else (hk-map-insert k v acc))))))
|
||||||
|
m1
|
||||||
|
(hk-map-to-asc-list m2))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-intersection-with
|
||||||
|
(fn
|
||||||
|
(f m1 m2)
|
||||||
|
(reduce
|
||||||
|
(fn
|
||||||
|
(acc p)
|
||||||
|
(let
|
||||||
|
((k (first p)) (v1 (nth p 1)))
|
||||||
|
(let
|
||||||
|
((look (hk-map-lookup k m2)))
|
||||||
|
(cond
|
||||||
|
((= (first look) "Just")
|
||||||
|
(hk-map-insert k (f v1 (nth look 1)) acc))
|
||||||
|
(:else acc)))))
|
||||||
|
hk-map-empty
|
||||||
|
(hk-map-to-asc-list m1))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-difference
|
||||||
|
(fn
|
||||||
|
(m1 m2)
|
||||||
|
(reduce
|
||||||
|
(fn
|
||||||
|
(acc p)
|
||||||
|
(let
|
||||||
|
((k (first p)) (v (nth p 1)))
|
||||||
|
(cond ((hk-map-member k m2) acc) (:else (hk-map-insert k v acc)))))
|
||||||
|
hk-map-empty
|
||||||
|
(hk-map-to-asc-list m1))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-foldl-with-key
|
||||||
|
(fn
|
||||||
|
(f acc m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) acc)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((acc1 (hk-map-foldl-with-key f acc (hk-map-left m))))
|
||||||
|
(let
|
||||||
|
((acc2 (f acc1 (hk-map-key m) (hk-map-val m))))
|
||||||
|
(hk-map-foldl-with-key f acc2 (hk-map-right m))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-foldr-with-key
|
||||||
|
(fn
|
||||||
|
(f acc m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) acc)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((acc1 (hk-map-foldr-with-key f acc (hk-map-right m))))
|
||||||
|
(let
|
||||||
|
((acc2 (f (hk-map-key m) (hk-map-val m) acc1)))
|
||||||
|
(hk-map-foldr-with-key f acc2 (hk-map-left m))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-map-with-key
|
||||||
|
(fn
|
||||||
|
(f m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) m)
|
||||||
|
(:else
|
||||||
|
(list
|
||||||
|
"Map-Node"
|
||||||
|
(hk-map-key m)
|
||||||
|
(f (hk-map-key m) (hk-map-val m))
|
||||||
|
(hk-map-map-with-key f (hk-map-left m))
|
||||||
|
(hk-map-map-with-key f (hk-map-right m))
|
||||||
|
(hk-map-size m))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-filter-with-key
|
||||||
|
(fn
|
||||||
|
(p m)
|
||||||
|
(hk-map-foldr-with-key
|
||||||
|
(fn (k v acc) (cond ((p k v) (hk-map-insert k v acc)) (:else acc)))
|
||||||
|
hk-map-empty
|
||||||
|
m)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-adjust
|
||||||
|
(fn
|
||||||
|
(f k m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) m)
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk)
|
||||||
|
(hk-map-node
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-adjust f k (hk-map-left m))
|
||||||
|
(hk-map-right m)))
|
||||||
|
((> k mk)
|
||||||
|
(hk-map-node
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-adjust f k (hk-map-right m))))
|
||||||
|
(:else
|
||||||
|
(hk-map-node
|
||||||
|
mk
|
||||||
|
(f (hk-map-val m))
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-right m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-insert-with
|
||||||
|
(fn
|
||||||
|
(f k v m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (hk-map-singleton k v))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-insert-with f k v (hk-map-left m))
|
||||||
|
(hk-map-right m)))
|
||||||
|
((> k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-insert-with f k v (hk-map-right m))))
|
||||||
|
(:else
|
||||||
|
(hk-map-node
|
||||||
|
mk
|
||||||
|
(f v (hk-map-val m))
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-right m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-insert-with-key
|
||||||
|
(fn
|
||||||
|
(f k v m)
|
||||||
|
(cond
|
||||||
|
((hk-map-empty? m) (hk-map-singleton k v))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((mk (hk-map-key m)))
|
||||||
|
(cond
|
||||||
|
((< k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-insert-with-key f k v (hk-map-left m))
|
||||||
|
(hk-map-right m)))
|
||||||
|
((> k mk)
|
||||||
|
(hk-map-balance
|
||||||
|
mk
|
||||||
|
(hk-map-val m)
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-insert-with-key f k v (hk-map-right m))))
|
||||||
|
(:else
|
||||||
|
(hk-map-node
|
||||||
|
mk
|
||||||
|
(f k v (hk-map-val m))
|
||||||
|
(hk-map-left m)
|
||||||
|
(hk-map-right m)))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-map-alter
|
||||||
|
(fn
|
||||||
|
(f k m)
|
||||||
|
(let
|
||||||
|
((look (hk-map-lookup k m)))
|
||||||
|
(let
|
||||||
|
((res (f look)))
|
||||||
|
(cond
|
||||||
|
((= (first res) "Nothing") (hk-map-delete k m))
|
||||||
|
(:else (hk-map-insert k (nth res 1) m)))))))
|
||||||
@@ -87,45 +87,41 @@
|
|||||||
((nil? res) nil)
|
((nil? res) nil)
|
||||||
(:else (assoc res (nth pat 1) val)))))
|
(:else (assoc res (nth pat 1) val)))))
|
||||||
(:else
|
(:else
|
||||||
(let ((fv (hk-force val)))
|
(let
|
||||||
|
((fv (hk-force val)))
|
||||||
(cond
|
(cond
|
||||||
((= tag "p-int")
|
((= tag "p-int")
|
||||||
(if
|
(if (and (number? fv) (= fv (nth pat 1))) env nil))
|
||||||
(and (number? fv) (= fv (nth pat 1)))
|
|
||||||
env
|
|
||||||
nil))
|
|
||||||
((= tag "p-float")
|
((= tag "p-float")
|
||||||
(if
|
(if (and (number? fv) (= fv (nth pat 1))) env nil))
|
||||||
(and (number? fv) (= fv (nth pat 1)))
|
|
||||||
env
|
|
||||||
nil))
|
|
||||||
((= tag "p-string")
|
((= tag "p-string")
|
||||||
(if
|
(if (and (string? fv) (= fv (nth pat 1))) env nil))
|
||||||
(and (string? fv) (= fv (nth pat 1)))
|
|
||||||
env
|
|
||||||
nil))
|
|
||||||
((= tag "p-char")
|
((= tag "p-char")
|
||||||
(if
|
(if (and (string? fv) (= fv (nth pat 1))) env nil))
|
||||||
(and (string? fv) (= fv (nth pat 1)))
|
|
||||||
env
|
|
||||||
nil))
|
|
||||||
((= tag "p-con")
|
((= tag "p-con")
|
||||||
(let
|
(let
|
||||||
((pat-name (nth pat 1)) (pat-args (nth pat 2)))
|
((pat-name (nth pat 1)) (pat-args (nth pat 2)))
|
||||||
(cond
|
(cond
|
||||||
|
((and (= pat-name ":") (hk-str? fv) (not (hk-str-null? fv)))
|
||||||
|
(let
|
||||||
|
((str-head (hk-str-head fv))
|
||||||
|
(str-tail (hk-str-tail fv)))
|
||||||
|
(let
|
||||||
|
((head-pat (nth pat-args 0))
|
||||||
|
(tail-pat (nth pat-args 1)))
|
||||||
|
(let
|
||||||
|
((res (hk-match head-pat str-head env)))
|
||||||
|
(cond
|
||||||
|
((nil? res) nil)
|
||||||
|
(:else (hk-match tail-pat str-tail res)))))))
|
||||||
((not (hk-is-con-val? fv)) nil)
|
((not (hk-is-con-val? fv)) nil)
|
||||||
((not (= (hk-val-con-name fv) pat-name)) nil)
|
((not (= (hk-val-con-name fv) pat-name)) nil)
|
||||||
(:else
|
(:else
|
||||||
(let
|
(let
|
||||||
((val-args (hk-val-con-args fv)))
|
((val-args (hk-val-con-args fv)))
|
||||||
(cond
|
(cond
|
||||||
((not (= (len pat-args) (len val-args)))
|
((not (= (len val-args) (len pat-args))) nil)
|
||||||
nil)
|
(:else (hk-match-all pat-args val-args env))))))))
|
||||||
(:else
|
|
||||||
(hk-match-all
|
|
||||||
pat-args
|
|
||||||
val-args
|
|
||||||
env))))))))
|
|
||||||
((= tag "p-tuple")
|
((= tag "p-tuple")
|
||||||
(let
|
(let
|
||||||
((items (nth pat 1)))
|
((items (nth pat 1)))
|
||||||
@@ -134,13 +130,8 @@
|
|||||||
((not (= (hk-val-con-name fv) "Tuple")) nil)
|
((not (= (hk-val-con-name fv) "Tuple")) nil)
|
||||||
((not (= (len (hk-val-con-args fv)) (len items)))
|
((not (= (len (hk-val-con-args fv)) (len items)))
|
||||||
nil)
|
nil)
|
||||||
(:else
|
(:else (hk-match-all items (hk-val-con-args fv) env)))))
|
||||||
(hk-match-all
|
((= tag "p-list") (hk-match-list-pat (nth pat 1) fv env))
|
||||||
items
|
|
||||||
(hk-val-con-args fv)
|
|
||||||
env)))))
|
|
||||||
((= tag "p-list")
|
|
||||||
(hk-match-list-pat (nth pat 1) fv env))
|
|
||||||
(:else nil))))))))))
|
(:else nil))))))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -161,17 +152,26 @@
|
|||||||
hk-match-list-pat
|
hk-match-list-pat
|
||||||
(fn
|
(fn
|
||||||
(items val env)
|
(items val env)
|
||||||
(let ((fv (hk-force val)))
|
(let
|
||||||
|
((fv (hk-force val)))
|
||||||
(cond
|
(cond
|
||||||
((empty? items)
|
((empty? items)
|
||||||
(if
|
(if
|
||||||
(and
|
(or
|
||||||
(hk-is-con-val? fv)
|
(and (hk-is-con-val? fv) (= (hk-val-con-name fv) "[]"))
|
||||||
(= (hk-val-con-name fv) "[]"))
|
(and (hk-str? fv) (hk-str-null? fv)))
|
||||||
env
|
env
|
||||||
nil))
|
nil))
|
||||||
(:else
|
(:else
|
||||||
(cond
|
(cond
|
||||||
|
((and (hk-str? fv) (not (hk-str-null? fv)))
|
||||||
|
(let
|
||||||
|
((h (hk-str-head fv)) (t (hk-str-tail fv)))
|
||||||
|
(let
|
||||||
|
((res (hk-match (first items) h env)))
|
||||||
|
(cond
|
||||||
|
((nil? res) nil)
|
||||||
|
(:else (hk-match-list-pat (rest items) t res))))))
|
||||||
((not (hk-is-con-val? fv)) nil)
|
((not (hk-is-con-val? fv)) nil)
|
||||||
((not (= (hk-val-con-name fv) ":")) nil)
|
((not (= (hk-val-con-name fv) ":")) nil)
|
||||||
(:else
|
(:else
|
||||||
@@ -183,11 +183,7 @@
|
|||||||
((res (hk-match (first items) h env)))
|
((res (hk-match (first items) h env)))
|
||||||
(cond
|
(cond
|
||||||
((nil? res) nil)
|
((nil? res) nil)
|
||||||
(:else
|
(:else (hk-match-list-pat (rest items) t res)))))))))))))
|
||||||
(hk-match-list-pat
|
|
||||||
(rest items)
|
|
||||||
t
|
|
||||||
res)))))))))))))
|
|
||||||
|
|
||||||
;; ── Convenience: parse a pattern from source for tests ─────
|
;; ── Convenience: parse a pattern from source for tests ─────
|
||||||
;; (Uses the parser's case-alt entry — `case _ of pat -> 0` —
|
;; (Uses the parser's case-alt entry — `case _ of pat -> 0` —
|
||||||
|
|||||||
@@ -208,9 +208,19 @@
|
|||||||
((= (get t "type") "char")
|
((= (get t "type") "char")
|
||||||
(do (hk-advance!) (list :char (get t "value"))))
|
(do (hk-advance!) (list :char (get t "value"))))
|
||||||
((= (get t "type") "varid")
|
((= (get t "type") "varid")
|
||||||
(do (hk-advance!) (list :var (get t "value"))))
|
(do
|
||||||
|
(hk-advance!)
|
||||||
|
(cond
|
||||||
|
((hk-match? "lbrace" nil)
|
||||||
|
(hk-parse-rec-update (list :var (get t "value"))))
|
||||||
|
(:else (list :var (get t "value"))))))
|
||||||
((= (get t "type") "conid")
|
((= (get t "type") "conid")
|
||||||
(do (hk-advance!) (list :con (get t "value"))))
|
(do
|
||||||
|
(hk-advance!)
|
||||||
|
(cond
|
||||||
|
((hk-match? "lbrace" nil)
|
||||||
|
(hk-parse-rec-create (get t "value")))
|
||||||
|
(:else (list :con (get t "value"))))))
|
||||||
((= (get t "type") "qvarid")
|
((= (get t "type") "qvarid")
|
||||||
(do (hk-advance!) (list :var (get t "value"))))
|
(do (hk-advance!) (list :var (get t "value"))))
|
||||||
((= (get t "type") "qconid")
|
((= (get t "type") "qconid")
|
||||||
@@ -265,9 +275,18 @@
|
|||||||
(list :sect-right op-name expr-e))))))
|
(list :sect-right op-name expr-e))))))
|
||||||
(:else
|
(:else
|
||||||
(let
|
(let
|
||||||
((first-e (hk-parse-expr-inner))
|
((first-e (hk-parse-expr-inner)))
|
||||||
(items (list))
|
(cond
|
||||||
(is-tuple false))
|
((hk-match? "reservedop" "::")
|
||||||
|
(do
|
||||||
|
(hk-advance!)
|
||||||
|
(let
|
||||||
|
((ann-type (hk-parse-type)))
|
||||||
|
(hk-expect! "rparen" nil)
|
||||||
|
(list :type-ann first-e ann-type))))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((items (list)) (is-tuple false))
|
||||||
(append! items first-e)
|
(append! items first-e)
|
||||||
(define
|
(define
|
||||||
hk-tup-loop
|
hk-tup-loop
|
||||||
@@ -296,7 +315,7 @@
|
|||||||
(hk-consume-op!)
|
(hk-consume-op!)
|
||||||
(hk-advance!)
|
(hk-advance!)
|
||||||
(list :sect-left op-name first-e)))
|
(list :sect-left op-name first-e)))
|
||||||
(:else (hk-err "expected ')' after expression"))))))))))))))
|
(:else (hk-err "expected ')' after expression")))))))))))))))))
|
||||||
(define
|
(define
|
||||||
hk-comp-qual-is-gen?
|
hk-comp-qual-is-gen?
|
||||||
(fn
|
(fn
|
||||||
@@ -456,6 +475,90 @@
|
|||||||
(do
|
(do
|
||||||
(hk-expect! "rbracket" nil)
|
(hk-expect! "rbracket" nil)
|
||||||
(list :list (list first-e))))))))))
|
(list :list (list first-e))))))))))
|
||||||
|
(define
|
||||||
|
hk-parse-rec-create
|
||||||
|
(fn
|
||||||
|
(cname)
|
||||||
|
(begin
|
||||||
|
(hk-expect! "lbrace" nil)
|
||||||
|
(let
|
||||||
|
((fields (list)))
|
||||||
|
(define
|
||||||
|
hk-rc-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(hk-match? "varid" nil)
|
||||||
|
(let
|
||||||
|
((fname (get (hk-advance!) "value")))
|
||||||
|
(begin
|
||||||
|
(hk-expect! "reservedop" "=")
|
||||||
|
(let
|
||||||
|
((fexpr (hk-parse-expr-inner)))
|
||||||
|
(begin
|
||||||
|
(append! fields (list fname fexpr))
|
||||||
|
(when
|
||||||
|
(hk-match? "comma" nil)
|
||||||
|
(begin (hk-advance!) (hk-rc-loop))))))))))
|
||||||
|
(hk-rc-loop)
|
||||||
|
(hk-expect! "rbrace" nil)
|
||||||
|
(list :rec-create cname fields)))))
|
||||||
|
(define
|
||||||
|
hk-parse-rec-update
|
||||||
|
(fn
|
||||||
|
(rec-expr)
|
||||||
|
(begin
|
||||||
|
(hk-expect! "lbrace" nil)
|
||||||
|
(let
|
||||||
|
((fields (list)))
|
||||||
|
(define
|
||||||
|
hk-ru-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(hk-match? "varid" nil)
|
||||||
|
(let
|
||||||
|
((fname (get (hk-advance!) "value")))
|
||||||
|
(begin
|
||||||
|
(hk-expect! "reservedop" "=")
|
||||||
|
(let
|
||||||
|
((fexpr (hk-parse-expr-inner)))
|
||||||
|
(begin
|
||||||
|
(append! fields (list fname fexpr))
|
||||||
|
(when
|
||||||
|
(hk-match? "comma" nil)
|
||||||
|
(begin (hk-advance!) (hk-ru-loop))))))))))
|
||||||
|
(hk-ru-loop)
|
||||||
|
(hk-expect! "rbrace" nil)
|
||||||
|
(list :rec-update rec-expr fields)))))
|
||||||
|
(define
|
||||||
|
hk-parse-rec-pat
|
||||||
|
(fn
|
||||||
|
(cname)
|
||||||
|
(begin
|
||||||
|
(hk-expect! "lbrace" nil)
|
||||||
|
(let
|
||||||
|
((field-pats (list)))
|
||||||
|
(define
|
||||||
|
hk-rp-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(hk-match? "varid" nil)
|
||||||
|
(let
|
||||||
|
((fname (get (hk-advance!) "value")))
|
||||||
|
(begin
|
||||||
|
(hk-expect! "reservedop" "=")
|
||||||
|
(let
|
||||||
|
((fpat (hk-parse-pat)))
|
||||||
|
(begin
|
||||||
|
(append! field-pats (list fname fpat))
|
||||||
|
(when
|
||||||
|
(hk-match? "comma" nil)
|
||||||
|
(begin (hk-advance!) (hk-rp-loop))))))))))
|
||||||
|
(hk-rp-loop)
|
||||||
|
(hk-expect! "rbrace" nil)
|
||||||
|
(list :p-rec cname field-pats)))))
|
||||||
(define
|
(define
|
||||||
hk-parse-fexp
|
hk-parse-fexp
|
||||||
(fn
|
(fn
|
||||||
@@ -696,7 +799,12 @@
|
|||||||
(:else
|
(:else
|
||||||
(do (hk-advance!) (list :p-var (get t "value")))))))
|
(do (hk-advance!) (list :p-var (get t "value")))))))
|
||||||
((= (get t "type") "conid")
|
((= (get t "type") "conid")
|
||||||
(do (hk-advance!) (list :p-con (get t "value") (list))))
|
(do
|
||||||
|
(hk-advance!)
|
||||||
|
(cond
|
||||||
|
((hk-match? "lbrace" nil)
|
||||||
|
(hk-parse-rec-pat (get t "value")))
|
||||||
|
(:else (list :p-con (get t "value") (list))))))
|
||||||
((= (get t "type") "qconid")
|
((= (get t "type") "qconid")
|
||||||
(do (hk-advance!) (list :p-con (get t "value") (list))))
|
(do (hk-advance!) (list :p-con (get t "value") (list))))
|
||||||
((= (get t "type") "lparen") (hk-parse-paren-pat))
|
((= (get t "type") "lparen") (hk-parse-paren-pat))
|
||||||
@@ -762,16 +870,24 @@
|
|||||||
(cond
|
(cond
|
||||||
((and (not (nil? t)) (or (= (get t "type") "conid") (= (get t "type") "qconid")))
|
((and (not (nil? t)) (or (= (get t "type") "conid") (= (get t "type") "qconid")))
|
||||||
(let
|
(let
|
||||||
((name (get (hk-advance!) "value")) (args (list)))
|
((name (get (hk-advance!) "value")))
|
||||||
|
(cond
|
||||||
|
((hk-match? "lbrace" nil)
|
||||||
|
(hk-parse-rec-pat name))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((args (list)))
|
||||||
(define
|
(define
|
||||||
hk-pca-loop
|
hk-pca-loop
|
||||||
(fn
|
(fn
|
||||||
()
|
()
|
||||||
(when
|
(when
|
||||||
(hk-apat-start? (hk-peek))
|
(hk-apat-start? (hk-peek))
|
||||||
(do (append! args (hk-parse-apat)) (hk-pca-loop)))))
|
(do
|
||||||
|
(append! args (hk-parse-apat))
|
||||||
|
(hk-pca-loop)))))
|
||||||
(hk-pca-loop)
|
(hk-pca-loop)
|
||||||
(list :p-con name args)))
|
(list :p-con name args))))))
|
||||||
(:else (hk-parse-apat))))))
|
(:else (hk-parse-apat))))))
|
||||||
(define
|
(define
|
||||||
hk-parse-pat
|
hk-parse-pat
|
||||||
@@ -1212,16 +1328,47 @@
|
|||||||
(not (hk-match? "conid" nil))
|
(not (hk-match? "conid" nil))
|
||||||
(hk-err "expected constructor name"))
|
(hk-err "expected constructor name"))
|
||||||
(let
|
(let
|
||||||
((name (get (hk-advance!) "value")) (fields (list)))
|
((name (get (hk-advance!) "value")))
|
||||||
|
(cond
|
||||||
|
((hk-match? "lbrace" nil)
|
||||||
|
(begin
|
||||||
|
(hk-advance!)
|
||||||
|
(let
|
||||||
|
((rec-fields (list)))
|
||||||
|
(define
|
||||||
|
hk-rec-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(hk-match? "varid" nil)
|
||||||
|
(let
|
||||||
|
((fname (get (hk-advance!) "value")))
|
||||||
|
(begin
|
||||||
|
(hk-expect! "reservedop" "::")
|
||||||
|
(let
|
||||||
|
((ftype (hk-parse-type)))
|
||||||
|
(begin
|
||||||
|
(append! rec-fields (list fname ftype))
|
||||||
|
(when
|
||||||
|
(hk-match? "comma" nil)
|
||||||
|
(begin (hk-advance!) (hk-rec-loop))))))))))
|
||||||
|
(hk-rec-loop)
|
||||||
|
(hk-expect! "rbrace" nil)
|
||||||
|
(list :con-rec name rec-fields))))
|
||||||
|
(:else
|
||||||
|
(let
|
||||||
|
((fields (list)))
|
||||||
(define
|
(define
|
||||||
hk-cd-loop
|
hk-cd-loop
|
||||||
(fn
|
(fn
|
||||||
()
|
()
|
||||||
(when
|
(when
|
||||||
(hk-atype-start? (hk-peek))
|
(hk-atype-start? (hk-peek))
|
||||||
(do (append! fields (hk-parse-atype)) (hk-cd-loop)))))
|
(begin
|
||||||
|
(append! fields (hk-parse-atype))
|
||||||
|
(hk-cd-loop)))))
|
||||||
(hk-cd-loop)
|
(hk-cd-loop)
|
||||||
(list :con-def name fields))))
|
(list :con-def name fields)))))))
|
||||||
(define
|
(define
|
||||||
hk-parse-tvars
|
hk-parse-tvars
|
||||||
(fn
|
(fn
|
||||||
@@ -1586,10 +1733,18 @@
|
|||||||
(= (hk-peek-type) "eof")
|
(= (hk-peek-type) "eof")
|
||||||
(hk-match? "vrbrace" nil)
|
(hk-match? "vrbrace" nil)
|
||||||
(hk-match? "rbrace" nil))))
|
(hk-match? "rbrace" nil))))
|
||||||
|
(define
|
||||||
|
hk-body-step
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((hk-match? "reserved" "import")
|
||||||
|
(append! imports (hk-parse-import)))
|
||||||
|
(:else (append! decls (hk-parse-decl))))))
|
||||||
(when
|
(when
|
||||||
(not (hk-body-at-end?))
|
(not (hk-body-at-end?))
|
||||||
(do
|
(do
|
||||||
(append! decls (hk-parse-decl))
|
(hk-body-step)
|
||||||
(define
|
(define
|
||||||
hk-body-loop
|
hk-body-loop
|
||||||
(fn
|
(fn
|
||||||
@@ -1600,7 +1755,7 @@
|
|||||||
(hk-advance!)
|
(hk-advance!)
|
||||||
(when
|
(when
|
||||||
(not (hk-body-at-end?))
|
(not (hk-body-at-end?))
|
||||||
(append! decls (hk-parse-decl)))
|
(hk-body-step))
|
||||||
(hk-body-loop)))))
|
(hk-body-loop)))))
|
||||||
(hk-body-loop)))
|
(hk-body-loop)))
|
||||||
(list imports decls))))
|
(list imports decls))))
|
||||||
|
|||||||
@@ -12,12 +12,7 @@
|
|||||||
|
|
||||||
(define
|
(define
|
||||||
hk-register-con!
|
hk-register-con!
|
||||||
(fn
|
(fn (cname arity type-name) (dict-set! hk-constructors cname {:arity arity :type type-name})))
|
||||||
(cname arity type-name)
|
|
||||||
(dict-set!
|
|
||||||
hk-constructors
|
|
||||||
cname
|
|
||||||
{:arity arity :type type-name})))
|
|
||||||
|
|
||||||
(define hk-is-con? (fn (name) (has-key? hk-constructors name)))
|
(define hk-is-con? (fn (name) (has-key? hk-constructors name)))
|
||||||
|
|
||||||
@@ -48,26 +43,15 @@
|
|||||||
(fn
|
(fn
|
||||||
(data-node)
|
(data-node)
|
||||||
(let
|
(let
|
||||||
((type-name (nth data-node 1))
|
((type-name (nth data-node 1)) (cons-list (nth data-node 3)))
|
||||||
(cons-list (nth data-node 3)))
|
|
||||||
(for-each
|
(for-each
|
||||||
(fn
|
(fn (cd) (hk-register-con! (nth cd 1) (len (nth cd 2)) type-name))
|
||||||
(cd)
|
|
||||||
(hk-register-con!
|
|
||||||
(nth cd 1)
|
|
||||||
(len (nth cd 2))
|
|
||||||
type-name))
|
|
||||||
cons-list))))
|
cons-list))))
|
||||||
|
|
||||||
;; (:newtype NAME TVARS CNAME FIELD)
|
;; (:newtype NAME TVARS CNAME FIELD)
|
||||||
(define
|
(define
|
||||||
hk-register-newtype!
|
hk-register-newtype!
|
||||||
(fn
|
(fn (nt-node) (hk-register-con! (nth nt-node 3) 1 (nth nt-node 1))))
|
||||||
(nt-node)
|
|
||||||
(hk-register-con!
|
|
||||||
(nth nt-node 3)
|
|
||||||
1
|
|
||||||
(nth nt-node 1))))
|
|
||||||
|
|
||||||
;; Walk a decls list, registering every `data` / `newtype` decl.
|
;; Walk a decls list, registering every `data` / `newtype` decl.
|
||||||
(define
|
(define
|
||||||
@@ -78,15 +62,9 @@
|
|||||||
(fn
|
(fn
|
||||||
(d)
|
(d)
|
||||||
(cond
|
(cond
|
||||||
((and
|
((and (list? d) (not (empty? d)) (= (first d) "data"))
|
||||||
(list? d)
|
|
||||||
(not (empty? d))
|
|
||||||
(= (first d) "data"))
|
|
||||||
(hk-register-data! d))
|
(hk-register-data! d))
|
||||||
((and
|
((and (list? d) (not (empty? d)) (= (first d) "newtype"))
|
||||||
(list? d)
|
|
||||||
(not (empty? d))
|
|
||||||
(= (first d) "newtype"))
|
|
||||||
(hk-register-newtype! d))
|
(hk-register-newtype! d))
|
||||||
(:else nil)))
|
(:else nil)))
|
||||||
decls)))
|
decls)))
|
||||||
@@ -99,16 +77,12 @@
|
|||||||
((nil? ast) nil)
|
((nil? ast) nil)
|
||||||
((not (list? ast)) nil)
|
((not (list? ast)) nil)
|
||||||
((empty? ast) nil)
|
((empty? ast) nil)
|
||||||
((= (first ast) "program")
|
((= (first ast) "program") (hk-register-decls! (nth ast 1)))
|
||||||
(hk-register-decls! (nth ast 1)))
|
((= (first ast) "module") (hk-register-decls! (nth ast 4)))
|
||||||
((= (first ast) "module")
|
|
||||||
(hk-register-decls! (nth ast 4)))
|
|
||||||
(:else nil))))
|
(:else nil))))
|
||||||
|
|
||||||
;; Convenience: source → AST → desugar → register.
|
;; Convenience: source → AST → desugar → register.
|
||||||
(define
|
(define hk-load-source! (fn (src) (hk-register-program! (hk-core src))))
|
||||||
hk-load-source!
|
|
||||||
(fn (src) (hk-register-program! (hk-core src))))
|
|
||||||
|
|
||||||
;; ── Built-in constructors pre-registered ─────────────────────
|
;; ── Built-in constructors pre-registered ─────────────────────
|
||||||
;; Bool — used implicitly by `if`, comparison operators.
|
;; Bool — used implicitly by `if`, comparison operators.
|
||||||
@@ -128,3 +102,49 @@
|
|||||||
(hk-register-con! "LT" 0 "Ordering")
|
(hk-register-con! "LT" 0 "Ordering")
|
||||||
(hk-register-con! "EQ" 0 "Ordering")
|
(hk-register-con! "EQ" 0 "Ordering")
|
||||||
(hk-register-con! "GT" 0 "Ordering")
|
(hk-register-con! "GT" 0 "Ordering")
|
||||||
|
(hk-register-con! "SomeException" 1 "SomeException")
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-str?
|
||||||
|
(fn (v) (or (string? v) (and (dict? v) (has-key? v "hk-str")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-str-head
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(if
|
||||||
|
(string? v)
|
||||||
|
(char-code (char-at v 0))
|
||||||
|
(char-code (char-at (get v "hk-str") (get v "hk-off"))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-str-tail
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(let
|
||||||
|
((buf (if (string? v) v (get v "hk-str")))
|
||||||
|
(off (if (string? v) 1 (+ (get v "hk-off") 1))))
|
||||||
|
(if (>= off (string-length buf)) (list "[]") {:hk-off off :hk-str buf}))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-str-null?
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(if
|
||||||
|
(string? v)
|
||||||
|
(= (string-length v) 0)
|
||||||
|
(>= (get v "hk-off") (string-length (get v "hk-str"))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-str-to-native
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(if
|
||||||
|
(string? v)
|
||||||
|
v
|
||||||
|
(let
|
||||||
|
((buf (get v "hk-str")) (off (get v "hk-off")))
|
||||||
|
(reduce
|
||||||
|
(fn (acc i) (str acc (char-at buf i)))
|
||||||
|
""
|
||||||
|
(range off (string-length buf)))))))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"date": "2026-05-06",
|
"date": "2026-05-08",
|
||||||
"total_pass": 156,
|
"total_pass": 285,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"programs": {
|
"programs": {
|
||||||
"fib": {"pass": 2, "fail": 0},
|
"fib": {"pass": 2, "fail": 0},
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"nqueens": {"pass": 2, "fail": 0},
|
"nqueens": {"pass": 2, "fail": 0},
|
||||||
"calculator": {"pass": 5, "fail": 0},
|
"calculator": {"pass": 5, "fail": 0},
|
||||||
"collatz": {"pass": 11, "fail": 0},
|
"collatz": {"pass": 11, "fail": 0},
|
||||||
"palindrome": {"pass": 8, "fail": 0},
|
"palindrome": {"pass": 12, "fail": 0},
|
||||||
"maybe": {"pass": 12, "fail": 0},
|
"maybe": {"pass": 12, "fail": 0},
|
||||||
"fizzbuzz": {"pass": 12, "fail": 0},
|
"fizzbuzz": {"pass": 12, "fail": 0},
|
||||||
"anagram": {"pass": 9, "fail": 0},
|
"anagram": {"pass": 9, "fail": 0},
|
||||||
@@ -19,7 +19,25 @@
|
|||||||
"primes": {"pass": 12, "fail": 0},
|
"primes": {"pass": 12, "fail": 0},
|
||||||
"zipwith": {"pass": 9, "fail": 0},
|
"zipwith": {"pass": 9, "fail": 0},
|
||||||
"matrix": {"pass": 8, "fail": 0},
|
"matrix": {"pass": 8, "fail": 0},
|
||||||
"wordcount": {"pass": 7, "fail": 0},
|
"wordcount": {"pass": 10, "fail": 0},
|
||||||
"powers": {"pass": 14, "fail": 0}
|
"powers": {"pass": 14, "fail": 0},
|
||||||
|
"caesar": {"pass": 8, "fail": 0},
|
||||||
|
"runlength-str": {"pass": 9, "fail": 0},
|
||||||
|
"showadt": {"pass": 5, "fail": 0},
|
||||||
|
"showio": {"pass": 5, "fail": 0},
|
||||||
|
"partial": {"pass": 7, "fail": 0},
|
||||||
|
"statistics": {"pass": 5, "fail": 0},
|
||||||
|
"newton": {"pass": 5, "fail": 0},
|
||||||
|
"wordfreq": {"pass": 7, "fail": 0},
|
||||||
|
"mapgraph": {"pass": 6, "fail": 0},
|
||||||
|
"uniquewords": {"pass": 4, "fail": 0},
|
||||||
|
"setops": {"pass": 8, "fail": 0},
|
||||||
|
"shapes": {"pass": 5, "fail": 0},
|
||||||
|
"person": {"pass": 7, "fail": 0},
|
||||||
|
"config": {"pass": 10, "fail": 0},
|
||||||
|
"counter": {"pass": 7, "fail": 0},
|
||||||
|
"accumulate": {"pass": 8, "fail": 0},
|
||||||
|
"safediv": {"pass": 8, "fail": 0},
|
||||||
|
"trycatch": {"pass": 8, "fail": 0}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Haskell-on-SX Scoreboard
|
# Haskell-on-SX Scoreboard
|
||||||
|
|
||||||
Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
Updated 2026-05-08 · Phase 6 (prelude extras + 18 programs)
|
||||||
|
|
||||||
| Program | Tests | Status |
|
| Program | Tests | Status |
|
||||||
|---------|-------|--------|
|
|---------|-------|--------|
|
||||||
@@ -10,7 +10,7 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
|||||||
| nqueens.hs | 2/2 | ✓ |
|
| nqueens.hs | 2/2 | ✓ |
|
||||||
| calculator.hs | 5/5 | ✓ |
|
| calculator.hs | 5/5 | ✓ |
|
||||||
| collatz.hs | 11/11 | ✓ |
|
| collatz.hs | 11/11 | ✓ |
|
||||||
| palindrome.hs | 8/8 | ✓ |
|
| palindrome.hs | 12/12 | ✓ |
|
||||||
| maybe.hs | 12/12 | ✓ |
|
| maybe.hs | 12/12 | ✓ |
|
||||||
| fizzbuzz.hs | 12/12 | ✓ |
|
| fizzbuzz.hs | 12/12 | ✓ |
|
||||||
| anagram.hs | 9/9 | ✓ |
|
| anagram.hs | 9/9 | ✓ |
|
||||||
@@ -20,6 +20,24 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
|||||||
| primes.hs | 12/12 | ✓ |
|
| primes.hs | 12/12 | ✓ |
|
||||||
| zipwith.hs | 9/9 | ✓ |
|
| zipwith.hs | 9/9 | ✓ |
|
||||||
| matrix.hs | 8/8 | ✓ |
|
| matrix.hs | 8/8 | ✓ |
|
||||||
| wordcount.hs | 7/7 | ✓ |
|
| wordcount.hs | 10/10 | ✓ |
|
||||||
| powers.hs | 14/14 | ✓ |
|
| powers.hs | 14/14 | ✓ |
|
||||||
| **Total** | **156/156** | **18/18 programs** |
|
| caesar.hs | 8/8 | ✓ |
|
||||||
|
| runlength-str.hs | 9/9 | ✓ |
|
||||||
|
| showadt.hs | 5/5 | ✓ |
|
||||||
|
| showio.hs | 5/5 | ✓ |
|
||||||
|
| partial.hs | 7/7 | ✓ |
|
||||||
|
| statistics.hs | 5/5 | ✓ |
|
||||||
|
| newton.hs | 5/5 | ✓ |
|
||||||
|
| wordfreq.hs | 7/7 | ✓ |
|
||||||
|
| mapgraph.hs | 6/6 | ✓ |
|
||||||
|
| uniquewords.hs | 4/4 | ✓ |
|
||||||
|
| setops.hs | 8/8 | ✓ |
|
||||||
|
| shapes.hs | 5/5 | ✓ |
|
||||||
|
| person.hs | 7/7 | ✓ |
|
||||||
|
| config.hs | 10/10 | ✓ |
|
||||||
|
| counter.hs | 7/7 | ✓ |
|
||||||
|
| accumulate.hs | 8/8 | ✓ |
|
||||||
|
| safediv.hs | 8/8 | ✓ |
|
||||||
|
| trycatch.hs | 8/8 | ✓ |
|
||||||
|
| **Total** | **285/285** | **36/36 programs** |
|
||||||
|
|||||||
62
lib/haskell/set.sx
Normal file
62
lib/haskell/set.sx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
;; set.sx — Phase 12 Data.Set: wraps Data.Map with unit values.
|
||||||
|
;;
|
||||||
|
;; A Set is a Map from key to (). All set operations delegate to the map
|
||||||
|
;; ops, ignoring the value side. Storage representation matches Data.Map:
|
||||||
|
;;
|
||||||
|
;; Empty → ("Map-Empty")
|
||||||
|
;; Node → ("Map-Node" key () left right size)
|
||||||
|
;;
|
||||||
|
;; Tradeoff: trivial maintenance burden, slight overhead per node from
|
||||||
|
;; the unused value slot. Faster path forward than re-implementing the
|
||||||
|
;; weight-balanced BST.
|
||||||
|
;;
|
||||||
|
;; Functions live in this file; the Haskell-level `import Data.Set` /
|
||||||
|
;; `import qualified Data.Set as Set` wiring (next Phase 12 box) binds
|
||||||
|
;; them under the chosen alias.
|
||||||
|
|
||||||
|
(define hk-set-unit (list "Tuple"))
|
||||||
|
|
||||||
|
(define hk-set-empty hk-map-empty)
|
||||||
|
|
||||||
|
(define hk-set-singleton (fn (k) (hk-map-singleton k hk-set-unit)))
|
||||||
|
|
||||||
|
(define hk-set-insert (fn (k s) (hk-map-insert k hk-set-unit s)))
|
||||||
|
|
||||||
|
(define hk-set-delete hk-map-delete)
|
||||||
|
(define hk-set-member hk-map-member)
|
||||||
|
(define hk-set-size hk-map-size)
|
||||||
|
(define hk-set-null hk-map-null)
|
||||||
|
(define hk-set-to-asc-list hk-map-keys)
|
||||||
|
(define hk-set-to-list hk-map-keys)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-from-list
|
||||||
|
(fn (xs) (reduce (fn (acc k) (hk-set-insert k acc)) hk-set-empty xs)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-union
|
||||||
|
(fn (a b) (hk-map-union-with (fn (x y) hk-set-unit) a b)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-intersection
|
||||||
|
(fn (a b) (hk-map-intersection-with (fn (x y) hk-set-unit) a b)))
|
||||||
|
|
||||||
|
(define hk-set-difference hk-map-difference)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-is-subset-of
|
||||||
|
(fn (a b) (= (hk-map-size (hk-map-difference a b)) 0)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-filter
|
||||||
|
(fn (p s) (hk-map-filter-with-key (fn (k v) (p k)) s)))
|
||||||
|
|
||||||
|
(define hk-set-map (fn (f s) (hk-set-from-list (map f (hk-map-keys s)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-foldr
|
||||||
|
(fn (f z s) (hk-map-foldr-with-key (fn (k v acc) (f k acc)) z s)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-set-foldl
|
||||||
|
(fn (f z s) (hk-map-foldl-with-key (fn (acc k v) (f acc k)) z s)))
|
||||||
@@ -55,6 +55,8 @@ for FILE in "${FILES[@]}"; do
|
|||||||
(load "lib/haskell/runtime.sx")
|
(load "lib/haskell/runtime.sx")
|
||||||
(load "lib/haskell/match.sx")
|
(load "lib/haskell/match.sx")
|
||||||
(load "lib/haskell/eval.sx")
|
(load "lib/haskell/eval.sx")
|
||||||
|
(load "lib/haskell/map.sx")
|
||||||
|
(load "lib/haskell/set.sx")
|
||||||
$INFER_LOAD
|
$INFER_LOAD
|
||||||
(load "lib/haskell/testlib.sx")
|
(load "lib/haskell/testlib.sx")
|
||||||
(epoch 2)
|
(epoch 2)
|
||||||
@@ -98,6 +100,8 @@ EPOCHS
|
|||||||
(load "lib/haskell/runtime.sx")
|
(load "lib/haskell/runtime.sx")
|
||||||
(load "lib/haskell/match.sx")
|
(load "lib/haskell/match.sx")
|
||||||
(load "lib/haskell/eval.sx")
|
(load "lib/haskell/eval.sx")
|
||||||
|
(load "lib/haskell/map.sx")
|
||||||
|
(load "lib/haskell/set.sx")
|
||||||
$INFER_LOAD
|
$INFER_LOAD
|
||||||
(load "lib/haskell/testlib.sx")
|
(load "lib/haskell/testlib.sx")
|
||||||
(epoch 2)
|
(epoch 2)
|
||||||
|
|||||||
@@ -56,3 +56,21 @@
|
|||||||
(append!
|
(append!
|
||||||
hk-test-fails
|
hk-test-fails
|
||||||
{:actual actual :expected expected :name name})))))
|
{:actual actual :expected expected :name name})))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-test-error
|
||||||
|
(fn
|
||||||
|
(name thunk expected-substring)
|
||||||
|
(let
|
||||||
|
((caught (guard (e (true (if (string? e) e (str e)))) (begin (thunk) nil))))
|
||||||
|
(cond
|
||||||
|
((nil? caught)
|
||||||
|
(do
|
||||||
|
(set! hk-test-fail (+ hk-test-fail 1))
|
||||||
|
(append! hk-test-fails {:actual "no error raised" :expected (str "error containing: " expected-substring) :name name})))
|
||||||
|
((>= (index-of caught expected-substring) 0)
|
||||||
|
(set! hk-test-pass (+ hk-test-pass 1)))
|
||||||
|
(:else
|
||||||
|
(do
|
||||||
|
(set! hk-test-fail (+ hk-test-fail 1))
|
||||||
|
(append! hk-test-fails {:actual caught :expected (str "error containing: " expected-substring) :name name})))))))
|
||||||
|
|||||||
86
lib/haskell/tests/class-defaults.sx
Normal file
86
lib/haskell/tests/class-defaults.sx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
;; class-defaults.sx — Phase 13: class default method implementations.
|
||||||
|
|
||||||
|
;; ── Eq default: myNeq derived from myEq via `not (myEq x y)` ──
|
||||||
|
(define
|
||||||
|
hk-myeq-source
|
||||||
|
"class MyEq a where\n myEq :: a -> a -> Bool\n myNeq :: a -> a -> Bool\n myNeq x y = not (myEq x y)\ninstance MyEq Int where\n myEq x y = x == y\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Eq default: myNeq 3 5 = True (no explicit myNeq in instance)"
|
||||||
|
(hk-deep-force (hk-run (str hk-myeq-source "main = myNeq 3 5\n")))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Eq default: myNeq 3 3 = False"
|
||||||
|
(hk-deep-force (hk-run (str hk-myeq-source "main = myNeq 3 3\n")))
|
||||||
|
(list "False"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Eq default: myEq still works in same instance"
|
||||||
|
(hk-deep-force (hk-run (str hk-myeq-source "main = myEq 7 7\n")))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
;; ── Override path: instance can still provide the method explicitly. ──
|
||||||
|
(hk-test
|
||||||
|
"Default override: instance-provided beats class default"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Hi a where\n greet :: a -> String\n greet x = \"default\"\ninstance Hi Bool where\n greet x = \"override\"\nmain = greet True"))
|
||||||
|
"override")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Default fallback: empty instance picks default"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Hi a where\n greet :: a -> String\n greet x = \"default\"\ninstance Hi Bool where\nmain = greet True"))
|
||||||
|
"default")
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-myord-source
|
||||||
|
"class MyOrd a where\n myCmp :: a -> a -> Bool\n myMax :: a -> a -> a\n myMin :: a -> a -> a\n myMax a b = if myCmp a b then a else b\n myMin a b = if myCmp a b then b else a\ninstance MyOrd Int where\n myCmp x y = x >= y\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Ord default: myMax 3 5 = 5"
|
||||||
|
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 3 5\n")))
|
||||||
|
5)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Ord default: myMax 8 2 = 8"
|
||||||
|
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 8 2\n")))
|
||||||
|
8)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Ord default: myMin 3 5 = 3"
|
||||||
|
(hk-deep-force (hk-run (str hk-myord-source "main = myMin 3 5\n")))
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Ord default: myMin 8 2 = 2"
|
||||||
|
(hk-deep-force (hk-run (str hk-myord-source "main = myMin 8 2\n")))
|
||||||
|
2)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Ord default: myMax of equals returns first"
|
||||||
|
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 4 4\n")))
|
||||||
|
4)
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-mynum-source
|
||||||
|
"class MyNum a where\n mySub :: a -> a -> a\n myLt :: a -> a -> Bool\n myNegate :: a -> a\n myAbs :: a -> a\n myNegate x = mySub (mySub x x) x\n myAbs x = if myLt x (mySub x x) then myNegate x else x\ninstance MyNum Int where\n mySub x y = x - y\n myLt x y = x < y\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Num default: myNegate 5 = -5"
|
||||||
|
(hk-deep-force (hk-run (str hk-mynum-source "main = myNegate 5\n")))
|
||||||
|
-5)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Num default: myAbs (myNegate 7) = 7"
|
||||||
|
(hk-deep-force (hk-run (str hk-mynum-source "main = myAbs (myNegate 7)\n")))
|
||||||
|
7)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Num default: myAbs 9 = 9"
|
||||||
|
(hk-deep-force (hk-run (str hk-mynum-source "main = myAbs 9\n")))
|
||||||
|
9)
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
"deriving Show: constructor with arg"
|
"deriving Show: constructor with arg"
|
||||||
(hk-deep-force
|
(hk-deep-force
|
||||||
(hk-run "data Wrapper = Wrap Int deriving (Show)\nmain = show (Wrap 42)"))
|
(hk-run "data Wrapper = Wrap Int deriving (Show)\nmain = show (Wrap 42)"))
|
||||||
"(Wrap 42)")
|
"Wrap 42")
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"deriving Show: nested constructors"
|
"deriving Show: nested constructors"
|
||||||
(hk-deep-force
|
(hk-deep-force
|
||||||
(hk-run
|
(hk-run
|
||||||
"data Tree = Leaf | Node Int Tree Tree deriving (Show)\nmain = show (Node 1 Leaf Leaf)"))
|
"data Tree = Leaf | Node Int Tree Tree deriving (Show)\nmain = show (Node 1 Leaf Leaf)"))
|
||||||
"(Node 1 Leaf Leaf)")
|
"Node 1 Leaf Leaf")
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"deriving Show: second constructor"
|
"deriving Show: second constructor"
|
||||||
@@ -30,6 +30,31 @@
|
|||||||
|
|
||||||
;; ─── Eq ──────────────────────────────────────────────────────────────────────
|
;; ─── Eq ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"deriving Show: nested ADT wraps inner constructor in parens"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"data Tree = Leaf | Node Int Tree Tree deriving (Show)\nmain = show (Node 1 Leaf (Node 2 Leaf Leaf))"))
|
||||||
|
"Node 1 Leaf (Node 2 Leaf Leaf)")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"deriving Show: Maybe Maybe wraps inner Just"
|
||||||
|
(hk-deep-force (hk-run "main = show (Just (Just 3))"))
|
||||||
|
"Just (Just 3)")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"deriving Show: negative argument wrapped in parens"
|
||||||
|
(hk-deep-force (hk-run "main = show (Just (negate 3))"))
|
||||||
|
"Just (-3)")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"deriving Show: list element does not need parens"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "data Box = Box [Int] deriving (Show)\nmain = show (Box [1,2,3])"))
|
||||||
|
"Box [1,2,3]")
|
||||||
|
|
||||||
|
;; ─── combined Eq + Show ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"deriving Eq: same constructor"
|
"deriving Eq: same constructor"
|
||||||
(hk-deep-force
|
(hk-deep-force
|
||||||
@@ -58,14 +83,12 @@
|
|||||||
"data Color = Red | Green | Blue deriving (Eq)\nmain = show (Red /= Blue)"))
|
"data Color = Red | Green | Blue deriving (Eq)\nmain = show (Red /= Blue)"))
|
||||||
"True")
|
"True")
|
||||||
|
|
||||||
;; ─── combined Eq + Show ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"deriving Eq Show: combined in parens"
|
"deriving Eq Show: combined"
|
||||||
(hk-deep-force
|
(hk-deep-force
|
||||||
(hk-run
|
(hk-run
|
||||||
"data Shape = Circle Int | Square Int deriving (Eq, Show)\nmain = show (Circle 5)"))
|
"data Shape = Circle Int | Square Int deriving (Eq, Show)\nmain = show (Circle 5)"))
|
||||||
"(Circle 5)")
|
"Circle 5")
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"deriving Eq Show: eq on constructor with arg"
|
"deriving Eq Show: eq on constructor with arg"
|
||||||
|
|||||||
99
lib/haskell/tests/errors.sx
Normal file
99
lib/haskell/tests/errors.sx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
;; errors.sx — Phase 9 error / undefined / partial-fn coverage via hk-test-error.
|
||||||
|
|
||||||
|
;; ── error builtin ────────────────────────────────────────────
|
||||||
|
(define
|
||||||
|
hk-as-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(cond
|
||||||
|
((and (list? xs) (= (first xs) "[]")) (list))
|
||||||
|
((and (list? xs) (= (first xs) ":"))
|
||||||
|
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||||
|
(:else xs))))
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"error: raises with literal message"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = error \"boom\"")))
|
||||||
|
"hk-error: boom")
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"error: raises with computed message"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = error (\"oops: \" ++ show 42)")))
|
||||||
|
"hk-error: oops: 42")
|
||||||
|
|
||||||
|
;; ── undefined ────────────────────────────────────────────────
|
||||||
|
(hk-test-error
|
||||||
|
"error: nested in if branch (only fires when forced)"
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(hk-deep-force (hk-run "main = if 1 == 1 then error \"taken\" else 0")))
|
||||||
|
"taken")
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"undefined: raises Prelude.undefined"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = undefined")))
|
||||||
|
"Prelude.undefined")
|
||||||
|
|
||||||
|
;; The non-strict path: undefined doesn't fire when not forced.
|
||||||
|
(hk-test-error
|
||||||
|
"undefined: forced via arithmetic"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = undefined + 1")))
|
||||||
|
"Prelude.undefined")
|
||||||
|
|
||||||
|
;; ── partial functions ───────────────────────────────────────
|
||||||
|
(hk-test
|
||||||
|
"undefined: lazy, not forced when discarded"
|
||||||
|
(hk-deep-force (hk-run "main = let _ = undefined in 5"))
|
||||||
|
5)
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"head []: raises Prelude.head: empty list"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = head []")))
|
||||||
|
"Prelude.head: empty list")
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"tail []: raises Prelude.tail: empty list"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = tail []")))
|
||||||
|
"Prelude.tail: empty list")
|
||||||
|
|
||||||
|
;; head and tail still work on non-empty lists.
|
||||||
|
(hk-test-error
|
||||||
|
"fromJust Nothing: raises Maybe.fromJust: Nothing"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = fromJust Nothing")))
|
||||||
|
"Maybe.fromJust: Nothing")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"head [42]: still works"
|
||||||
|
(hk-deep-force (hk-run "main = head [42]"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
;; ── error in IO context ─────────────────────────────────────
|
||||||
|
(hk-test
|
||||||
|
"tail [1,2,3]: still works"
|
||||||
|
(hk-as-list (hk-deep-force (hk-run "main = tail [1,2,3]")))
|
||||||
|
(list 2 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-run-io: error in main lands in io-lines"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = error \"caught here\"")))
|
||||||
|
(>= (index-of (str lines) "caught here") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; ── hk-test-error helper itself ─────────────────────────────
|
||||||
|
(hk-test
|
||||||
|
"hk-run-io: putStrLn before error preserves earlier output"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = do { putStrLn \"first\"; error \"died\"; putStrLn \"never\" }")))
|
||||||
|
(and
|
||||||
|
(>= (index-of (str lines) "first") 0)
|
||||||
|
(>= (index-of (str lines) "died") 0)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; hk-as-list helper for converting a forced Haskell cons into an SX list.
|
||||||
|
(hk-test-error
|
||||||
|
"hk-test-error: matches partial substring inside wrapped exception"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = error \"unique-marker-xyz\"")))
|
||||||
|
"unique-marker-xyz")
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
@@ -231,16 +231,82 @@
|
|||||||
1)
|
1)
|
||||||
|
|
||||||
;; ── Laziness: app args evaluate only when forced ──
|
;; ── Laziness: app args evaluate only when forced ──
|
||||||
|
(hk-test
|
||||||
|
"error builtin: raises with hk-error prefix"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "hk-error: boom") 0)))
|
||||||
|
(begin (hk-deep-force (hk-run "main = error \"boom\"")) false))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"error builtin: raises with computed message"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "hk-error: oops: 42") 0)))
|
||||||
|
(begin
|
||||||
|
(hk-deep-force (hk-run "main = error (\"oops: \" ++ show 42)"))
|
||||||
|
false))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"undefined: raises hk-error with Prelude.undefined message"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "hk-error: Prelude.undefined") 0)))
|
||||||
|
(begin (hk-deep-force (hk-run "main = undefined")) false))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"undefined: lazy — only fires when forced"
|
||||||
|
(hk-deep-force (hk-run "main = if True then 42 else undefined"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"head []: raises Prelude.head: empty list"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "Prelude.head: empty list") 0)))
|
||||||
|
(begin (hk-deep-force (hk-run "main = head []")) false))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"tail []: raises Prelude.tail: empty list"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "Prelude.tail: empty list") 0)))
|
||||||
|
(begin (hk-deep-force (hk-run "main = tail []")) false))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; ── not / id built-ins ──
|
||||||
|
(hk-test
|
||||||
|
"fromJust Nothing: raises Maybe.fromJust: Nothing"
|
||||||
|
(guard
|
||||||
|
(e (true (>= (index-of e "Maybe.fromJust: Nothing") 0)))
|
||||||
|
(begin (hk-deep-force (hk-run "main = fromJust Nothing")) false))
|
||||||
|
true)
|
||||||
|
(hk-test
|
||||||
|
"fromJust (Just 5) = 5"
|
||||||
|
(hk-deep-force (hk-run "main = fromJust (Just 5)"))
|
||||||
|
5)
|
||||||
|
(hk-test
|
||||||
|
"head [42] = 42 (still works for non-empty)"
|
||||||
|
(hk-deep-force (hk-run "main = head [42]"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"hk-test-error helper: catches matching error"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = error \"boom\"")))
|
||||||
|
"hk-error: boom")
|
||||||
|
|
||||||
|
(hk-test-error
|
||||||
|
"hk-test-error helper: catches head [] error"
|
||||||
|
(fn () (hk-deep-force (hk-run "main = head []")))
|
||||||
|
"Prelude.head: empty list")
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"second arg never forced"
|
"second arg never forced"
|
||||||
(hk-eval-expr-source
|
(hk-eval-expr-source "(\\x y -> x) 1 (error \"never\")")
|
||||||
"(\\x y -> x) 1 (error \"never\")")
|
|
||||||
1)
|
1)
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"first arg never forced"
|
"first arg never forced"
|
||||||
(hk-eval-expr-source
|
(hk-eval-expr-source "(\\x y -> y) (error \"never\") 99")
|
||||||
"(\\x y -> y) (error \"never\") 99")
|
|
||||||
99)
|
99)
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
@@ -251,9 +317,7 @@
|
|||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"lazy: const drops its second argument"
|
"lazy: const drops its second argument"
|
||||||
(hk-prog-val
|
(hk-prog-val "const x y = x\nresult = const 5 (error \"boom\")" "result")
|
||||||
"const x y = x\nresult = const 5 (error \"boom\")"
|
|
||||||
"result")
|
|
||||||
5)
|
5)
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
@@ -270,9 +334,10 @@
|
|||||||
"result")
|
"result")
|
||||||
(list "True"))
|
(list "True"))
|
||||||
|
|
||||||
;; ── not / id built-ins ──
|
|
||||||
(hk-test "not True" (hk-eval-expr-source "not True") (list "False"))
|
(hk-test "not True" (hk-eval-expr-source "not True") (list "False"))
|
||||||
|
|
||||||
(hk-test "not False" (hk-eval-expr-source "not False") (list "True"))
|
(hk-test "not False" (hk-eval-expr-source "not False") (list "True"))
|
||||||
|
|
||||||
(hk-test "id" (hk-eval-expr-source "id 42") 42)
|
(hk-test "id" (hk-eval-expr-source "id 42") 42)
|
||||||
|
|
||||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
|
|||||||
105
lib/haskell/tests/exceptions.sx
Normal file
105
lib/haskell/tests/exceptions.sx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
;; Phase 16 — Exception handling unit tests.
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"catch — success path returns the action result"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = catch (return 42) (\\(SomeException m) -> return 0)"))
|
||||||
|
(list "IO" 42))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"catch — error caught, handler receives message"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = catch (error \"boom\") (\\(SomeException m) -> return m)"))
|
||||||
|
(list "IO" "boom"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"try — success returns Right v"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "main = try (return 42)"))
|
||||||
|
(list "IO" (list "Right" 42)))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"try — error returns Left (SomeException msg)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "main = try (error \"oops\")"))
|
||||||
|
(list "IO" (list "Left" (list "SomeException" "oops"))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"handle — flip catch — caught error message"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = handle (\\(SomeException m) -> return m) (error \"hot\")"))
|
||||||
|
(list "IO" "hot"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"throwIO + catch — handler sees the SomeException"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = catch (throwIO (SomeException \"bang\")) (\\(SomeException m) -> return m)"))
|
||||||
|
(list "IO" "bang"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"throwIO + try — Left side"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = try (throwIO (SomeException \"x\"))"))
|
||||||
|
(list "IO" (list "Left" (list "SomeException" "x"))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"evaluate — pure value returns IO v"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "main = evaluate (1 + 2 + 3)"))
|
||||||
|
(list "IO" 6))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"evaluate — error surfaces as catchable exception"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = catch (evaluate (error \"deep\")) (\\(SomeException m) -> return m)"))
|
||||||
|
(list "IO" "deep"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"nested catch — inner handler runs first"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = catch (catch (error \"inner\") (\\(SomeException m) -> error (m ++ \"-rethrown\"))) (\\(SomeException m) -> return m)"))
|
||||||
|
(list "IO" "inner-rethrown"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"catch chain — handler can succeed inside IO"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"main = do { x <- catch (error \"e1\") (\\(SomeException m) -> return 100); return (x + 1) }"))
|
||||||
|
(list "IO" 101))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"try then bind on Right"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"branch (Right v) = return (v * 2)
|
||||||
|
branch (Left _) = return 0
|
||||||
|
main = do { r <- try (return 21); branch r }"))
|
||||||
|
(list "IO" 42))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"try then bind on Left"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"branch (Right _) = return \"ok\"
|
||||||
|
branch (Left (SomeException m)) = return m
|
||||||
|
main = do { r <- try (error \"failed\"); branch r }"))
|
||||||
|
(list "IO" "failed"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"catch — handler can use closed-over IORef"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef
|
||||||
|
main = do
|
||||||
|
r <- IORef.newIORef 0
|
||||||
|
catch (error \"x\") (\\(SomeException m) -> IORef.writeIORef r 7)
|
||||||
|
v <- IORef.readIORef r
|
||||||
|
return v"))
|
||||||
|
(list "IO" 7))
|
||||||
31
lib/haskell/tests/instance-where.sx
Normal file
31
lib/haskell/tests/instance-where.sx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
;; instance-where.sx — Phase 13: where-clauses inside instance bodies.
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"instance method body with where-helper (Bool)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Greet a where\n greet :: a -> String\ninstance Greet Bool where\n greet x = mkMsg x\n where mkMsg True = \"yes\"\n mkMsg False = \"no\"\nmain = greet True"))
|
||||||
|
"yes")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"instance method body with where-helper (False branch)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Greet a where\n greet :: a -> String\ninstance Greet Bool where\n greet x = mkMsg x\n where mkMsg True = \"yes\"\n mkMsg False = \"no\"\nmain = greet False"))
|
||||||
|
"no")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"instance method body with where-binding referenced multiple times"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Twice a where\n twice :: a -> Int\ninstance Twice Int where\n twice x = h + h\n where h = x + 1\nmain = twice 5"))
|
||||||
|
12)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"instance method body with multi-binding where"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"class Calc a where\n calc :: a -> Int\ninstance Calc Int where\n calc x = a + b\n where a = x * 2\n b = x + 1\nmain = calc 3"))
|
||||||
|
10)
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
@@ -64,12 +64,11 @@
|
|||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
"readFile error on missing file"
|
"readFile error on missing file"
|
||||||
(guard
|
|
||||||
(e (true (>= (index-of e "file not found") 0)))
|
|
||||||
(begin
|
(begin
|
||||||
(set! hk-vfs (dict))
|
(set! hk-vfs (dict))
|
||||||
(hk-run-io "main = readFile \"no.txt\" >>= putStrLn")
|
(let
|
||||||
false))
|
((lines (hk-run-io "main = readFile \"no.txt\" >>= putStrLn")))
|
||||||
|
(>= (index-of (str lines) "file not found") 0)))
|
||||||
true)
|
true)
|
||||||
|
|
||||||
(hk-test
|
(hk-test
|
||||||
|
|||||||
94
lib/haskell/tests/ioref.sx
Normal file
94
lib/haskell/tests/ioref.sx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
;; Phase 15 — IORef unit tests.
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newIORef + readIORef returns initial value"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 42; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 42))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"writeIORef updates the cell"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 99; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 99))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"writeIORef returns IO ()"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 1 }"))
|
||||||
|
(list "IO" (list "Tuple")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"modifyIORef applies a function"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 5; IORef.modifyIORef r (\\x -> x * 2); v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 10))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"modifyIORef' (strict) applies a function"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 7; IORef.modifyIORef' r (\\x -> x + 3); v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 10))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"two reads return the same value"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 11; a <- IORef.readIORef r; b <- IORef.readIORef r; return (a + b) }"))
|
||||||
|
(list "IO" 22))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"shared ref across do-steps: write then read"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 1; IORef.writeIORef r 2; IORef.writeIORef r 3; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"two refs are independent"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r1 <- IORef.newIORef 1; r2 <- IORef.newIORef 2; IORef.writeIORef r1 10; a <- IORef.readIORef r1; b <- IORef.readIORef r2; return (a + b) }"))
|
||||||
|
(list "IO" 12))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"string-valued IORef"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef \"hi\"; IORef.writeIORef r \"bye\"; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" "bye"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"list-valued IORef + cons"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef [1,2,3]; IORef.modifyIORef r (\\xs -> 0 : xs); v <- IORef.readIORef r; return v }"))
|
||||||
|
(list
|
||||||
|
"IO"
|
||||||
|
(list ":" 0 (list ":" 1 (list ":" 2 (list ":" 3 (list "[]")))))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter loop: increment N times"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nloop r 0 = return ()\nloop r n = do { IORef.modifyIORef r (\\x -> x + 1); loop r (n - 1) }\nmain = do { r <- IORef.newIORef 0; loop r 10; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 10))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"modifyIORef' inside a loop"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\ngo r 0 = return ()\ngo r n = do { IORef.modifyIORef' r (\\x -> x + n); go r (n - 1) }\nmain = do { r <- IORef.newIORef 0; go r 5; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 15))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newIORef inside a function passed via parameter"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.IORef as IORef\nbump r = IORef.modifyIORef r (\\x -> x + 100)\nmain = do { r <- IORef.newIORef 1; bump r; v <- IORef.readIORef r; return v }"))
|
||||||
|
(list "IO" 101))
|
||||||
196
lib/haskell/tests/map.sx
Normal file
196
lib/haskell/tests/map.sx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
;; map.sx — Phase 11 Data.Map unit tests.
|
||||||
|
;;
|
||||||
|
;; Tests both the SX-level `hk-map-*` helpers and the Haskell-level
|
||||||
|
;; `Map.*` aliases bound by the import handler.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-as-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(cond
|
||||||
|
((and (list? xs) (= (first xs) "[]")) (list))
|
||||||
|
((and (list? xs) (= (first xs) ":"))
|
||||||
|
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||||
|
(:else xs))))
|
||||||
|
|
||||||
|
;; ── SX-level (direct hk-map-*) ───────────────────────────────
|
||||||
|
(hk-test
|
||||||
|
"hk-map-empty: size 0, null true"
|
||||||
|
(list (hk-map-size hk-map-empty) (hk-map-null hk-map-empty))
|
||||||
|
(list 0 true))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-singleton: lookup hit"
|
||||||
|
(let
|
||||||
|
((m (hk-map-singleton 5 "five")))
|
||||||
|
(list (hk-map-size m) (hk-map-lookup 5 m)))
|
||||||
|
(list 1 (list "Just" "five")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-insert: lookup hit on inserted"
|
||||||
|
(let ((m (hk-map-insert 1 "a" hk-map-empty))) (hk-map-lookup 1 m))
|
||||||
|
(list "Just" "a"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-lookup: miss returns Nothing"
|
||||||
|
(hk-map-lookup 99 (hk-map-singleton 1 "a"))
|
||||||
|
(list "Nothing"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-insert: overwrites existing key"
|
||||||
|
(let
|
||||||
|
((m (hk-map-insert 1 "second" (hk-map-insert 1 "first" hk-map-empty))))
|
||||||
|
(hk-map-lookup 1 m))
|
||||||
|
(list "Just" "second"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-delete: removes key"
|
||||||
|
(let
|
||||||
|
((m (hk-map-insert 2 "b" (hk-map-insert 1 "a" hk-map-empty))))
|
||||||
|
(let
|
||||||
|
((m2 (hk-map-delete 1 m)))
|
||||||
|
(list (hk-map-size m2) (hk-map-lookup 1 m2) (hk-map-lookup 2 m2))))
|
||||||
|
(list 1 (list "Nothing") (list "Just" "b")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-delete: missing key is no-op"
|
||||||
|
(let ((m (hk-map-singleton 1 "a"))) (hk-map-size (hk-map-delete 99 m)))
|
||||||
|
1)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-member: true on existing"
|
||||||
|
(hk-map-member 1 (hk-map-singleton 1 "a"))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-member: false on missing"
|
||||||
|
(hk-map-member 99 (hk-map-singleton 1 "a"))
|
||||||
|
false)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-from-list: builds map; keys sorted"
|
||||||
|
(hk-map-keys
|
||||||
|
(hk-map-from-list
|
||||||
|
(list (list 3 "c") (list 1 "a") (list 5 "e") (list 2 "b"))))
|
||||||
|
(list 1 2 3 5))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-from-list: duplicates — last wins"
|
||||||
|
(hk-map-lookup
|
||||||
|
1
|
||||||
|
(hk-map-from-list (list (list 1 "first") (list 1 "second"))))
|
||||||
|
(list "Just" "second"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-to-asc-list: ordered traversal"
|
||||||
|
(hk-map-to-asc-list
|
||||||
|
(hk-map-from-list (list (list 3 "c") (list 1 "a") (list 2 "b"))))
|
||||||
|
(list (list 1 "a") (list 2 "b") (list 3 "c")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-elems: in key order"
|
||||||
|
(hk-map-elems
|
||||||
|
(hk-map-from-list (list (list 3 30) (list 1 10) (list 2 20))))
|
||||||
|
(list 10 20 30))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-union-with: combines duplicates"
|
||||||
|
(hk-map-to-asc-list
|
||||||
|
(hk-map-union-with
|
||||||
|
(fn (a b) (str a "+" b))
|
||||||
|
(hk-map-from-list (list (list 1 "a") (list 2 "b")))
|
||||||
|
(hk-map-from-list (list (list 2 "B") (list 3 "c")))))
|
||||||
|
(list (list 1 "a") (list 2 "b+B") (list 3 "c")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-intersection-with: keeps shared keys"
|
||||||
|
(hk-map-to-asc-list
|
||||||
|
(hk-map-intersection-with
|
||||||
|
+
|
||||||
|
(hk-map-from-list (list (list 1 10) (list 2 20)))
|
||||||
|
(hk-map-from-list (list (list 2 200) (list 3 30)))))
|
||||||
|
(list (list 2 220)))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-difference: drops m2 keys"
|
||||||
|
(hk-map-keys
|
||||||
|
(hk-map-difference
|
||||||
|
(hk-map-from-list (list (list 1 "a") (list 2 "b") (list 3 "c")))
|
||||||
|
(hk-map-from-list (list (list 2 "x")))))
|
||||||
|
(list 1 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-foldl-with-key: in-order accumulate"
|
||||||
|
(hk-map-foldl-with-key
|
||||||
|
(fn (acc k v) (str acc k v))
|
||||||
|
""
|
||||||
|
(hk-map-from-list (list (list 3 "c") (list 1 "a") (list 2 "b"))))
|
||||||
|
"1a2b3c")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-map-with-key: transforms values"
|
||||||
|
(hk-map-to-asc-list
|
||||||
|
(hk-map-map-with-key
|
||||||
|
(fn (k v) (* k v))
|
||||||
|
(hk-map-from-list (list (list 2 10) (list 3 100)))))
|
||||||
|
(list (list 2 20) (list 3 300)))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-filter-with-key: keeps matches"
|
||||||
|
(hk-map-keys
|
||||||
|
(hk-map-filter-with-key
|
||||||
|
(fn (k v) (> k 1))
|
||||||
|
(hk-map-from-list (list (list 1 "a") (list 2 "b") (list 3 "c")))))
|
||||||
|
(list 2 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-adjust: applies f to existing"
|
||||||
|
(hk-map-lookup
|
||||||
|
1
|
||||||
|
(hk-map-adjust (fn (v) (* v 10)) 1 (hk-map-singleton 1 5)))
|
||||||
|
(list "Just" 50))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-insert-with: combines on existing"
|
||||||
|
(hk-map-lookup 1 (hk-map-insert-with + 1 5 (hk-map-singleton 1 10)))
|
||||||
|
(list "Just" 15))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"hk-map-alter: Nothing → delete"
|
||||||
|
(hk-map-size
|
||||||
|
(hk-map-alter
|
||||||
|
(fn (mv) (list "Nothing"))
|
||||||
|
1
|
||||||
|
(hk-map-from-list (list (list 1 "a") (list 2 "b")))))
|
||||||
|
1)
|
||||||
|
|
||||||
|
;; ── Haskell-level (Map.*) via import wiring ─────────────────
|
||||||
|
(hk-test
|
||||||
|
"Map.size after Map.insert chain"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.Map as Map\nmain = Map.size (Map.insert 2 \"b\" (Map.insert 1 \"a\" Map.empty))"))
|
||||||
|
2)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Map.lookup hit"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.Map as Map\nmain = Map.lookup 1 (Map.insert 1 \"a\" Map.empty)"))
|
||||||
|
(list "Just" "a"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Map.lookup miss"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.Map as Map\nmain = Map.lookup 99 (Map.insert 1 \"a\" Map.empty)"))
|
||||||
|
(list "Nothing"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"Map.member true"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
"import qualified Data.Map as Map\nmain = Map.member 5 (Map.insert 5 \"x\" Map.empty)"))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
180
lib/haskell/tests/numerics.sx
Normal file
180
lib/haskell/tests/numerics.sx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
;; numerics.sx — Phase 10 numeric tower verification.
|
||||||
|
;;
|
||||||
|
;; Practical integer-precision limit in Haskell-on-SX:
|
||||||
|
;; • Raw SX `(* a b)` stays exact up to ±2^62 (≈ 4.6e18, OCaml int63).
|
||||||
|
;; • BUT the Haskell tokenizer/parser parses an integer literal as a float
|
||||||
|
;; once it exceeds 2^53 (≈ 9.007e15). Once any operand is a float, the
|
||||||
|
;; binop result is a float (and decimal-precision is lost past 2^53).
|
||||||
|
;; • Therefore: programs that stay below ~9e15 are exact; larger literals
|
||||||
|
;; or accumulated products silently become floats. `factorial 18` is the
|
||||||
|
;; last factorial that stays exact (6.4e15); `factorial 19` already floats.
|
||||||
|
;;
|
||||||
|
;; In Haskell terms, `Int` and `Integer` both currently map to SX number, so
|
||||||
|
;; we don't yet support arbitrary-precision Integer. Documented; unbounded
|
||||||
|
;; Integer is out of scope for Phase 10 — see Phase 11+ if it becomes needed.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-as-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(cond
|
||||||
|
((and (list? xs) (= (first xs) "[]")) (list))
|
||||||
|
((and (list? xs) (= (first xs) ":"))
|
||||||
|
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||||
|
(:else xs))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"factorial 10 = 3628800 (small, exact)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 10"))
|
||||||
|
3628800)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"factorial 15 = 1307674368000 (mid-range, exact)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 15"))
|
||||||
|
1307674368000)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"factorial 18 = 6402373705728000 (last exact factorial)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 18"))
|
||||||
|
6402373705728000)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"1000000 * 1000000 = 10^12 (exact)"
|
||||||
|
(hk-deep-force (hk-run "main = 1000000 * 1000000"))
|
||||||
|
1000000000000)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"1000000000 * 1000000000 = 10^18 (exact, at boundary)"
|
||||||
|
(hk-deep-force (hk-run "main = 1000000000 * 1000000000"))
|
||||||
|
1e+18)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"2^62 boundary: pow accumulates exactly"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "pow b 0 = 1\npow b n = b * pow b (n - 1)\nmain = pow 2 62"))
|
||||||
|
4.6116860184273879e+18)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"show factorial 12 = 479001600 (whole, fits in 32-bit)"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = show (fact 12)"))
|
||||||
|
"479001600")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"negate large positive — preserves magnitude"
|
||||||
|
(hk-deep-force (hk-run "main = negate 1000000000000000000"))
|
||||||
|
-1e+18)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"abs negative large — preserves magnitude"
|
||||||
|
(hk-deep-force (hk-run "main = abs (negate 1000000000000000000)"))
|
||||||
|
1e+18)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"div on large ints"
|
||||||
|
(hk-deep-force (hk-run "main = div 1000000000000000000 1000000000"))
|
||||||
|
1000000000)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromIntegral 42 = 42 (identity in our runtime)"
|
||||||
|
(hk-deep-force (hk-run "main = fromIntegral 42"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromIntegral preserves negative"
|
||||||
|
(hk-deep-force (hk-run "main = fromIntegral (negate 7)"))
|
||||||
|
-7)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromIntegral round-trips through arithmetic"
|
||||||
|
(hk-deep-force (hk-run "main = fromIntegral 5 + fromIntegral 3"))
|
||||||
|
8)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromIntegral in a program (mixing with map)"
|
||||||
|
(hk-as-list (hk-deep-force (hk-run "main = map fromIntegral [1,2,3]")))
|
||||||
|
(list 1 2 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"toInteger 100 = 100 (identity)"
|
||||||
|
(hk-deep-force (hk-run "main = toInteger 100"))
|
||||||
|
100)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromInteger 7 = 7 (identity)"
|
||||||
|
(hk-deep-force (hk-run "main = fromInteger 7"))
|
||||||
|
7)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"toInteger / fromInteger round-trip"
|
||||||
|
(hk-deep-force (hk-run "main = fromInteger (toInteger 42)"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"toInteger preserves negative"
|
||||||
|
(hk-deep-force (hk-run "main = toInteger (negate 13)"))
|
||||||
|
-13)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"show 3.14 = 3.14"
|
||||||
|
(hk-deep-force (hk-run "main = show 3.14"))
|
||||||
|
"3.14")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"show 1.0e10 — whole-valued float renders as decimal (int/float ambiguity)"
|
||||||
|
(hk-deep-force (hk-run "main = show 1.0e10"))
|
||||||
|
"10000000000")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"show 0.001 uses scientific form (sub-0.1)"
|
||||||
|
(hk-deep-force (hk-run "main = show 0.001"))
|
||||||
|
"1.0e-3")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"show negative float"
|
||||||
|
(hk-deep-force (hk-run "main = show (negate 3.14)"))
|
||||||
|
"-3.14")
|
||||||
|
|
||||||
|
(hk-test "sqrt 16 = 4" (hk-deep-force (hk-run "main = sqrt 16")) 4)
|
||||||
|
|
||||||
|
(hk-test "floor 3.7 = 3" (hk-deep-force (hk-run "main = floor 3.7")) 3)
|
||||||
|
|
||||||
|
(hk-test "ceiling 3.2 = 4" (hk-deep-force (hk-run "main = ceiling 3.2")) 4)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"ceiling on whole = self"
|
||||||
|
(hk-deep-force (hk-run "main = ceiling 4"))
|
||||||
|
4)
|
||||||
|
|
||||||
|
(hk-test "round 2.6 = 3" (hk-deep-force (hk-run "main = round 2.6")) 3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"truncate -3.7 = -3"
|
||||||
|
(hk-deep-force (hk-run "main = truncate (negate 3.7)"))
|
||||||
|
-3)
|
||||||
|
|
||||||
|
(hk-test "recip 4.0 = 0.25" (hk-deep-force (hk-run "main = recip 4.0")) 0.25)
|
||||||
|
|
||||||
|
(hk-test "1.0 / 4.0 = 0.25" (hk-deep-force (hk-run "main = 1.0 / 4.0")) 0.25)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"fromRational 0.5 = 0.5 (identity)"
|
||||||
|
(hk-deep-force (hk-run "main = fromRational 0.5"))
|
||||||
|
0.5)
|
||||||
|
|
||||||
|
(hk-test "pi ≈ 3.14159" (hk-deep-force (hk-run "main = pi")) 3.14159)
|
||||||
|
|
||||||
|
(hk-test "exp 0 = 1" (hk-deep-force (hk-run "main = exp 0")) 1)
|
||||||
|
|
||||||
|
(hk-test "sin 0 = 0" (hk-deep-force (hk-run "main = sin 0")) 0)
|
||||||
|
|
||||||
|
(hk-test "cos 0 = 1" (hk-deep-force (hk-run "main = cos 0")) 1)
|
||||||
|
|
||||||
|
(hk-test "2 ** 10 = 1024" (hk-deep-force (hk-run "main = 2 ** 10")) 1024)
|
||||||
|
|
||||||
|
(hk-test "log (exp 5) ≈ 5" (hk-deep-force (hk-run "main = log (exp 5)")) 5)
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
102
lib/haskell/tests/parse-extras.sx
Normal file
102
lib/haskell/tests/parse-extras.sx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
;; Phase 17 — parser polish unit tests.
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: literal int annotated"
|
||||||
|
(hk-deep-force (hk-run "main = (42 :: Int)"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: arithmetic annotated"
|
||||||
|
(hk-deep-force (hk-run "main = (1 + 2 :: Int)"))
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: function arg annotated"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "f x = x + 1\nmain = f (1 :: Int)"))
|
||||||
|
2)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: string annotated"
|
||||||
|
(hk-deep-force (hk-run "main = (\"hi\" :: String)"))
|
||||||
|
"hi")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: bool annotated"
|
||||||
|
(hk-deep-force (hk-run "main = (True :: Bool)"))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: tuple annotated"
|
||||||
|
(hk-deep-force (hk-run "main = ((1, 2) :: (Int, Int))"))
|
||||||
|
(list "Tuple" 1 2))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: nested annotation in arithmetic"
|
||||||
|
(hk-deep-force (hk-run "main = (1 :: Int) + (2 :: Int)"))
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"type-ann: function-typed annotation passes through eval"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "main = let f = ((\\x -> x + 1) :: Int -> Int) in f 5"))
|
||||||
|
6)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"no regression: plain parens still work"
|
||||||
|
(hk-deep-force (hk-run "main = (5)"))
|
||||||
|
5)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"no regression: 3-tuple still works"
|
||||||
|
(hk-deep-force (hk-run "main = (1, 2, 3)"))
|
||||||
|
(list "Tuple" 1 2 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"no regression: section-left still works"
|
||||||
|
(hk-deep-force (hk-run "main = (3 +) 4"))
|
||||||
|
7)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"no regression: section-right still works"
|
||||||
|
(hk-deep-force (hk-run "main = (+ 3) 4"))
|
||||||
|
7)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"import: still works as the very first decl"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "import qualified Data.IORef as I
|
||||||
|
main = do { r <- I.newIORef 7; I.readIORef r }"))
|
||||||
|
(list "IO" 7))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"import: between decls — after main"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "main = do { r <- I.newIORef 11; I.readIORef r }
|
||||||
|
import qualified Data.IORef as I"))
|
||||||
|
(list "IO" 11))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"import: between two decls — uses helper after import"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "f x = x + 100
|
||||||
|
import qualified Data.IORef as I
|
||||||
|
main = do { r <- I.newIORef 5; I.modifyIORef r f; I.readIORef r }"))
|
||||||
|
(list "IO" 105))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"import: two imports in different positions"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "import qualified Data.IORef as I
|
||||||
|
helper x = x * 2
|
||||||
|
import qualified Data.Map as M
|
||||||
|
main = do { r <- I.newIORef (helper 21); I.readIORef r }"))
|
||||||
|
(list "IO" 42))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"import: unqualified, mid-file"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run "go x = x
|
||||||
|
import Data.IORef
|
||||||
|
main = go 9"))
|
||||||
|
9)
|
||||||
81
lib/haskell/tests/program-accumulate.sx
Normal file
81
lib/haskell/tests/program-accumulate.sx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
;; accumulate.hs — accumulate results into an IORef [Int] (Phase 15 conformance).
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-accumulate-source
|
||||||
|
"import qualified Data.IORef as IORef\n\npush :: IORef [Int] -> Int -> IO ()\npush r x = IORef.modifyIORef r (\\xs -> x : xs)\n\npushAll :: IORef [Int] -> [Int] -> IO ()\npushAll r [] = return ()\npushAll r (x:xs) = do\n push r x\n pushAll r xs\n\nreadReversed :: IORef [Int] -> IO [Int]\nreadReversed r = do\n xs <- IORef.readIORef r\n return (reverse xs)\n\ndoubleEach :: IORef [Int] -> [Int] -> IO ()\ndoubleEach r [] = return ()\ndoubleEach r (x:xs) = do\n push r (x * 2)\n doubleEach r xs\n\nsumIntoRef :: IORef Int -> [Int] -> IO ()\nsumIntoRef r [] = return ()\nsumIntoRef r (x:xs) = do\n IORef.modifyIORef r (\\acc -> acc + x)\n sumIntoRef r xs\n\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — push three then read length"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef []; push r 1; push r 2; push r 3; xs <- IORef.readIORef r; return (length xs) }")))
|
||||||
|
(list "IO" 3))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — pushAll preserves reverse order"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef []; pushAll r [1,2,3,4]; xs <- IORef.readIORef r; return xs }")))
|
||||||
|
(list
|
||||||
|
"IO"
|
||||||
|
(list ":" 4 (list ":" 3 (list ":" 2 (list ":" 1 (list "[]")))))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — readReversed gives original order"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef []; pushAll r [10,20,30]; readReversed r }")))
|
||||||
|
(list "IO" (list ":" 10 (list ":" 20 (list ":" 30 (list "[]"))))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — doubleEach maps then accumulates"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef []; doubleEach r [1,2,3]; readReversed r }")))
|
||||||
|
(list "IO" (list ":" 2 (list ":" 4 (list ":" 6 (list "[]"))))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — sum into Int IORef"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef 0; sumIntoRef r [1,2,3,4,5]; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 15))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — empty list leaves ref untouched"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef [99]; pushAll r []; xs <- IORef.readIORef r; return xs }")))
|
||||||
|
(list "IO" (list ":" 99 (list "[]"))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — pushAll then sumIntoRef on the same input"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"main = do { r <- IORef.newIORef 0; sumIntoRef r [10,20,30,40]; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 100))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"accumulate.hs — accumulate results from a recursive helper"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-accumulate-source
|
||||||
|
"squaresUpTo r 0 = return ()\nsquaresUpTo r n = do { push r (n * n); squaresUpTo r (n - 1) }\nmain = do { r <- IORef.newIORef []; squaresUpTo r 4; readReversed r }")))
|
||||||
|
(list
|
||||||
|
"IO"
|
||||||
|
(list ":" 16 (list ":" 9 (list ":" 4 (list ":" 1 (list "[]")))))))
|
||||||
80
lib/haskell/tests/program-caesar.sx
Normal file
80
lib/haskell/tests/program-caesar.sx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
;; caesar.hs — Caesar cipher.
|
||||||
|
;; Source: https://rosettacode.org/wiki/Caesar_cipher#Haskell (adapted).
|
||||||
|
;;
|
||||||
|
;; Exercises chr, ord, isUpper, isLower, mod, string pattern matching
|
||||||
|
;; (x:xs) over a String (which is now a [Char] string view), and map
|
||||||
|
;; from the Phase 7 string=[Char] foundation.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-prog-val
|
||||||
|
(fn
|
||||||
|
(src name)
|
||||||
|
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-as-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(cond
|
||||||
|
((and (list? xs) (= (first xs) "[]")) (list))
|
||||||
|
((and (list? xs) (= (first xs) ":"))
|
||||||
|
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||||
|
(:else xs))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-caesar-source
|
||||||
|
"shift n c = if isUpper c\n then chr (mod ((ord c) - 65 + n) 26 + 65)\n else if isLower c\n then chr (mod ((ord c) - 97 + n) 26 + 97)\n else chr c\n\ncaesarRec n [] = []\ncaesarRec n (x:xs) = shift n x : caesarRec n xs\n\ncaesarMap n s = map (shift n) s\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec 3 \"ABC\" = DEF"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 3 \"ABC\"\n") "r"))
|
||||||
|
(list "D" "E" "F"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec 13 \"Hello\" = Uryyb"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 13 \"Hello\"\n") "r"))
|
||||||
|
(list "U" "r" "y" "y" "b"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec 1 \"AZ\" wraps to BA"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 1 \"AZ\"\n") "r"))
|
||||||
|
(list "B" "A"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec 0 \"World\" identity"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 0 \"World\"\n") "r"))
|
||||||
|
(list "W" "o" "r" "l" "d"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec preserves punctuation"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 3 \"Hi!\"\n") "r"))
|
||||||
|
(list "K" "l" "!"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarMap 3 \"abc\" via map"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarMap 3 \"abc\"\n") "r"))
|
||||||
|
(list "d" "e" "f"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarMap 13 round-trips with caesarMap 13"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val
|
||||||
|
(str
|
||||||
|
hk-caesar-source
|
||||||
|
"r = caesarMap 13 (foldr (\\c acc -> c : acc) [] (caesarMap 13 \"Hello\"))\n")
|
||||||
|
"r"))
|
||||||
|
(list "H" "e" "l" "l" "o"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"caesar.hs — caesarRec 25 \"AB\" = ZA"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-caesar-source "r = caesarRec 25 \"AB\"\n") "r"))
|
||||||
|
(list "Z" "A"))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
63
lib/haskell/tests/program-config.sx
Normal file
63
lib/haskell/tests/program-config.sx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
;; config.hs — multi-field config record; partial update; defaultConfig
|
||||||
|
;; constant.
|
||||||
|
;;
|
||||||
|
;; Exercises Phase 14: 4-field record, defaultConfig as a CAF, partial
|
||||||
|
;; updates that change one or two fields, accessors over derived configs.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-config-source
|
||||||
|
"data Config = Config { host :: String, port :: Int, retries :: Int, debug :: Bool } deriving (Show)\n\ndefaultConfig = Config { host = \"localhost\", port = 8080, retries = 3, debug = False }\n\ndevConfig = defaultConfig { debug = True }\nremoteConfig = defaultConfig { host = \"api.example.com\", port = 443 }\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — defaultConfig host"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = host defaultConfig")))
|
||||||
|
"localhost")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — defaultConfig port"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = port defaultConfig")))
|
||||||
|
8080)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — defaultConfig retries"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-config-source "main = retries defaultConfig")))
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — devConfig flips debug"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = debug devConfig")))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — devConfig preserves host"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = host devConfig")))
|
||||||
|
"localhost")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — devConfig preserves port"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = port devConfig")))
|
||||||
|
8080)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — remoteConfig new host"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = host remoteConfig")))
|
||||||
|
"api.example.com")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — remoteConfig new port"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = port remoteConfig")))
|
||||||
|
443)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — remoteConfig preserves retries"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-config-source "main = retries remoteConfig")))
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"config.hs — remoteConfig preserves debug"
|
||||||
|
(hk-deep-force (hk-run (str hk-config-source "main = debug remoteConfig")))
|
||||||
|
(list "False"))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
66
lib/haskell/tests/program-counter.sx
Normal file
66
lib/haskell/tests/program-counter.sx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
;; counter.hs — IORef-backed mutable counter (Phase 15 conformance).
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-counter-source
|
||||||
|
"import qualified Data.IORef as IORef\n\ncount :: IORef Int -> Int -> IO ()\ncount r 0 = return ()\ncount r n = do\n IORef.modifyIORef r (\\x -> x + 1)\n count r (n - 1)\n\ncountBy :: IORef Int -> Int -> Int -> IO ()\ncountBy r step 0 = return ()\ncountBy r step n = do\n IORef.modifyIORef r (\\x -> x + step)\n countBy r step (n - 1)\n\nnewCounter :: Int -> IO (IORef Int)\nnewCounter v = IORef.newIORef v\n\nbumpAndRead :: IORef Int -> IO Int\nbumpAndRead r = do\n IORef.modifyIORef r (\\x -> x + 1)\n IORef.readIORef r\n\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — start at 0, count 5 ⇒ 5"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"main = do { r <- newCounter 0; count r 5; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 5))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — start at 100, count 10 ⇒ 110"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"main = do { r <- newCounter 100; count r 10; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 110))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — countBy step 5, n 4 ⇒ 20"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"main = do { r <- newCounter 0; countBy r 5 4; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 20))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — bumpAndRead returns updated value"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str hk-counter-source "main = do { r <- newCounter 41; bumpAndRead r }")))
|
||||||
|
(list "IO" 42))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — count then countBy compose"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"main = do { r <- newCounter 0; count r 3; countBy r 10 2; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 23))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — two independent counters"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"main = do { a <- newCounter 0; b <- newCounter 0; count a 7; countBy b 100 2; va <- IORef.readIORef a; vb <- IORef.readIORef b; return (va + vb) }")))
|
||||||
|
(list "IO" 207))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"counter.hs — modifyIORef' (strict) variant"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-counter-source
|
||||||
|
"tick r 0 = return ()\ntick r n = do { IORef.modifyIORef' r (\\x -> x + 1); tick r (n - 1) }\nmain = do { r <- newCounter 0; tick r 50; v <- IORef.readIORef r; return v }")))
|
||||||
|
(list "IO" 50))
|
||||||
46
lib/haskell/tests/program-mapgraph.sx
Normal file
46
lib/haskell/tests/program-mapgraph.sx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
;; mapgraph.hs — adjacency-list using Data.Map (BFS-style traversal).
|
||||||
|
;;
|
||||||
|
;; Exercises Phase 11: `import qualified Data.Map as Map`, `Map.empty`,
|
||||||
|
;; `Map.insert`, `Map.lookup`, `Map.findWithDefault`. Adjacency lists are
|
||||||
|
;; stored as `Map Int [Int]`; `neighbors` does a default-empty lookup.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-mapgraph-source
|
||||||
|
"import qualified Data.Map as Map\n\nemptyG = Map.empty\n\naddEdge u v g = Map.insertWith add u [v] g\n where add new old = new ++ old\n\nbuild = addEdge 1 2 (addEdge 1 3 (addEdge 2 4 (addEdge 3 4 (addEdge 4 5 emptyG))))\n\nneighbors n g = Map.findWithDefault [] n g\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — neighbors of 1"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-mapgraph-source "main = neighbors 1 build\n")))
|
||||||
|
(list ":" 2 (list ":" 3 (list "[]"))))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — neighbors of 4"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-mapgraph-source "main = neighbors 4 build\n")))
|
||||||
|
(list ":" 5 (list "[]")))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — neighbors of 5 (leaf, no entry) defaults to []"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-mapgraph-source "main = neighbors 5 build\n")))
|
||||||
|
(list "[]"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — neighbors of 99 (absent) defaults to []"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-mapgraph-source "main = neighbors 99 build\n")))
|
||||||
|
(list "[]"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — Map.member 1"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-mapgraph-source "main = Map.member 1 build\n")))
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"mapgraph.hs — Map.size = 4 source nodes"
|
||||||
|
(hk-deep-force (hk-run (str hk-mapgraph-source "main = Map.size build\n")))
|
||||||
|
4)
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
49
lib/haskell/tests/program-newton.sx
Normal file
49
lib/haskell/tests/program-newton.sx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
;; newton.hs — Newton's method for square root.
|
||||||
|
;; Source: classic numerical analysis exercise.
|
||||||
|
;;
|
||||||
|
;; Exercises Phase 10: `Float`, `abs`, `/`, iteration via `until`.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-prog-val
|
||||||
|
(fn
|
||||||
|
(src name)
|
||||||
|
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-newton-source
|
||||||
|
"improve x guess = (guess + x / guess) / 2\n\ngoodEnough x guess = abs (guess * guess - x) < 0.0001\n\nnewtonSqrt x = newtonHelp x 1.0\n\nnewtonHelp x guess = if goodEnough x guess\n then guess\n else newtonHelp x (improve x guess)\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newton.hs — newtonSqrt 4 ≈ 2"
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-newton-source "r = abs (newtonSqrt 4.0 - 2.0) < 0.001\n")
|
||||||
|
"r")
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newton.hs — newtonSqrt 9 ≈ 3"
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-newton-source "r = abs (newtonSqrt 9.0 - 3.0) < 0.001\n")
|
||||||
|
"r")
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newton.hs — newtonSqrt 2 ≈ 1.41421"
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-newton-source "r = abs (newtonSqrt 2.0 - 1.41421) < 0.001\n")
|
||||||
|
"r")
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newton.hs — improve converges (one step)"
|
||||||
|
(hk-prog-val (str hk-newton-source "r = improve 4.0 1.0\n") "r")
|
||||||
|
2.5)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"newton.hs — newtonSqrt 100 ≈ 10"
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-newton-source "r = abs (newtonSqrt 100.0 - 10.0) < 0.001\n")
|
||||||
|
"r")
|
||||||
|
(list "True"))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
58
lib/haskell/tests/program-partial.sx
Normal file
58
lib/haskell/tests/program-partial.sx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
;; partial.hs — exercises Phase 9 partial functions caught at the top level.
|
||||||
|
;;
|
||||||
|
;; Each program calls a partial function on bad input; hk-run-io catches the
|
||||||
|
;; raise and appends the error message to io-lines so tests can inspect.
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — main = print (head [])"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = print (head [])")))
|
||||||
|
(>= (index-of (str lines) "Prelude.head: empty list") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — main = print (tail [])"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = print (tail [])")))
|
||||||
|
(>= (index-of (str lines) "Prelude.tail: empty list") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — main = print (fromJust Nothing)"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = print (fromJust Nothing)")))
|
||||||
|
(>= (index-of (str lines) "Maybe.fromJust: Nothing") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — putStrLn before error preserves prior output"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = do { putStrLn \"step 1\"; putStrLn (show (head [])); putStrLn \"never\" }")))
|
||||||
|
(and
|
||||||
|
(>= (index-of (str lines) "step 1") 0)
|
||||||
|
(>= (index-of (str lines) "Prelude.head: empty list") 0)
|
||||||
|
(= (index-of (str lines) "never") -1)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — undefined as IO action"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = print undefined")))
|
||||||
|
(>= (index-of (str lines) "Prelude.undefined") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — catches error from a user-thrown error"
|
||||||
|
(let
|
||||||
|
((lines (hk-run-io "main = error \"boom from main\"")))
|
||||||
|
(>= (index-of (str lines) "boom from main") 0))
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; Negative case: when no error is raised, io-lines doesn't contain
|
||||||
|
;; "Prelude" prefixes from our error path.
|
||||||
|
(hk-test
|
||||||
|
"partial.hs — happy path: head [42] succeeds, no error in output"
|
||||||
|
(hk-run-io "main = print (head [42])")
|
||||||
|
(list "42"))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
51
lib/haskell/tests/program-person.sx
Normal file
51
lib/haskell/tests/program-person.sx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
;; person.hs — record type with accessors, update, deriving Show.
|
||||||
|
;;
|
||||||
|
;; Exercises Phase 14: data with record syntax, accessor functions,
|
||||||
|
;; record creation, record update, deriving Show on a record.
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-person-source
|
||||||
|
"data Person = Person { name :: String, age :: Int } deriving (Show)\n\nalice = Person { name = \"alice\", age = 30 }\nbob = Person { name = \"bob\", age = 25 }\n\nbirthday p = p { age = age p + 1 }\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — alice's name"
|
||||||
|
(hk-deep-force (hk-run (str hk-person-source "main = name alice")))
|
||||||
|
"alice")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — alice's age"
|
||||||
|
(hk-deep-force (hk-run (str hk-person-source "main = age alice")))
|
||||||
|
30)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — birthday adds one year"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-person-source "main = age (birthday alice)")))
|
||||||
|
31)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — birthday preserves name"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run (str hk-person-source "main = name (birthday alice)")))
|
||||||
|
"alice")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — show alice"
|
||||||
|
(hk-deep-force (hk-run (str hk-person-source "main = show alice")))
|
||||||
|
"Person \"alice\" 30")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — bob has different name"
|
||||||
|
(hk-deep-force (hk-run (str hk-person-source "main = name bob")))
|
||||||
|
"bob")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"person.hs — pattern match in function"
|
||||||
|
(hk-deep-force
|
||||||
|
(hk-run
|
||||||
|
(str
|
||||||
|
hk-person-source
|
||||||
|
"greet (Person { name = n }) = \"Hi, \" ++ n\nmain = greet alice")))
|
||||||
|
"Hi, alice")
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
83
lib/haskell/tests/program-runlength-str.sx
Normal file
83
lib/haskell/tests/program-runlength-str.sx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
;; runlength-str.hs — run-length encoding on a String.
|
||||||
|
;; Source: https://rosettacode.org/wiki/Run-length_encoding#Haskell (adapted).
|
||||||
|
;;
|
||||||
|
;; Exercises String pattern matching `(x:xs)`, `span` over a string view,
|
||||||
|
;; tuple construction `(Int, Char)`, character equality, and tuple-in-cons
|
||||||
|
;; patterns `((n, c) : rest)` — all enabled by Phase 7 string=[Char].
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-prog-val
|
||||||
|
(fn
|
||||||
|
(src name)
|
||||||
|
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-as-list
|
||||||
|
(fn
|
||||||
|
(xs)
|
||||||
|
(cond
|
||||||
|
((and (list? xs) (= (first xs) "[]")) (list))
|
||||||
|
((and (list? xs) (= (first xs) ":"))
|
||||||
|
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||||
|
(:else xs))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hk-rle-source
|
||||||
|
"encodeRL [] = []\nencodeRL (x:xs) = let (same, rest) = span eqX xs\n eqX y = y == x\n in (1 + length same, x) : encodeRL rest\n\nreplicateRL 0 _ = []\nreplicateRL n c = c : replicateRL (n - 1) c\n\ndecodeRL [] = []\ndecodeRL ((n, c) : rest) = replicateRL n c ++ decodeRL rest\n")
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — encodeRL [] = []"
|
||||||
|
(hk-as-list (hk-prog-val (str hk-rle-source "r = encodeRL \"\"\n") "r"))
|
||||||
|
(list))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — length (encodeRL \"aabbbcc\") = 3"
|
||||||
|
(hk-prog-val (str hk-rle-source "r = length (encodeRL \"aabbbcc\")\n") "r")
|
||||||
|
3)
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — map fst (encodeRL \"aabbbcc\") = [2,3,2]"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-rle-source "r = map fst (encodeRL \"aabbbcc\")\n") "r"))
|
||||||
|
(list 2 3 2))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — map snd (encodeRL \"aabbbcc\") = [97,98,99]"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-rle-source "r = map snd (encodeRL \"aabbbcc\")\n") "r"))
|
||||||
|
(list 97 98 99))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — counts of encodeRL \"aabbbccddddee\" = [2,3,2,4,2]"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-rle-source "r = map fst (encodeRL \"aabbbccddddee\")\n")
|
||||||
|
"r"))
|
||||||
|
(list 2 3 2 4 2))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — chars of encodeRL \"aabbbccddddee\" = [97,98,99,100,101]"
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val
|
||||||
|
(str hk-rle-source "r = map snd (encodeRL \"aabbbccddddee\")\n")
|
||||||
|
"r"))
|
||||||
|
(list 97 98 99 100 101))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — singleton encodeRL \"x\""
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-rle-source "r = map fst (encodeRL \"x\")\n") "r"))
|
||||||
|
(list 1))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — decodeRL round-trip preserves \"aabbbcc\""
|
||||||
|
(hk-as-list
|
||||||
|
(hk-prog-val (str hk-rle-source "r = decodeRL (encodeRL \"aabbbcc\")\n") "r"))
|
||||||
|
(list 97 97 98 98 98 99 99))
|
||||||
|
|
||||||
|
(hk-test
|
||||||
|
"rle.hs — replicateRL 4 65 = [65,65,65,65]"
|
||||||
|
(hk-as-list (hk-prog-val (str hk-rle-source "r = replicateRL 4 65\n") "r"))
|
||||||
|
(list 65 65 65 65))
|
||||||
|
|
||||||
|
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user