Repository: source-academy/js-slang
Pull request #165 (2018-12-15 19:51:40)

Implement display and raw_display

coveralls: ## Pull Request Test Coverage Report for [Build 572](https://coveralls.io/builds/20655386)

* **13** of **14**   **(92.86%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **87.521%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/createContext.ts](https://coveralls.io/builds/20655386/source?filename=src%2FcreateContext.ts#L165) | 11 | 12 | 91.67%
<!-- | **Total:** | **13** | **14** | **92.86%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/20655386/badge)](https://coveralls.io/builds/20655386) |
| :-- | --: |
| Change from base [Build 550](https://coveralls.io/builds/20638384): |  0.0% |
| Covered Lines: | 1221 |
| Relevant Lines: | 1384 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/package.json b/package.json
index fea44329..faece674 100644
--- a/package.json
+++ b/package.json
@@ -71,9 +71,10 @@
       "/dist/"
     ],
     "coveragePathIgnorePatterns": [
+      "/dist/",
       "/node_modules/",
       "/src/typings/",
-      "/dist/"
+      "/src/utils/testing.ts"
     ]
   }
 }
diff --git a/src/createContext.ts b/src/createContext.ts
index ac5f01a6..946d2ced 100644
--- a/src/createContext.ts
+++ b/src/createContext.ts
@@ -82,22 +82,24 @@ export const importExternalSymbols = (context: Context, externalSymbols: string[
 export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIns) => {
   ensureGlobalEnvironmentExist(context)
 
-  const display = (v: Value) => externalBuiltIns.display(v, context.externalContext)
+  const rawDisplay = (v: Value) => externalBuiltIns.rawDisplay(v, context.externalContext)
+  const display = (v: Value) => (rawDisplay(stringify(v)), v)
   const prompt = (v: Value) => externalBuiltIns.prompt(v, context.externalContext)
   const alert = (v: Value) => externalBuiltIns.alert(v, context.externalContext)
   const visualiseList = (v: Value) => externalBuiltIns.visualiseList(v, context.externalContext)
 
   if (context.chapter >= 1) {
     defineBuiltin(context, 'runtime()', misc.runtime)
-    defineBuiltin(context, 'display(value)', display)
-    defineBuiltin(context, 'stringify(value)', stringify)
-    defineBuiltin(context, 'error(message)', misc.error_message)
-    defineBuiltin(context, 'prompt(message)', prompt)
-    defineBuiltin(context, 'is_number(value)', misc.is_number)
-    defineBuiltin(context, 'is_string(value)', misc.is_string)
-    defineBuiltin(context, 'is_function(value)', misc.is_function)
-    defineBuiltin(context, 'is_boolean(value)', misc.is_boolean)
-    defineBuiltin(context, 'is_undefined(value)', misc.is_undefined)
+    defineBuiltin(context, 'display(val)', display)
+    defineBuiltin(context, 'raw_display(str)', rawDisplay)
+    defineBuiltin(context, 'stringify(val)', stringify)
+    defineBuiltin(context, 'error(str)', misc.error_message)
+    defineBuiltin(context, 'prompt(str)', prompt)
+    defineBuiltin(context, 'is_number(val)', misc.is_number)
+    defineBuiltin(context, 'is_string(val)', misc.is_string)
+    defineBuiltin(context, 'is_function(val)', misc.is_function)
+    defineBuiltin(context, 'is_boolean(val)', misc.is_boolean)
+    defineBuiltin(context, 'is_undefined(val)', misc.is_undefined)
     defineBuiltin(context, 'parse_int(str, radix)', misc.parse_int)
     defineBuiltin(context, 'undefined', undefined)
     defineBuiltin(context, 'NaN', NaN)
@@ -163,18 +165,18 @@ export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIn
     defineBuiltin(context, 'alert(val)', alert)
     // tslint:disable-next-line:ban-types
     defineBuiltin(context, 'timed(fun)', (f: Function) =>
-      misc.timed(context, f, context.externalContext, externalBuiltIns.display)
+      misc.timed(context, f, context.externalContext, externalBuiltIns.rawDisplay)
     )
     defineBuiltin(context, 'assoc(val, xs)', list.assoc)
   }
 }
 
 const defaultBuiltIns: CustomBuiltIns = {
-  display: misc.display,
+  rawDisplay: misc.rawDisplay,
   // See issue #5
-  prompt: misc.display,
+  prompt: misc.rawDisplay,
   // See issue #11
-  alert: misc.display,
+  alert: misc.rawDisplay,
   visualiseList: (v: Value) => {
     throw new Error('List visualizer is not enabled')
   }
diff --git a/src/stdlib/misc.ts b/src/stdlib/misc.ts
index 609c1d35..d827fafa 100644
--- a/src/stdlib/misc.ts
+++ b/src/stdlib/misc.ts
@@ -8,10 +8,9 @@ import { Context, Value } from '../types'
  * @param externalContext a property of Context that can hold
  *   any information required for external use (optional).
  */
-export function display(value: Value, externalContext: any) {
-  const output = stringify(value)
+export function rawDisplay(value: Value, externalContext: any) {
   // tslint:disable-next-line:no-console
-  console.log(output)
+  console.log(value.toString())
   return value
 }
 
diff --git a/src/types.ts b/src/types.ts
index 4fb523be..6466f38b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,7 +7,7 @@ import * as es from 'estree'
  * different implementations. e.g display() in a web application.
  */
 export interface CustomBuiltIns {
-  display: (value: Value, externalContext: any) => void
+  rawDisplay: (value: Value, externalContext: any) => void
   prompt: (value: Value, externalContext: any) => string | null
   alert: (value: Value, externalContext: any) => void
   /* Used for list visualisation. See #12 */


Test patch:
diff --git a/src/__tests__/__snapshots__/display.ts.snap b/src/__tests__/__snapshots__/display.ts.snap
new file mode 100644
index 00000000..ed924b6d
--- /dev/null
+++ b/src/__tests__/__snapshots__/display.ts.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`display can be used to display (escaped) strings 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display(\\"Tom's assisstant said: \\\\\\"tuna.\\\\\\"\\");",
+  "displayResult": "\\"Tom's assisstant said: \\\\\\"tuna.\\\\\\"\\"",
+  "result": "Tom's assisstant said: \\"tuna.\\"",
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display arrays 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display([1, 2, [4, 5]]);",
+  "displayResult": "[1, 2, [4, 5]]",
+  "result": Array [
+    1,
+    2,
+    Array [
+      4,
+      5,
+    ],
+  ],
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display functions 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display(x => x); display((x, y) => x + y);",
+  "displayResult": "x => x
+(x, y) => x + y",
+  "result": [Function],
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display funny numbers 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display(1e38); display(NaN); display(Infinity);",
+  "displayResult": "1e+38
+NaN
+Infinity",
+  "result": Infinity,
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display lists 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display(list(1, 2));",
+  "displayResult": "[1, [2, null]]",
+  "result": Array [
+    1,
+    Array [
+      2,
+      null,
+    ],
+  ],
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display numbers 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display(0);",
+  "displayResult": "0",
+  "result": 0,
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`display can be used to display objects 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "display({a: 1, b: 2, c: {d: 3}});",
+  "displayResult": "{\\"a\\": 1, \\"b\\": 2, \\"c\\": {\\"d\\": 3}}",
+  "result": Object {
+    "a": 1,
+    "b": 2,
+    "c": Object {
+      "d": 3,
+    },
+  },
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`raw_display can be used to display (unescaped) strings directly 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "raw_display(\\"Tom's assisstant said: \\\\\\"tuna.\\\\\\"\\");",
+  "displayResult": "Tom's assisstant said: \\"tuna.\\"",
+  "result": "Tom's assisstant said: \\"tuna.\\"",
+  "visualiseListResult": undefined,
+}
+`;
diff --git a/src/__tests__/__snapshots__/index.ts.snap b/src/__tests__/__snapshots__/index.ts.snap
index 58937da3..a9fb4d51 100644
--- a/src/__tests__/__snapshots__/index.ts.snap
+++ b/src/__tests__/__snapshots__/index.ts.snap
@@ -2,255 +2,481 @@
 
 exports[`Allow display to return value it is displaying 1`] = `
 Object {
-  "status": "finished",
-  "value": 50,
+  "alertResult": undefined,
+  "code": "25*(display(1+1));",
+  "displayResult": "2",
+  "result": 50,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Arrays toString matches up with JS 1`] = `
 Object {
-  "status": "finished",
-  "value": "1,2",
+  "alertResult": undefined,
+  "code": "\\"\\" + [1, 2];",
+  "displayResult": undefined,
+  "result": "1,2",
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Arrow function definition returns itself 1`] = `
 Object {
-  "status": "finished",
-  "value": [Function],
+  "alertResult": undefined,
+  "code": "() => 42;",
+  "displayResult": undefined,
+  "result": [Function],
+  "visualiseListResult": undefined,
 }
 `;
 
-exports[`Arrow function definition returns itself 2`] = `[Function]`;
-
 exports[`Arrow function infinite recursion with list args represents CallExpression well 1`] = `
-"Line 2: Infinite recursion
-  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]]).."
+Object {
+  "alertResult": undefined,
+  "code": "const f = xs => append(f(xs), list());
+f(list(1, 2));",
+  "displayResult": undefined,
+  "error": "Line 1: Infinite recursion
+  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]])..",
+  "visualiseListResult": undefined,
+}
 `;
 
-exports[`Builtins hide their implementation 1`] = `
+exports[`Builtins hide their implementation when stringify 1`] = `
 Object {
-  "status": "finished",
-  "value": "function pair(left, right) {
+  "alertResult": undefined,
+  "code": "stringify(pair);",
+  "displayResult": undefined,
+  "result": "function pair(left, right) {
 	[implementation hidden]
 }",
+  "visualiseListResult": undefined,
 }
 `;
 
-exports[`Builtins hide their implementation 2`] = `
+exports[`Builtins hide their implementation when toString 1`] = `
 Object {
-  "status": "finished",
-  "value": "function pair(left, right) {
+  "alertResult": undefined,
+  "code": "\\"\\"+pair;",
+  "displayResult": undefined,
+  "result": "function pair(left, right) {
 	[implementation hidden]
 }",
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Can overwrite lets when assignment is allowed 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "function test() {
+  let variable = false;
+  variable = true;
+  return variable;
+}
+test();",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
-exports[`Cannot overwrite consts even when assignment is allowed 1`] = `"Line 4: Cannot assign new value to constant constant"`;
+exports[`Cannot overwrite consts even when assignment is allowed 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "function test(){
+  const constant = 3;
+  constant = 4;
+  return constant;
+}
+test();",
+  "displayResult": undefined,
+  "error": "Line 3: Cannot assign new value to constant constant",
+  "visualiseListResult": undefined,
+}
+`;
 
 exports[`Deep object assignment and retrieval 1`] = `
 Object {
-  "status": "finished",
-  "value": "string",
+  "alertResult": undefined,
+  "code": "const o = {};
+o.a = {};
+o.a.b = {};
+o.a.b.c = \\"string\\";
+o.a.b.c;",
+  "displayResult": undefined,
+  "result": "string",
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Empty code returns undefined 1`] = `
 Object {
-  "status": "finished",
-  "value": undefined,
+  "alertResult": undefined,
+  "code": "",
+  "displayResult": undefined,
+  "result": undefined,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Factorial arrow function 1`] = `
 Object {
-  "status": "finished",
-  "value": 120,
+  "alertResult": undefined,
+  "code": "const fac = (i) => i === 1 ? 1 : i * fac(i-1);
+fac(5);",
+  "displayResult": undefined,
+  "result": 120,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Function infinite recursion with list args represents CallExpression well 1`] = `
-"Line 2: Infinite recursion
-  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]]).."
+Object {
+  "alertResult": undefined,
+  "code": "function f(xs) { return append(f(xs), list()); }
+f(list(1, 2));",
+  "displayResult": undefined,
+  "error": "Line 1: Infinite recursion
+  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]])..",
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Functions passed into non-source functions remain equal 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "function t(x, y, z) {
+  return x + y + z;
+}
+identity(t) === t && t(1, 2, 3) === 6;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
+}
 `;
 
 exports[`Objects toString matches up with JS 1`] = `
 Object {
-  "status": "finished",
-  "value": "[object Object]",
+  "alertResult": undefined,
+  "code": "\\"\\" + ({a: 1});",
+  "displayResult": undefined,
+  "result": "[object Object]",
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Simple arrow function infinite recursion represents CallExpression well 1`] = `
-"Line 1: Infinite recursion
-  x(x => x(x)(x))..  x(x => x(x)(x))..  x(x => x(x)(x)).."
+Object {
+  "alertResult": undefined,
+  "code": "(x => x(x)(x))(x => x(x)(x));",
+  "displayResult": undefined,
+  "error": "Line 1: Infinite recursion
+  x(x => x(x)(x))..  x(x => x(x)(x))..  x(x => x(x)(x))..",
+  "visualiseListResult": undefined,
+}
 `;
 
 exports[`Simple function infinite recursion represents CallExpression well 1`] = `
-"Line 1: Infinite recursion
+Object {
+  "alertResult": undefined,
+  "code": "function f(x) {return x(x)(x);} f(f);",
+  "displayResult": undefined,
+  "error": "Line 1: Infinite recursion
   x(function f(x) {
   return x(x)(x);
 })..  x(function f(x) {
   return x(x)(x);
 })..  x(function f(x) {
   return x(x)(x);
-}).."
+})..",
+  "visualiseListResult": undefined,
+}
 `;
 
 exports[`Simple object assignment and retrieval 1`] = `
 Object {
-  "status": "finished",
-  "value": 1,
+  "alertResult": undefined,
+  "code": "const o = {};
+o.a = 1;
+o.a;",
+  "displayResult": undefined,
+  "result": 1,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Single boolean self-evaluates to itself 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Single number self-evaluates to itself 1`] = `
 Object {
-  "status": "finished",
-  "value": 42,
+  "alertResult": undefined,
+  "code": "42;",
+  "displayResult": undefined,
+  "result": 42,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`Single string self-evaluates to itself 1`] = `
 Object {
-  "status": "finished",
-  "value": "42",
+  "alertResult": undefined,
+  "code": "'42';",
+  "displayResult": undefined,
+  "result": "42",
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Test apply_in_underlying_javascript 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "apply_in_underlying_javascript((a, b, c) => a * b * c, list(2, 5, 6));",
+  "displayResult": undefined,
+  "result": 60,
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Test equal for different lists 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "!equal(list(1, 2), pair(1, 2)) && !equal(list(1, 2, 3), list(1, list(2, 3)));",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Test equal for lists 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "equal(list(1, 2), pair(1, pair(2, null))) && equal(list(1, 2, 3, 4), list(1, 2, 3, 4));",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
+}
+`;
+
+exports[`Test equal for primitives 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "equal(1, 1) && equal(\\"str\\", \\"str\\") && equal(null, null) && !equal(1, 2) && !equal(\\"str\\", \\"\\");",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`false if with empty else works 1`] = `
 Object {
-  "status": "finished",
-  "value": undefined,
+  "alertResult": undefined,
+  "code": "if (false) {
+} else {
+}",
+  "displayResult": undefined,
+  "result": undefined,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`false if with nonempty if works 1`] = `
 Object {
-  "status": "finished",
-  "value": 2,
+  "alertResult": undefined,
+  "code": "if (false) {
+} else {
+  2;
+}",
+  "displayResult": undefined,
+  "result": 2,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`functions toString (mostly) matches up with JS 1`] = `
 Object {
-  "status": "finished",
-  "value": "a => bfunction f(x) {
+  "alertResult": undefined,
+  "code": "function f(x) {
+  return 5;
+}
+\\"\\" + (a=>b) + f;",
+  "displayResult": undefined,
+  "result": "a => bfunction f(x) {
   return 5;
 }",
+  "visualiseListResult": undefined,
 }
 `;
 
-exports[`parseError for missing semicolon 1`] = `"Line 1: Missing semicolon at the end of statement"`;
+exports[`parseError for missing semicolon 1`] = `
+Object {
+  "alertResult": undefined,
+  "code": "42",
+  "displayResult": undefined,
+  "error": "Line 1: Missing semicolon at the end of statement",
+  "visualiseListResult": undefined,
+}
+`;
 
 exports[`primitives toString matches up with JS 1`] = `
 Object {
-  "status": "finished",
-  "value": "truefalse11.5nullundefinedNaN",
+  "alertResult": undefined,
+  "code": "\\"\\" + true + false + 1 + 1.5 + null + undefined + NaN;",
+  "displayResult": undefined,
+  "result": "truefalse11.5nullundefinedNaN",
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test && shortcircuiting 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "false && 1();",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test || shortcircuiting 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true || 1();",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test false && false 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "false && false;",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test false && true 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "false && true;",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test false || false 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "false || false;",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test false || true 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "false || true;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test false conditional expression 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "false ? true : false;",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test true && false 1`] = `
 Object {
-  "status": "finished",
-  "value": false,
+  "alertResult": undefined,
+  "code": "true && false;",
+  "displayResult": undefined,
+  "result": false,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test true && true 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true && true;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test true || false 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true || false;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test true || true 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true || true;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`test true conditional expression 1`] = `
 Object {
-  "status": "finished",
-  "value": true,
+  "alertResult": undefined,
+  "code": "true ? true : false;",
+  "displayResult": undefined,
+  "result": true,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`true if with empty if works 1`] = `
 Object {
-  "status": "finished",
-  "value": undefined,
+  "alertResult": undefined,
+  "code": "if (true) {
+} else {
+}",
+  "displayResult": undefined,
+  "result": undefined,
+  "visualiseListResult": undefined,
 }
 `;
 
 exports[`true if with nonempty if works 1`] = `
 Object {
-  "status": "finished",
-  "value": 1,
+  "alertResult": undefined,
+  "code": "if (true) {
+  1;
+} else {
+}",
+  "displayResult": undefined,
+  "result": 1,
+  "visualiseListResult": undefined,
 }
 `;
diff --git a/src/__tests__/__snapshots__/stdlib.ts.snap b/src/__tests__/__snapshots__/stdlib.ts.snap
index 1661e81f..0155d3b7 100644
--- a/src/__tests__/__snapshots__/stdlib.ts.snap
+++ b/src/__tests__/__snapshots__/stdlib.ts.snap
@@ -1,190 +1,190 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Syntaxes are allowed in the chapter they are introduced 1`] = `
+exports[`Builtins work as expected 1`] = `
 "
       display('message');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 2`] = `""`;
+exports[`Builtins work as expected 2`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 3`] = `
+exports[`Builtins work as expected 3`] = `
 "
       error('error!');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 4`] = `"Line 2: Error: \\"error!\\""`;
+exports[`Builtins work as expected 4`] = `"Line 2: Error: \\"error!\\""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 5`] = `
+exports[`Builtins work as expected 5`] = `
 "
       is_undefined(undefined);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 6`] = `""`;
+exports[`Builtins work as expected 6`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 7`] = `
+exports[`Builtins work as expected 7`] = `
 "
       is_undefined(null);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 8`] = `""`;
+exports[`Builtins work as expected 8`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 9`] = `
+exports[`Builtins work as expected 9`] = `
 "
       is_null(undefined);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 10`] = `""`;
+exports[`Builtins work as expected 10`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 11`] = `
+exports[`Builtins work as expected 11`] = `
 "
       is_null(null);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 12`] = `""`;
+exports[`Builtins work as expected 12`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 13`] = `
+exports[`Builtins work as expected 13`] = `
 "
       is_string('string');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 14`] = `""`;
+exports[`Builtins work as expected 14`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 15`] = `
+exports[`Builtins work as expected 15`] = `
 "
       is_string('true');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 16`] = `""`;
+exports[`Builtins work as expected 16`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 17`] = `
+exports[`Builtins work as expected 17`] = `
 "
       is_string('1');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 18`] = `""`;
+exports[`Builtins work as expected 18`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 19`] = `
+exports[`Builtins work as expected 19`] = `
 "
       is_string(true);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 20`] = `""`;
+exports[`Builtins work as expected 20`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 21`] = `
+exports[`Builtins work as expected 21`] = `
 "
       is_string(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 22`] = `""`;
+exports[`Builtins work as expected 22`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 23`] = `
+exports[`Builtins work as expected 23`] = `
 "
       is_number('string');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 24`] = `""`;
+exports[`Builtins work as expected 24`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 25`] = `
+exports[`Builtins work as expected 25`] = `
 "
       is_number('true');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 26`] = `""`;
+exports[`Builtins work as expected 26`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 27`] = `
+exports[`Builtins work as expected 27`] = `
 "
       is_number('1');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 28`] = `""`;
+exports[`Builtins work as expected 28`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 29`] = `
+exports[`Builtins work as expected 29`] = `
 "
       is_number(true);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 30`] = `""`;
+exports[`Builtins work as expected 30`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 31`] = `
+exports[`Builtins work as expected 31`] = `
 "
       is_number(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 32`] = `""`;
+exports[`Builtins work as expected 32`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 33`] = `
+exports[`Builtins work as expected 33`] = `
 "
       is_boolean('string');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 34`] = `""`;
+exports[`Builtins work as expected 34`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 35`] = `
+exports[`Builtins work as expected 35`] = `
 "
       is_boolean('true');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 36`] = `""`;
+exports[`Builtins work as expected 36`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 37`] = `
+exports[`Builtins work as expected 37`] = `
 "
       is_boolean('1');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 38`] = `""`;
+exports[`Builtins work as expected 38`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 39`] = `
+exports[`Builtins work as expected 39`] = `
 "
       is_boolean(true);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 40`] = `""`;
+exports[`Builtins work as expected 40`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 41`] = `
+exports[`Builtins work as expected 41`] = `
 "
       is_boolean(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 42`] = `""`;
+exports[`Builtins work as expected 42`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 43`] = `
+exports[`Builtins work as expected 43`] = `
 "
       is_function(display);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 44`] = `""`;
+exports[`Builtins work as expected 44`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 45`] = `
+exports[`Builtins work as expected 45`] = `
 "
       is_function(x => x);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 46`] = `""`;
+exports[`Builtins work as expected 46`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 47`] = `
+exports[`Builtins work as expected 47`] = `
 "
       function f(x) {
         return x;
@@ -193,169 +193,169 @@ exports[`Syntaxes are allowed in the chapter they are introduced 47`] = `
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 48`] = `""`;
+exports[`Builtins work as expected 48`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 49`] = `
+exports[`Builtins work as expected 49`] = `
 "
       is_function(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 50`] = `""`;
+exports[`Builtins work as expected 50`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 51`] = `
+exports[`Builtins work as expected 51`] = `
 "
       is_array(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 52`] = `""`;
+exports[`Builtins work as expected 52`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 53`] = `
+exports[`Builtins work as expected 53`] = `
 "
       is_array(pair(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 54`] = `""`;
+exports[`Builtins work as expected 54`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 55`] = `
+exports[`Builtins work as expected 55`] = `
 "
       is_array([1]);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 56`] = `""`;
+exports[`Builtins work as expected 56`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 57`] = `
+exports[`Builtins work as expected 57`] = `
 "
       is_object(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 58`] = `""`;
+exports[`Builtins work as expected 58`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 59`] = `
+exports[`Builtins work as expected 59`] = `
 "
       is_object(pair(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 60`] = `""`;
+exports[`Builtins work as expected 60`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 61`] = `
+exports[`Builtins work as expected 61`] = `
 "
       is_object([1]);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 62`] = `""`;
+exports[`Builtins work as expected 62`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 63`] = `
+exports[`Builtins work as expected 63`] = `
 "
       is_object({});
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 64`] = `""`;
+exports[`Builtins work as expected 64`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 65`] = `
+exports[`Builtins work as expected 65`] = `
 "
       is_object({a: 1});
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 66`] = `""`;
+exports[`Builtins work as expected 66`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 67`] = `
+exports[`Builtins work as expected 67`] = `
 "
       is_object(x => x);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 68`] = `""`;
+exports[`Builtins work as expected 68`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 69`] = `
+exports[`Builtins work as expected 69`] = `
 "
       is_object(display);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 70`] = `""`;
+exports[`Builtins work as expected 70`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 71`] = `
+exports[`Builtins work as expected 71`] = `
 "
       is_object(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 72`] = `""`;
+exports[`Builtins work as expected 72`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 73`] = `
+exports[`Builtins work as expected 73`] = `
 "
       is_object('string');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 74`] = `""`;
+exports[`Builtins work as expected 74`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 75`] = `
+exports[`Builtins work as expected 75`] = `
 "
       is_object(true);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 76`] = `""`;
+exports[`Builtins work as expected 76`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 77`] = `
+exports[`Builtins work as expected 77`] = `
 "
       is_NaN(1 / 0);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 78`] = `""`;
+exports[`Builtins work as expected 78`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 79`] = `
+exports[`Builtins work as expected 79`] = `
 "
       is_NaN(NaN);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 80`] = `""`;
+exports[`Builtins work as expected 80`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 81`] = `
+exports[`Builtins work as expected 81`] = `
 "
       is_NaN(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 82`] = `""`;
+exports[`Builtins work as expected 82`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 83`] = `
+exports[`Builtins work as expected 83`] = `
 "
       is_NaN(x => x);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 84`] = `""`;
+exports[`Builtins work as expected 84`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 85`] = `
+exports[`Builtins work as expected 85`] = `
 "
       has_own_property({a: 1, b: 2}, 'a');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 86`] = `""`;
+exports[`Builtins work as expected 86`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 87`] = `
+exports[`Builtins work as expected 87`] = `
 "
       has_own_property({a: 1, b: 2}, 'c');
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 88`] = `""`;
+exports[`Builtins work as expected 88`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 89`] = `
+exports[`Builtins work as expected 89`] = `
 "
       let o = {a: 1};
       let o2 = {b: 2};
@@ -364,9 +364,9 @@ exports[`Syntaxes are allowed in the chapter they are introduced 89`] = `
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 90`] = `""`;
+exports[`Builtins work as expected 90`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 91`] = `
+exports[`Builtins work as expected 91`] = `
 "
       let o = {a: 1};
       let o2 = {b: 2};
@@ -375,9 +375,9 @@ exports[`Syntaxes are allowed in the chapter they are introduced 91`] = `
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 92`] = `""`;
+exports[`Builtins work as expected 92`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 93`] = `
+exports[`Builtins work as expected 93`] = `
 "
       let o = {a: 1};
       let o2 = {b: 2};
@@ -386,41 +386,41 @@ exports[`Syntaxes are allowed in the chapter they are introduced 93`] = `
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 94`] = `""`;
+exports[`Builtins work as expected 94`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 95`] = `
+exports[`Builtins work as expected 95`] = `
 "
       array_length([1]);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 96`] = `""`;
+exports[`Builtins work as expected 96`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 97`] = `
+exports[`Builtins work as expected 97`] = `
 "
       parse_int('10', 10);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 98`] = `""`;
+exports[`Builtins work as expected 98`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 99`] = `
+exports[`Builtins work as expected 99`] = `
 "
       parse_int('10', 2);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 100`] = `""`;
+exports[`Builtins work as expected 100`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 101`] = `
+exports[`Builtins work as expected 101`] = `
 "
       is_number(runtime());
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 102`] = `""`;
+exports[`Builtins work as expected 102`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 103`] = `
+exports[`Builtins work as expected 103`] = `
 "
       const x = runtime();
       const f = x => x === 0 ? x : f(x-1);
@@ -430,108 +430,108 @@ exports[`Syntaxes are allowed in the chapter they are introduced 103`] = `
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 104`] = `""`;
+exports[`Builtins work as expected 104`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 105`] = `
+exports[`Builtins work as expected 105`] = `
 "
       pair(1, 2);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 106`] = `""`;
+exports[`Builtins work as expected 106`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 107`] = `
+exports[`Builtins work as expected 107`] = `
 "
       list(1, 2);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 108`] = `""`;
+exports[`Builtins work as expected 108`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 109`] = `
+exports[`Builtins work as expected 109`] = `
 "
       is_list(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 110`] = `""`;
+exports[`Builtins work as expected 110`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 111`] = `
+exports[`Builtins work as expected 111`] = `
 "
       is_list(pair(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 112`] = `""`;
+exports[`Builtins work as expected 112`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 113`] = `
+exports[`Builtins work as expected 113`] = `
 "
       is_list(list(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 114`] = `""`;
+exports[`Builtins work as expected 114`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 115`] = `
+exports[`Builtins work as expected 115`] = `
 "
       head(pair(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 116`] = `""`;
+exports[`Builtins work as expected 116`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 117`] = `
+exports[`Builtins work as expected 117`] = `
 "
       tail(pair(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 118`] = `""`;
+exports[`Builtins work as expected 118`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 119`] = `
+exports[`Builtins work as expected 119`] = `
 "
       head(null);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 120`] = `"Line 2: Error: head(xs) expects a pair as argument xs, but encountered null"`;
+exports[`Builtins work as expected 120`] = `"Line 2: Error: head(xs) expects a pair as argument xs, but encountered null"`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 121`] = `
+exports[`Builtins work as expected 121`] = `
 "
       tail(null);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 122`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered null"`;
+exports[`Builtins work as expected 122`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered null"`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 123`] = `
+exports[`Builtins work as expected 123`] = `
 "
       head(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 124`] = `"Line 2: Error: head(xs) expects a pair as argument xs, but encountered 1"`;
+exports[`Builtins work as expected 124`] = `"Line 2: Error: head(xs) expects a pair as argument xs, but encountered 1"`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 125`] = `
+exports[`Builtins work as expected 125`] = `
 "
       tail(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 126`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered 1"`;
+exports[`Builtins work as expected 126`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered 1"`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 127`] = `
+exports[`Builtins work as expected 127`] = `
 "
       length(list(1, 2));
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 128`] = `""`;
+exports[`Builtins work as expected 128`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 129`] = `
+exports[`Builtins work as expected 129`] = `
 "
       length(1);
       "
 `;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 130`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered 1"`;
+exports[`Builtins work as expected 130`] = `"Line 2: Error: tail(xs) expects a pair as argument xs, but encountered 1"`;
diff --git a/src/__tests__/display.ts b/src/__tests__/display.ts
new file mode 100644
index 00000000..56efe227
--- /dev/null
+++ b/src/__tests__/display.ts
@@ -0,0 +1,49 @@
+import { expectDisplayResult } from '../utils/testing'
+
+test('display can be used to display numbers', () => {
+  return expectDisplayResult(`display(0);`).toMatchInlineSnapshot(`"0"`)
+})
+
+test('display can be used to display funny numbers', () => {
+  return expectDisplayResult(`display(1e38); display(NaN); display(Infinity);`)
+    .toMatchInlineSnapshot(`
+"1e+38
+NaN
+Infinity"
+`)
+})
+
+test('display can be used to display (escaped) strings', () => {
+  return expectDisplayResult(
+    `display("Tom's assisstant said: \\"tuna.\\"");`
+  ).toMatchInlineSnapshot(`"\\"Tom's assisstant said: \\\\\\"tuna.\\\\\\"\\""`)
+})
+
+test('raw_display can be used to display (unescaped) strings directly', () => {
+  return expectDisplayResult(
+    `raw_display("Tom's assisstant said: \\"tuna.\\"");`
+  ).toMatchInlineSnapshot(`"Tom's assisstant said: \\"tuna.\\""`)
+})
+
+test('display can be used to display functions', () => {
+  return expectDisplayResult(`display(x => x); display((x, y) => x + y);`).toMatchInlineSnapshot(`
+"x => x
+(x, y) => x + y"
+`)
+})
+
+test('display can be used to display lists', () => {
+  return expectDisplayResult(`display(list(1, 2));`, 2).toMatchInlineSnapshot(`"[1, [2, null]]"`)
+})
+
+test('display can be used to display arrays', () => {
+  return expectDisplayResult(`display([1, 2, [4, 5]]);`, 3).toMatchInlineSnapshot(
+    `"[1, 2, [4, 5]]"`
+  )
+})
+
+test('display can be used to display objects', () => {
+  return expectDisplayResult(`display({a: 1, b: 2, c: {d: 3}});`, 100).toMatchInlineSnapshot(
+    `"{\\"a\\": 1, \\"b\\": 2, \\"c\\": {\\"d\\": 3}}"`
+  )
+})
diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts
index 9b6a65c0..bde309e9 100644
--- a/src/__tests__/index.ts
+++ b/src/__tests__/index.ts
@@ -1,593 +1,325 @@
-import { stripIndent } from 'common-tags'
-import { defineBuiltin } from '../createContext'
-import { parseError, runInContext } from '../index'
-import { mockContext } from '../mocks/context'
-import { Finished } from '../types'
+import {
+  expectError,
+  expectErrorNoSnapshot,
+  expectResult,
+  expectToLooselyMatchJS,
+  expectToMatchJS,
+  stripIndent
+} from '../utils/testing'
 
 test('Empty code returns undefined', () => {
-  const code = ''
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(undefined)
-  })
+  return expectResult('').toBe(undefined)
 })
 
 test('Single string self-evaluates to itself', () => {
-  const code = "'42';"
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe('42')
-  })
+  return expectResult("'42';").toBe('42')
 })
 
 test('Allow display to return value it is displaying', () => {
-  const code = '25*(display(1+1));'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(50)
-  })
+  return expectResult('25*(display(1+1));').toBe(50)
 })
 
 test('Single number self-evaluates to itself', () => {
-  const code = '42;'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(42)
-  })
+  return expectResult('42;').toBe(42)
 })
 
 test('Single boolean self-evaluates to itself', () => {
-  const code = 'true;'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult('true;').toBe(true)
 })
 
 test('Arrow function definition returns itself', () => {
-  const code = '() => 42;'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toMatchSnapshot()
-  })
-})
-
-test('Builtins hide their implementation', () => {
-  const code = 'stringify(pair);'
-  const context = mockContext(2)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-  })
-})
-
-test('Builtins hide their implementation', () => {
-  const code = '""+pair;'
-  const context = mockContext(2)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-  })
+  return expectResult('() => 42;').toMatchInlineSnapshot(`[Function]`)
+})
+
+test('Builtins hide their implementation when stringify', () => {
+  return expectResult('stringify(pair);', 2).toMatchInlineSnapshot(`
+"function pair(left, right) {
+	[implementation hidden]
+}"
+`)
+})
+
+test('Builtins hide their implementation when toString', () => {
+  return expectResult('""+pair;', 2).toMatchInlineSnapshot(`
+"function pair(left, right) {
+	[implementation hidden]
+}"
+`)
 })
 
 test('Objects toString matches up with JS', () => {
-  const code = '"" + ({a: 1});'
-  const context = mockContext(100)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    // tslint:disable-next-line:no-eval
-    expect((obj as Finished).value).toEqual(eval(code))
-  })
+  return expectToMatchJS('"" + ({a: 1});', 100)
 })
 
 test('Arrays toString matches up with JS', () => {
-  const code = '"" + [1, 2];'
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    // tslint:disable-next-line:no-eval
-    expect((obj as Finished).value).toEqual(eval(code))
-  })
+  return expectToMatchJS('"" + [1, 2];', 3)
 })
 
 test('functions toString (mostly) matches up with JS', () => {
-  const code = `
-    function f(x) {
-      return 5;
-    }
-    "" + (a=>b) + f;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    // tslint:disable-next-line:no-eval
-    expect((obj as Finished).value.replace(/ /g, '')).toEqual(eval(code).replace(/ /g, ''))
-  })
+  return expectToLooselyMatchJS(stripIndent`
+  function f(x) {
+    return 5;
+  }
+  "" + (a=>b) + f;
+  `)
 })
 
 test('primitives toString matches up with JS', () => {
-  const code = '"" + true + false + 1 + 1.5 + null + undefined + NaN;'
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    // tslint:disable-next-line:no-eval
-    expect((obj as Finished).value).toEqual(eval(code))
-  })
+  return expectToMatchJS('"" + true + false + 1 + 1.5 + null + undefined + NaN;', 2)
 })
 
 test('Factorial arrow function', () => {
-  const code = stripIndent`
+  return expectResult(stripIndent`
     const fac = (i) => i === 1 ? 1 : i * fac(i-1);
     fac(5);
-  `
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(120)
-  })
+  `).toBe(120)
 })
 
 test('parseError for missing semicolon', () => {
-  const code = '42'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  return expectError('42').toMatchInlineSnapshot(
+    `"Line 1: Missing semicolon at the end of statement"`
+  )
 })
 
 test('Simple arrow function infinite recursion represents CallExpression well', () => {
-  const code = '(x => x(x)(x))(x => x(x)(x));'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  return expectError('(x => x(x)(x))(x => x(x)(x));').toMatchInlineSnapshot(`
+"Line 1: Infinite recursion
+  x(x => x(x)(x))..  x(x => x(x)(x))..  x(x => x(x)(x)).."
+`)
 }, 30000)
 
 test('Simple function infinite recursion represents CallExpression well', () => {
-  const code = 'function f(x) {return x(x)(x);} f(f);'
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  return expectError('function f(x) {return x(x)(x);} f(f);').toMatchInlineSnapshot(`
+"Line 1: Infinite recursion
+  x(function f(x) {
+  return x(x)(x);
+})..  x(function f(x) {
+  return x(x)(x);
+})..  x(function f(x) {
+  return x(x)(x);
+}).."
+`)
 }, 30000)
 
 test('Cannot overwrite consts even when assignment is allowed', () => {
-  const code = `
-  function test(){
-    const constant = 3;
-    constant = 4;
-    return constant;
-  }
-  test();
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('error')
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  return expectError(
+    stripIndent`
+    function test(){
+      const constant = 3;
+      constant = 4;
+      return constant;
+    }
+    test();
+  `,
+    3
+  ).toMatchInlineSnapshot(`"Line 3: Cannot assign new value to constant constant"`)
 })
 
 test('Can overwrite lets when assignment is allowed', () => {
-  const code = `
-  function test(){
-    let variable = false;
-    variable = true;
-    return variable;
-  }
-  test();
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult(
+    stripIndent`
+    function test() {
+      let variable = false;
+      variable = true;
+      return variable;
+    }
+    test();
+  `,
+    3
+  ).toBe(true)
 })
 
 test('Arrow function infinite recursion with list args represents CallExpression well', () => {
-  const code = `
+  return expectError(
+    stripIndent`
     const f = xs => append(f(xs), list());
     f(list(1, 2));
-  `
-  const context = mockContext(2)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  `,
+    2
+  ).toMatchInlineSnapshot(`
+"Line 1: Infinite recursion
+  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]]).."
+`)
 }, 30000)
 
 test('Function infinite recursion with list args represents CallExpression well', () => {
-  const code = `
+  return expectError(
+    stripIndent`
     function f(xs) { return append(f(xs), list()); }
     f(list(1, 2));
-  `
-  const context = mockContext(2)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toMatchSnapshot()
-  })
+  `,
+    2
+  ).toMatchInlineSnapshot(`
+"Line 1: Infinite recursion
+  f([1, [2, null]])..  f([1, [2, null]])..  f([1, [2, null]]).."
+`)
 }, 30000)
 
 test('Arrow function infinite recursion with different args represents CallExpression well', () => {
-  const code = `
+  return expectErrorNoSnapshot(stripIndent`
     const f = i => f(i+1) - 1;
     f(0);
-  `
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toEqual(
-      expect.stringMatching(/^Line 2: Infinite recursion\n\ *(f\(\d*\)[^f]{2,4}){3}/)
-    )
-  })
+  `).toEqual(expect.stringMatching(/^Line 1: Infinite recursion\n\ *(f\(\d*\)[^f]{2,4}){3}/))
 }, 30000)
 
 test('Function infinite recursion with different args represents CallExpression well', () => {
-  const code = `
+  return expectErrorNoSnapshot(stripIndent`
     function f(i) { return f(i+1) - 1; }
     f(0);
-  `
-  const context = mockContext()
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    const errors = parseError(context.errors)
-    expect(errors).toEqual(
-      expect.stringMatching(/^Line 2: Infinite recursion\n\ *(f\(\d*\)[^f]{2,4}){3}/)
-    )
-  })
+  `).toEqual(expect.stringMatching(/^Line 1: Infinite recursion\n\ *(f\(\d*\)[^f]{2,4}){3}/))
 }, 30000)
 
 test('Functions passed into non-source functions remain equal', () => {
-  const code = `
-  function t(x, y, z) {
-    return x + y + z;
-  }
-  identity(t) === t && t(1, 2, 3) === 6;
-  `
-  const context = mockContext(4)
-  defineBuiltin(context, 'identity(x)', (x: any) => x)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult(
+    stripIndent`
+    function t(x, y, z) {
+      return x + y + z;
+    }
+    identity(t) === t && t(1, 2, 3) === 6;
+  `,
+    1,
+    { 'identity(x)': (x: any) => x }
+  ).toBe(true)
 })
 
 test('Simple object assignment and retrieval', () => {
-  const code = `
+  return expectResult(
+    stripIndent`
     const o = {};
     o.a = 1;
     o.a;
-   `
-  const context = mockContext(100)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(1)
-  })
+  `,
+    100
+  ).toBe(1)
 })
 
 test('Deep object assignment and retrieval', () => {
-  const code = `
+  return expectResult(
+    stripIndent`
     const o = {};
     o.a = {};
     o.a.b = {};
     o.a.b.c = "string";
     o.a.b.c;
-   `
-  const context = mockContext(100)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj).toMatchSnapshot()
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe('string')
-  })
+  `,
+    100
+  ).toBe('string')
 })
 
 test('Test apply_in_underlying_javascript', () => {
-  const code = `
-  apply_in_underlying_javascript((a, b, c) => a * b * c, list(2, 5, 6));
-  `
-  const context = mockContext(4)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(60)
-  })
+  return expectResult(
+    stripIndent`
+    apply_in_underlying_javascript((a, b, c) => a * b * c, list(2, 5, 6));
+  `,
+    4
+  ).toBe(60)
 })
 
 test('Test equal for primitives', () => {
-  const code = `
-  equal(1, 1) && equal("str", "str") && equal(null, null) && !equal(1, 2) && !equal("str", "");
-  `
-  const context = mockContext(4)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult(
+    stripIndent`
+    equal(1, 1) && equal("str", "str") && equal(null, null) && !equal(1, 2) && !equal("str", "");
+  `,
+    2
+  ).toBe(true)
 })
 
 test('Test equal for lists', () => {
-  const code = `
-  equal(list(1, 2), pair(1, pair(2, null))) && equal(list(1, 2, 3, 4), list(1, 2, 3, 4));
-  `
-  const context = mockContext(4)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult(
+    stripIndent`
+    equal(list(1, 2), pair(1, pair(2, null))) && equal(list(1, 2, 3, 4), list(1, 2, 3, 4));
+  `,
+    2
+  ).toBe(true)
 })
 
 test('Test equal for different lists', () => {
-  const code = `
-  !equal(list(1, 2), pair(1, 2)) && !equal(list(1, 2, 3), list(1, list(2, 3)));
-  `
-  const context = mockContext(4)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectResult(
+    stripIndent`
+    !equal(list(1, 2), pair(1, 2)) && !equal(list(1, 2, 3), list(1, list(2, 3)));
+  `,
+    2
+  ).toBe(true)
 })
 
 test('true if with empty if works', () => {
-  const code = `
+  return expectResult(stripIndent`
     if (true) {
     } else {
     }
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(undefined)
-  })
+  `).toBe(undefined)
 })
 
 test('true if with nonempty if works', () => {
-  const code = `
+  return expectResult(stripIndent`
     if (true) {
       1;
     } else {
     }
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(1)
-  })
+  `).toBe(1)
 })
 
 test('false if with empty else works', () => {
-  const code = `
+  return expectResult(stripIndent`
     if (false) {
     } else {
     }
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(undefined)
-  })
+  `).toBe(undefined)
 })
 
 test('false if with nonempty if works', () => {
-  const code = `
+  return expectResult(stripIndent`
     if (false) {
     } else {
       2;
     }
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(2)
-  })
+  `).toBe(2)
 })
 
 test('test true conditional expression', () => {
-  const code = `
-    true ? true : false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('true ? true : false;')
 })
 
 test('test false conditional expression', () => {
-  const code = `
-    false ? true : false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('false ? true : false;')
 })
 
 test('test false && true', () => {
-  const code = `
-    false && true;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('false && true;')
 })
 
 test('test false && false', () => {
-  const code = `
-    false && false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('false && false;')
 })
 
 test('test true && false', () => {
-  const code = `
-    true && false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('true && false;')
 })
 
 test('test true && true', () => {
-  const code = `
-    true && true;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('true && true;')
 })
 
 test('test && shortcircuiting', () => {
-  const code = `
-    false && 1();
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('false && 1();')
 })
 
 test('test false || true', () => {
-  const code = `
-    false || true;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('false || true;')
 })
 
 test('test false || false', () => {
-  const code = `
-    false || false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(false)
-  })
+  return expectToMatchJS('false || false;')
 })
 
 test('test true || false', () => {
-  const code = `
-    true || false;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('true || false;')
 })
 
 test('test true || true', () => {
-  const code = `
-    true || true;
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('true || true;')
 })
 
 test('test || shortcircuiting', () => {
-  const code = `
-    true || 1();
-  `
-  const context = mockContext(3)
-  const promise = runInContext(code, context, { scheduler: 'preemptive' })
-  return promise.then(obj => {
-    expect(obj.status).toBe('finished')
-    expect(obj).toMatchSnapshot()
-    expect((obj as Finished).value).toBe(true)
-  })
+  return expectToMatchJS('true || 1();')
 })
diff --git a/src/__tests__/stdlib.ts b/src/__tests__/stdlib.ts
index 9ac4d331..c032ad95 100644
--- a/src/__tests__/stdlib.ts
+++ b/src/__tests__/stdlib.ts
@@ -2,7 +2,7 @@ import { parseError, runInContext } from '../index'
 import { mockContext } from '../mocks/context'
 import { Finished } from '../types'
 
-test('Syntaxes are allowed in the chapter they are introduced', () => {
+test('Builtins work as expected', () => {
   const code = [
     [
       1,
diff --git a/src/utils/testing.ts b/src/utils/testing.ts
new file mode 100644
index 00000000..0abcf7e7
--- /dev/null
+++ b/src/utils/testing.ts
@@ -0,0 +1,197 @@
+export { stripIndent, oneLine } from 'common-tags'
+
+import { default as createContext, defineBuiltin } from '../createContext'
+import { parseError, runInContext } from '../index'
+import { Context, CustomBuiltIns, Finished, Result, Value } from '../types'
+
+export interface TestContext extends Context {
+  displayResult?: string
+  promptResult?: string
+  alertResult?: string
+  visualiseListResult?: Value[]
+}
+
+interface TestBuiltins {
+  // tslint:disable-next-line:ban-types
+  [funcName: string]: Function
+}
+
+function createTestContext(
+  chapter: number | TestContext = 1,
+  testBuiltins?: TestBuiltins
+): TestContext {
+  if (chapter === undefined) {
+    chapter = 1
+  }
+
+  if (typeof chapter === 'number') {
+    const context: TestContext = createContext(chapter, [], undefined, {
+      rawDisplay: (str, externalContext) => {
+        context.displayResult = context.displayResult ? context.displayResult + '\n' + str : str
+        return str
+      },
+      prompt: (str, externalContext) => {
+        context.promptResult = context.promptResult ? context.promptResult + '\n' + str : str
+        return null
+      },
+      alert: (str, externalContext) => {
+        context.alertResult = context.alertResult ? context.alertResult + '\n' + str : str
+      },
+      visualiseList: value => {
+        if (context.visualiseListResult !== undefined) {
+          context.visualiseListResult.push(value)
+        } else {
+          context.visualiseListResult = [value]
+        }
+      }
+    } as CustomBuiltIns)
+
+    if (testBuiltins !== undefined) {
+      for (const name in testBuiltins) {
+        if (testBuiltins.hasOwnProperty(name)) {
+          defineBuiltin(context, name, testBuiltins[name])
+        }
+      }
+    }
+
+    return context
+  } else {
+    return chapter
+  }
+}
+
+function expectSuccess(code: string, testContext: TestContext, result: Result) {
+  expect(testContext.errors).toEqual([])
+  expect(result.status).toBe('finished')
+  expect({
+    code,
+    displayResult: testContext.displayResult,
+    alertResult: testContext.alertResult,
+    visualiseListResult: testContext.visualiseListResult,
+    result: (result as Finished).value
+  }).toMatchSnapshot()
+}
+
+function expectFailure(code: string, testContext: TestContext, result: Result) {
+  expect(testContext.errors).not.toEqual([])
+  expect(result.status).toBe('error')
+  expect({
+    code,
+    displayResult: testContext.displayResult,
+    alertResult: testContext.alertResult,
+    visualiseListResult: testContext.visualiseListResult,
+    error: parseError(testContext.errors)
+  }).toMatchSnapshot()
+}
+
+function expectFailureNoSnapshot(code: string, testContext: TestContext, result: Result) {
+  expect(testContext.errors).not.toEqual([])
+  expect(result.status).toBe('error')
+}
+
+export function expectDisplayResult(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return expect(
+    runInContext(code, testContext, { scheduler }).then(result => {
+      expectSuccess(code, testContext, result)
+      return testContext.displayResult!
+    })
+  ).resolves
+}
+
+export function expectResult(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return expect(
+    runInContext(code, testContext, { scheduler }).then(result => {
+      expectSuccess(code, testContext, result)
+      return (result as Finished).value
+    })
+  ).resolves
+}
+
+export function expectError(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return expect(
+    runInContext(code, testContext, { scheduler }).then(result => {
+      expectFailure(code, testContext, result)
+      return parseError(testContext.errors)
+    })
+  ).resolves
+}
+
+export function expectErrorNoSnapshot(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return expect(
+    runInContext(code, testContext, { scheduler }).then(result => {
+      expectFailureNoSnapshot(code, testContext, result)
+      return parseError(testContext.errors)
+    })
+  ).resolves
+}
+
+export function snapshotSuccess(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return runInContext(code, testContext, { scheduler }).then(result => {
+    expectSuccess(code, testContext, result)
+  })
+}
+
+export function snapshotFailure(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return runInContext(code, testContext, { scheduler }).then(result => {
+    expectFailure(code, testContext, result)
+  })
+}
+
+export function expectToMatchJS(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  // tslint:disable-next-line:no-eval
+  return expectResult(code, chapterOrContext, testBuiltins).toEqual(eval(code))
+}
+
+export function expectToLooselyMatchJS(
+  code: string,
+  chapterOrContext?: number | TestContext,
+  testBuiltins?: TestBuiltins
+) {
+  const testContext = createTestContext(chapterOrContext, testBuiltins)
+  const scheduler = 'preemptive'
+  return runInContext(code, testContext, { scheduler }).then(result => {
+    expectSuccess(code, testContext, result)
+    // tslint:disable-next-line:no-eval
+    expect((result as Finished).value.replace(/ /g, '')).toEqual(eval(code).replace(/ /g, ''))
+  })
+}