From 3b8b17bda0968211b12b3ea02674ee4e56ae6095 Mon Sep 17 00:00:00 2001
From: Thomas Wood <thomas.wood09@imperial.ac.uk>
Date: Tue, 8 Mar 2016 17:21:11 +0000
Subject: [PATCH] Typechecker for the converted esprima ast.

---
 esprima-to-ast.js |  28 +++++---
 package.json      |   1 +
 test/parser.js    | 172 +++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 187 insertions(+), 14 deletions(-)

diff --git a/esprima-to-ast.js b/esprima-to-ast.js
index 6f03013..4ea5a79 100644
--- a/esprima-to-ast.js
+++ b/esprima-to-ast.js
@@ -508,26 +508,31 @@ function esprimaToAST(prog) {
 
 
 /* Custom Error handler with JSON pretty-printing */
-function ASTErrorJSONReplacer(key, value, depth) {
-  if (depth === undefined) { depth = 3; }
 
-  if (typeof value === "object") {
-    if (depth > 0) {
-      for (var nestKey in value) {
-        value[nestKey] = ASTErrorJSONReplacer(nestKey, value[nestKey], depth - 1);
+function toString(ast, maxDepth) {
+  if (maxDepth === undefined) { maxDepth = 3; }
+
+  return JSON.stringify(ast, function ASTErrorJSONReplacer(key, value, depth) {
+    if (depth === undefined) { depth = maxDepth; }
+
+    if (typeof value === "object") {
+      if (depth > 0) {
+        for (var nestKey in value) {
+          value[nestKey] = ASTErrorJSONReplacer(nestKey, value[nestKey], depth - 1);
+        }
+      } else {
+        value = "RECURSION TRUNCATED";
       }
-    } else {
-      value = "RECURSION TRUNCATED";
     }
-  }
 
-  return value;
+    return value;
+  }, 2);
 }
 
 function NewASTErrorType(name, parentError) {
   var error = function (message, expr) {
     this.name = name;
-    this.message = message + "\n" + JSON.stringify(expr, ASTErrorJSONReplacer, 2);
+    this.message = message + "\n" + toString(expr);
     if (Error.captureStackTrace) { Error.captureStackTrace(this, error); }
   }
   error.prototype = Object.create(parentError.prototype);
@@ -541,3 +546,4 @@ var UnsupportedSyntaxError = NewASTErrorType("UnsupportedSyntaxError", EsprimaTo
 module.exports.esprimaToAST = esprimaToAST;
 module.exports.EsprimaToASTError = EsprimaToASTError;
 module.exports.UnsupportedSyntaxError = UnsupportedSyntaxError;
+module.exports.toString = toString;
diff --git a/package.json b/package.json
index 8d1d5d9..3b51e68 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
     "esprima": "^1.2.5"
   },
   "devDependencies": {
+    "chai": "^3.5.0",
     "klaw": "^1.1.3",
     "mocha": "^2.4.5",
     "mz": "^2.3.1",
diff --git a/test/parser.js b/test/parser.js
index 2e083e1..8a33c8e 100644
--- a/test/parser.js
+++ b/test/parser.js
@@ -1,7 +1,10 @@
+"use strict";
+
 var fs = require('mz/fs');
 var walk = require('klaw');
 var filter = require('through2-filter');
 fs.readlinkSync = require('readlink').sync; // a non-broken readlink...
+var assert = require('chai').assert;
 
 var esprima = require('esprima');
 var esprimaToAST = require('../esprima-to-ast.js');
@@ -31,6 +34,169 @@ function isParserNegativeTest(str) {
   return /(?:SyntaxError|\?!NotEarlyError)/.test(negative);
 }
 
+function typecheckAST(ast) {
+  /* typename -> ((tagname -> fieldname -> type)|type|type => (tagname -> fieldname -> type)) */
+  const types = {
+    list: function (x) {return {"[]": {}, "::": {head: x, tail: x + " list"}}},
+    option: function (x) {return {None: {}, Some: {value: x}}},
+    unary_op: {
+      Coq_unary_op_delete: {}, Coq_unary_op_void: {}, Coq_unary_op_typeof: {}, Coq_unary_op_post_incr: {},
+      Coq_unary_op_post_decr: {}, Coq_unary_op_pre_incr: {}, Coq_unary_op_pre_decr: {}, Coq_unary_op_add: {},
+      Coq_unary_op_neg: {}, Coq_unary_op_bitwise_not: {}, Coq_unary_op_not: {},
+    },
+    binary_op: {
+      Coq_binary_op_mult: {}, Coq_binary_op_div: {}, Coq_binary_op_mod: {}, Coq_binary_op_add: {},
+      Coq_binary_op_sub: {}, Coq_binary_op_left_shift: {}, Coq_binary_op_right_shift: {},
+      Coq_binary_op_unsigned_right_shift: {}, Coq_binary_op_lt: {}, Coq_binary_op_gt: {}, Coq_binary_op_le: {},
+      Coq_binary_op_ge: {}, Coq_binary_op_instanceof: {}, Coq_binary_op_in: {}, Coq_binary_op_equal: {},
+      Coq_binary_op_disequal: {}, Coq_binary_op_strict_equal: {}, Coq_binary_op_strict_disequal: {},
+      Coq_binary_op_bitwise_and: {}, Coq_binary_op_bitwise_or: {}, Coq_binary_op_bitwise_xor: {}, Coq_binary_op_and: {},
+      Coq_binary_op_or: {}, Coq_binary_op_coma: {},
+    },
+    literal: {
+      Coq_literal_null: {},
+      Coq_literal_bool : {value: "boolean"},
+      Coq_literal_number : {value: "number"},
+      Coq_literal_string : {value: "string"},
+    },
+    label: {
+      Coq_label_empty: {},
+      Coq_label_string : {value: "string"},
+    },
+    label_set: "label list",
+    strictness_flag: "boolean",
+    propname: {
+      Coq_propname_identifier : {value: "string"},
+      Coq_propname_string : {value: "string"},
+      Coq_propname_number : {value: "number"},
+    },
+    expr: {
+      Coq_expr_this: {},
+      Coq_expr_identifier : {name: "string"},
+      Coq_expr_literal : {value: "literal"},
+      Coq_expr_object : {fields: "(propname * propbody) list"},
+      Coq_expr_array : {elements: "expr option list"},
+      Coq_expr_function : {func_name_opt: "string option", arg_names: "string list", body: "funcbody"},
+      Coq_expr_access : {obj: "expr", field: "expr"},
+      Coq_expr_member : {obj: "expr", field_name: "string"},
+      Coq_expr_new : {func: "expr", args: "expr list"},
+      Coq_expr_call : {func: "expr", args: "expr list"},
+      Coq_expr_unary_op : {op: "unary_op", arg: "expr"},
+      Coq_expr_binary_op : {arg1: "expr", op: "binary_op", arg2: "expr"},
+      Coq_expr_conditional : {cond: "expr", then_branch: "expr", else_branch: "expr"},
+      Coq_expr_assign : {left_expr: "expr", op_opt: "binary_op option", right_expr: "expr"},
+    },
+    propbody: {
+      Coq_propbody_val : {expr: "expr"},
+      Coq_propbody_get : {body: "funcbody"},
+      Coq_propbody_set : {names: "string list", body: "funcbody"},
+    },
+    funcbody: {
+      Coq_funcbody_intro : {prog: "prog", source: "string"},
+    },
+    stat: {
+      Coq_stat_expr : {expr: "expr"},
+      Coq_stat_label : {label: "string", stat: "stat"},
+      Coq_stat_block : {stats: "stat list"},
+      Coq_stat_var_decl : {decls: "(string * expr option) list"},
+      Coq_stat_if : {cond: "expr", then_branch: "stat", else_branch: "stat option"},
+      Coq_stat_do_while : {labels: "label_set", body: "stat", cond: "expr"},
+      Coq_stat_while : {labels: "label_set", cond: "expr", body: "stat"},
+      Coq_stat_with : {obj: "expr", stat: "stat"},
+      Coq_stat_throw : {arg: "expr"},
+      Coq_stat_return : {arg_opt: "expr option"},
+      Coq_stat_break : {label: "label"},
+      Coq_stat_continue : {label: "label"},
+      Coq_stat_try : {body: "stat", catch_stats_opt: "(string * stat) option", finally_opt: "stat option"},
+      Coq_stat_for : {labels: "label_set", init: "expr option", cond: "expr option", step: "expr option", body: "stat"},
+      Coq_stat_for_var : {labels: "label_set", init: "(string * expr option) list", cond: "expr option", step: "expr option", body: "stat"},
+      Coq_stat_for_in : {labels: "label_set", id: "expr", obj: "expr", body: "stat"},
+      Coq_stat_for_in_var : {labels: "label_set", id: "string", init: "expr option", obj: "expr", body: "stat"},
+      Coq_stat_debugger: {},
+      Coq_stat_switch : {labels: "label_set", arg: "expr", body: "switchbody"},
+    },
+    switchbody: {
+      Coq_switchbody_nodefault : {clauses: "switchclause list"},
+      Coq_switchbody_withdefault : {clauses_before: "switchclause list", clause_default: "stat list", clauses_after: "switchclause list"},
+    },
+    switchclause: {
+      Coq_switchclause_intro : {arg: "expr", stats: "stat list"},
+    },
+    prog: {
+      Coq_prog_intro : {strictness: "strictness_flag", elements: "element list"},
+    },
+    element: {
+      Coq_element_stat : {stat: "stat"},
+      Coq_element_func_decl : {func_name: "string", arg_names: "string list", body: "funcbody"},
+    },
+    propdefs: "(propname * propbody) list",
+    elements: "element list",
+    /* funcdecl: { { funcdecl_name : string;
+       funcdecl_parameters : string list;
+       funcdecl_body : funcbody } */
+  };
+
+  var isBaseType = function(type) {
+    return type === "string" ||
+           type === "number" ||
+           type === "boolean";
+  };
+
+  // Returns a base type name, or an object containing the type's constructors
+  var getType = function(type) {
+    if (isBaseType(type)) {
+      return type;
+    }
+
+    // Test for poly type
+    var i = type.lastIndexOf(' ');
+    if (i >= 0) {
+      var polyTypeName = type.substring(i+1);
+      var polyTypeConstr = getType(polyTypeName);
+      assert.isFunction(polyTypeConstr);
+      var instance = polyTypeConstr(type.substring(0, i));
+      instance._typeName = polyTypeName;
+      return instance;
+    }
+
+    assert(types.hasOwnProperty(type), "Type " + type + " not present in type environment");
+    var t = types[type];
+    if (typeof t === "string") {
+      // type alias, recursively lookup
+      return getType(t);
+    } else {
+      t._typeName = type;
+      return t;
+    }
+  };
+
+  var errorMsg = function (value, msg) {
+    return _ => esprimaToAST.toString(value, 1) + " " + msg;
+  };
+
+  var typecheck = function(type, value) {
+    var t = getType(type);
+    if (isBaseType(t)) {
+      assert(t === typeof value, errorMsg(value, "was expected to have type of "+t));
+    } else {
+      assert(value.hasOwnProperty("type"), errorMsg(value, "doesn't have a type property"));
+      assert.strictEqual(t._typeName, value.type);
+      assert(value.hasOwnProperty("tag"), errorMsg(value, "doesn't have a tag property"));
+      assert.notStrictEqual(value.tag, "_typeName");
+      assert(t.hasOwnProperty(value.tag), value.tag + " is a not a valid constructor of " + t._typeName);
+
+      // Test each field defined in the type constructor
+      var constructor = t[value.tag];
+      Object.keys(constructor).forEach(field => {
+        assert(value.hasOwnProperty(field), errorMsg(value, "doesn't have a " + field + " property"));
+        typecheck(constructor[field], value[field]);
+      });
+    }
+  };
+
+  return typecheck("prog", ast);
+}
+
 walk(test262path)
 .pipe(filter.obj(file => file.stats.isFile() && file.path.endsWith(".js")))
 .on('readable', function() {
@@ -65,13 +231,13 @@ walk(test262path)
         });
 
         it('converts', function() {
-          var prog;
           try {
-            prog = esprima.parse(source, {loc: true});
+            var prog = esprima.parse(source, {loc: true});
           } catch(e) { return; }
 
           try {
-            esprimaToAST.esprimaToAST(prog);
+            var ast = esprimaToAST.esprimaToAST(prog);
+            typecheckAST(ast);
           } catch (e) {
             if (e instanceof esprimaToAST.UnsupportedSyntaxError) {
             } else {
-- 
GitLab