swift5: step8_macros
authorOleg Montak <montak.o@ya.ru>
Tue, 5 Nov 2019 17:32:08 +0000 (20:32 +0300)
committerOleg Montak <montak.o@ya.ru>
Fri, 8 Nov 2019 18:46:18 +0000 (21:46 +0300)
swift5/Package.swift
swift5/Sources/core/Core.swift
swift5/Sources/core/Errors.swift
swift5/Sources/core/Printer.swift
swift5/Sources/core/Types.swift
swift5/Sources/step8_macros/main.swift [new file with mode: 0644]

index 5886b65..ec03fde 100644 (file)
@@ -15,6 +15,7 @@ let package = Package(
         .executable(name: "step5_tco", targets: ["step5_tco"]),
         .executable(name: "step6_file", targets: ["step6_file"]),
         .executable(name: "step7_quote", targets: ["step7_quote"]),
+        .executable(name: "step8_macros", targets: ["step8_macros"])
     ],
     dependencies: [
         // Dependencies declare other packages that this package depends on.
@@ -31,6 +32,7 @@ let package = Package(
         .target(name: "step4_if_fn_do", dependencies: ["core"]),
         .target(name: "step5_tco", dependencies: ["core"]),
         .target(name: "step6_file", dependencies: ["core"]),
-        .target(name: "step7_quote", dependencies: ["core"])
+        .target(name: "step7_quote", dependencies: ["core"]),
+        .target(name: "step8_macros", dependencies: ["core"])
     ]
 )
index a486aec..95ecd79 100644 (file)
@@ -148,6 +148,41 @@ private extension Func {
         }
         return .list(values)
     }
+
+    static let nth = Func { args in
+        guard args.count == 2 else { throw MalError.invalidArguments("nth") }
+        guard case let .number(index) = args[1] else { throw MalError.invalidArguments("nth") }
+
+        switch args.first {
+        case let .list(values), let .vector(values):
+            guard values.indices ~= index else { throw MalError.outOfRange() }
+            return values[index]
+        default:
+            throw MalError.invalidArguments("nth")
+        }
+    }
+
+    static let first = Func { args in
+        switch args.first {
+        case let .list(values), let .vector(values):
+            return values.first ?? .null
+        case .null:
+            return .null
+        default:
+            throw MalError.invalidArguments("first")
+        }
+    }
+
+    static let rest = Func { args in
+        switch args.first {
+        case let .list(values), let .vector(values):
+            return .list(Array(values.dropFirst()))
+        case .null:
+            return .list([])
+        default:
+            throw MalError.invalidArguments("rest")
+        }
+    }
 }
 
 private let data: [String: Expr] = [
@@ -176,7 +211,10 @@ private let data: [String: Expr] = [
     "reset!": .function(.reset),
     "swap!": .function(.swap),
     "cons": .function(.cons),
-    "concat": .function(.concat)
+    "concat": .function(.concat),
+    "nth": .function(.nth),
+    "first": .function(.first),
+    "rest": .function(.rest),
 ]
 
 public enum Core {
index 750cb2f..ee05dd1 100644 (file)
@@ -33,4 +33,8 @@ extension MalError {
     public static func invalidArguments(_ name: String) -> MalError {
         return MalError("\(name): invalid arguments")
     }
+
+    public static func outOfRange() -> MalError {
+        return MalError("index out of range")
+    }
 }
index 93a935b..46676d6 100644 (file)
@@ -25,8 +25,8 @@ extension Expr {
             return b ? "true" : "false"
         case .null:
             return "nil"
-        case .function:
-            return "#<function>"
+        case let .function(fn):
+            return fn.isMacro ? "#<macro-function>" : "#<function>"
         case let .atom(expr):
             return "(atom \(print(expr.val)))"
         }
index 36a35c6..22c1de7 100644 (file)
@@ -51,17 +51,20 @@ final public class Func {
     public let ast: Expr?
     public let params: [String]
     public let env: Env?
+    public var isMacro: Bool
 
     public init(
         ast: Expr? = nil,
         params: [String] = [],
         env: Env? = nil,
+        isMacro: Bool = false,
         run: @escaping ([Expr]) throws -> Expr
     ) {
         self.run = run
         self.ast = ast
         self.params = params
         self.env = env
+        self.isMacro = isMacro
     }
 }
 
diff --git a/swift5/Sources/step8_macros/main.swift b/swift5/Sources/step8_macros/main.swift
new file mode 100644 (file)
index 0000000..8934aaa
--- /dev/null
@@ -0,0 +1,251 @@
+import Foundation
+import core
+
+func read(_ s: String) throws -> Expr {
+    return try Reader.read(s)
+}
+
+private func isPair(_ expr: Expr) -> Bool {
+    switch expr {
+    case let .list(values), let .vector(values):
+        return !values.isEmpty
+    default:
+        return false
+    }
+}
+
+private func asListOrVector(_ expr: Expr) -> [Expr]? {
+    switch expr {
+    case let .list(values), let .vector(values):
+        return values
+    default:
+        return nil
+    }
+}
+
+private func quasiquote(_ expr: Expr) throws -> Expr {
+    if !isPair(expr) {
+        return .list([.symbol("quote"), expr])
+    }
+    guard let ast = asListOrVector(expr), !ast.isEmpty else {
+        throw MalError.invalidArguments("quasiquote")
+    }
+
+    if case .symbol("unquote") = ast[0] {
+        guard ast.count > 1 else { throw MalError.invalidArguments("unquote") }
+        return ast[1]
+    }
+
+    if isPair(ast[0]), let ast0 = asListOrVector(ast[0]) {
+        if case .symbol("splice-unquote") = ast0.first {
+            guard ast0.count > 1 else { throw MalError.invalidArguments("splice-unquote") }
+            let rest = try quasiquote(.list(Array(ast[1...])))
+            return .list([.symbol("concat"), ast0[1], rest])
+        }
+    }
+
+    let rest = try quasiquote(.list(Array(ast[1...])))
+    return .list([.symbol("cons"), try quasiquote(ast[0]), rest])
+}
+
+private func isMacroCall(_ expr: Expr, env: Env) -> Bool {
+    if case let .list(ast) = expr,
+        case let .symbol(name) = ast.first,
+        case let .function(fn) = try? env.get(name) {
+            return fn.isMacro
+    }
+    return false
+}
+
+private func macroExpand(_ expr: Expr, env: Env) throws -> Expr {
+    var expr = expr
+    while true {
+        guard case let .list(ast) = expr,
+            case let .symbol(name) = ast.first,
+            case let .function(fn) = try? env.get(name),
+            fn.isMacro else {
+                break
+        }
+
+        expr = try fn.run(Array(ast.dropFirst()))
+    }
+    return expr
+}
+
+private func evalAst(_ expr: Expr, env: Env) throws -> Expr {
+    switch expr {
+    case let .symbol(name):
+        return try env.get(name)
+    case let .vector(values):
+        return .vector(try values.map { try eval($0, env: env) })
+    case let .hashmap(values):
+        return .hashmap(try values.mapValues { try eval($0, env: env) })
+    case let .list(ast):
+        return .list(try ast.map { try eval($0, env: env) })
+    default:
+        return expr
+    }
+}
+
+func eval(_ expr: Expr, env: Env) throws -> Expr {
+
+    var env = env
+    var expr = expr
+
+    while true {
+
+        expr = try macroExpand(expr, env: env)
+
+        guard case let .list(ast) = expr else {
+            return try evalAst(expr, env: env)
+        }
+
+        if ast.isEmpty {
+            return expr
+        }
+
+        switch ast[0] {
+
+        case .symbol("def!"):
+            guard ast.count == 3 else { throw MalError.invalidArguments("def!") }
+            guard case let .symbol(name) = ast[1] else { throw MalError.invalidArguments("def!") }
+
+            let val = try eval(ast[2], env: env)
+            env.set(forKey: name, val: val)
+            return val
+
+        case .symbol("let*"):
+            guard ast.count == 3 else { throw MalError.invalidArguments("let*") }
+
+            switch ast[1] {
+            case let .list(bindable), let .vector(bindable):
+                let letEnv = Env(outer: env)
+
+                for i in stride(from: 0, to: bindable.count - 1, by: 2) {
+                    guard case let .symbol(key) = bindable[i] else { throw MalError.invalidArguments("let*") }
+                    let value = bindable[i + 1]
+                    letEnv.set(forKey: key, val: try eval(value, env: letEnv))
+                }
+
+                expr = ast[2]
+                env = letEnv
+            default:
+                throw MalError.invalidArguments("let*")
+            }
+
+        case .symbol("quote"):
+            guard ast.count == 2 else { throw MalError.invalidArguments("quote") }
+            return ast[1]
+
+        case .symbol("quasiquote"):
+            guard ast.count == 2 else { throw MalError.invalidArguments("quasiquote") }
+            expr = try quasiquote(ast[1])
+
+        case .symbol("defmacro!"):
+            guard ast.count == 3 else { throw MalError.invalidArguments("defmacro!") }
+            guard case let .symbol(name) = ast[1] else { throw MalError.invalidArguments("defmacro!") }
+
+            guard case let .function(val) = try eval(ast[2], env: env) else { throw MalError.invalidArguments("defmacro!") }
+            val.isMacro = true
+            env.set(forKey: name, val: .function(val))
+            return .function(val)
+
+        case .symbol("macroexpand"):
+            guard ast.count == 2 else { throw MalError.invalidArguments("macroexpand") }
+            return try macroExpand(ast[1], env: env)
+
+        case .symbol("do"):
+            let exprsToEval = ast.dropFirst()
+            guard !exprsToEval.isEmpty else { throw MalError.invalidArguments("do") }
+            _ = try exprsToEval.dropLast().map { try eval($0, env: env) }
+            expr = exprsToEval.last!
+
+        case .symbol("if"):
+            guard 3...4 ~= ast.count else { throw MalError.invalidArguments("if") }
+
+            switch try eval(ast[1], env: env) {
+            case .bool(false), .null:
+                if let falseBranch = ast[safe: 3] {
+                    expr = falseBranch
+                } else {
+                    expr = .null
+                }
+            default:
+                expr = ast[2]
+            }
+
+        case .symbol("fn*"):
+            guard ast.count == 3 else { throw MalError("fn*") }
+            let binds: [String]
+
+            switch ast[1] {
+            case let .list(xs), let .vector(xs):
+                binds = try xs.map {
+                    guard case let .symbol(name) = $0 else { throw MalError.invalidArguments("fn*") }
+                    return name
+                }
+            default:
+                throw MalError.invalidArguments("fn*")
+            }
+
+            let run: ([Expr]) throws -> Expr = { args in
+                let fEnv = try Env(binds: binds, exprs: args, outer: env)
+                return try eval(ast[2], env: fEnv)
+            }
+
+            let f = Func(ast: ast[2], params: binds, env: env, run: run)
+            return .function(f)
+
+        default:
+            guard case let .list(evaluatedList) = try evalAst(expr, env: env) else { fatalError() }
+            guard case let .function(fn) = evaluatedList[0] else { throw MalError("not a function: \(evaluatedList[0])") }
+
+            let args = Array(evaluatedList.dropFirst())
+            if let ast = fn.ast, let fnEnv = fn.env {
+                let newEnv = try Env(binds: fn.params, exprs: args, outer: fnEnv)
+                env = newEnv
+                expr = ast
+            } else {
+                return try fn.run(args)
+            }
+        }
+    }
+}
+
+func print(_ expr: Expr) -> String {
+    return Expr.print(expr)
+}
+
+func rep(_ s: String, env: Env) -> String {
+    do {
+        let expr = try read(s)
+        let resExpr = try eval(expr, env: env)
+        let resultStr = print(resExpr)
+        return resultStr
+    } catch {
+        return error.localizedDescription
+    }
+}
+
+let replEnv: Env = Env(data: Core.ns.data)
+
+replEnv.set(forKey: "eval", val: .function(Func { args in
+    guard let expr = args.first else { throw MalError.invalidArguments("eval") }
+    return try eval(expr, env: replEnv)
+}))
+replEnv.set(forKey: "*ARGV*", val: .list(CommandLine.arguments.dropFirst(2).map(Expr.string)))
+
+_ = rep("(def! not (fn* (a) (if a false true)))", env: replEnv)
+_ = rep(#"(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))"#, env: replEnv)
+_ = rep(#"(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))"#, env: replEnv)
+
+if CommandLine.arguments.count > 1 {
+    _ = rep("(load-file \"" + CommandLine.arguments[1] + "\")", env: replEnv)
+    exit(0)
+}
+
+while true {
+    print("user> ", terminator: "")
+    guard let s = readLine() else { break }
+    print(rep(s, env: replEnv))
+}