Skip to content

Commit 9337c0a

Browse files
committed
Add logpoint support
1 parent afa9264 commit 9337c0a

6 files changed

Lines changed: 143 additions & 4 deletions

File tree

src/main/java/com/laytonsmith/core/environments/Breakpoint.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class Breakpoint {
2828
private final ParseTree compiledCondition;
2929
private final int hitCountThreshold;
3030
private int hitCount;
31+
private final String logMessage;
3132

3233
/**
3334
* Creates an unconditional breakpoint at the given file and line.
@@ -36,7 +37,7 @@ public class Breakpoint {
3637
* @param line The 1-indexed line number. Must be positive.
3738
*/
3839
public Breakpoint(File file, int line) {
39-
this(file, line, null, 0);
40+
this(file, line, null, 0, null);
4041
}
4142

4243
/**
@@ -49,9 +50,27 @@ public Breakpoint(File file, int line) {
4950
* @param condition A MethodScript expression to evaluate, or null for unconditional.
5051
* @param hitCountThreshold Number of times the breakpoint must be hit before pausing
5152
* (0 means pause on first hit).
52-
* @throws IllegalArgumentException if the condition cannot be compiled
5353
*/
5454
public Breakpoint(File file, int line, String condition, int hitCountThreshold) {
55+
this(file, line, condition, hitCountThreshold, null);
56+
}
57+
58+
/**
59+
* Creates a breakpoint with optional condition, hit count threshold, and log message.
60+
* If a condition is provided, it is compiled immediately so that syntax
61+
* errors are caught early. If a logMessage is provided, this breakpoint
62+
* acts as a log point: instead of pausing, it logs the message and continues.
63+
* Expressions in {@code {braces}} within the log message are evaluated at hit time.
64+
*
65+
* @param file The source file. Must not be null.
66+
* @param line The 1-indexed line number. Must be positive.
67+
* @param condition A MethodScript expression to evaluate, or null for unconditional.
68+
* @param hitCountThreshold Number of times the breakpoint must be hit before pausing
69+
* (0 means pause on first hit).
70+
* @param logMessage A message to log instead of pausing, or null for a normal breakpoint.
71+
* @throws IllegalArgumentException if the condition cannot be compiled
72+
*/
73+
public Breakpoint(File file, int line, String condition, int hitCountThreshold, String logMessage) {
5574
if(file == null) {
5675
throw new IllegalArgumentException("Breakpoint file must not be null");
5776
}
@@ -74,6 +93,21 @@ public Breakpoint(File file, int line, String condition, int hitCountThreshold)
7493
} else {
7594
this.compiledCondition = null;
7695
}
96+
this.logMessage = logMessage;
97+
}
98+
99+
/**
100+
* Returns the log message template, or null if this is a normal breakpoint.
101+
*/
102+
public String logMessage() {
103+
return logMessage;
104+
}
105+
106+
/**
107+
* Returns true if this is a log point (logs instead of pausing).
108+
*/
109+
public boolean isLogPoint() {
110+
return logMessage != null && !logMessage.isEmpty();
77111
}
78112

79113
/**

src/main/java/com/laytonsmith/core/environments/DebugContext.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public enum ExceptionBreakMode {
8181
private ThreadingMode threadingMode;
8282
private volatile Thread mainThread;
8383

84+
// Log point deduplication: tracks last fired log point to avoid firing
85+
// multiple times for different AST nodes on the same source line.
86+
private File lastLogPointFile;
87+
private int lastLogPointLine = -1;
88+
8489
// Per-thread debug state (step mode, pause latch, etc.)
8590
private final ConcurrentHashMap<Thread, ThreadDebugState> threadStates = new ConcurrentHashMap<>();
8691

@@ -335,6 +340,24 @@ public boolean shouldPause(Target source, int userCallDepth, Environment env) {
335340
} else {
336341
shouldStop = true;
337342
}
343+
if(shouldStop && bp.isLogPoint()) {
344+
// Deduplicate: only fire once per visit to a source line.
345+
// Resets when execution moves to a different line.
346+
if(!source.file().equals(lastLogPointFile) || source.line() != lastLogPointLine) {
347+
lastLogPointFile = source.file();
348+
lastLogPointLine = source.line();
349+
String msg = interpolateLogMessage(bp.logMessage(), env);
350+
listener.onLogPoint(msg);
351+
}
352+
return false;
353+
}
354+
}
355+
356+
// Clear log point dedup when we move to a different line
357+
if(lastLogPointLine != -1
358+
&& (!source.file().equals(lastLogPointFile) || source.line() != lastLogPointLine)) {
359+
lastLogPointFile = null;
360+
lastLogPointLine = -1;
338361
}
339362

340363
if(!shouldStop) {
@@ -400,6 +423,46 @@ private boolean evaluateBreakpointCondition(Breakpoint bp, Environment env) {
400423
return true;
401424
}
402425

426+
/**
427+
* Interpolates a DAP log message template. Expressions in {@code {braces}} are
428+
* evaluated as MethodScript and their string value substituted in.
429+
*/
430+
private String interpolateLogMessage(String template, Environment env) {
431+
StringBuilder sb = new StringBuilder();
432+
int i = 0;
433+
while(i < template.length()) {
434+
char c = template.charAt(i);
435+
if(c == '{') {
436+
int end = template.indexOf('}', i + 1);
437+
if(end == -1) {
438+
sb.append(c);
439+
i++;
440+
} else {
441+
String expr = template.substring(i + 1, end);
442+
try {
443+
Mixed result = MethodScriptCompiler.execute(
444+
MethodScriptCompiler.compile(
445+
MethodScriptCompiler.lex(expr, null, null, true),
446+
null, Environment.getDefaultEnvClasses()),
447+
env, null, null);
448+
while(result instanceof IVariable iv) {
449+
result = env.getEnv(GlobalEnv.class).GetVarList()
450+
.get(iv.getVariableName(), iv.getTarget(), env).ival();
451+
}
452+
sb.append(result.val());
453+
} catch(Exception e) {
454+
sb.append("{").append(expr).append("}");
455+
}
456+
i = end + 1;
457+
}
458+
} else {
459+
sb.append(c);
460+
i++;
461+
}
462+
}
463+
return sb.toString();
464+
}
465+
403466
/**
404467
* Sets the exception break mode.
405468
*

src/main/java/com/laytonsmith/core/environments/DebugListener.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,15 @@ default void onThreadStarted(int dapThreadId, String name) {
5050
default void onThreadExited(int dapThreadId) {
5151
// no-op by default
5252
}
53+
54+
/**
55+
* Called when a log point breakpoint is hit. The message has already been
56+
* interpolated (expressions in {@code {braces}} evaluated).
57+
* Default implementation does nothing.
58+
*
59+
* @param message The interpolated log message
60+
*/
61+
default void onLogPoint(String message) {
62+
// no-op by default
63+
}
5364
}

src/main/java/com/laytonsmith/core/functions/StringHandling.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ public ParseTree optimizeDynamic(Target t, Environment env,
257257
// sconcat only returns a string (except in the special case above) so we need to
258258
// return the string value if it's not already a string
259259
try {
260-
if(InstanceofUtil.isInstanceof(child.getData(), CString.TYPE, env)) {
260+
if(child.isConst() && InstanceofUtil.isInstanceof(child.getData(), CString.TYPE, env)) {
261261
return child;
262262
}
263263
} catch (IllegalArgumentException ex) {

src/main/java/com/laytonsmith/tools/debugger/MSDebugServer.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ public CompletableFuture<Capabilities> initialize(InitializeRequestArguments arg
316316
caps.setSupportsEvaluateForHovers(true);
317317
caps.setSupportsConditionalBreakpoints(true);
318318
caps.setSupportsHitConditionalBreakpoints(true);
319+
caps.setSupportsLogPoints(true);
319320

320321
ExceptionBreakpointsFilter allFilter = new ExceptionBreakpointsFilter();
321322
allFilter.setFilter("all");
@@ -408,6 +409,7 @@ public CompletableFuture<SetBreakpointsResponse> setBreakpoints(SetBreakpointsAr
408409
for(SourceBreakpoint sbp : sourceBreakpoints) {
409410
String condition = sbp.getCondition();
410411
String hitCondition = sbp.getHitCondition();
412+
String logMessage = sbp.getLogMessage();
411413
int hitThreshold = 0;
412414
if(hitCondition != null && !hitCondition.isEmpty()) {
413415
try {
@@ -421,7 +423,7 @@ public CompletableFuture<SetBreakpointsResponse> setBreakpoints(SetBreakpointsAr
421423
bp.setLine(sbp.getLine());
422424
bp.setSource(source);
423425
try {
424-
debugBps.add(new Breakpoint(file, sbp.getLine(), condition, hitThreshold));
426+
debugBps.add(new Breakpoint(file, sbp.getLine(), condition, hitThreshold, logMessage));
425427
bp.setVerified(true);
426428
} catch(IllegalArgumentException e) {
427429
bp.setVerified(false);
@@ -903,5 +905,10 @@ public void onThreadExited(int dapThreadId) {
903905
client.thread(args);
904906
}
905907
}
908+
909+
@Override
910+
public void onLogPoint(String message) {
911+
sendOutput("console", message + "\n");
912+
}
906913
}
907914
}

src/test/java/com/laytonsmith/core/DebugInfrastructureTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ private Mixed executeWithDebugger(String script, DebugContext debugCtx) throws E
9494
*/
9595
private static class TestDebugListener implements DebugListener {
9696
final List<PausedState> pauses = new ArrayList<>();
97+
final List<String> logMessages = new ArrayList<>();
9798
boolean completed = false;
9899
int resumeCount = 0;
99100

@@ -112,6 +113,11 @@ public void onCompleted() {
112113
completed = true;
113114
}
114115

116+
@Override
117+
public void onLogPoint(String message) {
118+
logMessages.add(message);
119+
}
120+
115121
Script.DebugSnapshot snapshot(int index) {
116122
return (Script.DebugSnapshot) pauses.get(index);
117123
}
@@ -648,4 +654,22 @@ private void dapStepOverNewThread(DebugContext.ThreadingMode mode) throws Except
648654
h.awaitStop(2, 5));
649655
}
650656
}
657+
658+
@Test
659+
public void testLogPoint() throws Exception {
660+
String script = "@x = 42\n@y = @x + 1\n@z = @y + 1";
661+
TestDebugListener listener = new TestDebugListener();
662+
DebugContext debugCtx = new DebugContext(listener, DebugContext.ThreadingMode.ASYNCHRONOUS,
663+
Thread.currentThread());
664+
debugCtx.addBreakpoint(new Breakpoint(TEST_FILE, 2, null, 0, "x is {@x}"));
665+
666+
executeWithDebugger(script, debugCtx);
667+
668+
// Log point should not cause a pause
669+
assertEquals(0, listener.pauses.size());
670+
// But should have logged the message
671+
assertEquals(1, listener.logMessages.size());
672+
assertEquals("x is 42", listener.logMessages.get(0));
673+
assertTrue(listener.completed);
674+
}
651675
}

0 commit comments

Comments
 (0)