about summary refs log tree commit diff stats
path: root/src/js/javascript.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-09-12 00:30:21 +0200
committerbptato <nincsnevem662@gmail.com>2022-09-12 00:30:21 +0200
commit51ea622d58bfca19212fac1800cfb033bb85ec39 (patch)
treeb75891690f67b190c60584751f2a30c96f342fdc /src/js/javascript.nim
parente38402dfa1bbc33db6b9d9736517eb45533d595c (diff)
downloadchawan-51ea622d58bfca19212fac1800cfb033bb85ec39.tar.gz
Add JS binding generation
Diffstat (limited to 'src/js/javascript.nim')
-rw-r--r--src/js/javascript.nim1224
1 files changed, 1183 insertions, 41 deletions
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index c728283a..11253875 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -1,4 +1,9 @@
+import macros
 import options
+import sequtils
+import strformat
+import strutils
+import tables
 
 import bindings/quickjs
 
@@ -23,17 +28,69 @@ type
     ctx*: JSContext
     val*: JSValue
 
-  JSContextOpaque* = object
-    err*: string
+  JSContextOpaque* = ref object
+    creg: Table[string, JSClassID]
+    gclaz: string
+    sym_iterator: JSAtom
+    sym_asyncIterator: JSAtom
+    done: JSAtom
+    next: JSAtom
+    value: JSAtom
 
-func newJSRuntime*(): JSRuntime =
+  JSRuntimeOpaque* = ref object
+    plist: Table[pointer, pointer]
+    flist: seq[seq[JSCFunctionListEntry]]
+
+  JSFunctionList* = openArray[JSCFunctionListEntry]
+
+func getOpaque*(ctx: JSContext): JSContextOpaque =
+  return cast[JSContextOpaque](JS_GetContextOpaque(ctx))
+
+func getOpaque*(rt: JSRuntime): JSRuntimeOpaque =
+  return cast[JSRuntimeOpaque](JS_GetRuntimeOpaque(rt))
+
+var runtimes {.threadVar.}: seq[JSRuntime]
+
+proc newJSRuntime*(): JSRuntime =
   result = JS_NewRuntime()
+  runtimes.add(result)
+  var opaque = new(JSRuntimeOpaque)
+  GC_ref(opaque)
+  JS_SetRuntimeOpaque(result, cast[pointer](opaque))
 
 proc newJSContext*(rt: JSRuntime): JSContext =
-  result = JS_NewContext(rt)
-  let opaque = cast[ptr JSContextOpaque](alloc0(sizeof(JSContextOpaque)))
-  opaque.err = ""
-  JS_SetContextOpaque(result, opaque)
+  let ctx = JS_NewContext(rt)
+  var opaque = new(JSContextOpaque)
+  GC_ref(opaque)
+
+  block:
+    let global = JS_GetGlobalObject(ctx)
+    block:
+      let sym = JS_GetPropertyStr(ctx, global, "Symbol")
+      block:
+        let it = JS_GetPropertyStr(ctx, sym, "iterator")
+        assert JS_IsSymbol(it)
+        opaque.sym_iterator = JS_ValueToAtom(ctx, it)
+        JS_FreeValue(ctx, it)
+      block:
+        let ait = JS_GetPropertyStr(ctx, sym, "asyncIterator")
+        assert JS_IsSymbol(ait)
+        opaque.sym_asyncIterator = JS_ValueToAtom(ctx, ait)
+        JS_FreeValue(ctx, ait)
+      block:
+        let s = "done"
+        opaque.done = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len))
+      block:
+        let s = "value"
+        opaque.value = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len))
+      block:
+        let s = "next"
+        opaque.next = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len))
+      JS_FreeValue(ctx, sym)
+    JS_FreeValue(ctx, global)
+
+  JS_SetContextOpaque(ctx, cast[pointer](opaque))
+  return ctx
 
 proc newJSContextRaw*(rt: JSRuntime): JSContext =
   result = JS_NewContextRaw(rt)
@@ -42,16 +99,41 @@ func getJSObject*(ctx: JSContext, v: JSValue): JSObject =
   result.ctx = ctx
   result.val = v
 
-func getJSObject*(ctx: JSContext, argv: ptr JSValue, i: int): JSObject =
-  getJSObject(ctx, cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[])
+func getJSValue*(ctx: JSContext, argv: ptr JSValue, i: int): JSValue =
+  cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[]
 
 func newJSObject*(ctx: JSContext): JSObject =
   result.ctx = ctx
   result.val = JS_NewObject(ctx)
 
-func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int): JSObject =
+func newJSObject*(ctx: JSContext, cid: JSClassID): JSObject =
+  result.ctx = ctx
+  result.val = JS_NewObjectClass(ctx, cid)
+
+func newJSObject*(ctx: JSContext, proto: JSObject): JSObject =
+  result.ctx = ctx
+  result.val = JS_NewObjectProto(ctx, proto.val)
+
+func getClass*(ctx: JSContext, class: string): JSClassID =
+  # This function *should* never fail.
+  ctx.getOpaque().creg[class]
+
+func getClassProto*(ctx: JSContext, cid: JSClassID): JSObject =
+  return JSObject(ctx: ctx, val: JS_GetClassProto(ctx, cid))
+
+func findClass*(ctx: JSContext, class: string): Option[JSClassID] =
+  let opaque = ctx.getOpaque()
+  if class in opaque.creg:
+    return some(opaque.creg[class])
+  return none(JSClassID)
+
+func newJSObject*(ctx: JSContext, class: string): JSObject =
   result.ctx = ctx
-  result.val = JS_NewCFunction(ctx, fun, cstring(name), argc)
+  result.val = JS_NewObjectClass(ctx, ctx.getClass(class))
+
+func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSObject =
+  result.ctx = ctx
+  result.val = JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic))
 
 func getGlobalObject*(ctx: JSContext): JSObject =
   result.ctx = ctx
@@ -65,53 +147,1113 @@ func getProperty*(obj: JSObject, s: string): JSObject =
   result.ctx = obj.ctx
   result.val = JS_GetPropertyStr(obj.ctx, obj.val, cstring(s));
 
-func getOpaque*(ctx: JSContext): ptr JSContextOpaque =
-  return cast[ptr JSContextOpaque](JS_GetContextOpaque(ctx))
+proc free*(ctx: var JSContext) =
+  var opaque = ctx.getOpaque()
+  if opaque != nil:
+    JS_FreeAtom(ctx, opaque.sym_iterator)
+    JS_FreeAtom(ctx, opaque.sym_asyncIterator)
+    JS_FreeAtom(ctx, opaque.done)
+    JS_FreeAtom(ctx, opaque.next)
+    GC_unref(opaque)
+  JS_FreeContext(ctx)
+  ctx = nil
+
+proc free*(rt: var JSRuntime) =
+  let opaque = rt.getOpaque()
+  GC_unref(opaque)
+  JS_FreeRuntime(rt)
+  runtimes.del(runtimes.find(rt))
+  rt = nil
+
+proc free*(obj: JSObject) =
+  JS_FreeValue(obj.ctx, obj.val)
+  #TODO maybe? obj.val = JS_NULL
 
-func toString*(obj: JSObject): Option[string] =
-  var plen: int
-  let outp = JS_ToCStringLen(obj.ctx, addr plen, obj.val) # cstring
+proc setOpaque*[T](obj: JSObject, opaque: T) =
+  let rt = JS_GetRuntime(obj.ctx)
+  let rtOpaque = rt.getOpaque()
+  let p = JS_VALUE_GET_PTR(obj.val)
+  let header = cast[ptr JSRefCountHeader](p)
+  inc header.ref_count # add jsvalue reference
+  rtOpaque.plist[cast[pointer](opaque)] = p
+  JS_SetOpaque(obj.val, cast[pointer](opaque))
+
+func isGlobal*(ctx: JSContext, class: string): bool =
+  assert class != ""
+  return ctx.getOpaque().gclaz == class
+
+# A hack to retrieve a given val's class id.
+# Used when we can't query ctx's hash table.
+func getClassID*(val: JSValue): JSClassID =
+  const index = sizeof(cint) + # gc_ref_count
+              sizeof(uint8) + # gc_mark
+              sizeof(uint8) # bit field
+  return cast[ptr uint16](cast[int](JS_VALUE_GET_PTR(val)) + index)[]
+
+func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer =
+  # Unfortunately, we can't change the global object's class.
+  #TODO: or maybe we can, but I'm afraid of breaking something.
+  # This needs further investigation.
+  if ctx.isGlobal(class):
+    let global = ctx.getGlobalObject()
+    let opaque = JS_GetOpaque(global.val, 1) # JS_CLASS_OBJECT
+    free(global)
+    return opaque
+  return JS_GetOpaque(val, ctx.getClass(class))
+
+func getOpaque*(obj: JSObject, class: string): pointer = getOpaque(obj.ctx, obj.val, class)
+
+proc setInterruptHandler*(rt: JSRuntime, cb: JSInterruptHandler, opaque: pointer = nil) =
+  JS_SetInterruptHandler(rt, cb, opaque)
+
+func toString*(ctx: JSContext, val: JSValue): Option[string] =
+  var plen: csize_t
+  let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring
   if outp != nil:
     var ret = newString(plen)
     for i in 0..<plen:
       ret[i] = outp[i]
     result = some(ret)
-    JS_FreeCString(obj.ctx, outp) # refc
+    JS_FreeCString(ctx, outp)
+
+func toString*(obj: JSObject): Option[string] = toString(obj.ctx, obj.val)
 
 func `$`*(obj: JSObject): string =
   return obj.toString().get("")
 
 func isUndefined*(obj: JSObject): bool = JS_IsUndefined(obj.val)
+func isNull*(obj: JSObject): bool = JS_IsNull(obj.val)
 func isException*(obj: JSObject): bool = JS_IsException(obj.val)
+func isError*(obj: JSObject): bool = JS_IsError(obj.ctx, obj.val)
+
+func isGlobal*(obj: JSObject): bool =
+  let global = obj.ctx.getGlobalObject()
+  result = JS_VALUE_GET_PTR(global.val) == JS_VALUE_GET_PTR(obj.val)
+
+func isInstanceOf*(obj: JSObject, class: string): bool =
+  return obj.getOpaque(class) != nil
 
 proc setProperty*(obj: JSObject, name: string, prop: JSObject) =
-  discard JS_SetPropertyStr(obj.ctx, obj.val, cstring(name), prop.val)
-
-proc setFunctionProperty*(obj: JSObject, name: string, fun: JSCFunction) =
-  obj.setProperty(name, obj.ctx.newJSCFunction(name, fun, 1))
-  #  (proc(ctx: JSContext, obj: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} =
-  #    var invoc: seq[JSValue]
-  #    for i in 0..<argc:
-  #      let arg = cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[]
-  #      invoc.add(arg)
-  #    return fun(JSObject(ctx: ctx, qjs: obj), invoc)
-  #), cstring(name), 1))
+  if JS_SetPropertyStr(obj.ctx, obj.val, cstring(name), prop.val) <= 0:
+    raise newException(Defect, "Failed to set property string: " & name)
 
-proc free*(ctx: var JSContext) =
-  let opaque = ctx.getOpaque()
-  if opaque != nil:
-    dealloc(opaque)
-  JS_FreeContext(ctx)
-  ctx = nil
+proc setProperty*(obj: JSObject, name: string, fun: JSCFunction, argc: int = 0) =
+  obj.setProperty(name, obj.ctx.newJSCFunction(name, fun, argc))
 
-proc free*(rt: var JSRuntime) =
-  JS_FreeRuntime(rt)
-  rt = nil
+func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, funcs: JSFunctionList, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID =
+  let rt = JS_GetRuntime(ctx)
+  discard JS_NewClassID(addr result)
+  var ctxOpaque = ctx.getOpaque()
+  var rtOpaque = rt.getOpaque()
+  ctxOpaque.creg[$cdef.class_name] = result
+  if JS_NewClass(rt, result, cdef) != 0:
+    raise newException(Defect, "Failed to allocate JS class: " & $cdef.class_name)
+  var proto: JSObject
+  if parent != 0:
+    let parentProto = ctx.getClassProto(parent)
+    proto = ctx.newJSObject(parentProto)
+    free(parentProto)
+  else:
+    proto = ctx.newJSObject()
+  if funcs.len > 0:
+    rtOpaque.flist.add(funcs.toSeq())
+    JS_SetPropertyFunctionList(ctx, proto.val, addr rtOpaque.flist[^1][0], cint(funcs.len))
+  JS_SetClassProto(ctx, result, proto.val)
+  if asglobal:
+    let global = ctx.getGlobalObject()
+    assert ctxOpaque.gclaz == ""
+    ctxOpaque.gclaz = $cdef.class_name
+    if JS_SetPrototype(ctx, global.val, proto.val) != 1:
+      raise newException(Defect, "Failed to set global prototype: " & $cdef.class_name)
+    free(global)
+  if addto.issome:
+    let jctor = ctx.newJSCFunction($cdef.class_name, cctor, 0, JS_CFUNC_constructor)
+    JS_SetConstructor(ctx, jctor.val, proto.val)
+    addto.get.setProperty($cdef.class_name, jctor)
 
-proc free*(obj: JSObject) =
-  JS_FreeValue(obj.ctx, obj.val)
-  #TODO maybe? obj.val = JS_NULL
+type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]]
+
+func getMinArgs(params: seq[FuncParam]): int =
+  for i in 0..<params.len:
+    let it = params[i]
+    if it[2].issome:
+      return i
+    let t = it.t
+    if t.kind == nnkBracketExpr:
+      if t.typeKind == varargs.getType().typeKind:
+        assert i == params.high, "Not even nim can properly handle this..."
+        return i
+  return params.len
+
+func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue): Option[T] =
+  when T is int:
+    when sizeof(int) <= sizeof(int32):
+      var ret: int32
+      if JS_ToInt32(ctx, addr ret, val) < 0:
+        return none(T)
+      return some(ret)
+    else:
+      var ret: int64
+      if JS_ToInt64(ctx, addr ret, val) < 0:
+        return none(T)
+      return some(ret)
+  elif T is uint:
+    when sizeof(int) <= sizeof(int32):
+      var ret: uint32
+      if JS_ToUint32(ctx, addr ret, val) < 0:
+        return none(T)
+      return some(ret)
+    else:
+      var ret: uint32
+      if JS_ToUint32(ctx, addr ret, val) < 0:
+        return none(T)
+      return some(cast[uint](ret))
+  elif T is int32:
+    var ret: int32
+    if JS_ToInt32(ctx, addr ret, val) < 0:
+      return none(T)
+    return some(ret)
+  elif T is int64:
+    var ret: int64
+    if JS_ToInt64(ctx, addr ret, val) < 0:
+      return none(T)
+    return some(ret)
+  elif T is uint32:
+    var ret: uint32
+    if JS_ToUint32(ctx, addr ret, val) < 0:
+      return none(T)
+    return some(ret)
+  elif T is uint64:
+    var ret: uint32
+    if JS_ToUint32(ctx, addr ret, val) < 0:
+      return none(T)
+    return some(cast[uint64](ret))
+
+proc fromJS[T](ctx: JSContext, val: JSValue): Option[T]
+
+macro len(t: type tuple): int =
+  let i = t.getType()[1].len - 1 # - tuple
+  newLit(i)
+
+macro fromJSTupleBody(a: tuple) =
+  let len = a.getType().len - 1
+  let done = ident("done")
+  result = newStmtList(quote do:
+    var `done`: Option[bool])
+  for i in 0..<len:
+    result.add(quote do:
+      let next = JS_Call(ctx, next_method, it, 0, nil)
+      if JS_IsException(next):
+        return none(T)
+      defer: JS_FreeValue(ctx, next)
+      let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
+      if JS_IsException(doneVal):
+        return none(T)
+      defer: JS_FreeValue(ctx, doneVal)
+      `done` = fromJS[bool](ctx, doneVal)
+      if `done`.isnone: # exception
+        return none(T)
+      if `done`.get:
+        JS_ThrowTypeError(ctx, "Too few arguments in sequence (got %d, expected %d)", `i`, `len`)
+        return none(T)
+      let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().value)
+      if JS_IsException(valueVal):
+        return none(T)
+      defer: JS_FreeValue(ctx, valueVal)
+      let genericRes = fromJS[typeof(result.get[`i`])](ctx, valueVal)
+      if genericRes.isnone: # exception
+        return none(T)
+      `a`[`i`] = genericRes.get
+    )
+    if i == len - 1:
+      result.add(quote do:
+        var i = `i`
+        # we're simulating a sequence, so we must query all remaining parameters too:
+        while not `done`.get:
+          inc i
+          let next = JS_Call(ctx, next_method, it, 0, nil)
+          if JS_IsException(next):
+            return none(T)
+          defer: JS_FreeValue(ctx, next)
+          let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
+          if JS_IsException(doneVal):
+            return none(T)
+          defer: JS_FreeValue(ctx, doneVal)
+          `done` = fromJS[bool](ctx, doneVal)
+          if `done`.isnone: # exception
+            return none(T)
+          if `done`.get:
+            JS_ThrowTypeError(ctx, "Too many arguments in sequence (got %d, expected %d)", i, `len`)
+            return none(T)
+          JS_FreeValue(ctx, JS_GetProperty(ctx, next, ctx.getOpaque().value))
+      )
+
+proc fromJSTuple[T: tuple](ctx: JSContext, val: JSValue): Option[T] =
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_iterator)
+  if JS_IsException(itprop):
+    return none(T)
+  defer: JS_FreeValue(ctx, itprop)
+  let it = JS_Call(ctx, itprop, val, 0, nil)
+  if JS_IsException(it):
+    return none(T)
+  defer: JS_FreeValue(ctx, it)
+  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().next)
+  if JS_IsException(next_method):
+    return none(T)
+  defer: JS_FreeValue(ctx, next_method)
+  var x: T
+  result = some(x)
+  fromJSTupleBody(result.get)
+
+proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] =
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_iterator)
+  if JS_IsException(itprop):
+    return none(seq[T])
+  defer: JS_FreeValue(ctx, itprop)
+  let it = JS_Call(ctx, itprop, val, 0, nil)
+  if JS_IsException(it):
+    return none(seq[T])
+  defer: JS_FreeValue(ctx, it)
+  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().next)
+  if JS_IsException(next_method):
+    return none(seq[T])
+  defer: JS_FreeValue(ctx, next_method)
+  result = some(newSeq[T]())
+  while true:
+    let next = JS_Call(ctx, next_method, it, 0, nil)
+    if JS_IsException(next):
+      return none(seq[T])
+    defer: JS_FreeValue(ctx, next)
+    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
+    if JS_IsException(doneVal):
+      return none(seq[T])
+    defer: JS_FreeValue(ctx, doneVal)
+    let done = fromJS[bool](ctx, doneVal)
+    if done.isnone: # exception
+      return none(seq[T])
+    if done.get:
+      break
+    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().value)
+    if JS_IsException(valueVal):
+      return none(seq[T])
+    defer: JS_FreeValue(ctx, valueVal)
+    let genericRes = fromJS[typeof(result.get[0])](ctx, valueVal)
+    if genericRes.isnone: # exception
+      return none(seq[T])
+    result.get.add(genericRes.get)
+
+proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] =
+  var ptab: ptr JSPropertyEnum
+  #TODO free ptab
+  var plen: uint32
+  let flags = cint(JS_GPN_STRING_MASK)
+  if JS_GetOwnPropertyNames(ctx, addr ptab, addr plen, val, flags) < -1:
+    # exception
+    return none(Table[A, B])
+  defer:
+    for i in 0..<plen:
+      let prop = cast[ptr JSPropertyEnum](cast[int](ptab) + sizeof(ptab[]) * int(i))
+      JS_FreeAtom(ctx, prop.atom)
+    js_free(ctx, ptab)
+  result = some(Table[A, B]())
+  for i in 0..<plen:
+    let prop = cast[ptr JSPropertyEnum](cast[int](ptab) + sizeof(ptab[]) * int(i))
+    let atom = prop.atom
+    let k = JS_AtomToValue(ctx, atom)
+    defer: JS_FreeValue(ctx, k)
+    let kn = fromJS[A](ctx, k)
+    if kn.isnone: # exception
+      return none(Table[A, B])
+    let v = JS_GetProperty(ctx, val, atom)
+    defer: JS_FreeValue(ctx, v)
+    let vn = fromJS[B](ctx, v)
+    if vn.isnone: # exception
+      return none(Table[A, B])
+    result.get[kn.get] = vn.get
+
+#TODO handle exceptions in all cases
+proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] =
+  when T is string:
+    return toString(ctx, val)
+  elif typeof(result.get) is Option: # unwrap
+    return fromJS[typeof(result.get)](ctx, val)
+  elif T is seq:
+    return fromJSSeq[typeof(result.get[0])](ctx, val)
+  elif T is tuple:
+    return fromJSTuple[T](ctx, val)
+  elif T is bool:
+    let ret = JS_ToBool(ctx, val)
+    if ret == -1: # exception
+      return none(T)
+    if ret == 0:
+      return some(false)
+    return some(true)
+  elif typeof(result.get) is Table:
+    return fromJSTable[typeof(result.get.keys), typeof(result.get.values)](ctx, val)
+  elif T is SomeInteger:
+    return fromJSInt[T](obj.ctx, obj.val)
+  elif T is SomeFloat:
+    let f64: float64
+    if JS_ToFloat64(ctx, addr f64, val) < 0:
+      return none(T)
+    return some(cast[T](f64))
+  else:
+    let op = cast[T](getOpaque(ctx, val, $T))
+    if op == nil:
+      JS_ThrowTypeError(ctx, "Value is not an instance of %s", $T)
+      return none(T)
+    return some(op)
+
+func toJSString(ctx: JSContext, str: string): JSValue =
+  return JS_NewString(ctx, cstring(str))
+
+func toJSInt(ctx: JSContext, n: SomeInteger): JSValue =
+  when n is int:
+    when sizeof(int) <= sizeof(int32):
+      return JS_NewInt32(ctx, n)
+    else:
+      return JS_NewInt64(ctx, n)
+  elif n is uint:
+    when sizeof(uint) <= sizeof(uint32):
+      return JS_NewUint32(ctx, n)
+    else:
+      return JS_NewUint64(ctx, n)
+  elif n is int32:
+    return JS_NewInt32(ctx, n)
+  elif n is int64:
+    return JS_NewInt64(ctx, n)
+  elif n is uint32:
+    return JS_NewUint32(ctx, n)
+  elif n is uint64:
+    return JS_NewUint64(ctx, n)
+
+func toJSNumber(ctx: JSContext, n: SomeNumber): JSValue =
+  when n is SomeInteger:
+    return toJSInt(n)
+  else:
+    return JS_NewFloat64(n)
+
+func toJSBool(ctx: JSContext, b: bool): JSValue =
+  return JS_NewBool(ctx, b)
+
+func toJSObject[T](ctx: JSContext, obj: T, class: string): JSValue =
+  let claz = ctx.findClass(class)
+  assert claz.issome
+  let op = JS_GetRuntime(ctx).getOpaque()
+  let p = cast[pointer](obj)
+  if p in op.plist:
+    # a JSValue already points to this object.
+    return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, op.plist[p]))
+  let jsObj = ctx.newJSObject(claz.get)
+  jsObj.setOpaque(obj)
+  return jsObj.val
+
+proc toJS*[T](ctx: JSContext, obj: T, class = ""): JSValue =
+  when T is string:
+    ctx.toJSString(obj)
+  elif T is SomeNumber:
+    ctx.toJSNumber(obj)
+  elif T is bool:
+    ctx.toJSBool(obj)
+  else:
+    ctx.toJSObject(obj, class)
+
+type
+  JS_Error = object of CatchableError
+
+  JS_SyntaxError* = object of JS_Error
+  JS_TypeError* = object of JS_Error
+  JS_ReferenceError* = object of JS_Error
+  JS_RangeError* = object of JS_Error
+  JS_InternalError* = object of JS_Error
+
+  JSFuncGenerator = object
+    original: NimNode
+    copied: NimNode
+    hasthis: bool
+    funcName: string
+    generics: Table[string, seq[NimNode]]
+    funcParams: seq[FuncParam]
+    thisType: string
+    returnType: Option[NimNode]
+    newName: string
+    newBranchList: seq[NimNode]
+    jsFunCallLists: seq[NimNode]
+    jsFunCallList: NimNode
+    jsFunCall: NimNode
+    jsCallAndRet: NimNode
+    minArgs: int
+    i: int # nim parameters accounted for
+    j: int # js parameters accounted for (not including fix ones, e.g. `this')
+    res: NimNode
+
+var RegisteredFunctions {.compileTime.}: Table[string, seq[(NimNode, NimNode)]]
+
+proc getGenerics(fun: NimNode): Table[string, seq[NimNode]] =
+  var node = fun.findChild(it.kind == nnkBracket)
+  if node.kind == nnkNilLit:
+    return # no bracket
+  node = node.findChild(it.kind == nnkGenericParams)
+  if node.kind == nnkNilLit:
+    return # no generics
+  node = node.findChild(it.kind == nnkIdentDefs)
+  var stack: seq[NimNode]
+  for i in countdown(node.len - 1, 0): stack.add(node[i])
+  var gen_name: NimNode
+  var gen_types: seq[NimNode]
+  template add_gen =
+    if gen_name != nil:
+      assert gen_types.len != 0
+      result[gen_name.strVal] = gen_types
+      gen_types.setLen(0)
+
+  while stack.len > 0:
+    let node = stack.pop()
+    case node.kind
+    of nnkIdent:
+      add_gen
+      gen_name = node
+    of nnkSym:
+      assert gen_name != nil
+      gen_types.add(node)
+    of nnkInfix:
+      assert node[0].eqIdent(ident("|")) or node[0].eqIdent(ident("or")), "Only OR generics are supported."
+      for i in countdown(node.len - 1, 1): stack.add(node[i]) # except infix ident
+    of nnkBracketExpr:
+      gen_types.add(node)
+    else:
+      discard
+  add_gen
+
+proc getParams(fun: NimNode): seq[FuncParam] =
+  let formalParams = fun.findChild(it.kind == nnkFormalParams)
+  var funcParams: seq[FuncParam]
+  var returnType = none(NimNode)
+  if formalParams[0].kind != nnkEmpty:
+    returnType = some(formalParams[0])
+  for i in 1..<fun.params.len:
+    let it = formalParams[i]
+    let name = $it[0]
+    let tt = it[1]
+    let t = if it[1].kind != nnkEmpty:
+      `tt`
+    else:
+      let x = it[2]
+      quote do:
+        typeof(`x`)
+    let val = if it[2].kind != nnkEmpty:
+      some(it[2])
+    else:
+      none(NimNode)
+    var g = none(NimNode)
+    funcParams.add((name, t, val, g))
+  funcParams
+
+proc getReturn(fun: NimNode): Option[NimNode] =
+  let formalParams = fun.findChild(it.kind == nnkFormalParams)
+  if formalParams[0].kind != nnkEmpty:
+    some(formalParams[0])
+  else:
+    none(NimNode)
+
+template getJSParams(): untyped =
+  [
+    (quote do: JSValue),
+    newIdentDefs(ident("ctx"), quote do: JSContext),
+    newIdentDefs(ident("this"), quote do: JSValue),
+    newIdentDefs(ident("argc"), quote do: int),
+    newIdentDefs(ident("argv"), quote do: ptr JSValue)
+  ]
+
+template getJSGetterParams(): untyped =
+  [
+    (quote do: JSValue),
+    newIdentDefs(ident("ctx"), quote do: JSContext),
+    newIdentDefs(ident("this"), quote do: JSValue),
+  ]
+
+template getJSSetterParams(): untyped =
+  [
+    (quote do: JSValue),
+    newIdentDefs(ident("ctx"), quote do: JSContext),
+    newIdentDefs(ident("this"), quote do: JSValue),
+    newIdentDefs(ident("val"), quote do: JSValue),
+  ]
+
+proc addParam2(gen: var JSFuncGenerator, s, t, val: NimNode, fallback: NimNode = nil) =
+  let stmt = quote do:
+    fromJS_or_return(`t`, ctx, `val`)
+  for i in 0..gen.jsFunCallLists.high:
+    if fallback == nil:
+      gen.jsFunCallLists[i].add(newLetStmt(s, stmt))
+    else:
+      let j = gen.j
+      gen.jsFunCallLists[i].add(newLetStmt(s, quote do:
+        if `j` < argc: `stmt` else: `fallback`))
+
+proc addValueParam(gen: var JSFuncGenerator, s, t: NimNode, fallback: NimNode = nil) =
+  let j = gen.j
+  gen.addParam2(s, t, quote do: getJSValue(ctx, argv, `j`), fallback)
+
+proc addFixParam(gen: var JSFuncGenerator, name: string) =
+  let s = ident("arg_" & $gen.i)
+  let t = gen.funcParams[gen.i][1]
+  let id = ident(name)
+  gen.addParam2(s, t, id)
+  if gen.jsFunCall != nil:
+    gen.jsFunCall.add(s)
+  inc gen.i
+
+proc addUnionParamBranch(gen: var JSFuncGenerator, query, newBranch: NimNode, fallback: NimNode = nil) =
+  let i = gen.i
+  let query = if fallback == nil: query else:
+    quote do: (
+      if `i` >= argc:
+       false
+      else:
+       `query`
+    )
+  let newBranch = newStmtList(newBranch)
+  for i in 0..gen.jsFunCallLists.high:
+    var ifstmt = newIfStmt((query, newBranch))
+    let oldBranch = newStmtList()
+    ifstmt.add(newTree(nnkElse, oldBranch))
+    gen.jsFunCallLists[i].add(ifstmt)
+    gen.jsFunCallLists[i] = oldBranch
+  gen.newBranchList.add(newBranch)
+
+proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: NimNode = nil) =
+  # Union types.
+  #TODO lots of types missing
+  let j = gen.j
+  let flattened = gen.generics[tt.strVal] # flattened member types
+  var tableg = none(NimNode)
+  var seqg = none(NimNode)
+  var hasString = false
+  for g in flattened:
+    if g.len > 0 and g[0] == Table.getType():
+      tableg = some(g)
+    elif g.typekind == ntySequence:
+      seqg = some(g)
+    elif g == string.getType():
+      hasString = true
+  # 4. If V is null or undefined, then:
+  if tableg.issome:
+    let a = tableg.get[1]
+    let b = tableg.get[2]
+    gen.addUnionParamBranch(quote do: (
+        let val = getJSValue(ctx, argv, `j`)
+        JS_IsNull(val) or JS_IsUndefined(val)
+      ),
+      quote do:
+        let `s` = Table[`a`, `b`](), #TODO is this correct?
+      fallback)
+  # 10. If Type(V) is Object, then:
+  if tableg.issome or seqg.issome:
+    # Sequence:
+    if seqg.issome:
+      let query = quote do:
+        (
+          let o = getJSValue(ctx, argv, `j`)
+          JS_IsObject(o) and (
+            let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_iterator)
+            if JS_IsException(prop):
+              return JS_EXCEPTION
+            let ret = not JS_IsUndefined(prop)
+            JS_FreeValue(ctx, prop)
+            ret
+          )
+        )
+      let a = seqg.get[1]
+      gen.addUnionParamBranch(query, quote do:
+        let `s` = fromJS_or_return(seq[`a`], ctx, getJSValue(ctx, argv, `j`)),
+        fallback)
+    # Record:
+    if tableg.issome:
+      let a = tableg.get[1]
+      let b = tableg.get[2]
+      let query = quote do:
+        JS_IsObject(getJSValue(ctx, argv, `j`))
+      gen.addUnionParamBranch(query, quote do:
+        let `s` = fromJS_or_return(Table[`a`, `b`], ctx, getJSValue(ctx, argv, `j`)),
+        fallback)
+
+  # 14. If types includes a string type, then return the result of converting V
+  # to that type.
+  # TODO else typeerror
+  gen.addParam2(s, string.getType(), quote do: getJSValue(ctx, argv, `j`), fallback)
+
+  for branch in gen.newBranchList:
+    gen.jsFunCallLists.add(branch)
+  gen.newBranchList.setLen(0)
+
+proc addRequiredParams(gen: var JSFuncGenerator) =
+  let minArgs = gen.funcParams.getMinArgs()
+  while gen.i < minArgs:
+    let s = ident("arg_" & $gen.i)
+    let tt = gen.funcParams[gen.i][1]
+    if tt.typeKind == ntyGenericParam:
+      gen.addUnionParam(tt, s)
+    else:
+      gen.addValueParam(s, tt)
+    if gen.jsFunCall != nil:
+      gen.jsFunCall.add(s)
+    inc gen.j
+    inc gen.i
+
+proc addOptionalParams(gen: var JSFuncGenerator) =
+  while gen.i < gen.funcParams.len:
+    let j = gen.j
+    let s = ident("arg_" & $gen.i)
+    let tt = gen.funcParams[gen.i][1]
+    if tt.typeKind == varargs.getType().typeKind: # pray it's not a generic...
+      let vt = tt[1].getType()
+      for i in 0..gen.jsFunCallLists.high:
+        gen.jsFunCallLists[i].add(newLetStmt(s, quote do:
+          (
+            var valist: seq[`vt`]
+            for i in `j`..<argc:
+              let it = fromJS_or_return(`vt`, ctx, getJSValue(ctx, argv, i))
+              valist.add(it)
+            valist
+          )
+        ))
+    else:
+      let fallback = gen.funcParams[gen.i][2].get
+      if tt.typeKind == ntyGenericParam:
+        gen.addUnionParam(tt, s, fallback)
+      else:
+        gen.addValueParam(s, tt, fallback)
+    if gen.jsFunCall != nil:
+      gen.jsFunCall.add(s)
+    inc gen.j
+    inc gen.i
+
+proc finishFunCallList(gen: var JSFuncGenerator) =
+  for branch in gen.jsFunCallLists:
+    branch.add(gen.jsFunCall)
+
+var js_funcs {.compileTime.}: Table[string, JSFuncGenerator]
+
+proc registerFunction(gen: JSFuncGenerator) =
+  if gen.thisType notin RegisteredFunctions:
+    RegisteredFunctions[gen.thisType] = @[(newStrLitNode(gen.funcName), ident(gen.newName))]
+  else:
+    RegisteredFunctions[gen.thisType].add((newStrLitNode(gen.funcName), ident(gen.newName)))
+  js_funcs[gen.funcName] = gen
+
+var js_errors {.compileTime.}: Table[string, seq[string]]
+
+export JS_ThrowTypeError, JS_ThrowRangeError, JS_ThrowSyntaxError,
+       JS_ThrowInternalError, JS_ThrowReferenceError
+
+proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
+  let tt = gen.thisType
+  let fn = gen.funcName
+  let ma = if gen.hasthis: gen.minArgs - 1 else: gen.minArgs
+  assert ma >= 0
+  result = newStmtList()
+  if isva:
+    result.add(quote do: 
+      if argc < `ma`:
+        return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed", `ma`, argc)
+    )
+  if gen.hasthis:
+    result.add(quote do:
+      var this = JSObject(ctx: ctx, val: this)
+      if not (this.isUndefined() or ctx.isGlobal(`tt`)) and not this.isInstanceOf(`tt`):
+        # undefined -> global.
+        return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `tt`)
+    )
+
+  if gen.funcName in js_errors:
+    var tryWrap = newNimNode(nnkTryStmt)
+    tryWrap.add(gen.jsCallAndRet)
+    for error in js_errors[gen.funcName]:
+      let ename = ident("JS_" & error)
+      var exceptBranch = newNimNode(nnkExceptBranch)
+      let eid = ident("e")
+      exceptBranch.add(newNimNode(nnkInfix).add(ident("as"), ename, eid))
+      let throwName = ident("JS_Throw" & error)
+      exceptBranch.add(newCall(throwName,
+                               ident("ctx"),
+                               newLit("%s"),
+                               newCall(ident("cstring"),
+                                       newDotExpr(eid, ident("msg")))))
+      tryWrap.add(exceptBranch)
+    gen.jsCallAndRet = tryWrap
+  result.add(gen.jsCallAndRet)
+
+proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true): NimNode =
+  let jsBody = gen.newJSProcBody(isva)
+  let jsPragmas = newNimNode(nnkPragma).add(ident("cdecl"))
+  result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas)
+  gen.res = result
+
+# Note: this causes the entire nim function body to be inlined inside the JS
+# interface function.
+#TODO: implement actual inlining (so we can e.g. get rid of JS_Error, use format strings, etc.)
+macro JS_THROW*(a: untyped, b: string) =
+  let es = ident("JS_" & a.strVal)
+  result = quote do:
+    block when_js:
+      raise newException(`es`, `b`)
+
+proc setupGenerator(fun: NimNode, hasthis = true, hasfuncall = true): JSFuncGenerator =
+  result.funcName = $fun[0]
+  if result.funcName == "$":
+    # stringifier
+    result.funcName = "toString"
+  result.generics = getGenerics(fun)
+  result.funcParams = getParams(fun)
+  result.returnType = getReturn(fun)
+  result.minArgs = result.funcParams.getMinArgs()
+  result.original = fun
+  result.hasthis = hasthis
+  result.jsFunCallList = newStmtList()
+  result.jsFunCallLists.add(result.jsFunCallList)
+  if hasfuncall:
+    result.jsFunCall = newCall(fun[0])
+
+# this might be pretty slow...
+#TODO ideally we wouldn't need separate functions at all. Not sure how that
+# could be achieved, maybe using options?
+proc rewriteExceptions(gen: var JSFuncGenerator, errors: var seq[string], node: NimNode) =
+  for i in countdown(node.len - 1, 0):
+    let c = node[i]
+    if c.kind == nnkCommand and c[0].eqIdent ident("JS_THROW"):
+      if gen.copied == nil:
+        gen.copied = copy(gen.original)
+      node[i] = newNimNode(nnkReturnStmt).add(newNilLit())
+      if c[1].strVal notin errors:
+        errors.add(c[1].strVal)
+    elif c.len > 0:
+      gen.rewriteExceptions(errors, c)
+  
+proc rewriteExceptions(gen: var JSFuncGenerator) =
+  let ostmts = gen.original.findChild(it.kind == nnkStmtList)
+  var errors: seq[string]
+  gen.rewriteExceptions(errors, ostmts)
+  assert gen.copied != nil
+  var name: string
+  if gen.copied[0].kind == nnkIdent:
+    name = gen.copied[0].strVal
+  elif gen.copied[0].kind == nnkPostfix:
+    name = gen.copied[0][1].strVal
+  else:
+    assert false, "No JS_THROW statement found in proc with jserr pragma."
+  name &= "_exceptions"
+  gen.copied[0] = ident(name)
+  js_errors[name] = errors
+
+macro jserr*(fun: untyped) =
+  var gen: JSFuncGenerator
+  gen.original = fun
+  gen.rewriteExceptions()
+  var pragma = gen.original.findChild(it.kind == nnkPragma)
+  for i in 0..<pragma.len:
+    if pragma[i] == ident("jsctor"):
+      pragma.del(i)
+  gen.copied.addPragma(quote do: inline)
+
+  result = newStmtList(gen.original, gen.copied)
+
+macro jsctor*(fun: typed) =
+  var gen = setupGenerator(fun, hasthis = false)
+  if gen.funcName in js_funcs:
+    #TODO TODO TODO implement function overloading
+    error("Function overloading hasn't been implemented yet...")
+    result = newStmtList(fun)
+  else:
+    if gen.returnType.get.kind == nnkRefTy:
+      gen.thisType = gen.returnType.get[0].strVal
+    else:
+      gen.thisType = gen.returnType.get.strVal
+    gen.newName = "js_new_" & gen.thisType & '_' & gen.funcName
+    gen.addRequiredParams()
+    gen.addOptionalParams()
+    gen.finishFunCallList()
+    let jfcl = gen.jsFunCallList
+    let tt = gen.thisType
+    gen.jsCallAndRet = quote do:
+      return ctx.toJS((`jfcl`), `tt`)
+    discard gen.newJSProc(getJSParams())
+    gen.registerFunction()
+    result = newStmtList(fun)
+
+macro jsget*(fun: typed) =
+  var gen = setupGenerator(fun)
+  gen.newName = "js_get_" & gen.funcParams[0][0] & '_' & gen.funcName
+  gen.thisType = $gen.funcParams[0][1]
+  if gen.minArgs != 1 or gen.funcParams.len != gen.minArgs:
+    error("jsget functions must accept one parameter")
+  if gen.returnType.isnone:
+    error("jsget functions must have a return type")
+  gen.addFixParam("this")
+  gen.finishFunCallList()
+  let jfcl = gen.jsFunCallList
+  let tt = gen.thisType
+  gen.jsCallAndRet = quote do:
+    return ctx.toJS(`jfcl`, `tt`)
+  let jsProc = gen.newJSProc(getJSGetterParams(), false)
+  gen.registerFunction()
+  result = newStmtList(fun, jsProc)
+
+macro jsset*(fun: typed) =
+  var gen = setupGenerator(fun)
+  gen.newName = "js_set_" & gen.funcParams[0][0] & '_' & gen.funcName
+  gen.thisType = $gen.funcParams[0][1]
+  if gen.minArgs != 2 or gen.funcParams.len != gen.minArgs:
+    error("jsset functions must accept two parameters")
+  if gen.returnType.issome:
+    error("jsset functions must not have a return type")
+  gen.addFixParam("this")
+  gen.addFixParam("val")
+  gen.finishFunCallList()
+  let jfcl = gen.jsFunCallList
+  # Ideally we could simulate JS setters using nim setters, but nim setters
+  # won't accept types that don't match their reflected field's type.
+  gen.jsCallAndRet = quote do:
+    `jfcl`
+    return JS_DupValue(ctx, val)
+  let jsProc = gen.newJSProc(getJSSetterParams(), false)
+  gen.registerFunction()
+  result = newStmtList(fun, jsProc)
+
+macro jsfunc*(fun: typed) =
+  var gen = setupGenerator(fun)
+  gen.newName = "js_"
+  if gen.funcParams.len > 0:
+    gen.newName &= gen.funcParams[0][0] & "_"
+  gen.newName &= gen.funcName
+  assert gen.minArgs > 0 #TODO support zero-param (=global, no parent object)
+  gen.thisType = $gen.funcParams[0][1]
+  gen.addFixParam("this")
+  gen.addRequiredParams()
+  gen.addOptionalParams()
+  gen.finishFunCallList()
+  let jfcl = gen.jsFunCallList
+  let tt = gen.thisType
+  gen.jsCallAndRet = if gen.returnType.issome:
+    quote do:
+      return ctx.toJS(`jfcl`, `tt`)
+  else:
+    quote do:
+      `jfcl`
+      return JS_UNDEFINED
+  let jsProc = gen.newJSProc(getJSParams())
+  gen.registerFunction()
+  result = newStmtList(fun, jsProc)
+
+#WARNING footgun ahead: for some reason, these must be declared *after* their
+# macro counterparts, or they override the macros.
+template jsget*() {.pragma.}
+template jsset*() {.pragma.}
+
+proc findPragmas(otab: var Table[string, seq[NimNode]], t: NimNode) =
+  let typ = t.getTypeInst()[1] # The type, as declared.
+  var impl = typ.getTypeImpl() # ref t
+  assert impl.kind == nnkRefTy, "Only ref nodes are supported..."
+  impl = impl[0].getImpl()
+  # stolen from std's macros.customPragmaNode
+  var identDefsStack = newSeq[NimNode](impl[2].len)
+  for i in 0..<identDefsStack.len: identDefsStack[i] = impl[2][i]
+  while identDefsStack.len > 0:
+    var identDefs = identDefsStack.pop()
+
+    case identDefs.kind
+    of nnkRecList:
+      for child in identDefs.children:
+        identDefsStack.add(child)
+    of nnkRecCase:
+      # Add condition definition
+      identDefsStack.add(identDefs[0])
+      # Add branches
+      for i in 1 ..< identDefs.len:
+        identDefsStack.add(identDefs[i].last)
+    else:
+      for i in 0 .. identDefs.len - 3:
+        let varNode = identDefs[i]
+        if varNode.kind == nnkPragmaExpr:
+          var varName = varNode[0]
+          if varName.kind == nnkPostfix:
+            # This is a public field. We are skipping the postfix *
+            varName = varName[1]
+          for pragma in varNode[1]:
+            let pragmaName = ($pragma).tolower()
+            if pragmaName in otab: #TODO this isn't style sensitive...
+              otab[pragmaName].add(varName)
+
+proc nim_finalize_for_js[T](obj: T) =
+  for rt in runtimes:
+    let rtOpaque = rt.getOpaque()
+    if tables.hasKey(rtOpaque.plist, cast[pointer](obj)):
+      let p = rtOpaque.plist[cast[pointer](obj)]
+      let val = JS_MKPTR(JS_TAG_OBJECT, p)
+      let header = cast[ptr JSRefCountHeader](p)
+      if header.ref_count > 1:
+        # References to this value still exist in JS, so we
+        # * copy the opaque's value
+        # * increase the new value's refcount by 1
+        # * set the new value as the new opaque
+        # * add the new value to the pointer table
+        # Now it's on JS to decrement the new object's refcount.
+        # (Yeah, kind of an ugly hack. But it starts to look better when
+        # the alternative is writing a cycle collector...)
+        let newop = new(T)
+        newop[] = obj[]
+        GC_ref(newop)
+        let np = cast[pointer](newop)
+        JS_SetOpaque(val, np)
+        rtOpaque.plist[np] = p
+      else:
+        # This was the last reference to the JS value.
+        # Clear val's opaque so our refcount isn't decreased again.
+        JS_SetOpaque(val, nil)
+      tables.del(rtOpaque.plist, cast[pointer](obj))
+      # Decrement jsvalue's refcount. This is needed in both cases to
+      # trigger the JS finalizer and free the JS value.
+      JS_FreeValueRT(rt, val)
+
+proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} =
+  return JS_ThrowTypeError(ctx, "Illegal constructor")
+
+template fromJS_or_return*(t, ctx, val: untyped): untyped =
+  (
+    let x = fromJS[t](ctx, val)
+    if x.isnone:
+      return JS_EXCEPTION
+    x.get
+  )
+
+macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID =
+  let s = t.strVal
+  var sctr = ident("js_illegal_ctor")
+  var sfin = ident("js_" & s & "ClassFin")
+  var ctorFun: NimNode
+  var ctorImpl: NimNode
+  var setters, getters: Table[string, NimNode]
+  var pragmas: Table[string, seq[NimNode]]
+  pragmas["jsget"] = @[]
+  pragmas["jsset"] = @[]
+  pragmas.findPragmas(t)
+  result = newStmtList()
+  #TODO use magic functions instead
+  for node in pragmas["jsget"]:
+    let id = ident("js_get_" & s & "_" & $node)
+    let fn = $node
+    result.add(quote do:
+      proc `id`(ctx: JSContext, this: JSValue): JSValue =
+        if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not JSObject(ctx: ctx, val: this).isInstanceOf(`s`):
+          # undefined -> global.
+          return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`)
+        let arg_0 = fromJS_or_return(`t`, ctx, this)
+        return toJS(ctx, arg_0.`node`, $typeof(arg_0.`node`))
+    )
+    RegisteredFunctions[s].add((node, id))
+  for node in pragmas["jsset"]:
+    let id = ident("js_set_" & s & "_" & $node)
+    let fn = $node
+    result.add(quote do:
+      proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue =
+        var this = JSObject(ctx: ctx, val: this)
+        if not (this.isUndefined() or ctx.isGlobal(`s`)) and not this.isInstanceOf(`s`):
+          # undefined -> global.
+          return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`)
+        let arg_0 = fromJS[`t`](this)
+        let arg_1 = JSObject(ctx: ctx, val: val)
+        arg_0.`node` = fromJS[typeof(arg_0.`node`)](arg_1)
+        return JS_DupValue(ctx, arg_1.val)
+    )
+    RegisteredFunctions[s].add((node, id))
+  let tabList = newNimNode(nnkBracket)
+  for fun in RegisteredFunctions[s]:
+    let f0 = fun[0]
+    let f1 = fun[1]
+    if f1.strVal.startsWith("js_new"):
+      ctorImpl = js_funcs[$f0].res
+      if ctorFun != nil:
+        error("Class " & $s & " has 2+ constructors.")
+      ctorFun = f1
+    elif f1.strVal.startsWith("js_get"):
+      getters[$f0] = f1
+    elif f1.strVal.startsWith("js_set"):
+      setters[$f0] = f1
+    else:
+      tabList.add(quote do:
+        JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
+  for k, v in getters:
+    if k in setters:
+      let s = setters[k]
+      tabList.add(newCall((quote do: JS_CGETSET_DEF),
+                          newLit(k),
+                          newNimNode(nnkCast).add(quote do: JSGetterFunction,
+                                                  v),
+                          newNimNode(nnkCast).add(quote do: JSSetterFunction,
+                                                  s)))
+    else:
+      tabList.add(newCall((quote do: JS_CGETSET_DEF),
+                          newLit(k),
+                          newNimNode(nnkCast).add(quote do: JSGetterFunction,
+                                                  v),
+                          newNilLit()))
+  for k, v in setters:
+    if k notin getters:
+      tabList.add(newCall((quote do: JS_CGETSET_DEF),
+                          newLit(k),
+                          newNilLit(),
+                          newNimNode(nnkCast).add(quote do: JSSetterFunction,
+                                                  v)))
+
+  if ctorFun != nil:
+    sctr = ctorFun
+    result.add(ctorImpl)
+
+  result.add(quote do:
+    proc `sfin`(rt: JSRuntime, val: JSValue) {.cdecl.} =
+      let opaque = JS_GetOpaque(val, val.getClassID())
+      if opaque != nil:
+        # This means the nim value is no longer referenced by anything but this
+        # JSValue. Meaning we can just unref and remove it from the pointer
+        # table.
+        GC_unref(cast[`t`](opaque))
+        let rtOpaque = rt.getOpaque()
+        rtOpaque.plist.del(opaque)
+  )
+
+  result.add(quote do:
+    block:
+      # See the definition of `new':
+      # > **Note**:
+      # > The `finalizer` refers to the type `T`, not to the object!
+      # > This means that for each object of type `T` the finalizer will be
+      # > called!
+      # We exploit this by setting a finalizer here, which can then unregister
+      # any associated JS object from all relevant runtimes.
+      var x: `t`
+      new(x, nim_finalize_for_js)
+      const classDef = JSClassDef(class_name: `s`, finalizer: `sfin`)
+      `ctx`.newJSClass(JSClassDefConst(unsafeAddr classDef), `sctr`, `tabList`, `parent`, `asglobal`, `addto`)
+  )
+
+proc getMemoryUsage*(rt: JSRuntime): string =
+  var m: JSMemoryUsage
+  JS_ComputeMemoryUsage(rt, addr m)
+  result = fmt"""
+memory allocated: {m.malloc_count} {m.malloc_size} ({float(m.malloc_size)/float(m.malloc_count):.1f}/block)
+memory used: {m.memory_used_count} {m.memory_used_size} ({float(m.malloc_size-m.memory_used_size)/float(m.memory_used_count):.1f} average slack)
+atoms: {m.atom_count} {m.atom_size} ({float(m.atom_size)/float(m.atom_count):.1f}/atom)
+strings: {m.str_count} {m.str_size} ({float(m.str_size)/float(m.str_count):.1f}/string)
+objects: {m.obj_count} {m.obj_size} ({float(m.obj_size)/float(m.obj_count):.1f}/object)
+properties: {m.prop_count} {m.prop_size} ({float(m.prop_size)/float(m.obj_count):.1f}/object)
+shapes: {m.shape_count} {m.shape_size} ({float(m.shape_size)/float(m.shape_count):.1f}/shape)
+js functions: {m.js_func_count} {m.js_func_size} ({float(m.js_func_size)/float(m.js_func_count):.1f}/function)
+native functions: {m.c_func_count}
+arrays: {m.array_count}
+fast arrays: {m.fast_array_count}
+fast array elements: {m.fast_array_elements} {m.fast_array_elements*sizeof(JSValue)} ({float(m.fast_array_elements)/float(m.fast_array_count):.1f})
+binary objects: {m.binary_object_count} {m.binary_object_size}"""
 
 proc eval*(ctx: JSContext, s: string, file: string, eval_flags: int): JSObject =
   result.ctx = ctx
-  result.val = JS_Eval(ctx, cstring(s), s.len, cstring(file), eval_flags)
+  result.val = JS_Eval(ctx, cstring(s), cint(s.len), cstring(file), cint(eval_flags))