on master help sign in
Merge branch 'debugger'
7bc4690Evan Czaplicki
libraries/Debug.elm+21−1
@@ -2,9 +2,14 @@ module Debug where
2{-| This library is for investigating bugs or performance problems. It should
2{-| This library is for investigating bugs or performance problems. It should
3*not* be used in production code.
3*not* be used in production code.
4
4
5+# Console Debugging
5@docs log, crash
6@docs log, crash
7+
8+# Time-Travel Debugging
9+@docs watch, watchSummary, trace
6-}
10-}
7
11
12+import Graphics.Collage (Form)
8import Native.Debug
13import Native.Debug
9import Native.Error
14import Native.Error
10
15
@@ -43,4 +48,19 @@ writing a function.
43Use the `Maybe` or `Either` libraries instead.
48Use the `Maybe` or `Either` libraries instead.
44-}
49-}
45crash : String -> a
50crash : String -> a
46−crash = Native.Error.raise
51+crash = Native.Error.raise
52+
53+{-| Watch a particular value in the reactive debugger.
54+-}
55+watch : String -> a -> a
56+watch = Native.Debug.watch
57+
58+{-| Watch a particular value in the reactive debugger.
59+-}
60+watchSummary : String -> (a -> b) -> a -> a
61+watchSummary = Native.Debug.watchSummary
62+
63+{-| Trace a Form over time in the reactive debugger.
64+-}
65+trace : String -> Form -> Form
66+trace = Native.Debug.tracePath
libraries/Native/Debug.js+40−4
@@ -3,10 +3,13 @@ Elm.Native.Debug.make = function(elm) {
3 elm.Native = elm.Native || {};
3 elm.Native = elm.Native || {};
4 elm.Native.Debug = elm.Native.Debug || {};
4 elm.Native.Debug = elm.Native.Debug || {};
5 if (elm.Native.Debug.values) return elm.Native.Debug.values;
5 if (elm.Native.Debug.values) return elm.Native.Debug.values;
6+ if ('values' in Elm.Native.Debug)
7+ return elm.Native.Debug.values = Elm.Native.Debug.values;
6
8
7 var show = Elm.Native.Show.make(elm).show;
9 var show = Elm.Native.Show.make(elm).show;
10+ var replace = Elm.Native.Utils.make(elm).replace;
8
11
9− function log(tag,value) {
12+ function log(tag, value) {
10 var msg = tag + ': ' + show(value);
13 var msg = tag + ': ' + show(value);
11 var process = process || {};
14 var process = process || {};
12 if (process.stdout) {
15 if (process.stdout) {
@@ -17,8 +20,41 @@ Elm.Native.Debug.make = function(elm) {
17 return value;
20 return value;
18 }
21 }
19
22
23+ function tracePath(debugId, form) {
24+ return replace([["debugTracePathId",debugId]], form);
25+ }
26+
27+ function WatchTracker() {
28+ this.frames = [{}];
29+ this.clear = function() {
30+ this.watches = {};
31+ };
32+ this.pushFrame = function() {
33+ var lastFrame = this.frames[this.frames.length - 1];
34+ this.frames.push(lastFrame);
35+ }
36+ this.notify = function(tag, value) {
37+ this.frames[this.frames.length - 1][tag] = value;
38+ };
39+ }
40+ var watchTracker = new WatchTracker();
41+
42+ function watch(tag, value) {
43+ watchTracker.notify(tag, value);
44+ return value;
45+ }
46+
47+ function watchSummary(tag, f, value) {
48+ watchTracker.notify(tag, f(value));
49+ return value;
50+ }
51+
20− return elm.Native.Debug.values = {
52+ Elm.Native.Debug.values = {
53+ tracePath: F2(tracePath),
21− log: F2(log)
54+ log: F2(log),
55+ watch: F2(watch),
56+ watchSummary:F3(watchSummary),
57+ watchTracker: watchTracker
22 };
58 };
23−
59+ return elm.Native.Debug.values = Elm.Native.Debug.values;
24};
60};
libraries/Native/Show.js+1−1
@@ -18,7 +18,7 @@ Elm.Native.Show.make = function(elm) {
18 } else if (type === "boolean") {
18 } else if (type === "boolean") {
19 return v ? "True" : "False";
19 return v ? "True" : "False";
20 } else if (type === "number") {
20 } else if (type === "number") {
21− return v+"";
21+ return v.toFixed(2).replace(/\.0+$/g, '');
22 } else if ((v instanceof String) && v.isChar) {
22 } else if ((v instanceof String) && v.isChar) {
23 return "'" + addSlashes(v) + "'";
23 return "'" + addSlashes(v) + "'";
24 } else if (type === "string") {
24 } else if (type === "string") {
libraries/Native/Signal/Signal.js+24−2
@@ -17,6 +17,8 @@ Elm.Native.Signal.make = function(elm) {
17 }
17 }
18
18
19 function Input(base) {
19 function Input(base) {
20+ this.nodeType = "input";
21+
20 this.id = Utils.guid();
22 this.id = Utils.guid();
21 this.value = base;
23 this.value = base;
22 this.kids = [];
24 this.kids = [];
@@ -31,6 +33,8 @@ Elm.Native.Signal.make = function(elm) {
31 }
33 }
32
34
33 function LiftN(update, args) {
35 function LiftN(update, args) {
36+ this.nodeType = "lift";
37+
34 this.id = Utils.guid();
38 this.id = Utils.guid();
35 this.value = update();
39 this.value = update();
36 this.kids = [];
40 this.kids = [];
@@ -86,6 +90,8 @@ Elm.Native.Signal.make = function(elm) {
86 }
90 }
87
91
88 function Foldp(step, state, input) {
92 function Foldp(step, state, input) {
93+ this.nodeType = "foldp";
94+
89 this.id = Utils.guid();
95 this.id = Utils.guid();
90 this.value = state;
96 this.value = state;
91 this.kids = [];
97 this.kids = [];
@@ -104,6 +110,8 @@ Elm.Native.Signal.make = function(elm) {
104 }
110 }
105
111
106 function DropIf(pred,base,input) {
112 function DropIf(pred,base,input) {
113+ this.nodeType = "dropIf";
114+
107 this.id = Utils.guid();
115 this.id = Utils.guid();
108 this.value = pred(input.value) ? base : input.value;
116 this.value = pred(input.value) ? base : input.value;
109 this.kids = [];
117 this.kids = [];
@@ -116,6 +124,8 @@ Elm.Native.Signal.make = function(elm) {
116 }
124 }
117
125
118 function DropRepeats(input) {
126 function DropRepeats(input) {
127+ this.nodeType = "dropRepeats";
128+
119 this.id = Utils.guid();
129 this.id = Utils.guid();
120 this.value = input.value;
130 this.value = input.value;
121 this.kids = [];
131 this.kids = [];
@@ -128,11 +138,14 @@ Elm.Native.Signal.make = function(elm) {
128 }
138 }
129
139
130 function timestamp(a) {
140 function timestamp(a) {
131− function update() { return Utils.Tuple2(Date.now(), a.value); }
141+ function update() { return Utils.Tuple2(elm.timer.now(), a.value); }
132 return new LiftN(update, [a]);
142 return new LiftN(update, [a]);
133 }
143 }
134
144
135 function SampleOn(s1,s2) {
145 function SampleOn(s1,s2) {
146+ this.nodeType = "sampleOn";
147+ this.indirectParent = s1;
148+
136 this.id = Utils.guid();
149 this.id = Utils.guid();
137 this.value = s2.value;
150 this.value = s2.value;
138 this.kids = [];
151 this.kids = [];
@@ -157,6 +170,8 @@ Elm.Native.Signal.make = function(elm) {
157 function sampleOn(s1,s2) { return new SampleOn(s1,s2); }
170 function sampleOn(s1,s2) { return new SampleOn(s1,s2); }
158
171
159 function delay(t,s) {
172 function delay(t,s) {
173+ this.nodeType = "delay";
174+
160 var delayed = new Input(s.value);
175 var delayed = new Input(s.value);
161 var firstEvent = true;
176 var firstEvent = true;
162 function update(v) {
177 function update(v) {
@@ -168,6 +183,8 @@ Elm.Native.Signal.make = function(elm) {
168 }
183 }
169
184
170 function Merge(s1,s2) {
185 function Merge(s1,s2) {
186+ this.nodeType = "merge";
187+
171 this.id = Utils.guid();
188 this.id = Utils.guid();
172 this.value = s1.value;
189 this.value = s1.value;
173 this.kids = [];
190 this.kids = [];
@@ -199,7 +216,12 @@ Elm.Native.Signal.make = function(elm) {
199 function merges(ss) { return A2(foldr1, F2(merge), ss); }
216 function merges(ss) { return A2(foldr1, F2(merge), ss); }
200
217
201 return elm.Native.Signal.values = {
218 return elm.Native.Signal.values = {
219+ input: function(v) { return new Input(v); },
202− constant : function(v) { return new Input(v); },
220+ constant : function(v) {
221+ var s = new Input(v);
222+ s.nodeType = "constant";
223+ return s;
224+ },
203 lift : F2(lift ),
225 lift : F2(lift ),
204 lift2 : F3(lift2),
226 lift2 : F3(lift2),
205 lift3 : F4(lift3),
227 lift3 : F4(lift3),
libraries/Native/Signal/Time.js+15−9
@@ -12,11 +12,15 @@ Elm.Native.Time.make = function(elm) {
12
12
13 function fpsWhen(desiredFPS, isOn) {
13 function fpsWhen(desiredFPS, isOn) {
14 var msPerFrame = 1000 / desiredFPS;
14 var msPerFrame = 1000 / desiredFPS;
15− var prev = Date.now(), curr = prev, diff = 0, wasOn = true;
15+ var prev = elm.timer.now(), curr = prev, diff = 0, wasOn = true;
16− var ticker = Signal.constant(diff);
16+ var ticker = NS.input(diff);
17− function tick(zero) { return function() {
17+ function tick(zero) {
18+ return function() {
18− curr = Date.now();
19+ curr = elm.timer.now();
19 diff = zero ? 0 : curr - prev;
20 diff = zero ? 0 : curr - prev;
21+ if (prev > curr) {
22+ diff = 0;
23+ }
20 prev = curr;
24 prev = curr;
21 elm.notify(ticker.id, diff);
25 elm.notify(ticker.id, diff);
22 };
26 };
@@ -24,7 +28,7 @@ Elm.Native.Time.make = function(elm) {
24 var timeoutID = 0;
28 var timeoutID = 0;
25 function f(isOn, t) {
29 function f(isOn, t) {
26 if (isOn) {
30 if (isOn) {
27− timeoutID = setTimeout(tick(!wasOn && isOn), msPerFrame);
31+ timeoutID = elm.setTimeout(tick(!wasOn && isOn), msPerFrame);
28 } else if (wasOn) {
32 } else if (wasOn) {
29 clearTimeout(timeoutID);
33 clearTimeout(timeoutID);
30 }
34 }
@@ -35,10 +39,12 @@ Elm.Native.Time.make = function(elm) {
35 }
39 }
36
40
37 function every(t) {
41 function every(t) {
38− var clock = Signal.constant(Date.now());
42+ var clock = NS.input(elm.timer.now());
39− setInterval(function() {
43+ function tellTime() {
40− elm.notify(clock.id, Date.now());
44+ elm.notify(clock.id, elm.timer.now());
41− }, t);
45+ elm.setTimeout(tellTime, t);
46+ }
47+ elm.setTimeout(tellTime, t);
42 return clock;
48 return clock;
43 }
49 }
44
50
libraries/Native/Signal/Touch.js+2−2
@@ -61,11 +61,11 @@ Elm.Native.Touch.make = function(elm) {
61 dict.insert(e.identifier,
61 dict.insert(e.identifier,
62 {x: point._0,
62 {x: point._0,
63 y: point._1,
63 y: point._1,
64− t: Date.now()});
64+ t: elm.timer.now()});
65 }
65 }
66 function end(e) {
66 function end(e) {
67 var t = dict.remove(e.identifier);
67 var t = dict.remove(e.identifier);
68− if (Date.now() - t.t < tapTime) {
68+ if (elm.timer.now() - t.t < tapTime) {
69 hasTap = true;
69 hasTap = true;
70 tap = {_:{}, x:t.x, y:t.y};
70 tap = {_:{}, x:t.x, y:t.y};
71 }
71 }
libraries/Native/Signal/Window.js+2−1
@@ -6,6 +6,7 @@ Elm.Native.Window.make = function(elm) {
6 if (elm.Native.Window.values) return elm.Native.Window.values;
6 if (elm.Native.Window.values) return elm.Native.Window.values;
7
7
8 var Signal = Elm.Signal.make(elm);
8 var Signal = Elm.Signal.make(elm);
9+ var NS = Elm.Native.Signal.make(elm);
9 var Tuple2 = Elm.Native.Utils.make(elm).Tuple2;
10 var Tuple2 = Elm.Native.Utils.make(elm).Tuple2;
10
11
11 function getWidth() { return elm.node.clientWidth; }
12 function getWidth() { return elm.node.clientWidth; }
@@ -16,7 +17,7 @@ Elm.Native.Window.make = function(elm) {
16 return elm.node.clientHeight;
17 return elm.node.clientHeight;
17 }
18 }
18
19
19− var dimensions = Signal.constant(Tuple2(getWidth(), getHeight()));
20+ var dimensions = NS.input(Tuple2(getWidth(), getHeight()));
20 dimensions.defaultNumberOfKids = 2;
21 dimensions.defaultNumberOfKids = 2;
21
22
22 // Do not move width and height into Elm. By setting the default number of kids,
23 // Do not move width and height into Elm. By setting the default number of kids,
runtime/Debugger.jsnew file+604
@@ -0,0 +1,604 @@
1+(function() {
2+'use strict';
3+
4+if (!window.location.origin) {
5+ window.location.origin =
6+ window.location.protocol + "//" +
7+ window.location.hostname +
8+ (window.location.port ? (':' + window.location.port) : '');
9+}
10+
11+Elm.fullscreenDebugHooks = function(module, hotSwapState /* =undefined */) {
12+ var exposedDebugger = {};
13+ function debuggerAttach(module, hotSwapState) {
14+ return {
15+ make: function(runtime) {
16+ var wrappedModule = debugModule(module, runtime);
17+ exposedDebugger = debuggerInit(wrappedModule, runtime, hotSwapState);
18+ return wrappedModule.debuggedModule;
19+ }
20+ }
21+ }
22+ var mainHandle = Elm.fullscreen(debuggerAttach(module,hotSwapState));
23+ mainHandle.debugger = exposedDebugger;
24+ return mainHandle;
25+};
26+
27+var EVENTS_PER_SAVE = 100;
28+
29+function debugModule(module, runtime) {
30+ var programPaused = false;
31+ var recordedEvents = [];
32+ var asyncCallbacks = [];
33+ var snapshots = [];
34+ var watchTracker = Elm.Native.Debug.make(runtime).watchTracker;
35+ var pauseTime = 0;
36+ var eventsUntilSnapshot = EVENTS_PER_SAVE;
37+ runtime.debuggerStatus = runtime.debuggerStatus || {};
38+ runtime.debuggerStatus.eventCounter = runtime.debuggerStatus.eventCounter || 0;
39+
40+ // runtime is the prototype of wrappedRuntime
41+ // so we can access all runtime properties too
42+ var wrappedRuntime = Object.create(runtime);
43+ wrappedRuntime.notify = notifyWrapper;
44+ wrappedRuntime.setTimeout = setTimeoutWrapper;
45+
46+ // make a copy of the wrappedRuntime
47+ var assignedPropTracker = Object.create(wrappedRuntime);
48+ var debuggedModule = module.make(assignedPropTracker);
49+
50+ // make sure the signal graph is actually a signal & extract the visual model
51+ if ( !('recv' in debuggedModule.main) ) {
52+ debuggedModule.main = Elm.Signal.make(runtime).constant(debuggedModule.main);
53+ }
54+
55+ // The main module stores imported modules onto the runtime.
56+ // To ensure only one instance of each module is created,
57+ // we assign them back on the original runtime object.
58+ Object.keys(assignedPropTracker).forEach(function(key) {
59+ runtime[key] = assignedPropTracker[key];
60+ });
61+
62+ var signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs);
63+ var tracePath = tracePathInit(runtime, debuggedModule.main);
64+
65+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
66+
67+ function notifyWrapper(id, v) {
68+ var timestep = runtime.timer.now();
69+
70+ if (programPaused) {
71+ // ignore async events generated while playing back
72+ // or user events while program is paused
73+ return false;
74+ }
75+ else {
76+ recordEvent(id, v, timestep);
77+ var changed = runtime.notify(id, v, timestep);
78+ snapshotOnCheckpoint();
79+ if (parent.window) {
80+ parent.window.postMessage("elmNotify", window.location.origin);
81+ }
82+ return changed;
83+ }
84+ };
85+
86+ function setTimeoutWrapper(func, delayMs) {
87+ if (programPaused) {
88+ // Don't push timers and such to the callback stack while we're paused.
89+ // It causes too many callbacks to be fired during unpausing.
90+ return 0;
91+ }
92+ var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
93+ var timerId = setTimeout(function() {
94+ cbObj.executed = true;
95+ func();
96+ }, delayMs);
97+ cbObj.timerId = timerId;
98+ asyncCallbacks.push(cbObj);
99+ return timerId;
100+ }
101+
102+ function recordEvent(id, v, timestep) {
103+ watchTracker.pushFrame();
104+ recordedEvents.push({ id:id, value:v, timestep:timestep });
105+ runtime.debuggerStatus.eventCounter += 1;
106+ }
107+
108+ function clearAsyncCallbacks() {
109+ asyncCallbacks.forEach(function(timer) {
110+ if (!timer.executed) {
111+ clearTimeout(timer.timerId);
112+ }
113+ });
114+ }
115+
116+ function clearRecordedEvents() {
117+ recordedEvents = [];
118+ runtime.debuggerStatus.eventCounter = 0;
119+ }
120+
121+ function getRecordedEventsLength() {
122+ return recordedEvents.length;
123+ }
124+
125+ function getRecordedEventAt(i) {
126+ return recordedEvents[i];
127+ }
128+
129+ function copyRecordedEvents() {
130+ return recordedEvents.slice();
131+ }
132+
133+ function loadRecordedEvents(events) {
134+ recordedEvents = events.slice();
135+ }
136+
137+ function clearSnapshots() {
138+ snapshots = [snapshotSignalGraph(signalGraphNodes)];
139+ }
140+
141+ function getSnapshotAt(i) {
142+ var snapshotEvent = Math.floor(i / EVENTS_PER_SAVE);
143+ assert(snapshotEvent < snapshots.length && snapshotEvent >= 0,
144+ "Out of bounds index: " + snapshotEvent);
145+ return snapshots[snapshotEvent];
146+ }
147+
148+ function snapshotOnCheckpoint() {
149+ if (eventsUntilSnapshot === 1) {
150+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
151+ eventsUntilSnapshot = EVENTS_PER_SAVE;
152+ } else {
153+ eventsUntilSnapshot -= 1;
154+ }
155+ }
156+
157+ function setPaused() {
158+ programPaused = true;
159+ clearAsyncCallbacks();
160+ pauseTime = Date.now();
161+ tracePath.stopRecording();
162+ preventInputEvents();
163+ }
164+
165+ function setContinue(position) {
166+ var pauseDelay = Date.now() - pauseTime;
167+ runtime.timer.addDelay(pauseDelay);
168+ programPaused = false;
169+
170+ // we need to dump the events that are ahead of where we're continuing.
171+ var lastSnapshotPosition = Math.floor(position / EVENTS_PER_SAVE);
172+ eventsUntilSnapshot = EVENTS_PER_SAVE - (position % EVENTS_PER_SAVE);
173+ snapshots = snapshots.slice(0, lastSnapshotPosition + 1);
174+
175+ if (position < getRecordedEventsLength()) {
176+ var lastEventTime = recordedEvents[position].timestep;
177+ var scrubTime = runtime.timer.now() - lastEventTime;
178+ runtime.timer.addDelay(scrubTime);
179+ }
180+
181+ recordedEvents = recordedEvents.slice(0, position);
182+ tracePath.clearTracesAfter(position);
183+ runtime.debuggerStatus.eventCounter = position;
184+ executeCallbacks(asyncCallbacks);
185+ permitInputEvents();
186+
187+ tracePath.startRecording();
188+ }
189+
190+ function getPaused() {
191+ return programPaused;
192+ }
193+
194+ function preventInputEvents(){
195+ var events =
196+ [ "click", "mousemove", "mouseup", "mousedown", "mouseclick"
197+ , "keydown", "keypress", "keyup", "touchstart", "touchend"
198+ , "touchcancel", "touchleave", "touchmove", "pointermove"
199+ , "pointerdown", "pointerup", "pointerover", "pointerout"
200+ , "pointerenter", "pointerleave", "pointercancel"
201+ ];
202+
203+ var ignore = function(e) {
204+ var evt = e ? e : window.event;
205+ if (evt.stopPropagation) {
206+ evt.stopPropagation();
207+ }
208+ if (evt.cancelBubble !== null) {
209+ evt.cancelBubble = true;
210+ }
211+ if (evt.preventDefault) {
212+ evt.preventDefault();
213+ }
214+ return false;
215+ };
216+
217+ var ignoringDiv = document.getElementById("elmEventIgnorer");
218+ if (!ignoringDiv) {
219+ ignoringDiv = document.createElement("div");
220+ ignoringDiv.id = "elmEventIgnorer";
221+ ignoringDiv.style.position = "absolute";
222+ ignoringDiv.style.top = "0px";
223+ ignoringDiv.style.left = "0px";
224+ ignoringDiv.style.width = "100%";
225+ ignoringDiv.style.height = "100%";
226+
227+ for (var i = events.length; i-- ;) {
228+ ignoringDiv.addEventListener(events[i], ignore, true);
229+ }
230+ runtime.node.appendChild(ignoringDiv);
231+ }
232+ }
233+
234+ function permitInputEvents(){
235+ var ignoringDiv = document.getElementById("elmEventIgnorer");
236+ ignoringDiv.parentNode.removeChild(ignoringDiv);
237+ }
238+
239+ return {
240+ debuggedModule: debuggedModule,
241+ signalGraphNodes: signalGraphNodes,
242+ initialSnapshot: snapshotSignalGraph(signalGraphNodes),
243+ initialAsyncCallbacks: asyncCallbacks.slice(),
244+ // API functions
245+ clearAsyncCallbacks: clearAsyncCallbacks,
246+ clearRecordedEvents: clearRecordedEvents,
247+ getRecordedEventsLength: getRecordedEventsLength,
248+ getRecordedEventAt: getRecordedEventAt,
249+ copyRecordedEvents: copyRecordedEvents,
250+ loadRecordedEvents: loadRecordedEvents,
251+ clearSnapshots: clearSnapshots,
252+ getSnapshotAt: getSnapshotAt,
253+ snapshotOnCheckpoint: snapshotOnCheckpoint,
254+ getPaused: getPaused,
255+ setPaused: setPaused,
256+ setContinue: setContinue,
257+ tracePath: tracePath,
258+ watchTracker: watchTracker
259+ };
260+}
261+
262+// The debuggerHistory variable is passed in on hotswap. It represents
263+// the a state of the debugger for it to assume during init. It contains
264+// the paused state of the debugger, the recorded events, and the current
265+// event being processed.
266+function debuggerInit(debugModule, runtime, debuggerHistory /* =undefined */) {
267+ var currentEventIndex = 0;
268+
269+ function resetProgram(position) {
270+ var closestSnapshot = debugModule.getSnapshotAt(position);
271+ debugModule.clearAsyncCallbacks();
272+ restoreSnapshot(debugModule.signalGraphNodes, closestSnapshot);
273+ redrawGraphics();
274+ }
275+
276+ function restartProgram() {
277+ pauseProgram();
278+ resetProgram(0);
279+ debugModule.watchTracker.clear();
280+ debugModule.tracePath.clearTraces();
281+ debugModule.clearRecordedEvents();
282+ debugModule.clearSnapshots();
283+ debugModule.setContinue(0);
284+ executeCallbacks(debugModule.initialAsyncCallbacks);
285+ }
286+
287+ function pauseProgram() {
288+ debugModule.setPaused();
289+ currentEventIndex = debugModule.getRecordedEventsLength();
290+ }
291+
292+ function continueProgram() {
293+ if (debugModule.getPaused())
294+ {
295+ var closestSnapshotIndex =
296+ Math.floor(currentEventIndex / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
297+ resetProgram(currentEventIndex);
298+ var continueIndex = currentEventIndex;
299+ currentEventIndex = closestSnapshotIndex;
300+ stepTo(continueIndex);
301+ debugModule.setContinue(currentEventIndex);
302+ }
303+ }
304+
305+ function stepTo(index) {
306+ if (!debugModule.getPaused()) {
307+ debugModule.setPaused();
308+ resetProgram();
309+ }
310+
311+ if (index < 0 || index > getMaxSteps()) {
312+ throw "Index out of bounds: " + index;
313+ }
314+
315+ if (index < currentEventIndex) {
316+ var closestSnapshotIndex = Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
317+ resetProgram(index);
318+ currentEventIndex = closestSnapshotIndex;
319+ }
320+
321+ while (currentEventIndex < index) {
322+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
323+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
324+
325+ currentEventIndex += 1;
326+ }
327+ }
328+
329+ function getMaxSteps() {
330+ return debugModule.getRecordedEventsLength();
331+ }
332+
333+ function redrawGraphics() {
334+ var main = debugModule.debuggedModule.main
335+ for (var i = main.kids.length ; i-- ; ) {
336+ main.kids[i].recv(runtime.timer.now(), true, main.id);
337+ }
338+ }
339+
340+ function getHotSwapState() {
341+ var continueIndex = currentEventIndex;
342+ if (!debugModule.getPaused()) {
343+ continueIndex = getMaxSteps();
344+ }
345+ return {
346+ paused: debugModule.getPaused(),
347+ recordedEvents: debugModule.copyRecordedEvents(),
348+ currentEventIndex: continueIndex
349+ };
350+ }
351+
352+ function dispose() {
353+ var parentNode = runtime.node.parentNode;
354+ parentNode.removeChild(debugModule.tracePath.canvas);
355+ parentNode.removeChild(runtime.node);
356+ }
357+
358+ if (debuggerHistory) {
359+ // The problem is that we want to previous paused state. But
360+ // by the time JS reaches here, the old code has been swapped out
361+ // and the new modules are being generated. So we can ask the
362+ // debugging console what it thinks the pause state is and go
363+ // from there.
364+ var paused = debuggerHistory.paused;
365+ debugModule.setPaused();
366+ debugModule.loadRecordedEvents(debuggerHistory.recordedEvents);
367+ var index = getMaxSteps();
368+ runtime.debuggerStatus.eventCounter = 0;
369+ debugModule.tracePath.clearTraces();
370+
371+ // draw new trace path
372+ debugModule.tracePath.startRecording();
373+ while(currentEventIndex < index) {
374+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
375+ runtime.debuggerStatus.eventCounter += 1;
376+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
377+ debugModule.snapshotOnCheckpoint();
378+ currentEventIndex += 1;
379+ }
380+ debugModule.tracePath.stopRecording();
381+
382+ stepTo(debuggerHistory.currentEventIndex);
383+ if (!paused) {
384+ debugModule.setContinue(debuggerHistory.currentEventIndex);
385+ }
386+ }
387+
388+ runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
389+
390+ var elmDebugger = {
391+ restart: restartProgram,
392+ pause: pauseProgram,
393+ kontinue: continueProgram,
394+ getMaxSteps: getMaxSteps,
395+ stepTo: stepTo,
396+ getPaused: debugModule.getPaused,
397+ getHotSwapState: getHotSwapState,
398+ dispose: dispose,
399+ allNodes: debugModule.signalGraphNodes,
400+ watchTracker: debugModule.watchTracker
401+ };
402+
403+ return elmDebugger;
404+}
405+
406+function Point(x, y) {
407+ this.x = x;
408+ this.y = y;
409+
410+ this.translate = function(x, y) {
411+ return new Point(this.x + x, this.y + y);
412+ }
413+
414+ this.equals = function(p) {
415+ return this.x == p.x && this.y == p.y;
416+ }
417+}
418+
419+function tracePathInit(runtime, signalGraphMain) {
420+ var List = Elm.List.make(runtime);
421+ var Signal = Elm.Signal.make(runtime);
422+ var tracePathNode = A2(Signal.lift, graphicsUpdate, signalGraphMain);
423+ var tracePathCanvas = createCanvas();
424+ var tracePositions = {};
425+ var recordingTraces = true;
426+
427+ function findPositions(currentScene) {
428+ var positions = {};
429+ function processElement(elem, offset) {
430+ if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
431+ {
432+ List.map(F2(processForm)(offset))(elem.element.model.forms);
433+ }
434+ }
435+
436+ function processForm(offset, form) {
437+ if (form.form.ctor == "FElement")
438+ {
439+ processElement(form.form._0, offset.translate(form.x, -form.y));
440+ }
441+ if (form.form.ctor == "FGroup")
442+ {
443+ var newOffset = offset.translate(form.x, -form.y);
444+ List.map(F2(processForm)(newOffset))(form.form._1);
445+ }
446+ if (form.debugTracePathId)
447+ {
448+ positions[form.debugTracePathId] = new Point(form.x + offset.x, -form.y + offset.y);
449+ }
450+ }
451+
452+ processElement(currentScene, new Point(0, 0));
453+ return positions;
454+ }
455+
456+ function appendPositions(positions) {
457+ for (var id in positions) {
458+ var pos = positions[id];
459+ if (tracePositions.hasOwnProperty(id)) {
460+ tracePositions[id].push(pos);
461+ }
462+ else {
463+ tracePositions[id] = [pos];
464+ }
465+ if (tracePositions[id].length < runtime.debuggerStatus.eventCounter) {
466+ var padCount = runtime.debuggerStatus.eventCounter - tracePositions[id].length;
467+ var lastTracePosition = tracePositions[id][tracePositions[id].length - 1];
468+ for (var i = padCount; i--;) {
469+ tracePositions[id].push(lastTracePosition)
470+ }
471+ }
472+ assert(tracePositions[id].length === runtime.debuggerStatus.eventCounter,
473+ "We don't have a 1-1 mapping of trace positions to events");
474+ }
475+ }
476+
477+ function graphicsUpdate(currentScene) {
478+ if (!recordingTraces) {
479+ return;
480+ }
481+
482+ var ctx = tracePathCanvas.getContext('2d');
483+ ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
484+
485+ ctx.save();
486+ ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
487+ appendPositions(findPositions(currentScene));
488+ for (var id in tracePositions)
489+ {
490+ ctx.beginPath();
491+ var points = tracePositions[id];
492+ for (var i=0; i < points.length; i++)
493+ {
494+ var p = points[i];
495+ if (i == 0) {
496+ ctx.moveTo(p.x, p.y);
497+ }
498+ else {
499+ ctx.lineTo(p.x, p.y);
500+ }
501+ }
502+ ctx.lineWidth = 1;
503+ ctx.strokeStyle = "rgba(50, 50, 50, 0.4)";
504+ ctx.stroke();
505+ }
506+
507+ ctx.restore();
508+ }
509+
510+ function clearTraces() {
511+ tracePositions = {};
512+ }
513+
514+ function stopRecording() {
515+ recordingTraces = false;
516+ }
517+
518+ function startRecording() {
519+ recordingTraces = true;
520+ }
521+
522+ function clearTracesAfter(position) {
523+ var newTraces = {};
524+ for (var id in tracePositions) {
525+ newTraces[id] = tracePositions[id].slice(0,position);
526+ }
527+ tracePositions = newTraces;
528+ }
529+
530+ return {
531+ graphicsUpdate: graphicsUpdate,
532+ canvas: tracePathCanvas,
533+ clearTraces: clearTraces,
534+ clearTracesAfter: clearTracesAfter,
535+ stopRecording: stopRecording,
536+ startRecording: startRecording
537+ }
538+}
539+
540+function executeCallbacks(callbacks) {
541+ callbacks.forEach(function(timer) {
542+ if (!timer.executed) {
543+ var func = timer.func;
544+ timer.executed = true;
545+ func();
546+ }
547+ });
548+}
549+
550+function createCanvas() {
551+ var c = document.createElement('canvas');
552+ c.width = window.innerWidth;
553+ c.height = window.innerHeight;
554+ c.style.position = "absolute";
555+ c.style.top = "0";
556+ c.style.left = "0";
557+ c.style.pointerEvents = "none";
558+ return c;
559+}
560+
561+function assert(bool, msg) {
562+ if (!bool) {
563+ throw "Assertion error: " + msg;
564+ }
565+}
566+
567+function snapshotSignalGraph(signalGraphNodes) {
568+ var nodeValues = [];
569+
570+ signalGraphNodes.forEach(function(node) {
571+ nodeValues.push({ value: node.value, id: node.id });
572+ });
573+
574+ return nodeValues;
575+};
576+
577+function restoreSnapshot(signalGraphNodes, snapshot) {
578+ assert(signalGraphNodes.length == snapshot.length,
579+ "saved program state has wrong length");
580+ for (var i=0; i < signalGraphNodes.length; i++) {
581+ var node = signalGraphNodes[i];
582+ var state = snapshot[i];
583+ assert(node.id == state.id, "the nodes moved position");
584+
585+ node.value = state.value;
586+ }
587+}
588+
589+function flattenSignalGraph(nodes) {
590+ var nodesById = {};
591+
592+ function addAllToDict(node) {
593+ nodesById[node.id] = node;
594+ node.kids.forEach(addAllToDict);
595+ }
596+ nodes.forEach(addAllToDict);
597+
598+ var allNodes = Object.keys(nodesById).sort().map(function(key) {
599+ return nodesById[key];
600+ });
601+ return allNodes;
602+};
603+
604+}());
runtime/Init.js+41−3
@@ -31,6 +31,29 @@ function init(display, container, module, ports, moduleToReplace) {
31 // defining state needed for an instance of the Elm RTS
31 // defining state needed for an instance of the Elm RTS
32 var inputs = [];
32 var inputs = [];
33
33
34+ /* OFFSET
35+ * Elm's time traveling debugger lets you interrupt the smooth flow of time
36+ * by pausing and continuing program execution. To ensure the user sees a
37+ * program that moves smoothly through the pause/continue time gap,
38+ * we need to adjsut the value of Date.now().
39+ */
40+ var timer = function() {
41+ var inducedDelay = 0;
42+
43+ var now = function() {
44+ return Date.now() - inducedDelay;
45+ };
46+
47+ var addDelay = function(d) {
48+ inducedDelay += d;
49+ return inducedDelay;
50+ };
51+
52+ return { now : now
53+ , addDelay : addDelay
54+ }
55+ }();
56+
34 var updateInProgress = false;
57 var updateInProgress = false;
35 function notify(id, v) {
58 function notify(id, v) {
36 if (updateInProgress) {
59 if (updateInProgress) {
@@ -40,12 +63,15 @@ function init(display, container, module, ports, moduleToReplace) {
40 'Definitely report this to <https://github.com/elm-lang/Elm/issues>\n');
63 'Definitely report this to <https://github.com/elm-lang/Elm/issues>\n');
41 }
64 }
42 updateInProgress = true;
65 updateInProgress = true;
43− var timestep = Date.now();
66+ var timestep = timer.now();
44 for (var i = inputs.length; i--; ) {
67 for (var i = inputs.length; i--; ) {
45 inputs[i].recv(timestep, id, v);
68 inputs[i].recv(timestep, id, v);
46 }
69 }
47 updateInProgress = false;
70 updateInProgress = false;
48 }
71 }
72+ function setTimeout(func, delay) {
73+ window.setTimeout(func, delay);
74+ }
49
75
50 var listeners = [];
76 var listeners = [];
51 function addListener(relevantInputs, domNode, eventName, func) {
77 function addListener(relevantInputs, domNode, eventName, func) {
@@ -67,11 +93,13 @@ function init(display, container, module, ports, moduleToReplace) {
67 // object. This permits many Elm programs to be embedded per document.
93 // object. This permits many Elm programs to be embedded per document.
68 var elm = {
94 var elm = {
69 notify:notify,
95 notify:notify,
96+ setTimeout:setTimeout,
70 node:container,
97 node:container,
71 display:display,
98 display:display,
72 id:ElmRuntime.guid(),
99 id:ElmRuntime.guid(),
73 addListener:addListener,
100 addListener:addListener,
74 inputs:inputs,
101 inputs:inputs,
102+ timer:timer,
75 ports: { incoming:ports, outgoing:{}, uses:portUses }
103 ports: { incoming:ports, outgoing:{}, uses:portUses }
76 };
104 };
77
105
@@ -84,6 +112,11 @@ function init(display, container, module, ports, moduleToReplace) {
84 return newElm;
112 return newElm;
85 }
113 }
86
114
115+ function dispose() {
116+ removeListeners(listeners);
117+ inputs = [];
118+ }
119+
87 var Module = {};
120 var Module = {};
88 var reportAnyErrors = function() {};
121 var reportAnyErrors = function() {};
89 try {
122 try {
@@ -110,7 +143,11 @@ function init(display, container, module, ports, moduleToReplace) {
110 }
143 }
111
144
112 reportAnyErrors();
145 reportAnyErrors();
146+ return {
147+ swap:swap,
113− return { swap:swap, ports:elm.ports.outgoing };
148+ ports:elm.ports.outgoing,
149+ dispose:dispose
150+ };
114};
151};
115
152
116function checkPorts(elm) {
153function checkPorts(elm) {
@@ -203,8 +240,9 @@ function addReceivers(ports) {
203}
240}
204
241
205function initGraphics(elm, Module) {
242function initGraphics(elm, Module) {
206− if (!('main' in Module))
243+ if (!('main' in Module)) {
207 throw new Error("'main' is missing! What do I display?!");
244 throw new Error("'main' is missing! What do I display?!");
245+ }
208
246
209 var signalGraph = Module.main;
247 var signalGraph = Module.main;
210
248