-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async as #286
Conversation
Also get rid of `getRobotLoc` command since it is no longer needed.
I don't know if you saw my other recent comment but I am skeptical of this whole approach. And since the CESK machine for a win condition check is run in its own separate sub-call to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, @byorgey I got the background as
to work 🙂
There a few things to iron out, mainly around printing, but the core of the change is pretty small (new async value, step (as&poll), IO cancel).
src/Swarm/Game/Step.hs
Outdated
VAsync (GAsync n1 _) -> \case | ||
VAsync (GAsync n2 _) -> return $ compare n1 n2 | ||
VInt n2 -> return $ compare (fromIntegral n1) n2 | ||
v2 -> incompatCmp v1 v2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably a bad idea as the VAsync
value should be unobtainable anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I don't think there is any reason for VAsync
values to be comparable.
(VtyEvent (V.EvKey (V.KChar 'c') [V.MCtrl])) -> | ||
let cancel' = do | ||
cm <- let m = s ^. gameState . robotMap . to (! "base") . machine in cancel m | ||
pure (s & gameState . robotMap . ix "base" . machine .~ cm) | ||
in continue =<< liftIO cancel' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Funny thing, I could not figure out how to make ix
a Getter here or do the update in IO. 🤔
src/Swarm/Language/Syntax.hs
Outdated
| -- | Type for values that can not be directly created in code. | ||
-- They are shown as e.g. @(__r1__)@, like C-style internal mangled | ||
-- variables. With custom character it is slightly better then | ||
-- using TInt. | ||
TNonCode Char Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@byorgey the name and printing is not great, but I did not want to create another TAsync
here.
I changed it for VRef
too, what do you think about it? 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't like this. VAsync
and VRef
do not show up in the surface language, so there should not be a way to represent them in Syntax
. Pretty-printing Value
s via valueToTerm
was a horrible lazy hack in the first place; we should be trying to get rid of technical debt, not letting my lazy decisions in the past snowball into even more technical debt for ourselves. We should either just do something simple and stupid for now in valueToTerm
for VAsync
(like we were doing for VRef
), or bite the bullet and write a proper Value
pretty-printer.
src/Swarm/Language/Typecheck.hs
Outdated
@@ -384,6 +384,7 @@ infer s@(Syntax l t) = (`catchError` addLocToTypeErr s) $ case t of | |||
ty2 <- maybe id (`withBinding` Forall [] a) mx $ infer c2 | |||
_ <- decomposeCmdTy ty2 | |||
return ty2 | |||
TNonCode {} -> return UTyInt -- TODO: probably wrong, but I do not understand this code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@byorgey I might need a little help here 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is nothing sensible to do here because there is no way to know what the type of a TNonCode
is. Another reason that we should get rid of it. =)
src/Swarm/Game/Step.hs
Outdated
|
||
va <- | ||
sendIO . Async.async $ | ||
either VString id -- TODO: that is not right... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This produces a wrong type on exception:
┌───────────────────────────────────┐
│ > as "base" {raise "tree"} │
│ "user exception: tree" : ∀ a1. a1 │
└───────────────────────────────────┘
But that should probably be solved with a better type of GAsync
.
@byorgey right, did not see your comment here 🙂
Sure, it is easy that way, but I thought the original idea was to delegate it to some invisible system robot?
I think it will only do a one pass over to as "base" {wait 10000000} // waits once
as "base" {let f = wait 1; f in f}
testLIST_ALL // run "example/list" Either way it should not stop us from making our godly robots more omnipotent and I do not see much overhead with this async change (we are passing around current immutable state, which is cheap). 👼 |
I think I considered that briefly but decided against it since I don't see much benefit, besides possibly saving a tiny bit of code because we already have code to run programs contained in robots. But in the end I think we would need a bunch of special cases either way. A win condition checker is fundamentally different than a robot. Actually I'm not even sure I see how you could delegate the win checking to an invisible system robot. How would that work, exactly? What program would it run? Some kind of infinite loop? And how would it signal that you have won?
Can you elaborate on how that would work? I am honestly curious, I can't imagine how it would work. Or at least I don't see how running the win condition checking in a special system robot vs as a separate program would make this any easier to support.
Right, but to me this doesn't really seem like a problem worth solving. |
Stepping back, part of me thinks this is cool, but part of me wonders whether it is extra complication for little benefit. I would be happy to be wrong, but it feels to me like a solution without a problem. Can you explain exactly what problem this is supposed to be solving? |
@byorgey Thinking about it some more, your Long term I would love to take advantage of more CPUs (CESK only runs on one) in game and the
I am not sure, but would the CESK machine in robot turn With the more direct signalling, other robots could wait&check if others finished. My dream tutorial looks like this:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried to give some useful feedback even though I am still on the fence about whether it is worth merging this. I guess it is worthwhile seeing how good we can make this before making a final decision about merging it.
I am not sure it is a good idea to always run as
in an async computation. Or perhaps we should have two versions, normal as
which runs in an async computation, and unsafeAs
which doesn't. However (vague brainstorming) it might also be cool to put this more in the user's control, i.e. some kind of built-in promise
type with async
and await
commands...?
src/Swarm/Game/CESK.hs
Outdated
s' = resetBlackholes <$> getStore cesk | ||
getStore :: CESK -> IO Store | ||
getStore (In _ _ s _) = pure s | ||
getStore (Out v s _) = (case v of VAsync (GAsync _ as) -> Async.cancel as; _ -> pure ()) >> pure s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't guaranteed to cancel all currently running async computations, because there are lots of other places VAsync
values could be hiding: in the environment, in a closure, ... I don't know how important this is. Perhaps not that important, since if an async computation keeps running but nothing ever asks for its value it only wastes some CPU cycles, it won't cause any crashes or anything like that. If it is important, we would have to do something like put references to currently outstanding async computations in the store, so that we can find them all when we want to cancel them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I do not want to cancel all async computations - the ones currently being run by base are enough.
We could store the async values in GameState and add some way to cancel all of them in the UI.
src/Swarm/Game/Step.hs
Outdated
-- ----------------------------------------------------------------------------- | ||
-- TODO: move this code out | ||
-- Slightly edited copy of Unit.hs version of runCESK: | ||
let runCESK' :: CESK -> StateT Robot (StateT GameState IO) (Either Text Value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we already have this function in Step.hs
? Hmm, or maybe that's only in my challenge-mode
branch? Feel free to copy it: https://github.com/byorgey/swarm/pull/285/files#diff-bb4a5ea570cea9f815cdd1d92b33d4b6bd8daaa7d74c7014916c642c159e7d80R147-R173
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I will do so during rebase 🙂
src/Swarm/Game/Step.hs
Outdated
|
||
-- Return the value returned by the hypothetical command. | ||
return $ Out v s k | ||
i <- gensym <+= 1 | ||
return $ Out (VAsync $ GAsync i va) s (FApp (VCApp Poll []) : k) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually, it probably makes more sense to make a new kind of Frame
called FPoll
rather than making Poll
a primitive function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went the other way and made poll
a real command. 🤷♂️
src/Swarm/Game/Step.hs
Outdated
mv <- sendIO $ Async.poll va | ||
-- let mv = Nothing :: Maybe (Either SomeException Value) | ||
let (msg, cesk) = case mv of | ||
Nothing -> ("in progress", Waiting (time + 8) (Out (VAsync ga) s (FApp (VCApp Poll []) : k))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it makes sense to use some kind of exponential backoff (up to a maximum), e.g. if it hasn't finished after 8 steps, then the next time we wait 16, then 32, ... up to a maximum of 64 or something like that? For a long-running computation, checking every 8 time steps seems like a lot of polling, especially if we are writing to the robot log every time we check. We could put the waiting time to use as an argument of FPoll
, i.e. FPoll n
means "check if the async computation is done, and if not, wait n
time steps until checking again".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I let await
take int
waiting period, but it would be fun to give it int -> int
function instead 😄
src/Swarm/Game/Step.hs
Outdated
VString t1 -> \case VString t2 -> return (compare t1 t2); v2 -> incompatCmp v1 v2 | ||
VDir d1 -> \case VDir d2 -> return (compare d1 d2); v2 -> incompatCmp v1 v2 | ||
VBool b1 -> \case VBool b2 -> return (compare b1 b2); v2 -> incompatCmp v1 v2 | ||
VInt n1 -> \case | ||
VInt n2 -> return (compare n1 n2) | ||
VAsync (GAsync n2 _) -> return $ compare n1 (fromIntegral n2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this at all. Why would we compare a VInt
against the number in a GAsync
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 🙂
src/Swarm/Language/Syntax.hs
Outdated
deriving (Eq, Ord, Enum, Bounded, Data, Show) | ||
|
||
allConst :: [Const] | ||
allConst = [minBound .. maxBound] | ||
allConst = [minBound .. As] -- no Poll in code! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, see, this is telling us that we really should make poll a type of Frame
instead of a Const
. =)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went full commands instead. 🙂
src/Swarm/Language/Syntax.hs
Outdated
| -- | Type for values that can not be directly created in code. | ||
-- They are shown as e.g. @(__r1__)@, like C-style internal mangled | ||
-- variables. With custom character it is slightly better then | ||
-- using TInt. | ||
TNonCode Char Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't like this. VAsync
and VRef
do not show up in the surface language, so there should not be a way to represent them in Syntax
. Pretty-printing Value
s via valueToTerm
was a horrible lazy hack in the first place; we should be trying to get rid of technical debt, not letting my lazy decisions in the past snowball into even more technical debt for ourselves. We should either just do something simple and stupid for now in valueToTerm
for VAsync
(like we were doing for VRef
), or bite the bullet and write a proper Value
pretty-printer.
src/Swarm/Language/Typecheck.hs
Outdated
@@ -384,6 +384,7 @@ infer s@(Syntax l t) = (`catchError` addLocToTypeErr s) $ case t of | |||
ty2 <- maybe id (`withBinding` Forall [] a) mx $ infer c2 | |||
_ <- decomposeCmdTy ty2 | |||
return ty2 | |||
TNonCode {} -> return UTyInt -- TODO: probably wrong, but I do not understand this code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is nothing sensible to do here because there is no way to know what the type of a TNonCode
is. Another reason that we should get rid of it. =)
src/Swarm/Language/Typecheck.hs
Outdated
@@ -476,6 +477,7 @@ inferConst c = toU $ case c of | |||
Exp -> arithBinT | |||
AppF -> [tyQ| (a -> b) -> a -> b |] | |||
As -> [tyQ| string -> {cmd a} -> cmd a |] | |||
Poll -> [tyQ| cmd a |] -- TODO: poll is NOT exported as command |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, Poll
does not have any sensible type---yet another reason it should be a Frame
instead of a Const
.
handleREPLEvent s = \case | ||
(VtyEvent (V.EvKey (V.KChar 'c') [V.MCtrl])) -> | ||
let cancel' = do | ||
cm <- let m = s ^. gameState . robotMap . to (! "base") . machine in cancel m |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using (! "base")
will crash if there is no robot named "base"
which is a big no-no! I will have to think about how to rewrite this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I have no idea, let me know if you think of something 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean one simple thing is just to wrap this in a check: first check if the robotMap
has a key "base"
and if so do the cancelling stuff.
Agreed! Though I think we could also do this more simply by running multiple robots in parallel.
Yes, that would work, but it would mean we have to specially handle that robot to check its return value and restart it. We wouldn't really get much benefit from treating it as a robot as opposed to just a special standalone CESK machine.
Yes, this sounds really cool, but I wonder if we could do this more simply by adding a mechanism for mutable state, aka "static variables". If there is a CESK machine we run on every tick, when it notices that the first check point is finished, it sets a mutable variable in the store; when we run it on the next tick, we pass the same store to it so it can have state that persists across runs. It then sees that the first checkpoint is complete so it only checks for the second checkpoint, and so on. |
For entities that should always be drawn as if the robot knows what they are. Mostly useful in creating challenge scenarios.
Co-authored-by: Ondřej Šebek <[email protected]>
Co-authored-by: Ondřej Šebek <[email protected]>
We don't actually need an enumeration type for different game types. There are really several orthogonal things going on: 1. How the world, robots, inventory, etc. are initialized. 2. Whether there is a winning condition. 3. Whether we are in "creative mode", allowing the player to do anything they want.
@byorgey I went with the (Oops, the poll type is actually wrong, let me fix that...) |
Hmm,
I tried with changing it to |
VUnit -> \case VUnit -> return EQ; v2 -> incompatCmp VUnit v2 | ||
VInt n1 -> \case VInt n2 -> return (compare n1 n2); v2 -> incompatCmp v1 v2 | ||
VString t1 -> \case VString t2 -> return (compare t1 t2); v2 -> incompatCmp v1 v2 | ||
VDir d1 -> \case VDir d2 -> return (compare d1 d2); v2 -> incompatCmp v1 v2 | ||
VBool b1 -> \case VBool b2 -> return (compare b1 b2); v2 -> incompatCmp v1 v2 | ||
VAsync _ a1 -> \case VAsync _ a2 -> return $ compare a1 a2; v2 -> incompatCmp v1 v2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really think comparing VAsync
values makes much sense. What would you use this for? And what would it mean for one to be less than another?
deriving (Eq, Show) | ||
|
||
-- | Type for asynchronous computation which may raise an Exn. | ||
newtype GAsync a = GAsync {asyncComp :: Async.Async (Either Exn a)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need the GAsync
type? What's the benefit of defining GAsync
as opposed to inlining its definition directly into VAsync
(i.e. VAsync :: Term -> Async.Async (Either Exn Value) -> Value
)? It kind of feels like unnecessary indirection to me.
@@ -530,6 +544,10 @@ pattern TBind v t1 t2 = SBind v (STerm t1) (STerm t2) | |||
pattern TDelay :: DelayType -> Term -> Term | |||
pattern TDelay m t = SDelay m (STerm t) | |||
|
|||
-- | Match a TDelay without syntax |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment needs to be updated.
What is the purpose of SFuture
/ TFuture
? It seems like it can never actually result from parsing surface syntax, right? Is it only for pretty-printing values? As I think I mentioned before I really don't want to add extra stuff to the AST just to be able to pretty-print values.
As | ||
| -- | Hypothetically run a command as if you were another robot, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async
doesn't actually have the sense of "as if you were another robot", does it?
handleREPLEvent s = \case | ||
(VtyEvent (V.EvKey (V.KChar 'c') [V.MCtrl])) -> | ||
let cancel' = do | ||
cm <- let m = s ^. gameState . robotMap . to (! "base") . machine in cancel m |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean one simple thing is just to wrap this in a check: first check if the robotMap
has a key "base"
and if so do the cancelling stuff.
time <- use ticks | ||
mv <- sendIO $ Async.poll a | ||
return $ case mv of | ||
Nothing -> Waiting (time + 8) (Out va s (FApp (VCApp Await []) : k)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think an FExec
is needed, so that the Await
is re-executed once it is applied.
Nothing -> Waiting (time + 8) (Out va s (FApp (VCApp Await []) : k)) | |
Nothing -> Waiting (time + 8) (Out va s (FApp (VCApp Await []) : FExec : k)) |
I do not think we need this now, but if we ever want to explore this direction we can look here for tips. 🙂 |
async
library to check onas
computation until it is done