stack traces in the brush editor
after 35 thousand years it's finally here good erro message
This commit is contained in:
parent
c1612b2a94
commit
e49885c83a
11 changed files with 710 additions and 150 deletions
|
@ -2,6 +2,7 @@
|
|||
|
||||
:root {
|
||||
--color-text: #111;
|
||||
--color-text-dim: #777;
|
||||
--color-error: #db344b;
|
||||
|
||||
--color-brand-blue: #40b1f4;
|
||||
|
@ -190,6 +191,12 @@ pre:has(code) {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Abbreviations */
|
||||
|
||||
abbr {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
|
||||
:root {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CodeEditor } from "rkgk/code-editor.js";
|
||||
import { CodeEditor, Selection } from "rkgk/code-editor.js";
|
||||
import { SaveData } from "rkgk/framework.js";
|
||||
import { builtInPresets } from "rkgk/brush-box.js";
|
||||
|
||||
|
@ -33,6 +33,10 @@ export class BrushEditor extends HTMLElement {
|
|||
|
||||
this.codeEditor = this.appendChild(
|
||||
new CodeEditor([
|
||||
{
|
||||
className: "layer-syntax",
|
||||
render: (code, element) => this.#renderSyntax(code, element),
|
||||
},
|
||||
{
|
||||
className: "layer-error-squiggles",
|
||||
render: (code, element) => this.#renderErrorSquiggles(code, element),
|
||||
|
@ -116,8 +120,32 @@ export class BrushEditor extends HTMLElement {
|
|||
} else if (result.errorKind == "plain") {
|
||||
this.errorHeader.textContent = result.message;
|
||||
} else if (result.errorKind == "exception") {
|
||||
// TODO: This should show a stack trace.
|
||||
this.errorArea.textContent = `an exception occurred: ${result.message}`;
|
||||
let renderer = new ErrorException(result);
|
||||
let squiggles = renderer.prepareSquiggles();
|
||||
|
||||
this.codeEditor.rebuildLineMap();
|
||||
this.errorSquiggles = this.#computeErrorSquiggles(
|
||||
this.codeEditor.lineMap,
|
||||
squiggles.diagnostics,
|
||||
);
|
||||
this.codeEditor.renderLayer("layer-error-squiggles");
|
||||
|
||||
renderer.addEventListener(".functionNameMouseEnter", (event) => {
|
||||
squiggles.highlightSegment(event.frameIndex);
|
||||
});
|
||||
|
||||
renderer.addEventListener(".functionNameMouseLeave", () => {
|
||||
squiggles.highlightSegment(null);
|
||||
});
|
||||
|
||||
renderer.addEventListener(".functionNameClick", (event) => {
|
||||
let span = event.frame.span;
|
||||
this.codeEditor.textArea.focus();
|
||||
this.codeEditor.setSelection(new Selection(span.start, span.end));
|
||||
});
|
||||
|
||||
this.errorArea.replaceChildren();
|
||||
this.errorArea.appendChild(renderer);
|
||||
} else {
|
||||
console.warn(`unknown error kind: ${result.errorKind}`);
|
||||
this.errorHeader.textContent = "(unknown error kind)";
|
||||
|
@ -206,6 +234,10 @@ export class BrushEditor extends HTMLElement {
|
|||
let spanElement = lineElement.appendChild(document.createElement("span"));
|
||||
spanElement.classList.add("squiggle", "squiggle-error");
|
||||
spanElement.textContent = text;
|
||||
|
||||
for (let diagnostic of segment.diagnostics) {
|
||||
diagnostic.customizeSegment?.(diagnostic, spanElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -213,6 +245,151 @@ export class BrushEditor extends HTMLElement {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#renderSyntax(lines, element) {
|
||||
// TODO: Syntax highlighting.
|
||||
// Right now we just render a layer of black text to have the text visible in the code editor.
|
||||
element.textContent = lines.string;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rkgk-brush-editor", BrushEditor);
|
||||
|
||||
class ErrorException extends HTMLElement {
|
||||
constructor(result) {
|
||||
super();
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
prepareSquiggles() {
|
||||
let diagnostics = [];
|
||||
let segments = [];
|
||||
|
||||
const customizeSegment = (diagnostic, segment) => {
|
||||
// The control flow here is kind of spaghetti, but basically this fills out `segments`
|
||||
// as the squiggles are being rendered.
|
||||
// Once squiggles are rendered, you're free to call highlightSegment(i).
|
||||
segments.push({
|
||||
frameIndex: diagnostic.frameIndex,
|
||||
colorIndex: diagnostic.colorIndex,
|
||||
segment,
|
||||
});
|
||||
if (diagnostic.colorIndex != null) {
|
||||
segment.classList.add("squiggle-colored");
|
||||
segment.style.setProperty("--color-index", diagnostic.colorIndex);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < this.result.stackTrace.length; ++i) {
|
||||
let frame = this.result.stackTrace[i];
|
||||
if (frame.span != null) {
|
||||
diagnostics.push({
|
||||
start: frame.span.start,
|
||||
end: frame.span.end,
|
||||
|
||||
customizeSegment,
|
||||
frameIndex: i,
|
||||
colorIndex: i / this.result.stackTrace.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
diagnostics,
|
||||
highlightSegment(frameIndex) {
|
||||
let justHighlighted = new Set();
|
||||
for (let entry of segments.values()) {
|
||||
let segment = entry.segment;
|
||||
if (entry.frameIndex == frameIndex) {
|
||||
segment.classList.add("squiggle-highlighted");
|
||||
// This logic exists so that a specific segment can display on top of
|
||||
// another one if it's highlighted.
|
||||
segment.style.setProperty("--highlight-color-index", entry.colorIndex);
|
||||
justHighlighted.add(segment);
|
||||
} else if (!justHighlighted.has(segment)) {
|
||||
segment.classList.remove("squiggle-highlighted");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
let message = this.appendChild(document.createElement("p"));
|
||||
message.classList.add("message");
|
||||
message.textContent = this.result.message;
|
||||
|
||||
let stackTrace = this.appendChild(document.createElement("ol"));
|
||||
stackTrace.classList.add("stack-trace");
|
||||
for (let i = 0; i < this.result.stackTrace.length; ++i) {
|
||||
let frame = this.result.stackTrace[i];
|
||||
let line = stackTrace.appendChild(document.createElement("li"));
|
||||
|
||||
let [_, name, lambdasString] = frame.functionName.match(/([^λ]+)(λ*)/);
|
||||
|
||||
let inCaption = line.appendChild(document.createElement("span"));
|
||||
inCaption.classList.add("in");
|
||||
inCaption.innerText = "in ";
|
||||
|
||||
let functionName = line.appendChild(document.createElement("button"));
|
||||
functionName.classList.add("function-name");
|
||||
functionName.disabled = true;
|
||||
functionName.innerText = name;
|
||||
if (name == "(brush)") {
|
||||
functionName.classList.add("function-name-brush");
|
||||
functionName.title = "Top-level brush code";
|
||||
}
|
||||
let lambdas = functionName.appendChild(document.createElement("abbr"));
|
||||
lambdas.classList.add("lambdas");
|
||||
lambdas.textContent = lambdasString;
|
||||
lambdas.title =
|
||||
"λ - unnamed function\n" +
|
||||
"Symbolizes functions that were not given a name.\n" +
|
||||
"Each additional λ is one level of nesting inside another function.";
|
||||
|
||||
if (frame.isSystem) {
|
||||
line.append(" ");
|
||||
let system = line.appendChild(document.createElement("abbr"));
|
||||
system.classList.add("system");
|
||||
system.innerText = "(built-in)";
|
||||
system.title =
|
||||
"This function is built into rakugaki and does not exist in your brush's source code, so you can't see it in the editor.";
|
||||
}
|
||||
|
||||
if (frame.span != null) {
|
||||
functionName.disabled = false;
|
||||
functionName.classList.add("source-link");
|
||||
functionName.style.setProperty("--color-index", i / this.result.stackTrace.length);
|
||||
|
||||
functionName.addEventListener("mouseenter", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameMouseEnter"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
functionName.addEventListener("mouseleave", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameMouseLeave"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
functionName.addEventListener("click", () => {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event(".functionNameClick"), {
|
||||
frameIndex: i,
|
||||
frame,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rkgk-brush-editor-error-exception", ErrorException);
|
||||
|
|
|
@ -244,7 +244,7 @@ export class Haku {
|
|||
w2.haku2_vm_destroy(this.#pVm2);
|
||||
w2.haku2_defs_destroy(this.#pDefs2);
|
||||
w2.haku2_limits_destroy(this.#pLimits2);
|
||||
w2.haku_dealloc(this.#bytecode2.ptr);
|
||||
freeString2(this.#bytecode2);
|
||||
}
|
||||
|
||||
setBrush(code) {
|
||||
|
@ -332,12 +332,49 @@ export class Haku {
|
|||
return exn;
|
||||
}
|
||||
|
||||
#stackTrace() {
|
||||
let count = w2.haku2_vm_stackframe_count(this.#pVm2);
|
||||
let trace = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
let isSystem = w2.haku2_vm_stackframe_is_system(this.#pVm2, i) != 0;
|
||||
let pc = w2.haku2_vm_stackframe_pc(this.#pVm2, i);
|
||||
|
||||
let span = null;
|
||||
if (!isSystem && pc > 0) {
|
||||
// NOTE: We find the span at (pc - 1), because stack frames' program counters are
|
||||
// situated at where control flow _ought_ to return, and not where the function call
|
||||
// is located. Therefore, to pull the program counter back into the function call,
|
||||
// we subtract 1.
|
||||
let pSpan = w.haku_bytecode_find_span(this.#pInstance, Math.max(0, pc - 1));
|
||||
if (pSpan != 0) {
|
||||
span = {
|
||||
start: w.haku_span_start(pSpan),
|
||||
end: w.haku_span_end(pSpan),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
trace.push({
|
||||
isSystem,
|
||||
pc,
|
||||
span,
|
||||
functionName: readString(
|
||||
memory2,
|
||||
w2.haku2_vm_stackframe_function_name_len(this.#pVm2, i),
|
||||
w2.haku2_vm_stackframe_function_name(this.#pVm2, i),
|
||||
),
|
||||
});
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
#exceptionResult() {
|
||||
return {
|
||||
status: "error",
|
||||
errorKind: "exception",
|
||||
description: "Runtime error",
|
||||
message: this.#exceptionMessage(),
|
||||
stackTrace: this.#stackTrace(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* index.css - styles for index.html and generally main parts of the app
|
||||
For shared styles (such as color definitions) check out base.css. */
|
||||
|
||||
* {
|
||||
html {
|
||||
/* On the main page, we don't really want to permit selecting things.
|
||||
It comes off as janky-looking. */
|
||||
user-select: none;
|
||||
|
@ -412,22 +412,47 @@ rkgk-code-editor {
|
|||
|
||||
& > .squiggle-error {
|
||||
text-decoration-color: var(--color-error);
|
||||
|
||||
&.squiggle-colored {
|
||||
--color: oklch(40% 100% calc(var(--color-index) * 300));
|
||||
text-decoration-color: var(--color);
|
||||
background-color: oklch(from var(--color) 60% c h / 0.13);
|
||||
|
||||
&.squiggle-highlighted {
|
||||
--highlight-color-index: var(--color-index);
|
||||
text-decoration: none;
|
||||
background-color: oklch(
|
||||
40% 100% calc(var(--highlight-color-index) * 300)
|
||||
);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .layer-syntax {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
& > textarea {
|
||||
display: block;
|
||||
width: calc(100% - var(--gutter-width));
|
||||
|
||||
margin: 0;
|
||||
margin-left: var(--gutter-width);
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
|
||||
white-space: pre-wrap;
|
||||
border: none;
|
||||
|
||||
background: none;
|
||||
color: transparent;
|
||||
caret-color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
/* The outline is displayed on the parent element to also surround the gutter. */
|
||||
|
@ -490,6 +515,67 @@ rkgk-brush-editor.rkgk-panel {
|
|||
margin: 0;
|
||||
color: var(--color-error);
|
||||
white-space: pre-wrap;
|
||||
|
||||
user-select: text;
|
||||
|
||||
max-height: 20em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
rkgk-brush-editor-error-exception {
|
||||
& > .message {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
& > .stack-trace {
|
||||
margin: 0;
|
||||
margin-left: 4ch;
|
||||
|
||||
color: var(--color-text);
|
||||
|
||||
& > li {
|
||||
&::marker {
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
& > button.function-name {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
|
||||
user-select: text;
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
&:not(:disabled) {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
&.source-link {
|
||||
--color-index: 0; /* set by JavaScript */
|
||||
--color: oklch(40% 100% calc(var(--color-index) * 300));
|
||||
|
||||
color: var(--color);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .system {
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue