Skip to content

Commit 0cf2f6c

Browse files
committed
Expand set of eligible symbol characters. Introduce "call traces" to runtime exceptions. New procedures set-max-call-stack!" and call-stack-procedures for library (lispkit debug). Support wait argument for procedure thread-terminate in library (lispkit thread)`.
1 parent 9808e26 commit 0cf2f6c

9 files changed

+178
-31
lines changed

Sources/LispKit/Compiler/RuntimeError.swift

+59-12
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
3434
public let irritants: [Expr]
3535
public private(set) var library: Expr?
3636
public private(set) var stackTrace: [Procedure]?
37+
public private(set) var callTrace: [String]?
3738

3839
internal init(_ pos: SourcePosition,
3940
_ descriptor: ErrorDescriptor,
4041
_ irritants: [Expr],
41-
_ stackTrace: [Procedure]? = nil) {
42+
stackTrace: [Procedure]? = nil,
43+
callTrace: [String]? = nil) {
4244
self.pos = pos
4345
self.descriptor = descriptor
4446
self.irritants = irritants
45-
self.stackTrace = nil
47+
self.stackTrace = stackTrace
48+
self.callTrace = callTrace
4649
}
4750

4851
public class func lexical(_ error: LexicalError,
@@ -129,15 +132,25 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
129132
}
130133

131134
public class func abortion(at pos: SourcePosition = SourcePosition.unknown,
132-
stackTrace: [Procedure]? = nil) -> RuntimeError {
133-
return RuntimeError(pos, ErrorDescriptor.abortion, [], stackTrace)
135+
stackTrace: [Procedure]? = nil,
136+
callTrace: [String]? = nil) -> RuntimeError {
137+
return RuntimeError(pos,
138+
ErrorDescriptor.abortion,
139+
[],
140+
stackTrace: stackTrace,
141+
callTrace: callTrace)
134142
}
135143

136144
public class func uncaught(_ expr: Expr,
137145
at pos: SourcePosition = SourcePosition.unknown,
138-
stackTrace: [Procedure]? = nil) -> RuntimeError {
146+
stackTrace: [Procedure]? = nil,
147+
callTrace: [String]? = nil) -> RuntimeError {
139148
// TODO: figure out if we want uncaught of uncaught exceptions?
140-
return RuntimeError(pos, ErrorDescriptor.uncaught, [expr], stackTrace)
149+
return RuntimeError(pos,
150+
ErrorDescriptor.uncaught,
151+
[expr],
152+
stackTrace: stackTrace,
153+
callTrace: callTrace)
141154
}
142155

143156
public class func custom(_ kind: String,
@@ -148,7 +161,29 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
148161
}
149162

150163
public func at(_ pos: SourcePosition) -> RuntimeError {
151-
return RuntimeError(pos, self.descriptor, self.irritants, self.stackTrace)
164+
return RuntimeError(pos,
165+
self.descriptor,
166+
self.irritants,
167+
stackTrace: self.stackTrace,
168+
callTrace: self.callTrace)
169+
}
170+
171+
@discardableResult public func attach(callTrace: [String]?) -> RuntimeError {
172+
if self.callTrace == nil {
173+
self.callTrace = callTrace
174+
}
175+
return self
176+
}
177+
178+
@discardableResult public func attach(vm: VirtualMachine,
179+
current: Procedure? = nil) -> RuntimeError {
180+
if self.stackTrace == nil {
181+
self.stackTrace = vm.getStackTrace(current: current)
182+
}
183+
if self.callTrace == nil {
184+
self.callTrace = vm.getCallTraceInfo(current: current)
185+
}
186+
return self
152187
}
153188

154189
@discardableResult public func attach(stackTrace: [Procedure]) -> RuntimeError {
@@ -212,7 +247,7 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
212247
irritantSeparator: String = ", ",
213248
positionHeader: String? = "\nat: ",
214249
libraryHeader: String? = "\nlibrary: ",
215-
stackTraceHeader: String? = "\nstack trace: ",
250+
stackTraceHeader: String? = "\ncall trace: ",
216251
stackTraceSeparator: String = ", ") -> String {
217252
if self.descriptor == .uncaught,
218253
self.irritants.count == 1,
@@ -269,14 +304,26 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
269304
let libraryName = self.library?.description {
270305
builder.append("\(libraryHeader)\(libraryName)")
271306
}
272-
if let stackTraceHeader = stackTraceHeader,
273-
let stackTrace = self.stackTrace {
307+
if let stackTraceHeader = stackTraceHeader, self.callTrace != nil || self.stackTrace != nil {
274308
builder = StringBuilder(prefix: builder.description,
275309
postfix: "",
276310
separator: stackTraceSeparator,
277311
initial: stackTraceHeader)
278-
for proc in stackTrace {
279-
builder.append(proc.name)
312+
if let callTrace = self.callTrace {
313+
for call in callTrace {
314+
builder.append(call)
315+
}
316+
if let stackTrace = self.stackTrace, stackTrace.count > callTrace.count {
317+
if stackTrace.count == callTrace.count + 1 {
318+
builder.append("+1 call")
319+
} else {
320+
builder.append("+\(stackTrace.count - callTrace.count) calls")
321+
}
322+
}
323+
} else if let stackTrace = self.stackTrace {
324+
for proc in stackTrace {
325+
builder.append(proc.name)
326+
}
280327
}
281328
}
282329
return builder.description

Sources/LispKit/Compiler/Scanner.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ let UHEXDIGITS = CharacterSet(charactersIn: "ABCDEF")
11031103
let LETTERS = CharacterSet.letters
11041104
let UPPER_LETTERS = CharacterSet.uppercaseLetters
11051105
let LOWER_LETTERS = CharacterSet.lowercaseLetters
1106-
let INITIALS = CharacterSet(charactersIn: "!$%&*/:<=>?^_~@")
1106+
let INITIALS = CharacterSet(charactersIn: "!$%&*/:<=>?^_~@…⋯€→←↔︎︎≥≤«»≣≈∪∩∅∞≠")
11071107
let SUBSEQUENTS = CharacterSet(charactersIn: "+-.")
11081108
let SIGNSUBSEQUENTS = CharacterSet(charactersIn: "+-")
11091109

Sources/LispKit/Primitives/DebugLibrary.swift

+20-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public final class DebugLibrary: NativeLibrary {
5454
self.define(Procedure("loaded-sources", self.loadedSources))
5555
self.define(Procedure("environment-info", self.environmentInfo))
5656
self.define(Procedure("stack-size", self.stackSize))
57+
self.define(Procedure("set-max-call-stack!", self.setMaxCallStack))
58+
self.define(Procedure("call-stack-procedures", self.callStackProcedures))
5759
self.define(Procedure("call-stack-trace", self.callStackTrace))
5860
self.define(Procedure("internal-call-stack", self.internalCallStack))
5961
}
@@ -275,11 +277,27 @@ public final class DebugLibrary: NativeLibrary {
275277
return .makeNumber(self.context.evaluator.machine.sp)
276278
}
277279

278-
private func callStackTrace() -> Expr {
280+
private func setMaxCallStack(_ expr: Expr) throws -> Expr {
281+
self.context.evaluator.maxCallStack = try expr.asInt(above: 0, below: 1000)
282+
return .void
283+
}
284+
285+
private func callStackProcedures() -> Expr {
279286
let procs = self.context.evaluator.machine.getStackTrace()
280287
var res: Expr = .null
281288
for proc in procs {
282-
res = .pair(.symbol(self.context.symbols.intern(proc.name)), res)
289+
res = .pair(.procedure(proc), res)
290+
}
291+
return res
292+
}
293+
294+
private func callStackTrace() -> Expr {
295+
guard let exprs = self.context.evaluator.machine.getCallTrace(cap: 1000) else {
296+
return .false
297+
}
298+
var res: Expr = .null
299+
for expr in exprs {
300+
res = .pair(expr, res)
283301
}
284302
return res
285303
}

Sources/LispKit/Primitives/DynamicControlLibrary.swift

+16-6
Original file line numberDiff line numberDiff line change
@@ -346,22 +346,30 @@ public final class DynamicControlLibrary: NativeLibrary {
346346
if !stackTrace.isEmpty {
347347
stackTrace.removeFirst()
348348
}
349+
var callTrace = self.context.evaluator.machine.getCallTraceInfo()
350+
if callTrace != nil && callTrace!.count > 0 {
351+
callTrace!.removeFirst()
352+
}
349353
return .error(RuntimeError.custom("error",
350354
message.unescapedDescription,
351-
Array(irritants.toExprs().0)).attach(stackTrace: stackTrace))
355+
Array(irritants.toExprs().0))
356+
.attach(stackTrace: stackTrace)
357+
.attach(callTrace: callTrace))
352358
}
353359

354360
private func makeAssertionError(procName: Expr, expr: Expr) throws -> Expr {
355361
guard case .string(_) = procName else {
356362
throw RuntimeError.type(procName, expected: [.strType])
357363
}
358364
return .error(RuntimeError.eval(.assertion, procName, expr)
359-
.attach(stackTrace: self.context.evaluator.machine.getStackTrace()))
365+
.attach(vm: self.context.evaluator.machine))
360366
}
361367

362368
private func makeUncaught(expr: Expr) -> Expr {
363-
return .error(RuntimeError.uncaught(expr,
364-
stackTrace: self.context.evaluator.machine.getStackTrace()))
369+
return .error(RuntimeError.uncaught(
370+
expr,
371+
stackTrace: self.context.evaluator.machine.getStackTrace(),
372+
callTrace: self.context.evaluator.machine.getCallTraceInfo()))
365373
}
366374

367375
private func error(args: Arguments) throws -> (Procedure, Exprs) {
@@ -372,7 +380,7 @@ public final class DynamicControlLibrary: NativeLibrary {
372380
RuntimeError.custom("error",
373381
args.first!.unescapedDescription,
374382
args.count == 1 ? [] : Array(args[args.startIndex+1..<args.endIndex]))
375-
.attach(stackTrace: self.context.evaluator.machine.getStackTrace())
383+
.attach(vm: self.context.evaluator.machine)
376384
if let raiseProc = self.raiseProc {
377385
var args = Exprs()
378386
args.append(.error(error))
@@ -390,7 +398,9 @@ public final class DynamicControlLibrary: NativeLibrary {
390398
let assertionError =
391399
RuntimeError.eval(.assertion,
392400
.makeString(stackTrace.first?.originalName ?? "procedure"),
393-
args.first!).attach(stackTrace: stackTrace)
401+
args.first!)
402+
.attach(stackTrace: stackTrace)
403+
.attach(callTrace: self.context.evaluator.machine.getCallTraceInfo())
394404
if let raiseProc = self.raiseProc {
395405
var args = Exprs()
396406
args.append(.error(assertionError))

Sources/LispKit/Primitives/ThreadLibrary.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,9 @@ public final class ThreadLibrary: NativeLibrary {
236236
return .void
237237
}
238238

239-
private func threadTerminate(th: Expr) throws -> Expr {
239+
private func threadTerminate(th: Expr, wait: Expr?) throws -> Expr {
240240
let thread = try self.thread(from: th)
241-
if let t = thread.value.abort(), Thread.current !== t {
241+
if let t = thread.value.abort(), Thread.current !== t, wait?.isTrue ?? true {
242242
self.context.evaluator.threads.waitForTermination(of: t)
243243
}
244244
return .void

Sources/LispKit/Runtime/Evaluator.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ public final class Evaluator: TrackedObject {
4848
/// Will be set to true if the `exit` function was invoked
4949
public internal(set) var exitTriggered: Bool = false
5050

51-
/// When set to true, will print call and return traces
51+
/// When set to true, will print call and return traces
5252
public var traceCalls: CallTracingMode = .off
5353

54+
/// Call stack cap
55+
public var maxCallStack: Int = 20
56+
5457
/// The main virtual machine
5558
private let main: VirtualMachine
5659

Sources/LispKit/Runtime/SymbolTable.swift

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class SymbolTable: Sequence {
3131
private var gensymCounter: UInt64 = 0
3232

3333
public let undef = Symbol("<undef>")
34+
public let dotdotdot = Symbol("")
3435
public let ellipsis = Symbol("...")
3536
public let wildcard = Symbol("_")
3637
public let append = Symbol("append")

Sources/LispKit/Runtime/Threads/EvalThread.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public final class EvalThread: ManagedObject, ThreadBlocker, CustomStringConvert
146146
/// Initializer for representing main (= primary) threads.
147147
internal init(worker: EvalThreadWorker) {
148148
self.name = .symbol(Symbol(uninterned: "main"))
149-
self.threadTag = .false
149+
self.threadTag = .box(Cell(.false))
150150
self.state = .runnable
151151
self.config = nil
152152
self.worker = worker

Sources/LispKit/Runtime/VirtualMachine.swift

+74-6
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,72 @@ public final class VirtualMachine: ManagedObject {
553553
return stackTrace
554554
}
555555

556+
public func getCallTrace(current: Procedure? = nil, cap: Int? = nil) -> [Expr]? {
557+
var cap = cap ?? self.context.evaluator.maxCallStack
558+
guard cap > 0 else {
559+
return nil
560+
}
561+
var stackTrace: [Expr] = []
562+
if let current = current {
563+
cap -= 1
564+
stackTrace.append(.makeList(.symbol(self.context.symbols.intern(current.name)),
565+
.symbol(self.context.symbols.dotdotdot)))
566+
}
567+
var fp = self.registers.fp
568+
while fp > 0 && cap > 0 {
569+
guard case .procedure(let proc) = self.stack[fp &- 1] else {
570+
// This may happen if an error is thrown
571+
return stackTrace
572+
}
573+
let arities = proc.arity
574+
var min = Int.max
575+
var max: Int? = 0
576+
for arity in arities {
577+
switch arity {
578+
case .exact(let n):
579+
if n < min {
580+
min = n
581+
}
582+
if let m = max, n > m {
583+
max = n
584+
}
585+
case .atLeast(let n):
586+
if n < min {
587+
min = n
588+
}
589+
max = nil
590+
}
591+
}
592+
var call = Expr.null
593+
if max == nil || max! > min {
594+
call = .pair(.symbol(self.context.symbols.dotdotdot), call)
595+
}
596+
while min > 0 {
597+
min -= 1
598+
let offset = fp &+ min
599+
if offset >= 0 && offset < self.sp {
600+
call = .pair(self.stack[fp &+ min], call)
601+
}
602+
}
603+
cap -= 1
604+
stackTrace.append(.pair(.symbol(self.context.symbols.intern(proc.name)), call))
605+
if fp > 2 {
606+
guard case .fixnum(let newfp) = self.stack[fp &- 3] else {
607+
// This may happen if an error is thrown
608+
return stackTrace
609+
}
610+
fp = Int(newfp)
611+
} else {
612+
fp = 0
613+
}
614+
}
615+
return stackTrace
616+
}
617+
618+
public func getCallTraceInfo(current: Procedure? = nil, cap: Int? = nil) -> [String]? {
619+
return self.getCallTrace(current: current, cap: cap)?.map { expr in expr.description }
620+
}
621+
556622
@inline(__always) private func printCallTrace(_ n: Int, tailCall: Bool = false) -> Procedure? {
557623
if self.context.evaluator.traceCalls != .off && self.sp > (n &+ 1) {
558624
if case .procedure(let proc) = self.stack[self.sp &- n &- 1],
@@ -863,11 +929,11 @@ public final class VirtualMachine: ManagedObject {
863929
}
864930
} catch let error as RuntimeError {
865931
if error.stackTrace == nil {
866-
error.attach(stackTrace: self.getStackTrace(current: proc))
932+
error.attach(vm: self, current: proc)
867933
}
868934
throw error
869935
} catch let error {
870-
throw RuntimeError.os(error).attach(stackTrace: self.getStackTrace(current: proc))
936+
throw RuntimeError.os(error).attach(vm: self, current: proc)
871937
}
872938
// Handle continuations
873939
if case .rawContinuation(let vmState) = proc.kind {
@@ -927,23 +993,25 @@ public final class VirtualMachine: ManagedObject {
927993
do {
928994
let res = try self.execute()
929995
guard !self.abortionRequested else {
930-
throw RuntimeError.abortion(stackTrace: self.getStackTrace())
996+
throw RuntimeError.abortion(stackTrace: self.getStackTrace(),
997+
callTrace: self.getCallTraceInfo())
931998
}
932999
return res
9331000
} catch let error as RuntimeError {
9341001
if !self.abortionRequested && error.stackTrace == nil {
935-
error.attach(stackTrace: self.getStackTrace())
1002+
error.attach(vm: self)
9361003
}
9371004
throw error
9381005
} catch let error {
939-
throw RuntimeError.os(error).attach(stackTrace: self.getStackTrace())
1006+
throw RuntimeError.os(error).attach(vm: self)
9401007
}
9411008
}
9421009

9431010
private func execute() throws -> Expr {
9441011
while self.registers.ip >= 0 && self.registers.ip < self.registers.code.instructions.count {
9451012
guard !self.abortionRequested else {
946-
throw RuntimeError.abortion(stackTrace: self.getStackTrace())
1013+
throw RuntimeError.abortion(stackTrace: self.getStackTrace(),
1014+
callTrace: self.getCallTraceInfo())
9471015
}
9481016
self.collectGarbageIfNeeded()
9491017
/*

0 commit comments

Comments
 (0)