From 8e6e7dce43706029f9cd389e824e02084603d5c1 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 24 Mar 2026 11:43:42 +0000 Subject: [PATCH] Transpile dom.sx + browser.sx into bundle; add FFI variable aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dom-lib and browser-lib were listed in ADAPTER_FILES but never actually transpiled — their functions only existed as native PLATFORM_*_JS code. Add them to the build loop so the FFI library wrappers are compiled. Add hostCall/hostGet/etc. variable aliases for transpiled code, and console-log to browser.sx for runtime-eval'd SX code. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/javascript/bootstrap.py | 2 +- hosts/javascript/platform.py | 9 + shared/static/scripts/sx-browser.js | 420 +++++++++++++++++++++++++++- web/lib/browser.sx | 5 + 4 files changed, 434 insertions(+), 2 deletions(-) diff --git a/hosts/javascript/bootstrap.py b/hosts/javascript/bootstrap.py index b7d1a95..a145617 100644 --- a/hosts/javascript/bootstrap.py +++ b/hosts/javascript/bootstrap.py @@ -137,7 +137,7 @@ def compile_ref_to_js( ("content.sx", "content (content-addressed computation)"), ("render.sx", "render (core)"), ] - for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"): + for name in ("parser", "html", "sx", "dom-lib", "browser-lib", "dom", "engine", "orchestration", "boot"): if name in adapter_set: sx_files.append(ADAPTER_FILES[name]) # Use explicit ordering for spec modules (respects dependencies) diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 2605f52..463be70 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -1580,6 +1580,15 @@ PLATFORM_JS_POST = ''' promise.then(cb); } }; + // Aliases for transpiled dom.sx / browser.sx code (transpiler mangles host-* names) + var hostGlobal = PRIMITIVES["host-global"]; + var hostGet = PRIMITIVES["host-get"]; + var hostSet = PRIMITIVES["host-set!"]; + var hostCall = PRIMITIVES["host-call"]; + var hostNew = PRIMITIVES["host-new"]; + var hostCallback = PRIMITIVES["host-callback"]; + var hostTypeof = PRIMITIVES["host-typeof"]; + var hostAwait = PRIMITIVES["host-await"]; // processBindings and evalCond — now specced in render.sx, bootstrapped above diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index c7b0c2b..84f7b2b 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-24T11:14:33Z"; + var SX_VERSION = "2026-03-24T11:42:17Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -730,6 +730,15 @@ promise.then(cb); } }; + // Aliases for transpiled dom.sx / browser.sx code (transpiler mangles host-* names) + var hostGlobal = PRIMITIVES["host-global"]; + var hostGet = PRIMITIVES["host-get"]; + var hostSet = PRIMITIVES["host-set!"]; + var hostCall = PRIMITIVES["host-call"]; + var hostNew = PRIMITIVES["host-new"]; + var hostCallback = PRIMITIVES["host-callback"]; + var hostTypeof = PRIMITIVES["host-typeof"]; + var hostAwait = PRIMITIVES["host-await"]; // processBindings and evalCond — now specced in render.sx, bootstrapped above @@ -2960,6 +2969,415 @@ PRIMITIVES["aser-special"] = aserSpecial; PRIMITIVES["eval-case-aser"] = evalCaseAser; + // === Transpiled from lib/dom (DOM library) === + + // dom-document + var domDocument = function() { return hostGlobal("document"); }; +PRIMITIVES["dom-document"] = domDocument; + + // dom-window + var domWindow = function() { return hostGlobal("window"); }; +PRIMITIVES["dom-window"] = domWindow; + + // dom-body + var domBody = function() { return hostGet(domDocument(), "body"); }; +PRIMITIVES["dom-body"] = domBody; + + // dom-head + var domHead = function() { return hostGet(domDocument(), "head"); }; +PRIMITIVES["dom-head"] = domHead; + + // dom-create-element + var domCreateElement = function(tag) { return hostCall(domDocument(), "createElement", tag); }; +PRIMITIVES["dom-create-element"] = domCreateElement; + + // create-text-node + var createTextNode = function(s) { return hostCall(domDocument(), "createTextNode", s); }; +PRIMITIVES["create-text-node"] = createTextNode; + + // create-fragment + var createFragment = function() { return hostCall(domDocument(), "createDocumentFragment"); }; +PRIMITIVES["create-fragment"] = createFragment; + + // dom-append + var domAppend = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "appendChild", child) : NIL); }; +PRIMITIVES["dom-append"] = domAppend; + + // dom-prepend + var domPrepend = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "prepend", child) : NIL); }; +PRIMITIVES["dom-prepend"] = domPrepend; + + // dom-insert-before + var domInsertBefore = function(parent, child, ref) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "insertBefore", child, ref) : NIL); }; +PRIMITIVES["dom-insert-before"] = domInsertBefore; + + // dom-remove-child + var domRemoveChild = function(parent, child) { return (isSxTruthy((isSxTruthy(parent) && child)) ? hostCall(parent, "removeChild", child) : NIL); }; +PRIMITIVES["dom-remove-child"] = domRemoveChild; + + // dom-replace-child + var domReplaceChild = function(parent, newChild, oldChild) { return (isSxTruthy((isSxTruthy(parent) && isSxTruthy(newChild) && oldChild)) ? hostCall(parent, "replaceChild", newChild, oldChild) : NIL); }; +PRIMITIVES["dom-replace-child"] = domReplaceChild; + + // dom-clone + var domClone = function(node, deep) { return hostCall(node, "cloneNode", (isSxTruthy(isNil(deep)) ? true : deep)); }; +PRIMITIVES["dom-clone"] = domClone; + + // dom-query + var domQuery = function(sel) { return hostCall(domDocument(), "querySelector", sel); }; +PRIMITIVES["dom-query"] = domQuery; + + // dom-query-all + var domQueryAll = function(root, sel) { return (isSxTruthy(isNil(sel)) ? hostCall(domDocument(), "querySelectorAll", root) : hostCall(root, "querySelectorAll", sel)); }; +PRIMITIVES["dom-query-all"] = domQueryAll; + + // dom-query-by-id + var domQueryById = function(id) { return hostCall(domDocument(), "getElementById", id); }; +PRIMITIVES["dom-query-by-id"] = domQueryById; + + // dom-closest + var domClosest = function(el, sel) { return (isSxTruthy(el) ? hostCall(el, "closest", sel) : NIL); }; +PRIMITIVES["dom-closest"] = domClosest; + + // dom-matches? + var domMatches = function(el, sel) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "matches"))) ? hostCall(el, "matches", sel) : false); }; +PRIMITIVES["dom-matches?"] = domMatches; + + // dom-get-attr + var domGetAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "getAttribute"))) ? (function() { + var v = hostCall(el, "getAttribute", name); + return (isSxTruthy(isNil(v)) ? NIL : v); +})() : NIL); }; +PRIMITIVES["dom-get-attr"] = domGetAttr; + + // dom-set-attr + var domSetAttr = function(el, name, val) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "setAttribute"))) ? hostCall(el, "setAttribute", name, val) : NIL); }; +PRIMITIVES["dom-set-attr"] = domSetAttr; + + // dom-remove-attr + var domRemoveAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "removeAttribute"))) ? hostCall(el, "removeAttribute", name) : NIL); }; +PRIMITIVES["dom-remove-attr"] = domRemoveAttr; + + // dom-has-attr? + var domHasAttr = function(el, name) { return (isSxTruthy((isSxTruthy(el) && hostGet(el, "hasAttribute"))) ? hostCall(el, "hasAttribute", name) : false); }; +PRIMITIVES["dom-has-attr?"] = domHasAttr; + + // dom-add-class + var domAddClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "add", cls) : NIL); }; +PRIMITIVES["dom-add-class"] = domAddClass; + + // dom-remove-class + var domRemoveClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "remove", cls) : NIL); }; +PRIMITIVES["dom-remove-class"] = domRemoveClass; + + // dom-has-class? + var domHasClass = function(el, cls) { return (isSxTruthy(el) ? hostCall(hostGet(el, "classList"), "contains", cls) : false); }; +PRIMITIVES["dom-has-class?"] = domHasClass; + + // dom-text-content + var domTextContent = function(el) { return hostGet(el, "textContent"); }; +PRIMITIVES["dom-text-content"] = domTextContent; + + // dom-set-text-content + var domSetTextContent = function(el, val) { return hostSet(el, "textContent", val); }; +PRIMITIVES["dom-set-text-content"] = domSetTextContent; + + // dom-inner-html + var domInnerHtml = function(el) { return hostGet(el, "innerHTML"); }; +PRIMITIVES["dom-inner-html"] = domInnerHtml; + + // dom-set-inner-html + var domSetInnerHtml = function(el, val) { return hostSet(el, "innerHTML", val); }; +PRIMITIVES["dom-set-inner-html"] = domSetInnerHtml; + + // dom-outer-html + var domOuterHtml = function(el) { return hostGet(el, "outerHTML"); }; +PRIMITIVES["dom-outer-html"] = domOuterHtml; + + // dom-insert-adjacent-html + var domInsertAdjacentHtml = function(el, position, html) { return hostCall(el, "insertAdjacentHTML", position, html); }; +PRIMITIVES["dom-insert-adjacent-html"] = domInsertAdjacentHtml; + + // dom-get-style + var domGetStyle = function(el, prop) { return hostGet(hostGet(el, "style"), prop); }; +PRIMITIVES["dom-get-style"] = domGetStyle; + + // dom-set-style + var domSetStyle = function(el, prop, val) { return hostCall(hostGet(el, "style"), "setProperty", prop, val); }; +PRIMITIVES["dom-set-style"] = domSetStyle; + + // dom-get-prop + var domGetProp = function(el, name) { return hostGet(el, name); }; +PRIMITIVES["dom-get-prop"] = domGetProp; + + // dom-set-prop + var domSetProp = function(el, name, val) { return hostSet(el, name, val); }; +PRIMITIVES["dom-set-prop"] = domSetProp; + + // dom-tag-name + var domTagName = function(el) { return (isSxTruthy(el) ? lower(sxOr(hostGet(el, "tagName"), "")) : ""); }; +PRIMITIVES["dom-tag-name"] = domTagName; + + // dom-node-type + var domNodeType = function(el) { return hostGet(el, "nodeType"); }; +PRIMITIVES["dom-node-type"] = domNodeType; + + // dom-node-name + var domNodeName = function(el) { return hostGet(el, "nodeName"); }; +PRIMITIVES["dom-node-name"] = domNodeName; + + // dom-id + var domId = function(el) { return hostGet(el, "id"); }; +PRIMITIVES["dom-id"] = domId; + + // dom-parent + var domParent = function(el) { return hostGet(el, "parentNode"); }; +PRIMITIVES["dom-parent"] = domParent; + + // dom-first-child + var domFirstChild = function(el) { return hostGet(el, "firstChild"); }; +PRIMITIVES["dom-first-child"] = domFirstChild; + + // dom-next-sibling + var domNextSibling = function(el) { return hostGet(el, "nextSibling"); }; +PRIMITIVES["dom-next-sibling"] = domNextSibling; + + // dom-child-list + var domChildList = function(el) { return (isSxTruthy(el) ? hostCall(hostGlobal("Array"), "from", hostGet(el, "childNodes")) : []); }; +PRIMITIVES["dom-child-list"] = domChildList; + + // dom-is-fragment? + var domIsFragment = function(el) { return (hostGet(el, "nodeType") == 11); }; +PRIMITIVES["dom-is-fragment?"] = domIsFragment; + + // dom-focus + var domFocus = function(el) { return (isSxTruthy(el) ? hostCall(el, "focus") : NIL); }; +PRIMITIVES["dom-focus"] = domFocus; + + // dom-parse-html + var domParseHtml = function(html) { return (function() { + var parser = hostNew("DOMParser"); + var doc = hostCall(parser, "parseFromString", html, "text/html"); + return hostGet(hostGet(doc, "body"), "childNodes"); +})(); }; +PRIMITIVES["dom-parse-html"] = domParseHtml; + + // dom-listen + var domListen = function(el, eventName, handler) { return (function() { + var cb = hostCallback(handler); + hostCall(el, "addEventListener", eventName, cb); + return function() { return hostCall(el, "removeEventListener", eventName, cb); }; +})(); }; +PRIMITIVES["dom-listen"] = domListen; + + // dom-dispatch + var domDispatch = function(el, eventName, detail) { return (function() { + var evt = hostNew("CustomEvent", eventName, {["detail"]: detail, ["bubbles"]: true}); + return hostCall(el, "dispatchEvent", evt); +})(); }; +PRIMITIVES["dom-dispatch"] = domDispatch; + + // event-detail + var eventDetail = function(evt) { return hostGet(evt, "detail"); }; +PRIMITIVES["event-detail"] = eventDetail; + + // prevent-default + var preventDefault_ = function(e) { return (isSxTruthy(e) ? hostCall(e, "preventDefault") : NIL); }; +PRIMITIVES["prevent-default"] = preventDefault_; + + // stop-propagation + var stopPropagation_ = function(e) { return (isSxTruthy(e) ? hostCall(e, "stopPropagation") : NIL); }; +PRIMITIVES["stop-propagation"] = stopPropagation_; + + // event-modifier-key? + var eventModifierKey_p = function(e) { return (isSxTruthy(e) && sxOr(hostGet(e, "ctrlKey"), hostGet(e, "metaKey"), hostGet(e, "shiftKey"), hostGet(e, "altKey"))); }; +PRIMITIVES["event-modifier-key?"] = eventModifierKey_p; + + // element-value + var elementValue = function(el) { return (isSxTruthy((isSxTruthy(el) && !isSxTruthy(isNil(hostGet(el, "value"))))) ? hostGet(el, "value") : NIL); }; +PRIMITIVES["element-value"] = elementValue; + + // error-message + var errorMessage = function(e) { return (isSxTruthy((isSxTruthy(e) && hostGet(e, "message"))) ? hostGet(e, "message") : (String(e))); }; +PRIMITIVES["error-message"] = errorMessage; + + // dom-get-data + var domGetData = function(el, key) { return (function() { + var store = hostGet(el, "__sx_data"); + return (isSxTruthy(store) ? hostGet(store, key) : NIL); +})(); }; +PRIMITIVES["dom-get-data"] = domGetData; + + // dom-set-data + var domSetData = function(el, key, val) { if (isSxTruthy(!isSxTruthy(hostGet(el, "__sx_data")))) { + hostSet(el, "__sx_data", {}); +} +return hostSet(hostGet(el, "__sx_data"), key, val); }; +PRIMITIVES["dom-set-data"] = domSetData; + + // dom-append-to-head + var domAppendToHead = function(el) { return (isSxTruthy(domHead()) ? hostCall(domHead(), "appendChild", el) : NIL); }; +PRIMITIVES["dom-append-to-head"] = domAppendToHead; + + // set-document-title + var setDocumentTitle = function(title) { return hostSet(domDocument(), "title", title); }; +PRIMITIVES["set-document-title"] = setDocumentTitle; + + + // === Transpiled from lib/browser (browser API library) === + + // browser-location-href + var browserLocationHref = function() { return hostGet(hostGet(domWindow(), "location"), "href"); }; +PRIMITIVES["browser-location-href"] = browserLocationHref; + + // browser-location-pathname + var browserLocationPathname = function() { return hostGet(hostGet(domWindow(), "location"), "pathname"); }; +PRIMITIVES["browser-location-pathname"] = browserLocationPathname; + + // browser-location-origin + var browserLocationOrigin = function() { return hostGet(hostGet(domWindow(), "location"), "origin"); }; +PRIMITIVES["browser-location-origin"] = browserLocationOrigin; + + // browser-same-origin? + var browserSameOrigin = function(url) { return startsWith(url, browserLocationOrigin()); }; +PRIMITIVES["browser-same-origin?"] = browserSameOrigin; + + // browser-push-state + var browserPushState = function(state, title, url) { return hostCall(hostGet(domWindow(), "history"), "pushState", state, title, url); }; +PRIMITIVES["browser-push-state"] = browserPushState; + + // browser-replace-state + var browserReplaceState = function(state, title, url) { return hostCall(hostGet(domWindow(), "history"), "replaceState", state, title, url); }; +PRIMITIVES["browser-replace-state"] = browserReplaceState; + + // browser-reload + var browserReload = function() { return hostCall(hostGet(domWindow(), "location"), "reload"); }; +PRIMITIVES["browser-reload"] = browserReload; + + // browser-navigate + var browserNavigate = function(url) { return hostSet(hostGet(domWindow(), "location"), "href", url); }; +PRIMITIVES["browser-navigate"] = browserNavigate; + + // local-storage-get + var localStorageGet = function(key) { return hostCall(hostGet(domWindow(), "localStorage"), "getItem", key); }; +PRIMITIVES["local-storage-get"] = localStorageGet; + + // local-storage-set + var localStorageSet = function(key, val) { return hostCall(hostGet(domWindow(), "localStorage"), "setItem", key, val); }; +PRIMITIVES["local-storage-set"] = localStorageSet; + + // local-storage-remove + var localStorageRemove = function(key) { return hostCall(hostGet(domWindow(), "localStorage"), "removeItem", key); }; +PRIMITIVES["local-storage-remove"] = localStorageRemove; + + // set-timeout + var setTimeout_ = function(fnVal, ms) { return hostCall(domWindow(), "setTimeout", hostCallback(fnVal), ms); }; +PRIMITIVES["set-timeout"] = setTimeout_; + + // set-interval + var setInterval_ = function(fnVal, ms) { return hostCall(domWindow(), "setInterval", hostCallback(fnVal), ms); }; +PRIMITIVES["set-interval"] = setInterval_; + + // clear-timeout + var clearTimeout_ = function(id) { return hostCall(domWindow(), "clearTimeout", id); }; +PRIMITIVES["clear-timeout"] = clearTimeout_; + + // clear-interval + var clearInterval_ = function(id) { return hostCall(domWindow(), "clearInterval", id); }; +PRIMITIVES["clear-interval"] = clearInterval_; + + // request-animation-frame + var requestAnimationFrame_ = function(fnVal) { return hostCall(domWindow(), "requestAnimationFrame", hostCallback(fnVal)); }; +PRIMITIVES["request-animation-frame"] = requestAnimationFrame_; + + // fetch-request + var fetchRequest = function(url, opts) { return hostCall(domWindow(), "fetch", url, opts); }; +PRIMITIVES["fetch-request"] = fetchRequest; + + // new-abort-controller + var newAbortController = function() { return hostNew("AbortController"); }; +PRIMITIVES["new-abort-controller"] = newAbortController; + + // controller-signal + var controllerSignal = function(controller) { return hostGet(controller, "signal"); }; +PRIMITIVES["controller-signal"] = controllerSignal; + + // controller-abort + var controllerAbort = function(controller) { return hostCall(controller, "abort"); }; +PRIMITIVES["controller-abort"] = controllerAbort; + + // promise-then + var promiseThen = function(p, onResolve, onReject) { return (function() { + var cbResolve = hostCallback(onResolve); + var cbReject = (isSxTruthy(onReject) ? hostCallback(onReject) : NIL); + return (isSxTruthy(cbReject) ? hostCall(hostCall(p, "then", cbResolve), "catch", cbReject) : hostCall(p, "then", cbResolve)); +})(); }; +PRIMITIVES["promise-then"] = promiseThen; + + // promise-resolve + var promiseResolve = function(val) { return hostCall(hostGlobal("Promise"), "resolve", val); }; +PRIMITIVES["promise-resolve"] = promiseResolve; + + // promise-delayed + var promiseDelayed = function(ms, val) { return hostNew("Promise", hostCallback(function(resolve) { return setTimeout_(function() { return hostCall(resolve, "call", NIL, val); }, ms); })); }; +PRIMITIVES["promise-delayed"] = promiseDelayed; + + // browser-confirm + var browserConfirm = function(msg) { return hostCall(domWindow(), "confirm", msg); }; +PRIMITIVES["browser-confirm"] = browserConfirm; + + // browser-prompt + var browserPrompt = function(msg, default_) { return hostCall(domWindow(), "prompt", msg, default_); }; +PRIMITIVES["browser-prompt"] = browserPrompt; + + // browser-media-matches? + var browserMediaMatches = function(query) { return hostGet(hostCall(domWindow(), "matchMedia", query), "matches"); }; +PRIMITIVES["browser-media-matches?"] = browserMediaMatches; + + // json-parse + var jsonParse = function(s) { return hostCall(hostGlobal("JSON"), "parse", s); }; +PRIMITIVES["json-parse"] = jsonParse; + + // log-info + var logInfo = function(msg) { return hostCall(hostGlobal("console"), "log", (String("[sx] ") + String(msg))); }; +PRIMITIVES["log-info"] = logInfo; + + // log-warn + var logWarn = function(msg) { return hostCall(hostGlobal("console"), "warn", (String("[sx] ") + String(msg))); }; +PRIMITIVES["log-warn"] = logWarn; + + // console-log + var consoleLog = function() { var args = Array.prototype.slice.call(arguments, 0); return hostCall(hostGlobal("console"), "log", join(" ", cons("[sx]", map(str, args)))); }; +PRIMITIVES["console-log"] = consoleLog; + + // now-ms + var nowMs = function() { return hostCall(hostGlobal("Date"), "now"); }; +PRIMITIVES["now-ms"] = nowMs; + + // schedule-idle + var scheduleIdle = function(f) { return (function() { + var cb = hostCallback(f); + return (isSxTruthy(hostGet(domWindow(), "requestIdleCallback")) ? hostCall(domWindow(), "requestIdleCallback", cb) : setTimeout_(cb, 0)); +})(); }; +PRIMITIVES["schedule-idle"] = scheduleIdle; + + // set-cookie + var setCookie = function(name, value, days) { return (function() { + var d = sxOr(days, 365); + var expires = hostCall(hostNew("Date", (hostCall(hostGlobal("Date"), "now") + (d * 86400000.0))), "toUTCString"); + return hostSet(domDocument(), "cookie", (String(name) + String("=") + String(hostCall(NIL, "encodeURIComponent", value)) + String(";expires=") + String(expires) + String(";path=/;SameSite=Lax"))); +})(); }; +PRIMITIVES["set-cookie"] = setCookie; + + // get-cookie + var getCookie = function(name) { return (function() { + var cookies = hostGet(domDocument(), "cookie"); + var match = hostCall(cookies, "match", hostNew("RegExp", (String("(?:^|;\\s*)") + String(name) + String("=([^;]*)")))); + return (isSxTruthy(match) ? hostCall(NIL, "decodeURIComponent", hostGet(match, 1)) : NIL); +})(); }; +PRIMITIVES["get-cookie"] = getCookie; + + // === Transpiled from adapter-dom === // SVG_NS diff --git a/web/lib/browser.sx b/web/lib/browser.sx index 94d17ab..8d91344 100644 --- a/web/lib/browser.sx +++ b/web/lib/browser.sx @@ -166,6 +166,11 @@ (fn (msg) (host-call (host-global "console") "warn" (str "[sx] " msg)))) +(define console-log + (fn (&rest args) + (host-call (host-global "console") "log" + (join " " (cons "[sx]" (map str args)))))) + (define now-ms (fn () (host-call (host-global "Date") "now")))