Skip to content

Commit 3dbc461

Browse files
committed
eval mode
note that we no longer show the timeout error new UX kind of assumes timing out is fine and normal plus it's obvious whether an expression has finished eval by whether there are redexes highlighted (at least for small expressions) and anyway, the old `"No selection for eval"` message was never actually shown, since we mistakenly didn't show the `error` field when `expr` was `Nothing`. we still show `"No definition selected for evaluation"` now, so nothing has been lost with this change UX a bit weird for now... requires further thought e.g. changing an eval opt resets the interactive-eval state, i.e. back to fulleval output Signed-off-by: George Thomas <[email protected]>
1 parent 825187f commit 3dbc461

File tree

5 files changed

+177
-41
lines changed

5 files changed

+177
-41
lines changed

primer-miso/primer-miso.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ library
4747
, containers >=0.6.0.1 && <0.7.0
4848
, data-default ^>=0.8.0.0
4949
, deriving-aeson >=0.2 && <0.3.0
50+
, exceptions >=0.10.4 && <0.11.0
5051
, extra >=1.7.10 && <1.8.0
5152
, jsaddle ^>=0.9.9.2
5253
, jsaddle-dom ^>=0.9.9.2
@@ -60,6 +61,7 @@ library
6061
, stm >=2.5 && <2.6.0
6162
, stm-containers >=1.1 && <1.3.0
6263
, text >=2.0 && <2.2
64+
, transformers >=0.5.6.2 && <0.7.0
6365
, uniplate >=1.6 && <1.7.0
6466
, uuid-types ^>=1.0.5.1
6567

primer-miso/src/Primer/Miso.hs

Lines changed: 121 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import Foreword
1212

1313
import Clay qualified
1414
import Control.Monad.Except (liftEither)
15-
import Control.Monad.Log (Severity (Notice), WithSeverity, msgSeverity)
15+
import Control.Monad.Log (WithSeverity)
16+
import Control.Monad.Trans.Maybe (runMaybeT)
1617
import Data.Aeson (FromJSON, ToJSON)
1718
import Data.Data (Data (..))
1819
import Data.Default qualified as Default
1920
import Data.Generics.Uniplate.Data (children)
21+
import Data.List.Extra (compareLength)
2022
import Data.Map ((!?))
2123
import Data.Map qualified as Map
2224
import Data.Tree (Tree)
@@ -129,9 +131,9 @@ import Primer.Core (
129131
import Primer.Core qualified as Primer
130132
import Primer.Core.Utils (forgetTypeMetadata)
131133
import Primer.Def (defAST)
132-
import Primer.Eval (Dir (..), NormalOrderOptions (..))
134+
import Primer.Eval (Dir (..), EvalLog, NormalOrderOptions (..), findRedex, redexes, step)
133135
import Primer.Eval.Redex (RunRedexOptions (..), ViewRedexOptions (..))
134-
import Primer.EvalFullStep (EvalFullError (TimedOut), EvalLog, evalFull)
136+
import Primer.EvalFullStep (EvalFullError (TimedOut), evalFull)
135137
import Primer.JSON (CustomJSON (..), PrimerJSON)
136138
import Primer.Log (runPureLogT)
137139
import Primer.Miso.Layout (
@@ -154,21 +156,24 @@ import Primer.Miso.Util (
154156
bindingsInExpr,
155157
bindingsInType,
156158
clayToMiso,
159+
exitFullscreen,
157160
findASTDef,
158161
kindsInType,
162+
logAllToConsole,
159163
nodeSelectionType,
160164
optToName,
161165
readMs,
162166
realToClay,
167+
requestFullscreen,
168+
runEval,
163169
runMutationWithNullDb,
164-
runTC,
165170
selectedDefName,
166171
showMs,
167172
startAppWithSavedState,
168173
stringToOpt,
169174
typeBindingsInExpr,
170175
)
171-
import Primer.Name (Name, unName)
176+
import Primer.Name (Name, NameCounter, unName)
172177
import Primer.Typecheck (SmartHoles (SmartHoles), buildTypingContext, exprTtoExpr, typeTtoType)
173178

174179
start :: JSM ()
@@ -187,11 +192,15 @@ start =
187192
}
188193
, eval =
189194
EvalModel
190-
{ expr = Nothing
191-
, error = Nothing
192-
, opts =
195+
{ opts =
193196
EvalOpts
194-
{ normalOrder = UnderBinders
197+
{ -- TODO we're setting these to `False` so that the initial states match visuals
198+
-- would maybe be better to use our defaults from tests and and old frontend instead
199+
-- (see `evalFullTest` - grouped lets, push and elide, aggressive)
200+
-- requires Miso stuff - not sure how to set initial state properly
201+
-- OTOH, maybe just reduce steps to 0? esp. now that we have interactive eval
202+
-- since the initial state isn't really displayed there anyway
203+
normalOrder = UnderBinders
195204
, viewRedex =
196205
ViewRedexOptions
197206
{ groupedLets = False
@@ -202,10 +211,11 @@ start =
202211
RunRedexOptions
203212
{ pushAndElide = False
204213
}
205-
, stepLimit = 10
214+
, stepLimit = 0
206215
, dir = Chk
207216
}
208217
, fullscreen = False
218+
, history = []
209219
}
210220
}
211221
}
@@ -245,10 +255,17 @@ data ActionPanelModel = ActionPanelModel
245255
deriving (ToJSON, FromJSON) via PrimerJSON ActionPanelModel
246256

247257
data EvalModel = EvalModel
248-
{ expr :: Maybe Expr
249-
, error :: Maybe MisoString
250-
, opts :: EvalOpts
258+
{ opts :: EvalOpts
251259
, fullscreen :: Bool
260+
, history :: [(Expr, (ID, NameCounter), ([ID], Maybe ID))]
261+
-- ^ current first, original last
262+
-- (where original is the expr we get to from non-interactive eval)
263+
-- includes eval counter states after producing the expr
264+
-- empty means no def selection
265+
-- TODO set default steps down to 0 on account of this? be nice to be less arbitrary anyway, but maybe orthogonal
266+
-- also, we now cache redexes here - not strictly necessary but a simple optimisation that also makes undo code simpler
267+
-- well, now it's the only place we put them, but still we don't _have_ to remember old ones
268+
-- TODO use record? `data EvalHistoryEntry`?
252269
}
253270
deriving stock (Eq, Show, Generic)
254271
deriving (ToJSON, FromJSON) via PrimerJSON EvalModel
@@ -276,6 +293,8 @@ data Action
276293
| SetApp Primer.App.App
277294
| SetEvalOpts (EvalOpts -> EvalOpts)
278295
| ToggleFullscreenEval
296+
| ChooseRedex ID
297+
| StepBackEval
279298

280299
updateModel :: Action -> Model -> Effect Action Model
281300
updateModel =
@@ -333,7 +352,55 @@ updateModel =
333352
SetEvalOpts f -> do
334353
#components % #eval % #opts %= f
335354
setEval
336-
ToggleFullscreenEval -> #components % #eval % #fullscreen %= not
355+
ToggleFullscreenEval -> do
356+
#components % #eval % #fullscreen %= not
357+
-- TODO this is nice on mobile...
358+
-- orthogonal to this commit
359+
-- we should really separate the notions of "eval takes up whole app" and "app is actually fullscreen"
360+
-- JSaddle code needs clean up - error handling, not using `eval` etc.
361+
-- ideally, mobile browsers would just have a generic option for this, like pressing f11 on desktop...
362+
-- Opera Mini has this but it's clunky
363+
fs' <- use $ #components % #eval % #fullscreen
364+
scheduleIO_ $
365+
void
366+
if fs'
367+
then requestFullscreen
368+
else exitFullscreen
369+
ChooseRedex id -> do
370+
app <- use #app
371+
history <- use $ #components % #eval % #history
372+
opts <- use $ #components % #eval % #opts
373+
let
374+
-- TODO DRY with view
375+
defs = progAllDefs (appProg app)
376+
tydefs = progAllTypeDefs (appProg app)
377+
defs' = snd <$> defs
378+
tydefs' = snd <$> tydefs
379+
let exprToEvalMaybe = case history of
380+
(e0', s0', _) : _ -> Just (e0', s0')
381+
[] -> Nothing
382+
case exprToEvalMaybe of
383+
-- TODO `Nothing` means no def selected...
384+
-- this should only actually happen on race conditions,
385+
-- given that `ChooseRedex` will only be triggered by clicking on an eval expr
386+
Nothing -> pure ()
387+
Just (currentEvalExpr, s0) -> do
388+
let ((res, evalStepLogs), s1) =
389+
runEval s0 $ step opts.viewRedex.avoidShadowing' tydefs' defs' currentEvalExpr opts.dir id
390+
scheduleIO_ $ logAllToConsole evalStepLogs
391+
case res of
392+
-- TODO set the error field instead?
393+
Left err -> scheduleIO_ $ consoleLog $ "eval error: " <> show err
394+
-- TODO do something with detail? at least note that it's unused?
395+
Right (expr, _detail) -> do
396+
scheduleIO_ $ logAllToConsole redexesLogs
397+
#components % #eval % #history %= ((expr, s1, rxs) :)
398+
where
399+
(rxs, redexesLogs) = getRedexes opts tydefs' defs' expr
400+
StepBackEval -> do
401+
(#components % #eval % #history) %= \case
402+
[] -> [] -- TODO this shouldn't really happen - we should actually grey out the button in this state
403+
_ : es -> es
337404
where
338405
-- TODO the only part of this that should really require `IO` is writing to a database
339406
-- (currently we use `NullDb` anyway but this will change)
@@ -353,28 +420,30 @@ updateModel =
353420
opts <- use $ #components % #eval % #opts
354421
fullscreen <- use $ #components % #eval % #fullscreen
355422
let (tydefs, defs, maybeDef) = getDefs app
423+
-- TODO clean this whole section up a bit in separate commit
424+
-- maybe re-inline `runEval`? now only used twice (`runPureLogT @_ @(WithSeverity EvalLog)`)
356425
evalModel <- case maybeDef of
357-
Nothing -> pure EvalModel{expr = Nothing, error = Just "No selection for eval", opts, fullscreen}
426+
Nothing -> pure EvalModel{opts, fullscreen, history = []}
358427
Just def -> do
359428
let nextId = succ $ appIdCounter app
360429
nextName = succ $ appNameCounter app
361430
-- TODO put this in to a background thread rather than blocking whole program for expensive evaluations
362-
(evalResult, logs) =
363-
either absurd identity
364-
. runTC (succ nextId, nextName)
365-
. runPureLogT
431+
((evalResult, evalFullLogs), s') =
432+
runEval (succ nextId, nextName)
366433
. evalFull opts.normalOrder opts.viewRedex opts.runRedex tydefs defs opts.stepLimit opts.dir
367434
$ Ann (Meta nextId Nothing Nothing) (exprTtoExpr def.expr) (typeTtoType def.sig)
368-
scheduleIO_ $ logAllToConsole @EvalLog logs
369-
pure case evalResult of
370-
Left (TimedOut expr) -> EvalModel{expr = Just expr, error = Just "Eval timed out:", opts, fullscreen}
371-
Right expr -> EvalModel{expr = Just expr, error = Nothing, opts, fullscreen}
435+
expr = case evalResult of
436+
Left (TimedOut e) -> e
437+
Right e -> e
438+
(rxs, redexesLogs) = getRedexes opts tydefs defs expr
439+
scheduleIO_ $ logAllToConsole evalFullLogs
440+
scheduleIO_ $ logAllToConsole redexesLogs
441+
pure EvalModel{opts, fullscreen, history = [(expr, s', rxs)]}
372442
#components % #eval .= evalModel
373-
-- TODO better logging, including handling different severities appropriately
374-
logAllToConsole :: Show a => Seq (WithSeverity a) -> JSM ()
375-
logAllToConsole logs =
376-
let issues = filter ((<= Notice) . msgSeverity) $ toList logs
377-
in unless (null issues) $ consoleLog $ ms $ unlines $ map show issues
443+
getRedexes (opts :: EvalOpts) tydefs defs expr = runIdentity $ runPureLogT @_ @(WithSeverity EvalLog) do
444+
normalOrderRedex <- runMaybeT $ getID <$> findRedex opts.normalOrder opts.viewRedex tydefs defs opts.dir expr
445+
allRedexes <- redexes opts.viewRedex.avoidShadowing' tydefs defs opts.dir expr
446+
pure (allRedexes, normalOrderRedex)
378447
-- TODO DRY this with `viewModel`
379448
-- when we use Miso components it might be easier to compute this in one place then send messages around
380449
getDefs app =
@@ -512,6 +581,10 @@ viewModel Model{..} =
512581
]
513582
in div_
514583
[id_ "options"]
584+
-- TODO some of these should in principle be able to be set on the fly for interactive eval
585+
-- i.e. without resetting history
586+
-- possibly crucial to debugging eval issues I've been seeing
587+
-- of course it's hard to square this with wanting the non-interactive eval output to reflect opts
515588
[ checkBox "Stop at binders" \b -> #normalOrder .~ if b then StopAtBinders else UnderBinders
516589
, checkBox "Grouped lets" \b -> #viewRedex % #groupedLets .~ b
517590
, checkBox "Aggressive elision" \b -> #viewRedex % #aggressiveElision .~ b
@@ -530,17 +603,28 @@ viewModel Model{..} =
530603
]
531604
, text "Steps"
532605
]
533-
, button_ [onClick ToggleFullscreenEval] [""]
534606
]
607+
, div_
608+
-- TODO add styling here, and make this a separate commit
609+
-- maybe even just squash with introduction of the fullscreen button
610+
[id_ ""]
611+
$ munless (compareLength components.eval.history 2 == LT) [button_ [onClick StepBackEval] [""]]
612+
<> [ button_ [onClick ToggleFullscreenEval] [""]
613+
]
535614
]
536-
<> case components.eval.expr of
537-
Nothing -> [text "No definition selected for evaluation"]
538-
Just expr ->
539-
( case components.eval.error of
540-
Nothing -> []
541-
Just s -> [text s]
542-
)
543-
<> [fst . viewTree $ viewTreeExpr mkMeta expr]
615+
<> case components.eval.history of
616+
[] -> [text "No definition selected for evaluation"]
617+
(expr, _, rxs) : _ ->
618+
[fst . viewTree $ viewTreeExpr (mkMeta' . getID) expr]
619+
where
620+
mkMeta' id =
621+
if not $ id `elem` fst rxs
622+
then (Just id, Nothing, NoHighlight)
623+
else
624+
( Just id
625+
, Just $ ChooseRedex id
626+
, if snd rxs == Just id then AnimatedHighlight else SimpleHighlight
627+
)
544628
]
545629
where
546630
mkMeta = const (Nothing, Nothing, NoHighlight)

primer-miso/src/Primer/Miso/Util.hs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77

88
-- | Things which should really be upstreamed rather than living in this project.
99
module Primer.Miso.Util (
10+
requestFullscreen,
11+
exitFullscreen,
1012
startAppWithSavedState,
1113
showMs,
1214
readMs,
15+
logAllToConsole,
1316
clayToMiso,
1417
P2,
1518
unitX,
1619
unit_X,
1720
unitY,
1821
unit_Y,
1922
runTC,
23+
runEval,
2024
TypeT,
2125
TermMeta',
2226
NodeSelectionT,
@@ -50,7 +54,7 @@ import Clay.Stylesheet qualified as Clay
5054
import Control.Concurrent.STM (atomically, newTBQueueIO)
5155
import Control.Monad.Extra (eitherM)
5256
import Control.Monad.Fresh (MonadFresh (..))
53-
import Control.Monad.Log (WithSeverity)
57+
import Control.Monad.Log (Severity (Notice), WithSeverity, msgSeverity)
5458
import Data.Aeson (FromJSON, ToJSON)
5559
import Data.Bitraversable (bitraverse)
5660
import Data.Map qualified as Map
@@ -62,6 +66,7 @@ import Linear.Affine (Point (..), unP)
6266
import Miso (
6367
App (initialAction, model, subs, update, view),
6468
JSM,
69+
consoleLog,
6570
getLocalStorage,
6671
mapSub,
6772
setLocalStorage,
@@ -127,13 +132,25 @@ import Primer.Core (
127132
)
128133
import Primer.Database qualified as DB
129134
import Primer.Def (ASTDef (..), Def (..), DefMap, astDefExpr)
135+
import Primer.Eval (AvoidShadowing (..), EvalLog, ViewRedexOptions (..))
130136
import Primer.JSON (CustomJSON (..), PrimerJSON)
131-
import Primer.Log (runPureLogT)
137+
import Primer.Log (PureLogT, runPureLogT)
132138
import Primer.Name (Name, NameCounter)
133139
import Primer.TypeDef (TypeDefMap)
134140
import Primer.Typecheck (ExprT, exprTtoExpr, typeTtoType)
135141
import StmContainers.Map qualified as StmMap
136142

143+
-- import Language.Javascript.JSaddle hiding ((<#))
144+
import Language.Javascript.JSaddle qualified as J
145+
146+
-- import Optics
147+
148+
-- exitFullscreen x = (doc J.# ("exitFullscreen" :: MisoString)) ()
149+
requestFullscreen :: JSM J.JSVal
150+
requestFullscreen = J.eval ("document.documentElement1.requestFullscreen()" :: MisoString)
151+
exitFullscreen :: JSM J.JSVal
152+
exitFullscreen = J.eval ("document.exitFullscreen()" :: MisoString)
153+
137154
{- Miso -}
138155

139156
-- https://github.com/dmjio/miso/issues/749
@@ -165,6 +182,12 @@ showMs = ms . show @_ @String
165182
readMs :: Read a => MisoString -> Maybe a
166183
readMs = readMaybe @_ @String . fromMisoString
167184

185+
-- TODO better logging, including handling different severities appropriately
186+
logAllToConsole :: Show a => Seq (WithSeverity a) -> JSM ()
187+
logAllToConsole logs =
188+
let issues = filter ((<= Notice) . msgSeverity) $ toList logs
189+
in unless (null issues) $ consoleLog $ ms $ unlines $ map show issues
190+
168191
{- Clay -}
169192

170193
-- https://github.com/sebastiaanvisser/clay/issues/208
@@ -223,8 +246,14 @@ instance MonadFresh ID (M e) where
223246
fresh = M $ _1 <<%= succ
224247
instance MonadFresh NameCounter (M e) where
225248
fresh = M $ _2 <<%= succ
226-
runTC :: (ID, NameCounter) -> M e a -> Either e a
227-
runTC s0 = runExcept . flip evalStateT s0 . (.unM)
249+
runTC :: (ID, NameCounter) -> M e a -> (Either e (a, (ID, NameCounter)))
250+
runTC s0 = runExcept . flip runStateT s0 . (.unM)
251+
252+
runEval ::
253+
(ID, NameCounter) ->
254+
PureLogT (WithSeverity EvalLog) (M Void) a ->
255+
((a, Seq (WithSeverity EvalLog)), (ID, NameCounter))
256+
runEval s = either absurd identity . runTC s . runPureLogT
228257

229258
-- analogous with `ExprT`/`TypeT`
230259
-- type KindT = Kind' KindMetaT
@@ -382,3 +411,7 @@ runMutationWithNullDb req app = do
382411
(_res, logs) <- either absurd (first $ first Right) <$> race runDB runReq
383412
res <- atomically $ StmMap.lookup sid sessions -- returning `_res` would mean discarding the ID counter state
384413
pure (logs, maybe (error "impossible: ") (.sessionApp) res)
414+
415+
-- TODO `ViewRedexOptions` should use `AvoidShadowing` instead of `Bool`
416+
instance HasField "avoidShadowing'" ViewRedexOptions AvoidShadowing where
417+
getField o = if o.avoidShadowing then AvoidShadowing else NoAvoidShadowing

0 commit comments

Comments
 (0)