From 5e8f0b6b069346cee92e67e1e6fde7ee64b72a06 Mon Sep 17 00:00:00 2001 From: Ari Prakash Date: Thu, 12 Jun 2025 16:18:50 -0400 Subject: [PATCH 1/5] add new properties to reactor syntax --- src/arr/compiler/compile-structs.arr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/arr/compiler/compile-structs.arr b/src/arr/compiler/compile-structs.arr index 563a1e010..1f2dbd013 100644 --- a/src/arr/compiler/compile-structs.arr +++ b/src/arr/compiler/compile-structs.arr @@ -3317,14 +3317,20 @@ no-globals = globals([string-dict:], [string-dict:], [string-dict:]) reactor-optional-fields = [SD.string-dict: "last-image", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-tick", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-tick-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "to-draw", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-key", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-key-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-raw-key", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-raw-key-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-mouse", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-mouse-send", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-recieve", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-recieve-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "stop-when", {(l): A.a-name(l, A.s-type-global("Function"))}, "seconds-per-tick", {(l): A.a-name(l, A.s-type-global("NumPositive"))}, + "close-when-stop", {(l): A.a-name(l, A.s-type-global("Boolean"))}, "title", {(l): A.a-name(l, A.s-type-global("String"))}, - "close-when-stop", {(l): A.a-name(l, A.s-type-global("Boolean"))} ] reactor-fields = reactor-optional-fields.set("init", {(l): A.a-any(l)}) From 6e29038aa64d70b09c2a9575a35db56e3f02061b Mon Sep 17 00:00:00 2001 From: Ari Prakash Date: Thu, 12 Jun 2025 16:19:08 -0400 Subject: [PATCH 2/5] plumbing through connector object --- src/arr/trove/reactor-events.arr | 4 ++++ src/js/trove/reactors.js | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/arr/trove/reactor-events.arr b/src/arr/trove/reactor-events.arr index 49dca13c7..aa85104ba 100644 --- a/src/arr/trove/reactor-events.arr +++ b/src/arr/trove/reactor-events.arr @@ -23,3 +23,7 @@ data RawKeyEventType: | key-press end +data SendingHandlerResult: + | update(new-state :: S) + | send(new-state :: S, to-send :: M) +end diff --git a/src/js/trove/reactors.js b/src/js/trove/reactors.js index 263032649..512e24979 100644 --- a/src/js/trove/reactors.js +++ b/src/js/trove/reactors.js @@ -50,7 +50,8 @@ aliases: { "Event": "ReactorEvent", "RawKeyEventType": "RawKeyEventType", - "Reactor": ["local", "Reactor"] + "Reactor": ["local", "Reactor"], + "SendingHandlerResult": "SendingHandlerResult", }, datatypes: { "Reactor": ["data", "Reactor", ["a"], [], { @@ -201,6 +202,26 @@ return makeReactorRaw(newVal, handlers, tracing, trace.concat(thisInteractTrace)); }, "interact"); }), + "interact-connect": runtime.makeMethod1(function(self, connector) { + // connector: + // register-on-message: Message -> ... + // handle-message-return: Message -> void + // dispose: () -> void + // reconnect: () -> void + checkArity(2, arguments, "interact-connect", true); + let thisInteractTrace = []; + let tracer = null; + if (tracing) { + tracer = (newVal, oldVal, k) => { + thisInteractTrace.push(newVal); + k(); + }; + } + return runtime.safeCall( + () => externalInteractionHandler(init, handlers, tracer, connector), + (newVal) => makeReactorRaw(newVal, handlers, tracing, trace.concat(thisInteractTrace)), + "interact-connect"); + }), "start-trace": runtime.makeMethod0(function(self) { checkArity(1, arguments, "start-trace", true); return makeReactorRaw(init, handlers, true, [init]); From 16bd375d913eec9133f644c112ee92bba0e6179b Mon Sep 17 00:00:00 2001 From: Ari Prakash Date: Tue, 22 Jul 2025 23:16:58 -0400 Subject: [PATCH 3/5] add respond method as analogue to react --- src/arr/trove/reactor-events.arr | 10 +++ src/js/trove/reactors.js | 128 ++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/src/arr/trove/reactor-events.arr b/src/arr/trove/reactor-events.arr index aa85104ba..26a31ffa5 100644 --- a/src/arr/trove/reactor-events.arr +++ b/src/arr/trove/reactor-events.arr @@ -10,6 +10,11 @@ type Modifiers = { control :: Boolean } +# TODO: add other functions that we need +type Connector = { + handle-message-return :: (M -> Nothing) +} + data Event: | time-tick | mouse(x :: Number, y :: Number, kind :: String) @@ -17,6 +22,11 @@ data Event: | raw-key(symbol :: String, code :: Number, key :: String, action :: String, modifiers :: Modifiers) end +data ConnectedEvent: + | message(msg :: M) + | event(e :: Event) +end + data RawKeyEventType: | key-up | key-down diff --git a/src/js/trove/reactors.js b/src/js/trove/reactors.js index 512e24979..cc425cc33 100644 --- a/src/js/trove/reactors.js +++ b/src/js/trove/reactors.js @@ -24,7 +24,13 @@ name: "ValueSkeleton" }, "Table": { tag: "name", origin: { "import-type": "uri", uri: "builtin://global" }, - name: "Table" } + name: "Table" }, + "SendingHandlerResult": { tag: "name", + origin: { "import-type": "uri", uri: "builtin://reactor-events" }, + name: "SendingHandlerResult" }, + "ConnectedEvent": { tag: "name", + origin: { "import-type": "uri", uri: "builtin://reactor-events" }, + name: "ConnectedEvent" }, }, values: { "keypress": ["arrow", ["String"], ["local", "Event"]], @@ -34,6 +40,11 @@ "key-up": ["local", "RawKeyEventType"], "key-down": ["local", "RawKeyEventType"], "key-press": ["local", "RawKeyEventType"], + // TODO: make these forall + "update": ["arrow", ["Any"], ["local", "SendingHandlerResult"]], + "send": ["arrow", ["Any", "Any"], ["local", "SendingHandlerResult"]], + "message": ["arrow", ["Any", "Any"], ["local", "ConnectedEvent"]], + "event": ["arrow", ["Any", "Any"], ["local", "ConnectedEvent"]], "get-value": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]], "get-instance": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]], @@ -100,6 +111,8 @@ }; var annEvent = gtf(reactorEvents, "Event"); + var annConnectedEvent = gtf(reactorEvents, "ConnectedEvent"); + var annSendingHandlerResult = gtf(reactorEvents, "SendingHandlerResult"); var annNatural = runtime.makeFlatPredAnn(runtime.Number, runtime.makeFunction(function(val) { return jsnums.isInteger(val) && jsnums.greaterThanOrEqual(val, 0, runtime.NumberErrbacks); }, "Natural Number"), "Natural Number"); @@ -113,10 +126,44 @@ } var isEvent = gmf(reactorEvents, "is-Event"); + var isConnectedEvent = gmf(reactorEvents, "is-ConnectedEvent"); + var isSendingHandlerResult = gmf(reactorEvents, "is-SendingHandlerResult"); var externalInteractionHandler = null; var setInteract = function(newInteract) { externalInteractionHandler = newInteract; } + function callOrError(handlers, handlerName, args, cb) { + if(handlers.hasOwnProperty(handlerName)) { + var funObj = handlers[handlerName].app; + return runtime.safeCall(function() { + return funObj.apply(funObj, args); + }, cb, "react:" + handlerName); + } + else { + runtime.ffi.throwMessageException("No " + handlerName + " handler defined"); + } + } + function handleBaseEvent(handlers, init, event, cb) { + return runtime.ffi.cases(isEvent, "Event", event, { + keypress: function(key) { + return callOrError(handlers, "on-key", [init, key], cb); + }, + "time-tick": function() { + return callOrError(handlers, "on-tick", [init], cb); + }, + mouse: function(x, y, kind) { + return callOrError(handlers, "on-mouse", [init, x, y, kind], cb); + }, + "raw-key": function(key, type, caps, shift, alt, command, control) { + // NOTE(joe): we intentionally don't use all the fields above, assuming + // that users of on-raw-key are OK with consuming an event object + // rather than the fields of the event. This is mainly because typing + // out 8 parameters is pretty unreasonable, and this fancy version + // will mainly be used by folks who have gone through at least Reactive + return callOrError(handlers, "on-raw-key", [init, event], cb); + } + }); + } var makeReactor = function(init, fields) { runtime.ffi.checkArity(2, arguments, "reactor", false); c1("make-reactor", fields, annObject); @@ -259,24 +306,14 @@ react: runtime.makeMethod1(function(self, event) { checkArity(2, arguments, "react", true); c1("react", event, annEvent); - function callOrError(handlerName, args) { - if(handlers.hasOwnProperty(handlerName)) { - var funObj = handlers[handlerName].app; - return runtime.safeCall(function() { - return funObj.apply(funObj, args); - }, function(newVal) { - if(tracing) { - var newTrace = trace.concat([newVal]); - } - else { - var newTrace = trace; - } - return makeReactorRaw(newVal, handlers, tracing, newTrace); - }, "react:" + handlerName); + function reactCallback(newVal) { + if(tracing) { + var newTrace = trace.concat([newVal]); } else { - runtime.ffi.throwMessageException("No " + handlerName + " handler defined"); + var newTrace = trace; } + return makeReactorRaw(newVal, handlers, tracing, newTrace); } return runtime.safeCall(function() { if(handlers["stop-when"]) { @@ -288,29 +325,46 @@ }, function(stop) { if(stop) { return self; + } else { + return handleBaseEvent(handlers, init, event, reactCallback); } - else { - return runtime.ffi.cases(isEvent, "Event", event, { - keypress: function(key) { - return callOrError("on-key", [init, key]); - }, - "time-tick": function() { - return callOrError("on-tick", [init]); - }, - mouse: function(x, y, kind) { - return callOrError("on-mouse", [init, x, y, kind]); - }, - "raw-key": function(key, type, caps, shift, alt, command, control) { - // NOTE(joe): we intentionally don't use all the fields above, assuming - // that users of on-raw-key are OK with consuming an event object - // rather than the fields of the event. This is mainly because typing - // out 8 parameters is pretty unreasonable, and this fancy version - // will mainly be used by folks who have gone through at least Reactive - return callOrError("on-raw-key", [init, event]); - } + }, "react:stop-when"); + }), + respond: runtime.makeMethod2(function(self, event, connector) { + checkArity(3, arguments, "react", true); + c1("respond", event, annConnectedEvent); + function respondCallback(newVal) { + runtime.ffi.cases(isSendingHandlerResult, "SendingHandlerResult", newVal, { + update: (val) => + runtime.makeTuple([ + makeReactorRaw(val, handlers, tracing, trace.concat([val])), + runtime.ffi.makeNone(), + ]), + send: (val, msg) => + runtime.safeCall( + () => gf(connector, "handle-message-return").app(msg), + () => + runtime.makeTuple([ + makeReactorRaw(val, handlers, tracing, trace.concat([val])), + runtime.ffi.makeSome(msg), + ]), + ), + }); + } + return runtime.safeCall( + () => handlers["stop-when"]?.app(init) ?? false, + (stop) => { + if (stop) { + return self; + } else { + return runtime.ffi.cases(isConnectedEvent, "ConnectedEvent", event, { + message: (msg) => + callOrError(handlers, "on-recieve", [init, msg], respondCallback), + event: (e) => handleBaseEvent(handlers, init, e, respondCallback), }); } - }, "react:stop-when"); + } + ) }), "is-stopped": runtime.makeMethod0(function(self) { checkArity(1, arguments, "is-stopped", true); @@ -402,6 +456,8 @@ "key-down": gmf(reactorEvents, "key-down"), "key-press": gmf(reactorEvents, "key-press"), "make-reactor": F(makeReactor, "make-reactor"), + "update": gmf(reactorEvents, "update"), + "send": gmf(reactorEvents, "send"), "get-value": F(getValue, "get-value"), "get-instance": F(getValue, "get-instance"), From aeb6211771e02e4a6945b674fedd3c597daf629d Mon Sep 17 00:00:00 2001 From: Ari Prakash Date: Thu, 24 Jul 2025 20:49:41 -0400 Subject: [PATCH 4/5] spell receive correctly --- src/arr/compiler/compile-structs.arr | 4 ++-- src/js/trove/reactors.js | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/arr/compiler/compile-structs.arr b/src/arr/compiler/compile-structs.arr index 1f2dbd013..8d02d4e30 100644 --- a/src/arr/compiler/compile-structs.arr +++ b/src/arr/compiler/compile-structs.arr @@ -3325,8 +3325,8 @@ reactor-optional-fields = [SD.string-dict: "on-raw-key-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-mouse", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-mouse-send", {(l): A.a-name(l, A.s-type-global("Function"))}, - "on-recieve", {(l): A.a-name(l, A.s-type-global("Function"))}, - "on-recieve-send", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-receive", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-receive-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "stop-when", {(l): A.a-name(l, A.s-type-global("Function"))}, "seconds-per-tick", {(l): A.a-name(l, A.s-type-global("NumPositive"))}, "close-when-stop", {(l): A.a-name(l, A.s-type-global("Boolean"))}, diff --git a/src/js/trove/reactors.js b/src/js/trove/reactors.js index cc425cc33..cf560f688 100644 --- a/src/js/trove/reactors.js +++ b/src/js/trove/reactors.js @@ -133,6 +133,12 @@ externalInteractionHandler = newInteract; } function callOrError(handlers, handlerName, args, cb) { + if(handlers.hasOwnProperty(handlerName + "-send")) { + var funObj = handlers[handlerName + "-send"].app; + return runtime.safeCall(function() { + return funObj.apply(funObj, args); + }, cb, "react:" + handlerName); + } if(handlers.hasOwnProperty(handlerName)) { var funObj = handlers[handlerName].app; return runtime.safeCall(function() { @@ -334,7 +340,7 @@ checkArity(3, arguments, "react", true); c1("respond", event, annConnectedEvent); function respondCallback(newVal) { - runtime.ffi.cases(isSendingHandlerResult, "SendingHandlerResult", newVal, { + return runtime.ffi.cases(isSendingHandlerResult, "SendingHandlerResult", newVal, { update: (val) => runtime.makeTuple([ makeReactorRaw(val, handlers, tracing, trace.concat([val])), @@ -352,14 +358,20 @@ }); } return runtime.safeCall( - () => handlers["stop-when"]?.app(init) ?? false, + () => { + if (handlers["stop-when"]) { + return handlers["stop-when"].app(init); + } else { + return false; + } + }, (stop) => { if (stop) { return self; } else { return runtime.ffi.cases(isConnectedEvent, "ConnectedEvent", event, { message: (msg) => - callOrError(handlers, "on-recieve", [init, msg], respondCallback), + callOrError(handlers, "on-receive", [init, msg], respondCallback), event: (e) => handleBaseEvent(handlers, init, e, respondCallback), }); } @@ -458,6 +470,8 @@ "make-reactor": F(makeReactor, "make-reactor"), "update": gmf(reactorEvents, "update"), "send": gmf(reactorEvents, "send"), + "message": gmf(reactorEvents, "message"), + "event": gmf(reactorEvents, "event"), "get-value": F(getValue, "get-value"), "get-instance": F(getValue, "get-instance"), From 57e6d03a3df47543cc1a18f1024edd06d347c34c Mon Sep 17 00:00:00 2001 From: Ari Prakash Date: Thu, 24 Jul 2025 20:57:28 -0400 Subject: [PATCH 5/5] type reactor-events constructors correctly --- src/js/trove/reactors.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/js/trove/reactors.js b/src/js/trove/reactors.js index cf560f688..72c3924af 100644 --- a/src/js/trove/reactors.js +++ b/src/js/trove/reactors.js @@ -40,11 +40,17 @@ "key-up": ["local", "RawKeyEventType"], "key-down": ["local", "RawKeyEventType"], "key-press": ["local", "RawKeyEventType"], - // TODO: make these forall - "update": ["arrow", ["Any"], ["local", "SendingHandlerResult"]], - "send": ["arrow", ["Any", "Any"], ["local", "SendingHandlerResult"]], - "message": ["arrow", ["Any", "Any"], ["local", "ConnectedEvent"]], - "event": ["arrow", ["Any", "Any"], ["local", "ConnectedEvent"]], + + "update": ["forall", ["s", "m"], + ["arrow", ["tid", "s"], + ["tyapp", ["local", "SendingHandlerResult"], [["tid", "s"], ["tid", "m"]]]]], + "send": ["forall", ["s", "m"], + ["arrow", [["tid", "s"], ["tid", "m"]], + ["tyapp", ["local", "SendingHandlerResult"], [["tid", "s"], ["tid", "m"]]]]], + "message": ["forall", ["m"] + ["arrow", [["tid", "m"]], ["tyapp", ["local", "ConnectedEvent"], [["tid", "m"]]]]], + "event": ["forall", ["m"] + ["arrow", [["local", "Event"]], ["tyapp", ["local", "ConnectedEvent"], [["tid", "m"]]]]], "get-value": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]], "get-instance": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]],