about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-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>