From 347964beaee77c198d7317db46eac2fe6ffcd8a4 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 09:00:00 +0200 Subject: [PATCH] feat(macro): add syntax-rules macro expander --- Sources/KiteCore/Macros.swift | 227 ++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Sources/KiteCore/Macros.swift diff --git a/Sources/KiteCore/Macros.swift b/Sources/KiteCore/Macros.swift new file mode 100644 index 0000000..f062501 --- /dev/null +++ b/Sources/KiteCore/Macros.swift @@ -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 { + var acc = Set() + 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.. [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.., _ 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.. 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 +}