diff --git a/Public/index.js b/Public/index.js
index 88dfc25..84328f0 100644
--- a/Public/index.js
+++ b/Public/index.js
@@ -11,4 +11,4 @@ import "./js/misc/icons";
import "bootstrap";
import { App } from "./js/app";
-const app = new App();
+new App();
diff --git a/Public/js/app.js b/Public/js/app.js
index 3b570b8..892679f 100644
--- a/Public/js/app.js
+++ b/Public/js/app.js
@@ -4,11 +4,8 @@ import { ExpressionField } from "./views/expression_field";
import { MatchOptions } from "./views/match_options";
import { TestEditor } from "./views/test_editor";
import { DSLView } from "./views/dsl_view";
-
import { DSLEditor } from "./views/dsl_editor";
-
-import { DSLHighlighter } from "./views/dsl_highlighter";
-
+import { DebuggerText } from "./views/debugger_text";
import { Runner } from "./runner";
export class App {
@@ -23,12 +20,6 @@ export class App {
this.expressionField.addEventListener("change", () =>
this.onExpressionFieldChange()
);
- this.expressionField.addEventListener("hover", () => {
- this.onExpressionFieldHover();
- });
- this.expressionField.addEventListener("unhover", () => {
- this.onExpressionFieldUnhover();
- });
this.matchOptions = new MatchOptions();
this.matchOptions.addEventListener("change", () =>
@@ -42,15 +33,56 @@ export class App {
this.onPatternTestEditorChange()
);
+ this.debuggerText = new DebuggerText(
+ document.getElementById("debugger-text-container")
+ );
+
+ this.debuggerGoStartButton = document.getElementById("debugger-go-start");
+ this.debuggerGoStartButton.addEventListener("click", () => {
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.value = 1;
+ this.onDebuggerStepChange();
+ });
+
+ this.debuggerStepBackwardButton = document.getElementById(
+ "debugger-step-backward"
+ );
+ this.debuggerStepBackwardButton.addEventListener("click", () => {
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.value = Math.max(1, parseInt(matchStepRange.value) - 1);
+ this.onDebuggerStepChange();
+ });
+
+ this.debuggerStepForwardButton = document.getElementById(
+ "debugger-step-forward"
+ );
+ this.debuggerStepForwardButton.addEventListener("click", () => {
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.value = Math.min(
+ parseInt(matchStepRange.value) + 1,
+ parseInt(matchStepRange.max)
+ );
+ this.onDebuggerStepChange();
+ });
+
+ this.debuggerGoEndButton = document.getElementById("debugger-go-end");
+ this.debuggerGoEndButton.addEventListener("click", () => {
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.value = matchStepRange.max;
+ this.onDebuggerStepChange();
+ });
+
+ this.debuggerModal = document.getElementById("debugger-modal");
+ this.debuggerModal.addEventListener("shown.bs.modal", () =>
+ this.launchDebugger()
+ );
+
this.dslView = new DSLView(document.getElementById("dsl-view-container"));
this.runner = new Runner();
this.runner.onready = this.onRunnerReady.bind(this);
this.runner.onresponse = this.onRunnerResponse.bind(this);
- this.dslHighlighter = new DSLHighlighter(this.dslView.editor);
- this.dslTokens = [];
-
this.stateProxy = {
builder: "",
text2: "",
@@ -105,8 +137,6 @@ export class App {
const expressionField = this.expressionField;
const matchOptions = this.matchOptions;
const patternTestEditor = this.patternTestEditor;
- const dslEditor = this.dslEditor;
- const builderTestEditor = this.builderTestEditor;
if (expressionField) {
expressionField.value = e.data.value.pattern;
@@ -117,16 +147,6 @@ export class App {
if (patternTestEditor) {
patternTestEditor.value = e.data.value.text1;
}
- if (dslEditor) {
- dslEditor.value = e.data.value.builder;
- } else {
- this.stateProxy.builder = DSLEditor.defaultValue;
- }
- if (builderTestEditor) {
- builderTestEditor.value = e.data.value.text2;
- } else {
- this.stateProxy.text2 = TestEditor.defaultValue;
- }
}
};
}
@@ -139,8 +159,6 @@ export class App {
const expressionField = this.expressionField;
const matchOptions = this.matchOptions;
const patternTestEditor = this.patternTestEditor;
- const dslEditor = this.dslEditor;
- const builderTestEditor = this.builderTestEditor;
this.stateRestorationWorker.postMessage({
type: "encode",
@@ -148,8 +166,6 @@ export class App {
pattern: expressionField ? expressionField.value : "",
options: matchOptions ? matchOptions.value : [],
text1: patternTestEditor ? patternTestEditor.value : "",
- builder: dslEditor ? dslEditor.value : "",
- text2: builderTestEditor ? builderTestEditor.value : "",
},
});
}
@@ -176,6 +192,32 @@ export class App {
}
}
+ launchDebugger() {
+ const expressionField = this.expressionField;
+ const patternTestEditor = this.patternTestEditor;
+
+ const expression = expressionField.value;
+ const text = patternTestEditor.value;
+
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.value = 1;
+ matchStepRange.min = 1;
+
+ const matchStep = document.getElementById("debugger-match-step");
+ matchStep.textContent = "1";
+
+ const debuggerPattern = document.getElementById("debugger-regex");
+ debuggerPattern.textContent = expression;
+
+ this.debuggerText.value = text;
+
+ this.onDebuggerStepChange();
+
+ matchStepRange.addEventListener("input", (e) => {
+ this.onDebuggerStepChange();
+ });
+ }
+
onExpressionFieldChange() {
if (!this.expressionField.value) {
this.updateMatchCount(0, "match-count");
@@ -223,35 +265,6 @@ export class App {
}
}
- onExpressionFieldHover() {
- const related = this.expressionField.hoverToken.related;
- const location =
- (related && related.location) || this.expressionField.hoverToken.location;
- const tokens = this.dslTokens
- .filter(
- (token) =>
- token.patternLocation.start <= location.start &&
- token.patternLocation.end >= location.end
- )
- .map((token) => {
- return {
- ...token,
- sourceLength: token.sourceLocation.end - token.sourceLocation.start,
- patternLength:
- token.patternLocation.end - token.patternLocation.start,
- };
- });
- tokens.sort(
- (a, b) =>
- a.patternLength - b.patternLength || a.sourceLength - b.sourceLength
- );
- this.dslHighlighter.draw(tokens.slice(0, 1));
- }
-
- onExpressionFieldUnhover() {
- this.dslHighlighter.clear();
- }
-
onMatchOptionsChange() {
this.onPatternTestEditorChange();
}
@@ -285,8 +298,32 @@ export class App {
this.encodeState();
}
- onDSLEditorChange() {
- this.encodeState();
+ onDebuggerStepChange() {
+ const method = "debug";
+ const params = {
+ method,
+ pattern: document.getElementById("debugger-regex").textContent,
+ text: this.debuggerText.value,
+ matchOptions: this.matchOptions.value,
+ step: document.getElementById("debugger-step-range").value,
+ };
+
+ if (this.runner.isReady) {
+ this.runner.run(params);
+ } else {
+ const headers = {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ };
+ const body = JSON.stringify(params);
+ fetch(`/api/rest/${method}`, { method: "POST", headers, body })
+ .then((response) => {
+ return response.json();
+ })
+ .then((response) => {
+ this.onRunnerResponse(response);
+ });
+ }
}
onRunnerReady() {
@@ -324,14 +361,57 @@ export class App {
}
this.patternTestEditor.error = response.error;
break;
- case "parseDSL":
+ case "debug":
if (response.result) {
- const tokens = JSON.parse(response.result);
- this.dslTokens = tokens;
- } else {
- this.dslTokens = [];
+ const metrics = JSON.parse(response.result);
+
+ const matchStep = document.getElementById("debugger-match-step");
+ matchStep.textContent = metrics.step;
+
+ const matchStepRange = document.getElementById("debugger-step-range");
+ matchStepRange.max = metrics.stepCount;
+
+ const instructions = document.getElementById("debugger-instructions");
+ instructions.innerHTML = "";
+
+ metrics.instructions.forEach((instruction, i) => {
+ const tr = document.createElement("tr");
+
+ if (i === metrics.programCounter) {
+ tr.classList.add("table-primary");
+ }
+
+ const programCounter = document.createElement("td");
+ programCounter.style =
+ "width: 1%; text-align: right; white-space: nowrap; padding-left: 1em; padding-right: 1em;";
+ programCounter.textContent = i + 1;
+ tr.appendChild(programCounter);
+
+ const inst = document.createElement("td");
+ inst.style = "white-space: nowrap;";
+ inst.textContent = instruction;
+ tr.appendChild(inst);
+ instructions.appendChild(tr);
+ });
+
+ const totalCycleCount = document.getElementById(
+ "debugger-total-cycle-count"
+ );
+ totalCycleCount.textContent = metrics.totalCycleCount;
+
+ const resets = document.getElementById("debugger-resets");
+ resets.textContent = metrics.resets;
+
+ const backtracks = document.getElementById("debugger-backtracks");
+
+ const previousBacktracks = backtracks.textContent;
+ backtracks.textContent = metrics.backtracks;
+
+ this.debuggerText.highlighter.draw(
+ metrics.traces,
+ previousBacktracks < metrics.backtracks ? metrics.failure : null
+ );
}
- this.dslView.error = response.error;
break;
}
}
diff --git a/Public/js/misc/icons.js b/Public/js/misc/icons.js
index 9ac7c75..5b7898a 100644
--- a/Public/js/misc/icons.js
+++ b/Public/js/misc/icons.js
@@ -5,6 +5,10 @@ import {
faFlag,
faOctagonXmark,
faHeart,
+ faBackwardStep,
+ faForwardStep,
+ faCaretLeft,
+ faCaretRight,
} from "@fortawesome/pro-solid-svg-icons";
import {
faAt,
@@ -20,6 +24,11 @@ library.add(
faOctagonXmark,
faHeart,
+ faBackwardStep,
+ faForwardStep,
+ faCaretLeft,
+ faCaretRight,
+
faAt,
faCheck,
faCommentAltSmile,
diff --git a/Public/js/views/debugger_highlighter.js b/Public/js/views/debugger_highlighter.js
new file mode 100644
index 0000000..321c8a9
--- /dev/null
+++ b/Public/js/views/debugger_highlighter.js
@@ -0,0 +1,90 @@
+"use strict";
+
+import Editor from "./editor";
+
+export default class DebuggerHighlighter {
+ constructor(editor) {
+ this.editor = editor;
+ this.activeMarks = [];
+ this.widgets = [];
+ }
+
+ draw(traces, backtrack) {
+ this.clear();
+
+ const editor = this.editor;
+ editor.operation(() => {
+ const doc = editor.getDoc();
+ const marks = this.activeMarks;
+
+ for (const trace of traces) {
+ const className = "debuggermatch";
+ const location = Editor.calcRangePos(
+ this.editor,
+ trace.location.start,
+ trace.location.end - trace.location.start
+ );
+ marks.push(
+ doc.markText(location.startPos, location.endPos, {
+ className: className,
+ })
+ );
+
+ if (trace.location.start === trace.location.end) {
+ const pos = doc.posFromIndex(trace.location.start);
+
+ const widget = document.createElement("span");
+ widget.className = className;
+
+ widget.style.position = "absolute";
+ widget.style.zIndex = "10";
+ widget.style.height = editor.defaultTextHeight() * 1.5 + "px";
+ widget.style.width = "1px";
+
+ const coords = editor.charCoords(pos, "local");
+ widget.style.left = coords.left + "px";
+ widget.style.top = coords.top + 2 + "px";
+
+ editor.getWrapperElement().appendChild(widget);
+
+ this.widgets.push(widget);
+ }
+ }
+
+ if (backtrack) {
+ const pos = doc.posFromIndex(backtrack.start);
+
+ const widget = document.createElement("span");
+ widget.className = "debuggerbacktrack";
+
+ widget.style.position = "absolute";
+ widget.style.zIndex = "10";
+ widget.style.height = editor.defaultTextHeight() * 1.5 + "px";
+ widget.style.width = editor.defaultCharWidth() + "px";
+
+ const coords = editor.charCoords(pos, "local");
+ widget.style.left = coords.left + "px";
+ widget.style.top = coords.top + 2 + "px";
+
+ editor.getWrapperElement().appendChild(widget);
+
+ this.widgets.push(widget);
+ }
+ });
+ }
+
+ clear() {
+ this.editor.operation(() => {
+ let marks = this.activeMarks;
+ for (var i = 0, l = marks.length; i < l; i++) {
+ marks[i].clear();
+ }
+ marks.length = 0;
+
+ for (const widget of this.widgets) {
+ widget.remove();
+ }
+ this.widgets.length = 0;
+ });
+ }
+}
diff --git a/Public/js/views/debugger_text.js b/Public/js/views/debugger_text.js
new file mode 100644
index 0000000..e769d97
--- /dev/null
+++ b/Public/js/views/debugger_text.js
@@ -0,0 +1,35 @@
+"use strict";
+
+import Editor from "./editor";
+import DebuggerHighlighter from "./debugger_highlighter";
+
+export class DebuggerText {
+ constructor(container) {
+ this.container = container;
+ this.init(container);
+ }
+
+ get value() {
+ return this.editor.getValue();
+ }
+
+ set value(val) {
+ this.editor.setValue(val);
+ }
+
+ init(container) {
+ const editor = Editor.create(
+ container,
+ {
+ lineWrapping: true,
+ screenReaderLabel: "Debugger Test View",
+ readOnly: true,
+ },
+ "100%",
+ "100%"
+ );
+ this.editor = editor;
+
+ this.highlighter = new DebuggerHighlighter(editor);
+ }
+}
diff --git a/Public/scss/default.scss b/Public/scss/default.scss
index 66e78f9..9cbcb5e 100644
--- a/Public/scss/default.scss
+++ b/Public/scss/default.scss
@@ -1,3 +1,5 @@
+$table-cell-padding-y-sm: 0.05rem;
+
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@@ -11,9 +13,12 @@
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
+@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
-@import "bootstrap/scss/nav";
+@import "bootstrap/scss/close";
+@import "bootstrap/scss/modal";
+@import "bootstrap/scss/helpers";
@import "bootstrap/scss/utilities/api";
diff --git a/Sources/App/Debugger/Context.swift b/Sources/App/Debugger/Context.swift
new file mode 100644
index 0000000..e436093
--- /dev/null
+++ b/Sources/App/Debugger/Context.swift
@@ -0,0 +1,41 @@
+import Foundation
+
+extension Debugger {
+ class Context {
+ static let shared = Context()
+
+ var instructions: [String] = []
+ var programCounter = 0
+
+ var stepCount = 0
+ var breakPoint: Int?
+
+ var start: Int = 0
+ var current: Int = 0
+ var failurePosition: Int = 0
+
+ var totalCycleCount = 0
+ var resets = 0
+ var backtracks = 0
+
+ private init(stepCount: Int = 0, breakPoint: Int? = nil) {
+ self.stepCount = stepCount
+ self.breakPoint = breakPoint
+ }
+
+ func reset() {
+ instructions = []
+ programCounter = 0
+
+ stepCount = 0
+ breakPoint = nil
+
+ start = 0
+ current = 0
+
+ totalCycleCount = 0
+ resets = 0
+ backtracks = 0
+ }
+ }
+}
diff --git a/Sources/App/Debugger/Debugger.swift b/Sources/App/Debugger/Debugger.swift
new file mode 100644
index 0000000..84232c1
--- /dev/null
+++ b/Sources/App/Debugger/Debugger.swift
@@ -0,0 +1,86 @@
+import Foundation
+@testable import _RegexParser
+@testable @_spi(RegexBenchmark) import _StringProcessing
+
+struct Debugger {
+ func run(pattern: String, text: String, matchingOptions: [String] = []) throws {
+ let ast = try _RegexParser.parse(pattern, .traditional)
+
+ var sequence = [AST.MatchingOption]()
+ if matchingOptions.contains("i") {
+ sequence.append(.init(.caseInsensitive, location: .fake))
+ }
+ if matchingOptions.contains("s") {
+ sequence.append(.init(.singleLine, location: .fake))
+ }
+ if matchingOptions.contains("asciiOnlyWordCharacters") {
+ sequence.append(.init(.asciiOnlyWord, location: .fake))
+ }
+ if matchingOptions.contains("asciiOnlyDigits") {
+ sequence.append(.init(.asciiOnlyDigit, location: .fake))
+ }
+ if matchingOptions.contains("asciiOnlyWhitespace") {
+ sequence.append(.init(.asciiOnlySpace, location: .fake))
+ }
+ if matchingOptions.contains("asciiOnlyCharacterClasses") {
+ sequence.append(.init(.asciiOnlyPOSIXProps, location: .fake))
+ }
+ sequence.append(.init(.graphemeClusterSemantics, location: .fake))
+
+ var options = MatchingOptions()
+ options.apply(AST.MatchingOptionSequence(adding: sequence))
+
+ let program = try compile(ast, options: options)
+
+ Debugger.Context.shared.instructions = program.instructions.map {
+ $0.description
+ }
+
+ let inputRange = text.startIndex..._firstMatch(
+ program,
+ using: &cpu
+ )
+ } catch {}
+ }
+
+ func compile(_ ast: AST, options: MatchingOptions) throws -> MEProgram {
+ let compiler = Compiler(tree: ast.dslTree, compileOptions: [.enableMetrics])
+ compiler.options = options
+ return try compiler.emit()
+ }
+
+ struct Metrics: Codable {
+ var instructions: [String]
+ var programCounter: Int
+
+ var stepCount: Int
+ var step: Int
+
+ var totalCycleCount: Int
+ var resets: Int
+ var backtracks: Int
+
+ var traces: [Trace]
+ var failure: Location
+ }
+
+ struct Trace: Codable {
+ let location: Location
+ }
+
+ struct Location: Codable {
+ let start: Int
+ let end: Int
+ }
+}
diff --git a/Sources/App/Debugger/Executor.swift b/Sources/App/Debugger/Executor.swift
new file mode 100644
index 0000000..01b6145
--- /dev/null
+++ b/Sources/App/Debugger/Executor.swift
@@ -0,0 +1,155 @@
+@testable import _RegexParser
+@testable import _StringProcessing
+
+enum Executor