feat(macro): add syntax-rules macro expander
This commit is contained in:
1 parent
af0a0aca5b
commit
347964beae
1 file changed
+227
@@ -0,0 +1,227 @@
|
||||
public final class Gensym {
|
||||
private var counter = 0
|
||||
func next() -> Int {
|
||||
counter += 1
|
||||
return counter
|
||||
}
|
||||
}
|
||||
|
||||
indirect enum MatchTree {
|
||||
case leaf(Value)
|
||||
case seq([MatchTree])
|
||||
}
|
||||
|
||||
let ellipsisName = "..."
|
||||
|
||||
public final class SyntaxRules {
|
||||
let literals: [String]
|
||||
let rules: [(pattern: Value, template: Value)]
|
||||
|
||||
init(literals: [String], rules: [(Value, Value)]) {
|
||||
self.literals = literals
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
static func parse(_ spec: Value) throws -> SyntaxRules {
|
||||
let elems = try listToArray(spec)
|
||||
guard elems.count >= 2, case .symbol(let head) = elems[0], baseName(head) == "syntax-rules" else {
|
||||
throw schemeError("malformed syntax-rules")
|
||||
}
|
||||
let literalForms = try listToArray(elems[1])
|
||||
var literals: [String] = []
|
||||
for lit in literalForms {
|
||||
guard case .symbol(let s) = lit else { throw schemeError("syntax-rules literal must be symbol") }
|
||||
literals.append(baseName(s))
|
||||
}
|
||||
var rules: [(Value, Value)] = []
|
||||
for ruleForm in elems[2...] {
|
||||
let ruleParts = try listToArray(ruleForm)
|
||||
guard ruleParts.count >= 2 else { throw schemeError("malformed syntax rule") }
|
||||
let pattern = ruleParts[0]
|
||||
let template: Value
|
||||
if ruleParts.count == 2 {
|
||||
template = ruleParts[1]
|
||||
} else {
|
||||
template = arrayToList([.symbol("begin")] + Array(ruleParts[1...]))
|
||||
}
|
||||
rules.append((pattern, template))
|
||||
}
|
||||
return SyntaxRules(literals: literals, rules: rules)
|
||||
}
|
||||
|
||||
func expand(_ form: Value, _ gensym: Gensym) throws -> Value {
|
||||
let formArgs: Value
|
||||
if case .pair(let p) = form { formArgs = p.cdr } else { formArgs = .null }
|
||||
for (pattern, template) in rules {
|
||||
let patternArgs: Value
|
||||
if case .pair(let pp) = pattern { patternArgs = pp.cdr } else { patternArgs = .null }
|
||||
if let bindings = matchPattern(patternArgs, formArgs, literals) {
|
||||
let vars = patternVarsIn(pattern, literals)
|
||||
let markId = gensym.next()
|
||||
return try instantiate(template, bindings, markId, vars, false)
|
||||
}
|
||||
}
|
||||
throw schemeError("no matching syntax rule for \(displayString(form))")
|
||||
}
|
||||
}
|
||||
|
||||
func patternVarsIn(_ pattern: Value, _ literals: [String]) -> Set<String> {
|
||||
var acc = Set<String>()
|
||||
func walk(_ v: Value) {
|
||||
switch v {
|
||||
case .symbol(let s):
|
||||
if s == "_" || baseName(s) == ellipsisName { return }
|
||||
if literals.contains(baseName(s)) { return }
|
||||
acc.insert(s)
|
||||
case .pair(let p):
|
||||
walk(p.car)
|
||||
walk(p.cdr)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
walk(pattern)
|
||||
return acc
|
||||
}
|
||||
|
||||
func matchPattern(_ pattern: Value, _ form: Value, _ literals: [String]) -> [String: MatchTree]? {
|
||||
switch pattern {
|
||||
case .symbol(let s):
|
||||
if s == "_" { return [:] }
|
||||
if baseName(s) == ellipsisName { return nil }
|
||||
if literals.contains(baseName(s)) {
|
||||
if case .symbol(let fs) = form, baseName(fs) == baseName(s) { return [:] }
|
||||
return nil
|
||||
}
|
||||
return [s: .leaf(form)]
|
||||
case .null:
|
||||
if case .null = form { return [:] }
|
||||
return nil
|
||||
case .pair:
|
||||
let (pElems, pTail) = listToArrayWithTail(pattern)
|
||||
let (fElems, fTail) = listToArrayWithTail(form)
|
||||
return matchSeq(pElems, pTail, fElems, fTail, literals)
|
||||
case .bool, .integer, .rational, .real, .char, .string:
|
||||
return equalValues(pattern, form) ? [:] : nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func ellipsisIndex(_ pElems: [Value]) -> Int? {
|
||||
for i in 1..<max(pElems.count, 1) {
|
||||
if i < pElems.count, case .symbol(let s) = pElems[i], baseName(s) == ellipsisName {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchSeq(_ pElems: [Value], _ pTail: Value, _ fElems: [Value], _ fTail: Value, _ literals: [String]) -> [String: MatchTree]? {
|
||||
if let e = ellipsisIndex(pElems) {
|
||||
let repeated = pElems[e - 1]
|
||||
let preCount = e - 1
|
||||
let postPatterns = Array(pElems[(e + 1)...])
|
||||
let postCount = postPatterns.count
|
||||
if fElems.count < preCount + postCount { return nil }
|
||||
var bindings: [String: MatchTree] = [:]
|
||||
for i in 0..<preCount {
|
||||
guard let m = matchPattern(pElems[i], fElems[i], literals) else { return nil }
|
||||
bindings.merge(m) { _, new in new }
|
||||
}
|
||||
let repeatCount = fElems.count - preCount - postCount
|
||||
var iterMatches: [[String: MatchTree]] = []
|
||||
for j in 0..<repeatCount {
|
||||
guard let m = matchPattern(repeated, fElems[preCount + j], literals) else { return nil }
|
||||
iterMatches.append(m)
|
||||
}
|
||||
for v in patternVarsIn(repeated, literals) {
|
||||
bindings[v] = .seq(iterMatches.map { $0[v] ?? .leaf(.unspecified) })
|
||||
}
|
||||
for i in 0..<postCount {
|
||||
guard let m = matchPattern(postPatterns[i], fElems[preCount + repeatCount + i], literals) else { return nil }
|
||||
bindings.merge(m) { _, new in new }
|
||||
}
|
||||
guard let mt = matchPattern(pTail, fTail, literals) else { return nil }
|
||||
bindings.merge(mt) { _, new in new }
|
||||
return bindings
|
||||
}
|
||||
if fElems.count != pElems.count { return nil }
|
||||
var bindings: [String: MatchTree] = [:]
|
||||
for i in 0..<pElems.count {
|
||||
guard let m = matchPattern(pElems[i], fElems[i], literals) else { return nil }
|
||||
bindings.merge(m) { _, new in new }
|
||||
}
|
||||
guard let mt = matchPattern(pTail, fTail, literals) else { return nil }
|
||||
bindings.merge(mt) { _, new in new }
|
||||
return bindings
|
||||
}
|
||||
|
||||
func instantiate(_ template: Value, _ bindings: [String: MatchTree], _ markId: Int, _ patternVars: Set<String>, _ inQuote: Bool) throws -> Value {
|
||||
switch template {
|
||||
case .symbol(let s):
|
||||
if patternVars.contains(s) {
|
||||
guard case .leaf(let v)? = bindings[s] else {
|
||||
throw schemeError("pattern variable used at wrong ellipsis depth: \(baseName(s))")
|
||||
}
|
||||
return v
|
||||
}
|
||||
if inQuote { return template }
|
||||
if baseName(s) == ellipsisName { return template }
|
||||
return .symbol(s + String(hygieneMark) + String(markId))
|
||||
case .pair:
|
||||
let (elems, tail) = listToArrayWithTail(template)
|
||||
if elems.count == 2, case .symbol(let s0) = elems[0], baseName(s0) == "quote", case .null = tail {
|
||||
let inner = try instantiate(elems[1], bindings, markId, patternVars, true)
|
||||
return arrayToList([.symbol("quote"), inner])
|
||||
}
|
||||
var out: [Value] = []
|
||||
var i = 0
|
||||
while i < elems.count {
|
||||
if i + 1 < elems.count, case .symbol(let e) = elems[i + 1], baseName(e) == ellipsisName {
|
||||
let sub = elems[i]
|
||||
let evars = patternVarsIn(sub, []).filter {
|
||||
if case .seq? = bindings[$0] { return true }
|
||||
return false
|
||||
}
|
||||
let n = try lockstepCount(Array(evars), bindings)
|
||||
for j in 0..<n {
|
||||
var child = bindings
|
||||
for v in evars {
|
||||
if case .seq(let arr)? = bindings[v] { child[v] = arr[j] }
|
||||
}
|
||||
out.append(try instantiate(sub, child, markId, patternVars, inQuote))
|
||||
}
|
||||
i += 2
|
||||
} else {
|
||||
out.append(try instantiate(elems[i], bindings, markId, patternVars, inQuote))
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
let newTail: Value
|
||||
if case .null = tail {
|
||||
newTail = .null
|
||||
} else {
|
||||
newTail = try instantiate(tail, bindings, markId, patternVars, inQuote)
|
||||
}
|
||||
return arrayToList(out, tail: newTail)
|
||||
default:
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
private func lockstepCount(_ evars: [String], _ bindings: [String: MatchTree]) throws -> Int {
|
||||
var count: Int? = nil
|
||||
for v in evars {
|
||||
if case .seq(let arr)? = bindings[v] {
|
||||
if let c = count, c != arr.count {
|
||||
throw schemeError("mismatched ellipsis counts")
|
||||
}
|
||||
count = arr.count
|
||||
}
|
||||
}
|
||||
guard let n = count else {
|
||||
throw schemeError("ellipsis template with no pattern variables")
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in new issue
Block a user