Last active
August 4, 2020 11:01
-
-
Save bellbind/27f96a2a3f6ed71ea85db92c9f906008 to your computer and use it in GitHub Desktop.
[swift4][macos] commandline REPL for JavaScriptCore with toplevel await
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env swift | |
// REPL for JavaScriptCore (for arrow keys, use rlwrap command) | |
import JavaScriptCore | |
class Timeout: NSObject, JSExport { | |
let item: DispatchWorkItem | |
init(_ item: DispatchWorkItem) {self.item = item} | |
} | |
func installBuiltins(_ ctx: JSContext) { | |
// simple console.log() | |
let console = ctx.evaluateScript("console") | |
let log: @convention(block) (JSValue) -> () = {arg in | |
if arg.isUndefined {print()} else {print(arg)} | |
} | |
console?.setValue(log, forProperty: "log") | |
// id = setTimeout(fn, ms, v) and clearTimeout(id) | |
let setTimeout: @convention(block) (JSValue, JSValue, JSValue) -> JSValue = {fn, ms, v in | |
let n = ms.toNumber().doubleValue / 1000 | |
let item = DispatchWorkItem() {fn.call(withArguments: [v as Any])} | |
DispatchQueue.global().asyncAfter(deadline: .now() + n, execute: item) | |
return JSValue(object: Timeout(item), in: fn.context) | |
} | |
ctx.globalObject.setValue(setTimeout, forProperty: "setTimeout") | |
let clearTimeout: @convention(block) (JSValue) -> () = {value in | |
if value.isInstance(of: Timeout.self) { | |
let timeout = value.toObjectOf(Timeout.self) as? Timeout | |
timeout?.item.cancel() | |
} | |
} | |
ctx.globalObject.setValue(clearTimeout, forProperty: "clearTimeout") | |
} | |
class Repl { | |
private let sem: DispatchSemaphore | |
private let ctx: JSContext | |
private let syntaxError: JSValue | |
private let notEoS: JSValue | |
private let promise: JSValue | |
init() { | |
self.sem = DispatchSemaphore(value: 0) | |
self.ctx = JSContext() | |
self.promise = self.ctx.evaluateScript("Promise") | |
self.ctx.evaluateScript("(") | |
self.syntaxError = self.ctx.exception.forProperty("constructor") | |
self.notEoS = self.ctx.exception.forProperty("message") | |
self.ctx.exception = nil | |
installBuiltins(self.ctx) | |
} | |
private func repl(_ lines: String, _ prompt: String) { | |
print(prompt, terminator: "") | |
guard let line = readLine(strippingNewline: false) else { | |
self.sem.signal() | |
return | |
} | |
let code = lines + line | |
if code == "\n" {return asyncRepl(lines, prompt)} | |
let value = self.ctx.evaluateScript(code) | |
if let exp = self.ctx.exception { | |
self.ctx.exception = nil | |
let se = exp.isInstance(of: self.syntaxError) | |
let cont = se && exp.forProperty("message").isEqual(to: self.notEoS) | |
if cont { | |
return asyncRepl(code, "+ ") | |
} else if se { | |
// try toplevel await wrapping | |
let asyncF = self.ctx.evaluateScript("(async () => \(code))") | |
if self.ctx.exception == nil { | |
if let promise = asyncF?.call(withArguments: []) { | |
if promise.isInstance(of: self.promise) { | |
callPromise(promise) | |
return asyncRepl("", "> ") | |
} | |
} | |
} | |
self.ctx.exception = nil | |
} | |
print(exp) | |
return asyncRepl("", "> ") | |
} | |
if let value = value {print(value)} | |
return asyncRepl("", "> ") | |
} | |
private func asyncRepl(_ lines: String, _ prompt: String) { | |
DispatchQueue.global().async {[weak self] in self?.repl(lines, prompt)} | |
} | |
private func callPromise(_ promise: JSValue) { | |
let out: @convention(block) (JSValue) -> () = {obj in print("[resolved] \(obj)")} | |
let cb1 = JSValue(object: out, in: self.ctx) | |
let err: @convention(block) (JSValue) -> () = {obj in print("[rejected] \(obj)")} | |
let cb2 = JSValue(object: err, in: self.ctx) | |
promise.invokeMethod("then", withArguments: [cb1 as Any, cb2 as Any]) | |
} | |
func run() { | |
self.asyncRepl("", "> ") | |
self.sem.wait() | |
} | |
} | |
Repl().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment