Skip to content

Commit 4c5658a

Browse files
committed
Support let-forms for handling keyword-based argument lists.
1 parent 00d420f commit 4c5658a

File tree

5 files changed

+171
-4
lines changed

5 files changed

+171
-4
lines changed

Sources/LispKit/Compiler/EvalError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public enum EvalError: Int, Hashable {
102102
case unknownInterpolateAlgorithm
103103
case unsupportedGradientColorSpec
104104
case invalidRegexpMatchingOption
105+
case unknownKeyword
106+
case expectedKeywordArg
105107

106108
public var message: String {
107109
switch self {
@@ -259,6 +261,10 @@ public enum EvalError: Int, Hashable {
259261
return "unsupported gradient color specification: $0"
260262
case .invalidRegexpMatchingOption:
261263
return "invalid regexp matching option: $0"
264+
case .unknownKeyword:
265+
return "unknown keyword $1; cannot assign value $0"
266+
case .expectedKeywordArg:
267+
return "expected keyword argument: $0"
262268
}
263269
}
264270

Sources/LispKit/Compiler/RuntimeError.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ public class RuntimeError: Error, Hashable, CustomStringConvertible {
3535
public private(set) var library: Expr?
3636
public private(set) var stackTrace: [Procedure]?
3737

38-
private init(_ pos: SourcePosition,
39-
_ descriptor: ErrorDescriptor,
40-
_ irritants: [Expr],
41-
_ stackTrace: [Procedure]? = nil) {
38+
internal init(_ pos: SourcePosition,
39+
_ descriptor: ErrorDescriptor,
40+
_ irritants: [Expr],
41+
_ stackTrace: [Procedure]? = nil) {
4242
self.pos = pos
4343
self.descriptor = descriptor
4444
self.irritants = irritants

Sources/LispKit/Primitives/ControlFlowLibrary.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public final class ControlFlowLibrary: NativeLibrary {
3737
self.define(SpecialForm("let*-values", self.compileLetStarValues))
3838
self.define(SpecialForm("let-optionals", self.compileLetOptionals))
3939
self.define(SpecialForm("let*-optionals", self.compileLetStarOptionals))
40+
self.define(SpecialForm("let-keywords", self.compileLetKeywords))
41+
self.define(SpecialForm("let*-keywords", self.compileLetStarKeywords))
4042
self.define(SpecialForm("let-syntax", self.compileLetSyntax))
4143
self.define(SpecialForm("letrec-syntax", self.compileLetRecSyntax))
4244
self.define(SpecialForm("do", self.compileDo))
@@ -343,6 +345,129 @@ public final class ControlFlowLibrary: NativeLibrary {
343345
return group
344346
}
345347

348+
private func compileLetKeywords(_ compiler: Compiler,
349+
expr: Expr,
350+
env: Env,
351+
tail: Bool) throws -> Bool {
352+
guard case .pair(_, .pair(let optlist, .pair(let first, let body))) = expr else {
353+
throw RuntimeError.argumentCount(of: "let-keywords", min: 2, expr: expr)
354+
}
355+
let initialLocals = compiler.numLocals
356+
switch first {
357+
case .null:
358+
return try compiler.compileSeq(.pair(optlist, body),
359+
in: env,
360+
inTailPos: tail)
361+
case .pair(_, _):
362+
try compiler.compile(optlist, in: env, inTailPos: false)
363+
let group = try self.compileKeywordBindings(compiler, first, in: env, atomic: true)
364+
compiler.emit(.pop)
365+
let res = try compiler.compileSeq(body,
366+
in: Env(group),
367+
inTailPos: tail)
368+
return compiler.finalizeBindings(group, exit: res, initialLocals: initialLocals)
369+
default:
370+
throw RuntimeError.type(first, expected: [.listType])
371+
}
372+
}
373+
374+
private func compileLetStarKeywords(_ compiler: Compiler,
375+
expr: Expr,
376+
env: Env,
377+
tail: Bool) throws -> Bool {
378+
guard case .pair(_, .pair(let optlist, .pair(let first, let body))) = expr else {
379+
throw RuntimeError.argumentCount(of: "let*-keywords", min: 2, expr: expr)
380+
}
381+
let initialLocals = compiler.numLocals
382+
switch first {
383+
case .null:
384+
return try compiler.compileSeq(.pair(optlist, body),
385+
in: env,
386+
inTailPos: tail)
387+
case .pair(_, _):
388+
try compiler.compile(optlist, in: env, inTailPos: false)
389+
let group = try self.compileKeywordBindings(compiler, first, in: env, atomic: false)
390+
compiler.emit(.pop)
391+
let res = try compiler.compileSeq(body,
392+
in: Env(group),
393+
inTailPos: tail)
394+
return compiler.finalizeBindings(group, exit: res, initialLocals: initialLocals)
395+
default:
396+
throw RuntimeError.type(first, expected: [.listType])
397+
}
398+
}
399+
400+
private func compileKeywordBindings(_ compiler: Compiler,
401+
_ bindingList: Expr,
402+
in lenv: Env,
403+
atomic: Bool) throws -> BindingGroup {
404+
let group = BindingGroup(owner: compiler, parent: lenv)
405+
let env = atomic ? lenv : .local(group)
406+
var bindings = bindingList
407+
var prevIndex = -1
408+
let initialIp = compiler.emitPlaceholder()
409+
let backfillIp = compiler.offsetToNext(0)
410+
// Backfill keyword bindings with defaults
411+
while case .pair(let binding, let rest) = bindings {
412+
guard case .pair(.symbol(let sym), .pair(let expr, .null)) = binding else {
413+
throw RuntimeError.eval(.malformedBinding, binding, bindingList)
414+
}
415+
compiler.emit(.pushUndef)
416+
let pushValueIp = compiler.emitPlaceholder()
417+
compiler.emit(.eq)
418+
let alreadySetIp = compiler.emitPlaceholder()
419+
try compiler.compile(expr, in: env, inTailPos: false)
420+
let binding = group.allocBindingFor(sym)
421+
guard binding.index > prevIndex else {
422+
throw RuntimeError.eval(.duplicateBinding, .symbol(sym), bindingList)
423+
}
424+
compiler.emit(binding.isValue ? .setLocal(binding.index) : .setLocalValue(binding.index))
425+
compiler.patch(binding.isValue ? .pushLocal(binding.index) : .pushLocalValue(binding.index),
426+
at: pushValueIp)
427+
compiler.patch(.branchIfNot(compiler.offsetToNext(alreadySetIp)), at: alreadySetIp)
428+
prevIndex = binding.index
429+
bindings = rest
430+
}
431+
guard bindings.isNull else {
432+
throw RuntimeError.eval(.malformedBindings, bindingList)
433+
}
434+
let finalIp = compiler.emitPlaceholder()
435+
compiler.patch(.branch(compiler.offsetToNext(initialIp)), at: initialIp)
436+
// Allocate space for all the bindings
437+
bindings = bindingList
438+
while case .pair(.pair(.symbol(let sym), _), let rest) = bindings {
439+
let binding = group.allocBindingFor(sym)
440+
compiler.emit(.pushUndef)
441+
compiler.emit(binding.isValue ? .setLocal(binding.index) : .makeLocalVariable(binding.index))
442+
bindings = rest
443+
}
444+
// Process keyword list
445+
bindings = bindingList
446+
let loopIp = compiler.emit(.dup)
447+
compiler.emit(.isNull)
448+
let listEmptyIp = compiler.emitPlaceholder()
449+
compiler.emit(.deconsKeyword)
450+
while case .pair(.pair(.symbol(let sym), _), let rest) = bindings {
451+
let binding = group.allocBindingFor(sym)
452+
compiler.emit(.dup)
453+
compiler.pushConstant(.symbol(compiler.context.symbols.intern(sym.identifier + ":")))
454+
compiler.emit(.eq)
455+
let keywordCompIp = compiler.emitPlaceholder()
456+
compiler.emit(.pop)
457+
compiler.emit(binding.isValue ? .setLocal(binding.index) : .makeLocalVariable(binding.index))
458+
compiler.emit(.branch(-compiler.offsetToNext(loopIp)))
459+
compiler.patch(.branchIfNot(compiler.offsetToNext(keywordCompIp)), at: keywordCompIp)
460+
bindings = rest
461+
}
462+
compiler.emit(.raiseError(EvalError.unknownKeyword.rawValue, 2))
463+
compiler.patch(.branchIf(compiler.offsetToNext(listEmptyIp)), at: listEmptyIp)
464+
// Jumop to the default backfill
465+
compiler.emit(.branch(-compiler.offsetToNext(backfillIp)))
466+
// Patch instructions jumping to the end
467+
compiler.patch(.branch(compiler.offsetToNext(finalIp)), at: finalIp)
468+
return group
469+
}
470+
346471
private func compileLetSyntax(_ compiler: Compiler,
347472
expr: Expr,
348473
env: Env, tail: Bool) throws -> Bool {

Sources/LispKit/Runtime/Instruction.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ public enum Instruction: CustomStringConvertible {
317317
/// the stack is not null.
318318
case isNull
319319

320+
/// **`is_undef`**: Pushes `#false` onto the stack if the current value on top of
321+
/// the stack is not undefined.
322+
case isUndef
323+
320324
/// **`list` _n_**: Pops the top _n_ values off the stack and constructs a list out of
321325
/// them on top of the stack.
322326
case list(Int)
@@ -329,6 +333,10 @@ public enum Instruction: CustomStringConvertible {
329333
/// onto the stack.
330334
case decons
331335

336+
/// **`decons_keyword`**: Pops a list off the stack, pushing the first two elements as well as the
337+
/// tail of the second pair onto the stack
338+
case deconsKeyword
339+
332340
/// **`car`**: Pops a pair off the stack and pushes its head onto the stack.
333341
case car
334342

@@ -425,6 +433,10 @@ public enum Instruction: CustomStringConvertible {
425433

426434
// Miscellaneous ----------------------------------------------------------------------------
427435

436+
/// **`raise_error` _err_,_n_**: Raises the given evaluation error _err_ using the top _n_
437+
/// elements on top of the stack as irritants.
438+
case raiseError(Int, Int)
439+
428440
/// **`push_current_time`**: Pushes the current time as a flonum onto the stack. The time
429441
/// is expressed as seconds since January 1, 1970 at 00:00.
430442
case pushCurrentTime
@@ -624,6 +636,8 @@ public enum Instruction: CustomStringConvertible {
624636
return "store_in_promise"
625637
case .swap:
626638
return "swap"
639+
case .raiseError(let err, let n):
640+
return "raise_error \(err), \(n)"
627641
case .pushCurrentTime:
628642
return "push_current_time"
629643
case .display:
@@ -640,10 +654,14 @@ public enum Instruction: CustomStringConvertible {
640654
return "is_pair"
641655
case .isNull:
642656
return "is_null"
657+
case .isUndef:
658+
return "is_undef"
643659
case .cons:
644660
return "cons"
645661
case .decons:
646662
return "decons"
663+
case .deconsKeyword:
664+
return "decons_keyword"
647665
case .car:
648666
return "car"
649667
case .cdr:

Sources/LispKit/Runtime/VirtualMachine.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,14 @@ public final class VirtualMachine: TrackedObject {
14821482
self.stack[self.sp] = .undef
14831483
// Re-execute force
14841484
self.registers.ip = self.registers.ip &- 2
1485+
case .raiseError(let err, let n):
1486+
var irritants: [Expr] = []
1487+
for _ in 0..<n {
1488+
irritants.insert(self.pop(), at: 0)
1489+
}
1490+
throw RuntimeError(SourcePosition.unknown,
1491+
ErrorDescriptor.eval(EvalError(rawValue: err)!),
1492+
irritants)
14851493
case .pushCurrentTime:
14861494
self.push(.flonum(Timer.currentTimeInSec))
14871495
case .display:
@@ -1508,6 +1516,8 @@ public final class VirtualMachine: TrackedObject {
15081516
}
15091517
case .isNull:
15101518
self.push(.makeBoolean(self.popUnsafe() == .null))
1519+
case .isUndef:
1520+
self.push(.makeBoolean(self.popUnsafe() == .undef))
15111521
case .cons:
15121522
let cdr = self.pop()
15131523
self.push(.pair(self.popUnsafe(), cdr))
@@ -1518,6 +1528,14 @@ public final class VirtualMachine: TrackedObject {
15181528
}
15191529
self.push(cdr)
15201530
self.push(car)
1531+
case .deconsKeyword:
1532+
let expr = self.popUnsafe()
1533+
guard case .pair(let fst, .pair(let snd, let cdr)) = expr else {
1534+
throw RuntimeError.eval(.expectedKeywordArg, expr)
1535+
}
1536+
self.push(cdr)
1537+
self.push(snd)
1538+
self.push(fst)
15211539
case .car:
15221540
let expr = self.popUnsafe()
15231541
guard case .pair(let car, _) = expr else {

0 commit comments

Comments
 (0)