Compare commits
466 Commits
74ce9e7c75
...
loops/data
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df5e92c46 | |||
| fadcdbd6a9 | |||
| ce98d97728 | |||
| 82dfa20e82 | |||
| 66aa003461 | |||
| 6bae94bae1 | |||
| 7a94a47e26 | |||
| 917ffe5ccc | |||
| ba60db2eef | |||
| 00881f84eb | |||
| 9e380fd96e | |||
| c6f646607e | |||
| 285cd530eb | |||
| dcae125955 | |||
| 9a16f27075 | |||
| a9e4eea334 | |||
| 3a1ecaa362 | |||
| 69a53ece43 | |||
| 96c9e90743 | |||
| 5bcda5c88c | |||
| 4b5e75dc3e | |||
| 2a1d8eeab2 | |||
| 2c8c1f75b3 | |||
| d437727f1d | |||
| a4ef271459 | |||
| 62a5a29d5b | |||
| 17d6f58cc5 | |||
| e981368dcf | |||
| 4a7cff2f6b | |||
| 21c541bd1b | |||
| 0985dc6386 | |||
| f12edc8fd9 | |||
| 9edccb8f33 | |||
| 8e508bc90f | |||
| 5f4defe99e | |||
| d20df7aa8c | |||
| 96f66d3596 | |||
| 254052a43b | |||
| ec7e4dd5c4 | |||
| 370df5b8e5 | |||
| a648247ae4 | |||
| 5a3db1a458 | |||
| 549cb5ea84 | |||
| 30880927f2 | |||
| e0c7de1a1c | |||
| de734b27b8 | |||
| 7a64be22d8 | |||
| 9695d31dab | |||
| fc6979a371 | |||
| 43fa31375d | |||
| 4a643a5c52 | |||
| ce8fed6b22 | |||
| 5100c5d5a6 | |||
| 9c5a697e45 | |||
| 282a3d3d06 | |||
| 57a1dbb232 | |||
| a53e47b415 | |||
| a080ce656c | |||
| 2a01d8ac91 | |||
| 71b73bd87e | |||
| e2c149e60a | |||
| d66ddc614b | |||
| f33a8d69f5 | |||
| 148c3f2068 | |||
| 18fb54a8c5 | |||
| cf634ad2b1 | |||
| 380580af17 | |||
| cc64ec5cf2 | |||
| c7315f5877 | |||
| 9054fe983d | |||
| 408fc27366 | |||
| b95d8c5a63 | |||
| a63d67247a | |||
| d09ed83fa1 | |||
| 55286cc5bc | |||
| 5a1dc4392f | |||
| 790c17dfc1 | |||
| de302fc236 | |||
| 3cc760082c | |||
| ce603e9879 | |||
| 6d04cf7bf2 | |||
| caec05eb27 | |||
| d964f58c48 | |||
| 7ce723f732 | |||
| 6457eb668c | |||
| 9bc70fd2a9 | |||
| 8046df7ce5 | |||
| 5c1807c832 | |||
| 9bd6bbb7e7 | |||
| 1eb9d0f8d2 | |||
| f182d04e6a | |||
| ab2c40c14c | |||
| d3c34b46b9 | |||
| 80dac0051d | |||
| b661318a45 | |||
| 47d9d07f2e | |||
| d75c61d408 | |||
| 3dae27737c | |||
| f962560652 | |||
| 863e9d93a4 | |||
| a677585639 | |||
| 2defa5e739 | |||
| 64157e9e81 | |||
| e0d447e2ce | |||
| 63ad4563cb | |||
| c04f38a1ba | |||
| 6915730029 | |||
| a774cd26c1 | |||
| b13819c50c | |||
| d9cf00f287 | |||
| 69a0886214 | |||
| 0c0ed0605a | |||
| 5f27125f01 | |||
| da27958d67 | |||
| d27622d45e | |||
| b6cf20dac7 | |||
| c8b232d40e | |||
| 251e6e1bab | |||
| 0dd2fa3058 | |||
| 67ff2a3ae8 | |||
| aaabe370d6 | |||
| 637ba4102f | |||
| 7cf8b74d1d | |||
| d473f39b04 | |||
| d5e66474fe | |||
| 64d36fa66e | |||
| dec1cf3fbe | |||
| 52df09655d | |||
| 5a28cf5dd3 | |||
| f480eb943c | |||
| edc7e865b4 | |||
| fc13acb805 | |||
| ca151d7ed5 | |||
| 322eb1d034 | |||
| be820d0337 | |||
| d755caeb9a | |||
| 3e77dd4ded | |||
| 0f13052900 | |||
| e37167a58e | |||
| 49eb22243a | |||
| 20a61de693 | |||
| ed0853f4a0 | |||
| ec26b61cbe | |||
| bee4e0846c | |||
| f591ee17c3 | |||
| a5044cfc08 | |||
| 6c171d4906 | |||
| 4cb5302232 | |||
| 0e022ab670 | |||
| 1900726fc9 | |||
| c48911e591 | |||
| a66c0f66f0 | |||
| 16167c5d9b | |||
| 1fbfdfe4ae | |||
| 6328b810bd | |||
| 84d210b6b3 | |||
| c08e217e2a | |||
| d22361e471 | |||
| 00121e137e | |||
| 986d6411d0 | |||
| 3628a504db | |||
| 4c71c5a75e | |||
| 621e99e456 | |||
| 6d39111992 | |||
| 9eecbde61e | |||
| 4dbd3a0b34 | |||
| 7b050fb217 | |||
| 0679edf568 | |||
| fa2cdee164 | |||
| 5dd85b86ef | |||
| 559b0df900 | |||
| ba9ab4e65a | |||
| fc8a391656 | |||
| 3d2bdc52b5 | |||
| d441807c8e | |||
| e1cf75103b | |||
| 2ef773a3c9 | |||
| 30722dfe1c | |||
| 1f49242ae3 | |||
| b19f2017d0 | |||
| 57cfee8267 | |||
| 58dcff2639 | |||
| d570da1dea | |||
| 0eced4c34c | |||
| b7ad5152d8 | |||
| 1824058aa3 | |||
| 023bc2d80c | |||
| d67e04a9ad | |||
| ccf8a0fb90 | |||
| c265c6e376 | |||
| 2f7f8189ea | |||
| d25cb1223e | |||
| e80e655b51 | |||
| 4332b4032f | |||
| e85a828de8 | |||
| 882a4b76cb | |||
| 3489c9f131 | |||
| d39ef786ba | |||
| a32561a07d | |||
| c56f400403 | |||
| c63c0d26e8 | |||
| c5ceb9c718 | |||
| e42aec8957 | |||
| ce72070d2a | |||
| 32efdfe4aa | |||
| e06e3ad014 | |||
| 40f0e73386 | |||
| 83dbb5958a | |||
| ad914b413c | |||
| 7dfa092ed2 | |||
| 16cf4d9316 | |||
| 03e9df3ecf | |||
| e11fbd6140 | |||
| eaab8db840 | |||
| 248dca5b32 | |||
| c5d9a8b789 | |||
| 71ad7d2d24 | |||
| c03ba9eccb | |||
| 3c83985841 | |||
| 6a6a94e203 | |||
| be26f77410 | |||
| 8a009df4a3 | |||
| 2249863d2d | |||
| 2314735431 | |||
| d21cde336a | |||
| 859361d86a | |||
| f0f339709e | |||
| 09d65d2d7b | |||
| 0596376199 | |||
| 35511db15b | |||
| f86d07401d | |||
| 6bfb7b19f4 | |||
| 74e020359f | |||
| 40ce4df6b1 | |||
| db52a6d77c | |||
| 0cc36450c4 | |||
| 679b45e3fc | |||
| 21e8e51174 | |||
| b0c135412a | |||
| f1428009fd | |||
| 9f57234d1e | |||
| 1751cd05ea | |||
| 041cb9f3ef | |||
| 096faf2c40 | |||
| 578e54f06d | |||
| 82d16597e0 | |||
| ed42561071 | |||
| 6d8f366439 | |||
| 225fa2e86d | |||
| 1c45262577 | |||
| cfe5371354 | |||
| 48eaeb0421 | |||
| c93fe4453a | |||
| 623529d3be | |||
| bf190b8fc4 | |||
| bc45b7abf5 | |||
| 2c61be39de | |||
| 6c1a953c80 | |||
| d3e71ba356 | |||
| ea064346e1 | |||
| 23c44cf6cf | |||
| 5e0fcb9316 | |||
| d295ab8463 | |||
| afddc92c70 | |||
| 95f96efb78 | |||
| 95b22a648d | |||
| cffd3bec83 | |||
| eb5babaf99 | |||
| a49b1a9f79 | |||
| 263d9aae68 | |||
| fb51620a4c | |||
| 60a8eb24e0 | |||
| 0dbf9b9f73 | |||
| 7b11f3d44a | |||
| a26be0bfd0 | |||
| 9ed3e4faaf | |||
| ac013c9381 | |||
| 72ccaf4565 | |||
| d8cf74fd28 | |||
| 0f63216adc | |||
| ecd89270c0 | |||
| 092da5b819 | |||
| 40bf4c38f1 | |||
| b46bef2808 | |||
| 41a69ecca7 | |||
| 5c00b5c58b | |||
| 622c0851ce | |||
| d8f3f8c3b2 | |||
| 17b5acb71f | |||
| 0753982a02 | |||
| 2606b83920 | |||
| 2f8abb18a3 | |||
| 68124adc3b | |||
| 2de96e7f4f | |||
| 8f3b0d9301 | |||
| f6a1b53c7b | |||
| 5a402a02be | |||
| 42c7a593cf | |||
| 37f8ed74c7 | |||
| 7acbea01ae | |||
| bf9d342c6e | |||
| 7f642a5082 | |||
| 85cef7d80f | |||
| e667d3bc51 | |||
| c26cd500b4 | |||
| 0bef67dd47 | |||
| 8f8f9623e0 | |||
| 297f0603e5 | |||
| 35ace3e74c | |||
| ac4e9ac96e | |||
| 6a40e991b3 | |||
| e9ddf31181 | |||
| 26ee00dff1 | |||
| f547ebf43e | |||
| b14ac6cd70 | |||
| 6d534e8c42 | |||
| 7190a8b1d2 | |||
| 79190e4dac | |||
| 7b72c064c4 | |||
| e7169af985 | |||
| abbb1fe5c6 | |||
| 846650da07 | |||
| 0276571f08 | |||
| fee62a20f0 | |||
| 42184797f1 | |||
| d5aa8a2e74 | |||
| 20e23d233c | |||
| d9b7e1e392 | |||
| d47db58cde | |||
| f4ef4033de | |||
| 73e86fa8e8 | |||
| 51bc075da5 | |||
| 894fd24c3a | |||
| a3abe47286 | |||
| d25a97d464 | |||
| df6480cd96 | |||
| 7990ee5ffe | |||
| 19bd2cb92d | |||
| 1723808517 | |||
| 9256719fa8 | |||
| 0746c90729 | |||
| 83cb75a87b | |||
| eeb4e48230 | |||
| eef2bfdd89 | |||
| c4d9efc8c4 | |||
| 4baf16ac13 | |||
| b40c70a348 | |||
| 310b649fe7 | |||
| 5ddd558eb7 | |||
| 68d81f59a6 | |||
| 245b097c93 | |||
| 2dadb6a521 | |||
| cc800c3004 | |||
| 606b5da1a1 | |||
| 87072e61c1 | |||
| 8b972483ae | |||
| 21c4a7fd5e | |||
| cb59fbba13 | |||
| 54b54f4e19 | |||
| 92adf9d496 | |||
| cabb0467ab | |||
| 820132b839 | |||
| 7480c0f9c9 | |||
| c36fd5b208 | |||
| 41fac7ac29 | |||
| 4c48a8dd57 | |||
| a48110417b | |||
| 61c9697f67 | |||
| f2993f0582 | |||
| da2e6b1bca | |||
| 8e8c2a73d6 | |||
| f38558fcc1 | |||
| daea280837 | |||
| 11917f1bfa | |||
| 875e9ba317 | |||
| f715d23e10 | |||
| 5a76a04010 | |||
| c8d7fdd59a | |||
| a14fe05632 | |||
| 4f4b735958 | |||
| 4b69650336 | |||
| a0bbf74c01 | |||
| 35f498ec80 | |||
| da8ba104a6 | |||
| 82da16e4bb | |||
| 037acc7998 | |||
| 247bd85cda | |||
| b41d9d143b | |||
| d663c91f4b | |||
| 11ee71d846 | |||
| 835fffb834 | |||
| bb18c05083 | |||
| 6a1cbdcbdb | |||
| 4c43918a99 | |||
| d7244d1dc8 | |||
| 1b1b67c72e | |||
| 3a755947ef | |||
| 880503e2b6 | |||
| e989ff3865 | |||
| 973085e15f | |||
| 9f71706bc8 | |||
| 8e2a633b7f | |||
| cc2a296306 | |||
| 9c8da50003 | |||
| 3003c8a069 | |||
| 8c62137d32 | |||
| 161fa613f2 | |||
| ba63cdf8c4 | |||
| 573f9fa4b3 | |||
| 8ac669c739 | |||
| 8e4bdb7216 | |||
| 20a643806b | |||
| ea1bdab82c | |||
| 04164aa2d4 | |||
| 35aa998fcc | |||
| 6ee052593c | |||
| 2b117288f6 | |||
| 8a9168c8d5 | |||
| 912649c426 | |||
| 67a5f13713 | |||
| dbba2fe418 | |||
| c73b696494 | |||
| 1a17d8d232 | |||
| 666e29d5f0 | |||
| 9facbb4836 | |||
| a12dcef327 | |||
| d33c520318 | |||
| 9be65d7d60 | |||
| db8d7aca91 | |||
| d31565d556 | |||
| 337c8265cd | |||
| a4538c71a8 | |||
| 5ff2b7068e | |||
| f011d01b49 | |||
| 122053eda3 | |||
| 7bbffa0401 | |||
| 3044a16817 | |||
| a8a798c592 | |||
| 19c97989d7 | |||
| ff38499bd5 | |||
| e01a3baa5b | |||
| 484b55281b | |||
| 070a983848 | |||
| 13e0254261 | |||
| 1340284bc8 | |||
| 4f98f5f89d | |||
| 4ed7ffe9dd | |||
| 84e7bc8a24 | |||
| cd489b19be | |||
| 7735eb7512 | |||
| 04a25d17d0 | |||
| 4e2e2c781c | |||
| cc5315a5e6 | |||
| 0e53e88b02 | |||
| fba92c2b69 | |||
| 1aa06237f1 | |||
| e9c8f803b5 | |||
| ef81fffb6f | |||
| cab7ca883f | |||
| bf0d72fd2f | |||
| defbe0a612 | |||
| 869b0b552d | |||
| 58dbbc5d8b | |||
| 36234f0132 | |||
| 6ccef45ce4 | |||
| c07ff90f6b |
@@ -1129,6 +1129,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
|
||||
PRIMITIVES["symbol?"] = function(x) { return x != null && x._sym === true; };
|
||||
PRIMITIVES["keyword?"] = function(x) { return x != null && x._kw === true; };
|
||||
PRIMITIVES["adt?"] = function(x) { return x !== null && typeof x === "object" && x._adtv === true; };
|
||||
PRIMITIVES["component-affinity"] = componentAffinity;
|
||||
''',
|
||||
|
||||
@@ -1475,6 +1476,22 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
};
|
||||
PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); };
|
||||
PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; };
|
||||
|
||||
// Short aliases — terser names; append accepts any value
|
||||
PRIMITIVES["make-buffer"] = function() { return new SxStringBuffer(); };
|
||||
PRIMITIVES["buffer?"] = function(x) { return x instanceof SxStringBuffer; };
|
||||
PRIMITIVES["buffer-append!"] = function(buf, v) {
|
||||
var s;
|
||||
if (v === null || v === undefined || v === NIL) s = "";
|
||||
else if (typeof v === "string") s = v;
|
||||
else if (typeof v === "boolean") s = v ? "true" : "false";
|
||||
else if (typeof v === "number") s = String(v);
|
||||
else if (v && typeof v === "object" && typeof v.name === "string" && v.constructor && v.constructor.name === "Symbol") s = v.name;
|
||||
else s = (typeof inspect === "function") ? inspect(v) : String(v);
|
||||
buf.parts.push(s); buf.len += s.length; return NIL;
|
||||
};
|
||||
PRIMITIVES["buffer->string"] = function(buf) { return buf.parts.join(""); };
|
||||
PRIMITIVES["buffer-length"] = function(buf) { return buf.len; };
|
||||
''',
|
||||
|
||||
"stdlib.format": '''
|
||||
@@ -1933,12 +1950,30 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._regexp) return "regexp";
|
||||
if (x._bytevector) return "bytevector";
|
||||
if (x._rational) return "rational";
|
||||
if (x._adtv) return x._type;
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// AdtValue — native algebraic data type instance (Step 6 mirror of OCaml Step 5).
|
||||
// Constructed by define-type. Carries _adt:true plus _adtv:true tag so type-of
|
||||
// returns the type name rather than "dict". dict? remains true (shim approach)
|
||||
// so spec-level match-pattern in evaluator.sx works without changes.
|
||||
function makeAdtValue(typeName, ctorName, fields) {
|
||||
return {
|
||||
_adtv: true,
|
||||
_adt: true,
|
||||
_type: typeName,
|
||||
_ctor: ctorName,
|
||||
_fields: fields
|
||||
};
|
||||
}
|
||||
function isAdtValue(x) {
|
||||
return x !== null && typeof x === "object" && x._adtv === true;
|
||||
}
|
||||
|
||||
function symbolName(s) { return s.name; }
|
||||
function keywordName(k) { return k.name; }
|
||||
function makeSymbol(n) { return new Symbol(n); }
|
||||
@@ -2105,6 +2140,13 @@ PLATFORM_JS_PRE = '''
|
||||
// hostError — throw a host-level error that propagates out of cekRun.
|
||||
function hostError(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); }
|
||||
|
||||
// hostWarn — emit a host-level warning to console (no-op if console missing).
|
||||
function hostWarn(msg) {
|
||||
var m = typeof msg === "string" ? msg : inspect(msg);
|
||||
if (typeof console !== "undefined" && console.warn) console.warn(m);
|
||||
return NIL;
|
||||
}
|
||||
|
||||
// Render dispatch — call the active adapter's render function.
|
||||
// Set by each adapter when loaded; defaults to identity (no rendering).
|
||||
var _renderExprFn = null;
|
||||
@@ -2126,7 +2168,16 @@ PLATFORM_JS_PRE = '''
|
||||
}
|
||||
|
||||
function error(msg) { throw new Error(msg); }
|
||||
function inspect(x) { return JSON.stringify(x); }
|
||||
function inspect(x) {
|
||||
if (x !== null && typeof x === "object" && x._adtv === true) {
|
||||
var fs = x._fields || [];
|
||||
if (fs.length === 0) return "(" + x._ctor + ")";
|
||||
var parts = [];
|
||||
for (var i = 0; i < fs.length; i++) parts.push(inspect(fs[i]));
|
||||
return "(" + x._ctor + " " + parts.join(" ") + ")";
|
||||
}
|
||||
return JSON.stringify(x);
|
||||
}
|
||||
function debugLog() { console.error.apply(console, ["[sx-debug]"].concat(Array.prototype.slice.call(arguments))); }
|
||||
|
||||
'''
|
||||
@@ -2450,6 +2501,7 @@ CEK_FIXUPS_JS = '''
|
||||
// Platform functions — defined in platform_js.py, not in .sx spec files.
|
||||
// Spec defines self-register via js-emit-define; these are the platform interface.
|
||||
PRIMITIVES["type-of"] = typeOf;
|
||||
PRIMITIVES["inspect"] = inspect;
|
||||
PRIMITIVES["symbol-name"] = symbolName;
|
||||
PRIMITIVES["keyword-name"] = keywordName;
|
||||
PRIMITIVES["callable?"] = isCallable;
|
||||
@@ -2771,8 +2823,8 @@ PLATFORM_DOM_JS = """
|
||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||
var wrapped = isLambda(handler)
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
? function(e) { try { var r = cekCall(handler, NIL); if (globalThis._driveAsync) globalThis._driveAsync(r); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { var r = cekCall(handler, [e]); if (globalThis._driveAsync) globalThis._driveAsync(r); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
: handler;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
var passiveEvents = { touchstart: 1, touchmove: 1, wheel: 1, scroll: 1 };
|
||||
@@ -3981,6 +4033,11 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
// -----------------------------------------------------------------------
|
||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||
PRIMITIVES["host-error"] = function(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); };
|
||||
PRIMITIVES["host-warn"] = function(msg) {
|
||||
var m = typeof msg === "string" ? msg : inspect(msg);
|
||||
if (typeof console !== "undefined" && console.warn) console.warn(m);
|
||||
return NIL;
|
||||
};
|
||||
PRIMITIVES["try-catch"] = function(tryFn, catchFn) {
|
||||
try {
|
||||
return cekRun(continueWithCall(tryFn, [], makeEnv(), [], []));
|
||||
@@ -4103,7 +4160,56 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
function clearStores() { _storeRegistry = {}; return NIL; }
|
||||
PRIMITIVES["def-store"] = defStore;
|
||||
PRIMITIVES["use-store"] = useStore;
|
||||
PRIMITIVES["clear-stores"] = clearStores;''']
|
||||
PRIMITIVES["clear-stores"] = clearStores;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// define-type override — produces native AdtValue instances (Step 6).
|
||||
// The transpiled sfDefineType from evaluator.sx creates plain dict
|
||||
// instances. We override here to construct AdtValue via makeAdtValue so
|
||||
// type-of returns the type name and adt? can distinguish from dicts.
|
||||
// dict? still returns true for AdtValue (shim) so spec-level match-pattern
|
||||
// continues to work without changes.
|
||||
// -----------------------------------------------------------------------
|
||||
var _sfDefineTypeAdt = function(args, env) {
|
||||
var typeSym = first(args);
|
||||
var ctorSpecs = rest(args);
|
||||
var typeName = symbolName(typeSym);
|
||||
var ctorNames = map(function(spec) { return symbolName(first(spec)); }, ctorSpecs);
|
||||
if (!isSxTruthy(envHas(env, "*adt-registry*"))) {
|
||||
envBind(env, "*adt-registry*", {});
|
||||
}
|
||||
envGet(env, "*adt-registry*")[typeName] = ctorNames;
|
||||
envBind(env, typeName + "?", function(v) { return isAdtValue(v) && v._type === typeName; });
|
||||
for (var _i = 0; _i < ctorSpecs.length; _i++) {
|
||||
(function(spec) {
|
||||
var cn = symbolName(first(spec));
|
||||
var fieldNames = map(function(f) { return symbolName(f); }, rest(spec));
|
||||
var arity = fieldNames.length;
|
||||
envBind(env, cn, function() {
|
||||
var ctorArgs = Array.prototype.slice.call(arguments, 0);
|
||||
if (ctorArgs.length !== arity) {
|
||||
throw new Error(cn + ": expected " + arity + " args, got " + ctorArgs.length);
|
||||
}
|
||||
return makeAdtValue(typeName, cn, ctorArgs);
|
||||
});
|
||||
envBind(env, cn + "?", function(v) { return isAdtValue(v) && v._ctor === cn; });
|
||||
for (var _j = 0; _j < fieldNames.length; _j++) {
|
||||
(function(idx, fieldName) {
|
||||
envBind(env, cn + "-" + fieldName, function(v) {
|
||||
if (!isAdtValue(v)) throw new Error(cn + "-" + fieldName + ": not an ADT");
|
||||
if (idx >= v._fields.length) throw new Error(cn + "-" + fieldName + ": index out of bounds");
|
||||
return v._fields[idx];
|
||||
});
|
||||
})(_j, fieldNames[_j]);
|
||||
}
|
||||
})(ctorSpecs[_i]);
|
||||
}
|
||||
return NIL;
|
||||
};
|
||||
PRIMITIVES["sf-define-type"] = _sfDefineTypeAdt;
|
||||
registerSpecialForm("define-type", _sfDefineTypeAdt);
|
||||
PRIMITIVES["make-adt-value"] = makeAdtValue;
|
||||
PRIMITIVES["adt-value?"] = isAdtValue;''']
|
||||
if has_deps:
|
||||
lines.append('''
|
||||
// Platform deps functions (native JS, not transpiled — need explicit registration)
|
||||
|
||||
73
hosts/ocaml/bin/bench_cek.ml
Normal file
73
hosts/ocaml/bin/bench_cek.ml
Normal file
@@ -0,0 +1,73 @@
|
||||
(** CEK benchmark — measures throughput of the CEK evaluator on tight loops.
|
||||
|
||||
Usage:
|
||||
dune exec bin/bench_cek.exe
|
||||
dune exec bin/bench_cek.exe -- 5 (5 runs each)
|
||||
*)
|
||||
|
||||
open Sx_types
|
||||
open Sx_parser
|
||||
|
||||
let parse_one s =
|
||||
let exprs = parse_all s in
|
||||
match exprs with
|
||||
| e :: _ -> e
|
||||
| [] -> failwith "empty parse"
|
||||
|
||||
let parse_many s = parse_all s
|
||||
|
||||
let bench_run name setup expr iters =
|
||||
let env = Sx_types.make_env () in
|
||||
(* Run setup forms in env *)
|
||||
List.iter (fun e -> ignore (Sx_ref.eval_expr e (Env env))) setup;
|
||||
let times = ref [] in
|
||||
for _ = 1 to iters do
|
||||
Gc.full_major ();
|
||||
let t0 = Unix.gettimeofday () in
|
||||
let _r = Sx_ref.eval_expr expr (Env env) in
|
||||
let t1 = Unix.gettimeofday () in
|
||||
times := (t1 -. t0) :: !times
|
||||
done;
|
||||
let sorted = List.sort compare !times in
|
||||
let median = List.nth sorted (iters / 2) in
|
||||
let min_t = List.nth sorted 0 in
|
||||
let max_t = List.nth sorted (iters - 1) in
|
||||
Printf.printf " %-22s min=%8.2fms median=%8.2fms max=%8.2fms\n%!"
|
||||
name (min_t *. 1000.0) (median *. 1000.0) (max_t *. 1000.0);
|
||||
median
|
||||
|
||||
let () =
|
||||
let iters =
|
||||
if Array.length Sys.argv > 1
|
||||
then int_of_string Sys.argv.(1)
|
||||
else 5
|
||||
in
|
||||
Printf.printf "CEK benchmark (%d runs each, taking median)\n%!" iters;
|
||||
Printf.printf "==========================================\n%!";
|
||||
|
||||
(* fib 18 — recursive function call benchmark, smallish *)
|
||||
let fib_setup = parse_many "(define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))" in
|
||||
let fib_expr = parse_one "(fib 18)" in
|
||||
let _ = bench_run "fib(18)" fib_setup fib_expr iters in
|
||||
|
||||
(* loop 5000 — tight let loop *)
|
||||
let loop_setup = parse_many "(define (loop n acc) (if (= n 0) acc (loop (- n 1) (+ acc 1))))" in
|
||||
let loop_expr = parse_one "(loop 5000 0)" in
|
||||
let _ = bench_run "loop(5000)" loop_setup loop_expr iters in
|
||||
|
||||
(* map+square over 1000 elem list *)
|
||||
let map_setup = parse_many "(define (range-list n) (let loop ((i 0) (acc (list))) (if (= i n) acc (loop (+ i 1) (cons i acc))))) (define xs (range-list 1000))" in
|
||||
let map_expr = parse_one "(map (fn (x) (* x x)) xs)" in
|
||||
let _ = bench_run "map sq xs(1000)" map_setup map_expr iters in
|
||||
|
||||
(* reduce + over 2000 elem list *)
|
||||
let red_setup = parse_many "(define (range-list n) (let loop ((i 0) (acc (list))) (if (= i n) acc (loop (+ i 1) (cons i acc))))) (define ys (range-list 2000))" in
|
||||
let red_expr = parse_one "(reduce + 0 ys)" in
|
||||
let _ = bench_run "reduce + ys(2000)" red_setup red_expr iters in
|
||||
|
||||
(* let-heavy: many bindings + if *)
|
||||
let lh_setup = parse_many "(define (lh n) (let ((a 1) (b 2) (c 3) (d 4)) (if (= n 0) (+ a b c d) (lh (- n 1)))))" in
|
||||
let lh_expr = parse_one "(lh 2000)" in
|
||||
let _ = bench_run "let-heavy(2000)" lh_setup lh_expr iters in
|
||||
|
||||
Printf.printf "\nDone.\n%!"
|
||||
46
hosts/ocaml/bin/bench_inspect.ml
Normal file
46
hosts/ocaml/bin/bench_inspect.ml
Normal file
@@ -0,0 +1,46 @@
|
||||
(* Benchmark inspect on representative SX values.
|
||||
Takes min of 9 runs of n iterations to dampen GC noise. *)
|
||||
open Sx_types
|
||||
|
||||
let rec make_tree d =
|
||||
if d = 0 then String "leaf"
|
||||
else List [String "node"; make_tree (d - 1); make_tree (d - 1); make_tree (d - 1)]
|
||||
|
||||
let bench_min label f n runs =
|
||||
let times = ref [] in
|
||||
for _ = 1 to runs do
|
||||
Gc.compact ();
|
||||
let t0 = Unix.gettimeofday () in
|
||||
for _ = 1 to n do ignore (f ()) done;
|
||||
let t1 = Unix.gettimeofday () in
|
||||
times := (t1 -. t0) :: !times
|
||||
done;
|
||||
let sorted = List.sort compare !times in
|
||||
let min_t = List.nth sorted 0 in
|
||||
let median = List.nth sorted (runs / 2) in
|
||||
Printf.printf " %-30s min=%6.2fms median=%6.2fms (n=%d * %d runs)\n%!"
|
||||
label (min_t *. 1000.0 /. float_of_int n)
|
||||
(median *. 1000.0 /. float_of_int n) n runs
|
||||
|
||||
let () =
|
||||
let tree8 = make_tree 8 in
|
||||
let s = inspect tree8 in
|
||||
Printf.printf "tree-d8 inspect len=%d\n%!" (String.length s);
|
||||
bench_min "inspect tree-d8" (fun () -> inspect tree8) 50 9;
|
||||
|
||||
let tree10 = make_tree 10 in
|
||||
let s = inspect tree10 in
|
||||
Printf.printf "tree-d10 inspect len=%d\n%!" (String.length s);
|
||||
bench_min "inspect tree-d10" (fun () -> inspect tree10) 5 9;
|
||||
|
||||
let dict_xs = make_dict () in
|
||||
for i = 0 to 999 do
|
||||
Hashtbl.replace dict_xs (string_of_int i) (Integer i)
|
||||
done;
|
||||
let d = Dict dict_xs in
|
||||
bench_min "inspect dict-1000" (fun () -> inspect d) 100 9;
|
||||
|
||||
let xs = ref [] in
|
||||
for i = 0 to 1999 do xs := Integer i :: !xs done;
|
||||
let lst = List !xs in
|
||||
bench_min "inspect list-2000" (fun () -> inspect lst) 200 9
|
||||
155
hosts/ocaml/bin/bench_vm.ml
Normal file
155
hosts/ocaml/bin/bench_vm.ml
Normal file
@@ -0,0 +1,155 @@
|
||||
(** VM bytecode benchmark — measures throughput of the VM (compiled bytecode).
|
||||
|
||||
Loads the SX compiler via CEK, then for each test:
|
||||
1. Define the function via CEK (as a Lambda).
|
||||
2. Trigger JIT compilation via Sx_vm.jit_compile_lambda.
|
||||
3. Call the compiled VmClosure repeatedly via Sx_vm.call_closure.
|
||||
|
||||
This measures pure VM execution time on the JIT path. *)
|
||||
|
||||
open Sx_types
|
||||
|
||||
let load_compiler env globals =
|
||||
let compiler_path =
|
||||
if Sys.file_exists "lib/compiler.sx" then "lib/compiler.sx"
|
||||
else if Sys.file_exists "../../lib/compiler.sx" then "../../lib/compiler.sx"
|
||||
else if Sys.file_exists "../../../lib/compiler.sx" then "../../../lib/compiler.sx"
|
||||
else failwith "compiler.sx not found"
|
||||
in
|
||||
let ic = open_in compiler_path in
|
||||
let src = really_input_string ic (in_channel_length ic) in
|
||||
close_in ic;
|
||||
let exprs = Sx_parser.parse_all src in
|
||||
List.iter (fun e -> ignore (Sx_ref.eval_expr e (Env env))) exprs;
|
||||
let rec sync e =
|
||||
Hashtbl.iter (fun id v ->
|
||||
let name = Sx_types.unintern id in
|
||||
Hashtbl.replace globals name v) e.bindings;
|
||||
match e.parent with Some p -> sync p | None -> ()
|
||||
in
|
||||
sync env
|
||||
|
||||
let _make_globals env =
|
||||
let g = Hashtbl.create 512 in
|
||||
Hashtbl.iter (fun name fn ->
|
||||
Hashtbl.replace g name (NativeFn (name, fn))
|
||||
) Sx_primitives.primitives;
|
||||
let rec sync e =
|
||||
Hashtbl.iter (fun id v ->
|
||||
let name = Sx_types.unintern id in
|
||||
if not (Hashtbl.mem g name) then Hashtbl.replace g name v) e.bindings;
|
||||
match e.parent with Some p -> sync p | None -> ()
|
||||
in
|
||||
sync env;
|
||||
g
|
||||
|
||||
let define_fn env globals name params body_src =
|
||||
(* Define via CEK so we get a Lambda value with proper closure. *)
|
||||
let body_expr = match Sx_parser.parse_all body_src with
|
||||
| [e] -> e
|
||||
| _ -> failwith "expected one body expression"
|
||||
in
|
||||
let param_syms = List (List.map (fun p -> Symbol p) params) in
|
||||
let define_expr = List [Symbol "define"; Symbol name; List [Symbol "fn"; param_syms; body_expr]] in
|
||||
ignore (Sx_ref.eval_expr define_expr (Env env));
|
||||
(* Sync env to globals so JIT can resolve free vars. *)
|
||||
let rec sync e =
|
||||
Hashtbl.iter (fun id v ->
|
||||
let n = Sx_types.unintern id in
|
||||
Hashtbl.replace globals n v) e.bindings;
|
||||
match e.parent with Some p -> sync p | None -> ()
|
||||
in
|
||||
sync env;
|
||||
(* Now find the Lambda and JIT-compile it. *)
|
||||
let lam_val = Hashtbl.find globals name in
|
||||
match lam_val with
|
||||
| Lambda l ->
|
||||
(match Sx_vm.jit_compile_lambda l globals with
|
||||
| Some cl ->
|
||||
l.l_compiled <- Some cl;
|
||||
Hashtbl.replace globals name (NativeFn (name, fun args ->
|
||||
Sx_vm.call_closure cl args globals));
|
||||
cl
|
||||
| None ->
|
||||
failwith (Printf.sprintf "JIT failed for %s" name))
|
||||
| _ -> failwith (Printf.sprintf "%s is not a Lambda after define" name)
|
||||
|
||||
let bench_call name cl globals args iters =
|
||||
let times = ref [] in
|
||||
for _ = 1 to iters do
|
||||
Gc.full_major ();
|
||||
let t0 = Unix.gettimeofday () in
|
||||
let _r = Sx_vm.call_closure cl args globals in
|
||||
let t1 = Unix.gettimeofday () in
|
||||
times := (t1 -. t0) :: !times
|
||||
done;
|
||||
let sorted = List.sort compare !times in
|
||||
let median = List.nth sorted (iters / 2) in
|
||||
let min_t = List.nth sorted 0 in
|
||||
let max_t = List.nth sorted (iters - 1) in
|
||||
Printf.printf " %-22s min=%8.2fms median=%8.2fms max=%8.2fms\n%!"
|
||||
name (min_t *. 1000.0) (median *. 1000.0) (max_t *. 1000.0);
|
||||
median
|
||||
|
||||
let () =
|
||||
let iters =
|
||||
if Array.length Sys.argv > 1
|
||||
then int_of_string Sys.argv.(1)
|
||||
else 7
|
||||
in
|
||||
Printf.printf "VM (bytecode/JIT) benchmark (%d runs each, taking median)\n%!" iters;
|
||||
Printf.printf "========================================================\n%!";
|
||||
|
||||
let env = Sx_types.make_env () in
|
||||
let bind n fn = ignore (Sx_types.env_bind env n (NativeFn (n, fn))) in
|
||||
(* Seed env with primitives as NativeFn so CEK lookups work. *)
|
||||
Hashtbl.iter (fun name fn ->
|
||||
Hashtbl.replace env.bindings (Sx_types.intern name) (NativeFn (name, fn))
|
||||
) Sx_primitives.primitives;
|
||||
(* Helpers the SX compiler relies on but aren't kernel primitives. *)
|
||||
bind "symbol-name" (fun args -> match args with
|
||||
| [Symbol s] -> String s | _ -> raise (Eval_error "symbol-name"));
|
||||
bind "keyword-name" (fun args -> match args with
|
||||
| [Keyword k] -> String k | _ -> raise (Eval_error "keyword-name"));
|
||||
bind "make-symbol" (fun args -> match args with
|
||||
| [String s] -> Symbol s
|
||||
| [v] -> Symbol (Sx_types.value_to_string v)
|
||||
| _ -> raise (Eval_error "make-symbol"));
|
||||
bind "sx-serialize" (fun args -> match args with
|
||||
| [v] -> String (Sx_types.inspect v)
|
||||
| _ -> raise (Eval_error "sx-serialize"));
|
||||
let globals = Hashtbl.create 1024 in
|
||||
Hashtbl.iter (fun name fn ->
|
||||
Hashtbl.replace globals name (NativeFn (name, fn))
|
||||
) Sx_primitives.primitives;
|
||||
Printf.printf "Loading compiler.sx ... %!";
|
||||
let t0 = Unix.gettimeofday () in
|
||||
load_compiler env globals;
|
||||
Printf.printf "%.0fms\n%!" ((Unix.gettimeofday () -. t0) *. 1000.0);
|
||||
|
||||
(* fib(22) — recursive call benchmark *)
|
||||
let fib_cl = define_fn env globals "fib" ["n"]
|
||||
"(if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))" in
|
||||
let _ = bench_call "fib(22)" fib_cl globals [Number 22.0] iters in
|
||||
|
||||
(* tight loop *)
|
||||
let loop_cl = define_fn env globals "loop" ["n"; "acc"]
|
||||
"(if (= n 0) acc (loop (- n 1) (+ acc 1)))" in
|
||||
let _ = bench_call "loop(200000)" loop_cl globals [Number 200000.0; Number 0.0] iters in
|
||||
|
||||
(* sum-to *)
|
||||
let sum_cl = define_fn env globals "sum_to" ["n"; "acc"]
|
||||
"(if (= n 0) acc (sum_to (- n 1) (+ acc n)))" in
|
||||
let _ = bench_call "sum-to(50000)" sum_cl globals [Number 50000.0; Number 0.0] iters in
|
||||
|
||||
(* count-lt: comparison-heavy *)
|
||||
let cnt_cl = define_fn env globals "count_lt" ["n"; "acc"]
|
||||
"(if (= n 0) acc (count_lt (- n 1) (if (< n 10000) (+ acc 1) acc)))" in
|
||||
let _ = bench_call "count-lt(20000)" cnt_cl globals [Number 20000.0; Number 0.0] iters in
|
||||
|
||||
(* count-eq: equality-heavy on multiples of 7 *)
|
||||
let eq_cl = define_fn env globals "count_eq" ["n"; "acc"]
|
||||
"(if (= n 0) acc (count_eq (- n 1) (if (= 0 (- n (* 7 (/ n 7)))) (+ acc 1) acc)))" in
|
||||
let _ = bench_call "count-eq(20000)" eq_cl globals [Number 20000.0; Number 0.0] iters in
|
||||
|
||||
Printf.printf "\nDone.\n%!"
|
||||
@@ -1,5 +1,5 @@
|
||||
(executables
|
||||
(names run_tests debug_set sx_server integration_tests)
|
||||
(names run_tests debug_set sx_server integration_tests bench_cek bench_inspect bench_vm)
|
||||
(libraries sx unix threads.posix otfm yojson))
|
||||
|
||||
(executable
|
||||
|
||||
@@ -1892,8 +1892,34 @@ let handle_sx_harness_eval args =
|
||||
let file = args |> member "file" |> to_string_option in
|
||||
let setup_str = args |> member "setup" |> to_string_option in
|
||||
let files_json = try args |> member "files" with _ -> `Null in
|
||||
let host_stubs = match args |> member "host_stubs" with `Bool b -> b | _ -> false in
|
||||
let e = !env in
|
||||
let warnings = ref [] in
|
||||
(* Inject stub host primitives so files using host-get/host-new/etc. can load *)
|
||||
if host_stubs then begin
|
||||
let stubs = {|
|
||||
(define host-global (fn (&rest _) nil))
|
||||
(define host-get (fn (&rest _) nil))
|
||||
(define host-set! (fn (obj k v) v))
|
||||
(define host-call (fn (&rest _) nil))
|
||||
(define host-new (fn (&rest _) (dict)))
|
||||
(define host-callback (fn (f) f))
|
||||
(define host-typeof (fn (&rest _) "string"))
|
||||
(define hs-ref-eq (fn (a b) (identical? a b)))
|
||||
(define host-call-fn (fn (&rest _) nil))
|
||||
(define host-iter? (fn (&rest _) false))
|
||||
(define host-to-list (fn (&rest _) (list)))
|
||||
(define host-await (fn (&rest _) nil))
|
||||
(define host-new-function (fn (&rest _) nil))
|
||||
(define load-library! (fn (&rest _) false))
|
||||
|} in
|
||||
let stub_exprs = Sx_parser.parse_all stubs in
|
||||
List.iter (fun expr ->
|
||||
try ignore (Sx_ref.eval_expr expr (Env e))
|
||||
with exn ->
|
||||
warnings := Printf.sprintf "Stub warning: %s" (Printexc.to_string exn) :: !warnings
|
||||
) stub_exprs
|
||||
end;
|
||||
(* Collect all files to load *)
|
||||
let all_files = match files_json with
|
||||
| `List items ->
|
||||
@@ -3018,7 +3044,8 @@ let tool_definitions = `List [
|
||||
("mock", `Assoc [("type", `String "string"); ("description", `String "Optional mock platform overrides as SX dict, e.g. {:fetch (fn (url) {:status 200})}")]);
|
||||
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
|
||||
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
|
||||
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")])]
|
||||
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")]);
|
||||
("host_stubs", `Assoc [("type", `String "boolean"); ("description", `String "If true, inject nil-returning stubs for host-get/host-set!/host-call/host-new/etc. so files that use host primitives can load in the harness")])]
|
||||
["expr"];
|
||||
tool "sx_nav" "Manage sx-docs navigation and articles. Modes: list (all nav items with status), check (validate consistency), add (create article + nav entry), delete (remove nav entry + page fn), move (move entry between sections, rewriting hrefs)."
|
||||
[("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: list, check, add, delete, or move")]);
|
||||
|
||||
@@ -2899,6 +2899,9 @@ let run_spec_tests env test_files =
|
||||
load_module "parser.sx" hs_dir;
|
||||
load_module "compiler.sx" hs_dir;
|
||||
load_module "runtime.sx" hs_dir;
|
||||
let hs_plugins_dir = Filename.concat hs_dir "plugins" in
|
||||
load_module "worker.sx" hs_plugins_dir;
|
||||
load_module "prolog.sx" hs_plugins_dir;
|
||||
load_module "integration.sx" hs_dir;
|
||||
load_module "htmx.sx" hs_dir;
|
||||
(* Override console-log to avoid str on circular mock DOM refs *)
|
||||
|
||||
@@ -703,6 +703,11 @@ let setup_evaluator_bridge env =
|
||||
| [expr; e] -> Sx_ref.eval_expr expr (Env (Sx_runtime.unwrap_env e))
|
||||
| [expr] -> Sx_ref.eval_expr expr (Env env)
|
||||
| _ -> raise (Eval_error "eval-expr: expected (expr env?)"));
|
||||
(* eval-in-env: (env expr) → result. Evaluates expr in the given env. *)
|
||||
Sx_primitives.register "eval-in-env" (fun args ->
|
||||
match args with
|
||||
| [e; expr] -> Sx_ref.eval_expr expr e
|
||||
| _ -> raise (Eval_error "eval-in-env: (env expr)"));
|
||||
bind "trampoline" (fun args ->
|
||||
match args with
|
||||
| [v] ->
|
||||
@@ -764,7 +769,13 @@ let setup_evaluator_bridge env =
|
||||
| _ -> raise (Eval_error "register-special-form!: expected (name handler)"));
|
||||
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms);
|
||||
ignore (Sx_ref.register_special_form (String "<>") (NativeFn ("<>", fun args ->
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))))
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))));
|
||||
(* current-env: special form — returns current lexical env as a first-class value *)
|
||||
ignore (Sx_ref.register_special_form (String "current-env")
|
||||
(NativeFn ("current-env", fun args ->
|
||||
match args with
|
||||
| [_arg_list; env_val] -> env_val
|
||||
| _ -> Nil)))
|
||||
|
||||
(* ---- Type predicates and introspection ---- *)
|
||||
let setup_introspection env =
|
||||
@@ -950,7 +961,24 @@ let setup_env_operations env =
|
||||
bind "env-has?" (fun args -> match args with [e; String k] -> Bool (Sx_types.env_has (uw e) k) | [e; Keyword k] -> Bool (Sx_types.env_has (uw e) k) | _ -> raise (Eval_error "env-has?: expected env and string"));
|
||||
bind "env-bind!" (fun args -> match args with [e; String k; v] -> Sx_types.env_bind (uw e) k v | [e; Keyword k; v] -> Sx_types.env_bind (uw e) k v | _ -> raise (Eval_error "env-bind!: expected env, key, value"));
|
||||
bind "env-set!" (fun args -> match args with [e; String k; v] -> Sx_types.env_set (uw e) k v | [e; Keyword k; v] -> Sx_types.env_set (uw e) k v | _ -> raise (Eval_error "env-set!: expected env, key, value"));
|
||||
bind "env-extend" (fun args -> match args with [e] -> Env (Sx_types.env_extend (uw e)) | _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-extend" (fun args ->
|
||||
match args with
|
||||
| e :: pairs ->
|
||||
let child = Sx_types.env_extend (uw e) in
|
||||
let rec go = function
|
||||
| [] -> ()
|
||||
| k :: v :: rest ->
|
||||
ignore (Sx_types.env_bind child (Sx_runtime.value_to_str k) v); go rest
|
||||
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
||||
go pairs; Env child
|
||||
| _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-lookup" (fun args ->
|
||||
match args with
|
||||
| [e; key] ->
|
||||
let k = Sx_runtime.value_to_str key in
|
||||
let raw = uw e in
|
||||
if Sx_types.env_has raw k then Sx_types.env_get raw k else Nil
|
||||
| _ -> raise (Eval_error "env-lookup: (env key)"));
|
||||
bind "env-merge" (fun args -> match args with [a; b] -> Sx_runtime.env_merge a b | _ -> raise (Eval_error "env-merge: expected 2 envs"))
|
||||
|
||||
(* ---- Strict mode (gradual type system support) ---- *)
|
||||
|
||||
@@ -82,7 +82,10 @@ let cek_run_iterative state =
|
||||
s := cek_step !s
|
||||
done;
|
||||
(match cek_suspended_p !s with
|
||||
| Bool true -> raise (Eval_error "IO suspension in non-IO context")
|
||||
| Bool true ->
|
||||
(match !_cek_io_suspend_hook with
|
||||
| Some hook -> hook !s
|
||||
| None -> raise (Eval_error "IO suspension in non-IO context"))
|
||||
| _ -> cek_value !s)
|
||||
with Eval_error msg ->
|
||||
_last_error_kont_ref := cek_kont !s;
|
||||
@@ -308,6 +311,23 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
|
||||
output
|
||||
)
|
||||
|
||||
# Patch transpiled cek_run to invoke _cek_io_suspend_hook on suspension
|
||||
# instead of unconditionally raising Eval_error. This is the fix for the
|
||||
# tree-walk eval_expr path: sf_letrec init exprs / non-last body exprs,
|
||||
# macro bodies, qq_expand, dynamic-wind / scope / provide bodies all use
|
||||
# `trampoline (eval_expr ...)` and were swallowing CEK suspensions as
|
||||
# "IO suspension in non-IO context" errors. With the hook, the suspension
|
||||
# propagates as VmSuspended to the outer driver (browser callFn / server
|
||||
# eval_expr_io). When the hook is unset (pure-CEK harness), the legacy
|
||||
# error is preserved as the fallback.
|
||||
output = re.sub(
|
||||
r'\(raise \(Eval_error \(value_to_str \(String "IO suspension in non-IO context"\)\)\)\)',
|
||||
'(match !_cek_io_suspend_hook with Some hook -> hook final | None -> '
|
||||
'(raise (Eval_error (value_to_str (String "IO suspension in non-IO context")))))',
|
||||
output,
|
||||
count=1,
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
|
||||
@@ -355,7 +355,9 @@ let vm_create_closure vm_val frame_val code_val =
|
||||
let f = unwrap_frame frame_val in
|
||||
let uv_count = match code_val with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0
|
||||
in
|
||||
let upvalues = Array.init uv_count (fun _ ->
|
||||
|
||||
@@ -75,6 +75,9 @@ cp "$ROOT/shared/sx/templates/tw.sx" "$DIST/sx/"
|
||||
for f in tokenizer parser compiler runtime integration htmx; do
|
||||
cp "$ROOT/lib/hyperscript/$f.sx" "$DIST/sx/hs-$f.sx"
|
||||
done
|
||||
for f in worker prolog; do
|
||||
cp "$ROOT/lib/hyperscript/plugins/$f.sx" "$DIST/sx/hs-$f.sx"
|
||||
done
|
||||
|
||||
# Summary
|
||||
WASM_SIZE=$(du -sh "$DIST/sx_browser.bc.wasm.assets" | cut -f1)
|
||||
|
||||
@@ -85,6 +85,7 @@ const FILES = [
|
||||
'harness-web.sx', 'engine.sx', 'orchestration.sx',
|
||||
// Hyperscript modules — loaded on demand via transparent lazy loader
|
||||
'hs-tokenizer.sx', 'hs-parser.sx', 'hs-compiler.sx', 'hs-runtime.sx',
|
||||
'hs-worker.sx', 'hs-prolog.sx',
|
||||
'hs-integration.sx', 'hs-htmx.sx',
|
||||
'boot.sx',
|
||||
];
|
||||
@@ -455,8 +456,10 @@ for (const file of FILES) {
|
||||
'hs-parser': ['hs-tokenizer'],
|
||||
'hs-compiler': ['hs-tokenizer', 'hs-parser'],
|
||||
'hs-runtime': ['hs-tokenizer', 'hs-parser', 'hs-compiler'],
|
||||
'hs-integration': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime'],
|
||||
'hs-htmx': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime', 'hs-integration'],
|
||||
'hs-worker': ['hs-tokenizer', 'hs-parser'],
|
||||
'hs-prolog': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime'],
|
||||
'hs-integration': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime', 'hs-worker', 'hs-prolog'],
|
||||
'hs-htmx': ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime', 'hs-worker', 'hs-prolog', 'hs-integration'],
|
||||
};
|
||||
manifest[key] = {
|
||||
file: sxbcFile,
|
||||
@@ -477,7 +480,7 @@ if (entryFile) {
|
||||
const lazyDeps = entryFile.deps.filter(d => LAZY_ENTRY_DEPS.has(d));
|
||||
// Hyperscript modules aren't define-library, so not auto-detected as deps.
|
||||
// Load them lazily after boot — eager loading breaks the boot sequence.
|
||||
const HS_LAZY = ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime', 'hs-integration', 'hs-htmx'];
|
||||
const HS_LAZY = ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime', 'hs-worker', 'hs-prolog', 'hs-integration', 'hs-htmx'];
|
||||
for (const m of HS_LAZY) {
|
||||
if (manifest[m] && !lazyDeps.includes(m)) lazyDeps.push(m);
|
||||
}
|
||||
|
||||
@@ -344,6 +344,12 @@ let api_eval src_js =
|
||||
sync_env_to_vm ();
|
||||
return_via_side_channel (value_to_js result)
|
||||
with
|
||||
| Sx_vm.VmSuspended _ ->
|
||||
(* Top-level eval encountered an IO suspension propagated via the
|
||||
cek_run hook (perform inside letrec init / non-last body / macro /
|
||||
qq tree-walked path). K.eval doesn't drive resumption — surface as
|
||||
a clear error so the caller knows to use callFn instead. *)
|
||||
Js.Unsafe.inject (Js.string "Error: IO suspension in non-IO context (use callFn for IO-aware paths)")
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
| Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg))
|
||||
|
||||
@@ -371,6 +377,8 @@ let api_eval_vm src_js =
|
||||
) _vm_globals;
|
||||
return_via_side_channel (value_to_js result)
|
||||
with
|
||||
| Sx_vm.VmSuspended _ ->
|
||||
Js.Unsafe.inject (Js.string "Error: IO suspension in non-IO context (use callFn for IO-aware paths)")
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
| Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg))
|
||||
| Not_found -> Js.Unsafe.inject (Js.string "Error: compile-module not loaded")
|
||||
@@ -381,7 +389,10 @@ let api_eval_expr expr_js _env_js =
|
||||
let result = Sx_ref.eval_expr expr (Env global_env) in
|
||||
sync_env_to_vm ();
|
||||
return_via_side_channel (value_to_js result)
|
||||
with Eval_error msg ->
|
||||
with
|
||||
| Sx_vm.VmSuspended _ ->
|
||||
Js.Unsafe.inject (Js.string "Error: IO suspension in non-IO context (use callFn for IO-aware paths)")
|
||||
| Eval_error msg ->
|
||||
Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
|
||||
let api_load src_js =
|
||||
@@ -704,8 +715,10 @@ let () =
|
||||
| List (Symbol "code" :: rest) ->
|
||||
let d = Hashtbl.create 8 in
|
||||
let rec parse_kv = function
|
||||
| Keyword "arity" :: Number n :: rest -> Hashtbl.replace d "arity" (Number n); parse_kv rest
|
||||
| Keyword "upvalue-count" :: Number n :: rest -> Hashtbl.replace d "upvalue-count" (Number n); parse_kv rest
|
||||
| Keyword "arity" :: (Number _ as n) :: rest -> Hashtbl.replace d "arity" n; parse_kv rest
|
||||
| Keyword "arity" :: (Integer _ as n) :: rest -> Hashtbl.replace d "arity" n; parse_kv rest
|
||||
| Keyword "upvalue-count" :: (Number _ as n) :: rest -> Hashtbl.replace d "upvalue-count" n; parse_kv rest
|
||||
| Keyword "upvalue-count" :: (Integer _ as n) :: rest -> Hashtbl.replace d "upvalue-count" n; parse_kv rest
|
||||
| Keyword "bytecode" :: List nums :: rest ->
|
||||
Hashtbl.replace d "bytecode" (List nums); parse_kv rest
|
||||
| Keyword "constants" :: List consts :: rest ->
|
||||
|
||||
172
hosts/ocaml/browser/test_letrec_resume.js
Normal file
172
hosts/ocaml/browser/test_letrec_resume.js
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
// Repro: letrec sibling bindings nil after perform/resume in browser kernel
|
||||
//
|
||||
// Bug: After a CEK IO suspension (perform / hs-wait) resumes in the
|
||||
// WASM browser kernel, calling a sibling letrec binding could return
|
||||
// nil, with the error surfaced as `[sx] resume: Not callable: nil`.
|
||||
//
|
||||
// Root cause: cek-run / cek_run_iterative raised
|
||||
// `"IO suspension in non-IO context"` when a tree-walked eval_expr
|
||||
// (sf_letrec init exprs / non-last body, macro body, qq unquote, scope
|
||||
// body, provide body, dynamic-wind) hit a perform. The CEK suspension
|
||||
// was created correctly but never propagated through the OCaml-side
|
||||
// _cek_io_suspend_hook, so the outer driver never saw VmSuspended.
|
||||
//
|
||||
// Fix: cek_run / cek_run_iterative now invoke _cek_io_suspend_hook on
|
||||
// suspension (raising VmSuspended for the outer driver). When the hook
|
||||
// is unset (pure-CEK harness), they fall back to the legacy error.
|
||||
//
|
||||
// This test exercises the WASM kernel through K.callFn — the path that
|
||||
// browser event handlers use. Suspension surfaces as a JS object with
|
||||
// {suspended, request, resume(result)} that the test drives synchronously.
|
||||
//
|
||||
// Companion: spec/tests/test-letrec-resume-treewalk.sx tests the
|
||||
// CEK-only path through the OCaml test runner.
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const KERNEL = path.join(__dirname, '..', '_build', 'default', 'browser', 'sx_browser.bc.js');
|
||||
if (!fs.existsSync(KERNEL)) {
|
||||
console.error('FATAL: missing ' + KERNEL + ' — run `dune build` from hosts/ocaml first');
|
||||
process.exit(2);
|
||||
}
|
||||
require(KERNEL);
|
||||
const K = globalThis.SxKernel;
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
const failures = [];
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
const r = fn();
|
||||
if (r === true) {
|
||||
passed++;
|
||||
console.log(' PASS: ' + name);
|
||||
} else {
|
||||
failed++;
|
||||
failures.push({ name, error: 'got ' + JSON.stringify(r) });
|
||||
console.log(' FAIL: ' + name + ' — got ' + JSON.stringify(r));
|
||||
}
|
||||
} catch (e) {
|
||||
failed++;
|
||||
failures.push({ name, error: e.message || String(e) });
|
||||
console.log(' FAIL: ' + name + ' — ' + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function driveSync(result) {
|
||||
while (result && typeof result === 'object' && result.suspended) {
|
||||
result = result.resume(null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function callExpr(src) {
|
||||
K.eval('(define _t-fn (fn () ' + src + '))');
|
||||
const fn = K.eval('_t-fn');
|
||||
return driveSync(K.callFn(fn, []));
|
||||
}
|
||||
|
||||
console.log('\n=== letrec + perform/resume regression tests ===\n');
|
||||
|
||||
test('basic letrec without perform', () =>
|
||||
callExpr('(letrec ((f (fn () "ok"))) (f))') === 'ok');
|
||||
|
||||
test('callFn perform suspends and resumes with nil', () => {
|
||||
K.eval('(define _t-perform (fn () (perform {:op "io"})))');
|
||||
let r = K.callFn(K.eval('_t-perform'), []);
|
||||
if (!r || !r.suspended) return 'no suspension: ' + JSON.stringify(r);
|
||||
return r.resume(null) === null;
|
||||
});
|
||||
|
||||
test('letrec, single binding, perform/resume', () =>
|
||||
callExpr('(letrec ((f (fn () (perform {:op "io"})))) (f))') === null);
|
||||
|
||||
test('letrec, 2 bindings, body calls sibling after suspended call', () =>
|
||||
callExpr(`
|
||||
(letrec
|
||||
((wait-then (fn () (do (perform {:op "io"}) "wait-done")))
|
||||
(other-fn (fn () "other-result")))
|
||||
(do (wait-then) (other-fn)))`) === 'other-result');
|
||||
|
||||
test('letrec, suspending fn calls sibling after own perform', () =>
|
||||
callExpr(`
|
||||
(letrec
|
||||
((wait-and-call (fn () (do (perform {:op "io"}) (other-fn))))
|
||||
(other-fn (fn () "from-sibling")))
|
||||
(wait-and-call))`) === 'from-sibling');
|
||||
|
||||
test('letrec, fn references sibling value after perform/resume', () =>
|
||||
callExpr(`
|
||||
(letrec
|
||||
((shared "shared-state")
|
||||
(do-fn (fn () (do (perform {:op "io"}) shared))))
|
||||
(do-fn))`) === 'shared-state');
|
||||
|
||||
test('letrec, recursive self-call after perform (wait-boot pattern)', () => {
|
||||
K.eval('(define _wb-c 0)');
|
||||
K.eval('(set! _wb-c 0)');
|
||||
return callExpr(`
|
||||
(letrec ((wait-boot (fn ()
|
||||
(do (perform {:op "io"})
|
||||
(if (>= _wb-c 1)
|
||||
"done"
|
||||
(do (set! _wb-c (+ 1 _wb-c))
|
||||
(wait-boot)))))))
|
||||
(wait-boot))`) === 'done';
|
||||
});
|
||||
|
||||
test('top-level define + perform + sibling call after resume', () => {
|
||||
K.eval('(define do-suspend-x (fn () (do (perform {:op "io"}) (do-other-x))))');
|
||||
K.eval('(define do-other-x (fn () "ok-from-other"))');
|
||||
return callExpr('(do-suspend-x)') === 'ok-from-other';
|
||||
});
|
||||
|
||||
test('letrec, two performs (sequential) then sibling call', () =>
|
||||
callExpr(`
|
||||
(letrec
|
||||
((wait-twice (fn () (do (perform {:op "io1"}) (perform {:op "io2"}) (other))))
|
||||
(other (fn () "after-double")))
|
||||
(wait-twice))`) === 'after-double');
|
||||
|
||||
// === Tree-walk paths that previously raised "IO suspension in non-IO context" ===
|
||||
|
||||
test('letrec init expr with perform — suspension propagates (no error)', () => {
|
||||
let r;
|
||||
try { r = callExpr('(letrec ((x (perform {:op "io"}))) "ok")'); }
|
||||
catch (e) { return 'threw: ' + e.message; }
|
||||
return r === null || r === 'ok';
|
||||
});
|
||||
|
||||
test('letrec non-last body with perform — suspension propagates (no error)', () => {
|
||||
let r;
|
||||
try { r = callExpr('(letrec ((x 1)) (perform {:op "io"}) "after")'); }
|
||||
catch (e) { return 'threw: ' + e.message; }
|
||||
return r === null || r === 'after';
|
||||
});
|
||||
|
||||
test('macro body with perform — suspension propagates', () => {
|
||||
K.eval('(defmacro _m1 (form) (do (perform {:op "io"}) form))');
|
||||
let r;
|
||||
try { r = callExpr('(_m1 "macro-ok")'); }
|
||||
catch (e) { return 'threw: ' + e.message; }
|
||||
return r === 'macro-ok' || r === null;
|
||||
});
|
||||
|
||||
test('quasiquote unquote with perform — suspension propagates', () => {
|
||||
let r;
|
||||
try { r = callExpr('(let ((y "yyy")) `(a ,(do (perform {:op "io"}) y) c))'); }
|
||||
catch (e) { return 'threw: ' + e.message; }
|
||||
return r !== undefined;
|
||||
});
|
||||
|
||||
console.log('\n--- Results ---');
|
||||
console.log('passed: ' + passed);
|
||||
console.log('failed: ' + failed);
|
||||
if (failed > 0) {
|
||||
console.log('\nFailures:');
|
||||
failures.forEach(f => console.log(' - ' + f.name + ': ' + f.error));
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
@@ -1,4 +1,4 @@
|
||||
(library
|
||||
(name sx)
|
||||
(wrapped false)
|
||||
(libraries re re.pcre))
|
||||
(libraries re re.pcre unix))
|
||||
|
||||
@@ -200,7 +200,30 @@ and compile_qq_list em items scope =
|
||||
|
||||
(* compile-call *)
|
||||
and compile_call em head args scope tail_p =
|
||||
(let is_prim = (let _and = (prim_call "=" [(type_of (head)); (String "symbol")]) in if not (sx_truthy _and) then _and else (let name = (symbol_name (head)) in (let _and = (Bool (not (sx_truthy ((prim_call "=" [(get ((scope_resolve (scope) (name))) ((String "type"))); (String "local")]))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((prim_call "=" [(get ((scope_resolve (scope) (name))) ((String "type"))); (String "upvalue")]))))) in if not (sx_truthy _and) then _and else (is_primitive (name)))))) in (if sx_truthy (is_prim) then (let name = (symbol_name (head)) in let argc = (len (args)) in let name_idx = (pool_add ((get (em) ((String "pool")))) (name)) in (let () = ignore ((List.iter (fun a -> ignore ((compile_expr (em) (a) (scope) ((Bool false))))) (sx_to_list args); Nil)) in (let () = ignore ((emit_op (em) ((Number 52.0)))) in (let () = ignore ((emit_u16 (em) (name_idx))) in (emit_byte (em) (argc)))))) else (let () = ignore ((compile_expr (em) (head) (scope) ((Bool false)))) in (let () = ignore ((List.iter (fun a -> ignore ((compile_expr (em) (a) (scope) ((Bool false))))) (sx_to_list args); Nil)) in (if sx_truthy (tail_p) then (let () = ignore ((emit_op (em) ((Number 49.0)))) in (emit_byte (em) ((len (args))))) else (let () = ignore ((emit_op (em) ((Number 48.0)))) in (emit_byte (em) ((len (args))))))))))
|
||||
(let is_prim = (let _and = (prim_call "=" [(type_of (head)); (String "symbol")]) in if not (sx_truthy _and) then _and else (let name = (symbol_name (head)) in (let _and = (Bool (not (sx_truthy ((prim_call "=" [(get ((scope_resolve (scope) (name))) ((String "type"))); (String "local")]))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((prim_call "=" [(get ((scope_resolve (scope) (name))) ((String "type"))); (String "upvalue")]))))) in if not (sx_truthy _and) then _and else (is_primitive (name)))))) in (if sx_truthy (is_prim) then (let name = (symbol_name (head)) in let argc = (len (args)) in
|
||||
(* Specialized opcode for hot 2-arg / 1-arg primitives. *)
|
||||
let specialized_op = (match name, argc with
|
||||
| String "+", Number 2.0 -> Some 160
|
||||
| String "-", Number 2.0 -> Some 161
|
||||
| String "*", Number 2.0 -> Some 162
|
||||
| String "/", Number 2.0 -> Some 163
|
||||
| String "=", Number 2.0 -> Some 164
|
||||
| String "<", Number 2.0 -> Some 165
|
||||
| String ">", Number 2.0 -> Some 166
|
||||
| String "cons", Number 2.0 -> Some 172
|
||||
| String "not", Number 1.0 -> Some 167
|
||||
| String "len", Number 1.0 -> Some 168
|
||||
| String "first", Number 1.0 -> Some 169
|
||||
| String "rest", Number 1.0 -> Some 170
|
||||
| _ -> None) in
|
||||
(let () = ignore ((List.iter (fun a -> ignore ((compile_expr (em) (a) (scope) ((Bool false))))) (sx_to_list args); Nil)) in
|
||||
(match specialized_op with
|
||||
| Some op -> emit_op em (Number (float_of_int op))
|
||||
| None ->
|
||||
let name_idx = (pool_add ((get (em) ((String "pool")))) (name)) in
|
||||
let () = ignore ((emit_op (em) ((Number 52.0)))) in
|
||||
let () = ignore ((emit_u16 (em) (name_idx))) in
|
||||
emit_byte (em) (argc)))) else (let () = ignore ((compile_expr (em) (head) (scope) ((Bool false)))) in (let () = ignore ((List.iter (fun a -> ignore ((compile_expr (em) (a) (scope) ((Bool false))))) (sx_to_list args); Nil)) in (if sx_truthy (tail_p) then (let () = ignore ((emit_op (em) ((Number 49.0)))) in (emit_byte (em) ((len (args))))) else (let () = ignore ((emit_op (em) ((Number 48.0)))) in (emit_byte (em) ((len (args))))))))))
|
||||
|
||||
(* compile *)
|
||||
and compile expr =
|
||||
|
||||
@@ -666,7 +666,9 @@ let () =
|
||||
register "list?" (fun args ->
|
||||
match args with [List _] | [ListRef _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "list?: 1 arg"));
|
||||
register "dict?" (fun args ->
|
||||
match args with [Dict _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "dict?: 1 arg"));
|
||||
match args with [Dict _] -> Bool true | [AdtValue _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "dict?: 1 arg"));
|
||||
register "adt?" (fun args ->
|
||||
match args with [AdtValue _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "adt?: 1 arg"));
|
||||
register "symbol?" (fun args ->
|
||||
match args with [Symbol _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "symbol?: 1 arg"));
|
||||
register "keyword?" (fun args ->
|
||||
@@ -1279,6 +1281,11 @@ let () =
|
||||
match args with [String msg] -> raise (Eval_error msg)
|
||||
| [a] -> raise (Eval_error (to_string a))
|
||||
| _ -> raise (Eval_error "host-error: 1 arg"));
|
||||
register "host-warn" (fun args ->
|
||||
match args with
|
||||
| [String msg] -> prerr_endline msg; Nil
|
||||
| [a] -> prerr_endline (to_string a); Nil
|
||||
| _ -> raise (Eval_error "host-warn: 1 arg"));
|
||||
register "try-catch" (fun args ->
|
||||
match args with
|
||||
| [try_fn; catch_fn] ->
|
||||
@@ -1600,6 +1607,32 @@ let () =
|
||||
match args with [StringBuffer buf] -> Integer (Buffer.length buf)
|
||||
| _ -> raise (Eval_error "string-buffer-length: expected (buffer)"));
|
||||
|
||||
(* Short aliases — same StringBuffer value, terser names for hot paths.
|
||||
Append accepts any value: strings pass through, others get inspected/coerced. *)
|
||||
register "make-buffer" (fun _ -> StringBuffer (Buffer.create 64));
|
||||
register "buffer?" (fun args ->
|
||||
match args with [StringBuffer _] -> Bool true | [_] -> Bool false
|
||||
| _ -> raise (Eval_error "buffer?: expected 1 arg"));
|
||||
register "buffer-append!" (fun args ->
|
||||
match args with
|
||||
| [StringBuffer buf; String s] -> Buffer.add_string buf s; Nil
|
||||
| [StringBuffer buf; Integer n] -> Buffer.add_string buf (string_of_int n); Nil
|
||||
| [StringBuffer buf; Number n] -> Buffer.add_string buf (Sx_types.format_number n); Nil
|
||||
| [StringBuffer buf; Symbol s] -> Buffer.add_string buf s; Nil
|
||||
| [StringBuffer buf; Char n] ->
|
||||
Buffer.add_utf_8_uchar buf (Uchar.of_int n); Nil
|
||||
| [StringBuffer buf; Nil] -> Buffer.add_string buf ""; Nil
|
||||
| [StringBuffer buf; Bool true] -> Buffer.add_string buf "true"; Nil
|
||||
| [StringBuffer buf; Bool false] -> Buffer.add_string buf "false"; Nil
|
||||
| [StringBuffer buf; v] -> Buffer.add_string buf (inspect v); Nil
|
||||
| _ -> raise (Eval_error "buffer-append!: expected (buffer value)"));
|
||||
register "buffer->string" (fun args ->
|
||||
match args with [StringBuffer buf] -> String (Buffer.contents buf)
|
||||
| _ -> raise (Eval_error "buffer->string: expected (buffer)"));
|
||||
register "buffer-length" (fun args ->
|
||||
match args with [StringBuffer buf] -> Integer (Buffer.length buf)
|
||||
| _ -> raise (Eval_error "buffer-length: expected (buffer)"));
|
||||
|
||||
(* Capability-based sandboxing — gate IO operations *)
|
||||
let cap_stack : string list ref = ref [] in
|
||||
register "with-capabilities" (fun args ->
|
||||
@@ -3000,4 +3033,705 @@ let () =
|
||||
List.iteri (fun i c -> Bytes.set b i c) bytes_list;
|
||||
SxBytevector b
|
||||
| [Nil] -> SxBytevector (Bytes.create 0)
|
||||
| _ -> raise (Eval_error "list->bytevector: expected list"))
|
||||
| _ -> raise (Eval_error "list->bytevector: expected list"));
|
||||
|
||||
(* === File I/O === *)
|
||||
register "file-read" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
(try
|
||||
let ic = open_in path in
|
||||
let n = in_channel_length ic in
|
||||
let s = Bytes.create n in
|
||||
really_input ic s 0 n;
|
||||
close_in ic;
|
||||
String (Bytes.to_string s)
|
||||
with Sys_error msg -> raise (Eval_error ("file-read: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-read: (path)"));
|
||||
|
||||
register "file-write" (fun args ->
|
||||
match args with
|
||||
| [String path; String content] ->
|
||||
(try
|
||||
let oc = open_out path in
|
||||
output_string oc content;
|
||||
close_out oc;
|
||||
Nil
|
||||
with Sys_error msg -> raise (Eval_error ("file-write: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-write: (path content)"));
|
||||
|
||||
register "file-append" (fun args ->
|
||||
match args with
|
||||
| [String path; String content] ->
|
||||
(try
|
||||
let oc = open_out_gen [Open_append; Open_creat; Open_wronly; Open_text] 0o644 path in
|
||||
output_string oc content;
|
||||
close_out oc;
|
||||
Nil
|
||||
with Sys_error msg -> raise (Eval_error ("file-append: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-append: (path content)"));
|
||||
|
||||
register "file-exists?" (fun args ->
|
||||
match args with
|
||||
| [String path] -> Bool (Sys.file_exists path)
|
||||
| _ -> raise (Eval_error "file-exists?: (path)"));
|
||||
|
||||
register "file-glob" (fun args ->
|
||||
let glob_match pat str =
|
||||
let pn = String.length pat and sn = String.length str in
|
||||
let rec go pi si =
|
||||
if pi = pn then si = sn
|
||||
else match pat.[pi] with
|
||||
| '*' ->
|
||||
let rec try_from i = i <= sn && (go (pi+1) i || try_from (i+1)) in
|
||||
try_from si
|
||||
| '?' -> si < sn && go (pi+1) (si+1)
|
||||
| '[' ->
|
||||
let pi' = ref (pi+1) in
|
||||
let negate = !pi' < pn && pat.[!pi'] = '^' in
|
||||
if negate then incr pi';
|
||||
let matched = ref false in
|
||||
while !pi' < pn && pat.[!pi'] <> ']' do
|
||||
let c1 = pat.[!pi'] in
|
||||
incr pi';
|
||||
if !pi' + 1 < pn && pat.[!pi'] = '-' then begin
|
||||
let c2 = pat.[!pi' + 1] in
|
||||
pi' := !pi' + 2;
|
||||
if si < sn && str.[si] >= c1 && str.[si] <= c2 then matched := true
|
||||
end else if si < sn && str.[si] = c1 then matched := true
|
||||
done;
|
||||
if !pi' < pn then incr pi';
|
||||
((!matched && not negate) || (not !matched && negate)) && go !pi' (si+1)
|
||||
| c -> si < sn && str.[si] = c && go (pi+1) (si+1)
|
||||
in go 0 0
|
||||
in
|
||||
let glob_paths pat =
|
||||
let dir = Filename.dirname pat in
|
||||
let base_pat = Filename.basename pat in
|
||||
let dir' = if dir = "." && not (String.length pat > 1 && pat.[0] = '.') then "." else dir in
|
||||
(try
|
||||
let entries = Sys.readdir dir' in
|
||||
Array.fold_left (fun acc entry ->
|
||||
if glob_match base_pat entry then
|
||||
let full = if dir' = "." then entry else Filename.concat dir' entry in
|
||||
full :: acc
|
||||
else acc
|
||||
) [] entries
|
||||
|> List.sort String.compare
|
||||
with Sys_error _ -> [])
|
||||
in
|
||||
match args with
|
||||
| [String pat] -> List (List.map (fun s -> String s) (glob_paths pat))
|
||||
| _ -> raise (Eval_error "file-glob: (pattern)"));
|
||||
|
||||
(* === File metadata + ops (Phase 5d) === *)
|
||||
let stat_or = function
|
||||
| String path -> (try Some (Unix.stat path) with _ -> None)
|
||||
| _ -> raise (Eval_error "file: path must be a string")
|
||||
in
|
||||
register "file-size" (fun args ->
|
||||
match args with
|
||||
| [v] -> (match stat_or v with Some s -> Integer s.Unix.st_size | None -> Integer 0)
|
||||
| _ -> raise (Eval_error "file-size: (path)"));
|
||||
register "file-mtime" (fun args ->
|
||||
match args with
|
||||
| [v] -> (match stat_or v with Some s -> Integer (int_of_float s.Unix.st_mtime) | None -> Integer 0)
|
||||
| _ -> raise (Eval_error "file-mtime: (path)"));
|
||||
register "file-isfile?" (fun args ->
|
||||
match args with
|
||||
| [v] -> (match stat_or v with Some s -> Bool (s.Unix.st_kind = Unix.S_REG) | None -> Bool false)
|
||||
| _ -> raise (Eval_error "file-isfile?: (path)"));
|
||||
register "file-isdir?" (fun args ->
|
||||
match args with
|
||||
| [v] -> (match stat_or v with Some s -> Bool (s.Unix.st_kind = Unix.S_DIR) | None -> Bool false)
|
||||
| _ -> raise (Eval_error "file-isdir?: (path)"));
|
||||
register "file-readable?" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
Bool (try Unix.access path [Unix.R_OK]; true with _ -> false)
|
||||
| _ -> raise (Eval_error "file-readable?: (path)"));
|
||||
register "file-writable?" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
Bool (try Unix.access path [Unix.W_OK]; true with _ -> false)
|
||||
| _ -> raise (Eval_error "file-writable?: (path)"));
|
||||
register "file-stat" (fun args ->
|
||||
match args with
|
||||
| [v] ->
|
||||
(match stat_or v with
|
||||
| None -> Nil
|
||||
| Some s ->
|
||||
let d = Hashtbl.create 6 in
|
||||
Hashtbl.replace d "size" (Integer s.Unix.st_size);
|
||||
Hashtbl.replace d "mtime" (Integer (int_of_float s.Unix.st_mtime));
|
||||
Hashtbl.replace d "atime" (Integer (int_of_float s.Unix.st_atime));
|
||||
Hashtbl.replace d "ctime" (Integer (int_of_float s.Unix.st_ctime));
|
||||
Hashtbl.replace d "mode" (Integer s.Unix.st_perm);
|
||||
Hashtbl.replace d "type" (String (match s.Unix.st_kind with
|
||||
| Unix.S_REG -> "file" | Unix.S_DIR -> "directory"
|
||||
| Unix.S_LNK -> "link" | Unix.S_CHR -> "characterSpecial"
|
||||
| Unix.S_BLK -> "blockSpecial" | Unix.S_FIFO -> "fifo"
|
||||
| Unix.S_SOCK -> "socket"));
|
||||
Dict d)
|
||||
| _ -> raise (Eval_error "file-stat: (path)"));
|
||||
register "file-delete" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
(try
|
||||
if Sys.is_directory path then Unix.rmdir path
|
||||
else Unix.unlink path
|
||||
with
|
||||
| Unix.Unix_error (Unix.ENOENT, _, _) -> () (* tolerate missing *)
|
||||
| Unix.Unix_error (e, _, _) -> raise (Eval_error ("file-delete: " ^ Unix.error_message e)));
|
||||
Nil
|
||||
| _ -> raise (Eval_error "file-delete: (path)"));
|
||||
register "file-mkdir" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
let rec mk p =
|
||||
if p = "" || p = "." || p = "/" then ()
|
||||
else if Sys.file_exists p then ()
|
||||
else begin
|
||||
mk (Filename.dirname p);
|
||||
(try Unix.mkdir p 0o755
|
||||
with Unix.Unix_error (Unix.EEXIST, _, _) -> ())
|
||||
end
|
||||
in
|
||||
(try mk path
|
||||
with Unix.Unix_error (e, _, _) -> raise (Eval_error ("file-mkdir: " ^ Unix.error_message e)));
|
||||
Nil
|
||||
| _ -> raise (Eval_error "file-mkdir: (path)"));
|
||||
register "file-copy" (fun args ->
|
||||
match args with
|
||||
| [String src; String dst] ->
|
||||
(try
|
||||
let ic = open_in_bin src in
|
||||
let oc = open_out_bin dst in
|
||||
let buf = Bytes.create 8192 in
|
||||
let rec loop () =
|
||||
let n = input ic buf 0 (Bytes.length buf) in
|
||||
if n > 0 then (output oc buf 0 n; loop ())
|
||||
in
|
||||
loop ();
|
||||
close_in ic;
|
||||
close_out oc;
|
||||
Nil
|
||||
with
|
||||
| Sys_error msg -> raise (Eval_error ("file-copy: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-copy: (src dst)"));
|
||||
register "file-rename" (fun args ->
|
||||
match args with
|
||||
| [String src; String dst] ->
|
||||
(try Sys.rename src dst with Sys_error msg -> raise (Eval_error ("file-rename: " ^ msg)));
|
||||
Nil
|
||||
| _ -> raise (Eval_error "file-rename: (src dst)"));
|
||||
|
||||
(* === Channels (random-access + blocking control) === *)
|
||||
let channel_table : (string, Unix.file_descr * string * bool ref * bool ref) Hashtbl.t = Hashtbl.create 16 in
|
||||
let channel_next_id = ref 0 in
|
||||
let parse_open_mode mode =
|
||||
match mode with
|
||||
| "r" -> [Unix.O_RDONLY]
|
||||
| "w" -> [Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC]
|
||||
| "a" -> [Unix.O_WRONLY; Unix.O_CREAT; Unix.O_APPEND]
|
||||
| "r+" -> [Unix.O_RDWR]
|
||||
| "w+" -> [Unix.O_RDWR; Unix.O_CREAT; Unix.O_TRUNC]
|
||||
| "a+" -> [Unix.O_RDWR; Unix.O_CREAT; Unix.O_APPEND]
|
||||
| _ -> raise (Eval_error ("channel-open: invalid mode " ^ mode))
|
||||
in
|
||||
let chan_get name =
|
||||
match Hashtbl.find_opt channel_table name with
|
||||
| Some c -> c
|
||||
| None -> raise (Eval_error ("channel: no such channel " ^ name))
|
||||
in
|
||||
register "channel-open" (fun args ->
|
||||
match args with
|
||||
| [String path; String mode] ->
|
||||
(try
|
||||
let fd = Unix.openfile path (parse_open_mode mode) 0o644 in
|
||||
let id = !channel_next_id in
|
||||
incr channel_next_id;
|
||||
let name = Printf.sprintf "file%d" id in
|
||||
Hashtbl.replace channel_table name (fd, mode, ref false, ref true);
|
||||
String name
|
||||
with Unix.Unix_error (e, _, _) -> raise (Eval_error ("channel-open: " ^ Unix.error_message e)))
|
||||
| _ -> raise (Eval_error "channel-open: (path mode)"));
|
||||
|
||||
register "channel-close" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (fd, _, _, _) = chan_get name in
|
||||
(try Unix.close fd with _ -> ());
|
||||
Hashtbl.remove channel_table name;
|
||||
Nil
|
||||
| _ -> raise (Eval_error "channel-close: (channel)"));
|
||||
|
||||
register "channel-read" (fun args ->
|
||||
let (name, max_n) = match args with
|
||||
| [String n] -> (n, -1)
|
||||
| [String n; Integer m] -> (n, m)
|
||||
| [String n; Number m] -> (n, int_of_float m)
|
||||
| _ -> raise (Eval_error "channel-read: (channel ?n?)")
|
||||
in
|
||||
let (fd, _, eof, _) = chan_get name in
|
||||
let chunk = 8192 in
|
||||
let buf = Bytes.create chunk in
|
||||
let buffer = Buffer.create chunk in
|
||||
let total = ref 0 in
|
||||
let stop = ref false in
|
||||
while not !stop do
|
||||
let want = if max_n < 0 then chunk else min chunk (max_n - !total) in
|
||||
if want <= 0 then stop := true
|
||||
else begin
|
||||
try
|
||||
let r = Unix.read fd buf 0 want in
|
||||
if r = 0 then begin eof := true; stop := true end
|
||||
else begin
|
||||
Buffer.add_subbytes buffer buf 0 r;
|
||||
total := !total + r
|
||||
end
|
||||
with
|
||||
| Unix.Unix_error (Unix.EAGAIN, _, _)
|
||||
| Unix.Unix_error (Unix.EWOULDBLOCK, _, _) -> stop := true
|
||||
end
|
||||
done;
|
||||
String (Buffer.contents buffer));
|
||||
|
||||
register "channel-read-line" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (fd, _, eof, _) = chan_get name in
|
||||
let buf = Buffer.create 80 in
|
||||
let one = Bytes.create 1 in
|
||||
let got_data = ref false in
|
||||
let stop = ref false in
|
||||
while not !stop do
|
||||
try
|
||||
let r = Unix.read fd one 0 1 in
|
||||
if r = 0 then begin eof := true; stop := true end
|
||||
else begin
|
||||
got_data := true;
|
||||
let c = Bytes.get one 0 in
|
||||
if c = '\n' then stop := true
|
||||
else Buffer.add_char buf c
|
||||
end
|
||||
with
|
||||
| Unix.Unix_error (Unix.EAGAIN, _, _)
|
||||
| Unix.Unix_error (Unix.EWOULDBLOCK, _, _) -> stop := true
|
||||
done;
|
||||
if !got_data then String (Buffer.contents buf) else Nil
|
||||
| _ -> raise (Eval_error "channel-read-line: (channel)"));
|
||||
|
||||
register "channel-write" (fun args ->
|
||||
match args with
|
||||
| [String name; String s] ->
|
||||
let (fd, _, _, _) = chan_get name in
|
||||
let b = Bytes.of_string s in
|
||||
let n = Bytes.length b in
|
||||
let written = ref 0 in
|
||||
while !written < n do
|
||||
(try
|
||||
let w = Unix.write fd b !written (n - !written) in
|
||||
written := !written + w
|
||||
with
|
||||
| Unix.Unix_error (Unix.EAGAIN, _, _)
|
||||
| Unix.Unix_error (Unix.EWOULDBLOCK, _, _) ->
|
||||
(* short write — let caller retry *)
|
||||
written := n)
|
||||
done;
|
||||
Nil
|
||||
| _ -> raise (Eval_error "channel-write: (channel string)"));
|
||||
|
||||
register "channel-flush" (fun args ->
|
||||
match args with
|
||||
| [String name] -> let _ = chan_get name in Nil (* no userspace buffer *)
|
||||
| _ -> raise (Eval_error "channel-flush: (channel)"));
|
||||
|
||||
register "channel-seek" (fun args ->
|
||||
let (name, offset, whence) = match args with
|
||||
| [String n; Integer o] -> (n, o, "start")
|
||||
| [String n; Number o] -> (n, int_of_float o, "start")
|
||||
| [String n; Integer o; String w] -> (n, o, w)
|
||||
| [String n; Number o; String w] -> (n, int_of_float o, w)
|
||||
| _ -> raise (Eval_error "channel-seek: (channel offset ?whence?)")
|
||||
in
|
||||
let (fd, _, eof, _) = chan_get name in
|
||||
let cmd = match whence with
|
||||
| "start" -> Unix.SEEK_SET
|
||||
| "current" -> Unix.SEEK_CUR
|
||||
| "end" -> Unix.SEEK_END
|
||||
| _ -> raise (Eval_error ("channel-seek: invalid whence " ^ whence))
|
||||
in
|
||||
let _ = Unix.lseek fd offset cmd in
|
||||
eof := false;
|
||||
Nil);
|
||||
|
||||
register "channel-tell" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (fd, _, _, _) = chan_get name in
|
||||
Integer (Unix.lseek fd 0 Unix.SEEK_CUR)
|
||||
| _ -> raise (Eval_error "channel-tell: (channel)"));
|
||||
|
||||
register "channel-eof?" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (_, _, eof, _) = chan_get name in
|
||||
Bool !eof
|
||||
| _ -> raise (Eval_error "channel-eof?: (channel)"));
|
||||
|
||||
register "channel-blocking?" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (_, _, _, blocking) = chan_get name in
|
||||
Bool !blocking
|
||||
| _ -> raise (Eval_error "channel-blocking?: (channel)"));
|
||||
|
||||
register "channel-set-blocking!" (fun args ->
|
||||
match args with
|
||||
| [String name; Bool b] ->
|
||||
let (fd, _, _, blocking) = chan_get name in
|
||||
blocking := b;
|
||||
(try
|
||||
if b then Unix.clear_nonblock fd
|
||||
else Unix.set_nonblock fd
|
||||
with _ -> ());
|
||||
Nil
|
||||
| _ -> raise (Eval_error "channel-set-blocking!: (channel bool)"));
|
||||
|
||||
(* === Sockets === wrapping Unix.socket/connect/bind/listen/accept *)
|
||||
let resolve_inet_addr host =
|
||||
if host = "" || host = "0.0.0.0" then Unix.inet_addr_any
|
||||
else if host = "localhost" then Unix.inet_addr_loopback
|
||||
else
|
||||
try Unix.inet_addr_of_string host
|
||||
with _ ->
|
||||
try
|
||||
let entry = Unix.gethostbyname host in
|
||||
if Array.length entry.Unix.h_addr_list = 0 then
|
||||
raise (Eval_error ("socket: cannot resolve " ^ host))
|
||||
else entry.Unix.h_addr_list.(0)
|
||||
with Not_found -> raise (Eval_error ("socket: cannot resolve " ^ host))
|
||||
in
|
||||
let port_of v = match v with
|
||||
| Integer n -> n
|
||||
| Number n -> int_of_float n
|
||||
| _ -> raise (Eval_error "socket: port must be a number")
|
||||
in
|
||||
let alloc_chan_name () =
|
||||
let id = !channel_next_id in
|
||||
incr channel_next_id;
|
||||
Printf.sprintf "sock%d" id
|
||||
in
|
||||
|
||||
register "socket-connect" (fun args ->
|
||||
match args with
|
||||
| [String host; port_v] ->
|
||||
let port = port_of port_v in
|
||||
let addr = Unix.ADDR_INET (resolve_inet_addr host, port) in
|
||||
let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
|
||||
(try Unix.connect sock addr
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
(try Unix.close sock with _ -> ());
|
||||
raise (Eval_error ("socket-connect: " ^ Unix.error_message e)));
|
||||
let name = alloc_chan_name () in
|
||||
Hashtbl.replace channel_table name (sock, "rw", ref false, ref true);
|
||||
String name
|
||||
| _ -> raise (Eval_error "socket-connect: (host port)"));
|
||||
|
||||
(* Non-blocking connect: returns channel immediately. Connection completes
|
||||
when the channel becomes writable; query channel-async-error? after to
|
||||
confirm success or get the error. *)
|
||||
register "socket-connect-async" (fun args ->
|
||||
match args with
|
||||
| [String host; port_v] ->
|
||||
let port = port_of port_v in
|
||||
let addr = Unix.ADDR_INET (resolve_inet_addr host, port) in
|
||||
let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
|
||||
Unix.set_nonblock sock;
|
||||
(try Unix.connect sock addr
|
||||
with
|
||||
| Unix.Unix_error (Unix.EINPROGRESS, _, _)
|
||||
| Unix.Unix_error (Unix.EWOULDBLOCK, _, _) -> ()
|
||||
| Unix.Unix_error (e, _, _) ->
|
||||
(try Unix.close sock with _ -> ());
|
||||
raise (Eval_error ("socket-connect-async: " ^ Unix.error_message e)));
|
||||
let name = alloc_chan_name () in
|
||||
Hashtbl.replace channel_table name (sock, "rw", ref false, ref false);
|
||||
String name
|
||||
| _ -> raise (Eval_error "socket-connect-async: (host port)"));
|
||||
|
||||
(* After a non-blocking connect completes (channel writable), check whether
|
||||
the connect succeeded. Returns "" on success, error message on failure. *)
|
||||
register "channel-async-error" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (fd, _, _, _) = chan_get name in
|
||||
(try
|
||||
let err = Unix.getsockopt_error fd in
|
||||
match err with
|
||||
| None -> String ""
|
||||
| Some e -> String (Unix.error_message e)
|
||||
with
|
||||
| Unix.Unix_error (e, _, _) -> String (Unix.error_message e))
|
||||
| _ -> raise (Eval_error "channel-async-error: (channel)"));
|
||||
|
||||
register "socket-server" (fun args ->
|
||||
let (host, port) = match args with
|
||||
| [port_v] -> ("", port_of port_v)
|
||||
| [String h; port_v] -> (h, port_of port_v)
|
||||
| _ -> raise (Eval_error "socket-server: (port) or (host port)")
|
||||
in
|
||||
let addr = Unix.ADDR_INET (resolve_inet_addr host, port) in
|
||||
let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
|
||||
Unix.setsockopt sock Unix.SO_REUSEADDR true;
|
||||
(try Unix.bind sock addr
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
(try Unix.close sock with _ -> ());
|
||||
raise (Eval_error ("socket-server: bind: " ^ Unix.error_message e)));
|
||||
Unix.listen sock 8;
|
||||
let name = alloc_chan_name () in
|
||||
Hashtbl.replace channel_table name (sock, "server", ref false, ref true);
|
||||
String name);
|
||||
|
||||
register "socket-accept" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let (sock, _, _, _) = chan_get name in
|
||||
let (client_sock, client_addr) =
|
||||
try Unix.accept sock
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
raise (Eval_error ("socket-accept: " ^ Unix.error_message e))
|
||||
in
|
||||
let (host_str, port) = match client_addr with
|
||||
| Unix.ADDR_INET (addr, p) -> (Unix.string_of_inet_addr addr, p)
|
||||
| Unix.ADDR_UNIX path -> (path, 0)
|
||||
in
|
||||
let client_name = alloc_chan_name () in
|
||||
Hashtbl.replace channel_table client_name (client_sock, "rw", ref false, ref true);
|
||||
let d = Hashtbl.create 3 in
|
||||
Hashtbl.replace d "channel" (String client_name);
|
||||
Hashtbl.replace d "host" (String host_str);
|
||||
Hashtbl.replace d "port" (Integer port);
|
||||
Dict d
|
||||
| _ -> raise (Eval_error "socket-accept: (server-channel)"));
|
||||
|
||||
(* io-select-channels: (read-list write-list timeout-ms) → {:readable [...] :writable [...]}
|
||||
timeout-ms < 0 → block indefinitely; 0 → poll. Returns ready channel names. *)
|
||||
register "io-select-channels" (fun args ->
|
||||
let to_ms v = match v with
|
||||
| Integer n -> n
|
||||
| Number n -> int_of_float n
|
||||
| _ -> raise (Eval_error "io-select-channels: timeout must be a number")
|
||||
in
|
||||
let to_list v = match v with
|
||||
| List xs | ListRef { contents = xs } -> xs
|
||||
| Nil -> []
|
||||
| _ -> raise (Eval_error "io-select-channels: expected list")
|
||||
in
|
||||
let chan_name_of v = match v with
|
||||
| String s -> s
|
||||
| _ -> raise (Eval_error "io-select-channels: channel must be a string")
|
||||
in
|
||||
let (read_list, write_list, timeout_ms) = match args with
|
||||
| [r; w; t] -> (to_list r, to_list w, to_ms t)
|
||||
| _ -> raise (Eval_error "io-select-channels: (read-list write-list timeout-ms)")
|
||||
in
|
||||
let read_pairs = List.map (fun v ->
|
||||
let name = chan_name_of v in
|
||||
let (fd, _, _, _) = chan_get name in (name, fd)) read_list in
|
||||
let write_pairs = List.map (fun v ->
|
||||
let name = chan_name_of v in
|
||||
let (fd, _, _, _) = chan_get name in (name, fd)) write_list in
|
||||
let read_fds = List.map snd read_pairs in
|
||||
let write_fds = List.map snd write_pairs in
|
||||
let timeout = if timeout_ms < 0 then -1.0 else float_of_int timeout_ms /. 1000.0 in
|
||||
let (ready_r, ready_w, _) =
|
||||
try Unix.select read_fds write_fds [] timeout
|
||||
with Unix.Unix_error (Unix.EINTR, _, _) -> ([], [], [])
|
||||
in
|
||||
let names_of pairs ready =
|
||||
List.filter_map (fun (n, fd) ->
|
||||
if List.exists (fun rfd -> rfd = fd) ready then Some (String n) else None
|
||||
) pairs
|
||||
in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "readable" (List (names_of read_pairs ready_r));
|
||||
Hashtbl.replace d "writable" (List (names_of write_pairs ready_w));
|
||||
Dict d);
|
||||
|
||||
(* === Clock === *)
|
||||
register "clock-seconds" (fun args ->
|
||||
match args with
|
||||
| [] -> Integer (int_of_float (Unix.gettimeofday ()))
|
||||
| _ -> raise (Eval_error "clock-seconds: no args"));
|
||||
|
||||
register "clock-milliseconds" (fun args ->
|
||||
match args with
|
||||
| [] -> Integer (int_of_float (Unix.gettimeofday () *. 1000.0))
|
||||
| _ -> raise (Eval_error "clock-milliseconds: no args"));
|
||||
|
||||
let format_tm tm tz_label =
|
||||
fun fmt ->
|
||||
let buf = Buffer.create 32 in
|
||||
let n = String.length fmt in
|
||||
let i = ref 0 in
|
||||
while !i < n do
|
||||
if fmt.[!i] = '%' && !i + 1 < n then begin
|
||||
(match fmt.[!i + 1] with
|
||||
| 'Y' -> Buffer.add_string buf (Printf.sprintf "%04d" (1900 + tm.Unix.tm_year))
|
||||
| 'y' -> Buffer.add_string buf (Printf.sprintf "%02d" ((1900 + tm.Unix.tm_year) mod 100))
|
||||
| 'm' -> Buffer.add_string buf (Printf.sprintf "%02d" (tm.Unix.tm_mon + 1))
|
||||
| 'd' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_mday)
|
||||
| 'e' -> Buffer.add_string buf (Printf.sprintf "%2d" tm.Unix.tm_mday)
|
||||
| 'H' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_hour)
|
||||
| 'I' -> let h = tm.Unix.tm_hour mod 12 in
|
||||
Buffer.add_string buf (Printf.sprintf "%02d" (if h = 0 then 12 else h))
|
||||
| 'p' -> Buffer.add_string buf (if tm.Unix.tm_hour < 12 then "AM" else "PM")
|
||||
| 'M' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_min)
|
||||
| 'S' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_sec)
|
||||
| 'j' -> Buffer.add_string buf (Printf.sprintf "%03d" (tm.Unix.tm_yday + 1))
|
||||
| 'w' -> Buffer.add_string buf (string_of_int tm.Unix.tm_wday)
|
||||
| 'Z' -> Buffer.add_string buf tz_label
|
||||
| 'a' -> let days = [|"Sun";"Mon";"Tue";"Wed";"Thu";"Fri";"Sat"|] in
|
||||
Buffer.add_string buf days.(tm.Unix.tm_wday)
|
||||
| 'A' -> let days = [|"Sunday";"Monday";"Tuesday";"Wednesday";"Thursday";"Friday";"Saturday"|] in
|
||||
Buffer.add_string buf days.(tm.Unix.tm_wday)
|
||||
| 'b' | 'h' -> let mons = [|"Jan";"Feb";"Mar";"Apr";"May";"Jun";"Jul";"Aug";"Sep";"Oct";"Nov";"Dec"|] in
|
||||
Buffer.add_string buf mons.(tm.Unix.tm_mon)
|
||||
| 'B' -> let mons = [|"January";"February";"March";"April";"May";"June";"July";"August";"September";"October";"November";"December"|] in
|
||||
Buffer.add_string buf mons.(tm.Unix.tm_mon)
|
||||
| '%' -> Buffer.add_char buf '%'
|
||||
| c -> Buffer.add_char buf '%'; Buffer.add_char buf c);
|
||||
i := !i + 2
|
||||
end else begin
|
||||
Buffer.add_char buf fmt.[!i];
|
||||
incr i
|
||||
end
|
||||
done;
|
||||
Buffer.contents buf
|
||||
in
|
||||
register "clock-format" (fun args ->
|
||||
let (t, fmt, tz) = match args with
|
||||
| [Integer t] -> (t, "%a %b %e %H:%M:%S %Z %Y", "utc")
|
||||
| [Integer t; String f] -> (t, f, "utc")
|
||||
| [Integer t; String f; String z] -> (t, f, z)
|
||||
| _ -> raise (Eval_error "clock-format: (seconds [format [tz]])")
|
||||
in
|
||||
let tm =
|
||||
if tz = "local" then Unix.localtime (float_of_int t)
|
||||
else Unix.gmtime (float_of_int t)
|
||||
in
|
||||
let label = if tz = "local" then "" else "UTC" in
|
||||
String (format_tm tm label fmt));
|
||||
|
||||
(* clock-scan: parse a date string with format, return seconds.
|
||||
Supports the same format specifiers as clock-format (fixed-width ones).
|
||||
tz: "utc" (default) or "local". *)
|
||||
let timegm (tm : Unix.tm) =
|
||||
let is_leap y = y mod 4 = 0 && (y mod 100 <> 0 || y mod 400 = 0) in
|
||||
let days_in_month = [|31;28;31;30;31;30;31;31;30;31;30;31|] in
|
||||
let year = tm.Unix.tm_year + 1900 in
|
||||
let mon = tm.Unix.tm_mon in
|
||||
let mday = tm.Unix.tm_mday in
|
||||
let total_days = ref 0 in
|
||||
if year >= 1970 then begin
|
||||
for y = 1970 to year - 1 do
|
||||
total_days := !total_days + (if is_leap y then 366 else 365)
|
||||
done
|
||||
end else begin
|
||||
for y = year to 1969 do
|
||||
total_days := !total_days - (if is_leap y then 366 else 365)
|
||||
done
|
||||
end;
|
||||
for m = 0 to mon - 1 do
|
||||
total_days := !total_days + days_in_month.(m);
|
||||
if m = 1 && is_leap year then incr total_days
|
||||
done;
|
||||
total_days := !total_days + mday - 1;
|
||||
!total_days * 86400
|
||||
+ tm.Unix.tm_hour * 3600
|
||||
+ tm.Unix.tm_min * 60
|
||||
+ tm.Unix.tm_sec
|
||||
in
|
||||
register "clock-scan" (fun args ->
|
||||
let (str, fmt, tz) = match args with
|
||||
| [String s; String f] -> (s, f, "utc")
|
||||
| [String s; String f; String z] -> (s, f, z)
|
||||
| _ -> raise (Eval_error "clock-scan: (str fmt [tz])")
|
||||
in
|
||||
let n = String.length fmt and sn = String.length str in
|
||||
let tm = ref { Unix.tm_year = 70; tm_mon = 0; tm_mday = 1;
|
||||
tm_hour = 0; tm_min = 0; tm_sec = 0;
|
||||
tm_wday = 0; tm_yday = 0; tm_isdst = false } in
|
||||
let i = ref 0 and j = ref 0 in
|
||||
let read_n_digits k =
|
||||
let s = ref "" in
|
||||
let cnt = ref 0 in
|
||||
while !cnt < k && !j < sn && str.[!j] >= '0' && str.[!j] <= '9' do
|
||||
s := !s ^ String.make 1 str.[!j];
|
||||
incr j; incr cnt
|
||||
done;
|
||||
if !s = "" then 0 else int_of_string !s
|
||||
in
|
||||
let skip_ws () =
|
||||
while !j < sn && (str.[!j] = ' ' || str.[!j] = '\t') do incr j done
|
||||
in
|
||||
while !i < n do
|
||||
if fmt.[!i] = '%' && !i + 1 < n then begin
|
||||
(match fmt.[!i + 1] with
|
||||
| 'Y' -> tm := { !tm with tm_year = read_n_digits 4 - 1900 }
|
||||
| 'y' -> let y = read_n_digits 2 in
|
||||
tm := { !tm with tm_year = (if y < 70 then 100 + y else y) }
|
||||
| 'm' -> tm := { !tm with tm_mon = read_n_digits 2 - 1 }
|
||||
| 'd' | 'e' -> skip_ws (); tm := { !tm with tm_mday = read_n_digits 2 }
|
||||
| 'H' | 'I' -> tm := { !tm with tm_hour = read_n_digits 2 }
|
||||
| 'M' -> tm := { !tm with tm_min = read_n_digits 2 }
|
||||
| 'S' -> tm := { !tm with tm_sec = read_n_digits 2 }
|
||||
| '%' -> if !j < sn && str.[!j] = '%' then incr j
|
||||
| _ -> () (* unsupported specifier — skip *)
|
||||
);
|
||||
i := !i + 2
|
||||
end else begin
|
||||
if fmt.[!i] = ' ' then skip_ws ()
|
||||
else if !j < sn && str.[!j] = fmt.[!i] then incr j;
|
||||
incr i
|
||||
end
|
||||
done;
|
||||
let secs =
|
||||
if tz = "local" then int_of_float (fst (Unix.mktime !tm))
|
||||
else timegm !tm
|
||||
in
|
||||
Integer secs);
|
||||
|
||||
(* === Env-as-value (Phase 4) === *)
|
||||
|
||||
(* env-lookup: (env key) → value or nil. Works on Env, Dict, or Nil. *)
|
||||
register "env-lookup" (fun args ->
|
||||
let unwrap = function
|
||||
| Env e -> e
|
||||
| Nil -> make_env ()
|
||||
| _ -> raise (Eval_error "env-lookup: first arg must be an environment") in
|
||||
match args with
|
||||
| [env_val; key] ->
|
||||
let e = unwrap env_val in
|
||||
let k = value_to_string key in
|
||||
if env_has e k then env_get e k else Nil
|
||||
| _ -> raise (Eval_error "env-lookup: (env key)"));
|
||||
|
||||
(* env-extend: (env [key val ...]) → new child env with optional bindings. *)
|
||||
register "env-extend" (fun args ->
|
||||
match args with
|
||||
| [] -> raise (Eval_error "env-extend: requires at least one arg")
|
||||
| env_val :: pairs ->
|
||||
let parent_env = match env_val with
|
||||
| Env e -> e
|
||||
| Nil -> make_env ()
|
||||
| _ -> raise (Eval_error "env-extend: first arg must be an environment") in
|
||||
let child = env_extend parent_env in
|
||||
let rec add_bindings = function
|
||||
| [] -> ()
|
||||
| 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
|
||||
add_bindings pairs;
|
||||
Env child)
|
||||
|
||||
@@ -614,7 +614,7 @@ and cek_step_loop state =
|
||||
|
||||
(* cek-run *)
|
||||
and cek_run state =
|
||||
(let final = (cek_step_loop (state)) in (if sx_truthy ((cek_suspended_p (final))) then (raise (Eval_error (value_to_str (String "IO suspension in non-IO context")))) else (cek_value (final))))
|
||||
(let final = (cek_step_loop (state)) in (if sx_truthy ((cek_suspended_p (final))) then (match !_cek_io_suspend_hook with Some hook -> hook final | None -> (raise (Eval_error (value_to_str (String "IO suspension in non-IO context"))))) else (cek_value (final))))
|
||||
|
||||
(* cek-resume *)
|
||||
and cek_resume suspended_state result' =
|
||||
@@ -759,7 +759,78 @@ and match_pattern pattern value env =
|
||||
|
||||
(* step-sf-match *)
|
||||
and step_sf_match args env kont =
|
||||
(let val' = (trampoline ((eval_expr ((first (args))) (env)))) in let clauses = (rest (args)) in (let result' = (match_find_clause (val') (clauses) (env)) in (if sx_truthy ((is_nil (result'))) then (make_cek_value ((String (sx_str [(String "match: no clause matched "); (inspect (val'))]))) (env) ((kont_push ((make_raise_eval_frame (env) ((Bool false)))) (kont)))) else (make_cek_state ((nth (result') ((Number 1.0)))) ((first (result'))) (kont)))))
|
||||
(let val' = (trampoline ((eval_expr ((first (args))) (env)))) in let clauses = (rest (args)) in (let () = ignore (match_check_exhaustiveness val' clauses env) in (let result' = (match_find_clause (val') (clauses) (env)) in (if sx_truthy ((is_nil (result'))) then (make_cek_value ((String (sx_str [(String "match: no clause matched "); (inspect (val'))]))) (env) ((kont_push ((make_raise_eval_frame (env) ((Bool false)))) (kont)))) else (make_cek_state ((nth (result') ((Number 1.0)))) ((first (result'))) (kont))))))
|
||||
|
||||
(* match-check-exhaustiveness — Step 8 hand-patched into sx_ref.ml *)
|
||||
and match_check_exhaustiveness val' clauses env =
|
||||
let is_else_pat p =
|
||||
match p with
|
||||
| Symbol "_" | Symbol "else" -> true
|
||||
| Keyword "else" -> true
|
||||
| _ -> false
|
||||
in
|
||||
let clause_is_else c =
|
||||
match c with
|
||||
| List (p :: _) -> is_else_pat p
|
||||
| _ -> false
|
||||
in
|
||||
let clause_ctor_name c =
|
||||
match c with
|
||||
| List (List (Symbol n :: _) :: _) -> Some n
|
||||
| _ -> None
|
||||
in
|
||||
let type_name_opt = match val' with
|
||||
| AdtValue a -> Some a.av_type
|
||||
| Dict d ->
|
||||
(match Hashtbl.find_opt d "_adt" with
|
||||
| Some (Bool true) ->
|
||||
(match Hashtbl.find_opt d "_type" with
|
||||
| Some (String s) -> Some s
|
||||
| _ -> None)
|
||||
| _ -> None)
|
||||
| _ -> None
|
||||
in
|
||||
match type_name_opt with
|
||||
| None -> Nil
|
||||
| Some type_name ->
|
||||
if not (sx_truthy (env_has env (String "*adt-registry*"))) then Nil
|
||||
else
|
||||
let registry = env_get env (String "*adt-registry*") in
|
||||
let registered = match registry with
|
||||
| Dict r ->
|
||||
(match Hashtbl.find_opt r type_name with
|
||||
| Some (List ctors) -> Some ctors
|
||||
| _ -> None)
|
||||
| _ -> None in
|
||||
(match registered with
|
||||
| None -> Nil
|
||||
| Some ctor_vals ->
|
||||
let clauses_list = match clauses with List xs -> xs | _ -> [] in
|
||||
if List.exists clause_is_else clauses_list then Nil
|
||||
else
|
||||
let clause_ctors = List.filter_map clause_ctor_name clauses_list in
|
||||
let registered_names = List.filter_map (function
|
||||
| String s -> Some s | _ -> None) ctor_vals in
|
||||
let missing = List.filter (fun c -> not (List.mem c clause_ctors)) registered_names in
|
||||
if missing = [] then Nil
|
||||
else begin
|
||||
if not (sx_truthy (env_has env (String "*adt-warned*"))) then
|
||||
ignore (env_bind env (String "*adt-warned*") (Dict (Hashtbl.create 4)));
|
||||
let warned = env_get env (String "*adt-warned*") in
|
||||
let key = type_name ^ "|" ^ String.concat "," missing in
|
||||
let already = match warned with
|
||||
| Dict w -> (match Hashtbl.find_opt w key with Some (Bool true) -> true | _ -> false)
|
||||
| _ -> false in
|
||||
if already then Nil
|
||||
else begin
|
||||
(match warned with
|
||||
| Dict w -> Hashtbl.replace w key (Bool true)
|
||||
| _ -> ());
|
||||
let msg = "[sx] match: non-exhaustive — " ^ type_name ^ ": missing " ^ String.concat ", " missing in
|
||||
ignore (host_warn (String msg));
|
||||
Nil
|
||||
end
|
||||
end)
|
||||
|
||||
(* step-sf-handler-bind *)
|
||||
and step_sf_handler_bind args env kont =
|
||||
@@ -981,7 +1052,14 @@ let cek_run_iterative state =
|
||||
s := cek_step !s
|
||||
done;
|
||||
(match cek_suspended_p !s with
|
||||
| Bool true -> raise (Eval_error "IO suspension in non-IO context")
|
||||
| Bool true ->
|
||||
(* Propagate suspension via the OCaml-side hook so it converts to
|
||||
VmSuspended and flows to the outer driver (value_to_js / resume
|
||||
callback). Without the hook (pure CEK harness), keep the legacy
|
||||
error so test runners surface the misuse. *)
|
||||
(match !_cek_io_suspend_hook with
|
||||
| Some hook -> hook !s
|
||||
| None -> raise (Eval_error "IO suspension in non-IO context"))
|
||||
| _ -> cek_value !s)
|
||||
with Eval_error msg ->
|
||||
_last_error_kont_ref := cek_kont !s;
|
||||
@@ -1054,8 +1132,7 @@ let sf_define_type args env_val =
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
|
||||
(match Hashtbl.find_opt d "_type" with Some (String t) -> t = type_name | _ -> false))
|
||||
| AdtValue a -> Bool (a.av_type = type_name)
|
||||
| _ -> Bool false)
|
||||
| _ -> Bool false)));
|
||||
List.iter (fun spec ->
|
||||
@@ -1069,21 +1146,18 @@ let sf_define_type args env_val =
|
||||
if List.length ctor_args <> arity then
|
||||
raise (Eval_error (Printf.sprintf "%s: expected %d args, got %d"
|
||||
cn arity (List.length ctor_args)))
|
||||
else begin
|
||||
let d = Hashtbl.create 4 in
|
||||
Hashtbl.replace d "_adt" (Bool true);
|
||||
Hashtbl.replace d "_type" (String type_name);
|
||||
Hashtbl.replace d "_ctor" (String cn);
|
||||
Hashtbl.replace d "_fields" (List ctor_args);
|
||||
Dict d
|
||||
end));
|
||||
else
|
||||
AdtValue {
|
||||
av_type = type_name;
|
||||
av_ctor = cn;
|
||||
av_fields = Array.of_list ctor_args;
|
||||
}));
|
||||
env_bind_v (cn ^ "?")
|
||||
(NativeFn (cn ^ "?", fun pargs ->
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
|
||||
(match Hashtbl.find_opt d "_ctor" with Some (String c) -> c = cn | _ -> false))
|
||||
| AdtValue a -> Bool (a.av_ctor = cn)
|
||||
| _ -> Bool false)
|
||||
| _ -> Bool false)));
|
||||
List.iteri (fun idx fname ->
|
||||
@@ -1092,13 +1166,10 @@ let sf_define_type args env_val =
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d ->
|
||||
(match Hashtbl.find_opt d "_fields" with
|
||||
| Some (List fs) ->
|
||||
if idx < List.length fs then List.nth fs idx
|
||||
else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds"))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT")))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not a dict")))
|
||||
| AdtValue a ->
|
||||
if idx < Array.length a.av_fields then a.av_fields.(idx)
|
||||
else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds"))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT")))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": expected 1 arg")))))
|
||||
) field_names
|
||||
| _ -> ())
|
||||
|
||||
@@ -6,11 +6,72 @@
|
||||
|
||||
open Sx_types
|
||||
|
||||
(** Call a registered primitive by name. *)
|
||||
(** Fast path equality — same as Sx_primitives.safe_eq for the common cases
|
||||
that show up in hot dispatch (string vs string, etc). Falls through to
|
||||
the registered "=" primitive for complex cases. *)
|
||||
let rec _fast_eq a b =
|
||||
if a == b then true
|
||||
else match a, b with
|
||||
| String x, String y -> x = y
|
||||
| Integer x, Integer y -> x = y
|
||||
| Number x, Number y -> x = y
|
||||
| Integer x, Number y -> float_of_int x = y
|
||||
| Number x, Integer y -> x = float_of_int y
|
||||
| Bool x, Bool y -> x = y
|
||||
| Nil, Nil -> true
|
||||
| Symbol x, Symbol y -> x = y
|
||||
| Keyword x, Keyword y -> x = y
|
||||
| List la, List lb ->
|
||||
(try List.for_all2 _fast_eq la lb with Invalid_argument _ -> false)
|
||||
| _ -> false
|
||||
|
||||
(** Call a registered primitive by name.
|
||||
Fast path for hot dispatch primitives ([=], [<], [>], [<=], [>=], [empty?],
|
||||
[first], [rest], [len]) skips the Hashtbl lookup entirely — these are
|
||||
called millions of times in the CEK [step_continue]/[step_eval] dispatch. *)
|
||||
let prim_call name args =
|
||||
match Hashtbl.find_opt Sx_primitives.primitives name with
|
||||
| Some f -> f args
|
||||
| None -> raise (Eval_error ("Unknown primitive: " ^ name))
|
||||
(* Hot path: most-frequently-called primitives by step_continue dispatch *)
|
||||
match name, args with
|
||||
| "=", [a; b] -> Bool (_fast_eq a b)
|
||||
| "empty?", [List []] -> Bool true
|
||||
| "empty?", [List _] -> Bool false
|
||||
| "empty?", [ListRef { contents = [] }] -> Bool true
|
||||
| "empty?", [ListRef _] -> Bool false
|
||||
| "empty?", [Nil] -> Bool true
|
||||
| "first", [List (x :: _)] -> x
|
||||
| "first", [List []] -> Nil
|
||||
| "first", [ListRef { contents = (x :: _) }] -> x
|
||||
| "first", [ListRef _] -> Nil
|
||||
| "first", [Nil] -> Nil
|
||||
| "rest", [List (_ :: xs)] -> List xs
|
||||
| "rest", [List []] -> List []
|
||||
| "rest", [ListRef { contents = (_ :: xs) }] -> List xs
|
||||
| "rest", [ListRef _] -> List []
|
||||
| "rest", [Nil] -> List []
|
||||
| "len", [List l] -> Integer (List.length l)
|
||||
| "len", [ListRef r] -> Integer (List.length !r)
|
||||
| "len", [String s] -> Integer (String.length s)
|
||||
| "len", [Nil] -> Integer 0
|
||||
| "<", [Integer x; Integer y] -> Bool (x < y)
|
||||
| "<", [Number x; Number y] -> Bool (x < y)
|
||||
| "<", [Integer x; Number y] -> Bool (float_of_int x < y)
|
||||
| "<", [Number x; Integer y] -> Bool (x < float_of_int y)
|
||||
| ">", [Integer x; Integer y] -> Bool (x > y)
|
||||
| ">", [Number x; Number y] -> Bool (x > y)
|
||||
| ">", [Integer x; Number y] -> Bool (float_of_int x > y)
|
||||
| ">", [Number x; Integer y] -> Bool (x > float_of_int y)
|
||||
| "<=", [Integer x; Integer y] -> Bool (x <= y)
|
||||
| "<=", [Number x; Number y] -> Bool (x <= y)
|
||||
| "<=", [Integer x; Number y] -> Bool (float_of_int x <= y)
|
||||
| "<=", [Number x; Integer y] -> Bool (x <= float_of_int y)
|
||||
| ">=", [Integer x; Integer y] -> Bool (x >= y)
|
||||
| ">=", [Number x; Number y] -> Bool (x >= y)
|
||||
| ">=", [Integer x; Number y] -> Bool (float_of_int x >= y)
|
||||
| ">=", [Number x; Integer y] -> Bool (x >= float_of_int y)
|
||||
| _ ->
|
||||
match Hashtbl.find_opt Sx_primitives.primitives name with
|
||||
| Some f -> f args
|
||||
| None -> raise (Eval_error ("Unknown primitive: " ^ name))
|
||||
|
||||
(** Convert any SX value to an OCaml string (internal). *)
|
||||
let value_to_str = function
|
||||
@@ -209,6 +270,13 @@ let get_val container key =
|
||||
| _ -> Nil)
|
||||
| Dict d, String k -> dict_get d k
|
||||
| Dict d, Keyword k -> dict_get d k
|
||||
| AdtValue a, String k | AdtValue a, Keyword k ->
|
||||
(match k with
|
||||
| "_adt" -> Bool true
|
||||
| "_type" -> String a.av_type
|
||||
| "_ctor" -> String a.av_ctor
|
||||
| "_fields" -> List (Array.to_list a.av_fields)
|
||||
| _ -> Nil)
|
||||
| (List l | ListRef { contents = l }), Number n ->
|
||||
(try List.nth l (int_of_float n) with _ -> Nil)
|
||||
| (List l | ListRef { contents = l }), Integer n ->
|
||||
@@ -404,6 +472,10 @@ let callcc_continuation_winders_len v = match v with
|
||||
let host_error msg =
|
||||
raise (Eval_error (value_to_str msg))
|
||||
|
||||
let host_warn msg =
|
||||
prerr_endline (value_to_str msg);
|
||||
Nil
|
||||
|
||||
let dynamic_wind_call before body after _env =
|
||||
ignore (sx_call before []);
|
||||
let result = sx_call body [] in
|
||||
@@ -539,3 +611,4 @@ let jit_try_call f args =
|
||||
(match hook f arg_list with Some result -> incr _jit_hit; result | None -> incr _jit_miss; _jit_skip_sentinel)
|
||||
| _ -> incr _jit_skip; _jit_skip_sentinel
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,16 @@ and value =
|
||||
| SxSet of (string, value) Hashtbl.t (** Mutable set keyed by inspect(value). *)
|
||||
| SxRegexp of string * string * Re.re (** Regexp: source, flags, compiled. *)
|
||||
| SxBytevector of bytes (** Mutable bytevector — R7RS bytevector type. *)
|
||||
| AdtValue of adt_value (** Native algebraic data type instance — opaque sum type. *)
|
||||
|
||||
(** Algebraic data type instance — produced by [define-type] constructors.
|
||||
[av_type] is the type name (e.g. "Maybe"), [av_ctor] is the constructor
|
||||
name (e.g. "Just"), [av_fields] are the positional field values. *)
|
||||
and adt_value = {
|
||||
av_type : string;
|
||||
av_ctor : string;
|
||||
av_fields : value array;
|
||||
}
|
||||
|
||||
(** String input port: source string + mutable cursor position. *)
|
||||
and sx_port_kind =
|
||||
@@ -520,6 +530,7 @@ let type_of = function
|
||||
| SxSet _ -> "set"
|
||||
| SxRegexp _ -> "regexp"
|
||||
| SxBytevector _ -> "bytevector"
|
||||
| AdtValue a -> a.av_type
|
||||
|
||||
let is_nil = function Nil -> true | _ -> false
|
||||
let is_lambda = function Lambda _ -> true | _ -> false
|
||||
@@ -806,14 +817,15 @@ let dict_vals (d : dict) =
|
||||
|
||||
(** {1 Value display} *)
|
||||
|
||||
let rec inspect = function
|
||||
| Nil -> "nil"
|
||||
| Bool true -> "true"
|
||||
| Bool false -> "false"
|
||||
| Integer n -> string_of_int n
|
||||
| Number n -> format_number n
|
||||
(* Single shared buffer for the entire inspect recursion — eliminates
|
||||
the per-level [String.concat (List.map inspect ...)] allocation. *)
|
||||
let rec inspect_into buf = function
|
||||
| Nil -> Buffer.add_string buf "nil"
|
||||
| Bool true -> Buffer.add_string buf "true"
|
||||
| Bool false -> Buffer.add_string buf "false"
|
||||
| Integer n -> Buffer.add_string buf (string_of_int n)
|
||||
| Number n -> Buffer.add_string buf (format_number n)
|
||||
| String s ->
|
||||
let buf = Buffer.create (String.length s + 2) in
|
||||
Buffer.add_char buf '"';
|
||||
String.iter (function
|
||||
| '"' -> Buffer.add_string buf "\\\""
|
||||
@@ -822,66 +834,129 @@ let rec inspect = function
|
||||
| '\r' -> Buffer.add_string buf "\\r"
|
||||
| '\t' -> Buffer.add_string buf "\\t"
|
||||
| c -> Buffer.add_char buf c) s;
|
||||
Buffer.add_char buf '"';
|
||||
Buffer.contents buf
|
||||
| Symbol s -> s
|
||||
| Keyword k -> ":" ^ k
|
||||
Buffer.add_char buf '"'
|
||||
| Symbol s -> Buffer.add_string buf s
|
||||
| Keyword k -> Buffer.add_char buf ':'; Buffer.add_string buf k
|
||||
| List items | ListRef { contents = items } ->
|
||||
"(" ^ String.concat " " (List.map inspect items) ^ ")"
|
||||
Buffer.add_char buf '(';
|
||||
(match items with
|
||||
| [] -> ()
|
||||
| x :: rest ->
|
||||
inspect_into buf x;
|
||||
List.iter (fun v -> Buffer.add_char buf ' '; inspect_into buf v) rest);
|
||||
Buffer.add_char buf ')'
|
||||
| Dict d ->
|
||||
let pairs = Hashtbl.fold (fun k v acc ->
|
||||
(Printf.sprintf ":%s %s" k (inspect v)) :: acc) d [] in
|
||||
"{" ^ String.concat " " pairs ^ "}"
|
||||
Buffer.add_char buf '{';
|
||||
let first = ref true in
|
||||
Hashtbl.iter (fun k v ->
|
||||
if !first then first := false else Buffer.add_char buf ' ';
|
||||
Buffer.add_char buf ':'; Buffer.add_string buf k;
|
||||
Buffer.add_char buf ' '; inspect_into buf v) d;
|
||||
Buffer.add_char buf '}'
|
||||
| Lambda l ->
|
||||
let tag = match l.l_name with Some n -> n | None -> "lambda" in
|
||||
Printf.sprintf "<%s(%s)>" tag (String.concat ", " l.l_params)
|
||||
Buffer.add_char buf '<'; Buffer.add_string buf tag;
|
||||
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " l.l_params);
|
||||
Buffer.add_string buf ")>"
|
||||
| Component c ->
|
||||
Printf.sprintf "<Component ~%s(%s)>" c.c_name (String.concat ", " c.c_params)
|
||||
Buffer.add_string buf "<Component ~"; Buffer.add_string buf c.c_name;
|
||||
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " c.c_params);
|
||||
Buffer.add_string buf ")>"
|
||||
| Island i ->
|
||||
Printf.sprintf "<Island ~%s(%s)>" i.i_name (String.concat ", " i.i_params)
|
||||
Buffer.add_string buf "<Island ~"; Buffer.add_string buf i.i_name;
|
||||
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " i.i_params);
|
||||
Buffer.add_string buf ")>"
|
||||
| Macro m ->
|
||||
let tag = match m.m_name with Some n -> n | None -> "macro" in
|
||||
Printf.sprintf "<%s(%s)>" tag (String.concat ", " m.m_params)
|
||||
| Thunk _ -> "<thunk>"
|
||||
| Continuation (_, _) -> "<continuation>"
|
||||
| CallccContinuation (_, _) -> "<callcc-continuation>"
|
||||
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name
|
||||
| Signal _ -> "<signal>"
|
||||
| RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s)
|
||||
| Spread _ -> "<spread>"
|
||||
| SxExpr s -> Printf.sprintf "\"<sx-expr:%d>\"" (String.length s)
|
||||
| Env _ -> "<env>"
|
||||
| CekState _ -> "<cek-state>"
|
||||
| CekFrame f -> Printf.sprintf "<frame:%s>" f.cf_type
|
||||
| VmClosure cl -> Printf.sprintf "<vm:%s>" (match cl.vm_name with Some n -> n | None -> "anon")
|
||||
Buffer.add_char buf '<'; Buffer.add_string buf tag;
|
||||
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " m.m_params);
|
||||
Buffer.add_string buf ")>"
|
||||
| Thunk _ -> Buffer.add_string buf "<thunk>"
|
||||
| Continuation (_, _) -> Buffer.add_string buf "<continuation>"
|
||||
| CallccContinuation (_, _) -> Buffer.add_string buf "<callcc-continuation>"
|
||||
| NativeFn (name, _) ->
|
||||
Buffer.add_string buf "<native:"; Buffer.add_string buf name; Buffer.add_char buf '>'
|
||||
| Signal _ -> Buffer.add_string buf "<signal>"
|
||||
| RawHTML s ->
|
||||
Buffer.add_string buf "\"<raw-html:";
|
||||
Buffer.add_string buf (string_of_int (String.length s));
|
||||
Buffer.add_string buf ">\""
|
||||
| Spread _ -> Buffer.add_string buf "<spread>"
|
||||
| SxExpr s ->
|
||||
Buffer.add_string buf "\"<sx-expr:";
|
||||
Buffer.add_string buf (string_of_int (String.length s));
|
||||
Buffer.add_string buf ">\""
|
||||
| Env _ -> Buffer.add_string buf "<env>"
|
||||
| CekState _ -> Buffer.add_string buf "<cek-state>"
|
||||
| CekFrame f ->
|
||||
Buffer.add_string buf "<frame:"; Buffer.add_string buf f.cf_type; Buffer.add_char buf '>'
|
||||
| VmClosure cl ->
|
||||
Buffer.add_string buf "<vm:";
|
||||
Buffer.add_string buf (match cl.vm_name with Some n -> n | None -> "anon");
|
||||
Buffer.add_char buf '>'
|
||||
| Record r ->
|
||||
let fields = Array.to_list (Array.mapi (fun i v ->
|
||||
Printf.sprintf "%s=%s" r.r_type.rt_fields.(i) (inspect v)
|
||||
) r.r_fields) in
|
||||
Printf.sprintf "<record:%s %s>" r.r_type.rt_name (String.concat " " fields)
|
||||
| Parameter p -> Printf.sprintf "<parameter:%s>" p.pm_uid
|
||||
Buffer.add_string buf "<record:"; Buffer.add_string buf r.r_type.rt_name;
|
||||
Array.iteri (fun i v ->
|
||||
Buffer.add_char buf ' ';
|
||||
Buffer.add_string buf r.r_type.rt_fields.(i);
|
||||
Buffer.add_char buf '=';
|
||||
inspect_into buf v) r.r_fields;
|
||||
Buffer.add_char buf '>'
|
||||
| Parameter p ->
|
||||
Buffer.add_string buf "<parameter:"; Buffer.add_string buf p.pm_uid; Buffer.add_char buf '>'
|
||||
| Vector arr ->
|
||||
let elts = Array.to_list (Array.map inspect arr) in
|
||||
Printf.sprintf "#(%s)" (String.concat " " elts)
|
||||
| VmFrame f -> Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base
|
||||
| VmMachine m -> Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames)
|
||||
| StringBuffer buf -> Printf.sprintf "<string-buffer:%d>" (Buffer.length buf)
|
||||
| HashTable ht -> Printf.sprintf "<hash-table:%d>" (Hashtbl.length ht)
|
||||
Buffer.add_string buf "#(";
|
||||
Array.iteri (fun i v ->
|
||||
if i > 0 then Buffer.add_char buf ' ';
|
||||
inspect_into buf v) arr;
|
||||
Buffer.add_char buf ')'
|
||||
| VmFrame f ->
|
||||
Buffer.add_string buf (Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base)
|
||||
| VmMachine m ->
|
||||
Buffer.add_string buf (Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames))
|
||||
| StringBuffer b ->
|
||||
Buffer.add_string buf (Printf.sprintf "<string-buffer:%d>" (Buffer.length b))
|
||||
| HashTable ht ->
|
||||
Buffer.add_string buf (Printf.sprintf "<hash-table:%d>" (Hashtbl.length ht))
|
||||
| Char n ->
|
||||
let name = match n with
|
||||
| 32 -> "space" | 10 -> "newline" | 9 -> "tab"
|
||||
| 13 -> "return" | 0 -> "nul" | 27 -> "escape"
|
||||
| 127 -> "delete" | 8 -> "backspace"
|
||||
| _ -> let buf = Buffer.create 1 in
|
||||
Buffer.add_utf_8_uchar buf (Uchar.of_int n);
|
||||
Buffer.contents buf
|
||||
in "#\\" ^ name
|
||||
| Eof -> "#!eof"
|
||||
Buffer.add_string buf "#\\";
|
||||
(match n with
|
||||
| 32 -> Buffer.add_string buf "space"
|
||||
| 10 -> Buffer.add_string buf "newline"
|
||||
| 9 -> Buffer.add_string buf "tab"
|
||||
| 13 -> Buffer.add_string buf "return"
|
||||
| 0 -> Buffer.add_string buf "nul"
|
||||
| 27 -> Buffer.add_string buf "escape"
|
||||
| 127 -> Buffer.add_string buf "delete"
|
||||
| 8 -> Buffer.add_string buf "backspace"
|
||||
| _ -> Buffer.add_utf_8_uchar buf (Uchar.of_int n))
|
||||
| Eof -> Buffer.add_string buf "#!eof"
|
||||
| Port { sp_kind = PortInput (_, pos); sp_closed } ->
|
||||
Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else "")
|
||||
| Port { sp_kind = PortOutput buf; sp_closed } ->
|
||||
Printf.sprintf "<output-port:len=%d%s>" (Buffer.length buf) (if sp_closed then ":closed" else "")
|
||||
| Rational (n, d) -> Printf.sprintf "%d/%d" n d
|
||||
| SxSet ht -> Printf.sprintf "<set:%d>" (Hashtbl.length ht)
|
||||
| SxRegexp (src, flags, _) -> Printf.sprintf "#/%s/%s" src flags
|
||||
| SxBytevector b -> Printf.sprintf "#u8(%s)" (String.concat " " (List.init (Bytes.length b) (fun i -> string_of_int (Char.code (Bytes.get b i)))))
|
||||
Buffer.add_string buf (Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else ""))
|
||||
| Port { sp_kind = PortOutput b; sp_closed } ->
|
||||
Buffer.add_string buf (Printf.sprintf "<output-port:len=%d%s>" (Buffer.length b) (if sp_closed then ":closed" else ""))
|
||||
| Rational (n, d) ->
|
||||
Buffer.add_string buf (string_of_int n); Buffer.add_char buf '/';
|
||||
Buffer.add_string buf (string_of_int d)
|
||||
| SxSet ht ->
|
||||
Buffer.add_string buf (Printf.sprintf "<set:%d>" (Hashtbl.length ht))
|
||||
| SxRegexp (src, flags, _) ->
|
||||
Buffer.add_string buf "#/"; Buffer.add_string buf src;
|
||||
Buffer.add_char buf '/'; Buffer.add_string buf flags
|
||||
| SxBytevector b ->
|
||||
Buffer.add_string buf "#u8(";
|
||||
let n = Bytes.length b in
|
||||
for i = 0 to n - 1 do
|
||||
if i > 0 then Buffer.add_char buf ' ';
|
||||
Buffer.add_string buf (string_of_int (Char.code (Bytes.get b i)))
|
||||
done;
|
||||
Buffer.add_char buf ')'
|
||||
| AdtValue a ->
|
||||
Buffer.add_char buf '('; Buffer.add_string buf a.av_ctor;
|
||||
Array.iter (fun v -> Buffer.add_char buf ' '; inspect_into buf v) a.av_fields;
|
||||
Buffer.add_char buf ')'
|
||||
|
||||
let inspect v =
|
||||
let buf = Buffer.create 64 in
|
||||
inspect_into buf v;
|
||||
Buffer.contents buf
|
||||
|
||||
@@ -327,7 +327,18 @@ and call_closure_reuse cl args =
|
||||
vm.sp <- saved_sp;
|
||||
raise e);
|
||||
vm.frames <- saved_frames;
|
||||
pop vm
|
||||
(* Snapshot/restore sp around the popped result.
|
||||
OP_RETURN normally leaves sp = saved_sp + 1, but the bytecode-exhausted
|
||||
path (or a callee that returns a closure whose own RETURN leaves extra
|
||||
stack residue) can leave sp inconsistent. Read the result at the
|
||||
expected slot and reset sp explicitly so the parent frame's
|
||||
intermediate values are not corrupted. *)
|
||||
let result =
|
||||
if vm.sp > saved_sp then vm.stack.(vm.sp - 1)
|
||||
else Nil
|
||||
in
|
||||
vm.sp <- saved_sp;
|
||||
result
|
||||
| None ->
|
||||
call_closure cl args cl.vm_env_ref
|
||||
|
||||
@@ -631,7 +642,9 @@ and run vm =
|
||||
(* Read upvalue descriptors from bytecode *)
|
||||
let uv_count = match code_val with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0
|
||||
in
|
||||
let upvalues = Array.init uv_count (fun _ ->
|
||||
@@ -731,38 +744,57 @@ and run vm =
|
||||
| 160 (* OP_ADD *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y -> Integer (x + y)
|
||||
| Number x, Number y -> Number (x +. y)
|
||||
| Integer x, Number y -> Number (float_of_int x +. y)
|
||||
| Number x, Integer y -> Number (x +. float_of_int y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "+") [a; b])
|
||||
| 161 (* OP_SUB *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y -> Integer (x - y)
|
||||
| Number x, Number y -> Number (x -. y)
|
||||
| Integer x, Number y -> Number (float_of_int x -. y)
|
||||
| Number x, Integer y -> Number (x -. float_of_int y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "-") [a; b])
|
||||
| 162 (* OP_MUL *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y -> Integer (x * y)
|
||||
| Number x, Number y -> Number (x *. y)
|
||||
| Integer x, Number y -> Number (float_of_int x *. y)
|
||||
| Number x, Integer y -> Number (x *. float_of_int y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "*") [a; b])
|
||||
| 163 (* OP_DIV *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y when y <> 0 && x mod y = 0 -> Integer (x / y)
|
||||
| Integer x, Integer y -> Number (float_of_int x /. float_of_int y)
|
||||
| Number x, Number y -> Number (x /. y)
|
||||
| Integer x, Number y -> Number (float_of_int x /. y)
|
||||
| Number x, Integer y -> Number (x /. float_of_int y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b])
|
||||
| 164 (* OP_EQ *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm ((Hashtbl.find Sx_primitives.primitives "=") [a; b])
|
||||
push vm (Bool (Sx_runtime._fast_eq a b))
|
||||
| 165 (* OP_LT *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y -> Bool (x < y)
|
||||
| Number x, Number y -> Bool (x < y)
|
||||
| Integer x, Number y -> Bool (float_of_int x < y)
|
||||
| Number x, Integer y -> Bool (x < float_of_int y)
|
||||
| String x, String y -> Bool (x < y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "<") [a; b])
|
||||
| _ -> Sx_runtime.prim_call "<" [a; b])
|
||||
| 166 (* OP_GT *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
| Integer x, Integer y -> Bool (x > y)
|
||||
| Number x, Number y -> Bool (x > y)
|
||||
| Integer x, Number y -> Bool (float_of_int x > y)
|
||||
| Number x, Integer y -> Bool (x > float_of_int y)
|
||||
| String x, String y -> Bool (x > y)
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives ">") [a; b])
|
||||
| _ -> Sx_runtime.prim_call ">" [a; b])
|
||||
| 167 (* OP_NOT *) ->
|
||||
let v = pop vm in
|
||||
push vm (Bool (not (sx_truthy v)))
|
||||
@@ -885,9 +917,17 @@ let resume_vm vm result =
|
||||
let rec restore_reuse pending =
|
||||
match pending with
|
||||
| [] -> ()
|
||||
| (saved_frames, _saved_sp) :: rest ->
|
||||
| (saved_frames, saved_sp) :: rest ->
|
||||
let callback_result = pop vm in
|
||||
vm.frames <- saved_frames;
|
||||
(* Restore sp to the value captured before the suspended callee was
|
||||
pushed. The callee's locals/temps may still be on the stack above
|
||||
saved_sp; without this reset, subsequent LOCAL_GET/SET in the
|
||||
caller frame (e.g. letrec sibling bindings waiting on the call)
|
||||
see stale callee data instead of their own slots. Mirrors the
|
||||
OP_RETURN+sp-reset semantics that sync `call_closure_reuse`
|
||||
relies on for clean caller-frame state. *)
|
||||
if saved_sp < vm.sp then vm.sp <- saved_sp;
|
||||
push vm callback_result;
|
||||
(try
|
||||
run vm;
|
||||
@@ -1269,7 +1309,9 @@ let trace_run src globals =
|
||||
let code_val2 = frame.closure.vm_code.vc_constants.(idx) in
|
||||
let uv_count = match code_val2 with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0 in
|
||||
let upvalues = Array.init uv_count (fun _ ->
|
||||
let is_local = read_u8 frame in
|
||||
@@ -1390,7 +1432,9 @@ let disassemble (code : vm_code) =
|
||||
if op = 51 && idx < Array.length consts then begin
|
||||
let uv_count = match consts.(idx) with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0 in
|
||||
ip := !ip + uv_count * 2
|
||||
end
|
||||
|
||||
@@ -270,7 +270,9 @@ let vm_create_closure vm_val frame_val code_val =
|
||||
let f = unwrap_frame frame_val in
|
||||
let uv_count = match code_val with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0
|
||||
in
|
||||
let upvalues = Array.init uv_count (fun _ ->
|
||||
|
||||
@@ -265,7 +265,9 @@ let vm_create_closure vm_val frame_val code_val =
|
||||
let f = unwrap_frame frame_val in
|
||||
let uv_count = match code_val with
|
||||
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0)
|
||||
| Some (Integer n) -> n
|
||||
| Some (Number n) -> int_of_float n
|
||||
| _ -> 0)
|
||||
| _ -> 0
|
||||
in
|
||||
let upvalues = Array.init uv_count (fun _ ->
|
||||
|
||||
116
lib/apl/conformance.sh
Executable file
116
lib/apl/conformance.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/apl/conformance.sh — run APL test suites, emit scoreboard.json + scoreboard.md.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUITES=(structural operators dfn tradfn valence programs system idioms eval-ops pipeline)
|
||||
|
||||
OUT_JSON="lib/apl/scoreboard.json"
|
||||
OUT_MD="lib/apl/scoreboard.md"
|
||||
|
||||
run_suite() {
|
||||
local suite=$1
|
||||
local file="lib/apl/tests/${suite}.sx"
|
||||
local TMP
|
||||
TMP=$(mktemp)
|
||||
cat > "$TMP" << EPOCHS
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/r7rs.sx")
|
||||
(load "lib/apl/runtime.sx")
|
||||
(load "lib/apl/tokenizer.sx")
|
||||
(load "lib/apl/parser.sx")
|
||||
(load "lib/apl/transpile.sx")
|
||||
(epoch 2)
|
||||
(eval "(define apl-test-pass 0)")
|
||||
(eval "(define apl-test-fail 0)")
|
||||
(eval "(define apl-test (fn (name got expected) (if (= got expected) (set! apl-test-pass (+ apl-test-pass 1)) (set! apl-test-fail (+ apl-test-fail 1)))))")
|
||||
(epoch 3)
|
||||
(load "${file}")
|
||||
(epoch 4)
|
||||
(eval "(list apl-test-pass apl-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
local OUTPUT
|
||||
OUTPUT=$(timeout 300 "$SX_SERVER" < "$TMP" 2>/dev/null)
|
||||
rm -f "$TMP"
|
||||
|
||||
local LINE
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 4 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 4 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 4 //; s/\)$//')
|
||||
fi
|
||||
|
||||
local P F
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
P=${P:-0}
|
||||
F=${F:-0}
|
||||
echo "${P} ${F}"
|
||||
}
|
||||
|
||||
declare -A SUITE_PASS
|
||||
declare -A SUITE_FAIL
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
|
||||
echo "Running APL conformance suite..." >&2
|
||||
for s in "${SUITES[@]}"; do
|
||||
read -r p f < <(run_suite "$s")
|
||||
SUITE_PASS[$s]=$p
|
||||
SUITE_FAIL[$s]=$f
|
||||
TOTAL_PASS=$((TOTAL_PASS + p))
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + f))
|
||||
printf " %-12s %d/%d\n" "$s" "$p" "$((p+f))" >&2
|
||||
done
|
||||
|
||||
# scoreboard.json
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "suites": {\n'
|
||||
first=1
|
||||
for s in "${SUITES[@]}"; do
|
||||
if [ $first -eq 0 ]; then printf ',\n'; fi
|
||||
printf ' "%s": {"pass": %d, "fail": %d}' "$s" "${SUITE_PASS[$s]}" "${SUITE_FAIL[$s]}"
|
||||
first=0
|
||||
done
|
||||
printf '\n },\n'
|
||||
printf ' "total_pass": %d,\n' "$TOTAL_PASS"
|
||||
printf ' "total_fail": %d,\n' "$TOTAL_FAIL"
|
||||
printf ' "total": %d\n' "$((TOTAL_PASS + TOTAL_FAIL))"
|
||||
printf '}\n'
|
||||
} > "$OUT_JSON"
|
||||
|
||||
# scoreboard.md
|
||||
{
|
||||
printf '# APL Conformance Scoreboard\n\n'
|
||||
printf '_Generated by `lib/apl/conformance.sh`_\n\n'
|
||||
printf '| Suite | Pass | Fail | Total |\n'
|
||||
printf '|-------|-----:|-----:|------:|\n'
|
||||
for s in "${SUITES[@]}"; do
|
||||
p=${SUITE_PASS[$s]}
|
||||
f=${SUITE_FAIL[$s]}
|
||||
printf '| %s | %d | %d | %d |\n' "$s" "$p" "$f" "$((p+f))"
|
||||
done
|
||||
printf '| **Total** | **%d** | **%d** | **%d** |\n' "$TOTAL_PASS" "$TOTAL_FAIL" "$((TOTAL_PASS + TOTAL_FAIL))"
|
||||
printf '\n'
|
||||
printf '## Notes\n\n'
|
||||
printf '%s\n' '- Suites use the standard `apl-test name got expected` framework loaded against `lib/apl/runtime.sx` + `lib/apl/transpile.sx`.'
|
||||
printf '%s\n' '- `lib/apl/tests/parse.sx` and `lib/apl/tests/scalar.sx` use their own self-contained frameworks and are excluded from this scoreboard.'
|
||||
} > "$OUT_MD"
|
||||
|
||||
echo "Wrote $OUT_JSON and $OUT_MD" >&2
|
||||
echo "Total: $TOTAL_PASS pass, $TOTAL_FAIL fail" >&2
|
||||
|
||||
[ "$TOTAL_FAIL" -eq 0 ]
|
||||
674
lib/apl/parser.sx
Normal file
674
lib/apl/parser.sx
Normal file
@@ -0,0 +1,674 @@
|
||||
; APL Parser — right-to-left expression parser
|
||||
;
|
||||
; Takes a token list (output of apl-tokenize) and produces an AST.
|
||||
; APL evaluates right-to-left with no precedence among functions.
|
||||
; Operators bind to the function immediately to their left in the source.
|
||||
;
|
||||
; AST node types:
|
||||
; (:num n) number literal
|
||||
; (:str s) string literal
|
||||
; (:vec n1 n2 ...) strand (juxtaposed literals)
|
||||
; (:name "x") name reference / alpha / omega
|
||||
; (:assign "x" expr) assignment x←expr
|
||||
; (:monad fn arg) monadic function call
|
||||
; (:dyad fn left right) dyadic function call
|
||||
; (:derived-fn op fn) derived function: f/ f¨ f⍨
|
||||
; (:derived-fn2 "." f g) inner product: f.g
|
||||
; (:outer "∘." fn) outer product: ∘.f
|
||||
; (:fn-glyph "⍳") function reference
|
||||
; (:fn-name "foo") named-function reference (dfn variable)
|
||||
; (:dfn stmt...) {⍺+⍵} anonymous function
|
||||
; (:guard cond expr) cond:expr guard inside dfn
|
||||
; (:program stmt...) multi-statement sequence
|
||||
|
||||
; ============================================================
|
||||
; Glyph classification sets
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
apl-parse-op-glyphs
|
||||
(list "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@"))
|
||||
|
||||
(define
|
||||
apl-parse-fn-glyphs
|
||||
(list
|
||||
"+"
|
||||
"-"
|
||||
"×"
|
||||
"÷"
|
||||
"*"
|
||||
"⍟"
|
||||
"⌈"
|
||||
"⌊"
|
||||
"|"
|
||||
"!"
|
||||
"?"
|
||||
"○"
|
||||
"~"
|
||||
"<"
|
||||
"≤"
|
||||
"="
|
||||
"≥"
|
||||
">"
|
||||
"≠"
|
||||
"≢"
|
||||
"≡"
|
||||
"∊"
|
||||
"∧"
|
||||
"∨"
|
||||
"⍱"
|
||||
"⍲"
|
||||
","
|
||||
"⍪"
|
||||
"⍴"
|
||||
"⌽"
|
||||
"⊖"
|
||||
"⍉"
|
||||
"↑"
|
||||
"↓"
|
||||
"⊂"
|
||||
"⊃"
|
||||
"⊆"
|
||||
"∪"
|
||||
"∩"
|
||||
"⍳"
|
||||
"⍸"
|
||||
"⌷"
|
||||
"⍋"
|
||||
"⍒"
|
||||
"⊥"
|
||||
"⊤"
|
||||
"⊣"
|
||||
"⊢"
|
||||
"⍎"
|
||||
"⍕"))
|
||||
|
||||
(define apl-quad-fn-names (list "⎕FMT" "⎕←"))
|
||||
|
||||
(define apl-known-fn-names (list))
|
||||
|
||||
; ============================================================
|
||||
; 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
|
||||
apl-parse-fn-glyph?
|
||||
(fn (v) (some (fn (g) (= g v)) apl-parse-fn-glyphs)))
|
||||
|
||||
(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
|
||||
is-op-tok?
|
||||
(fn
|
||||
(tok)
|
||||
(and (= (tok-type tok) :glyph) (apl-parse-op-glyph? (tok-val tok)))))
|
||||
|
||||
; ============================================================
|
||||
; Build a derived-fn node by chaining operators left-to-right
|
||||
; (+/¨ → (:derived-fn "¨" (:derived-fn "/" (:fn-glyph "+"))))
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
is-fn-tok?
|
||||
(fn
|
||||
(tok)
|
||||
(or
|
||||
(and (= (tok-type tok) :glyph) (apl-parse-fn-glyph? (tok-val tok)))
|
||||
(and
|
||||
(= (tok-type tok) :name)
|
||||
(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-loop
|
||||
(fn
|
||||
(tokens i acc)
|
||||
(if
|
||||
(>= i (len tokens))
|
||||
{:end i :ops acc}
|
||||
(let
|
||||
((tok (nth tokens i)))
|
||||
(if
|
||||
(is-op-tok? tok)
|
||||
(collect-ops-loop tokens (+ i 1) (append acc (tok-val tok)))
|
||||
{:end i :ops acc})))))
|
||||
|
||||
; ============================================================
|
||||
; Segment collection: scan tokens left-to-right, building
|
||||
; a list of {:kind "val"/"fn" :node ast} segments.
|
||||
; Operators following function glyphs are merged into
|
||||
; derived-fn nodes during this pass.
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
build-derived-fn
|
||||
(fn
|
||||
(fn-node ops)
|
||||
(if
|
||||
(= (len ops) 0)
|
||||
fn-node
|
||||
(build-derived-fn (list :derived-fn (first ops) fn-node) (rest ops)))))
|
||||
|
||||
(define
|
||||
find-matching-close
|
||||
(fn
|
||||
(tokens start open-type close-type)
|
||||
(find-matching-close-loop tokens start open-type close-type 1)))
|
||||
|
||||
; ============================================================
|
||||
; Build tree from segment list
|
||||
;
|
||||
; 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
|
||||
find-matching-close-loop
|
||||
(fn
|
||||
(tokens i open-type close-type depth)
|
||||
(if
|
||||
(>= i (len tokens))
|
||||
(len tokens)
|
||||
(let
|
||||
((tt (tok-type (nth tokens i))))
|
||||
(cond
|
||||
((= tt open-type)
|
||||
(find-matching-close-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
open-type
|
||||
close-type
|
||||
(+ depth 1)))
|
||||
((= tt close-type)
|
||||
(if
|
||||
(= depth 1)
|
||||
i
|
||||
(find-matching-close-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
open-type
|
||||
close-type
|
||||
(- depth 1))))
|
||||
(true
|
||||
(find-matching-close-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
open-type
|
||||
close-type
|
||||
depth)))))))
|
||||
|
||||
(define
|
||||
collect-segments
|
||||
(fn (tokens) (collect-segments-loop tokens 0 (list))))
|
||||
|
||||
; Build an array node from 0..n value segments
|
||||
; If n=1 → return that segment's node
|
||||
; If n>1 → return (:vec node1 node2 ...)
|
||||
(define
|
||||
collect-segments-loop
|
||||
(fn
|
||||
(tokens i acc)
|
||||
(if
|
||||
(>= i (len tokens))
|
||||
acc
|
||||
(let
|
||||
((tok (nth tokens i)) (n (len tokens)))
|
||||
(let
|
||||
((tt (tok-type tok)) (tv (tok-val tok)))
|
||||
(cond
|
||||
((or (= tt :diamond) (= tt :newline) (= tt :semi))
|
||||
(collect-segments-loop tokens (+ i 1) acc))
|
||||
((= tt :num)
|
||||
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :num tv)})))
|
||||
((= tt :str)
|
||||
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :str tv)})))
|
||||
((= tt :name)
|
||||
(cond
|
||||
((some (fn (q) (= q tv)) apl-quad-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-glyph tv) ops)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(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
|
||||
((br (maybe-bracket (list :name tv) tokens (+ i 1))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)}))))))
|
||||
((= tt :lparen)
|
||||
(let
|
||||
((end (find-matching-close tokens (+ i 1) :lparen :rparen)))
|
||||
(let
|
||||
((inner-tokens (slice tokens (+ i 1) end))
|
||||
(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
|
||||
((br (maybe-bracket (parse-apl-expr inner-tokens) tokens after)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)}))))))))
|
||||
((= tt :lbrace)
|
||||
(let
|
||||
((end (find-matching-close tokens (+ i 1) :lbrace :rbrace)))
|
||||
(let
|
||||
((inner-tokens (slice tokens (+ i 1) end))
|
||||
(after (+ end 1)))
|
||||
(collect-segments-loop tokens after (append acc {:kind "fn" :node (parse-dfn inner-tokens)})))))
|
||||
((= tt :glyph)
|
||||
(cond
|
||||
((or (= tv "⍺") (= tv "⍵"))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "val" :node (list :name tv)})))
|
||||
((= tv "∇")
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "fn" :node (list :fn-glyph "∇")})))
|
||||
((and (= tv "∘") (< (+ i 1) n) (= (tok-val (nth tokens (+ i 1))) "."))
|
||||
(if
|
||||
(and (< (+ i 2) n) (is-fn-tok? (nth tokens (+ i 2))))
|
||||
(let
|
||||
((fn-tv (tok-val (nth tokens (+ i 2)))))
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 3))))
|
||||
(let
|
||||
((ops (get op-result :ops))
|
||||
(ni (get op-result :end)))
|
||||
(let
|
||||
((fn-node (build-derived-fn (list :fn-glyph fn-tv) ops)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(append acc {:kind "fn" :node (list :outer "∘." fn-node)}))))))
|
||||
(collect-segments-loop tokens (+ i 1) acc)))
|
||||
((apl-parse-fn-glyph? tv)
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 1))))
|
||||
(let
|
||||
((ops (get op-result :ops))
|
||||
(ni (get op-result :end)))
|
||||
(if
|
||||
(and
|
||||
(= (len ops) 1)
|
||||
(= (first ops) ".")
|
||||
(< ni n)
|
||||
(is-fn-tok? (nth tokens ni)))
|
||||
(let
|
||||
((g-tv (tok-val (nth tokens ni))))
|
||||
(let
|
||||
((op-result2 (collect-ops tokens (+ ni 1))))
|
||||
(let
|
||||
((ops2 (get op-result2 :ops))
|
||||
(ni2 (get op-result2 :end)))
|
||||
(let
|
||||
((g-node (build-derived-fn (list :fn-glyph g-tv) ops2)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni2
|
||||
(append acc {:kind "fn" :node (list :derived-fn2 "." (list :fn-glyph tv) g-node)}))))))
|
||||
(let
|
||||
((fn-node (build-derived-fn (list :fn-glyph tv) ops)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node})))))))
|
||||
((apl-parse-op-glyph? tv)
|
||||
(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)))
|
||||
|
||||
|
||||
; ============================================================
|
||||
; Split token list on statement separators (diamond / newline)
|
||||
; Only splits at depth 0 (ignores separators inside { } or ( ) )
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
find-first-fn-loop
|
||||
(fn
|
||||
(segs i)
|
||||
(if
|
||||
(>= i (len segs))
|
||||
-1
|
||||
(if
|
||||
(= (get (nth segs i) :kind) "fn")
|
||||
i
|
||||
(find-first-fn-loop segs (+ i 1))))))
|
||||
|
||||
(define
|
||||
segs-to-array
|
||||
(fn
|
||||
(segs)
|
||||
(if
|
||||
(= (len segs) 1)
|
||||
(get (first segs) :node)
|
||||
(cons :vec (map (fn (s) (get s :node)) segs)))))
|
||||
|
||||
; ============================================================
|
||||
; Parse a dfn body (tokens between { and })
|
||||
; Handles guard expressions: cond : expr
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
build-tree
|
||||
(fn
|
||||
(segs)
|
||||
(cond
|
||||
((= (len segs) 0) nil)
|
||||
((= (len segs) 1) (get (first segs) :node))
|
||||
((every? (fn (s) (= (get s :kind) "val")) segs)
|
||||
(segs-to-array segs))
|
||||
(true
|
||||
(let
|
||||
((fn-idx (find-first-fn segs)))
|
||||
(cond
|
||||
((= fn-idx -1) (segs-to-array segs))
|
||||
((= fn-idx 0)
|
||||
(list
|
||||
:monad (get (first segs) :node)
|
||||
(build-tree (rest segs))))
|
||||
(true
|
||||
(let
|
||||
((left-segs (slice segs 0 fn-idx))
|
||||
(fn-seg (nth segs fn-idx))
|
||||
(right-segs (slice segs (+ fn-idx 1))))
|
||||
(list
|
||||
:dyad (get fn-seg :node)
|
||||
(segs-to-array left-segs)
|
||||
(build-tree right-segs))))))))))
|
||||
|
||||
(define
|
||||
split-statements
|
||||
(fn (tokens) (split-statements-loop tokens (list) (list) 0)))
|
||||
|
||||
(define
|
||||
split-statements-loop
|
||||
(fn
|
||||
(tokens current-stmt acc depth)
|
||||
(if
|
||||
(= (len tokens) 0)
|
||||
(if (> (len current-stmt) 0) (append acc (list current-stmt)) acc)
|
||||
(let
|
||||
((tok (first tokens))
|
||||
(rest-toks (rest tokens))
|
||||
(tt (tok-type (first tokens))))
|
||||
(cond
|
||||
((or (= tt :lparen) (= tt :lbrace) (= tt :lbracket))
|
||||
(split-statements-loop
|
||||
rest-toks
|
||||
(append current-stmt tok)
|
||||
acc
|
||||
(+ depth 1)))
|
||||
((or (= tt :rparen) (= tt :rbrace) (= tt :rbracket))
|
||||
(split-statements-loop
|
||||
rest-toks
|
||||
(append current-stmt tok)
|
||||
acc
|
||||
(- depth 1)))
|
||||
((and (> depth 0) (or (= tt :diamond) (= tt :newline)))
|
||||
(split-statements-loop
|
||||
rest-toks
|
||||
(append current-stmt tok)
|
||||
acc
|
||||
depth))
|
||||
((and (= depth 0) (or (= tt :diamond) (= tt :newline)))
|
||||
(if
|
||||
(> (len current-stmt) 0)
|
||||
(split-statements-loop
|
||||
rest-toks
|
||||
(list)
|
||||
(append acc (list current-stmt))
|
||||
depth)
|
||||
(split-statements-loop rest-toks (list) acc depth)))
|
||||
(true
|
||||
(split-statements-loop
|
||||
rest-toks
|
||||
(append current-stmt tok)
|
||||
acc
|
||||
depth)))))))
|
||||
|
||||
(define
|
||||
parse-dfn
|
||||
(fn
|
||||
(tokens)
|
||||
(let
|
||||
((stmt-groups (split-statements tokens)))
|
||||
(let ((stmts (map parse-dfn-stmt stmt-groups))) (cons :dfn stmts)))))
|
||||
|
||||
; ============================================================
|
||||
; Parse a single statement (assignment or expression)
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
parse-dfn-stmt
|
||||
(fn
|
||||
(tokens)
|
||||
(let
|
||||
((colon-idx (find-top-level-colon tokens 0)))
|
||||
(if
|
||||
(>= colon-idx 0)
|
||||
(let
|
||||
((cond-tokens (slice tokens 0 colon-idx))
|
||||
(body-tokens (slice tokens (+ colon-idx 1))))
|
||||
(list
|
||||
:guard (parse-apl-expr cond-tokens)
|
||||
(parse-apl-expr body-tokens)))
|
||||
(parse-stmt tokens)))))
|
||||
|
||||
; ============================================================
|
||||
; Parse an expression from a flat token list
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
find-top-level-colon
|
||||
(fn (tokens i) (find-top-level-colon-loop tokens i 0)))
|
||||
|
||||
; ============================================================
|
||||
; Main entry point
|
||||
; parse-apl: string → AST
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
find-top-level-colon-loop
|
||||
(fn
|
||||
(tokens i depth)
|
||||
(if
|
||||
(>= i (len tokens))
|
||||
-1
|
||||
(let
|
||||
((tok (nth tokens i)) (tt (tok-type (nth tokens i))))
|
||||
(cond
|
||||
((or (= tt :lparen) (= tt :lbrace) (= tt :lbracket))
|
||||
(find-top-level-colon-loop tokens (+ i 1) (+ depth 1)))
|
||||
((or (= tt :rparen) (= tt :rbrace) (= tt :rbracket))
|
||||
(find-top-level-colon-loop tokens (+ i 1) (- depth 1)))
|
||||
((and (= tt :colon) (= depth 0)) i)
|
||||
(true (find-top-level-colon-loop tokens (+ i 1) depth)))))))
|
||||
|
||||
(define
|
||||
parse-stmt
|
||||
(fn
|
||||
(tokens)
|
||||
(if
|
||||
(and
|
||||
(>= (len tokens) 2)
|
||||
(= (tok-type (nth tokens 0)) :name)
|
||||
(= (tok-type (nth tokens 1)) :assign))
|
||||
(list
|
||||
:assign (tok-val (nth tokens 0))
|
||||
(parse-apl-expr (slice tokens 2)))
|
||||
(parse-apl-expr tokens))))
|
||||
|
||||
(define
|
||||
parse-apl-expr
|
||||
(fn
|
||||
(tokens)
|
||||
(let
|
||||
((segs (collect-segments tokens)))
|
||||
(if (= (len segs) 0) nil (build-tree segs)))))
|
||||
|
||||
(define
|
||||
parse-apl
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((tokens (apl-tokenize src)))
|
||||
(let
|
||||
((stmt-groups (split-statements tokens)))
|
||||
(begin
|
||||
(apl-collect-fn-bindings stmt-groups)
|
||||
(if
|
||||
(= (len stmt-groups) 0)
|
||||
nil
|
||||
(if
|
||||
(= (len stmt-groups) 1)
|
||||
(parse-stmt (first 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
|
||||
maybe-bracket
|
||||
(fn
|
||||
(val-node tokens after)
|
||||
(if
|
||||
(and
|
||||
(< after (len tokens))
|
||||
(= (tok-type (nth tokens after)) :lbracket))
|
||||
(let
|
||||
((end (find-matching-close tokens (+ after 1) :lbracket :rbracket)))
|
||||
(let
|
||||
((inner-tokens (slice tokens (+ after 1) end))
|
||||
(next-after (+ end 1)))
|
||||
(let
|
||||
((sections (split-bracket-content inner-tokens)))
|
||||
(if
|
||||
(= (len sections) 1)
|
||||
(let
|
||||
((idx-expr (parse-apl-expr inner-tokens)))
|
||||
(let
|
||||
((indexed (list :dyad (list :fn-glyph "⌷") idx-expr val-node)))
|
||||
(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))))
|
||||
1568
lib/apl/runtime.sx
1568
lib/apl/runtime.sx
File diff suppressed because it is too large
Load Diff
17
lib/apl/scoreboard.json
Normal file
17
lib/apl/scoreboard.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"suites": {
|
||||
"structural": {"pass": 94, "fail": 0},
|
||||
"operators": {"pass": 117, "fail": 0},
|
||||
"dfn": {"pass": 24, "fail": 0},
|
||||
"tradfn": {"pass": 25, "fail": 0},
|
||||
"valence": {"pass": 14, "fail": 0},
|
||||
"programs": {"pass": 45, "fail": 0},
|
||||
"system": {"pass": 13, "fail": 0},
|
||||
"idioms": {"pass": 64, "fail": 0},
|
||||
"eval-ops": {"pass": 14, "fail": 0},
|
||||
"pipeline": {"pass": 40, "fail": 0}
|
||||
},
|
||||
"total_pass": 450,
|
||||
"total_fail": 0,
|
||||
"total": 450
|
||||
}
|
||||
22
lib/apl/scoreboard.md
Normal file
22
lib/apl/scoreboard.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# APL Conformance Scoreboard
|
||||
|
||||
_Generated by `lib/apl/conformance.sh`_
|
||||
|
||||
| Suite | Pass | Fail | Total |
|
||||
|-------|-----:|-----:|------:|
|
||||
| structural | 94 | 0 | 94 |
|
||||
| operators | 117 | 0 | 117 |
|
||||
| dfn | 24 | 0 | 24 |
|
||||
| tradfn | 25 | 0 | 25 |
|
||||
| valence | 14 | 0 | 14 |
|
||||
| programs | 45 | 0 | 45 |
|
||||
| system | 13 | 0 | 13 |
|
||||
| idioms | 64 | 0 | 64 |
|
||||
| eval-ops | 14 | 0 | 14 |
|
||||
| pipeline | 40 | 0 | 40 |
|
||||
| **Total** | **450** | **0** | **450** |
|
||||
|
||||
## Notes
|
||||
|
||||
- Suites use the standard `apl-test name got expected` framework loaded against `lib/apl/runtime.sx` + `lib/apl/transpile.sx`.
|
||||
- `lib/apl/tests/parse.sx` and `lib/apl/tests/scalar.sx` use their own self-contained frameworks and are excluded from this scoreboard.
|
||||
@@ -4,9 +4,9 @@
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
SX_SERVER="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
@@ -18,19 +18,38 @@ TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/r7rs.sx")
|
||||
(load "lib/apl/runtime.sx")
|
||||
(load "lib/apl/tokenizer.sx")
|
||||
(load "lib/apl/parser.sx")
|
||||
(load "lib/apl/transpile.sx")
|
||||
(epoch 2)
|
||||
(load "lib/apl/tests/runtime.sx")
|
||||
(eval "(define apl-test-pass 0)")
|
||||
(eval "(define apl-test-fail 0)")
|
||||
(eval "(define apl-test-fails (list))")
|
||||
(eval "(define apl-test (fn (name got expected) (if (= got expected) (set! apl-test-pass (+ apl-test-pass 1)) (begin (set! apl-test-fail (+ apl-test-fail 1)) (set! apl-test-fails (append apl-test-fails (list {:name name :got got :expected expected})))))))")
|
||||
(epoch 3)
|
||||
(load "lib/apl/tests/structural.sx")
|
||||
(load "lib/apl/tests/operators.sx")
|
||||
(load "lib/apl/tests/dfn.sx")
|
||||
(load "lib/apl/tests/tradfn.sx")
|
||||
(load "lib/apl/tests/valence.sx")
|
||||
(load "lib/apl/tests/programs.sx")
|
||||
(load "lib/apl/tests/system.sx")
|
||||
(load "lib/apl/tests/idioms.sx")
|
||||
(load "lib/apl/tests/eval-ops.sx")
|
||||
(load "lib/apl/tests/pipeline.sx")
|
||||
(load "lib/apl/tests/programs-e2e.sx")
|
||||
(epoch 4)
|
||||
(eval "(list apl-test-pass apl-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
OUTPUT=$(timeout 300 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 4 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 4 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 4 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
|
||||
227
lib/apl/tests/dfn.sx
Normal file
227
lib/apl/tests/dfn.sx
Normal file
@@ -0,0 +1,227 @@
|
||||
; Tests for apl-eval-ast and apl-call-dfn (manual AST construction).
|
||||
|
||||
(define rv (fn (arr) (get arr :ravel)))
|
||||
(define sh (fn (arr) (get arr :shape)))
|
||||
|
||||
(define mknum (fn (n) (list :num n)))
|
||||
(define mkname (fn (s) (list :name s)))
|
||||
(define mkfg (fn (g) (list :fn-glyph g)))
|
||||
(define mkmon (fn (g a) (list :monad (mkfg g) a)))
|
||||
(define mkdyd (fn (g l r) (list :dyad (mkfg g) l r)))
|
||||
(define mkdfn1 (fn (body) (list :dfn body)))
|
||||
(define mkprog (fn (stmts) (cons :program stmts)))
|
||||
|
||||
(define mkasg (fn (mkname expr) (list :assign mkname expr)))
|
||||
|
||||
(define mkgrd (fn (c e) (list :guard c e)))
|
||||
|
||||
(define mkdfn (fn (stmts) (cons :dfn stmts)))
|
||||
|
||||
(apl-test
|
||||
"eval :num literal"
|
||||
(rv (apl-eval-ast (mknum 42) {}))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"eval :num literal shape"
|
||||
(sh (apl-eval-ast (mknum 42) {}))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"eval :dyad +"
|
||||
(rv (apl-eval-ast (mkdyd "+" (mknum 2) (mknum 3)) {}))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"eval :dyad ×"
|
||||
(rv (apl-eval-ast (mkdyd "×" (mknum 6) (mknum 7)) {}))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"eval :monad - (negate)"
|
||||
(rv (apl-eval-ast (mkmon "-" (mknum 7)) {}))
|
||||
(list -7))
|
||||
|
||||
(apl-test
|
||||
"eval :monad ⌊ (floor)"
|
||||
(rv (apl-eval-ast (mkmon "⌊" (mknum 3)) {}))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"eval :name ⍵ from env"
|
||||
(rv (apl-eval-ast (mkname "⍵") {:omega (apl-scalar 99) :alpha nil}))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"eval :name ⍺ from env"
|
||||
(rv (apl-eval-ast (mkname "⍺") {:omega nil :alpha (apl-scalar 7)}))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"dfn {⍵+1} called monadic"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn1 (mkdyd "+" (mkname "⍵") (mknum 1)))
|
||||
(apl-scalar 5)))
|
||||
(list 6))
|
||||
|
||||
(apl-test
|
||||
"dfn {⍺+⍵} called dyadic"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn1 (mkdyd "+" (mkname "⍺") (mkname "⍵")))
|
||||
(apl-scalar 4)
|
||||
(apl-scalar 9)))
|
||||
(list 13))
|
||||
|
||||
(apl-test
|
||||
"dfn {⍺×⍵} dyadic on vectors"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn1 (mkdyd "×" (mkname "⍺") (mkname "⍵")))
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 10 40 90))
|
||||
|
||||
(apl-test
|
||||
"dfn {-⍵} monadic negate"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn1 (mkmon "-" (mkname "⍵")))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list -1 -2 -3))
|
||||
|
||||
(apl-test
|
||||
"dfn {⍺-⍵} dyadic subtract scalar"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn1 (mkdyd "-" (mkname "⍺") (mkname "⍵")))
|
||||
(apl-scalar 10)
|
||||
(apl-scalar 3)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"dfn {⌈⍺,⍵} not used (just verify : missing) — ceiling of right"
|
||||
(rv
|
||||
(apl-call-dfn-m (mkdfn1 (mkmon "⌈" (mkname "⍵"))) (apl-scalar 5)))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"dfn nested dyad"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn1
|
||||
(mkdyd "+" (mkname "⍺") (mkdyd "×" (mkname "⍵") (mknum 2))))
|
||||
(apl-scalar 1)
|
||||
(apl-scalar 3)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"dfn local assign x←⍵+1; ⍺×x"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn
|
||||
(list
|
||||
(mkasg "x" (mkdyd "+" (mkname "⍵") (mknum 1)))
|
||||
(mkdyd "×" (mkname "⍺") (mkname "x"))))
|
||||
(apl-scalar 3)
|
||||
(apl-scalar 4)))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"dfn guard: 0=⍵:99; ⍵×2 (true branch)"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkgrd (mkdyd "=" (mknum 0) (mkname "⍵")) (mknum 99))
|
||||
(mkdyd "×" (mkname "⍵") (mknum 2))))
|
||||
(apl-scalar 0)))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"dfn guard: 0=⍵:99; ⍵×2 (false branch)"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkgrd (mkdyd "=" (mknum 0) (mkname "⍵")) (mknum 99))
|
||||
(mkdyd "×" (mkname "⍵") (mknum 2))))
|
||||
(apl-scalar 5)))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"dfn default ⍺←10 used (monadic call)"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkasg "⍺" (mknum 10))
|
||||
(mkdyd "+" (mkname "⍺") (mkname "⍵"))))
|
||||
(apl-scalar 5)))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"dfn default ⍺←10 ignored when ⍺ given (dyadic call)"
|
||||
(rv
|
||||
(apl-call-dfn
|
||||
(mkdfn
|
||||
(list
|
||||
(mkasg "⍺" (mknum 10))
|
||||
(mkdyd "+" (mkname "⍺") (mkname "⍵"))))
|
||||
(apl-scalar 100)
|
||||
(apl-scalar 5)))
|
||||
(list 105))
|
||||
|
||||
(apl-test
|
||||
"dfn ∇ recursion: factorial via guard"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkgrd (mkdyd "=" (mknum 0) (mkname "⍵")) (mknum 1))
|
||||
(mkdyd
|
||||
"×"
|
||||
(mkname "⍵")
|
||||
(mkmon "∇" (mkdyd "-" (mkname "⍵") (mknum 1))))))
|
||||
(apl-scalar 5)))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"dfn ∇ recursion: 3 → 6 (factorial)"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkgrd (mkdyd "=" (mknum 0) (mkname "⍵")) (mknum 1))
|
||||
(mkdyd
|
||||
"×"
|
||||
(mkname "⍵")
|
||||
(mkmon "∇" (mkdyd "-" (mkname "⍵") (mknum 1))))))
|
||||
(apl-scalar 3)))
|
||||
(list 6))
|
||||
|
||||
(apl-test
|
||||
"dfn local: x←⍵+10; y←x×2; y"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkasg "x" (mkdyd "+" (mkname "⍵") (mknum 10)))
|
||||
(mkasg "y" (mkdyd "×" (mkname "x") (mknum 2)))
|
||||
(mkname "y")))
|
||||
(apl-scalar 5)))
|
||||
(list 30))
|
||||
|
||||
(apl-test
|
||||
"dfn first guard wins: many guards"
|
||||
(rv
|
||||
(apl-call-dfn-m
|
||||
(mkdfn
|
||||
(list
|
||||
(mkgrd (mkdyd "=" (mknum 1) (mkname "⍵")) (mknum 100))
|
||||
(mkgrd (mkdyd "=" (mknum 2) (mkname "⍵")) (mknum 200))
|
||||
(mkgrd (mkdyd "=" (mknum 3) (mkname "⍵")) (mknum 300))
|
||||
(mknum 0)))
|
||||
(apl-scalar 2)))
|
||||
(list 200))
|
||||
147
lib/apl/tests/eval-ops.sx
Normal file
147
lib/apl/tests/eval-ops.sx
Normal file
@@ -0,0 +1,147 @@
|
||||
; Tests for operator handling in apl-eval-ast (Phase 7).
|
||||
; Manual AST construction; verifies :derived-fn / :outer / :derived-fn2
|
||||
; route through apl-resolve-monadic / apl-resolve-dyadic correctly.
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
(define mknum (fn (n) (list :num n)))
|
||||
(define mkfg (fn (g) (list :fn-glyph g)))
|
||||
(define mkmon (fn (g a) (list :monad g a)))
|
||||
(define mkdyd (fn (g l r) (list :dyad g l r)))
|
||||
(define mkder (fn (op f) (list :derived-fn op f)))
|
||||
(define mkdr2 (fn (op f g) (list :derived-fn2 op f g)))
|
||||
(define mkout (fn (f) (list :outer "∘." f)))
|
||||
|
||||
; helper: literal vector AST via :vec (from list of values)
|
||||
(define mkvec (fn (xs) (cons :vec (map (fn (n) (mknum n)) xs))))
|
||||
|
||||
; ---------- monadic operators ----------
|
||||
|
||||
(apl-test
|
||||
"eval-ast +/ ⍳5 → 15"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "/" (mkfg "+")) (mkmon (mkfg "⍳") (mknum 5)))
|
||||
{}))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"eval-ast ×/ ⍳5 → 120"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "/" (mkfg "×")) (mkmon (mkfg "⍳") (mknum 5)))
|
||||
{}))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"eval-ast ⌈/ — max reduce"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "/" (mkfg "⌈")) (mkvec (list 3 1 4 1 5 9 2 6)))
|
||||
{}))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"eval-ast +\\ scan"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "\\" (mkfg "+")) (mkvec (list 1 2 3 4 5)))
|
||||
{}))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
(apl-test
|
||||
"eval-ast +⌿ first-axis reduce on vector"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "⌿" (mkfg "+")) (mkvec (list 1 2 3 4 5)))
|
||||
{}))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"eval-ast -¨ each-negate"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "¨" (mkfg "-")) (mkvec (list 1 2 3 4)))
|
||||
{}))
|
||||
(list -1 -2 -3 -4))
|
||||
|
||||
(apl-test
|
||||
"eval-ast +⍨ commute (double via x+x)"
|
||||
(mkrv
|
||||
(apl-eval-ast (mkmon (mkder "⍨" (mkfg "+")) (mknum 7)) {}))
|
||||
(list 14))
|
||||
|
||||
; ---------- dyadic operators ----------
|
||||
|
||||
(apl-test
|
||||
"eval-ast outer ∘.× — multiplication table"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkdyd
|
||||
(mkout (mkfg "×"))
|
||||
(mkvec (list 1 2 3))
|
||||
(mkvec (list 1 2 3)))
|
||||
{}))
|
||||
(list 1 2 3 2 4 6 3 6 9))
|
||||
|
||||
(apl-test
|
||||
"eval-ast outer ∘.× shape (3 3)"
|
||||
(mksh
|
||||
(apl-eval-ast
|
||||
(mkdyd
|
||||
(mkout (mkfg "×"))
|
||||
(mkvec (list 1 2 3))
|
||||
(mkvec (list 1 2 3)))
|
||||
{}))
|
||||
(list 3 3))
|
||||
|
||||
(apl-test
|
||||
"eval-ast inner +.× — dot product"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkdyd
|
||||
(mkdr2 "." (mkfg "+") (mkfg "×"))
|
||||
(mkvec (list 1 2 3))
|
||||
(mkvec (list 4 5 6)))
|
||||
{}))
|
||||
(list 32))
|
||||
|
||||
(apl-test
|
||||
"eval-ast inner ∧.= equal vectors"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkdyd
|
||||
(mkdr2 "." (mkfg "∧") (mkfg "="))
|
||||
(mkvec (list 1 2 3))
|
||||
(mkvec (list 1 2 3)))
|
||||
{}))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"eval-ast each-dyadic +¨"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkdyd
|
||||
(mkder "¨" (mkfg "+"))
|
||||
(mkvec (list 1 2 3))
|
||||
(mkvec (list 10 20 30)))
|
||||
{}))
|
||||
(list 11 22 33))
|
||||
|
||||
(apl-test
|
||||
"eval-ast commute -⍨ (subtract swapped)"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkdyd (mkder "⍨" (mkfg "-")) (mknum 5) (mknum 3))
|
||||
{}))
|
||||
(list -2))
|
||||
|
||||
; ---------- nested operators ----------
|
||||
|
||||
(apl-test
|
||||
"eval-ast +/¨ — sum of each"
|
||||
(mkrv
|
||||
(apl-eval-ast
|
||||
(mkmon (mkder "/" (mkfg "+")) (mkvec (list 10 20 30)))
|
||||
{}))
|
||||
(list 60))
|
||||
359
lib/apl/tests/idioms.sx
Normal file
359
lib/apl/tests/idioms.sx
Normal file
@@ -0,0 +1,359 @@
|
||||
; APL idiom corpus — classic Roger Hui / Phil Last idioms expressed
|
||||
; through our runtime primitives. Each test names the APL one-liner
|
||||
; and verifies the equivalent runtime call.
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
|
||||
; ---------- reductions ----------
|
||||
|
||||
(apl-test
|
||||
"+/⍵ — sum"
|
||||
(mkrv (apl-reduce apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"(+/⍵)÷⍴⍵ — mean"
|
||||
(mkrv
|
||||
(apl-div
|
||||
(apl-reduce apl-add (make-array (list 5) (list 1 2 3 4 5)))
|
||||
(apl-scalar 5)))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"⌈/⍵ — max"
|
||||
(mkrv (apl-reduce apl-max (make-array (list 6) (list 3 1 4 1 5 9))))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"⌊/⍵ — min"
|
||||
(mkrv (apl-reduce apl-min (make-array (list 6) (list 3 1 4 1 5 9))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"(⌈/⍵)-⌊/⍵ — range"
|
||||
(mkrv
|
||||
(apl-sub
|
||||
(apl-reduce apl-max (make-array (list 6) (list 3 1 4 1 5 9)))
|
||||
(apl-reduce apl-min (make-array (list 6) (list 3 1 4 1 5 9)))))
|
||||
(list 8))
|
||||
|
||||
(apl-test
|
||||
"×/⍵ — product"
|
||||
(mkrv (apl-reduce apl-mul (make-array (list 4) (list 1 2 3 4))))
|
||||
(list 24))
|
||||
|
||||
(apl-test
|
||||
"+\\⍵ — running sum"
|
||||
(mkrv (apl-scan apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
; ---------- sort / order ----------
|
||||
|
||||
(apl-test
|
||||
"⍵[⍋⍵] — sort ascending"
|
||||
(mkrv (apl-quicksort (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 1 1 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"⌽⍵ — reverse"
|
||||
(mkrv (apl-reverse (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 5 4 3 2 1))
|
||||
|
||||
(apl-test
|
||||
"⊃⌽⍵ — last element"
|
||||
(mkrv
|
||||
(apl-disclose (apl-reverse (make-array (list 4) (list 10 20 30 40)))))
|
||||
(list 40))
|
||||
|
||||
(apl-test
|
||||
"1↑⍵ — first element"
|
||||
(mkrv
|
||||
(apl-take (apl-scalar 1) (make-array (list 4) (list 10 20 30 40))))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"1↓⍵ — drop first"
|
||||
(mkrv
|
||||
(apl-drop (apl-scalar 1) (make-array (list 4) (list 10 20 30 40))))
|
||||
(list 20 30 40))
|
||||
|
||||
(apl-test
|
||||
"¯1↓⍵ — drop last"
|
||||
(mkrv
|
||||
(apl-drop (apl-scalar -1) (make-array (list 4) (list 10 20 30 40))))
|
||||
(list 10 20 30))
|
||||
|
||||
; ---------- counts / membership ----------
|
||||
|
||||
(apl-test
|
||||
"≢⍵ — tally"
|
||||
(mkrv (apl-tally (make-array (list 7) (list 9 8 7 6 5 4 3))))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"+/⍵=v — count occurrences of v"
|
||||
(mkrv
|
||||
(apl-reduce
|
||||
apl-add
|
||||
(apl-eq (make-array (list 7) (list 1 2 3 2 1 3 2)) (apl-scalar 2))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"0=N|M — divisibility test"
|
||||
(mkrv (apl-eq (apl-scalar 0) (apl-mod (apl-scalar 3) (apl-scalar 12))))
|
||||
(list 1))
|
||||
|
||||
; ---------- shape constructors ----------
|
||||
|
||||
(apl-test
|
||||
"N⍴1 — vector of N ones"
|
||||
(mkrv (apl-reshape (apl-scalar 5) (apl-scalar 1)))
|
||||
(list 1 1 1 1 1))
|
||||
|
||||
(apl-test
|
||||
"(N N)⍴0 — N×N zero matrix"
|
||||
(mkrv (apl-reshape (make-array (list 2) (list 3 3)) (apl-scalar 0)))
|
||||
(list 0 0 0 0 0 0 0 0 0))
|
||||
|
||||
(apl-test
|
||||
"⍳∘.=⍳ — N×N identity matrix"
|
||||
(mkrv
|
||||
(apl-outer apl-eq (apl-iota (apl-scalar 3)) (apl-iota (apl-scalar 3))))
|
||||
(list 1 0 0 0 1 0 0 0 1))
|
||||
|
||||
(apl-test
|
||||
"⍳∘.×⍳ — multiplication table"
|
||||
(mkrv
|
||||
(apl-outer apl-mul (apl-iota (apl-scalar 3)) (apl-iota (apl-scalar 3))))
|
||||
(list 1 2 3 2 4 6 3 6 9))
|
||||
|
||||
; ---------- numerical idioms ----------
|
||||
|
||||
(apl-test
|
||||
"+\\⍳N — triangular numbers"
|
||||
(mkrv (apl-scan apl-add (apl-iota (apl-scalar 5))))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
(apl-test
|
||||
"+/⍳N=N×(N+1)÷2 — sum of 1..N"
|
||||
(mkrv (apl-reduce apl-add (apl-iota (apl-scalar 10))))
|
||||
(list 55))
|
||||
|
||||
(apl-test
|
||||
"×/⍳N — factorial via iota"
|
||||
(mkrv (apl-reduce apl-mul (apl-iota (apl-scalar 5))))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"2|⍵ — parity (1=odd)"
|
||||
(mkrv (apl-mod (apl-scalar 2) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 0 1 0 1))
|
||||
|
||||
(apl-test
|
||||
"+/2|⍵ — count odd"
|
||||
(mkrv
|
||||
(apl-reduce
|
||||
apl-add
|
||||
(apl-mod (apl-scalar 2) (make-array (list 5) (list 1 2 3 4 5)))))
|
||||
(list 3))
|
||||
|
||||
; ---------- boolean idioms ----------
|
||||
|
||||
(apl-test
|
||||
"∧/⍵ — all-true"
|
||||
(mkrv (apl-reduce apl-and (make-array (list 4) (list 1 1 1 1))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"∧/⍵ — all-true with zero is false"
|
||||
(mkrv (apl-reduce apl-and (make-array (list 4) (list 1 1 0 1))))
|
||||
(list 0))
|
||||
|
||||
(apl-test
|
||||
"∨/⍵ — any-true"
|
||||
(mkrv (apl-reduce apl-or (make-array (list 4) (list 0 0 1 0))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"∨/⍵ — any-true all zero is false"
|
||||
(mkrv (apl-reduce apl-or (make-array (list 4) (list 0 0 0 0))))
|
||||
(list 0))
|
||||
|
||||
; ---------- selection / scaling ----------
|
||||
|
||||
(apl-test
|
||||
"⍵×⍵ — square each"
|
||||
(mkrv
|
||||
(apl-mul
|
||||
(make-array (list 4) (list 1 2 3 4))
|
||||
(make-array (list 4) (list 1 2 3 4))))
|
||||
(list 1 4 9 16))
|
||||
|
||||
(apl-test
|
||||
"+/⍵×⍵ — sum of squares"
|
||||
(mkrv
|
||||
(apl-reduce
|
||||
apl-add
|
||||
(apl-mul
|
||||
(make-array (list 4) (list 1 2 3 4))
|
||||
(make-array (list 4) (list 1 2 3 4)))))
|
||||
(list 30))
|
||||
|
||||
(apl-test
|
||||
"⍵-(+/⍵)÷⍴⍵ — mean-centered"
|
||||
(mkrv
|
||||
(apl-sub
|
||||
(make-array (list 5) (list 2 4 6 8 10))
|
||||
(apl-div
|
||||
(apl-reduce apl-add (make-array (list 5) (list 2 4 6 8 10)))
|
||||
(apl-scalar 5))))
|
||||
(list -4 -2 0 2 4))
|
||||
|
||||
; ---------- shape / structure ----------
|
||||
|
||||
(apl-test
|
||||
",⍵ — ravel"
|
||||
(mkrv (apl-ravel (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"⍴⍴⍵ — rank"
|
||||
(mkrv
|
||||
(apl-shape (apl-shape (make-array (list 2 3) (list 1 2 3 4 5 6)))))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"src: +/⍳N → triangular(N)"
|
||||
(mkrv (apl-run "+/⍳100"))
|
||||
(list 5050))
|
||||
|
||||
(apl-test "src: ×/⍳N → N!" (mkrv (apl-run "×/⍳6")) (list 720))
|
||||
|
||||
(apl-test
|
||||
"src: ⌈/V — max"
|
||||
(mkrv (apl-run "⌈/3 1 4 1 5 9 2 6"))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"src: ⌊/V — min"
|
||||
(mkrv (apl-run "⌊/3 1 4 1 5 9 2 6"))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"src: range = (⌈/V) - ⌊/V"
|
||||
(mkrv (apl-run "(⌈/3 1 4 1 5 9 2 6) - ⌊/3 1 4 1 5 9 2 6"))
|
||||
(list 8))
|
||||
|
||||
(apl-test
|
||||
"src: +\\V — running sum"
|
||||
(mkrv (apl-run "+\\1 2 3 4 5"))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
(apl-test
|
||||
"src: ×\\V — running product"
|
||||
(mkrv (apl-run "×\\1 2 3 4 5"))
|
||||
(list 1 2 6 24 120))
|
||||
|
||||
(apl-test
|
||||
"src: V × V — squares"
|
||||
(mkrv (apl-run "(⍳5) × ⍳5"))
|
||||
(list 1 4 9 16 25))
|
||||
|
||||
(apl-test
|
||||
"src: +/V × V — sum of squares"
|
||||
(mkrv (apl-run "+/(⍳5) × ⍳5"))
|
||||
(list 55))
|
||||
|
||||
(apl-test "src: ∧/V — all-true" (mkrv (apl-run "∧/1 1 1 1")) (list 1))
|
||||
|
||||
(apl-test "src: ∨/V — any-true" (mkrv (apl-run "∨/0 0 1 0")) (list 1))
|
||||
|
||||
(apl-test "src: 0 = N|M — divides" (mkrv (apl-run "0 = 3 | 12")) (list 1))
|
||||
|
||||
(apl-test
|
||||
"src: 2 | V — parity"
|
||||
(mkrv (apl-run "2 | 1 2 3 4 5 6"))
|
||||
(list 1 0 1 0 1 0))
|
||||
|
||||
(apl-test
|
||||
"src: +/2|V — count odd"
|
||||
(mkrv (apl-run "+/2 | 1 2 3 4 5 6"))
|
||||
(list 3))
|
||||
|
||||
(apl-test "src: ⍴ V" (mkrv (apl-run "⍴ 1 2 3 4 5")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"src: ⍴⍴ M — rank"
|
||||
(mkrv (apl-run "⍴ ⍴ (2 3) ⍴ ⍳6"))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"src: N⍴1 — vector of ones"
|
||||
(mkrv (apl-run "5 ⍴ 1"))
|
||||
(list 1 1 1 1 1))
|
||||
|
||||
(apl-test
|
||||
"src: ⍳N ∘.= ⍳N — identity matrix"
|
||||
(mkrv (apl-run "(⍳3) ∘.= ⍳3"))
|
||||
(list 1 0 0 0 1 0 0 0 1))
|
||||
|
||||
(apl-test
|
||||
"src: ⍳N ∘.× ⍳N — multiplication table"
|
||||
(mkrv (apl-run "(⍳3) ∘.× ⍳3"))
|
||||
(list 1 2 3 2 4 6 3 6 9))
|
||||
|
||||
(apl-test
|
||||
"src: V +.× V — dot product"
|
||||
(mkrv (apl-run "1 2 3 +.× 4 5 6"))
|
||||
(list 32))
|
||||
|
||||
(apl-test
|
||||
"src: ∧.= V — vectors equal?"
|
||||
(mkrv (apl-run "1 2 3 ∧.= 1 2 3"))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"src: V[1] — first element"
|
||||
(mkrv (apl-run "(10 20 30 40)[1]"))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"src: 1↑V — first via take"
|
||||
(mkrv (apl-run "1 ↑ 10 20 30 40"))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"src: 1↓V — drop first"
|
||||
(mkrv (apl-run "1 ↓ 10 20 30 40"))
|
||||
(list 20 30 40))
|
||||
|
||||
(apl-test
|
||||
"src: ¯1↓V — drop last"
|
||||
(mkrv (apl-run "¯1 ↓ 10 20 30 40"))
|
||||
(list 10 20 30))
|
||||
|
||||
(apl-test
|
||||
"src: ⌽V — reverse"
|
||||
(mkrv (apl-run "⌽ 1 2 3 4 5"))
|
||||
(list 5 4 3 2 1))
|
||||
|
||||
(apl-test
|
||||
"src: ≢V — tally"
|
||||
(mkrv (apl-run "≢ 9 8 7 6 5 4 3 2 1"))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"src: ,M — ravel"
|
||||
(mkrv (apl-run ", (2 3) ⍴ ⍳6"))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"src: A=V — count occurrences"
|
||||
(mkrv (apl-run "+/2 = 1 2 3 2 1 3 2"))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"src: ⌈/(V × V) — max squared"
|
||||
(mkrv (apl-run "⌈/(1 2 3 4 5) × 1 2 3 4 5"))
|
||||
(list 25))
|
||||
791
lib/apl/tests/operators.sx
Normal file
791
lib/apl/tests/operators.sx
Normal file
@@ -0,0 +1,791 @@
|
||||
(define rv (fn (arr) (get arr :ravel)))
|
||||
(define sh (fn (arr) (get arr :shape)))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ vector"
|
||||
(rv (apl-reduce apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"reduce x/ vector"
|
||||
(rv (apl-reduce apl-mul (make-array (list 4) (list 1 2 3 4))))
|
||||
(list 24))
|
||||
|
||||
(apl-test
|
||||
"reduce max/ vector"
|
||||
(rv (apl-reduce apl-max (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"reduce min/ vector"
|
||||
(rv (apl-reduce apl-min (make-array (list 3) (list 3 1 4))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"reduce and/ all true"
|
||||
(rv (apl-reduce apl-and (make-array (list 3) (list 1 1 1))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"reduce or/ with true"
|
||||
(rv (apl-reduce apl-or (make-array (list 3) (list 0 0 1))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ single element"
|
||||
(rv (apl-reduce apl-add (make-array (list 1) (list 42))))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ scalar no-op"
|
||||
(rv (apl-reduce apl-add (apl-scalar 7)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ shape is scalar"
|
||||
(sh (apl-reduce apl-add (make-array (list 4) (list 1 2 3 4))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ matrix row sums shape"
|
||||
(sh (apl-reduce apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"reduce +/ matrix row sums values"
|
||||
(rv (apl-reduce apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 6 15))
|
||||
|
||||
(apl-test
|
||||
"reduce max/ matrix row maxima"
|
||||
(rv (apl-reduce apl-max (make-array (list 2 3) (list 3 1 4 1 5 9))))
|
||||
(list 4 9))
|
||||
|
||||
(apl-test
|
||||
"reduce-first +/ vector same as reduce"
|
||||
(rv (apl-reduce-first apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"reduce-first +/ matrix col sums shape"
|
||||
(sh
|
||||
(apl-reduce-first apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"reduce-first +/ matrix col sums values"
|
||||
(rv
|
||||
(apl-reduce-first apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 5 7 9))
|
||||
|
||||
(apl-test
|
||||
"reduce-first max/ matrix col maxima"
|
||||
(rv
|
||||
(apl-reduce-first apl-max (make-array (list 3 2) (list 1 9 2 8 3 7))))
|
||||
(list 3 9))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ vector"
|
||||
(rv (apl-scan apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
(apl-test
|
||||
"scan x\\ vector cumulative product"
|
||||
(rv (apl-scan apl-mul (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 6 24 120))
|
||||
|
||||
(apl-test
|
||||
"scan max\\ vector running max"
|
||||
(rv (apl-scan apl-max (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 3 3 4 4 5))
|
||||
|
||||
(apl-test
|
||||
"scan min\\ vector running min"
|
||||
(rv (apl-scan apl-min (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 3 1 1 1 1))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ single element"
|
||||
(rv (apl-scan apl-add (make-array (list 1) (list 42))))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ scalar no-op"
|
||||
(rv (apl-scan apl-add (apl-scalar 7)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ vector preserves shape"
|
||||
(sh (apl-scan apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ matrix preserves shape"
|
||||
(sh (apl-scan apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"scan +\\ matrix row-wise"
|
||||
(rv (apl-scan apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 3 6 4 9 15))
|
||||
|
||||
(apl-test
|
||||
"scan max\\ matrix row-wise running max"
|
||||
(rv (apl-scan apl-max (make-array (list 2 3) (list 3 1 4 1 5 9))))
|
||||
(list 3 3 4 1 5 9))
|
||||
|
||||
(apl-test
|
||||
"scan-first +\\ vector same as scan"
|
||||
(rv (apl-scan-first apl-add (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
(apl-test
|
||||
"scan-first +\\ scalar no-op"
|
||||
(rv (apl-scan-first apl-add (apl-scalar 9)))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"scan-first +\\ matrix preserves shape"
|
||||
(sh (apl-scan-first apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"scan-first +\\ matrix col-wise"
|
||||
(rv (apl-scan-first apl-add (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3 5 7 9))
|
||||
|
||||
(apl-test
|
||||
"scan-first max\\ matrix col-wise running max"
|
||||
(rv (apl-scan-first apl-max (make-array (list 3 2) (list 3 1 4 1 5 9))))
|
||||
(list 3 1 4 1 5 9))
|
||||
|
||||
(apl-test
|
||||
"each negate vector"
|
||||
(rv (apl-each apl-neg-m (make-array (list 3) (list 1 2 3))))
|
||||
(list -1 -2 -3))
|
||||
|
||||
(apl-test
|
||||
"each negate vector preserves shape"
|
||||
(sh (apl-each apl-neg-m (make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"each reciprocal vector"
|
||||
(rv (apl-each apl-recip (make-array (list 3) (list 1 2 4))))
|
||||
(list 1 (/ 1 2) (/ 1 4)))
|
||||
|
||||
(apl-test
|
||||
"each abs vector"
|
||||
(rv (apl-each apl-abs (make-array (list 4) (list -1 2 -3 4))))
|
||||
(list 1 2 3 4))
|
||||
|
||||
(apl-test "each scalar" (rv (apl-each apl-neg-m (apl-scalar 5))) (list -5))
|
||||
|
||||
(apl-test
|
||||
"each scalar shape"
|
||||
(sh (apl-each apl-neg-m (apl-scalar 5)))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"each negate matrix shape"
|
||||
(sh (apl-each apl-neg-m (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"each negate matrix values"
|
||||
(rv (apl-each apl-neg-m (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list -1 -2 -3 -4 -5 -6))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic scalar+scalar"
|
||||
(rv (apl-each-dyadic apl-add (apl-scalar 3) (apl-scalar 4)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic scalar+vector"
|
||||
(rv
|
||||
(apl-each-dyadic
|
||||
apl-add
|
||||
(apl-scalar 10)
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 11 12 13))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic vector+scalar"
|
||||
(rv
|
||||
(apl-each-dyadic
|
||||
apl-add
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(apl-scalar 10)))
|
||||
(list 11 12 13))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic vector+vector"
|
||||
(rv
|
||||
(apl-each-dyadic
|
||||
apl-add
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 11 22 33))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic mul matrix+matrix shape"
|
||||
(sh
|
||||
(apl-each-dyadic
|
||||
apl-mul
|
||||
(make-array (list 2 2) (list 1 2 3 4))
|
||||
(make-array (list 2 2) (list 5 6 7 8))))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"each-dyadic mul matrix+matrix values"
|
||||
(rv
|
||||
(apl-each-dyadic
|
||||
apl-mul
|
||||
(make-array (list 2 2) (list 1 2 3 4))
|
||||
(make-array (list 2 2) (list 5 6 7 8))))
|
||||
(list 5 12 21 32))
|
||||
|
||||
(apl-test
|
||||
"outer product mult table values"
|
||||
(rv
|
||||
(apl-outer
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 1 2 3 2 4 6 3 6 9))
|
||||
|
||||
(apl-test
|
||||
"outer product mult table shape"
|
||||
(sh
|
||||
(apl-outer
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 3 3))
|
||||
|
||||
(apl-test
|
||||
"outer product add table values"
|
||||
(rv
|
||||
(apl-outer
|
||||
apl-add
|
||||
(make-array (list 2) (list 1 2))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 11 21 31 12 22 32))
|
||||
|
||||
(apl-test
|
||||
"outer product add table shape"
|
||||
(sh
|
||||
(apl-outer
|
||||
apl-add
|
||||
(make-array (list 2) (list 1 2))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"outer product scalar+vector shape"
|
||||
(sh
|
||||
(apl-outer apl-mul (apl-scalar 5) (make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"outer product scalar+vector values"
|
||||
(rv
|
||||
(apl-outer apl-mul (apl-scalar 5) (make-array (list 3) (list 1 2 3))))
|
||||
(list 5 10 15))
|
||||
|
||||
(apl-test
|
||||
"outer product vector+scalar shape"
|
||||
(sh
|
||||
(apl-outer apl-mul (make-array (list 3) (list 1 2 3)) (apl-scalar 10)))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"outer product scalar+scalar"
|
||||
(rv (apl-outer apl-mul (apl-scalar 6) (apl-scalar 7)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"outer product scalar+scalar shape"
|
||||
(sh (apl-outer apl-mul (apl-scalar 6) (apl-scalar 7)))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"outer product equality identity matrix values"
|
||||
(rv
|
||||
(apl-outer
|
||||
apl-eq
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 1 0 0 0 1 0 0 0 1))
|
||||
|
||||
(apl-test
|
||||
"outer product matrix+vector rank doubling shape"
|
||||
(sh
|
||||
(apl-outer
|
||||
apl-add
|
||||
(make-array (list 2 2) (list 1 2 3 4))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 2 2 3))
|
||||
|
||||
(apl-test
|
||||
"outer product matrix+vector rank doubling values"
|
||||
(rv
|
||||
(apl-outer
|
||||
apl-add
|
||||
(make-array (list 2 2) (list 1 2 3 4))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 11 21 31 12 22 32 13 23 33 14 24 34))
|
||||
|
||||
(apl-test
|
||||
"inner +.× dot product"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 4 5 6))))
|
||||
(list 32))
|
||||
|
||||
(apl-test
|
||||
"inner +.× dot product shape is scalar"
|
||||
(sh
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 4 5 6))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"inner +.× matrix multiply 2x3 * 3x2 shape"
|
||||
(sh
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3 2) (list 7 8 9 10 11 12))))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"inner +.× matrix multiply 2x3 * 3x2 values"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3 2) (list 7 8 9 10 11 12))))
|
||||
(list 58 64 139 154))
|
||||
|
||||
(apl-test
|
||||
"inner +.× identity matrix 2x2"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 2 2) (list 1 0 0 1))
|
||||
(make-array (list 2 2) (list 5 6 7 8))))
|
||||
(list 5 6 7 8))
|
||||
|
||||
(apl-test
|
||||
"inner ∧.= equal vectors"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-and
|
||||
apl-eq
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"inner ∧.= unequal vectors"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-and
|
||||
apl-eq
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 9 3))))
|
||||
(list 0))
|
||||
|
||||
(apl-test
|
||||
"inner +.× matrix * vector shape"
|
||||
(sh
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3) (list 7 8 9))))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"inner +.× matrix * vector values"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3) (list 7 8 9))))
|
||||
(list 50 122))
|
||||
|
||||
(apl-test
|
||||
"inner +.× vector * matrix shape"
|
||||
(sh
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3 2) (list 4 5 6 7 8 9))))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"inner +.× vector * matrix values"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3 2) (list 4 5 6 7 8 9))))
|
||||
(list 40 46))
|
||||
|
||||
(apl-test
|
||||
"inner +.× single-element vectors"
|
||||
(rv
|
||||
(apl-inner
|
||||
apl-add
|
||||
apl-mul
|
||||
(make-array (list 1) (list 6))
|
||||
(make-array (list 1) (list 7))))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"commute +⍨ scalar doubles"
|
||||
(rv (apl-commute apl-add (apl-scalar 5)))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"commute ×⍨ vector squares"
|
||||
(rv (apl-commute apl-mul (make-array (list 4) (list 1 2 3 4))))
|
||||
(list 1 4 9 16))
|
||||
|
||||
(apl-test
|
||||
"commute +⍨ vector doubles"
|
||||
(rv (apl-commute apl-add (make-array (list 3) (list 1 2 3))))
|
||||
(list 2 4 6))
|
||||
|
||||
(apl-test
|
||||
"commute +⍨ shape preserved"
|
||||
(sh (apl-commute apl-add (make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"commute ×⍨ matrix shape preserved"
|
||||
(sh (apl-commute apl-mul (make-array (list 2 2) (list 1 2 3 4))))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"commute-dyadic -⍨ swaps subtraction"
|
||||
(rv (apl-commute-dyadic apl-sub (apl-scalar 5) (apl-scalar 3)))
|
||||
(list -2))
|
||||
|
||||
(apl-test
|
||||
"commute-dyadic ÷⍨ swaps division"
|
||||
(rv (apl-commute-dyadic apl-div (apl-scalar 4) (apl-scalar 12)))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"commute-dyadic -⍨ on vectors"
|
||||
(rv
|
||||
(apl-commute-dyadic
|
||||
apl-sub
|
||||
(make-array (list 3) (list 10 20 30))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list -9 -18 -27))
|
||||
|
||||
(apl-test
|
||||
"commute-dyadic +⍨ commutative same result"
|
||||
(rv
|
||||
(apl-commute-dyadic
|
||||
apl-add
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 11 22 33))
|
||||
|
||||
(apl-test
|
||||
"commute-dyadic ×⍨ commutative same result"
|
||||
(rv
|
||||
(apl-commute-dyadic
|
||||
apl-mul
|
||||
(make-array (list 3) (list 2 3 4))
|
||||
(make-array (list 3) (list 5 6 7))))
|
||||
(list 10 18 28))
|
||||
|
||||
(apl-test
|
||||
"compose -∘| scalar (negative abs)"
|
||||
(rv (apl-compose apl-neg-m apl-abs (apl-scalar -7)))
|
||||
(list -7))
|
||||
|
||||
(apl-test
|
||||
"compose -∘| vector"
|
||||
(rv
|
||||
(apl-compose apl-neg-m apl-abs (make-array (list 4) (list -1 2 -3 4))))
|
||||
(list -1 -2 -3 -4))
|
||||
|
||||
(apl-test
|
||||
"compose ⌊∘- (floor of negate)"
|
||||
(rv (apl-compose apl-floor apl-neg-m (make-array (list 3) (list 1 2 3))))
|
||||
(list -1 -2 -3))
|
||||
|
||||
(apl-test
|
||||
"compose -∘| matrix shape preserved"
|
||||
(sh
|
||||
(apl-compose apl-neg-m apl-abs (make-array (list 2 2) (list -1 2 -3 4))))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"compose-dyadic +∘- equals subtract scalar"
|
||||
(rv (apl-compose-dyadic apl-add apl-neg-m (apl-scalar 10) (apl-scalar 3)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"compose-dyadic +∘- equals subtract vector"
|
||||
(rv
|
||||
(apl-compose-dyadic
|
||||
apl-add
|
||||
apl-neg-m
|
||||
(make-array (list 3) (list 10 20 30))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 9 18 27))
|
||||
|
||||
(apl-test
|
||||
"compose-dyadic -∘| (subtract abs)"
|
||||
(rv (apl-compose-dyadic apl-sub apl-abs (apl-scalar 10) (apl-scalar -3)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"compose-dyadic ×∘- (multiply by negative)"
|
||||
(rv
|
||||
(apl-compose-dyadic
|
||||
apl-mul
|
||||
apl-neg-m
|
||||
(make-array (list 3) (list 2 3 4))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list -2 -6 -12))
|
||||
|
||||
(apl-test
|
||||
"compose-dyadic shape preserved"
|
||||
(sh
|
||||
(apl-compose-dyadic
|
||||
apl-add
|
||||
apl-neg-m
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 2 3) (list 1 1 1 1 1 1))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"power n=0 identity"
|
||||
(rv (apl-power (fn (a) (apl-add a (apl-scalar 1))) 0 (apl-scalar 5)))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"power increment by 3"
|
||||
(rv (apl-power (fn (a) (apl-add a (apl-scalar 1))) 3 (apl-scalar 0)))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"power double 4 times = 16"
|
||||
(rv (apl-power (fn (a) (apl-mul a (apl-scalar 2))) 4 (apl-scalar 1)))
|
||||
(list 16))
|
||||
|
||||
(apl-test
|
||||
"power on vector +5"
|
||||
(rv
|
||||
(apl-power
|
||||
(fn (a) (apl-add a (apl-scalar 1)))
|
||||
5
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 6 7 8))
|
||||
|
||||
(apl-test
|
||||
"power on vector preserves shape"
|
||||
(sh
|
||||
(apl-power
|
||||
(fn (a) (apl-add a (apl-scalar 1)))
|
||||
5
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"power on matrix"
|
||||
(rv
|
||||
(apl-power
|
||||
(fn (a) (apl-mul a (apl-scalar 3)))
|
||||
2
|
||||
(make-array (list 2 2) (list 1 2 3 4))))
|
||||
(list 9 18 27 36))
|
||||
|
||||
(apl-test
|
||||
"power-fixed identity stops immediately"
|
||||
(rv (apl-power-fixed (fn (a) a) (make-array (list 3) (list 1 2 3))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"power-fixed floor half scalar to 0"
|
||||
(rv
|
||||
(apl-power-fixed
|
||||
(fn (a) (apl-floor (apl-div a (apl-scalar 2))))
|
||||
(apl-scalar 100)))
|
||||
(list 0))
|
||||
|
||||
(apl-test
|
||||
"power-fixed shape preserved"
|
||||
(sh
|
||||
(apl-power-fixed (fn (a) a) (make-array (list 2 2) (list 1 2 3 4))))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"rank tally⍤1 row tallies"
|
||||
(rv (apl-rank apl-tally 1 (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3 3))
|
||||
|
||||
(apl-test
|
||||
"rank tally⍤1 row tallies shape"
|
||||
(sh (apl-rank apl-tally 1 (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"rank neg⍤0 vector scalar cells"
|
||||
(rv (apl-rank apl-neg-m 0 (make-array (list 3) (list 1 2 3))))
|
||||
(list -1 -2 -3))
|
||||
|
||||
(apl-test
|
||||
"rank neg⍤0 vector preserves shape"
|
||||
(sh (apl-rank apl-neg-m 0 (make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"rank neg⍤1 matrix per-row"
|
||||
(rv (apl-rank apl-neg-m 1 (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list -1 -2 -3 -4 -5 -6))
|
||||
|
||||
(apl-test
|
||||
"rank neg⍤1 matrix preserves shape"
|
||||
(sh (apl-rank apl-neg-m 1 (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"rank k>=rank fallthrough"
|
||||
(rv (apl-rank apl-tally 5 (make-array (list 4) (list 1 2 3 4))))
|
||||
(list 4))
|
||||
|
||||
(apl-test
|
||||
"rank tally⍤2 whole matrix tally"
|
||||
(rv
|
||||
(apl-rank
|
||||
apl-tally
|
||||
2
|
||||
(make-array (list 3 5) (list 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"rank reverse⍤1 matrix reverse rows"
|
||||
(rv (apl-rank apl-reverse 1 (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3 2 1 6 5 4))
|
||||
|
||||
(apl-test
|
||||
"rank tally⍤1 3x4 row tallies"
|
||||
(rv
|
||||
(apl-rank
|
||||
apl-tally
|
||||
1
|
||||
(make-array (list 3 4) (list 1 2 3 4 5 6 7 8 9 10 11 12))))
|
||||
(list 4 4 4))
|
||||
|
||||
(apl-test
|
||||
"at-replace single index"
|
||||
(rv
|
||||
(apl-at-replace
|
||||
(apl-scalar 99)
|
||||
(make-array (list 1) (list 2))
|
||||
(make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 99 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"at-replace multiple indices vector vals"
|
||||
(rv
|
||||
(apl-at-replace
|
||||
(make-array (list 2) (list 99 88))
|
||||
(make-array (list 2) (list 2 4))
|
||||
(make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 99 3 88 5))
|
||||
|
||||
(apl-test
|
||||
"at-replace scalar broadcast"
|
||||
(rv
|
||||
(apl-at-replace
|
||||
(apl-scalar 0)
|
||||
(make-array (list 3) (list 1 3 5))
|
||||
(make-array (list 5) (list 10 20 30 40 50))))
|
||||
(list 0 20 0 40 0))
|
||||
|
||||
(apl-test
|
||||
"at-replace preserves shape"
|
||||
(sh
|
||||
(apl-at-replace
|
||||
(apl-scalar 99)
|
||||
(make-array (list 1) (list 2))
|
||||
(make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"at-replace last index"
|
||||
(rv
|
||||
(apl-at-replace
|
||||
(apl-scalar 99)
|
||||
(make-array (list 1) (list 5))
|
||||
(make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3 4 99))
|
||||
|
||||
(apl-test
|
||||
"at-replace on matrix linear-index"
|
||||
(rv
|
||||
(apl-at-replace
|
||||
(apl-scalar 99)
|
||||
(make-array (list 1) (list 3))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 99 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"at-apply negate at indices"
|
||||
(rv
|
||||
(apl-at-apply
|
||||
apl-neg-m
|
||||
(make-array (list 3) (list 1 3 5))
|
||||
(make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list -1 2 -3 4 -5))
|
||||
|
||||
(apl-test
|
||||
"at-apply double at index 1"
|
||||
(rv
|
||||
(apl-at-apply
|
||||
(fn (a) (apl-mul a (apl-scalar 2)))
|
||||
(make-array (list 1) (list 1))
|
||||
(make-array (list 2) (list 5 10))))
|
||||
(list 10 10))
|
||||
|
||||
(apl-test
|
||||
"at-apply preserves shape"
|
||||
(sh
|
||||
(apl-at-apply
|
||||
apl-neg-m
|
||||
(make-array (list 2) (list 1 3))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"at-apply on matrix linear-index"
|
||||
(rv
|
||||
(apl-at-apply
|
||||
apl-neg-m
|
||||
(make-array (list 2) (list 1 6))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list -1 2 3 4 5 -6))
|
||||
340
lib/apl/tests/parse.sx
Normal file
340
lib/apl/tests/parse.sx
Normal file
@@ -0,0 +1,340 @@
|
||||
(define apl-test-count 0)
|
||||
(define apl-test-pass 0)
|
||||
(define apl-test-fails (list))
|
||||
|
||||
(define apl-test
|
||||
(fn (name actual expected)
|
||||
(begin
|
||||
(set! apl-test-count (+ apl-test-count 1))
|
||||
(if (= actual expected)
|
||||
(set! apl-test-pass (+ apl-test-pass 1))
|
||||
(append! apl-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define tok-types
|
||||
(fn (src)
|
||||
(map (fn (t) (get t :type)) (apl-tokenize src))))
|
||||
|
||||
(define tok-values
|
||||
(fn (src)
|
||||
(map (fn (t) (get t :value)) (apl-tokenize src))))
|
||||
|
||||
(define tok-count
|
||||
(fn (src)
|
||||
(len (apl-tokenize src))))
|
||||
|
||||
(define tok-type-at
|
||||
(fn (src i)
|
||||
(get (nth (apl-tokenize src) i) :type)))
|
||||
|
||||
(define tok-value-at
|
||||
(fn (src i)
|
||||
(get (nth (apl-tokenize src) i) :value)))
|
||||
|
||||
(apl-test "empty: no tokens" (tok-count "") 0)
|
||||
(apl-test "empty: whitespace only" (tok-count " ") 0)
|
||||
(apl-test "num: zero" (tok-values "0") (list 0))
|
||||
(apl-test "num: positive" (tok-values "42") (list 42))
|
||||
(apl-test "num: large" (tok-values "12345") (list 12345))
|
||||
(apl-test "num: negative" (tok-values "¯5") (list -5))
|
||||
(apl-test "num: negative zero" (tok-values "¯0") (list 0))
|
||||
(apl-test "num: strand count" (tok-count "1 2 3") 3)
|
||||
(apl-test "num: strand types" (tok-types "1 2 3") (list :num :num :num))
|
||||
(apl-test "num: strand values" (tok-values "1 2 3") (list 1 2 3))
|
||||
(apl-test "num: neg in strand" (tok-values "1 ¯2 3") (list 1 -2 3))
|
||||
(apl-test "str: empty" (tok-values "''") (list ""))
|
||||
(apl-test "str: single char" (tok-values "'a'") (list "a"))
|
||||
(apl-test "str: word" (tok-values "'hello'") (list "hello"))
|
||||
(apl-test "str: escaped quote" (tok-values "''''") (list "'"))
|
||||
(apl-test "str: type" (tok-types "'abc'") (list :str))
|
||||
(apl-test "name: simple" (tok-values "foo") (list "foo"))
|
||||
(apl-test "name: type" (tok-types "foo") (list :name))
|
||||
(apl-test "name: mixed case" (tok-values "MyVar") (list "MyVar"))
|
||||
(apl-test "name: with digits" (tok-values "x1") (list "x1"))
|
||||
(apl-test "name: system var" (tok-values "⎕IO") (list "⎕IO"))
|
||||
(apl-test "name: system var type" (tok-types "⎕IO") (list :name))
|
||||
(apl-test "glyph: plus" (tok-types "+") (list :glyph))
|
||||
(apl-test "glyph: plus value" (tok-values "+") (list "+"))
|
||||
(apl-test "glyph: iota" (tok-values "⍳") (list "⍳"))
|
||||
(apl-test "glyph: reduce" (tok-values "+/") (list "+" "/"))
|
||||
(apl-test "glyph: floor" (tok-values "⌊") (list "⌊"))
|
||||
(apl-test "glyph: rho" (tok-values "⍴") (list "⍴"))
|
||||
(apl-test "glyph: alpha omega" (tok-types "⍺ ⍵") (list :glyph :glyph))
|
||||
(apl-test "punct: lparen" (tok-types "(") (list :lparen))
|
||||
(apl-test "punct: rparen" (tok-types ")") (list :rparen))
|
||||
(apl-test "punct: brackets" (tok-types "[42]") (list :lbracket :num :rbracket))
|
||||
(apl-test "punct: braces" (tok-types "{}") (list :lbrace :rbrace))
|
||||
(apl-test "punct: semi" (tok-types ";") (list :semi))
|
||||
(apl-test "assign: arrow" (tok-types "x←1") (list :name :assign :num))
|
||||
(apl-test "diamond: separator" (tok-types "1⋄2") (list :num :diamond :num))
|
||||
(apl-test "newline: emitted" (tok-types "1\n2") (list :num :newline :num))
|
||||
(apl-test "comment: skipped" (tok-count "⍝ ignore me") 0)
|
||||
(apl-test "comment: rest ignored" (tok-count "1 ⍝ note") 1)
|
||||
(apl-test "colon: bare" (tok-types ":") (list :colon))
|
||||
(apl-test "keyword: If" (tok-values ":If") (list ":If"))
|
||||
(apl-test "keyword: type" (tok-types ":While") (list :keyword))
|
||||
(apl-test "keyword: EndFor" (tok-values ":EndFor") (list ":EndFor"))
|
||||
(apl-test "expr: +/ ⍳ 5" (tok-types "+/ ⍳ 5") (list :glyph :glyph :glyph :num))
|
||||
(apl-test "expr: x←42" (tok-count "x←42") 3)
|
||||
(apl-test "expr: dfn body" (tok-types "{⍺+⍵}")
|
||||
(list :lbrace :glyph :glyph :glyph :rbrace))
|
||||
|
||||
(define apl-tokenize-test-summary
|
||||
(str "tokenizer " apl-test-pass "/" apl-test-count
|
||||
(if (= (len apl-test-fails) 0) "" (str " FAILS: " apl-test-fails))))
|
||||
|
||||
; ===========================================================================
|
||||
; Parser tests
|
||||
; ===========================================================================
|
||||
|
||||
; Helper: parse an APL source string and return the AST
|
||||
(define parse
|
||||
(fn (src) (parse-apl src)))
|
||||
|
||||
; Helper: build an expected AST node using keyword-tagged lists
|
||||
(define num-node (fn (n) (list :num n)))
|
||||
(define str-node (fn (s) (list :str s)))
|
||||
(define name-node (fn (n) (list :name n)))
|
||||
(define fn-node (fn (g) (list :fn-glyph g)))
|
||||
(define fn-nm (fn (n) (list :fn-name n)))
|
||||
(define assign-node (fn (nm expr) (list :assign nm expr)))
|
||||
(define monad-node (fn (f a) (list :monad f a)))
|
||||
(define dyad-node (fn (f l r) (list :dyad f l r)))
|
||||
(define derived-fn (fn (op f) (list :derived-fn op f)))
|
||||
(define derived-fn2 (fn (op f g) (list :derived-fn2 op f g)))
|
||||
(define outer-node (fn (f) (list :outer "∘." f)))
|
||||
(define guard-node (fn (c e) (list :guard c e)))
|
||||
|
||||
; ---- numeric literals ----
|
||||
|
||||
(apl-test "parse: num literal"
|
||||
(parse "42")
|
||||
(num-node 42))
|
||||
|
||||
(apl-test "parse: negative num"
|
||||
(parse "¯3")
|
||||
(num-node -3))
|
||||
|
||||
(apl-test "parse: zero"
|
||||
(parse "0")
|
||||
(num-node 0))
|
||||
|
||||
; ---- string literals ----
|
||||
|
||||
(apl-test "parse: str literal"
|
||||
(parse "'hello'")
|
||||
(str-node "hello"))
|
||||
|
||||
(apl-test "parse: empty str"
|
||||
(parse "''")
|
||||
(str-node ""))
|
||||
|
||||
; ---- name reference ----
|
||||
|
||||
(apl-test "parse: name"
|
||||
(parse "x")
|
||||
(name-node "x"))
|
||||
|
||||
(apl-test "parse: system name"
|
||||
(parse "⎕IO")
|
||||
(name-node "⎕IO"))
|
||||
|
||||
; ---- strands (vec nodes) ----
|
||||
|
||||
(apl-test "parse: strand 3 nums"
|
||||
(parse "1 2 3")
|
||||
(list :vec (num-node 1) (num-node 2) (num-node 3)))
|
||||
|
||||
(apl-test "parse: strand 2 nums"
|
||||
(parse "1 2")
|
||||
(list :vec (num-node 1) (num-node 2)))
|
||||
|
||||
(apl-test "parse: strand with negatives"
|
||||
(parse "1 ¯2 3")
|
||||
(list :vec (num-node 1) (num-node -2) (num-node 3)))
|
||||
|
||||
; ---- assignment ----
|
||||
|
||||
(apl-test "parse: assignment"
|
||||
(parse "x←42")
|
||||
(assign-node "x" (num-node 42)))
|
||||
|
||||
(apl-test "parse: assignment with spaces"
|
||||
(parse "x ← 42")
|
||||
(assign-node "x" (num-node 42)))
|
||||
|
||||
(apl-test "parse: assignment of expr"
|
||||
(parse "r←2+3")
|
||||
(assign-node "r" (dyad-node (fn-node "+") (num-node 2) (num-node 3))))
|
||||
|
||||
; ---- monadic functions ----
|
||||
|
||||
(apl-test "parse: monadic iota"
|
||||
(parse "⍳5")
|
||||
(monad-node (fn-node "⍳") (num-node 5)))
|
||||
|
||||
(apl-test "parse: monadic iota with space"
|
||||
(parse "⍳ 5")
|
||||
(monad-node (fn-node "⍳") (num-node 5)))
|
||||
|
||||
(apl-test "parse: monadic negate"
|
||||
(parse "-3")
|
||||
(monad-node (fn-node "-") (num-node 3)))
|
||||
|
||||
(apl-test "parse: monadic floor"
|
||||
(parse "⌊2")
|
||||
(monad-node (fn-node "⌊") (num-node 2)))
|
||||
|
||||
(apl-test "parse: monadic of name"
|
||||
(parse "⍴x")
|
||||
(monad-node (fn-node "⍴") (name-node "x")))
|
||||
|
||||
; ---- dyadic functions ----
|
||||
|
||||
(apl-test "parse: dyadic plus"
|
||||
(parse "2+3")
|
||||
(dyad-node (fn-node "+") (num-node 2) (num-node 3)))
|
||||
|
||||
(apl-test "parse: dyadic times"
|
||||
(parse "2×3")
|
||||
(dyad-node (fn-node "×") (num-node 2) (num-node 3)))
|
||||
|
||||
(apl-test "parse: dyadic with names"
|
||||
(parse "x+y")
|
||||
(dyad-node (fn-node "+") (name-node "x") (name-node "y")))
|
||||
|
||||
; ---- right-to-left evaluation ----
|
||||
|
||||
(apl-test "parse: right-to-left 2×3+4"
|
||||
(parse "2×3+4")
|
||||
(dyad-node (fn-node "×") (num-node 2)
|
||||
(dyad-node (fn-node "+") (num-node 3) (num-node 4))))
|
||||
|
||||
(apl-test "parse: right-to-left chain"
|
||||
(parse "1+2×3-4")
|
||||
(dyad-node (fn-node "+") (num-node 1)
|
||||
(dyad-node (fn-node "×") (num-node 2)
|
||||
(dyad-node (fn-node "-") (num-node 3) (num-node 4)))))
|
||||
|
||||
; ---- parenthesized subexpressions ----
|
||||
|
||||
(apl-test "parse: parens override order"
|
||||
(parse "(2+3)×4")
|
||||
(dyad-node (fn-node "×")
|
||||
(dyad-node (fn-node "+") (num-node 2) (num-node 3))
|
||||
(num-node 4)))
|
||||
|
||||
(apl-test "parse: nested parens"
|
||||
(parse "((2+3))")
|
||||
(dyad-node (fn-node "+") (num-node 2) (num-node 3)))
|
||||
|
||||
(apl-test "parse: paren in dyadic right"
|
||||
(parse "2×(3+4)")
|
||||
(dyad-node (fn-node "×") (num-node 2)
|
||||
(dyad-node (fn-node "+") (num-node 3) (num-node 4))))
|
||||
|
||||
; ---- operators → derived functions ----
|
||||
|
||||
(apl-test "parse: reduce +"
|
||||
(parse "+/x")
|
||||
(monad-node (derived-fn "/" (fn-node "+")) (name-node "x")))
|
||||
|
||||
(apl-test "parse: reduce iota"
|
||||
(parse "+/⍳5")
|
||||
(monad-node (derived-fn "/" (fn-node "+"))
|
||||
(monad-node (fn-node "⍳") (num-node 5))))
|
||||
|
||||
(apl-test "parse: scan"
|
||||
(parse "+\\x")
|
||||
(monad-node (derived-fn "\\" (fn-node "+")) (name-node "x")))
|
||||
|
||||
(apl-test "parse: each"
|
||||
(parse "⍳¨x")
|
||||
(monad-node (derived-fn "¨" (fn-node "⍳")) (name-node "x")))
|
||||
|
||||
(apl-test "parse: commute"
|
||||
(parse "-⍨3")
|
||||
(monad-node (derived-fn "⍨" (fn-node "-")) (num-node 3)))
|
||||
|
||||
(apl-test "parse: stacked ops"
|
||||
(parse "+/¨x")
|
||||
(monad-node (derived-fn "¨" (derived-fn "/" (fn-node "+"))) (name-node "x")))
|
||||
|
||||
; ---- outer product ----
|
||||
|
||||
(apl-test "parse: outer product monadic"
|
||||
(parse "∘.×")
|
||||
(outer-node (fn-node "×")))
|
||||
|
||||
(apl-test "parse: outer product dyadic names"
|
||||
(parse "x ∘.× y")
|
||||
(dyad-node (outer-node (fn-node "×")) (name-node "x") (name-node "y")))
|
||||
|
||||
(apl-test "parse: outer product dyadic strands"
|
||||
(parse "1 2 3 ∘.× 4 5 6")
|
||||
(dyad-node (outer-node (fn-node "×"))
|
||||
(list :vec (num-node 1) (num-node 2) (num-node 3))
|
||||
(list :vec (num-node 4) (num-node 5) (num-node 6))))
|
||||
|
||||
; ---- inner product ----
|
||||
|
||||
(apl-test "parse: inner product"
|
||||
(parse "+.×")
|
||||
(derived-fn2 "." (fn-node "+") (fn-node "×")))
|
||||
|
||||
(apl-test "parse: inner product applied"
|
||||
(parse "a +.× b")
|
||||
(dyad-node (derived-fn2 "." (fn-node "+") (fn-node "×"))
|
||||
(name-node "a") (name-node "b")))
|
||||
|
||||
; ---- dfn (anonymous function) ----
|
||||
|
||||
(apl-test "parse: simple dfn"
|
||||
(parse "{⍺+⍵}")
|
||||
(list :dfn (dyad-node (fn-node "+") (name-node "⍺") (name-node "⍵"))))
|
||||
|
||||
(apl-test "parse: monadic dfn"
|
||||
(parse "{⍵×2}")
|
||||
(list :dfn (dyad-node (fn-node "×") (name-node "⍵") (num-node 2))))
|
||||
|
||||
(apl-test "parse: dfn self-ref"
|
||||
(parse "{⍵≤1:1 ⋄ ⍵×∇ ⍵-1}")
|
||||
(list :dfn
|
||||
(guard-node (dyad-node (fn-node "≤") (name-node "⍵") (num-node 1)) (num-node 1))
|
||||
(dyad-node (fn-node "×") (name-node "⍵")
|
||||
(monad-node (fn-node "∇") (dyad-node (fn-node "-") (name-node "⍵") (num-node 1))))))
|
||||
|
||||
; ---- dfn applied ----
|
||||
|
||||
(apl-test "parse: dfn as function"
|
||||
(parse "{⍺+⍵} 3")
|
||||
(monad-node
|
||||
(list :dfn (dyad-node (fn-node "+") (name-node "⍺") (name-node "⍵")))
|
||||
(num-node 3)))
|
||||
|
||||
; ---- multi-statement ----
|
||||
|
||||
(apl-test "parse: diamond separator"
|
||||
(let ((result (parse "x←1 ⋄ x+2")))
|
||||
(= (first result) :program))
|
||||
true)
|
||||
|
||||
(apl-test "parse: diamond first stmt"
|
||||
(let ((result (parse "x←1 ⋄ x+2")))
|
||||
(nth result 1))
|
||||
(assign-node "x" (num-node 1)))
|
||||
|
||||
(apl-test "parse: diamond second stmt"
|
||||
(let ((result (parse "x←1 ⋄ x+2")))
|
||||
(nth result 2))
|
||||
(dyad-node (fn-node "+") (name-node "x") (num-node 2)))
|
||||
|
||||
; ---- combined summary ----
|
||||
|
||||
(define apl-parse-test-count (- apl-test-count 46))
|
||||
(define apl-parse-test-pass (- apl-test-pass 46))
|
||||
|
||||
(define apl-test-summary
|
||||
(str
|
||||
"tokenizer 46/46 | "
|
||||
"parser " apl-parse-test-pass "/" apl-parse-test-count
|
||||
(if (= (len apl-test-fails) 0) "" (str " FAILS: " apl-test-fails))))
|
||||
314
lib/apl/tests/pipeline.sx
Normal file
314
lib/apl/tests/pipeline.sx
Normal file
@@ -0,0 +1,314 @@
|
||||
; End-to-end pipeline tests: source string → tokenize → parse → eval-ast → array.
|
||||
; Verifies the full stack as a single function call (apl-run).
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
|
||||
; ---------- scalars ----------
|
||||
|
||||
(apl-test "apl-run \"42\" → scalar 42" (mkrv (apl-run "42")) (list 42))
|
||||
|
||||
(apl-test "apl-run \"¯7\" → scalar -7" (mkrv (apl-run "¯7")) (list -7))
|
||||
|
||||
; ---------- strands ----------
|
||||
|
||||
(apl-test
|
||||
"apl-run \"1 2 3\" → vector"
|
||||
(mkrv (apl-run "1 2 3"))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test "apl-run \"1 2 3\" shape" (mksh (apl-run "1 2 3")) (list 3))
|
||||
|
||||
; ---------- dyadic arithmetic ----------
|
||||
|
||||
(apl-test "apl-run \"2 + 3\" → 5" (mkrv (apl-run "2 + 3")) (list 5))
|
||||
|
||||
(apl-run "2 × 3 + 4") ; right-to-left
|
||||
|
||||
(apl-test
|
||||
"apl-run \"2 × 3 + 4\" → 14 (right-to-left)"
|
||||
(mkrv (apl-run "2 × 3 + 4"))
|
||||
(list 14))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"1 2 3 + 4 5 6\" → 5 7 9"
|
||||
(mkrv (apl-run "1 2 3 + 4 5 6"))
|
||||
(list 5 7 9))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"3 × 1 2 3 4\" → scalar broadcast"
|
||||
(mkrv (apl-run "3 × 1 2 3 4"))
|
||||
(list 3 6 9 12))
|
||||
|
||||
; ---------- monadic primitives ----------
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⍳5\" → 1..5"
|
||||
(mkrv (apl-run "⍳5"))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"-3\" → -3 (monadic negate)"
|
||||
(mkrv (apl-run "-3"))
|
||||
(list -3))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⌈/ 1 3 9 5 7\" → 9 (max-reduce)"
|
||||
(mkrv (apl-run "⌈/ 1 3 9 5 7"))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⌊/ 4 7 2 9 1 3\" → 1 (min-reduce)"
|
||||
(mkrv (apl-run "⌊/ 4 7 2 9 1 3"))
|
||||
(list 1))
|
||||
|
||||
; ---------- operators ----------
|
||||
|
||||
(apl-test "apl-run \"+/⍳5\" → 15" (mkrv (apl-run "+/⍳5")) (list 15))
|
||||
|
||||
(apl-test "apl-run \"×/⍳5\" → 120" (mkrv (apl-run "×/⍳5")) (list 120))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⌈/3 1 4 1 5 9 2\" → 9"
|
||||
(mkrv (apl-run "⌈/3 1 4 1 5 9 2"))
|
||||
(list 9))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"+\\\\⍳5\" → triangular numbers"
|
||||
(mkrv (apl-run "+\\⍳5"))
|
||||
(list 1 3 6 10 15))
|
||||
|
||||
; ---------- outer / inner products ----------
|
||||
|
||||
(apl-test
|
||||
"apl-run \"1 2 3 ∘.× 1 2 3\" → mult table values"
|
||||
(mkrv (apl-run "1 2 3 ∘.× 1 2 3"))
|
||||
(list 1 2 3 2 4 6 3 6 9))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"1 2 3 +.× 4 5 6\" → dot product 32"
|
||||
(mkrv (apl-run "1 2 3 +.× 4 5 6"))
|
||||
(list 32))
|
||||
|
||||
; ---------- shape ----------
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⍴ 1 2 3 4 5\" → 5"
|
||||
(mkrv (apl-run "⍴ 1 2 3 4 5"))
|
||||
(list 5))
|
||||
|
||||
(apl-test "apl-run \"⍴⍳10\" → 10" (mkrv (apl-run "⍴⍳10")) (list 10))
|
||||
|
||||
; ---------- comparison ----------
|
||||
|
||||
(apl-test "apl-run \"3 < 5\" → 1" (mkrv (apl-run "3 < 5")) (list 1))
|
||||
|
||||
(apl-test "apl-run \"5 = 5\" → 1" (mkrv (apl-run "5 = 5")) (list 1))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"1 2 3 = 1 0 3\" → 1 0 1"
|
||||
(mkrv (apl-run "1 2 3 = 1 0 3"))
|
||||
(list 1 0 1))
|
||||
|
||||
; ---------- famous one-liners ----------
|
||||
|
||||
(apl-test
|
||||
"apl-run \"+/(⍳10)\" → sum 1..10 = 55"
|
||||
(mkrv (apl-run "+/(⍳10)"))
|
||||
(list 55))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"×/⍳10\" → 10! = 3628800"
|
||||
(mkrv (apl-run "×/⍳10"))
|
||||
(list 3628800))
|
||||
|
||||
(apl-test "apl-run \"⎕IO\" → 1" (mkrv (apl-run "⎕IO")) (list 1))
|
||||
|
||||
(apl-test "apl-run \"⎕ML\" → 1" (mkrv (apl-run "⎕ML")) (list 1))
|
||||
|
||||
(apl-test "apl-run \"⎕FR\" → 1248" (mkrv (apl-run "⎕FR")) (list 1248))
|
||||
|
||||
(apl-test "apl-run \"⎕TS\" shape (7)" (mksh (apl-run "⎕TS")) (list 7))
|
||||
|
||||
(apl-test "apl-run \"⎕FMT 42\" → \"42\"" (apl-run "⎕FMT 42") "42")
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⎕FMT 1 2 3\" → \"1 2 3\""
|
||||
(apl-run "⎕FMT 1 2 3")
|
||||
"1 2 3")
|
||||
|
||||
(apl-test
|
||||
"apl-run \"⎕FMT ⍳5\" → \"1 2 3 4 5\""
|
||||
(apl-run "⎕FMT ⍳5")
|
||||
"1 2 3 4 5")
|
||||
|
||||
(apl-test "apl-run \"⎕IO + 4\" → 5" (mkrv (apl-run "⎕IO + 4")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"(10 20 30 40 50)[3]\" → 30"
|
||||
(mkrv (apl-run "(10 20 30 40 50)[3]"))
|
||||
(list 30))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"(⍳10)[5]\" → 5"
|
||||
(mkrv (apl-run "(⍳10)[5]"))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"A ← 100 200 300 ⋄ A[2]\" → 200"
|
||||
(mkrv (apl-run "A ← 100 200 300 ⋄ A[2]"))
|
||||
(list 200))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"V ← ⍳10 ⋄ V[3]\" → 3"
|
||||
(mkrv (apl-run "V ← ⍳10 ⋄ V[3]"))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"(10 20 30)[1]\" → 10 (1-indexed)"
|
||||
(mkrv (apl-run "(10 20 30)[1]"))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"V ← 10 20 30 40 50 ⋄ V[3] + 1\" → 31"
|
||||
(mkrv (apl-run "V ← 10 20 30 40 50 ⋄ V[3] + 1"))
|
||||
(list 31))
|
||||
|
||||
(apl-test
|
||||
"apl-run \"(⍳5)[3] × 7\" → 21"
|
||||
(mkrv (apl-run "(⍳5)[3] × 7"))
|
||||
(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))
|
||||
96
lib/apl/tests/programs-e2e.sx
Normal file
96
lib/apl/tests/programs-e2e.sx
Normal file
@@ -0,0 +1,96 @@
|
||||
; 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))
|
||||
306
lib/apl/tests/programs.sx
Normal file
306
lib/apl/tests/programs.sx
Normal file
@@ -0,0 +1,306 @@
|
||||
; Tests for classic APL programs (lib/apl/tests/programs/*.apl).
|
||||
; Programs are showcase APL source; runtime impl is in lib/apl/runtime.sx.
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
|
||||
; ===== primes (Sieve of Eratosthenes) =====
|
||||
|
||||
(apl-test "primes 1 → empty" (mkrv (apl-primes 1)) (list))
|
||||
|
||||
(apl-test "primes 2 → just 2" (mkrv (apl-primes 2)) (list 2))
|
||||
|
||||
(apl-test "primes 10 → 2 3 5 7" (mkrv (apl-primes 10)) (list 2 3 5 7))
|
||||
|
||||
(apl-test
|
||||
"primes 20 → 2 3 5 7 11 13 17 19"
|
||||
(mkrv (apl-primes 20))
|
||||
(list 2 3 5 7 11 13 17 19))
|
||||
|
||||
(apl-test
|
||||
"primes 30"
|
||||
(mkrv (apl-primes 30))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"primes 50"
|
||||
(mkrv (apl-primes 50))
|
||||
(list 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47))
|
||||
|
||||
(apl-test "primes 7 length" (first (mksh (apl-primes 7))) 4)
|
||||
|
||||
(apl-test "primes 100 has 25 primes" (first (mksh (apl-primes 100))) 25)
|
||||
|
||||
; ===== compress helper sanity =====
|
||||
|
||||
(apl-test
|
||||
"compress 1 0 1 0 1 / 10 20 30 40 50"
|
||||
(mkrv
|
||||
(apl-compress
|
||||
(make-array (list 5) (list 1 0 1 0 1))
|
||||
(make-array (list 5) (list 10 20 30 40 50))))
|
||||
(list 10 30 50))
|
||||
|
||||
(apl-test
|
||||
"compress all-zero mask → empty"
|
||||
(mkrv
|
||||
(apl-compress
|
||||
(make-array (list 3) (list 0 0 0))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"compress all-one mask → full vector"
|
||||
(mkrv
|
||||
(apl-compress
|
||||
(make-array (list 3) (list 1 1 1))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"life: empty 5x5 stays empty"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(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))))
|
||||
(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: horizontal blinker → vertical blinker"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(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))))
|
||||
(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: vertical blinker → horizontal blinker"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(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))))
|
||||
(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: blinker has period 2"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(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)))))
|
||||
(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: 2x2 block stable on 5x5"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(list 0 0 0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0))))
|
||||
(list 0 0 0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0))
|
||||
|
||||
(apl-test
|
||||
"life: shape preserved"
|
||||
(mksh
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 5 5)
|
||||
(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))))
|
||||
(list 5 5))
|
||||
|
||||
(apl-test
|
||||
"life: glider on 6x6 advances"
|
||||
(mkrv
|
||||
(apl-life-step
|
||||
(make-array
|
||||
(list 6 6)
|
||||
(list
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
1
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0))))
|
||||
(list
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
0
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=0 stays bounded"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list 0)) 100))
|
||||
(list 100))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=-1 cycle bounded"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list -1)) 100))
|
||||
(list 100))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=-2 boundary stays bounded"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list -2)) 100))
|
||||
(list 100))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=0.25 boundary stays bounded"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list 0.25)) 100))
|
||||
(list 100))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=1 escapes at iter 3"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list 1)) 100))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=0.5 escapes at iter 5"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list 0.5)) 100))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot batched grid (rank-polymorphic)"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 5) (list -2 -1 0 1 2)) 10))
|
||||
(list 10 10 10 3 2))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot batched preserves shape"
|
||||
(mksh (apl-mandelbrot-1d (make-array (list 5) (list -2 -1 0 1 2)) 10))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"mandelbrot c=-1.5 stays bounded"
|
||||
(mkrv (apl-mandelbrot-1d (make-array (list 1) (list -1.5)) 100))
|
||||
(list 100))
|
||||
|
||||
(apl-test "queens 1 → 1 solution" (mkrv (apl-queens 1)) (list 1))
|
||||
|
||||
(apl-test "queens 2 → 0 solutions" (mkrv (apl-queens 2)) (list 0))
|
||||
|
||||
(apl-test "queens 3 → 0 solutions" (mkrv (apl-queens 3)) (list 0))
|
||||
|
||||
(apl-test "queens 4 → 2 solutions" (mkrv (apl-queens 4)) (list 2))
|
||||
|
||||
(apl-test "queens 5 → 10 solutions" (mkrv (apl-queens 5)) (list 10))
|
||||
|
||||
(apl-test "queens 6 → 4 solutions" (mkrv (apl-queens 6)) (list 4))
|
||||
|
||||
(apl-test "queens 7 → 40 solutions" (mkrv (apl-queens 7)) (list 40))
|
||||
|
||||
(apl-test "queens 8 → 92 solutions" (mkrv (apl-queens 8)) (list 92))
|
||||
|
||||
(apl-test "permutations of 3 has 6" (len (apl-permutations 3)) 6)
|
||||
|
||||
(apl-test "permutations of 4 has 24" (len (apl-permutations 4)) 24)
|
||||
|
||||
(apl-test
|
||||
"quicksort empty"
|
||||
(mkrv (apl-quicksort (make-array (list 0) (list))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"quicksort single"
|
||||
(mkrv (apl-quicksort (make-array (list 1) (list 42))))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"quicksort already sorted"
|
||||
(mkrv (apl-quicksort (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"quicksort reverse sorted"
|
||||
(mkrv (apl-quicksort (make-array (list 5) (list 5 4 3 2 1))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"quicksort with duplicates"
|
||||
(mkrv (apl-quicksort (make-array (list 7) (list 3 1 4 1 5 9 2))))
|
||||
(list 1 1 2 3 4 5 9))
|
||||
|
||||
(apl-test
|
||||
"quicksort all equal"
|
||||
(mkrv (apl-quicksort (make-array (list 5) (list 7 7 7 7 7))))
|
||||
(list 7 7 7 7 7))
|
||||
|
||||
(apl-test
|
||||
"quicksort negatives"
|
||||
(mkrv (apl-quicksort (make-array (list 5) (list -3 1 -1 2 0))))
|
||||
(list -3 -1 0 1 2))
|
||||
|
||||
(apl-test
|
||||
"quicksort 11-element pi"
|
||||
(mkrv
|
||||
(apl-quicksort (make-array (list 11) (list 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 preserves length"
|
||||
(first
|
||||
(mksh (apl-quicksort (make-array (list 7) (list 3 1 4 1 5 9 2)))))
|
||||
7)
|
||||
22
lib/apl/tests/programs/life.apl
Normal file
22
lib/apl/tests/programs/life.apl
Normal file
@@ -0,0 +1,22 @@
|
||||
⍝ Conway's Game of Life — toroidal one-liner
|
||||
⍝
|
||||
⍝ The classic Roger Hui formulation:
|
||||
⍝ life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵}
|
||||
⍝
|
||||
⍝ Read right-to-left:
|
||||
⍝ ⊂⍵ : enclose the board (so it's a single scalar item)
|
||||
⍝ ¯1 0 1 ⌽¨ ⊂⍵ : produce 3 horizontally-shifted copies
|
||||
⍝ ¯1 0 1 ∘.⊖ … : outer-product with vertical shifts → 3×3 = 9 shifts
|
||||
⍝ +/ +/ … : sum the 9 boards element-wise → neighbor-count + self
|
||||
⍝ 3 4 = … : boolean — count is exactly 3 or exactly 4
|
||||
⍝ 1 ⍵ ∨.∧ … : "alive next" iff (count=3) or (alive AND count=4)
|
||||
⍝ ⊃ … : disclose back to a 2D board
|
||||
⍝
|
||||
⍝ Rules in plain language:
|
||||
⍝ - dead cell + 3 live neighbors → born
|
||||
⍝ - live cell + 2 or 3 live neighbors → survives
|
||||
⍝ - all else → dies
|
||||
⍝
|
||||
⍝ Toroidal: edges wrap (rotate is cyclic).
|
||||
|
||||
life ← {⊃1 ⍵ ∨.∧ 3 4 = +/ +/ ¯1 0 1 ∘.⊖ ¯1 0 1 ⌽¨ ⊂⍵}
|
||||
29
lib/apl/tests/programs/mandelbrot.apl
Normal file
29
lib/apl/tests/programs/mandelbrot.apl
Normal file
@@ -0,0 +1,29 @@
|
||||
⍝ Mandelbrot — real-axis subset
|
||||
⍝
|
||||
⍝ For complex c, the Mandelbrot set is { c : |z_n| stays bounded } where
|
||||
⍝ z_0 = 0, z_{n+1} = z_n² + c.
|
||||
⍝ Restricting c (and z) to ℝ gives the segment c ∈ [-2, 1/4]
|
||||
⍝ where the iteration stays bounded.
|
||||
⍝
|
||||
⍝ Rank-polymorphic batched-iteration form:
|
||||
⍝ mandelbrot ← {⍵ ⍵⍵ ⍺⍺ +,(⍺⍺ × ⍺⍺) }
|
||||
⍝
|
||||
⍝ Pseudocode (as we don't have ⎕ system fns yet):
|
||||
⍝ z ← 0×c ⍝ start at zero
|
||||
⍝ alive ← 1+0×c ⍝ all "still in"
|
||||
⍝ for k iterations:
|
||||
⍝ alive ← alive ∧ 4 ≥ z×z ⍝ still bounded?
|
||||
⍝ z ← alive × c + z×z ⍝ freeze escaped via mask
|
||||
⍝ count ← count + alive ⍝ tally surviving iters
|
||||
⍝
|
||||
⍝ Examples (count after 100 iterations):
|
||||
⍝ c=0 : 100 (z stays at 0)
|
||||
⍝ c=-1 : 100 (cycles 0,-1,0,-1,...)
|
||||
⍝ c=-2 : 100 (settles at 2 — boundary)
|
||||
⍝ c=0.25 : 100 (boundary — converges to 0.5)
|
||||
⍝ c=0.5 : 5 (escapes by iteration 6)
|
||||
⍝ c=1 : 3 (escapes quickly)
|
||||
⍝
|
||||
⍝ Real-axis Mandelbrot set: bounded for c ∈ [-2, 0.25].
|
||||
|
||||
mandelbrot ← {z←alive←count←0×⍵ ⋄ {alive←alive∧4≥z×z ⋄ z←alive×⍵+z×z ⋄ count+←alive}⍣⍺⊢⍵}
|
||||
18
lib/apl/tests/programs/n-queens.apl
Normal file
18
lib/apl/tests/programs/n-queens.apl
Normal file
@@ -0,0 +1,18 @@
|
||||
⍝ N-Queens — count solutions to placing N non-attacking queens on N×N
|
||||
⍝
|
||||
⍝ A solution is encoded as a permutation P of 1..N where P[i] is the
|
||||
⍝ column of the queen in row i. Rows and columns are then automatically
|
||||
⍝ unique (it's a permutation). We must additionally rule out queens
|
||||
⍝ sharing a diagonal: |i-j| = |P[i]-P[j]| for any pair.
|
||||
⍝
|
||||
⍝ Backtracking via reduce — the classic Roger Hui style:
|
||||
⍝ queens ← {≢{⍵,¨⍨↓(0=∊(¨⍳⍴⍵)≠.+|⍵)/⍳⍴⍵}/(⍳⍵)⍴⊂⍳⍵}
|
||||
⍝
|
||||
⍝ Plain reading:
|
||||
⍝ permute 1..N, keep those where no two queens share a diagonal.
|
||||
⍝
|
||||
⍝ Known solution counts (OEIS A000170):
|
||||
⍝ N 1 2 3 4 5 6 7 8 9 10
|
||||
⍝ q(N) 1 0 0 2 10 4 40 92 352 724
|
||||
|
||||
queens ← {≢({(i j)←⍺⍵ ⋄ (|i-j)≠|(P[i])-(P[j])}⌿permutations ⍵)}
|
||||
16
lib/apl/tests/programs/primes.apl
Normal file
16
lib/apl/tests/programs/primes.apl
Normal file
@@ -0,0 +1,16 @@
|
||||
⍝ Sieve of Eratosthenes — the classic APL one-liner
|
||||
⍝ primes ← (2=+⌿0=A∘.|A)/A←⍳N
|
||||
⍝
|
||||
⍝ Read right-to-left:
|
||||
⍝ A ← ⍳N : A is 1..N
|
||||
⍝ A∘.|A : outer-product residue table — M[i,j] = A[j] mod A[i]
|
||||
⍝ 0=... : boolean — true where A[i] divides A[j]
|
||||
⍝ +⌿... : column sums — count of divisors per A[j]
|
||||
⍝ 2=... : true for numbers with exactly 2 divisors (1 and self) → primes
|
||||
⍝ .../A : compress — select A[j] where mask[j] is true
|
||||
⍝
|
||||
⍝ Examples:
|
||||
⍝ primes 10 → 2 3 5 7
|
||||
⍝ primes 30 → 2 3 5 7 11 13 17 19 23 29
|
||||
|
||||
primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵}
|
||||
25
lib/apl/tests/programs/quicksort.apl
Normal file
25
lib/apl/tests/programs/quicksort.apl
Normal file
@@ -0,0 +1,25 @@
|
||||
⍝ Quicksort — the classic Roger Hui one-liner
|
||||
⍝
|
||||
⍝ Q ← {1≥≢⍵:⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p←⍵⌷⍨?≢⍵}
|
||||
⍝
|
||||
⍝ Read right-to-left:
|
||||
⍝ ?≢⍵ : pick a random index in 1..length
|
||||
⍝ ⍵⌷⍨… : take that element as pivot p
|
||||
⍝ ⍵>p : boolean — elements greater than pivot
|
||||
⍝ ∇⍵⌿⍨… : recursively sort the > partition
|
||||
⍝ (p=⍵)/⍵ : keep elements equal to pivot
|
||||
⍝ ⍵<p : boolean — elements less than pivot
|
||||
⍝ ∇⍵⌿⍨… : recursively sort the < partition
|
||||
⍝ , : catenate ⟨less⟩ ⟨equal⟩ ⟨greater⟩
|
||||
⍝ 1≥≢⍵:⍵ : guard — base case for length ≤ 1
|
||||
⍝
|
||||
⍝ Stability: not stable on duplicates (but eq-class is preserved as a block).
|
||||
⍝ Worst case O(N²) on already-sorted input with deterministic pivot;
|
||||
⍝ randomized pivot selection gives expected O(N log N).
|
||||
⍝
|
||||
⍝ Examples:
|
||||
⍝ Q 3 1 4 1 5 9 2 6 5 3 5 → 1 1 2 3 3 4 5 5 5 6 9
|
||||
⍝ Q ⍳0 → ⍬ (empty)
|
||||
⍝ Q ,42 → 42
|
||||
|
||||
quicksort ← {1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p}
|
||||
369
lib/apl/tests/scalar.sx
Normal file
369
lib/apl/tests/scalar.sx
Normal file
@@ -0,0 +1,369 @@
|
||||
; APL scalar primitives test suite
|
||||
; Requires: lib/apl/runtime.sx
|
||||
|
||||
; ============================================================
|
||||
; Test framework
|
||||
; ============================================================
|
||||
|
||||
(define apl-rt-count 0)
|
||||
(define apl-rt-pass 0)
|
||||
(define apl-rt-fails (list))
|
||||
|
||||
; Element-wise list comparison (handles both List and ListRef)
|
||||
(define
|
||||
lists-eq
|
||||
(fn
|
||||
(a b)
|
||||
(if
|
||||
(and (= (len a) 0) (= (len b) 0))
|
||||
true
|
||||
(if
|
||||
(not (= (len a) (len b)))
|
||||
false
|
||||
(if
|
||||
(not (= (first a) (first b)))
|
||||
false
|
||||
(lists-eq (rest a) (rest b)))))))
|
||||
|
||||
(define
|
||||
apl-rt-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(begin
|
||||
(set! apl-rt-count (+ apl-rt-count 1))
|
||||
(if
|
||||
(equal? actual expected)
|
||||
(set! apl-rt-pass (+ apl-rt-pass 1))
|
||||
(append! apl-rt-fails {:actual actual :expected expected :name name})))))
|
||||
|
||||
; Test that a ravel equals a plain list (handles ListRef vs List)
|
||||
(define
|
||||
ravel-test
|
||||
(fn
|
||||
(name arr expected-list)
|
||||
(begin
|
||||
(set! apl-rt-count (+ apl-rt-count 1))
|
||||
(let
|
||||
((actual (get arr :ravel)))
|
||||
(if
|
||||
(lists-eq actual expected-list)
|
||||
(set! apl-rt-pass (+ apl-rt-pass 1))
|
||||
(append! apl-rt-fails {:actual actual :expected expected-list :name name}))))))
|
||||
|
||||
; Test a scalar ravel value (single-element list)
|
||||
(define
|
||||
scalar-test
|
||||
(fn (name arr expected-val) (ravel-test name arr (list expected-val))))
|
||||
|
||||
; ============================================================
|
||||
; Array constructor tests
|
||||
; ============================================================
|
||||
|
||||
(apl-rt-test
|
||||
"scalar: shape is empty list"
|
||||
(get (apl-scalar 5) :shape)
|
||||
(list))
|
||||
|
||||
(apl-rt-test
|
||||
"scalar: ravel has one element"
|
||||
(get (apl-scalar 5) :ravel)
|
||||
(list 5))
|
||||
|
||||
(apl-rt-test "scalar: rank 0" (array-rank (apl-scalar 5)) 0)
|
||||
|
||||
(apl-rt-test "scalar? returns true for scalar" (scalar? (apl-scalar 5)) true)
|
||||
|
||||
(apl-rt-test "scalar: zero" (get (apl-scalar 0) :ravel) (list 0))
|
||||
|
||||
(apl-rt-test
|
||||
"vector: shape is (3)"
|
||||
(get (apl-vector (list 1 2 3)) :shape)
|
||||
(list 3))
|
||||
|
||||
(apl-rt-test
|
||||
"vector: ravel matches input"
|
||||
(get (apl-vector (list 1 2 3)) :ravel)
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-rt-test "vector: rank 1" (array-rank (apl-vector (list 1 2 3))) 1)
|
||||
|
||||
(apl-rt-test
|
||||
"scalar? returns false for vector"
|
||||
(scalar? (apl-vector (list 1 2 3)))
|
||||
false)
|
||||
|
||||
(apl-rt-test
|
||||
"make-array: rank 2"
|
||||
(array-rank (make-array (list 2 3) (list 1 2 3 4 5 6)))
|
||||
2)
|
||||
|
||||
(apl-rt-test
|
||||
"make-array: shape"
|
||||
(get (make-array (list 2 3) (list 1 2 3 4 5 6)) :shape)
|
||||
(list 2 3))
|
||||
|
||||
(apl-rt-test
|
||||
"array-ref: first element"
|
||||
(array-ref (apl-vector (list 10 20 30)) 0)
|
||||
10)
|
||||
|
||||
(apl-rt-test
|
||||
"array-ref: last element"
|
||||
(array-ref (apl-vector (list 10 20 30)) 2)
|
||||
30)
|
||||
|
||||
(apl-rt-test "enclose: wraps in rank-0" (scalar? (enclose 42)) true)
|
||||
|
||||
(apl-rt-test
|
||||
"enclose: ravel contains value"
|
||||
(get (enclose 42) :ravel)
|
||||
(list 42))
|
||||
|
||||
(apl-rt-test "disclose: unwraps rank-0" (disclose (enclose 42)) 42)
|
||||
|
||||
; ============================================================
|
||||
; Shape primitive tests
|
||||
; ============================================================
|
||||
|
||||
(ravel-test "⍴ scalar: returns empty" (apl-shape (apl-scalar 5)) (list))
|
||||
|
||||
(ravel-test
|
||||
"⍴ vector: returns (3)"
|
||||
(apl-shape (apl-vector (list 1 2 3)))
|
||||
(list 3))
|
||||
|
||||
(ravel-test
|
||||
"⍴ matrix: returns (2 3)"
|
||||
(apl-shape (make-array (list 2 3) (list 1 2 3 4 5 6)))
|
||||
(list 2 3))
|
||||
|
||||
(ravel-test
|
||||
", ravel scalar: vector of 1"
|
||||
(apl-ravel (apl-scalar 5))
|
||||
(list 5))
|
||||
|
||||
(apl-rt-test
|
||||
", ravel vector: same elements"
|
||||
(get (apl-ravel (apl-vector (list 1 2 3))) :ravel)
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-rt-test
|
||||
", ravel matrix: all elements"
|
||||
(get (apl-ravel (make-array (list 2 3) (list 1 2 3 4 5 6))) :ravel)
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(scalar-test "≢ tally scalar: 1" (apl-tally (apl-scalar 5)) 1)
|
||||
|
||||
(scalar-test
|
||||
"≢ tally vector: first dimension"
|
||||
(apl-tally (apl-vector (list 1 2 3)))
|
||||
3)
|
||||
|
||||
(scalar-test
|
||||
"≢ tally matrix: first dimension"
|
||||
(apl-tally (make-array (list 2 3) (list 1 2 3 4 5 6)))
|
||||
2)
|
||||
|
||||
(scalar-test
|
||||
"≡ depth flat vector: 0"
|
||||
(apl-depth (apl-vector (list 1 2 3)))
|
||||
0)
|
||||
|
||||
(scalar-test "≡ depth scalar: 0" (apl-depth (apl-scalar 5)) 0)
|
||||
|
||||
(scalar-test
|
||||
"≡ depth nested (enclose in vector): 1"
|
||||
(apl-depth (enclose (apl-vector (list 1 2 3))))
|
||||
1)
|
||||
|
||||
; ============================================================
|
||||
; ⍳ iota tests
|
||||
; ============================================================
|
||||
|
||||
(apl-rt-test
|
||||
"⍳5 shape is (5)"
|
||||
(get (apl-iota (apl-scalar 5)) :shape)
|
||||
(list 5))
|
||||
|
||||
(ravel-test "⍳5 ravel is 1..5" (apl-iota (apl-scalar 5)) (list 1 2 3 4 5))
|
||||
|
||||
(ravel-test "⍳1 ravel is (1)" (apl-iota (apl-scalar 1)) (list 1))
|
||||
|
||||
(ravel-test "⍳0 ravel is empty" (apl-iota (apl-scalar 0)) (list))
|
||||
|
||||
(apl-rt-test "apl-io is 1" apl-io 1)
|
||||
|
||||
; ============================================================
|
||||
; Arithmetic broadcast tests
|
||||
; ============================================================
|
||||
|
||||
(scalar-test
|
||||
"+ scalar scalar: 3+4=7"
|
||||
(apl-add (apl-scalar 3) (apl-scalar 4))
|
||||
7)
|
||||
|
||||
(ravel-test
|
||||
"+ vector scalar: +10"
|
||||
(apl-add (apl-vector (list 1 2 3)) (apl-scalar 10))
|
||||
(list 11 12 13))
|
||||
|
||||
(ravel-test
|
||||
"+ scalar vector: 10+"
|
||||
(apl-add (apl-scalar 10) (apl-vector (list 1 2 3)))
|
||||
(list 11 12 13))
|
||||
|
||||
(ravel-test
|
||||
"+ vector vector"
|
||||
(apl-add (apl-vector (list 1 2 3)) (apl-vector (list 4 5 6)))
|
||||
(list 5 7 9))
|
||||
|
||||
(scalar-test "- negate monadic" (apl-neg-m (apl-scalar 5)) -5)
|
||||
|
||||
(scalar-test "- dyadic 10-3=7" (apl-sub (apl-scalar 10) (apl-scalar 3)) 7)
|
||||
|
||||
(scalar-test "× signum positive" (apl-signum (apl-scalar 7)) 1)
|
||||
|
||||
(scalar-test "× signum negative" (apl-signum (apl-scalar -3)) -1)
|
||||
|
||||
(scalar-test "× signum zero" (apl-signum (apl-scalar 0)) 0)
|
||||
|
||||
(scalar-test "× dyadic 3×4=12" (apl-mul (apl-scalar 3) (apl-scalar 4)) 12)
|
||||
|
||||
(scalar-test "÷ reciprocal 1÷4=0.25" (apl-recip (apl-scalar 4)) 0.25)
|
||||
|
||||
(scalar-test
|
||||
"÷ dyadic 10÷4=2.5"
|
||||
(apl-div (apl-scalar 10) (apl-scalar 4))
|
||||
2.5)
|
||||
|
||||
(scalar-test "⌈ ceiling 2.3→3" (apl-ceil (apl-scalar 2.3)) 3)
|
||||
|
||||
(scalar-test "⌈ max 3 5 → 5" (apl-max (apl-scalar 3) (apl-scalar 5)) 5)
|
||||
|
||||
(scalar-test "⌊ floor 2.7→2" (apl-floor (apl-scalar 2.7)) 2)
|
||||
|
||||
(scalar-test "⌊ min 3 5 → 3" (apl-min (apl-scalar 3) (apl-scalar 5)) 3)
|
||||
|
||||
(scalar-test "* exp monadic e^0=1" (apl-exp (apl-scalar 0)) 1)
|
||||
|
||||
(scalar-test
|
||||
"* pow dyadic 2^10=1024"
|
||||
(apl-pow (apl-scalar 2) (apl-scalar 10))
|
||||
1024)
|
||||
|
||||
(scalar-test "⍟ ln 1=0" (apl-ln (apl-scalar 1)) 0)
|
||||
|
||||
(scalar-test "| abs positive" (apl-abs (apl-scalar 5)) 5)
|
||||
|
||||
(scalar-test "| abs negative" (apl-abs (apl-scalar -5)) 5)
|
||||
|
||||
(scalar-test "| mod 3|7=1" (apl-mod (apl-scalar 3) (apl-scalar 7)) 1)
|
||||
|
||||
(scalar-test "! factorial 5!=120" (apl-fact (apl-scalar 5)) 120)
|
||||
|
||||
(scalar-test "! factorial 0!=1" (apl-fact (apl-scalar 0)) 1)
|
||||
|
||||
(scalar-test
|
||||
"! binomial 4 choose 2 = 6"
|
||||
(apl-binomial (apl-scalar 4) (apl-scalar 2))
|
||||
6)
|
||||
|
||||
(scalar-test "○ pi×0=0" (apl-pi-times (apl-scalar 0)) 0)
|
||||
|
||||
(scalar-test "○ trig sin(0)=0" (apl-trig (apl-scalar 1) (apl-scalar 0)) 0)
|
||||
|
||||
(scalar-test "○ trig cos(0)=1" (apl-trig (apl-scalar 2) (apl-scalar 0)) 1)
|
||||
|
||||
; ============================================================
|
||||
; Comparison tests
|
||||
; ============================================================
|
||||
|
||||
(scalar-test "< less: 3<5 → 1" (apl-lt (apl-scalar 3) (apl-scalar 5)) 1)
|
||||
|
||||
(scalar-test "< less: 5<3 → 0" (apl-lt (apl-scalar 5) (apl-scalar 3)) 0)
|
||||
|
||||
(scalar-test
|
||||
"≤ le equal: 3≤3 → 1"
|
||||
(apl-le (apl-scalar 3) (apl-scalar 3))
|
||||
1)
|
||||
|
||||
(scalar-test "= eq: 5=5 → 1" (apl-eq (apl-scalar 5) (apl-scalar 5)) 1)
|
||||
|
||||
(scalar-test "= ne: 5=6 → 0" (apl-eq (apl-scalar 5) (apl-scalar 6)) 0)
|
||||
|
||||
(scalar-test "≥ ge: 5≥3 → 1" (apl-ge (apl-scalar 5) (apl-scalar 3)) 1)
|
||||
|
||||
(scalar-test "> gt: 5>3 → 1" (apl-gt (apl-scalar 5) (apl-scalar 3)) 1)
|
||||
|
||||
(scalar-test "≠ ne: 5≠3 → 1" (apl-ne (apl-scalar 5) (apl-scalar 3)) 1)
|
||||
|
||||
(ravel-test
|
||||
"comparison vector broadcast: 1 2 3 < 2 → 1 0 0"
|
||||
(apl-lt (apl-vector (list 1 2 3)) (apl-scalar 2))
|
||||
(list 1 0 0))
|
||||
|
||||
; ============================================================
|
||||
; Logical tests
|
||||
; ============================================================
|
||||
|
||||
(scalar-test "~ not 0 → 1" (apl-not (apl-scalar 0)) 1)
|
||||
|
||||
(scalar-test "~ not 1 → 0" (apl-not (apl-scalar 1)) 0)
|
||||
|
||||
(ravel-test
|
||||
"~ not vector: 1 0 1 0 → 0 1 0 1"
|
||||
(apl-not (apl-vector (list 1 0 1 0)))
|
||||
(list 0 1 0 1))
|
||||
|
||||
(scalar-test
|
||||
"∧ and 1∧1 → 1"
|
||||
(apl-and (apl-scalar 1) (apl-scalar 1))
|
||||
1)
|
||||
|
||||
(scalar-test
|
||||
"∧ and 1∧0 → 0"
|
||||
(apl-and (apl-scalar 1) (apl-scalar 0))
|
||||
0)
|
||||
|
||||
(scalar-test "∨ or 0∨1 → 1" (apl-or (apl-scalar 0) (apl-scalar 1)) 1)
|
||||
|
||||
(scalar-test "∨ or 0∨0 → 0" (apl-or (apl-scalar 0) (apl-scalar 0)) 0)
|
||||
|
||||
(scalar-test
|
||||
"⍱ nor 0⍱0 → 1"
|
||||
(apl-nor (apl-scalar 0) (apl-scalar 0))
|
||||
1)
|
||||
|
||||
(scalar-test
|
||||
"⍱ nor 1⍱0 → 0"
|
||||
(apl-nor (apl-scalar 1) (apl-scalar 0))
|
||||
0)
|
||||
|
||||
(scalar-test
|
||||
"⍲ nand 1⍲1 → 0"
|
||||
(apl-nand (apl-scalar 1) (apl-scalar 1))
|
||||
0)
|
||||
|
||||
(scalar-test
|
||||
"⍲ nand 1⍲0 → 1"
|
||||
(apl-nand (apl-scalar 1) (apl-scalar 0))
|
||||
1)
|
||||
|
||||
; ============================================================
|
||||
; plus-m identity test
|
||||
; ============================================================
|
||||
|
||||
(scalar-test "+ monadic identity: +5 → 5" (apl-plus-m (apl-scalar 5)) 5)
|
||||
|
||||
; ============================================================
|
||||
; Summary
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
apl-scalar-summary
|
||||
(str
|
||||
"scalar "
|
||||
apl-rt-pass
|
||||
"/"
|
||||
apl-rt-count
|
||||
(if (= (len apl-rt-fails) 0) "" (str " FAILS: " apl-rt-fails))))
|
||||
608
lib/apl/tests/structural.sx
Normal file
608
lib/apl/tests/structural.sx
Normal file
@@ -0,0 +1,608 @@
|
||||
;; lib/apl/tests/structural.sx — Phase 3: structural primitives
|
||||
;; Tests for: apl-reshape, apl-ravel, apl-transpose, apl-transpose-dyadic
|
||||
;; Loaded after runtime.sx; shares apl-test / apl-test-pass / apl-test-fail.
|
||||
|
||||
(define rv (fn (arr) (get arr :ravel)))
|
||||
(define sh (fn (arr) (get arr :shape)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Ravel (monadic ,)
|
||||
;; ---------------------------------------------------------------------------
|
||||
(apl-test "ravel scalar" (rv (apl-ravel (apl-scalar 5))) (list 5))
|
||||
|
||||
(apl-test
|
||||
"ravel vector"
|
||||
(rv (apl-ravel (make-array (list 3) (list 1 2 3))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"ravel matrix"
|
||||
(rv (apl-ravel (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"ravel shape is rank-1"
|
||||
(sh (apl-ravel (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 6))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Reshape (dyadic ⍴)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"reshape 2x3 ravel"
|
||||
(rv
|
||||
(apl-reshape
|
||||
(make-array (list 2) (list 2 3))
|
||||
(make-array (list 6) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"reshape 2x3 shape"
|
||||
(sh
|
||||
(apl-reshape
|
||||
(make-array (list 2) (list 2 3))
|
||||
(make-array (list 6) (list 1 2 3 4 5 6))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"reshape cycle 6 from 1 2"
|
||||
(rv
|
||||
(apl-reshape
|
||||
(make-array (list 1) (list 6))
|
||||
(make-array (list 2) (list 1 2))))
|
||||
(list 1 2 1 2 1 2))
|
||||
|
||||
(apl-test
|
||||
"reshape cycle 2x3 from 1 2"
|
||||
(rv
|
||||
(apl-reshape
|
||||
(make-array (list 2) (list 2 3))
|
||||
(make-array (list 2) (list 1 2))))
|
||||
(list 1 2 1 2 1 2))
|
||||
|
||||
(apl-test
|
||||
"reshape scalar fill"
|
||||
(rv (apl-reshape (make-array (list 1) (list 4)) (apl-scalar 7)))
|
||||
(list 7 7 7 7))
|
||||
|
||||
(apl-test
|
||||
"reshape truncate"
|
||||
(rv
|
||||
(apl-reshape
|
||||
(make-array (list 1) (list 3))
|
||||
(make-array (list 6) (list 10 20 30 40 50 60))))
|
||||
(list 10 20 30))
|
||||
|
||||
(apl-test
|
||||
"reshape matrix to vector"
|
||||
(sh
|
||||
(apl-reshape
|
||||
(make-array (list 1) (list 6))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 6))
|
||||
|
||||
(apl-test
|
||||
"reshape 2x2x3"
|
||||
(sh
|
||||
(apl-reshape
|
||||
(make-array (list 3) (list 2 2 3))
|
||||
(make-array (list 12) (range 1 13))))
|
||||
(list 2 2 3))
|
||||
|
||||
(apl-test
|
||||
"reshape to empty"
|
||||
(rv
|
||||
(apl-reshape
|
||||
(make-array (list 1) (list 0))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Monadic transpose (⍉)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"transpose scalar shape"
|
||||
(sh (apl-transpose (apl-scalar 99)))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"transpose scalar ravel"
|
||||
(rv (apl-transpose (apl-scalar 99)))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"transpose vector shape"
|
||||
(sh (apl-transpose (make-array (list 3) (list 3 1 4))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"transpose vector ravel"
|
||||
(rv (apl-transpose (make-array (list 3) (list 3 1 4))))
|
||||
(list 3 1 4))
|
||||
|
||||
(apl-test
|
||||
"transpose 2x3 shape"
|
||||
(sh (apl-transpose (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3 2))
|
||||
|
||||
(apl-test
|
||||
"transpose 2x3 ravel"
|
||||
(rv (apl-transpose (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 4 2 5 3 6))
|
||||
|
||||
(apl-test
|
||||
"transpose 3x3"
|
||||
(rv (apl-transpose (make-array (list 3 3) (list 1 2 3 4 5 6 7 8 9))))
|
||||
(list 1 4 7 2 5 8 3 6 9))
|
||||
|
||||
(apl-test
|
||||
"transpose 1x4 shape"
|
||||
(sh (apl-transpose (make-array (list 1 4) (list 1 2 3 4))))
|
||||
(list 4 1))
|
||||
|
||||
(apl-test
|
||||
"transpose twice identity"
|
||||
(rv
|
||||
(apl-transpose
|
||||
(apl-transpose (make-array (list 2 3) (list 1 2 3 4 5 6)))))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"transpose 3d shape"
|
||||
(sh (apl-transpose (make-array (list 2 3 4) (range 0 24))))
|
||||
(list 4 3 2))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Dyadic transpose (perm⍉arr)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"dyadic-transpose identity"
|
||||
(rv
|
||||
(apl-transpose-dyadic
|
||||
(make-array (list 2) (list 1 2))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"dyadic-transpose swap 2x3"
|
||||
(rv
|
||||
(apl-transpose-dyadic
|
||||
(make-array (list 2) (list 2 1))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 4 2 5 3 6))
|
||||
|
||||
(apl-test
|
||||
"dyadic-transpose swap shape"
|
||||
(sh
|
||||
(apl-transpose-dyadic
|
||||
(make-array (list 2) (list 2 1))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3 2))
|
||||
|
||||
(apl-test
|
||||
"dyadic-transpose 3d shape"
|
||||
(sh
|
||||
(apl-transpose-dyadic
|
||||
(make-array (list 3) (list 2 1 3))
|
||||
(make-array (list 2 3 4) (range 0 24))))
|
||||
(list 3 2 4))
|
||||
|
||||
(apl-test
|
||||
"take 3 from front"
|
||||
(rv (apl-take (apl-scalar 3) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"take 0"
|
||||
(rv (apl-take (apl-scalar 0) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"take -2 from back"
|
||||
(rv (apl-take (apl-scalar -2) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 4 5))
|
||||
|
||||
(apl-test
|
||||
"take over-take pads with 0"
|
||||
(rv (apl-take (apl-scalar 7) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3 4 5 0 0))
|
||||
|
||||
(apl-test
|
||||
"take matrix 1 row 2 cols shape"
|
||||
(sh
|
||||
(apl-take
|
||||
(make-array (list 2) (list 1 2))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2))
|
||||
|
||||
(apl-test
|
||||
"take matrix 1 row 2 cols ravel"
|
||||
(rv
|
||||
(apl-take
|
||||
(make-array (list 2) (list 1 2))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2))
|
||||
|
||||
(apl-test
|
||||
"take matrix negative row"
|
||||
(rv
|
||||
(apl-take
|
||||
(make-array (list 2) (list -1 3))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"drop 2 from front"
|
||||
(rv (apl-drop (apl-scalar 2) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"drop -2 from back"
|
||||
(rv (apl-drop (apl-scalar -2) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"drop all"
|
||||
(rv (apl-drop (apl-scalar 5) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"drop 0"
|
||||
(rv (apl-drop (apl-scalar 0) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"drop matrix 1 row shape"
|
||||
(sh
|
||||
(apl-drop
|
||||
(make-array (list 2) (list 1 0))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 3))
|
||||
|
||||
(apl-test
|
||||
"drop matrix 1 row ravel"
|
||||
(rv
|
||||
(apl-drop
|
||||
(make-array (list 2) (list 1 0))
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 4 5 6))
|
||||
|
||||
(apl-test
|
||||
"reverse vector"
|
||||
(rv (apl-reverse (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 5 4 3 2 1))
|
||||
|
||||
(apl-test
|
||||
"reverse scalar identity"
|
||||
(rv (apl-reverse (apl-scalar 42)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"reverse matrix last axis"
|
||||
(rv (apl-reverse (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 3 2 1 6 5 4))
|
||||
|
||||
(apl-test
|
||||
"reverse-first matrix"
|
||||
(rv (apl-reverse-first (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 4 5 6 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"reverse-first vector identity"
|
||||
(rv (apl-reverse-first (make-array (list 4) (list 1 2 3 4))))
|
||||
(list 4 3 2 1))
|
||||
|
||||
(apl-test
|
||||
"rotate vector left by 2"
|
||||
(rv (apl-rotate (apl-scalar 2) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 3 4 5 1 2))
|
||||
|
||||
(apl-test
|
||||
"rotate vector right by 1 (negative)"
|
||||
(rv (apl-rotate (apl-scalar -1) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 5 1 2 3 4))
|
||||
|
||||
(apl-test
|
||||
"rotate by 0 is identity"
|
||||
(rv (apl-rotate (apl-scalar 0) (make-array (list 5) (list 1 2 3 4 5))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"rotate matrix last axis"
|
||||
(rv
|
||||
(apl-rotate (apl-scalar 1) (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 2 3 1 5 6 4))
|
||||
|
||||
(apl-test
|
||||
"rotate-first matrix"
|
||||
(rv
|
||||
(apl-rotate-first
|
||||
(apl-scalar 1)
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 4 5 6 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"cat v,v ravel"
|
||||
(rv
|
||||
(apl-catenate
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 2) (list 4 5))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(apl-test
|
||||
"cat v,v shape"
|
||||
(sh
|
||||
(apl-catenate
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 2) (list 4 5))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"cat scalar,v"
|
||||
(rv (apl-catenate (apl-scalar 99) (make-array (list 3) (list 1 2 3))))
|
||||
(list 99 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"cat v,scalar"
|
||||
(rv (apl-catenate (make-array (list 3) (list 1 2 3)) (apl-scalar 99)))
|
||||
(list 1 2 3 99))
|
||||
|
||||
(apl-test
|
||||
"cat matrix last-axis shape"
|
||||
(sh
|
||||
(apl-catenate
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 2 2) (list 7 8 9 10))))
|
||||
(list 2 5))
|
||||
|
||||
(apl-test
|
||||
"cat matrix last-axis ravel"
|
||||
(rv
|
||||
(apl-catenate
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 2 2) (list 7 8 9 10))))
|
||||
(list 1 2 3 7 8 4 5 6 9 10))
|
||||
|
||||
(apl-test
|
||||
"cat-first v,v shape"
|
||||
(sh
|
||||
(apl-catenate-first
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 2) (list 4 5))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"cat-first matrix shape"
|
||||
(sh
|
||||
(apl-catenate-first
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3 3) (list 11 12 13 14 15 16 17 18 19))))
|
||||
(list 5 3))
|
||||
|
||||
(apl-test
|
||||
"cat-first matrix ravel"
|
||||
(rv
|
||||
(apl-catenate-first
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3 3) (list 11 12 13 14 15 16 17 18 19))))
|
||||
(list 1 2 3 4 5 6 11 12 13 14 15 16 17 18 19))
|
||||
|
||||
(apl-test
|
||||
"squad scalar into vector"
|
||||
(rv
|
||||
(apl-squad (apl-scalar 2) (make-array (list 5) (list 10 20 30 40 50))))
|
||||
(list 20))
|
||||
|
||||
(apl-test
|
||||
"squad first element"
|
||||
(rv (apl-squad (apl-scalar 1) (make-array (list 3) (list 10 20 30))))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"squad last element"
|
||||
(rv
|
||||
(apl-squad (apl-scalar 5) (make-array (list 5) (list 10 20 30 40 50))))
|
||||
(list 50))
|
||||
|
||||
(apl-test
|
||||
"squad fully specified matrix element"
|
||||
(rv
|
||||
(apl-squad
|
||||
(make-array (list 2) (list 2 3))
|
||||
(make-array (list 3 4) (list 1 2 3 4 5 6 7 8 9 10 11 12))))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"squad partial row of matrix shape"
|
||||
(sh
|
||||
(apl-squad
|
||||
(apl-scalar 2)
|
||||
(make-array (list 3 4) (list 1 2 3 4 5 6 7 8 9 10 11 12))))
|
||||
(list 4))
|
||||
|
||||
(apl-test
|
||||
"squad partial row of matrix ravel"
|
||||
(rv
|
||||
(apl-squad
|
||||
(apl-scalar 2)
|
||||
(make-array (list 3 4) (list 1 2 3 4 5 6 7 8 9 10 11 12))))
|
||||
(list 5 6 7 8))
|
||||
|
||||
(apl-test
|
||||
"squad partial 3d slice shape"
|
||||
(sh (apl-squad (apl-scalar 1) (make-array (list 2 3 4) (range 1 25))))
|
||||
(list 3 4))
|
||||
|
||||
(apl-test
|
||||
"grade-up basic"
|
||||
(rv (apl-grade-up (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 2 4 1 3 5))
|
||||
|
||||
(apl-test
|
||||
"grade-up shape"
|
||||
(sh (apl-grade-up (make-array (list 4) (list 4 1 3 2))))
|
||||
(list 4))
|
||||
|
||||
(apl-test
|
||||
"grade-up no duplicates"
|
||||
(rv (apl-grade-up (make-array (list 4) (list 4 1 3 2))))
|
||||
(list 2 4 3 1))
|
||||
|
||||
(apl-test
|
||||
"grade-up already sorted"
|
||||
(rv (apl-grade-up (make-array (list 3) (list 1 2 3))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"grade-up reverse sorted"
|
||||
(rv (apl-grade-up (make-array (list 3) (list 3 2 1))))
|
||||
(list 3 2 1))
|
||||
|
||||
(apl-test
|
||||
"grade-down basic"
|
||||
(rv (apl-grade-down (make-array (list 5) (list 3 1 4 1 5))))
|
||||
(list 5 3 1 2 4))
|
||||
|
||||
(apl-test
|
||||
"grade-down no duplicates"
|
||||
(rv (apl-grade-down (make-array (list 4) (list 4 1 3 2))))
|
||||
(list 1 3 4 2))
|
||||
|
||||
(apl-test
|
||||
"grade-up single element"
|
||||
(rv (apl-grade-up (make-array (list 1) (list 42))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"enclose shape is scalar"
|
||||
(sh (apl-enclose (make-array (list 3) (list 1 2 3))))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"enclose ravel length is 1"
|
||||
(len (rv (apl-enclose (make-array (list 3) (list 1 2 3)))))
|
||||
1)
|
||||
|
||||
(apl-test
|
||||
"enclose inner ravel"
|
||||
(rv (first (rv (apl-enclose (make-array (list 3) (list 1 2 3))))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"disclose of enclose round-trips ravel"
|
||||
(rv (apl-disclose (apl-enclose (make-array (list 3) (list 10 20 30)))))
|
||||
(list 10 20 30))
|
||||
|
||||
(apl-test
|
||||
"disclose of enclose round-trips shape"
|
||||
(sh (apl-disclose (apl-enclose (make-array (list 3) (list 10 20 30)))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"disclose scalar ravel"
|
||||
(rv (apl-disclose (apl-scalar 42)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"disclose vector ravel"
|
||||
(rv (apl-disclose (make-array (list 3) (list 5 6 7))))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"disclose matrix returns first row"
|
||||
(rv (apl-disclose (make-array (list 2 3) (list 1 2 3 4 5 6))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"member basic"
|
||||
(rv
|
||||
(apl-member
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 2) (list 2 3))))
|
||||
(list 0 1 1))
|
||||
|
||||
(apl-test
|
||||
"member all absent"
|
||||
(rv
|
||||
(apl-member
|
||||
(make-array (list 3) (list 4 5 6))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list 0 0 0))
|
||||
|
||||
(apl-test
|
||||
"member scalar"
|
||||
(rv (apl-member (apl-scalar 5) (make-array (list 3) (list 1 5 9))))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"member shape preserved"
|
||||
(sh
|
||||
(apl-member
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3) (list 1 3 5))))
|
||||
(list 2 3))
|
||||
|
||||
(apl-test
|
||||
"member matrix ravel"
|
||||
(rv
|
||||
(apl-member
|
||||
(make-array (list 2 3) (list 1 2 3 4 5 6))
|
||||
(make-array (list 3) (list 1 3 5))))
|
||||
(list 1 0 1 0 1 0))
|
||||
|
||||
(apl-test
|
||||
"index-of basic"
|
||||
(rv
|
||||
(apl-index-of
|
||||
(make-array (list 4) (list 10 20 30 40))
|
||||
(make-array (list 3) (list 20 40 10))))
|
||||
(list 2 4 1))
|
||||
|
||||
(apl-test
|
||||
"index-of not-found"
|
||||
(rv
|
||||
(apl-index-of
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 2) (list 5 2))))
|
||||
(list 4 2))
|
||||
|
||||
(apl-test
|
||||
"index-of scalar right"
|
||||
(rv
|
||||
(apl-index-of (make-array (list 3) (list 10 20 30)) (apl-scalar 20)))
|
||||
(list 2))
|
||||
|
||||
(apl-test
|
||||
"without basic"
|
||||
(rv
|
||||
(apl-without
|
||||
(make-array (list 5) (list 1 2 3 4 5))
|
||||
(make-array (list 2) (list 2 4))))
|
||||
(list 1 3 5))
|
||||
|
||||
(apl-test
|
||||
"without shape"
|
||||
(sh
|
||||
(apl-without
|
||||
(make-array (list 5) (list 1 2 3 4 5))
|
||||
(make-array (list 2) (list 2 4))))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"without nothing removed"
|
||||
(rv
|
||||
(apl-without
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 4 5 6))))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"without all removed"
|
||||
(rv
|
||||
(apl-without
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 1 2 3))))
|
||||
(list))
|
||||
48
lib/apl/tests/system.sx
Normal file
48
lib/apl/tests/system.sx
Normal file
@@ -0,0 +1,48 @@
|
||||
; Tests for APL ⎕ system functions.
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
|
||||
(apl-test "⎕IO returns 1" (mkrv (apl-quad-io)) (list 1))
|
||||
|
||||
(apl-test "⎕ML returns 1" (mkrv (apl-quad-ml)) (list 1))
|
||||
|
||||
(apl-test "⎕FR returns 1248" (mkrv (apl-quad-fr)) (list 1248))
|
||||
|
||||
(apl-test "⎕TS shape is 7" (mksh (apl-quad-ts)) (list 7))
|
||||
|
||||
(apl-test "⎕TS year is 1970 default" (first (mkrv (apl-quad-ts))) 1970)
|
||||
|
||||
(apl-test "⎕FMT scalar 42" (apl-quad-fmt (apl-scalar 42)) "42")
|
||||
|
||||
(apl-test "⎕FMT scalar negative" (apl-quad-fmt (apl-scalar -7)) "-7")
|
||||
|
||||
(apl-test
|
||||
"⎕FMT empty vector"
|
||||
(apl-quad-fmt (make-array (list 0) (list)))
|
||||
"")
|
||||
|
||||
(apl-test
|
||||
"⎕FMT singleton vector"
|
||||
(apl-quad-fmt (make-array (list 1) (list 42)))
|
||||
"42")
|
||||
|
||||
(apl-test
|
||||
"⎕FMT vector"
|
||||
(apl-quad-fmt (make-array (list 5) (list 1 2 3 4 5)))
|
||||
"1 2 3 4 5")
|
||||
|
||||
(apl-test
|
||||
"⎕FMT matrix 2x3"
|
||||
(apl-quad-fmt (make-array (list 2 3) (list 1 2 3 4 5 6)))
|
||||
"1 2 3\n4 5 6\n")
|
||||
|
||||
(apl-test
|
||||
"⎕← (print) returns its arg"
|
||||
(mkrv (apl-quad-print (apl-scalar 99)))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"⎕← preserves shape"
|
||||
(mksh (apl-quad-print (make-array (list 3) (list 1 2 3))))
|
||||
(list 3))
|
||||
156
lib/apl/tests/tradfn.sx
Normal file
156
lib/apl/tests/tradfn.sx
Normal file
@@ -0,0 +1,156 @@
|
||||
; Tests for apl-call-tradfn (manual structure construction).
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
(define mknum (fn (n) (list :num n)))
|
||||
(define mknm (fn (s) (list :name s)))
|
||||
(define mkfg (fn (g) (list :fn-glyph g)))
|
||||
(define mkmon (fn (g a) (list :monad (mkfg g) a)))
|
||||
(define mkdyd (fn (g l r) (list :dyad (mkfg g) l r)))
|
||||
(define mkasg (fn (n e) (list :assign n e)))
|
||||
(define mkbr (fn (e) (list :branch e)))
|
||||
|
||||
(define mkif (fn (c t e) (list :if c t e)))
|
||||
|
||||
(define mkwhile (fn (c b) (list :while c b)))
|
||||
|
||||
(define mkfor (fn (v i b) (list :for v i b)))
|
||||
|
||||
(define mksel (fn (v cs d) (list :select v cs d)))
|
||||
|
||||
(define mktrap (fn (codes t c) (list :trap codes t c)))
|
||||
|
||||
(define mkthr (fn (code msg) (list :throw code msg)))
|
||||
|
||||
(apl-test
|
||||
"tradfn R←L+W simple add"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mkdyd "+" (mknm "L") (mknm "W")))) :alpha "L"} (apl-scalar 5) (apl-scalar 7)))
|
||||
(list 12))
|
||||
|
||||
(apl-test
|
||||
"tradfn R←L×W"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mkdyd "×" (mknm "L") (mknm "W")))) :alpha "L"} (apl-scalar 6) (apl-scalar 7)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"tradfn monadic R←-W"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mkmon "-" (mknm "W")))) :alpha nil} nil (apl-scalar 9)))
|
||||
(list -9))
|
||||
|
||||
(apl-test
|
||||
"tradfn →0 exits early"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mknm "W")) (mkbr (mknum 0)) (mkasg "R" (mknum 999))) :alpha nil} nil (apl-scalar 7)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"tradfn branch to line 3 skips line 2"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkbr (mknum 3)) (mkasg "R" (mknum 999)) (mkasg "R" (mknum 42))) :alpha nil} nil (apl-scalar 0)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"tradfn local var t←W+1; R←t×2"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "t" (mkdyd "+" (mknm "W") (mknum 1))) (mkasg "R" (mkdyd "×" (mknm "t") (mknum 2)))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 12))
|
||||
|
||||
(apl-test
|
||||
"tradfn vector args"
|
||||
(mkrv
|
||||
(apl-call-tradfn
|
||||
{:result "R" :omega "W" :stmts (list (mkasg "R" (mkdyd "+" (mknm "L") (mknm "W")))) :alpha "L"}
|
||||
(make-array (list 3) (list 1 2 3))
|
||||
(make-array (list 3) (list 10 20 30))))
|
||||
(list 11 22 33))
|
||||
|
||||
(apl-test
|
||||
"tradfn unset result returns nil"
|
||||
(apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkbr (mknum 0))) :alpha nil} nil (apl-scalar 5))
|
||||
nil)
|
||||
|
||||
(apl-test
|
||||
"tradfn run-off end returns result"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mkdyd "×" (mknm "W") (mknum 3)))) :alpha nil} nil (apl-scalar 7)))
|
||||
(list 21))
|
||||
|
||||
(apl-test
|
||||
"tradfn loop sum 1+2+...+5 via branch"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "i" (mknum 1)) (mkasg "R" (mknum 0)) (mkasg "R" (mkdyd "+" (mknm "R") (mknm "i"))) (mkasg "i" (mkdyd "+" (mknm "i") (mknum 1))) (mkbr (mkdyd "×" (mkdyd "≤" (mknm "i") (mknm "W")) (mknum 3)))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"tradfn :If true branch"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkif (mkdyd ">" (mknm "W") (mknum 0)) (list (mkasg "R" (mknum 1))) (list (mkasg "R" (mknum 0))))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"tradfn :If false branch"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkif (mkdyd ">" (mknm "W") (mknum 100)) (list (mkasg "R" (mknum 1))) (list (mkasg "R" (mknum 0))))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 0))
|
||||
|
||||
(apl-test
|
||||
"tradfn :While sum 1..N"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "i" (mknum 1)) (mkasg "R" (mknum 0)) (mkwhile (mkdyd "≤" (mknm "i") (mknm "W")) (list (mkasg "R" (mkdyd "+" (mknm "R") (mknm "i"))) (mkasg "i" (mkdyd "+" (mknm "i") (mknum 1)))))) :alpha nil} nil (apl-scalar 10)))
|
||||
(list 55))
|
||||
|
||||
(apl-test
|
||||
"tradfn :For sum elements"
|
||||
(mkrv
|
||||
(apl-call-tradfn
|
||||
{:result "R" :omega "W" :stmts (list (mkasg "R" (mknum 0)) (mkfor "x" (mknm "W") (list (mkasg "R" (mkdyd "+" (mknm "R") (mknm "x")))))) :alpha nil}
|
||||
nil
|
||||
(make-array (list 4) (list 10 20 30 40))))
|
||||
(list 100))
|
||||
|
||||
(apl-test
|
||||
"tradfn :For with empty vector"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mknum 99)) (mkfor "x" (mknm "W") (list (mkasg "R" (mkdyd "+" (mknm "R") (mknm "x")))))) :alpha nil} nil (make-array (list 0) (list))))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Select dispatch hit"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mksel (mknm "W") (list (list (mknum 1) (mkasg "R" (mknum 100))) (list (mknum 2) (mkasg "R" (mknum 200))) (list (mknum 3) (mkasg "R" (mknum 300)))) (list (mkasg "R" (mknum 0))))) :alpha nil} nil (apl-scalar 2)))
|
||||
(list 200))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Select default block"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mksel (mknm "W") (list (list (mknum 1) (mkasg "R" (mknum 100))) (list (mknum 2) (mkasg "R" (mknum 200)))) (list (mkasg "R" (mknum -1))))) :alpha nil} nil (apl-scalar 99)))
|
||||
(list -1))
|
||||
|
||||
(apl-test
|
||||
"tradfn nested :If"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkif (mkdyd ">" (mknm "W") (mknum 0)) (list (mkif (mkdyd ">" (mknm "W") (mknum 10)) (list (mkasg "R" (mknum 2))) (list (mkasg "R" (mknum 1))))) (list (mkasg "R" (mknum 0))))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"tradfn :If assigns persist outside"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mknum 0)) (mkif (mkdyd ">" (mknm "W") (mknum 0)) (list (mkasg "R" (mknum 42))) (list)) (mkasg "R" (mkdyd "+" (mknm "R") (mknum 1)))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 43))
|
||||
|
||||
(apl-test
|
||||
"tradfn :For factorial 1..5"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega "W" :stmts (list (mkasg "R" (mknum 1)) (mkfor "x" (mkmon "⍳" (mknm "W")) (list (mkasg "R" (mkdyd "×" (mknm "R") (mknm "x")))))) :alpha nil} nil (apl-scalar 5)))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Trap normal flow (no error)"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega nil :stmts (list (mktrap (list 0) (list (mkasg "R" (mknum 99))) (list (mkasg "R" (mknum -1))))) :alpha nil} nil nil))
|
||||
(list 99))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Trap catches matching code"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega nil :stmts (list (mktrap (list 5) (list (mkthr 5 "boom")) (list (mkasg "R" (mknum 42))))) :alpha nil} nil nil))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Trap catch-all (code 0)"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega nil :stmts (list (mktrap (list 0) (list (mkthr 99 "any")) (list (mkasg "R" (mknum 1))))) :alpha nil} nil nil))
|
||||
(list 1))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Trap catches one of many codes"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega nil :stmts (list (mktrap (list 1 2 3) (list (mkthr 2 "two")) (list (mkasg "R" (mknum 22))))) :alpha nil} nil nil))
|
||||
(list 22))
|
||||
|
||||
(apl-test
|
||||
"tradfn :Trap continues to next stmt after catch"
|
||||
(mkrv (apl-call-tradfn {:result "R" :omega nil :stmts (list (mktrap (list 7) (list (mkthr 7 "c")) (list (mkasg "R" (mknum 10)))) (mkasg "R" (mkdyd "+" (mknm "R") (mknum 5)))) :alpha nil} nil nil))
|
||||
(list 15))
|
||||
81
lib/apl/tests/valence.sx
Normal file
81
lib/apl/tests/valence.sx
Normal file
@@ -0,0 +1,81 @@
|
||||
; Tests for valence detection (apl-dfn-valence, apl-tradfn-valence)
|
||||
; and unified dispatch (apl-call).
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mknum (fn (n) (list :num n)))
|
||||
(define mknm (fn (s) (list :name s)))
|
||||
(define mkfg (fn (g) (list :fn-glyph g)))
|
||||
(define mkmon (fn (g a) (list :monad (mkfg g) a)))
|
||||
(define mkdyd (fn (g l r) (list :dyad (mkfg g) l r)))
|
||||
(define mkasg (fn (n e) (list :assign n e)))
|
||||
(define mkdfn (fn (stmts) (cons :dfn stmts)))
|
||||
|
||||
(apl-test
|
||||
"dfn-valence niladic body=42"
|
||||
(apl-dfn-valence (mkdfn (list (mknum 42))))
|
||||
:niladic)
|
||||
|
||||
(apl-test
|
||||
"dfn-valence monadic body=⍵+1"
|
||||
(apl-dfn-valence (mkdfn (list (mkdyd "+" (mknm "⍵") (mknum 1)))))
|
||||
:monadic)
|
||||
|
||||
(apl-test
|
||||
"dfn-valence dyadic body=⍺+⍵"
|
||||
(apl-dfn-valence (mkdfn (list (mkdyd "+" (mknm "⍺") (mknm "⍵")))))
|
||||
:dyadic)
|
||||
|
||||
(apl-test
|
||||
"dfn-valence dyadic mentions ⍺ via local"
|
||||
(apl-dfn-valence (mkdfn (list (mkasg "x" (mknm "⍺")) (mknm "x"))))
|
||||
:dyadic)
|
||||
|
||||
(apl-test
|
||||
"dfn-valence dyadic deep nest"
|
||||
(apl-dfn-valence
|
||||
(mkdfn (list (mkmon "-" (mkdyd "×" (mknm "⍺") (mknm "⍵"))))))
|
||||
:dyadic)
|
||||
|
||||
(apl-test "tradfn-valence niladic" (apl-tradfn-valence {:result "R" :omega nil :stmts (list) :alpha nil}) :niladic)
|
||||
|
||||
(apl-test "tradfn-valence monadic" (apl-tradfn-valence {:result "R" :omega "W" :stmts (list) :alpha nil}) :monadic)
|
||||
|
||||
(apl-test "tradfn-valence dyadic" (apl-tradfn-valence {:result "R" :omega "W" :stmts (list) :alpha "L"}) :dyadic)
|
||||
|
||||
(apl-test
|
||||
"apl-call dfn niladic"
|
||||
(mkrv (apl-call (mkdfn (list (mknum 42))) nil nil))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"apl-call dfn monadic"
|
||||
(mkrv
|
||||
(apl-call
|
||||
(mkdfn (list (mkdyd "+" (mknm "⍵") (mknum 1))))
|
||||
nil
|
||||
(apl-scalar 5)))
|
||||
(list 6))
|
||||
|
||||
(apl-test
|
||||
"apl-call dfn dyadic"
|
||||
(mkrv
|
||||
(apl-call
|
||||
(mkdfn (list (mkdyd "+" (mknm "⍺") (mknm "⍵"))))
|
||||
(apl-scalar 3)
|
||||
(apl-scalar 4)))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"apl-call tradfn dyadic"
|
||||
(mkrv (apl-call {:result "R" :omega "W" :stmts (list (mkasg "R" (mkdyd "×" (mknm "L") (mknm "W")))) :alpha "L"} (apl-scalar 6) (apl-scalar 7)))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"apl-call tradfn monadic"
|
||||
(mkrv (apl-call {:result "R" :omega "W" :stmts (list (mkasg "R" (mkmon "-" (mknm "W")))) :alpha nil} nil (apl-scalar 9)))
|
||||
(list -9))
|
||||
|
||||
(apl-test
|
||||
"apl-call tradfn niladic returns nil result"
|
||||
(apl-call {:result "R" :omega nil :stmts (list) :alpha nil} nil nil)
|
||||
nil)
|
||||
180
lib/apl/tokenizer.sx
Normal file
180
lib/apl/tokenizer.sx
Normal file
@@ -0,0 +1,180 @@
|
||||
(define apl-glyph-set
|
||||
(list "+" "-" "×" "÷" "*" "⍟" "⌈" "⌊" "|" "!" "?" "○" "~" "<" "≤" "=" "≥" ">" "≠"
|
||||
"≢" "≡" "∊" "∧" "∨" "⍱" "⍲" "," "⍪" "⍴" "⌽" "⊖" "⍉" "↑" "↓" "⊂" "⊃" "⊆"
|
||||
"∪" "∩" "⍳" "⍸" "⌷" "⍋" "⍒" "⊥" "⊤" "⊣" "⊢" "⍎" "⍕"
|
||||
"⍺" "⍵" "∇" "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@" "¯"))
|
||||
|
||||
(define apl-glyph?
|
||||
(fn (ch)
|
||||
(some (fn (g) (= g ch)) apl-glyph-set)))
|
||||
|
||||
(define apl-digit?
|
||||
(fn (ch)
|
||||
(and (string? ch) (>= ch "0") (<= ch "9"))))
|
||||
|
||||
(define apl-alpha?
|
||||
(fn (ch)
|
||||
(and (string? ch)
|
||||
(or (and (>= ch "a") (<= ch "z"))
|
||||
(and (>= ch "A") (<= ch "Z"))
|
||||
(= ch "_")))))
|
||||
|
||||
(define apl-tokenize
|
||||
(fn (source)
|
||||
(let ((pos 0)
|
||||
(src-len (len source))
|
||||
(tokens (list)))
|
||||
|
||||
(define tok-push!
|
||||
(fn (type value)
|
||||
(append! tokens {:type type :value value})))
|
||||
|
||||
(define cur-sw?
|
||||
(fn (ch)
|
||||
(and (< pos src-len) (starts-with? (slice source pos) ch))))
|
||||
|
||||
(define cur-byte
|
||||
(fn ()
|
||||
(if (< pos src-len) (nth source pos) nil)))
|
||||
|
||||
(define advance!
|
||||
(fn ()
|
||||
(set! pos (+ pos 1))))
|
||||
|
||||
(define consume!
|
||||
(fn (ch)
|
||||
(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)))))
|
||||
|
||||
(define read-digits!
|
||||
(fn (acc)
|
||||
(if (and (< pos src-len) (apl-digit? (cur-byte)))
|
||||
(let ((ch (cur-byte)))
|
||||
(begin
|
||||
(advance!)
|
||||
(read-digits! (str acc ch))))
|
||||
acc)))
|
||||
|
||||
(define read-ident-cont!
|
||||
(fn ()
|
||||
(when (and (< pos src-len)
|
||||
(let ((ch (cur-byte)))
|
||||
(or (apl-alpha? ch) (apl-digit? ch))))
|
||||
(begin
|
||||
(advance!)
|
||||
(read-ident-cont!)))))
|
||||
|
||||
(define read-string!
|
||||
(fn (acc)
|
||||
(cond
|
||||
((>= pos src-len) acc)
|
||||
((cur-sw? "'")
|
||||
(if (and (< (+ pos 1) src-len) (cur-sw? "'"))
|
||||
(begin
|
||||
(advance!)
|
||||
(advance!)
|
||||
(read-string! (str acc "'")))
|
||||
(begin (advance!) acc)))
|
||||
(true
|
||||
(let ((ch (cur-byte)))
|
||||
(begin
|
||||
(advance!)
|
||||
(read-string! (str acc ch))))))))
|
||||
|
||||
(define skip-line!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (not (cur-sw? "\n")))
|
||||
(begin
|
||||
(advance!)
|
||||
(skip-line!)))))
|
||||
|
||||
(define scan!
|
||||
(fn ()
|
||||
(when (< pos src-len)
|
||||
(let ((ch (cur-byte)))
|
||||
(cond
|
||||
((or (= ch " ") (= ch "\t") (= ch "\r"))
|
||||
(begin (advance!) (scan!)))
|
||||
((= ch "\n")
|
||||
(begin (advance!) (tok-push! :newline nil) (scan!)))
|
||||
((cur-sw? "⍝")
|
||||
(begin (skip-line!) (scan!)))
|
||||
((cur-sw? "⋄")
|
||||
(begin (consume! "⋄") (tok-push! :diamond nil) (scan!)))
|
||||
((= ch "(")
|
||||
(begin (advance!) (tok-push! :lparen nil) (scan!)))
|
||||
((= ch ")")
|
||||
(begin (advance!) (tok-push! :rparen nil) (scan!)))
|
||||
((= ch "[")
|
||||
(begin (advance!) (tok-push! :lbracket nil) (scan!)))
|
||||
((= ch "]")
|
||||
(begin (advance!) (tok-push! :rbracket nil) (scan!)))
|
||||
((= ch "{")
|
||||
(begin (advance!) (tok-push! :lbrace nil) (scan!)))
|
||||
((= ch "}")
|
||||
(begin (advance!) (tok-push! :rbrace nil) (scan!)))
|
||||
((= ch ";")
|
||||
(begin (advance!) (tok-push! :semi nil) (scan!)))
|
||||
((cur-sw? "←")
|
||||
(begin (consume! "←") (tok-push! :assign nil) (scan!)))
|
||||
((= ch ":")
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(advance!)
|
||||
(if (and (< pos src-len) (apl-alpha? (cur-byte)))
|
||||
(begin
|
||||
(read-ident-cont!)
|
||||
(tok-push! :keyword (slice source start pos)))
|
||||
(tok-push! :colon nil))
|
||||
(scan!))))
|
||||
((and (cur-sw? "¯")
|
||||
(< (+ pos (len "¯")) src-len)
|
||||
(apl-digit? (nth source (+ pos (len "¯")))))
|
||||
(begin
|
||||
(consume! "¯")
|
||||
(let ((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!)))
|
||||
((apl-digit? ch)
|
||||
(begin
|
||||
(let ((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!)))
|
||||
((= ch "'")
|
||||
(begin
|
||||
(advance!)
|
||||
(let ((s (read-string! "")))
|
||||
(tok-push! :str s))
|
||||
(scan!)))
|
||||
((or (apl-alpha? ch) (cur-sw? "⎕"))
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(if (cur-sw? "⎕") (consume! "⎕") (advance!))
|
||||
(if (and (< pos src-len) (cur-sw? "←"))
|
||||
(consume! "←")
|
||||
(read-ident-cont!))
|
||||
(tok-push! :name (slice source start pos))
|
||||
(scan!))))
|
||||
(true
|
||||
(let ((g (find-glyph)))
|
||||
(if g
|
||||
(begin (consume! g) (tok-push! :glyph g) (scan!))
|
||||
(begin (advance!) (scan!))))))))))
|
||||
|
||||
(scan!)
|
||||
tokens)))
|
||||
540
lib/apl/transpile.sx
Normal file
540
lib/apl/transpile.sx
Normal file
@@ -0,0 +1,540 @@
|
||||
; APL transpile / AST evaluator
|
||||
;
|
||||
; Walks parsed AST nodes and evaluates against the runtime.
|
||||
; Entry points:
|
||||
; apl-eval-ast : node × env → value
|
||||
; apl-eval-stmts : stmt-list × env → value (handles guards, locals, ⍺← default)
|
||||
; apl-call-dfn : dfn-ast × ⍺ × ⍵ → value (dyadic)
|
||||
; apl-call-dfn-m : dfn-ast × ⍵ → value (monadic)
|
||||
;
|
||||
; Env is a dict; ⍺ stored under "alpha", ⍵ under "omega",
|
||||
; the dfn-ast itself under "nabla" (for ∇ recursion),
|
||||
; user names under their literal name.
|
||||
|
||||
(define
|
||||
apl-monadic-fn
|
||||
(fn
|
||||
(g)
|
||||
(cond
|
||||
((= g "+") apl-plus-m)
|
||||
((= g "-") apl-neg-m)
|
||||
((= g "×") apl-signum)
|
||||
((= g "÷") apl-recip)
|
||||
((= g "⌈") apl-ceil)
|
||||
((= g "⌊") apl-floor)
|
||||
((= g "⍳") apl-iota)
|
||||
((= g "|") apl-abs)
|
||||
((= g "*") apl-exp)
|
||||
((= g "⍟") apl-ln)
|
||||
((= g "!") apl-fact)
|
||||
((= g "○") apl-pi-times)
|
||||
((= g "~") apl-not)
|
||||
((= g "≢") apl-tally)
|
||||
((= g "⍴") apl-shape)
|
||||
((= g "≡") apl-depth)
|
||||
((= g "⊂") apl-enclose)
|
||||
((= g "⊃") apl-disclose)
|
||||
((= g ",") apl-ravel)
|
||||
((= g "⌽") apl-reverse)
|
||||
((= g "⊖") apl-reverse-first)
|
||||
((= g "⍋") apl-grade-up)
|
||||
((= g "⍒") apl-grade-down)
|
||||
((= g "⎕FMT") apl-quad-fmt)
|
||||
((= g "⎕←") apl-quad-print)
|
||||
(else (error "no monadic fn for glyph")))))
|
||||
|
||||
(define
|
||||
apl-dyadic-fn
|
||||
(fn
|
||||
(g)
|
||||
(cond
|
||||
((= g "+") apl-add)
|
||||
((= g "-") apl-sub)
|
||||
((= g "×") apl-mul)
|
||||
((= g "÷") apl-div)
|
||||
((= g "⌈") apl-max)
|
||||
((= g "⌊") apl-min)
|
||||
((= g "*") apl-pow)
|
||||
((= g "⍟") apl-log)
|
||||
((= g "|") apl-mod)
|
||||
((= g "!") apl-binomial)
|
||||
((= g "○") apl-trig)
|
||||
((= g "<") apl-lt)
|
||||
((= g "≤") apl-le)
|
||||
((= g "=") apl-eq)
|
||||
((= g "≥") apl-ge)
|
||||
((= g ">") apl-gt)
|
||||
((= g "≠") apl-ne)
|
||||
((= g "∧") apl-and)
|
||||
((= g "∨") apl-or)
|
||||
((= g "⍱") apl-nor)
|
||||
((= g "⍲") apl-nand)
|
||||
((= g ",") apl-catenate)
|
||||
((= g "⍪") apl-catenate-first)
|
||||
((= g "⍴") apl-reshape)
|
||||
((= g "↑") apl-take)
|
||||
((= g "↓") apl-drop)
|
||||
((= g "⌷") apl-squad)
|
||||
((= g "⌽") apl-rotate)
|
||||
((= g "⊖") apl-rotate-first)
|
||||
((= g "∊") apl-member)
|
||||
((= g "⍳") apl-index-of)
|
||||
((= g "~") apl-without)
|
||||
(else (error "no dyadic fn for glyph")))))
|
||||
|
||||
(define
|
||||
apl-truthy?
|
||||
(fn
|
||||
(v)
|
||||
(let
|
||||
((rv (get v :ravel)))
|
||||
(if (and (= (len rv) 1) (= (first rv) 0)) false true))))
|
||||
|
||||
(define
|
||||
apl-eval-ast
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((tag (first node)))
|
||||
(cond
|
||||
((= 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)
|
||||
(let
|
||||
((items (rest node)))
|
||||
(let
|
||||
((vals (map (fn (n) (apl-eval-ast n env)) items)))
|
||||
(make-array
|
||||
(list (len vals))
|
||||
(map (fn (v) (first (get v :ravel))) vals)))))
|
||||
((= tag :name)
|
||||
(let
|
||||
((nm (nth node 1)))
|
||||
(cond
|
||||
((= nm "⍺") (get env "alpha"))
|
||||
((= nm "⍵") (get env "omega"))
|
||||
((= nm "⎕IO") (apl-quad-io))
|
||||
((= nm "⎕ML") (apl-quad-ml))
|
||||
((= nm "⎕FR") (apl-quad-fr))
|
||||
((= nm "⎕TS") (apl-quad-ts))
|
||||
(else (get env nm)))))
|
||||
((= tag :monad)
|
||||
(let
|
||||
((fn-node (nth node 1)) (arg (nth node 2)))
|
||||
(if
|
||||
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
||||
(apl-call-dfn-m (get env "nabla") (apl-eval-ast arg env))
|
||||
((apl-resolve-monadic fn-node env) (apl-eval-ast arg env)))))
|
||||
((= tag :dyad)
|
||||
(let
|
||||
((fn-node (nth node 1))
|
||||
(lhs (nth node 2))
|
||||
(rhs (nth node 3)))
|
||||
(if
|
||||
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
||||
(apl-call-dfn
|
||||
(get env "nabla")
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env))
|
||||
((apl-resolve-dyadic fn-node env)
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env)))))
|
||||
((= tag :program) (apl-eval-stmts (rest node) env))
|
||||
((= 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))))
|
||||
(else (error (list "apl-eval-ast: unknown node tag" tag node)))))))
|
||||
|
||||
(define
|
||||
apl-eval-stmts
|
||||
(fn
|
||||
(stmts env)
|
||||
(if
|
||||
(= (len stmts) 0)
|
||||
nil
|
||||
(let
|
||||
((stmt (first stmts)) (more (rest stmts)))
|
||||
(let
|
||||
((tag (first stmt)))
|
||||
(cond
|
||||
((= tag :guard)
|
||||
(let
|
||||
((cond-val (apl-eval-ast (nth stmt 1) env)))
|
||||
(if
|
||||
(apl-truthy? cond-val)
|
||||
(apl-eval-ast (nth stmt 2) env)
|
||||
(apl-eval-stmts more env))))
|
||||
((and (= tag :assign) (= (nth stmt 1) "⍺"))
|
||||
(if
|
||||
(get env "alpha")
|
||||
(apl-eval-stmts more env)
|
||||
(let
|
||||
((v (apl-eval-ast (nth stmt 2) env)))
|
||||
(apl-eval-stmts more (assoc env "alpha" v)))))
|
||||
((= tag :assign)
|
||||
(let
|
||||
((v (apl-eval-ast (nth stmt 2) env)))
|
||||
(apl-eval-stmts more (assoc env (nth stmt 1) v))))
|
||||
((= (len more) 0) (apl-eval-ast stmt env))
|
||||
(else (begin (apl-eval-ast stmt env) (apl-eval-stmts more env)))))))))
|
||||
|
||||
(define
|
||||
apl-call-dfn
|
||||
(fn
|
||||
(dfn-ast alpha omega)
|
||||
(let
|
||||
((stmts (rest dfn-ast)) (env {:omega omega :nabla dfn-ast :alpha alpha}))
|
||||
(apl-eval-stmts stmts env))))
|
||||
|
||||
(define
|
||||
apl-call-dfn-m
|
||||
(fn
|
||||
(dfn-ast omega)
|
||||
(let
|
||||
((stmts (rest dfn-ast)) (env {:omega omega :nabla dfn-ast :alpha nil}))
|
||||
(apl-eval-stmts stmts env))))
|
||||
|
||||
(define
|
||||
apl-tradfn-eval-block
|
||||
(fn
|
||||
(stmts env)
|
||||
(if
|
||||
(= (len stmts) 0)
|
||||
env
|
||||
(let
|
||||
((stmt (first stmts)))
|
||||
(apl-tradfn-eval-block (rest stmts) (apl-tradfn-eval-stmt stmt env))))))
|
||||
|
||||
(define
|
||||
apl-tradfn-eval-while
|
||||
(fn
|
||||
(cond-expr body env)
|
||||
(let
|
||||
((cond-val (apl-eval-ast cond-expr env)))
|
||||
(if
|
||||
(apl-truthy? cond-val)
|
||||
(apl-tradfn-eval-while
|
||||
cond-expr
|
||||
body
|
||||
(apl-tradfn-eval-block body env))
|
||||
env))))
|
||||
|
||||
(define
|
||||
apl-tradfn-eval-for
|
||||
(fn
|
||||
(var-name items body env)
|
||||
(if
|
||||
(= (len items) 0)
|
||||
env
|
||||
(let
|
||||
((env-with-var (assoc env var-name (apl-scalar (first items)))))
|
||||
(apl-tradfn-eval-for
|
||||
var-name
|
||||
(rest items)
|
||||
body
|
||||
(apl-tradfn-eval-block body env-with-var))))))
|
||||
|
||||
(define
|
||||
apl-tradfn-eval-select
|
||||
(fn
|
||||
(val cases default-block env)
|
||||
(if
|
||||
(= (len cases) 0)
|
||||
(apl-tradfn-eval-block default-block env)
|
||||
(let
|
||||
((c (first cases)))
|
||||
(let
|
||||
((case-val (apl-eval-ast (first c) env)))
|
||||
(if
|
||||
(= (first (get val :ravel)) (first (get case-val :ravel)))
|
||||
(apl-tradfn-eval-block (rest c) env)
|
||||
(apl-tradfn-eval-select val (rest cases) default-block env)))))))
|
||||
|
||||
(define
|
||||
apl-tradfn-eval-stmt
|
||||
(fn
|
||||
(stmt env)
|
||||
(let
|
||||
((tag (first stmt)))
|
||||
(cond
|
||||
((= tag :assign)
|
||||
(assoc env (nth stmt 1) (apl-eval-ast (nth stmt 2) env)))
|
||||
((= tag :if)
|
||||
(let
|
||||
((cond-val (apl-eval-ast (nth stmt 1) env)))
|
||||
(if
|
||||
(apl-truthy? cond-val)
|
||||
(apl-tradfn-eval-block (nth stmt 2) env)
|
||||
(apl-tradfn-eval-block (nth stmt 3) env))))
|
||||
((= tag :while)
|
||||
(apl-tradfn-eval-while (nth stmt 1) (nth stmt 2) env))
|
||||
((= tag :for)
|
||||
(let
|
||||
((iter-val (apl-eval-ast (nth stmt 2) env)))
|
||||
(apl-tradfn-eval-for
|
||||
(nth stmt 1)
|
||||
(get iter-val :ravel)
|
||||
(nth stmt 3)
|
||||
env)))
|
||||
((= tag :select)
|
||||
(let
|
||||
((val (apl-eval-ast (nth stmt 1) env)))
|
||||
(apl-tradfn-eval-select val (nth stmt 2) (nth stmt 3) env)))
|
||||
((= tag :trap)
|
||||
(let
|
||||
((codes (nth stmt 1))
|
||||
(try-block (nth stmt 2))
|
||||
(catch-block (nth stmt 3)))
|
||||
(guard
|
||||
(e
|
||||
((apl-trap-matches? codes e)
|
||||
(apl-tradfn-eval-block catch-block env)))
|
||||
(apl-tradfn-eval-block try-block env))))
|
||||
((= tag :throw) (apl-throw (nth stmt 1) (nth stmt 2)))
|
||||
(else (begin (apl-eval-ast stmt env) env))))))
|
||||
|
||||
(define
|
||||
apl-tradfn-loop
|
||||
(fn
|
||||
(stmts line env result-name)
|
||||
(cond
|
||||
((= line 0) (get env result-name))
|
||||
((> line (len stmts)) (get env result-name))
|
||||
(else
|
||||
(let
|
||||
((stmt (nth stmts (- line 1))))
|
||||
(let
|
||||
((tag (first stmt)))
|
||||
(cond
|
||||
((= tag :branch)
|
||||
(let
|
||||
((target (apl-eval-ast (nth stmt 1) env)))
|
||||
(let
|
||||
((target-num (first (get target :ravel))))
|
||||
(apl-tradfn-loop stmts target-num env result-name))))
|
||||
(else
|
||||
(apl-tradfn-loop
|
||||
stmts
|
||||
(+ line 1)
|
||||
(apl-tradfn-eval-stmt stmt env)
|
||||
result-name)))))))))
|
||||
|
||||
(define
|
||||
apl-call-tradfn
|
||||
(fn
|
||||
(tradfn alpha omega)
|
||||
(let
|
||||
((stmts (get tradfn :stmts))
|
||||
(result-name (get tradfn :result))
|
||||
(alpha-name (get tradfn :alpha))
|
||||
(omega-name (get tradfn :omega)))
|
||||
(let
|
||||
((env-a (if alpha-name (assoc {} alpha-name alpha) {})))
|
||||
(let
|
||||
((env-ao (if omega-name (assoc env-a omega-name omega) env-a)))
|
||||
(apl-tradfn-loop stmts 1 env-ao result-name))))))
|
||||
|
||||
(define
|
||||
apl-ast-mentions-list?
|
||||
(fn
|
||||
(lst target)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
false
|
||||
(if
|
||||
(apl-ast-mentions? (first lst) target)
|
||||
true
|
||||
(apl-ast-mentions-list? (rest lst) target)))))
|
||||
|
||||
(define
|
||||
apl-ast-mentions?
|
||||
(fn
|
||||
(node target)
|
||||
(cond
|
||||
((not (list? node)) false)
|
||||
((= (len node) 0) false)
|
||||
((and (= (first node) :name) (= (nth node 1) target)) true)
|
||||
(else (apl-ast-mentions-list? (rest node) target)))))
|
||||
|
||||
(define
|
||||
apl-dfn-valence
|
||||
(fn
|
||||
(dfn-ast)
|
||||
(let
|
||||
((body (rest dfn-ast)))
|
||||
(cond
|
||||
((apl-ast-mentions-list? body "⍺") :dyadic)
|
||||
((apl-ast-mentions-list? body "⍵") :monadic)
|
||||
(else :niladic)))))
|
||||
|
||||
(define
|
||||
apl-tradfn-valence
|
||||
(fn
|
||||
(tradfn)
|
||||
(cond
|
||||
((get tradfn :alpha) :dyadic)
|
||||
((get tradfn :omega) :monadic)
|
||||
(else :niladic))))
|
||||
|
||||
(define
|
||||
apl-call
|
||||
(fn
|
||||
(f alpha omega)
|
||||
(cond
|
||||
((and (list? f) (> (len f) 0) (= (first f) :dfn))
|
||||
(if alpha (apl-call-dfn f alpha omega) (apl-call-dfn-m f omega)))
|
||||
((dict? f) (apl-call-tradfn f alpha omega))
|
||||
(else (error "apl-call: not a function")))))
|
||||
|
||||
(define
|
||||
apl-resolve-monadic
|
||||
(fn
|
||||
(fn-node env)
|
||||
(let
|
||||
((tag (first fn-node)))
|
||||
(cond
|
||||
((= tag :fn-glyph) (apl-monadic-fn (nth fn-node 1)))
|
||||
((= tag :derived-fn)
|
||||
(let
|
||||
((op (nth fn-node 1)) (inner (nth fn-node 2)))
|
||||
(cond
|
||||
((= op "/")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-reduce f arr))))
|
||||
((= op "⌿")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-reduce-first f arr))))
|
||||
((= op "\\")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-scan f arr))))
|
||||
((= op "⍀")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-scan-first f arr))))
|
||||
((= op "¨")
|
||||
(let
|
||||
((f (apl-resolve-monadic inner env)))
|
||||
(fn (arr) (apl-each f arr))))
|
||||
((= op "⍨")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-commute f arr))))
|
||||
(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"))))))
|
||||
|
||||
(define
|
||||
apl-resolve-dyadic
|
||||
(fn
|
||||
(fn-node env)
|
||||
(let
|
||||
((tag (first fn-node)))
|
||||
(cond
|
||||
((= tag :fn-glyph) (apl-dyadic-fn (nth fn-node 1)))
|
||||
((= tag :derived-fn)
|
||||
(let
|
||||
((op (nth fn-node 1)) (inner (nth fn-node 2)))
|
||||
(cond
|
||||
((= op "¨")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (a b) (apl-each-dyadic f a b))))
|
||||
((= op "⍨")
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (a b) (apl-commute-dyadic f a b))))
|
||||
(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)
|
||||
(let
|
||||
((inner (nth fn-node 2)))
|
||||
(let
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (a b) (apl-outer f a b)))))
|
||||
((= tag :derived-fn2)
|
||||
(let
|
||||
((f-node (nth fn-node 2)) (g-node (nth fn-node 3)))
|
||||
(let
|
||||
((f (apl-resolve-dyadic f-node env))
|
||||
(g (apl-resolve-dyadic g-node env)))
|
||||
(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"))))))
|
||||
|
||||
(define apl-run (fn (src) (apl-eval-ast (parse-apl src) {})))
|
||||
@@ -30,7 +30,7 @@ run_suite() {
|
||||
local name="$1" load_files="$2" pass_var="$3" fail_var="$4" failures_var="$5"
|
||||
local TMP; TMP=$(mktemp)
|
||||
{
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n'
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(load "lib/guest/prefix.sx")\n'
|
||||
local i=2
|
||||
for f in $load_files; do
|
||||
printf '(epoch %d)\n(load "%s")\n' "$i" "$f"
|
||||
|
||||
@@ -23,13 +23,19 @@
|
||||
(cl-numberp? x)
|
||||
(let ((t (type-of x))) (or (= t "number") (= t "rational"))))
|
||||
|
||||
(define cl-integerp? integer?)
|
||||
(define cl-floatp? float?)
|
||||
(define cl-rationalp? rational?)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(integerp? integer?)
|
||||
(floatp? float?)
|
||||
(rationalp? rational?)
|
||||
))
|
||||
|
||||
(define (cl-realp? x) (or (integer? x) (float? x) (rational? x)))
|
||||
|
||||
(define cl-characterp? char?)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(characterp? char?)
|
||||
))
|
||||
(define cl-stringp? (fn (x) (= (type-of x) "string")))
|
||||
(define cl-symbolp? (fn (x) (= (type-of x) "symbol")))
|
||||
(define cl-keywordp? (fn (x) (= (type-of x) "keyword")))
|
||||
@@ -44,8 +50,11 @@
|
||||
(= t "native-fn")
|
||||
(= t "component"))))
|
||||
|
||||
(define cl-vectorp? vector?)
|
||||
(define cl-arrayp? vector?)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(vectorp? vector?)
|
||||
(arrayp? vector?)
|
||||
))
|
||||
|
||||
;; sx_server: (rest (list x)) returns () not nil — cl-empty? handles both
|
||||
(define
|
||||
@@ -56,19 +65,25 @@
|
||||
;; 2. Arithmetic — thin aliases to spec primitives
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-mod modulo)
|
||||
(define cl-rem remainder)
|
||||
(define cl-gcd gcd)
|
||||
(define cl-lcm lcm)
|
||||
(define cl-expt expt)
|
||||
(define cl-floor floor)
|
||||
(define cl-ceiling ceil)
|
||||
(define cl-truncate truncate)
|
||||
(define cl-round round)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(mod modulo)
|
||||
(rem remainder)
|
||||
gcd
|
||||
lcm
|
||||
expt
|
||||
floor
|
||||
(ceiling ceil)
|
||||
truncate
|
||||
round
|
||||
))
|
||||
(define cl-abs (fn (x) (if (< x 0) (- 0 x) x)))
|
||||
(define cl-min (fn (a b) (if (< a b) a b)))
|
||||
(define cl-max (fn (a b) (if (> a b) a b)))
|
||||
(define cl-quotient quotient)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
quotient
|
||||
))
|
||||
|
||||
(define
|
||||
(cl-signum x)
|
||||
@@ -87,21 +102,27 @@
|
||||
;; 3. Character functions — alias spec char primitives + CL name mapping
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-char->integer char->integer)
|
||||
(define cl-integer->char integer->char)
|
||||
(define cl-char-upcase char-upcase)
|
||||
(define cl-char-downcase char-downcase)
|
||||
(define cl-char-code char->integer)
|
||||
(define cl-code-char integer->char)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
char->integer
|
||||
integer->char
|
||||
char-upcase
|
||||
char-downcase
|
||||
(char-code char->integer)
|
||||
(code-char integer->char)
|
||||
))
|
||||
|
||||
(define cl-char=? char=?)
|
||||
(define cl-char<? char<?)
|
||||
(define cl-char>? char>?)
|
||||
(define cl-char<=? char<=?)
|
||||
(define cl-char>=? char>=?)
|
||||
(define cl-char-ci=? char-ci=?)
|
||||
(define cl-char-ci<? char-ci<?)
|
||||
(define cl-char-ci>? char-ci>?)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
char=?
|
||||
char<?
|
||||
char>?
|
||||
char<=?
|
||||
char>=?
|
||||
char-ci=?
|
||||
char-ci<?
|
||||
char-ci>?
|
||||
))
|
||||
|
||||
;; Inline predicates — char-alphabetic?/char-numeric? unreliable in sx_server
|
||||
(define
|
||||
@@ -152,8 +173,11 @@
|
||||
(cl-format dest template &rest args)
|
||||
(let ((s (apply format (cons template args)))) (if (= dest nil) s s)))
|
||||
|
||||
(define cl-write-to-string write-to-string)
|
||||
(define cl-princ-to-string display-to-string)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
write-to-string
|
||||
(princ-to-string display-to-string)
|
||||
))
|
||||
|
||||
;; CL read-from-string: parse value from a string using SX port
|
||||
(define
|
||||
@@ -161,18 +185,27 @@
|
||||
(let ((p (open-input-string s))) (read p)))
|
||||
|
||||
;; String stream (output)
|
||||
(define cl-make-string-output-stream open-output-string)
|
||||
(define cl-get-output-stream-string get-output-string)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(make-string-output-stream open-output-string)
|
||||
(get-output-stream-string get-output-string)
|
||||
))
|
||||
|
||||
;; String stream (input)
|
||||
(define cl-make-string-input-stream open-input-string)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
(make-string-input-stream open-input-string)
|
||||
))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Gensym
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-gensym gensym)
|
||||
(define cl-gentemp gensym)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
gensym
|
||||
(gentemp gensym)
|
||||
))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Multiple values (CL: values / nth-value)
|
||||
@@ -203,16 +236,19 @@
|
||||
;; 7. Sets (CL: adjoin / member / union / intersection / set-difference)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-make-set make-set)
|
||||
(define cl-set? set?)
|
||||
(define cl-set-add set-add!)
|
||||
(define cl-set-memberp set-member?)
|
||||
(define cl-set-remove set-remove!)
|
||||
(define cl-set-union set-union)
|
||||
(define cl-set-intersect set-intersection)
|
||||
(define cl-set-difference set-difference)
|
||||
(define cl-list->set list->set)
|
||||
(define cl-set->list set->list)
|
||||
(prefix-rename "cl-"
|
||||
'(
|
||||
make-set
|
||||
set?
|
||||
(set-add set-add!)
|
||||
(set-memberp set-member?)
|
||||
(set-remove set-remove!)
|
||||
set-union
|
||||
(set-intersect set-intersection)
|
||||
set-difference
|
||||
list->set
|
||||
set->list
|
||||
))
|
||||
|
||||
;; CL: (member item list) — returns tail starting at item, or nil
|
||||
(define
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-05-05T12:35:09Z",
|
||||
"generated": "2026-05-06T22:55:42Z",
|
||||
"total_pass": 518,
|
||||
"total_fail": 0,
|
||||
"suites": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Common Lisp on SX — Scoreboard
|
||||
|
||||
_Generated: 2026-05-05 12:35 UTC_
|
||||
_Generated: 2026-05-06 22:55 UTC_
|
||||
|
||||
| Suite | Pass | Fail | Status |
|
||||
|-------|------|------|--------|
|
||||
|
||||
@@ -1008,11 +1008,27 @@
|
||||
(let
|
||||
((name (symbol-name head))
|
||||
(argc (len args))
|
||||
(name-idx (pool-add (get em "pool") name)))
|
||||
(specialized-op (cond
|
||||
(and (= argc 2) (= name "+")) 160
|
||||
(and (= argc 2) (= name "-")) 161
|
||||
(and (= argc 2) (= name "*")) 162
|
||||
(and (= argc 2) (= name "/")) 163
|
||||
(and (= argc 2) (= name "=")) 164
|
||||
(and (= argc 2) (= name "<")) 165
|
||||
(and (= argc 2) (= name ">")) 166
|
||||
(and (= argc 2) (= name "cons")) 172
|
||||
(and (= argc 1) (= name "not")) 167
|
||||
(and (= argc 1) (= name "len")) 168
|
||||
(and (= argc 1) (= name "first")) 169
|
||||
(and (= argc 1) (= name "rest")) 170
|
||||
:else nil)))
|
||||
(for-each (fn (a) (compile-expr em a scope false)) args)
|
||||
(emit-op em 52)
|
||||
(emit-u16 em name-idx)
|
||||
(emit-byte em argc))
|
||||
(if specialized-op
|
||||
(emit-op em specialized-op)
|
||||
(let ((name-idx (pool-add (get em "pool") name)))
|
||||
(emit-op em 52)
|
||||
(emit-u16 em name-idx)
|
||||
(emit-byte em argc))))
|
||||
(do
|
||||
(compile-expr em head scope false)
|
||||
(for-each (fn (a) (compile-expr em a scope false)) args)
|
||||
|
||||
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))))))
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 530,
|
||||
"total": 530,
|
||||
"total_pass": 0,
|
||||
"total": 0,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":346,"total":346,"status":"ok"},
|
||||
{"name":"runtime","pass":39,"total":39,"status":"ok"},
|
||||
{"name":"ring","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"ping-pong","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
||||
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
||||
{"name":"fib","pass":8,"total":8,"status":"ok"}
|
||||
{"name":"tokenize","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"parse","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"eval","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"runtime","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"ring","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"ping-pong","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"bank","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"echo","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"fib","pass":0,"total":0,"status":"ok"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Erlang-on-SX Scoreboard
|
||||
|
||||
**Total: 530 / 530 tests passing**
|
||||
**Total: 0 / 0 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 346 | 346 |
|
||||
| ✅ | runtime | 39 | 39 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
| ✅ | bank | 8 | 8 |
|
||||
| ✅ | echo | 7 | 7 |
|
||||
| ✅ | fib | 8 | 8 |
|
||||
| ✅ | tokenize | 0 | 0 |
|
||||
| ✅ | parse | 0 | 0 |
|
||||
| ✅ | eval | 0 | 0 |
|
||||
| ✅ | runtime | 0 | 0 |
|
||||
| ✅ | ring | 0 | 0 |
|
||||
| ✅ | ping-pong | 0 | 0 |
|
||||
| ✅ | bank | 0 | 0 |
|
||||
| ✅ | echo | 0 | 0 |
|
||||
| ✅ | fib | 0 | 0 |
|
||||
|
||||
|
||||
Generated by `lib/erlang/conformance.sh`.
|
||||
|
||||
44
lib/fiber.sx
Normal file
44
lib/fiber.sx
Normal file
@@ -0,0 +1,44 @@
|
||||
; lib/fiber.sx — pure SX fiber library using call/cc
|
||||
;
|
||||
; A fiber is a cooperative coroutine with true suspension (no eager
|
||||
; pre-execution). Each fiber is a dict {:resume fn :done? fn}.
|
||||
;
|
||||
; make-fiber body → fiber dict
|
||||
; body = (fn (yield init-val) ...) — body receives yield + first resume val
|
||||
; yield = (fn (val) ...) — suspends fiber, returns val to resumer
|
||||
;
|
||||
; fiber-resume f v → next yielded value, or nil when body returns
|
||||
; fiber-done? f → true after body has returned
|
||||
|
||||
(define make-fiber
|
||||
(fn (body)
|
||||
(let
|
||||
((resume-k nil)
|
||||
(caller-k nil)
|
||||
(done false))
|
||||
(let
|
||||
((yield
|
||||
(fn (val)
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! resume-k k)
|
||||
(caller-k val))))))
|
||||
{:resume
|
||||
(fn (val)
|
||||
(if
|
||||
done
|
||||
nil
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! caller-k k)
|
||||
(if
|
||||
(nil? resume-k)
|
||||
(begin
|
||||
(body yield val)
|
||||
(set! done true)
|
||||
(k nil))
|
||||
(resume-k val))))))
|
||||
:done? (fn () done)}))))
|
||||
|
||||
(define fiber-resume (fn (f v) ((get f :resume) v)))
|
||||
(define fiber-done? (fn (f) ((get f :done?))))
|
||||
92
lib/guest/ast.sx
Normal file
92
lib/guest/ast.sx
Normal file
@@ -0,0 +1,92 @@
|
||||
;; lib/guest/ast.sx — canonical AST node shapes.
|
||||
;;
|
||||
;; A guest's parser may emit its own AST in whatever shape is convenient
|
||||
;; for that language's evaluator/transpiler. This file gives a SHARED
|
||||
;; canonical shape that cross-language tools (formatters, highlighters,
|
||||
;; debuggers) can target without per-language adapters.
|
||||
;;
|
||||
;; Each canonical node is a tagged list: (KIND ...payload).
|
||||
;;
|
||||
;; Constructors (return a canonical node):
|
||||
;;
|
||||
;; (ast-literal VALUE) — number / string / bool / nil
|
||||
;; (ast-var NAME) — identifier reference
|
||||
;; (ast-app FN ARGS) — function application
|
||||
;; (ast-lambda PARAMS BODY) — anonymous function
|
||||
;; (ast-let BINDINGS BODY) — local bindings
|
||||
;; (ast-letrec BINDINGS BODY) — recursive local bindings
|
||||
;; (ast-if TEST THEN ELSE) — conditional
|
||||
;; (ast-match-clause PATTERN BODY) — one match arm
|
||||
;; (ast-module NAME BODY) — module declaration
|
||||
;; (ast-import NAME) — import directive
|
||||
;;
|
||||
;; Predicates: (ast-literal? X), (ast-var? X), …
|
||||
;; Generic: (ast? X) — any canonical node
|
||||
;; (ast-kind X) — :literal / :var / :app / …
|
||||
;;
|
||||
;; Accessors (one per payload field):
|
||||
;; (ast-literal-value N)
|
||||
;; (ast-var-name N)
|
||||
;; (ast-app-fn N) (ast-app-args N)
|
||||
;; (ast-lambda-params N) (ast-lambda-body N)
|
||||
;; (ast-let-bindings N) (ast-let-body N)
|
||||
;; (ast-letrec-bindings N) (ast-letrec-body N)
|
||||
;; (ast-if-test N) (ast-if-then N) (ast-if-else N)
|
||||
;; (ast-match-clause-pattern N)
|
||||
;; (ast-match-clause-body N)
|
||||
;; (ast-module-name N) (ast-module-body N)
|
||||
;; (ast-import-name N)
|
||||
|
||||
(define ast-literal (fn (v) (list :literal v)))
|
||||
(define ast-var (fn (n) (list :var n)))
|
||||
(define ast-app (fn (f args) (list :app f args)))
|
||||
(define ast-lambda (fn (ps body) (list :lambda ps body)))
|
||||
(define ast-let (fn (bs body) (list :let bs body)))
|
||||
(define ast-letrec (fn (bs body) (list :letrec bs body)))
|
||||
(define ast-if (fn (t th el) (list :if t th el)))
|
||||
(define ast-match-clause (fn (p body) (list :match-clause p body)))
|
||||
(define ast-module (fn (n body) (list :module n body)))
|
||||
(define ast-import (fn (n) (list :import n)))
|
||||
|
||||
(define ast-kind (fn (x) (if (and (list? x) (not (empty? x))) (first x) nil)))
|
||||
|
||||
(define
|
||||
ast?
|
||||
(fn (x)
|
||||
(and (list? x)
|
||||
(not (empty? x))
|
||||
(let ((k (first x)))
|
||||
(or (= k :literal) (= k :var) (= k :app)
|
||||
(= k :lambda) (= k :let) (= k :letrec)
|
||||
(= k :if) (= k :match-clause)
|
||||
(= k :module) (= k :import))))))
|
||||
|
||||
(define ast-literal? (fn (x) (and (ast? x) (= (first x) :literal))))
|
||||
(define ast-var? (fn (x) (and (ast? x) (= (first x) :var))))
|
||||
(define ast-app? (fn (x) (and (ast? x) (= (first x) :app))))
|
||||
(define ast-lambda? (fn (x) (and (ast? x) (= (first x) :lambda))))
|
||||
(define ast-let? (fn (x) (and (ast? x) (= (first x) :let))))
|
||||
(define ast-letrec? (fn (x) (and (ast? x) (= (first x) :letrec))))
|
||||
(define ast-if? (fn (x) (and (ast? x) (= (first x) :if))))
|
||||
(define ast-match-clause? (fn (x) (and (ast? x) (= (first x) :match-clause))))
|
||||
(define ast-module? (fn (x) (and (ast? x) (= (first x) :module))))
|
||||
(define ast-import? (fn (x) (and (ast? x) (= (first x) :import))))
|
||||
|
||||
(define ast-literal-value (fn (n) (nth n 1)))
|
||||
(define ast-var-name (fn (n) (nth n 1)))
|
||||
(define ast-app-fn (fn (n) (nth n 1)))
|
||||
(define ast-app-args (fn (n) (nth n 2)))
|
||||
(define ast-lambda-params (fn (n) (nth n 1)))
|
||||
(define ast-lambda-body (fn (n) (nth n 2)))
|
||||
(define ast-let-bindings (fn (n) (nth n 1)))
|
||||
(define ast-let-body (fn (n) (nth n 2)))
|
||||
(define ast-letrec-bindings (fn (n) (nth n 1)))
|
||||
(define ast-letrec-body (fn (n) (nth n 2)))
|
||||
(define ast-if-test (fn (n) (nth n 1)))
|
||||
(define ast-if-then (fn (n) (nth n 2)))
|
||||
(define ast-if-else (fn (n) (nth n 3)))
|
||||
(define ast-match-clause-pattern (fn (n) (nth n 1)))
|
||||
(define ast-match-clause-body (fn (n) (nth n 2)))
|
||||
(define ast-module-name (fn (n) (nth n 1)))
|
||||
(define ast-module-body (fn (n) (nth n 2)))
|
||||
(define ast-import-name (fn (n) (nth n 1)))
|
||||
18
lib/guest/baseline/apl.json
Normal file
18
lib/guest/baseline/apl.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lang": "apl",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/apl/test.sh",
|
||||
"totals": {
|
||||
"pass": 73,
|
||||
"fail": 0,
|
||||
"total": 73
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "all",
|
||||
"pass": 73,
|
||||
"fail": 0,
|
||||
"total": 73
|
||||
}
|
||||
]
|
||||
}
|
||||
86
lib/guest/baseline/common-lisp.json
Normal file
86
lib/guest/baseline/common-lisp.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"lang": "common-lisp",
|
||||
"captured": "2026-05-06T22:59:46Z",
|
||||
"suite_command": "bash lib/common-lisp/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 518,
|
||||
"fail": 0,
|
||||
"total": 518
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "Phase 1: tokenizer/reader",
|
||||
"pass": 79,
|
||||
"fail": 0,
|
||||
"total": 79
|
||||
},
|
||||
{
|
||||
"name": "Phase 1: parser/lambda-lists",
|
||||
"pass": 31,
|
||||
"fail": 0,
|
||||
"total": 31
|
||||
},
|
||||
{
|
||||
"name": "Phase 2: evaluator",
|
||||
"pass": 182,
|
||||
"fail": 0,
|
||||
"total": 182
|
||||
},
|
||||
{
|
||||
"name": "Phase 3: condition system",
|
||||
"pass": 59,
|
||||
"fail": 0,
|
||||
"total": 59
|
||||
},
|
||||
{
|
||||
"name": "Phase 3: restart-demo",
|
||||
"pass": 7,
|
||||
"fail": 0,
|
||||
"total": 7
|
||||
},
|
||||
{
|
||||
"name": "Phase 3: parse-recover",
|
||||
"pass": 6,
|
||||
"fail": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"name": "Phase 3: interactive-debugger",
|
||||
"pass": 7,
|
||||
"fail": 0,
|
||||
"total": 7
|
||||
},
|
||||
{
|
||||
"name": "Phase 4: CLOS",
|
||||
"pass": 41,
|
||||
"fail": 0,
|
||||
"total": 41
|
||||
},
|
||||
{
|
||||
"name": "Phase 4: geometry",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "Phase 4: mop-trace",
|
||||
"pass": 13,
|
||||
"fail": 0,
|
||||
"total": 13
|
||||
},
|
||||
{
|
||||
"name": "Phase 5: macros+LOOP",
|
||||
"pass": 27,
|
||||
"fail": 0,
|
||||
"total": 27
|
||||
},
|
||||
{
|
||||
"name": "Phase 6: stdlib",
|
||||
"pass": 54,
|
||||
"fail": 0,
|
||||
"total": 54
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/common-lisp/scoreboard.json",
|
||||
"note": "Step 2: previous baseline (309) was lower because Phase 2 (evaluator, +182 tests) and Phase 6 (stdlib, +27 tests) results were under-counted by the original conformance.sh's parser. Re-running with prefix.sx loaded reveals true counts. No tests regressed."
|
||||
}
|
||||
67
lib/guest/baseline/erlang.json
Normal file
67
lib/guest/baseline/erlang.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"lang": "erlang",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/erlang/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "tokenize",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "parse",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "eval",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "runtime",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "ring",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "ping-pong",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "echo",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
},
|
||||
{
|
||||
"name": "fib",
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"total": 0
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/erlang/scoreboard.json"
|
||||
}
|
||||
18
lib/guest/baseline/forth.json
Normal file
18
lib/guest/baseline/forth.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lang": "forth",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/forth/test.sh",
|
||||
"totals": {
|
||||
"pass": 64,
|
||||
"fail": 0,
|
||||
"total": 64
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "all",
|
||||
"pass": 64,
|
||||
"fail": 0,
|
||||
"total": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
122
lib/guest/baseline/haskell.json
Normal file
122
lib/guest/baseline/haskell.json
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"lang": "haskell",
|
||||
"captured": "2026-05-06T22:46:16Z",
|
||||
"suite_command": "bash lib/haskell/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 156,
|
||||
"fail": 0,
|
||||
"total": 156
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "fib",
|
||||
"pass": 2,
|
||||
"fail": 0,
|
||||
"total": 2
|
||||
},
|
||||
{
|
||||
"name": "sieve",
|
||||
"pass": 2,
|
||||
"fail": 0,
|
||||
"total": 2
|
||||
},
|
||||
{
|
||||
"name": "quicksort",
|
||||
"pass": 5,
|
||||
"fail": 0,
|
||||
"total": 5
|
||||
},
|
||||
{
|
||||
"name": "nqueens",
|
||||
"pass": 2,
|
||||
"fail": 0,
|
||||
"total": 2
|
||||
},
|
||||
{
|
||||
"name": "calculator",
|
||||
"pass": 5,
|
||||
"fail": 0,
|
||||
"total": 5
|
||||
},
|
||||
{
|
||||
"name": "collatz",
|
||||
"pass": 11,
|
||||
"fail": 0,
|
||||
"total": 11
|
||||
},
|
||||
{
|
||||
"name": "palindrome",
|
||||
"pass": 8,
|
||||
"fail": 0,
|
||||
"total": 8
|
||||
},
|
||||
{
|
||||
"name": "maybe",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "fizzbuzz",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "anagram",
|
||||
"pass": 9,
|
||||
"fail": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"name": "roman",
|
||||
"pass": 14,
|
||||
"fail": 0,
|
||||
"total": 14
|
||||
},
|
||||
{
|
||||
"name": "binary",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "either",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "primes",
|
||||
"pass": 12,
|
||||
"fail": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"name": "zipwith",
|
||||
"pass": 9,
|
||||
"fail": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"name": "matrix",
|
||||
"pass": 8,
|
||||
"fail": 0,
|
||||
"total": 8
|
||||
},
|
||||
{
|
||||
"name": "wordcount",
|
||||
"pass": 7,
|
||||
"fail": 0,
|
||||
"total": 7
|
||||
},
|
||||
{
|
||||
"name": "powers",
|
||||
"pass": 14,
|
||||
"fail": 0,
|
||||
"total": 14
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/haskell/scoreboard.json",
|
||||
"note": "Step 1: previous baseline (0/18) was an artefact of the old conformance.sh bug \u2014 its (ok-len 3 ...) grep never matched, defaulting every program to 0 pass / 1 fail. Shared driver in Step 1 reads counters correctly."
|
||||
}
|
||||
75
lib/guest/baseline/js.json
Normal file
75
lib/guest/baseline/js.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"lang": "js",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/js/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 94,
|
||||
"fail": 54,
|
||||
"total": 148
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "test262-slice",
|
||||
"pass": 94,
|
||||
"fail": 54,
|
||||
"total": 148,
|
||||
"failing_tests": [
|
||||
"arithmetic/bitnot",
|
||||
"arithmetic/mixed_concat",
|
||||
"async/await_promise_all",
|
||||
"closures/sum_sq",
|
||||
"coercion/implicit_str_add",
|
||||
"collections/array_index",
|
||||
"collections/array_nested",
|
||||
"collections/string_index",
|
||||
"functions/rest_param",
|
||||
"loops/for_break",
|
||||
"loops/for_continue",
|
||||
"loops/nested_for",
|
||||
"loops/while_basic",
|
||||
"loops/while_break_infinite",
|
||||
"objects/array_filter_reduce",
|
||||
"objects/array_map",
|
||||
"objects/array_method_chain",
|
||||
"objects/array_mutate",
|
||||
"objects/array_push_length",
|
||||
"objects/arrow_lexical_this",
|
||||
"objects/class_basic",
|
||||
"objects/class_extend_chain",
|
||||
"objects/class_inherit",
|
||||
"objects/counter_closure",
|
||||
"objects/in_operator",
|
||||
"objects/instanceof",
|
||||
"objects/method_this",
|
||||
"objects/new_constructor",
|
||||
"objects/object_mutate",
|
||||
"objects/prototype_chain",
|
||||
"objects/string_method",
|
||||
"objects/string_slice",
|
||||
"promises/executor_throws",
|
||||
"promises/finally_passthrough",
|
||||
"promises/microtask_ordering",
|
||||
"promises/new_promise_reject",
|
||||
"promises/new_promise_resolve",
|
||||
"promises/promise_all",
|
||||
"promises/promise_all_empty",
|
||||
"promises/promise_all_nonpromise",
|
||||
"promises/promise_all_reject",
|
||||
"promises/promise_race",
|
||||
"promises/promise_resolve_already_promise",
|
||||
"promises/reject_catch",
|
||||
"promises/resolve_adopts",
|
||||
"promises/resolve_then",
|
||||
"promises/then_chain",
|
||||
"promises/then_throw_catch",
|
||||
"statements/block_scope",
|
||||
"statements/const_multi",
|
||||
"statements/if_else_false",
|
||||
"statements/if_else_true",
|
||||
"statements/let_init",
|
||||
"statements/var_decl"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/js/conformance.sh-output"
|
||||
}
|
||||
18
lib/guest/baseline/lua.json
Normal file
18
lib/guest/baseline/lua.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lang": "lua",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/lua/test.sh",
|
||||
"totals": {
|
||||
"pass": 185,
|
||||
"fail": 0,
|
||||
"total": 185
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "all",
|
||||
"pass": 185,
|
||||
"fail": 0,
|
||||
"total": 185
|
||||
}
|
||||
]
|
||||
}
|
||||
187
lib/guest/baseline/prolog.json
Normal file
187
lib/guest/baseline/prolog.json
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"lang": "prolog",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/prolog/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 590,
|
||||
"fail": 0,
|
||||
"total": 590
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "parse",
|
||||
"pass": 25,
|
||||
"fail": 0,
|
||||
"total": 25
|
||||
},
|
||||
{
|
||||
"name": "unify",
|
||||
"pass": 47,
|
||||
"fail": 0,
|
||||
"total": 47
|
||||
},
|
||||
{
|
||||
"name": "clausedb",
|
||||
"pass": 14,
|
||||
"fail": 0,
|
||||
"total": 14
|
||||
},
|
||||
{
|
||||
"name": "solve",
|
||||
"pass": 62,
|
||||
"fail": 0,
|
||||
"total": 62
|
||||
},
|
||||
{
|
||||
"name": "operators",
|
||||
"pass": 19,
|
||||
"fail": 0,
|
||||
"total": 19
|
||||
},
|
||||
{
|
||||
"name": "dynamic",
|
||||
"pass": 11,
|
||||
"fail": 0,
|
||||
"total": 11
|
||||
},
|
||||
{
|
||||
"name": "findall",
|
||||
"pass": 11,
|
||||
"fail": 0,
|
||||
"total": 11
|
||||
},
|
||||
{
|
||||
"name": "term_inspect",
|
||||
"pass": 14,
|
||||
"fail": 0,
|
||||
"total": 14
|
||||
},
|
||||
{
|
||||
"name": "append",
|
||||
"pass": 6,
|
||||
"fail": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"name": "reverse",
|
||||
"pass": 6,
|
||||
"fail": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"name": "member",
|
||||
"pass": 7,
|
||||
"fail": 0,
|
||||
"total": 7
|
||||
},
|
||||
{
|
||||
"name": "nqueens",
|
||||
"pass": 6,
|
||||
"fail": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"name": "family",
|
||||
"pass": 10,
|
||||
"fail": 0,
|
||||
"total": 10
|
||||
},
|
||||
{
|
||||
"name": "atoms",
|
||||
"pass": 34,
|
||||
"fail": 0,
|
||||
"total": 34
|
||||
},
|
||||
{
|
||||
"name": "query_api",
|
||||
"pass": 16,
|
||||
"fail": 0,
|
||||
"total": 16
|
||||
},
|
||||
{
|
||||
"name": "iso_predicates",
|
||||
"pass": 29,
|
||||
"fail": 0,
|
||||
"total": 29
|
||||
},
|
||||
{
|
||||
"name": "meta_predicates",
|
||||
"pass": 25,
|
||||
"fail": 0,
|
||||
"total": 25
|
||||
},
|
||||
{
|
||||
"name": "list_predicates",
|
||||
"pass": 33,
|
||||
"fail": 0,
|
||||
"total": 33
|
||||
},
|
||||
{
|
||||
"name": "meta_call",
|
||||
"pass": 15,
|
||||
"fail": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"name": "set_predicates",
|
||||
"pass": 15,
|
||||
"fail": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"name": "char_predicates",
|
||||
"pass": 27,
|
||||
"fail": 0,
|
||||
"total": 27
|
||||
},
|
||||
{
|
||||
"name": "io_predicates",
|
||||
"pass": 24,
|
||||
"fail": 0,
|
||||
"total": 24
|
||||
},
|
||||
{
|
||||
"name": "assert_rules",
|
||||
"pass": 15,
|
||||
"fail": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"name": "string_agg",
|
||||
"pass": 25,
|
||||
"fail": 0,
|
||||
"total": 25
|
||||
},
|
||||
{
|
||||
"name": "advanced",
|
||||
"pass": 21,
|
||||
"fail": 0,
|
||||
"total": 21
|
||||
},
|
||||
{
|
||||
"name": "compiler",
|
||||
"pass": 17,
|
||||
"fail": 0,
|
||||
"total": 17
|
||||
},
|
||||
{
|
||||
"name": "cross_validate",
|
||||
"pass": 17,
|
||||
"fail": 0,
|
||||
"total": 17
|
||||
},
|
||||
{
|
||||
"name": "integration",
|
||||
"pass": 20,
|
||||
"fail": 0,
|
||||
"total": 20
|
||||
},
|
||||
{
|
||||
"name": "hs_bridge",
|
||||
"pass": 19,
|
||||
"fail": 0,
|
||||
"total": 19
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/prolog/scoreboard.json"
|
||||
}
|
||||
18
lib/guest/baseline/ruby.json
Normal file
18
lib/guest/baseline/ruby.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lang": "ruby",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/ruby/test.sh",
|
||||
"totals": {
|
||||
"pass": 76,
|
||||
"fail": 0,
|
||||
"total": 76
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "all",
|
||||
"pass": 76,
|
||||
"fail": 0,
|
||||
"total": 76
|
||||
}
|
||||
]
|
||||
}
|
||||
25
lib/guest/baseline/smalltalk.json
Normal file
25
lib/guest/baseline/smalltalk.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"lang": "smalltalk",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/smalltalk/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 625,
|
||||
"fail": 4,
|
||||
"total": 629
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "all",
|
||||
"pass": 625,
|
||||
"fail": 4,
|
||||
"total": 629
|
||||
},
|
||||
{
|
||||
"name": "classic-corpus",
|
||||
"pass": 4,
|
||||
"fail": 1,
|
||||
"total": 5
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/smalltalk/scoreboard.json"
|
||||
}
|
||||
37
lib/guest/baseline/tcl.json
Normal file
37
lib/guest/baseline/tcl.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"lang": "tcl",
|
||||
"captured": "2026-05-06T22:01:00Z",
|
||||
"suite_command": "bash lib/tcl/conformance.sh",
|
||||
"totals": {
|
||||
"pass": 3,
|
||||
"fail": 1,
|
||||
"total": 4
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"name": "assert",
|
||||
"pass": 1,
|
||||
"fail": 0,
|
||||
"total": 1
|
||||
},
|
||||
{
|
||||
"name": "event-loop",
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"total": 1
|
||||
},
|
||||
{
|
||||
"name": "for-each-line",
|
||||
"pass": 1,
|
||||
"fail": 0,
|
||||
"total": 1
|
||||
},
|
||||
{
|
||||
"name": "with-temp-var",
|
||||
"pass": 1,
|
||||
"fail": 0,
|
||||
"total": 1
|
||||
}
|
||||
],
|
||||
"source_scoreboard": "lib/tcl/scoreboard.json"
|
||||
}
|
||||
221
lib/guest/conformance.sh
Executable file
221
lib/guest/conformance.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/guest/conformance.sh — shared, config-driven conformance driver.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/guest/conformance.sh <conf-file>
|
||||
#
|
||||
# The conf file is a bash file that sets:
|
||||
# LANG_NAME e.g. prolog
|
||||
# PRELOADS=( ... ) .sx files to load before any suite (path from repo root)
|
||||
# SUITES=( ... ) colon-separated entries; format depends on MODE
|
||||
# MODE "dict" or "counters"
|
||||
# COUNTERS_PASS (counters mode) global symbol for the pass counter
|
||||
# COUNTERS_FAIL (counters mode) global symbol for the fail counter
|
||||
# TIMEOUT_PER_SUITE (optional, counters mode) seconds per suite, default 120
|
||||
# SCOREBOARD_DIR (optional) defaults to lib/$LANG_NAME
|
||||
#
|
||||
# It may override the bash functions emit_scoreboard_json / emit_scoreboard_md
|
||||
# to produce the per-language scoreboard schema. Defaults are provided.
|
||||
#
|
||||
# Suite formats:
|
||||
# MODE=dict — "name:test-file:(runner-fn)"
|
||||
# The runner expression is evaluated and is expected to
|
||||
# return a dict with :passed/:failed/:total.
|
||||
# MODE=counters — "name:test-file"
|
||||
# Each suite is run in a fresh sx_server session: preloads
|
||||
# are loaded, then the test file, then counters are read.
|
||||
# The suite is treated as starting from counters (0, 0).
|
||||
#
|
||||
# Output:
|
||||
# Writes $SCOREBOARD_DIR/scoreboard.json and $SCOREBOARD_DIR/scoreboard.md.
|
||||
# Exits 0 if every suite is green, 1 otherwise.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 <conf-file>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CONF="$1"
|
||||
if [ ! -f "$CONF" ]; then
|
||||
echo "config not found: $CONF" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Defaults — the conf file may override these.
|
||||
LANG_NAME=
|
||||
PRELOADS=()
|
||||
SUITES=()
|
||||
MODE=dict
|
||||
COUNTERS_PASS=
|
||||
COUNTERS_FAIL=
|
||||
TIMEOUT_PER_SUITE=120
|
||||
SCOREBOARD_DIR=
|
||||
|
||||
emit_scoreboard_json() {
|
||||
# Generic schema. Per-lang configs override this for byte-equality with
|
||||
# historical scoreboards.
|
||||
local n=${#GC_NAMES[@]} i sep
|
||||
printf '{\n'
|
||||
printf ' "lang": "%s",\n' "$LANG_NAME"
|
||||
printf ' "total_passed": %d,\n' "$GC_TOTAL_PASS"
|
||||
printf ' "total_failed": %d,\n' "$GC_TOTAL_FAIL"
|
||||
printf ' "total": %d,\n' "$GC_TOTAL"
|
||||
printf ' "suites": ['
|
||||
for ((i=0; i<n; i++)); do
|
||||
sep=","; [ $i -eq $((n-1)) ] && sep=""
|
||||
printf '\n {"name":"%s","passed":%d,"failed":%d,"total":%d}%s' \
|
||||
"${GC_NAMES[$i]}" "${GC_PASS[$i]}" "${GC_FAIL[$i]}" "${GC_TOTAL_S[$i]}" "$sep"
|
||||
done
|
||||
printf '\n ],\n'
|
||||
printf ' "generated": "%s"\n' "$(date -Iseconds 2>/dev/null || date)"
|
||||
printf '}\n'
|
||||
}
|
||||
|
||||
emit_scoreboard_md() {
|
||||
local n=${#GC_NAMES[@]} i status
|
||||
printf '# %s scoreboard\n\n' "$LANG_NAME"
|
||||
printf '**%d / %d passing** (%d failure(s)).\n\n' "$GC_TOTAL_PASS" "$GC_TOTAL" "$GC_TOTAL_FAIL"
|
||||
printf '| Suite | Passed | Total | Status |\n'
|
||||
printf '|-------|--------|-------|--------|\n'
|
||||
for ((i=0; i<n; i++)); do
|
||||
status="ok"; [ "${GC_FAIL[$i]}" -gt 0 ] && status="FAIL"
|
||||
printf '| %s | %d | %d | %s |\n' \
|
||||
"${GC_NAMES[$i]}" "${GC_PASS[$i]}" "${GC_TOTAL_S[$i]}" "$status"
|
||||
done
|
||||
}
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONF"
|
||||
|
||||
if [ -z "$LANG_NAME" ]; then
|
||||
echo "LANG_NAME not set in $CONF" >&2
|
||||
exit 2
|
||||
fi
|
||||
SCOREBOARD_DIR="${SCOREBOARD_DIR:-lib/$LANG_NAME}"
|
||||
|
||||
SX="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX" ]; then
|
||||
MAIN_ROOT=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}')
|
||||
if [ -n "${MAIN_ROOT:-}" ] && [ -x "$MAIN_ROOT/$SX" ]; then
|
||||
SX="$MAIN_ROOT/$SX"
|
||||
else
|
||||
echo "ERROR: sx_server.exe not found (set SX_SERVER to override)." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
GC_NAMES=()
|
||||
GC_PASS=()
|
||||
GC_FAIL=()
|
||||
GC_TOTAL_S=()
|
||||
|
||||
parse_result_line() {
|
||||
# Match a (gc-result "name" P F T) line.
|
||||
local line="$1"
|
||||
if [[ "$line" =~ ^\(gc-result\ \"([^\"]+)\"\ ([0-9]+)\ ([0-9]+)\ ([0-9]+)\)$ ]]; then
|
||||
GC_NAMES+=("${BASH_REMATCH[1]}")
|
||||
GC_PASS+=("${BASH_REMATCH[2]}")
|
||||
GC_FAIL+=("${BASH_REMATCH[3]}")
|
||||
GC_TOTAL_S+=("${BASH_REMATCH[4]}")
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
dict)
|
||||
SCRIPT='(epoch 1)
|
||||
'
|
||||
for f in "${PRELOADS[@]}"; do
|
||||
SCRIPT+='(load "'"$f"'")
|
||||
'
|
||||
done
|
||||
SCRIPT+='(load "lib/guest/conformance.sx")
|
||||
'
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r _ file _ <<< "$entry"
|
||||
SCRIPT+='(load "'"$file"'")
|
||||
'
|
||||
done
|
||||
SCRIPT+='(epoch 2)
|
||||
'
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r name _ runner <<< "$entry"
|
||||
SCRIPT+='(eval "(gc-dict-result \"'"$name"'\" '"$runner"')")
|
||||
'
|
||||
done
|
||||
OUTPUT=$(printf '%s' "$SCRIPT" | "$SX" 2>&1)
|
||||
expected=${#SUITES[@]}
|
||||
matched=0
|
||||
while IFS= read -r line; do
|
||||
if parse_result_line "$line"; then
|
||||
matched=$((matched + 1))
|
||||
fi
|
||||
done <<< "$OUTPUT"
|
||||
if [ "$matched" -ne "$expected" ]; then
|
||||
echo "Expected $expected suite results, got $matched" >&2
|
||||
echo "---- raw output ----" >&2
|
||||
printf '%s\n' "$OUTPUT" >&2
|
||||
exit 3
|
||||
fi
|
||||
;;
|
||||
counters)
|
||||
if [ -z "$COUNTERS_PASS" ] || [ -z "$COUNTERS_FAIL" ]; then
|
||||
echo "MODE=counters requires COUNTERS_PASS and COUNTERS_FAIL in $CONF" >&2
|
||||
exit 2
|
||||
fi
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r name file <<< "$entry"
|
||||
TMPFILE=$(mktemp)
|
||||
{
|
||||
printf '(epoch 1)\n'
|
||||
for f in "${PRELOADS[@]}"; do printf '(load "%s")\n' "$f"; done
|
||||
printf '(load "lib/guest/conformance.sx")\n'
|
||||
printf '(epoch 2)\n'
|
||||
printf '(load "%s")\n' "$file"
|
||||
printf '(epoch 3)\n'
|
||||
printf '(eval "(gc-counters-result \\"%s\\" 0 0 %s %s)")\n' \
|
||||
"$name" "$COUNTERS_PASS" "$COUNTERS_FAIL"
|
||||
} > "$TMPFILE"
|
||||
OUTPUT=$(timeout "$TIMEOUT_PER_SUITE" "$SX" < "$TMPFILE" 2>&1 || true)
|
||||
rm -f "$TMPFILE"
|
||||
result=$(printf '%s\n' "$OUTPUT" | grep -E '^\(gc-result ' | tail -1 || true)
|
||||
if [ -n "$result" ] && parse_result_line "$result"; then
|
||||
:
|
||||
else
|
||||
# Suite hung or crashed before emitting a result. Record 0/1 so it
|
||||
# shows up as a failure rather than vanishing.
|
||||
GC_NAMES+=("$name")
|
||||
GC_PASS+=(0)
|
||||
GC_FAIL+=(1)
|
||||
GC_TOTAL_S+=(1)
|
||||
fi
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "Unknown MODE=$MODE in $CONF (expected dict|counters)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
GC_TOTAL_PASS=0
|
||||
GC_TOTAL_FAIL=0
|
||||
GC_TOTAL=0
|
||||
for ((i=0; i<${#GC_NAMES[@]}; i++)); do
|
||||
GC_TOTAL_PASS=$((GC_TOTAL_PASS + GC_PASS[i]))
|
||||
GC_TOTAL_FAIL=$((GC_TOTAL_FAIL + GC_FAIL[i]))
|
||||
GC_TOTAL=$((GC_TOTAL + GC_TOTAL_S[i]))
|
||||
done
|
||||
|
||||
mkdir -p "$SCOREBOARD_DIR"
|
||||
emit_scoreboard_json > "$SCOREBOARD_DIR/scoreboard.json"
|
||||
emit_scoreboard_md > "$SCOREBOARD_DIR/scoreboard.md"
|
||||
|
||||
if [ "$GC_TOTAL_FAIL" -gt 0 ]; then
|
||||
echo "$GC_TOTAL_FAIL failure(s) across $GC_TOTAL tests" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "All $GC_TOTAL tests pass."
|
||||
40
lib/guest/conformance.sx
Normal file
40
lib/guest/conformance.sx
Normal file
@@ -0,0 +1,40 @@
|
||||
;; lib/guest/conformance.sx — shared helpers for the guest conformance driver.
|
||||
;;
|
||||
;; The bash driver lib/guest/conformance.sh loads this file and then for each
|
||||
;; suite emits an (eval "...") form whose result is a tagged list:
|
||||
;;
|
||||
;; (gc-result NAME PASSED FAILED TOTAL)
|
||||
;;
|
||||
;; The driver greps these from sx_server's output and aggregates them.
|
||||
;;
|
||||
;; Two suite shapes are supported:
|
||||
;;
|
||||
;; :dict — runner expression returns a dict with :passed/:failed/:total.
|
||||
;; (gc-dict-result "parse" (pl-parse-tests-run!))
|
||||
;;
|
||||
;; :counters — runner has no return value, mutates pass/fail global counters.
|
||||
;; (gc-counters-result NAME P0 F0 PASS FAIL)
|
||||
;; where P0/F0 are the counters captured BEFORE the suite ran
|
||||
;; and PASS/FAIL are the counters AFTER.
|
||||
|
||||
(define
|
||||
gc-dict-result
|
||||
(fn
|
||||
(name r)
|
||||
(list
|
||||
(quote gc-result)
|
||||
name
|
||||
(get r :passed)
|
||||
(get r :failed)
|
||||
(get r :total))))
|
||||
|
||||
(define
|
||||
gc-counters-result
|
||||
(fn
|
||||
(name p0 f0 p1 f1)
|
||||
(list
|
||||
(quote gc-result)
|
||||
name
|
||||
(- p1 p0)
|
||||
(- f1 f0)
|
||||
(- (+ p1 f1) (+ p0 f0)))))
|
||||
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))))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user