Repository: source-academy/js-slang
Pull request #144 (2018-12-08 09:08:25)

Make || and && shorthand for ?: and improve TCO
|| and && are now transformed into ?: before being evaluated.
TCO now handles tail calls hidden inside ?:, ||, && expressions properly.

martin-henz: I like this a lot. No need to talk about && and || any longer.
thomastanck: Blocked until #140 is merged as that one handles proper error handling of infinite recursion.

Patch:
diff --git a/src/interop.ts b/src/interop.ts
index e19ab6b4..981bfa57 100644
--- a/src/interop.ts
+++ b/src/interop.ts
@@ -2,7 +2,7 @@ import { generate } from 'astring'
 
 import { MAX_LIST_DISPLAY_LENGTH } from './constants'
 import { apply } from './interpreter'
-import { ArrowClosure, Closure, Context, Value } from './types'
+import { Closure, Context, Value } from './types'
 
 export const closureToJS = (value: Value, context: Context, klass: string) => {
   function DummyClass(this: Value) {
@@ -56,8 +56,8 @@ const arrayToString = (value: Value[], length: number) => {
 }
 
 export const toString = (value: Value, length = 0): string => {
-  if (value instanceof ArrowClosure || value instanceof Closure) {
-    return generate(value.node)
+  if (value instanceof Closure) {
+    return generate(value.originalNode)
   } else if (Array.isArray(value)) {
     if (length > MAX_LIST_DISPLAY_LENGTH) {
       return '...<truncated>'
diff --git a/src/interpreter.ts b/src/interpreter.ts
index 8c03ef26..431163bc 100644
--- a/src/interpreter.ts
+++ b/src/interpreter.ts
@@ -4,7 +4,7 @@ import * as es from 'estree'
 import * as constants from './constants'
 import { toString } from './interop'
 import * as errors from './interpreter-errors'
-import { ArrowClosure, Closure, Context, ErrorSeverity, Frame, SourceError, Value, Environment } from './types'
+import { Closure, Context, ErrorSeverity, Frame, SourceError, Value, Environment } from './types'
 import { createNode } from './utils/node'
 import * as rttc from './utils/rttc'
 
@@ -21,7 +21,7 @@ class TailCallReturnValue {
 }
 
 const createFrame = (
-  closure: ArrowClosure | Closure,
+  closure: Closure,
   args: Value[],
   callExpression?: es.CallExpression
 ): Frame => {
@@ -167,7 +167,7 @@ const setVariable = (context: Context, name: string, value: any) => {
 
 const checkNumberOfArguments = (
   context: Context,
-  callee: ArrowClosure | Closure,
+  callee: Closure,
   args: Value[],
   exp: es.CallExpression
 ) => {
@@ -185,6 +185,30 @@ function* getArgs(context: Context, call: es.CallExpression) {
   return args
 }
 
+function transformLogicalExpression(node: es.LogicalExpression): es.ConditionalExpression {
+  if (node.operator === '&&') {
+    return <es.ConditionalExpression> {
+      type: 'ConditionalExpression',
+      test: node.left,
+      consequent: node.right,
+      alternate: {
+        type: 'Literal',
+        value: false
+      }
+    }
+  } else {
+    return <es.ConditionalExpression> {
+      type: 'ConditionalExpression',
+      test: node.left,
+      consequent: {
+        type: 'Literal',
+        value: true
+      },
+      alternate: node.right
+    }
+  }
+}
+
 export type Evaluator<T extends es.Node> = (node: T, context: Context) => IterableIterator<Value>
 
 /**
@@ -215,8 +239,8 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
   FunctionExpression: function*(node: es.FunctionExpression, context: Context) {
     return new Closure(node, currentFrame(context), context)
   },
-  ArrowFunctionExpression: function*(node: es.Function, context: Context) {
-    return new ArrowClosure(node, currentFrame(context), context)
+  ArrowFunctionExpression: function*(node: es.ArrowFunctionExpression, context: Context) {
+    return Closure.makeFromArrowFunction(node, currentFrame(context), context)
   },
   Identifier: function*(node: es.Identifier, context: Context) {
     return getVariable(context, node.name)
@@ -324,24 +348,7 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
     return yield* this.IfStatement(node, context)
   },
   LogicalExpression: function*(node: es.LogicalExpression, context: Context) {
-    const left = yield* evaluate(node.left, context)
-    let error = rttc.checkLogicalExpression(context, left, true)
-    if (error) {
-      handleError(context, error)
-      return undefined
-    } else if ((node.operator === '&&' && left) || (node.operator === '||' && !left)) {
-      // only evaluate right if required (lazy); but when we do, check typeof right
-      const right = yield* evaluate(node.right, context)
-      error = rttc.checkLogicalExpression(context, left, right)
-      if (error) {
-        handleError(context, error)
-        return undefined
-      } else {
-        return right
-      }
-    } else {
-      return left
-    }
+    return yield* this.IfStatement(transformLogicalExpression(node), context)
   },
   VariableDeclaration: function*(node: es.VariableDeclaration, context: Context) {
     const declaration = node.declarations[0]
@@ -480,16 +487,37 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
     return yield* evaluate(node.expression, context)
   },
   *ReturnStatement(node: es.ReturnStatement, context: Context) {
-    if (node.argument) {
-      if (node.argument.type === 'CallExpression') {
-        const callee = yield* evaluate(node.argument.callee, context)
-        const args = yield* getArgs(context, node.argument)
-        return new TailCallReturnValue(callee, args, node.argument)
+    let return_expression = node.argument
+    if (!return_expression) {
+      return new ReturnValue(undefined)
+    }
+
+    // If we have a conditional expression, reduce it until we get something else
+    while (return_expression.type === 'LogicalExpression' ||
+      return_expression.type === 'ConditionalExpression') {
+      if (return_expression.type === 'LogicalExpression') {
+        return_expression = transformLogicalExpression(return_expression)
+      }
+      const test = yield* evaluate(return_expression.test, context)
+      const error = rttc.checkIfStatement(context, test)
+      if (error) {
+        handleError(context, error)
+        return undefined
+      }
+      if (test) {
+        return_expression = return_expression.consequent
       } else {
-        return new ReturnValue(yield* evaluate(node.argument, context))
+        return_expression = return_expression.alternate
       }
+    }
+
+    // If we are now left with a CallExpression, then we use TCO
+    if (return_expression.type === 'CallExpression') {
+      const callee = yield* evaluate(return_expression.callee, context)
+      const args = yield* getArgs(context, return_expression)
+      return new TailCallReturnValue(callee, args, return_expression)
     } else {
-      return new ReturnValue(undefined)
+      return new ReturnValue(yield* evaluate(return_expression, context))
     }
   },
   WhileStatement: function*(node: es.WhileStatement, context: Context) {
@@ -566,7 +594,7 @@ export function* evaluate(node: es.Node, context: Context) {
 
 export function* apply(
   context: Context,
-  fun: ArrowClosure | Closure | Value,
+  fun: Closure | Value,
   args: Value[],
   node?: es.CallExpression,
   thisContext?: Value
@@ -594,17 +622,6 @@ export function* apply(
         // No Return Value, set it as undefined
         result = new ReturnValue(undefined)
       }
-    } else if (fun instanceof ArrowClosure) {
-      checkNumberOfArguments(context, fun, args, node!)
-      const frame = createFrame(fun, args, node)
-      frame.thisContext = thisContext
-      if (result instanceof TailCallReturnValue) {
-        replaceFrame(context, frame)
-      } else {
-        pushFrame(context, frame)
-        total++
-      }
-      result = new ReturnValue(yield* evaluate(fun.node.body, context))
     } else if (typeof fun === 'function') {
       try {
         result = fun.apply(thisContext, args)
diff --git a/src/types.ts b/src/types.ts
index bc5ee34b..4dfdea40 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -171,11 +171,14 @@ export class Closure extends Callable {
   // tslint:disable-next-line:ban-types
   public fun: Function
 
+  /** The original node that created this Closure **/
+  public originalNode: es.Function
+
   constructor(public node: es.FunctionExpression, public frame: Frame, context: Context) {
     super(function (this: any, ...args: any[]) {
       return funJS.apply(this, args)
     })
-    this.node = node
+    this.originalNode = node
     try {
       if (this.node.id) {
         this.functionName = this.node.id.name
@@ -186,29 +189,43 @@ export class Closure extends Callable {
     const funJS = closureToJS(this, context, this.functionName)
     this.fun = funJS
   }
-}
 
-/**
- * Modified from class Closure, for construction of arrow functions.
- */
-export class ArrowClosure extends Callable {
-  /** Keep track how many lambdas are created */
-  private static arrowCtr = 0
+  static makeFromArrowFunction(node: es.ArrowFunctionExpression, frame: Frame, context: Context) {
+    function isExpressionBody(body: es.BlockStatement | es.Expression): body is es.Expression {
+      return body.type !== 'BlockStatement'
+    }
 
-  /** Unique ID defined for anonymous closure */
-  public functionName: string
+    let closure = null
+    if (isExpressionBody(node.body)) {
+      closure = new Closure(<es.FunctionExpression> {
+        type: 'FunctionExpression',
+        loc: node.loc,
+        id: null,
+        params: node.params,
+        body: <es.BlockStatement> {
+          type: 'BlockStatement',
+          loc: node.body.loc,
+          body: [{
+            type: 'ReturnStatement',
+            loc: node.body.loc,
+            argument: node.body
+          }]
+        }
+      }, frame, context)
+    } else {
+      closure = new Closure(<es.FunctionExpression> {
+        type: 'FunctionExpression',
+        loc: node.loc,
+        id: null,
+        params: node.params,
+        body: node.body
+      }, frame, context)
+    }
 
-  /** Fake closure function */
-  // tslint:disable-next-line:ban-types
-  public fun: Function
+    // Set the closure's nod to point back at the original one
+    closure.originalNode = node
 
-  constructor(public node: es.Function, public frame: Frame, context: Context) {
-    super(function (this: any, ...args: any[]) {
-      return funJS.apply(this, args)
-    })
-    this.functionName = `Anonymous${++ArrowClosure.arrowCtr}`
-    const funJS = closureToJS(this, context, this.functionName)
-    this.fun = funJS
+    return closure
   }
 }
 
diff --git a/src/utils/node.ts b/src/utils/node.ts
index 3f2c9cc8..8fa7bd45 100644
--- a/src/utils/node.ts
+++ b/src/utils/node.ts
@@ -3,7 +3,7 @@
  */
 import { SimpleWalker, Walker } from 'acorn/dist/walk'
 import * as es from 'estree'
-import { Closure, Value } from '../types'
+import { Value } from '../types'
 
 /**
  * Check whether two nodes are equal.
@@ -104,9 +104,6 @@ const freshId = (() => {
  * @returns {Node}
  */
 export const createNode = (value: Value): es.Node => {
-  if (value instanceof Closure) {
-    return value.node
-  }
   return createLiteralNode(value)
 }
 
diff --git a/src/utils/rttc.ts b/src/utils/rttc.ts
index 4f0a6104..02d6d870 100644
--- a/src/utils/rttc.ts
+++ b/src/utils/rttc.ts
@@ -1,6 +1,5 @@
 import * as es from 'estree'
 import {
-  ArrowClosure,
   Closure,
   Context,
   ErrorSeverity,
@@ -35,7 +34,7 @@ export class TypeError implements SourceError {
  * identifed as functions
  */
 const typeOf = (v: Value) => {
-  if (v instanceof Closure || v instanceof ArrowClosure || typeof v === 'function') {
+  if (v instanceof Closure || typeof v === 'function') {
     return 'function'
   } else {
     return typeof v
@@ -108,17 +107,6 @@ export const checkBinaryExpression = (
   }
 }
 
-export const checkLogicalExpression = (context: Context, left: Value, right: Value) => {
-  const node = context.runtime.nodes[0]
-  if (!isBool(left)) {
-    return new TypeError(node, LHS, 'boolean', typeOf(left))
-  } else if (!isBool(right)) {
-    return new TypeError(node, RHS, 'boolean', typeOf(right))
-  } else {
-    return
-  }
-}
-
 export const checkIfStatement = (context: Context, test: Value) => {
   const node = context.runtime.nodes[0]
   return isBool(test) ? undefined : new TypeError(node, ' as condition', 'boolean', typeOf(test))


Test patch:
diff --git a/src/__tests__/__snapshots__/allowed_syntax.ts.snap b/src/__tests__/__snapshots__/allowed_syntax.ts.snap
index 6d238da5..33afbce6 100644
--- a/src/__tests__/__snapshots__/allowed_syntax.ts.snap
+++ b/src/__tests__/__snapshots__/allowed_syntax.ts.snap
@@ -51,11 +51,7 @@ exports[`Syntaxes are allowed in the chapter they are introduced 10`] = `
 
 exports[`Syntaxes are allowed in the chapter they are introduced 11`] = `""`;
 
-exports[`Syntaxes are allowed in the chapter they are introduced 12`] = `
-ReturnValue {
-  "value": 3,
-}
-`;
+exports[`Syntaxes are allowed in the chapter they are introduced 12`] = `3`;
 
 exports[`Syntaxes are allowed in the chapter they are introduced 13`] = `
 "
diff --git a/src/__tests__/__snapshots__/index.ts.snap b/src/__tests__/__snapshots__/index.ts.snap
index 840971ad..2164db69 100644
--- a/src/__tests__/__snapshots__/index.ts.snap
+++ b/src/__tests__/__snapshots__/index.ts.snap
@@ -18,6 +18,11 @@ Object {
 
 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]]).."
+`;
+
 exports[`Cannot overwrite consts even when assignment is allowed 1`] = `"Line 4: Cannot assign new value to constant constant"`;
 
 exports[`Deep object assignment and retrieval 1`] = `
@@ -41,7 +46,7 @@ Object {
 }
 `;
 
-exports[`Inifinite recursion with list args represents CallExpression well 1`] = `
+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]]).."
 `;
@@ -60,9 +65,20 @@ Object {
 }
 `;
 
-exports[`Simple inifinite recursion represents CallExpression well 1`] = `
+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)).."
+`;
+
+exports[`Simple function infinite recursion represents CallExpression well 1`] = `
 "Line 1: Infinite recursion
-  x(x => x(x))..  x(x => x(x))..  x(x => x(x)).."
+  x(function f(x) {
+  return x(x)(x);
+})..  x(function f(x) {
+  return x(x)(x);
+})..  x(function f(x) {
+  return x(x)(x);
+}).."
 `;
 
 exports[`Simple object assignment and retrieval 1`] = `
diff --git a/src/__tests__/__snapshots__/tailcall-return.ts.snap b/src/__tests__/__snapshots__/tailcall-return.ts.snap
new file mode 100644
index 00000000..67858c0f
--- /dev/null
+++ b/src/__tests__/__snapshots__/tailcall-return.ts.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Simple tail call returns work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail call in boolean operators work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail call in conditional expressions work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail call in nested mix of conditional expressions boolean operators work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail calls in arrow block functions work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail calls in arrow functions work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail calls in mutual recursion with arrow functions work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
+
+exports[`Tail calls in mutual recursion work 1`] = `
+Object {
+  "status": "finished",
+  "value": 10000,
+}
+`;
diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts
index a58d6acf..e310a510 100644
--- a/src/__tests__/index.ts
+++ b/src/__tests__/index.ts
@@ -119,9 +119,23 @@ test('parseError for missing semicolon', () => {
 })
 
 test(
-  'Simple inifinite recursion represents CallExpression well',
+  'Simple arrow function infinite recursion represents CallExpression well',
   () => {
-    const code = '(x => x(x))(x => x(x));'
+    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()
+    })
+  },
+  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 => {
@@ -169,11 +183,11 @@ test('Can overwrite lets when assignment is allowed', () => {
 })
 
 test(
-  'Inifinite recursion with list args represents CallExpression well',
+  'Arrow function infinite recursion with list args represents CallExpression well',
   () => {
     const code = `
-    const f = xs => f(xs);
-    f(list(1, 2 ));
+    const f = xs => append(f(xs), list());
+    f(list(1, 2));
   `
     const context = mockContext(2)
     const promise = runInContext(code, context, { scheduler: 'preemptive' })
@@ -186,10 +200,46 @@ test(
 )
 
 test(
-  'Inifinite recursion with different args represents CallExpression well',
+  'Function infinite recursion with list args represents CallExpression well',
+  () => {
+    const code = `
+    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()
+    })
+  },
+  30000
+)
+
+test(
+  'Arrow function infinite recursion with different args represents CallExpression well',
+  () => {
+    const code = `
+    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}/)
+      )
+    })
+  },
+  30000
+)
+
+test(
+  'Function infinite recursion with different args represents CallExpression well',
   () => {
     const code = `
-    const f = i => f(i+1);
+    function f(i) { return f(i+1) - 1; }
     f(0);
   `
     const context = mockContext()
diff --git a/src/__tests__/interpreter-errors.ts b/src/__tests__/interpreter-errors.ts
index f0b4c3bb..6202a3fa 100644
--- a/src/__tests__/interpreter-errors.ts
+++ b/src/__tests__/interpreter-errors.ts
@@ -186,7 +186,7 @@ test('Infinite recursion with a block bodied function', () => {
   return promise.then(obj => {
     expect(obj.status).toBe('error')
     expect(parseError(context.errors)).toEqual(
-      expect.stringMatching(/Infinite recursion\n *(i\(\d*\)[^f]{2,4}){3}/)
+      expect.stringMatching(/Infinite recursion\n *(i\(\d*\)[^i]{2,4}){3}/)
     )
   })
 })
@@ -206,7 +206,7 @@ test('Infinite recursion with function calls in argument', () => {
   return promise.then(obj => {
     expect(obj.status).toBe('error')
     expect(parseError(context.errors)).toEqual(
-      expect.stringMatching(/Infinite recursion\n *(i\(\d*, 1\)[^f]{2,4}){3}/)
+      expect.stringMatching(/Infinite recursion\n *(i\(\d*, 1\)[^i]{2,4}){2}[ir]/)
     )
   })
 })
diff --git a/src/__tests__/tailcall-return.ts b/src/__tests__/tailcall-return.ts
new file mode 100644
index 00000000..edefbdb1
--- /dev/null
+++ b/src/__tests__/tailcall-return.ts
@@ -0,0 +1,179 @@
+import { mockContext } from '../mocks/context'
+import { parseError, runInContext } from '../index'
+import { Finished } from '../types'
+
+test('Check that stack is at most 10k in size', () => {
+  const code = `
+    function f(x) {
+      if (x <= 0) {
+        return 0;
+      } else {
+        return 1 + f(x-1);
+      }
+    }
+    f(10000);
+  `
+  const context = mockContext()
+  const promise = runInContext(code, context, { scheduler: 'preemptive' })
+  return promise.then(obj => {
+    expect(obj.status).toBe('error')
+    expect(parseError(context.errors)).toEqual(
+      expect.stringMatching(/Infinite recursion\n([^f]*f){3}/))
+  })
+})
+
+test('Simple tail call returns work', () => {
+  const code = `
+    function f(x, y) {
+      if (x <= 0) {
+        return y;
+      } else {
+        return f(x-1, y+1);
+      }
+    }
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail call in conditional expressions work', () => {
+  const code = `
+    function f(x, y) {
+      return x <= 0 ? y : f(x-1, y+1);
+    }
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail call in boolean operators work', () => {
+  const code = `
+    function f(x, y) {
+      if (x <= 0) {
+        return y;
+      } else {
+        return false || f(x-1, y+1);
+      }
+    }
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail call in nested mix of conditional expressions boolean operators work', () => {
+  const code = `
+    function f(x, y) {
+      return x <= 0 ? y : false || x > 0 ? f(x-1, y+1) : 'unreachable';
+    }
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail calls in arrow functions work', () => {
+  const code = `
+    const f = (x, y) => x <= 0 ? y : f(x-1, y+1);
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail calls in arrow block functions work', () => {
+  const code = `
+    const f = (x, y) => {
+      if (x <= 0) {
+        return y;
+      } else {
+        return f(x-1, y+1);
+      }
+    };
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail calls in mutual recursion work', () => {
+  const code = `
+    function f(x, y) {
+      if (x <= 0) {
+        return y;
+      } else {
+        return g(x-1, y+1);
+      }
+    }
+    function g(x, y) {
+      if (x <= 0) {
+        return y;
+      } else {
+        return f(x-1, y+1);
+      }
+    }
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
+
+test('Tail calls in mutual recursion with arrow functions work', () => {
+  const code = `
+    const f = (x, y) => x <= 0 ? y : g(x-1, y+1);
+    const g = (x, y) => x <= 0 ? y : f(x-1, y+1);
+    f(5000, 5000);
+  `
+  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(10000)
+    expect(context.errors).toEqual([])
+  })
+})
diff --git a/src/utils/__tests__/__snapshots__/rttc.ts.snap b/src/utils/__tests__/__snapshots__/rttc.ts.snap
index 2470df4e..8706245a 100644
--- a/src/utils/__tests__/__snapshots__/rttc.ts.snap
+++ b/src/utils/__tests__/__snapshots__/rttc.ts.snap
@@ -256,34 +256,6 @@ exports[`Invalid binary type combinations for + return TypeError 11`] = `"Expect
 
 exports[`Invalid binary type combinations for + return TypeError 12`] = `"Expected string on left hand side of operation, got function."`;
 
-exports[`Invalid logical type combinations return TypeError 1`] = `"Expected boolean on left hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 2`] = `"Expected boolean on right hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 3`] = `"Expected boolean on left hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 4`] = `"Expected boolean on right hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 5`] = `"Expected boolean on left hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 6`] = `"Expected boolean on left hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 7`] = `"Expected boolean on left hand side of operation, got string."`;
-
-exports[`Invalid logical type combinations return TypeError 8`] = `"Expected boolean on left hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 9`] = `"Expected boolean on right hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 10`] = `"Expected boolean on left hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 11`] = `"Expected boolean on right hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 12`] = `"Expected boolean on left hand side of operation, got function."`;
-
-exports[`Invalid logical type combinations return TypeError 13`] = `"Expected boolean on left hand side of operation, got number."`;
-
-exports[`Invalid logical type combinations return TypeError 14`] = `"Expected boolean on left hand side of operation, got string."`;
-
 exports[`Invalid ternary/if test expressions return TypeError 1`] = `"Expected boolean as condition, got number."`;
 
 exports[`Invalid ternary/if test expressions return TypeError 2`] = `"Expected boolean as condition, got string."`;
diff --git a/src/utils/__tests__/rttc.ts b/src/utils/__tests__/rttc.ts
index 0f08ed2d..8091838f 100644
--- a/src/utils/__tests__/rttc.ts
+++ b/src/utils/__tests__/rttc.ts
@@ -199,34 +199,6 @@ test('Invalid binary type combinations for (<|>|<=|>=) return TypeError', () =>
   errors.map(error => expect(error.elaborate()).toMatchSnapshot())
 })
 
-test('Valid logical type combinations are OK', () => {
-  const operatorValues = [[bool, bool], [bool, bool]]
-  const context = mockRuntimeContext()
-  const errors = operatorValues.map(opVals => {
-    return rttc.checkLogicalExpression(context, opVals[0], opVals[1])
-  })
-  errors.map(error => expect(error).toBeUndefined())
-})
-
-test('Invalid logical type combinations return TypeError', () => {
-  const operatorValues = [
-    [num, bool],
-    [bool, num],
-    [func, bool],
-    [bool, func],
-    [func, func],
-    [num, num],
-    [str, str]
-  ]
-  const context = mockRuntimeContext()
-  const errors = operatorValues.map(opVals => {
-    return rttc.checkLogicalExpression(context, opVals[0], opVals[1])
-  }) as rttc.TypeError[]
-  errors.map(error => expect(error).toBeInstanceOf(rttc.TypeError))
-  errors.map(error => expect(error.explain()).toMatchSnapshot())
-  errors.map(error => expect(error.elaborate()).toMatchSnapshot())
-})
-
 test('Valid ternary/if test expressions are OK', () => {
   const operatorValues = [bool]
   const context = mockRuntimeContext()