From b37ea420399de9197e6ae80f17f9dc4555dfe299 Mon Sep 17 00:00:00 2001 From: Pete Vilter Date: Mon, 27 Jul 2015 17:26:47 -0700 Subject: [PATCH] start integrating debugger based on task-based runtime API see (github.com/vilterp/new-debugger-api) for prior history still haven't fully merged UI, but it runs. --- Setup.hs | 4 +- backend/Compile.hs | 2 +- elm-package.json | 2 + frontend/Debugger.elm | 194 ++++++ frontend/Debugger/Model.elm | 205 ++++++ frontend/Debugger/Reflect.elm | 12 + frontend/Debugger/RuntimeApi.elm | 207 ++++++ frontend/Debugger/Service.elm | 353 ++++++++++ frontend/Native/Debugger/Reflect.js | 35 + frontend/Native/Debugger/RuntimeApi.js | 539 +++++++++++++++ frontend/debugger-implementation.js | 869 +------------------------ 11 files changed, 1554 insertions(+), 868 deletions(-) create mode 100644 frontend/Debugger.elm create mode 100644 frontend/Debugger/Model.elm create mode 100644 frontend/Debugger/Reflect.elm create mode 100644 frontend/Debugger/RuntimeApi.elm create mode 100644 frontend/Debugger/Service.elm create mode 100644 frontend/Native/Debugger/Reflect.js create mode 100644 frontend/Native/Debugger/RuntimeApi.js diff --git a/Setup.hs b/Setup.hs index e4a5da6..72c3908 100644 --- a/Setup.hs +++ b/Setup.hs @@ -33,11 +33,11 @@ output = buildSideBar :: IO () buildSideBar = do (exitCode, out, err) <- - readProcessWithExitCode "elm-make" [ "--yes", "frontend" "Overlay.elm", "--output=" ++ output ] "" + readProcessWithExitCode "elm-make" [ "--yes", "frontend" "Debugger.elm", "--output=" ++ output ] "" case exitCode of ExitSuccess -> return () ExitFailure _ -> - do hPutStrLn stderr ("Failed to build Overlay.elm\n\n" ++ out ++ err) + do hPutStrLn stderr ("Failed to build Debugger.elm\n\n" ++ out ++ err) exitFailure diff --git a/backend/Compile.hs b/backend/Compile.hs index e90e8d6..c32a357 100644 --- a/backend/Compile.hs +++ b/backend/Compile.hs @@ -118,5 +118,5 @@ initialize debug name filePath = in "var runningElmModule =\n " ++ case debug of - True -> "Elm.fullscreenDebug('" ++ moduleName ++ "', '" ++ filePath ++ "');" + True -> "Elm.fullscreenDebug(Elm." ++ moduleName ++ ", '" ++ filePath ++ "');" False -> "Elm.fullscreen(Elm." ++ moduleName ++ ");" diff --git a/elm-package.json b/elm-package.json index f9d39bc..5836c01 100644 --- a/elm-package.json +++ b/elm-package.json @@ -11,6 +11,8 @@ "dependencies": { "elm-lang/core": "2.0.0 <= v < 3.0.0", "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "vilterp/fancy-start-app": "1.0.0 <= v < 2.0.0", + "imeckler/empty": "1.0.0 <= v < 2.0.0", "evancz/elm-markdown": "1.1.4 <= v < 2.0.0", "jystic/elm-font-awesome": "1.0.0 <= v < 2.0.0" }, diff --git a/frontend/Debugger.elm b/frontend/Debugger.elm new file mode 100644 index 0000000..e6faf8c --- /dev/null +++ b/frontend/Debugger.elm @@ -0,0 +1,194 @@ +module Debugger where + +import Html exposing (..) +import Html.Attributes as Attr exposing (..) +import Html.Events exposing (..) +import Signal +import Task exposing (Task) +import Json.Decode as JsDec +import String + +import FancyStartApp +import Empty exposing (..) + +import Debugger.RuntimeApi as API +import Debugger.Model as DM +import Debugger.Service as Service + +type alias Model = + { sidebarVisible : Bool + , permitSwaps : Bool + , serviceState : DM.Model + } + + +initModel : Model +initModel = + { sidebarVisible = True + , permitSwaps = True + , serviceState = DM.Uninitialized + } + + +type Action + = SidebarVisible Bool + | PermitSwaps Bool + | NewServiceState DM.Model + | CompilationErrors CompilationErrors + + +type alias CompilationErrors = + String + + +(html, uiTasks) = + FancyStartApp.start + { initialState = initModel + , initialTasks = (\loopback -> + [Signal.send (Service.commandsMailbox ()).address (DM.Initialize initMod)]) + , externalActions = + Signal.map NewServiceState Service.state + , view = view + , update = update + } + +main = + html + + +(=>) = (,) + + +view : Signal.Address Action -> Model -> Html +view addr state = + let + mainVal = + case state.serviceState of + DM.Active activeAttrs -> + activeAttrs.mainVal + + _ -> + div [] [] + + in + div [] + [ div [] + [ mainVal ] + , viewSidebar addr state + ] + + +viewSidebar : Signal.Address Action -> Model -> Html +viewSidebar addr state = + let + body = + case state.serviceState of + DM.Active activeAttrs -> + activeSidebarBody addr activeAttrs + + _ -> + text "Initialzing..." + in + -- TODO: event blocker + -- TODO: toggle tab + div + [ style + [ "position" => "absolute" + , "width" => "300px" + , "right" => "0px" + , "background-color" => "gray" + , "color" => "white" + , "z-index" => "1" + ] + ] + [ body ] + + +activeSidebarBody : Signal.Address Action -> DM.ActiveAttrs -> Html +activeSidebarBody addr activeAttrs = + let + commandsAddr = + (Service.commandsMailbox ()).address + + numFrames = + DM.numFrames activeAttrs + + curFrame = + DM.curFrameIdx activeAttrs + in + div [] + [ div [] + [ button + [ onClick + commandsAddr + (if DM.isPlaying activeAttrs then + DM.Pause + else + DM.ForkFrom curFrame True) + ] + [ text + (if DM.isPlaying activeAttrs then "Pause" else "Play") + ] + , button + [ onClick commandsAddr <| + DM.ForkFrom 0 (DM.isPlaying activeAttrs) + ] + [ text "Reset" ] + ] + , div [] + [ div [] + [ text <| + "frame idx: " + ++ (toString <| curFrame) + ++ "; numFrames: " ++ toString numFrames + , input + [ type' "range" + , Attr.min "0" + , Attr.max <| toString <| numFrames - 1 + , Attr.value <| toString <| curFrame + , on + "input" + (JsDec.at ["target","value"] + (JsDec.customDecoder JsDec.string String.toInt)) + (\idx -> + Signal.message + commandsAddr + (DM.GetNodeState {start=idx, end=idx} [DM.mainId activeAttrs])) + ] + [] + ] + ] + ] + + +update : FancyStartApp.UpdateFun Model Empty Action +update loopback now action state = + case action of + SidebarVisible visible -> + ( { state | sidebarVisible <- visible } + , [] + ) + + PermitSwaps permitSwaps -> + ( { state | permitSwaps <- permitSwaps } + , [] + ) + + NewServiceState serviceState -> + ( { state | serviceState <- serviceState } + , [] + ) + +-- INPUT PORT: initial module + +port initMod : API.ElmModule + +-- TASK PORTS + +port uiTasksPort : Signal (Task Empty ()) +port uiTasksPort = + uiTasks + +port debugServiceTasks : Signal (Task Empty ()) +port debugServiceTasks = + Service.tasks diff --git a/frontend/Debugger/Model.elm b/frontend/Debugger/Model.elm new file mode 100644 index 0000000..a99f717 --- /dev/null +++ b/frontend/Debugger/Model.elm @@ -0,0 +1,205 @@ +module Debugger.Model where + +import Dict +import Set +import Html exposing (Html) +import Debug +import Debugger.Reflect as Reflect + +import Debugger.RuntimeApi as API + + +type Model + = Uninitialized + | Initializing + | Active ActiveAttrs + + +type alias ActiveAttrs = + { session : API.DebugSession + , sessionState : SessionState + , mainVal : Html + , exprLogs : Dict.Dict API.ExprTag API.ValueLog + , nodeLogs : Dict.Dict API.NodeId API.ValueLog + , subscribedNodes : Set.Set API.NodeId + } + + +initialActiveAttrs : API.DebugSession -> Html -> ActiveAttrs +initialActiveAttrs session mainVal = + { session = session + , sessionState = Playing Nothing + , mainVal = mainVal + , exprLogs = Dict.empty + , nodeLogs = Dict.empty + , subscribedNodes = Set.empty + } + + +type SessionState + = Playing (Maybe RunningCmd) + | Paused Int (Maybe RunningCmd) + | Forking API.FrameIndex Bool + | AlmostPlaying + | Pausing + | SwapError SwapError + + +type RunningCmd + = Swapping + | GettingNodeState API.FrameInterval + | Subscribing Bool + + +-- maybe don't need to reify these after all +type Command + = Initialize API.ElmModule + | Pause + | ForkFrom API.FrameIndex Bool + | Subscribe API.NodeId Bool + | GetNodeState API.FrameInterval (List API.NodeId) + | Swap API.ElmModule + | NoOpCommand + + +type Response + = IsActive API.DebugSession API.ValueSet + | IsSubscribed (Maybe API.ValueLog) + | HasForked API.ValueSet + | IsPlaying + | IsPaused (Maybe API.FrameInterval) + | SwapResult (Result SwapError API.ValueSet) + | GotNodeState (List (API.NodeId, API.ValueLog)) + + +type alias SwapError = + String + + +type Notification + = NewFrame API.NewFrameNotification + -- TODO: task update + | NoOpNot + + +type Action + = Notification Notification + | Command Command + | Response Response + + + +getMainValFromLogs : API.DebugSession -> List (Int, API.ValueLog) -> Html +getMainValFromLogs session logs = + let + mainId = + (API.sgShape session).mainId + + in + logs + |> List.filter (\(id, val) -> id == mainId) + |> List.head + |> getMaybe "no log with main id" + |> snd -- value log + |> List.head + |> getMaybe "log empty" + |> snd -- js elm value + |> Reflect.getHtml + + +getMainVal : API.DebugSession -> API.ValueSet -> Html +getMainVal session values = + let + mainId = + (API.sgShape session).mainId + + in + values + |> List.filter (\(id, val) -> id == mainId) + |> List.head + |> getMaybe "no value with main id" + |> snd + |> Reflect.getHtml + + +appendToLog : API.FrameIndex -> API.JsElmValue -> Maybe API.ValueLog -> Maybe API.ValueLog +appendToLog curFrame value maybeLog = + let + pair = + (curFrame, value) + + newLog = + case maybeLog of + Just log -> + log ++ [pair] + + Nothing -> + [pair] + in + Just newLog + + +updateLogs : API.FrameIndex + -> Dict.Dict comparable API.ValueLog + -> List (comparable, API.JsElmValue) + -> (comparable -> Bool) + -> Dict.Dict comparable API.ValueLog +updateLogs curFrame logs updates idPred = + List.foldl + (\(tag, value) logs -> + Dict.update tag (appendToLog curFrame value) logs) + logs + (List.filter (fst >> idPred) updates) + + +isPlaying : ActiveAttrs -> Bool +isPlaying activeAttrs = + case activeAttrs.sessionState of + Playing _ -> + True + + _ -> + False + + +mainId : ActiveAttrs -> API.NodeId +mainId activeAttrs = + (API.sgShape activeAttrs.session).mainId + + +numFrames : ActiveAttrs -> Int +numFrames activeAttrs = + API.numFrames activeAttrs.session + + +curFrameIdx : ActiveAttrs -> Int +curFrameIdx activeAttrs = + case activeAttrs.sessionState of + Paused idx _ -> + idx + + _ -> + numFrames activeAttrs - 1 + + +getLast : List a -> a +getLast list = + case list of + [] -> + Debug.crash "getLast of empty list" + + [x] -> + x + + (x::xs) -> + getLast xs + + +getMaybe : String -> Maybe a -> a +getMaybe msg maybe = + case maybe of + Just x -> + x + + Nothing -> + Debug.crash msg diff --git a/frontend/Debugger/Reflect.elm b/frontend/Debugger/Reflect.elm new file mode 100644 index 0000000..c2cab19 --- /dev/null +++ b/frontend/Debugger/Reflect.elm @@ -0,0 +1,12 @@ +module Debugger.Reflect where + +import Debugger.RuntimeApi as API +import Html + +import Native.Debugger.Reflect + +getHtml : API.JsElmValue -> Html.Html +getHtml = + Native.Debugger.Reflect.getHtml + +-- TODO: repr of Elm values in Elm \ No newline at end of file diff --git a/frontend/Debugger/RuntimeApi.elm b/frontend/Debugger/RuntimeApi.elm new file mode 100644 index 0000000..c469ffc --- /dev/null +++ b/frontend/Debugger/RuntimeApi.elm @@ -0,0 +1,207 @@ +module Debugger.RuntimeApi where + +import Task exposing (Task) +import Dict +import Json.Encode as JsEnc +import Time + +import Native.Debugger.RuntimeApi + + +type alias FrameIndex = + Int + + +type alias JsElmValue = + JsEnc.Value + + +type DebugSession + = DebugSession -- opaque + + +type alias NodeId = + Int + + +type alias Event = + { value : JsElmValue + , nodeId : NodeId + , time : Time.Time + } + + +emptyEvent = + { value = JsEnc.null + , nodeId = 0 + , time = 0 + } + + +sgShape : DebugSession -> SGShape +sgShape = + Native.Debugger.RuntimeApi.sgShape + + +numFrames : DebugSession -> Int +numFrames = + Native.Debugger.RuntimeApi.numFrames + + +-- can decode + +-- new frame +type alias NewFrameNotification = + { event : Event + , flaggedExprValues : List (ExprTag, JsElmValue) + , subscribedNodeValues : ValueSet + } + + +emptyNotification = + { event = emptyEvent + , flaggedExprValues = [] + , subscribedNodeValues = [] + } + + +-- for time being, these are only set by Debug.log +type alias ExprTag = + String + + +-- QUERIES + +{-| We do this with *lists* of nodes or exprs +a/o just one at a time, because simulating the module forward in the +specified time interval and gathering the all SG or expr values we're +interested in at each step is better than simulating it once for each +point of interest. -} +getNodeState : DebugSession -> FrameInterval -> List NodeId -> Task String (List (NodeId, ValueLog)) +getNodeState = + Native.Debugger.RuntimeApi.getNodeState + + +getInputHistory : DebugSession -> InputHistory +getInputHistory = + Native.Debugger.RuntimeApi.getHistory + + +emptyInputHistory : InputHistory +emptyInputHistory = + Native.Debugger.RuntimeApi.emptyInputHistory + + +{-| +- Forgets all frames after given frame index +- Returns values of subscribed nodes at given frame index +- Module plays from given frame index +-} +forkFrom : DebugSession -> FrameIndex -> Task String ValueSet +forkFrom = + Native.Debugger.RuntimeApi.forkFrom + + +{-| Interpreted as inclusive -} +type alias FrameInterval = + { start : FrameIndex + , end : FrameIndex + } + + +type alias ValueLog = + List (FrameIndex, JsElmValue) + + +type alias ValueSet = + List (NodeId, JsElmValue) + + +-- COMMANDS + +{-| Swap in new module. Starts off playing. +Subscribes to the list of nodes returned by the given function (3rd arg), +and returns their initial values. -} +initialize : ElmModule + -> InputHistory + -> Signal.Address NewFrameNotification + -> (SGShape -> List NodeId) + -> Task SwapError (DebugSession, ValueSet) +initialize = + Native.Debugger.RuntimeApi.initialize + + +-- how is the previous session killed? + +type alias ElmModule = + JsEnc.Value + + +evalModule : CompiledElmModule -> ElmModule +evalModule = + Native.Debugger.RuntimeApi.evalModule + + +type alias CompiledElmModule = + { name : String + , code : String -- javascript + } + + +-- opaque +-- must start from time 0 +-- TODO: if this is opaque, how are we gonna serialize it for download? +-- could just JSON-ize it on the JS side... +type InputHistory = + InputHistory + + +type alias SubscriptionSet = + List NodeId + + +type alias SGShape = + { nodes : Dict.Dict NodeId NodeInfo + , mainId : NodeId + } + + +type alias NodeInfo = + { name : String + , nodeType : NodeType + , kids : List NodeId + } + + +type NodeType + = InputPort + | CoreLibInput + | Mailbox + | InternalNode + | OutputPort + | Main + + +{-| Various ways replaying the InputHistory on the new code wouldn't make sense -} +type alias SwapError = + String + + +{-| Error with () means it was already in that state -} +setPlaying : DebugSession -> Bool -> Task () () +setPlaying = + Native.Debugger.RuntimeApi.setPlaying + + +-- TODO: this should return the current values (what does that mean tho) +{-| Error with () means it was already in that state -} +setSubscribedToNode : DebugSession -> NodeId -> Bool -> Task () () +setSubscribedToNode = + Native.Debugger.RuntimeApi.setSubscribedToNode + + +-- Util + +prettyPrint : JsElmValue -> String +prettyPrint val = + Native.Debugger.RuntimeApi.prettyPrint val " " diff --git a/frontend/Debugger/Service.elm b/frontend/Debugger/Service.elm new file mode 100644 index 0000000..47063cc --- /dev/null +++ b/frontend/Debugger/Service.elm @@ -0,0 +1,353 @@ +module Debugger.Service where + +import Signal exposing (Signal) +import Task exposing (Task) +import Dict +import Set +import Html +import Debug + +import FancyStartApp +import Empty exposing (Empty) + +import Debugger.RuntimeApi as API +import Debugger.Model exposing (..) +import Debugger.Reflect as Reflect + + +state : Signal Model +state = + fst stateAndTasks + + +tasks : Signal (Task Empty ()) +tasks = + snd stateAndTasks + + +stateAndTasks = + FancyStartApp.start + { initialState = Uninitialized + -- w/b swapping? + -- initialization happens later (?) + , initialTasks = always [] + , externalActions = + Signal.mergeMany + [ Signal.map Notification notificationsMailbox.signal + , Signal.map Command (commandsMailbox ()).signal + ] + , view = always identity + , update = update + } + + +-- why do I have to do this? +commandsMailbox : () -> Signal.Mailbox Command +commandsMailbox _ = + mailbox + + +mailbox = + Signal.mailbox NoOpCommand + + +notificationsMailbox : Signal.Mailbox Notification +notificationsMailbox = + Signal.mailbox NoOpNot + + +{-| so what happens if you subscribe to something +while playing? Pause => +-} + +update : FancyStartApp.UpdateFun Model Empty Action +update loopback now action state = + case state of + Uninitialized -> + case action of + Command (Initialize mod) -> + ( Initializing + , [ API.initialize + mod + API.emptyInputHistory + (Signal.forwardTo notificationsMailbox.address NewFrame) + (\shape -> [shape.mainId]) + |> Task.map (\(session, values) -> Response <| IsActive session values) + |> Task.mapError (\swapErr -> Debug.crash swapErr) + |> loopback + ] + ) + + _ -> + Debug.crash "..." + + Initializing -> + case action of + Response (IsActive session values) -> + ( Active <| initialActiveAttrs session (getMainVal session values) + , [] + ) + + _ -> + Debug.crash "..." + + Active activeAttrs -> + let + (newAAs, tasks) = updateActive loopback now action activeAttrs + in + (Active newAAs, tasks) + +updateActive : FancyStartApp.UpdateFun ActiveAttrs Empty Action +updateActive loopback now action state = + let d = Debug.log "(act, state)" (action, state.sessionState) + in case state.sessionState of + Playing maybeCommand -> + case maybeCommand of + Just cmdOut -> + case cmdOut of + Swapping -> + case action of + Response (SwapResult res) -> + case res of + Ok values -> + ( { state | mainVal <- getMainVal state.session values + , sessionState <- Playing Nothing + } + , [] + ) + + Err swapErr -> + ( { state | sessionState <- SwapError swapErr } + , [] + ) + + _ -> + Debug.crash "..." + + Subscribing bool -> + case action of + -- TODO: factor out SUB + Response (IsSubscribed maybeVals) -> + (state, []) + + _ -> + Debug.crash "..." + + _ -> + Debug.crash "..." + + Nothing -> + case action of + Notification not -> + case not of + NewFrame newFrame -> + let + curFrame = + curFrameIdx state + + newExprLogs = + updateLogs + curFrame + state.exprLogs + newFrame.flaggedExprValues + (always True) + + newNodeLogs = + updateLogs + curFrame + state.nodeLogs + newFrame.subscribedNodeValues + (\id -> id /= mainId state) + + mainValue = + newFrame.subscribedNodeValues + |> List.filter (\(id, val) -> id == mainId state) + |> List.head + |> Maybe.map (snd >> Reflect.getHtml) + |> Maybe.withDefault state.mainVal + in + ( { state + | exprLogs <- newExprLogs + , nodeLogs <- newNodeLogs + , mainVal <- mainValue + } + , [] + ) + + NoOpNot -> + (state, []) + + Command Pause -> + ( { state | sessionState <- Pausing } + , [ API.setPlaying state.session False + |> Task.mapError (\_ -> Debug.crash "sup") + |> Task.map (always (Response <| IsPaused Nothing)) + |> loopback + ] + ) + + Command (ForkFrom idx playingAfter) -> + ( { state | + sessionState <- Forking idx True + }, + [ API.forkFrom state.session 0 + |> Task.mapError (\_ -> Debug.crash "...") + |> Task.map (Response << HasForked) + |> loopback + ] + ) + + Command (GetNodeState interval nodeIds) -> + ( { state | + sessionState <- Pausing + } + , [ (API.setPlaying state.session False + |> Task.mapError (\_ -> Debug.crash "...") + |> Task.map (always <| Response <| IsPaused <| Just interval) + |> loopback) + `Task.andThen` (\_ -> + -- TODO: factor this out + API.getNodeState state.session interval nodeIds + |> Task.mapError (\_ -> Debug.crash "...") + |> Task.map (Response << GotNodeState) + |> loopback + ) + ] + ) + + Command (Swap mod) -> + (state, []) + + _ -> + Debug.crash "unexpected action in playing state" + + Paused pausedIdx maybeCommand -> + case maybeCommand of + Just cmdOut -> + case cmdOut of + Swapping -> + case action of + Response (SwapResult res) -> + (state, []) + + _ -> + Debug.crash "..." + + GettingNodeState interval -> + case action of + Response (GotNodeState values) -> + ( { state + | mainVal <- + getMainValFromLogs state.session values + , sessionState <- + Paused interval.start Nothing + } + , [] + ) + + _ -> + Debug.crash "..." + + Subscribing subbing -> + -- TODO: factor out SUB + case action of + Response (IsSubscribed maybeLog) -> + case (subbing, maybeLog) of + (True, Just valLog) -> + (state, []) + + (False, Nothing) -> + (state, []) + + _ -> + Debug.crash "..." + + _ -> + Debug.crash "..." + + Nothing -> + case action of + Command (Subscribe nodeId sub) -> + (state, []) + + Command (Swap mod) -> + (state, []) + + Command (GetNodeState interval nodes) -> + ( { state | + sessionState <- + Paused pausedIdx (Just <| GettingNodeState interval) + } + , [ API.getNodeState state.session interval nodes + |> Task.mapError (\_ -> Debug.crash "...") + |> Task.map (Response << GotNodeState) + |> loopback + ] + ) + + Command (ForkFrom frameIdx playAfter) -> + ( {state | sessionState <- Forking frameIdx playAfter } + , [ API.forkFrom state.session frameIdx + |> Task.mapError (\msg -> Debug.crash msg) + |> Task.map (\vals -> Response (HasForked vals)) + |> loopback + ] + ) + + _ -> + Debug.crash "..." + + Forking idx playingAfter -> + case action of + Response (HasForked values) -> + -- TODO: get main, etc... + ( { state + | sessionState <- + if playingAfter then + AlmostPlaying + else + Paused idx Nothing + , mainVal <- getMainVal state.session values + } + , if playingAfter then + [ API.setPlaying state.session True + |> Task.mapError (\_ -> Debug.crash "already playing") + |> Task.map (always <| Response IsPlaying) + |> loopback + ] + else + [] + ) + + _ -> + Debug.crash "unexpected action in Forking state" + + Pausing -> + case action of + Response (IsPaused maybeInt) -> + let + cmdOut = + maybeInt |> Maybe.map GettingNodeState + in + ( { state | sessionState <- Paused (curFrameIdx state) cmdOut } + , [] + ) + + _ -> + Debug.crash "..." + + AlmostPlaying -> + case action of + Response IsPlaying -> + ( { state | sessionState <- Playing Nothing } + , [] + ) + + _ -> + Debug.crash "unexpected action in playing state" + + SwapError _ -> + case action of + _ -> + -- TODO: you can reset... + Debug.crash "action in SwapError state" diff --git a/frontend/Native/Debugger/Reflect.js b/frontend/Native/Debugger/Reflect.js new file mode 100644 index 0000000..a0fa04c --- /dev/null +++ b/frontend/Native/Debugger/Reflect.js @@ -0,0 +1,35 @@ +Elm.Native = Elm.Native || {}; +Elm.Native.Debugger = Elm.Native.Debugger || {}; +Elm.Native.Debugger.Reflect = Elm.Native.Debugger.Reflect || {}; + +Elm.Native.Debugger.Reflect = {}; +Elm.Native.Debugger.Reflect.make = function(localRuntime) { + localRuntime.Native = localRuntime.Native || {}; + localRuntime.Native.Debugger = localRuntime.Native.Debugger || {}; + localRuntime.Native.Debugger.Reflect = localRuntime.Native.Debugger.Reflect || {}; + if ('values' in localRuntime.Native.Debugger.Reflect) + { + return localRuntime.Native.Debugger.Reflect.values; + } + + var Signal = Elm.Native.Signal.make (localRuntime); + var Task = Elm.Native.Task.make (localRuntime); + var Utils = Elm.Native.Utils.make (localRuntime); + var List = Elm.Native.List.make (localRuntime); + var Dict = Elm.Dict.make (localRuntime); + + function getHtml(html) + { + // TODO: I hear this is bad (http://webreflection.blogspot.com/2013/03/5-reasons-you-should-avoid-proto.html) + // but instanceof didn't work for some reason. find a workaround. + if(html.__proto__.type == "VirtualNode" || html.__proto__.type == "VirtualText") { + return html; + } else { + throw new Error("not html"); + } + } + + return localRuntime.Native.Debugger.Reflect.values = { + getHtml: getHtml + }; +}; diff --git a/frontend/Native/Debugger/RuntimeApi.js b/frontend/Native/Debugger/RuntimeApi.js new file mode 100644 index 0000000..ad17b5f --- /dev/null +++ b/frontend/Native/Debugger/RuntimeApi.js @@ -0,0 +1,539 @@ +Elm.Native = Elm.Native || {}; +Elm.Native.Debugger = Elm.Native.Debugger || {}; +Elm.Native.Debugger.RuntimeApi = Elm.Native.Debugger.RuntimeApi || {}; + +Elm.Native.Debugger.RuntimeApi = {}; +Elm.Native.Debugger.RuntimeApi.make = function(localRuntime) { + localRuntime.Native = localRuntime.Native || {}; + localRuntime.Native.Debugger = localRuntime.Native.Debugger || {}; + localRuntime.Native.Debugger.RuntimeApi = localRuntime.Native.Debugger.RuntimeApi || {}; + if ('values' in localRuntime.Native.Debugger.RuntimeApi) + { + return localRuntime.Native.Debugger.RuntimeApi.values; + } + + var Signal = Elm.Native.Signal.make (localRuntime); + var Task = Elm.Native.Task.make (localRuntime); + var Utils = Elm.Native.Utils.make (localRuntime); + var List = Elm.Native.List.make (localRuntime); + var Dict = Elm.Dict.make (localRuntime); + + function sgShape(session) { + return session.shape; + } + + function numFrames(session) { + return session.events.length + 1; + } + + // QUERIES + + function getNodeState(session, frameInterval, nodeIds) + { + return Task.asyncFunction(function(callback) { + assertNotDisposed(session); + assertPaused(session); + nodeIds = List.toArray(nodeIds); + + jumpTo(session, frameInterval.start); + + // go through the target range + var valueLogs = {}; + nodeIds.forEach(function(nodeId) { + valueLogs[nodeId] = []; + }); + for(var idx = frameInterval.start; idx <= frameInterval.end; idx++) + { + // get values + nodeIds.forEach(function(nodeId) { + valueLogs[nodeId].push(Utils.Tuple2(idx, session.sgNodes[nodeId].value)); + }); + // push event + if(idx < frameInterval.end) + { + var event = session.events[idx]; + session.originalNotify(event.nodeId, event.value); + } + } + + var logs = nodeIds.map(function(nodeId) { + return Utils.Tuple2(nodeId, List.fromArray(valueLogs[nodeId])); + }); + + callback(Task.succeed(List.fromArray(logs))); + }); + } + + function getInputHistory(session) + { + return Task.asyncFunction(function(callback) { + assertNotDisposed(session); + var history = []; // List Event, I guess + callback(Task.succeed(history)); + }); + } + + function emptyInputHistory() + { + return {}; // TODO + } + + function forkFrom(session, frameIdx) + { + return Task.asyncFunction(function(callback) { + assertNotDisposed(session); + // TODO: I don't *think* we need to pause here. + jumpTo(session, frameIdx); + session.events.splice(frameIdx); + session.snapshots.splice(Math.floor(frameIdx / EVENTS_PER_SAVE) + 1); + var nodeVals = session.subscribedNodeIds.map(function(nodeId) { + return Utils.Tuple2(nodeId, session.sgNodes[nodeId].value); + }); + callback(Task.succeed(List.fromArray(nodeVals))); + }); + } + + function evalModule(compiled) { + window.eval(compiledModule.code); + var elmModule = Elm; + var names = moduleName.split('.'); + for (var i = 0; i < names.length; ++i) + { + elmModule = elmModule[names[i]]; + } + return elmModule; + } + + // COMMANDS + + function initialize(module, inputHistory, notificationAddress, initialNodesFun) + { + return Task.asyncFunction(function(callback) { + var debugeeLocalRuntime; + var moduleBeingDebugged = Elm.fullscreen({ + make: function(runtime) { + debugeeLocalRuntime = runtime; + return module.make(runtime); + } + }, {}, false); + + /* TODO: + - do some kind of validation: can this input history be replayed + - over this code? + - is the signal graph the same? otherwise sub set invalid + - replay the input history + */ + + var sgNodes = flattenSignalGraph(debugeeLocalRuntime); + var sgShape = getSgShape(sgNodes); + var session = { + module: moduleBeingDebugged, + runtime: debugeeLocalRuntime, + originalNotify: debugeeLocalRuntime.notify, + sgNodes: sgNodes, + delay: 0, // TODO: think delay stuff through! + // TODO: delay, totalTimeLost, asyncCallbacks + asyncCallbacks: [], + events: [], + snapshots: [takeSnapshot(sgNodes)], + shape: sgShape, // TODO actually get main id + notificationAddress: notificationAddress, + disposed: false, + playing: true, + subscribedNodeIds: List.toArray(initialNodesFun(sgShape)) + }; + + function getSgShape(nodes) { + var mainId; + var nodeTuples = Object.keys(nodes).map(function(nodeId) { + var node = nodes[nodeId]; + var nodeType; + if(node.name == 'input-mailbox') { + nodeType = {ctor: 'Mailbox'}; + } else if(node.name.indexOf('input') == 0) { + nodeType = {ctor: 'CoreLibInput'}; + } else if(node.isOutput && node.isOutput) { + if(node.name == 'output-main') { + nodeType = {ctor:'Main'}; + mainId = node.id; + } else { + nodeType = {ctor:'OutputPort'} + } + } else { + nodeType = {ctor: 'InternalNode'}; + } + var info = { + _: {}, + name: node.name, + nodeType: nodeType, + kids: List.fromArray( + node.kids ? node.kids.map(function(kid) {return kid.id}) : [] + ) + }; + return Utils.Tuple2(node.id, info); + }); + return { + _: {}, + nodes: Dict.fromList(List.fromArray(nodeTuples)), + mainId: mainId + } + } + + // set up event recording + debugeeLocalRuntime.notify = function(id, value) { + if (!session.playing) + { + return false; + } + + session.flaggedExprValues = []; + + var changed = session.originalNotify(id, value); + + // Record the event + var event = { + _: {}, + value: value, + nodeId: id, + time: session.runtime.timer.now() + } + session.events.push(event); + // take snapshot if necessary + if(session.events.length % EVENTS_PER_SAVE == 0) + { + session.snapshots.push(takeSnapshot(session.sgNodes)); + } + + var subscribedNodeValues = session.subscribedNodeIds.map(function(nodeId) { + var node = session.sgNodes[nodeId]; + return Utils.Tuple2(nodeId, node.value); + }); + // send notification + var notification = { + _: {}, + event: event, + flaggedExprValues: List.fromArray(session.flaggedExprValues), + subscribedNodeValues: List.fromArray(subscribedNodeValues) + } + Task.perform(notificationAddress._0(notification)); + + // TODO: add traces + + return changed; + }; + + debugeeLocalRuntime.setTimeout = function(thunk, delay) { + if (!session.playing) + { + return 0; + } + + var callback = { + thunk: thunk, + id: 0, + executed: false + }; + + callback.id = setTimeout(function() { + callback.executed = true; + thunk(); + }, delay); + + // TODO: this isn't fully hooked up yet + session.asyncCallbacks.push(callback); + return callback.id; + }; + + debugeeLocalRuntime.timer.now = function() { + // TODO: not sure how to get time of last event + // if (debugState.paused || debugState.swapInProgress) + // { + // var event = debugState.events[debugState.index]; + // return event.time; + // } + return Date.now() - session.delay; + }; + debugeeLocalRuntime.debug = { + log: function(tag, value) { + if (!session.playing) + { + return; + } + // TODO: save actual value; pretty print on + session.flaggedExprValues.push(Utils.Tuple2(tag, value)); + }, + trace: function(tag, form) { + // TODO: ... + return replace([['trace', tag]], form); + } + }; + + // get values of initial subscription + + var initNodeVals = session.subscribedNodeIds.map(function(nodeId) { + return Utils.Tuple2(nodeId, session.sgNodes[nodeId].value); + }); + + var result = Utils.Tuple2(session, List.fromArray(initNodeVals)); + + callback(Task.succeed(result)); + }); + } + + function setPlaying(session, playing) + { + return Task.asyncFunction(function(callback) { + assertNotDisposed(session); + if(session.playing) { + if(!playing) { + // PAUSE + // TODO asyncCallback stuff for timers + session.playing = playing; + callback(Task.succeed(Utils.Tuple0)); + } else { + callback(Task.fail(Utils.Tuple0)); + } + } else { + if(playing) { + // PLAY + session.playing = playing; + callback(Task.succeed(Utils.Tuple0)); + } else { + callback(Task.fail(Utils.Tuple0)); + } + } + callback(Task.succeed(Utils.Tuple0)); + }); + } + + function setSubscribedToNode(session, nodeId, subscribed) + { + return Task.asyncFunction(function(callback) { + assertNotDisposed(session); + var idx = session.subscribedNodeIds.indexOf(nodeId); + var alreadySubscribed = idx != -1; + if(subscribed) { + if(alreadySubscribed) { + callback(Task.fail(Utils.Tuple0)); + } else { + session.subscribedNodeIds.push(nodeId); + callback(Task.succeed(Utils.Tuple0)); + } + } else { + if(alreadySubscribed) { + session.subscribedNodeIds.splice(idx, 1); + callback(Task.succeed(Utils.Tuple0)); + } else { + callback(Task.fail(Utils.Tuple0)); + } + } + }); + } + + // not exposed + function jumpTo(session, frameIdx) + { + // get to it + var snapshotBeforeIdx = Math.floor(frameIdx / EVENTS_PER_SAVE); + var snapshot = session.snapshots[snapshotBeforeIdx]; + for(var nodeId in snapshot) { + session.sgNodes[nodeId].value = snapshot[nodeId]; + } + var snapshotBeforeFrameIdx = snapshotBeforeIdx * EVENTS_PER_SAVE; + for(var idx=snapshotBeforeFrameIdx; idx < frameIdx; idx++) + { + var event = session.events[idx]; + session.originalNotify(event.nodeId, event.value); + } + } + + return localRuntime.Native.Debugger.RuntimeApi.values = { + sgShape: sgShape, + numFrames: numFrames, + getNodeState: F3(getNodeState), + getInputHistory: getInputHistory, + emptyInputHistory: emptyInputHistory, + forkFrom: F2(forkFrom), + evalModule: evalModule, + initialize: F4(initialize), + setPlaying: F2(setPlaying), + setSubscribedToNode: F3(setSubscribedToNode), + prettyPrint: F2(prettyPrint) + }; +}; + +// Utils + +var EVENTS_PER_SAVE = 100; + +function assert(bool, msg) +{ + if(!bool) + { + throw new Error("Assertion error: " + msg); + } +} + +function assertNotDisposed(session) +{ + assert(!session.disposed, "attempting to work with disposed session"); +} + +function assertPaused(session) { + assert(!session.playing, "session needs to be paused"); +} + +// returns array of node references, indexed by node id (?) +function flattenSignalGraph(runtime) { + var nodesById = {}; + + function addAllToDict(node) + { + nodesById[node.id] = node; + if(node.kids) { + node.kids.forEach(addAllToDict); + } + } + runtime.inputs.forEach(addAllToDict); + + return nodesById; +} + + +// returns snapshot +function takeSnapshot(signalGraphNodes) +{ + var nodeValues = {}; + + Object.keys(signalGraphNodes).forEach(function(nodeId) { + var node = signalGraphNodes[nodeId]; + nodeValues[nodeId] = node.value; + }); + + return nodeValues; +} + +var prettyPrint = function() { + + var independentRuntime = {}; + var List; + var ElmArray; + var Dict; + + var toString = function(v, separator) { + var type = typeof v; + if (type === "function") { + var name = v.func ? v.func.name : v.name; + return ''; + } else if (type === "boolean") { + return v ? "True" : "False"; + } else if (type === "number") { + return v.toFixed(2).replace(/\.0+$/g, ''); + } else if ((v instanceof String) && v.isChar) { + return "'" + addSlashes(v) + "'"; + } else if (type === "string") { + return '"' + addSlashes(v) + '"'; + } else if (type === "object" && '_' in v && probablyPublic(v)) { + var output = []; + for (var k in v._) { + for (var i = v._[k].length; i--; ) { + output.push(k + " = " + toString(v._[k][i], separator)); + } + } + for (var k in v) { + if (k === '_') continue; + output.push(k + " = " + toString(v[k], separator)); + } + if (output.length === 0) return "{}"; + var body = "\n" + output.join(",\n"); + return "{" + body.replace(/\n/g,"\n" + separator) + "\n}"; + } else if (type === "object" && 'ctor' in v) { + if (v.ctor.substring(0,6) === "_Tuple") { + var output = []; + for (var k in v) { + if (k === 'ctor') continue; + output.push(toString(v[k], separator)); + } + return "(" + output.join(", ") + ")"; + } else if (v.ctor === "_Array") { + if (!ElmArray) { + ElmArray = Elm.Array.make(independentRuntime); + } + var list = ElmArray.toList(v); + return "Array.fromList " + toString(list, separator); + } else if (v.ctor === "::") { + var output = '[\n' + toString(v._0, separator); + v = v._1; + while (v && v.ctor === "::") { + output += ",\n" + toString(v._0, separator); + v = v._1; + } + return output.replace(/\n/g,"\n" + separator) + "\n]"; + } else if (v.ctor === "[]") { + return "[]"; + } else if (v.ctor === "RBNode" || v.ctor === "RBEmpty") { + if (!Dict || !List) { + Dict = Elm.Dict.make(independentRuntime); + List = Elm.List.make(independentRuntime); + } + var list = Dict.toList(v); + var name = "Dict"; + if (list.ctor === "::" && list._0._1.ctor === "_Tuple0") { + name = "Set"; + list = A2(List.map, function(x){return x._0}, list); + } + return name + ".fromList " + toString(list, separator); + } else { + var output = ""; + for (var i in v) { + if (i === 'ctor') continue; + var str = toString(v[i], separator); + var parenless = str[0] === '{' || + str[0] === '<' || + str[0] === "[" || + str.indexOf(' ') < 0; + output += ' ' + (parenless ? str : "(" + str + ')'); + } + return v.ctor + output; + } + } + if (type === 'object' && 'notify' in v) return ''; + return ""; + }; + + function addSlashes(str) + { + return str.replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + .replace(/\r/g, '\\r') + .replace(/\v/g, '\\v') + .replace(/\0/g, '\\0') + .replace(/\'/g, "\\'") + .replace(/\"/g, '\\"'); + } + + function probablyPublic(v) + { + var keys = Object.keys(v); + var len = keys.length; + if (len === 3 + && 'props' in v + && 'element' in v) return false; + if (len === 5 + && 'horizontal' in v + && 'vertical' in v + && 'x' in v + && 'y' in v) return false; + if (len === 7 + && 'theta' in v + && 'scale' in v + && 'x' in v + && 'y' in v + && 'alpha' in v + && 'form' in v) return false; + return true; + } + + return toString; +}(); diff --git a/frontend/debugger-implementation.js b/frontend/debugger-implementation.js index fceca3e..28c9fde 100644 --- a/frontend/debugger-implementation.js +++ b/frontend/debugger-implementation.js @@ -72,871 +72,10 @@ function ignore(e) // CODE TO SET UP A MODULE FOR DEBUGGING -Elm.fullscreenDebug = function(moduleName, fileName) { - var result = initModuleWithDebugState(moduleName); - - var container = document.createElement("div"); - container.style.width = "100%"; - container.style.height = "100%"; - container.style.position = "absolute"; - container.style.top = 0; - container.style.left = 0; - document.body.appendChild(container); - - var overlay = Elm.embed(Elm.Overlay, container, { - eventCounter: 0, - watches: [], - showSwap: true +Elm.fullscreenDebug = function(module, fileName) { + Elm.fullscreen(Elm.Debugger, { + initMod: module }); - - var sideBar = document.getElementById('elm-reactor-side-bar'); - sideBar.addEventListener("click", blockClicks); - function blockClicks(e) - { - var event = e || window.event; - event.cancelBubble = true; - if (event.stopPropagation) - { - event.stopPropagation(); - } - } - - var eventBlocker = document.getElementById('elm-reactor-event-blocker'); - for (var i = eventsToIgnore.length; i-- ;) - { - eventBlocker.addEventListener(eventsToIgnore[i], ignore, true); - } - - function updateWatches(index) - { - overlay.ports.watches.send(watchesAt(index, result.debugState)); - } - - overlay.ports.scrubTo.subscribe(function(index) { - jumpTo(index, result.debugState); - updateWatches(index); - }); - - overlay.ports.pause.subscribe(function(paused) { - if (paused) - { - pause(result.debugState); - } - else - { - unpause(result.debugState); - redoTraces(result.debugState); - } - }); - - overlay.ports.restart.subscribe(function() { - restart(result.debugState); - updateWatches(0); - }); - - overlay.ports.permitSwap.subscribe(function(permitSwaps) { - result.debugState.permitSwaps = permitSwaps; - }); - - result.debugState.onNotify = function(debugState) { - overlay.ports.eventCounter.send(debugState.index); - updateWatches(debugState.index); - }; - - // handle swaps - var updates = 'ws://' + window.location.host + '/socket?file=' + fileName - var connection = new WebSocket(updates); - connection.addEventListener('message', function(event) { - if (result.debugState.permitSwaps) - { - result = swap(event.data, result); - updateWatches(result.debugState.index); - } - }); - window.addEventListener("unload", function() { - connection.close(); - }); - - return result.module; }; - -function initModuleWithDebugState(moduleName) -{ - var debugState; - - function make(localRuntime) - { - var result = initAndWrap(getModule(moduleName), localRuntime); - debugState = result.debugState; - return result.values; - } - - return { - module: Elm.fullscreen({ make: make }), - debugState: debugState - }; -} - -function getModule(moduleName) -{ - var elmModule = Elm; - var names = moduleName.split('.'); - for (var i = 0; i < names.length; ++i) - { - elmModule = elmModule[names[i]]; - } - return elmModule; -} - - -// DEBUG STATE - -function emptyDebugState() -{ - return { - paused: false, - pausedAtTime: 0, - totalTimeLost: 0, - - index: 0, - events: [], - watches: [{}], - snapshots: [], - asyncCallbacks: [], - - initialSnapshot: [], - initialAsyncCallbacks: [], - signalGraphNodes: [], - - traces: {}, - traceCanvas: createCanvas(), - - permitSwaps: true, - swapInProgress: false, - - onNotify: function() {}, - refreshScreen: function() {}, - node: null, - notify: function() {} - }; -} - -function restart(debugState) -{ - var running = !debugState.paused; - if (running) - { - pause(debugState); - } - debugState.index = 0; - debugState.events = []; - debugState.watches = [debugState.watches[0]]; - - var snap = debugState.initialSnapshot; - debugState.snapshots = [snap]; - for (var i = snap.length; i--; ) - { - debugState.signalGraphNodes[i].value = snap[i].value; - } - - debugState.asyncCallbacks = debugState.initialAsyncCallbacks.map(function(thunk) { - return { - thunk: thunk, - id: 0, - executed: false - }; - }); - - debugState.traces = {}; - redoTraces(debugState); - debugState.refreshScreen(); - - if (running) - { - unpause(debugState); - } -} - -function pause(debugState) -{ - if (debugState.paused) - { - return; - } - debugState.paused = true; - pauseAsyncCallbacks(debugState); - debugState.pausedAtTime = Date.now(); -} - -function unpause(debugState) -{ - debugState.paused = false; - - // add delay due to the pause itself - var pauseDelay = Date.now() - debugState.pausedAtTime; - debugState.totalTimeLost += pauseDelay; - - // add delay if travelling to older event - if (debugState.index < debugState.events.length - 1) - { - debugState.totalTimeLost = Date.now() - debugState.events[debugState.index].time; - } - - // clear out future snapshots, events, and traces - var nearestSnapshotIndex = Math.floor(debugState.index / EVENTS_PER_SAVE); - debugState.snapshots = debugState.snapshots.slice(0, nearestSnapshotIndex + 1); - debugState.events = debugState.events.slice(0, debugState.index); - clearTracesAfter(debugState.index, debugState); - clearWatchesAfter(debugState.index, debugState); - - unpauseAsyncCallbacks(debugState.asyncCallbacks); -} - -function jumpTo(index, debugState) -{ - if (!debugState.paused) - { - pause(debugState); - } - - assert( - 0 <= index && index <= debugState.events.length, - "Trying to step to non-existent event index " + index - ); - - var potentialIndex = indexOfSnapshotBefore(index); - if (index < debugState.index || potentialIndex > debugState.index) - { - var snapshot = getNearestSnapshot(index, debugState.snapshots); - - for (var i = debugState.signalGraphNodes.length; i-- ; ) - { - debugState.signalGraphNodes[i].value = snapshot[i].value; - } - - debugState.index = potentialIndex; - } - - while (debugState.index < index) - { - var event = debugState.events[debugState.index]; - debugState.notify(event.id, event.value); - debugState.index += 1; - } - redoTraces(debugState); -} - -function swap(rawJsonResponse, oldResult) -{ - var error = document.getElementById(ERROR_MESSAGE_ID); - if (error) - { - error.parentNode.removeChild(error); - } - - var response = JSON.parse(rawJsonResponse); - - if (!response.code) - { - var msg = response.error || 'something went wrong with swap'; - document.body.appendChild(initErrorMessage(msg)); - return oldResult; - } - // TODO: pause/unpause? - pauseAsyncCallbacks(oldResult.debugState); - window.eval(response.code); - - // remove old nodes - oldResult.debugState.node.parentNode.removeChild(oldResult.debugState.node); - document.body.removeChild(oldResult.debugState.traceCanvas); - oldResult.module.dispose(); - - var result = initModuleWithDebugState(response.name); - transferState(oldResult.debugState, result.debugState); - return result; -} - -function transferState(previousDebugState, debugState) -{ - debugState.swapInProgress = true; - debugState.events = previousDebugState.events; - debugState.onNotify = previousDebugState.onNotify; - - if (previousDebugState.paused) - { - debugState.paused = true; - pauseAsyncCallbacks(debugState); - debugState.pausedAtTime = previousDebugState.pausedAtTime; - debugState.totalTimeLost = previousDebugState.totalTimeLost; - } - - while (debugState.index < debugState.events.length) - { - var event = debugState.events[debugState.index]; - pushWatchFrame(debugState); - debugState.notify(event.id, event.value); - debugState.index += 1; - snapshotIfNeeded(debugState); - } - redoTraces(debugState); - debugState.swapInProgress = false; - - jumpTo(previousDebugState.index, debugState); -} - - -// CALLBACKS - -// TODO: is it weird that the callbacks array never shrinks? - -function unpauseAsyncCallbacks(callbacks) -{ - callbacks.forEach(function(callback) { - if (!callback.executed) - { - callback.executed = true; - callback.thunk(); - } - }); -} - -function pauseAsyncCallbacks(debugState) -{ - debugState.asyncCallbacks.forEach(function(callback) { - if (!callback.executed) - { - clearTimeout(callback.id); - } - }); -} - - - -// TRACES - -function clearTracesAfter(index, debugState) -{ - var newTraces = {}; - for (var id in debugState.traces) - { - var trace = debugState.traces[id]; - for (var i = trace.length; i--; ) - { - if (trace[i].index < index) - { - newTraces[id] = debugState.traces[id].slice(0, i + 1); - break; - } - } - } - debugState.traces = newTraces; -} - -function createCanvas() { - var canvas = document.createElement('canvas'); - // TODO: make dimensions adjust based on screen size - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - canvas.style.position = "absolute"; - canvas.style.top = "0"; - canvas.style.left = "0"; - canvas.style.pointerEvents = "none"; - return canvas; -} - -function addTraces(debugState) -{ - var ctx = debugState.traceCanvas.getContext('2d'); - - ctx.save(); - for (var id in debugState.traces) - { - var points = debugState.traces[id]; - if (points.length < 2) - { - continue; - } - var lastTracePoint = points[points.length - 1]; - if (lastTracePoint.index < debugState.index - 1) - { - continue; - } - ctx.beginPath(); - ctx.moveTo(lastTracePoint.x, lastTracePoint.y); - var secondToLastTracePoint = points[points.length - 2]; - ctx.lineTo(secondToLastTracePoint.x, secondToLastTracePoint.y); - - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(50, 50, 50, 0.4)"; - ctx.stroke(); - } - ctx.restore(); -} - -function redoTraces(debugState) { - var ctx = debugState.traceCanvas.getContext('2d'); - - // TODO: be more clever about the size of the canvas on resize - ctx.clearRect(0, 0, debugState.traceCanvas.width, debugState.traceCanvas.height); - - ctx.save(); - for (var id in debugState.traces) - { - var points = debugState.traces[id]; - var length = points.length; - if (length < 2) - { - continue; - } - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.moveTo(points[0].x, points[0].y); - var currentIndex = debugState.index; - var traceIndex = points[0].index; - for (var i = 1; traceIndex < currentIndex && i < length; ++i) - { - var point = points[i]; - ctx.lineTo(point.x, point.y); - traceIndex = point.index; - } - ctx.strokeStyle = "rgba(50, 50, 50, 0.4)"; - ctx.stroke(); - - for (; i < length; ++i) - { - var point = points[i]; - ctx.lineTo(point.x, point.y); - traceIndex = point.index; - } - ctx.strokeStyle = "rgba(50, 50, 50, 0.2)"; - ctx.stroke(); - } - ctx.restore(); -} - -function makeTraceRecorder(debugState, runtime) -{ - var List = Elm.List.make(runtime); - var Transform = Elm.Transform2D.make(runtime); - - function crawlElement(element) - { - if (debugState.paused && !debugState.swapInProgress) - { - return; - } - - var e = element.element; - if (!e) - { - return; - } - if (e.ctor === 'Custom' && e.type === 'Collage') - { - var w = element.props.width; - var h = element.props.height; - var identity = A6( Transform.matrix, 1, 0, 0, -1, w/2, h/2 ); - return A2(List.map, crawlForm(identity), e.model.forms); - } - } - - function crawlForm(matrix) - { - return function(form) { - if (form.form.ctor == "FGroup") - { - var scale = form.scale; - var localMatrix = A6( Transform.matrix, scale, 0, 0, scale, form.x, form.y ); - - var theta = form.theta - if (theta !== 0) - { - localMatrix = A2( Transform.multiply, localMatrix, Transform.rotation(theta) ); - } - - var newMatrix = A2( Transform.multiply, matrix, localMatrix ); - A2(List.map, crawlForm(newMatrix), form.form._1); - } - - var tag = form.trace; - if (!tag) - { - return; - } - - var x = matrix[0] * form.x + matrix[1] * form.y + matrix[2]; - var y = matrix[3] * form.x + matrix[4] * form.y + matrix[5]; - - if ( !(tag in debugState.traces) ) - { - debugState.traces[tag] = [{ index: debugState.index, x: x, y: y }]; - return; - } - - var trace = debugState.traces[tag]; - var lastPoint = trace[trace.length - 1]; - if (lastPoint.x === x && lastPoint.y === y) - { - return; - } - trace.push({ index: debugState.index, x: x, y: y }); - } - } - - return crawlElement; -} - - -// SNAPSHOTS - -var EVENTS_PER_SAVE = 100; - -function snapshotIfNeeded(debugState) -{ - if (debugState.index % EVENTS_PER_SAVE === 0) - { - debugState.snapshots.push(createSnapshot(debugState.signalGraphNodes)); - } -} - -function indexOfSnapshotBefore(index) -{ - return Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE; -} - -function getNearestSnapshot(i, snapshots) -{ - var snapshotIndex = Math.floor(i / EVENTS_PER_SAVE); - assert( - snapshotIndex < snapshots.length && snapshotIndex >= 0, - "Trying to access non-existent snapshot (event " + i + ", snapshot " + snapshotIndex + ")" - ); - return snapshots[snapshotIndex]; -} - -function createSnapshot(signalGraphNodes) -{ - var nodeValues = []; - - signalGraphNodes.forEach(function(node) { - nodeValues.push({ value: node.value, id: node.id }); - }); - - return nodeValues; -} - -function flattenSignalGraph(nodes) -{ - var nodesById = {}; - - function addAllToDict(node) - { - nodesById[node.id] = node; - node.kids.forEach(addAllToDict); - } - nodes.forEach(addAllToDict); - - var allNodes = Object.keys(nodesById).sort(compareNumbers).map(function(key) { - return nodesById[key]; - }); - return allNodes; -} - -function compareNumbers(a, b) -{ - return a - b; -} - - -// WRAP THE RUNTIME - -function initAndWrap(elmModule, runtime) -{ - var debugState = emptyDebugState(); - - // runtime is the prototype of wrappedRuntime - // so we can access all runtime properties too - var wrappedRuntime = Object.create(runtime); - wrappedRuntime.notify = notifyWrapper; - wrappedRuntime.setTimeout = setTimeoutWrapper; - - // make a copy of the wrappedRuntime - var assignedPropTracker = Object.create(wrappedRuntime); - var values = elmModule.make(assignedPropTracker); - - // make sure the signal graph is actually a signal & extract the visual model - var Signal = Elm.Signal.make(assignedPropTracker); - if ( !('notify' in values.main) ) - { - values.main = Signal.constant(values.main); - } - A2(Signal.map, makeTraceRecorder(debugState, assignedPropTracker), values.main); - - debugState.refreshScreen = function() { - var main = values.main - for (var i = main.kids.length ; i-- ; ) - { - main.kids[i].notify(runtime.timer.now(), true, main.id); - } - }; - - // The main module stores imported modules onto the runtime. - // To ensure only one instance of each module is created, - // we assign them back on the original runtime object. - Object.keys(assignedPropTracker).forEach(function(key) { - runtime[key] = assignedPropTracker[key]; - }); - - debugState.signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs); - debugState.initialSnapshot = createSnapshot(debugState.signalGraphNodes); - debugState.snapshots = [debugState.initialSnapshot]; - debugState.initialAsyncCallbacks = debugState.asyncCallbacks.map(function(callback) { - return callback.thunk; - }); - debugState.node = runtime.node; - debugState.notify = runtime.notify; - - // Tracing stuff - document.body.appendChild(debugState.traceCanvas); - - var replace = Elm.Native.Utils.make(assignedPropTracker).replace; - - runtime.timer.now = function() { - if (debugState.paused || debugState.swapInProgress) - { - var event = debugState.events[debugState.index]; - return event.time; - } - return Date.now() - debugState.totalTimeLost; - }; - - runtime.debug = {}; - - runtime.debug.trace = function(tag, form) { - return replace([['trace', tag]], form); - } - - runtime.debug.watch = function(tag, value) { - if (debugState.paused && !debugState.swapInProgress) - { - return; - } - var index = debugState.index; - var numWatches = debugState.watches.length - 1; - assert( - index === numWatches, - 'number of watch frames (' + numWatches + ') should match current index (' + index + ')'); - debugState.watches[debugState.index][tag] = value; - } - - function notifyWrapper(id, value) - { - // Ignore all events that occur while the program is paused. - if (debugState.paused) - { - return false; - } - - // Record the event - debugState.events.push({ id: id, value: value, time: runtime.timer.now() }); - debugState.index += 1; - pushWatchFrame(debugState); - - var changed = runtime.notify(id, value); - - snapshotIfNeeded(debugState); - debugState.onNotify(debugState); - addTraces(debugState); - - return changed; - } - - function setTimeoutWrapper(thunk, delay) - { - if (debugState.paused) - { - return 0; - } - - var callback = { - thunk: thunk, - id: 0, - executed: false - }; - - callback.id = setTimeout(function() { - callback.executed = true; - thunk(); - }, delay); - - debugState.asyncCallbacks.push(callback); - return callback.id; - } - - return { - values: values, - debugState: debugState - }; -} - - -// WATCHES - -function watchesAt(index, debugState) -{ - var watchSnapshot = []; - var watches = debugState.watches[index]; - - for (var name in watches) - { - var value = prettyPrint(watches[name], " "); - watchSnapshot.push([ name, value ]); - } - return watchSnapshot; -} - -function pushWatchFrame(debugState) -{ - var length = debugState.watches.length; - var oldFrame = length === 0 ? {} : debugState.watches[length - 1]; - var newFrame = {}; - for (var tag in oldFrame) - { - newFrame[tag] = oldFrame[tag]; - } - debugState.watches.push(newFrame); -} - -function clearWatchesAfter(index, debugState) -{ - debugState.watches = debugState.watches.slice(0, index + 1); -} - -var prettyPrint = function() { - - var independentRuntime = {}; - var List; - var ElmArray; - var Dict; - - var toString = function(v, separator) { - var type = typeof v; - if (type === "function") { - var name = v.func ? v.func.name : v.name; - return ''; - } else if (type === "boolean") { - return v ? "True" : "False"; - } else if (type === "number") { - return v.toFixed(2).replace(/\.0+$/g, ''); - } else if ((v instanceof String) && v.isChar) { - return "'" + addSlashes(v) + "'"; - } else if (type === "string") { - return '"' + addSlashes(v) + '"'; - } else if (type === "object" && '_' in v && probablyPublic(v)) { - var output = []; - for (var k in v._) { - for (var i = v._[k].length; i--; ) { - output.push(k + " = " + toString(v._[k][i], separator)); - } - } - for (var k in v) { - if (k === '_') continue; - output.push(k + " = " + toString(v[k], separator)); - } - if (output.length === 0) return "{}"; - var body = "\n" + output.join(",\n"); - return "{" + body.replace(/\n/g,"\n" + separator) + "\n}"; - } else if (type === "object" && 'ctor' in v) { - if (v.ctor.substring(0,6) === "_Tuple") { - var output = []; - for (var k in v) { - if (k === 'ctor') continue; - output.push(toString(v[k], separator)); - } - return "(" + output.join(", ") + ")"; - } else if (v.ctor === "_Array") { - if (!ElmArray) { - ElmArray = Elm.Array.make(independentRuntime); - } - var list = ElmArray.toList(v); - return "Array.fromList " + toString(list, separator); - } else if (v.ctor === "::") { - var output = '[\n' + toString(v._0, separator); - v = v._1; - while (v && v.ctor === "::") { - output += ",\n" + toString(v._0, separator); - v = v._1; - } - return output.replace(/\n/g,"\n" + separator) + "\n]"; - } else if (v.ctor === "[]") { - return "[]"; - } else if (v.ctor === "RBNode" || v.ctor === "RBEmpty") { - if (!Dict || !List) { - Dict = Elm.Dict.make(independentRuntime); - List = Elm.List.make(independentRuntime); - } - var list = Dict.toList(v); - var name = "Dict"; - if (list.ctor === "::" && list._0._1.ctor === "_Tuple0") { - name = "Set"; - list = A2(List.map, function(x){return x._0}, list); - } - return name + ".fromList " + toString(list, separator); - } else { - var output = ""; - for (var i in v) { - if (i === 'ctor') continue; - var str = toString(v[i], separator); - var parenless = str[0] === '{' || - str[0] === '<' || - str[0] === "[" || - str.indexOf(' ') < 0; - output += ' ' + (parenless ? str : "(" + str + ')'); - } - return v.ctor + output; - } - } - if (type === 'object' && 'notify' in v) return ''; - return ""; - }; - - function addSlashes(str) - { - return str.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\n') - .replace(/\t/g, '\\t') - .replace(/\r/g, '\\r') - .replace(/\v/g, '\\v') - .replace(/\0/g, '\\0') - .replace(/\'/g, "\\'") - .replace(/\"/g, '\\"'); - } - - function probablyPublic(v) - { - var keys = Object.keys(v); - var len = keys.length; - if (len === 3 - && 'props' in v - && 'element' in v) return false; - if (len === 5 - && 'horizontal' in v - && 'vertical' in v - && 'x' in v - && 'y' in v) return false; - if (len === 7 - && 'theta' in v - && 'scale' in v - && 'x' in v - && 'y' in v - && 'alpha' in v - && 'form' in v) return false; - return true; - } - - return toString; -}(); - - -}()); +})(); \ No newline at end of file