about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-05-03 01:41:38 +0200
committerbptato <nincsnevem662@gmail.com>2024-05-03 01:58:12 +0200
commit970378356d0d7239b332baa37470455391b5e6e4 (patch)
tree87d93162295b12652137193982c5b3c88e1a3758
parentc48f2caedabbcda03724c43935f4175aac3ecf90 (diff)
downloadchawan-970378356d0d7239b332baa37470455391b5e6e4.tar.gz
js: fix various leaks etc.
Previously we didn't actually free the main JS runtime, probably because
you can't do this without first waiting for JS to unwind the stack.
(This has the unfortunate effect that code now *can* run after quit().
TODO: find a fix for this.)

This isn't a huge problem per se, we only have one of these and the OS
can clean it up.  However, it also disabled the JS_FreeRuntime leak
check, which resulted in sieve-like behavior (manual refcounting is
a pain).

So now we choose the other tradeoff: quit no longer runs exitnow, but
it waits for the event loop to run to the end and only then exits the
browser.  Then, before exit we free the JS context & runtime, and also
all JS values allocated by config.

Fixes:

* fix `ad' flag not being set for just one siteconf/omnirule
* fix various leaks (since leak check is enabled now)
* use ptr UncheckedArray[JSValue] for QJS bindings that take an array
* allow JSAtom in jsgetprop etc., also disallow int types other than
  uint32
* do not set a destructor for globals
-rw-r--r--res/chawan.html6
-rw-r--r--res/config.toml2
-rw-r--r--src/bindings/quickjs.nim25
-rw-r--r--src/config/config.nim9
-rw-r--r--src/config/toml.nim3
-rw-r--r--src/html/dom.nim37
-rw-r--r--src/html/env.nim6
-rw-r--r--src/html/event.nim17
-rw-r--r--src/io/dynstream.nim6
-rw-r--r--src/js/console.nim4
-rw-r--r--src/js/domexception.nim2
-rw-r--r--src/js/fromjs.nim112
-rw-r--r--src/js/javascript.nim94
-rw-r--r--src/js/jsutils.nim9
-rw-r--r--src/js/opaque.nim91
-rw-r--r--src/js/tojs.nim39
-rw-r--r--src/local/client.nim77
-rw-r--r--src/local/pager.nim44
-rw-r--r--test/js/class.html30
-rw-r--r--test/js/encode_decode.html57
20 files changed, 360 insertions, 310 deletions
diff --git a/res/chawan.html b/res/chawan.html
index 6ce6950b..07c76f84 100644
--- a/res/chawan.html
+++ b/res/chawan.html
@@ -68,9 +68,9 @@ up/down by one row
 <li><kbd>C-f</kbd>, <kbd>C-b</kbd> (or <kbd>PgDn</kbd>, <kbd>PgUp</kbd>)</kbd>:
 scroll up/down by an entire page
 <li><kbd>{number}G</kbd> (or <kbd>{number}gg</kbd>): jump to {number}'th line
-<li><kbd>g0<kbd>: jump to first character of the current line's visible part
-<li><kbd>gc<kbd>: jump to center of the current line's visible part
-<li><kbd>g$<kbd>: jump to last character of the current line's visible part
+<li><kbd>g0</kbd>: jump to first character of the current line's visible part
+<li><kbd>gc</kbd>: jump to center of the current line's visible part
+<li><kbd>g$</kbd>: jump to last character of the current line's visible part
 <li><kbd>{</kbd>, <kbd>}</kbd>: move cursor to the previous/next paragraph
 <li><kbd>-</kbd>, <kbd>+</kbd> (or <kbd>zh</kbd>, <kbd>zl</kbd>): shift
 screen to the left/right by one cell
diff --git a/res/config.toml b/res/config.toml
index a33b187b..21514c63 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -287,7 +287,7 @@ force-pixels-per-line = false
 
 [[omnirule]]
 match = '^ddg:'
-substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + encodeURIComponent(x.split(":").slice(1).join(":"))'
+substitute-url = 'x => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + encodeURIComponent(x.split(":").slice(1).join(":"))'
 
 [page]
 # buffer commands
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim
index 164f7dad..7a3eee54 100644
--- a/src/bindings/quickjs.nim
+++ b/src/bindings/quickjs.nim
@@ -80,7 +80,8 @@ type
   JSCFunction* = proc(ctx: JSContext; this_val: JSValue; argc: cint;
       argv: ptr UncheckedArray[JSValue]): JSValue {.cdecl.}
   JSCFunctionData* = proc(ctx: JSContext; this_val: JSValue; argc: cint;
-    argv: ptr JSValue; magic: cint; func_data: ptr JSValue): JSValue {.cdecl.}
+    argv: ptr UncheckedArray[JSValue]; magic: cint;
+    func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.}
   JSGetterFunction* = proc(ctx: JSContext; this_val: JSValue): JSValue {.cdecl.}
   JSSetterFunction* = proc(ctx: JSContext; this_val, val: JSValue):
     JSValue {.cdecl.}
@@ -90,7 +91,7 @@ type
     magic: cint): JSValue {.cdecl.}
   JSInterruptHandler* = proc(rt: JSRuntime; opaque: pointer): cint {.cdecl.}
   JSClassID* = uint32
-  JSAtom* = uint32
+  JSAtom* = distinct uint32
   JSClassFinalizer* = proc(rt: JSRuntime; val: JSValue) {.cdecl.}
   JSClassCheckDestroy* = proc(rt: JSRuntime; val: JSValue): JS_BOOL {.cdecl.}
   JSClassGCMark* = proc(rt: JSRuntime; val: JSValue; mark_func: JS_MarkFunc)
@@ -100,8 +101,8 @@ type
     module_name: cstringConst; opaque: pointer): cstring {.cdecl.}
   JSModuleLoaderFunc* = proc(ctx: JSContext; module_name: cstringConst,
     opaque: pointer): JSModuleDef {.cdecl.}
-  JSJobFunc* = proc(ctx: JSContext; argc: cint; argv: ptr JSValue): JSValue
-    {.cdecl.}
+  JSJobFunc* = proc(ctx: JSContext; argc: cint;
+    argv: ptr UncheckedArray[JSValue]): JSValue {.cdecl.}
   JSGCObjectHeader* {.importc, header: qjsheader.} = object
   JSFreeArrayBufferDataFunc* = proc(rt: JSRuntime; opaque, p: pointer) {.cdecl.}
 
@@ -185,7 +186,7 @@ type
     base: cint
 
   JSCFunctionListEntryPropList = object
-    tab: ptr JSCFunctionListEntry
+    tab: ptr UncheckedArray[JSCFunctionListEntry]
     len: cint
 
   JSCFunctionListEntryU* {.union.} = object
@@ -372,8 +373,8 @@ proc JS_NewObjectClass*(ctx: JSContext; class_id: JSClassID): JSValue
 proc JS_NewObjectProto*(ctx: JSContext; proto: JSValue): JSValue
 proc JS_NewObjectProtoClass*(ctx: JSContext; proto: JSValue;
   class_id: JSClassID): JSValue
-proc JS_NewPromiseCapability*(ctx: JSContext; resolving_funcs: ptr JSValue):
-  JSValue
+proc JS_NewPromiseCapability*(ctx: JSContext;
+  resolving_funcs: ptr UncheckedArray[JSValue]): JSValue
 proc JS_SetOpaque*(obj: JSValue; opaque: pointer)
 proc JS_GetOpaque*(obj: JSValue; class_id: JSClassID): pointer
 proc JS_GetOpaque2*(ctx: JSContext; obj: JSValue; class_id: JSClassID): pointer
@@ -422,7 +423,7 @@ proc JS_FreeAtomRT*(rt: JSRuntime; atom: JSAtom)
 proc JS_NewCFunction2*(ctx: JSContext; cfunc: JSCFunction; name: cstring;
   length: cint; proto: JSCFunctionEnum; magic: cint): JSValue
 proc JS_NewCFunctionData*(ctx: JSContext; cfunc: JSCFunctionData;
-  length, magic, data_len: cint; data: ptr JSValue): JSValue
+  length, magic, data_len: cint; data: ptr UncheckedArray[JSValue]): JSValue
 proc JS_NewCFunction*(ctx: JSContext; cfunc: JSCFunction; name: cstring;
   length: cint): JSValue
 
@@ -453,13 +454,13 @@ proc JS_GetOwnPropertyNames*(ctx: JSContext;
 proc JS_GetOwnProperty*(ctx: JSContext; desc: ptr JSPropertyDescriptor;
   obj: JSValue; prop: JSAtom): cint
 proc JS_Call*(ctx: JSContext; func_obj, this_obj: JSValue; argc: cint;
-  argv: ptr JSValue): JSValue
+  argv: ptr UncheckedArray[JSValue]): JSValue
 proc JS_NewObjectFromCtor*(ctx: JSContext; ctor: JSValue;
   class_id: JSClassID): JSValue
 proc JS_Invoke*(ctx: JSContext; this_obj: JSValue; atom: JSAtom; argc: cint;
-  argv: ptr JSValue): JSValue
+  argv: ptr UncheckedArray[JSValue]): JSValue
 proc JS_CallConstructor*(ctx: JSContext; func_obj: JSValue; argc: cint;
-  argv: ptr JSValue): JSValue
+  argv: ptr UncheckedArray[JSValue]): JSValue
 
 proc JS_DefineProperty*(ctx: JSContext; this_obj: JSValue; prop: JSAtom;
   val, getter, setter: JSValue; flags: cint): cint
@@ -544,7 +545,7 @@ proc JS_GetImportMeta*(ctx: JSContext; m: JSModuleDef): JSValue
 proc JS_GetModuleName*(ctx: JSContext; m: JSModuleDef): JSAtom
 
 proc JS_EnqueueJob*(ctx: JSContext; job_func: JSJobFunc; argc: cint;
-  argv: ptr JSValue): cint
+  argv: ptr UncheckedArray[JSValue]): cint
 proc JS_IsJobPending*(rt: JSRuntime): JS_BOOL
 proc JS_ExecutePendingJob*(rt: JSRuntime; pctx: ptr JSContext): cint
 
diff --git a/src/config/config.nim b/src/config/config.nim
index 56c448d7..b38d3129 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -131,6 +131,7 @@ type
 
   Config* = ref object
     jsctx: JSContext
+    jsvfns*: seq[JSValueFunction]
     configdir {.jsget.}: string
     `include` {.jsget.}: seq[ChaPathResolved]
     start* {.jsget.}: StartConfig
@@ -363,7 +364,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
     k: string) =
   typeCheck(v, tvtTable, k)
   for fk, fv in x.fieldPairs:
-    when typeof(fv) isnot JSContext:
+    when typeof(fv) isnot JSContext|seq[JSValueFunction]:
       let kebabk = snakeToKebabCase(fk)
       if kebabk in v:
         let kkk = if k != "":
@@ -597,6 +598,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var JSValueFunction;
   if not JS_IsFunction(ctx.config.jsctx, fun):
     raise newException(ValueError, k & " is not a function")
   x = JSValueFunction(fun: fun)
+  ctx.config.jsvfns.add(x) # so we can clean it up on exit
 
 proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
     v: TomlValue; k: string) =
@@ -667,8 +669,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
     if vv.t == tvtTable:
       ctx.parseConfigValue(x, vv, kkk)
     else: # tvtString
-      # skip initial "cmd.", we don't need it
-      x.init.add((kkk.substr("cmd.".len), vv.s))
+      x.init.add((kkk, vv.s))
 
 type ParseConfigResult* = object
   success*: bool
@@ -797,9 +798,11 @@ proc initCommands*(config: Config): Err[string] =
     if JS_IsException(fun):
       return err(ctx.getExceptionMsg())
     if not JS_IsFunction(ctx, fun):
+      JS_FreeValue(ctx, fun)
       return err(k & " is not a function")
     ctx.definePropertyE(objIt, name, JS_DupValue(ctx, fun))
     config.cmd.map[k] = fun
+    JS_FreeValue(ctx, objIt)
   config.cmd.jsObj = JS_DupValue(ctx, obj)
   config.cmd.init = @[]
   ok()
diff --git a/src/config/toml.nim b/src/config/toml.nim
index f8ab9a08..992a0cbc 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -369,12 +369,11 @@ proc consumeNoState(state: var TomlParser): Result[bool, TomlError] =
             let key = table.key.join('.')
             return state.err("re-definition of node " & key &
               " as table array (was " & $last.t & ")")
-          last.ad = true
           let val = TomlValue(t: tvtTable, tab: table)
           last.a.add(val)
         else:
           let val = TomlValue(t: tvtTable, tab: table)
-          let last = TomlValue(t: tvtArray, a: @[val])
+          let last = TomlValue(t: tvtArray, a: @[val], ad: true)
           node.map[table.key[^1]] = last
       state.currkey = table.key
       state.node = table
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 6f57960a..739d01fe 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -25,6 +25,7 @@ import js/domexception
 import js/error
 import js/fromjs
 import js/javascript
+import js/jsutils
 import js/opaque
 import js/propertyenumlist
 import js/timeout
@@ -370,7 +371,6 @@ jsDestructor(Navigator)
 jsDestructor(PluginArray)
 jsDestructor(MimeTypeArray)
 jsDestructor(Screen)
-jsDestructor(Window)
 
 jsDestructor(Element)
 jsDestructor(HTMLElement)
@@ -1276,21 +1276,22 @@ const SupportedTokensMap = {
     "next", "pingback", "preconnect", "prefetch", "preload", "search",
     "stylesheet"
   ]
-}.toTable()
+}
 
 func supports(tokenList: DOMTokenList; token: string):
     JSResult[bool] {.jsfunc.} =
   let localName = tokenList.element.document.toStaticAtom(tokenList.localName)
-  if localName in SupportedTokensMap:
-    let lowercase = token.toLowerAscii()
-    return ok(lowercase in SupportedTokensMap[localName])
+  for it in SupportedTokensMap:
+    if it[0] == localName:
+      let lowercase = token.toLowerAscii()
+      return ok(lowercase in it[1])
   return err(newTypeError("No supported tokens defined for attribute"))
 
 func value(tokenList: DOMTokenList): string {.jsfget.} =
   return $tokenList
 
-func getter(tokenList: DOMTokenList; i: int): Option[string] {.jsgetprop.} =
-  return tokenList.item(i)
+func getter(tokenList: DOMTokenList; i: uint32): Option[string] {.jsgetprop.} =
+  return tokenList.item(int(i))
 
 # DOMStringMap
 func validateAttributeName(name: string): Err[DOMException] =
@@ -1352,15 +1353,15 @@ func names(ctx: JSContext; map: ptr DOMStringMap): JSPropertyEnumList
 func length(nodeList: NodeList): uint32 {.jsfget.} =
   return uint32(nodeList.len)
 
-func hasprop(nodeList: NodeList; i: int): bool {.jshasprop.} =
-  return i < nodeList.len
+func hasprop(nodeList: NodeList; i: uint32): bool {.jshasprop.} =
+  return int(i) < nodeList.len
 
 func item(nodeList: NodeList; i: int): Node {.jsfunc.} =
   if i < nodeList.len:
     return nodeList.snapshot[i]
 
-func getter(nodeList: NodeList; i: int): Option[Node] {.jsgetprop.} =
-  return option(nodeList.item(i))
+func getter(nodeList: NodeList; i: uint32): Option[Node] {.jsgetprop.} =
+  return option(nodeList.item(int(i)))
 
 func names(ctx: JSContext; nodeList: NodeList): JSPropertyEnumList
     {.jspropnames.} =
@@ -1417,14 +1418,15 @@ func names(ctx: JSContext; collection: HTMLCollection): JSPropertyEnumList
 proc length(collection: HTMLAllCollection): uint32 {.jsfget.} =
   return uint32(collection.len)
 
-func hasprop(collection: HTMLAllCollection; i: int): bool {.jshasprop.} =
-  return i < collection.len
+func hasprop(collection: HTMLAllCollection; i: uint32): bool {.jshasprop.} =
+  return int(i) < collection.len
 
-func item(collection: HTMLAllCollection; i: int): Element {.jsfunc.} =
+func item(collection: HTMLAllCollection; i: uint32): Element {.jsfunc.} =
+  let i = int(i)
   if i < collection.len:
     return Element(collection.snapshot[i])
 
-func getter(collection: HTMLAllCollection; i: int): Option[Element]
+func getter(collection: HTMLAllCollection; i: uint32): Option[Element]
     {.jsgetprop.} =
   return option(collection.item(i))
 
@@ -1452,7 +1454,7 @@ proc newLocation*(window: Window): Location =
   let ctx = window.jsctx
   if ctx != nil:
     let val = toJS(ctx, location)
-    let valueOf = ctx.getOpaque().Object_prototype_valueOf
+    let valueOf = ctx.getOpaque().valRefs[jsvObjectPrototypeValueOf]
     defineProperty(ctx, val, "valueOf", JS_DupValue(ctx, valueOf))
     defineProperty(ctx, val, "toPrimitive", JS_UNDEFINED)
     #TODO [[DefaultProperties]]
@@ -4179,7 +4181,8 @@ proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValue;
   let buf = this.bitmap.toPNG(outlen)
   let blob = newBlob(buf, outlen, "image/png", proc() = dealloc(buf))
   var jsBlob = toJS(ctx, blob)
-  let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, addr jsBlob)
+  let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, jsBlob.toJSValueArray())
+  JS_FreeValue(ctx, jsBlob)
   # Hack. TODO: implement JSValue to callback
   if res == JS_EXCEPTION:
     return JS_EXCEPTION
diff --git a/src/html/env.nim b/src/html/env.nim
index c9d99dc2..00b6f18c 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -68,10 +68,10 @@ proc item(pluginArray: ptr PluginArray): JSValue {.jsfunc.} = JS_NULL
 proc length(pluginArray: ptr PluginArray): uint32 {.jsfget.} = 0
 proc item(mimeTypeArray: ptr MimeTypeArray): JSValue {.jsfunc.} = JS_NULL
 proc length(mimeTypeArray: ptr MimeTypeArray): uint32 {.jsfget.} = 0
-proc getter(pluginArray: ptr PluginArray; i: int): Option[JSValue]
+proc getter(pluginArray: ptr PluginArray; i: uint32): Option[JSValue]
     {.jsgetprop.} =
   discard
-proc getter(mimeTypeArray: ptr MimeTypeArray; i: int): Option[JSValue]
+proc getter(mimeTypeArray: ptr MimeTypeArray; i: uint32): Option[JSValue]
     {.jsgetprop.} =
   discard
 
@@ -193,7 +193,7 @@ proc addScripting*(window: Window; selector: Selector[int]) =
   ctx.addEventModule()
   let eventTargetCID = ctx.getClass("EventTarget")
   ctx.registerType(Window, asglobal = true, parent = eventTargetCID)
-  ctx.setGlobal(global, window)
+  ctx.setGlobal(window)
   JS_FreeValue(ctx, global)
   ctx.addDOMExceptionModule()
   ctx.addConsoleModule()
diff --git a/src/html/event.nim b/src/html/event.nim
index 9616d55e..b74649e8 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -7,6 +7,7 @@ import js/error
 import js/fromjs
 import js/javascript
 import js/jstypes
+import js/jsutils
 import js/tojs
 import types/opt
 
@@ -198,9 +199,10 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event):
   if JS_IsNull(listener.callback):
     return JS_UNDEFINED
   let jsTarget = ctx.toJS(event.currentTarget)
-  var jsEvent = ctx.toJS(event)
+  let jsEvent = ctx.toJS(event)
   if JS_IsFunction(ctx, listener.callback):
-    let ret = JS_Call(ctx, listener.callback, jsTarget, 1, addr jsEvent)
+    let ret = JS_Call(ctx, listener.callback, jsTarget, 1,
+      jsEvent.toJSValueArray())
     JS_FreeValue(ctx, jsTarget)
     JS_FreeValue(ctx, jsEvent)
     return ret
@@ -210,7 +212,7 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event):
     JS_FreeValue(ctx, jsTarget)
     JS_FreeValue(ctx, jsEvent)
     return handler
-  let ret = JS_Call(ctx, handler, jsTarget, 1, addr jsEvent)
+  let ret = JS_Call(ctx, handler, jsTarget, 1, jsEvent.toJSValueArray())
   JS_FreeValue(ctx, jsTarget)
   JS_FreeValue(ctx, jsEvent)
   return ret
@@ -255,9 +257,12 @@ proc flattenMore(ctx: JSContext; options: JSValue):
   var once = false
   var passive: Option[bool]
   if JS_IsObject(options):
-    once = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "once"))
-      .get(false)
-    let x = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "passive"))
+    let jsOnce = JS_GetPropertyStr(ctx, options, "once")
+    once = fromJS[bool](ctx, jsOnce).get(false)
+    JS_FreeValue(ctx, jsOnce)
+    let jsPassive = JS_GetPropertyStr(ctx, options, "passive")
+    let x = fromJS[bool](ctx, jsPassive)
+    JS_FreeValue(ctx, jsPassive)
     if x.isSome:
       passive = some(x.get)
   return (capture, once, passive)
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index 8032f5ec..65077734 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -45,10 +45,12 @@ proc sendDataLoop*(s: DynStream; buffer: pointer; len: int) =
       break
 
 proc sendDataLoop*(s: DynStream; buffer: openArray[uint8]) {.inline.} =
-  s.sendDataLoop(unsafeAddr buffer[0], buffer.len)
+  if buffer.len > 0:
+    s.sendDataLoop(unsafeAddr buffer[0], buffer.len)
 
 proc sendDataLoop*(s: DynStream; buffer: openArray[char]) {.inline.} =
-  s.sendDataLoop(unsafeAddr buffer[0], buffer.len)
+  if buffer.len > 0:
+    s.sendDataLoop(unsafeAddr buffer[0], buffer.len)
 
 proc write*(s: DynStream; buffer: openArray[char]) {.inline.} =
   s.sendDataLoop(buffer)
diff --git a/src/js/console.nim b/src/js/console.nim
index 305dfa9b..3b28df5e 100644
--- a/src/js/console.nim
+++ b/src/js/console.nim
@@ -19,8 +19,8 @@ proc newConsole*(err: DynStream; clearFun: proc() = nil; showFun: proc() = nil;
   )
 
 proc log*(console: Console; ss: varargs[string]) {.jsfunc.} =
-  for i in 0..<ss.len:
-    console.err.write(ss[i])
+  for i, s in ss:
+    console.err.write(s)
     if i != ss.high:
       console.err.write(' ')
   console.err.write('\n')
diff --git a/src/js/domexception.nim b/src/js/domexception.nim
index 3d4a4a1e..f916fb59 100644
--- a/src/js/domexception.nim
+++ b/src/js/domexception.nim
@@ -37,7 +37,7 @@ type
 
 jsDestructor(DOMException)
 
-proc newDOMException*(message = "", name = "Error"): DOMException {.jsctor.} =
+proc newDOMException*(message = ""; name = "Error"): DOMException {.jsctor.} =
   return DOMException(
     e: JS_DOM_EXCEPTION,
     name: name,
diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim
index 527b6221..c5c80da1 100644
--- a/src/js/fromjs.nim
+++ b/src/js/fromjs.nim
@@ -7,6 +7,7 @@ import bindings/quickjs
 import io/promise
 import js/error
 import js/jstypes
+import js/jsutils
 import js/opaque
 import types/opt
 import utils/twtstr
@@ -84,11 +85,6 @@ func fromJSInt[T: SomeInteger](ctx: JSContext; val: JSValue):
     if JS_ToInt32(ctx, addr ret, val) < 0:
       return err()
     return ok(int(ret))
-  elif T is uint:
-    var ret: uint32
-    if JS_ToUint32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(uint(ret))
   elif T is int32:
     var ret: int32
     if JS_ToInt32(ctx, addr ret, val) < 0:
@@ -123,11 +119,11 @@ macro fromJSTupleBody(a: tuple) =
     var `done`: bool)
   for i in 0..<len:
     result.add(quote do:
-      let next = JS_Call(ctx, next_method, it, 0, nil)
+      let next = JS_Call(ctx, nextMethod, it, 0, nil)
       if JS_IsException(next):
         return err()
       defer: JS_FreeValue(ctx, next)
-      let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+      let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone])
       if JS_IsException(doneVal):
         return err()
       defer: JS_FreeValue(ctx, doneVal)
@@ -135,7 +131,7 @@ macro fromJSTupleBody(a: tuple) =
       if `done`:
         return errTypeError("Too few arguments in sequence (got " & $`i` &
           ", expected " & $`len` & ")")
-      let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
+      let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue])
       if JS_IsException(valueVal):
         return err()
       defer: JS_FreeValue(ctx, valueVal)
@@ -143,23 +139,23 @@ macro fromJSTupleBody(a: tuple) =
     )
     if i == len - 1:
       result.add(quote do:
-        let next = JS_Call(ctx, next_method, it, 0, nil)
+        let next = JS_Call(ctx, nextMethod, it, 0, nil)
         if JS_IsException(next):
           return err()
         defer: JS_FreeValue(ctx, next)
-        let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+        let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone])
         `done` = ?fromJS[bool](ctx, doneVal)
         var i = `i`
         # we're emulating a sequence, so we must query all remaining parameters
         # too:
         while not `done`:
           inc i
-          let next = JS_Call(ctx, next_method, it, 0, nil)
+          let next = JS_Call(ctx, nextMethod, it, 0, nil)
           if JS_IsException(next):
             return err()
           defer: JS_FreeValue(ctx, next)
           let doneVal = JS_GetProperty(ctx, next,
-            ctx.getOpaque().str_refs[DONE])
+            ctx.getOpaque().strRefs[jstDone])
           if JS_IsException(doneVal):
             return err()
           defer: JS_FreeValue(ctx, doneVal)
@@ -169,11 +165,11 @@ macro fromJSTupleBody(a: tuple) =
               ", expected " & $`len` & ")"
             return err(newTypeError(msg))
           JS_FreeValue(ctx, JS_GetProperty(ctx, next,
-            ctx.getOpaque().str_refs[VALUE]))
+            ctx.getOpaque().strRefs[jstValue]))
       )
 
 proc fromJSTuple[T: tuple](ctx: JSContext; val: JSValue): JSResult[T] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator])
   if JS_IsException(itprop):
     return err()
   defer: JS_FreeValue(ctx, itprop)
@@ -181,16 +177,16 @@ proc fromJSTuple[T: tuple](ctx: JSContext; val: JSValue): JSResult[T] =
   if JS_IsException(it):
     return err()
   defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
+  let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext])
+  if JS_IsException(nextMethod):
     return err()
-  defer: JS_FreeValue(ctx, next_method)
+  defer: JS_FreeValue(ctx, nextMethod)
   var x: T
   fromJSTupleBody(x)
   return ok(x)
 
 proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator])
   if JS_IsException(itprop):
     return err()
   defer: JS_FreeValue(ctx, itprop)
@@ -198,24 +194,24 @@ proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] =
   if JS_IsException(it):
     return err()
   defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
+  let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext])
+  if JS_IsException(nextMethod):
     return err()
-  defer: JS_FreeValue(ctx, next_method)
+  defer: JS_FreeValue(ctx, nextMethod)
   var s = newSeq[T]()
   while true:
-    let next = JS_Call(ctx, next_method, it, 0, nil)
+    let next = JS_Call(ctx, nextMethod, it, 0, nil)
     if JS_IsException(next):
       return err()
     defer: JS_FreeValue(ctx, next)
-    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone])
     if JS_IsException(doneVal):
       return err()
     defer: JS_FreeValue(ctx, doneVal)
     let done = ?fromJS[bool](ctx, doneVal)
     if done:
       break
-    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
+    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue])
     if JS_IsException(valueVal):
       return err()
     defer: JS_FreeValue(ctx, valueVal)
@@ -224,7 +220,7 @@ proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] =
   return ok(s)
 
 proc fromJSSet[T](ctx: JSContext; val: JSValue): JSResult[set[T]] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator])
   if JS_IsException(itprop):
     return err()
   defer: JS_FreeValue(ctx, itprop)
@@ -232,28 +228,28 @@ proc fromJSSet[T](ctx: JSContext; val: JSValue): JSResult[set[T]] =
   if JS_IsException(it):
     return err()
   defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
+  let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext])
+  if JS_IsException(nextMethod):
     return err()
-  defer: JS_FreeValue(ctx, next_method)
+  defer: JS_FreeValue(ctx, nextMethod)
   var s: set[T]
   while true:
-    let next = JS_Call(ctx, next_method, it, 0, nil)
+    let next = JS_Call(ctx, nextMethod, it, 0, nil)
     if JS_IsException(next):
       return err()
     defer: JS_FreeValue(ctx, next)
-    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone])
     if JS_IsException(doneVal):
       return err()
     defer: JS_FreeValue(ctx, doneVal)
     let done = ?fromJS[bool](ctx, doneVal)
     if done:
       break
-    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
+    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue])
     if JS_IsException(valueVal):
       return err()
     defer: JS_FreeValue(ctx, valueVal)
-    let genericRes = ?fromJS[typeof(s.items)](ctx, valueVal)
+    let genericRes = ?fromJS[T](ctx, valueVal)
     s.incl(genericRes)
   return ok(s)
 
@@ -341,12 +337,14 @@ proc fromJSDict[T: JSDict](ctx: JSContext; val: JSValue): JSResult[T] =
   if not JS_IsUndefined(val) and not JS_IsNull(val) and not JS_IsObject(val):
     return err(newTypeError("Dictionary is not an object"))
   #TODO throw on missing required values
-  var d: T
+  var d = T()
   if JS_IsObject(val):
     for k, v in d.fieldPairs:
       let esm = JS_GetPropertyStr(ctx, val, k)
       if not JS_IsUndefined(esm):
         v = ?fromJS[typeof(v)](ctx, esm)
+      when v isnot JSValue:
+        JS_FreeValue(ctx, esm)
   return ok(d)
 
 proc fromJSArrayBuffer(ctx: JSContext; val: JSValue): JSResult[JSArrayBuffer] =
@@ -377,25 +375,34 @@ proc fromJSArrayBufferView(ctx: JSContext; val: JSValue):
   return ok(view)
 
 proc promiseThenCallback(ctx: JSContext; this_val: JSValue; argc: cint;
-    argv: ptr JSValue; magic: cint; func_data: ptr JSValue): JSValue {.cdecl.} =
-  let op = JS_GetOpaque(func_data[], JS_GetClassID(func_data[]))
-  let p = cast[EmptyPromise](op)
-  p.resolve()
-  GC_unref(p)
+    argv: ptr UncheckedArray[JSValue]; magic: cint;
+    func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} =
+  let fun = func_data[0]
+  let op = JS_GetOpaque(fun, JS_GetClassID(fun))
+  if op != nil:
+    let p = cast[EmptyPromise](op)
+    p.resolve()
+    GC_unref(p)
+    JS_SetOpaque(fun, nil)
   return JS_UNDEFINED
 
 proc fromJSEmptyPromise(ctx: JSContext; val: JSValue): JSResult[EmptyPromise] =
   if not JS_IsObject(val):
     return err(newTypeError("Value is not an object"))
-  #TODO I have a feeling this leaks memory in some cases :(
   var p = EmptyPromise()
   GC_ref(p)
-  var tmp = JS_NewObject(ctx)
+  let tmp = JS_NewObject(ctx)
   JS_SetOpaque(tmp, cast[pointer](p))
-  var fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1, addr tmp)
-  let res = JS_Invoke(ctx, val, ctx.getOpaque().str_refs[THEN], 1, addr fun)
+  let fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1,
+    tmp.toJSValueArray())
+  JS_FreeValue(ctx, tmp)
+  let res = JS_Invoke(ctx, val, ctx.getOpaque().strRefs[jstThen], 1,
+    fun.toJSValueArray())
+  JS_FreeValue(ctx, fun)
   if JS_IsException(res):
+    JS_FreeValue(ctx, res)
     return err()
+  JS_FreeValue(ctx, res)
   return ok(p)
 
 type FromJSAllowedT = (object and not (Result|Option|Table|JSValue|JSDict|
@@ -445,18 +452,25 @@ proc fromJS*[T](ctx: JSContext; val: JSValue): JSResult[T] =
   else:
     return fromJS2(ctx, val, $T)
 
-const JS_ATOM_TAG_INT = cuint(1u32 shl 31)
+const JS_ATOM_TAG_INT = 1u32 shl 31
 
 func JS_IsNumber*(v: JSAtom): JS_BOOL =
-  return (cast[cuint](v) and JS_ATOM_TAG_INT) != 0
+  return (uint32(v) and JS_ATOM_TAG_INT) != 0
 
-func fromJS*[T: string|uint32](ctx: JSContext; atom: JSAtom): Opt[T] =
-  when T is SomeNumber:
+func fromJS*[T: string|uint32|JSAtom](ctx: JSContext; atom: JSAtom): Opt[T] =
+  when T is JSAtom:
+    return ok(atom)
+  elif T is SomeNumber:
     if JS_IsNumber(atom):
-      return ok(T(cast[uint32](atom) and (not JS_ATOM_TAG_INT)))
+      return ok(uint32(atom) and (not JS_ATOM_TAG_INT))
+    return err()
   else:
-    let val = JS_AtomToValue(ctx, atom)
-    return toString(ctx, val)
+    let cs = JS_AtomToCString(ctx, atom)
+    if cs == nil:
+      return err()
+    let s = $cs
+    JS_FreeCString(ctx, cs)
+    return ok(s)
 
 proc fromJSPObj[T](ctx: JSContext; val: JSValue): JSResult[ptr T] =
   return cast[JSResult[ptr T]](fromJSPObj0(ctx, val, $T))
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 8a1a0794..2266a3c7 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -30,8 +30,8 @@
 #   can only be used on object fields. (I initially wanted to use the same
 #   keyword, unfortunately that didn't work out.)
 # {.jsgetprop.} for property getters. Called when GetOwnProperty would return
-#   nothing. The key must be either a string or an integer (preferably uint32),
-#   since it is converted from a JSAtom.
+#   nothing. The key must be either a JSAtom, uint32 or string.  (Note that the
+#   string option copies.)
 # {.jssetprop.} for property setters. Called on SetProperty - in fact this
 #   is the set() method of Proxy, except it always returns true. Same rules as
 #   jsgetprop for keys.
@@ -77,11 +77,11 @@ export
   JS_EVAL_FLAG_STRIP,
   JS_EVAL_FLAG_COMPILE_ONLY
 
-export JSRuntime, JSContext, JSValue, JSClassID
+export JSRuntime, JSContext, JSValue, JSClassID, JSAtom
 
 export
   JS_GetGlobalObject, JS_FreeValue, JS_IsException, JS_GetPropertyStr,
-  JS_IsFunction, JS_NewCFunctionData, JS_Call, JS_DupValue
+  JS_IsFunction, JS_NewCFunctionData, JS_Call, JS_DupValue, JS_IsUndefined
 
 when sizeof(int) < sizeof(int64):
   export quickjs.`==`
@@ -159,40 +159,41 @@ func newJSCFunction*(ctx: JSContext; name: string; fun: JSCFunction;
   return JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto,
     cint(magic))
 
-proc free*(ctx: var JSContext) =
+proc free*(ctx: JSContext) =
   var opaque = ctx.getOpaque()
   if opaque != nil:
-    for a in opaque.sym_refs:
+    for a in opaque.symRefs:
       JS_FreeAtom(ctx, a)
-    for a in opaque.str_refs:
+    for a in opaque.strRefs:
       JS_FreeAtom(ctx, a)
+    for v in opaque.valRefs:
+      JS_FreeValue(ctx, v)
     for classid, v in opaque.ctors:
       JS_FreeValue(ctx, v)
-    JS_FreeValue(ctx, opaque.Array_prototype_values)
-    JS_FreeValue(ctx, opaque.Object_prototype_valueOf)
-    JS_FreeValue(ctx, opaque.Uint8Array_ctor)
-    JS_FreeValue(ctx, opaque.Set_ctor)
-    JS_FreeValue(ctx, opaque.Function_ctor)
-    for v in opaque.err_ctors:
+    for v in opaque.errCtorRefs:
       JS_FreeValue(ctx, v)
+    if opaque.globalUnref != nil:
+      opaque.globalUnref()
     GC_unref(opaque)
   JS_FreeContext(ctx)
-  ctx = nil
 
-proc free*(rt: var JSRuntime) =
+proc free*(rt: JSRuntime) =
   let opaque = rt.getOpaque()
   GC_unref(opaque)
   JS_FreeRuntime(rt)
   runtimes.del(runtimes.find(rt))
-  rt = nil
 
-proc setGlobal*[T](ctx: JSContext; global: JSValue; obj: T) =
+proc setGlobal*[T](ctx: JSContext; obj: T) =
   # Add JSValue reference.
-  let p = JS_VALUE_GET_PTR(global)
-  let header = cast[ptr JSRefCountHeader](p)
-  inc header.ref_count
-  ctx.setOpaque(global, cast[pointer](obj))
+  let global = JS_GetGlobalObject(ctx)
+  let opaque = cast[pointer](obj)
+  ctx.setOpaque(global, opaque)
   GC_ref(obj)
+  let rtOpaque = JS_GetRuntime(ctx).getOpaque()
+  ctx.getOpaque().globalUnref = proc() =
+    GC_unref(obj)
+    rtOpaque.plist.del(opaque)
+  JS_FreeValue(ctx, global)
 
 proc setInterruptHandler*(rt: JSRuntime; cb: JSInterruptHandler;
     opaque: pointer = nil) =
@@ -266,13 +267,13 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string;
     ctxOpaque.htmldda = result
   if finalizer != nil:
     rtOpaque.fins[result] = finalizer
-  var proto: JSValue
-  if parent != 0:
+  let proto = if parent != 0:
     let parentProto = JS_GetClassProto(ctx, parent)
-    proto = JS_NewObjectProtoClass(ctx, parentProto, parent)
+    let x = JS_NewObjectProtoClass(ctx, parentProto, parent)
     JS_FreeValue(ctx, parentProto)
+    x
   else:
-    proto = JS_NewObject(ctx)
+    JS_NewObject(ctx)
   if funcs.len > 0:
     # We avoid funcs being GC'ed by putting the list in rtOpaque.
     # (QuickJS uses the pointer later.)
@@ -282,11 +283,12 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string;
       cint(funcs.len))
   #TODO check if this is an indexed property getter
   if cdef.exotic != nil and cdef.exotic.get_own_property != nil:
-    let val = JS_DupValue(ctx, ctxOpaque.Array_prototype_values)
-    doAssert JS_SetProperty(ctx, proto, ctxOpaque.sym_refs[ITERATOR], val) == 1
+    let val = JS_DupValue(ctx, ctxOpaque.valRefs[jsvArrayPrototypeValues])
+    let itSym = ctxOpaque.symRefs[jsyIterator]
+    doAssert JS_SetProperty(ctx, proto, itSym, val) == 1
   let news = JS_NewAtomString(ctx, cdef.class_name)
   doAssert not JS_IsException(news)
-  ctx.definePropertyC(proto, ctxOpaque.sym_refs[TO_STRING_TAG],
+  ctx.definePropertyC(proto, ctxOpaque.symRefs[jsyToStringTag],
     JS_DupValue(ctx, news))
   JS_SetClassProto(ctx, result, proto)
   ctx.addClassUnforgeable(proto, result, parent, unforgeable)
@@ -295,7 +297,7 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string;
     assert ctxOpaque.gclaz == ""
     ctxOpaque.gclaz = tname
     ctxOpaque.gparent = parent
-    ctx.definePropertyC(global, ctxOpaque.sym_refs[TO_STRING_TAG],
+    ctx.definePropertyC(global, ctxOpaque.symRefs[jsyToStringTag],
       JS_DupValue(ctx, news))
     if JS_SetPrototype(ctx, global, proto) != 1:
       raise newException(Defect, "Failed to set global prototype: " &
@@ -313,7 +315,7 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string;
       cint(staticfuns.len))
   JS_SetConstructor(ctx, jctor, proto)
   if errid.isSome:
-    ctx.getOpaque().err_ctors[errid.get] = JS_DupValue(ctx, jctor)
+    ctx.getOpaque().errCtorRefs[errid.get] = JS_DupValue(ctx, jctor)
   ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor)
   if not nointerface:
     if JS_IsNull(namespace):
@@ -322,6 +324,8 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string;
       JS_FreeValue(ctx, global)
     else:
       ctx.definePropertyCW(namespace, $cdef.class_name, jctor)
+  else:
+    JS_FreeValue(ctx, jctor)
 
 type FuncParam = tuple
   name: string
@@ -596,7 +600,7 @@ proc addUnionParamBranch(gen: var JSFuncGenerator; query, newBranch: NimNode;
 func isSequence*(ctx: JSContext; o: JSValue): bool =
   if not JS_IsObject(o):
     return false
-  let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_refs[ITERATOR])
+  let prop = JS_GetProperty(ctx, o, ctx.getOpaque().symRefs[jsyIterator])
   # prop can't be exception (throws_ref_error is 0 and tag is object)
   result = not JS_IsUndefined(prop)
   JS_FreeValue(ctx, prop)
@@ -1358,10 +1362,9 @@ func jsname(info: RegistryInfo): string =
   return info.tname
 
 proc newRegistryInfo(t: NimNode; name: string): RegistryInfo =
-  let info = RegistryInfo(
+  return RegistryInfo(
     t: t,
     name: name,
-    dfin: ident("js_" & t.strVal & "ClassCheckDestroy"),
     classDef: ident("classDef"),
     tabList: newNimNode(nnkBracket),
     tabUnforgeable: newNimNode(nnkBracket),
@@ -1374,9 +1377,6 @@ proc newRegistryInfo(t: NimNode; name: string): RegistryInfo =
     propHasFun: newNilLit(),
     propNamesFun: newNilLit()
   )
-  if info.tname notin jsDtors:
-    warning("No destructor has been defined for type " & info.tname)
-  return info
 
 proc bindConstructor(stmts: NimNode; info: var RegistryInfo): NimNode =
   if info.ctorFun != nil:
@@ -1604,14 +1604,21 @@ proc bindEndStmts(endstmts: NimNode; info: RegistryInfo) =
       )
       let `classDef` = JSClassDefConst(addr cd))
 
-macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0,
-    asglobal = false, nointerface = false, name: static string = "",
-    has_extra_getset: static bool = false,
-    extra_getset: static openArray[TabGetSet] = [],
-    namespace: JSValue = JS_NULL, errid = opt(JSErrorEnum),
-    ishtmldda = false): JSClassID =
+macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0;
+    asglobal: static bool = false; nointerface = false; name: static string = "";
+    has_extra_getset: static bool = false;
+    extra_getset: static openArray[TabGetSet] = []; namespace = JS_NULL;
+    errid = opt(JSErrorEnum); ishtmldda = false): JSClassID =
   var stmts = newStmtList()
   var info = newRegistryInfo(t, name)
+  if not asglobal:
+    info.dfin = ident("js_" & t.strVal & "ClassCheckDestroy")
+    if info.tname notin jsDtors:
+      warning("No destructor has been defined for type " & info.tname)
+  else:
+    info.dfin = newNilLit()
+    if info.tname in jsDtors:
+      error("Global object " & info.tname & " must not have a destructor!")
   let pragmas = findPragmas(t)
   stmts.registerGetters(info, pragmas.jsget)
   stmts.registerSetters(info, pragmas.jsset)
@@ -1623,7 +1630,8 @@ macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0,
     # been passed to it at all.
     stmts.bindExtraGetSet(info, extra_getset)
   let sctr = stmts.bindConstructor(info)
-  stmts.bindCheckDestroy(info)
+  if not asglobal:
+    stmts.bindCheckDestroy(info)
   let endstmts = newStmtList()
   endstmts.bindEndStmts(info)
   let tabList = info.tabList
diff --git a/src/js/jsutils.nim b/src/js/jsutils.nim
new file mode 100644
index 00000000..b8a8398e
--- /dev/null
+++ b/src/js/jsutils.nim
@@ -0,0 +1,9 @@
+import bindings/quickjs
+
+template toJSValueArray*(a: openArray[JSValue]): ptr UncheckedArray[JSValue] =
+  cast[ptr UncheckedArray[JSValue]](unsafeAddr a[0])
+
+# Warning: this must be a template, because we're taking the address of
+# the passed value, and Nim is pass-by-value.
+template toJSValueArray*(a: JSValue): ptr UncheckedArray[JSValue] =
+  cast[ptr UncheckedArray[JSValue]](unsafeAddr a)
diff --git a/src/js/opaque.nim b/src/js/opaque.nim
index 490747bc..93900502 100644
--- a/src/js/opaque.nim
+++ b/src/js/opaque.nim
@@ -5,17 +5,24 @@ import js/error
 import types/opt
 
 type
-  JSSymbolRefs* = enum
-    ITERATOR = "iterator"
-    ASYNC_ITERATOR = "asyncIterator"
-    TO_STRING_TAG = "toStringTag"
-
-  JSStrRefs* = enum
-    DONE = "done"
-    VALUE = "value"
-    NEXT = "next"
-    PROTOTYPE = "prototype"
-    THEN = "then"
+  JSSymbolRef* = enum
+    jsyIterator = "iterator"
+    jsyAsyncIterator = "asyncIterator"
+    jsyToStringTag = "toStringTag"
+
+  JSStrRef* = enum
+    jstDone = "done"
+    jstValue = "value"
+    jstNext = "next"
+    jstPrototype = "prototype"
+    jstThen = "then"
+
+  JSValueRef* = enum
+    jsvArrayPrototypeValues = "Array.prototype.values"
+    jsvUint8Array = "Uint8Array"
+    jsvObjectPrototypeValueOf = "Object.prototype.valueOf"
+    jsvSet = "Set"
+    jsvFunction = "Function"
 
   JSContextOpaque* = ref object
     creg*: Table[string, JSClassID]
@@ -28,15 +35,12 @@ type
     unforgeable*: Table[JSClassID, seq[JSCFunctionListEntry]]
     gclaz*: string
     gparent*: JSClassID
-    sym_refs*: array[JSSymbolRefs, JSAtom]
-    str_refs*: array[JSStrRefs, JSAtom]
-    Array_prototype_values*: JSValue
-    Object_prototype_valueOf*: JSValue
-    Uint8Array_ctor*: JSValue
-    Set_ctor*: JSValue
-    Function_ctor*: JSValue
-    err_ctors*: array[JSErrorEnum, JSValue]
+    symRefs*: array[JSSymbolRef, JSAtom]
+    strRefs*: array[JSStrRef, JSAtom]
+    valRefs*: array[JSValueRef, JSValue]
+    errCtorRefs*: array[JSErrorEnum, JSValue]
     htmldda*: JSClassID # only one of these exists: document.all.
+    globalUnref*: proc() {.closure.}
 
   JSFinalizerFunction* = proc(rt: JSRuntime; val: JSValue) {.nimcall.}
 
@@ -51,41 +55,26 @@ func newJSContextOpaque*(ctx: JSContext): JSContextOpaque =
   let opaque = JSContextOpaque()
   block: # get well-known symbols and other functions
     let global = JS_GetGlobalObject(ctx)
-    block:
-      let sym = JS_GetPropertyStr(ctx, global, "Symbol")
-      for s in JSSymbolRefs:
-        let name = $s
-        let val = JS_GetPropertyStr(ctx, sym, cstring(name))
-        assert JS_IsSymbol(val)
-        opaque.sym_refs[s] = JS_ValueToAtom(ctx, val)
-        JS_FreeValue(ctx, val)
-      JS_FreeValue(ctx, sym)
-      for s in JSStrRefs:
-        let ss = $s
-        opaque.str_refs[s] = JS_NewAtomLen(ctx, cstring(ss), csize_t(ss.len))
-    block:
-      let arrproto = JS_GetClassProto(ctx, JS_CLASS_ARRAY)
-      opaque.Array_prototype_values = JS_GetPropertyStr(ctx, arrproto,
-        "values")
-      JS_FreeValue(ctx, arrproto)
-    block:
-      let objproto = JS_GetClassProto(ctx, JS_CLASS_OBJECT)
-      opaque.Object_prototype_valueOf = JS_GetPropertyStr(ctx, objproto,
-        "valueOf")
-      JS_FreeValue(ctx, objproto)
-    block:
-      opaque.Uint8Array_ctor = JS_GetPropertyStr(ctx, global, "Uint8Array")
-      assert not JS_IsException(opaque.Uint8Array_ctor)
-    block:
-      opaque.Set_ctor = JS_GetPropertyStr(ctx, global, "Set")
-      assert not JS_IsException(opaque.Set_ctor)
-    block:
-      opaque.Function_ctor = JS_GetPropertyStr(ctx, global, "Function")
-      assert not JS_IsException(opaque.Function_ctor)
+    let sym = JS_GetPropertyStr(ctx, global, "Symbol")
+    for s in JSSymbolRef:
+      let name = $s
+      let val = JS_GetPropertyStr(ctx, sym, cstring(name))
+      assert JS_IsSymbol(val)
+      opaque.symRefs[s] = JS_ValueToAtom(ctx, val)
+      JS_FreeValue(ctx, val)
+    JS_FreeValue(ctx, sym)
+    for s in JSStrRef:
+      let ss = $s
+      opaque.strRefs[s] = JS_NewAtomLen(ctx, cstring(ss), csize_t(ss.len))
+    for s in JSValueRef:
+      let ss = $s
+      let ret = JS_Eval(ctx, cstring(ss), csize_t(ss.len), "<init>", 0)
+      assert JS_IsFunction(ctx, ret)
+      opaque.valRefs[s] = ret
     for e in JSErrorEnum:
       let s = $e
       let err = JS_GetPropertyStr(ctx, global, cstring(s))
-      opaque.err_ctors[e] = err
+      opaque.errCtorRefs[e] = err
     JS_FreeValue(ctx, global)
   return opaque
 
diff --git a/src/js/tojs.nim b/src/js/tojs.nim
index 2ee8fd47..7831a6a9 100644
--- a/src/js/tojs.nim
+++ b/src/js/tojs.nim
@@ -44,6 +44,7 @@ import bindings/quickjs
 import io/promise
 import js/error
 import js/jstypes
+import js/jsutils
 import js/opaque
 import js/typeptr
 import types/opt
@@ -148,8 +149,8 @@ proc newFunction*(ctx: JSContext; args: openArray[string]; body: string):
   for arg in args:
     paramList.add(toJS(ctx, arg))
   paramList.add(toJS(ctx, body))
-  let fun = JS_CallConstructor(ctx, ctx.getOpaque().Function_ctor,
-    cint(paramList.len), addr paramList[0])
+  let fun = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvFunction],
+    cint(paramList.len), paramList.toJSValueArray())
   for param in paramList:
     JS_FreeValue(ctx, param)
   return fun
@@ -234,7 +235,8 @@ proc toJS*[T](ctx: JSContext; s: set[T]): JSValue =
   var a = toJS(ctx, x)
   if JS_IsException(a):
     return a
-  let ret = JS_CallConstructor(ctx, ctx.getOpaque().Set_ctor, 1, addr a)
+  let ret = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvSet], 1,
+    a.toJSValueArray())
   JS_FreeValue(ctx, a)
   return ret
 
@@ -319,12 +321,12 @@ proc toJS(ctx: JSContext; j: JSValue): JSValue =
 
 proc toJS(ctx: JSContext; promise: EmptyPromise): JSValue =
   var resolving_funcs: array[2, JSValue]
-  let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0])
+  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
   if JS_IsException(jsPromise):
     return JS_EXCEPTION
   promise.then(proc() =
-    var x = JS_UNDEFINED
-    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, addr x)
+    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1,
+      JS_UNDEFINED.toJSValueArray())
     JS_FreeValue(ctx, res)
     JS_FreeValue(ctx, resolving_funcs[0])
     JS_FreeValue(ctx, resolving_funcs[1]))
@@ -332,12 +334,13 @@ proc toJS(ctx: JSContext; promise: EmptyPromise): JSValue =
 
 proc toJS[T](ctx: JSContext; promise: Promise[T]): JSValue =
   var resolving_funcs: array[2, JSValue]
-  let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0])
+  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
   if JS_IsException(jsPromise):
     return JS_EXCEPTION
   promise.then(proc(x: T) =
-    var x = toJS(ctx, x)
-    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, addr x)
+    let x = toJS(ctx, x)
+    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1,
+      x.toJSValueArray())
     JS_FreeValue(ctx, res)
     JS_FreeValue(ctx, x)
     JS_FreeValue(ctx, resolving_funcs[0])
@@ -346,7 +349,7 @@ proc toJS[T](ctx: JSContext; promise: Promise[T]): JSValue =
 
 proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue =
   var resolving_funcs: array[2, JSValue]
-  let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0])
+  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
   if JS_IsException(jsPromise):
     return JS_EXCEPTION
   promise.then(proc(x: Result[T, E]) =
@@ -355,7 +358,8 @@ proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue =
         JS_UNDEFINED
       else:
         toJS(ctx, x.get)
-      let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, unsafeAddr x)
+      let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1,
+        x.toJSValueArray())
       JS_FreeValue(ctx, res)
       JS_FreeValue(ctx, x)
     else: # err
@@ -363,7 +367,8 @@ proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue =
         JS_UNDEFINED
       else:
         toJS(ctx, x.error)
-      let res = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, unsafeAddr x)
+      let res = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1,
+        x.toJSValueArray())
       JS_FreeValue(ctx, res)
       JS_FreeValue(ctx, x)
     JS_FreeValue(ctx, resolving_funcs[0])
@@ -376,8 +381,8 @@ proc toJS*(ctx: JSContext; err: JSError): JSValue =
   var msg = toJS(ctx, err.message)
   if JS_IsException(msg):
     return msg
-  let ctor = ctx.getOpaque().err_ctors[err.e]
-  let ret = JS_CallConstructor(ctx, ctor, 1, addr msg)
+  let ctor = ctx.getOpaque().errCtorRefs[err.e]
+  let ret = JS_CallConstructor(ctx, ctor, 1, msg.toJSValueArray())
   JS_FreeValue(ctx, msg)
   return ret
 
@@ -385,9 +390,9 @@ proc toJS*(ctx: JSContext; abuf: JSArrayBuffer): JSValue =
   return JS_NewArrayBuffer(ctx, abuf.p, abuf.len, abuf.dealloc, nil, false)
 
 proc toJS*(ctx: JSContext; u8a: JSUint8Array): JSValue =
-  var jsabuf = toJS(ctx, u8a.abuf)
-  let ctor = ctx.getOpaque().Uint8Array_ctor
-  let ret = JS_CallConstructor(ctx, ctor, 1, addr jsabuf)
+  let jsabuf = toJS(ctx, u8a.abuf)
+  let ctor = ctx.getOpaque().valRefs[jsvUint8Array]
+  let ret = JS_CallConstructor(ctx, ctor, 1, jsabuf.toJSValueArray())
   JS_FreeValue(ctx, jsabuf)
   return ret
 
diff --git a/src/local/client.nim b/src/local/client.nim
index 73b17c99..359d143c 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -35,6 +35,7 @@ import js/fromjs
 import js/intl
 import js/javascript
 import js/jstypes
+import js/jsutils
 import js/module
 import js/timeout
 import js/tojs
@@ -58,6 +59,7 @@ import chagashi/charset
 type
   Client* = ref object
     alive: bool
+    dead: bool
     config {.jsget.}: Config
     consoleWrapper: ConsoleWrapper
     fdmap: Table[int, Container]
@@ -67,14 +69,13 @@ type
     pager {.jsget.}: Pager
     timeouts: TimeoutState
     pressed: tuple[col: int; row: int]
+    exitCode: int
 
   ConsoleWrapper = object
     console: Console
     container: Container
     prev: Container
 
-jsDestructor(Client)
-
 func console(client: Client): Console {.jsfget.} =
   return client.consoleWrapper.console
 
@@ -90,12 +91,6 @@ template forkserver(client: Client): ForkServer =
 template readChar(client: Client): char =
   client.pager.term.readChar()
 
-proc finalize(client: Client) {.jsfin.} =
-  if client.jsctx != nil:
-    free(client.jsctx)
-  if client.jsrt != nil:
-    free(client.jsrt)
-
 proc fetch[T: Request|string](client: Client; req: T;
     init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} =
   let req = ?newRequest(client.jsctx, req, init)
@@ -155,19 +150,32 @@ proc suspend(client: Client) {.jsfunc.} =
   discard kill(0, cint(SIGTSTP))
   client.pager.term.restart()
 
-proc quit(client: Client; code = 0) {.jsfunc.} =
-  if client.alive:
+proc jsQuit(client: Client; code = 0) {.jsfunc: "quit".} =
+  client.exitCode = code
+  client.alive = false
+
+proc quit(client: Client; code = 0) =
+  if not client.dead:
+    # dead is set to true when quit is called; it indicates that the
+    # client has been destroyed.
+    # alive is set to false when jsQuit is called; it is a request to
+    # destroy the client.
+    client.dead = true
     client.alive = false
     client.pager.quit()
+    for val in client.config.cmd.map.values:
+      JS_FreeValue(client.jsctx, val)
+    for fn in client.config.jsvfns:
+      JS_FreeValue(client.jsctx, fn)
     let ctx = client.jsctx
-    var global = JS_GetGlobalObject(ctx)
-    JS_FreeValue(ctx, global)
-    if client.jsctx != nil:
-      free(client.jsctx)
-    #TODO
-    #if client.jsrt != nil:
-    #  free(client.jsrt)
-  quit(code)
+    let rt = client.jsrt
+    # Force the runtime to collect all memory, so QJS can check for
+    # leaks.
+    client[].reset()
+    GC_fullCollect()
+    ctx.free()
+    rt.free()
+  exitnow(code)
 
 proc feedNext(client: Client) {.jsfunc.} =
   client.feednext = true
@@ -192,12 +200,11 @@ proc evalAction(client: Client; action: string; arg0: int32): EmptyPromise =
   p.resolve()
   if JS_IsFunction(ctx, ret):
     if arg0 != 0:
-      var arg0 = toJS(ctx, arg0)
-      let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 1, addr arg0)
+      let arg0 = toJS(ctx, arg0)
+      let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 1, arg0.toJSValueArray())
       JS_FreeValue(ctx, arg0)
       JS_FreeValue(ctx, ret)
       ret = ret2
-      JS_FreeValue(ctx, arg0)
     else: # no precnum
       let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 0, nil)
       JS_FreeValue(ctx, ret)
@@ -514,11 +521,11 @@ proc handleError(client: Client; fd: int) =
   if client.pager.term.istream != nil and fd == client.pager.term.istream.fd:
     #TODO do something here...
     stderr.write("Error in tty\n")
-    quit(1)
+    client.quit(1)
   elif fd == client.forkserver.estream.fd:
     #TODO do something here...
     stderr.write("Fork server crashed :(\n")
-    quit(1)
+    client.quit(1)
   elif fd in client.loader.connecting:
     #TODO handle error?
     discard
@@ -546,7 +553,7 @@ proc inputLoop(client: Client) =
   let selector = client.selector
   selector.registerHandle(int(client.pager.term.istream.fd), {Read}, 0)
   let sigwinch = selector.registerSignal(int(SIGWINCH), 0)
-  while true:
+  while client.alive:
     let events = client.selector.select(-1)
     for event in events:
       if Read in event.events:
@@ -576,7 +583,7 @@ proc inputLoop(client: Client) =
       if not client.pager.hasload:
         # Failed to load every single URL the user passed us. We quit, and that
         # will dump all alerts to stderr.
-        quit(1)
+        client.quit(1)
       else:
         # At least one connection has succeeded, but we have nothing to display.
         # Normally, this means that the input stream has been redirected to a
@@ -586,7 +593,7 @@ proc inputLoop(client: Client) =
         # loader, and then asking for confirmation if there is at least one.
         client.pager.term.setCursor(0, client.pager.term.attrs.height - 1)
         client.pager.term.anyKey("Hit any key to quit Chawan:")
-        quit(0)
+        client.quit(0)
     client.pager.showAlerts()
     client.pager.draw()
 
@@ -598,7 +605,7 @@ func hasSelectFds(client: Client): bool =
     client.pager.procmap.len > 0
 
 proc headlessLoop(client: Client) =
-  while client.hasSelectFds():
+  while client.alive and client.hasSelectFds():
     let events = client.selector.select(-1)
     for event in events:
       if Read in event.events:
@@ -698,7 +705,7 @@ proc dumpBuffers(client: Client) =
       client.console.log("Error in buffer", $container.url)
       # check for errors
       client.handleRead(client.forkserver.estream.fd)
-      quit(1)
+      client.quit(1)
 
 proc launchClient*(client: Client; pages: seq[string];
     contentType: Option[string]; cs: Charset; dump: bool) =
@@ -824,13 +831,19 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext;
   let loader = FileLoader(process: loaderPid, clientPid: getCurrentProcessId())
   loader.setSocketDir(config.external.tmpdir)
   pager.setLoader(loader)
-  let client = Client(config: config, jsrt: jsrt, jsctx: jsctx, pager: pager)
+  let client = Client(
+    config: config,
+    jsrt: jsrt,
+    jsctx: jsctx,
+    pager: pager,
+    alive: true
+  )
   jsrt.setInterruptHandler(interruptHandler, cast[pointer](client))
-  var global = JS_GetGlobalObject(jsctx)
   jsctx.registerType(Client, asglobal = true)
-  setGlobal(jsctx, global, client)
+  jsctx.setGlobal(client)
+  let global = JS_GetGlobalObject(jsctx)
   jsctx.definePropertyE(global, "cmd", config.cmd.jsObj)
-  config.cmd.jsObj = JS_NULL
   JS_FreeValue(jsctx, global)
+  config.cmd.jsObj = JS_NULL
   client.addJSModules(jsctx)
   return client
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 4ef4b4de..875f0df4 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -10,6 +10,7 @@ import std/tables
 import std/unicode
 
 import bindings/libregexp
+import bindings/quickjs
 import config/config
 import config/mailcap
 import io/bufreader
@@ -24,6 +25,7 @@ import js/error
 import js/fromjs
 import js/javascript
 import js/jstypes
+import js/jsutils
 import js/regex
 import js/tojs
 import loader/connecterror
@@ -175,6 +177,7 @@ proc setContainer*(pager: Pager; c: Container) {.jsfunc.} =
     pager.term.setTitle(c.getTitle())
 
 proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} =
+  result = false
   if pager.container != nil:
     let cval = toJS(ctx, pager.container)
     let val = JS_GetPropertyStr(ctx, cval, s)
@@ -182,22 +185,29 @@ proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} =
       result = true
     JS_FreeValue(ctx, val)
 
-proc reflect(ctx: JSContext; this_val: JSValue; argc: cint; argv: ptr JSValue;
-    magic: cint; func_data: ptr JSValue): JSValue {.cdecl.} =
-  let fun = cast[ptr JSValue](cast[int](func_data) + sizeof(JSValue))[]
-  return JS_Call(ctx, fun, func_data[], argc, argv)
+proc reflect(ctx: JSContext; this_val: JSValue; argc: cint;
+    argv: ptr UncheckedArray[JSValue]; magic: cint;
+    func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} =
+  let obj = func_data[0]
+  let fun = func_data[1]
+  return JS_Call(ctx, fun, obj, argc, argv)
 
-proc getter(ctx: JSContext; pager: Pager; s: string): Option[JSValue]
+proc getter(ctx: JSContext; pager: Pager; a: JSAtom): Option[JSValue]
     {.jsgetprop.} =
   if pager.container != nil:
     let cval = toJS(ctx, pager.container)
-    let val = JS_GetPropertyStr(ctx, cval, s)
-    if val != JS_UNDEFINED:
-      if JS_IsFunction(ctx, val):
-        var func_data = @[cval, val]
-        let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, addr func_data[0])
-        return some(fun)
+    let val = JS_GetProperty(ctx, cval, a)
+    if JS_IsFunction(ctx, val):
+      let func_data = @[cval, val]
+      let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2,
+        func_data.toJSValueArray())
+      JS_FreeValue(ctx, cval)
+      JS_FreeValue(ctx, val)
+      return some(fun)
+    JS_FreeValue(ctx, cval)
+    if not JS_IsUndefined(val):
       return some(val)
+  return none(JSValue)
 
 proc searchNext(pager: Pager; n = 1) {.jsfunc.} =
   if pager.regex.isSome:
@@ -880,8 +890,8 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
       continue
     if sc.rewrite_url.isSome:
       let fun = sc.rewrite_url.get
-      var arg1 = ctx.toJS(url)
-      let ret = JS_Call(ctx, fun, JS_UNDEFINED, 1, addr arg1)
+      var arg0 = ctx.toJS(url)
+      let ret = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray())
       let nu = fromJS[URL](ctx, ret)
       if nu.isOk:
         if nu.get != nil:
@@ -889,7 +899,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
       elif JS_IsException(ret):
         #TODO should writeException the message to console
         pager.alert("Error rewriting URL: " & ctx.getExceptionMsg(nu.error))
-      JS_FreeValue(ctx, arg1)
+      JS_FreeValue(ctx, arg0)
       JS_FreeValue(ctx, ret)
     if sc.cookie.isSome:
       if sc.cookie.get:
@@ -986,9 +996,11 @@ proc omniRewrite(pager: Pager; s: string): string =
     if rule.match.match(s):
       let fun = rule.substitute_url.get
       let ctx = pager.jsctx
-      var arg1 = ctx.toJS(s)
-      let jsRet = JS_Call(ctx, fun, JS_UNDEFINED, 1, addr arg1)
+      var arg0 = ctx.toJS(s)
+      let jsRet = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray())
       let ret = fromJS[string](ctx, jsRet)
+      JS_FreeValue(ctx, jsRet)
+      JS_FreeValue(ctx, arg0)
       if ret.isOk:
         return ret.get
       pager.alert("Error in substitution of " & $rule.match & " for " & s &
diff --git a/test/js/class.html b/test/js/class.html
index 7c14049e..5ee2f869 100644
--- a/test/js/class.html
+++ b/test/js/class.html
@@ -1,23 +1,17 @@
 <!doctype html>
 <title>Element class test</title>
 <div class="a b c">Fail</div>
+<script src=asserts.js></script>
 <script>
-(function() {
-	let div = document.getElementsByClassName("a")[0]
-	const classes = ["a", "b", "c"];
-	let cl = div.classList;
-	for (let i = 0; i < classes.length; ++i) {
-		if (cl[i] !== classes[i])
-			return;
-	}
-	const classes2 = ["x", "y", "z"];
-	div.setAttribute("class", classes2.join(' '));
-	let i = 0;
-	for (let x of cl) {
-		if (x != classes2[i])
-			return;
-		++i;
-	}
-	div.textContent = "Success";
-})();
+const div = document.getElementsByClassName("a")[0]
+const classes = ["a", "b", "c"];
+let cl = div.classList;
+for (let i = 0; i < classes.length; ++i)
+	assert_equals(cl[i], classes[i]);
+const classes2 = ["x", "y", "z"];
+div.setAttribute("class", classes2.join(' '));
+let i = 0;
+for (let x of cl)
+	assert_equals(x, classes2[i++]);
+div.textContent = "Success";
 </script>
diff --git a/test/js/encode_decode.html b/test/js/encode_decode.html
index 069ddc72..82e9676c 100644
--- a/test/js/encode_decode.html
+++ b/test/js/encode_decode.html
@@ -1,39 +1,32 @@
 <!doctype html>
 <title>TextEncoder/TextDecoder test</title>
 <div id="success">Fail</div>
+<script src=asserts.js></script>
 <script>
-(function() {
-	/* Adapted from: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
-	function base64ToBytes(base64) {
-		const binString = atob(base64);
-		const result = [];
-		for (const c of binString)
-			result.push(Uint8Array.from(c, (m) => m.codePointAt(0)));
-		return result;
-	}
+/* Adapted from: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
+function base64ToBytes(base64) {
+	const binString = atob(base64);
+	const result = [];
+	for (const c of binString)
+		result.push(Uint8Array.from(c, (m) => m.codePointAt(0)));
+	return result;
+}
 
-	function bytesToBase64(bytes) {
-		const binString = String.fromCodePoint(...bytes);
-		return btoa(binString);
-	}
+function bytesToBase64(bytes) {
+	const binString = String.fromCodePoint(...bytes);
+	return btoa(binString);
+}
 
-	const utf8 = new TextEncoder().encode("a Ā 𐀀 文 🦄")
-	const b64utf8 = bytesToBase64(utf8);
-	if (b64utf8 !== "YSDEgCDwkICAIOaWhyDwn6aE") {
-		console.log(b64utf8);
-		return;
-	}
-	const dec = new TextDecoder();
-	const bytes = base64ToBytes(b64utf8);
-	const a = [];
-	let res = "";
-	for (const c of bytes)
-		res += dec.decode(c, {stream: true});
-	res += dec.decode();
-	if (res !== "a Ā 𐀀 文 🦄") {
-		console.log(res);
-		return;
-	}
-	document.getElementById("success").textContent = "Success";
-})();
+const utf8 = new TextEncoder().encode("a Ā 𐀀 文 🦄")
+const b64utf8 = bytesToBase64(utf8);
+assert_equals(b64utf8, "YSDEgCDwkICAIOaWhyDwn6aE")
+const dec = new TextDecoder();
+const bytes = base64ToBytes(b64utf8);
+const a = [];
+let res = "";
+for (const c of bytes)
+	res += dec.decode(c, {stream: true});
+res += dec.decode();
+assert_equals(res, "a Ā 𐀀 文 🦄");
+document.getElementById("success").textContent = "Success";
 </script>